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 #new members > โœ” Evaluating code string @ ๐Ÿ’ฌ):

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 #Is there code for X? > get Parser from Lean @ ๐Ÿ’ฌ)

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