use anyhow::{anyhow, bail, Result};
use std::collections::HashMap;
use wasm_encoder::{
BlockType, CodeSection, ConstExpr, DataSection, EntityType, ExportKind, ExportSection,
Function, GlobalSection, GlobalType, ImportSection, MemArg, MemorySection, MemoryType, Module,
ValType,
};
use wit_bindgen_core::abi::Bitcast;
use wit_parser::abi::{AbiVariant, FlatTypes, WasmSignature, WasmType};
use wit_parser::{
Function as WitFunction, Handle, Int, InterfaceId, Resolve, ResourceIntrinsic, SizeAlign, Type,
TypeDefKind, TypeId, TypeOwner, WasmImport, WorldId, WorldItem, WorldKey,
};
use super::super::indices::LocalsBuilder;
use super::super::resolve::{hook_callback_mangling, sync_mangling};
pub(crate) const EXPORT_MEMORY: &str = "memory";
pub(crate) const EXPORT_CABI_REALLOC: &str = "cabi_realloc";
pub(crate) const EXPORT_INITIALIZE: &str = "_initialize";
pub(crate) const WASM_PAGE_SIZE: u32 = 64 * 1024;
pub(crate) struct GlobalIndices {
pub bump: u32,
pub call_id_counter: u32,
}
pub(crate) fn emit_memory_and_globals(module: &mut Module, bump_start: u32) -> GlobalIndices {
let pages_for_static = bump_start.div_ceil(WASM_PAGE_SIZE).max(1);
let mut memory = MemorySection::new();
memory.memory(MemoryType {
minimum: pages_for_static as u64,
maximum: None,
memory64: false,
shared: false,
page_size_log2: None,
});
module.section(&memory);
let mut globals = GlobalSection::new();
let mut next_global: u32 = 0;
let mut alloc_global = |val_type: ValType, init: ConstExpr| {
globals.global(
GlobalType {
val_type,
mutable: true,
shared: false,
},
&init,
);
let idx = next_global;
next_global += 1;
idx
};
let bump = alloc_global(ValType::I32, ConstExpr::i32_const(bump_start as i32));
let call_id_counter = alloc_global(ValType::I64, ConstExpr::i64_const(0));
module.section(&globals);
GlobalIndices {
bump,
call_id_counter,
}
}
pub(crate) fn emit_cabi_realloc(code: &mut CodeSection, bump_global: u32) {
const PARAM_COUNT: u32 = 4;
const ALIGN_LOCAL: u32 = 2;
const NEW_SIZE_LOCAL: u32 = 3;
let mut locals = LocalsBuilder::new(PARAM_COUNT);
let aligned = locals.alloc_local(ValType::I32);
let new_bump = locals.alloc_local(ValType::I32);
let mut f = Function::new_with_locals_types(locals.freeze().locals);
f.instructions().global_get(bump_global);
f.instructions().local_get(ALIGN_LOCAL);
f.instructions().i32_const(1);
f.instructions().i32_sub();
f.instructions().i32_add();
f.instructions().local_get(ALIGN_LOCAL);
f.instructions().i32_const(1);
f.instructions().i32_sub();
f.instructions().i32_const(-1);
f.instructions().i32_xor();
f.instructions().i32_and();
f.instructions().local_set(aligned);
f.instructions().local_get(aligned);
f.instructions().local_get(NEW_SIZE_LOCAL);
f.instructions().i32_add();
f.instructions().local_tee(new_bump);
f.instructions().memory_size(0);
f.instructions()
.i32_const(WASM_PAGE_SIZE.trailing_zeros() as i32);
f.instructions().i32_shl();
f.instructions().i32_gt_u();
f.instructions().if_(BlockType::Empty);
{
f.instructions().local_get(new_bump);
f.instructions().memory_size(0);
f.instructions()
.i32_const(WASM_PAGE_SIZE.trailing_zeros() as i32);
f.instructions().i32_shl();
f.instructions().i32_sub();
f.instructions().i32_const(1);
f.instructions().i32_sub();
f.instructions()
.i32_const(WASM_PAGE_SIZE.trailing_zeros() as i32);
f.instructions().i32_shr_u();
f.instructions().i32_const(1);
f.instructions().i32_add();
f.instructions().memory_grow(0);
f.instructions().i32_const(-1);
f.instructions().i32_eq();
f.instructions().if_(BlockType::Empty);
f.instructions().unreachable();
f.instructions().end();
}
f.instructions().end();
f.instructions().local_get(new_bump);
f.instructions().global_set(bump_global);
f.instructions().local_get(aligned);
f.instructions().end();
code.function(&f);
}
#[derive(Clone, Copy)]
pub(crate) struct BumpReset {
pub global: u32,
pub saved_local: u32,
}
pub(crate) fn emit_bump_save(f: &mut Function, br: BumpReset) {
f.instructions().global_get(br.global);
f.instructions().local_set(br.saved_local);
}
pub(crate) fn emit_bump_restore(f: &mut Function, br: BumpReset) {
f.instructions().local_get(br.saved_local);
f.instructions().global_set(br.global);
}
pub(crate) struct WrapperExport<'a> {
pub export_name: &'a str,
pub cabi_post_idx: Option<u32>,
}
pub(crate) fn emit_export_section(
module: &mut Module,
wrappers: &[WrapperExport<'_>],
wrapper_base: u32,
init_idx: u32,
cabi_realloc_idx: u32,
) {
let mut exports = ExportSection::new();
for (i, w) in wrappers.iter().enumerate() {
exports.export(w.export_name, ExportKind::Func, wrapper_base + i as u32);
if let Some(post_idx) = w.cabi_post_idx {
exports.export(
&format!("cabi_post_{}", w.export_name),
ExportKind::Func,
post_idx,
);
}
}
exports.export(EXPORT_MEMORY, ExportKind::Memory, 0);
exports.export(EXPORT_CABI_REALLOC, ExportKind::Func, cabi_realloc_idx);
exports.export(EXPORT_INITIALIZE, ExportKind::Func, init_idx);
module.section(&exports);
}
pub(crate) fn emit_data_section(module: &mut Module, segments: &[(u32, Vec<u8>)]) {
if segments.is_empty() {
return;
}
let mut data = DataSection::new();
for (offset, bytes) in segments {
data.active(
0,
&ConstExpr::i32_const(*offset as i32),
bytes.iter().copied(),
);
}
module.section(&data);
}
pub(crate) fn empty_function() -> Function {
let mut f = Function::new_with_locals_types([]);
f.instructions().end();
f
}
pub(crate) fn val_types(types: &[WasmType]) -> Vec<ValType> {
types.iter().copied().map(wasm_type_to_val).collect()
}
pub(crate) fn wasm_type_to_val(wt: WasmType) -> ValType {
match wt {
WasmType::I32 | WasmType::Pointer | WasmType::Length => ValType::I32,
WasmType::I64 | WasmType::PointerOrI64 => ValType::I64,
WasmType::F32 => ValType::F32,
WasmType::F64 => ValType::F64,
}
}
pub(crate) fn direct_return_type(export_sig: &WasmSignature) -> Option<ValType> {
if !export_sig.retptr && export_sig.results.len() == 1 {
Some(wasm_type_to_val(export_sig.results[0]))
} else {
None
}
}
pub(crate) fn emit_bitcast(f: &mut Function, bc: &Bitcast) {
use Bitcast::*;
match bc {
None => {}
I32ToI64 => {
f.instructions().i64_extend_i32_u();
}
I64ToI32 => {
f.instructions().i32_wrap_i64();
}
F32ToI32 => {
f.instructions().i32_reinterpret_f32();
}
I32ToF32 => {
f.instructions().f32_reinterpret_i32();
}
F64ToI64 => {
f.instructions().i64_reinterpret_f64();
}
I64ToF64 => {
f.instructions().f64_reinterpret_i64();
}
F32ToI64 => {
f.instructions().i32_reinterpret_f32();
f.instructions().i64_extend_i32_u();
}
I64ToF32 => {
f.instructions().i32_wrap_i64();
f.instructions().f32_reinterpret_i32();
}
PToI32 | I32ToP | I32ToL | LToI32 | PToL | LToP => {}
I64ToP64 | P64ToI64 => {}
PToP64 | LToI64 => {
f.instructions().i64_extend_i32_u();
}
P64ToP | I64ToL => {
f.instructions().i32_wrap_i64();
}
Sequence(pair) => {
let [a, b] = pair.as_ref();
emit_bitcast(f, a);
emit_bitcast(f, b);
}
}
}
pub(crate) fn emit_handler_call(
f: &mut Function,
nparams: u32,
import_retptr: bool,
retptr_offset: Option<i32>,
handler_idx: u32,
) {
for p in 0..nparams {
f.instructions().local_get(p);
}
if import_retptr {
let off = retptr_offset.expect("import_retptr → retptr_offset must be Some");
f.instructions().i32_const(off);
}
f.instructions().call(handler_idx);
}
pub(crate) fn require_indirect_params_supported_shape(
resolve: &Resolve,
fn_name: &str,
func: &WitFunction,
) -> Result<()> {
for param in &func.params {
if !is_supported_indirect_params_ty(resolve, ¶m.ty) {
bail!(
"async function `{fn_name}` has params that overflow \
MAX_FLAT_ASYNC_PARAMS ({}) AND param `{}` carries an \
unsupported type.",
Resolve::MAX_FLAT_ASYNC_PARAMS,
param.name,
);
}
if super::compat::flat_types(resolve, ¶m.ty, None).is_none() {
bail!(
"async function `{fn_name}` param `{}` flat representation \
exceeds MAX_FLAT_PARAMS ({}).",
param.name,
super::compat::MAX_FLAT_PARAMS,
);
}
}
Ok(())
}
fn is_supported_indirect_params_ty(resolve: &Resolve, ty: &Type) -> bool {
match ty {
Type::Bool
| Type::S8
| Type::U8
| Type::S16
| Type::U16
| Type::S32
| Type::U32
| Type::S64
| Type::U64
| Type::F32
| Type::F64
| Type::Char
| Type::String
| Type::ErrorContext => true,
Type::Id(id) => match &resolve.types[*id].kind {
TypeDefKind::Type(inner) => is_supported_indirect_params_ty(resolve, inner),
TypeDefKind::Record(r) => r
.fields
.iter()
.all(|f| is_supported_indirect_params_ty(resolve, &f.ty)),
TypeDefKind::Tuple(t) => t
.types
.iter()
.all(|t| is_supported_indirect_params_ty(resolve, t)),
TypeDefKind::Variant(v) => v.cases.iter().all(|c| match &c.ty {
Some(t) => is_supported_indirect_params_ty(resolve, t),
None => true,
}),
TypeDefKind::Option(t) => is_supported_indirect_params_ty(resolve, t),
TypeDefKind::Result(r) => {
r.ok.as_ref()
.is_none_or(|t| is_supported_indirect_params_ty(resolve, t))
&& r.err
.as_ref()
.is_none_or(|t| is_supported_indirect_params_ty(resolve, t))
}
TypeDefKind::List(t) | TypeDefKind::FixedLengthList(t, _) => {
is_supported_indirect_params_ty(resolve, t)
}
TypeDefKind::Map(k, v) => {
is_supported_indirect_params_ty(resolve, k)
&& is_supported_indirect_params_ty(resolve, v)
}
TypeDefKind::Enum(_)
| TypeDefKind::Flags(_)
| TypeDefKind::Handle(_)
| TypeDefKind::Future(_)
| TypeDefKind::Stream(_) => true,
TypeDefKind::Resource | TypeDefKind::Unknown => false,
},
}
}
pub(crate) fn build_lower_params_to_memory(
resolve: &Resolve,
sizes: &SizeAlign,
indices: &mut super::super::indices::LocalsBuilder,
func: &WitFunction,
params_record_base: i32,
) -> Vec<wasm_encoder::Instruction<'static>> {
use super::compat::flat_types;
use wit_bindgen_core::abi::lower_to_memory;
let param_types: Vec<Type> = func.params.iter().map(|p| p.ty).collect();
let field_offsets = sizes.field_offsets(¶m_types);
let total_flat_count: u32 = param_types
.iter()
.map(|ty| {
flat_types(resolve, ty, None)
.unwrap_or_else(|| {
unreachable!(
"param flat width exceeds MAX_FLAT_PARAMS — \
require_indirect_params_supported_shape should reject upstream"
)
})
.len() as u32
})
.sum();
let addr_local = indices.alloc_local(wasm_encoder::ValType::I32);
let mut bg = super::WasmEncoderBindgen::new(sizes, addr_local, indices)
.with_param_flat_count(total_flat_count);
for ((field_off, _field_size), ty) in field_offsets.iter().zip(¶m_types) {
let effective_addr = params_record_base + field_off.size_wasm32() as i32;
bg.emit_set_addr_const(effective_addr);
lower_to_memory(resolve, &mut bg, (), (), ty);
}
bg.into_instructions()
}
pub(crate) fn emit_wrapper_return(
f: &mut Function,
result_local: Option<u32>,
export_retptr: bool,
retptr_offset: Option<i32>,
) {
if let Some(local) = result_local {
f.instructions().local_get(local);
} else if export_retptr {
let off = retptr_offset.expect("export_retptr → retptr_offset must be Some");
f.instructions().i32_const(off);
}
}
pub(crate) struct RecordLayout {
pub size: u32,
pub align: u32,
pub field_offsets: std::collections::HashMap<String, u32>,
}
impl RecordLayout {
pub(crate) fn for_named_fields(sizes: &SizeAlign, fields: &[(String, Type)]) -> Self {
let types: Vec<Type> = fields.iter().map(|(_, t)| *t).collect();
let info = sizes.record(&types);
let offs = sizes.field_offsets(&types);
Self {
size: info.size.size_wasm32() as u32,
align: info.align.align_wasm32() as u32,
field_offsets: fields
.iter()
.zip(offs)
.map(|((name, _), (off, _))| (name.clone(), off.size_wasm32() as u32))
.collect(),
}
}
pub(crate) fn for_record_typedef(sizes: &SizeAlign, resolve: &Resolve, id: TypeId) -> Self {
let typedef = &resolve.types[id];
let TypeDefKind::Record(r) = &typedef.kind else {
panic!(
"RecordLayout::for_record_typedef called with non-record typedef `{:?}`",
typedef.name
);
};
let fields: Vec<(String, Type)> = r.fields.iter().map(|f| (f.name.clone(), f.ty)).collect();
Self::for_named_fields(sizes, &fields)
}
pub(crate) fn offset_of(&self, name: &str) -> u32 {
*self.field_offsets.get(name).unwrap_or_else(|| {
let mut keys: Vec<&str> = self.field_offsets.keys().map(|s| s.as_str()).collect();
keys.sort();
panic!("RecordLayout: no field named `{name}` (record has: {keys:?})")
})
}
}
pub(crate) struct HookImport {
pub module: String,
pub name: String,
pub sig: WasmSignature,
pub params: Vec<(String, Type)>,
}
pub(crate) fn find_imported_hook(
resolve: &Resolve,
world_id: WorldId,
target_iface: &str,
) -> Option<HookImport> {
let world = &resolve.worlds[world_id];
world.imports.iter().find_map(|(key, item)| {
let WorldItem::Interface { id, .. } = item else {
return None;
};
if resolve.id_of(*id).as_deref() != Some(target_iface) {
return None;
}
let func = resolve.interfaces[*id].functions.values().next()?;
let (module, name) = resolve.wasm_import_name(
hook_callback_mangling(),
WasmImport::Func {
interface: Some(key),
func,
},
);
Some(HookImport {
module,
name,
sig: resolve.wasm_signature(AbiVariant::GuestImportAsync, func),
params: func.params.iter().map(|p| (p.name.clone(), p.ty)).collect(),
})
})
}
pub(crate) fn synthesize_adapter_world_wit(
package_name: &str,
world_name: &str,
target_interface: &str,
hook_iface_imports: &[String],
) -> String {
let mut wit = format!("package {package_name};\n\nworld {world_name} {{\n");
wit.push_str(&format!(" import {target_interface};\n"));
wit.push_str(&format!(" export {target_interface};\n"));
for iface in hook_iface_imports {
wit.push_str(&format!(" import {iface};\n"));
}
wit.push_str("}\n");
wit
}
pub(crate) fn find_common_typeid(resolve: &Resolve, type_name: &str) -> Result<TypeId> {
for (id, _) in resolve.interfaces.iter() {
let Some(qname) = resolve.id_of(id) else {
continue;
};
let unversioned = qname.split('@').next().unwrap_or(&qname);
if unversioned == "splicer:common/types" {
return resolve.interfaces[id]
.types
.get(type_name)
.copied()
.ok_or_else(|| anyhow!("`splicer:common/types` is missing typedef `{type_name}`"));
}
}
bail!("resolve has no `splicer:common/types` interface — was the common WIT loaded?")
}
pub(crate) fn call_id_layout(resolve: &Resolve, sizes: &SizeAlign) -> Result<CallIdLayout> {
let id = find_common_typeid(resolve, "call-id")?;
Ok(CallIdLayout(RecordLayout::for_record_typedef(
sizes, resolve, id,
)))
}
pub(crate) fn option_payload_offset(sizes: &SizeAlign, payload_ty: &Type) -> u32 {
sizes
.payload_offset(Int::U8, [Some(payload_ty)])
.size_wasm32() as u32
}
pub(crate) const SLICE_PTR_OFFSET: u32 = 0;
pub(crate) const SLICE_LEN_OFFSET: u32 = 4;
pub(crate) const STRING_FLAT_BYTES: u32 = 8;
pub(crate) const MAX_UTF8_LEN: u32 = char::MAX.len_utf8() as u32;
pub(crate) const OPTION_NONE: u8 = 0;
pub(crate) const OPTION_SOME: u8 = 1;
pub(crate) const I32_STORE_LOG2_ALIGN: u32 = 2;
pub(crate) const I8_STORE_LOG2_ALIGN: u32 = 0;
pub(crate) const I64_STORE_LOG2_ALIGN: u32 = 3;
const CALLID_IFACE: &str = "interface-name";
const CALLID_FN: &str = "function-name";
const CALLID_ID: &str = "id";
pub(crate) struct CallIdLayout(RecordLayout);
impl CallIdLayout {
pub(crate) fn size(&self) -> u32 {
self.0.size
}
pub(crate) fn align(&self) -> u32 {
self.0.align
}
pub(crate) fn iface_off(&self) -> u32 {
self.0.offset_of(CALLID_IFACE)
}
pub(crate) fn fn_off(&self) -> u32 {
self.0.offset_of(CALLID_FN)
}
pub(crate) fn id_off(&self) -> u32 {
self.0.offset_of(CALLID_ID)
}
pub(crate) fn store_names_in_blob(
&self,
blob: &mut [u8],
base: usize,
iface_name: BlobSlice,
fn_name: BlobSlice,
) {
store_slice_in_blob(blob, base + self.iface_off() as usize, iface_name);
store_slice_in_blob(blob, base + self.fn_off() as usize, fn_name);
}
}
pub(crate) fn store_slice_in_blob(blob: &mut [u8], off: usize, slice: BlobSlice) {
blob[off + SLICE_PTR_OFFSET as usize..][..4].copy_from_slice(&(slice.off as i32).to_le_bytes());
blob[off + SLICE_LEN_OFFSET as usize..][..4].copy_from_slice(&(slice.len as i32).to_le_bytes());
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct BlobSlice {
pub off: u32,
pub len: u32,
}
impl BlobSlice {
pub(crate) const EMPTY: BlobSlice = BlobSlice { off: 0, len: 0 };
}
pub(crate) fn emit_cabi_realloc_call(
f: &mut Function,
cabi_realloc_idx: u32,
align: u32,
size: u32,
dest_local: u32,
) {
debug_assert!(
size <= i32::MAX as u32,
"cabi_realloc size {size} doesn't fit in signed i32",
);
f.instructions().i32_const(0);
f.instructions().i32_const(0);
f.instructions().i32_const(align as i32);
f.instructions().i32_const(size as i32);
f.instructions().call(cabi_realloc_idx);
f.instructions().local_set(dest_local);
}
pub(crate) fn emit_trap_if_list_overflows_cell_slab(
f: &mut Function,
len_local: u32,
elem_count: u32,
next_cell_idx_local: u32,
cell_size: u32,
) {
assert!(elem_count > 0, "element plan must contribute ≥1 cell");
assert!(cell_size > 0, "cell_size must be positive");
let cell_limit = (i32::MAX as u32) / cell_size;
f.instructions().local_get(len_local);
f.instructions().i32_const(cell_limit as i32);
f.instructions().local_get(next_cell_idx_local);
f.instructions().i32_sub();
if elem_count != 1 {
f.instructions().i32_const(elem_count as i32);
f.instructions().i32_div_u();
}
f.instructions().i32_gt_u();
f.instructions().if_(BlockType::Empty);
f.instructions().unreachable();
f.instructions().end();
}
pub(crate) fn emit_cabi_realloc_call_runtime(
f: &mut Function,
cabi_realloc_idx: u32,
align: u32,
count_local: u32,
elem_bytes: u32,
dest_local: u32,
) {
assert!(elem_bytes > 0, "elem_bytes must be positive");
let max_count = (i32::MAX as u32) / elem_bytes;
f.instructions().local_get(count_local);
f.instructions().i32_const(max_count as i32);
f.instructions().i32_gt_u();
f.instructions().if_(BlockType::Empty);
f.instructions().unreachable();
f.instructions().end();
f.instructions().i32_const(0);
f.instructions().i32_const(0);
f.instructions().i32_const(align as i32);
f.instructions().local_get(count_local);
if elem_bytes != 1 {
f.instructions().i32_const(elem_bytes as i32);
f.instructions().i32_mul();
}
f.instructions().call(cabi_realloc_idx);
f.instructions().local_set(dest_local);
}
pub(crate) fn emit_store_slice_ptr_runtime(
f: &mut Function,
base_ptr: i32,
field_off: u32,
ptr_local: u32,
) {
f.instructions().i32_const(base_ptr);
f.instructions().local_get(ptr_local);
f.instructions().i32_store(MemArg {
offset: (field_off + SLICE_PTR_OFFSET) as u64,
align: I32_STORE_LOG2_ALIGN,
memory_index: 0,
});
}
pub(crate) fn emit_store_slice_len_runtime(
f: &mut Function,
base_ptr: i32,
field_off: u32,
len_local: u32,
) {
f.instructions().i32_const(base_ptr);
f.instructions().local_get(len_local);
f.instructions().i32_store(MemArg {
offset: (field_off + SLICE_LEN_OFFSET) as u64,
align: I32_STORE_LOG2_ALIGN,
memory_index: 0,
});
}
pub(crate) fn emit_store_slice(f: &mut Function, base_ptr: i32, field_off: u32, slice: BlobSlice) {
let store = |f: &mut Function, sub_off: u32, value: i32| {
f.instructions().i32_const(base_ptr);
f.instructions().i32_const(value);
f.instructions().i32_store(MemArg {
offset: (field_off + sub_off) as u64,
align: I32_STORE_LOG2_ALIGN,
memory_index: 0,
});
};
store(f, SLICE_PTR_OFFSET, slice.off as i32);
store(f, SLICE_LEN_OFFSET, slice.len as i32);
}
pub(crate) fn emit_store_i64_local(f: &mut Function, base_ptr: i32, field_off: u32, local: u32) {
f.instructions().i32_const(base_ptr);
f.instructions().local_get(local);
f.instructions().i64_store(MemArg {
offset: field_off as u64,
align: I64_STORE_LOG2_ALIGN,
memory_index: 0,
});
}
pub(crate) fn emit_alloc_call_id(f: &mut Function, counter_global: u32, id_local: u32) {
f.instructions().global_get(counter_global);
f.instructions().i64_const(1);
f.instructions().i64_add();
f.instructions().local_tee(id_local);
f.instructions().global_set(counter_global);
}
pub(crate) fn require_no_inline_resources(
resolve: &Resolve,
target_iface: InterfaceId,
) -> Result<()> {
let iface = &resolve.interfaces[target_iface];
for (ty_name, &tid) in &iface.types {
let td = &resolve.types[tid];
if matches!(td.kind, TypeDefKind::Resource)
&& matches!(td.owner, TypeOwner::Interface(owner) if owner == target_iface)
{
let iface_name = resolve
.id_of(target_iface)
.unwrap_or_else(|| iface.name.clone().unwrap_or_default());
bail!(
"interface `{iface_name}` declares resource `{ty_name}` inline. \
Splicer's wrapper-component pattern can't preserve resource \
type identity for inline resources — runtime handle traffic \
between the import side and export side will be rejected. \
Move `{ty_name}` into a sibling `types` interface and \
reference it via `use types.{{{ty_name}}}` (the wasi-style \
factored-types pattern)."
);
}
}
Ok(())
}
pub(crate) fn collect_borrow_drops(resolve: &Resolve, func: &WitFunction) -> Vec<(u32, TypeId)> {
let mut out = Vec::new();
let mut flat_idx: u32 = 0;
for param in &func.params {
if let Type::Id(tid) = param.ty {
if let TypeDefKind::Handle(Handle::Borrow(rid)) = &resolve.types[tid].kind {
out.push((flat_idx, resolve_type_alias(resolve, *rid)));
flat_idx += 1;
continue;
}
}
let mut storage = vec![WasmType::I32; 32];
let mut flat = FlatTypes::new(storage.as_mut_slice());
if !resolve.push_flat(¶m.ty, &mut flat) {
return Vec::new();
}
flat_idx += flat.to_vec().len() as u32;
}
out
}
pub(crate) fn resolve_type_alias(resolve: &Resolve, mut tid: TypeId) -> TypeId {
while let TypeDefKind::Type(Type::Id(next)) = &resolve.types[tid].kind {
tid = *next;
}
tid
}
pub(crate) fn emit_resource_drop_imports<Fd>(
imports: &mut ImportSection,
resolve: &Resolve,
per_func: &[Fd],
drops_of: impl Fn(&Fd) -> &[(u32, TypeId)],
drop_ty: Option<u32>,
mut alloc_func: impl FnMut() -> u32,
) -> HashMap<TypeId, u32> {
let Some(drop_ty) = drop_ty else {
return HashMap::new();
};
let mut unique: Vec<TypeId> = per_func
.iter()
.flat_map(|fd| drops_of(fd).iter().map(|(_, rid)| *rid))
.collect();
unique.sort();
unique.dedup();
let mut out: HashMap<TypeId, u32> = HashMap::new();
for rid in unique {
let owner_iface = match resolve.types[rid].owner {
TypeOwner::Interface(iid) => iid,
other => panic!(
"borrow resource {rid:?} has owner {other:?}, expected TypeOwner::Interface — \
emit_borrow_drops's HashMap lookup would panic with no entry",
),
};
let owner_key = WorldKey::Interface(owner_iface);
let imp = WasmImport::ResourceIntrinsic {
interface: Some(&owner_key),
resource: rid,
intrinsic: ResourceIntrinsic::ImportedDrop,
};
let (module_name, field_name) = resolve.wasm_import_name(sync_mangling(), imp);
imports.import(&module_name, &field_name, EntityType::Function(drop_ty));
out.insert(rid, alloc_func());
}
out
}
pub(crate) fn emit_borrow_drops(
f: &mut Function,
borrow_drops: &[(u32, TypeId)],
resource_drop: &HashMap<TypeId, u32>,
) {
for (flat_idx, rid) in borrow_drops {
let drop_fn = resource_drop[rid];
f.instructions().local_get(*flat_idx);
f.instructions().call(drop_fn);
}
}
pub(crate) fn emit_populate_call_id(
f: &mut Function,
base_ptr: i32,
call_off: u32,
callid_layout: &CallIdLayout,
iface_name: BlobSlice,
fn_name: BlobSlice,
id_local: u32,
) {
let iface_off = call_off + callid_layout.iface_off();
let fn_off = call_off + callid_layout.fn_off();
let id_off = call_off + callid_layout.id_off();
emit_store_slice(f, base_ptr, iface_off, iface_name);
emit_store_slice(f, base_ptr, fn_off, fn_name);
emit_store_i64_local(f, base_ptr, id_off, id_local);
}
#[cfg(test)]
mod tests {
use super::super::super::indices::LocalsBuilder;
use super::*;
use wasm_encoder::Instruction;
use wasm_encoder::{CodeSection, EntityType, FunctionSection, ImportSection, TypeSection};
fn unreachable_count_for(elem_bytes: u32) -> usize {
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().function(
[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
[ValType::I32],
);
types.ty().function([ValType::I32], []);
module.section(&types);
let mut imports = ImportSection::new();
imports.import("env", "cabi_realloc", EntityType::Function(0));
module.section(&imports);
let mut funcs = FunctionSection::new();
funcs.function(1);
module.section(&funcs);
let mut code = CodeSection::new();
let mut f = Function::new([(1, ValType::I32)]);
emit_cabi_realloc_call_runtime(&mut f, 0, 4, 0, elem_bytes, 1);
f.instructions().end();
code.function(&f);
module.section(&code);
let bytes = module.finish();
wasmparser::Validator::new()
.validate_all(&bytes)
.expect("emit_cabi_realloc_call_runtime output must validate");
let parser = wasmparser::Parser::new(0);
for payload in parser.parse_all(&bytes) {
if let Ok(wasmparser::Payload::CodeSectionEntry(body)) = payload {
return body
.get_operators_reader()
.expect("ops reader")
.into_iter()
.filter(|op| matches!(op, Ok(wasmparser::Operator::Unreachable)))
.count();
}
}
panic!("no CodeSectionEntry in module");
}
#[test]
fn cabi_realloc_runtime_emits_overflow_trap() {
for elem_bytes in [1u32, 4, 16] {
let count = unreachable_count_for(elem_bytes);
assert_eq!(
count, 1,
"elem_bytes={elem_bytes}: expected 1 `unreachable`, found {count}",
);
}
}
fn list_overflow_unreachable_count(elem_count: u32, cell_size: u32) -> usize {
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().function([ValType::I32, ValType::I32], []);
module.section(&types);
let mut funcs = FunctionSection::new();
funcs.function(0);
module.section(&funcs);
let mut code = CodeSection::new();
let mut f = Function::new([]);
emit_trap_if_list_overflows_cell_slab(&mut f, 0, elem_count, 1, cell_size);
f.instructions().end();
code.function(&f);
module.section(&code);
let bytes = module.finish();
wasmparser::Validator::new()
.validate_all(&bytes)
.expect("emit_trap_if_list_overflows_cell_slab output must validate");
let parser = wasmparser::Parser::new(0);
for payload in parser.parse_all(&bytes) {
if let Ok(wasmparser::Payload::CodeSectionEntry(body)) = payload {
return body
.get_operators_reader()
.expect("ops reader")
.into_iter()
.filter(|op| matches!(op, Ok(wasmparser::Operator::Unreachable)))
.count();
}
}
panic!("no CodeSectionEntry in module");
}
#[test]
fn list_overflow_trap_emits_unreachable_for_every_shape() {
for &elem_count in &[1u32, 2, 16] {
for &cell_size in &[8u32, 16] {
let count = list_overflow_unreachable_count(elem_count, cell_size);
assert_eq!(
count, 1,
"elem_count={elem_count} cell_size={cell_size}: expected 1 `unreachable`, found {count}",
);
}
}
}
#[derive(Debug, Eq, PartialEq)]
enum StoreSig {
I32,
I64,
F32,
F64,
I32_8,
}
impl StoreSig {
fn match_offset(&self, inst: &Instruction<'_>) -> Option<u64> {
match (self, inst) {
(StoreSig::I32, Instruction::I32Store(ma)) => Some(ma.offset),
(StoreSig::I64, Instruction::I64Store(ma)) => Some(ma.offset),
(StoreSig::F32, Instruction::F32Store(ma)) => Some(ma.offset),
(StoreSig::F64, Instruction::F64Store(ma)) => Some(ma.offset),
(StoreSig::I32_8, Instruction::I32Store8(ma)) => Some(ma.offset),
_ => None,
}
}
}
fn run_lower_params(
wit: &str,
pkg_iface: &str,
params_record_base: i32,
nparams_flat: u32,
) -> (SizeAlign, Vec<Instruction<'static>>) {
let mut resolve = Resolve::default();
resolve.push_str("test.wit", wit).expect("parse test WIT");
let (_, iface) = resolve
.interfaces
.iter()
.find(|(id, _)| resolve.id_of(*id).as_deref() == Some(pkg_iface))
.expect("target interface present");
let func = iface
.functions
.values()
.next()
.expect("interface must have one fn")
.clone();
let mut sizes = SizeAlign::default();
sizes.fill(&resolve);
let mut indices = LocalsBuilder::new(nparams_flat);
let insts =
build_lower_params_to_memory(&resolve, &sizes, &mut indices, &func, params_record_base);
(sizes, insts)
}
#[test]
fn build_lower_params_to_memory_pins_mixed_primitive_layout() {
const BASE: i32 = 1024;
let wit = r#"
package my:shape@1.0.0;
interface api {
mixed: async func(
a: u32, b: u64, c: f32, d: f64, e: bool, f: char
) -> u32;
}
"#;
let (sizes, insts) = run_lower_params(wit, "my:shape/api@1.0.0", BASE, 6);
let param_types = [
Type::U32,
Type::U64,
Type::F32,
Type::F64,
Type::Bool,
Type::Char,
];
let field_offs = sizes.field_offsets(¶m_types);
let expected: Vec<(i64, StoreSig)> = field_offs
.iter()
.zip([
StoreSig::I32, StoreSig::I64, StoreSig::F32, StoreSig::F64, StoreSig::I32_8, StoreSig::I32, ])
.map(|((off, _size), sig)| (BASE as i64 + off.size_wasm32() as i64, sig))
.collect();
const ADDR_LOCAL: u32 = 6;
const BLOCK_LEN: usize = 7;
assert_eq!(
insts.len(),
BLOCK_LEN * expected.len(),
"expected {} instructions per param, got {} total",
BLOCK_LEN,
insts.len(),
);
for (param_i, (eff_addr, sig)) in expected.iter().enumerate() {
let block = &insts[param_i * BLOCK_LEN..(param_i + 1) * BLOCK_LEN];
match &block[0] {
Instruction::I32Const(v) => assert_eq!(
*v as i64, *eff_addr,
"param {param_i}: i32.const should stage effective addr",
),
other => panic!("param {param_i}: block[0] expected I32Const, got {other:?}"),
}
assert!(
matches!(&block[1], Instruction::LocalSet(idx) if *idx == ADDR_LOCAL),
"param {param_i}: block[1] expected LocalSet({ADDR_LOCAL}), got {:?}",
&block[1],
);
assert!(
matches!(&block[2], Instruction::LocalGet(idx) if *idx == param_i as u32),
"param {param_i}: block[2] expected LocalGet({param_i}), got {:?}",
&block[2],
);
let tmp = match &block[3] {
Instruction::LocalSet(idx) => *idx,
other => panic!("param {param_i}: block[3] expected LocalSet, got {other:?}"),
};
assert!(
matches!(&block[4], Instruction::LocalGet(idx) if *idx == ADDR_LOCAL),
"param {param_i}: block[4] expected LocalGet({ADDR_LOCAL}), got {:?}",
&block[4],
);
assert!(
matches!(&block[5], Instruction::LocalGet(idx) if *idx == tmp),
"param {param_i}: block[5] should reload the same tmp ({tmp}) stored at [3], got {:?}",
&block[5],
);
let actual_offset = sig.match_offset(&block[6]).unwrap_or_else(|| {
panic!(
"param {param_i}: block[6] expected {sig:?} store, got {:?}",
&block[6],
)
});
assert_eq!(
actual_offset, 0,
"param {param_i}: store offset should be 0 (field offset goes in addr_local)",
);
}
}
#[test]
fn build_lower_params_to_memory_pins_record_field_offsets() {
const BASE: i32 = 2048;
let wit = r#"
package my:shape@1.0.0;
interface api {
record mixed-rec {
a: u32, b: u64, c: f32, d: f64, e: bool, f: char,
}
only-rec: async func(r: mixed-rec) -> u32;
}
"#;
let (sizes, insts) = run_lower_params(wit, "my:shape/api@1.0.0", BASE, 6);
let field_tys = [
Type::U32,
Type::U64,
Type::F32,
Type::F64,
Type::Bool,
Type::Char,
];
let field_offs = sizes.field_offsets(&field_tys);
let expected: Vec<(u64, StoreSig)> = field_offs
.iter()
.zip([
StoreSig::I32,
StoreSig::I64,
StoreSig::F32,
StoreSig::F64,
StoreSig::I32_8,
StoreSig::I32,
])
.map(|((off, _size), sig)| (off.size_wasm32() as u64, sig))
.collect();
const HEAD_LEN: usize = 2;
const FIELD_LEN: usize = 5;
assert_eq!(
insts.len(),
HEAD_LEN + FIELD_LEN * expected.len(),
"expected {HEAD_LEN} setup + {FIELD_LEN} per field, got {} total",
insts.len(),
);
const ADDR_LOCAL: u32 = 6;
match &insts[0] {
Instruction::I32Const(v) => {
assert_eq!(*v, BASE, "head: i32.const should stage params record base")
}
other => panic!("head[0] expected I32Const, got {other:?}"),
}
assert!(
matches!(&insts[1], Instruction::LocalSet(idx) if *idx == ADDR_LOCAL),
"head[1] expected LocalSet({ADDR_LOCAL}), got {:?}",
&insts[1],
);
for (field_i, (expected_off, sig)) in expected.iter().enumerate() {
let block = &insts[HEAD_LEN + field_i * FIELD_LEN..][..FIELD_LEN];
assert!(
matches!(&block[0], Instruction::LocalGet(idx) if *idx == field_i as u32),
"field {field_i}: block[0] expected LocalGet({field_i}), got {:?}",
&block[0],
);
let tmp = match &block[1] {
Instruction::LocalSet(idx) => *idx,
other => panic!("field {field_i}: block[1] expected LocalSet, got {other:?}"),
};
assert!(
matches!(&block[2], Instruction::LocalGet(idx) if *idx == ADDR_LOCAL),
"field {field_i}: block[2] expected LocalGet({ADDR_LOCAL}), got {:?}",
&block[2],
);
assert!(
matches!(&block[3], Instruction::LocalGet(idx) if *idx == tmp),
"field {field_i}: block[3] should reload tmp ({tmp}), got {:?}",
&block[3],
);
let actual_offset = sig.match_offset(&block[4]).unwrap_or_else(|| {
panic!(
"field {field_i}: block[4] expected {sig:?} store, got {:?}",
&block[4],
)
});
assert_eq!(
actual_offset, *expected_off,
"field {field_i}: store offset should match canonical record layout",
);
}
}
#[test]
fn build_lower_params_to_memory_reuses_tmp_across_same_valtype() {
const BASE: i32 = 64;
let wit = r#"
package my:shape@1.0.0;
interface api {
many: async func(
a: u32, b: u32, c: u32, d: u32, e: u32
) -> u32;
}
"#;
let (_sizes, insts) = run_lower_params(wit, "my:shape/api@1.0.0", BASE, 5);
const BLOCK_LEN: usize = 7;
assert_eq!(insts.len(), BLOCK_LEN * 5);
let first_tmp = match &insts[3] {
Instruction::LocalSet(idx) => *idx,
other => panic!("block[0]'s [3] expected LocalSet, got {other:?}"),
};
for i in 1..5 {
let block = &insts[i * BLOCK_LEN..(i + 1) * BLOCK_LEN];
assert!(
matches!(&block[3], Instruction::LocalSet(idx) if *idx == first_tmp),
"param {i}: tmp local should match first param's tmp ({first_tmp})",
);
}
}
}