Zulip Chat Archive
Stream: metaprogramming / tactics
Topic: Skip command if it doesn't parse
Eric Wieser (Jul 25 2025 at 12:36):
Is it possible to write a nicer version of this?
import Lean
open Lean Elab Parser
def runParserCategoryOnLit (env : Environment) (catName : Name) (input : TSyntax `str) : MetaM Syntax := do
let p := andthenFn whitespace (categoryParserFnImpl catName)
-- replace the source with the unquoted versoin
let .some pos := input.raw.getPos? | failure
let pos := pos + ' '
let map ← getFileMap
let source := map.source.extract 0 pos ++ input.getString
let ictx := {
fileMap := ← getFileMap, fileName := ←getFileName,
input := source }
let s := p.run ictx { env, options := {} } (getTokenTable env) ({
pos := pos
cache := initCacheForInput source
})
if !s.allErrors.isEmpty then
throwError m!"{s.toErrorMsg ictx}"
else if ictx.input.atEnd s.pos then
return s.stxStack.back
else
let msg := (s.mkError "end of input").toErrorMsg ictx
throwError m!"{msg}"
elab "try_parse_command" s:str : command => do
let env ← getEnv
let stx ← try
Command.liftTermElabM <| runParserCategoryOnLit env `command s
catch e =>
Lean.logWarning m!"Skipping: {e.toMessageData}"
return
Command.elabCommand stx
-- works
try_parse_command "run_cmd Lean.logInfo \"nice\""
-- ignores the failure, as intended
try_parse_command "some_unimplemented_cmd"
-- error message is in the wrong position; can we drop the quotes?
try_parse_command "run_cmd Lean.logInfo \"oops\n\n\n\n\n\n\n\" + 'c' "
Eric Wieser (Jul 25 2025 at 12:37):
In particular, the \ escapes inside the string literal mess with the error positions
Robin Arnez (Jul 25 2025 at 13:32):
You'll have to write your own string decode functions:
import Lean
import Std
open Lean Elab Parser
partial def decodeStrLitWithPositionMapAux (s : String) (off : String.Pos) (i : String.Pos) (acc : String) :
StateT (Array String.Pos) Option String := do
let istart := i
let c := s.get i
let i := s.next i
if c == '\x22' then -- '\"' except zulip doesn't like that
modify fun posMap => posMap.push (off + istart)
pure acc
else if s.atEnd i then
none
else if c == '\\' then do
if let some (c, i') := Syntax.decodeQuotedChar s i then
modify fun posMap => posMap ++ Array.replicate c.utf8Size (off + istart)
decodeStrLitWithPositionMapAux s off i' (acc.push c)
else if let some i := Syntax.decodeStringGap s i then
decodeStrLitWithPositionMapAux s off i acc
else
none
else
modify fun posMap => posMap ++ Array.replicate c.utf8Size (off + istart)
decodeStrLitWithPositionMapAux s off i (acc.push c)
partial def decodeRawStrLitWithPositionMapAux (s : String) (off : String.Pos) (i : String.Pos) (num : Nat) : String × Array String.Pos :=
let c := s.get i
let i := s.next i
if c == '#' then
decodeRawStrLitWithPositionMapAux s off i (num + 1)
else
let str := s.extract i ⟨s.utf8ByteSize - (num + 1)⟩
(str, (0...=str.utf8ByteSize).iter.map (fun n => off + i + ⟨n⟩) |>.toArray)
def decodeStrLitWithPositionMap (s : String) (off : String.Pos) : Option (String × Array String.Pos) :=
if s.get 0 == 'r' then
some <| decodeRawStrLitWithPositionMapAux s off ⟨1⟩ 0
else
(decodeStrLitWithPositionMapAux s off ⟨1⟩ "").run #[]
def Lean.TSyntax.getStringWithPositionMap (stx : StrLit) : String × Array String.Pos :=
match stx.raw with
| Syntax.node _ k args =>
if h : k == strLitKind ∧ args.size = 1 then
match args[0]'(Nat.lt_of_sub_eq_succ h.2) with
| Syntax.atom info val => (decodeStrLitWithPositionMap val (info.getPos?.getD 0)).getD ("", #[])
| _ => ("", #[])
else
("", #[])
| _ => ("", #[])
def Lean.Syntax.mapInfo (stx : Syntax) (f : SourceInfo → SourceInfo) : Syntax :=
match stx with
| .missing => .missing
| .atom info val => .atom (f info) val
| .ident info rawVal val pre => .ident (f info) rawVal val pre
| .node info kind args => .node (f info) kind (args.map (·.mapInfo f))
def runParserCategoryOnLit (catName : Name) (input : TSyntax `str) : MetaM Syntax := do
let pmctx : ParserModuleContext := { env := ← getEnv, options := ← getOptions, currNamespace := ← getCurrNamespace, openDecls := ← getOpenDecls }
let p := andthenFn whitespace (categoryParserFnImpl catName)
let map ← getFileMap
let (str, posMap) := input.getStringWithPositionMap
let ictx := {
fileMap := FileMap.ofString str, fileName := ← getFileName,
input := str }
let s := p.run ictx pmctx (getTokenTable (← getEnv)) ({
pos := 0
cache := initCacheForInput str
})
let mapSubstring : Substring → Substring
| ⟨_, startPos, endPos⟩ => ⟨map.source, posMap[startPos.byteIdx]!, posMap[endPos.byteIdx]!⟩
if !s.allErrors.isEmpty then
throwError m!"{s.toErrorMsg ictx}"
else if ictx.input.atEnd s.pos then
return s.stxStack.back.mapInfo fun
| .original l p t e => .original (mapSubstring l) posMap[p.byteIdx]! (mapSubstring t) posMap[e.byteIdx]!
| info => info
else
let msg := (s.mkError "end of input").toErrorMsg ictx
throwError m!"{msg}"
elab "try_parse_command" s:str : command => do
let stx ← try
Command.liftTermElabM <| runParserCategoryOnLit `command s
catch e =>
Lean.logWarning m!"Skipping: {e.toMessageData}"
return
Command.elabCommand stx
Edward van de Meent (Jul 26 2025 at 12:27):
are you looking for something like #check_failure?
Eric Wieser (Jul 26 2025 at 12:36):
That doesn't work for parse failures
Eric Wieser (Jul 26 2025 at 12:37):
And #parse doesn't run the command when it succeeds
Damiano Testa (Jul 26 2025 at 14:20):
Maybe we could add a flag to #parse, to make it also run the command?
Robin Arnez (Jul 26 2025 at 15:02):
The purpose of #parse is testing parsing, not elaboration though, isn't it?
Robin Arnez (Jul 26 2025 at 15:03):
Anyways, what exactly do you need this for?
Kyle Miller (Jul 26 2025 at 15:23):
You can sidestep the escaping issue by using raw strings. Here's an implementation that requires raw strings then looks into the atom itself to figure out the size of the r###" prefix:
code
Example:
try_parse_command r###"run_cmd Lean.logInfo "oops\n\n\n\n\n\n\n" + 'c' "###
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Last updated: Dec 20 2025 at 21:32 UTC