Zulip Chat Archive

Stream: metaprogramming / tactics

Topic: parsing a DSL


Paige Thomas (Sep 25 2024 at 03:12):

I'm trying to see if I can use meta programming to parse a first order logic formula, starting here

open Lean Elab

inductive Formula : Type
  | pred_ : String  List String  Formula

syntax (name := pred) str "(" (str ("," str)*)? ")" : term

@[macro pred]
def predMacro : Macro := fun stx =>
match stx with
| `( $p ($vs) ) => `(Formula.pred_ $p $vs)
| _ => Macro.throwUnsupported

#check "P" ()

However, this gives "elaboration function for 'pred' has not been implemented", which seems straightforward, but I'm confused as to why this doesn't work as is.

Paige Thomas (Sep 25 2024 at 03:13):

(and I'm not sure how to implement one to fix it)

Mario Carneiro (Sep 25 2024 at 16:00):

your pattern match is not sufficient, you only matched functions with exactly one argument

Mario Carneiro (Sep 25 2024 at 16:01):

as a result, the macro is going to the throwUnsupported branch, meaning that it is left unelaborated and you get that message

Paige Thomas (Sep 26 2024 at 05:39):

Thank you.

Paige Thomas (Sep 26 2024 at 05:48):

Is there a way to code this so that quotations around the strings are not needed?

inductive Formula : Type
  | pred_ : String  List String  Formula
  | eq_ : String  String  Formula
  | true_ : Formula
  | false_ : Formula
  | not_ : Formula  Formula
  | imp_ : Formula  Formula  Formula
  | forall_ : String  Formula  Formula
  | exists_ : String  Formula  Formula
  deriving Inhabited, DecidableEq


def Formula.toString : Formula  String
  | pred_ X xs =>
    if xs.isEmpty
    then s! "{X}"
    else s! "({X} {xs})"
  | eq_ x y => s! "({x} = {y})"
  | true_ => "T"
  | false_ => "F"
  | not_ phi => s! "¬ {phi.toString}"
  | imp_ phi psi => s! "({phi.toString} → {psi.toString})"
  | forall_ x phi => s! "(∀ {x}. {phi.toString})"
  | exists_ x phi => s! "(∃ {x}. {phi.toString})"

instance : ToString Formula :=
  { toString := fun (F : Formula) => F.toString }

instance : Repr Formula :=
  { reprPrec := fun (F : Formula) _ => F.toString.toFormat }


declare_syntax_cat formula

syntax str "(" str,* ")" : formula
syntax "(" str "=" str ")" : formula
syntax "T" : formula
syntax "F" : formula
syntax "~" formula : formula
syntax "(" formula "->" formula ")" : formula
syntax "(" "A." str formula ")" : formula
syntax "(" "E." str formula ")" : formula

syntax "[Formula|" formula "]" : term

macro_rules
  | `([Formula| $X:str ( $xs,* )]) => `(Formula.pred_ $X [$xs,*])
  | `([Formula| ( $x:str = $y:str )]) => `(Formula.eq_ $x $y)
  | `([Formula| T]) => `(Formula.true_)
  | `([Formula| F]) => `(Formula.false_)
  | `([Formula| ~ $phi:formula]) => `(Formula.not_ [Formula| $phi])
  | `([Formula| ($phi:formula -> $psi:formula)]) => `(Formula.imp_ [Formula| $phi] [Formula| $psi])
  | `([Formula| ( A. $x $phi)]) => `(Formula.forall_ $x [Formula| $phi])
  | `([Formula| ( E. $x $phi)]) => `(Formula.exists_ $x [Formula| $phi])

#check [Formula| "P" ()]
#check [Formula| "P" ("x")]
#check [Formula| "P" ("x", "y")]
#check [Formula| ( "P" ("x", "y") -> "P" () )]
#check [Formula| ( ~ "P" ("x", "y") -> ("x" = "y") )]
#check [Formula| (A. "x" "P" ("x"))]
#check [Formula| (E. "x" "P" ("x"))]

For example, to have #check [Formula| ( P (x, y) -> P () )] work?

Damiano Testa (Sep 26 2024 at 07:27):

Can you assume that what is in str is an ident? (This means that it could be a theorem name: roughly, it starts with a letter, consists only of letters, digits, underscore and the odd unicode charater).

Paige Thomas (Sep 26 2024 at 07:28):

Yes, I think that would work.

Paige Thomas (Sep 26 2024 at 07:31):

Do you mean something like
| `([Formula| $X:ident ( $xs,* )]) => `(Formula.pred_ $X [$xs,*])
I'm not sure how to convert the $X from an ident to a string.

Damiano Testa (Sep 26 2024 at 07:31):

If that is allowed, and id is an identifier, then getId gives you a Name and Name.toString converts it into a string.

Damiano Testa (Sep 26 2024 at 07:32):

So, you could use id.getId.toString.

Paige Thomas (Sep 26 2024 at 07:34):

If I do

syntax ident "(" str,* ")" : formula
...
  | `([Formula| $X:ident ( $xs,* )]) => `(Formula.pred_ ($X).getId.toString [$xs,*])
...
#check [Formula| P ()]

I get

Formula.pred_ (sorryAx String true) [] : Formula

mathlib-demo.lean:56:17

unknown identifier 'P'

Damiano Testa (Sep 26 2024 at 07:35):

Let me try to play with it.

Damiano Testa (Sep 26 2024 at 07:45):

I have not tried all of them, but this seems to work on the first #check:

macro_rules
  | `([Formula| $X:ident ( $xs,* )]) => let Xstr := Lean.Syntax.mkStrLit X.getId.toString; `(Formula.pred_ $Xstr [$xs,*])

Damiano Testa (Sep 26 2024 at 07:46):

I replaced all strs in your syntax declarations with idents. After that, you convert the ident to an str via Lean.Syntax.mkStrLit X.getId.toString and you pass it back to your Formula.

Damiano Testa (Sep 26 2024 at 07:46):

I imagine that you could probably set things up to directly use idents and not need to convert into strings, with a little advance planning.

Paige Thomas (Sep 26 2024 at 07:48):

Beautiful! Thank you!!

Paige Thomas (Sep 26 2024 at 07:51):

How did you handle the $xs,*?

Paige Thomas (Sep 26 2024 at 07:52):

Is there a way to map that translation over it?

Damiano Testa (Sep 26 2024 at 07:53):

Ah, I got lazy and did not undo those: let me have a look!

Damiano Testa (Sep 26 2024 at 08:07):

Sorry, I have to run now and I have not really had a chance to fix it. The solution is something along these lines, but as is, it does not work:

let xstrs : Lean.Syntax.TSepArray `ident "," := xs.getElems.map (Lean.Syntax.mkStrLit ·.getId.toString)
let xstrs : List (Lean.TSyntax `term):= (xs.getElems.map (Lean.Syntax.mkStrLit ·.getId.toString)).toList

Paige Thomas (Sep 26 2024 at 08:07):

Ok, thank you!

Paige Thomas (Sep 29 2024 at 01:20):

I was wondering if someone might be able to tell me what I am doing wrong here:

import Lean

open Lean Elab Meta


inductive Formula : Type
  | pred_ : String  List String  Formula
  | eq_ : String  String  Formula
  | true_ : Formula
  | false_ : Formula
  | not_ : Formula  Formula
  | imp_ : Formula  Formula  Formula
  | forall_ : String  Formula  Formula
  | exists_ : String  Formula  Formula
  deriving Inhabited, DecidableEq


def Formula.toString : Formula  String
  | pred_ X xs =>
    if xs.isEmpty
    then s! "{X}"
    else s! "({X} {xs})"
  | eq_ x y => s! "({x} = {y})"
  | true_ => "T"
  | false_ => "F"
  | not_ phi => s! "¬ {phi.toString}"
  | imp_ phi psi => s! "({phi.toString} → {psi.toString})"
  | forall_ x phi => s! "(∀ {x}. {phi.toString})"
  | exists_ x phi => s! "(∃ {x}. {phi.toString})"

instance : ToString Formula :=
  { toString := fun (F : Formula) => F.toString }

instance : Repr Formula :=
  { reprPrec := fun (F : Formula) _ => F.toString.toFormat }


declare_syntax_cat formula

syntax ident "(" ident,* ")" : formula
syntax "(" ident "=" ident ")" : formula
syntax "T" : formula
syntax "F" : formula
syntax "~" formula : formula
syntax "(" formula "->" formula ")" : formula
syntax "(" "A." ident formula ")" : formula
syntax "(" "E." ident formula ")" : formula


partial def elabFormula : Syntax  MetaM Expr
  | `(formula| $X:ident ($xs,*)) =>
    let X' : Expr := Lean.mkStrLit X.getId.toString;
    let xs' : Array Expr := xs.getElems.map (Lean.mkStrLit ·.getId.toString);
    mkAppM ``Formula.pred_ ((#[X']) ++ xs')

  | `(formula| ($x:ident = $y:ident)) =>
    let x' : Expr := Lean.mkStrLit x.getId.toString;
    let y' : Expr := Lean.mkStrLit y.getId.toString;
    mkAppM ``Formula.eq_ #[x', y']

  | `(formula| T) => mkAppM ``Formula.true_ #[]

  | `(formula| F) => mkAppM ``Formula.false_ #[]

  | `(formula| ~ $phi) => do
    let phi' : Expr  elabFormula phi;
    mkAppM ``Formula.not_ #[phi']

  | `(formula| ($phi:formula -> $psi:formula)) => do
    let phi' : Expr  elabFormula phi;
    let psi' : Expr  elabFormula psi;
    mkAppM ``Formula.imp_ #[phi', psi']

  | `(formula| (A. $x:ident $phi)) => do
    let x' : Expr := Lean.mkStrLit x.getId.toString;
    let phi' : Expr  elabFormula phi;
    mkAppM ``Formula.forall_ #[x', phi']

  | `(formula| (E. $x:ident $phi)) => do
    let x' : Expr := Lean.mkStrLit x.getId.toString;
    let phi' : Expr  elabFormula phi;
    mkAppM ``Formula.exists_ #[x', phi']

  | _ => throwUnsupportedSyntax


elab ">>" p:formula "<<" : term => elabFormula p

#check >> P () << works, but #check >> P (x) << gives

application type mismatch
  Formula.pred_ "P" "x"
argument
  "x"
has type
  String : Type
but is expected to have type
  List String : Type

and #check >> ( A. x P () ) << gives

application type mismatch
  Formula.forall_ "x" (Formula.pred_ "P")
argument
  Formula.pred_ "P"
has type
  List String  Formula : Type
but is expected to have type
  Formula : Type

Jannis Limperg (Sep 29 2024 at 09:44):

In the pred_ case of elabFormula, you construct the application Formula.pred_ X' x1 ... xn where the xi are elements of xs. Instead, you want to turn the list of syntax objects xs into a list literal. Complete code:

import Lean

open Lean Elab Meta

inductive Formula : Type
  | pred_ : String  List String  Formula
  | eq_ : String  String  Formula
  | true_ : Formula
  | false_ : Formula
  | not_ : Formula  Formula
  | imp_ : Formula  Formula  Formula
  | forall_ : String  Formula  Formula
  | exists_ : String  Formula  Formula
  deriving Inhabited, DecidableEq, Repr

def Formula.toString : Formula  String
  | pred_ X xs =>
    if xs.isEmpty
    then s! "{X}"
    else s! "({X} {xs})"
  | eq_ x y => s! "({x} = {y})"
  | true_ => "T"
  | false_ => "F"
  | not_ phi => s! "¬ {phi.toString}"
  | imp_ phi psi => s! "({phi.toString} → {psi.toString})"
  | forall_ x phi => s! "(∀ {x}. {phi.toString})"
  | exists_ x phi => s! "(∃ {x}. {phi.toString})"

instance : ToString Formula :=
  { toString := fun (F : Formula) => F.toString }

declare_syntax_cat formula

syntax ident "(" ident,* ")" : formula
syntax "(" ident "=" ident ")" : formula
syntax "T" : formula
syntax "F" : formula
syntax "~" formula : formula
syntax "(" formula "->" formula ")" : formula
syntax "(" "A." ident formula ")" : formula
syntax "(" "E." ident formula ")" : formula


partial def elabFormula : Syntax  MetaM Expr
  | `(formula| $X:ident ($xs,*)) => do
    let X' : Expr := Lean.mkStrLit X.getId.toString;
    let xs' : Array Expr := xs.getElems.map (Lean.mkStrLit ·.getId.toString);
    let xsList  mkListLit (.const ``String []) xs'.toList
    mkAppM ``Formula.pred_ #[X', xsList]

  | `(formula| ($x:ident = $y:ident)) =>
    let x' : Expr := Lean.mkStrLit x.getId.toString;
    let y' : Expr := Lean.mkStrLit y.getId.toString;
    mkAppM ``Formula.eq_ #[x', y']

  | `(formula| T) => mkAppM ``Formula.true_ #[]

  | `(formula| F) => mkAppM ``Formula.false_ #[]

  | `(formula| ~ $phi) => do
    let phi' : Expr  elabFormula phi;
    mkAppM ``Formula.not_ #[phi']

  | `(formula| ($phi:formula -> $psi:formula)) => do
    let phi' : Expr  elabFormula phi;
    let psi' : Expr  elabFormula psi;
    mkAppM ``Formula.imp_ #[phi', psi']

  | `(formula| (A. $x:ident $phi)) => do
    let x' : Expr := Lean.mkStrLit x.getId.toString;
    let phi' : Expr  elabFormula phi;
    mkAppM ``Formula.forall_ #[x', phi']

  | `(formula| (E. $x:ident $phi)) => do
    let x' : Expr := Lean.mkStrLit x.getId.toString;
    let phi' : Expr  elabFormula phi;
    mkAppM ``Formula.exists_ #[x', phi']

  | _ => throwUnsupportedSyntax


elab ">>" p:formula "<<" : term => elabFormula p

#check >> P () <<
#check >> P (x) <<
#check >> ( A. x P () ) <<

Some other suggestions:

  • Derive Repr. Repr is supposed to give a string that can be parsed back by Lean, so it's best to derive it. (ToString, on the other hand, is free-form.)
  • Use delimiters for your formulas that give an unambiguous parse, e.g. (formula| ...), instead of >> ... <<. The latter can be parsed as a regular Lean term involving the operators >> and <<. It seems to work for the examples, but I wouldn't risk complications later.
  • Semicolons in do blocks are unnecessary.

Paige Thomas (Sep 29 2024 at 14:57):

@Jannis Limperg Thank you!

Paige Thomas (Oct 06 2024 at 03:37):

I was wondering if someone might know how to make an Expr for Option.none : Option Nat. The #check here gives "incorrect number of universe levels none", which I'm not sure how to interpret.

import Lean

open Lean Elab Meta

inductive Term_ : Type
| Hole : Option Nat  Term_
deriving Inhabited, DecidableEq, Repr

declare_syntax_cat term_

syntax "?" : term_

partial def elabTerm : Syntax  MetaM Expr
  | `(term_| ?) => do
    mkAppM ``Term_.Hole #[mkConst ``Option.none []]

  | _ => throwUnsupportedSyntax


elab "(Term_|" e:term_ ")" : term => elabTerm e

#check (Term_| ?)

Chris Bailey (Oct 06 2024 at 03:55):

import Lean

open Lean Elab Meta

inductive Term_ : Type
| Hole : Option Nat  Term_
deriving Inhabited, DecidableEq, Repr

declare_syntax_cat term_

syntax "?" : term_

partial def elabTerm : Syntax  MetaM Expr
  | `(term_| ?) => do
    let op  mkAppOptM ``Option.none #[some (.const ``Nat [])]
    mkAppM ``Term_.Hole #[op]

  | _ => throwUnsupportedSyntax

elab "(Term_|" e:term_ ")" : term => elabTerm e

#check (Term_| ?)

Chris Bailey (Oct 06 2024 at 03:57):

Option.none needs more of a hint since it's polymorphic and has a universe parameter (it has one implicit type argument), but mkAppM doesn't try to synthesize the implicit for you (per the docstring).

Paige Thomas (Oct 06 2024 at 03:58):

Is that what the #[some (.const ``Nat [])] is doing?

Chris Bailey (Oct 06 2024 at 03:59):

Yeah, the dosctring for mkAppOptM explains the general idea in more depth, but it lets you provide the implicits in the array (consider it like @Option.none <arg>)

Paige Thomas (Oct 06 2024 at 04:00):

Oh, I see.

Chris Bailey (Oct 06 2024 at 04:00):

Well I guess in this case @Option.none Nat

Paige Thomas (Oct 06 2024 at 04:00):

Thank you!

Paige Thomas (Jan 11 2025 at 19:32):

(deleted)

Paige Thomas (Jan 11 2025 at 19:40):

(deleted)

Paige Thomas (Jan 11 2025 at 22:20):

I'm a bit confused about this error that shows up on the last line in the example:

import Lean
import Batteries.Tactic.Lint.Frontend
import Mathlib.Util.CompileInductive


set_option autoImplicit false


/--
  The type of formulas.
-/
inductive Formula_ (α : Type) : Type
  | false_ : Formula_ α
  | true_ : Formula_ α
  | atom_ : α  Formula_ α
  | not_ : Formula_ α  Formula_ α
  | and_ : Formula_ α  Formula_ α  Formula_ α
  | or_ : Formula_ α  Formula_ α  Formula_ α
  | imp_ : Formula_ α  Formula_ α  Formula_ α
  | iff_ : Formula_ α  Formula_ α  Formula_ α
  | forall_ : String  Formula_ α  Formula_ α
  | exists_ : String  Formula_ α  Formula_ α
  deriving Inhabited, DecidableEq, Repr

compile_inductive% Formula_


open Lean Elab Meta

declare_syntax_cat formula


syntax "F" : formula
syntax "T" : formula
syntax ident : formula
syntax "~" formula : formula
syntax "(" formula "->" formula ")" : formula
syntax "(" formula "/\\" formula ")" : formula
syntax "(" formula "\\/" formula ")" : formula
syntax "(" formula "<->" formula ")" : formula
syntax "(" "A." ident formula ")" : formula
syntax "(" "E." ident formula ")" : formula


partial def elabFormula : Syntax  MetaM Expr
  | `(formula| F) => mkAppOptM ``Formula_.false_ #[some (.const ``String [])]

  | `(formula| T) => mkAppOptM ``Formula_.true_ #[some (.const ``String [])]

  | `(formula| $a:ident) => do
    let x' : Expr := Lean.mkStrLit a.getId.toString
    mkAppOptM ``Formula_.atom_ #[some (.const ``String []), x']

  | `(formula| ~ $phi) => do
    let phi' : Expr  elabFormula phi
    mkAppOptM ``Formula_.not_ #[some (.const ``String []), phi']

  | `(formula| ($phi:formula /\ $psi:formula)) => do
    let phi' : Expr  elabFormula phi
    let psi' : Expr  elabFormula psi
    mkAppOptM ``Formula_.and_ #[some (.const ``String []), phi', psi']

  | `(formula| ($phi:formula \/ $psi:formula)) => do
    let phi' : Expr  elabFormula phi
    let psi' : Expr  elabFormula psi
    mkAppOptM ``Formula_.or_ #[some (.const ``String []), phi', psi']

  | `(formula| ($phi:formula -> $psi:formula)) => do
    let phi' : Expr  elabFormula phi
    let psi' : Expr  elabFormula psi
    mkAppOptM ``Formula_.imp_ #[some (.const ``String []), phi', psi']

  | `(formula| ($phi:formula <-> $psi:formula)) => do
    let phi' : Expr  elabFormula phi
    let psi' : Expr  elabFormula psi
    mkAppOptM ``Formula_.iff_ #[some (.const ``String []), phi', psi']

  | `(formula| (A. $x:ident $phi)) => do
    let x' : Expr := Lean.mkStrLit x.getId.toString
    let phi' : Expr  elabFormula phi
    mkAppOptM ``Formula_.forall_ #[some (.const ``String []), x', phi']

  | `(formula| (E. $x:ident $phi)) => do
    let x' : Expr := Lean.mkStrLit x.getId.toString
    let phi' : Expr  elabFormula phi
    mkAppOptM ``Formula_.exists_ #[some (.const ``String []), x', phi']

  | _ => throwUnsupportedSyntax


elab "(Formula_|" p:formula ")" : term => elabFormula p

#check (Formula_| P )
#check (Formula_| ( A. x P ) )
#check (Formula_| ( A. x (A. y (P <-> Q ) ) ) )


example
  (α : Type)
  (F : Formula_ α) : True := sorry
unexpected token 'F'; expected '_' or identifier

It appears for some reason or another it is viewing the F as the token that would get parsed to Formula_.false_?


Last updated: May 02 2025 at 03:31 UTC