Zulip Chat Archive
Stream: lean4
Topic: Evaluating an expression provided as a string
Jeremy Tan (Oct 12 2025 at 04:49):
I want to define a function that parses arithmetic expressions provided as strings, and returns the value of the expression as a rational number if the string can be parsed, and 0 otherwise. The code below doesn't work (evaluate% comes from ):
import Mathlib
open Lean Parser Elab Tactic Meta
/-- Turn a string into an expression, similar to Python's `eval`. -/
elab "evaluate%" code:str : term <= expectedType => do
let .ok stx := Parser.runParserCategory (โ getEnv) `term code.getString |
throwError s!"Failed to parse {code}"
Term.elabTerm stx expectedType
#eval evaluate% "(1 / 3 + 5 - 7 / 9 : โ)" -- mkRat 41 9
/-- A mapping from `Fin 4` to the four basic arithmetic operations. -/
def opMap : Fin 4 โ String
| 0 => "+"
| 1 => "-"
| 2 => "*"
| 3 => "/"
def evalWhiteboard (ops : Fin 4 โ Fin 4) : โ :=
withMainContext do
let w := s!"(1 {opMap (ops 0)} 3 {opMap (ops 1)} 5 {opMap (ops 2)} 7 {opMap (ops 3)} 9 : โ)"
match runParserCategory (โ getEnv) `term w with
| .error e => (0 : โ)
| .ok stx => Term.elabTerm stx _
How can I get it to work?
Jeremy Tan (Oct 12 2025 at 04:51):
So basically I want to turn evaluate% into a standard definition, returning 0 for unparseable inputs or inputs not evaluating to a rational
Kenny Lau (Oct 12 2025 at 06:21):
well, you want to run meta-code in main-land, that's i don't think allowed
Jeremy Tan (Oct 12 2025 at 06:57):
I think I've got it @Kenny Lau (based on )
import Mathlib
section
open Lean Parser
/-- The four basic arithmetic operations. -/
inductive Op where
| add | sub | mul | div
deriving DecidableEq, Repr, Inhabited
/-- A type of arithmetic expressions on natural numbers, but evaluating in the rationals. -/
inductive Arith where
| val : โ โ Arith
| app : Op โ Arith โ Arith โ Arith
deriving DecidableEq, Repr, Inhabited
unsafe def parseArith : Parser := arithParser 0 where
arithParserFnCore : ParserFn :=
prattParser `arith parsingTables .default (mkAntiquot "arith" `arith (isPseudoKind := true)).fn
arithParser (prec : โ) : Parser :=
{ fn := adaptCacheableContextFn ({ ยท with prec }) (withCacheFn `arith arithParserFnCore) }
num : Parser := Term.num
add : TrailingParser := trailing_parser:30:30 " + " >> arithParser 31
sub : TrailingParser := trailing_parser:30:30 " - " >> arithParser 31
mul : TrailingParser := trailing_parser:35:35 " * " >> arithParser 36
div : TrailingParser := trailing_parser:35:35 " / " >> arithParser 36
parsingTables : PrattParsingTables := {
trailingParsers := []
trailingTable := [(`ยซ+ยป, add, 30), (`ยซ-ยป, sub, 30), (`ยซ*ยป, mul, 35), (`ยซ/ยป, div, 35)].foldl
(fun map (a, b) โฆ map.insert a b) {}
leadingParsers := []
leadingTable := [(`ยซ$ยป, num, maxPrec), (`num, num, maxPrec)].foldl
(fun map (a, b) โฆ map.insert a b) {}
}
partial def evalArith : TSyntax `arith โ โ
| `(parseArith| $a:arith + $b:arith) => evalArith a + evalArith b
| `(parseArith| $a:arith - $b:arith) => evalArith a - evalArith b
| `(parseArith| $a:arith * $b:arith) => evalArith a * evalArith b
| `(parseArith| $a:arith / $b:arith) => evalArith a / evalArith b
| `(parseArith| $n:num) => n.getNat
| _ => 0
instance : Insert Token TokenTable := โจfun a b โฆ b.insert a aโฉ
instance : Singleton Token TokenTable := โจfun a โฆ insert a โ
โฉ
def evalString (input : String) : โ := unsafe
let env : Environment := unsafeCast ()
let tokens : TokenTable := {"+", "-", "*", "/"}
let state := parseArith.fn.run (mkInputContext input "<input>")
(ParserModuleContext.mk env {} .anonymous []) tokens (mkParserState input)
if state.hasError then 0 else evalArith โจstate.stxStack.backโฉ
end
#eval evalString "1 + 3 + 5 + 7 / 9"
Are there any further optimisations I can make?
Kenny Lau (Oct 12 2025 at 06:58):
actually i was trying another approach myself
Jeremy Tan (Oct 12 2025 at 07:01):
can you share it?
Kenny Lau (Oct 12 2025 at 07:02):
i haven't finished it
Kenny Lau (Oct 12 2025 at 07:05):
it doesn't work, which confirms my suspicion that you can't run metacode in main-land
Kenny Lau (Oct 12 2025 at 07:05):
import Mathlib
open Lean Parser Elab Tactic Meta Qq
unsafe def evalRat (code : String) : TermElabM โ := do
let .ok stx := Parser.runParserCategory (โ getEnv) `term code | return 67
let qS โ Term.elabTermAndSynthesize stx (mkConst ``Rat)
let q โ unsafe evalExpr โ (mkConst ``Rat) qS
return q
/-- Turn a string into an expression, similar to Python's `eval`. -/
elab "evaluate%" code:str : term <= _expectedType => do
let q โ unsafe evalRat code.getString
return toExpr q
#check evaluate% "(1 / 3 + 5 - 7 / 9 : โ)" -- mkRat 41 9
elab "evaluate_arbitrary%" i:term : term <= _expectedType => do
let sS โ Term.elabTermAndSynthesize i (mkConst ``Rat)
let str โ unsafe evalExpr String (mkConst ``String) sS
let q โ unsafe evalRat str
return toExpr q
/-- A mapping from `Fin 4` to the four basic arithmetic operations. -/
def opMap : Fin 4 โ String
| 0 => "+"
| 1 => "-"
| 2 => "*"
| 3 => "/"
def w (ops : Fin 4 โ Fin 4) : String :=
s!"(1 {opMap (ops 0)} 3 {opMap (ops 1)} 5 {opMap (ops 2)} 7 {opMap (ops 3)} 9 : โ)"
#check evaluate_arbitrary% (w ![3,0,1,3])
def evalWhiteboard (ops : Fin 4 โ Fin 4) : โ :=
evaluate_arbitrary% (w ops)
Kenny Lau (Oct 12 2025 at 07:05):
(kernel) declaration has free variables '_tmpโ'
Kenny Lau (Oct 12 2025 at 07:06):
or maybe that's not the reason and i missed something
Kenny Lau (Oct 12 2025 at 07:10):
@Jeremy Tan i think if i start again i would just write a parser myself like a programming exercise, like really working with the string here and not using any meta code at all
Kenny Lau (Oct 12 2025 at 07:10):
I think that's the only sensible way
Kenny Lau (Oct 12 2025 at 07:11):
see String.toNat?
Kenny Lau (Oct 12 2025 at 07:11):
is this agreeable to you?
Jeremy Tan (Oct 12 2025 at 07:16):
I'm just trying to understand how the working code I posted actually works
Kenny Lau (Oct 12 2025 at 07:24):
@Jeremy Tan well, run "magically" does the conversion to you in non-metaland from String to Syntax
Kenny Lau (Oct 12 2025 at 07:24):
then evalArith just parses the syntax manually
Kenny Lau (Oct 12 2025 at 07:26):
part of the code is also to define the custom syntax category "Arith"
Kenny Lau (Oct 12 2025 at 07:26):
it restricts the input to natural numbers with +-*/ rather than any possible way to make a rational number
Kenny Lau (Oct 12 2025 at 07:26):
so in particular mkRat 41 9 will not be valid input
Kenny Lau (Oct 12 2025 at 07:27):
but it was valid input in the first code you posted, which just uses the biggest syntax category possible, which is term
Kenny Lau (Oct 12 2025 at 07:35):
I tried to get rid of that big codeblock and just use the usual way to declare syntax, but it gets stuck in a loop for some reason:
import Mathlib
open Lean Parser Elab Tactic Meta Qq
declare_syntax_cat arith
syntax num : arith
syntax arith "+" arith : arith
syntax arith "-" arith : arith
syntax arith "*" arith : arith
syntax arith "/" arith : arith
partial def evalArith : TSyntax `arith โ โ
| `(arith| $a:arith + $b:arith) => evalArith a + evalArith b
| `(arith| $a:arith - $b:arith) => evalArith a - evalArith b
| `(arith| $a:arith * $b:arith) => evalArith a * evalArith b
| `(arith| $a:arith / $b:arith) => evalArith a / evalArith b
| `(arith| $n:num) => n.getNat
| _ => 0
unsafe def evalRat (s : String) : โ :=
match runParserCategory (unsafeCast ()) `arith s with
| .error _ => 67
| .ok stx => evalArith (.mk stx)
-- stuck in a loop, i don't know why
-- #eval evalRat "1 + 2 / 3"
Robin Arnez (Oct 12 2025 at 08:14):
Well, where does it get the information to run a parser category from? The environment of course... and you didn't provide an environment
Kenny Lau (Oct 12 2025 at 08:15):
@Robin Arnez but in the solution above, it got away with let env : Environment := unsafeCast (), what's the difference here?
Robin Arnez (Oct 12 2025 at 08:18):
In the solution above, the parser was run directly with a parser that contained all parsers you needed. However, when you run a parser category, there's no way for it to know the parsers associated to it unless you have an environment
Kenny Lau (Oct 12 2025 at 08:19):
is there a way to get the parser at that point of the file?
Robin Arnez (Oct 12 2025 at 08:21):
Well, the parsers that exists already have a problem, which is that they use the category parser for arith, which again needs the environment
Kenny Lau (Oct 12 2025 at 08:21):
yes, but I want to call the environment at this point of the file, and get a parser out of it, and then use that parser for the rest of the file
Robin Arnez (Oct 12 2025 at 08:21):
So either you find a way to get an environment with the stuff you need or you need to modify something from above (potentially automatically)
Jeremy Tan (Oct 12 2025 at 13:51):
Robin Arnez said:
In the solution above, the parser was run directly with a parser that contained all parsers you needed. However, when you run a parser category, there's no way for it to know the parsers associated to it unless you have an environment
Which solution are you referring to with "solution above"?
Kenny Lau (Oct 12 2025 at 13:51):
the one you posted
Last updated: Dec 20 2025 at 21:32 UTC