Zulip Chat Archive
Stream: general
Topic: metaprogramming: escape from elaborator
Quinn (May 21 2025 at 18:35):
I have an aggressive elaborator that coerces identifiers to strings, which makes sense for my usecase but is leading to heartbreak when I try to prepend the type/syntax category i'm elaborating with a quantified variable. This is a minimal reproducer
import Lean
open Lean Elab Meta
def Env : Type := String → Int
def Assertion : Type := Env → Prop
def Assert (assertion : Assertion) (σ : Env) : Prop := assertion σ
declare_syntax_cat assertion
syntax ident "=" num : assertion
syntax ident "=" ident : assertion
@[simp] def strIntEq (x : String) (y : Int) : Assertion := fun σ => σ x = y
@[simp] def strStrEq (x y : String) : Assertion := fun σ => σ x = σ y
def elabAssertionLit : Syntax → MetaM Expr
| `(assertion| $x:ident = $n:num) => do
let x <- pure $ mkStrLit x.getId.toString
let n <- pure $ mkIntLit n.getNat
mkAppM ``strIntEq #[x, n]
| `(assertion| $x:ident = $y:ident) => do
let x <- pure $ mkStrLit x.getId.toString
let y <- pure $ mkStrLit y.getId.toString
mkAppM ``strStrEq #[x, y]
| _ => throwUnsupportedSyntax
elab "astn " a:assertion : term => elabAssertionLit a
#reduce astn x = 2 -- good
-- Want: escape
#reduce forall (n : Int), Assert (astn n = x) (fun s => if s == "x" then 1 else 0) --bad: "unused variable: `n`"
-- I would like to be able to say `forall n, Assert (astn ~n = x) σ`, where the `~` _escapes_ to the quantified Int instead of getting interpreted as a string
I desire to figure out the escape, as i think the aggressive elaborator is right for most of my needed functionality
Kyle Miller (May 21 2025 at 20:35):
You can add ~term as additional syntax. I broke your syntax up into an assertionTerm category to reduce the number of cases to make. With a bit more effort, you could simplify elabAssertionLit by having some assertionTerm elaborator.
import Lean
open Lean Elab Meta
def Env : Type := String → Int
def Assertion : Type := Env → Prop
def Assert (assertion : Assertion) (σ : Env) : Prop := assertion σ
declare_syntax_cat assertionTerm
syntax ident : assertionTerm
syntax num : assertionTerm
syntax "~" term:max : assertionTerm
declare_syntax_cat assertion
syntax assertionTerm "=" assertionTerm : assertion
@[simp] def strIntEq (x : String) (y : Int) : Assertion := fun σ => σ x = y
@[simp] def strStrEq (x y : String) : Assertion := fun σ => σ x = σ y
@[simp] def strIntEqStr (x : Int) (y : String) : Assertion := fun σ => x = σ y
def elabAssertionLit : Syntax → TermElabM Expr
| `(assertion| $x:ident = $n:num) => do
let x <- pure $ mkStrLit x.getId.toString
let n <- pure $ mkIntLit n.getNat
mkAppM ``strIntEq #[x, n]
| `(assertion| $x:ident = $y:ident) => do
let x <- pure $ mkStrLit x.getId.toString
let y <- pure $ mkStrLit y.getId.toString
mkAppM ``strStrEq #[x, y]
| `(assertion| ~$t:term = $y:ident) => do
let x <- Term.elabTermEnsuringType t (some <| .const ``Int [])
let y <- pure $ mkStrLit y.getId.toString
mkAppM ``strIntEqStr #[x, y]
| _ => throwUnsupportedSyntax
elab "astn " a:assertion : term => elabAssertionLit a
#reduce astn x = 2
#reduce forall (n : Int), Assert (astn ~n = x) (fun s => if s == "x" then 1 else 0)
Quinn (May 21 2025 at 20:38):
woah thanks a ton! i'll take a look. During the meanwhile i cooked up this
| `(assertion| $n:num != $x:ident) => do
let n <- pure $ mkIntLit n.getNat
let localDecls ← getLCtx
match localDecls.findFromUserName? x.getId with
| some decl =>
let x <- pure $ decl.toExpr
mkAppM ``valValNe #[x, n]
| none =>
let x <- pure $ mkStrLit x.getId.toString
mkAppM ``strValNe #[x, n]
which got me a lot closer, but yours is probably better
Quinn (May 21 2025 at 21:29):
heck yeah! got the exact right functionality from your suggestion.
Quinn (May 21 2025 at 21:40):
i'm disappointed that mkAppM doesn't accept a quoted lambda, like \\`(fun x y => x + y). forcing me to define def plus ... outside of scope (for some reason it also didn't allow the use of a local let`)
Kyle Miller (May 22 2025 at 01:57):
It looks like you could implement all of this with a macro rather than an elaborator.
The mkAppM function is rather low-level. You can also construct syntax using syntax quotations and pass it to elabTerm instead, which is more flexible (and sort of like making a macro from within an elaborator).
Re why not using a local let, when metaprogramming you have to think about what exists when and what representations they have. The mkAppM function creates an Expr by looking at the type of a constant, then creates an application of that constant. With a local let, that only exists at elaboration time and it's thrown away. (And at runtime it's an actual value, not an expression.) Theoretically one could write something to extract a let binding and turn it into an Expr, but that's not something mkAppM could do.
Last updated: Dec 20 2025 at 21:32 UTC