Skip to content

fix(mono): cross-module + closure-body generic monomorphization, in verifier↔codegen lockstep (#774, #873)#888

Closed
aallan wants to merge 1 commit into
mainfrom
fix/774-873-mono-discovery
Closed

fix(mono): cross-module + closure-body generic monomorphization, in verifier↔codegen lockstep (#774, #873)#888
aallan wants to merge 1 commit into
mainfrom
fix/774-873-mono-discovery

Conversation

@aallan

@aallan aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner

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 $gid at run, both gid(42) and genmod::gid(42)). Plus the #814-review variant: an imported generic shadowed by a local and module-qualified-called (g::gen(5)) false-Tier-1'dvera verify proved ensures(result==5) against the module generic's contract while vera run executed the local shadow and returned 105.

#873 — a generic called only inside a closure body was never rewritten (unknown func $are_equal at run); discovery already walked the closure body, but the lifted closure didn't rewrite the call.

Design

  • Clone ownership: the importer owns imported-generic clones — discovered from its own call sites (ast.ModuleCall now 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).
  • Unshadowed imported generics route via _generic_fn_info (bare + qualified); shadowed ones emit under mod$<path>$gen$… reached only by the qualified call (_module_qualified_generic_bases), so the bare name stays on the local shadow (§8.5.2).
  • The verifier discovers the same sets (_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

Docs

CHANGELOG [Unreleased] (one bullet per issue); #774 and #873 KNOWN_ISSUES Bugs rows removed; spec §11.16; conformance programs ch08_cross_module_generic{,_lib}, ch09_generic_in_closure. No version bump.

Closes #774
Closes #873

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Expanded the conformance suite (now 112 programs) and refreshed related runner/test coverage figures.
    • Added new conformance coverage and regression tests for cross-module generics and generics called only from inside closure bodies.
  • Bug Fixes
    • Improved monomorphisation for imported generics across modules, including module-qualified calls and shadowing scenarios.
    • Fixed generic resolution/rewriting for generics used solely within lifted closure bodies to avoid verification/runtime mismatches.
  • Documentation / Tests
    • Updated spec guidance, documentation, known issues, and test expectations to match the new coverage.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 83e8e3e6-b996-4ebd-b8bf-ac62feb7760d

📥 Commits

Reviewing files that changed from the base of the PR and between 4063b30 and 0947e50.

⛔ Files ignored due to path filters (7)
  • 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/ch08_cross_module_generic.vera is excluded by !**/*.vera
  • tests/conformance/ch08_cross_module_generic_lib.vera is excluded by !**/*.vera
  • tests/conformance/ch09_generic_in_closure.vera is excluded by !**/*.vera
📒 Files selected for processing (21)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • spec/11-compilation.md
  • tests/conformance/manifest.json
  • tests/test_codegen_closures.py
  • tests/test_codegen_modules.py
  • tests/test_monomorphize_differential.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/modules.py
  • vera/codegen/monomorphize.py
  • vera/monomorphize.py
  • vera/obligations/session.py
  • vera/verifier.py
  • vera/wasm/context.py
📝 Walkthrough

Walkthrough

This PR adds cross-module generic monomorphisation and closure-body generic handling, with matching verifier discovery, WASM resolution, tests, and updated documentation counts.

Changes

Cross-module and closure generic monomorphisation fix

Layer / File(s) Summary
Shared instantiation discovery
vera/monomorphize.py, tests/test_monomorphize_differential.py
Generic instantiation discovery now recognises module-qualified calls, shares argument-tuple inference, and adds cross-module differential helpers and parity tests.
Codegen import harvesting and monomorphisation state
vera/codegen/core.py, vera/codegen/modules.py
CodeGenerator tracks imported generic registries and module-qualified bases, and module registration now harvests imported public generics instead of skipping them.
Shadowed imported generic emission
vera/codegen/monomorphize.py
Imported generics are merged into local discovery, shadowed qualified calls are discovered, cloned, rewritten, and extended through transitive dependencies.
WASM resolution and mirrored verifier discovery
vera/wasm/context.py, vera/codegen/functions.py, vera/codegen/closures.py, vera/verifier.py
Module-qualified generic resolution is centralised in WasmContext, closure compilation gets the needed context, and the verifier mirrors imported/shadowed generic discovery.
Regression coverage and conformance entries
tests/conformance/manifest.json, tests/test_codegen_closures.py, tests/test_codegen_modules.py
Conformance entries and regression tests cover imported generics, shadowed module-qualified calls, closure-body-only generics, and verifier/codegen parity.

Documentation updates for suite counts and changelog

Layer / File(s) Summary
Conformance and test count updates
AGENTS.md, CLAUDE.md, FAQ.md, ROADMAP.md, TESTING.md
Documentation counts for conformance programs and tests are updated across the project docs and testing guide.
Known issues, changelog, and spec note
CHANGELOG.md, KNOWN_ISSUES.md, spec/11-compilation.md
The changelog adds two fixed bullets, the known-issues table removes two rows and adds two new bug entries, and the compilation spec adds a cross-module generic monomorphisation clarification.

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

Possibly related PRs

  • aallan/vera#767: Shares the generic instantiation discovery and verifier/codegen parity surface.
  • aallan/vera#816: Overlaps with module-qualified call resolution in vera/wasm/context.py.
  • aallan/vera#454: Also changes lifted-closure compilation context in vera/codegen/closures.py.

Suggested labels: compiler, tests, spec, 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 is concise and accurately captures the cross-module and closure-body monomorphisation fixes plus verifier/codegen lockstep for #774 and #873.
Linked Issues check ✅ Passed The code and test changes address both #774 and #873, including imported generic instantiation, shadowed qualified calls, closure-body discovery, and verifier/codegen parity.
Out of Scope Changes check ✅ Passed The extra documentation, changelog, roadmap, and known-issues edits are all part of the stated PR objectives and do not appear out of scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Changelog Covers Public-Surface Changes ✅ Passed CHANGELOG’s #774 bullet covers imported generic monomorphisation, bare/qualified calls, shadowing, and verifier lockstep, matching the spec/11-compilation.md change.
Spec And Implementation Move Together ✅ Passed PASS: spec/08-modules.md and spec/11-compilation.md already cover module-qualified calls, shadowing and monomorphisation; vera/ implements that behaviour, including lifted closures.
Diagnostics Carry An Error Code ✅ Passed Touched monomorphisation/closure paths add no new diagnostics; I found no changed diagnostic sites lacking stable [EW]### codes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/774-873-mono-discovery

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

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.84211% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.24%. Comparing base (af55183) to head (0947e50).

Files with missing lines Patch % Lines
vera/codegen/monomorphize.py 95.72% 5 Missing ⚠️
vera/verifier.py 97.65% 3 Missing ⚠️
vera/wasm/context.py 93.33% 1 Missing ⚠️
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              
Flag Coverage Δ
javascript 78.07% <ø> (ø)
python 95.07% <96.84%> (+0.02%) ⬆️

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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 01225e8 and 6380d30.

⛔ Files ignored due to path filters (7)
  • 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/ch08_cross_module_generic.vera is excluded by !**/*.vera
  • tests/conformance/ch08_cross_module_generic_lib.vera is excluded by !**/*.vera
  • tests/conformance/ch09_generic_in_closure.vera is excluded by !**/*.vera
📒 Files selected for processing (20)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • spec/11-compilation.md
  • tests/conformance/manifest.json
  • tests/test_codegen_closures.py
  • tests/test_codegen_modules.py
  • tests/test_monomorphize_differential.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/modules.py
  • vera/codegen/monomorphize.py
  • vera/monomorphize.py
  • vera/verifier.py
  • vera/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

Comment thread tests/conformance/manifest.json
Comment thread tests/test_monomorphize_differential.py
Comment thread vera/codegen/monomorphize.py
Comment thread vera/wasm/context.py Outdated
aallan pushed a commit that referenced this pull request Jul 3, 2026
…, #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]>
@aallan aallan force-pushed the fix/774-873-mono-discovery branch 2 times, most recently from c13f6d3 to 4063b30 Compare July 3, 2026 09:41
aallan pushed a commit that referenced this pull request Jul 3, 2026
…, #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]>

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6380d30 and c13f6d3.

⛔ Files ignored due to path filters (7)
  • 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/ch08_cross_module_generic.vera is excluded by !**/*.vera
  • tests/conformance/ch08_cross_module_generic_lib.vera is excluded by !**/*.vera
  • tests/conformance/ch09_generic_in_closure.vera is excluded by !**/*.vera
📒 Files selected for processing (20)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • spec/11-compilation.md
  • tests/conformance/manifest.json
  • tests/test_codegen_closures.py
  • tests/test_codegen_modules.py
  • tests/test_monomorphize_differential.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/modules.py
  • vera/codegen/monomorphize.py
  • vera/monomorphize.py
  • vera/verifier.py
  • vera/wasm/context.py
🔗 Linked repositories identified

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

  • aallan/vera-bench (manual)

Comment thread tests/test_codegen_modules.py

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between c13f6d3 and 4063b30.

⛔ Files ignored due to path filters (7)
  • 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/ch08_cross_module_generic.vera is excluded by !**/*.vera
  • tests/conformance/ch08_cross_module_generic_lib.vera is excluded by !**/*.vera
  • tests/conformance/ch09_generic_in_closure.vera is excluded by !**/*.vera
📒 Files selected for processing (20)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • FAQ.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • spec/11-compilation.md
  • tests/conformance/manifest.json
  • tests/test_codegen_closures.py
  • tests/test_codegen_modules.py
  • tests/test_monomorphize_differential.py
  • vera/codegen/closures.py
  • vera/codegen/core.py
  • vera/codegen/functions.py
  • vera/codegen/modules.py
  • vera/codegen/monomorphize.py
  • vera/monomorphize.py
  • vera/verifier.py
  • vera/wasm/context.py
🔗 Linked repositories identified

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

  • aallan/vera-bench (manual)

Comment thread FAQ.md Outdated
Comment thread TESTING.md Outdated
Comment thread vera/verifier.py
aallan pushed a commit that referenced this pull request Jul 3, 2026
…, #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]>
@aallan aallan force-pushed the fix/774-873-mono-discovery branch from 4063b30 to 24fca62 Compare July 3, 2026 10:47
@aallan

aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…, #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]>
@aallan

aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@aallan aallan force-pushed the fix/774-873-mono-discovery branch from 24fca62 to 0947e50 Compare July 3, 2026 11:06
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@aallan

aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Independent orchestrator soundness verification (post-rebase, head 0947e50) — the false-Tier-1 differential on a lying imported generic:

  • Module g exports public forall<T> fn liar(@T -> @Int) returning 0 but declaring ensures(@Int.result == 9); the importer calls g::liar(5).
  • vera check → passes (check-green).
  • vera verify --jsonok: false, error_code: E500, catching the lying clone statically at the importer.
  • vera run → traps ensures(@Int.result == 9) failed in liar$Int (ground truth).

Pre-fix this was ok: true (false Tier-1) + run-trap. The verify/run differential now agrees — the imported-generic clone is verified at the importer, so a lying module contract is an honest E500 rather than a silent Tier-1 pass. Confirms the keystone fix on the rebased branch.

aallan pushed a commit that referenced this pull request Jul 3, 2026
@aallan

aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Superseded by the release/v0.1.0 integration branch — this fix is merged there as 873dae1 (full six-fix suite green: 6162 passed). Per-bug review record preserved here; issue closure (#774/#773/#873) rides the single release/v0.1.0 → main PR. Closing this PR.

@aallan aallan closed this Jul 3, 2026
aallan pushed a commit that referenced this pull request Jul 3, 2026
… 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]>
aallan pushed a commit that referenced this pull request Jul 3, 2026
… 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]>
aallan pushed a commit that referenced this pull request Jul 3, 2026
… 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]>
aallan pushed a commit that referenced this pull request Jul 3, 2026
…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]>
aallan pushed a commit that referenced this pull request Jul 3, 2026
…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]>
aallan pushed a commit that referenced this pull request Jul 3, 2026
…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]>
@aallan aallan deleted the fix/774-873-mono-discovery branch July 4, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant