Skip to content

Handler semantics: deterministic ops, enclosing-context clauses, complete registration (#1210, #1211, #1215) - #1232

Merged
aallan merged 11 commits into
release/v0.1.10from
fix/1210-1211-1215-handler-semantics
Aug 7, 2026
Merged

Handler semantics: deterministic ops, enclosing-context clauses, complete registration (#1210, #1211, #1215)#1232
aallan merged 11 commits into
release/v0.1.10from
fix/1210-1211-1215-handler-semantics

Conversation

@aallan

@aallan aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Handler semantics: deterministic op ownership, enclosing-context clause ops, complete registration (#1210, #1211, #1215, #1231)

Part of the #1213 burndown (integration branch release/v0.1.10; PR B of the plan on the #1213 thread). Five commits, each through the full gate.

#1215 — bare effect-op resolution is deterministic and source-ordered. Two effects declaring one op name (builtin State and Http both declare get — no user effect needed) bound by frozenset iteration: the same program compiled on hash seeds 3/4 and failed E217 on the rest. The row now travels with the ast.EffectSet declaration order; resolution walks innermost enclosing handler → enclosing handlers outward → the declared row in SOURCE order → registered effects in registration order. Seed matrix 0–7: uniform. The reversed row proves source-order (E217 naming Http.get deterministically); a handled-inner shape proves precedence. Spec §7.4 gains the rule; §7.4.1 corrected (it claimed ambiguous bare calls are rejected — they are not).

#1231 — the type-argument leg of the same lottery, found by this fix's own seed sweep. Spec §7.3.3 permits effects(<State<Int>, State<Bool>>); _effect_type_mapping's frozenset fallback made the same program check-clean on half the seeds and E121 on the other half — and fixing it exposed a deterministic codegen desync underneath (the last State instantiation overwrote the first: state_get_Bool i32 emitted for a checker-typed Int i64). Both sides now walk the ordered row, first-written wins.

#1211 — a clause body's bare ops lower against the enclosing context. The checker has always resolved a nested handler's clause-body get/put against the context where the handler is declared; codegen lowered them against the handler's own cell — silent wrong values. StateClauseEntry now carries the whole declaration-time scope (the four op/result mirrors beside the existing decl_env), restored around clause-body and with-expression translation; _state_clause_family deliberately stays at the clause's own values (it types resume). Termination invariant rewritten: the restored registry is from strictly outside the handler, so re-entry walks outward through finite nesting. The checker=codegen=verifier matrix (8 shapes, nesting depths 1–3, bare + qualified ops, both result-type mirrors individually mutation-validated) pins every value to the checker's story — with verifier tier counts identical before/after, so "verifier already agrees" is verified, not assumed. Spec §7.5.2 gains the explicit sentence. Corpus: exactly two probe programs moved, both to their header-documented checker values.

#1210 — State/Exn registration walks every handler position. Clause bodies, state-init expressions, and with updates never registered the families their lowerings reference: check-green programs died at whole-module WAT compile (unknown func $vera.state_push_Bool), and the i32_pair silent skip made handle[State<String>] in a pure fn invalid WASM. All positions now register through one shared derivation, and the silent skip is the same loud E607 the declared path emits. Permanent cross-component differential: test_every_referenced_state_exn_symbol_is_declared sweeps 226 compiled programs / 132 distinct symbols with floors and its own red-proof.

Probes: conformance 196 → 199 (three run-level programs: clause-body enclosing, registration positions, op source-order); the #1210/#1211/#1215 probe pools promoted or deleted with per-file reasoning; all three remaining parse-broken probes dispositioned (two deleted as subject-covered-elsewhere, one deleted as premise-refuted); probes 250 → 240, parse-broken 5 → 2. KNOWN_ISSUES rows for the three issues deleted; CHANGELOG bullets name all four issues.

Closes #1210. Closes #1211. Closes #1215. Closes #1231.
(Close-keywords take effect at the release PR to main, per the integration-branch model.)

Review provenance

Implemented by the burndown's agent process on the post-consolidation shape; adversarial review to fixed-point and CodeRabbit ledger convergence follow on this PR before self-merge into the integration branch, per the #1213 protocol.

Summary by CodeRabbit

  • New Features

    • Effect operations now resolve deterministically according to declared source order, with handled effects taking precedence.
    • Nested handler clauses correctly resolve enclosing state cells for get and put.
    • Host imports are registered across handler, clause, state-initialisation and update expressions.
    • Unsupported state-cell and exception payload types now produce clear compilation errors.
  • Bug Fixes

    • Improved handler compilation, operation routing and diagnostics for state and exception effects.
    • Bounded deeply nested handler re-entry to prevent unbounded expansion.
  • Documentation

    • Updated project metrics, conformance counts, testing information and known issues.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR makes effect-operation resolution deterministic, preserves handler clause scope during WebAssembly lowering, adds State/Exn registration coverage, bounds nested clause expansion, and updates specifications, tests, documentation, and project totals.

Changes

Effect handler semantics

Layer / File(s) Summary
Deterministic effect ordering
spec/07-effects.md, vera/checker/*, vera/environment.py, vera/codegen/functions.py, tests/test_effect_op_determinism.py
Effect rows preserve written order for bare-operation lookup and type mapping. Handled effects take precedence. Duplicate mappings use first-wins behaviour.
Handler scope and registration
vera/wasm/helpers.py, vera/wasm/context.py, vera/wasm/calls_handlers.py, vera/codegen/*, tests/test_state_exn_registration.py
State clause entries retain declaration-time scope and registries. Scanning covers handler expressions, contracts, refinements, destructuring, and module-call arguments. Unsupported types produce E607 or E612.
Nested clause lowering and validation
vera/wasm/calls.py, vera/skip.py, tests/test_nested_handler_clause_ops.py, tests/test_nat_narrowing_return_differential.py, KNOWN_ISSUES.md, ROADMAP.md, SKILL.md
Clause operations use enclosing declaration context when addressable. Same-family nested operations are rejected. Clause re-entry is capped at eight levels.
Validation and project records
AGENTS.md, CLAUDE.md, FAQ.md, README.md, TESTING.md, tests/conformance/manifest.json, tests/probes/*, vera/README.md, CHANGELOG.md
The conformance suite now contains 199 programmes. The corpus contains 247 programmes. Project documentation reports 9,505 tests and records the compiler and specification changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • aallan/vera#1003 — Related handle[State<T>] clause lowering.
  • aallan/vera#668 — Related walker-completeness coverage.
  • aallan/vera#1202 — Related State/Exn registration, clause scoping, effect dispatch, and code-generation logic.

Suggested labels: compiler, tests, spec, docs

🚥 Pre-merge checks | ✅ 6 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Spec And Implementation Move Together ⚠️ Warning The compiler adds an 8-level clause re-entry cap and lowers clause bodies, but spec/ has no cap rule and spec/11 still says clauses are not compiled and scans only function bodies. Update spec/07-effects.md and spec/11-compilation.md with the clause-lowering limit, E602 boundary, and registration coverage for all lowered handler positions.
Diagnostics Carry An Error Code ⚠️ Warning New _warn_unsupported_state_cell and _warn_unsupported_exn_tag calls emit warning-severity diagnostics with E607/E612; warnings require W### codes. Use registered W607/W612 codes for these warning diagnostics, or change their severity to error if E607/E612 must remain error-family codes.
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the main changes: deterministic operations, enclosing-context clauses, and complete handler registration.
Docstring Coverage ✅ Passed Docstring coverage is 92.44% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Changelog Covers Public-Surface Changes ✅ Passed The PR changes only spec/07-effects.md among the listed surface files; CHANGELOG.md explicitly covers source-order resolution, enclosing clause operations, E602, and State/Exn registration diagnost...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1210-1211-1215-handler-semantics

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.09938% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.95%. Comparing base (8517a86) to head (0a64ae7).

Files with missing lines Patch % Lines
vera/environment.py 61.90% 16 Missing ⚠️
vera/codegen/compilability.py 97.87% 2 Missing ⚠️
vera/codegen/contracts.py 97.72% 1 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           release/v0.1.10    #1232      +/-   ##
===================================================
+ Coverage            93.93%   93.95%   +0.02%     
===================================================
  Files                  100      100              
  Lines                34838    35057     +219     
  Branches               458      458              
===================================================
+ Hits                 32725    32938     +213     
- Misses                2100     2106       +6     
  Partials                13       13              
Flag Coverage Δ
javascript 78.61% <ø> (ø)
python 95.60% <94.09%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 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 266: Synchronize the documentation metrics: in README.md at lines
266-266, change conformance programmes from 196 to 199; keep CLAUDE.md lines
61-63 at 247 corpus programmes and update AGENTS.md to 247; in vera/README.md
lines 126-126, revise the Code Generation codegen/ and wasm/ line totals to
match the module map.

In `@spec/07-effects.md`:
- Around line 73-74: Remove the alphabetical canonical-form requirement from
§7.3.2 for effect rows. Preserve the source-written order when representing or
comparing rows, so bare operation resolution continues to follow declaration
order as specified in §7.4; retain set equality only for effect containment.

In `@tests/test_nested_handler_clause_ops.py`:
- Around line 291-300: Extend the _CASES coverage with both missing
no-enclosing-handler scenarios: a single handle[State<T>] inside a function
declaring effects(<State<T>>) whose clause body uses bare get/put and must route
to the host cell rather than the handler clause, and an outermost handler in a
pure function where decl_effect_ops is empty. Add corresponding fixtures and
expected results, preserving the existing cases.

In `@tests/test_state_exn_registration.py`:
- Around line 279-283: Import VeraError and narrow the exception handler in the
corpus-sweep block around parse_file(), transform(), and codegen_compile() to
catch only VeraError. Preserve the continue behavior for declared
parse/transform failures, while allowing unexpected code-generation exceptions
to propagate.

In `@vera/codegen/compilability.py`:
- Around line 510-514: Update the Exn branch in the effect registration handler
to check the boolean result of _register_exn_tag(type_arg). When registration
fails, append type_arg to _unregistrable_state_cells (or the corresponding
existing failure-tracking collection) so the function is skipped and receives
the same E612 diagnostic path as failed State registration; retain successful
registration behavior unchanged.

In `@vera/environment.py`:
- Around line 2211-2275: Update ordered_effect_row() so its fallback sorting is
total and hash-seed independent for repeated effect names, using a stable
structural key derived from each EffectInstance’s type arguments in addition to
name. Add a regression test that leaves current_effect_order empty with
State<Int> and State<Bool> instances and verifies deterministic ordering and
_effect_type_mapping() selection.

In `@vera/README.md`:
- Line 96: Remove the leading space from the inline code label for helpers.py in
the documentation table, keeping any visual indentation outside the backticks so
the markdownlint MD038 violation is resolved.
🪄 Autofix

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: 3f19950f-4d29-48b4-b123-a872ae4446e3

📥 Commits

Reviewing files that changed from the base of the PR and between 6645e9f and 13de07d.

⛔ Files ignored due to path filters (18)
  • docs/SKILL.md is excluded by !docs/**
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • tests/conformance/ch07_clause_body_op_enclosing.vera is excluded by !**/*.vera
  • tests/conformance/ch07_effect_op_source_order.vera is excluded by !**/*.vera
  • tests/conformance/ch07_handler_registration_positions.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/alias_families/g1_array.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/alias_families/p17b_state_string_minimal.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/checker_gates/e128_array.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/checker_gates/e337_user_effect_exn.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/clause_scoping/a11_refined_pattern.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/dispatch_paths/p9_cross_family.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/nested_handlers/p11_init_nested.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/nested_handlers/p13_exn_in_clause.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/nested_handlers/p17_string_outer.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/nested_handlers/p17c_option_outer.vera is excluded by !**/*.vera
📒 Files selected for processing (28)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • spec/07-effects.md
  • tests/conformance/manifest.json
  • tests/probes/README.md
  • tests/probes/state_handlers/README.md
  • tests/test_effect_op_determinism.py
  • tests/test_nested_handler_clause_ops.py
  • tests/test_state_exn_registration.py
  • vera/README.md
  • vera/checker/calls.py
  • vera/checker/control.py
  • vera/checker/core.py
  • vera/checker/resolution.py
  • vera/codegen/compilability.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/environment.py
  • vera/wasm/calls_handlers.py
  • vera/wasm/context.py
  • vera/wasm/helpers.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)
💤 Files with no reviewable changes (1)
  • KNOWN_ISSUES.md

Comment thread README.md Outdated
Comment thread spec/07-effects.md Outdated
Comment thread tests/test_nested_handler_clause_ops.py
Comment thread tests/test_state_exn_registration.py
Comment thread vera/codegen/compilability.py Outdated
Comment thread vera/environment.py
Comment thread vera/README.md Outdated
aallan and others added 5 commits August 7, 2026 14:29
`Environment.lookup_effect_op` picked a bare op's signature by iterating
`ConcreteEffectRow.effects` — a frozenset.  When two effects in one row
declare the same op name, which one bound was a function of
PYTHONHASHSEED, not of the program.  No user `effect` declaration is
needed to reach it: the built-in `State` and `Http` both declare `get`,
so `effects(<State<Int>, Http>)` ran to 70 on some interpreter starts
and failed E217 on others, from identical source.

The row now travels beside an ordered tuple of the same instances, taken
from the `ast.EffectSet`'s own sequence — `_resolve_effect_row_ordered`
produces both views from one resolution, so they cannot come to describe
different effects.  A handler pushes its own effect to the FRONT of that
order for the duration of its body, which makes the whole rule one
ordered walk: innermost handler, then each enclosing handler, then the
declared row in source order, then the registered effects in
registration order (a dict, hence insertion order — now documented as
the deterministic fallback it is rather than left implicit).

`ordered_effect_row` is total over the row: a member the order tuple
does not mention is not dropped but sorted by name after the ordered
prefix, so a row assigned outside the checker stays deterministic too.

Clause bodies are checked outside their own handler's scope, so a bare
op there resolves against the enclosing context — the checker side of
the #1211 alignment, noted where the order is pushed.

Spec §7.4 states the resolution order, §7.3.2 notes that set equality
governs containment rather than this tie-break, and §7.4.1 now says
what the compiler does with an unqualified ambiguous call instead of
claiming it is rejected.

Co-Authored-By: Claude <noreply@anthropic.invalid>
A handler clause is not part of the body it refines.  The checker says so
structurally: clauses are checked before the handled effect joins the
effect row, so a bare `get`/`put` written in a clause body resolves
against whatever encloses the `handle` expression — the same rule that
already gave clause-body SLOT references the declaration scope (#1202).

Codegen disagreed.  Inlining a clause body cleared `_state_clause_ops`
(enough to stop a clause re-entering itself) but left `_effect_ops`
pointing at the handler's OWN host-cell imports.  With two nested
handlers over different cell families, a bare `put` in the inner
handler's clause body wrote the INNER cell while the checker had typed
it against the outer one: check-green, verify-clean, valid WASM, and a
silently wrong number.

The clause registry now carries the whole declaration-time scope in one
record — `StateClauseEntry` replaces the seven-tuple — instead of the
slot environment alone, which is what let the two halves drift.  The
inline restores all four registries around the clause body and its
`with` expression.  The two op RESULT-TYPE mirrors travel with them and
are separately load-bearing: a bare `get(())` in match-scrutinee
(`_effect_op_result_wt`) or array-element (`_effect_op_result_vera`)
position inside a clause body emitted invalid WASM for a check-green
program without them.  `resume` keeps the clause's own family — it types
this op's result, not the scope the body compiles in.

Termination becomes a property of the data rather than of a clear: the
restored registry is the one from strictly OUTSIDE this handler, so it
can never contain this clause, and each re-entry from a clause body
moves one handler outwards through a finite nesting.  The load-bearing
comment is rewritten to that invariant.

The verifier is unchanged, verified rather than assumed: the obligation
stream (tier-1 / tier-3 / total) over every shape is identical before
and after.  Across the whole 494-program corpus — examples/,
tests/conformance/, tests/probes/ — exactly two programs moved:
p9_cross_family (100000111 -> 301000, the checker's value) and
p17c_option_outer (invalid WASM -> 3).

The registry record drops the effect argument's alias-opaque source
spelling: nothing that unpacked the tuple ever read it.

Spec §7.5.2 states the rule for both halves — slot references and
operations — and names the termination consequence.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Codegen decides which host-cell imports and exception tags a module
DECLARES in one pass and which ones it CALLS in another.  The
declaration pass walked a `handle` expression's BODY alone — not its
clause bodies, not a clause's `with` state update, not its own
state-init expression — so a family reached only through one of those
went unregistered while the lowering emitted its calls regardless.  The
result was a check-green, verify-clean program that died at whole-module
WAT compilation with `unknown func $vera.state_push_Nat` or `unknown tag
$exn_Int`.  The same three positions are now walked by the IO / Markdown
/ Regex host-import scan too, which had the identical orphaned-import
shape for a host builtin in a clause body.

The `i32_pair` cell the walk used to skip in SILENCE — a
`handle[State<String>]` inside a `pure` function, where the
declared-effect gate never runs — is now the same loud E607 that gate
emits, reported at the offending cell's own location.  Both paths were
open-coding the accept-and-register decision; they now share one
derivation each (`_register_state_cell`, `_register_exn_tag`), so they
cannot come to accept different cell types.

TESTS.  Four shapes, one per position the walk missed, each required to
compile and produce a value derived from the language rules rather than
from what codegen emits; plus the E607 shape, asserted on the code and
on the ABSENCE of its old `state_push_String` symptom.  The permanent
check is a REGISTRATION-COMPLETENESS DIFFERENTIAL, because a unit test
on either pass cannot see the two drift apart: over every `examples/`
and `tests/conformance/` program that compiles (223 programs, 103
distinct `state_*`/`exn_*` symbol references), every symbol the emitted
WAT references must have a matching import or tag declaration.  It
carries floors on both counts and a can-go-red test that strips the
import lines and requires the extraction to report exactly those
symbols.

PROMOTION (#1213 burndown).  Three conformance programs, suite now 199:
`ch07_clause_body_op_enclosing` (§7.5.2, the clause-body operation scope
across two nesting depths and a `with` expression), `ch07_handler_
registration_positions` (§7.5, all four handler sub-expression
positions), and `ch07_effect_op_source_order` (§7.4, source-order and
innermost-handler resolution — the E217 half is already pinned by
`ch07_bare_effect_op_rejected`).  Ten probes are dispositioned and
deleted: seven whose shapes these programs and the three new regression
suites now carry, and three parse-broken ones —
`checker_gates/e337_user_effect_exn` (repaired it yields E152, which
`ch09_builtin_effect_redefinition_rejected` already pins),
`checker_gates/e128_array` (its subject, the E128 array quantifier
domain, is closed and pinned by `ch06_quantifier_array_domain_rejected`;
the file was broken on unrelated lambda-contract syntax and never
exercised it), and `clause_scoping/a11_refined_pattern` (its premise,
that a refined clause pattern erases to the base name, was answered NO
by the #1208 retreat, and its `@Nat{self >= 0}` is not Vera's
refinement spelling).  Both probe READMEs are updated.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The op-NAME fix's own hash-seed sweep surfaced the type-ARGUMENT leg of
the same lottery.  Spec §7.3.3 permits one effect twice with different
type arguments — `effects(<State<Int>, State<Bool>>)` is two independent
cells — but `_effect_type_mapping`'s row fallback iterated the row's
frozenset on the recorded premise that "a declared row cannot
meaningfully carry two instantiations of one effect".  It can, and the
identical program checked clean on four of the first eight PYTHONHASHSEED
values and failed E121 (`body has type Bool, expected Int`) on the other
four.

The row leg now walks `ordered_effect_row()` — the same ordered candidate
list bare op-name resolution uses — so the first instantiation written in
the row governs.

That alone left a deterministic desync: codegen builds its per-function
`effect_ops` by looping the declared row and letting each State effect
OVERWRITE the last, so it emitted `state_get_Bool` (i32) for a call the
checker had just settled as `Int` (i64) — invalid WASM from a check-green
program.  Codegen is aligned to the checker: first in source order wins,
for `get`, `put`, and `throw` alike.

The eight-seed sweep now returns one value at every seed, and it is the
source-order-first cell's (33, against a Bool cell holding `true` — a
value no default or other cell can produce).  A corpus differential over
`examples/`, `tests/conformance/` and `tests/probes/` shows nothing else
moved.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Skip-changelog: attribution edit inside the existing bullet

Co-Authored-By: Claude <noreply@anthropic.invalid>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vera/README.md (1)

27-30: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Number Resolve as an independent pipeline stage.

The document states that the compiler has seven independently testable stages, but the text version labels Resolve as 2b and Execute as 6. This presents six numbered stages and makes Resolve appear to be a Transform sub-stage.

Number Resolve as 3 and shift later stages to 47, or state explicitly that 2b is not one of the seven stages.

As per coding guidelines: keep parse, transform, resolve, typecheck, verify, compile, and execute as seven independently testable stages.

🤖 Prompt for 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.

In `@vera/README.md` around lines 27 - 30, Update the pipeline-stage numbering in
the README so Parse, Transform, Resolve, Type Check, Verify, Compile, and
Execute are explicitly numbered as seven independent stages. Change Resolve from
2b to 3 and shift all subsequent stage numbers through Execute from 4 to 7,
preserving the existing stage descriptions and order.

Sources: Coding guidelines, Path instructions

🤖 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.

Outside diff comments:
In `@vera/README.md`:
- Around line 27-30: Update the pipeline-stage numbering in the README so Parse,
Transform, Resolve, Type Check, Verify, Compile, and Execute are explicitly
numbered as seven independent stages. Change Resolve from 2b to 3 and shift all
subsequent stage numbers through Execute from 4 to 7, preserving the existing
stage descriptions and order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1a5ce9fe-241b-4e63-924a-70cb20fa6a50

📥 Commits

Reviewing files that changed from the base of the PR and between 13de07d and 89d54ca.

⛔ Files ignored due to path filters (2)
  • docs/SKILL.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (10)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • vera/README.md
  • vera/environment.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Adversarial review record

Two independent hostile reviewers audited the branch at its pre-rebase head (the rebase that followed reconciled documentation counts only; every handler-code change replayed byte-identical, verified by differential). A consolidated fix round covering both reviews plus the CodeRabbit round is in flight; its per-finding dispositions will be appended here.

Round 1 — semantics lens

Held under attack: the #1215/#1231 ordering (27 probes × 8 hash seeds all uniform; three-instantiation rows and duplicate spellings first-wins with checker=codegen=runtime agreement; a source-order-sensitive narrowing where the verifier's E503 and the runtime trap agree — base diverges with a missing guard on the wrongly-resolved cell); the #1211 matrix (20/20 ordered cross-family pairs from {Int, Nat, Bool, Float64, Byte} at the independently-derived values — base fails all 20, five of them silently wrong); snapshot nesting at depth 2; resume sourced from a bare get across widths; E607 location honesty; handler-in-AnonFn and handler-in-match-arm registration.

Findings:

  1. HIGH — the Exn registration arm discards its verdict (_register_exn_tag's boolean dropped): handle[Exn<Unit>] in a pure fn is check-green, verify-clean, unknown tag $exn_Unit at WAT — while the declared-row twin gets a clean E612 skip. Independently the same defect CodeRabbit's compilability finding named. → fix round X1.
  2. HIGH — the registration walk never visits contract predicates: a handle[State<Nat>] inside requires/ensures/assert dies at WAT compile on a check-green program, and the walker's coverage table asserts the impossibility ("no handle in pred"). Pre-existing, but squarely inside this PR's "every handler position" claim. → X2.
  3. MEDIUM — the Bare get/put inside a NESTED handler's clause body: checker and codegen target different cells (silent wrong value) #1211 outward re-entry is exponential in nesting depth: 2,097,235 WAT lines from a 106-line source at depth 18 (base is linear at ~95 lines). Termination holds; the branching factor doesn't. → X3 (cap per the DERIVED_HELPER_DEPTH_CAP precedent, loud skip past it).
  4. MEDIUM — §7.5.2's rule cannot hold for SAME-family nesting: the clause transform routes outward but the host intrinsic addresses the innermost family stack (rule says 5042, observed 5100 — a hybrid). → X4: loud refusal for the same-family clause-body shape, spec narrowed to different-family nesting, true outward addressing tracked as Same-family nested handlers: outward cell addressing for clause-body bare ops #1233 (filed).
  5. LOW, latent — _translate_handle_state restores without try/finally and mutates in place, one intra-body catch away from corrupting the caller's dict. → X5.

Round 2 — validation-honesty lens

Held under attack: all eight #1211 matrix pins hand-derived from spec §7.5.1/§7.5.2 with a checker-side instrument (negative-literal probes producing E503 exactly where the middle cell binds — proving "one level out, not all the way") — no agreement bias; all six pre-fix values reproduced at base; probe READMEs exact (240 files ↔ 240 rows); all three parse-broken deletion reasons verified against history; KNOWN_ISSUES one-to-one with the expected fixed-on-branch delta; the seed sweeps are real subprocess-per-seed with fixed seeds that catch the original bug deterministically (4 of the 6 chosen seeds fail at base).

Findings — a 13-mutant battery, 10 killed, 3 SURVIVED:

  1. HIGH — the fix's central line is untested (M1: reverting the declared-row _effect_ops snapshot leaves 9,284 tests green while a real program silently reverts to the pre-fix value, 301000 → 100000111): every matrix shape wraps the nested handler in another handler, so the declared-row disjunct of the PR's own spec sentence is never exercised. → S2 adds the killing shapes (also what CodeRabbit's test-coverage finding asked for).
  2. HIGH — the clause-registry restore is untested AND §7.5.2 self-contradicts (M5 survives; the pre-existing "performs the intrinsic operation" bullet endorses one semantics, the shipped code the other — distinguishable only under a transforming enclosing handler: base 5000111 / shipped 300100 / mutant 300050). Resolution ratified: the shipped through-the-enclosing-clause reading is the coherent one (the inner handle expression is lexically part of the outer handled body); the old bullet's true scope is self-reference only. → S1 reconciles the spec and pins the transforming shape.
  3. HIGH — the _scan_io_ops handler legs are untested (M13: reverting them leaves the suite identical while IO.print in a clause body becomes unknown func $vera.print). → S3.
  4. MEDIUM — the registration differential is name-only: it stays green on the PR's own sibling bug's invalid-WASM shape (registered-but-wrong-type). → S4a adds wasmtime instantiation per swept WAT, with a planted red-proof; S4b extends the red-proof to the Exn half; S4c fixes the "132 distinct symbols" claim (sum of per-program counts; globally distinct is 31).
  5. Smaller truths — the determinism docstring's seed statistic (six of eight, not four); §7.4.1's over-general "rejected" correction; §7.4's fallback wording (registered order, not declared); the CHANGELOG naming two deleted probe files; one inert conformance constant; the two pre-fix WasmtimeError cases stated as measured. → S5.

Rebase note

After the docs PR (#1230) merged, this branch conflicted on five documentation files (20 hunks, all count-class). Resolved by taking #1230's table structures, re-applying this branch's semantic rows, and setting every number from the doc-counts oracle (9,445 tests / 146 files / 199 conformance / 247 corpus — neither side's numbers were right). CHANGELOG unioned cleanly. Full gate green post-rebase; CI green at the rebased head.

…not honour (#1210, #1211, #1215)

Six code fixes, the §7.5.2 reconciliation they force, and the tests that
distinguish each one.

REGISTRATION (#1210)

- The handler walk's Exn arm called the shared tag registration and threw
  the boolean away, so `handle[Exn<Unit>]` in a `pure` function registered
  no tag and still compiled — `unknown tag $exn_Unit` at whole-module WAT,
  where the declared-row spelling of the same payload had always been a
  clean [E612] function drop.  Both arms now record what they could not
  register and the entry point drops the function, with the gate's own
  wording extracted so the two paths cannot say different things.

- The walk stopped at `decl.body`, but a contract is LOWERED code: a
  `handle[State<Nat>]` in a `requires`, an `ensures`, an `assert`, or a
  `decreases` measure emitted `state_push_Nat` against an import that was
  never declared.  Both pre-scans now walk the contract predicates through
  one shared enumeration (`contract_exprs`) — `decreases` needed it,
  carrying `exprs` rather than `expr`, which the old attribute-name
  shortcut skipped — and both walkers descend `assert` / `assume` /
  quantifier positions their coverage tables had declared structurally
  handler-free.

CLAUSE-BODY OPERATIONS (#1211, #1233)

- Same-family nesting is refused instead of lowered.  The clause TRANSFORM
  half routes outward correctly, but the host intrinsics address only the
  innermost pushed cell of a family, so `handle[State<Int>]` inside
  `handle[State<Int>]` (or inside a function declaring
  `effects(<State<Int>>)`) would write the INNER cell where §7.5.2 names an
  outer one.  That shape is now a loud [E602] citing #1233.  The gate keys
  on the whole pushed-cell stack rather than the adjacent handler, so an
  Int/Nat/Int/Nat nest — whose third level routes to the second while its
  `state_put_Nat` addresses the fourth's cell — is caught too; the index it
  compares against travels with the clause entry, exactly like the op
  registries, so a nested handler's own body keeps addressing its own cell.

- The outward re-entry is bounded.  Each clause-body operation re-expands
  another clause, so the emitted code is exponential in the nesting depth
  (850 / 1,582 / 4,330 / 15,143 WAT lines at depths 2 / 4 / 6 / 8; ~2M by
  18).  `STATE_CLAUSE_INLINE_DEPTH_CAP` = 8 follows the
  `DERIVED_HELPER_DEPTH_CAP` precedent: past it the function is a loud
  [E602] naming the cap, and the legitimate matrix reaches depth 2.

- The handler lowering saves and restores its op registries the way the
  clause inline already did — whole-dict replacement inside a try/finally,
  not in-place mutation with the restore outside it — so a `CodegenSkip`
  out of a handled body cannot leak this handler's `get`/`put` into the
  scope that catches it.  Same treatment for the `Exn` twin.

RESOLUTION ORDER (#1215)

- `ordered_effect_row()`'s fallback for a row member no order tuple
  mentions sorted by effect NAME alone, which is not a total order: §7.3.3
  lets one effect appear twice with different arguments, so `State<Int>`
  and `State<Bool>` tie and a stable sort hands back the frozenset's own
  iteration order — the seed dependence the method exists to remove, inside
  the method itself.  The key is now structural (name plus rendered type
  arguments), which `_effect_type_mapping` inherits.

SPEC §7.5.2, §7.4, §7.4.1, §7.3.2

§7.5.2 held two incompatible readings of the same operation: one bullet
said an operation inside a clause body performs the INTRINSIC operation,
the next said it belongs to the enclosing context.  They disagree exactly
when the enclosing handler declares a clause for it, and the shipped
semantics is the coherent one — the inner handle expression is lexically
part of the outer handled body, so an operation emerging from it is an
outer-body operation SITE and the outer clause runs on it.  The old bullet
is scoped to what is actually true of it (transforms do not cross a CALL
boundary; a clause never re-enters ITSELF), and a third bullet states the
same-family limitation.  §7.4's final resolution step names the REGISTERED
effects in registration order, matching the implementation; §7.4.1 says
"not rejected for ambiguity" is not "always accepted"; and §7.3.2 drops its
claim of an alphabetical canonical form — written order is meaningful and
`vera fmt` preserves it, so imposing one would change what a program means.

TESTS

Mutation-validated where a mutant is named: reverting the `_effect_ops`
restore reddens the new declared-row case alone; clearing rather than
restoring the clause registry turns the enclosing-clause case from 300100
to 300050; dropping the `_scan_io_ops` HandleExpr legs reproduces `unknown
func $vera.print`; a name-only effect sort key gives two distinct outcomes
across six seeds.  The registration differential gains a validation leg —
every swept module goes through `wasmtime.Module`, because a symbol
declared at the WRONG TYPE passes the name comparison while being invalid
WASM — with a planted retyped import proving it can go red, an Exn half for
the strip-and-compare red-proof, and honest floors (the summed reference
count and the globally distinct count are different numbers and both are
floored).  Its corpus loop no longer swallows every exception: declared
parse/transform failures skip, anything else propagates.

The #1203 clause-body-put fixture moves from one family to two.  It handled
`State<Nat>` while declaring `effects(<State<Nat>>)`, which is precisely the
unaddressable shape above; the @nat narrowing boundary it exists to pin is
unchanged, and removing the bare-path guard still reddens it.

DOCS

`ch07_clause_body_op_enclosing`'s three-deep case reads the middle cell's
initial value into its result before the inner clause overwrites it, so no
constant is inert.  The seed-count claim in the determinism suite matches
what was measured.  The pipeline documentation numbers seven stages,
because there are seven — `vera/README.md` said "seven-stage" and then
numbered Resolve as "2b", and `assets/diagrams/architecture.svg` carried
the same half-numbering, while the resolver has its own public entry point,
its own `cli.py` step, its own diagnostics and its own stage column in the
module map.  Module-map line totals, test counts and the corpus count are
re-synced to the tree.

Refs #1210, #1211, #1215, #1231, #1233

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Round-3 dispositions (appending to the review record above)

All findings from both adversarial rounds + the CodeRabbit round landed in a3bd7e67 (26 files, +1340/−182), one justified skip (the MD038 in-span space, which is load-bearing for the module-map tree rendering). Highlights:

  • X4 landed wider than the finding: top-of-stack membership was not the right refusal test — an Int/Nat/Int/Nat nest routing level 3→2 while the intrinsic addresses level 4 is the same bug at a distance. The gate compares the resolved family against every family pushed at-or-inside the addressable boundary (decl_addressable_from carried on the clause entry). p4/p26 refuse citing Same-family nested handlers: outward cell addressing for clause-body bare ops #1233; the 20-program different-family matrix stays green.
  • All three surviving mutants now die: M1 (declared-row snapshot) by the new declared-row case, M5 (clause-registry restore) by the transforming-handler pin asserting 300100, M13 (IO-ops legs) by the IO-in-clause-body test — each red-proof run against the reverted line in a scratch copy.
  • The registration differential validates, not just name-matches: every swept WAT is wasmtime-instantiated, with a planted retyped-import red-proof (the shape the name-only check provably missed); the red-proof covers State and Exn; the floors report summed references (132) and globally-distinct symbols (31) as the different numbers they are.
  • Contract predicates register handlers (requires/ensures/decreases/assert/quantifiers — the walker coverage claims are now true by recursion, not assertion), the exponential clause-inline re-entry is capped at 8 with a loud E602 (15,143 WAT lines at the cap; 626 + clean skip past it), §7.5.2's self-contradiction is reconciled with the through-the-enclosing-clause reading pinned, and the effect-row fallback sort key is structural.
  • One behavior change beyond the PR body's list, flagged explicitly: a clause-body bare op resolving into a same-family enclosing context — previously compiling with silently mis-addressed writes — is now a loud E602 citing Same-family nested handlers: outward cell addressing for clause-body bare ops #1233. One pre-existing test fixture had exactly that shape and was re-shaped to a different-family nest that preserves its @Nat-boundary purpose (mutation-confirmed).

Suite: 9,465 tests / 199 conformance / 247 corpus; full gate sweep green. CI + fresh CR pass at a3bd7e67 and a fixed-point adversarial pass on this round's diff are the remaining merge gates.

@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Outside-diff finding on vera/README.md (pipeline-stage numbering): fixed in a3bd7e6 — and reality pointed to renumbering rather than keeping the sub-stage: resolve_imports is its own public entry point, run as a distinct step by the CLI, with its own diagnostics accumulation, and the module map already gives Resolve a stage column while the prose said "seven-stage" and numbered six. The ASCII pipeline and architecture.svg (badges + its stages-five-and-six footnote) now number Parse 1 → Execute 7 consistently; the alt text lists resolve as an unnumbered peer and needed no change; the only other 2b reference repo-wide was that one line.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== matching documentation references =='
rg -n -i -C 2 'seven-stage|2b|resolve imports|resolve_imports|Parse|Execute' \
  vera/README.md architecture.svg 2>/dev/null || true

echo
echo '== resolve_imports definition and CLI call sites =='
rg -n -C 4 '(^\s*(def|async def)\s+resolve_imports\b|\bresolve_imports\s*\()' \
  --type py vera 2>/dev/null || true

echo
echo '== repository-wide 2b references =='
rg -n -i -C 1 '\b2b\b' . -g '!**/.git/**' || true

Length of output: 48421


@aallan The README and CLI structure support Resolve as stage 3. ModuleResolver.resolve_imports() is a public method, and vera/cli.py invokes it as a separate step.

The correction is not complete. assets/diagrams/architecture.svg still contains <!-- 2b · Resolve --> at Line 74. Update or remove that SVG comment so the source agrees with the displayed stage numbering.

You are interacting with an AI system.

… too

Skip-changelog: SVG source-comment sync with the stage renumbering

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Fixed in the just-pushed commit — and your line-74 catch generalized: the ENTIRE source-comment chain lagged the badge renumbering (2b·Resolve, 3·Type Check, 4·Verify, 5·Compile, 6·Execute). All seven comments now read 1·Parse through 7·Execute, matching the displayed badges; site-assets gate green.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

svg="assets/diagrams/architecture.svg"

echo "== Stage source comments =="
rg -n '<!--\s*[1-7](?:[a-z])?\s*·' "$svg" || true

echo
echo "== Obsolete numbering references =="
rg -n -i '2b\s*·\s*Resolve|seven-stage|stages-five-and-six' "$svg" || true

echo
echo "== Visible stage labels =="
rg -n 'Parse|Resolve|Type Check|Verify|Compile|Execute' "$svg" || true

Length of output: 1598


@aallan Confirmed. assets/diagrams/architecture.svg now has a complete, sequential source-comment chain from 1 · Parse through 7 · Execute. The visible stage labels match the comments. No obsolete 2b · Resolve reference remains.

You are interacting with an AI system.

@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review record — round 4 (fixed-point on the round-3 fix, a3bd7e67)

Verdict: not yet empty — 1 high, 2 medium, 3 low; the new gate machinery itself held under every attack.

  • HIGH — two walker positions remain unwalked: a systematic differential over every AST class with descendable expression fields (the instrument the corpus cannot be) found LetDestruct.value and ModuleCall.args named by neither pre-scan walker — let Tuple<@Nat,@Nat> = f(handle[State<Nat>]...) and vera.math::identn(handle[State<Nat>]...) are check-green and die at WAT with verbatim the symptoms this PR fixes elsewhere, and the Exn<Unit>-in-destructuring shape bypasses the new E612 gate. Pre-existing (verified identical at the parent), i.e. incomplete work rather than new breakage — the corpus has zero programs combining handle[ with either position.
  • MEDIUM — the validation leg's engine contradicts the Propagate drops to call_indirect carriers; call_indirect iff table #1192 lesson: bare wasmtime.Engine() where the repo's own helpers build Config(wasm_exceptions=True); on a resolved wasmtime with exceptions off by default, 10 of the 31 validated modules go spuriously red.
  • MEDIUM — the leg's stated scope is wrong 7×: "each swept module" vs the real 31-of-226 (handler-bearing only), and the sweep's checker-less shortcut build diverges from vera compile on 52 corpus programs, letting 25 negatives reach the sweep.
  • LOW: the depth cap's early raise leaves four registries un-restored on its own skip path (unobservable today — an enclosing finally always re-restores — but contrary to the round's own stated discipline; the fix is check-before-mutate); the spec bullet's bold lead-in overstates (same-family nesting without clause-body bare ops works fine); the E602 message says "bare" though the gate correctly covers the qualified spelling; the cap's actual boundary (9) and the qualified same-family case are unpinned.

Held under attack: the addressability window's arithmetic (alias spellings both directions, Byte/Int distinctness, four-deep alternation, with-expression scope, use-time semantics, own-body ops), Exn correctly outside the gate, cap reset per function, gate-before-cap ordering, all contract-predicate positions including where-helpers and double-registration, snapshot aliasing, the fixture reshape's @nat pin (independently re-mutation-tested), §7.5.2's four cases composing cleanly, and the full gate wall (9,315 tests, mypy, ruff, all doc gates).

Round-5 fix in flight for the six findings; dispositions will be appended.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vera/codegen/functions.py (1)

216-240: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prevent later State instances from remapping shadowed operations

The checker resolves a user-defined get before effect operations, but codegen checks _effect_ops before user functions. With State<Int>, State<Bool>, codegen can therefore route get to state_get_Bool and put to state_put_Int. Keep shadowed operations out of the effect table, or apply consistent shadowing to both operations.

🤖 Prompt for 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.

In `@vera/codegen/functions.py` around lines 216 - 240, Update the State effect
handling that populates effect_ops for “get” and “put” so once either operation
is shadowed by a user-defined function, later State instances cannot register or
remap it. Apply the same shadowing rule consistently to both operations,
preserving the first valid State operation in the effect table.
🤖 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 `@KNOWN_ISSUES.md`:
- Line 30: Add a regression case to _SAME_FAMILY_CASES in
tests/test_nested_handler_clause_ops.py for the four-level Int/Nat/Int/Nat
nested handler shape, asserting the expected E602 codegen skip when the target
family appears in the shadowed addressable slice. If this behavior is not
intended to be tested, remove the at-distance claim from KNOWN_ISSUES.md
instead.

In `@spec/07-effects.md`:
- Around line 71-73: Update the §7.3 lead-in to define an effect row as a
written, ordered sequence of effects while stating that containment uses set
equality per §7.3.2. Remove the conflicting “unordered set” description and
preserve the surrounding explanation that source order remains meaningful for
effect resolution and formatting.

In `@tests/test_nat_narrowing_return_differential.py`:
- Around line 1612-1618: Add an explicit zero-value test alongside
test_clause_body_put_negative_traps and test_clause_body_put_non_negative_passes
for _PUT_IN_CLAUSE_BODY, asserting _run(..., "go", 0) returns 0. Keep the
existing negative-trap and positive-value coverage unchanged.

In `@tests/test_nested_handler_clause_ops.py`:
- Around line 714-752: Add a boundary test for
`_deep_nest(STATE_CLAUSE_INLINE_DEPTH_CAP + 1)` that compiles the source and
asserts it emits E602, includes the cap value, and reports the
exponential-expansion message. Keep the existing at-cap test and the `CAP + 2`
test unchanged.

In `@vera/codegen/compilability.py`:
- Around line 32-37: Update contract_exprs to explicitly dispatch the supported
function-contract types Requires, Ensures, Decreases, and Invariant, yielding
each type’s appropriate expression field. Remove the getattr-based fallback and
raise an error for any unsupported Contract type, so mypy validates field access
and new types cannot be silently skipped.
- Around line 546-548: Initialize _unregistrable_exn_tags in
CodeGenerator.__init__ alongside _unregistrable_state_cells, while preserving
the existing per-function reset in _scan_body_for_state_handlers.

In `@vera/environment.py`:
- Around line 36-48: Update _effect_sort_key to include repr(ei.type_args) in
its returned tuple so refinement predicates and `#b` markers remain
distinguishable during deterministic sorting. Add coverage for the
State<Pos>/State<Neg> seed sweep using an empty current_effect_order, preserving
distinct ordering across hash seeds.

In `@vera/wasm/calls_handlers.py`:
- Around line 1920-1938: Move the STATE_CLAUSE_INLINE_DEPTH_CAP check in the
clause expansion flow before any registry or state mutations, including the
_addressable_from assignment, so CodegenSkip cannot leave partial state behind.
Remove the manual _addressable_from restoration from the early-exit branch, and
retain saved_addressable plus the addressable assignment immediately before the
existing try/finally; keep _clause_inline_depth incrementing unchanged.
- Around line 1700-1714: The family comparison in the call-handling logic must
avoid mangling the already-mangled result of _state_import_family(). Keep
entry.family comparisons compatible with the unmangled family values, but
compare bare-import target_family directly against the appropriately mangled
shadowed families so composite same-family nesting returns through CodegenSkip
instead of targeting the innermost cell. Add a regression covering bare-import
composite same-family nesting.

---

Outside diff comments:
In `@vera/codegen/functions.py`:
- Around line 216-240: Update the State effect handling that populates
effect_ops for “get” and “put” so once either operation is shadowed by a
user-defined function, later State instances cannot register or remap it. Apply
the same shadowing rule consistently to both operations, preserving the first
valid State operation in the effect table.
🪄 Autofix

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: a4f7851d-e923-415f-b0ed-581e85df6818

📥 Commits

Reviewing files that changed from the base of the PR and between 89d54ca and f3fea80.

⛔ Files ignored due to path filters (4)
  • assets/diagrams/architecture.svg is excluded by !**/*.svg
  • docs/SKILL.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • tests/conformance/ch07_clause_body_op_enclosing.vera is excluded by !**/*.vera
📒 Files selected for processing (22)
  • AGENTS.md
  • CHANGELOG.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • spec/07-effects.md
  • tests/test_effect_op_determinism.py
  • tests/test_nat_narrowing_return_differential.py
  • tests/test_nested_handler_clause_ops.py
  • tests/test_state_exn_registration.py
  • vera/README.md
  • vera/codegen/compilability.py
  • vera/codegen/functions.py
  • vera/environment.py
  • vera/skip.py
  • vera/wasm/calls.py
  • vera/wasm/calls_handlers.py
  • vera/wasm/context.py
  • vera/wasm/helpers.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread KNOWN_ISSUES.md
Comment thread spec/07-effects.md
Comment thread tests/test_nat_narrowing_return_differential.py
Comment thread tests/test_nested_handler_clause_ops.py
Comment thread vera/codegen/compilability.py Outdated
Comment thread vera/codegen/compilability.py
Comment thread vera/environment.py Outdated
Comment thread vera/wasm/calls_handlers.py
Comment thread vera/wasm/calls_handlers.py
…lling (#1210, #1211)

Three more positions reached the lowering without reaching either pre-scan
walker, and a fourth defect let a whole class of same-family nests past the
gate that exists to refuse them.

`LetDestruct.value` was absent from the `Block` dispatch of BOTH walkers, so
`let Tuple<@nat, @nat> = f(handle[State<Nat>] ...)` emitted `state_push_Nat`
against no import — and, because the round-3 uncompilable-payload gate is
driven by the same walk, an `Exn<Unit>` payload written there bypassed E612
outright where its `LetStmt` twin is a clean function drop.  `ModuleCall.args`
sat in both coverage tables as "tracked by the imported module's own scan",
true of the callee and false of the arguments, which are this module's own
lowered expressions.  And a SIGNATURE REFINEMENT predicate — a `{ @base | P }`
parameter or return type has `P` emitted as a boundary guard, and is reached
through the alias table rather than structurally, so no walk from `decl.body`
can find it; `_signature_refinement_predicates` is the signature's
`contract_exprs`, mirroring `_refinement_guard_parts`'s two bails so
registration equals what is emitted rather than exceeding it.

None of the three had a corpus instance, so the corpus-anchored differential
could not have found any of them.  The walkers now carry a schema-driven
FIELD-coverage gate: it reads the dataclass fields of `vera/ast.py` and fails
on any node class with an expression-carrying field that neither walker
dispatches on, unless the class is in an explicit ten-entry justified-ignore
table naming the route its expressions ARE reached by.  It is deliberately
stronger than `scripts/check_walker_coverage.py`, whose canonical set is the
`Expr` subclasses (`LetDestruct` is a `Stmt`) and whose verdict is "the class
is NAMED" (`ModuleCall` was named, with a disposition covering half the node).

The same-family refusal compared family names in two spellings.
`_pushed_cell_families` and a clause registry entry carry the canonical family
(`Option<Int>`); an import name carries the mangled one (`Option_LInt_R`); and
mangling is not idempotent, so re-mangling the already-mangled side made every
COMPOSITE family compare unequal to itself and the gate returned instead of
refusing.  `handle[State<Option<Int>>]` nested in `handle[State<Option<Int>>]`
with no outer `put` clause compiled and ran, returning 5100 where the
enclosing-context rule says 5042 — on the exact shape its scalar `Int` twin
was refusing correctly the whole time.  Both sides now normalise once, and the
message names the canonical cell.

The structural tiebreak that removed the row-order hash-seed dependence had
the same bug one level down: it rendered type arguments with `pretty_type`, a
presentation renderer that elides a refinement's predicate and strips a type
variable's built-in marker, so `effects(<State<Pos>, State<Neg>>)` over two
refinement aliases of one base tied again and fell back to frozenset order.

Also in this round: the clause-inline depth cap checks before it mutates (its
skip was the one exit that left the six clause registries replaced); the
same-family message covers the qualified `State.put` spelling that routes
through the same gate; `contract_exprs` dispatches on the real contract types
instead of probing for an attribute, and raises on an unknown one; the Exn
half of the walk's unregistrable-type record is declared beside its State
sibling; the registration differential validates through the exceptions-
enabled engine `execute()` uses and filters the conformance negatives out of
its sweep, with its scope and its two limits stated on the test; spec 7.3's
lead-in stops calling an effect row an unordered set, and 7.5.2's same-family
lead-in states the precise claim.

New pins: the composite gate bypass in both its branches with a scalar
differential and a different-family positive control, the four-level
Int/Nat/Int/Nat shadow-at-a-distance the KNOWN_ISSUES row asserts, the
qualified spelling, depth exactly CAP+1, the refinement-alias seed sweep, the
zero boundary of the clause-body put guard, and the declared-row op registry's
shadow symmetry across two State instantiations.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review record — round 5 (1bec6e8a): the two independent review streams converged on the same defect, and the instrument found a third hole

All 15 items (the round-4 adversarial findings + CodeRabbit's ten) landed in one commit; one skip with instrumented proof. The headlines:

  • The composite-family gate bypass (CR's mangling hunch) was real and the PR's worst defect: mangle_type_name is not idempotent, the gate re-mangled an already-mangled family, so every composite family compared unequal to itself — handle[State<Option<Int>>] nested in itself was check-green, verify-clean, valid WASM, and silently wrong (5100 vs the rule's 5042). Both branches now compare in one representation; four regression rows + a different-family control.
  • A THIRD unwalked position, found by building the permanent instrument: RefinementType.predicate — a handler inside a refinement predicate (type Big = { @Int | ...handle[State<Nat>]... }) is emitted as a boundary guard but was never registered; check-green, unknown-func. Fixed via the signature-predicate walk mirroring the guard emitter's own bails, because the justified-ignore entry would have been a lie.
  • The corpus-blind-spot class is dead as a class: TestPreScanWalkerFieldCoverage derives all 31 descendable AST classes from the dataclass schema and requires a real isinstance branch (comments do not count) or a justified-ignore entry (10, one line each); mutation-validated 5/5. It could not live in the Walker-completeness audit: every isinstance(expr, ast.X) dispatch chain enumerates all Expr subclasses #597 gate (Stmt-invisible, named≠covered) — the report records why.
  • LetDestruct/ModuleCall walked in both walkers (all five round-4 repros now run or loudly skip, incl. the E612-bypass shape); the sort key is structural through predicates (Pos/Neg seed sweep 2→1 outcomes); the validation leg uses the Propagate drops to call_indirect carriers; call_indirect iff table #1192 exceptions-enabled engine; the sweep's docstring states its checker-less-shortcut limits and filters manifest negatives; the cap check precedes all mutations (leak probe 1→0); contract_exprs dispatches explicitly and raises on new kinds; §7.3 and §7.5.2 wordings are now precise; zero-value and CAP+1 boundaries pinned. F13 (State get/put shadowing) skipped: instrumented across three programs — both ops guard identically per instance, first-instantiation-wins, shadow respected; a black-box pin added anyway.

Suite: 9,490 tests / 199 conformance / 247 corpus, full gate wall green. A round-6 fixed-point pass on this commit's diff runs next; with CI and CR's fresh pass, those are the merge gates.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 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`:
- Line 104: Fix the Markdown table row around the differential-test entry so it
contains exactly four cells: escape the literal pipe character inside the long
description, and remove only spaces immediately inside the affected backtick
code spans to satisfy MD038. Preserve the table content otherwise, then rerun
markdownlint-cli2 and confirm the MD056 and MD038 violations are resolved.
- Around line 9-11: Correct the affected table row in TESTING.md to satisfy
markdownlint MD056 and MD038, then ensure the documented test counts are
regenerated or verified by rerunning the count gate with pytest available. Keep
the reported counts accurate and preserve the stale-count scan result.

In `@tests/test_walker_defensive_branches_597.py`:
- Around line 398-413: Update the conformance coverage around _canonical_classes
and the Line 484 validation to track each (class_name, field_name) obligation
instead of reducing classes to set(canonical). Require every descendable field
to have either a walker dispatch route or an explicitly justified ignore route,
and add a mutation regression that removes a field route from an
already-dispatched AST class and verifies the check fails.

In `@vera/environment.py`:
- Around line 67-70: Update the FunctionType branch of _structural_type_key to
replace pretty_effect() with a recursive structural effect-row key that
preserves the row variable and sorted structural keys for nested EffectInstance
values. Ensure _effect_sort_key() deterministically distinguishes outer effect
instances so _effect_type_mapping() selects the same bare-operation type across
PYTHONHASHSEED values, and add cross-seed regressions covering refinement
predicates and builtin type-variable markers.
🪄 Autofix

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: 53e3eb76-6b0b-442b-ac8c-b6395fefa653

📥 Commits

Reviewing files that changed from the base of the PR and between f3fea80 and 1bec6e8.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (19)
  • CHANGELOG.md
  • FAQ.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • spec/07-effects.md
  • tests/codegen_helpers.py
  • tests/test_codegen_effects.py
  • tests/test_effect_op_determinism.py
  • tests/test_nat_narrowing_return_differential.py
  • tests/test_nested_handler_clause_ops.py
  • tests/test_state_exn_registration.py
  • tests/test_walker_defensive_branches_597.py
  • vera/README.md
  • vera/codegen/compilability.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/environment.py
  • vera/wasm/calls_handlers.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread TESTING.md Outdated
Comment thread TESTING.md Outdated
Comment thread tests/test_walker_defensive_branches_597.py
Comment thread vera/environment.py Outdated
@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review record — round 6 (fixed-point on 1bec6e8a): one confirmed defect, two accuracy nits; the round-5 machinery held

  • CONFIRMED — the refinement-predicate registration covers one of three boundary-guard routes. _signature_refinement_predicates enumerates only a named FnDecl's direct params + return, but the guard emitter is also reached through TUPLE-COMPONENT decomposition (params via contracts.py/functions.py, returns via contracts.py — a designed Implement refinement-type predicate verification #746/Codegen runtime guards for general refinement predicates (#746 follow-up) #762 emission site) and through ANONFN refined params/returns (closures.py — an AnonFn is not an FnDecl). All three miscompile check-green programs with the exact unknown-func symptom this commit closed for the direct case (repro: the commit's own Big alias with the param changed to @Tuple<Big, Int>). A neutering differential localizes it to the enumeration's scope. The knock-on is the honest part: the instrument's RefinementType justified-ignore entry is true of half the node's reach — the same disposition shape the instrument exists to catch. Fix direction ratified: hoist ONE derivation that both the emitters and the pre-scan consume, the posture this PR already uses elsewhere.
  • MINOR: the field-coverage gate is class-granular (a new class is loud; a new field on an already-dispatched class is not — latent today, all current fields screened as covered) — docstring scoped to the truth; and two docstrings say 31 validated modules where the measure is 30 (31 is the distinct-symbol count), with the bare-engine claim scoped to older supported wasmtimes (47.0.1 defaults exceptions on).
  • Held: the one-mangling normalization (alias-spelled and three-deep composite nests refuse in both branches; the different-family composite control runs to its checker-derived 7042; every E602 names the family canonically); the two bails mirror the guard emitter exactly (nested-refinement loud, erased-base correctly silent); the negative filter excludes exactly the 26 negatives with set-equality proof and 51/38/9/8 floor margins; the full gate wall at head (9,340 passed, mypy, ruff, all doc gates).

Round-7 fix in flight for the route coverage + the three wording items; dispositions will be appended.

The signature enumeration the pre-scans consume reached the boundary-guard
emitters by one of their four routes, because it was written as a copy of
what one of them does.  The guard layer is also entered by decomposing a
tuple PARAMETER into its components, by decomposing a tuple RETURN, and from
a closure's own refined formals and return — each lowering predicates
nothing registered.  A `Tuple<Big, Int>` parameter, a `Tuple<Big, Int>`
return, and `fn(@BIG -> @int)` / `fn(@int -> @BIG)` behind an `apply_fn` all
died at whole-module WAT with `unknown func $vera.state_push_Nat`, from
check-green, verify-clean programs.

There is now ONE derivation of what those guards check, living beside the
emitters it must equal.  `_tuple_component_guard_sites` owns the tuple
decomposition — the component classification, the `@Unit`-component skip,
the heap layout, and the fail-closed depth limit — and the emitter, the
return-epilogue gate and the host-import pre-scan all read it, where they
used to carry three hand-kept copies and the pre-scan's copy did not exist.
`_signature_refinement_predicates` enumerates on top of it and of
`_refinement_guard_parts`, so its two bails ARE the emitter's bails rather
than a mirror of them.

Registration equals emission in both directions.  Component decomposition
stays a named-function leg because the closure path emits no component
guards, so enumerating it for a closure would declare a host import nothing
calls — measured at 4 spurious imports for one closure formal.  Both walkers
now descend an `AnonFn`'s signature as well as its body, cycle-guarded:
`type R = { @int | ... fn(@r -> @int) ... }` type-checks, so expanding a
refinement that contains a closure refined by itself is a real cycle, and
unguarded the walk blows the recursion limit on a check-green program.

Two instruments were claiming more than they held.  The pre-scans'
field-coverage gate said a new FIELD on a dispatched class was loud; it was
class-granular, so a field added to a class that already had a branch passed
silently.  It now compares each dispatched class's descendable fields
against the names that walker's source reads — the name-based limit is
stated on the gate rather than papered over.  A new callgraph gate asserts
the structure that keeps the derivation single: every consumer reads the
shared decomposition, and none reclassifies behind it.  And the
`RefinementType` justified-ignore entry now names the derivation and all
four routes it covers, which is what it was asserting all along.

`_error_once` replaces the bespoke E618 site set, since the depth-limit E617
is now reachable from three consumers of one derivation.

Two count claims corrected: 31 was the number of distinct state/exn SYMBOLS
in the corpus, not of handler-bearing modules, which is 30.  The bare-engine
rationale is scoped to the claim that holds — on wasmtime 47.0.1 exceptions
default on, so the helper exists for the supported versions where they do
not, and what that costs is measured directly (10 of the 30 fail with
`wasm_exceptions` off).

Co-Authored-By: Claude <noreply@anthropic.invalid>
The #1229 row was owed since its filing; #1234/#1235 are round-7
discoveries (the closure-lift non-termination and the closure-boundary
component-guard gap), both pre-existing and tabled for PR C.

Skip-changelog: three-row bug-table bookkeeping, no compiler change

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review record — round 7 (ece4e4d6 + fd8a29f2): the guard-derivation hoist landed as REROUTED EMITTERS, not shared-helpers

  • G1: _tuple_component_guard_sites is now the one tuple decomposition (classification, Unit-skip, layout, recursion, depth limit), lazily consumed by the component-guard emitter, the guardability probe, and the pre-scan — divergence now requires editing one function. _signature_refinement_predicates moved beside the emitters, takes FnDecl OR AnonFn, and calls _refinement_guard_parts directly (its bails ARE the emitter's bails). Both walkers descend AnonFn signatures, cycle-guarded — and the guard is asserted in both directions (type R = { @Int | ... fn(@R -> @Int) ... } hits RecursionError unguarded). All four round-6 repros flip red→green with derived values; the direct and erased-base controls hold. The instrument's RefinementType entry is now TRUE, and a new callgraph gate (TestBoundaryGuardDerivationIsShared) asserts every consumer reads the shared decomposition — three mutations, three distinct precise reds.
  • G2: FIELD-granularity implemented, not just re-scoped — each dispatched class's descendable fields must appear as attribute reads in the walker source; the round-6 fake_extra mutation now goes red, with the honest name-collision limit documented.
  • G3: 31→30 corrected at both sites (+ CHANGELOG/TESTING retellings); the bare-engine rationale scoped to the supported-range claim with the measured 10-of-30 cost.
  • Round-7 discoveries, filed and tabled for PR C with KNOWN_ISSUES rows (fd8a29f2): vera compile never terminates on a self-referential refinement used in a signature (closure-lift queue feeds itself) #1234vera compile never terminates on a self-referential refinement used in a signature (check-green, no diagnostic; the closure-lift queue feeds itself — verified pre-existing with a faulthandler dump; the new pre-scan cycle guard keeps registration out of the loop, the lift loop is the residual); Closure boundaries skip tuple-component refinement guards: the #746 enforcement surface exists for named functions only #1235 — closure boundaries skip tuple-component refinement guards (the Implement refinement-type predicate verification #746 enforcement surface exists for named functions only; the new derivation makes the fix mechanical). Also added the row vera test scores untranslatable-requires trials as contract failures: String-length-constrained signatures spuriously FAIL #1229 had been owed since its filing.

Oracle: 9,505 tests / 199 conformance / 247 corpus; differential re-measured identical to round 5 (no spurious registrations). Round-8 mini-review runs next; with CI and CR's fresh pass, the merge gates stand.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@KNOWN_ISSUES.md`:
- Line 23: Update the `#1234` table row in KNOWN_ISSUES.md by escaping the pipe
inside the inline code example as \|, matching the neighbouring rows. Preserve
the existing code example and issue link so the Markdown table has exactly two
columns.

In `@vera/README.md`:
- Around line 129-141: Recalculate the line counts for every tracked module
listed under the codegen/ table in vera/README.md, then update the directory
total and each module’s count, including api.py, modules.py, monomorphize.py,
and assembly.py. Preserve the existing table structure and descriptions.
🪄 Autofix

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: 9f992234-3c1f-4228-93ad-c36902f6b7f8

📥 Commits

Reviewing files that changed from the base of the PR and between 1bec6e8 and fd8a29f.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (14)
  • CHANGELOG.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/codegen_helpers.py
  • tests/test_refinement_binder_convergence_1208.py
  • tests/test_state_exn_registration.py
  • tests/test_walker_defensive_branches_597.py
  • vera/README.md
  • vera/codegen/compilability.py
  • vera/codegen/contracts.py
  • vera/codegen/core.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • aallan/vera-bench (manual)

Comment thread KNOWN_ISSUES.md Outdated
Comment thread vera/README.md Outdated
@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Outside-diff finding on vera/codegen/functions.py (State get/put shadowing consistency): verified and skipped — the code already guards both operations identically. Instrumented WasmContext population across three programs: each op checks X not in self._fn_sigs and X not in effect_ops per State instance, so a user function shadowing get leaves only put registered (with the wt/vera mirrors correctly absent for the shadowed op), shadowing put leaves get plus both mirrors, and with no shadow the second instantiation never overwrites the first (first-written-wins, the #1231 rule). A black-box regression pinning the shadowed-op behavior was added in 1bec6e8 scoped to the probe's body.

@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review record — round 8 (closer, ece4e4d6 + fd8a29f2): ROUND ADDS NOTHING

The adversarial side converges at eight rounds. Independently re-proven: WAT byte-identity of the rerouted emitters across 8 programs (with the one deliberate difference scoped honestly — _error_once dedups the depth-limit E617 and the guardability probe now fails closed past the limit, return-only routes still loud); the co-extension pin goes red naming exactly the 4 spurious imports; BOTH walkers' cycle guards terminate and both raise RecursionError when neutered, with a legitimate nested-refinement control registering the full quadruple and no false positive; the callgraph gate names a bypassing consumer; the field gate's red-proof is genuinely synthetic; the three KNOWN_ISSUES rows match their issue bodies (#1234's hang independently reproduced on HEAD with a 45s timeout); the ledger reconciles to zero unaccounted (22 open = 15 rows + 7 fixed-on-branch); and the full gate wall holds (9,355 passed; all doc gates; E130 + SQL untouched).

Two low notes folded into the final round alongside CodeRabbit's six open findings: a one-clause #1235 back-reference in the contracts.py docstring (the repo's tracked-in convention), and the standing observation that closingIssuesReferences is empty by construction on a release-branch-based PR — the release→main PR carries the closes, where the pre-handover verification rule applies.

Remaining before merge: round 9 (the six CR findings — the two Majors being the (class, field) pair-keying that completes the walker gate, and the recursive structural effect-row key that closes the nested-row elision — plus four doc/table fixes and the back-reference), then CR's fresh pass at that head.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Codegen function outline =='
ast-grep outline vera/codegen/functions.py --items all --type function --match 'scan|state|handler|effect' || true

echo
echo '== State get/put registration guards =='
rg -n -C 8 'State|effect_ops|_fn_sigs|\bget\b|\bput\b' vera/codegen/functions.py

echo
echo '== Shadowing regression references =='
rg -n -C 6 'shadow|first.*(?:win|registr)|State.*(?:get|put)|(?:get|put).*State' \
  tests/test_codegen_effects.py tests 2>/dev/null || true

Length of output: 200644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== State registration branches in vera/codegen/functions.py =='
rg -n -C 16 \
  'if .*["'\'']get["'\''].*not in self\._fn_sigs|if .*["'\'']put["'\''].*not in self\._fn_sigs|effect_ops\[['\'']get['\'']\]|effect_ops\[['\'']put['\'']\]' \
  vera/codegen/functions.py || true

echo
echo '== Relevant test definitions in tests/test_codegen_effects.py =='
rg -n -C 18 \
  '^def test_.*(?:shadow|State)|^    def test_.*(?:shadow|State)|shadowing.*(?:get|put)|(?:get|put).*shadow' \
  tests/test_codegen_effects.py || true

Length of output: 5791


@aallan Confirmed from the current branch.

In vera/codegen/functions.py, Line 217 guards get with:

"get" not in self._fn_sigs and "get" not in effect_ops

Line 232 applies the equivalent guard to put.

tests/test_codegen_effects.py Line 2626 parametrizes both user-defined get and user-defined put. It verifies that the shadowed operation uses the user function and that the unshadowed sibling retains the first State<Int> intrinsic.

The finding does not apply. I withdraw it.

You are interacting with an AI system.

…down (#1210, #1215)

The pre-scan coverage gate obligated CLASSES.  A class discharged its
obligation the moment it had any `isinstance` branch, so a new
expression-carrying field on an already-dispatched class — the commonest way
an AST grows — was left to a second, weaker screen beside the gate.  The
obligation is now one per `(class, field)` pair, discharged by the
conjunction (branch AND field read) or by a justified-ignore entry, and the
two checks are one gate: 39 obligations, 28 dispatch-routed, 11 justified
through 9 class-level entries and 2 per-pair ones.  `HandlerClause` carries
two fields and so is written per pair; a class-level entry is permitted only
while its class has a single field, because otherwise it would exempt
whatever field the class grows next — the same class-keyed hole one table
down.  Both can-go-red proofs and the gate call one `_pair_holes`, so the
mutations exercise the check that runs.

The effect-order tiebreak's structural key stopped one level short.  A type
argument may itself be a function type, and a function type carries its own
effect ROW, whose leg was rendered by `pretty_effect` — which renders each
member through `pretty_type`, the presentation renderer that elides a
refinement's predicate and strips a type variable's built-in marker.  Two
outer instances differing only inside a nested row therefore tied for a
third time and fell back to `frozenset` order.  `_structural_effect_key`
renders the row structurally and sorts its members on the same key the
tiebreak uses, so the two mutually recurse; both nested cases are swept
across six `PYTHONHASHSEED` values, asserted separately so a regression
names the elision that came back.

`_signature_refinement_predicates`'s deliberate closure-path asymmetry now
names its tracker.  Table rows in `TESTING.md` and `KNOWN_ISSUES.md` escape
the pipes inside their refinement spellings, so the rows carry the cell
count the rest of their tables do.  Every count in `vera/README.md`'s module
map is re-measured from the tracked files, and the trailing total with them.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review record — round 9 (0a64ae76): the final round's dispositions

CodeRabbit's six + the round-8 closer's two notes, one commit: the walker gate's obligations are PAIR-keyed (39 over 31 classes; a real-new-field mutation invisible to the class-keyed gate goes red under the pair gate for both walkers, with a rot-guard on the ignore-table's shape rules); the structural key recurses through nested effect rows (both seed-tie cases 2→1 outcomes, pinned as separate legs); one CR finding was a false positive against current code (skipped with the grep evidence); the two table-escaping defects fixed with a repo-wide zero-unescaped-pipes scan; the module map re-measured table-wide (46 rows — the ±10% gate band is why drift accumulated, noted for the record); the #1235 back-reference clause added per the tracked-as convention. Round-8's key probes re-run clean after the changes (co-extension, cycle-guard both directions, both synthetic red-proofs, all four seed sweeps).

Suite: 9,505 collected / 9,355 passed; all gates green. The adversarial side converged at round 8; this round dispositions the outstanding review findings. Merge gates now: CI at this head + CodeRabbit's fresh pass adding nothing.

@aallan
aallan merged commit b27a67e into release/v0.1.10 Aug 7, 2026
26 checks passed
@aallan
aallan deleted the fix/1210-1211-1215-handler-semantics branch August 7, 2026 18:21
aallan added a commit to chethanuk/vera that referenced this pull request Aug 13, 2026
Spec §7.5 makes two cells distinct exactly when their resolved types are,
and a refinement is part of that type: `EffectInstance` holds the
`RefinedType`, §7.5.1 already required the state declaration to match it
"predicate included", and E125 refuses to pass one where another is
required.  Codegen collapsed it away — `family_name` stripped the
refinement before rendering — so under `type Pos = {@int | @Int.0 > 0}` and
`type Neg = {@int | @Int.0 < 0}` all three of `State<Pos>`, `State<Neg>` and
`State<Int>` shared one host cell.

A `Pos` handler wrapping a `Neg` handler, with a callee declaring
`effects(<State<Pos>>)` called inside the inner one, sent that callee's
`put(111)` to the `Neg` cell: `main` returned 1.  Check-green, verify-green,
silently wrong.  The family now renders the whole resolved type through
`types.structural_type_key`, so each of the three routes to its own cell.
The measured matrix, all four flips value-oracled in
`tests/test_family_naming.py` §8:

    Pos outside / Neg inside, callee bound to Pos   1 -> 111
    Neg outside / Pos inside, callee bound to Neg  -1 -> -222
    Int outside / Pos inside, callee bound to Int   0 -> 7
    three-deep Pos/Neg/Pos, middle clause body      E602 refusal -> 42

The last one is the PR aallan#1232 addressability-gate interaction: with all three
collapsed to `Int` the gate saw a same-family nest and refused the program
outright.  Distinct families make the shape legal again, and it routes per
§7.5.2 — the middle (Neg) clause body's bare `put` reaches the outermost Pos
handler, whose cell is the Pos family's innermost by then.

IDENTITY is now separate from REPRESENTATION, and that split is the rest of
the change.  Every decision keyed on the family's TEXT — i32/i64/f64,
pointer-ness for the GC shadow stack, pair-ness for a `String` payload, and
which aallan#1203 write guard applies — takes `naming.family_base_name` instead: a
family that discriminates the predicate matches nothing in the `"Nat"` /
`"Int"` / `"Byte"` / `"Bool"` / `"String"` tables, so one name doing both
jobs would have switched all of them off SILENTLY while the verifier went on
recording the guards as `tier3_runtime`.  The two are derived side by side
from one type expression and carried together (`CellNames`), never
substituted for each other.  A differential pins it: the refined `@Nat` cell
emits guard-for-guard what the bare one does, and routing the clause-path
guard back onto the identity drops it from two sites to one.

The seam that made this dangerous is gone rather than repaired.  Two sites
recovered a cell family by SLICING it back out of its own mangled
`$vera.state_put_…` dispatch target — the addressability gate and the bare
`put` guard — a second derivation that worked only because `Nat`/`Int`/
`Byte` mangle to themselves, and the one aallan#1233 round 5 found re-mangling an
already-mangled name at.  Both now read `_effect_op_cells`, a fourth
registry in lock-step with `_effect_ops`, so one canonical family is
threaded to both consumers and `_state_import_family` is deleted.

Host surfaces verified rather than assumed: the browser bundle's
`/^state_get_(.+)$/` synthesis splits the 223-character mangled suffixes,
keeps the two cells apart, and returns 111 under Node — the same answer
wasmtime gives.  The WASI target gates every `State` program before its
narrower import regex, unchanged.

Corpus: `ch07_state_refined_cell_family` promotes the repro at run level
with the 111 oracle.  `alias_families/p9_refined_state_minimal` and
`p25_refined_cell` asked "does a refined cell compile, and into what
family?"; both are dispositioned into that program and the guard
differential.  Nine probe programs' family symbols rename deterministically
and one, `write_guards/p8_refined_state`, splits 4 -> 8 imports — a refined
cell nested in its own base, which was one cell and is now correctly two.
Nothing else in the corpus moves.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant