Zulip Chat Archive
Stream: general
Topic: Definitional equality woes in metaprogramming (reification)
l1mey (Dec 27 2025 at 23:31):
(See mwe below) I am building a tactic that manipulates expressions in a reified intermediate representations similar to bv_decide. It operates on iN n, which is similar to a BitVec n:
inductive iN (bits : Nat) : Type where
| bitvec : BitVec bits → iN bits
| poison : iN bits
How it would generally work is the following:
(reify) Expr -> IR, returns a proof that IR.eval = expr
(denote) IR -> Expr, returns a proof that IR.eval = expr
given opt : IR -> IR and lhs:
ir = reify lhs (IR.eval ir = lhs)
ir' = opt ir (IR.eval ir ~> IR.eval ir') (use opt.wf)
rhs = denote ir' (IR.eval ir' = rhs)
chain the proofs to end up with lhs ~> rhs
I'm having issues creating my reify function. It currently accepts expressions of the form
fun {n} ... {m} (x : iN n) ... (z : iN m) : iN n => ...
and builds up two assignments
structure PackediN where
{n : Nat}
x : iN n
deriving BEq
abbrev Assignment := Lean.RArray PackediN
abbrev WidthAssignment := Lean.RArray Nat
such that if fun {n} (x : iN n) => x, it should assign
let ξ : WidthAssignment := (RArray.leaf n)
let σ : Assignment := RArray.leaf { n := ξ.get 0, x := x }
let ir : IR 0 := IR.var 0
and return a value ⟨IR.var 0, x_expr, pure none⟩ : ReifiedIR 0 where
structure ReifiedIR (idx : Nat) where
irExpr : IR idx
originalExpr : Expr
/- proof that IR.eval irExpr = originalExpr, or by rfl if none -/
proof : M (Option Expr)
Evaluating the ir given the two assignments assigns semantics to it. When reifying, similar to bv_decide, it assumes that the evaluation of the IR.var is definitionally equal to x. However, this is not the case, as you can see:
def eval (ξ : WidthAssignment) (σ : Assignment) : IR idx → iN (ξ.get idx)
| var id =>
let pack := σ.get id
/- h is always true, this if is for totality -/
if h : pack.n = (ξ.get idx) then
h ▸ pack.x
else
pack.truncate (ξ.get idx)
| const_poison => poison
theorem defeq {n} (x : iN n)
: IR.eval (RArray.leaf n) (RArray.leaf { n := (RArray.leaf n).get 0, x := x }) (IR.var 0 : IR 0) = x := by
rfl
/-
Tactic `rfl` failed: The left-hand side
IR.eval (RArray.leaf n) (RArray.leaf { n := (RArray.leaf n).get 0, x := x }) (IR.var 0)
is not definitionally equal to the right-hand side
x
-/
simp [IR.eval] -- this works!
Taking a look at the proofs that bv_decide creates, the difference between my tactic and bv_decide is shown. Since I want my tactic to work on rewriting quantified bitvectors in the IR, not just bitvectors with a fixed width, I need to introduce two different assignments. One set of assignments for the variables, and one set of assignments for the bitwidths.
theorem t8 (p : BitVec 8)
: Std.Tactic.BVDecide.BVExpr.eval (Lean.RArray.leaf { w := 8, bv := p })
(Std.Tactic.BVDecide.BVExpr.var 0) = p := by
rfl
/- when you introduce a quantifier, it fails to be definitionally equal! -/
theorem tn {n} (p : BitVec n)
: Std.Tactic.BVDecide.BVExpr.eval (Lean.RArray.leaf { w := n, bv := p })
(Std.Tactic.BVDecide.BVExpr.var 0) = p := by
rfl -- fails!
Creating a general theorem to prove this works as well, as you can see inlining the ξ : WidthAssignment makes this theorem type nice as ξ.get 0 = n holds as a definitional equality:
theorem reflect_var_eval_var {idx id} (x : iN n) (σ : Assignment)
(hb : (σ.get id).n = n)
(h : hb ▸ (σ.get id).x = x)
: IR.eval (RArray.leaf n) σ (IR.var id : IR idx) = x := by
simp [IR.eval, hb, h]
Furthermore, hb and h are definitional equalities as well when you inline σ in the "evals at atoms assignments" part of reify.
I guess the question I am asking is that how would I formulate this in a nice way in my tactic given that they're not definitionally equal? Or, maybe they are definitionally equal and I am formulating it wrongly?
An mwe is attached: mwe.lean, it doesn't depend on anything but Lean and Qq. I'm quite new to metaprogramming tactics in this style, so some guidance would be greatly appreciated!
l1mey (Dec 28 2025 at 02:12):
Maybe someone might be able to answer why the theorem defeq isn't provable by just definitional equality, is it because of the variables on the type?
theorem defeq {n} (x : iN n)
: IR.eval (RArray.leaf n) (RArray.leaf { n := (RArray.leaf n).get 0, x := x }) (IR.var 0 : IR 0) = x := by
rfl
/-
Tactic `rfl` failed: The left-hand side
IR.eval (RArray.leaf n) (RArray.leaf { n := (RArray.leaf n).get 0, x := x }) (IR.var 0)
is not definitionally equal to the right-hand side
x
-/
Aaron Liu (Dec 28 2025 at 02:24):
it's because you have a if h : pack.n = (ξ.get idx)
Aaron Liu (Dec 28 2025 at 02:25):
the way docs#Nat.decEq reduces this will only definitionally reduce to true when both sides are definitionally the same numeral (not a variable)
l1mey (Dec 28 2025 at 03:29):
I understand. I guess instead of definitional equality it would just suffice to create a simp context and do the equivalent of simp [IR.eval] to prove the goal. I'll add in caching as well like bv_decide.
Last question, what is the interaction with definitional equality and decidable equality? I thought definitional equality does reduction based on the definition, I didn't know it called out to decidable instances.
l1mey (Dec 28 2025 at 03:32):
Ah, nevermind. dite requires a decidable h.
Aaron Liu (Dec 28 2025 at 03:53):
I see above the if statement
/- h is always true, this if is for totality -/
if h : pack.n = (ξ.get idx) then
so maybe if you pass in a proof instead of making the function total, then you can still get definitional equality?
l1mey (Dec 28 2025 at 04:53):
The issue is I can't do such a thing, because the IR.eval function is meant to work for things that aren't .var as well. In the mwe and code snippets, I include cases for .const_poison with the rest of the cases omitted. It wouldn't work in general.
Last updated: Feb 28 2026 at 14:05 UTC