diff --git a/docs/superpowers/plans/2026-03-15-resource-adapter-fixes.md b/docs/superpowers/plans/2026-03-15-resource-adapter-fixes.md new file mode 100644 index 0000000..c88332f --- /dev/null +++ b/docs/superpowers/plans/2026-03-15-resource-adapter-fixes.md @@ -0,0 +1,307 @@ +# Resource Adapter Fixes Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix two bugs that cause 5 wit-bindgen fixtures to fail at runtime: ambiguous adapter matching (3 fixtures) and missing resource detection in the fallback resolver path (2 fixtures). + +**Architecture:** Bug 1 adds an `import_module` field to `AdapterSite` so the merger disambiguates same-named functions from different interfaces. Bug 2 upgrades the fallback single-export resolver path to use `lift_info_by_core_func` (which provides the component type index) and then populate `resource_params`/`resource_results` on `AdapterRequirements`. + +**Tech Stack:** Rust, wasmparser, wasm-encoder + +--- + +## Chunk 1: Bug 1 — AdapterSite import_module disambiguation + +### Task 1: Add `import_module` field to `AdapterSite` + +**Files:** +- Modify: `meld-core/src/resolver.rs:55-80` (AdapterSite struct) +- Modify: `meld-core/src/resolver.rs:1914-1925` (per-function path — set import_module) +- Modify: `meld-core/src/resolver.rs:2051-2062` (fallback path — set import_module) + +- [ ] **Step 1: Add the field to AdapterSite** + +In `meld-core/src/resolver.rs`, add a new field after `import_name`: + +```rust +pub struct AdapterSite { + pub from_component: usize, + pub from_module: usize, + pub import_name: String, + /// The core import's module name (e.g., "test:dep/test@0.1.0"). + /// Used to disambiguate when multiple interfaces export the same function name. + pub import_module: String, + pub import_func_type_idx: Option, + // ... rest unchanged +} +``` + +- [ ] **Step 2: Set import_module in per-function path (line ~1914)** + +The per-function path iterates `interface_func_imports` which filters `imp.module == *import_name`. So the import_module is `import_name` (the resolved interface name): + +```rust +graph.adapter_sites.push(AdapterSite { + from_component: *from_comp, + from_module: from_mod_idx, + import_name: (*func_name).to_string(), + import_module: import_name.clone(), // NEW + import_func_type_idx: caller_import_type_idx, + // ... rest unchanged +}); +``` + +- [ ] **Step 3: Set import_module in fallback path (line ~2051)** + +The fallback path matches imports where `imp.name == *import_name || imp.module == *import_name`. The import_module is `import_name` here too: + +```rust +graph.adapter_sites.push(AdapterSite { + from_component: *from_comp, + from_module: from_mod_idx, + import_name: import_name.clone(), + import_module: import_name.clone(), // NEW + import_func_type_idx: fallback_import_type_idx, + // ... rest unchanged +}); +``` + +- [ ] **Step 4: Set import_module in intra-component path** + +Search for other `AdapterSite` constructions (grep for `adapter_sites.push`) and add `import_module` there too. The intra-component path (around line 2130+) should set it from the available import info. + +- [ ] **Step 5: Build to verify compilation** + +Run: `cargo build 2>&1` +Expected: Clean build (no errors) + +### Task 2: Use `import_module` in merger matching + +**Files:** +- Modify: `meld-core/src/merger.rs:869-873` (adapter site lookup) + +- [ ] **Step 1: Update the `.find()` predicate** + +In `meld-core/src/merger.rs` at line 869, add `imp.module` check: + +```rust +let resolved = graph.adapter_sites.iter().find(|site| { + site.from_component == comp_idx + && site.from_module == mod_idx + && (imp.name == site.import_name || imp.module == site.import_name) + && (imp.module == site.import_module || imp.name == site.import_module) +}); +``` + +The double-or handles both orientations: in per-function imports, `imp.module` is the interface name and `imp.name` is the function name; in fallback imports, either could match. + +- [ ] **Step 2: Build and run resource_alias test** + +Run: `cargo test --package meld-core test_fuse_wit_bindgen_resource_alias -- --nocapture 2>&1` +Expected: PASS (core fusion) + +### Task 3: Use `import_module` in adapter wiring (lib.rs) + +**Files:** +- Modify: `meld-core/src/lib.rs:369` (adapter wiring) + +- [ ] **Step 1: Update the adapter wiring match** + +In `meld-core/src/lib.rs` at line 369: + +```rust +if (imp.name == site.import_name || imp.module == site.import_name) + && (imp.module == site.import_module || imp.name == site.import_module) +{ +``` + +- [ ] **Step 2: Build and verify** + +Run: `cargo build 2>&1` +Expected: Clean build + +### Task 4: Promote fixtures to runtime tests + +**Files:** +- Modify: `meld-core/tests/wit_bindgen_runtime.rs` + +- [ ] **Step 1: Change resource_alias, with-and-resources, versions from fuse_only_test to runtime_test** + +Replace `fuse_only_test!` with `runtime_test!` for: +- `test_fuse_wit_bindgen_resource_alias` → `test_runtime_wit_bindgen_resource_alias` +- `test_fuse_wit_bindgen_with_and_resources` → `test_runtime_wit_bindgen_with_and_resources` +- `test_fuse_wit_bindgen_versions` → `test_runtime_wit_bindgen_versions` + +Remove the corresponding comment lines explaining the known failures. + +- [ ] **Step 2: Run the three promoted tests** + +Run: `cargo test --package meld-core --test wit_bindgen_runtime resource_alias with_and_resources versions -- --nocapture 2>&1` +Expected: All 3 PASS + +- [ ] **Step 3: Run full test suite** + +Run: `cargo test 2>&1` +Expected: All tests pass (0 failures) + +- [ ] **Step 4: Commit** + +```bash +git add meld-core/src/resolver.rs meld-core/src/merger.rs meld-core/src/lib.rs meld-core/tests/wit_bindgen_runtime.rs +git commit -m "fix: disambiguate adapter matching with import_module field + +AdapterSite now tracks the caller's core import module name (e.g., +\"test:dep/test@0.1.0\") in addition to the bare function name. The +merger and adapter wiring use both fields to avoid wiring same-named +functions from different interfaces to the same adapter. + +Fixes: resource_alias, with-and-resources, versions fixtures" +``` + +--- + +## Chunk 2: Bug 2 — Fallback resolver path resource detection + +### Task 5: Upgrade fallback path to use `lift_info_by_core_func` + +**Files:** +- Modify: `meld-core/src/resolver.rs:1974-1992` (fallback path callee-side) + +- [ ] **Step 1: Replace `lift_options_by_core_func` with `lift_info_by_core_func`** + +In the fallback single-export matching path (line ~1979), change: + +```rust +// BEFORE: +let callee_lift_map = to_component.lift_options_by_core_func(); +// ... +if let Some(lift_opts) = + fb_comp_core_idx.and_then(|idx| callee_lift_map.get(idx)) +{ + requirements.callee_encoding = Some(lift_opts.string_encoding); + requirements.callee_post_return = lift_opts + .post_return + .and_then(|pr_idx| fb_core_to_local.get(&pr_idx).copied()); + requirements.callee_realloc = lift_opts.realloc; +} +``` + +```rust +// AFTER: +let callee_lift_map = to_component.lift_info_by_core_func(); +// ... +if let Some((comp_type_idx, lift_opts)) = + fb_comp_core_idx.and_then(|idx| callee_lift_map.get(idx)) +{ + requirements.callee_encoding = Some(lift_opts.string_encoding); + requirements.callee_post_return = lift_opts + .post_return + .and_then(|pr_idx| fb_core_to_local.get(&pr_idx).copied()); + requirements.callee_realloc = lift_opts.realloc; + + // Populate layout and resource info from component function type + // (mirrors per-function path at line ~1820) + if let Some(ct) = to_component.get_type_definition(*comp_type_idx) + && let ComponentTypeKind::Function { + params: comp_params, + results, + } = &ct.kind + { + let size = to_component.return_area_byte_size(results); + if size > 4 { + requirements.return_area_byte_size = Some(size); + } + requirements.pointer_pair_positions = + to_component.pointer_pair_param_positions(comp_params); + requirements.result_pointer_pair_offsets = + to_component.pointer_pair_result_offsets(results); + requirements.param_copy_layouts = + collect_param_copy_layouts(to_component, comp_params); + requirements.result_copy_layouts = + collect_result_copy_layouts(to_component, results); + requirements.conditional_pointer_pairs = + to_component.conditional_pointer_pair_positions(comp_params); + requirements.conditional_result_pointer_pairs = + to_component.conditional_pointer_pair_result_positions(results); + requirements.conditional_result_flat_pairs = + to_component.conditional_pointer_pair_result_flat_positions(results); + requirements.return_area_slots = + to_component.return_area_slots(results); + + let callee_resource_map = build_resource_type_to_import(to_component); + requirements.resource_params = resolve_resource_positions( + &callee_resource_map, + &to_component.resource_param_positions(comp_params), + "[resource-rep]", + ); + requirements.resource_results = resolve_resource_positions( + &callee_resource_map, + &to_component.resource_result_positions(results), + "[resource-new]", + ); + } +} +``` + +- [ ] **Step 2: Build to verify compilation** + +Run: `cargo build 2>&1` +Expected: Clean build + +### Task 6: Promote resource_floats and xcrate to runtime tests + +**Files:** +- Modify: `meld-core/tests/wit_bindgen_runtime.rs` + +- [ ] **Step 1: Change resource_floats and xcrate from fuse_only_test to runtime_test** + +Replace `fuse_only_test!` with `runtime_test!` for: +- `test_fuse_wit_bindgen_resource_floats` → `test_runtime_wit_bindgen_resource_floats` +- `test_fuse_wit_bindgen_xcrate` → `test_runtime_wit_bindgen_xcrate` + +Remove or update the corresponding comment lines. + +- [ ] **Step 2: Run the promoted tests** + +Run: `cargo test --package meld-core --test wit_bindgen_runtime resource_floats xcrate -- --nocapture 2>&1` +Expected: Both PASS + +- [ ] **Step 3: Run full test suite** + +Run: `cargo test 2>&1` +Expected: All tests pass (0 failures) + +- [ ] **Step 4: Commit** + +```bash +git add meld-core/src/resolver.rs meld-core/tests/wit_bindgen_runtime.rs +git commit -m "fix: populate resource params in fallback resolver path + +The single-export fallback matching path now uses lift_info_by_core_func +(which provides the component type index) instead of lift_options_by_core_func. +This enables populating resource_params, pointer pair positions, copy layouts, +and all other AdapterRequirements fields that were previously only set in +the per-function interface matching path. + +Fixes: resource_floats, xcrate fixtures" +``` + +### Task 7: Final verification and PR + +- [ ] **Step 1: Run full test suite** + +Run: `cargo test 2>&1` +Expected: All tests pass + +- [ ] **Step 2: Run clippy** + +Run: `cargo clippy --all-targets 2>&1` +Expected: No warnings + +- [ ] **Step 3: Create branch and PR** + +```bash +git checkout -b fix/resource-adapter-disambiguation +git push -u origin fix/resource-adapter-disambiguation +gh pr create --title "fix: disambiguate adapter matching and populate resource params in fallback path" --body "..." +``` diff --git a/docs/superpowers/plans/2026-03-16-3-component-borrow-forwarding.md b/docs/superpowers/plans/2026-03-16-3-component-borrow-forwarding.md new file mode 100644 index 0000000..c1fccbf --- /dev/null +++ b/docs/superpowers/plans/2026-03-16-3-component-borrow-forwarding.md @@ -0,0 +1,583 @@ +# 3-Component Borrow Forwarding Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix `borrow` forwarding when T is defined by a third component (neither caller nor callee). The adapter must: (1) call the caller's `[resource-rep]` to extract the representation, (2) call the callee's `[resource-new]` to create a handle in the callee's table, and (3) pass the new handle to the callee. + +**Scenario:** In `resource_floats` (6 components, 3 main: runner -> intermediate -> leaf), a borrow parameter on `float` crosses runner -> intermediate, but `float` is defined by `leaf`. The fused module has 3 separate `[resource-rep]float` imports: +- func 3: `[export]imports` (leaf's) +- func 28: `[export]exports` (intermediate's) +- func 69: `[export]test:resource-floats/test` (runner's) + +The adapter for runner -> intermediate must use func 69 (runner's rep) and func N (intermediate's new), but the current code cannot identify func 69 as belonging to the runner. + +**Root Cause:** `build_resource_import_maps` (fact.rs:49) builds a flat `(module, field) -> merged_func_idx` map with no component ownership information. When `analyze_call_site` (fact.rs:262) tries to find "a DIFFERENT module than callee's" `[resource-rep]`, it picks the first match, which may be the wrong component's import. With 3 candidates, the heuristic of "not the callee's module" is insufficient. + +**Architecture:** Add a `resource_rep_by_component` field to `MergedModule` that maps `(component_idx, resource_name) -> merged_func_idx`, and a corresponding `resource_new_by_component`. Populate these during `add_unresolved_imports` when processing `[resource-rep]` / `[resource-new]` function imports. Use them in `analyze_call_site` to look up the caller's `[resource-rep]` via `site.from_component` and the callee's `[resource-new]` via `site.to_component`. + +**Tech Stack:** Rust, wasmparser, wasm-encoder + +--- + +## Problem Analysis + +### Current Flow (2-component case, working) + +1. **Resolver** (`resolver.rs:877-929`): `resolve_resource_positions` resolves callee's resource params to `(import_module, import_field)` pairs. Sets `callee_defines_resource = true` when callee defines T. + +2. **Adapter** (`fact.rs:236-249`): `analyze_call_site` finds the callee's `[resource-rep]` by exact `(import_module, import_field)` lookup in `resource_rep_imports`. Emits `ResourceBorrowTransfer { rep_func, new_func: None }`. + +3. **Codegen** (`fact.rs:68-78`): `emit_resource_borrow_phase0` calls `rep_func(handle) -> rep`, passes rep to callee. + +### Current Flow (3-component case, BROKEN) + +1. **Resolver** (`resolver.rs:877-929`): `resolve_resource_positions` resolves callee's resource params. Sets `callee_defines_resource = false` because callee imports T. Also resolves `caller_resource_params` against caller's resource map. + +2. **Adapter** (`fact.rs:250-299`): `analyze_call_site` enters the `else` branch (line 250). Extracts `resource_name` from the callee's `import_field`. Then does: + - **Primary search** (line 263): iterates ALL `[resource-rep]` imports looking for one with matching resource name from a DIFFERENT module than callee's. **BUG:** with 3+ components, multiple `[resource-rep]float` exist from different modules. The `.find()` picks the first match, which may be leaf's (func 3) instead of runner's (func 69). + - **Fallback** (line 270): tries `caller_resource_params` to look up by exact `(caller_op.import_module, caller_op.import_field)`. This CAN work when `caller_resource_params` is populated, but the lookup is against `resource_rep_imports` which is a flat `(module, field) -> func_idx` map, and the caller's `(module, field)` may not have been synthesized. + +3. **Synthesis gap** (`resolver.rs:1033-1114`): `synthesize_missing_resource_imports` only synthesizes imports for `resource_params` (callee-side) and `resource_results`, NOT for `caller_resource_params`. So the caller's `[resource-rep]` might not exist in the merged module's imports if no core module originally imported it. + +### Three Bugs to Fix + +1. **`build_resource_import_maps` has no component ownership** — cannot distinguish caller's `[resource-rep]` from leaf's. +2. **`synthesize_missing_resource_imports` ignores caller-side** — caller's `[resource-rep]` may not be synthesized. +3. **`analyze_call_site` uses heuristic matching** — "different module than callee's" is ambiguous with 3+ candidates. + +--- + +## Chunk 1: Add Per-Component Resource Import Tracking to MergedModule + +### Task 1: Add `resource_rep_by_component` and `resource_new_by_component` fields + +**Files:** +- Modify: `meld-core/src/merger.rs:57-124` (MergedModule struct) + +- [ ] **Step 1: Add two new fields to MergedModule** + +In `meld-core/src/merger.rs`, after `import_realloc_indices` (line 123), add: + +```rust +/// Maps (component_idx, resource_name) → merged function index for [resource-rep]. +/// Populated during add_unresolved_imports when processing [resource-rep] function imports. +/// Used by adapter generation to find the correct component's [resource-rep]. +pub resource_rep_by_component: HashMap<(usize, String), u32>, + +/// Maps (component_idx, resource_name) → merged function index for [resource-new]. +/// Populated during add_unresolved_imports when processing [resource-new] function imports. +/// Used by adapter generation to find the correct component's [resource-new]. +pub resource_new_by_component: HashMap<(usize, String), u32>, +``` + +- [ ] **Step 2: Initialize the new fields in merge() constructor** + +In `meld-core/src/merger.rs:428-449`, add to the `MergedModule { ... }` constructor: + +```rust +resource_rep_by_component: HashMap::new(), +resource_new_by_component: HashMap::new(), +``` + +- [ ] **Step 3: Initialize the new fields in test helpers** + +Search for other `MergedModule { ... }` constructions in test code (line ~2455) and add the fields there too: + +```rust +resource_rep_by_component: HashMap::new(), +resource_new_by_component: HashMap::new(), +``` + +- [ ] **Step 4: Build to verify compilation** + +Run: `cargo build 2>&1` +Expected: Clean build + +### Task 2: Populate the maps during `add_unresolved_imports` + +**Files:** +- Modify: `meld-core/src/merger.rs:1283-1417` (add_unresolved_imports method) + +The key insight: `add_unresolved_imports` iterates `graph.unresolved_imports` and emits function imports. At line 1412, when it pushes a `MergedImport`, it knows: +- `unresolved.component_idx` — which component this import belongs to +- `name` — the field name (e.g., `[resource-rep]float` or `[resource-rep]float$5`) +- `func_position - 1` — the merged function index (0-based among imports) + +We need to extract the resource name from the field and record the mapping. + +- [ ] **Step 1: Add resource tracking after each function import is emitted** + +In `meld-core/src/merger.rs`, inside the `ImportKind::Function` branch of `add_unresolved_imports`, after the `merged.imports.push(MergedImport { ... })` at line 1416, add: + +```rust +// Track per-component resource import indices for adapter generation. +// The merged function index is (func_position - 1) because we +// incremented func_position before reaching this point. +let merged_func_idx = func_position - 1; +let eff_field_str = &dedup_key.1; // effective field name (no $N suffix) +if let Some(resource_name) = eff_field_str.strip_prefix("[resource-rep]") { + merged.resource_rep_by_component.insert( + (unresolved.component_idx, resource_name.to_string()), + merged_func_idx, + ); +} else if let Some(resource_name) = eff_field_str.strip_prefix("[resource-new]") { + merged.resource_new_by_component.insert( + (unresolved.component_idx, resource_name.to_string()), + merged_func_idx, + ); +} +``` + +**Important:** Use `eff_field_str` (from `dedup_key.1`), not `name` (which may have `$N` suffix in multi-memory mode). The dedup key's field is the effective field before suffixing. + +Also important: `func_position` is incremented BEFORE the import is pushed (line 1373), and its value at that point is `previous_count + 1`. But the merged function index is `func_position - 1` (0-based). Actually, re-examine: `func_position` starts at 0 and is incremented to 1 before the first import is emitted. So `func_position - 1 = 0` for the first import. This is correct. + +Wait, let me re-read the flow. At line 1373, `func_position += 1` is executed. At this point `func_position` has been incremented past the import being processed. The import's merged index is `func_position - 1`. But we should double-check: the merged function index for an import at position P is simply P (since imports occupy indices 0..import_counts.func - 1). And `func_position` after increment equals P+1. So `func_position - 1 = P`. Correct. + +- [ ] **Step 2: Handle dedup — when an import is deduped, also record it** + +When dedup fires (line 1358, `!emitted_func.insert(dedup_key.clone())` returns false), the code `continue`s without emitting a new import. But the same merged function index should still be recorded for this component. We need to look up the deduped position. + +In `compute_unresolved_import_assignments` (line 2210-2255), the dedup logic assigns the SAME position to duplicate entries. But `add_unresolved_imports` skips emitting for dupes and does `continue`. We need to add tracking before the `continue`: + +```rust +if !is_type_mismatch && !emitted_func.insert(dedup_key.clone()) { + // Duplicate with matching type — skip emitting, but still + // record per-component resource tracking. + // Look up the already-assigned position from the pre-computed assignments. + if let Some(&position) = unresolved_assignments_ref.func.get(&( + unresolved.component_idx, + unresolved.module_idx, + unresolved.module_name.clone(), + unresolved.field_name.clone(), + )) { + let eff_field_str = &dedup_key.1; + if let Some(resource_name) = eff_field_str.strip_prefix("[resource-rep]") { + merged.resource_rep_by_component.insert( + (unresolved.component_idx, resource_name.to_string()), + position, + ); + } else if let Some(resource_name) = eff_field_str.strip_prefix("[resource-new]") { + merged.resource_new_by_component.insert( + (unresolved.component_idx, resource_name.to_string()), + position, + ); + } + } + continue; +} +``` + +**Problem:** `add_unresolved_imports` currently does NOT have access to `unresolved_assignments`. It only takes `dedup_info`. We need to pass `unresolved_assignments` through. + +- [ ] **Step 3: Pass UnresolvedImportAssignments to add_unresolved_imports** + +In `meld-core/src/merger.rs`, update the `add_unresolved_imports` signature (line 1283): + +```rust +fn add_unresolved_imports( + &self, + graph: &DependencyGraph, + merged: &mut MergedModule, + shared_memory_plan: Option<&SharedMemoryPlan>, + dedup_info: &ImportDedupInfo, + unresolved_assignments: &UnresolvedImportAssignments, // NEW +) -> Result<()> { +``` + +Update the call site in `merge()` (line 466): + +```rust +self.add_unresolved_imports( + graph, + &mut merged, + shared_memory_plan.as_ref(), + &dedup_info, + &unresolved_assignments, // NEW +)?; +``` + +- [ ] **Step 4: Build and run existing tests** + +Run: `cargo test 2>&1` +Expected: All existing tests pass. The new fields are populated but not yet consumed. + +--- + +## Chunk 2: Synthesize Caller-Side Resource Imports + +### Task 3: Extend `synthesize_missing_resource_imports` for caller-side + +**Files:** +- Modify: `meld-core/src/resolver.rs:1033-1114` (synthesize_missing_resource_imports) + +Currently, `synthesize_missing_resource_imports` only collects needs from `site.requirements.resource_params` and `site.requirements.resource_results`, using `site.to_component` as the component_idx. For the 3-component case, the caller's `[resource-rep]` (from `caller_resource_params`) also needs to be synthesized. + +- [ ] **Step 1: Add caller-side resource needs** + +In `meld-core/src/resolver.rs`, inside `synthesize_missing_resource_imports`, after the loop that collects callee-side needs (line 1041-1056), add: + +```rust +// Also collect caller-side resource imports needed for 3-component chains. +// When callee_defines_resource is false, the adapter uses the CALLER's +// [resource-rep] and the CALLEE's [resource-new]. The callee's [resource-new] +// is already covered above (via resource_params with "[resource-rep]" → +// we need to also synthesize the [resource-new] counterpart and the +// caller's [resource-rep]). +for site in &graph.adapter_sites { + for op in &site.requirements.caller_resource_params { + needed.push(( + op.import_module.clone(), + op.import_field.clone(), + site.from_component, // CALLER component + )); + } + // For 3-component chains, also synthesize callee's [resource-new] + // (the callee's resource_params has [resource-rep] entries, but + // the adapter also needs [resource-new] for the same resource). + for op in &site.requirements.resource_params { + if !op.is_owned && !op.callee_defines_resource { + let new_field = op.import_field.replace("[resource-rep]", "[resource-new]"); + needed.push(( + op.import_module.clone(), + new_field, + site.to_component, + )); + } + } +} +``` + +- [ ] **Step 2: Build and verify** + +Run: `cargo build 2>&1` +Expected: Clean build + +- [ ] **Step 3: Run tests** + +Run: `cargo test 2>&1` +Expected: All existing tests pass + +--- + +## Chunk 3: Use Per-Component Maps in Adapter Generation + +### Task 4: Write failing test for 3-component borrow forwarding + +**Files:** +- Modify: `meld-core/tests/wit_bindgen_runtime.rs` + +- [ ] **Step 1: Promote resource_floats to runtime_test** + +In `meld-core/tests/wit_bindgen_runtime.rs`, find: + +```rust +// resource_floats: 3-component borrow forwarding needs caller→callee handle transfer +fuse_only_test!(test_fuse_wit_bindgen_resource_floats, "resource_floats"); +``` + +Replace with: + +```rust +runtime_test!(test_runtime_wit_bindgen_resource_floats, "resource_floats"); +``` + +- [ ] **Step 2: Run the test to confirm it fails** + +Run: `cargo test --package meld-core --test wit_bindgen_runtime resource_floats -- --nocapture 2>&1` +Expected: FAIL (runtime trap or wrong resource handle). This is our regression test. + +### Task 5: Refactor `analyze_call_site` to use per-component maps + +**Files:** +- Modify: `meld-core/src/adapter/fact.rs:132-303` (analyze_call_site) + +The current approach passes flat `resource_rep_imports` / `resource_new_imports` maps. We need to also pass (or access) the per-component maps from `MergedModule`. + +- [ ] **Step 1: Simplify the 3-component branch in analyze_call_site** + +Replace the entire `else` block (lines 250-299) with a direct lookup using the per-component maps: + +```rust +} else { + // 3-component case: neither caller nor callee defines the resource. + // + // Step 1: caller's [resource-rep](handle) → rep + // Use merged.resource_rep_by_component keyed by (from_component, resource_name) + // + // Step 2: callee's [resource-new](rep) → new_handle + // Use merged.resource_new_by_component keyed by (to_component, resource_name) + // + // Step 3: pass new_handle to callee + let resource_name = op + .import_field + .strip_prefix("[resource-rep]") + .unwrap_or(&op.import_field); + + // Look up caller's [resource-rep] by component index + let caller_rep_func = merged + .resource_rep_by_component + .get(&(site.from_component, resource_name.to_string())) + .copied() + .or_else(|| { + // Fallback: try caller_resource_params exact match + caller_ops.get(&op.flat_idx).and_then(|caller_op| { + resource_rep_imports + .get(&(caller_op.import_module.clone(), caller_op.import_field.clone())) + .copied() + }) + }) + .or_else(|| { + // Last resort: original heuristic (different module than callee's) + resource_rep_imports + .iter() + .find(|((module, field), _)| { + field.ends_with(resource_name) + && field.starts_with("[resource-rep]") + && *module != op.import_module + }) + .map(|(_, &idx)| idx) + }); + + // Look up callee's [resource-new] by component index + let callee_new_func = merged + .resource_new_by_component + .get(&(site.to_component, resource_name.to_string())) + .copied() + .or_else(|| { + // Fallback: original lookup + let new_field = op.import_field.replace("[resource-rep]", "[resource-new]"); + resource_new_imports + .get(&(op.import_module.clone(), new_field)) + .copied() + }); + + if let Some(rep_func) = caller_rep_func { + options + .resource_rep_calls + .push(super::ResourceBorrowTransfer { + param_idx: op.flat_idx, + rep_func, + new_func: callee_new_func, + }); + } else { + log::warn!( + "3-component borrow forwarding: no caller [resource-rep] found for \ + resource '{}' (from_component={}, to_component={})", + resource_name, + site.from_component, + site.to_component, + ); + } +} +``` + +**Layered fallback strategy:** +1. **Primary:** `resource_rep_by_component` with exact `(from_component, resource_name)` key. +2. **Secondary:** `caller_resource_params` exact `(module, field)` from resolver data. +3. **Tertiary:** Original heuristic ("different module") for backward compatibility. + +The primary path is the new, correct behavior. Fallbacks ensure no regression for edge cases where the new maps might not be populated (e.g., if synthesis was skipped). + +- [ ] **Step 2: Build and verify compilation** + +Run: `cargo build 2>&1` +Expected: Clean build + +### Task 6: Verify the fix works end-to-end + +- [ ] **Step 1: Run the promoted resource_floats test** + +Run: `cargo test --package meld-core --test wit_bindgen_runtime resource_floats -- --nocapture 2>&1` +Expected: PASS + +- [ ] **Step 2: Run the full test suite** + +Run: `cargo test 2>&1` +Expected: All tests pass + +- [ ] **Step 3: Run clippy** + +Run: `cargo clippy --all-targets 2>&1` +Expected: No warnings + +--- + +## Chunk 4: Edge Cases and Robustness + +### Task 7: Handle P2 wrapper components + +**Context:** In the `resource_floats` fixture, runner (comp 5) has NO `[resource-rep]` canonical entry. Its handles live in the P2 wrapper's `[export]test:resource-floats/test` instance (func 69). The P2 wrapper is a DIFFERENT component than the runner's main module. + +The `caller_resource_map` (built by `build_resource_type_to_import` in the resolver) may not find a resource entry for the runner component because the canonical `ResourceRep` is in the wrapper component, not in the runner's component. + +- [ ] **Step 1: Verify that unresolved import tracking handles the wrapper case** + +During `add_unresolved_imports`, the unresolved import for `[resource-rep]float` with `component_idx` matching the runner should exist (either originally or via synthesis). Check that `resource_rep_by_component` is populated correctly. + +Add a debug log in the population code (Task 2, Step 1): + +```rust +log::debug!( + "resource_rep_by_component: comp={}, resource={}, merged_func={}", + unresolved.component_idx, + resource_name, + merged_func_idx, +); +``` + +Run: `RUST_LOG=debug cargo test --package meld-core --test wit_bindgen_runtime resource_floats -- --nocapture 2>&1` + +Examine logs to verify the runner's component index maps to the correct `[resource-rep]float` merged function index. + +- [ ] **Step 2: If runner's [resource-rep] is not being tracked correctly** + +The issue may be that the unresolved import for runner's `[resource-rep]float` has `component_idx` equal to the P2 WRAPPER's component index, not the runner's main component index. In that case, the lookup `(site.from_component, "float")` would fail because `site.from_component` is the runner and the import is registered under the wrapper. + +**Fix:** In `add_unresolved_imports`, when recording `resource_rep_by_component`, also check if this import's module name matches a known wrapper pattern (e.g., `[export]test:*`) and record it under the wrapped component's index as well. + +Alternative (simpler): In `analyze_call_site`, when the primary lookup fails, iterate all entries in `resource_rep_by_component` with matching resource_name and pick the one whose component was "imported by" `from_component`. This requires no new data but is less precise. + +**Decision:** Defer this to runtime testing. If the unresolved imports' `component_idx` already correctly identifies the wrapper's component index as the runner's environment (which is likely since the P2 wrapper component is composed to provide the runner's imports), then `resource_rep_by_component` keyed by the wrapper's comp_idx would need a different lookup. The fallback chain in Task 5 handles this case. + +### Task 8: Unit test for per-component resource map population + +**Files:** +- Modify: `meld-core/src/merger.rs` (test module, line ~2700+) + +- [ ] **Step 1: Write a unit test that verifies resource_rep_by_component is populated** + +Add a test that creates a DependencyGraph with unresolved imports containing `[resource-rep]foo` for two different components, merges them, and checks that `resource_rep_by_component` has the correct mapping: + +```rust +#[test] +fn test_resource_rep_by_component_populated() { + // Create a graph with two components, each having a [resource-rep]foo import + // from different modules. Verify that after merge, resource_rep_by_component + // maps (comp_0, "foo") and (comp_1, "foo") to different merged func indices. + + // Setup: two components with minimal core modules... + // (follow existing test patterns in the file, e.g., test_unresolved_import_dedup) + + // Assert: + // assert!(merged.resource_rep_by_component.contains_key(&(0, "foo".to_string()))); + // assert!(merged.resource_rep_by_component.contains_key(&(1, "foo".to_string()))); + // assert_ne!( + // merged.resource_rep_by_component[&(0, "foo".to_string())], + // merged.resource_rep_by_component[&(1, "foo".to_string())], + // ); +} +``` + +- [ ] **Step 2: Run the test** + +Run: `cargo test --package meld-core test_resource_rep_by_component -- --nocapture 2>&1` +Expected: PASS + +--- + +## Chunk 5: Final Verification and Cleanup + +### Task 9: Verify all resource fixtures + +- [ ] **Step 1: Run all resource-related fixture tests** + +```bash +cargo test --package meld-core --test wit_bindgen_runtime resource -- --nocapture 2>&1 +``` + +Expected: All resource tests pass (resources, resource_alias, resource_aggregates, resource_floats, resource_borrow_in_record, etc.) + +- [ ] **Step 2: Run the full test suite** + +```bash +cargo test 2>&1 +``` + +Expected: All tests pass + +- [ ] **Step 3: Run clippy** + +```bash +cargo clippy --all-targets 2>&1 +``` + +Expected: No warnings + +### Task 10: Commit + +- [ ] **Step 1: Stage and commit** + +```bash +git add \ + meld-core/src/merger.rs \ + meld-core/src/resolver.rs \ + meld-core/src/adapter/fact.rs \ + meld-core/tests/wit_bindgen_runtime.rs +git commit -m "fix: 3-component borrow forwarding with per-component resource tracking + +When borrow crosses an adapter where neither caller nor callee defines +T (T defined by a third component), the adapter must use the caller's +[resource-rep] to extract the representation and the callee's [resource-new] +to create a handle in the callee's table. + +Previously, the adapter generator used a flat (module, field) map for +resource imports and a heuristic ('different module than callee') to find +the caller's [resource-rep]. With 3+ components, multiple [resource-rep] +imports exist and the heuristic picked the wrong one. + +Changes: +- Add resource_rep_by_component and resource_new_by_component fields to + MergedModule, mapping (component_idx, resource_name) to merged func idx +- Populate these maps during add_unresolved_imports +- Extend synthesize_missing_resource_imports to synthesize caller-side + [resource-rep] and callee-side [resource-new] for 3-component chains +- Refactor analyze_call_site to use per-component maps with layered + fallback for backward compatibility + +Fixes: resource_floats fixture (3-component borrow forwarding)" +``` + +--- + +## Appendix: Key File Locations + +| File | Lines | What to Change | +|------|-------|---------------| +| `meld-core/src/merger.rs` | 57-124 | Add `resource_rep_by_component`, `resource_new_by_component` to MergedModule | +| `meld-core/src/merger.rs` | 428-449 | Initialize new fields in merge() constructor | +| `meld-core/src/merger.rs` | 1283-1417 | Populate maps in add_unresolved_imports; pass assignments through | +| `meld-core/src/merger.rs` | ~2455 | Initialize new fields in test helper | +| `meld-core/src/resolver.rs` | 1033-1114 | Extend synthesize_missing_resource_imports for caller-side | +| `meld-core/src/adapter/fact.rs` | 250-299 | Replace heuristic with per-component map lookup | +| `meld-core/tests/wit_bindgen_runtime.rs` | ~654 | Promote resource_floats to runtime_test | + +## Appendix: Data Flow Diagram + +``` + RESOLVER (resolve) + │ + ├─ resolve_resource_positions(callee_map, ...) → resource_params + ├─ resolve_resource_positions(caller_map, ...) → caller_resource_params + └─ synthesize_missing_resource_imports + ├─ callee: resource_params → [resource-rep] imports + ├─ callee: resource_params (non-owned, !callee_defines) → [resource-new] imports ← NEW + └─ caller: caller_resource_params → [resource-rep] imports ← NEW + + MERGER (merge) + │ + ├─ compute_unresolved_import_assignments → positions + └─ add_unresolved_imports + ├─ emit MergedImport for each unresolved + ├─ populate import_memory_indices, import_realloc_indices + └─ populate resource_rep_by_component, resource_new_by_component ← NEW + + ADAPTER (generate) + │ + ├─ build_resource_import_maps → flat (module, field) → func_idx (EXISTING, kept as fallback) + └─ analyze_call_site + └─ 3-component branch: + ├─ PRIMARY: merged.resource_rep_by_component[(from_comp, name)] ← NEW + ├─ FALLBACK: caller_resource_params exact match (EXISTING) + └─ LAST: heuristic "different module" (EXISTING) +``` diff --git a/safety/stpa/resource-handle-stpa.yaml b/safety/stpa/resource-handle-stpa.yaml new file mode 100644 index 0000000..f432b6d --- /dev/null +++ b/safety/stpa/resource-handle-stpa.yaml @@ -0,0 +1,1078 @@ +# STPA Analysis: Resource Handle Management in Meld +# +# Systematic analysis of all failure modes for resource types (own, borrow) +# in the static component fusion pipeline. +# +# Context: The Component Model's resource types use handle tables per component +# instance. When meld fuses multiple components into a single core module, the +# adapter layer must correctly translate handles between the original +# component-instance tables and the fused module's shared execution environment. +# The canonical ABI spec (commit deb0b0a) defines lift_own, lower_own, +# lift_borrow, lower_borrow, resource.new, resource.rep, and resource.drop +# with precise semantics around handle table management, borrow tracking +# (num_lends), and destructor invocation. +# +# Analysis date: 2026-03-16 +# Author: STPA analysis agent +# Spec baseline: Component Model commit deb0b0a, Canonical ABI +# Source files: +# - meld-core/src/parser.rs (resource_param_positions, resource_result_positions, resolve_to_resource) +# - meld-core/src/resolver.rs (ResolvedResourceOp, build_resource_type_to_import, resolve_resource_positions) +# - meld-core/src/adapter/mod.rs (ResourceBorrowTransfer, AdapterOptions.resource_rep_calls) +# - meld-core/src/adapter/fact.rs (Phase 0, emit_resource_borrow_phase0, analyze_call_site) +# - meld-core/src/component_wrap.rs (ImportResolution::LocalResource, resource.drop/new/rep) + +# ============================================================================= +# New Hazard: Resource Handle Mismanagement +# ============================================================================= + +hazards: + - id: H-11 + title: Resource handle translation produces incorrect or leaked handles + description: > + Meld produces a fused module where resource handles (own, borrow) + are not correctly translated across component boundaries. This includes: + passing raw handles where representations are expected (or vice versa), + leaking handles that are never dropped, creating handles in the wrong + component's table, or failing to invoke destructors for owned resources. + The canonical ABI requires precise handle↔representation conversion + depending on whether the callee is the resource's implementation component. + losses: [L-1, L-2] + +sub-hazards: + - id: H-11.1 + parent: H-11 + title: Borrow handle passed as raw representation to non-impl callee + description: > + The adapter passes a borrow handle value directly to a callee that + does NOT define T. Per the canonical ABI, lower_borrow creates a new + borrowed handle in the destination's table when cx.inst is NOT t.rt.impl. + Passing the raw handle means the callee interprets it as a handle index + in its own table, which may point to a different resource or be out of + bounds, causing a trap or silent wrong-resource access. + + - id: H-11.2 + parent: H-11 + title: Own handle converted when callee should receive it unchanged + description: > + The adapter applies resource.rep to an own handle before passing it + to the callee. Per the canonical ABI, lower_own returns the raw rep only + when cx.inst IS t.rt.impl. The callee's core function calls from_handle/ + resource.rep internally. If the adapter pre-converts, the callee + double-converts and receives a garbage value. + + - id: H-11.3 + parent: H-11 + title: Own handle leaked in resource table (never dropped/freed) + description: > + When an own handle is transferred across components, the source + component should remove it (lift_own). In the fused module, if the + adapter passes the handle but does not arrange for removal from the + source table (no resource.drop call on the source side), the source + table grows unboundedly. In short-lived tests this is benign, but in + long-running systems it constitutes a resource leak leading to table + overflow (MAX_LENGTH = 2^28 - 1) and eventual trap. + + - id: H-11.4 + parent: H-11 + title: Resource.rep called on wrong handle table + description: > + In a 3-component chain (A→B→C), the adapter needs to call the CALLER's + resource.rep (to extract rep from caller's handle table) followed by the + CALLEE's resource.new (to create a handle in callee's table). If the + adapter calls the callee's resource.rep instead, it looks up the handle + index in the wrong table, returning an unrelated rep or trapping. + + - id: H-11.5 + parent: H-11 + title: Resource inside aggregate type (record, tuple, option, variant) not detected + description: > + The parser's resource_param_positions and resource_result_positions only + inspect top-level function parameters. A borrow or own nested + inside a record, tuple, option>, result, E>, or + variant is NOT detected. The adapter passes the aggregate type by value + or pointer without converting the embedded resource handle, violating + the canonical ABI lifting/lowering rules. + + - id: H-11.6 + parent: H-11 + title: Resource result (own) not converted via resource.new for caller + description: > + The resolver collects resource_results (own return values needing + rep→handle conversion via resource.new), but the adapter generator + (fact.rs) never processes them. The callee's core function returns a + raw rep, and the adapter passes it unchanged to the caller. If the + caller is not the resource's impl component, the caller interprets the + rep as a handle index, which may be out of bounds or point to a wrong + resource in the caller's table. + + - id: H-11.7 + parent: H-11 + title: Resource type identity mismatch due to aliasing + description: > + Resource types use object identity in the canonical ABI. When a resource + type is imported/re-exported through multiple components, the type ID + in one component may differ from the ID in another due to aliasing + (ExportAlias). The resolver's build_resource_type_to_import has Step 6 + (alias propagation) but uses a single-pass approach that may miss + multi-level alias chains (A→B→C where A aliases B which aliases C). + + - id: H-11.8 + parent: H-11 + title: Destructor not forwarded for locally-defined resources in P2 wrapper + description: > + When the P2 wrapper defines a local resource type, it must connect the + destructor (dtor) to the fused module's exported dtor function. If the + dtor export name format does not match (e.g., naming convention mismatch + between component's export alias and the wrapper's expected format), + the resource type is created without a destructor. Dropping the resource + silently succeeds without cleanup, leaking the underlying representation. + +# ============================================================================= +# Unsafe Control Actions: Resource Handling +# ============================================================================= + +resource-ucas: + control-action: "Generate resource handle translation in adapter trampolines" + controller: CTRL-ADAPTER + + not-providing: + - id: UCA-A-14 + description: > + Adapter generator does not emit resource.rep conversion for borrow + parameter when the callee defines T and expects raw representation + context: > + 2-component call where callee defines the resource type and the + function takes borrow parameter + hazards: [H-11, H-11.1] + rationale: > + Per canonical ABI lower_borrow: when cx.inst IS t.rt.impl, return v + (the rep) directly. Without conversion, callee receives a handle index + from the caller's table, not the raw pointer/representation it expects. + + - id: UCA-A-15 + description: > + Adapter generator does not emit resource.rep→resource.new chain for + borrow parameter in 3-component chain (caller→adapter→callee, + neither defines T) + context: > + 3-component chain A→B→C where resource T is defined by component D, + and the current call passes borrow from A to C + hazards: [H-11, H-11.4] + rationale: > + The caller's handle table maps caller-local handles to reps. The + callee's table is separate. The adapter must (1) call caller's + resource.rep to extract rep from caller's table, then (2) call + callee's resource.new to create a new handle in callee's table. + Missing either step causes wrong-table lookup. + + - id: UCA-A-16 + description: > + Adapter generator does not detect resource types nested inside + record, tuple, option, result, or variant parameters + context: > + Cross-component function takes record { name: string, handle: borrow } + or option> or tuple, u32> + hazards: [H-11, H-11.5] + rationale: > + resource_param_positions only checks top-level ComponentValType via + resolve_to_resource. It does not recurse into Defined(Record(...)), + Defined(Tuple(...)), Defined(Option(...)), etc. to find nested + Own(T)/Borrow(T) values. The inner handle is passed as a raw i32 + without conversion. + + - id: UCA-A-17 + description: > + Adapter generator does not emit resource.new conversion for own + return values when the caller does not define T + context: > + Cross-component call returns own and caller is NOT the impl + component for T + hazards: [H-11, H-11.6] + rationale: > + Per canonical ABI lower_own: when cx.inst is NOT t.rt.impl, a new + ResourceHandle is created in the caller's table. The callee's core + function returns a rep, and the adapter must call resource.new to + wrap it in the caller's table. The resolver populates resource_results + but fact.rs never reads them — the rep flows through unchanged. + + - id: UCA-A-18 + description: > + Adapter generator does not arrange for destruction of source-side + own handle after cross-component transfer + context: > + Cross-component call passes own parameter (ownership transfer) + hazards: [H-11, H-11.3] + rationale: > + Per canonical ABI lift_own: the handle is removed from the source's + table. In the fused module, the adapter must ensure the source-side + handle entry is freed (resource.drop on source side without invoking + dtor, since ownership is transferring not destroying). Currently the + adapter passes the handle through and the source-side entry persists, + leaking table slots. + + providing: + - id: UCA-A-19 + description: > + Adapter converts own parameter via resource.rep when callee should + receive handle unchanged + context: > + Cross-component call passes own to callee that is NOT the impl + component for T (callee will call resource.rep internally) + hazards: [H-11, H-11.2] + rationale: > + The current code correctly skips own in the resource_rep_calls + loop (is_owned continue). However, if this guard is accidentally + removed or the is_owned classification is wrong, double conversion + occurs. This UCA documents the invariant. + + - id: UCA-A-20 + description: > + Adapter calls callee's resource.rep instead of caller's resource.rep + in 3-component chain + context: > + 3-component chain where borrow passes from A to C, T defined + by component B which is neither A nor C + hazards: [H-11, H-11.4] + rationale: > + callee_defines_resource is false, so the code enters the 3-component + path. The resource.rep import lookup uses field.ends_with(resource_name) + with module != op.import_module. If multiple resource.rep imports + match (e.g., same resource name in different interfaces), the wrong + one may be selected. The first-match strategy is non-deterministic + when multiple candidates exist (HashMap iteration order). + + - id: UCA-A-21 + description: > + Adapter emits resource.new with wrong resource type index in P2 + wrapper for imported resource + context: > + Component imports a resource type from another component and the + P2 wrapper must define that resource type locally + hazards: [H-11, H-11.7, H-8] + rationale: > + Component_wrap.rs tracks component_type_idx as a running counter. + If resource type aliasing is not correctly followed (e.g., aliased + type index differs from the canonical type index used by resource.new), + the wrapper defines the wrong resource type and all handle operations + reference the wrong table. + + too-early-too-late: + - id: UCA-A-22 + description: > + Adapter emits resource.rep AFTER passing parameters to callee + instead of BEFORE + context: > + Cross-component call with borrow parameter in multi-memory mode + where both resource conversion and memory copy are needed + hazards: [H-11, H-11.1] + rationale: > + Phase 0 (resource conversion) must run before Phase 1 (memory copy) + and Phase 2 (callee call). If instruction ordering is wrong, the + callee receives an unconverted handle. Current code emits Phase 0 + first (line 338, 479, 594, 968, 1620 in fact.rs), but this ordering + invariant is fragile — any refactoring that moves Phase 0 after the + call instruction would silently break correctness. + + stopped-too-soon: + - id: UCA-A-23 + description: > + Resource type alias chain resolution stops at first level, missing + multi-level alias chains + context: > + Resource type aliased through two or more levels (ExportAlias(A) → + ExportAlias(B) → ResourceType) + hazards: [H-11, H-11.7] + rationale: > + build_resource_type_to_import Step 6 iterates component_type_defs + once, propagating entries from target to alias and vice versa. If + the alias chain has depth > 1 (A → B → C), a single pass may only + propagate C → B, leaving A unresolved. A second pass or transitive + closure is needed. + +# ============================================================================= +# Parser UCAs for Resource Handling +# ============================================================================= + +resource-parser-ucas: + control-action: "Parse and extract resource type information" + controller: CTRL-PARSER + + not-providing: + - id: UCA-P-11 + description: > + Parser does not detect resource types nested inside aggregate types + (records, tuples, options, results, variants) + context: > + Function signature includes record { handle: borrow, data: u32 } + or option> + hazards: [H-11, H-11.5] + rationale: > + resolve_to_resource only matches top-level Own(T), Borrow(T), and + Type(idx) that directly resolves to Own/Borrow. It does not recurse + into Defined(Record(...)), Defined(Tuple(...)), etc. This means + resource_param_positions returns empty for functions where all + resource handles are inside aggregate types. + + - id: UCA-P-12 + description: > + Parser does not track resource type provenance (which component + instance defines each resource type) + context: > + Multiple components define different resource types with the same name + (e.g., both component A and component B define a resource named "handle") + hazards: [H-11, H-11.7, H-1] + rationale: > + Resource type identity in the canonical ABI is by object identity, + not by name. Two resources named "handle" from different components + are distinct types. The parser uses u32 type indices which are + component-local. The resolver must correctly map these to distinct + resource types, but without provenance tracking, name collisions + can cause misidentification. + + providing: + - id: UCA-P-13 + description: > + Parser reports incorrect flat_count for resource types, causing + flat_idx calculation to be wrong for subsequent parameters + context: > + Function with mixed resource and non-resource parameters, e.g., + (borrow, string, own) + hazards: [H-11, H-4] + rationale: > + Resources have flat_count = 1 (a single i32 handle). If flat_count + incorrectly returns 0 or 2 for a resource type, all subsequent + parameter positions are shifted, causing the adapter to convert the + wrong parameter slot. + +# ============================================================================= +# Wrapper UCAs for Resource Handling +# ============================================================================= + +resource-wrapper-ucas: + control-action: "Emit resource type definitions and canon builtins in P2 wrapper" + controller: CTRL-WRAPPER + + not-providing: + - id: UCA-W-4 + description: > + Wrapper does not resolve toplevel resource type imports (resource + imported at the component level, not inside an interface) + context: > + Component imports a bare resource type at the top level, not nested + inside a named interface instance + hazards: [H-8, H-11] + rationale: > + resolve_import_to_instance builds its lookup map from + ComponentAliasEntry::InstanceExport entries. A toplevel resource + import does not create an InstanceExport alias, so the lookup fails. + This is the root cause of the resource-import-and-export fixture + failure. + + - id: UCA-W-5 + description: > + Wrapper does not emit resource.rep or resource.new canon builtins + for imported resources that the fused module's adapters need + context: > + Fused module imports [resource-rep]X and [resource-new]X functions + that the P2 wrapper must provide via canon builtins + hazards: [H-8, H-11] + rationale: > + The wrapper handles [resource-drop] specially (lines 1055-1075 of + component_wrap.rs) but routes [resource-rep] and [resource-new] + through the normal instance resolution path. If the fused module + imports these from an [export]-prefixed module, they are handled by + the LocalResource path. But if they come from a non-[export] module, + they fall through to resolve_import_to_instance which may fail. + + providing: + - id: UCA-W-6 + description: > + Wrapper defines resource type with wrong destructor (dtor connects + to wrong function or no function) + context: > + Component defines a resource with destructor, fused module exports + the dtor under a name that does not match the wrapper's expected + format "{interface}#[dtor]{resource}" + hazards: [H-8, H-11, H-11.8] + rationale: > + The naming convention for destructor exports is an implementation + detail of wit-bindgen. If a component is compiled with a different + tool or a different naming convention, the wrapper will not find the + dtor export and will create the resource type without a destructor. + Resources dropped by the runtime will not be cleaned up. + +# ============================================================================= +# Loss Scenarios for Resource Handle UCAs +# ============================================================================= + +loss-scenarios: + - id: LS-RH-1 + title: Borrow handle passed as-is to impl component without resource.rep + uca: UCA-A-14 + hazards: [H-11, H-11.1] + type: inadequate-control-algorithm + scenario: > + Component A calls component B's function f(borrow) where B defines T. + The adapter should call B's [resource-rep](handle) to extract the raw + representation, then pass the rep to B's core function (which expects rep + via the lower_borrow identity path). Instead, the adapter passes the + handle index directly. B's core function interprets the handle index as + a memory address (rep), reading from an arbitrary low address and + returning garbage or trapping on out-of-bounds access [H-11.1, H-1]. + causal-factors: + - resource_rep_calls is empty because build_resource_import_maps did not + find matching [resource-rep] import in merged module + - Fallback resolver path does not populate resource_params + - resolve_resource_positions returned empty due to unresolved type ID + + - id: LS-RH-2 + title: 3-component chain uses wrong resource.rep table + uca: UCA-A-20 + hazards: [H-11, H-11.4] + type: inadequate-control-algorithm + scenario: > + Components A, B, C form a chain. A passes borrow to C via an adapter. + T is defined by B. Both A and B have [resource-rep] imports in the merged + module. The adapter's first-match lookup (field.ends_with(resource_name) + && module != callee's module) selects B's [resource-rep] instead of A's. + B's resource.rep looks up the handle in B's table, not A's. The handle + index from A is invalid in B's table, causing a trap or returning an + unrelated representation [H-11.4]. This is the root cause of the + resource_floats fixture failure. + causal-factors: + - resource_rep_imports HashMap iteration order is non-deterministic + - .find() returns first match, not necessarily the correct one + - No assertion that the matched [resource-rep] belongs to the caller's + component, only that it differs from the callee's module name + process-model-flaw: > + The adapter generator assumes there is exactly one [resource-rep] import + per resource name from a non-callee module. With 3+ components, there + may be multiple, and the correct one is the caller's. + + - id: LS-RH-3 + title: Own result not wrapped in caller's handle table + uca: UCA-A-17 + hazards: [H-11, H-11.6] + type: inadequate-control-algorithm + scenario: > + Component A calls component B's function that returns own. B's core + function returns a raw representation (rep). Per the canonical ABI, + lower_own should create a ResourceHandle(T, rep, own=True) in A's handle + table when A is NOT T's impl. The adapter passes rep through unchanged. + A receives the rep as if it were a handle index, attempts to look it up + in its table, and traps (out of bounds) or reads a wrong entry + [H-11.6]. This is likely contributing to the resource_aggregates + fixture failure. + causal-factors: + - resource_results is populated by the resolver but never consumed by + fact.rs analyze_call_site + - No Phase N in the adapter handles own result conversion + - Comment in fact.rs states "Results are never converted" which is + incorrect for cross-component own returns + + - id: LS-RH-4 + title: Borrow inside record not converted + uca: UCA-A-16 + hazards: [H-11, H-11.5] + type: inadequate-process-model + scenario: > + A function takes record { name: string, handle: borrow }. The + parser's resource_param_positions iterates top-level params and calls + resolve_to_resource on each. The record type resolves to Defined(Record), + not Own/Borrow, so resolve_to_resource returns None. The handle field + inside the record is never detected. The adapter copies the record's + bytes across memories (if it crosses memories) but does not convert + the handle field from caller-table index to callee-table index. The + callee reads the handle field and interprets the caller's handle index + as its own, which may be invalid or refer to a different resource + [H-11.5]. This is the root cause of the resource_borrow_in_record + fixture failure. + causal-factors: + - resolve_to_resource does not recurse into record/tuple/variant fields + - resource_param_positions treats the entire record as a single non-resource + entity + - No nested resource detection in CopyLayout computation + process-model-flaw: > + The parser assumes resource types only appear as top-level function + parameters, never nested inside aggregate types. The Component Model + spec allows borrow and own anywhere a value type is accepted, + including inside records, tuples, options, results, and variants. + + - id: LS-RH-5 + title: Own handle leaks in resource table during ownership transfer + uca: UCA-A-18 + hazards: [H-11, H-11.3] + type: inadequate-control-algorithm + scenario: > + Component A calls a function with own parameter, transferring + ownership to component B. Per the canonical ABI, lift_own removes the + handle from A's table. In the fused module, the adapter passes the + handle through without calling resource.drop on A's side (no transfer + semantics). A's table entry persists but A has logically given up + ownership. If A later attempts to drop the handle, a double-free trap + occurs. If A never drops it, the table entry leaks. Over many calls, + the table fills to MAX_LENGTH (2^28 - 1) and traps [H-11.3]. + This is related to the ownership fixture failure. + causal-factors: + - Adapter treats own as passthrough (skips is_owned in resource loop) + - No mechanism to call resource.drop on source side without invoking dtor + - Component Model spec requires "detach without dtor" which is not + available as a single canon op + + - id: LS-RH-6 + title: P2 wrapper cannot resolve toplevel resource import + uca: UCA-W-4 + hazards: [H-8, H-11] + type: inadequate-process-model + scenario: > + Component has a toplevel resource type import (not inside an interface). + The fused module imports [resource-drop]X, [resource-rep]X, and + [resource-new]X from a module name that does not correspond to any + component instance. The wrapper's resolve_import_to_instance fails to + find a matching instance. The wrapper falls through to the error path: + "cannot resolve fused import ... to a component instance" [H-8]. + This is the root cause of the resource-import-and-export fixture failure. + causal-factors: + - build_instance_func_map only indexes ComponentAliasEntry::InstanceExport + entries with kind Func + - Toplevel resource imports create resource canon builtins without a + containing interface instance + - Category C fallback in import resolution only handles [resource-drop], + not [resource-rep] or [resource-new] + process-model-flaw: > + The wrapper assumes all non-[export]-prefixed resource operations resolve + through instance aliases. Toplevel resource imports are a valid pattern + but not modeled in the instance resolution logic. + + - id: LS-RH-7 + title: Resource + list combination corrupts data + uca: UCA-A-16 + hazards: [H-11, H-11.5, H-4] + type: inadequate-control-algorithm + scenario: > + A function takes list> or returns a record { items: list, + handle: own }. The list copy adapter handles the list pointer + pair correctly but treats the own field as a plain i32 without + resource handle conversion. After cross-memory copy, the handle value + copied to callee's memory references the caller's table, not the + callee's. The callee attempts to use the handle and either traps or + accesses the wrong resource [H-11.5]. If the function returns both + list data and resource handles via retptr, the retptr copy logic + handles pointer pairs for list fields but ignores the resource handle + slot. This is the root cause of the resource_with_lists fixture failure. + causal-factors: + - CopyLayout computation does not track resource fields within elements + - Return area slot classification treats resource handles as plain i32 values + - No "resource pair" concept analogous to "pointer pair" + + - id: LS-RH-8 + title: Alias chain depth > 1 not resolved for resource types + uca: UCA-A-23 + hazards: [H-11, H-11.7] + type: inadequate-control-algorithm + scenario: > + Component C imports resource T from B, which imported T from A (the + impl). In C's component_type_defs: type 10 = ExportAlias(type 8), + type 8 = ExportAlias(type 5), type 5 = resource T. The canonical + function ResourceRep is defined for type 5. build_resource_type_to_import + Step 6 iterates once: it propagates type 5 → type 8, but type 10 is + not yet connected to type 5. After the single pass, the function + signature that uses Borrow(type 10) cannot find the [resource-rep] + import. resolve_resource_positions returns empty, and no resource + conversion is emitted [H-11.7]. + causal-factors: + - Step 6 single-pass propagation assumes depth-1 alias chains + - No transitive closure computation for alias chains + - No error when resolve_resource_positions fails to find a match + +# ============================================================================= +# Gap Analysis: Existing SRs vs New UCAs +# ============================================================================= + +gap-analysis: + covered-by-existing-sr: + - uca: UCA-A-14 + sr: SR-25 + coverage: partial + note: > + SR-25 ("Resource handle pass-through") is misnamed — it was originally + written for simple pass-through but the implementation actually does + Phase 0 borrow conversion. The SR text says "pass resource handles + through without modification" which contradicts the actual behavior + and the canonical ABI requirement. SR-25 needs to be rewritten to + accurately describe the borrow→rep conversion. + + - uca: UCA-A-15 + sr: SR-25 + coverage: partial + note: > + SR-25 does not mention the 3-component chain case at all. The + implementation handles it (rep_func + new_func in ResourceBorrowTransfer) + but the requirement does not specify the expected behavior. + + - uca: UCA-A-19 + sr: SR-25 + coverage: covered + note: > + The code correctly skips own in the resource conversion loop + (is_owned continue guard in analyze_call_site line 232-233). + + not-covered-needs-new-sr: + - uca: UCA-A-16 + gap: > + No SR covers resource types nested inside aggregate types. This is a + fundamental gap in the parser's resource detection capability. + fixture-failures: [resource_borrow_in_record, resource_with_lists] + + - uca: UCA-A-17 + gap: > + No SR covers own result conversion. resource_results is collected + but never consumed. This violates the canonical ABI for cross-component + own returns. + fixture-failures: [resource_aggregates] + + - uca: UCA-A-18 + gap: > + No SR covers own ownership transfer (source-side handle cleanup). + The handle leak is a known limitation documented in MEMORY.md but + not formalized as a safety requirement. + fixture-failures: [ownership] + + - uca: UCA-A-20 + gap: > + No SR covers the disambiguation of resource.rep imports in multi- + component chains. The first-match strategy is non-deterministic. + fixture-failures: [resource_floats] + + - uca: UCA-P-11 + gap: > + No SR requires recursive resource detection in aggregate types. + fixture-failures: [resource_borrow_in_record] + + - uca: UCA-W-4 + gap: > + No SR covers toplevel resource import resolution in the P2 wrapper. + fixture-failures: [resource-import-and-export] + + - uca: UCA-A-23 + gap: > + No SR requires transitive closure for resource type alias resolution. + fixture-failures: [] # No known fixture failure, but latent risk + +# ============================================================================= +# Recommended New/Updated Safety Requirements +# ============================================================================= + +requirements: + - id: SR-25 + type: requirement + title: Correct borrow handle conversion in adapters + description: > + The adapter shall convert borrow handles to raw representations + before passing them to the callee, per the canonical ABI lower_borrow + semantics. When the callee defines T (callee_defines_resource = true), + the adapter shall call [resource-rep](handle) → rep and pass rep. When + neither caller nor callee defines T (3-component chain), the adapter + shall call the CALLER's [resource-rep](handle) → rep, then the CALLEE's + [resource-new](rep) → new_handle, and pass new_handle. Own parameters + shall NOT be converted (callee handles conversion internally). + status: implemented + tags: [stpa-derived, resource-handling] + links: + - type: derives-from + target: CC-A-14 + - type: derives-from + target: CC-A-15 + - type: derives-from + target: LS-RH-1 + - type: derives-from + target: LS-RH-2 + fields: + implementation: + - meld-core/src/adapter/fact.rs + - meld-core/src/adapter/mod.rs + spec-reference: "Canonical ABI lower_borrow: if cx.inst is t.rt.impl return v" + verification-method: test + verification-description: > + Runtime test with 2-component borrow call (callee defines T) and + 3-component borrow chain; verify correct rep/handle conversion. + + - id: SR-32 + type: requirement + title: Recursive resource detection in aggregate types + description: > + The parser shall detect own and borrow values nested at any depth + inside record, tuple, option, result, and variant types. For each + detected resource, the parser shall report its byte offset within the + aggregate and its flat ABI position. The adapter shall convert all + detected resource handles, whether top-level or nested. + status: draft + tags: [stpa-derived, resource-handling] + links: + - type: derives-from + target: CC-A-16 + - type: derives-from + target: CC-P-11 + - type: derives-from + target: LS-RH-4 + - type: derives-from + target: LS-RH-7 + fields: + implementation: + - meld-core/src/parser.rs + - meld-core/src/adapter/fact.rs + spec-reference: "Component Model MVP: value types include own and borrow at any position" + verification-method: test + verification-description: > + Runtime test with record { name: string, handle: borrow }, + option>, and list>. Verify all inner handles are + correctly converted across component boundaries. + + - id: SR-33 + type: requirement + title: Own result conversion via resource.new + description: > + When a cross-component call returns own and the caller is NOT the + implementation component for T, the adapter shall call [resource-new](rep) + to create a handle in the caller's handle table and return the handle + index to the caller. The resolver's resource_results data shall be + consumed by the adapter generator. + status: draft + tags: [stpa-derived, resource-handling] + links: + - type: derives-from + target: CC-A-17 + - type: derives-from + target: LS-RH-3 + fields: + implementation: + - meld-core/src/adapter/fact.rs + spec-reference: "Canonical ABI lower_own: if cx.inst is NOT t.rt.impl, create handle" + verification-method: test + verification-description: > + Runtime test with function returning own across component boundary. + Verify caller receives a valid handle (not raw rep) and can use it + in subsequent calls. + + - id: SR-34 + type: requirement + title: Own parameter ownership transfer semantics + description: > + When own is passed as a parameter in a cross-component call, the + adapter shall ensure the source component's handle table entry is + freed (ownership transferred, not leaked). The destructor shall NOT + be invoked during transfer — only the handle table entry is removed. + If the canonical "detach without dtor" operation is not available, + the adapter shall use resource.drop with a no-op dtor or document + the handle leak as a known limitation with a maximum table size bound. + status: draft + tags: [stpa-derived, resource-handling] + links: + - type: derives-from + target: CC-A-18 + - type: derives-from + target: LS-RH-5 + fields: + implementation: + - meld-core/src/adapter/fact.rs + spec-reference: "Canonical ABI lift_own: h = cx.inst.handles.remove(i)" + verification-method: test + verification-description: > + Runtime test with own parameter transfer. Verify source handle + is freed and destination can use the resource. Verify no double-free + on subsequent drop. + + - id: SR-35 + type: requirement + title: Deterministic resource.rep import disambiguation + description: > + In multi-component chains, the adapter shall select the correct + [resource-rep] import by matching the caller's component identity, + not by first-match string suffix. The disambiguation shall be + deterministic regardless of HashMap iteration order. + status: draft + tags: [stpa-derived, resource-handling] + links: + - type: derives-from + target: CC-A-20 + - type: derives-from + target: LS-RH-2 + fields: + implementation: + - meld-core/src/adapter/fact.rs + verification-method: test + verification-description: > + Test with 3-component chain where two non-callee components have + [resource-rep] imports for the same resource name. Verify the + adapter selects the caller's import, not an arbitrary one. + + - id: SR-36 + type: requirement + title: Transitive resource type alias resolution + description: > + build_resource_type_to_import shall resolve resource type aliases + transitively, handling alias chains of arbitrary depth. The resolution + shall compute the transitive closure so that Borrow(alias_N) correctly + maps to the canonical [resource-rep] import regardless of how many + ExportAlias levels separate the function parameter's type ID from the + canonical ResourceRep entry's type ID. + status: draft + tags: [stpa-derived, resource-handling] + links: + - type: derives-from + target: CC-A-23 + - type: derives-from + target: LS-RH-8 + fields: + implementation: + - meld-core/src/resolver.rs + verification-method: test + verification-description: > + Test with component having 3-level alias chain for resource type. + Verify resolve_resource_positions returns correct import pair. + + - id: SR-37 + type: requirement + title: Toplevel resource import resolution in P2 wrapper + description: > + The P2 wrapper shall resolve resource type imports that appear at + the component level (not inside a named interface instance). This + includes generating the appropriate resource type definition, canon + resource.new, canon resource.rep, and canon resource.drop for the + toplevel resource. The wrapper shall not fail with "cannot resolve + fused import" for valid resource import patterns. + status: draft + tags: [stpa-derived, resource-handling] + links: + - type: derives-from + target: CC-W-4 + - type: derives-from + target: LS-RH-6 + fields: + implementation: + - meld-core/src/component_wrap.rs + verification-method: test + verification-description: > + Test with resource-import-and-export fixture. Verify P2 wrapping + succeeds and the component passes wasm-tools validate. + + - id: SR-38 + type: requirement + title: Correct destructor binding for locally-defined resources + description: > + When the P2 wrapper defines a local resource type, it shall bind the + destructor to the correct fused module export function. The naming + convention shall handle both wit-bindgen format ("{interface}#[dtor]{resource}") + and alternative formats. If no destructor export is found, the wrapper + shall emit a diagnostic warning (not silently omit the destructor). + status: draft + tags: [stpa-derived, resource-handling] + links: + - type: derives-from + target: CC-W-6 + - type: derives-from + target: LS-RH-6 + fields: + implementation: + - meld-core/src/component_wrap.rs + verification-method: test + verification-description: > + Test with component defining resource with destructor. Verify + wrapper connects dtor export. Test with missing dtor export and + verify warning is emitted. + +# ============================================================================= +# Controller Constraints for Resource Handling +# ============================================================================= + +controller-constraints: + # Adapter constraints + - id: CC-A-14 + controller: CTRL-ADAPTER + constraint: > + Adapter must emit resource.rep conversion for borrow parameters when + the callee defines T (2-component case) + ucas: [UCA-A-14] + hazards: [H-11, H-11.1] + + - id: CC-A-15 + controller: CTRL-ADAPTER + constraint: > + Adapter must emit resource.rep→resource.new chain for borrow parameters + in 3-component chains (neither caller nor callee defines T) + ucas: [UCA-A-15] + hazards: [H-11, H-11.4] + + - id: CC-A-16 + controller: CTRL-ADAPTER + constraint: > + Adapter must detect and convert resource handles nested inside aggregate + types (record, tuple, option, result, variant) + ucas: [UCA-A-16] + hazards: [H-11, H-11.5] + + - id: CC-A-17 + controller: CTRL-ADAPTER + constraint: > + Adapter must emit resource.new for own return values when the caller + does not define T + ucas: [UCA-A-17] + hazards: [H-11, H-11.6] + + - id: CC-A-18 + controller: CTRL-ADAPTER + constraint: > + Adapter must arrange for source-side handle cleanup when own ownership + is transferred across component boundaries + ucas: [UCA-A-18] + hazards: [H-11, H-11.3] + + - id: CC-A-19 + controller: CTRL-ADAPTER + constraint: > + Adapter must NOT convert own parameters via resource.rep (callee + handles conversion internally) + ucas: [UCA-A-19] + hazards: [H-11, H-11.2] + + - id: CC-A-20 + controller: CTRL-ADAPTER + constraint: > + Adapter must select the caller's resource.rep import (not an arbitrary + match) when disambiguating in multi-component chains + ucas: [UCA-A-20] + hazards: [H-11, H-11.4] + + - id: CC-A-21 + controller: CTRL-ADAPTER + constraint: > + Adapter must emit Phase 0 (resource conversion) before Phase 1 (memory + copy) and Phase 2 (callee call) + ucas: [UCA-A-22] + hazards: [H-11, H-11.1] + + - id: CC-A-22 + controller: CTRL-ADAPTER + constraint: > + Resource type alias chains must be resolved transitively, not just to + depth 1 + ucas: [UCA-A-23] + hazards: [H-11, H-11.7] + + # Parser constraints + - id: CC-P-11 + controller: CTRL-PARSER + constraint: > + Parser must detect resource types (own, borrow) nested at any + depth inside aggregate value types + ucas: [UCA-P-11] + hazards: [H-11, H-11.5] + + - id: CC-P-12 + controller: CTRL-PARSER + constraint: > + Parser must track resource type provenance to distinguish same-named + resources from different components + ucas: [UCA-P-12] + hazards: [H-11, H-11.7, H-1] + + - id: CC-P-13 + controller: CTRL-PARSER + constraint: > + Parser must compute correct flat_count (= 1) for all resource types + ucas: [UCA-P-13] + hazards: [H-11, H-4] + + # Wrapper constraints + - id: CC-W-4 + controller: CTRL-WRAPPER + constraint: > + Wrapper must resolve toplevel resource type imports that are not + inside a named interface instance + ucas: [UCA-W-4] + hazards: [H-8, H-11] + + - id: CC-W-5 + controller: CTRL-WRAPPER + constraint: > + Wrapper must emit resource.rep and resource.new canon builtins for + all imported resources the fused module depends on + ucas: [UCA-W-5] + hazards: [H-8, H-11] + + - id: CC-W-6 + controller: CTRL-WRAPPER + constraint: > + Wrapper must bind destructors to correct fused module exports when + defining local resource types + ucas: [UCA-W-6] + hazards: [H-8, H-11, H-11.8] + +# ============================================================================= +# Fixture Failure → UCA Mapping +# ============================================================================= + +fixture-failure-mapping: + - fixture: resource_floats + failure-mode: "3-component borrow forwarding uses wrong resource.rep table" + primary-uca: UCA-A-20 + secondary-ucas: [UCA-A-15] + loss-scenario: LS-RH-2 + root-cause: > + First-match strategy in analyze_call_site selects wrong [resource-rep] + import when multiple candidates exist from different components. + recommended-sr: SR-35 + + - fixture: resource_aggregates + failure-mode: "own handle assertion failure — rep misinterpreted as handle" + primary-uca: UCA-A-17 + secondary-ucas: [UCA-A-18] + loss-scenario: LS-RH-3 + root-cause: > + Adapter does not call [resource-new] on own return values. Callee + returns rep, caller interprets it as handle index. + recommended-sr: SR-33 + + - fixture: ownership + failure-mode: "Resource ownership transfer trap" + primary-uca: UCA-A-18 + secondary-ucas: [UCA-A-17] + loss-scenario: LS-RH-5 + root-cause: > + Adapter does not clean up source-side handle table entry when own + ownership transfers. Handle leaks lead to double-free or stale handle + access. + recommended-sr: SR-34 + + - fixture: resource_borrow_in_record + failure-mode: "borrow inside record not converted" + primary-uca: UCA-A-16 + secondary-ucas: [UCA-P-11] + loss-scenario: LS-RH-4 + root-cause: > + resource_param_positions does not recurse into record fields to detect + borrow. Handle passes through without conversion. + recommended-sr: SR-32 + + - fixture: resource_with_lists + failure-mode: "Data corruption in resource+list combination" + primary-uca: UCA-A-16 + secondary-ucas: [UCA-A-17] + loss-scenario: LS-RH-7 + root-cause: > + CopyLayout and resource detection are independent subsystems. Neither + handles the intersection: resource handles inside list elements or + alongside list pointer pairs in return areas. + recommended-sr: SR-32 + + - fixture: resource-import-and-export + failure-mode: "P2 wrapper cannot resolve toplevel resource import" + primary-uca: UCA-W-4 + secondary-ucas: [UCA-W-5] + loss-scenario: LS-RH-6 + root-cause: > + resolve_import_to_instance does not handle resource imports at the + component level (outside of named interface instances). Fallback + Category C only covers [resource-drop], not [resource-rep]/[resource-new]. + recommended-sr: SR-37