use anyhow::{bail, Result};
use wit_parser::{Function as WitFunction, Type};
use super::super::abi::emit::BlobSlice;
use super::super::mem_layout::StaticLayout;
use super::blob::{resolve, NameInterner, RecordWriter, RelocPlan, Segment, SymRef, SymbolBases};
use super::lift::plan::Cell;
use super::lift::{
back_fill_record_fields_ptrs, build_char_scratch_map, build_enum_info_blob,
build_flags_info_maps, build_handle_info_maps, build_record_info_maps,
build_tuple_indices_blob, build_variant_info_maps, char_scratch_sizes, flags_scratch_sizes,
fold_cell_side_data, CellFillSources, CellSideData, CharScratch, CharScratchMaps,
FlagsInfoMaps, FlagsRuntimeFill, HandleInfoMaps, HandleRuntimeFill, InfoCounts, ParamLayout,
RecordInfoMaps, ResultLayout, ResultLift, ResultSource, ResultSourceLayout, SideTableBlob,
TupleIndicesBlob, VariantInfoMaps,
};
use super::schema::{
SchemaLayouts, FIELD_NAME, FIELD_TREE, ON_RET_CALL, ON_RET_RESULT, TREE_CELLS, TREE_ENUM_INFOS,
TREE_FLAGS_INFOS, TREE_HANDLE_INFOS, TREE_RECORD_INFOS, TREE_ROOT, TREE_VARIANT_INFOS,
};
use super::{AfterSetup, FuncClassified, FuncDispatch, FuncShape};
const EVENT_SLOT_SIZE: u32 = 8;
const EVENT_SLOT_ALIGN: u32 = 4;
const LAYOUT_SIZE_BUDGET: u32 = i32::MAX as u32;
#[cfg(not(test))]
const MAX_FLAT_SLOTS_PER_FN: u32 = 1 << 16;
#[cfg(test)]
const MAX_FLAT_SLOTS_PER_FN: u32 = 16;
#[cfg(not(test))]
const MAX_CELLS_PER_PARAM: u32 = 1 << 20;
#[cfg(test)]
const MAX_CELLS_PER_PARAM: u32 = 8;
fn check_layout_budget(per_func: &[FuncClassified]) -> Result<()> {
for (fn_idx, fd) in per_func.iter().enumerate() {
for (p_idx, p) in fd.params.iter().enumerate() {
if p.plan.flat_slot_count > MAX_FLAT_SLOTS_PER_FN {
bail!(
"fn[{fn_idx}] param[{p_idx}]: flat-slot count {} exceeds budget {MAX_FLAT_SLOTS_PER_FN}",
p.plan.flat_slot_count,
);
}
if p.plan.cell_count() > MAX_CELLS_PER_PARAM {
bail!(
"fn[{fn_idx}] param[{p_idx}]: cell count {} exceeds budget {MAX_CELLS_PER_PARAM}",
p.plan.cell_count(),
);
}
}
if let Some(rl) = fd.result_lift.as_ref() {
let cells = rl.compound().map_or(1, |c| c.plan.cell_count());
if cells > MAX_CELLS_PER_PARAM {
bail!(
"fn[{fn_idx}] result: cell count {cells} exceeds budget {MAX_CELLS_PER_PARAM}"
);
}
}
}
Ok(())
}
struct SingleCellFills<'a> {
flags_fill: &'a Option<FlagsRuntimeFill>,
char_scratch: &'a Option<i32>,
handle_fill: &'a Option<HandleRuntimeFill>,
}
fn single_cell_side_data(cell: &Cell, fills: &SingleCellFills<'_>) -> CellSideData {
match cell {
Cell::Flags { .. } => CellSideData::Flags(Box::new(
fills
.flags_fill
.clone()
.expect("flags-info fill must exist"),
)),
Cell::Char { .. } => CellSideData::Char {
scratch: CharScratch::Static {
scratch_addr: fills.char_scratch.expect("char-info scratch must exist"),
},
},
Cell::Handle { .. } => CellSideData::Handle(Box::new(
fills
.handle_fill
.clone()
.expect("handle-info fill must exist"),
)),
Cell::Bool { .. }
| Cell::IntegerSignExt { .. }
| Cell::IntegerZeroExt { .. }
| Cell::Integer64 { .. }
| Cell::FloatingF32 { .. }
| Cell::FloatingF64 { .. }
| Cell::Text { .. }
| Cell::Bytes { .. }
| Cell::EnumCase { .. } => CellSideData::None,
Cell::RecordOf { .. }
| Cell::TupleOf { .. }
| Cell::Option { .. }
| Cell::Result { .. }
| Cell::Variant { .. }
| Cell::ListOf { .. } => {
unreachable!("single_cell_side_data reached unsupported result Cell {cell:?}")
}
}
}
pub(super) struct StaticDataPlan {
pub(super) bump_start: u32,
pub(super) event_ptr: i32,
pub(super) hook_params_ptr: Option<u32>,
pub(super) data_segments: Vec<(u32, Vec<u8>)>,
}
#[derive(Clone, Copy, Default)]
struct FieldSideTables {
enum_infos: BlobSlice,
flags_infos: BlobSlice,
record_infos: BlobSlice,
variant_infos: BlobSlice,
handle_infos: BlobSlice,
}
impl FieldSideTables {
fn write_to_tree(&self, blob: &mut [u8], tree: &RecordWriter) {
tree.write_slice(blob, TREE_ENUM_INFOS, self.enum_infos);
tree.write_slice(blob, TREE_FLAGS_INFOS, self.flags_infos);
tree.write_slice(blob, TREE_RECORD_INFOS, self.record_infos);
tree.write_slice(blob, TREE_VARIANT_INFOS, self.variant_infos);
tree.write_slice(blob, TREE_HANDLE_INFOS, self.handle_infos);
}
}
fn write_field_record(
blob: &mut Vec<u8>,
schema: &SchemaLayouts,
cells: BlobSlice,
root: u32,
name: BlobSlice,
side_tables: FieldSideTables,
) {
let field = RecordWriter::extend_zero(blob, &schema.field_layout);
field.write_slice(blob, FIELD_NAME, name);
let tree = field.nested(FIELD_TREE, &schema.tree_layout);
tree.write_slice(blob, TREE_CELLS, cells);
side_tables.write_to_tree(blob, &tree);
tree.write_i32(blob, TREE_ROOT, root as i32);
}
fn build_fields_blob(
per_func: &[FuncClassified],
schema: &SchemaLayouts,
param_side_tables: &[Vec<FieldSideTables>],
) -> Vec<u8> {
let mut blob: Vec<u8> = Vec::new();
for (fn_idx, fd) in per_func.iter().enumerate() {
for (i, p) in fd.params.iter().enumerate() {
write_field_record(
&mut blob,
schema,
BlobSlice {
off: 0,
len: p.plan.cell_count(),
},
p.plan.root(),
p.name,
param_side_tables[fn_idx][i],
);
}
}
blob
}
fn build_after_params_blob(
per_func: &[FuncClassified],
schema: &SchemaLayouts,
iface_name: BlobSlice,
result_side_tables: &[FieldSideTables],
) -> Vec<u8> {
let Some(after_layout) = schema.after_hook.as_ref().map(|h| &h.params_layout) else {
return Vec::new();
};
let mut blob: Vec<u8> = Vec::new();
for (fn_idx, fd) in per_func.iter().enumerate() {
let entry = RecordWriter::extend_zero(&mut blob, after_layout);
schema.callid_layout.store_names_in_blob(
&mut blob,
entry.field_offset(ON_RET_CALL),
iface_name,
BlobSlice {
off: fd.fn_name_offset as u32,
len: fd.fn_name_len as u32,
},
);
if fd.result_lift.is_some() {
entry.write_option_some(&mut blob, ON_RET_RESULT);
let tree_base = entry.field_offset(ON_RET_RESULT) + schema.option_payload_off as usize;
let tree = RecordWriter::at(&schema.tree_layout, tree_base);
let (cells_len, root) = fd
.result_lift
.as_ref()
.and_then(|rl| rl.compound())
.map_or((1, 0), |c| (c.plan.cell_count(), c.plan.root()));
tree.write_slice(
&mut blob,
TREE_CELLS,
BlobSlice {
off: 0,
len: cells_len,
},
);
result_side_tables[fn_idx].write_to_tree(&mut blob, &tree);
tree.write_i32(&mut blob, TREE_ROOT, root as i32);
} else {
entry.write_option_none(&mut blob, ON_RET_RESULT);
}
}
blob
}
fn place_segment(
layout: &mut StaticLayout,
symbols: &mut SymbolBases,
relocs: &mut RelocPlan,
seg: Segment,
) -> u32 {
let (base, idx) = layout.place_data(seg.align, &seg.bytes);
symbols.set(seg.id, base);
relocs.record_segment(idx, base, seg.relocs);
base
}
fn resolve_param_result_ranges(
symbols: &SymbolBases,
per_param_sym: Vec<Vec<Option<SymRef>>>,
per_result_sym: Vec<Option<SymRef>>,
) -> (Vec<Vec<BlobSlice>>, Vec<BlobSlice>) {
let per_param = per_param_sym
.into_iter()
.map(|v| v.into_iter().map(|s| resolve(s, symbols)).collect())
.collect();
let per_result = per_result_sym
.into_iter()
.map(|s| resolve(s, symbols))
.collect();
(per_param, per_result)
}
pub(super) fn lay_out_static_memory(
per_func: Vec<FuncClassified>,
funcs: &[&WitFunction],
schema: &SchemaLayouts,
names: NameInterner,
iface_name: BlobSlice,
) -> Result<(Vec<FuncDispatch>, StaticDataPlan)> {
let n_funcs = per_func.len();
debug_assert_eq!(
per_func.len(),
funcs.len(),
"FuncClassified list and WitFunction list must be index-aligned",
);
check_layout_budget(&per_func)?;
let mut layout = StaticLayout::new();
let mut symbols = SymbolBases::new();
let mut relocs = RelocPlan::new();
let name_blob = names.into_bytes();
let _ = layout.place_data(1, &name_blob);
let flags_scratch_addrs: Vec<u32> = flags_scratch_sizes(&per_func)
.into_iter()
.map(|n_bytes| layout.reserve_scratch(4, n_bytes))
.collect();
let char_scratch_addrs: Vec<u32> = char_scratch_sizes(&per_func)
.into_iter()
.map(|n_bytes| layout.reserve_scratch(1, n_bytes))
.collect();
let CharScratchMaps {
per_cell: char_scratch_map,
per_result_single: char_per_result_single,
} = {
let mut iter = char_scratch_addrs.iter().copied();
let maps = build_char_scratch_map(&per_func, &mut iter);
debug_assert!(iter.next().is_none());
maps
};
let enum_info_id = symbols.alloc();
let record_tuples_id = symbols.alloc();
let tuple_indices_id = symbols.alloc();
let enum_info = build_enum_info_blob(&per_func, &schema.enum_info_layout, enum_info_id);
let SideTableBlob {
segment: enum_segment,
per_param: enum_per_param_sym,
per_result: enum_per_result_sym,
} = enum_info;
let mut flags_scratch_iter = flags_scratch_addrs.iter().copied();
let FlagsInfoMaps {
per_cell_fill: flags_per_cell_fill,
per_result_single_fill: flags_per_result_single_fill,
per_param_count: flags_per_param_count,
per_result_count: flags_per_result_count,
} = build_flags_info_maps(&per_func, &mut flags_scratch_iter);
debug_assert!(
flags_scratch_iter.next().is_none(),
"flags scratch reservations must be consumed once per Cell::Flags",
);
let RecordInfoMaps {
tuples: record_tuples_seg,
per_cell_fill: mut record_per_cell_fill,
per_param_count: record_per_param_count,
per_result_count: record_per_result_count,
} = build_record_info_maps(
&per_func,
&schema.record_field_tuple_layout,
record_tuples_id,
);
let TupleIndicesBlob {
segment: tuple_indices_seg,
per_cell_idx: tuple_indices_per_cell,
} = build_tuple_indices_blob(&per_func, tuple_indices_id);
let VariantInfoMaps {
per_cell_fill: variant_per_cell_fill,
per_param_count: variant_per_param_count,
per_result_count: variant_per_result_count,
} = build_variant_info_maps(&per_func);
let HandleInfoMaps {
per_cell_fill: handle_per_cell_fill,
per_result_single_fill: handle_per_result_single_fill,
per_param_count: handle_per_param_count,
per_result_count: handle_per_result_count,
} = build_handle_info_maps(&per_func);
let record_tuples_base =
place_segment(&mut layout, &mut symbols, &mut relocs, record_tuples_seg);
place_segment(&mut layout, &mut symbols, &mut relocs, enum_segment);
place_segment(&mut layout, &mut symbols, &mut relocs, tuple_indices_seg);
back_fill_record_fields_ptrs(&mut record_per_cell_fill, record_tuples_base);
let (enum_per_param, enum_per_result) =
resolve_param_result_ranges(&symbols, enum_per_param_sym, enum_per_result_sym);
let param_side_tables: Vec<Vec<FieldSideTables>> = (0..n_funcs)
.map(|fn_idx| {
(0..per_func[fn_idx].params.len())
.map(|p_idx| FieldSideTables {
enum_infos: enum_per_param[fn_idx][p_idx],
flags_infos: BlobSlice {
off: 0,
len: flags_per_param_count[fn_idx][p_idx],
},
record_infos: BlobSlice {
off: 0,
len: record_per_param_count[fn_idx][p_idx],
},
variant_infos: BlobSlice {
off: 0,
len: variant_per_param_count[fn_idx][p_idx],
},
handle_infos: BlobSlice {
off: 0,
len: handle_per_param_count[fn_idx][p_idx],
},
})
.collect()
})
.collect();
let result_side_tables: Vec<FieldSideTables> = (0..n_funcs)
.map(|fn_idx| FieldSideTables {
enum_infos: enum_per_result[fn_idx],
flags_infos: BlobSlice {
off: 0,
len: flags_per_result_count[fn_idx],
},
record_infos: BlobSlice {
off: 0,
len: record_per_result_count[fn_idx],
},
variant_infos: BlobSlice {
off: 0,
len: variant_per_result_count[fn_idx],
},
handle_infos: BlobSlice {
off: 0,
len: handle_per_result_count[fn_idx],
},
})
.collect();
let fields_blob = build_fields_blob(&per_func, schema, ¶m_side_tables);
let (fields_base, _) = layout.place_data(schema.field_layout.align, &fields_blob);
let fields_buf_offsets: Vec<u32> = {
let mut cursor = fields_base;
per_func
.iter()
.map(|fd| {
let here = cursor;
cursor += fd.params.len() as u32 * schema.field_layout.size;
here
})
.collect()
};
let after_blob = build_after_params_blob(&per_func, schema, iface_name, &result_side_tables);
let after_params_offsets: Vec<Option<i32>> =
match schema.after_hook.as_ref().map(|h| &h.params_layout) {
Some(al) => {
let (after_base, _) = layout.place_data(al.align, &after_blob);
let mut cursor = after_base;
(0..n_funcs)
.map(|_| {
let here = cursor as i32;
cursor += al.size;
Some(here)
})
.collect()
}
None => vec![None; n_funcs],
};
let event_ptr = layout.reserve_scratch(EVENT_SLOT_ALIGN, EVENT_SLOT_SIZE) as i32;
let hook_params_ptr = schema
.before_hook
.as_ref()
.map(|h| layout.reserve_scratch(h.params_layout.align, h.params_layout.size));
let retptr_offsets: Vec<Option<i32>> = per_func
.iter()
.zip(funcs.iter())
.map(|(fd, func)| {
if !(fd.export_sig.retptr || fd.import_sig.retptr) {
return None;
}
let result_ty = func
.result
.as_ref()
.expect("retptr → func.result is_some()");
let size = schema.size_align.size(result_ty).size_wasm32() as u32;
let align = schema.size_align.align(result_ty).align_wasm32() as u32;
Some(layout.reserve_scratch(align, size) as i32)
})
.collect();
let params_record_offsets: Vec<Option<i32>> = per_func
.iter()
.zip(funcs.iter())
.map(|(fd, func)| {
if !(matches!(fd.shape, FuncShape::Async(_)) && fd.import_sig.indirect_params) {
return None;
}
let param_types: Vec<Type> = func.params.iter().map(|p| p.ty).collect();
let info = schema.size_align.record(¶m_types);
let size = info.size.size_wasm32() as u32;
let align = info.align.align_wasm32() as u32;
Some(layout.reserve_scratch(align, size) as i32)
})
.collect();
let bump_start = layout.end().next_multiple_of(schema.cell_layout.align);
if bump_start > LAYOUT_SIZE_BUDGET {
bail!("static-data layout end {bump_start} exceeds i32 budget {LAYOUT_SIZE_BUDGET}");
}
let mut data_segments = layout.into_segments();
relocs.resolve(&symbols, &mut data_segments);
let dispatches: Vec<FuncDispatch> = per_func
.into_iter()
.enumerate()
.map(|(i, fc)| {
let params: Vec<ParamLayout> = fc
.params
.into_iter()
.enumerate()
.map(|(p_idx, lift)| {
let tuple_slices = tuple_indices_per_cell.resolve_param(i, p_idx, &symbols);
let sources = CellFillSources {
record_fill: record_per_cell_fill.for_param(i, p_idx),
tuple_indices: &tuple_slices,
flags_fill: flags_per_cell_fill.for_param(i, p_idx),
variant_fill: variant_per_cell_fill.for_param(i, p_idx),
char_scratch: char_scratch_map.for_param(i, p_idx),
handle_fill: handle_per_cell_fill.for_param(i, p_idx),
};
let cell_side = fold_cell_side_data(&lift.plan, &sources);
ParamLayout {
lift,
cell_side,
info_counts: InfoCounts {
handle: handle_per_param_count[i][p_idx],
flags: flags_per_param_count[i][p_idx],
record: record_per_param_count[i][p_idx],
variant: variant_per_param_count[i][p_idx],
},
}
})
.collect();
let retptr_offset = retptr_offsets[i];
let result_lift = fc.result_lift.map(|rl| {
let ResultLift { source, .. } = rl;
let layout_source = match source {
ResultSource::Direct(cell) => {
let fills = SingleCellFills {
flags_fill: &flags_per_result_single_fill[i],
char_scratch: &char_per_result_single[i],
handle_fill: &handle_per_result_single_fill[i],
};
let side_data = single_cell_side_data(&cell, &fills);
ResultSourceLayout::Direct { cell, side_data }
}
ResultSource::Compound(compound) => {
let tuple_slices = tuple_indices_per_cell.resolve_result(i, &symbols);
let sources = CellFillSources {
record_fill: record_per_cell_fill.for_result(i),
tuple_indices: &tuple_slices,
flags_fill: flags_per_cell_fill.for_result(i),
variant_fill: variant_per_cell_fill.for_result(i),
char_scratch: char_scratch_map.for_result(i),
handle_fill: handle_per_cell_fill.for_result(i),
};
let cell_side = fold_cell_side_data(&compound.plan, &sources);
ResultSourceLayout::Compound {
compound,
retptr_offset: retptr_offset
.expect("Compound → retptr scratch reserved"),
cell_side,
}
}
};
ResultLayout {
source: layout_source,
info_counts: InfoCounts {
handle: handle_per_result_count[i],
flags: flags_per_result_count[i],
record: record_per_result_count[i],
variant: variant_per_result_count[i],
},
}
});
let after = after_params_offsets[i].map(|params_offset| AfterSetup { params_offset });
FuncDispatch {
shape: fc.shape,
result_ty: fc.result_ty,
import_module: fc.import_module,
import_field: fc.import_field,
export_name: fc.export_name,
export_sig: fc.export_sig,
import_sig: fc.import_sig,
needs_cabi_post: fc.needs_cabi_post,
fn_name_offset: fc.fn_name_offset,
fn_name_len: fc.fn_name_len,
params,
fields_buf_offset: fields_buf_offsets[i],
retptr_offset,
params_record_offset: params_record_offsets[i],
result_lift,
after,
borrow_drops: fc.borrow_drops,
}
})
.collect();
Ok((
dispatches,
StaticDataPlan {
bump_start,
event_ptr,
hook_params_ptr,
data_segments,
},
))
}
#[cfg(test)]
mod tests {
use super::super::build_per_func_classified;
use super::super::schema::{compute_schema, SchemaLayouts};
use super::super::synthesize_adapter_world_wit;
use super::*;
use wit_parser::Resolve;
const TARGET_IFACE: &str = "test:layout-fixture/t@0.0.1";
const TARGET_WIT: &str = r#"
package test:layout-fixture@0.0.1;
interface t {
record point { x: u32, y: s32 }
f-noargs: func();
f-pair-u32: func(a: u32, b: u32) -> u32;
f-string: func(s: string);
f-string-result: func(x: u32) -> string;
f-record: func(p: point) -> bool;
}
"#;
struct LayoutEnv {
dispatches: Vec<FuncDispatch>,
plan: StaticDataPlan,
schema: SchemaLayouts,
}
impl LayoutEnv {
fn dispatch(&self, name: &str) -> &FuncDispatch {
self.dispatches
.iter()
.find(|fd| fd.export_name.contains(name))
.unwrap_or_else(|| panic!("no dispatch matching `{name}`"))
}
}
fn env() -> LayoutEnv {
env_with(true, true)
}
fn env_with(has_before: bool, has_after: bool) -> LayoutEnv {
use crate::contract::{versioned_interface, TIER2_AFTER, TIER2_BEFORE, TIER2_VERSION};
let common_wit = include_str!("../../../wit/common/world.wit");
let tier2_wit = include_str!("../../../wit/tier2/world.wit");
let mut resolve = Resolve::new();
resolve.push_str("test.wit", TARGET_WIT).unwrap();
resolve.push_str("common.wit", common_wit).unwrap();
resolve.push_str("tier2.wit", tier2_wit).unwrap();
let mut hook_ifaces: Vec<String> = Vec::new();
if has_before {
hook_ifaces.push(versioned_interface(TIER2_BEFORE, TIER2_VERSION));
}
if has_after {
hook_ifaces.push(versioned_interface(TIER2_AFTER, TIER2_VERSION));
}
let world_wit = synthesize_adapter_world_wit(
"test:layout-fixture-adapter",
"adapter",
TARGET_IFACE,
&hook_ifaces,
);
let world_pkg = resolve.push_str("world.wit", &world_wit).unwrap();
let world_id = resolve.select_world(&[world_pkg], Some("adapter")).unwrap();
let target_iface =
super::super::test_utils::iface_by_unversioned_qname(&resolve, "test:layout-fixture/t");
let funcs: Vec<&WitFunction> = resolve.interfaces[target_iface]
.functions
.values()
.collect();
let schema = compute_schema(&resolve, world_id, has_before, has_after).unwrap();
let mut names = NameInterner::new();
let iface_name = names.intern(TARGET_IFACE);
let classified =
build_per_func_classified(&resolve, target_iface, &funcs, &mut names).unwrap();
let (dispatches, plan) =
lay_out_static_memory(classified, &funcs, &schema, names, iface_name).unwrap();
LayoutEnv {
dispatches,
plan,
schema,
}
}
#[test]
fn fields_buf_offsets_per_func_are_contiguous() {
let env = env();
let fs = env.schema.field_layout.size;
assert!(env.dispatches.windows(2).all(|w| {
w[0].fields_buf_offset + (w[0].params.len() as u32) * fs == w[1].fields_buf_offset
}));
}
#[test]
fn after_setup_absent_when_after_hook_off() {
assert!(env_with(true, false)
.dispatches
.iter()
.all(|fd| fd.after.is_none()));
}
#[test]
fn after_setup_present_when_after_hook_on() {
assert!(env_with(true, true)
.dispatches
.iter()
.all(|fd| fd.after.is_some()));
}
#[test]
fn retptr_offset_set_iff_sig_uses_retptr() {
let env = env();
for fd in &env.dispatches {
assert_eq!(
fd.retptr_offset.is_some(),
fd.export_sig.retptr || fd.import_sig.retptr,
);
}
}
#[test]
fn fixture_covers_both_retptr_polarities() {
let env = env();
assert!(env.dispatches.iter().any(|fd| fd.retptr_offset.is_some()));
assert!(env.dispatches.iter().any(|fd| fd.retptr_offset.is_none()));
}
#[test]
fn dispatch_param_count_matches_wit_param_count() {
let env = env();
let counts: Vec<usize> = env.dispatches.iter().map(|fd| fd.params.len()).collect();
assert_eq!(counts, vec![0, 2, 1, 1, 1]);
}
#[test]
fn bump_start_aligned_to_cell_align() {
let env = env();
assert_eq!(env.plan.bump_start % env.schema.cell_layout.align, 0);
}
#[test]
fn bump_start_within_i32_budget() {
assert!(env().plan.bump_start <= i32::MAX as u32);
}
#[test]
fn data_segments_sit_below_bump_start() {
let env = env();
assert!(env
.plan
.data_segments
.iter()
.all(|(off, bytes)| off + bytes.len() as u32 <= env.plan.bump_start));
}
#[test]
fn fixture_includes_void_and_non_void_funcs() {
let env = env();
assert!(env.dispatch("f-noargs").result_lift.is_none());
assert!(env.dispatch("f-pair-u32").result_lift.is_some());
}
fn try_lay_out(target_wit: &str, target_iface_qname: &str) -> Result<()> {
use crate::contract::{versioned_interface, TIER2_AFTER, TIER2_BEFORE, TIER2_VERSION};
let common_wit = include_str!("../../../wit/common/world.wit");
let tier2_wit = include_str!("../../../wit/tier2/world.wit");
let mut resolve = Resolve::new();
resolve.push_str("test.wit", target_wit).unwrap();
resolve.push_str("common.wit", common_wit).unwrap();
resolve.push_str("tier2.wit", tier2_wit).unwrap();
let hook_ifaces = vec![
versioned_interface(TIER2_BEFORE, TIER2_VERSION),
versioned_interface(TIER2_AFTER, TIER2_VERSION),
];
let target_versioned = format!("{target_iface_qname}@0.0.1");
let world_wit = synthesize_adapter_world_wit(
"test:budget-fixture-adapter",
"adapter",
&target_versioned,
&hook_ifaces,
);
let world_pkg = resolve.push_str("world.wit", &world_wit).unwrap();
let world_id = resolve.select_world(&[world_pkg], Some("adapter")).unwrap();
let target_iface =
super::super::test_utils::iface_by_unversioned_qname(&resolve, target_iface_qname);
let funcs: Vec<&WitFunction> = resolve.interfaces[target_iface]
.functions
.values()
.collect();
let schema = compute_schema(&resolve, world_id, true, true).unwrap();
let mut names = NameInterner::new();
let iface_name = names.intern(&target_versioned);
let classified = build_per_func_classified(&resolve, target_iface, &funcs, &mut names)?;
lay_out_static_memory(classified, &funcs, &schema, names, iface_name).map(|_| ())
}
#[test]
fn flat_slot_budget_bails_when_param_flatten_exceeds_cap() {
let n = MAX_FLAT_SLOTS_PER_FN + 1;
let fields = (0..n)
.map(|i| format!("f{i}: u32"))
.collect::<Vec<_>>()
.join(", ");
let wit = format!(
"package test:budget-flat@0.0.1;\n\
interface t {{\n\
record big {{ {fields} }}\n\
bloat: func(b: big);\n\
}}\n"
);
let err = try_lay_out(&wit, "test:budget-flat/t")
.expect_err("flat-slot budget should bail at MAX_FLAT_SLOTS_PER_FN + 1");
let msg = err.to_string();
assert!(
msg.contains("flat-slot count") && msg.contains(&MAX_FLAT_SLOTS_PER_FN.to_string()),
"bail should name the budget, got: {msg}"
);
}
#[test]
fn cell_budget_bails_when_record_param_exceeds_cap() {
let n = MAX_CELLS_PER_PARAM;
let fields = (0..n)
.map(|i| format!("f{i}: u32"))
.collect::<Vec<_>>()
.join(", ");
let wit = format!(
"package test:budget-cells@0.0.1;\n\
interface t {{\n\
record big {{ {fields} }}\n\
bloat: func(b: big);\n\
}}\n"
);
let err = try_lay_out(&wit, "test:budget-cells/t")
.expect_err("cell budget should bail at MAX_CELLS_PER_PARAM + 1");
let msg = err.to_string();
assert!(
msg.contains("cell count") && msg.contains(&MAX_CELLS_PER_PARAM.to_string()),
"bail should name the budget, got: {msg}"
);
}
}