Zulip Chat Archive
Stream: general
Topic: Best practice to annotate AST?
xiao (Mar 04 2025 at 04:37):
I want to design a DSL, probably with some annotations like line number or doc-string. I may also extend the syntax tree for some specific syntax sugar. What is the best way to do this? In Haskell, there's one example with the Mu-type.
In that example, the basic language is the following.
inductive Exp where
| Const: Nat -> Exp
| Add: Exp -> Exp -> Exp
inductive Inst where
| Ann: String -> Inst
| Push: Nat -> Inst
| Add | Mult
deriving Repr
abbrev Prog := List Inst
def Exp.compile: Exp -> Prog
| .Const n => [.Push n]
| .Add a b => a.compile ++ b.compile ++ [.Add]
def Exp.eval: Exp -> Nat
| .Const n => n
| .Add a b => a.eval + b.eval
However, it will be a bit painful if you want to add extra fields or constructors to the defined Exp
, because you have to repeat the same logic many times.
inductive ExpA where
| Const: String -> Nat -> ExpA
| Add: String -> ExpA -> ExpA -> ExpA
def ExpA.compile: ExpA -> Prog
| .Const s n => [.Ann s, .Push n]
| .Add s a b => [.Ann s] ++ a.compile ++ b.compile ++ [.Add]
def ExpA.eval: ExpA -> Nat
| .Const _ n => n
| .Add _ a b => a.eval + b.eval
inductive ExpM where
| Const: Nat -> ExpM
| Add: ExpM -> ExpM -> ExpM
| Mult: ExpM -> ExpM -> ExpM
def ExpM.compile: ExpM -> Prog
| .Const n => [.Push n]
| .Add a b => a.compile ++ b.compile ++ [.Add]
| .Mult a b => a.compile ++ b.compile ++ [.Mult]
def ExpM.eval: ExpM -> Nat
| .Const n => n
| .Add a b => a.eval + b.eval
| .Mult a b => a.eval * b.eval
I understand that the inductive
type is itself the fixed point operator, so I tried the following. I made a functor and fixed it with another inductive type, which gives the catamorphism.
inductive ExpF (T: Type) where
| Const: Nat -> ExpF T
| Add: T -> T -> ExpF T
inductive Exp where
| In: ExpF Exp -> Exp
def Exp.const n := Exp.In (ExpF.Const n)
def Exp.add a b := Exp.In (.Add a b)
def Exp.cata {B} (f: ExpF B -> B): Exp -> B
| .In (.Const n) => f (.Const n)
| .In (.Add a b) => f (.Add (a.cata f) (b.cata f))
Then, the compile
is defined based on the functor ExpF
abbrev Prog := List Inst
def ExpF.compile: ExpF Prog -> Prog
| .Const n => [.Push n]
| .Add a b => a ++ b ++ [.Add]
def ExpF.eval: ExpF Nat -> Nat
| .Const n => n
| .Add a b => a + b
def Exp.eval := Exp.cata ExpF.eval
def Exp.compile := Exp.cata ExpF.compile
def e23: Exp := .add (.const 2) (.const 3)
example: e23.eval = 5 := by eq_refl
example: e23.compile = [.Push 2, .Push 3, .Add] := by eq_refl
It works well with ExpA
and ExpM
inductive ExpA where
| Ann: String -> ExpF ExpA -> ExpA
def ExpA.cata
{B: Type}
(f: String -> ExpF B -> B)
: ExpA -> B
| .Ann s (.Const n) => f s (.Const n)
| .Ann s (.Add a b) => f s (.Add (a.cata f) (b.cata f))
def ExpA.eval := ExpA.cata (fun _ b => ExpF.eval b)
def ExpA.compile := ExpA.cata (fun s b => .Ann s :: ExpF.compile b)
def a2: ExpA := (.Ann "pos1" (.Const 2))
def a3: ExpA := (.Ann "pos2" (.Const 3))
def a23: ExpA := (.Ann "pos3" (.Add a2 a3))
example: a23.eval = 5 := by eq_refl
example: a23.compile = [
Inst.Ann "pos3",
Inst.Ann "pos1",
Inst.Push 2,
Inst.Ann "pos2",
Inst.Push 3,
Inst.Add
] := by eq_refl
inductive ExpM where
| In: ExpF ExpM -> ExpM
| Mult: ExpM -> ExpM -> ExpM
def ExpM.mult := ExpM.Mult
def ExpM.const n := ExpM.In (.Const n)
def ExpM.add a b := ExpM.In (.Add a b)
def ExpM.cata {B} (f: ExpF B -> B) (mult: B -> B -> B): ExpM -> B
| .In (.Const n) => f (.Const n)
| .In (.Add n1 n2) => f (.Add (ExpM.cata f mult n1) (ExpM.cata f mult n2))
| .Mult a1 a2 => mult (a1.cata f mult) (a2.cata f mult)
def ExpM.eval := ExpM.cata ExpF.eval Nat.mul
def ExpM.compile := ExpM.cata ExpF.compile (fun a b => a ++ b ++ [.Mult])
def m523 := ExpM.mult (.const 5) (.add (.const 2) (.const 3))
example: m523.eval = 5 * (2 + 3) := by eq_refl
example: m523.compile = [
Inst.Push 5, Inst.Push 2, Inst.Push 3, Inst.Add, Inst.Mult
] := by eq_refl
This time I don't have to repeat the same logic many times, but the cons are that I have to rewrite the catamorphisms again and again, as well as the inductive principles (recursors).
def Exp.ind.{u}
{P: Exp -> Sort u}
(const: forall n, P (.const n))
(add: forall a b, P a -> P b -> P (.add a b))
:
forall (e: Exp), P e
:= fun e => match e with
| .In (.Const a) => const a
| .In (.Add a b) => add a b
(Exp.ind const add a) (Exp.ind const add b)
def ExpM.ind.{u}
{P: ExpM -> Sort u}
(const: forall n, P (.const n))
(add: forall a b, P a -> P b -> P (.add a b))
(mult: forall a b, P a -> P b -> P (.mult a b))
: forall e, P e
| .const n => const n
| .add a b => add a b (a.ind const add mult) (b.ind const add mult)
| .mult a b => mult a b (a.ind const add mult) (b.ind const add mult)
What is the best practice for such a tree-decoration problem?
Some one also suggested the following.
inductive ExpF (O: Type) where
| Const: Nat -> ExpF O
| Add: ExpF O -> ExpF O -> ExpF O
| Out: O -> ExpF O
abbrev Exp := ExpF Empty
def ExpF.compile: ExpF Prog -> Prog
| .Const n => [.Push n]
| .Add a b => a.compile ++ b.compile ++ [.Add]
| .Out p => p
def ExpF.eval: ExpF Nat -> Nat
| .Const n => n
| .Add a b => a.eval + b.eval
| .Out n => n
def ExpF.cata {B} (f: ExpF B -> B): Exp -> B
| .Const n => f (.Const n)
| .Add a b => f (.Add (.Out $ a.cata f) (.Out $ b.cata f))
| .Out o => o.elim
def e23: Exp := .Add (.Const 2) (.Const 3)
def Exp.compile := ExpF.cata ExpF.compile
def Exp.eval := ExpF.cata ExpF.eval
example: e23.compile = [Inst.Push 2, Inst.Push 3, Inst.Add] := by eq_refl
example: e23.eval = 5 := by eq_refl
inductive ExpM where
| In: ExpF ExpM -> ExpM
| Mult: ExpM -> ExpM -> ExpM
@[simp] def ExpM.const (n: Nat): ExpM := .In (.Const n)
@[simp] def ExpM.add (a b: ExpM): ExpM := .In (.Add (.Out a) (.Out b))
@[simp] def ExpM.mult := ExpM.Mult
def m523: ExpM := .mult (.const 5) (.add (.const 2) (.const 3))
@[simp]
def ExpM.cata {B} (f: ExpF B -> B) (mult: B -> B -> B): ExpM -> B
| .In (.Const n) => f (.Const n)
| .In (.Add a b) =>
f (.Add
(.Out $ ExpM.cata f mult (.In a))
(.Out $ ExpM.cata f mult (.In b))
)
| .In (.Out o) => f (.Out $ o.cata f mult)
| .Mult a b => mult (a.cata f mult) (b.cata f mult)
@[simp]
def ExpM.compile := ExpM.cata ExpF.compile (fun a b => a ++ b ++ [.Mult])
@[simp]
def ExpM.eval := ExpM.cata ExpF.eval (fun a b => a * b)
example: m523.compile
= [Inst.Push 5, Inst.Push 2, Inst.Push 3, Inst.Add, Inst.Mult]
:= by
simp [ExpF.compile, m523]
example: m523.eval = 5 * (2 + 3) := by
simp [ExpF.eval, m523]
Or even
inductive ExpF (C A O: Type) where
| Const: C -> Nat -> ExpF C A O
| Add: A -> ExpF C A O -> ExpF C A O -> ExpF C A O
| Out: O -> ExpF C A O
abbrev Exp := ExpF Unit Unit Empty
def Exp.const (n: Nat): Exp := .Const Unit.unit n
def Exp.add (a b: Exp): Exp := .Add Unit.unit a b
def ExpF.compile {C A: Type}: ExpF C A Prog -> Prog
| .Const _ n => [.Push n]
| .Add _ a b => a.compile ++ b.compile ++ [.Add]
| .Out p => p
Last updated: May 02 2025 at 03:31 UTC