Skip to content

feat: erased declarations in do notation - #15090

Merged
sgraf812 merged 28 commits into
masterfrom
sg/ghost-do
Sep 11, 2026
Merged

feat: erased declarations in do notation#15090
sgraf812 merged 28 commits into
masterfrom
sg/ghost-do

Conversation

@sgraf812

@sgraf812 sgraf812 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

This PR adds erased state to do notation: erased x := e, erased mut x := e, and erased x ← act declare verification-only variables that loop invariant clauses and assertions can read while compiled code carries only a dummy in their place.

def erasedDoubleSum (xs : List Nat) : Id Nat
    ensures r => ∃ n, r = 2 * n := do
  let mut acc := 0
  erased mut half : Nat := 0
  for x in xs invariant _pre _suff => acc = 2 * half do
    acc := acc + x + x
    half := half + x
  return acc

An erased variable is backed by the new type Erased in Init.Data.Erased, upstreamed from Mathlib.
erased is a doElem of its own beside let and have. Only the single variable form as the left-hand side pattern is supported.

@sgraf812 sgraf812 added the changelog-language Language features and metaprograms label Sep 9, 2026
@github-actions github-actions Bot added the toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN label Sep 9, 2026
@mathlib-lean-pr-testing

mathlib-lean-pr-testing Bot commented Sep 9, 2026

Copy link
Copy Markdown

Mathlib CI status (docs):

  • ❗ Batteries/Mathlib CI will not be attempted unless your PR branches off the nightly-with-mathlib branch. Try git rebase cc0ce2a3a57816b8e846006ffadfa9d8f34e0903 --onto d359ee1694e7e93efbcf2a23a56ea92340ccd001. You can force Mathlib CI using the force-mathlib-ci label. (2026-09-09 10:01:56)
  • ❗ Batteries/Mathlib CI will not be attempted unless your PR branches off the nightly-with-mathlib branch. Try git rebase cc0ce2a3a57816b8e846006ffadfa9d8f34e0903 --onto fe53f21e05980b97d7bbb6717cf7566e3700e576. You can force Mathlib CI using the force-mathlib-ci label. (2026-09-09 12:19:32)
  • 💥 Mathlib branch lean-pr-testing-15090 build failed against this PR. (2026-09-10 14:15:53) View Log
  • ❗ Mathlib CI can not be attempted yet, as the nightly-testing-2026-09-10 tag does not exist there yet. We will retry when you push more commits. If you rebase your branch onto nightly-with-mathlib, Mathlib CI should run now. You can force Mathlib CI using the force-mathlib-ci label. (2026-09-10 14:53:00)

@sgraf812 sgraf812 added changelog-library Library downstream Request a downstream-lean4 adaptation PR. labels Sep 10, 2026
@sgraf812
sgraf812 marked this pull request as ready for review September 10, 2026 12:24
@github-actions github-actions Bot added the mathlib4-nightly-available A branch for this PR exists at leanprover-community/mathlib4-nightly-testing:lean-pr-testing-NNNN label Sep 10, 2026
@mathlib-lean-pr-testing mathlib-lean-pr-testing Bot added the breaks-mathlib This is not necessarily a blocker for merging: but there needs to be a plan label Sep 10, 2026
@sgraf812 sgraf812 removed the changelog-library Library label Sep 10, 2026
@sgraf812 sgraf812 changed the title feat: ghost declarations in do notation feat: erased declarations in do notation Sep 10, 2026
@sgraf812
sgraf812 force-pushed the sg/ghost-do branch 2 times, most recently from d97bb75 to 61a42c0 Compare September 10, 2026 17:35
@downstream-lean4

Copy link
Copy Markdown

The adaptation PR for this PR is leanprover/downstream-lean4#64.

This PR adds ghost state to `do` notation: `ghost x := e`, `ghost mut x := e`, `ghost x ← act`, and ghost patterns declare verification-only variables that loop `invariant` clauses and assertions can read while compiled code carries only a dummy in their place.

A ghost variable holds a value of the new type `Erased` in `Init.Data.Erased`, a copy of Mathlib's type. Reads are written `x.out`, reassignments wrap the value in `Erased.mk`, and `@[macro_inline]` on `Erased.mk` moves the argument into erased constructor fields before the noncomputability check, so ghost updates compile while any use of a ghost value in a relevant position is rejected through the noncomputability of `Erased.out`. The `grind` lemmas `Erased.out_mk` and `Erased.mk_out` discharge the verification conditions ghost variables produce.

`ghost` is a doElem of its own beside `let` and `have`, and both forms elaborate nondependently. The parsers `doGhost` and `doGhostArrow` use `nonReservedSymbol "ghost " (includeIdent := true)`, so `ghost` stays a legal identifier, and a doElem head spelled `ghost … := …` or `ghost … ← …` always parses as a ghost declaration. A ghost pattern binds its variables plainly and shadows each with its ghost redeclaration; `ghost x ← act` runs the action and erases its result. Refutable ghost patterns and `|` alternatives are rejected, since erased data cannot decide control flow. Quotations naming the new parsers inside `Lean.Elab` use `internal.parseQuotWithCurrentStage` until stage0 includes them.
A ghost variable now appears at its underlying type everywhere the source names it: the local context, reassignment right-hand sides, `invariant` and `decreasing` clauses, and `assert`. The carried `Erased` binding takes an inaccessible name, and the source name binds a `.out` shadow that is zeta-substituted away at scope close, so it reaches proofs and never compiled code. Loop state tuples, join points, and nested-do tunneling carry the `Erased` binding; the shadow is re-introduced at every rebinding point.
A ghost variable's carried `Erased` binding and its `.out` shadow now share the source name, alternating by shadowing exactly as reassignments of `mut` variables do. `MutVar` returns to its original shape plus a `ghost` flag, the discipline `LetOrReassign` carries the ghost information as a `Bool` on `let` and `reassign`, and both ghost effects, registration and shadow binding, live in `elabWithReassignments`. Sites that pack variables into runtime state (loop state tuples, join points, effect forwarders) wrap ghost values in `Erased.mk` and their slot types in `Erased`. The shadow closes before the carried binder does, so zeta substitution never re-introduces a bound variable.
Concentrate all ghost handling in `elabDoLetOrReassign`: it upgrades a reassignment to a ghost reassignment by looking up the variable, rewrites ghost declarations and reassignments to bind the wrapped value, and expands ghost patterns; `elabDoGhost` and `elabDoReassign` reduce to funnels. A ghost pattern `ghost (a, b) := e` now never computes `e`: it binds `e` whole as one ghost variable and each pattern variable by matching that variable in erased positions, matching the single-variable form. Rename the shadow helpers to `withErasedProj`/`wrapErasedProjs`, match the carried type with `let_expr`, make the `Erased` imports `meta`, drop the trailing space in the `notFollowedBy` ghost entry, and remove the `ghost` field default.
A ghost pattern would bind its variables to `match` projections, which reduce worse in proofs than the tuple the variable holds, so `doGhost` and `doGhostArrow` parse only a single identifier.
Uses of a ghost variable resolve to its `.out` projection, so find-references and rename need the projection fvars in the alias table alongside the carried rebindings.
Tuple rebinds contribute only aliases for non-ghost variables, so the projections there do the same; reassignment-site projections already anchor at the site ident via `elabWithReassignments`, and join projections at the declaration ident like the join term infos.
The `ghost` bit on `LetOrReassign.reassign` only cached what `findMutVar?` already knows, so it is gone; `.reassign` is nullary again and `elabWithReassignments` looks the variables up. `wrapGhostDecl` overlapped `pushTypeIntoReassignment` inconsistently: it applied the user's ascription inside the wrap where the latter checks the ascription against the declared type and then pins that type. The wrap now runs after `pushTypeIntoReassignment` as the mechanical `t ↦ Erased t`, `e ↦ Erased.mk e`, so ghost reassignments get the same ascription semantics as plain ones.
A reassignment either has one variable, whose `MutVar` answers the question, or several, none of which is ghost: `checkMutVars` enforces that a ghost variable takes the single-variable form, `isGhost` reads the first variable, and `elabWithReassignments` binds the projections for all variables or none.
Every site that packs a mutable variable into runtime state repeated the lookup plus the ghost `Erased` wrap; the two accessors name it once, and `mkErasedApp` folds into `stateType`.
The bind binder takes the action's result at the variable's declared type under a fresh name, and the reassignment routes through `elabDoLetOrReassign`, the one home of type pinning, ghost wrapping, projection rebinding and alias registration. This also registers the previously missing `FVarAliasInfo` for plain arrow reassignments, which `elabDoIdDecl` never emitted.
`doReassignArrow` expands to `do let __x ← act; x := __x` for identifiers, patterns and holes alike, so `elabDoReassignArrow` is gone and `elabDoArrow` rejects `.reassign` as an elaborator bug. The `:=`-reassignment owns type pinning, ghost wrapping, projection rebinding and alias registration, and `InferControlInfo` sees the expansion, so no arrow-specific reassignment logic remains anywhere.
`doGhostArrow` expands to `do let __x ← act; ghost x := __x`, so its elaborator is gone and `InferControlInfo` reads the expansion. The ghost element is built with a named quotation and spliced, since the category grammar of the compiling stage predates `doGhost`.
Its one caller is `doLetArrow`: reassignment arrows expand to `:=` before elaboration, and `have` has no arrow form, so the `LetOrReassign` parameter and the dead `.have` and `.reassign` arms go. A pattern arrow re-emits a `let` unconditionally, which for the bind binder is indistinguishable from `have`.
`wrapErasedProjs`'s syntax-level lets survive into verification conditions where the join-point path inlines the projection, so the gadget call zeta-substitutes them after elaboration and annotation goals carry ghost projections inline uniformly.
The else form swallows the rest of the block into `rest?`, so coercing the element to `Unit` there rejects valid programs; state the fact where the hoist looks tempting.
`notFollowedByRedefinedTermToken` also guards `doExpr`, so listing `ghost` there broke every statement headed by an identifier of that name. Follow `assert` instead: the ghost parsers take priority `default+10`, which breaks their equal-length tie with the pattern-reassignment parse of `ghost x := e`, and `doExpr`'s trailing guard already rejects that shape. Applications, reassignments and binds of a variable named `ghost` parse again.
Restore the push-then-wrap order that the ghost-bit removal reverted: `pushTypeIntoReassignment` runs for ghost reassignments too, so a contradicting ascription errors like a plain one, and `wrapGhostDecl` is the mechanical wrap again. The ident form of `x ← act` moves from the expansion macro back to an elaborator that pins the variable's declared type on the bind, so a type error blames the action instead of the hygienic bind variable; patterns and the else rejection stay in the macro.
The pin forces an elaborator for the ident form, so the pattern form and the else rejection join it instead of living in a macro half; the legacy elaborator's native path resumes entirely.
An ascribed ghost binding wraps its value as `Erased.mk (e : t)`, so a mismatch reports the plain ascription error instead of an application mismatch inside the generated `Erased.mk`.
A definition that uses a ghost value in compiled code now reports what a ghost variable is and where its value is available, instead of naming `Erased.out` as a noncomputable dependency.
Name the evidence, `Erased.out`, state the inference and the rule declaratively, and keep the sibling message's cadence.
The term elaborator substitutes the `.out` projection let away itself, replacing the post-elaboration expression pass.
Both producers of a ghost declaration already guarantee the binder-free ident shape that `wrapGhostDecl` destructures.
The doElem keyword, the parser nodes, the elaborators, and the `MutVar` flag all carry the `erased` name, matching the `Erased` type that implements them.
@sgraf812
sgraf812 added this pull request to the merge queue Sep 11, 2026
Merged via the queue into master with commit a714e83 Sep 11, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaks-mathlib This is not necessarily a blocker for merging: but there needs to be a plan changelog-language Language features and metaprograms downstream Request a downstream-lean4 adaptation PR. mathlib4-nightly-available A branch for this PR exists at leanprover-community/mathlib4-nightly-testing:lean-pr-testing-NNNN toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant