Skip to content

Commit 57ccf62

Browse files
avrabeclaude
andauthored
feat: implement params-ptr lowering for >16 flat params (#65)
When a component function has more than MAX_FLAT_PARAMS (16) flat parameters, the canonical ABI uses the params-ptr calling convention: both caller and callee use a single i32 pointer to a buffer in linear memory instead of flat params on the stack. In multi-memory mode, the adapter must: 1. Allocate a buffer in the callee's memory via cabi_realloc 2. Bulk copy the params buffer from caller to callee memory 3. Fix up (ptr, len) pointer pairs inside the buffer by copying their pointed-to data (e.g., list elements) across memories 4. Convert borrow<T> resource handles inside the buffer to representations via [resource-rep] calls 5. Convert borrow<T> resource handles inside list element data via per-element fixup loops Changes: - parser.rs: Add total_flat_params(), params_area_byte_size(), params_area_max_align(), pointer_pair_params_byte_offsets(), params_area_slots(), resource_params_area_positions(), and collect_resource_byte_positions() methods - resolver.rs: Add params_area_byte_size, params_area_max_align, params_area_pointer_pair_offsets, params_area_copy_layouts, params_area_slots, and params_area_resource_positions fields to AdapterRequirements; populate in both resolution paths - adapter/mod.rs: Add ParamsAreaResourceFixup struct and params_area_borrow_fixups field to AdapterOptions - adapter/fact.rs: Add generate_params_ptr_adapter() method with full buffer copy, pointer pair fixup, borrow handle conversion, and inner list resource fixup; detect params-ptr case in generate_memory_copy_adapter() dispatch Promotes resource_aggregates fixture from fuse_only to runtime test. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1e19241 commit 57ccf62

File tree

5 files changed

+642
-3
lines changed

5 files changed

+642
-3
lines changed

meld-core/src/adapter/fact.rs

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,31 @@ impl FactStyleGenerator {
429429
}
430430
}
431431

432+
// Resolve resource handles inside the params-ptr buffer.
433+
// For borrow<T> where callee defines T, the adapter must convert
434+
// handle → rep at the byte offset within the buffer.
435+
for op in &site.requirements.params_area_resource_positions {
436+
if op.is_owned {
437+
continue; // own<T>: callee calls resource.rep internally
438+
}
439+
440+
if op.callee_defines_resource {
441+
// 2-component: use callee's [resource-rep]
442+
if let Some(&rep_func) =
443+
resource_rep_imports.get(&(op.import_module.clone(), op.import_field.clone()))
444+
{
445+
options
446+
.params_area_borrow_fixups
447+
.push(super::ParamsAreaResourceFixup {
448+
byte_offset: op.byte_offset,
449+
rep_func,
450+
is_owned: false,
451+
});
452+
}
453+
}
454+
// 3-component chains for params-area borrows could be added here
455+
}
456+
432457
options
433458
}
434459

@@ -617,6 +642,29 @@ impl FactStyleGenerator {
617642
);
618643
}
619644

645+
// --- Detect params-ptr calling convention ---
646+
// The canonical ABI uses params-ptr when flat params > MAX_FLAT_PARAMS (16):
647+
// caller (lowered): (params_ptr: i32) → result...
648+
// callee (lifted): (params_ptr: i32) → result...
649+
// Both sides use a single i32 pointer to a buffer in linear memory.
650+
// When memories differ, the adapter must copy the buffer across.
651+
let uses_params_ptr = site.requirements.params_area_byte_size.is_some();
652+
653+
if uses_params_ptr && options.caller_memory != options.callee_memory {
654+
log::debug!(
655+
"params-ptr adapter: generating for import={} (buffer={}B, {} ptr pairs, {} borrow fixups)",
656+
site.import_name,
657+
site.requirements.params_area_byte_size.unwrap_or(0),
658+
site.requirements.params_area_pointer_pair_offsets.len(),
659+
site.requirements
660+
.params_area_resource_positions
661+
.iter()
662+
.filter(|p| !p.is_owned && p.callee_defines_resource)
663+
.count(),
664+
);
665+
return self.generate_params_ptr_adapter(site, options, target_func, caller_type_idx);
666+
}
667+
620668
// --- Non-retptr path: use caller's type for declared signature ---
621669
let adapter_type_idx = caller_type_idx;
622670
let param_count = callee_param_count;
@@ -1155,6 +1203,295 @@ impl FactStyleGenerator {
11551203
Ok((adapter_type_idx, func))
11561204
}
11571205

1206+
/// Generate an adapter for the params-ptr calling convention.
1207+
///
1208+
/// When flat param count > MAX_FLAT_PARAMS (16), the canonical ABI stores all
1209+
/// params in a buffer in linear memory. Both caller and callee use:
1210+
/// (params_ptr: i32) → result...
1211+
///
1212+
/// The adapter bridges different memories:
1213+
/// 1. Allocate buffer in callee's memory via cabi_realloc
1214+
/// 2. Bulk copy the params buffer from caller to callee memory
1215+
/// 3. Fix up any (ptr, len) pairs inside the buffer — copy pointed-to data
1216+
/// from caller memory to callee memory and update the pointers
1217+
/// 4. Call callee with new pointer
1218+
/// 5. Return the result(s)
1219+
fn generate_params_ptr_adapter(
1220+
&self,
1221+
site: &AdapterSite,
1222+
options: &AdapterOptions,
1223+
target_func: u32,
1224+
caller_type_idx: u32,
1225+
) -> Result<(u32, Function)> {
1226+
let params_area_size = site.requirements.params_area_byte_size.unwrap_or(0);
1227+
let params_area_align = site.requirements.params_area_max_align.max(1);
1228+
let ptr_pair_offsets = &site.requirements.params_area_pointer_pair_offsets;
1229+
let copy_layouts = &site.requirements.params_area_copy_layouts;
1230+
1231+
let callee_realloc = options.callee_realloc.unwrap_or_else(|| {
1232+
log::warn!("params-ptr adapter: no callee realloc, buffer copy may fail");
1233+
0
1234+
});
1235+
1236+
// Check if any list copy layouts contain inner resources (borrow handles)
1237+
let has_inner_resources = copy_layouts.iter().any(|cl| {
1238+
matches!(cl,
1239+
crate::resolver::CopyLayout::Elements { inner_resources, .. }
1240+
if !inner_resources.is_empty()
1241+
)
1242+
});
1243+
1244+
// Local layout:
1245+
// 0: params_ptr (the function parameter — pointer to caller's memory)
1246+
// 1: callee_ptr (allocated pointer in callee's memory)
1247+
// 2..2+N: dest_ptr for each pointer pair copy
1248+
// 2+N: loop_counter (if inner resources need fixup)
1249+
let num_ptr_pairs = ptr_pair_offsets.len() as u32;
1250+
let loop_counter_count = if has_inner_resources { 1u32 } else { 0 };
1251+
let scratch_count = 1 + num_ptr_pairs + loop_counter_count; // callee_ptr + per-pair dest ptrs + loop counter
1252+
1253+
// Post-return needs result save locals
1254+
let has_post_return = options.callee_post_return.is_some();
1255+
// For params-ptr, the results come from the callee directly.
1256+
1257+
let mut local_decls: Vec<(u32, wasm_encoder::ValType)> = Vec::new();
1258+
if scratch_count > 0 {
1259+
local_decls.push((scratch_count, wasm_encoder::ValType::I32));
1260+
}
1261+
1262+
// We don't know result count from here, so we handle post-return simply:
1263+
// if there's a post-return, we'll save and restore results.
1264+
// But for params-ptr functions with resource results, result count should be 1 (i32).
1265+
// For simplicity: if has_post_return, add 1 i32 result save local.
1266+
let result_save_base = 1 + scratch_count; // after params_ptr(0) + scratch
1267+
if has_post_return {
1268+
local_decls.push((1, wasm_encoder::ValType::I32));
1269+
}
1270+
1271+
let mut func = Function::new(local_decls);
1272+
1273+
let params_ptr_local: u32 = 0;
1274+
let callee_ptr_local: u32 = 1;
1275+
let pair_dest_base: u32 = 2;
1276+
1277+
// --- Phase 1: Allocate buffer in callee's memory ---
1278+
// callee_ptr = cabi_realloc(0, 0, align, size)
1279+
func.instruction(&Instruction::I32Const(0)); // original_ptr
1280+
func.instruction(&Instruction::I32Const(0)); // original_size
1281+
func.instruction(&Instruction::I32Const(params_area_align as i32)); // alignment
1282+
func.instruction(&Instruction::I32Const(params_area_size as i32)); // new_size
1283+
func.instruction(&Instruction::Call(callee_realloc));
1284+
func.instruction(&Instruction::LocalSet(callee_ptr_local));
1285+
1286+
// --- Phase 2: Bulk copy the entire params buffer ---
1287+
// memory.copy $callee_mem $caller_mem (callee_ptr, params_ptr, size)
1288+
func.instruction(&Instruction::LocalGet(callee_ptr_local)); // dst
1289+
func.instruction(&Instruction::LocalGet(params_ptr_local)); // src
1290+
func.instruction(&Instruction::I32Const(params_area_size as i32)); // size
1291+
func.instruction(&Instruction::MemoryCopy {
1292+
src_mem: options.caller_memory,
1293+
dst_mem: options.callee_memory,
1294+
});
1295+
1296+
// --- Phase 3: Fix up pointer pairs inside the buffer ---
1297+
// For each (ptr, len) pair in the params buffer:
1298+
// 1. Read ptr and len from callee's copy of the buffer
1299+
// 2. Compute byte_size from len and the copy layout's byte_multiplier
1300+
// 3. Allocate in callee's memory: new_ptr = cabi_realloc(0, 0, 1, byte_size)
1301+
// 4. Copy data from caller's memory at old_ptr to callee's memory at new_ptr
1302+
// 5. Write new_ptr back into callee's buffer at the same offset
1303+
for (pair_idx, &byte_offset) in ptr_pair_offsets.iter().enumerate() {
1304+
let dest_local = pair_dest_base + pair_idx as u32;
1305+
let byte_mult = copy_layouts
1306+
.get(pair_idx)
1307+
.map(|cl| match cl {
1308+
crate::resolver::CopyLayout::Bulk { byte_multiplier } => *byte_multiplier,
1309+
crate::resolver::CopyLayout::Elements { element_size, .. } => *element_size,
1310+
})
1311+
.unwrap_or(1);
1312+
1313+
// Read old_ptr from callee's buffer: i32.load callee_mem (callee_ptr + byte_offset)
1314+
// Read old_len from callee's buffer: i32.load callee_mem (callee_ptr + byte_offset + 4)
1315+
1316+
// Allocate: new_ptr = cabi_realloc(0, 0, 1, len * byte_mult)
1317+
func.instruction(&Instruction::I32Const(0));
1318+
func.instruction(&Instruction::I32Const(0));
1319+
func.instruction(&Instruction::I32Const(1));
1320+
// Load len from callee's buffer
1321+
func.instruction(&Instruction::LocalGet(callee_ptr_local));
1322+
func.instruction(&Instruction::I32Load(wasm_encoder::MemArg {
1323+
offset: (byte_offset + 4) as u64,
1324+
align: 2,
1325+
memory_index: options.callee_memory,
1326+
}));
1327+
if byte_mult > 1 {
1328+
func.instruction(&Instruction::I32Const(byte_mult as i32));
1329+
func.instruction(&Instruction::I32Mul);
1330+
}
1331+
func.instruction(&Instruction::Call(callee_realloc));
1332+
func.instruction(&Instruction::LocalSet(dest_local));
1333+
1334+
// Copy data: memory.copy callee caller (new_ptr, old_ptr, len * byte_mult)
1335+
func.instruction(&Instruction::LocalGet(dest_local)); // dst (in callee mem)
1336+
// Load old_ptr from callee's buffer (this was copied from caller's buffer,
1337+
// so it points into caller's memory)
1338+
func.instruction(&Instruction::LocalGet(callee_ptr_local));
1339+
func.instruction(&Instruction::I32Load(wasm_encoder::MemArg {
1340+
offset: byte_offset as u64,
1341+
align: 2,
1342+
memory_index: options.callee_memory,
1343+
})); // src (in caller mem)
1344+
// Load len from callee's buffer
1345+
func.instruction(&Instruction::LocalGet(callee_ptr_local));
1346+
func.instruction(&Instruction::I32Load(wasm_encoder::MemArg {
1347+
offset: (byte_offset + 4) as u64,
1348+
align: 2,
1349+
memory_index: options.callee_memory,
1350+
}));
1351+
if byte_mult > 1 {
1352+
func.instruction(&Instruction::I32Const(byte_mult as i32));
1353+
func.instruction(&Instruction::I32Mul);
1354+
}
1355+
func.instruction(&Instruction::MemoryCopy {
1356+
src_mem: options.caller_memory,
1357+
dst_mem: options.callee_memory,
1358+
});
1359+
1360+
// Write new_ptr back into callee's buffer at byte_offset
1361+
func.instruction(&Instruction::LocalGet(callee_ptr_local));
1362+
func.instruction(&Instruction::LocalGet(dest_local));
1363+
func.instruction(&Instruction::I32Store(wasm_encoder::MemArg {
1364+
offset: byte_offset as u64,
1365+
align: 2,
1366+
memory_index: options.callee_memory,
1367+
}));
1368+
1369+
// Fix up inner resource handles in list elements.
1370+
// After bulk copy, borrow handles in the list data still reference
1371+
// the caller's resource table. Convert each borrow handle → rep.
1372+
if let Some(crate::resolver::CopyLayout::Elements {
1373+
element_size,
1374+
inner_resources,
1375+
..
1376+
}) = copy_layouts.get(pair_idx)
1377+
&& !inner_resources.is_empty()
1378+
{
1379+
let element_size = *element_size;
1380+
let loop_local = pair_dest_base + num_ptr_pairs;
1381+
1382+
// Initialize loop counter to 0
1383+
func.instruction(&Instruction::I32Const(0));
1384+
func.instruction(&Instruction::LocalSet(loop_local));
1385+
1386+
// block $exit { loop $cont {
1387+
func.instruction(&Instruction::Block(wasm_encoder::BlockType::Empty));
1388+
func.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty));
1389+
1390+
// if loop_counter >= len: break
1391+
func.instruction(&Instruction::LocalGet(loop_local));
1392+
// Load len from callee's buffer
1393+
func.instruction(&Instruction::LocalGet(callee_ptr_local));
1394+
func.instruction(&Instruction::I32Load(wasm_encoder::MemArg {
1395+
offset: (byte_offset + 4) as u64,
1396+
align: 2,
1397+
memory_index: options.callee_memory,
1398+
}));
1399+
func.instruction(&Instruction::I32GeU);
1400+
func.instruction(&Instruction::BrIf(1)); // break to $exit
1401+
1402+
for &(res_byte_offset, _resource_type_id, is_owned) in inner_resources {
1403+
if is_owned {
1404+
continue; // own<T>: callee handles internally
1405+
}
1406+
// Find [resource-rep] for this resource
1407+
if let Some(&rep_func) = options
1408+
.params_area_borrow_fixups
1409+
.first()
1410+
.map(|f| &f.rep_func)
1411+
.or_else(|| options.resource_rep_calls.first().map(|t| &t.rep_func))
1412+
{
1413+
// addr = dest_ptr + loop_counter * element_size + res_byte_offset
1414+
// Push addr for store
1415+
func.instruction(&Instruction::LocalGet(dest_local));
1416+
func.instruction(&Instruction::LocalGet(loop_local));
1417+
func.instruction(&Instruction::I32Const(element_size as i32));
1418+
func.instruction(&Instruction::I32Mul);
1419+
func.instruction(&Instruction::I32Add);
1420+
// Load handle from same addr + offset
1421+
func.instruction(&Instruction::LocalGet(dest_local));
1422+
func.instruction(&Instruction::LocalGet(loop_local));
1423+
func.instruction(&Instruction::I32Const(element_size as i32));
1424+
func.instruction(&Instruction::I32Mul);
1425+
func.instruction(&Instruction::I32Add);
1426+
func.instruction(&Instruction::I32Load(wasm_encoder::MemArg {
1427+
offset: res_byte_offset as u64,
1428+
align: 2,
1429+
memory_index: options.callee_memory,
1430+
}));
1431+
// Call [resource-rep](handle) → rep
1432+
func.instruction(&Instruction::Call(rep_func));
1433+
// Store rep back
1434+
func.instruction(&Instruction::I32Store(wasm_encoder::MemArg {
1435+
offset: res_byte_offset as u64,
1436+
align: 2,
1437+
memory_index: options.callee_memory,
1438+
}));
1439+
}
1440+
}
1441+
1442+
// loop_counter++
1443+
func.instruction(&Instruction::LocalGet(loop_local));
1444+
func.instruction(&Instruction::I32Const(1));
1445+
func.instruction(&Instruction::I32Add);
1446+
func.instruction(&Instruction::LocalSet(loop_local));
1447+
func.instruction(&Instruction::Br(0)); // continue to $cont
1448+
func.instruction(&Instruction::End); // end loop
1449+
func.instruction(&Instruction::End); // end block
1450+
}
1451+
}
1452+
1453+
// --- Phase 3.5: Convert borrow resource handles inside the buffer ---
1454+
// For borrow<T> where callee defines T, the adapter must convert
1455+
// handle → rep by calling [resource-rep] and writing the rep back.
1456+
for fixup in &options.params_area_borrow_fixups {
1457+
// Stack: callee_ptr (for i32.store dest)
1458+
func.instruction(&Instruction::LocalGet(callee_ptr_local));
1459+
// Load handle from callee's buffer at byte_offset
1460+
func.instruction(&Instruction::LocalGet(callee_ptr_local));
1461+
func.instruction(&Instruction::I32Load(wasm_encoder::MemArg {
1462+
offset: fixup.byte_offset as u64,
1463+
align: 2,
1464+
memory_index: options.callee_memory,
1465+
}));
1466+
// Call [resource-rep](handle) → rep
1467+
func.instruction(&Instruction::Call(fixup.rep_func));
1468+
// Store rep back at the same offset
1469+
func.instruction(&Instruction::I32Store(wasm_encoder::MemArg {
1470+
offset: fixup.byte_offset as u64,
1471+
align: 2,
1472+
memory_index: options.callee_memory,
1473+
}));
1474+
}
1475+
1476+
// --- Phase 4: Call callee with the new pointer ---
1477+
func.instruction(&Instruction::LocalGet(callee_ptr_local));
1478+
func.instruction(&Instruction::Call(target_func));
1479+
1480+
// --- Phase 5: Handle post-return if needed ---
1481+
if has_post_return {
1482+
// Save result (assume i32)
1483+
func.instruction(&Instruction::LocalSet(result_save_base));
1484+
// Call post-return (no args for params-ptr convention post-return)
1485+
func.instruction(&Instruction::Call(options.callee_post_return.unwrap()));
1486+
// Push result back
1487+
func.instruction(&Instruction::LocalGet(result_save_base));
1488+
}
1489+
1490+
func.instruction(&Instruction::End);
1491+
1492+
Ok((caller_type_idx, func))
1493+
}
1494+
11581495
/// Generate an adapter for the retptr calling convention.
11591496
///
11601497
/// In the canonical ABI, when a function returns heap-allocated types

meld-core/src/adapter/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,21 @@ pub struct AdapterOptions {
144144
/// Each entry: (byte_offset_in_element, merged_func_idx of [resource-rep]).
145145
/// Used after bulk list copy to convert handles in callee memory.
146146
pub inner_resource_fixups: Vec<(u32, u32)>,
147+
/// Resource borrow handles inside the params-ptr buffer that need
148+
/// handle→rep conversion. Each entry contains the byte offset within the
149+
/// buffer and the merged function index of `[resource-rep]`.
150+
pub params_area_borrow_fixups: Vec<ParamsAreaResourceFixup>,
151+
}
152+
153+
/// Describes a resource handle inside the params-ptr buffer that needs conversion.
154+
#[derive(Debug, Clone)]
155+
pub struct ParamsAreaResourceFixup {
156+
/// Byte offset within the params buffer
157+
pub byte_offset: u32,
158+
/// Merged function index of `[resource-rep]` to convert handle → rep
159+
pub rep_func: u32,
160+
/// Whether this is an own<T> (true) or borrow<T> (false)
161+
pub is_owned: bool,
147162
}
148163

149164
/// Describes how to transfer a `borrow<T>` handle across an adapter boundary.
@@ -190,6 +205,7 @@ impl Default for AdapterOptions {
190205
resource_rep_calls: Vec::new(),
191206
resource_new_calls: Vec::new(),
192207
inner_resource_fixups: Vec::new(),
208+
params_area_borrow_fixups: Vec::new(),
193209
}
194210
}
195211
}

0 commit comments

Comments
 (0)