Zulip Chat Archive
Stream: metaprogramming / tactics
Topic: Cryptic "unknown constant" error when using syntax extension
Dylan Ede (Jun 04 2024 at 15:10):
Hello! I'm new to Lean but I've dived straight into metaprogramming. I'm interested in implementing "staged compilation" in the style of Toatie (github). To this end I've tried to make some syntax extensions to emulate the staging part of Toatie's type system. All was going well, until I hit a cryptic error message as detailed in the code below, which appears to be caused by the use of my new syntax, since a desugared version of the offending function (also presented below) does not encounter the error. Does anyone have any idea what is going on?
Note that the code is necessarily split into two modules since environment extensions cannot be used in the same module they are defined in.
LeanHDL/StageExt.lean
import Lean
open Lean EnvExtension
-- This extra environment state tracks the current syntactic staging "depth"
initialize stageEnv : EnvExtension Nat ← registerEnvExtension $ pure 0
def getStage [Monad m] [MonadEnv m] : m Nat := stageEnv.getState <$> getEnv
-- Executes `x` in an environment with the specified `stageEnv` value
def withStage [Monad m] [MonadFinally m] [MonadEnv m] (s : Nat) (x : m α) : m α := do
withEnv (stageEnv.setState (← getEnv) s) x
LeanHDL/Basic.lean
import Lean
import LeanHDL.StageExt
/-
(Very) partial re-implementation in Lean of "Toatie: functional hardware description with dependent types" by Craig Ramsay
https://stax.strath.ac.uk/concern/theses/6w924c39c
-/
-- Unlike in the paper, I've put stage tracking in the `Code` type itself
-- Ideally there should be no way of using this type except through the syntax defined below
structure Code (stage : Nat) (α : Sort u) where
«~» : α
section Syntax
open Lean Elab Term
-- The corresponding syntax in the toatie paper is ⟨_⟩
syntax (name := «stx ⟪_⟫») "⟪" term "⟫" : term
@[term_elab «stx ⟪_⟫»]
def «elab ⟪_⟫» : TermElab
| `(⟪ $ty:term ⟫), ty? => do
let stage <- getStage
let stageLit := Lean.Syntax.mkNumLit $ toString stage
withStage stage.succ $ elabTerm (<- `(Code $stageLit:num $ty)) ty?
| _, _ => throwUnsupportedSyntax
-- The corresponding syntax in the toatie paper is ⟦_⟧
syntax (name := «stx [|_|]»)"[|" term "|]" : term
@[term_elab «stx [|_|]»]
def «elab [|_|]» : TermElab
| `([| $t |]), ty? => do
let stage <- getStage
let stageLit := Lean.Syntax.mkNumLit $ toString stage
withStage stage.succ $ elabTerm (<- `(Code.mk (stage := $stageLit:num) $t)) ty?
| _, _ => throwUnsupportedSyntax
-- The corresponding syntax in the toatie paper is ˜_
syntax:max (name := «stx ~_») "~" term:max : term
@[term_elab «stx ~_»]
def «elab ~_» : TermElab
| `(~ $t), ty? => do
let stage <- getStage
let stagePredLit := Lean.Syntax.mkNumLit $ toString stage.pred
let «⟪t⟫» <- withStage stage.pred $ elabTerm (<- `(Code.«~» (stage := $stagePredLit:num) $t)) ty?
if stage == Nat.zero && «⟪t⟫».hasFVar then
throwError "Free variables not allowed when escaping to stage 0"
pure «⟪t⟫»
| _, _ => throwUnsupportedSyntax
-- All of the following types and functions are direct translations from the examples in the paper
inductive Bit : Nat -> Type where
| O : Bit 0
| I : Bit 1
inductive Unsigned : Nat -> Nat -> Type where
| nil : Unsigned 0 0
| cons : Unsigned width val -> Bit b -> Unsigned width.succ (b + val * 2)
inductive BitPair : Nat -> Type where
| mk : Bit a -> Bit b -> {p : c = a * 2 + b} -> BitPair c
def bitAdd : Bit c -> Bit x -> Bit y -> BitPair (x + y + c)
| .O, .O, .O => BitPair.mk .O .O (p := rfl)
| .O, .O, .I => BitPair.mk .O .I (p := rfl)
| .O, .I, .O => BitPair.mk .O .I (p := rfl)
| .O, .I, .I => BitPair.mk .I .O (p := rfl)
| .I, .O, .O => BitPair.mk .O .I (p := rfl)
| .I, .O, .I => BitPair.mk .I .O (p := rfl)
| .I, .I, .O => BitPair.mk .I .O (p := rfl)
| .I, .I, .I => BitPair.mk .I .I (p := rfl)
/- Why does `addU` fail to compile with (only) the below error message?
unknown constant 'addU.match_2'
-/
def addU : {w x y : Nat} -> ⟪Unsigned w x⟫ -> ⟪Unsigned w y⟫ -> ⟪Bit c⟫ -> ⟪Unsigned w.succ (x + y + c)⟫
| 0, 0, 0, _, _, cin => cast (by simp) [|Unsigned.cons .nil ~cin|]
| w + 1, _, _, ux, uy, cin => [|
let .cons xbs xb := ~ux
let .cons ybs yb := ~uy
let .mk cin' lsb := bitAdd ~cin xb yb
let r := ~(addU [|xbs|] [|ybs|] [|cin'|])
cast (by congr 1; simp_arith [*]) (Unsigned.cons r lsb)
|]
termination_by w => w
-- Yet this desugared version works
def addU2 : {w x y : Nat} -> ⟪Unsigned w x⟫ -> ⟪Unsigned w y⟫ -> ⟪Bit c⟫ -> ⟪Unsigned w.succ (x + y + c)⟫
| 0, 0, 0, _, _, cin => cast (by simp) [|Unsigned.cons .nil ~cin|]
| w + 1, _, _, ux, uy, cin => Code.mk (stage := 0) $
let .cons xbs xb := (Code.«~» (stage := 0) ux)
let .cons ybs yb := (Code.«~» (stage := 0) uy)
let .mk cin' lsb := bitAdd (Code.«~» (stage := 0) cin) xb yb
let r := Code.«~» (stage := 0) $ addU2 [|xbs|] [|ybs|] [|cin'|]
cast (by congr 1; simp_arith [*]) (Unsigned.cons r lsb)
Dylan Ede (Jun 04 2024 at 21:54):
I think it might be relevant that the desugared version (addU2
) did not need termination_by w => w
, and that without that line for addU
, the error message is
fail to show termination for
addU
with errors
structural recursion failed, produced type incorrect term
unknown constant 'addU.match_2'
Is there any way of seeing what is being "produced" (in the sense of that error message) to see what is going wrong?
Dylan Ede (Jun 05 2024 at 14:11):
Bizarrely, I have found that if I place the definition for addU2
above addU
, addU
compiles without error.
Kyle Miller (Jun 05 2024 at 17:28):
By the way, rather than let stageLit := Lean.Syntax.mkNumLit $ toString stage
you should be able to write let stageLit := quote stage
or, later, just `(Code $(quote stage):num $ty)
Kyle Miller (Jun 05 2024 at 17:47):
Found the error: withEnv
is way too strong, since it reverts the entire environment, not only your environment extension.
Kyle Miller (Jun 05 2024 at 17:48):
This is a fix:
def withStage [Monad m] [MonadFinally m] [MonadEnv m] (s : Nat) (x : m α) : m α := do
let oldS ← getStage
try
setEnv <| stageEnv.setState (← getEnv) s
x
finally
setEnv <| stageEnv.setState (← getEnv) oldS
Kyle Miller (Jun 05 2024 at 17:50):
I also modified the main file, but it doesn't seem necessary for the fix. The main thing is for ~
notation being sure to instantiate metavariables and having it make progress on instance synthesis problems before doing the hasFVar
test. It's possible you also want to check that hasMVar
is false since that's another way fvars can slip in, but in the future.
-- The corresponding syntax in the toatie paper is ⟨_⟩
syntax (name := «stx ⟪_⟫») "⟪" term "⟫" : term
@[term_elab «stx ⟪_⟫»]
def «elab ⟪_⟫» : TermElab
| `(⟪ $ty:term ⟫), ty? => do
let stage <- getStage
withStage (stage + 1) <| elabTerm (<- `(Code $(quote stage):num $ty)) ty?
| _, _ => throwUnsupportedSyntax
-- The corresponding syntax in the toatie paper is ⟦_⟧
syntax (name := «stx [|_|]»)"[|" term "|]" : term
@[term_elab «stx [|_|]»]
def «elab [|_|]» : TermElab
| `([| $t |]), ty? => do
let stage <- getStage
withStage (stage + 1) <| elabTerm (<- `(Code.mk (stage := $(quote stage):num) $t)) ty?
| _, _ => throwUnsupportedSyntax
-- The corresponding syntax in the toatie paper is ˜_
syntax:max (name := «stx ~_») "~" term:max : term
@[term_elab «stx ~_»]
def «elab ~_» : TermElab
| `(~ $t), ty? => do
let stage <- getStage
let «⟪t⟫» <- withStage (stage - 1) <| withSynthesize <| elabTerm (<- `(Code.«~» (stage := $(quote (stage - 1)):num) $t)) ty?
Term.synthesizeSyntheticMVars
let «⟪t⟫» <- instantiateMVars «⟪t⟫»
if stage == Nat.zero && «⟪t⟫».hasFVar then
throwError "Free variables not allowed when escaping to stage 0"
pure «⟪t⟫»
| _, _ => throwUnsupportedSyntax
Dylan Ede (Jun 05 2024 at 19:03):
Kyle Miller said:
Found the error:
withEnv
is way too strong, since it reverts the entire environment, not only your environment extension.
Aha! D'oh. Seems obvious in hindsight! Thank you very much.
Dylan Ede (Jun 05 2024 at 19:31):
And thanks for the tip about free variables!
Dylan Ede (Jun 05 2024 at 20:38):
One thing I have noticed though, is that if I insert a tryPostpone
near the end of «elab ~_»
, it appears that the correct stage environment is forgotten by the time the elaborator is attempted again - i.e. it thinks it's in stage 0 when it isn't. Any idea why that would be?
Dylan Ede (Jun 05 2024 at 20:45):
Hmm, looks like there's not much I can do about that because only what's in Lean.Elab.Term.SavedContext
is preserved for the postponed elaborator call. So I guess I need to do something to prevent any postponement from within the elaboration of t
, perhaps by catching postponement exceptions using withoutPostponing
.
Dylan Ede (Jun 13 2024 at 12:15):
I found a solution to the postponing issue, which is to not use EnvExtension
, and instead use Lean.Option
, which is preserved in the SavedContext
. The implementations of getStage
and withStage
then change to the following:
register_option hdl.stage : Nat := {
defValue := 0
}
def getStage : TermElabM Nat := do
return (← getOptions).getNat `hdl.stage
def withStage (stage : Nat) (x : TermElabM α) : TermElabM α := do
withOptions (fun options => options.setNat `hdl.stage stage) x
The downside now is that a user could easily just set_option hdl.stage 1
and break the guarantees. Not sure if there's a way around that.
Kyle Miller (Jun 13 2024 at 15:09):
You don't have to register options, and in fact the defVal
is not used by getNat
.
If you don't register the option, users can't set_option
, and furthermore you can make it very untypeable by using Name.num
.
def hdlStageOptionName : Name := .num decl_name% 0
def getStage : Elab.Term.TermElabM Nat := do
return (← getOptions).getNat hdlStageOptionName
Dylan Ede (Jun 13 2024 at 15:30):
Ah, thanks!
Last updated: May 02 2025 at 03:31 UTC