Add dialect_option typecheck off to bypass DDM type checker#1125
Conversation
When a dialect sets `dialect_option typecheck off;`, elaboration skips `inferType` and `unifyTypes` for expression arguments. Implicit type parameter slots are filled with anonymous type placeholders (`.tvar _ ""`). This allows programs to elaborate even when the type checker cannot infer all type arguments (e.g., accessor-into-polymorphic-fn patterns). Changes: - Add `TypeExprF.skip` helper and `Dialect.typecheck` flag (AST.lean) - Refactor `Program.globalContext` into explicit `computeGlobalContext` - Add `typecheck` flag to `ElabContext` and `DeclContext` - Skip inferType/unifyTypes in `elabSyntaxArg` when typecheck is off - Fill unfilled type param slots with skip types in `runSyntaxElaborator` - Skip pre-registration in `elabOperation` when typecheck is off - Fix `applyNArgs` to handle tvar types (generates skip arg types) - Add `StrataDDL.setOptionCommand` with `dialect_option` syntax - Thread `Dialect.typecheck` through `elabProgramRest` → `DeclContext` - Update Ion.lean and Python/Specs/DDM.lean for Program refactor Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sorry from admit was causing the documentation build CI to fail. The proof is straightforward: array append with replicate fills to exactly n elements when args.size < n. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tautschnig
left a comment
There was a problem hiding this comment.
I'm concerned about the shape of the feature (not any specific details of the implementation). dialect_option typecheck off; is an escape hatch around a type-checker bug we don't yet understand. Merging this is fine and pragmatic, but I'd push for the following so the escape hatch doesn't silently become permanent technical debt:
-
File a tracking issue for the underlying type-checker bug. The PR description mentions "accessor-into-polymorphic-fn" as the specific failure mode from #650/#734, but there's no issue with "remove
typecheck offonce the root cause is fixed". Without that, any future user who hits a type error is going to reach fortypecheck offas a Band-Aid, and we won't have a way to tell whether the root cause has been addressed. -
Document which categories of program are expected to need this. The test file (
TypecheckSkip.lean) names exactly one shape (accessor → polymorphic fn). If that's the only expected use, say so in the DDM manual. If there are others, enumerate them. -
Warn about
typecheck off's blast radius in the docs.docs/verso/DDMDoc.leansays only what the option does, not what it doesn't do. Withtypecheck off, a program with a genuine type error (e.g., passing aLst IntewhereInteis expected) will elaborate silently. Downstream consumers — the VC generator, the symbolic evaluator, the SMT encoder — will then encounter the type error and produce confusing diagnostics ("Expression has type X when Y expected" at an IR level the user hasn't seen) or, worse, will silently proceed with ill-typed terms. A paragraph in the doc saying "turning this off means type errors surface at later pipeline stages with less-helpful messages" is fair warning. -
Audit
applyNArgs's new.tvarcase — it's a general change, not gated ontypecheck. AtElab/Core.lean:114-117, the new branch| .tvar ann _ => let placeholder := .placeholder ann let tvars := Array.replicate (n - args.size) placeholder .ok (⟨args ++ tvars, by simp [tvars]; omega⟩, placeholder)
runs regardless of the
typecheckflag. Previously the function errored ontvar; now it produces placeholder arguments. This means programs elaborated withtypecheck = true(the default) can now succeed where they previously errored — the broadening is not confined to the opt-in path. Is that intended? If so, worth a comment; if not, gate on(← read).typecheck. -
Program.globalContextrefactor is a cleanly-scoped improvement — removing the panic-in-default and exposingcomputeGlobalContext+Program.createis strictly better. I verified no other callers in-tree reach intoProgram.mkdirectly after this change. Good.
Test coverage:
The one test file covers the positive case (typecheck-off succeeds where typecheck-on fails). Missing cases that would pin the feature's behaviour:
- Negative test: typecheck off still catches unresolved identifiers. The PR description promises "Variable name resolution and global context population still work normally — only type inference is bypassed". A test that demonstrates an undefined identifier (e.g.,
assert [t]: undefined_name == 0;) still fails undertypecheck offwould pin this contract. - Test:
typecheck offacross a dialect import boundary. What happens when atypecheck = truedialect imports atypecheck = falsedialect?elabProgramRestthreadsd.typecheckintoDeclContext— wheredis the primary dialect. So the imported dialect's flag is ignored. Worth a test (and a docstring note) to pin this semantics. - Test: invalid
dialect_optionvalues fail cleanly.dialect_option typecheck maybe;ordialect_option nonsense on;should produce the explicit "expected 'on' or 'off'" / "Unknown option" errors — worth a#guard_msgsfixture to lock those error messages. - Test: option flag survives Ion round-trip. A DDM program with
typecheck offserialized to Ion and re-read should preserve the flag. If it doesn't, thetypecheckfield may need serialization support.
Proof-coverage suggestions:
Three small theorems pinning the load-bearing invariants:
/-- `placeholder` is definitionally a `tvar` with empty name. -/
@[simp] theorem TypeExprF.placeholder_def {α} (loc : α) :
TypeExprF.placeholder loc = TypeExprF.tvar loc "" := rfl
/-- `applyNArgs` on any tvar produces exactly `n` placeholder-typed
arguments — contract for the new `.tvar` branch. -/
theorem applyNArgs_tvar_placeholder
(tctx : TypingContext) (ann : _) (name : String) (n : Nat) :
applyNArgs tctx (.tvar ann name) n =
.ok (Vector.replicate n (.placeholder ann), .placeholder ann) := by
cases n <;> simp [applyNArgs, applyNArgs.aux, …]
/-- Elaboration with `typecheck = false` still rejects unresolved
identifier references (only type unification is bypassed). -/
theorem elabProgram_typecheck_off_preserves_resolution
(d : Dialect) (h : d.typecheck = false) (cmds : Array Operation) :
elabProgram d cmds = .ok p →
∀ op ∈ p.commands, ∀ id ∈ op.idents, identResolves p.globalContext idThe first is trivial; the second locks the new branch; the third is the user-visible contract ("name resolution still works") the PR description promises. Even as sorry-terminated stubs these would make the contract explicit.
Refactor / style:
elabSyntaxArghas three copies ofif !typecheck then return trees.set argIdx (some tree)at:1174,:1195(and conceptually a third for the unnamed preType arm). A tiny helperwould collapse the duplication and make future maintenance of the skip condition land in one place.private def bypassIfTypecheckOff (tree : Tree) (trees : …) : ElabM _ := do if !(← read).typecheck then return trees.set argIdx (some tree) else …
elabSetOptionCommanduses a nestedmatchon string pairs ("typecheck" × "on"/"off"). If more options are added, this will balloon. ADialectOptionsum type withfromKeyValue : String → String → Except String DialectOptionwould centralize the parsing.dialect_optionis a nice name, but the current implementation only recognizes one option (typecheck). If this is the only expected option long-term, consider a narrower syntax like@[typecheck off] dialect …. If more are coming, say so in a comment.- Minor:
TypeInfoconstruction atElab/Core.lean:1266-1273usesinputCtx := tctx0(the initial context) rather thant.resultContext. That's intentional (the placeholder doesn't depend on earlier args) but worth a one-line comment — a reader stepping through the generic flow will expect the per-arg context. - The renamed
TypeExprF.skip→TypeExprF.placeholder(commit 9449f1c) is an improvement. The docstring now says "An anonymous type placeholder used when type checking is skipped." — butapplyNArgsnow uses placeholder regardless of the typecheck flag. Update the docstring:/-- An anonymous type placeholder (`.tvar loc ""`) used when a concrete type cannot be inferred — either because type checking is skipped at the dialect level, or because `applyNArgs` encountered a tvar where an arrow type was expected. -/
|
I'll take a look at more detail. But I'm going to react to this:
In fact, I have a pretty good understanding of the issue #734 is a fix. However, there is a desire from the Strata Core folks to have this feature rather than expanding higher order type checking. I'll look into the more detailed review soon, and I appreciate it, but I'm kind of stuck here. We have three options from my perspective:
At this point, I am just ashamed how much work is going into not fixing an issue. I'll review the PR comments, but if it's too much work, then I'm going to move to option 2. |
- Add comment on applyNArgs tvar case explaining it's unconditional and why (tvar already represents an unresolved type; downstream unifyTypes still catches mismatches when typecheck is on) - Add warning paragraph to DDM manual about typecheck off blast radius - Fix typo: "Synthesisze" → "Synthesize" - Add #guard_msgs tests: unresolved identifiers still fail with typecheck off, invalid option values produce clean errors - Simplify placeholder docstring Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I disagree that this PR is problematic. I am fine mentioning this in the docs, but this option/escape hatch should be used when there is a separate typechecker that will be run on the resulting programs (as is the case for Core and Boole). It does not introduce any soundness/mistranslation issues, but rather reduces typechecking to a single function that can be reasoned about, avoiding some duplication. |
Add support for persisting the typecheck option through Ion
serialization. Uses a {type: option, name: "typecheck", value: "off"}
struct entry — only written when non-default, so existing Ion files
are read correctly (defaulting to typecheck = true).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
I second this. |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Responding to the top-level review: This isn't an escape hatch around a type-checker bug — it's a deliberate design choice. The Strata Core team implemented their own type checker downstream and doesn't want/need the DDM elaborator's type inference. They haven't ported everything away from Type/Expr at the DDM layer, and Re: the specific suggestions:
|
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 Overall this is a well-structured feature addition. The threading of the typecheck flag through the elaboration pipeline is clean, the Ion round-trip is handled correctly, and the test coverage demonstrates the core use case. A few items below.
Positives:
- The
computeGlobalContextextraction is a good refactor — it makesProgram.createthe single place where the panic lives, andglobalContextis now an explicit field rather than a computed default. - The
SymbolTableprivacy hardening and theSystemSymbolIds.leanextraction are clean improvements. - Test file is well-commented and covers the key scenarios (typecheck-on fails, typecheck-off succeeds, name resolution still works, invalid options error).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 All previous comments appear addressed — reviewer sign-off still needed.
Summary
The DDM type checker uses unification to infer implicit type arguments, but this fails in some cases — notably when template-generated accessors with unresolved tvar return types are composed with polymorphic functions. This PR adds a per-dialect option to bypass type checking entirely, filling implicit type parameter slots with anonymous type placeholders instead.
How to use
In a dialect definition, add
dialect_option typecheck off;to disable type checking for all programs using that dialect:Programs using the dialect will skip
inferTypeandunifyTypesfor expression arguments. Variable name resolution and global context population still work normally — only type inference is bypassed.Test changes
StrataTest/DDM/TypecheckSkip.leandefines an inline dialect with parameterized types, polymorphic functions, and perField accessor templates. It demonstrates that the accessor-into-polymorphic-fn pattern (from issue #650 / PR #734) produces type errors with typecheck on, and elaborates successfully with typecheck off.Details
TypeExprF.skip(anonymous type placeholder.tvar loc ""),Dialect.typecheck : Bool := true, refactorProgram.globalContextinto explicitcomputeGlobalContextfunctionElabContext.typecheck, skipinferType/unifyTypesinelabSyntaxArg, fill unfilled type param slots inrunSyntaxElaboratorpost-pass, skip pre-registration inelabOperation, fixapplyNArgsto handle tvar typesDeclContext.typecheckelabSetOptionCommandhandler dispatching on"typecheck"optionsetOptionCommandwith syntaxdialect_option <name> <value> ;Dialect.typecheckintoDeclContextinelabProgramRestProgramconstruction forglobalContextrefactorBy submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.