Skip to content

feat(#418): bind cabi-arena-realloc natively on the self-contained dissolve — synthesized in-module arena allocator - #799

Merged
avrabe merged 4 commits into
mainfrom
feat/47-418-arena-realloc
Jul 17, 2026
Merged

feat(#418): bind cabi-arena-realloc natively on the self-contained dissolve — synthesized in-module arena allocator#799
avrabe merged 4 commits into
mainfrom
feat/47-418-arena-realloc

Conversation

@avrabe

@avrabe avrabe commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What (#418, v0.47 Wave-2 Lane 6)

The --relocatable half of #418 was locked in PR #420 (undefined __cabi_arena_realloc symbol, TCB-bound at native link — untouched here, still asserted). This PR closes the other half: a self-contained dissolve of a module importing env::__cabi_arena_realloc (the wit-bindgen cabi-realloc-extern / meld-dissolve shape) previously degraded to an ET_REL "link me with the Kiln bridge" object — the arena import was the one unresolved seam blocking a fully self-contained image.

The binding: a wasm→wasm rewrite, not new codegen

synth_core::arena_bind: when the arena import is the module's only import, remove it and prepend a defined WebAssembly function implementing the #418 contract, compiled through synth's ordinary pipeline like any other module function (no hand-written machine code, no new selector/encoder paths, target-portable):

  • signature (old_ptr, old_len, align, new_len) -> ptr (i32×4 → i32);
  • old_len == 0 && new_len == 0 → returns align verbatim;
  • bump allocation from an appended mutable cursor global, aligned up per call;
  • realloc preserves min(old_len, new_len) bytes (the RawVec-grow pattern needs this);
  • bounded arena [arena_base, linmem_end) — exhaustion traps (unreachable → UDF), never memory.grow.

Index-preserving by construction: the function index space is imports-first, so sole-import removal + prepend gives the allocator the import's old index — every call/ref.func/export/element entry keeps its meaning, zero remapping. Untouched sections are byte-copied verbatim (count-patch only on function/global/code); the result is wasmparser::Validator-checked before use.

arena_base mirrors the shipped used-extent rule: max(data_end, i32-const global inits ≤ linmem) (covers __stack_pointer, wasm-ld's __heap_base/__data_end), floor 16, 16-aligned.

Decline matrix (all asserted in cabi_arena_bind_418.rs)

case behavior
no arena import byte-identical pass-through (frozen anchors intact)
--relocatable untouched #420 TCB seam (undefined symbol)
--no-bind-cabi-arena (pinned opt-out) pass-through → ET_REL + undefined symbol
other imports alongside keeps the host-linked seam entirely (never a half-bound hybrid)
--native-pointer-abi not bound (its SP-global promotion could misidentify the cursor global)
wrong signature loud decline (#418 … signature)
no/64-bit memory, non-const data offset, zero arena room loud decline with precise reason

Gates (red-first)

  • scripts/repro/cabi_arena_bind_418_differential.py + CI job arena-bind-418-oracle: red before (exits at the ET_EXEC assert — the import degraded the compile to ET_REL), green after. unicorn runs the image's own startup (R9/R10/R11 init, globals materialization, default --cortex-m self-contained image silently DROPS active (data) segments: .linear_memory is NoBits, initializer bytes absent -> every load from an initialized region reads 0 (silent miscompile) #758 data copy) then each export; wasmtime satisfies the import with a host arena implementing the contract at a deliberately different base, so only pointer-independent semantics can pass (preserved contents after grow, disjointness, alignment residue, (0,0,align,0) == align); the exhaustion case must trap on both sides.
  • Harness anti-vacuity locked: startup must reach the first compiled function AND leave R11 = __linear_memory_base (an initial min()-over-symbols bug ran no startup and still greened 4/6 cases on a zeroed machine — caught, fixed, asserted).
  • 7 unit tests on the transform; 5 integration tests; existing cabi_arena_realloc_linkability_418.rs still green; full workspace suite green (126 suites); clippy -D warnings + fmt clean; claim_check 21/21.

Honest residue

  • The real dissolved fixture (wit-bindgen#4 → wasm-tools#2 → meld#301 output) still hasn't landed in-repo; this locks the synth-layer mechanism on the synthetic shape. When gale drops the real .wasm, add it as a fixture case (issue comment thread already tracks this).
  • Binding scope: --all-exports self-contained Cortex-M path only (the dissolve path). Single-function compiles and non-Cortex-M targets keep the pass-through.
  • The default (non-relocatable) ET_REL path's legacy import handling for unbound imports is unchanged.

Refs #418, #420, meld#301, gale#89.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 4 commits July 17, 2026 06:22
…mport dissolve vs wasmtime host arena

The fixture is the wit-bindgen cabi-realloc-extern / meld-dissolve shape
(exported cabi_realloc routing to env::__cabi_arena_realloc, Rust/wasm-ld
layout globals). The gate asserts the default self-contained compile yields
ET_EXEC and every export matches wasmtime ground truth where the import is
satisfied by a host arena implementing the #418 contract (0/0 -> align, bump,
copy-on-grow, bounded + trap on exhaustion). Host arena uses a DIFFERENT base
than synth will, so only pointer-independent semantics can green.

RED today: the arena import degrades the compile to an ET_REL link-me object
(the embedder seam) — script exits at the ET_EXEC assert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…ealloc import with a synthesized in-module allocator

A wasm->wasm transform in synth-core: when env::__cabi_arena_realloc is the
module's ONLY import, remove it and prepend a DEFINED allocator function
implementing the #418 contract ((i32x4)->i32; old_len==0&&new_len==0 -> align;
bump allocation from an appended cursor global; copy-on-realloc of
min(old,new) bytes; bounded [arena_base, linmem_end) — traps on exhaustion,
never memory.grow). Imports-first index space + sole-import removal +
prepend = every call/export/elem index PRESERVED, no remapping; untouched
sections are byte-copied verbatim (count-patch only on function/global/code).

arena_base mirrors the shipped used-extent rule: max(data_end, i32-const
global inits <= linmem) — covering __stack_pointer / __heap_base /
__data_end — floor 16, 16-aligned. Loud declines: wrong signature, no/64-bit
memory, non-const data offsets, no defined functions, no arena room. Other
imports present -> NotApplicable pass-through (the module keeps the
host-linked seam). Result is Validator-checked before use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…arena-realloc importers, decline matrix + CI oracle

CLI: on the self-contained Cortex-M --all-exports path (ARM backend, not
--relocatable, not --native-pointer-abi), a sole env::__cabi_arena_realloc
import is bound via the synth-core arena-bind rewrite — the compile yields a
fully self-contained ET_EXEC image (previously: ET_REL 'link me with the Kiln
bridge', the embedder seam). Pinned opt-out: --no-bind-cabi-arena.

Untouched seams (asserted): --relocatable keeps the #420 TCB layering
(undefined symbol, host-link-bound); modules with OTHER imports keep the
host seam entirely (never a half-bound hybrid); modules without the import
pass through byte-identically.

Gates:
- scripts/repro/cabi_arena_bind_418_differential.py now GREEN (was red-first):
  unicorn runs the image's own startup then each export; wasmtime satisfies
  the import with a host arena at a DIFFERENT base — all six
  pointer-independent cases match, exhaustion traps on both sides. CI job
  arena-bind-418-oracle.
- cabi_arena_bind_418.rs: ET_EXEC + no undefined arena symbol + binding log;
  opt-out/relocatable restore the external seam; wrong signature declines
  loudly; mixed imports keep ET_REL.
- Harness anti-vacuity: startup must reach the first compiled function AND
  leave R11 = __linear_memory_base (a min()-over-symbols bug initially ran
  nothing and greened 4/6 cases on a zeroed machine — caught and locked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.07390% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-core/src/arena_bind.rs 95.85% 17 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit a5a0fc3 into main Jul 17, 2026
42 checks passed
@avrabe
avrabe deleted the feat/47-418-arena-realloc branch July 17, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant