Compiler/Proofs/IRGeneration/SupportedSpec.lean historically hand-restates the
set of programs the compiler accepts as standalone Props or pattern-matching
Bools. That style duplicates compiler logic: every time the compiler relaxes
or tightens a gate, the proof layer's enumeration silently drifts and the
"supported" envelope quietly diverges from what the compiler actually accepts.
The repo already established a better pattern in the event-emission predicates, where the proof-side support predicate is defined by calling the actual compiler gating function, so the two cannot disagree by construction:
-- Compiler side (Compiler/CompilationModel/EventEmission.lean)
def eventParamScalarCompileSupported (ty : ParamType) : Bool := ...
-- Proof side (this file)
def eventParamScalarProofSupported (ty : ParamType) : Bool :=
eventParamScalarCompileSupported tyThe proof-side name signals intent ("what proofs depend on for soundness"),
while the body delegates to the compile-side Bool that drives the compiler's
real branch. Downstream lemmas like
eventParamScalarProofSupported_eventIsDynamicType_eq_false then simp through
both names interchangeably.
This PR converts two scalar/leaf predicates in SupportedSpec.lean to the same
shape, as an exemplar that the remaining ~70 hand-restated predicates in the
file can follow.
The hand-restated Prop:
def SupportedExternalScalarParamType : ParamType → Prop
| .uint256 | .int256 | .uint8 | .uint16 | .address | .bytes32 | .bool => True
| _ => Falseis the exact mirror of the compiler-side gating function in
Compiler/CompilationModel/ParamLoading.lean:
def isScalarParamType : ParamType → Bool
| ParamType.uint256 | ParamType.int256 | ParamType.uint8 | ParamType.uint16
| ParamType.address | ParamType.bool | ParamType.bytes32 => true
| _ => falseThe new proof-side alias is one line:
def externalParamScalarProofSupported (ty : ParamType) : Bool :=
isScalarParamType tyThe hand-restated Prop:
def SupportedExternalReturnProfile : List ParamType → Prop
| [] => True
| [ty] => SupportedExternalScalarParamType ty
| _ => Falsebecomes a Bool that pushes the per-element decision down to the compiler:
def externalReturnProfileProofSupported (returns : List ParamType) : Bool :=
decide (returns.length ≤ 1) && returns.all isScalarParamTypeBecause the heavily-used hand-restated predicates currently sit in proof
preconditions (∀ param ∈ params, SupportedExternalScalarParamType param.ty) across
many sibling modules, we keep the old defs and prove
old ↔ new instead of rewriting every caller in one shot. The biconditionals
are the meaning-preservation oracle:
theorem SupportedExternalParamType_iff_externalParamScalarProofSupported
(ty : ParamType) :
SupportedExternalScalarParamType ty ↔ externalParamScalarProofSupported ty = true
theorem SupportedExternalReturnProfile_iff_externalReturnProfileProofSupported
(returns : List ParamType) :
SupportedExternalReturnProfile returns ↔
externalReturnProfileProofSupported returns = trueBoth proofs are a single cases ... <;> simp [...] / match ... with => simp
because the cases line up. No sorry, no new axiom, no native_decide.
For each hand-restated proof-side predicate P in SupportedSpec.lean (or a
sibling proof module):
-
Find the compiler-side gating function. Grep the
CompilationModeldirectory for aBool(orExcept/Option-returning) function whose match arms classify the same syntactic cases asP. For ParamType-level leaves the usual suspects areisScalarParamType,isSingleWordStaticParamType,isDynamicParamType,isWordArrayParam,internalDynamicParamSupported,supportedCustomErrorParamType,eventParamScalarCompileSupported, andindexedDynamicArrayElemSupported. For expression / statement predicates the gate is typically thecompile*function whoseExcept Stringfailure path encodes the unsupported cases — in which case the proof-side wrapper checks.isOk/.isSome. -
Introduce a thin proof-side wrapper. Add a single-line
def fooProofSupported ... := compilerGate ...whose body is just the compiler call. The naming convention is<feature><Shape>ProofSupportedto matcheventParamScalarProofSupported. Place it near the existing predicates so the pattern is locally visible. -
Prove agreement with the hand-restated form. Add
theorem P_iff_fooProofSupported : P x ↔ fooProofSupported x = true. For leaf scalar/leaf predicates this is a one-liner:cases x <;> simp [P, fooProofSupported, compilerGate]. For predicates over lists or composite IR, recurse withmatch/inductionand reuse the leaf agreement lemma. ForExcept String-shaped gates, the agreement lemma isP x ↔ (compilerGate x).isOkand the proof works by case-splitting on theExceptresult. -
Decide on retention. If
Pis referenced widely outside the file (usegrep -rn P --include='*.lean'), keepPand only add the agreement theorem — the lemma is enough to feed both directions into existing proofs. IfPhas few callers, inline-replace each caller with the new wrapper and deleteP; the agreement theorem then degrades into a self-test you can keep or drop. -
Build the affected module.
lake build Compiler.Proofs.IRGeneration.SupportedSpec(or the sibling module). Iterate until clean: nosorry, no newaxiom, nonative_decide. The typical failure modes are:simpcan't close a case because the compiler bool depends on a helper that isn't unfolded yet. Add it to thesimpset.- The compiler bool is strictly broader/narrower than the hand version.
This is a drift bug — fix the spec to match the compiler, or
restrict the new wrapper with
&& extraCondition. - The compiler gate is
Except/Option-shaped. Don'tsimpthe failure branch away; pattern-match it and prove the failure case maps toP x = False.
-
Once a critical mass of predicates is converted, audit the resulting
SupportedSpecand consider deleting the original hand-restated Props entirely, since every call site that usedP xnow factors throughP_iff_fooProofSupportedto talk aboutfooProofSupported x = true.
lake build Compiler.Proofs.IRGeneration.SupportedSpec was run after the
edits and failed, but not because of this conversion. The branch this work
sits on top of has a pre-existing missing-case error: a recent commit added a
new Expr.txOrigin variant (see commits introducing txOrigin in
Compiler/CompilationModel/UsageAnalysis.lean, LogicalPurity.lean,
ValidationInterop.lean, etc.) but the proof-side files that pattern-match
exhaustively on Expr were never updated. The first build failure surfaces
in Compiler/Proofs/IRGeneration/ExprCore.lean:18 (the exprBoundNames
mutual block):
error: Compiler/Proofs/IRGeneration/ExprCore.lean:18:2: Missing cases:
Expr.txOrigin
error: Lean exited with code 1
SupportedSpec.lean itself has approximately ten further exhaustive Expr
matches that would also need .txOrigin added (every exprTouchesUnsupported*Surface
function around lines 604, 754, 810, 863, 922, 990, 1053, 1114, 1176, 1896).
Fixing all of these is outside the conversion scope this task was chartered
for (replicating the eventParamScalarCompileSupported pattern). The two
new defs (externalParamScalarProofSupported,
externalReturnProfileProofSupported) and the two agreement theorems were
verified by inspection — they each rely only on simp with the relevant
definitions in scope and follow the same recipe that's already known to
compile for eventParamScalarProofSupported. They contain no sorry, no
new axiom, and no native_decide.
Recommended unblock path (separate, focused PR): sweep
Compiler/Proofs/IRGeneration/{ExprCore.lean, SupportedSpec.lean, FunctionBody.lean, SourceSemantics.lean, ...}
and add .txOrigin to every exhaustive Expr enumeration, mirroring the
existing .caller/.contractAddress/.chainid handling (zero bound names,
no helper calls, no unsupported surface). Once that lands,
lake build Compiler.Proofs.IRGeneration.SupportedSpec should pick up the
work here and the two agreement theorems should close as written.