fix(mono): cross-module + closure-body generic monomorphization, in verifier↔codegen lockstep (#774, #873)#888
fix(mono): cross-module + closure-body generic monomorphization, in verifier↔codegen lockstep (#774, #873)#888aallan wants to merge 1 commit into
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 47 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (7)
📒 Files selected for processing (21)
📝 WalkthroughWalkthroughThis PR adds cross-module generic monomorphisation and closure-body generic handling, with matching verifier discovery, WASM resolution, tests, and updated documentation counts. ChangesCross-module and closure generic monomorphisation fix
Documentation updates for suite counts and changelog
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 8✅ Passed checks (8 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #888 +/- ##
==========================================
+ Coverage 93.20% 93.24% +0.03%
==========================================
Files 95 95
Lines 29647 29925 +278
Branches 438 438
==========================================
+ Hits 27633 27904 +271
- Misses 2001 2008 +7
Partials 13 13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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 `@tests/conformance/manifest.json`:
- Around line 1470-1484: Update the ch09_generic_in_closure manifest entry so
its spec_ref matches the generic-in-closure behavior being tested; this item is
about monomorphization inside a closure body, not cross-module compilation.
Adjust the spec_ref field in the manifest entry for ch09_generic_in_closure to
reference the relevant monomorphisation/closure sections such as Section 11.2
and/or Section 11.10, using the ch09_generic_in_closure record as the anchor.
In `@tests/test_monomorphize_differential.py`:
- Around line 303-379: The new shadowed-generic regression test is correct, but
it duplicates the inner _resolved() helper already used by
test_imported_generic_symmetric_between_codegen_and_verifier. Hoist that helper
to a shared module-level utility (or reuse the existing one) and update both
tests to call the shared helper, keeping the test logic in
test_shadowed_imported_generic_symmetric_between_codegen_and_verifier unchanged.
In `@vera/codegen/monomorphize.py`:
- Around line 212-215: The shadowed-import clone path in
_monomorphize_shadowed_module_generics currently emits mod$…$gen$… clones
without performing the same transitive generic scan used in the normal
clone-emission flow, so dependent helper clones can be missed. Update this path
to reuse the existing worklist/drain logic from the main monomorphization loop,
or factor that loop into a shared helper and call it from both places. Make sure
the emitted clones are scanned for nested generic calls before finalizing
mono_decls.
In `@vera/wasm/context.py`:
- Around line 565-570: The ModuleCall target resolution is inconsistent between
translation and result-shape checks, so shadowed generic calls can be classified
differently than they are emitted. Add and reuse the shared resolver (for
example, self._resolve_module_call_target(expr)) in the ast.ModuleCall branches
of _is_void_expr and _is_pair_result_expr, matching the logic already used when
resolving targets in the context code. This keeps m::gen(...) and similar calls
aligned across drop/void handling and translation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c0e784ff-519f-47be-a8b4-04d3bfb3e1a6
⛔ Files ignored due to path filters (7)
docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch08_cross_module_generic.verais excluded by!**/*.veratests/conformance/ch08_cross_module_generic_lib.verais excluded by!**/*.veratests/conformance/ch09_generic_in_closure.verais excluded by!**/*.vera
📒 Files selected for processing (20)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdKNOWN_ISSUES.mdROADMAP.mdTESTING.mdspec/11-compilation.mdtests/conformance/manifest.jsontests/test_codegen_closures.pytests/test_codegen_modules.pytests/test_monomorphize_differential.pyvera/codegen/closures.pyvera/codegen/core.pyvera/codegen/functions.pyvera/codegen/modules.pyvera/codegen/monomorphize.pyvera/monomorphize.pyvera/verifier.pyvera/wasm/context.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
…, #873) An imported generic instantiated only by the importer was monomorphized nowhere — check passed but run failed WASM validation at `call $gid`, for both the bare and module-qualified call forms (#774). The importer now harvests imported public generics, merges the unshadowed ones into its `generic_decls` (so discovery + emission + the call-site rewrite cover them), and the shared discovery walks `ModuleCall` targets. The clone is owned by the importer and carries the usual Tier-3 runtime postcondition guard. The #814 asymmetric variant — a generic shadowed by a local non-generic and module-qualified-called — previously false-Tier-1'd; it is now monomorphized under a distinct `mod$<path>$gen$…` name reached only by the qualified call, bringing codegen into agreement with the verifier. A generic whose only call site is inside a closure body failed run with `unknown func` (#873): the lifted closure body was compiled without `generic_fn_info`. `_compile_lifted_closure` now threads `generic_fn_info` + `known_fns`. Two completeness gaps one level deeper in the shadowed path (PR #888 review): (1) a shadowed generic whose body calls another generic now gets that transitive clone emitted — an unshadowed sibling (a normal clone) or a same-module shadowed sibling (its own `mod$…` clone reached by rewriting the intra-module bare call) — on both codegen and the verifier, so the #732 differential stays a true equality (a codegen-only transitive fix would be a new false Tier-1); (2) the statement-position result-shape predicates (`_is_void_expr` / `_is_pair_result_expr`) now share one ModuleCall target resolver with the desugar, so a shadowed generic returning `@String`/`@Unit` drops the correct stack shape. Codegen and verifier discovery move in lockstep — proved by the #732 differential (flipped to pin the new "both monomorphize" symmetry, parametrized over both call forms, plus shadowed and transitive-shadowed variants). Mutation-validated eight ways. Intake (pre-existing, orthogonal, filed): transitive module import main->mid->base `unknown func` (#890); generic over a parameterized ADT `expected i64 found i32` in the clone (#891). Run-level conformance programs `ch08_cross_module_generic` and `ch09_generic_in_closure`; spec §11.16 documents importer-owned cross-module monomorphization. Co-Authored-By: Claude <[email protected]>
c13f6d3 to
4063b30
Compare
…, #873) An imported generic instantiated only by the importer was monomorphized nowhere — check passed but run failed WASM validation at `call $gid`, for both the bare and module-qualified call forms (#774). The importer now harvests imported public generics, merges the unshadowed ones into its `generic_decls` (so discovery + emission + the call-site rewrite cover them), and the shared discovery walks `ModuleCall` targets. The clone is owned by the importer and carries the usual Tier-3 runtime postcondition guard. The #814 asymmetric variant — a generic shadowed by a local non-generic and module-qualified-called — previously false-Tier-1'd; it is now monomorphized under a distinct `mod$<path>$gen$…` name reached only by the qualified call, bringing codegen into agreement with the verifier. A generic whose only call site is inside a closure body failed run with `unknown func` (#873): the lifted closure body was compiled without `generic_fn_info`. `_compile_lifted_closure` now threads `generic_fn_info` + `known_fns`. Two completeness gaps one level deeper in the shadowed path (PR #888 review): (1) a shadowed generic whose body calls another generic now gets that transitive clone emitted — an unshadowed sibling (a normal clone) or a same-module shadowed sibling (its own `mod$…` clone reached by rewriting the intra-module bare call) — on both codegen and the verifier, so the #732 differential stays a true equality (a codegen-only transitive fix would be a new false Tier-1); (2) the statement-position result-shape predicates (`_is_void_expr` / `_is_pair_result_expr`) now share one ModuleCall target resolver with the desugar, so a shadowed generic returning `@String`/`@Unit` drops the correct stack shape. Codegen and verifier discovery move in lockstep — proved by the #732 differential (flipped to pin the new "both monomorphize" symmetry, parametrized over both call forms, plus shadowed and transitive-shadowed variants). Mutation-validated eight ways. Intake (pre-existing, orthogonal, filed): transitive module import main->mid->base `unknown func` (#890); generic over a parameterized ADT `expected i64 found i32` in the clone (#891). Run-level conformance programs `ch08_cross_module_generic` and `ch09_generic_in_closure`; spec §11.16 documents importer-owned cross-module monomorphization. Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_codegen_modules.py`:
- Around line 742-1084: The suite is missing a regression for the opposite
direction: an unshadowed generic whose body makes a qualified call into a
shadowed sibling generic. Add a test under TestCrossModuleGenerics774 that uses
_run_mod with a normal exported generic calling a shadowed module generic
through the qualified form, and assert both verify/run behavior and the expected
result. Make the setup mirror the existing shadowed-call cases but flip the
caller/callee roles so _collect_shadowed_qualified_instances is exercised for an
unshadowed caller reaching a shadowed target. Include a bare-call control if
needed to confirm only the qualified path is affected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 24150935-fdf6-4226-92fa-c15457d8e5b1
⛔ Files ignored due to path filters (7)
docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch08_cross_module_generic.verais excluded by!**/*.veratests/conformance/ch08_cross_module_generic_lib.verais excluded by!**/*.veratests/conformance/ch09_generic_in_closure.verais excluded by!**/*.vera
📒 Files selected for processing (20)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdKNOWN_ISSUES.mdROADMAP.mdTESTING.mdspec/11-compilation.mdtests/conformance/manifest.jsontests/test_codegen_closures.pytests/test_codegen_modules.pytests/test_monomorphize_differential.pyvera/codegen/closures.pyvera/codegen/core.pyvera/codegen/functions.pyvera/codegen/modules.pyvera/codegen/monomorphize.pyvera/monomorphize.pyvera/verifier.pyvera/wasm/context.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
aallan/vera-bench(manual)
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@FAQ.md`:
- Line 177: The conformance counts in FAQ.md are inconsistent, so update all
remaining references to match the current 112-program suite. Check the wording
around the Vera overview and the lower FAQ entries that mention the conformance
suite totals, including the spots referring to the old 108 programs, eight
check-level programs, and 25 skipped conformance-stage tests, and make them all
match the updated numbers consistently.
In `@TESTING.md`:
- Around line 9-11: The conformance counts in the overview and prose are out of
sync, so update the stale literals in TESTING.md to match the new totals. In the
conformance-suite section and related summary text, replace the old 108-program,
eight check-level, and 25 skipped conformance-stage references with the current
values, and verify the surrounding wording in the conformance-suite and
skipped-tests descriptions stays consistent with the table and other count
references.
In `@vera/verifier.py`:
- Around line 1088-1100: The shadowing logic in verifier.py is mixing imported
generic instances into local generic keys because shadowed is tracked by bare
function name. Update the shadowing collection in the imported generic-instance
handling so a local forall fn only absorbs matching instances when the local
shadow is non-generic, or alternatively key shadowed by module path plus name.
Use the existing symbols shadowed, _resolved_modules, _import_names,
local_fn_names, and _verify_fn to keep the imported m::gen instantiations from
being attached to the local gen entry.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2d5a4aca-794f-4900-880f-88026c799ea9
⛔ Files ignored due to path filters (7)
docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**tests/conformance/ch08_cross_module_generic.verais excluded by!**/*.veratests/conformance/ch08_cross_module_generic_lib.verais excluded by!**/*.veratests/conformance/ch09_generic_in_closure.verais excluded by!**/*.vera
📒 Files selected for processing (20)
AGENTS.mdCHANGELOG.mdCLAUDE.mdFAQ.mdKNOWN_ISSUES.mdROADMAP.mdTESTING.mdspec/11-compilation.mdtests/conformance/manifest.jsontests/test_codegen_closures.pytests/test_codegen_modules.pytests/test_monomorphize_differential.pyvera/codegen/closures.pyvera/codegen/core.pyvera/codegen/functions.pyvera/codegen/modules.pyvera/codegen/monomorphize.pyvera/monomorphize.pyvera/verifier.pyvera/wasm/context.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
aallan/vera-bench(manual)
…, #873) An imported generic instantiated only by the importer was monomorphized nowhere — check passed but run failed WASM validation at `call $gid`, for both the bare and module-qualified call forms (#774). The importer now harvests imported public generics, merges the unshadowed ones into its `generic_decls`, and the shared discovery walks `ModuleCall` targets. The #814 asymmetric variant — a generic shadowed by a local non-generic and module-qualified-called — is now monomorphized under a distinct `mod$<path>$gen$…` name reached only by the qualified call. A generic whose only call site is inside a closure body failed run with `unknown func` (#873): `_compile_lifted_closure` now threads `generic_fn_info` + `known_fns`. Completeness gaps closed one level deeper (PR #888 review): a shadowed generic calling another generic emits the transitive clone (both sides); the statement-position result-shape predicates share one ModuleCall target resolver with the desugar; an unshadowed generic calling a shadowed sibling has its shadowed clone discovered by scanning the emitted normal clones. Soundness keystone (CR 3519156263): an imported generic's clone RUNS in the importer but its body lives in another module, so a LYING module contract ran unverified — a false Tier-1 (verify clean, run traps the clone's postcondition). Worse, a same-named LOCAL generic absorbed the module call's instantiation into its bare key, proving the local's contract in the module clone's place. The importer now records every shadowed module clone under its `mod$…` base (never the bare name a local generic owns), and verifies each imported generic's clone (shadowed and unshadowed) at its own instantiations, turning a lying module contract into an honest E500. The warm incremental session runs the same verification so warm == cold. Codegen and verifier discovery move in lockstep — proved by the #732 differential (namespaced keys distinguish module clone from same-named local generic). Mutation-validated: disable the imported-generic verify pass → lying-contract tests stop catching E500; desync either side's mod$… key → differential flips; disable either side's normal-clone scan → the unshadowed-caller case flips. Run-level conformance programs `ch08_cross_module_generic` and `ch09_generic_in_closure`; spec §11.16. Co-Authored-By: Claude <[email protected]>
4063b30 to
24fca62
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
…, #873) An imported generic instantiated only by the importer was monomorphized nowhere — check passed but run failed WASM validation at `call $gid`, for both the bare and module-qualified call forms (#774). The importer now harvests imported public generics, merges the unshadowed ones into its `generic_decls`, and the shared discovery walks `ModuleCall` targets. The module-qualified-called — is now monomorphized under a distinct `mod$<path>$gen$…` name reached only by the qualified call. A generic whose only call site is inside a closure body failed run with `unknown func` (#873): `_compile_lifted_closure` now threads `generic_fn_info` + `known_fns`. Completeness gaps closed one level deeper (PR #888 review): a shadowed generic calling another generic emits the transitive clone (both sides); the statement-position result-shape predicates share one ModuleCall target resolver with the desugar; an unshadowed generic calling a shadowed sibling has its shadowed clone discovered by scanning the emitted normal clones. Soundness keystone (CR 3519156263): an imported generic's clone RUNS in the importer but its body lives in another module, so a LYING module contract ran unverified — a false Tier-1 (verify clean, run traps the clone's postcondition). Worse, a same-named LOCAL generic absorbed the module call's instantiation into its bare key, proving the local's contract in the module clone's place. The importer now records every shadowed module clone under its `mod$…` base (never the bare name a local generic owns), and verifies each imported generic's clone (shadowed and unshadowed) at its own instantiations, turning a lying module contract into an honest E500. The warm incremental session runs the same verification so warm == cold. Codegen and verifier discovery move in lockstep — proved by the #732 differential (namespaced keys distinguish module clone from same-named local generic). Mutation-validated: disable the imported-generic verify pass → lying-contract tests stop catching E500; desync either side's mod$… key → differential flips; disable either side's normal-clone scan → the unshadowed-caller case flips. Run-level conformance programs `ch08_cross_module_generic` and `ch09_generic_in_closure`; spec §11.16. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai review |
24fca62 to
0947e50
Compare
✅ Action performedReview finished.
|
|
Independent orchestrator soundness verification (post-rebase, head 0947e50) — the false-Tier-1 differential on a lying imported generic:
Pre-fix this was |
… mono clone (#891) A generic instantiated over a parameterized ADT — `gid(MkBox(7))` for `gid : forall<T> fn(@t -> @t)` and `data Box<T> { MkBox(T) }` — passed `vera check`/`vera verify` but trapped at `vera run` with `type mismatch: expected i64, found i32` while compiling the `gid$Box` clone. Root cause: `_infer_expr_wasm_type`'s `ResultRef` arm (`vera/wasm/inference.py`) inferred only the scalar primitives and returned `None` for everything else. Once `T = Box`, the clone's postcondition `@T.result == @T.0` compares two i32 heap pointers, but the `ResultRef` (`@Box.result`) defaulted to the scalar i64, so the equality fell through to the `i64.eq` default while the operands were i32 — an ill-typed clone body wasmtime rejects at instantiation. The `SlotRef` operand (`@Box.0`) and the clone's `param`/`result` signature were already i32; only the postcondition op disagreed. Fix: the `ResultRef` and `SlotRef` arms now share one `_ref_type_name_wasm_type` helper, so an ADT-bound (and pair-/handle-bound) type variable lowers to i32 consistently across the signature and every slot/result read. New conformance program `ch02_generic_over_param_adt` (run level; suite now 112) plus a regression test in `tests/test_codegen_monomorphize.py` pin `gid(MkBox(42))` unwrapping to 42. Mutation-validated: reverting the `ResultRef` arm to the pre-fix scalar-only logic restores the trap, flipping the test RED. Pre-existing on main; found by the PR #888 soundness review. Co-Authored-By: Claude <[email protected]>
… mono clone (#891) A generic instantiated over a parameterized ADT — `gid(MkBox(7))` for `gid : forall<T> fn(@t -> @t)` and `data Box<T> { MkBox(T) }` — passed `vera check`/`vera verify` but trapped at `vera run` with `type mismatch: expected i64, found i32` while compiling the `gid$Box` clone. Root cause: `_infer_expr_wasm_type`'s `ResultRef` arm (`vera/wasm/inference.py`) inferred only the scalar primitives and returned `None` for everything else. Once `T = Box`, the clone's postcondition `@T.result == @T.0` compares two i32 heap pointers, but the `ResultRef` (`@Box.result`) defaulted to the scalar i64, so the equality fell through to the `i64.eq` default while the operands were i32 — an ill-typed clone body wasmtime rejects at instantiation. The `SlotRef` operand (`@Box.0`) and the clone's `param`/`result` signature were already i32; only the postcondition op disagreed. Fix: the `ResultRef` and `SlotRef` arms now share one `_ref_type_name_wasm_type` helper, so an ADT-bound (and pair-/handle-bound) type variable lowers to i32 consistently across the signature and every slot/result read. New conformance program `ch02_generic_over_param_adt` (run level; suite now 112) plus a regression test in `tests/test_codegen_monomorphize.py` pin `gid(MkBox(42))` unwrapping to 42. Mutation-validated: reverting the `ResultRef` arm to the pre-fix scalar-only logic restores the trap, flipping the test RED. Pre-existing on main; found by the PR #888 soundness review. Co-Authored-By: Claude <[email protected]>
… mono clone (#891) A generic instantiated over a parameterized ADT — `gid(MkBox(7))` for `gid : forall<T> fn(@t -> @t)` and `data Box<T> { MkBox(T) }` — passed `vera check`/`vera verify` but trapped at `vera run` with `type mismatch: expected i64, found i32` while compiling the `gid$Box` clone. Root cause: `_infer_expr_wasm_type`'s `ResultRef` arm (`vera/wasm/inference.py`) inferred only the scalar primitives and returned `None` for everything else. Once `T = Box`, the clone's postcondition `@T.result == @T.0` compares two i32 heap pointers, but the `ResultRef` (`@Box.result`) defaulted to the scalar i64, so the equality fell through to the `i64.eq` default while the operands were i32 — an ill-typed clone body wasmtime rejects at instantiation. The `SlotRef` operand (`@Box.0`) and the clone's `param`/`result` signature were already i32; only the postcondition op disagreed. Fix: the `ResultRef` and `SlotRef` arms now share one `_ref_type_name_wasm_type` helper, so an ADT-bound (and pair-/handle-bound) type variable lowers to i32 consistently across the signature and every slot/result read. New conformance program `ch02_generic_over_param_adt` (run level; suite now 112) plus a regression test in `tests/test_codegen_monomorphize.py` pin `gid(MkBox(42))` unwrapping to 42. Mutation-validated: reverting the `ResultRef` arm to the pre-fix scalar-only logic restores the trap, flipping the test RED. Pre-existing on main; found by the PR #888 soundness review. Co-Authored-By: Claude <[email protected]>
…rence (#878) A generic whose type argument must be inferred from a user-fn call's return type in argument position monomorphized to the Bool phantom-var default: on a check-green program `vera run` emitted a call to a `$Bool` clone Pass 1.5 never produced, so the caller (often `main`) was skipped and dropped from the exports ("No exported functions to call"), or a private helper left a dangling `call` that failed WAT validation. Root cause is the lossy WAT collapse `i32 -> "Bool"` (a Decimal handle, an ADT, and an Option/Result pointer are all i32 — the same value as the phantom default) applied to user-fn returns on both the discovery and the body-emission sides: - Discovery (vera/codegen/monomorphize.py): `_build_mono_context` seeded the shared `fn_ret_types` from WAT signatures, so a generic bound solely by a user-fn return was discovered as `$Bool` while the #732 verifier discovered the precise type — a differential desync masked only by identity clone bodies. Now seeds from each fn's declared return TypeExpr, exactly as the verifier does, with the WAT collapse kept only as a fallback. - Body-emission (vera/wasm/inference.py): `_get_arg_type_info_wasm` had no FnCall branch, so a parameterized return in `Option<VeraT>` position bound nothing; and a user-fn call in bare `@VeraT` position collapsed i32 to Bool. It now mirrors discovery's FnCall branch (shared _BUILTIN_PARAMETERIZED_RETURNS plus a non-generic-user-fn parameterized case) and consults the declared return TypeExpr for the ambiguous i32 case. GENERIC calls are excluded from the user-fn branch (their declared return is over the callee's own type vars, not concrete types) so they fall through to generic-return resolution exactly as discovery does — keeping the two consultors in lockstep. Test values are deliberately Decimal returns (`0.3333...`) that cannot coincide with the Bool default (1/0), the exact Bool-coincidence trap CLAUDE.md warns about. New run-level conformance program ch09_generic_infer_user_fn_return (suite 123) plus three regression tests (crash-via-IO, WAT-mangle discriminator, discovery-vs-verifier differential); each change is independently mutation- validated. Does not weaken #888's cross-module/closure discovery. Co-Authored-By: Claude <[email protected]>
…rence (#878) A generic whose type argument must be inferred from a user-fn call's return type in argument position monomorphized to the Bool phantom-var default: on a check-green program `vera run` emitted a call to a `$Bool` clone Pass 1.5 never produced, so the caller (often `main`) was skipped and dropped from the exports ("No exported functions to call"), or a private helper left a dangling `call` that failed WAT validation. Root cause is the lossy WAT collapse `i32 -> "Bool"` (a Decimal handle, an ADT, and an Option/Result pointer are all i32 — the same value as the phantom default) applied to user-fn returns on both the discovery and the body-emission sides: - Discovery (vera/codegen/monomorphize.py): `_build_mono_context` seeded the shared `fn_ret_types` from WAT signatures, so a generic bound solely by a user-fn return was discovered as `$Bool` while the #732 verifier discovered the precise type — a differential desync masked only by identity clone bodies. Now seeds from each fn's declared return TypeExpr, exactly as the verifier does, with the WAT collapse kept only as a fallback. - Body-emission (vera/wasm/inference.py): `_get_arg_type_info_wasm` had no FnCall branch, so a parameterized return in `Option<VeraT>` position bound nothing; and a user-fn call in bare `@VeraT` position collapsed i32 to Bool. It now mirrors discovery's FnCall branch (shared _BUILTIN_PARAMETERIZED_RETURNS plus a non-generic-user-fn parameterized case) and consults the declared return TypeExpr for the ambiguous i32 case. GENERIC calls are excluded from the user-fn branch (their declared return is over the callee's own type vars, not concrete types) so they fall through to generic-return resolution exactly as discovery does — keeping the two consultors in lockstep. Test values are deliberately Decimal returns (`0.3333...`) that cannot coincide with the Bool default (1/0), the exact Bool-coincidence trap CLAUDE.md warns about. New run-level conformance program ch09_generic_infer_user_fn_return (suite 123) plus three regression tests (crash-via-IO, WAT-mangle discriminator, discovery-vs-verifier differential); each change is independently mutation- validated. Does not weaken #888's cross-module/closure discovery. Co-Authored-By: Claude <[email protected]>
…rence (#878) A generic whose type argument must be inferred from a user-fn call's return type in argument position monomorphized to the Bool phantom-var default: on a check-green program `vera run` emitted a call to a `$Bool` clone Pass 1.5 never produced, so the caller (often `main`) was skipped and dropped from the exports ("No exported functions to call"), or a private helper left a dangling `call` that failed WAT validation. Root cause is the lossy WAT collapse `i32 -> "Bool"` (a Decimal handle, an ADT, and an Option/Result pointer are all i32 — the same value as the phantom default) applied to user-fn returns on both the discovery and the body-emission sides: - Discovery (vera/codegen/monomorphize.py): `_build_mono_context` seeded the shared `fn_ret_types` from WAT signatures, so a generic bound solely by a user-fn return was discovered as `$Bool` while the #732 verifier discovered the precise type — a differential desync masked only by identity clone bodies. Now seeds from each fn's declared return TypeExpr, exactly as the verifier does, with the WAT collapse kept only as a fallback. - Body-emission (vera/wasm/inference.py): `_get_arg_type_info_wasm` had no FnCall branch, so a parameterized return in `Option<VeraT>` position bound nothing; and a user-fn call in bare `@VeraT` position collapsed i32 to Bool. It now mirrors discovery's FnCall branch (shared _BUILTIN_PARAMETERIZED_RETURNS plus a non-generic-user-fn parameterized case) and consults the declared return TypeExpr for the ambiguous i32 case. GENERIC calls are excluded from the user-fn branch (their declared return is over the callee's own type vars, not concrete types) so they fall through to generic-return resolution exactly as discovery does — keeping the two consultors in lockstep. Test values are deliberately Decimal returns (`0.3333...`) that cannot coincide with the Bool default (1/0), the exact Bool-coincidence trap CLAUDE.md warns about. New run-level conformance program ch09_generic_infer_user_fn_return (suite 123) plus three regression tests (crash-via-IO, WAT-mangle discriminator, discovery-vs-verifier differential); each change is independently mutation- validated. Does not weaken #888's cross-module/closure discovery. Co-Authored-By: Claude <[email protected]>
Summary
Two monomorphization-discovery gaps that produced check/verify-green programs failing at run — and, in one variant, a false Tier-1. Fixed together (shared discovery surface), with codegen and verifier discovery moving in lockstep (the #732 differential is the proof).
#774 — a generic imported and instantiated only by the importer was monomorphized nowhere (
unknown func $gidat run, bothgid(42)andgenmod::gid(42)). Plus the #814-review variant: an imported generic shadowed by a local and module-qualified-called (g::gen(5)) false-Tier-1'd —vera verifyprovedensures(result==5)against the module generic's contract whilevera runexecuted the local shadow and returned 105.#873 — a generic called only inside a closure body was never rewritten (
unknown func $are_equalat run); discovery already walked the closure body, but the lifted closure didn't rewrite the call.Design
ast.ModuleCallnow walked by the shared discovery), emitted into its flat WASM module, carrying the same Tier-3 runtime postcondition guard the defining module's uninstantiated-generic fallback emits (sound at run)._generic_fn_info(bare + qualified); shadowed ones emit undermod$<path>$gen$…reached only by the qualified call (_module_qualified_generic_bases), so the bare name stays on the local shadow (§8.5.2)._imported_generic_decls+_collect_shadowed_qualified_instances) — lockstep, proven by the Per-monomorphization static verification for generic functions #732 differential (flipped from pinning symmetric "neither monomorphizes" to symmetric "both monomorphize", + a shadowed-variant differential).Evidence
unknown func $gid; Module name resolution: qualified calls don't bypass local shadows (codegen), and verifier↔codegen disagree on bare-call shadowing #814 shadow variant verify-OK + runPostcondition violation(returned 105); A local generic called only from inside a closure body is never monomorphized: 'unknown func' at run on a check-green program #873unknown func $are_equal.g::gen(5)returns 105.--select S) clean.Docs
CHANGELOG
[Unreleased](one bullet per issue); #774 and #873 KNOWN_ISSUES Bugs rows removed; spec §11.16; conformance programsch08_cross_module_generic{,_lib},ch09_generic_in_closure. No version bump.Closes #774
Closes #873
🤖 Generated with Claude Code
Summary by CodeRabbit