Skip to content

Route modules like the checker: one result table, clone-body registries, scoped membership (#1207, #1223, #1241, #1243, #1244, #1253) - #1272

Merged
aallan merged 9 commits into
release/v0.1.10from
fix/c1-routing
Aug 10, 2026
Merged

Route modules like the checker: one result table, clone-body registries, scoped membership (#1207, #1223, #1241, #1243, #1244, #1253)#1272
aallan merged 9 commits into
release/v0.1.10from
fix/c1-routing

Conversation

@aallan

@aallan aallan commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Part of the #1213 burndown (PR C1) — the module-routing cluster. Six commits plus the rebase resolution.

What this fixes

#1207 — one effect-op result table (3718b817). Monomorphization discovery and the WASM rewrite previously derived an effect-op's result type independently and disagreed on a get(()) array element driving a generic instantiation (pick$Nat vs pick$Int — a dangling call target, loud E602 drop). vera.slots.effect_op_result_names is now the single derivation; codegen's declared-effect-row registry, _translate_handle_state, and discovery's new scoped walk all read it. Scope is mirrored, not approximated: a handle installs its table over its body only (init and clause bodies stay in the enclosing context, per #1211's rule), and a declared-row op is never injected where a user function owns the name.

#1223 — generic-under-generic helpers instantiate their own callees (58dc60e0). The #1002 machinery instantiated the nested helper but never walked the helper's own callees, so a top-level generic called from inside the helper body went unregistered — E602 skip, E620 cascade, zero exports from check-clean verify-clean source. Hoisting and the top-level worklist are now a fixpoint over one shared leaf (collect_generic_helper_instances) that the verifier drives too; that same review surfaced and fixed the sibling case in the growing body set.

#1241 + #1243 — clone bodies route through the declaring module, both halves atomically (32f02c12). An imported generic's clone resolved its bare body calls through the IMPORTER's registries — running the importer's same-named private function (999 where the declaring module's returns 111), with type-discriminating shapes emitting invalid WASM from check-green source. The verifier's _declaring_module_scope now carries the module's function registry beside env/source/file, and codegen's clone-emission path threads _module_intra_renames the way Passes 2.5/2.6 already do. The pairing constraint is measured, not asserted: with the codegen half stashed, vera verify reports clean and the run raises the postcondition violation — so every test asserts the verify verdict AND the runtime value in one test against a standalone-module oracle, making it impossible for either half to pass alone. This closes the private / non-generic dimension only: the same routing question for an imported public generic whose name the importer also declares is a distinct hole in the clone-name space — both modules' gen2 mangle to gen2$Bool and one overwrites the other — tracked as #1274, pre-existing and byte-identical at ef7c04c0.

#1244 — every module's bodies check under its own import filter (03adbe6d). Import visibility was entry-point-dependent (E200 warn when checked directly, silence when reached as an import). Measured blast radius before implementing: ZERO — 7 corpus programs import a non-stdlib module, none of whose modules carries a standalone diagnostic. One existing expectation moved, toward strictness: the module-builtin-effect redeclaration fixture now expects exactly its standalone verdict. The change also removed a duplicate error-surfacing path (codegen-only, errors-only) in favor of one collection with the memo threaded.

#1253 — codegen's ADT membership is scoped (b711e2e9). Membership now derives from the owning namespace plus the module's own imports, public and in-filter — closing both the issue's measurement (['Array<?>', 'Array<Int>'] vs ['Array<Float>', 'Array<Int>']) and the private-sibling visibility dimension appended from PR #1254's review.

Rebase provenance

Rebased onto ef7c04c0 (post-C5) with the resolution as the visible final commit. Content integrity: diff-of-diffs over vera/ tests/ spec/ identical modulo hunk offsets across all 24 code files. The C5 interaction was probed with a purpose-built fixture (an imported forall<T> throwing a provably-negative payload, so #1268's new obligation lands inside a clone verified under this PR's pinned registries): identical verdicts on both trees, both variants. That probe surfaced a pre-existing C5 residual (the throw obligation does not survive monomorphization), recorded on #1268 — not this PR's.

Adjacent defect, filed not fixed

#1271: discovery inside a still-generic scope binds a callee's type variable to the enclosing variable's name, emitting a spurious pick$U clone that is loudly E604-skipped on every generic-under-generic program — the reason #1223's shapes live in pytest rather than conformance. Suppressing it changes the shared discovery walk with its own blast radius; queued separately.

Gates

pytest 9,917 passed / 132 skipped / 26 deselected (10,075 collected, oracle-matched); mypy clean; ruff + --select S clean; conformance 207/207; examples 42/42; corpus canonical 255/255; doc counts, site assets, diagnostic fields, limitations sync (45 rows), e602-clean, explicit encoding all green. Every commit passed the full 33-hook pre-commit gate; the rebased tree re-gated by hand end to end.

Closes #1207.
Closes #1223.
Closes #1241.
Closes #1243.
Closes #1244.
Closes #1253.

(Keywords take effect at the release PR.)

Summary by CodeRabbit

  • Bug Fixes

    • Improved module import checking with clearer, non-duplicated diagnostics.
    • Fixed ADT visibility, imported generic resolution, nested generic helpers, and State effect operations.
    • Corrected validation for throw payloads and literals against refinements.
    • Distinguished causes behind related type-checking diagnostic demotions.
  • Tests

    • Added conformance and regression coverage for modules, generics, handlers, and effect operations.
    • Expanded the suite to 10,079 tests and 207 conformance programmes.
  • Documentation

    • Updated project metrics, testing guidance, changelog, roadmap, FAQ, and known issues.

aallan and others added 7 commits August 10, 2026 13:32
…#1207)

An effect operation in a value position that fixes a generic's type
argument was answered by two independent consultors that disagreed:
monomorphization DISCOVERY had no effect-operation registry at all, so a
`get(())` driving an instantiation fell through to the literal-driven
`Int` default and emitted `pick$Int`, while the WASM call-REWRITE read
the cell's type from `_effect_op_result_vera` and called `pick$Nat`.  The
clone dangled, the caller was skipped with E602, `main` was dropped with
E620, and `vera compile` ended with "No exported functions" on a
check-green, verify-clean program.

`vera.slots.effect_op_result_names` is now the one derivation of op name
-> Vera result-type name, and all three sites that need that answer take
it from there: codegen's per-function registry built from the declared
`effects(<State<T>>)` row, the handler-expression registry inside
`_translate_handle_state`, and mono discovery's new scoped walk.

Scope is MIRRORED rather than approximated, because codegen's two
injection sites are scoped differently and a registry that ignored the
difference would move the desync rather than close it:

* a `handle` installs its table over its BODY only -- the state-init
  expression and the clause bodies belong to the enclosing context
  (#1211), so an operation written there still names the outer cell;
* an operation the DECLARED row provides is not injected when a user
  function already owns the name, which discovery now decides from the
  same function table (`_fn_sigs`, mirrored into `MonoContext.fn_names`)
  that codegen's `_effect_ops` guard consults.

The name is the alias-OPAQUE source spelling on both sides, so
`handle[State<Count>]` with `type Count = Nat` instantiates `pick$Count`;
resolving the alias on one side only would dangle exactly as before.

Test-first, both directions.  The proving check is a DIFFERENTIAL over
the two consultors, not a unit test on either -- the compiler's own E602
IS the two sides disagreeing -- and each case additionally pins WHICH
name they agreed on, so an alignment on the wrong one still fails.  RED
on the branch base: all four instantiation-driving cases failed with
"call target 'pick$Nat' / 'second$Nat' not registered in this module".
The shadowed-name control (`private fn get(@Unit -> @Bool)` in a
`State<Int>` row, whose clone must be `pick$Bool`) was green before and
after -- it guards the alignment against over-reaching rather than
re-testing the repro.

Conformance program `ch07_state_op_generic_instantiation` carries the
array-element, direct-argument and declared-effect-row forms end to end
(suite now 207); `tests/test_mono_effect_op_naming_1207.py` holds the
differential and both controls.  The `tests/probes/` `p15*` trio this
promotes is retired, per the #1213 probe-disposition workflow.

Co-Authored-By: Claude <noreply@anthropic.invalid>
A `forall<U>` `where`-helper under a `forall<T>` parent is instantiated
only during clone HOISTING -- outside the top-level worklist, which is
the loop that rescans every clone it emits.  So the helper clone's own
body was never walked, and a top-level generic called from inside the
helper was discovered only in its still-generic spelling, where the
argument's type is the enclosing type VARIABLE: `pick$U` got emitted
while the call-rewrite asked for `pick$Bool`.  On a check-clean,
verify-clean program that is an E602 skip, an E620 drop of the parent,
another of `main`, and "No exported functions".

Hoisting and the worklist are now a FIXPOINT rather than two phases:
each hoisting round re-seeds the worklist from the bodies it just
produced, and any clones that yields go back through hoisting for their
own where-trees.  The main worklist body is extracted as
`_drain_generic_worklist` so both drives are the same loop over the same
`seen` set.

The same walk had the SIBLING case, fixed here too: a helper's concrete
clone can call a sibling helper at a type only that clone knows (inside
the generic `outer<U>`, `inner(@U.1, @U.0)` binds `inner`'s variable to
the type variable's NAME), so the helper FAMILY is now discovered as a
family over a GROWING body set -- each clone fed back in -- instead of
one helper at a time against its still-generic siblings.

Both sides drive one leaf for that discovery,
`Monomorphizer.collect_generic_helper_instances`, with
`collect_clone_nested_generic_instances` re-expressed on top of it.

PAIRING.  The codegen half alone makes `pick<Bool>` an instantiation
codegen emits and the verifier never checks -- a false Tier-1 -- so the
verifier's `record_nested` grows the matching family fixpoint and feeds
each helper clone's top-level callees back into its own worklist.  The
shape is in `tests/test_monomorphize_differential.py`'s `_INLINE_CORPUS`,
whose #732 check asserts verifier >= codegen; with only the codegen half
applied it fails with

  verifier did not cover instantiation(s) codegen emits:
  [('gug_parent$where$gug_inner', ('Bool',)), ('gug_pick', ('Bool',)),
   ('gug_pick', ('U',))]

so the two halves cannot land apart.

Test-first, both directions.  RED on the branch base: six of the eight
cases in `tests/test_generic_under_generic_callees_1223.py` failed --
the user-generic, prelude-generic (`option_unwrap_or`) and two-level
nesting shapes, on both the compile-clean assertion and the
registered-vs-resolved differential.  The two that were green before and
after are the non-generic-parent control, which is what proves the
trigger is the generic ancestor rather than the nested helper.  The
differential captures the emitted mono-decl NAMES (not
`_emitted_instances`, whose nested entries are keyed by the concrete-free
lexical chain) and captures the rewrite side on `_resolve_generic_call`
(not from the WAT, which loses the dangling `call` along with the skipped
caller).

One residual is left as-is and reported rather than fixed here: the
still-generic spelling's `pick$U` clone is still emitted and still loudly
skipped (E604).  That is noise on an otherwise clean compile, not a wrong
answer, and suppressing it means filtering instantiations whose type
vector mentions an enclosing scope's type variables -- a change to the
shared walk with its own blast radius.

Co-Authored-By: Claude <noreply@anthropic.invalid>
, #1243)

A public generic in `glib` whose body calls `glib`'s private `need` is
cloned by the importer and compiled into the importer's flat module.
Both consumers of that clone body then resolved the bare name in the
IMPORTER's namespace:

* #1241, verifier -- `_scoped_fn_lookup` fell through to this program's
  registry, because `_declaring_module_scope` swapped the naming env, the
  source buffer and the file name but NOT the function registry.  The
  module's own `CalleeScope` (the #1225 contract-READING pin) is now also
  keyed by module path and carried by that scope, so a body declared in a
  module walks in the namespace it was written in.

* #1243, codegen -- the clone-emission door was the ONE door that did not
  thread `_module_intra_renames`, which Passes 2.5 and 2.6 already thread,
  so a bare sibling call landed on the importer's same-named function
  instead of the module's `mod$...` emission.

The checker's answer is the module's own (spec §8.5.1), and the
type-discriminating probe proves it: `glib`'s `need(@int -> @int)` beside
an importer's `need(@int -> @Bool)` checks clean, which only types if
glib's is meant.

MEASURED, both before and mid-fix.  At base: a module that verifies clean
and runs to 111 standalone is REFUSED through an importer (E500 on its
honest `ensures(@Int.result == 111)`, refuted against the importer's
function returning 999) and, compiled, traps on that same postcondition;
the type-discriminating pair emits invalid WASM (`expected i64, found
i32`) from check-green source.  With ONLY the verifier half applied
(measured by stashing the codegen half and re-running this suite), `vera
verify` reports clean and the run raises

  Postcondition violation in gen$Int(@int -> @int)
    ensures(@Int.result == 111) failed

-- the false Tier-1 the issue predicted, reproduced.

The commit structure makes one-half-green impossible because the TESTS
do, not because the diff is atomic: every case in
`tests/test_clone_body_declaring_module_1241_1243.py` asserts the verify
verdict AND the runtime value in the same test, so the verifier half
alone fails on the value and the codegen half alone fails on the verdict.
Every expected value is the oracle taken from the module verified and run
STANDALONE, never from what the importer produces.

Test-first, both directions.  RED at base: three cases (private callee,
two-hop private chain, type-discriminating pair).  GREEN before and
after: the standalone oracle, and the unshadowed-callee control -- which
is what pins the defect to the SHADOWED name rather than to cross-module
calls in general, so a reroute that fired unconditionally would show up
there instead of passing either way.

Co-Authored-By: Claude <noreply@anthropic.invalid>
`mid.vera` imports only `cap` from `deep` and its body also calls
`other`.  Checked directly that was an E200 -- correct, spec §8.5.1: a
module's bodies resolve in the namespace ITS file declares and imports.
Checked as a dependency of `main`, the same program was accepted in
silence, because the importer only REGISTERED each module (harvesting
what it declares) and never checked its bodies at all.  The lenient
verdict is the dangerous one: it lets a module use names it never
imported, and codegen then resolves them out of the importer's flat
namespace, so the program runs on a binding the module was never entitled
to.  The verifier has honoured the module-local rule regardless of entry
point since #1225; this is the checker catching up.

MEASUREMENT FIRST, as instructed.  Blast radius across the whole corpus
is ZERO: only 7 of the corpus programs import a non-stdlib module at all,
none of their imported modules carries a standalone diagnostic, and all
207 conformance programs and 42 examples pass unchanged.

One existing expectation moves, and toward the stricter reading.  A
module redeclaring the built-in `IO` effect and calling `IO.print(a, b)`
now reports E152 AND the E203 arity error -- exactly what `vera check` on
that module reports standalone (verified directly).  E203 names the
CANONICAL built-in's arity ("expects 1 argument(s), got 2"), so the
property the case was written for -- the rejected block is not registered
-- is now asserted directly rather than by its absence.

The same change closes the issue's second shape: a module body binding an
`@Int`-returning call to a `@Bool` slot checked clean through an
importer, verified Tier-1, and failed at compile.

It also removes a duplicate derivation rather than adding one.
`_collect_module_artifacts` already ran a full per-module check and
surfaced imported-body ERRORS -- but only on the codegen paths
(compile/run/serve/test), and warnings-only diagnostics like this one
never surfaced there either.  It now collects artifacts alone, with the
new pass's memo threaded into its sub-checkers so the added work stays
one body check per module across the whole call rather than N per module.

Test-first, both directions.  RED at base: the leaked unimported name,
the type-error-through-importer shape, and the report-once diamond.
GREEN before and after: the honest control that imports what it uses --
which is what keeps the new body check a visibility rule rather than a
blanket rejection of cross-module programs -- and both entry points into
an import cycle.  The cases assert EQUALITY between the two entry points
rather than "the importer warns", because the property is agreement: a
change making the standalone verdict lenient would satisfy a one-sided
assertion and must fail here on the standalone leg.

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

`_adt_layouts` is one map across every absorbed namespace, and the naming
environment's `data_types` set was derived from all of it -- so inside
`_module_alias_scope(blib)` a sibling module's ADTs were still members of
`blib`'s namespace, while the checker registers each module in isolation
and never sees them.  For a `blib` signature

  fn bcount(@array<Float>, @array<Int> -> @int)

with `Float` an ADT `alib` declares and `blib` never imports, the two
sides render the same declaration differently -- checker `['Array<?>',
'Array<Int>']`, codegen `['Array<Float>', 'Array<Int>']` -- which is the

Membership is now the owning namespace's own declarations plus what that
namespace IMPORTS: public only, and only the names an explicit import
list mentions, which is exactly the checker's view.  So an unimported
sibling ADT is as opaque to codegen as it is to the checker, and so is a
PRIVATE one -- the visibility dimension appended to the issue, where
codegen registers a module's private ADTs (#1008) but the checker
registers only its public ones.

Imports are read per namespace and never inherited, because §8.6.4
visibility is a property of the importer: a module reached transitively
from the entry program is a DIRECT import of whichever module names it,
and holds what THAT module's import list allows.  The active namespace
travels with the alias maps and the declaration-index space in
`_module_alias_scope`, so the three answers to "whose namespace is this?"
cannot come apart.

The permissive fallback is deliberate: with no computed membership (a
single-file program, or any point before `_register_modules` runs) the
reader takes the whole map, which is what codegen did everywhere before.
A wrongly-EMPTY membership would re-open the divergence in the other
direction, rendering a name the module does own as opaque.

Test-first, both directions.  The proving check is a DIFFERENTIAL over
the two slot tables, not an assertion on either -- both sides agreeing on
the wrong name would satisfy a one-sided check -- and each case also pins
the value the checker derives.  RED at base: the unimported-public-sibling
and private-sibling cases.  GREEN before and after: the imported positive
control (`Array<Float>` on both sides), which separates scoping the
membership from erasing cross-module ADTs, and the entry program's own
view of an ADT it imports by name.

The E609 rail is untouched and still refuses two modules declaring one
ADT name outright, so the owner-slot ordering half of the visibility case
stays masked behind it.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The module-routing cluster is closed, so its tracker rows come out of the
curated tables: #1207 (mono discovery vs effect-op result naming), #1223
(generic-under-generic callees), #1241 + #1243 (imported clone bodies
routed through the declaring module), #1244 (entry-point-dependent import
visibility), #1253 (codegen ADT membership).  The Bugs table goes from 10
rows to 4.

`ROADMAP.md`'s #1213 row drops #1207 from the remaining list; #1233
(outward cell addressing for same-family nested handlers) is what is left
of that cluster.

Each issue's CHANGELOG `[Unreleased]` entry landed with its own fix
commit, so nothing moves here but the tables.

Skip-changelog: bookkeeping only -- every issue's entry landed with its fix.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The six commits above were rebased from cd32636 onto ef7c04c.  Every
conflict was in a count/doc file; `vera/verifier.py` auto-merged.  This
commit carries the resolution work that only makes sense at the tip.

* COUNTS come from `scripts/check_doc_counts.py`, not from arithmetic:
  10,075 tests across 156 files, 207 conformance programs.  Upstream's
  10,034/151/206 and my 10,037/155/207 were both stale against the merged
  tree, so neither side's numbers were kept.
* `docs/llms-full.txt` and the rest of `docs/` are regenerated by
  `scripts/build_site.py` rather than hand-merged.
* `TESTING.md` loses a duplicate `test_nat_int_widening.py` row.  #1268
  reworded that row while this branch carried the pre-#1268 text, so a
  digit-insensitive resolution read the difference as an edit of mine and
  kept BOTH copies.  Upstream's is the current description and the live
  counts; mine goes.

Verified rather than assumed:

* CONTENT INTEGRITY -- a diff-of-diffs over `vera/ tests/ spec/`
  (excluding the count file `vera/README.md`) between
  cd32636..pre-rebase and ef7c04c..HEAD is identical modulo hunk
  offsets, across all 24 files.
* DOC MERGE -- a three-way check against the common base: every row
  either side added is present, every row either side deleted is absent,
  and a row only upstream reworded carries upstream's text.  That check
  is what found the duplicate above; the count oracle cannot see a
  description drift.
* KNOWN_ISSUES -- row-id set comparison.  Upstream deleted #1251's row
  and added #1268/#1269; this branch deletes six.  All three operations
  are applied, 45 rows, no duplicates.

C5 INTERACTION, probed rather than inferred.  #1268 obligates a `throw`
payload, and a clone containing a `throw` is now verified under this
branch's pinned module registry.  Measured on both trees with the same
fixture (an imported `forall<T>` whose body throws a provably-negative
payload into an `Exn<Nat>`): ef7c04c gives 4 Tier-1 + 1 Tier-3, and so
does this branch -- identical, with and without an importer-side shadow
of the payload function.  No movement.  The probe did surface a
PRE-EXISTING observation, present unchanged at ef7c04c and therefore not
this branch's: the same payload refutes as a loud E503 in a non-generic
function but demotes to Tier-3 inside a monomorphized clone.

Full manual gate on the rebased tree (hooks do not run on
`rebase --continue`): pytest 9917 passed / 132 skipped / 26 deselected,
mypy clean, ruff and ruff --select S clean, explicit-encoding, 207
conformance, 42 examples, 255 corpus canonical, doc counts, site assets,
diagnostic fields, limitations sync, e602-clean.

Skip-changelog: rebase resolution; every entry landed with its own fix commit.

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

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The compiler now checks imported module bodies under their own imports, scopes ADT membership per module, preserves declaring-module resolution for imported clones, and discovers nested generic instances to a fixpoint. Shared effect-operation naming and regression coverage were added. Documentation counts were updated.

Changes

Compiler correctness

Layer / File(s) Summary
Module visibility and body checking
vera/checker/*, vera/codegen/core.py, vera/codegen/modules.py, tests/test_adt_membership_scope_1253.py, tests/test_import_visibility_entry_point_1244.py, tests/test_checker_modules.py
Imported module bodies now use their own import visibility. Recursive checks share memoised state. Checker and code generator use module-scoped ADT membership.
Declaring-module clone resolution
vera/verifier.py, vera/codegen/core.py, tests/test_clone_body_declaring_module_1241_1243.py
Imported generic clones resolve bare calls through the declaring module’s function registry and compile with its rename map.
Effect-operation result naming
vera/slots.py, vera/monomorphize.py, vera/codegen/functions.py, vera/wasm/calls_handlers.py, tests/test_mono_effect_op_naming_1207.py, tests/conformance/manifest.json
effect_op_result_names provides shared State<T>.get result naming for monomorphisation, code generation, and WebAssembly handling.
Generic helper fixpoint discovery
vera/codegen/monomorphize.py, vera/monomorphize.py, vera/verifier.py, tests/test_generic_under_generic_callees_1223.py, tests/test_monomorphize_differential.py
Growing worklists discover sibling, nested, and top-level generic instances introduced by concrete helper clones.
Documentation and test inventory
AGENTS.md, CHANGELOG.md, CLAUDE.md, FAQ.md, KNOWN_ISSUES.md, README.md, ROADMAP.md, SKILL.md, TESTING.md, tests/probes/*, vera/README.md
Documentation records 207 conformance programmes, 255 corpus programmes, and 10,079 tests. Retired probes and known issues are updated.

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

Sequence Diagram(s)

sequenceDiagram
  participant SourceProgram
  participant TypeChecker
  participant ContractVerifier
  participant Monomorphizer
  participant CodeGenerator
  participant WasmHandlers
  SourceProgram->>TypeChecker: register modules and check bodies
  TypeChecker->>ContractVerifier: provide module artefacts and scopes
  ContractVerifier->>Monomorphizer: collect generic calls and effect-operation results
  Monomorphizer->>CodeGenerator: provide concrete helper instances
  CodeGenerator->>WasmHandlers: align generated effect-operation result names
Loading

Possibly related issues

  • aallan/vera#1213 — The changes centralise effect-operation result-name derivation across monomorphisation, code generation, and WebAssembly handling.

Possibly related PRs

  • aallan/vera#1013 — Shares generic where-helper scoping and monomorphisation work.
  • aallan/vera#1239 — Shares verifier module-scope and function-registry resolution work.
  • aallan/vera#1254 — Shares ADT ownership and module-scoped code-generation changes.

Suggested labels: compiler, tests, docs

🚥 Pre-merge checks | ✅ 8
✅ Passed checks (8 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 module-routing changes and identifies the affected areas and issue references.
Docstring Coverage ✅ Passed Docstring coverage is 81.94% 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 leaves vera/cli.py, vera/errors.py, spec/, vera/lsp/, and vera/codegen/api.py unchanged; CHANGELOG.md describes the changed checker diagnostics and code-generation behaviour.
Spec And Implementation Move Together ✅ Passed The PR changes no files under spec/. The vera changes correct existing rules for module visibility, effect resolution, generic monomorphisation, and cross-module compilation already stated in Chapt...
Diagnostics Carry An Error Code ✅ Passed The PR adds no Diagnostic, _error, or _warning call sites; its changed diagnostic expectation uses the registered stable code E203.
✨ 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/c1-routing

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

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.54656% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.05%. Comparing base (ef7c04c) to head (4e9b33d).

Files with missing lines Patch % Lines
vera/codegen/monomorphize.py 94.93% 4 Missing ⚠️
vera/slots.py 84.61% 2 Missing ⚠️
vera/checker/modules.py 97.29% 1 Missing ⚠️
vera/codegen/core.py 92.30% 1 Missing ⚠️
vera/codegen/modules.py 96.00% 1 Missing ⚠️
vera/monomorphize.py 96.96% 1 Missing ⚠️
vera/verifier.py 97.43% 1 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           release/v0.1.10    #1272      +/-   ##
===================================================
- Coverage            94.05%   94.05%   -0.01%     
===================================================
  Files                  100      100              
  Lines                35431    35594     +163     
  Branches               458      458              
===================================================
+ Hits                 33324    33477     +153     
- Misses                2094     2104      +10     
  Partials                13       13              
Flag Coverage Δ
javascript 78.61% <ø> (ø)
python 95.68% <95.54%> (-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: 8

Caution

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

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

1640-1650: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Recurse through imported where helpers when building fn_names. Codegen registers these helpers in _fn_sigs, but the verifier currently records only imported top-level functions. A helper named get or put can therefore be treated as an effect operation by verification but as a user function by codegen. Keep return-type inference top-level-only, but add the imported helper names recursively.

🤖 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/verifier.py` around lines 1640 - 1650, Update the `_resolved_modules`
traversal in the verifier to recursively collect names of imported `where`
helper functions into `fn_names`, including nested helpers such as `get` and
`put`. Keep `fn_ret_types` and `fn_ret_type_exprs` populated only from top-level
`ast.FnDecl` declarations, matching the existing return-type inference behavior.
🤖 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 `@AGENTS.md`:
- Line 11: Update the negative-fixture count in the conformance-suite
description to twenty-eight so it matches the 28 names enumerated in the list,
leaving the fixture names and verification requirements unchanged.

In `@TESTING.md`:
- Line 194: Update the “Running the conformance suite” text and every other
occurrence of the outdated 1,030 test count in TESTING.md to 1,035, while
preserving unrelated counts and Markdown formatting. Search the entire document
to ensure no old literal remains.

In `@tests/test_clone_body_declaring_module_1241_1243.py`:
- Around line 195-231: The _GLIB_CHAIN fixture comment incorrectly describes
nested where-helper coverage while mid and need are top-level private functions.
Either revise the comment to identify this as a two-hop private chain, or modify
the fixture so need is declared inside mid’s where block and add the
corresponding parameter wiring for the glib module.
- Around line 301-309: Complete the truncated explanatory clause in the
docstring of test_type_discriminating_callee_compiles at
tests/test_clone_body_declaring_module_1241_1243.py:301-309, stating that
routing to or calling the importer’s `@Bool` function would emit invalid WASM from
check-green source. Apply the same completed clause to the fixture comment at
tests/test_clone_body_declaring_module_1241_1243.py:144-148.
- Around line 149-173: Remove the duplicate _GLIB_TYPED fixture and reuse
_GLIB_VALUE in test_type_discriminating_callee_compiles, keeping type
discrimination solely in _APP_TYPED’s need(`@Int` -> `@Bool`) declaration. Update
the test setup or references so the glib fixture still uses the shared value
definition.
- Around line 74-89: Update _verify_codes to check proc.returncode and handle
non-JSON stdout by surfacing both stdout and stderr in the failure result,
clearly identifying which CLI output was unavailable or invalid. Also update
_run_value to safely handle successful executions with empty stdout, returning
an explicit failure marker instead of indexing an empty splitlines result.

In `@tests/test_import_visibility_entry_point_1244.py`:
- Around line 110-120: Update the `_cli` helper’s `subprocess.run` invocation to
include a finite timeout, ensuring non-terminating CLI subprocesses fail
promptly while preserving the existing command, output capture, encoding,
return-code handling, and environment setup.

In `@vera/codegen/modules.py`:
- Around line 686-690: The module membership setup around `_builtin_adt_names`
and `_adt_namespace_members` must include the conditionally injected prelude
ADTs (`Json`, `HtmlNode`, `Request`, and `Response`) in every namespace. Update
the post-`_register_modules` membership construction to track the actual
injected types so module-backed programs expose them through
`AliasEnv.data_types`, and add regression coverage for this behavior.

---

Outside diff comments:
In `@vera/verifier.py`:
- Around line 1640-1650: Update the `_resolved_modules` traversal in the
verifier to recursively collect names of imported `where` helper functions into
`fn_names`, including nested helpers such as `get` and `put`. Keep
`fn_ret_types` and `fn_ret_type_exprs` populated only from top-level
`ast.FnDecl` declarations, matching the existing return-type inference behavior.
🪄 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: cd43fd49-6743-4f21-903c-e1dc14f4f614

📥 Commits

Reviewing files that changed from the base of the PR and between ef7c04c and f4c5070.

⛔ Files ignored due to path filters (9)
  • 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_state_op_generic_instantiation.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/alias_families/p15_generic_elem.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/alias_families/p15b_append.vera is excluded by !**/*.vera
  • tests/probes/state_handlers/alias_families/p15c_direct.vera is excluded by !**/*.vera
📒 Files selected for processing (30)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • tests/conformance/manifest.json
  • tests/probes/README.md
  • tests/probes/state_handlers/README.md
  • tests/test_adt_membership_scope_1253.py
  • tests/test_checker_modules.py
  • tests/test_clone_body_declaring_module_1241_1243.py
  • tests/test_generic_under_generic_callees_1223.py
  • tests/test_import_visibility_entry_point_1244.py
  • tests/test_mono_effect_op_naming_1207.py
  • tests/test_monomorphize_differential.py
  • vera/README.md
  • vera/checker/core.py
  • vera/checker/modules.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/modules.py
  • vera/codegen/monomorphize.py
  • vera/monomorphize.py
  • vera/slots.py
  • vera/verifier.py
  • vera/wasm/calls_handlers.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 AGENTS.md Outdated
Comment thread TESTING.md
Comment thread tests/test_clone_body_declaring_module_1241_1243.py Outdated
Comment thread tests/test_clone_body_declaring_module_1241_1243.py Outdated
Comment thread tests/test_clone_body_declaring_module_1241_1243.py Outdated
Comment thread tests/test_clone_body_declaring_module_1241_1243.py
Comment thread tests/test_import_visibility_entry_point_1244.py
Comment thread vera/codegen/modules.py
@aallan

aallan commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Adversarial review record — round 1 (f4c50704)

Verdict: ship-worthy — all six fixes verified real, no regression found.

The atomic pair held every shape beyond the PR's own tests, each asserting verify verdict AND runtime value against the standalone-module oracle: transitive three-level with both privates shadowed, a diamond with both arms shadowed, a where-helper inside an imported clone, INTERLEAVED two-module verification (the registry-restore case), the importer's shadow as a where-helper and as a public fn, three instantiations of one clone — all broken at base, all correct at HEAD. Two uncovered directions were probed and are ALSO fixed: a silently-dropped precondition obligation (base reported the importer clean where the module standalone refuted E501) and the converse false rejection (the importer's stricter requires wrongly applied to the clone). _module_intra_renames changes no direct-call resolution: the 255-program corpus WAT differential shows exactly ONE mover — the newly added conformance file itself. #1244's strictness movement equals the standalone verdict everywhere probed (including one reject-vs-accept case that moved check-time-ward from a base compile failure — no overreach), warnings fire once under diamond fan-in, and the memo executes one body-check per module, verified by instrumentation. #1207's three-way agreement holds on new nesting shapes with distinct alias spellings; the four shadow carve-outs agree on both trees; seeds 0–3 byte-stable. #1223's fixpoint survives depth-3, mutual recursion, and the growing-body-set sibling, with verifier ⊇ codegen on all shapes and 19 base-failures matching the per-commit RED claims. The rebase resolution was independently recomputed: byte-identical diffs across all 24 code files.

Findings, fix round in flight:

  1. MAJOR, pre-existing, NOT closed here — filed as #1274: a shadowed PUBLIC GENERIC still misroutes silently — the generic clone namespace is unqualified, both modules' gen2$Bool collide, and _register_shadowed_import bails on every generic. False Tier-1 on the public rows of the visibility matrix; invalid WASM on the type-discriminating variant; byte-identical at base. Imported modules' private non-generic callees are not rerouted: clone bodies call the importer's same-named function #1243's close stands (its scope is the private non-generic dimension — the PR body now says so explicitly); the sibling is queued with Generic-under-generic discovery emits a spurious pick$U clone: type variables bound to the enclosing scope's NAME are instantiated and loudly E604-skipped #1271 in the follow-up that owns the clone namespace.
  2. Perf, intended trade-off — filed as #1275: Import visibility is entry-point-dependent: vera check accepts through an importer what it rejects standalone #1244 runs the pre-existing per-module harvest N times (measured N² — 2.06s at N=120 vs 0.27s base); the memo itself works exactly as claimed. The stale cost note (the LSP warm session now pays this) is corrected in the fix round.
  3. Minor: the Codegen ADT membership is global across modules: a sibling module's ADT resolves where the checker keeps the name opaque #1253 test file cannot run at base (it uses a Import visibility is entry-point-dependent: vera check accepts through an importer what it rejects standalone #1244-added method), so its RED/GREEN provenance isn't self-evidencing — the reviewer re-derived the substance independently (real fix, honest control); the fix round makes the test's base-comparison story explicit.
  4. Two code observations taken: a one-line fields()-completeness guard on the hand-enumerated HandleExpr walk, and one commit message's replace-vs-merge prose superseded by this record (both sites merge, agreeing with each other).

CodeRabbit's nine findings fold into the same round — with its modules.py injected-prelude-ADT membership finding cross-checked directly, since the review's membership surface did not specifically probe the conditionally-injected ADTs. Dispositions will be appended.

…guards

Adversarial round + CodeRabbit, folded into one round.

FIXED

* ADT membership now includes the PRELUDE-injected ADTs (CR).  `Json`,
  `HtmlNode`, `Request` and `Response` register in Pass 1.2, two passes
  after `_register_modules` computes the membership sets, so the built-in
  snapshot taken there necessarily missed them -- while the checker's
  `TypeEnv` has carried them from the start.  Global infrastructure is now
  DERIVED (every registered layout no namespace declares) instead of
  snapshotted, so it cannot go stale with registration order; the built-in
  snapshot is still unioned in, so a module declaring a built-in's name
  cannot hide it from everyone else.  RED at base on the membership
  assertion.  Measured inert at emission: the whole 255-program corpus
  compiles to BYTE-IDENTICAL WAT with and without the correction, which is
  expected -- `AliasEnv.data_types` changes an answer in exactly one place
  (`naming._resolve_named`) and only for `Decimal` and the one
  `REMOVED_ALIASES` entry, `Float`.  The test therefore asserts the SET,
  and says so.

* `tests/test_adt_membership_scope_1253.py` is base-runnable again.  It
  called `_modules_visible_to`, which THIS branch's #1244 commit adds, so
  its RED/GREEN claim was not reproducible from the artifact.  It now
  spells out the per-module resolved list the way `_collect_module_artifacts`
  has since #987.  Verified at `ef7c04c0`: 2 RED (both membership cases),
  2 GREEN (the controls) -- exactly the claim the commit makes.

* `fields()`-completeness guard on `Monomorphizer._collect_calls`'s
  `HandleExpr` arm.  That arm hand-enumerates its children because they
  are walked in DIFFERENT scopes, so it cannot use the generic recursion,
  which makes it the one place a new field would go silently unwalked --
  and an unwalked child hides every generic call inside it (a dangling
  clone with no diagnostic pointing here).  Mutation-validated: with an
  extra field on a `HandleExpr` subclass the guard fires; unmutated, it
  does not.

* The `_collect_module_artifacts` cost note was stale after #1244 and is
  corrected against what the code does now: THIS pass is still
  codegen-only and O(N^2), but a module's BODY is no longer checked only
  on those paths -- `check`, `verify` and the warm session all pay one
  sub-check per resolved module, and the session pays it again per
  re-check.  The registration-memo optimisation is cited as #1275.

* Test robustness (CR): the two-hop fixture's comment said "where helper"
  where `mid`/`need` are top-level privates; the truncated docstring and
  fixture comment now finish the sentence (routing to the importer's
  `@Bool` function emitted invalid WASM, `expected i64, found i32`, so the
  failure is a load trap rather than a wrong value); `_GLIB_TYPED` was a
  byte-identical duplicate of `_GLIB_VALUE` and is gone, with the
  discrimination kept in `_APP_TYPED`; `_verify_codes` surfaces a non-JSON
  stdout as itself instead of a bare `JSONDecodeError`, and `_run_value`
  distinguishes a zero exit that printed nothing from a value.  Finite
  300s timeouts on both files' CLI subprocesses.

* Doc counts by oracle: `TESTING.md`'s prose "1,030 parametrized tests"
  (the gated table row was already right) -> 1,035, confirmed by
  collecting `tests/test_conformance.py`.  `AGENTS.md`'s "twenty-six
  negative fixtures" -> twenty-eight, confirmed three ways: the sentence
  enumerates 28 names, the manifest carries 28 `expected_error` entries,
  and this branch's new conformance program is a `run`-level positive, so
  the figure was already stale at base.

* `KNOWN_ISSUES.md` gains the two owed rows: #1274 (the public-generic
  clone-name collision) and #1271 (the `pick$U` discovery noise), each
  citing the other as the shared clone-namespace follow-up.

SKIPPED, with the trace

* CR asked for imported WHERE-HELPER names to be collected recursively
  into `fn_names`.  Doing that would CREATE the desync it means to close.
  `fn_names` mirrors codegen's `_fn_sigs` for one decision -- is a
  declared row's `get`/`put` an op, or a user function shadowing it -- and
  #1015 hoists every imported module's helpers to parent-qualified names
  BEFORE registration.  Measured on a module whose helper is named `get`:
  codegen's `_fn_sigs` = {`outer`, `outer$where$get`}, the verifier's
  `fn_names` = {`outer`}, bare `get` absent from BOTH.  Adding it would
  suppress the State op for a row codegen still injects it for.  The
  asymmetry with LOCAL helpers (collected bare) is faithful: codegen keys
  those bare.  Recorded at the `fn_names` comment so it is not re-raised.

Full gate: pytest 9918 passed / 132 skipped / 26 deselected, mypy, ruff +
--select S, explicit-encoding, 207 conformance, 42 examples, 255 corpus
canonical, e602-clean, doc counts, site assets, diagnostic fields,
limitations sync (with --check-states, 47 rows).

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

aallan commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai On the outside-diff note for vera/verifier.py (~1640-1650, collecting imported where-helper names recursively into fn_names): verified against current code and SKIPPED — the suggested change would CREATE the desync it means to close. Since #1015, imported where-helpers are hoisted to parent-qualified names BEFORE registration: on a module whose helper is named get, codegen's _fn_sigs holds {outer, outer$where$get} and the verifier's fn_names holds {outer} — the bare name is absent from BOTH sides, which is the agreement that matters. Adding bare helper names to fn_names would suppress the State op for a row codegen still injects it for. The asymmetry with LOCAL helpers (collected bare) is faithful because codegen keys those bare. Recorded as a comment at the fn_names construction so the question isn't re-derived.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Tip

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

@aallan Confirmed. The bare imported where-helper name must remain absent from fn_names.

Imported helpers are registered with parent-qualified names, such as outer$where$get. Adding get to fn_names would suppress State-operation handling in the verifier while code generation still injects that operation. The local/imported asymmetry is therefore required for verifier and code generator agreement.

I withdraw the outside-diff finding. The recorded comment at the fn_names construction is appropriate.


✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/1272

Timestamp: 2026-08-10T14:00:17.949Z
Learning: In `vera/verifier.py`, imported `where` helpers are hoisted and registered under parent-qualified names such as `outer$where$get`. Do not add their bare helper names to verifier `fn_names`: code generation `_fn_sigs` also lacks those bare names, and adding them would suppress effect-operation classification in the verifier while code generation still injects the effect operation. Local `where` helpers differ because code generation registers their bare names.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@aallan

aallan commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Round-1 dispositions (4e31d5d7)

All ten items landed in one commit (thread replies carry the CodeRabbit details). The substantive ones: the prelude-ADT membership finding was REAL — Json was absent from every namespace's members (membership computes in Pass 0.5, prelude ADTs register in Pass 1.2) — fixed by deriving the infrastructure set rather than snapshotting names, with the honesty notes that the miss was inert at emission (255-program WAT byte-identical either way) and that the regression test deliberately does NOT assert the demand-injected ADTs. The where-helper suggestion was skipped with a mechanism trace: the #1015 hoisting means bare helper names are absent from both sides' registries, which is the agreement that matters — adding them to one side would create the desync. The #1253 test is now base-runnable (re-verified: 2 RED + 2 GREEN at ef7c04c0, making the commit's claim reproducible from the artifact); the fields() guard is mutation-validated; the cost note now states where the per-module checks actually run post-#1244; the #1274/#1271 rows landed; and the PR body carries the #1243 scoping sentence.

Gate at 4e31d5d7: pytest 9,918 / 132 / 26; conformance 207; examples 42; corpus canonical 255; doc counts (10,076/156/207); limitations sync --check-states at 47 rows; full battery green; the membership fix's corpus WAT differential shows zero movement. CI re-armed; the round-1 reviewer verifies the membership derivation next.

@aallan

aallan commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Verify pass (independent, 4e31d5d7) — five verdicts

1. Derivation boundary — PASS. The set logic traced: every resolved namespace contributes its declarations unconditionally, so a user ADT is excluded from the infrastructure set whenever ANY namespace declares it — the leak-blocking property, confirmed by a two-module counter-probe (Widget appears only in its declaring namespace, identical pre- and post-fix). The #1253 membership rule still holds post-fix on all three original cases. One incompleteness in the stated guarantee: the comment's built-in-snapshot union covers Pass-0.5 built-ins only — a module declaring data Json hides the prelude's Json from every namespace (pre-existing at both baselines, inert at emission, and no collision rail fires — the same pass-ordering asymmetry one layer down). The comment is being softened to state exactly that.

2. Inert at emission — PASS in the strongest form: 255-program WAT differential zero movers, and on a purpose-built Json-in-signatures module program the membership flips from absent-both to present-both while the emitted WAT is byte-identical — which is precisely why the regression test asserts the SET rather than a rendering.

3. The demand-injection narrowing — PASS: HtmlNode/Request/Response measured genuinely unregistered in a program that never mentions them; asserting them would have asserted that unregistered names are members.

4. The #1253 restructure — PASS for the four original cases (exact claimed RED/GREEN split at ef7c04c0, no AttributeError). The NEW fifth test is not base-runnable — it errors before its assertion at ef7c04c0, though it fails properly at the meaningful red baseline f4c50704, where it names the missing Json exactly. Being fixed in the closing micro-commit so the artifact's provenance story is uniform.

5. The fields() guard — PASS, independently mutation-validated: silent unmutated, raises with both sets named under a mutated subclass, and the same mutation passes silently at the pre-fix tip — genuinely new and load-bearing.

Nothing broken; 47 KNOWN_ISSUES rows with #1274/#1271 cross-citing; the cost note now describes what the code does. Final micro-commit in flight; merge follows it plus CI.

@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/monomorphize.py (1)

1428-1461: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Replace the handler operation registry instead of merging it.

Lines 1459-1461 retain operation result types from the enclosing scope. A handler body must use only its handled effect operation registry. The current merge can infer a generic clone from an outer operation that is not visible in the handler body.

This branch also does not exclude names in self.ctx.fn_names. A user function that shadows get can then be inferred as a State operation.

Build this registry from node.effect only, apply the same shadow filter as _row_op_result_types, and restore saved_ops afterwards. Add regressions for nested handlers with distinct operations and a user-defined get.

Proposed fix
-            self._op_result_types = {
-                **saved_ops, **effect_op_result_names([node.effect]),
-            }
+            self._op_result_types = {
+                op: result_type
+                for op, result_type in effect_op_result_names(
+                    [node.effect],
+                ).items()
+                if op not in self.ctx.fn_names
+            }
🤖 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/monomorphize.py` around lines 1428 - 1461, Update the HandleExpr branch
in _collect_calls to replace, rather than merge, _op_result_types with the
registry derived from node.effect, filtering names present in self.ctx.fn_names
consistently with _row_op_result_types. Keep the handler-body walk using only
this registry, and restore saved_ops after completing the handler scope; add
regressions covering nested handlers with distinct operations and a user-defined
get.
🤖 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/monomorphize.py`:
- Around line 1428-1461: Update the HandleExpr branch in _collect_calls to
replace, rather than merge, _op_result_types with the registry derived from
node.effect, filtering names present in self.ctx.fn_names consistently with
_row_op_result_types. Keep the handler-body walk using only this registry, and
restore saved_ops after completing the handler scope; add regressions covering
nested handlers with distinct operations and a user-defined get.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 086dd3af-b3cf-4bd7-bc59-80ea78ed6332

📥 Commits

Reviewing files that changed from the base of the PR and between f4c5070 and 4e31d5d.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (16)
  • AGENTS.md
  • CHANGELOG.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_adt_membership_scope_1253.py
  • tests/test_clone_body_declaring_module_1241_1243.py
  • tests/test_import_visibility_entry_point_1244.py
  • vera/README.md
  • vera/checker/core.py
  • vera/codegen/core.py
  • vera/codegen/modules.py
  • vera/monomorphize.py
  • vera/verifier.py
🔗 Linked repositories identified

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

  • aallan/vera-bench (manual)

…e merge

Three dispositions from the verify pass and the late CodeRabbit
outside-diff.

(1) The membership-fix comment overclaimed.  "The builtin snapshot is
unioned in, so a module declaring an ADT that shares a built-in's name
cannot hide the built-in from everyone else" holds only for the Pass-0.5
snapshot -- which does not contain the four demand-injected prelude names
this fix is about.  Reproduced: a module declaring `public data Json` puts
`Json` in the declared set (builtin_snap_has_Json=False), subtracting it
from infrastructure, and the ENTRY program's members lose the prelude
`Json`; no E609/E610 fires on that declaration either, the rails being
keyed on the same Pass-0.5 snapshot.  Inert today for the same reason the
rest of the rule is inert (`data_types` decides an answer only for
`Decimal` and `REMOVED_ALIASES`), and pre-existing at both baselines --
but a real constraint, so the comment now states what the union covers,
what it does not, and what closing it would take.

(2) `test_prelude_adts_are_members_of_every_namespace` still called the
membership helpers this fix adds, so at `ef7c04c0` it died on
`AttributeError` before reaching its assertion -- one test back down the
pattern round 1 removed.  It now reads `_alias_env.data_types` under the
pre-existing `_module_alias_scope`, which is the membership set as
consumers actually receive it, and states the invariant from the FIXTURE
(neither file declares an ADT, so every registered layout must be a
member) rather than from the implementation's own bookkeeping.

  ef7c04c (branch base)   prelude case PASSES -- no membership filter
                           exists yet, so every layout is a member and the
                           defect it pins does not exist.  The two #1253
                           slot-table cases are RED there, per their commit.
  f4c5070 (pre-fix tip)   prelude case RED on its assertion -- the
                           meaningful red baseline.  #1253 cases green.
  HEAD                     all five green.

No test in this file errors at any baseline now; each degrades to its
assertion.

(3) CR asked for the `HandleExpr` discovery branch to REPLACE the
enclosing operation registry rather than merge it.  SKIPPED -- it would
re-create #1207, measured rather than argued.  Both sites merge at HEAD
and agree: discovery writes `{**saved_ops, **effect_op_result_names(...)}`
and `_translate_handle_state` writes
`{**saved_result_vera, **effect_op_result_names(...)}`, while
`_translate_handle_exn` never touches the registry at all.  So a handler
contributing NO result type must leave the enclosing one answering.
Patched discovery to replace-semantics and compiled an `Exn` handler
nested inside a `State<Nat>` one: discovery emitted `pick$Int` where the
rewrite asked for `pick$Nat` -> E602, `main` dropped, "Available exports:
pick$Int".  That is #1207's exact signature.

CR's REGRESSION ask was legitimate even though the semantics is right:
under that patch the whole existing suite stayed green (33 passed), so
nothing pinned it.  Three cases added, and honestly only the first
discriminates merge from replace -- the other two pin adjacent semantics
CR named and pass under both:

  exn_nested_in_state      the decisive one; RED under replace
  nested_distinct_state    inner `State<Int>` owns `get` for its own body,
                           outer answers outside it (6 + 9 = 15)
  user_get_under_handler   a user `get(@Unit -> @int)` loses to the
                           operation INSIDE a handler body, matching
                           codegen's unconditional `_effect_ops` overwrite
                           (and unlike the declared-row site, where the
                           function wins -- both now pinned)

All three verified three-way beforehand: checker accepts, discovery and
the rewrite name one clone, the compiled program returns the checker's
value.  The invariant is recorded at the merge itself so the next reader
meets it there.

Gate: touched suites, mypy, ruff + --select S, doc counts, site assets,
limitations sync; full hook battery on the commit.

Skip-changelog: comment accuracy, test-harness coupling and regression pins; no behaviour change.

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

aallan commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai On the outside-diff suggestion for vera/monomorphize.py (~1428-1461, replace rather than merge the handler op registry): verified by EXPERIMENT and skipped — replace-semantics on the discovery side re-creates #1207's exact defect. The decisive shape is an Exn handler nested inside a State one: effect_op_result_names returns nothing for Exn, so under a replace patch discovery answers pick$Int against the rewrite's pick$Nat — E602, main dropped, 'Available exports: pick$Int', the disease this PR exists to close. Both sides MERGE at HEAD and agree (codegen's _translate_handle_state merges; _translate_handle_exn never touches the registry, so an Exn handler must leave the enclosing table answering); for a key the inner handler does supply, ordered merge and replace coincide (inner wins), which is why simpler shapes behave identically either way.

Your regression ask was legitimate regardless — under that replace patch the ENTIRE existing suite stayed green, so nothing pinned the semantics. Fixed in 4e9b33d with three cases: the decisive exn-nested-in-state shape (RED under replace), nested distinct State ops (inner owns its body, outer answers outside it), and the user-defined get you named — which loses to the operation inside a handler body but wins at the declared-row site, both directions now pinned, matching codegen's unconditional overwrite. Each verified three-way (checker accepts; discovery and rewrite name one clone; the compiled program returns the checker's value). The invariant is recorded at the merge site.

@aallan

aallan commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Merge gate (4e9b33dc) — closeout

Three dispositions: the membership comment now states exactly what the built-in union covers and doesn't (the prelude-shadowing asymmetry reproduced before softening — inert today, no collision rail, the same pass-ordering one layer down, with what closing it would take recorded); the prelude-membership test is decoupled from the fix's own helper and now degrades to its assertion at EVERY baseline (base passes because the filter doesn't exist there; the pre-fix tip is the meaningful RED, failing on the missing Json by name; HEAD green); and CodeRabbit's replace-vs-merge suggestion was skipped BY EXPERIMENT — the replace patch resurrects #1207's exact signature on the Exn-nested-in-State shape — while its regression ask was honored: the replace patch left the entire suite green, so three pinning cases landed, one decisive, with the user-get shadow rule pinned in both directions and the invariant recorded at the merge site.

Convergence state: adversarial fixed-point (round 1 ship-worthy → ten-item fix round → independent verify five-for-five → this gate), CodeRabbit 10/10 dispositioned with replies. Merges on the CI wall at 4e9b33dc; #1273 rebases onto the result.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Tip

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

@aallan Confirmed. I withdraw the replace-semantics suggestion.

effect_op_result_names returns no mapping for Exn. Therefore, replacing the registry at a nested Exn handler removes the enclosing State<Nat> mapping. Discovery then selects pick$Int while WASM rewriting selects pick$Nat, which recreates the #1207 failure.

The required invariant is:

  • Merge the enclosing registry with the handler registry.
  • If the inner handler defines a key, the inner mapping overwrites the outer mapping.
  • If the inner handler defines no key, retain the enclosing mapping.

The new regressions in 4e9b33dc cover the previously untested semantics. They also pin user-function and effect-operation precedence at both the handler body and declared-row sites.


✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/1272

Timestamp: 2026-08-10T14:33:52.600Z
Learning: In `vera/monomorphize.py`, handler effect-operation result registries must use ordered merge semantics. A nested `Exn` handler can produce no mapping through `effect_op_result_names`; replacing the registry would discard an enclosing `State<T>` mapping and cause generic discovery to disagree with WASM rewriting. Inner handler mappings overwrite same-name outer mappings, while absent inner mappings preserve outer mappings.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@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/codegen/core.py (1)

1293-1358: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Reject collisions with demand-injected prelude ADTs.

A module declaration such as data Json remains accepted. Line 1356 then removes Json from global infrastructure. Later, compile_program skips the injected prelude layout because Json is already in existing_adts at Lines 1523-1525 and 1572-1574. The entry namespace can therefore lose Prelude Json, while the checker still exposes it through TypeEnv.

Reserve all demand-injected prelude ADT names before module registration, or emit the existing collision diagnostic for such declarations. Add a regression that imports a module declaring Json while the entry program uses Prelude Json.

🤖 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/core.py` around lines 1293 - 1358, Reject module declarations
that collide with demand-injected prelude ADTs such as Json, HtmlNode, Request,
and Response before registration, using the existing collision diagnostic path.
Update the module-registration/prelude handling so these names remain reserved
and compile_program cannot skip their injected layouts via existing_adts. Add a
regression covering an imported module declaring Json while the entry program
uses Prelude Json.
🤖 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/codegen/core.py`:
- Around line 1293-1358: Reject module declarations that collide with
demand-injected prelude ADTs such as Json, HtmlNode, Request, and Response
before registration, using the existing collision diagnostic path. Update the
module-registration/prelude handling so these names remain reserved and
compile_program cannot skip their injected layouts via existing_adts. Add a
regression covering an imported module declaring Json while the entry program
uses Prelude Json.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54c375d3-10e4-4a99-89a9-14d281788144

📥 Commits

Reviewing files that changed from the base of the PR and between 4e31d5d and 4e9b33d.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (9)
  • FAQ.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_adt_membership_scope_1253.py
  • tests/test_mono_effect_op_naming_1207.py
  • vera/README.md
  • vera/codegen/core.py
  • vera/monomorphize.py
🔗 Linked repositories identified

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

  • aallan/vera-bench (manual)

@aallan

aallan commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai On the outside-diff suggestion to reserve the demand-injected prelude ADT names (vera/codegen/core.py ~1293-1358): verified and SKIPPED — the reservation would directly reverse ratified spec text. Spec §8.4.1's final sentence (restated in merged PR #1254 and left intact by the #1260 ruling): the prelude's data types 'are ordinary public declarations a program names, and shadows, like any other' — and the reserved namespace is the Vera prefix alone (E154). Within-namespace shadowing works today exactly as that sentence describes (a module declaring and using its own data Json checks, compiles, and runs, measured). Your precise collision scenario — an imported module declaring Json while the entry uses prelude Json — was also measured, and it already fails LOUDLY today: check-clean, then E602 inside the prelude's own json_get with an E620 cascade and zero exports, byte-identical at the branch base and both prior heads, so there is no silent-wrongness forcing a rejection rail; what's wrong there is diagnostic quality (the error points into the prelude rather than at the user's declaration). The genuine latent behind your finding — the cross-namespace member-set subtraction, inert at emission — is now tracked as #1277 with the full measurement, the fix direction (per-namespace subtraction or a complete Pass-0.5 snapshot, not reservation), and the diagnostic-quality case recorded on the same issue. No commit; the branch stands at 4e9b33d.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Tip

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

@aallan Confirmed. I withdraw the reservation suggestion.

Vera Specification §8.4.1 permits a program to name and shadow prelude data types. Reserving demand-injected names such as Json would violate that rule. E154 reserves only the Vera prefix.

The measured cross-namespace collision fails loudly with E602 and E620. It does not justify a rejection rail that changes valid shadowing semantics.

The remaining member-set subtraction issue is separate from name reservation. #1277 is the correct follow-up for a per-namespace subtraction or a complete Pass-0.5 snapshot, plus improved diagnostic attribution.


✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/1272

Timestamp: 2026-08-10T14:45:08.661Z
Learning: In Vera code generation, demand-injected prelude ADT names such as `Json` must not be globally reserved. Vera Specification §8.4.1 defines prelude data types as ordinary public declarations that a program can name and shadow; E154 reserves only the `Vera` prefix. Cross-namespace prelude/user-ADT membership handling is a separate issue and should be fixed through per-namespace membership subtraction or a complete early snapshot, not by rejecting shadowing.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@aallan
aallan merged commit 9d83df1 into release/v0.1.10 Aug 10, 2026
26 checks passed
@aallan
aallan deleted the fix/c1-routing branch August 10, 2026 14:47
aallan added a commit that referenced this pull request Aug 10, 2026
Rebased onto 9d83df1 (PR #1272).  Only doc and count files conflicted;
vera/codegen/functions.py and vera/wasm/calls_handlers.py — the two
sources both PRs touch — auto-merged, and the boundary suite, the
cross-module parity test and the nat-guard suite all stay green, so the
_boundary_base consumers and the CellNames threading survive C1's
monomorphization and routing work intact.

Conflict resolutions, all mechanical:

  CHANGELOG [Unreleased] — union.  C1's five bullets and this branch's
  four, none dropped, none reworded.  The last hunk was NOT a union: it
  carried C1's five plus a STALE copy of this branch's own bullet
  against the updated copy, so a blind union would have duplicated it;
  resolved to C1's five plus the updated one, verified single.

  KNOWN_ISSUES — both sides' row operations applied by ROW ID, never by
  position.  C1 deleted #1207/#1223/#1241/#1243/#1244/#1253 and added
  #1271/#1274; this branch deleted #1255/#1256/#1269 and adds #1276.
  Verified by set comparison over the whole file: every deletion absent,
  every addition present, no duplicates.

  Count-bearing prose — structure from upstream (it carries C1's 207
  conformance programs and 157 test files), every NUMBER re-derived by
  scripts/check_doc_counts.py against the merged tree rather than
  merged from either side: 10,138 tests across 157 files, 9,980 passed
  + 26 stress + 132 skipped.

  docs/* — regenerated by build_site.py, never hand-merged.

Content integrity: the diff over vera/ and tests/ (excluding the
count-bearing vera/README.md paragraph) is identical before and after
the rebase, modulo hunk offsets.

Full manual gate re-run, hooks not having fired on rebase --continue:
pytest 9,980 passed / 132 skipped, mypy clean, ruff and ruff --select S
clean, 207 conformance, 42 examples, 255 corpus programs canonical, doc
counts consistent, site assets coherent, diagnostic fields, limitations
sync and explicit encoding all OK.

Skip-changelog: rebase conflict resolution over count-bearing prose; no behaviour change

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