Skip to content

feat(option-A): per-component handle tables + bridging (3/3 trio passes)#109

Open
avrabe wants to merge 3 commits intomainfrom
feat/per-component-ht-bridging
Open

feat(option-A): per-component handle tables + bridging (3/3 trio passes)#109
avrabe wants to merge 3 commits intomainfrom
feat/per-component-ht-bridging

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented Apr 25, 2026

Summary

Implements Option A from issue #107 — per-component handle tables with cross-component bridging trampolines. Builds on PR #108's path B foundation. All three trio fixtures now pass at runtime, closing issue #75.

Phase 1 (commit 0632ec2)

Broaden reexporter_resources to also include the resource's DEFINER component. With resource_with_lists, this allocates 2 ht entries (was 1): one for the leaf definer, one for the intermediate re-exporter.

Phase 3 (commit 8437e3f)

Insert bridging trampolines in fact.rs adapter:

  • own result transfer when both caller and callee have ht: callee.ht_rep + caller.ht_new translates returned handles into caller's namespace.
  • borrow param transfer when caller has its OWN ht: caller.ht_rep + callee.ht_new translates outgoing handles into callee's namespace.

Phase 4 — trio close-out (commit 9149573)

Three layered fixes for the remaining 2 of 3 trio runtime failures:

  1. fact.rs — borrow-rep discriminator (both 2-comp and 3-comp branches). Method-like exports ([method]/[static]/[constructor]) on a locally-defined resource expect the REP as arg0 (wit-bindgen's _export_*_cabi calls ThingBorrow::lift(arg0) which derefs as *mut _ThingRep<T>). Top-level functions taking borrow<T> on a used resource expect a HANDLE (cabi calls Float::from_handle(arg0 as u32)). Suppress callee.ht_new for method-like calls; emit only caller.ht_rep so the rep flows through unchanged. Without this, the freshly-minted slot address gets passed as the rep, the deref reads 4 bytes at the slot (the just-stored rep), and Option's discriminant byte is the low byte of that rep — 0 for typical aligned box pointers → Option::unwrap on None.

  2. merger.rs — dtor suppression for re-exporter ht_drop. Phase 1's per-component HTs for definers store foreign reps placed there by own bridges (intermediate's HT contains leaf box pointers). The dtor (<iface>#[dtor]<rn>) blindly casts every stored value as *mut _ThingRep<LocalT> and Box::from_raw drops it; for foreign reps this misinterprets memory and triggers re-entrant drops via the wit-bindgen Resource::drop impl, producing unbounded recursion. Skip the dtor when the component is a re-exporter — the standard Box-pattern wit-bindgen design assumes the rep is owned by the component whose ht stores it, but Phase 1 broke that invariant.

  3. resolver.rs — sort reexporter_components and reexporter_resources before storing on the graph. They were collected from HashSet, whose iteration order is non-deterministic. Downstream HT allocation and the wrapper alias-fallback both make first-match decisions on this order, so the same fixture would sometimes wire [resource-drop] for the runner to leaf's ht_drop and sometimes intermediate's, producing the "passes manually, fails in cargo test" flakiness.

Status

Result
73-test wit-bindgen suite ✅ 73/73 (0 regressions)
resource_floats runtime ✅ PASSES
resource_with_lists runtime ✅ PASSES
resource-import-and-export runtime ✅ PASSES

All three trio fixtures promoted from fuse_only_test! to runtime_test!. Verified stable across 5/5 consecutive cargo test runs after the determinism fix.

Related

Test plan

  • cargo test --release --test wit_bindgen_runtime — 73/73
  • resource_floats runtime passes
  • resource_with_lists runtime passes
  • resource-import-and-export runtime passes
  • Stable across 5 consecutive runs

Branch

feat/per-component-ht-bridging head 9149573 — 3 commits on top of feat/conditional-resource-typing.

@temper-pulseengine
Copy link
Copy Markdown

AI Code Review for PR #109

pulseengine/meld:feat/per-component-ht-bridging → pulseengine/meld:main

This pull request introduces a new feature in the Meld tool, which is a static component fusion engine for WebAssembly. The main changes are:

  1. Opaque Resources: A new configuration option --opaque-rep has been added to mark resources as opaque-rep. This means that these resources have their representation treated as a u32 rather than a boxed pointer, and they skip the per-resource handle table layer in Meld.

  2. Resource Type Management: Opaque-rep resources are routed differently from standard Box-pattern resources:

    • The merger.rs::allocate_handle_tables function skips them since their reps are already valid integer handles.
    • The component_wrap.rs::local_resource_types function keys them by (component_idx, resource_name) rather than resource_name alone. This ensures that each component owns its own wasmtime resource type, matching the un-fused composition's semantics.
  3. Command Line Interface: The Meld CLI now supports parsing and handling the new --opaque-rep option. It also prints a list of opaque-rep resources if specified.

  4. Documentation Update: The documentation has been updated to reflect these changes, including examples and explanations for the new configuration option.

Overall, this feature enhances Meld's ability to handle complex WebAssembly component compositions while maintaining performance and security.


This review was generated by a local AI model. It is advisory only and may contain inaccuracies.

Reviewed at 8437e3f

@avrabe avrabe changed the title feat(option-A): per-component handle tables + bridging (1/3 trio) feat(option-A): per-component handle tables + bridging (3/3 trio passes) Apr 27, 2026
@avrabe avrabe marked this pull request as ready for review April 27, 2026 05:32
avrabe and others added 3 commits April 27, 2026 07:34
Previously reexporter_resources only listed re-exporter components
that need handle tables (where callee_defines_resource=false in
adapter sites). Option A's per-component-tables-with-bridging
architecture also needs the DEFINER component to have its own ht so
cross-component handle hand-offs can translate via Phase 3 bridging
trampolines: caller_handle -> caller_ht_rep -> rep -> callee_ht_new
-> callee_handle.

After this change, resource_with_lists shows TWO ht entries (was 1):
  comp 2 (intermediate/re-exporter) — its existing exports ht
  comp 0 (leaf/definer)             — new, allocated by Phase 1

Standard suite: 73/73, 0 regressions. resource_floats runtime still
passes (no regression). resource_with_lists still traps — that's
expected; Phases 2 (definer-fallback restriction) and 3 (bridging
trampolines in fact.rs) still pending.
…ndles

When per-component handle tables exist on BOTH sides of a cross-
component call (post-Phase-1), the handle value space is no longer
shared — a memory-pointer handle from one component's ht refers to
that component's memory, not the other's. Without translation, the
receiving component's ht_rep loads from its own memory at the
caller's offset → garbage.

Phase 3 inserts bridging trampolines in the FACT adapter:

own<T> result transfer (callee_defines_resource=true case, lines 715+):
  When both caller and callee have their own ht for the resource,
  emit ResourceOwnResultTransfer { rep_func: callee.ht_rep,
  new_func: caller.ht_new }. The pre-existing emit_resource_new_results
  emits (call rep_func)(call new_func) which performs the bridge:
    callee_handle → callee.ht_rep → rep → caller.ht_new → caller_handle.

borrow<T> param transfer (callee_defines_resource=true, lines 598+):
  When BOTH the caller has its own ht AND callee has one too, emit
  ResourceBorrowTransfer with callee.ht_new in the new_func slot
  (was None previously). Same emit phase produces the bridge.

Gating: bridge fires ONLY when caller and callee are different
components AND caller has its own ht for the resource. Without the
caller-has-ht check, the bridge over-fires for fixtures where the
caller uses canonical [resource-rep] (no ht), corrupting handles by
double-translating canonical → memory-pointer. The check restored
resource_floats after an initial regression.

Status:
- 73/73 standard suite passes (0 regressions)
- resource_floats runtime: still PASSES
- resource_with_lists, resource-import-and-export: still trap with
  Option::unwrap on None — bridges fire correctly per debug logs but
  the deeper issue is upstream (likely accidental ht_drop firing on
  cross-memory bridged box pointers, OR multi-memory addressing of
  reps stored in one component's memory and loaded by another).

The bridging architecture is correct in principle — both fixtures
need additional investigation of where the box pointer gets freed or
mis-addressed. Tracking under issue #107.
Three layered fixes get resource_floats, resource_with_lists, and
resource-import-and-export passing as runtime_test! fixtures:

1. fact.rs — discriminate the borrow lower based on whether the callee's
   exported function is a `[method]/[static]/[constructor]` on a locally
   defined resource (cabi expects REP) versus a top-level function taking
   borrow<T> on a `use`d resource (cabi expects HANDLE). Suppress
   `callee.ht_new` for the former; the rep-only path matches the canonical
   ABI lower of borrow<T>. Without this, the slot address minted by
   callee.ht_new gets passed as the rep, the export's `ThingBorrow::lift`
   derefs it, and Option's discriminant byte is the low byte of the
   stored rep (0 for typical aligned box pointers) → Option::unwrap on None.
   Applied symmetrically in both the 2-component (callee_defines=true)
   and 3-component (callee_defines=false) borrow branches.

2. merger.rs — suppress the ht_drop dtor invocation for re-exporter
   components. Phase 1's per-component HTs for definers store foreign reps
   placed there by own bridges (intermediate's HT contains leaf box
   pointers). The dtor (`<iface>#[dtor]<rn>`) casts every stored value as
   `*mut _ThingRep<LocalT>` and Box::from_raw drops it; for foreign reps
   this misinterprets memory and triggers re-entrant drops via the
   wit-bindgen Resource::drop impl, producing unbounded recursion. The
   standard wit-bindgen design assumes the rep is owned by the component
   whose ht stores it, but Phase 1 broke that invariant.

3. resolver.rs — sort reexporter_components and reexporter_resources
   before storing on the graph. They were collected from HashSet, whose
   iteration order is non-deterministic. Downstream HT allocation and the
   wrapper alias-fallback both make first-match decisions on this order,
   so the same fixture would sometimes wire `[resource-drop]` for the
   runner to leaf's ht_drop and sometimes intermediate's, producing the
   "passes manually, fails in cargo test" flakiness.

Promotes resource_floats, resource_with_lists, and
resource-import-and-export from fuse_only_test! to runtime_test! — closes
the trio runtime failures tracked in issue #75. Standard 73-test suite
still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@avrabe avrabe force-pushed the feat/per-component-ht-bridging branch from 9149573 to 38f8660 Compare April 27, 2026 05:41
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.

Promote resource_floats and resource_with_lists to runtime tests

1 participant