Skip to content

feat: per-resource handle tables + alias-fallback (1/3 trio runtime passes)#108

Merged
avrabe merged 9 commits intomainfrom
feat/conditional-resource-typing
Apr 27, 2026
Merged

feat: per-resource handle tables + alias-fallback (1/3 trio runtime passes)#108
avrabe merged 9 commits intomainfrom
feat/conditional-resource-typing

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented Apr 25, 2026

Summary

Architectural progression on issue #107 (under epic #106 / issue #92):

  • Per-resource handle table discrimination (Path B): rekey HandleTableInfo from HashMap<usize, _> to HashMap<(usize, String, String), _> so multi-resource re-exporters route per-resource instead of per-component. (commits e027bb1, c104b1b, 254bfab)
  • --opaque-rep CLI flag for resources built with pulseengine/wit-bindgen feat/opaque-rep-attribute. Plumbed end-to-end; conditional behavior is currently a no-op (architectural design pending). (commit e166cb4)
  • Alias-fallback for use-aliased resources where wasmtime unifies multiple WIT interfaces into one canonical type. Both definer-side and consumer-side branches now route to the unique re-exporter handle table. (commits 0035441, 73f95e6)
  • ht_drop invokes [dtor] before zeroing the slot — restores the canonical-ABI lifetime contract so wit-bindgen's Box-backed reps are properly dropped (and any Option::take on inner-handle fields fires at the right moment). (commit 9eb1229)

Test status

Test Result
73-test wit-bindgen runtime suite 73/73 (0 regressions)
8 two-component re-exporter fixtures 8/8
resource_floats runtime PASSES (was wasm unreachable)
resource_with_lists runtime FAILS Option::unwrap on None
resource-import-and-export runtime FAILS Option::unwrap on None
resource_floats_opaque (fork driver) Passes
resource_floats_opaque (meld) FAILS Same architectural class as the 2 trio failures

What this DOES NOT fix yet

Two of the trio fixtures still trap with Option::unwrap() on None. Diagnosis (issue #107): cross-component handle namespace conflation. Multiple components share a single handle table when their resources unify via use or shared interface — handles from different components address different memory layouts, and wit-bindgen's _ResourceRep<T>::val.as_ref().unwrap() reads garbage when handed a rep from the wrong component.

The principled fix is per-component handle tables with bridging trampolines at cross-component hand-offs (Option A in #107). That's a separate branch (feat/per-component-ht-bridging).

Why open this as a draft now

  • 73/73 standard suite + 1/3 trio is meaningful progress
  • The alias-fallback + dtor wiring are architecturally correct and should land regardless of how the remaining 2 trio failures are fixed
  • Soliciting review on the design surface before further iteration

Test plan

  • cargo test --release --test wit_bindgen_runtime — 73/73
  • cargo run --release --bin meld -- fuse tests/wit_bindgen/fixtures/resource_floats.wasm -o /tmp/f.wasm --component && wasmtime --invoke='run()' /tmp/f.wasm — outputs ()
  • cargo run --release --bin meld -- fuse tests/wit_bindgen/fixtures/resource_with_lists.wasm -o /tmp/r.wasm --component && wasmtime --invoke='run()' /tmp/r.wasm — traps (documented)
  • PR review

Related issues

Branch state

Worktree: /Users/r/git/pulseengine/meld-conditional-typing on feat/conditional-resource-typing head 9eb1229. Pushed to origin.

avrabe and others added 9 commits April 24, 2026 06:20
Adds scripts/explore/ — sibling pipeline to scripts/mythos/ — for
exploring design spaces where multiple alternatives exist and the
right answer requires "implementation as argumentation."

Key adaptations from mythos:
- Two-axis rank: viability (1-5) AND locus-of-fix (meld | wit-bindgen
  | spec | wasm-tools | hybrid). Honest "this isn't ours to fix"
  is a valid output.
- Oracle: a fuse_only_test! flips to a passing runtime_test! on the
  prototype branch, instead of mythos's failing-Kani-plus-PoC pair.
- "NOT VIABLE TODAY: <reason>" replaces "NO FINDING: could not
  satisfy oracle" as the honest-rejection escape hatch.
- Validator can reject for "real but uninteresting" the same way
  mythos does — high maintenance + low generalization is a
  three-strikes-out for ADR promotion.

Adds safety/adr/ as the emission target:
- schema.yaml defines design-question and design-adr shapes
- ADR-0 is the parent question for the re-exporter resource chain
  problem (epic #69 / issue #92), enumerating seven candidate paths
  (D, E, F, G, H, I, J) for sibling ADRs to explore in parallel
- Path I introduces the pulseengine/wit-bindgen fork as a venue for
  the opaque-rep convention proposal

No code changes. Pipeline templates only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rekey HandleTableInfo from HashMap<usize, _> to
HashMap<(usize, String, String), _>, so a re-exporter component with
multiple resources gets one handle table per (component, interface,
resource_name) instead of a single shared table that misroutes
imports the re-exporter passes through.

Background: 3-component re-exporter chains (resource_floats,
resource_with_lists, resource-import-and-export) trapped at runtime
with 'wasm unreachable' (rustc/LLVM assume(ptr.is_aligned()) debug
assertion firing because leaf code dereferenced the rep as a Box<T>
pointer when the rep was actually a small handle integer).
Convergent diagnosis from prototype paths D + E pinpointed the seam:
merger.rs:739 redirected ALL [resource-*] imports of any re-exporter
component through one shared ht_* regardless of which resource the
import was for.

Changes:
- resolver.rs: add reexporter_resources field to DependencyGraph,
  populated from adapter sites where callee_defines_resource=false.
- merger.rs: rekey handle_tables by (comp, iface, resource_name);
  refactor allocate_handle_tables to allocate one table per
  (component, resource); refactor the redirect loop with
  [export]-prefix / resource-graph discrimination to determine the
  owner of each [resource-*] import; add ht_export_suffix helper.
- fact.rs: add parse_resource_import helper; update three
  handle-table lookup sites to use per-resource keys (caller-side
  ht_rep, callee-side ht_new, callee-side ht_rep for own results).
- component_wrap.rs: update LocalResource ht_* lookup to use the new
  per-resource export naming.

Validation:
- 73-test wit-bindgen suite passes with 0 regressions.
- The originally-failing trio's trap shape changes from 'wasm
  unreachable' to 'unknown handle index 0x110004' — proving the
  per-resource routing now works (leaf no longer derefs handle as
  Box<T>) and narrowing the diagnosis to a remaining wrapper-boundary
  leak.
- Opaque-rep oracle (pulseengine/wit-bindgen feat/opaque-rep-attribute,
  resource_floats_opaque) continues to pass fuse oracle.

Known follow-up: at the outer wasmtime canon-lift boundary, a
memory-address handle from ht_new (table_base + 4) escapes into
wasmtime's resource validation. Needs unwrap shim in
component_wrap.rs to translate memory-address handles back to
canonical-ABI indices when the fused module's exports cross the
wrapper boundary.
…ports

Three follow-ups to the path B refactor (commit e027bb1) that extend
per-resource handle table routing to additional cases the original
commit didn't cover:

1. Iterate ALL components in merger.rs:739, not just those with their
   own handle tables. A pure consumer (the runner in a 3-component
   chain) holds handles allocated by the re-exporter's ht_new and
   must drop them through that same handle table — its [resource-drop]
   imports also need redirection.

2. component_wrap.rs:1254 falls back to ANY component's handle table
   matching (iface, rn) when the importer's own component_idx
   doesn't have one. Mirrors the merger.rs change at the wrapper
   boundary so consumer-side ImportResolution::LocalResource entries
   pick up the re-exporter's ht_* export instead of falling through
   to canonical resource ops.

3. strip_dollar_suffix helper strips meld's $N dedup suffix from
   resource names before handle-table lookup. The dedup suffix
   distinguishes duplicate canonical-ABI imports from different
   components but the canonical resource name is the same, so it
   must be stripped before keying handle_tables.

Status: 73/73 wit-bindgen suite still passes (0 regressions).
The originally-failing trio (resource_floats, resource_with_lists,
resource-import-and-export) still traps at runtime with 'unknown
handle index 0x110004' — the bug now narrowed to wit-bindgen's
intermediate code mixing ht_new-returned memory addresses with
leaf's canonical handles in the inner-Box / outer-Box composition.
Further work needs deeper instrumentation of the actual handle flow
across the cabi-export shim. Tracking as continued follow-up.
…back

Two refinements to the consumer-side handle-table routing introduced in
c104b1b:

1. ImportResolution::LocalResource gains an is_definer flag set by
   the resolver: true when the import came from an [export]-prefixed
   module (the importer is the resource's definer / owner), false
   when the importer is a pure consumer of someone else's resource.
   The wrapper-side fallback only routes through another component's
   handle table when is_definer=false. Previously the fallback would
   incorrectly route a definer's own [resource-new] through a
   re-exporter's ht_new — causing the definer's canonical
   [resource-rep] to later return whatever was stored there (a small
   handle integer instead of the actual Box pointer) and the user
   code's deref to panic with 'misaligned pointer dereference:
   address must be a multiple of 0x8 but is 0x2'.

2. merger.rs:739 redirect adds re-exporter-self-import detection:
   when a non-[export] [resource-*] import comes from a component
   that ALSO owns a handle table for the same (interface, resource),
   that import is the inner-component (definer) view from the re-
   exporter's perspective and must use canonical resource ops, NOT
   the re-exporter's own ht. Previously a re-exporter's import-side
   helpers were accidentally routed through its own ht, writing to
   memory address small_int and corrupting heap state.

Status:
- 73-test wit-bindgen suite still passes (0 regressions).
- resource_floats trio still traps at runtime with 'unknown handle
  index 0x110004' — the deeper architectural issue (memory-address
  handles from ht_new escaping into wasmtime canonical resource
  table at the wrapper boundary) remains. Tracked in issue #107.
Adds the infrastructure for marking individual (interface, resource) pairs
as opaque-rep — for re-exporter components built with the
pulseengine/wit-bindgen feat/opaque-rep-attribute fork that returns
u32 reps instead of boxed pointers. The flag is qualified
(--opaque-rep iface.resource, e.g. test:foo/bar.baz) and repeatable.

What's plumbed:
- meld-cli: --opaque-rep IFACE.RESOURCE arg, parsed to (String, String)
  tuples by splitting on the LAST '.' so qualified WIT interface names
  (which never contain '.') are preserved intact.
- FuserConfig.opaque_resources: Vec<(String, String)> field.
- Threaded through to Merger via with_opaque_resources(...) builder, and
  to wrap_as_component (currently the wrapper-side parameter is unused
  pending the right conditional strategy).
- All existing test FuserConfig initializers updated.

What's NOT yet doing anything (intentionally):
- Both conditional strategies tried in this spike were dead ends:
  (a) per-component local_resource_types keying alone produces a NEW
      'handle index used with the wrong type' trap because wasmtime's
      type system rejects cross-component handle passing for
      per-component-typed resources. Reverted.
  (b) identity ht_* functions break opaque-rep storage semantics —
      the rep stored at ht_new(rep) must be retrievable by a LATER
      ht_rep(handle), but identity collapses these. Reverted.
- The flag currently has no behavioral effect; the architectural fix
  for opaque-rep + meld fusion needs more design work.

Validation:
- Standard wit-bindgen suite: 73/73 (0 regressions).
- Full meld test suite: all 15 integration test files pass.
- Opaque oracle still traps with same shapes from issue #107.

The infrastructure is a clean foundation for future iteration on
conditional opaque-rep handling without re-doing the CLI/plumbing
work.
When intermediate has 'use test.{float}' (re-exporting leaf's test.float
as exports.float), wasmtime unifies them into one canonical resource
type. Path B previously refused to redirect [export]-prefixed (definer)
imports — leaf's [export]test:resource-floats/test [resource-rep]float
fell through to canon resource.rep, which never saw the memory-pointer
handles minted by intermediate's ht_new. Trap: 'unknown handle index'.

Fix: add a resource-name-only fallback in BOTH the merger redirect
(merger.rs:864) and the wrapper resolver (component_wrap.rs:1306) for
definer imports. When self-lookup misses, search any handle table
matching just the resource_name. In well-formed compositions there's
at most one re-exporter per logical resource, so the match is
unambiguous.

Status: 73/73 standard suite still passes (0 regressions). The trio
trap shape changes from 'unknown handle index 0x110004' (1st ht
entry) to 'unknown handle index 0x110010' (5th ht entry) — proving
more redirects are firing AND more handles are being allocated, but
something else still leaks to canon resource.*. Investigation ongoing.
…ame unification

The consumer-side LocalResource fallback was too aggressive: it blocked
redirect when the importing component owned a handle table for ANY
resource ending in the same name (e.g. intermediate's
'test:resource-floats/test [resource-rep]float' was blocked because
intermediate also has 'exports.float' ht — even though they're SEPARATE
imports unified at canon-type via 'use test.{float}').

Refine: check self-owns by SPECIFIC (component, iface, resource) tuple,
not by any-iface match. The resource-name-only alias fallback then
catches the unified consumer view and routes through the lone re-exporter
ht_drop / ht_rep / ht_new.

Also added an Instance::ResourceDrop alias-fallback at component_wrap.rs
(category C path) — for completeness, though resource_floats happens to
not hit that path. Keeps it symmetric with the LocalResource path.

Result on the trio:
- resource_floats:                NOW PASSES (was 'unknown handle index 0x110004')
- resource_with_lists:            still traps (wasm unreachable; was Option::unwrap)
- resource-import-and-export:     still traps (wasm unreachable; was Option::unwrap)

Standard 73-test suite: 73/73, 0 regressions.

The remaining two trio failures are likely the same architectural issue
in a slightly different shape — investigation continuing in Phase 5.
The previous ht_drop just zeroed mem[handle] — the wit-bindgen-rust
Box that backs the rep was never freed AND the canonical-ABI lifetime
hook (`[dtor]<resource>` exported by the owning component) never
fired. Wit-bindgen's _resource_dtor uses Box::from_raw to drop the
Box, which runs user-defined Drop impls (including any Option::take
on inner-handle fields).

Fix: in allocate_handle_tables, look up the matching dtor export
`<iface>#[dtor]<rn>` whose function origin matches the ht-owning
component, and prepend a call to it in ht_drop before zeroing.

Implementation:
- Search merged.exports for entries whose name contains `#[dtor]<rn>`
- Filter by func origin component matching the ht owner
- If found, ht_drop emits: if (handle != 0) { call dtor(load mem[handle]); store 0 }
- If not found (no Box-backed dtor — e.g. opaque-rep), just zero the slot
- handle == 0 short-circuit prevents double-free if drop is called twice

Status:
- 73/73 standard suite still passes (0 regressions)
- resource_floats: still PASSES
- resource_with_lists, resource-import-and-export: still trap with
  Option::unwrap() — but the dtor wiring is correct in principle and
  needed for memory-correctness anyway. The remaining trap is the
  cross-component handle aliasing problem (handles from different
  components share one ht namespace; deeper architectural fix needed).
The previous `name.contains("#[dtor]<rn>")` lookup picked the FIRST
matching export regardless of which interface owned it. For fixtures
with the same resource name across multiple interfaces (e.g.
resource_floats has dtors for `exports#[dtor]float`,
`imports#[dtor]float`, and `test:resource-floats/test#[dtor]float`
under one component), the contains-match could wire the wrong dtor.

The origin-comp filter (`func.origin.0 == comp_idx`) doesn't
disambiguate when one component defines the same resource in
multiple interfaces (which is exactly the re-exporter pattern with
`use foo.{r}`). `find_map` short-circuits on the first match,
making this latent unless the export iteration order happens to
favor the wrong one.

Mythos slop-hunt discover stage flagged this as
'buggy-but-untested' — `resource_floats` happened to pick the right
dtor by iteration order, masking the bug. Tightening to exact match
`<iface>#[dtor]<rn>` (with optional `$N` dedup suffix) makes the
selection unambiguous.

Validation: 73/73 standard suite unchanged. resource_floats runtime
still passes. The fix is safe (more specific match) and removes the
latent bug.
@avrabe avrabe marked this pull request as ready for review April 27, 2026 05:32
@avrabe avrabe merged commit 0551c06 into main Apr 27, 2026
4 checks passed
@avrabe avrabe deleted the feat/conditional-resource-typing branch April 27, 2026 05:32
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