Fix free variable issue in Boole#1153
Conversation
|
@ChengZ3 here is the fix to the issue you submitted. Thank you |
tautschnig
left a comment
There was a problem hiding this comment.
Nice root-cause writeup. The parallel fvarIsOp array was a classic "shadow data structure drifts out of sync with source of truth" bug — registerCommandSymbols emitted one slot per command while GlobalContext.vars grows by 1 + #constructors + #testers + #selectors for a datatype command (and by #funcs for a command_recfndefs). Reading directly from gctx.vars in getFVarIsOp removes the class of bug entirely. lake build passes on the branch; the added regression test also passes.
Four concerns below — (1) and (2) are the two I'd most like addressed; (3) and (4) are a proof suggestion and maintenance note.
1. globalVarTypes is now overloaded as both a type map and a command_var-name set. The new getFVarIsOp uses !globalVarTypes.contains name as the carve-out predicate. This works today, but it couples the semantics of the map (values used for parameter-type lookup elsewhere) to the semantics of the key set (membership used for op-vs-var classification). A future refactor that populates the map from another source (say, a procedure's local-var pre-pass) will silently change getFVarIsOp's behaviour. Two clean options:
- Add a dedicated
globalVarNames : Std.HashSet StringtoTranslateState, populated in the same pre-pass, and use it for the predicate. Cheap and explicit. - Keep one map but introduce a named predicate
private def TranslateState.isGlobalVar (st) (name : String) : Bool := st.globalVarTypes.contains namewith a docstring pinning the invariant. No state change; just encapsulation.
Either way, the inline comment on line 115 should spell out the invariant the predicate is relying on: "a name is in globalVarTypes iff it was introduced by a command_var". That's not a priori obvious from the field's name.
2. Test coverage is narrow for a fix that changes a hot path in translation. The regression reproduces exactly the ticket's failing case, which is the right baseline. But the fix claims to cover a broader scenario ("any datatype or command_recfndefs whose expansion exceeds 1 entry in gctx.vars"), and the test only exercises one position of one kind. Missing:
- A variant where the referenced datatype is not first (e.g.
colordeclared 2nd or 3rd). The pre-fix bug was position-dependent; the post-fix code should be position-independent. Pinning both halves guards against regressions in either direction. - A program with a
command_varglobal and a datatype referenced the same way. This exercises theglobalVarTypes.containscarve-out directly. - A
command_recfndefsblock with several recursive functions whose names are referenced in a later function.command_recfndefsis the other "one command → manygctx.varsentries" case per the removedregisterCommandSymbols. Without this test, a regression here would not be caught.
All three are small #strata seeds. I'd add them in this same test file or a datatype_tester_freevar_variants.lean.
3. Proof coverage suggestion. The new getFVarIsOp is a short, pure function with a crisp specification that reduces to reading gctx.vars[i]. Worth a theorem pinning the contract:
/-- `getFVarIsOp` agrees with `gctx.vars` classification, with the
`command_var` names carved out as terms. -/
theorem getFVarIsOp_spec (st : TranslateState) (m : SourceRange) (i : Nat) :
match (getFVarIsOp m i).run st with
| .ok (b, st') =>
st' = st ∧
match st.gctx.vars[i]? with
| some (name, .expr _) => b = !st.globalVarTypes.contains name
| some (_, .type _ _) => b = false
| none => False
| .error _ => st.gctx.vars[i]?.isNoneThis is a pure unfolding + case-split on gctx.vars[i]?; should go through without any lemmas. Its value is not in the proof effort but in documenting the contract — anyone reading getFVarIsOp in the future can see exactly what invariant it maintains, and a regression that breaks the carve-out will fail the proof.
Stronger theorem (optional, harder): getFVarIsOp i agrees with the classification DDM's elaborator uses to decide .fvar vs .op for the same index. That would require a spec from DDM's GlobalKind handling, which I don't see a clean statement of. Worth thinking about for a follow-up, not this PR.
4. Commit history. The branch has commits fix the datatype issue, regression, more, plus a merge commit. For merge, I'd squash to one or two commits with descriptive messages — the existing PR body is already a good commit message. Nit, doesn't block.
Minor refactor callouts:
- The comment block at the top of
TranslateState(lines 28-45) could briefly mention that op-vs-var classification for free variable references is derived fromgctx.vars(single source of truth), andglobalVarTypessupplies the only exception. That reframes future readers toward "consult gctx, not a per-command table" and prevents the next person from re-introducing a shadow table. registerCommandSymbolsis gone, but the command-tag cases it enumerated are still useful reference documentation (which commands introduce whichgctx.varsentries). Could move that list to a comment abovegetFVarIsOpas a table — "datatype: 1 + #ctors + #testers + #selectors; recfndefs: #funcs; typedecl: 1; constdecl/fndecl/fndef: 1; var: 1 (but carved out via globalVarTypes); procedure/block/axiom/distinct: 0". Explicit documentation of the gctx side helps anyone who has to touch this code after the fact.
Add a pull request description template that warns about the upcoming repository split. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
The `StrataTestMain` test driver spawns `lean` on files under `StrataTestExtra/`, which is not declared as a `lean_lib`. Some of those files (e.g. `DDM/Integration/Java/TestGen.lean`) import modules from the `Strata` library that are not in `StrataTest`'s transitive closure, most notably `Strata.DDM.Integration.Java`. Before this change, `lake test` on a clean `.lake` would only build the closure reachable from `StrataTest` and then fail at subprocess time with: error: object file '.../Strata/DDM/Integration/Java.olean' of module Strata.DDM.Integration.Java does not exist Running `lake build` first masked the bug because the `Strata` library is in `defaultTargets`. Add `Strata` to the `needs` list of `StrataTestMain` so Lake builds the full `Strata` library before the test driver runs, regardless of whether `lake build` was run beforehand. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
…ove unlabeled exit from Core (strata-org#1141) *Issue #, if available:* Closes strata-org#372 *Description of changes:* Formalizing scoped variable initialization: - Adds scoped environment semantics to blocks: step_block_done now projects the store through the parent store via projectStore, discarding block-local variables on exit. This applies to both StepStmt (deterministic) and StepKleene (nondeterministic). - Adds PostWF helper definition in Specification.lean for postcondition stability under projectStore. - Adds .block constructor to KleeneStmt and corresponding step_block/step_block_body/step_block_done to StepKleene, mirroring the deterministic block semantics. - Changes StmtToKleeneStmt (.block _ bss _) to produce .block b (wrapping in a Kleene block), so the Kleene translation preserves scoping. Removing unlabeled `exit` command (`exit;`, not `exit lbl;`): - The unlabeled `exit` command doesn't have clear meaning when it is inside `while`. In `while cond { ... exit; ... }`, is `exit` equivalent to `continue` or `break` in C/Python? No translation to Core was introducing the unlabeled `exit`, and the small-step semantics wasn't clear about the meaning of `exit` inside a loop. - The type checker of Core (Strata/Languages/Core/StatementType.lean) fails when an unlabeled `exit` appears inside the body of a while loop / if statement (unless it is wrapped by another nested block), but the DDM syntax of core allows it, so from user's perspective this gap is kind of surprising. - This patch removes the unlabeled `exit` case because it can always be simulated by a labeled block + labeled exit. On top of this, this also renames `touchedVars` to `modifiedOrDefinedVars` for clarity, and instead makes `touchedVars` all vars that are read + modified + defined. ### How to review? - Strata/DL/Imperative/StmtSemantics.lean has the most important update: the `.block` constructor now has the input store which will be used to 'project' the output store to the variables that have been defined before. Also, any block-exiting small steps like `step_block_done` will do the projection, to define variables which were defined inside the inner scope. - Correspondingly, the Kleene language was updated to add a notion of block and scoping to its syntax and semantics, otherwise DetToKleene doesn't prove. (Strata/DL/Imperative/KleeneStmt.lean, Strata/DL/Imperative/KleeneStmtSemantics.lean) - StrataTest/DL/Imperative/StepStmtTest.lean has a few new tests that show scoping works well. - DetToKleeneCorrect.lean and ProcBodyVerifyCorrect.lean shows that the top-level statements are not touched after this update. - Strata/DL/Imperative/HasVars.lean has the `touchedVars` update and Strata/Languages/Core/StatementSemantics.lean has some additional well-formedness about evaluator extension that are useful. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
…trata-org#1109) translateSort already handles .prim .string, but translateTerm had no arm for .prim (.string _): any SMT string literal produced by the encoder would fall through to the catch-all and raise 'unsupported term'. Add the missing arm plus the two string operations already supported in Denote.lean so that the two translators agree on the end-to-end-supported subset. Specifically, add arms for: - .prim (.string s) -> (mkString, toExpr s) - .app .str_length [s] _ -> (mkInt, Int.ofNat s.length) - .app .str_concat as _ -> (mkString, leftAssocOp mkStringAppend as) mkStringAppend uses instHAppendOfAppend + instAppendString, which is the same instance chain Lean elaborates inferInstance to for HAppend String String String. Regression tests in StrataTest/DL/SMT/TranslateTests.lean cover all three arms; each fails on origin/main (catch-all throw observable via #guard_msgs mismatch) and passes with this change. I additionally verified under a Meta.check-enabled harness that the produced Expr type-checks in the kernel, so the (fst = mkString/mkInt, snd = ...) pairs are internally consistent. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Kiro <kiro-agent@users.noreply.github.com> Co-authored-by: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
…trata-org#1140) Fixes strata-org#1139 Introduces a `Provenance` type that fully replaces `FileRange` and `SourceRange` as the canonical way to store source locations in metadata. This structurally eliminates the "SourceRange without file name" problem. The `Provenance` type has two constructors: - `Provenance.loc uri range` — a real source location (always requires both URI and range) - `Provenance.synthesized origin` — a node created programmatically, with a `SynthesizedOrigin` inductive type A `SynthesizedOrigin` inductive enforces that only canonical origins are used (`smtEncode`, `nondetIte`, `laurelParse`, `laurel`, `laurelToCore`, `structuredToUnstructured`), preventing typos and duplicates at the type level. Key changes: - The `.fileRange` variant is removed from `MetaDataElem.Value` — all metadata values now use `.provenance` exclusively - `MetaData.ofSourceRange` emits only a provenance element - `getProvenance` is the single source of truth for reading source locations from metadata - `setCallSiteFileRange` works directly with `Provenance` instead of roundtripping through `FileRange` - `getFileRange` delegates to `getProvenance` for extraction - `FileRange.unknown` and `SourceRange.none` eliminated from metadata construction - SMT DDM translator uses `smtAnn` combinator to reduce annotation boilerplate - SARIF output uses `getFileRange` (which reads provenance) `FileRange` remains as a utility struct for extraction and formatting (e.g., in `DiagnosticModel`), but is no longer a metadata value type. Existing tests pass unchanged. ## Follow-up - Migrate the B3, Boole, and Python grammar ASTs from using `SourceRange` as their annotation type parameter to `Provenance`. This would allow combining multiple files at the AST level and enable proper provenance tracking through translation passes.
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 Clean fix that correctly addresses the root cause: the misaligned fvarIsOp array is replaced by reading directly from gctx.vars, which is the single source of truth. The command_var carve-out is sound — the invariant that globalVarTypes keys exactly correspond to command_var names is maintained by the pre-pass. Test coverage exercises the original bug scenario plus three meaningful variants.
The getFVarIsOp_spec theorem is a nice machine-checked contract, though it's currently unreferenced (private and unused). If it's intended as living documentation, that's fine; if it's meant to be used by downstream proofs, it should be made accessible.
One minor documentation gap noted inline.
joscoh
left a comment
There was a problem hiding this comment.
This one can be targeting either main or main2, since unlike the other one, it doesn't depend (AFAIK) on anything currently in main2 only (i.e. the int-valued recursion PR). I will approve either way
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 The new commit since my last review is just a merge from main2 — no code changes addressing my inline nit about typesynonym missing from the getFVarIsOp docstring list. That said, it's a trivial documentation nit and the PR is already approved. Reviewer sign-off still needed for the nit, but it's non-blocking.
Co-Authored-By: Claude Sonnet 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.
|
Will merge once #1181 shows the internal benchmarks succeed |
Problem
When a Boole program had 4+ datatype declarations and the first datatype's
testers or selectors (e.g.
color..iscolor_Red,color..color_Red_0) werereferenced in a later function, the verifier reported them as free variables
during type-checking. Moving the referenced datatype to any position other than
first worked around the bug.
Closes #1142
Root cause
initFVarIsOpbuilt a symbol-class table by scanning program commands — oneentry per command (one
falseperdatatypecommand). But theGlobalContextregisters many entries per datatype: the type itself plus every constructor,
tester, and selector generated by templates. This made
fvarIsOptoo short andmisaligned with the actual
fvarindices.When the referenced datatype was declared first, its testers/selectors landed at
low
fvarindices that fell inside the misaligned table. Those indices returnedfalse(treat as type symbol), so the testers were emitted as.fvar(variablereferences) instead of
.op(function applications). The type-checker thenrejected them as undeclared free variables.
When the datatype was not first, the tester indices fell past the end of the
table and hit the fallback path in
getFVarIsOp, which correctly readsGlobalContext.vars[i]— the same source of truth the DDM elaborator uses forall symbols.
Fix
Removed
fvarIsOp,initFVarIsOp, andregisterCommandSymbolsentirely.getFVarIsOpnow reads directly fromGlobalContext.vars[i]:GlobalKind.type→ emit as.fvar(type symbol, not a callable)GlobalKind.expr→ emit as.op, except forcommand_varglobalvariables, which become procedure parameters in Core and must remain
.fvarThe
command_varcarve-out uses the existingglobalVarTypesmap (alreadycollected in the pre-pass), so no new state is needed.
Test
Added
StrataTest/Languages/Boole/datatype_tester_freevar.lean— a directregression for the issue's failing case:
colordeclared first among fourdatatypes, with
get_valreferencingcolor..iscolor_Redandcolor..color_Red_0.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.