Skip to content

fix(codegen): injective monomorphized WAT name mangling (#775)#883

Merged
aallan merged 1 commit into
mainfrom
fix/775-mangler-injective
Jul 3, 2026
Merged

fix(codegen): injective monomorphized WAT name mangling (#775)#883
aallan merged 1 commit into
mainfrom
fix/775-mangler-injective

Conversation

@aallan

@aallan aallan commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

_mangle_fn_name built monomorphized WAT symbols by lossy </>/, replacement, so distinct instantiations collided onto one symbol — g<A_B, C> and g<A, B_C> both mangled to g$A_B_C, and g<Map<String, Int>> collided with a user ADT literally named Map_String_Int. On current main every reachable collision is loud (both clones emit → wasmtime duplicate func identifier); the RED battery also settles the issue's open question — a silent-wrong-value shape was not constructible on main (three documented attempts, each route ends loud).

The fix

One shared escape, mangle_type_name in vera/monomorphize.py: ___, <_L, >_R, ,_C, space→_S, vectors joined with _J. Injectivity proof in the docstring (mechanical, prefix-code argument: every escaped output is uniquely decodable left-to-right; _J cannot occur inside a component). #773's _adt_eq_fn_name now delegates to it — one naming convention, not two. Three producer sites unified (clone emission; calls.py::_resolve_generic_call, previously a hand-copied sanitizer; inference.py ret-type lookup, previously a raw join that missed every parameterized instantiation — the three-site desync a partial fix would have introduced is pinned by a purpose-built nested-generic test).

Rename-purity differential (the re-baseline sign-off)

All 140 compilable conformance+example programs compiled on main and on the branch, WAT diffed: 134 byte-identical, 6 pure-rename, 0 semantic changes (9 distinct symbol renames, e.g. option_map$Int_Intoption_map$Int_JInt). One file is rename-pure modulo a bounded data-offset shift — the clone name is embedded in postcondition-message data strings. Replication script included in the report.

Evidence

  • RED on main: 6 tests fail for the collision reason (non-injective mangle: g<A_B, C> and g<A, B_C> both -> g$A_B_C; e2e duplicate func identifier).
  • Orchestrator flip probe (independent): the branch's tests/test_codegen_monomorphize.py on main = 6 failed / 73 passed; on the branch = 79 passed.
  • Mutation kills (4, pycache-purged): lossy encoding restored → 6 RED; call-site desync → 4 RED; registry-lookup desync → 1 RED (the test the old suite was blind to); _-doubling dropped → 1 RED (forging-pair battery).

Gates

pytest 5907 passed / 43 skipped (browser 111 passed, 0 skipped); mypy clean; ruff (incl. --select S) clean; conformance 108/108; examples 37/37; all doc gates; site assets rebuilt.

Docs

CHANGELOG [Unreleased]; #775 KNOWN_ISSUES Bugs row removed; counts refreshed. No version bump (rides the v0.1.0 burndown).

Closes #775

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed monomorphised WAT symbol name collisions by switching to injective escaping for generic instantiations.
    • Made generic call-site mangling and return-type lookup consistent, preventing symbol mismatches.
    • Unified type-name escaping for helper equality functions to avoid conflated identifiers.
  • Tests

    • Added a dedicated regression suite ensuring mangled names are injective, deterministic, and non-shared at emitted call sites.
    • Updated and expanded codegen expectations for corrected mono-suffixes and strengthened regex validation.
  • Documentation

    • Refreshed changelog, known issues, roadmap, and testing summaries/counts.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9005e7e0-1d5e-4ddc-878e-cb0392111712

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Fixes monomorphized WAT symbol collisions by introducing a shared injective type-name mangler, wiring clone emission, generic-call rewriting, registry lookup, and equality helper naming through it, and adding regression coverage plus matching test/documentation updates.

Changes

Injective mangling fix and propagation

Layer / File(s) Summary
Injective type-name mangler and _mangle_fn_name rework
vera/monomorphize.py
Adds mangle_type_name with injective escaping for Vera type strings and reworks _mangle_fn_name to join escaped components with _J.
Call-site and lookup wiring to shared mangler
vera/wasm/calls.py, vera/wasm/inference.py, vera/wasm/operators.py
Generic call rewriting, return-type registry lookup, and ADT equality naming now delegate to the shared mangler instead of local string sanitisation.
New injectivity and collision regression tests
tests/test_codegen_monomorphize.py
Adds unit and end-to-end coverage for distinct mangled outputs, collision reproduction, and lookup consistency.
Existing test/comment updates for new mangled-name format
tests/test_codegen_data_types.py, tests/test_checker_errors.py, tests/test_monomorphize_differential.py, tests/test_runtime_traps.py, vera/codegen/core.py
Updates docstrings, comments, and regex expectations to the new _J-based suffix format.
Changelog, known-issues, and metrics updates
CHANGELOG.md, KNOWN_ISSUES.md, ROADMAP.md, TESTING.md
Adds a changelog note, removes the resolved bug row, records the new bug entry, and bumps test-count metrics.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CallsResolver
  participant InferenceLookup
  participant AdtEqNaming
  participant Monomorphizer
  CallsResolver->>Monomorphizer: _mangle_fn_name(call target, type vars)
  InferenceLookup->>Monomorphizer: _mangle_fn_name(call.name, parts)
  AdtEqNaming->>Monomorphizer: mangle_type_name(type_name)
  Monomorphizer-->>CallsResolver: mangled call symbol
  Monomorphizer-->>InferenceLookup: mangled registry key
  Monomorphizer-->>AdtEqNaming: escaped type name
Loading

Possibly related issues

Possibly related PRs

  • aallan/vera#345: Both PRs modify generic function call WAT identifier mangling in vera/wasm/calls.py.
  • aallan/vera#454: Both PRs modify _infer_fncall_vera_type in vera/wasm/inference.py around monomorphised lookup behaviour.
  • aallan/vera#659: Both PRs affect monomorphisation suffix handling in vera/monomorphize.py and vera/wasm/calls.py.

Suggested labels: compiler, tests, docs

🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Spec And Implementation Move Together ⚠️ Warning WAT name-mangling semantics changed in vera/, but the commit touches no spec/ files and spec/11-compilation still only has the old generic monomorphisation wording/example. Update spec/11-compilation.md (or adjacent formal spec text) to define the new injective mangling scheme and its call-site/registry consistency, or revert the compiler-side semantic change.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing injective monomorphised WAT name mangling for code generation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 No files under vera/cli.py, vera/errors.py, spec/, vera/lsp/, or vera/codegen/api.py changed; the behaviour change is internal and is described in CHANGELOG.md.
Diagnostics Carry An Error Code ✅ Passed No new or changed diagnostics were introduced; the PR only updates WAT name mangling and related comments/tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/775-mangler-injective

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

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.01%. Comparing base (3e19e6b) to head (cb0f080).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #883   +/-   ##
=======================================
  Coverage   93.01%   93.01%           
=======================================
  Files          95       95           
  Lines       29233    29232    -1     
  Branches      360      360           
=======================================
  Hits        27190    27190           
+ Misses       2037     2036    -1     
  Partials        6        6           
Flag Coverage Δ
javascript 75.01% <ø> (ø)
python 95.01% <100.00%> (+<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: 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 `@vera/wasm/calls.py`:
- Around line 606-616: The comment in Monomorphizer._infer_type_args_from_call
is stale and contradicts the actual mangling contract; update the wording to say
phantom vars default to Bool, matching the existing mapping assignment and the
shared injective mangler behavior used by Monomorphizer._mangle_fn_name. Keep
the code unchanged and revise the inline documentation near the forall_vars loop
so it reflects the true default and avoids suggesting Unit anywhere in this
call-site path.
🪄 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: 22bbe5a3-d029-4e05-a3fa-0ffe0c4903cb

📥 Commits

Reviewing files that changed from the base of the PR and between c0af1b9 and 2966c3d.

⛔ Files ignored due to path filters (1)
  • tests/conformance/ch09_option_result_combinators.vera is excluded by !**/*.vera
📒 Files selected for processing (14)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_checker_errors.py
  • tests/test_codegen_data_types.py
  • tests/test_codegen_monomorphize.py
  • tests/test_monomorphize_differential.py
  • tests/test_runtime_traps.py
  • vera/codegen/core.py
  • vera/monomorphize.py
  • vera/wasm/calls.py
  • vera/wasm/inference.py
  • vera/wasm/operators.py
🔗 Linked repositories identified

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

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

Comment thread vera/wasm/calls.py Outdated
aallan pushed a commit that referenced this pull request Jul 3, 2026
…#883 review)

Skip-changelog: comment-only edit; the PR already carries its CHANGELOG bullet

Co-Authored-By: Claude <[email protected]>
aallan pushed a commit that referenced this pull request Jul 3, 2026
Shared prefix-code escape (mangle_type_name) proven injective; #773's
_adt_eq_fn_name delegates to it; three producer sites unified (clone
emission, calls.py, inference.py). Rename-purity differential: 0 semantic
WAT changes across the corpus. Includes the #884 intake row (Z3 sort-name
collision — the same lossy encoding still latent verifier-side).

Co-Authored-By: Claude <[email protected]>
@aallan aallan force-pushed the fix/775-mangler-injective branch from 551424f to cb0f080 Compare July 3, 2026 07:03
@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.

@aallan aallan merged commit 01225e8 into main Jul 3, 2026
27 checks passed
@aallan aallan deleted the fix/775-mangler-injective branch July 3, 2026 07:13
aallan pushed a commit that referenced this pull request Jul 3, 2026
…onger collide (#884)

The verifier named Z3 datatype sorts with the old lossy sanitize
(`key.replace("<", "_").replace(">", "").replace(", ", "_")`), so a
monomorphized `Box<Int>` and a flat ADT literally named `Box_Int` both
became the Z3 name `Box_Int`. Z3's per-context datatype cache conflates
same-named sorts (the last `create()` wins and earlier same-named sorts
silently adopt its structure), so `Box<Int>`'s sort acquired the flat
ADT's `MkBoxInt(Bool)` constructor — a false E500 counterexample
(`@Box<Int>.0 = MkBoxInt(False)`) on trivially-true valid code.

Both Z3 sort-name sites (`_get_or_create_adt_sort` and
`_get_or_create_tuple_sort` in `vera/smt.py`) now route through the
injective `mangle_type_name` mangler #775 introduced for WAT symbols, so
distinct type keys can never share a Z3 sort name. The full-type-string
sort cache and the #871 nested-Float64 fpEQ walk are unaffected (the cache
is keyed by the type string, not the Z3-visible name).

Soundness-safe: the conflation only freed field constraints, producing
conservative false negatives, never a false Tier-1 — a genuinely-false
ensures over the colliding pair is still disproved after the fix, now with
the correct `MkBox` constructor.

Both sort-name sites are independently pinned, one collision per site, so
neither `mangle_type_name` call can be reverted without flipping a test:

- ADT site (`_get_or_create_adt_sort`): the `Box<Int>` / flat `Box_Int`
  collision. Reverting it re-collides the sorts and flips the three
  `Box`-collision tests (verifies-clean, correct-constructor, and the
  false-prove probe) back to the false E500.
- Tuple site (`_get_or_create_tuple_sort`): a `Tuple<Tuple<Int>, Int>` vs
  `Tuple<Tuple<Int, Int>>` collision. Both keys lossy-sanitize to
  `Tuple_Tuple_Int_Int`; under the lossy name the two synthesised tuple
  sorts share one Z3 datatype, and a match arm projecting a field index
  the conflated sort does not carry crashes the verifier with
  `z3.z3types.Z3Exception: Invalid accessor index` at `sort.accessor`
  (in `_bind_pattern`). Reverting it flips the new tuple-collision test.
  Confirmed load-bearing: reverting this site with only the `Box`
  collision test present flipped nothing.

The rename also broke the verifier's Array-element reverse lookup, fixed
here since #884 caused it. `_get_element_sort_for_array`'s tier-3 fallback
reconstructed the `_z3_sorts` key by string surgery
(`elt_key.replace("_", "<", 1) + ">"`) that assumed the old lossy naming,
so after the rename a generic-ADT element (`Array<Box<Int>>` → sort
`Array_Box_LInt_R`) produced the non-matching candidate `Box<LInt_R>` and
the lookup silently returned None. Latent — tier 1's creation-time direct
map (`_array_element_sorts`) fires for every live path, and no corpus
program indexes an `Array<Generic<T>>` in a contract this way — but
dead-on-arrival for that shape. A new injective inverse
`unmangle_type_name` (a left-to-right prefix-code decode, round-trip-tested
against `mangle_type_name`) recovers the key correctly, and the stale
tier-3 comment is corrected.

Pinned by tests/test_verifier_sort_name_collision_884.py (ADT + tuple
collisions, rename-control, false-prove disproved for the right reason),
the ch06_adt_sort_disambiguation conformance program,
TestSmtContextDirect.test_element_sort_reverse_lookup_tier3_generic_adt
(the Array-element unit pin), and the unmangle round-trip / non-range-input
tests in TestMangleInjectivity. Mutation-validated: reverting either
sort-name site to the lossy sanitize flips its collision test RED, and
reverting the Array-element lookup to the old string surgery flips the
reverse-lookup unit test RED.

Pre-existing on main; found by the PR #883 adversarial panel.

Co-Authored-By: Claude <[email protected]>
aallan pushed a commit that referenced this pull request Jul 3, 2026
…onger collide (#884)

The verifier named Z3 datatype sorts with the old lossy sanitize
(`key.replace("<", "_").replace(">", "").replace(", ", "_")`), so a
monomorphized `Box<Int>` and a flat ADT literally named `Box_Int` both
became the Z3 name `Box_Int`. Z3's per-context datatype cache conflates
same-named sorts (the last `create()` wins and earlier same-named sorts
silently adopt its structure), so `Box<Int>`'s sort acquired the flat
ADT's `MkBoxInt(Bool)` constructor — a false E500 counterexample
(`@Box<Int>.0 = MkBoxInt(False)`) on trivially-true valid code.

Both Z3 sort-name sites (`_get_or_create_adt_sort` and
`_get_or_create_tuple_sort` in `vera/smt.py`) now route through the
injective `mangle_type_name` mangler #775 introduced for WAT symbols, so
distinct type keys can never share a Z3 sort name. The full-type-string
sort cache and the #871 nested-Float64 fpEQ walk are unaffected (the cache
is keyed by the type string, not the Z3-visible name).

Soundness-safe: the conflation only freed field constraints, producing
conservative false negatives, never a false Tier-1 — a genuinely-false
ensures over the colliding pair is still disproved after the fix, now with
the correct `MkBox` constructor.

Both sort-name sites are independently pinned, one collision per site, so
neither `mangle_type_name` call can be reverted without flipping a test:

- ADT site (`_get_or_create_adt_sort`): the `Box<Int>` / flat `Box_Int`
  collision. Reverting it re-collides the sorts and flips the three
  `Box`-collision tests (verifies-clean, correct-constructor, and the
  false-prove probe) back to the false E500.
- Tuple site (`_get_or_create_tuple_sort`): a `Tuple<Tuple<Int>, Int>` vs
  `Tuple<Tuple<Int, Int>>` collision. Both keys lossy-sanitize to
  `Tuple_Tuple_Int_Int`; under the lossy name the two synthesised tuple
  sorts share one Z3 datatype, and a match arm projecting a field index
  the conflated sort does not carry crashes the verifier with
  `z3.z3types.Z3Exception: Invalid accessor index` at `sort.accessor`
  (in `_bind_pattern`). Reverting it flips the new tuple-collision test.
  Confirmed load-bearing: reverting this site with only the `Box`
  collision test present flipped nothing.

The rename also broke the verifier's Array-element reverse lookup, fixed
here since #884 caused it. `_get_element_sort_for_array`'s tier-3 fallback
reconstructed the `_z3_sorts` key by string surgery
(`elt_key.replace("_", "<", 1) + ">"`) that assumed the old lossy naming,
so after the rename a generic-ADT element (`Array<Box<Int>>` → sort
`Array_Box_LInt_R`) produced the non-matching candidate `Box<LInt_R>` and
the lookup silently returned None. Latent — tier 1's creation-time direct
map (`_array_element_sorts`) fires for every live path, and no corpus
program indexes an `Array<Generic<T>>` in a contract this way — but
dead-on-arrival for that shape. A new injective inverse
`unmangle_type_name` (a left-to-right prefix-code decode, round-trip-tested
against `mangle_type_name`) recovers the key correctly, and the stale
tier-3 comment is corrected.

Pinned by tests/test_verifier_sort_name_collision_884.py (ADT + tuple
collisions, rename-control, false-prove disproved for the right reason),
the ch06_adt_sort_disambiguation conformance program,
TestSmtContextDirect.test_element_sort_reverse_lookup_tier3_generic_adt
(the Array-element unit pin), and the unmangle round-trip / non-range-input
tests in TestMangleInjectivity. Mutation-validated: reverting either
sort-name site to the lossy sanitize flips its collision test RED, and
reverting the Array-element lookup to the old string surgery flips the
reverse-lookup unit test RED.

Pre-existing on main; found by the PR #883 adversarial panel.

Co-Authored-By: Claude <[email protected]>
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.

Monomorphized WAT name mangling is non-injective (_mangle_fn_name collides distinct instantiations)

1 participant