Zulip Chat Archive
Stream: general
Topic: How to evaluate a DSL using the elaborator?
Andrej Bauer (Nov 30 2024 at 16:35):
I would like to define a small domain-specific language (DSL) and use it during elaboration to define complex objects. The catch is that not every expression in this DSL leads to a well-defined object, and the compilation itself is more than just macro expansion.
I can illustrate what I'd like to do with the following small example. Suppose we defined a type of expressions made of numerals, variables and addition:
mport Mathlib.Control.Basic
/-- An inductive type of expressions with variables ranging over type `V`. -/
inductive Expr where
/-- Numeric constant -/
| num : Nat → Expr
/-- Variable -/
| var : String → Expr
/-- Addition -/
| plus : Expr → Expr → Expr
Such an expression is said to be closed if it does not contain any variables. A closed expression may be evaluated to a number. Here is a function which does so, or reports a variable that was encountered:
/-- Evaluate a closed expression and either return
`inr n` where `n` is its value, or `inl x` is variable
`x` is encoutnered. -/
def eval_closed : Expr → Sum String Nat
| .num k => return k
| .var x => .inl x
| .plus e₁ e₂ =>
do
let n₁ ← eval_closed e₁
let n₂ ← eval_closed e₂
return (n₁ + n₂)
#eval eval_closed (.plus (.num 4) (.num 5)) -- .inr 9
#eval eval_closed (.plus (.num 4) (.var "x")) -- .inl "x"
I would like to use the above function, or something like it, to introduce special notation ⟦ e ⟧
where e
denotes an expression, such that either ⟦ e ⟧
evaluates/elaborates to a numeral value, or the user sees an error message "cannot compile e
as variable x
appears in it".
-- imaginary examples
#check ⟦ plus (num 3) (num 5) ⟧ -- this is elaborated to 8
#check ⟦ pus (num 3) (var "x") ⟧ -- this reports an error: "x occurs"
Naively one would expect the above function eval_closed
to be useful, but I suspect it's not "meta-level" enough.
From what I understand this is a job for macros and the elaborator. I am ok with introducing additional syntax for expressions, or even a syntax category, so that we can control what may appear within ⟦ - ⟧
.
If I am going wrong about it, I'd be glad to hear how to do it right.
Mario Carneiro (Nov 30 2024 at 16:48):
open Lean hiding Expr
open Qq
elab "⟦" x:term "⟧" : term => do
let e ← unsafe Elab.Term.evalTerm Expr q(Expr) x
match eval_closed e with
| .inr n => return q($n)
| .inl x => throwError "{x} occurs"
open Expr
#check ⟦ plus (num 3) (num 5) ⟧ -- this is elaborated to 8
#check ⟦ plus (num 3) (var "x") ⟧ -- this reports an error: "x occurs"
Andrej Bauer (Nov 30 2024 at 17:47):
Thanks!
Notification Bot (Nov 30 2024 at 17:47):
Andrej Bauer has marked this topic as resolved.
Notification Bot (Nov 30 2024 at 18:23):
Andrej Bauer has marked this topic as unresolved.
Andrej Bauer (Nov 30 2024 at 18:25):
Hmm, does Elab.Term.evalTerm
think that its first argument must come from Type
? Is there an analogous function that allows me to evaluate to a type? The following seems problematic:
elab "⟦" A:term "|" e:term "⟧" : term => do
let A ← unsafe Lean.Elab.Term.evalTerm Type A --
...
I get:
application type mismatch
@Lean.Elab.Term.evalTerm Type
argument
Type
has type
Type 1 : Type 2
but is expected to have type
Type : Type 1
Andrej Bauer (Nov 30 2024 at 18:31):
More generally, is there a place where I can read about the interaction between the elaborator and type classes? It looks like I need some type class instances to evalute my expression.
Kyle Miller (Nov 30 2024 at 18:38):
Instead of evalTerm
(which uses the compiler and ir evaluator for its evaluation strategy), you could also use whnf
and friends as an evaluation strategy — just thought I'd mention it. It's a bit awkward though, since the expression matching code feels much more ad hoc.
Andrej Bauer (Nov 30 2024 at 20:05):
Staring at the definition of Elab.Term.evalTerm
, it seems odd that it limits the type to the lowest universe. Is this a feature or a bug?
Andrej Bauer (Nov 30 2024 at 20:06):
Actually, Eval.evalExpr
already suffers from this feature. How deep is the rabbit hole, I wonder.
Kyle Miller (Nov 30 2024 at 20:11):
It's something you can get around. The main issue is that it's monadic, and bind is has a strong universe constraint, so if anything is at Type
everything will be. docs#Lean.Environment.evalConst is universe polymorphic
Andrej Bauer (Nov 30 2024 at 20:12):
evalConst
does not seem to be useful for the purposes of the problem at hand. It feels like I am doing something wrong, it can't be that hard to write a small meta-level evaluator (maybe it's just macros) that can fail.
Kyle Miller (Nov 30 2024 at 20:14):
To use something like evalConst
, you add a new constant to the environment first with the value you want to evaluate.
Kyle Miller (Nov 30 2024 at 20:14):
For example, #eval
adds a temporary constant like this
Henrik Böving (Nov 30 2024 at 20:14):
What exactly is your plan with the Type
here? An element of type Type
has no meaning in the Lean runtime. Do you want to work with the runtime representation of the type or with the meta one?
Andrej Bauer (Nov 30 2024 at 20:16):
I think we're solving the wrong problem. If you look at the top of the thread, I'd just like a little DSL that gets expanded at elaboration time, except that (a) it's recursive and (b) it can fail.
Andrej Bauer (Nov 30 2024 at 20:16):
I think recursiveness is not a problem.
Andrej Bauer (Nov 30 2024 at 20:16):
I think I should be going in the direction of Mini-project
Kyle Miller (Nov 30 2024 at 20:21):
I think we're solving the wrong problem.
That may be true, but I'm just responding to your questions about the Type constraints of evalTerm
Kyle Miller (Nov 30 2024 at 20:27):
Here's a reduction-based strategy @Andrej Bauer. It uses a recognizer for Expr
that reduces to constructors. Given the examples it seems each expression will already be in whnf, but why not.
namespace DSL
/-- An inductive type of expressions with variables ranging over type `V`. -/
inductive Expr where
/-- Numeric constant -/
| num : Nat → Expr
/-- Variable -/
| var : String → Expr
/-- Addition -/
| plus : Expr → Expr → Expr
def eval_closed : Expr → Except String Nat
| .num k => return k
| .var x => .error x
| .plus e₁ e₂ =>
do
let n₁ ← eval_closed e₁
let n₂ ← eval_closed e₂
return (n₁ + n₂)
open Lean.Meta
partial def ofLeanExpr? (e : Lean.Expr) : Lean.MetaM Expr := do
match ← whnf e with
| Lean.mkApp (.const ``Expr.num []) n =>
let .lit (.natVal n) ← whnf n | failure
return Expr.num n
| Lean.mkApp (.const ``Expr.var []) (.lit (.strVal s)) =>
return Expr.var s
| Lean.mkApp2 (.const ``Expr.plus []) e₁ e₂ =>
return Expr.plus (← ofLeanExpr? e₁) (← ofLeanExpr? e₂)
| _ => failure
open Lean.Elab
elab "⟦" x:term "⟧" : term => do
let e ← Term.elabTermAndSynthesize x (some <| .const ``Expr [])
let some e' ← Lean.observing? (ofLeanExpr? e) | throwError "expecting a term that is recognizably an `Expr`, not{Lean.indentD e}"
match eval_closed e' with
| .ok n => return Lean.toExpr n
| .error s => throwError m!"{s} occurs"
open Expr
#check ⟦ plus (num 3) (num 5) ⟧ -- 8 : Nat
#check ⟦ plus (num 3) (var "x") ⟧ -- error: x occurs
Mario Carneiro (Nov 30 2024 at 22:00):
Should ofLeanExpr?
be a derive
able typeclass function?
Kyle Miller (Nov 30 2024 at 22:28):
That seems like it could be fairly useful
Edward van de Meent (Dec 01 2024 at 08:11):
That would be basically a function that puts terms in whnf and then unquotes?
Edward van de Meent (Dec 01 2024 at 08:23):
I imagine this becomes a bit hard to do in a universe-polymorphic way?
Andrej Bauer (Dec 01 2024 at 10:49):
Thanks for these examples, that's great. In my particular case, Expr
is parameterized by a type A
. Everything is also universe-polymorphic, but that's not a real obstacle, I suppose, as in most examples we'll have A : Type
. (Also, eval_closed
needs an instance of PCA A
.) Let me see if I can adapt your examples to the situation – thanks again.
Andrej Bauer (Dec 01 2024 at 11:08):
BTW, there's a kludge that I can resort to. The carrier sets I work with are inhabited, so I could just interpret any variable I see with a default value. Kind of what programming languages did in the 1960s and 70s, where variables had default initial values. It was a lot of fun to debug.
Andrej Bauer (Dec 01 2024 at 13:53):
At the risk of asking too many questions, here's what I think might be the last one. In a macro, when I parse an ident
, how do I convert to a string or Name
or some such entity?
declare_syntax_cat combinator
-- syntax:10 "≪" ident "≫" combinator:10 : combinator
syntax:90 "var" ident : combinator
syntax:90 "S" : combinator
syntax:90 "K" : combinator
syntax:40 combinator:40 "⬝" combinator:35 : combinator
syntax "[pca:" combinator "]" : term
macro_rules
| `([pca: var $x:ident]) => `(Expr.var x.toString) -- <==== What should stand here for x.toString?
| `([pca: K]) => `(Expr.K)
| `([pca: S]) => `(Expr.S)
| `([pca: $a:combinator ⬝ $b:combinator]) => `(Expr.app [pca: $a] [pca: $b])
Adam Topaz (Dec 01 2024 at 14:26):
x.getId
will get you the name. You can make it a string from there
Adam Topaz (Dec 01 2024 at 14:26):
Andrej Bauer (Dec 01 2024 at 14:58):
The bit that confuses is me how to quote/unqoute this stuff. Here's a MWE:
declare_syntax_cat cow
syntax "moo" ident : cow
syntax "[bovine:" cow "]" : term
macro_rules
| `([bovine: moo $x]) => `($x.getId) -- STRUGGLING HERE
#check [bovine: moo x] -- Should produce x as a name or a string
Kevin Buzzard (Dec 01 2024 at 15:00):
At the risk of asking too many questions
I don't know if you've noticed how many new posts there are on this Zulip if you ignore it for a 24 hour period, but the long and short of it is that I don't think you should worry about asking lots of questions. I believe that me perpetually asking questions (some of them quite stupid) in 2017/18, and sometimes getting really insightful answers, was what accelerated my learning of the language (and I had essentially no programming experience before that). Remember also that you're not the only person who's learning from these conversations.
Adam Topaz (Dec 01 2024 at 15:03):
Oh I see. You can surely make an elaborator and use toExpr
. I don’t know off the top of my head how to do this in a macro and I’m away from my computer at the moment.
Shreyas Srinivas (Dec 01 2024 at 15:14):
Andrej Bauer said:
The bit that confuses is me how to quote/unqoute this stuff. Here's a MWE:
declare_syntax_cat cow syntax "moo" ident : cow syntax "[bovine:" cow "]" : term macro_rules | `([bovine: moo $x]) => `($x.getId) -- STRUGGLING HERE #check [bovine: moo x] -- Should produce x as a name or a string
In these cases I try Lean.quote
. It is another matter that I find the whole quotation and anti quotation syntax in lean horrendously confusing and resort to the kind of trial and error method that I already unlearnt in undergrad as a hack.
Shreyas Srinivas (Dec 01 2024 at 15:14):
@loogle Lean.quote
loogle (Dec 01 2024 at 15:14):
:exclamation: unknown identifier 'Lean.quote'
Did you mean "Lean.quote"
?
Shreyas Srinivas (Dec 01 2024 at 15:16):
@loogle Lean.Quote.quote
loogle (Dec 01 2024 at 15:16):
:search: Lean.Quote.quote
Mario Carneiro (Dec 01 2024 at 17:21):
like so:
macro_rules
| `([bovine: moo $x]) => `($(Lean.quote x.getId))
Mario Carneiro (Dec 01 2024 at 17:23):
(the quotation isn't really necessary here but maybe it generalizes better to if you want to do other things)
Kyle Miller (Dec 01 2024 at 18:11):
Answering the earlier form of the question:
macro_rules
| `([pca: var $x:ident]) => `(Expr.var $(quote x.getId.toString))
You can also consider letting Expr.var
take a Name
; having hierarchical names can be convenient, and it gives you access to the hygienic name system.
Last updated: May 02 2025 at 03:31 UTC