fix(checker): reject where-helper bodies reading the outer function's parameter slot (#969) - #977
Conversation
… parameter slot A `where`-helper body that referenced the OUTER function's parameter slot passed `vera check` and `vera verify`, then hard-failed `vera compile` with an internal E699 dangling-slot error: the checker checked helper bodies while the parent's value scope was still live (so the outer slot resolved), while both backends compile each helper as an independent, param-rooted scope. spec §5 makes where-helpers always local to the parent, each carrying its own mandatory contract over its own params; an implicit outer-frame capture would move a value across a contract boundary uncontracted (DESIGN principles 2 and 5), and capture already has its one canonical construct in closures. The checker now rejects: the parent's value-slot scope is popped before helper bodies are checked, so an outer-slot read is a natural E130 whose fix text explains helpers are closed, param-rooted scopes and steers the user to pass the value as an explicit argument. Parent `forall` TYPE params stay in scope, so a generic parent still parameterizes its helpers. The verifier is unchanged (it already verifies helpers param-rooted). The codegen dangling-slot invariant guard is kept; its comment now records that both known source routes into it (this bug and the merged #973) are sealed at check time. Refs #969 Co-Authored-By: Claude <noreply@anthropic.invalid>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## release/v0.1.4 #977 +/- ##
===============================================
Coverage 93.41% 93.41%
===============================================
Files 96 96
Lines 31380 31388 +8
Branches 456 456
===============================================
+ Hits 29314 29322 +8
Misses 2053 2053
Partials 13 13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
📝 WalkthroughWalkthroughThe checker treats ChangesWhere-helper scope validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant TypeChecker
participant WhereHelper
participant Diagnostic
TypeChecker->>TypeChecker: Remove parent value-slot scope
TypeChecker->>WhereHelper: Check helper body and contracts
WhereHelper->>Diagnostic: Report unresolved outer slot
Diagnostic-->>WhereHelper: Return E130 with explicit-argument guidance
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 8✅ Passed checks (8 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@greptileai review |
Greptile SummaryFixes a check/compile scope divergence for
Confidence Score: 5/5The change is safe to merge: it closes a well-understood scope desync between the checker and both backends, is fully mutation-validated, and leaves no known regression surface open. The core change — moving one pop_scope() call earlier and wrapping the where-fn loop in a narrowly scoped hint-context stack — is minimal, logically sound, and covered by 10 targeted unit tests plus a new negative conformance fixture. The try/finally around the hint stack ensures cleanup even when individual helper checks emit errors. Parent type params are correctly preserved through the separate saved_params save/restore path. All documentation surfaces are updated in lockstep. No files require special attention.
|
| Filename | Overview |
|---|---|
| vera/checker/core.py | Moves pop_scope() to before the where-fn loop so helper bodies cannot resolve outer parameter slots; adds _where_helper_outer_tnames stack for hint context; uses try/finally to keep the stack clean on errors |
| vera/checker/expressions.py | Adds a narrowly-gated E130 hint branch for where-helper bodies reading outer-param slot types; correctly places the branch before the generic hint and after the handler-state hint, matching the intended priority order |
| vera/wasm/operators.py | Updates comment on E699 guard to document that both previously-known source routes (#973, #969) are now sealed at check time; guard is correctly retained as a soundness net |
| tests/test_checker_functions.py | Adds TestWhereHelperScope with 10 tests covering: the reported bug, over-pop guard, generic type params, unrelated slot type, sibling-fn hint isolation, contract clauses, nested where-block parent/grandparent hint targeting, and handler-hint-wins ordering |
| tests/conformance/ch05_where_helper_outer_slot_rejected.vera | New negative conformance fixture: outer(@int) with helper(@Bool) that reads @Int.0 in its body; correctly declared at check level with expected_error: E130 |
| tests/conformance/manifest.json | Adds manifest entry for ch05_where_helper_outer_slot_rejected with correct level, spec_ref, expected_error, and feature tags |
| spec/05-functions.md | Adds a precise prose paragraph to §5.6 codifying the closed param-rooted scope rule for where-helpers, including the type-param exception and the E130 consequence |
| KNOWN_ISSUES.md | Removes the closed #969 row; adds the new #978 nested-where-blocks bug; keeps the section non-empty per the convention (five bugs remain open) |
Reviews (5): Last reviewed commit: "docs(testing): last three conformance-co..." | Re-trigger Greptile
Greptile SummaryThis PR closes #969 by making the type checker agree with the WASM backends: a
|
| Filename | Overview |
|---|---|
| vera/checker/core.py | Moves env.pop_scope() for the parent's value-slot frame to before the where-helper loop (was at end of step 9); wraps the helper-check loop in try/finally to guarantee _where_helper_outer_tnames cleanup. The reordering is safe: contracts and body type-checking happen at steps 5–7 before the pop, and parent type params are restored separately at step 9, so generics remain available through the loop. |
| vera/checker/expressions.py | Adds a narrowly-gated E130 hint branch for the where-helper-outer-slot case, mirroring the pattern from #973. Gate conditions — count == 0, non-empty _where_helper_outer_tnames, and tname in the innermost set — correctly limit the improved message to the exact failure scenario, keeping the generic lower-index hint for unrelated types and sibling functions. |
| vera/wasm/operators.py | Comment-only update on the E699 dangling-slot guard: documents that both previously-known source routes (#973, #969) are now closed at check time with E130, while explicitly preserving the guard as a soundness net for any future checker/backend scope desync. |
| tests/test_checker_functions.py | Adds TestWhereHelperScope with five targeted tests: the reported bug, guard against over-pop when helper has its own same-typed param, parent forall type params remaining in scope, unrelated type producing the generic hint, and sibling function not inheriting a stale hint. All necessary corner cases are covered. |
| tests/conformance/ch05_where_helper_outer_slot_rejected.vera | New negative conformance fixture (the 147th program): helper has @Bool as its parameter but its body references @Int.0, which belongs only to the outer outer(@int -> @int). Registered at level check with expected_error: E130, correctly establishing the check-level rejection as the spec-compliant baseline. |
| tests/conformance/manifest.json | Adds manifest entry for ch05_where_helper_outer_slot_rejected at chapter 5 with level: check and expected_error: E130. Entry is inserted between ch05_where_fn_higher_order and ch06_requires, maintaining chapter order. |
| spec/05-functions.md | Adds a spec paragraph formalising the closed-scope rule: helpers resolve slots only against their own params; parent value slots are out of scope; parent forall type params remain in scope; implicit outer-frame capture would cross a contract boundary and is E130. |
| TESTING.md | Count updates are internally consistent: test total 6,943→6,953 (+10), skipped 65→67 (+2 for the two level-limited skips on the new negative fixture). The skip table gains the two new test_verify and test_run rows for ch05_where_helper_outer_slot_rejected. |
Reviews (2): Last reviewed commit: "fix(checker): reject where-helper bodies..." | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/conformance/manifest.json`:
- Around line 808-821: Update the spec_ref field for the
ch05_where_helper_outer_slot_rejected fixture from Section 5.2 to Section 5.6.2,
“Mutual Recursion,” while leaving the remaining manifest metadata unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: eb4b036a-5500-41fb-a6fc-a97beb154127
⛔ Files ignored due to path filters (6)
docs/SKILL.mdis excluded by!docs/**docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch05_where_helper_outer_slot_rejected.verais excluded by!**/*.vera
📒 Files selected for processing (14)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdKNOWN_ISSUES.mdROADMAP.mdSKILL.mdTESTING.mdspec/05-functions.mdtests/conformance/manifest.jsontests/test_checker_functions.pyvera/checker/core.pyvera/checker/expressions.pyvera/wasm/operators.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
aallan/vera-bench(manual)
💤 Files with no reviewable changes (2)
- SKILL.md
- KNOWN_ISSUES.md
…mar, spec_ref, #978 rows Responds to the adversarial panel + pr-review round on PR #977. - expressions.py: rephrase the where-helper E130 hint to drop the ungrammatical "a @int" indefinite article ("add @t to the helper's parameters and pass the value at the call site"). - test_checker_functions.py: five new TestWhereHelperScope tests closing the analyzer's gaps — a helper reading an outer slot from its requires and its ensures clause (GAP A), nested-where immediate-parent vs grandparent-slot hint targeting (GAP C), and handler-state-vs-where hint ordering; the generic-type-param docstring is downgraded to what the test actually guards (the value-scope pop is harmless), since the type-param retention is spec-intent, not test-guarded. - ch05 fixture header + manifest: correct the closed-scope spec_ref from Section 5.2 to Section 5.6.2. - KNOWN_ISSUES.md + SKILL.md: track the nested-where codegen bug (#978) — grandchild helpers are registered but never emitted by the non-generic codegen path (check-green, compile fails with unknown func). - TESTING/ROADMAP/README/FAQ: test-count sync (6958); regenerate site assets after the SKILL.md row. Refs #969 #978 Skip-changelog: review-round refinements of this PR's unreleased #969 entry Co-Authored-By: Claude <noreply@anthropic.invalid>
Adversarial review panel + pr-review — recordFive panel lenses (pre/post differential, hint-stack interplay, spec/docs, backend agreement, diagnostic quality) with two adversarial skeptics per finding, plus three review agents (code, tests, docs). All confirmed findings are fixed in f72796f; the review round also surfaced a second new bug, filed as #978. The #969 fix itself: no correctness findings. The differential lens ran a probe battery at base vs head (own-param helpers, contract-clause reads, let-bound values, sibling helpers, recursive helpers, generic parents) and found only the intended flips — no silent run-result changes, no incomplete flips. The hint-interplay lens could not break the two-hint-stack machinery (a Panel findings (3 confirmed, all minor) — resolved
pr-review findings — resolved in f72796f (and the prior comment fix)
Reviewer scoreboard for this PR so far: panel 3 confirmed (1 unique bug discovery with root cause), pr-review agents 2 confirmed classes (contract-pin gap, comment overclaim), CodeRabbit 1 duplicate-valid (the manifest spec_ref, ~1h behind the panel), Greptile: no output yet. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Line 227: Update the active-development metrics sentence in README.md to
report 147 conformance programs instead of 146, keeping it consistent with the
updated test and conformance counts in FAQ.md and ROADMAP.md.
In `@TESTING.md`:
- Line 9: The TESTING.md overview reports an inconsistent test total: 6,860
passed + 26 stress + 67 skipped equals 6,953, not 6,958. Correct the Tests
metric to match the component counts, or document and verify the additional five
tests/statuses against the canonical test collection/count checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e88a776d-4b65-4e7b-ba83-cc041372082d
⛔ Files ignored due to path filters (3)
docs/SKILL.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**tests/conformance/ch05_where_helper_outer_slot_rejected.verais excluded by!**/*.vera
📒 Files selected for processing (10)
FAQ.mdKNOWN_ISSUES.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdtests/conformance/manifest.jsontests/test_checker_functions.pyvera/checker/core.pyvera/checker/expressions.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
aallan/vera-bench(manual)
…iled (PR #977 review) README kept 146 conformance programs beside the bumped test total (the doc-counts gate validates only README's test figure), and TESTING.md's passed/stress/skipped breakdown was not bumped with the total (6,860 + 26 + 67 = 6,953, not 6,958); the five new tests land in the passed bucket (6,865). Skip-changelog: docs-only count reconciliation within this PR Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@TESTING.md`:
- Around line 9-11: Update the stale conformance-program count in the
conformance-suite introduction from 146 to 147, ensuring all references in
TESTING.md match the overview.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1d4a6659-400c-4ee0-a705-229662e3ec0b
📒 Files selected for processing (2)
README.mdTESTING.md
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
aallan/vera-bench(manual)
…review) The suite-introduction prose and both check_conformance.py gate rows still said 146; the doc-counts gate validates the overview table and command-comment figures but not these three sites. Skip-changelog: docs-only count reconciliation within this PR Co-Authored-By: Claude <noreply@anthropic.invalid>
… three-subsystem agreement (#991) Round 3 of the PR #1013 review — four findings: 1. CHECKER leg of #991 (CodeRabbit outside-diff, confirmed live). The checker still resolved bare calls through the flat, last-wins `env.functions` registry, so a diamond whose two same-named `leaf`s differ in SIGNATURE was falsely REJECTED: branchA's `leaf(@Int.0)` synthesized against branchB's `@Int -> @String` leaf (registered last) and E121'd branchA's body ("has type String, expected Int") on a valid program. `_check_fn` now maintains a `_fn_scope_stack` (the chain of functions whose where-blocks are lexically in scope, spanning body, contracts, and the helper recursion), and `_lookup_function_scoped` resolves the nearest same-named helper innermost-out, then the pinned top-level info, then the flat registry — mirroring the verifier's `_scoped_fn_lookup` and codegen's parent-qualified hoist, so all three subsystems agree on helper-name scoping (the issue's closing clause). The commutativity analysis's effect-row lookup routes through the same scoped resolution. Rejected builtin-redefinition helpers (E151, #815) are skipped — the built-in stays canonical — and the #969/#977 slot-isolation invariants are untouched (value scopes are separate; the negative conformance fixtures still fail with their expected codes). 2. Decoy clone-scope test hardened (CodeRabbit inline 3565053529). `decoy` now carries a non-trivial postcondition discharged against its own helper; the tests assert per-function ensures obligations are present AND Tier-1 verified (never skipped) and execute both doors (host(1) == 8, decoy(5) == 0), so the test cannot pass via a skipped obligation or a misresolved helper. 3. `_fn_info_for_decl` caches keyed `(id(decl), visibility)` in both the verifier and the new checker twin (Greptile P2) — a cache hit can no longer silently ignore a differing visibility argument. Latent today; cheap to make impossible. 4. `mono_base_names` derives the base with `rsplit("$", 1)` (Greptile P2): `_mangle_fn_name` appends exactly one `$`-suffix (type args cannot contain `$`), so a `$`-qualified entry (a shadowed module clone `mod$path$gen$Int`, a per-clone hoisted helper) reduces to its qualified base — which can never equal a bare helper name — instead of collapsing to a first segment that could false-match a bare helper coincidentally named `mod`/`gen`. Pinned by TestCheckerHelperScope991 (the differing-signature diamond checks clean and runs to 4 — a value only reachable when checker, verifier, and codegen each resolve `leaf` to its own parent's helper) and the hardened TestGenericCloneEnclosingScope1013. Mutations: revert the checker scoping to the flat lookup -> both diamond tests RED; re-run of the enclosing-drop mutation -> the hardened decoy test RED. Negative conformance fixtures re-verified failing with their exact expected_error codes (both polarities). Co-Authored-By: Claude <noreply@anthropic.invalid>
Summary
Fixes #969: a
where-helper body that references the OUTER function's parameter slot passedvera checkandvera verify, then hard-failedvera compilewith a dangling-slot E699 — the checker checked helper bodies before the parent's scope popped, while the backends compile each helper as an independent param-rooted scope.Per spec §5 and DESIGN principles 2 and 5, where-helpers are closed, param-rooted scopes: helpers carry their own mandatory contracts over their own params, and an implicit outer-frame capture would move a value across a contract boundary uncontracted (capture semantics already have their one canonical construct — closures). The fix makes the checker agree with the backends:
vera/checker/core.py:_check_fnpops the parent's value-slot scope before the where-fn loop, so an outer slot read in a helper body becomes a natural E130. Parentforalltype params stay in scope through the loop (generic parents still parameterize helpers —ch09_generic_where_helperstays green).vera/checker/expressions.py: the E130 fix text steers to the design's canonical alternative when the failing type is one the parent bound (same narrow-gating pattern as Checker binds handler state into the handled body's slot scope; the backends do not — reconcile #973's hint —count == 0, only inside a helper body, masked everywhere else):vera/wasm/operators.py: with both Checker binds handler state into the handled body's slot scope; the backends do not — reconcile #973 (merged) and where-helper body referencing an outer param slot: check+verify green, compile E699 #969 (this PR) sealed at check time, the dangling-slot E699 guard's two known source routes are gone — the inline comment now records that precisely. The guard itself stays as a defensive net (no exhaustive-unreachability claim).Evidence
ch05_where_helper_outer_slot_rejected.vera(levelcheck,expected_error: E130, the 147th program) wrongly passed check pre-fix; the new unit test got no error. Both flip with the fix.__pycache__purged; 5/5 green on restore.-m "not stress"), mypy clean (97 files), all 147 conformance programs pass at their level, all 37 examples check + verify.@Tresolution — helper@Tresolves via the opaque-typevar fallback and monomorphization reads forall vars from the decl. The regression tests pin it regardless.Docs lockstep (same commit)
[Unreleased]bullet; counts reconciled across every surface (147 conformance, 15 check-level, 9 negative fixtures, 6,953 tests — including TESTING.md's prose enumeration and FAQ's gate-blind total); CLAUDE.md/AGENTS.md fixture lists updated; site assets regenerated.Closes #969
Summary by CodeRabbit
where-helper bodies are now checked in an isolated value-slot scope, so references to outer function parameter value slots are rejected with clearerE130guidance to pass values explicitly.where-helper scoping details and refreshed conformance/test totals from 146 to 147 programmes across the changelog and supporting docs.