use std::borrow::Cow;
use wasm_encoder::{BlockType, Instruction, MemArg, ValType};
use wit_bindgen_core::abi::{Bindgen, Bitcast, Instruction as AbiInst, WasmType};
use wit_parser::{Alignment, ArchitectureSize, Resolve, SizeAlign, Type};
use super::compat::{cast, flat_types};
use crate::adapter::indices::FunctionIndices;
pub(crate) struct WasmEncoderBindgen<'a> {
main: Vec<Instruction<'static>>,
block_buffers: Vec<ActiveBlock>,
completed_blocks: Vec<CompletedBlock>,
sizes: &'a SizeAlign,
addr_local: u32,
indices: &'a mut FunctionIndices,
}
struct ActiveBlock {
buffer: Vec<Instruction<'static>>,
iter_addr_local: Option<u32>,
}
struct CompletedBlock {
body: Vec<Instruction<'static>>,
iter_addr_local: Option<u32>,
}
impl<'a> WasmEncoderBindgen<'a> {
pub fn new(sizes: &'a SizeAlign, addr_local: u32, indices: &'a mut FunctionIndices) -> Self {
Self {
main: Vec::new(),
block_buffers: Vec::new(),
completed_blocks: Vec::new(),
sizes,
addr_local,
indices,
}
}
pub fn into_instructions(self) -> Vec<Instruction<'static>> {
assert!(
self.block_buffers.is_empty(),
"into_instructions called mid-block — push_block/finish_block unbalanced"
);
assert!(
self.completed_blocks.is_empty(),
"into_instructions called with unconsumed completed blocks \
(variant emit missing?)"
);
self.main
}
fn alloc_local(&mut self, ty: ValType) -> u32 {
self.indices.alloc_local(ty)
}
fn emit_one(&mut self, inst: Instruction<'static>) {
self.active_buf().push(inst);
}
fn active_buf(&mut self) -> &mut Vec<Instruction<'static>> {
match self.block_buffers.last_mut() {
Some(block) => &mut block.buffer,
None => &mut self.main,
}
}
fn current_addr_local(&self) -> u32 {
for block in self.block_buffers.iter().rev() {
if let Some(idx) = block.iter_addr_local {
return idx;
}
}
self.addr_local
}
fn emit_load(&mut self, offset: ArchitectureSize, load: LoadKind) {
let off = offset.size_wasm32() as u64;
let mem_arg = MemArg {
offset: off,
align: load.natural_align_log2(),
memory_index: 0,
};
let addr_local = self.current_addr_local();
self.emit_one(Instruction::LocalGet(addr_local));
self.emit_one(load.to_instruction(mem_arg));
}
fn emit_bitcast(&mut self, bc: &Bitcast) {
use Bitcast::*;
match bc {
None => {}
I32ToI64 => self.emit_one(Instruction::I64ExtendI32U),
I64ToI32 => self.emit_one(Instruction::I32WrapI64),
F32ToI32 => self.emit_one(Instruction::I32ReinterpretF32),
I32ToF32 => self.emit_one(Instruction::F32ReinterpretI32),
F64ToI64 => self.emit_one(Instruction::I64ReinterpretF64),
I64ToF64 => self.emit_one(Instruction::F64ReinterpretI64),
F32ToI64 => {
self.emit_one(Instruction::I32ReinterpretF32);
self.emit_one(Instruction::I64ExtendI32U);
}
I64ToF32 => {
self.emit_one(Instruction::I32WrapI64);
self.emit_one(Instruction::F32ReinterpretI32);
}
PToI32 | I32ToP | I32ToL | LToI32 | PToL | LToP => {}
I64ToP64 | P64ToI64 => {}
PToP64 | LToI64 => self.emit_one(Instruction::I64ExtendI32U),
P64ToP | I64ToL => self.emit_one(Instruction::I32WrapI64),
Sequence(pair) => {
let [a, b] = pair.as_ref();
self.emit_bitcast(a);
self.emit_bitcast(b);
}
}
}
fn start_block(&mut self) {
self.block_buffers.push(ActiveBlock {
buffer: Vec::new(),
iter_addr_local: None,
});
}
fn finish_block_body(&mut self) {
let active = self
.block_buffers
.pop()
.expect("finish_block without matching push_block");
self.completed_blocks.push(CompletedBlock {
body: active.buffer,
iter_addr_local: active.iter_addr_local,
});
}
fn emit_const_zero(&mut self, wt: WasmType) {
use WasmType::*;
let inst = match wt {
I32 | Pointer | Length => Instruction::I32Const(0),
I64 | PointerOrI64 => Instruction::I64Const(0),
F32 => Instruction::F32Const(0.0f32.into()),
F64 => Instruction::F64Const(0.0f64.into()),
};
self.emit_one(inst);
}
fn emit_variant_dispatch(
&mut self,
resolve: &Resolve,
variant_type: &Type,
arm_types: &[Option<Type>],
) {
let joined = flat_types(resolve, variant_type, None).expect(
"variant flat must fit in MAX_FLAT_PARAMS — larger variants are invalid per the \
canonical ABI spec",
);
assert!(
!joined.is_empty(),
"variant joined flat must include at least a discriminant"
);
let joined_payload: Vec<WasmType> = joined[1..].to_vec();
let disc_local = self.alloc_local(ValType::I32);
self.emit_one(Instruction::LocalSet(disc_local));
let payload_locals: Vec<u32> = joined_payload
.iter()
.map(|wt| self.alloc_local(wasm_type_to_val(*wt)))
.collect();
let n = arm_types.len();
let start = self.completed_blocks.len() - n;
let arm_blocks: Vec<CompletedBlock> = self.completed_blocks.drain(start..).collect();
let arm_flats: Vec<Vec<WasmType>> = arm_types
.iter()
.map(|opt_ty| match opt_ty {
None => Vec::new(),
Some(ty) => flat_types(resolve, ty, None).expect(
"arm flat must fit in MAX_FLAT_PARAMS — larger arms are invalid per the \
canonical ABI spec",
),
})
.collect();
self.emit_one(Instruction::Block(BlockType::Empty)); self.emit_one(Instruction::Block(BlockType::Empty)); for _ in 0..n {
self.emit_one(Instruction::Block(BlockType::Empty)); }
self.emit_one(Instruction::LocalGet(disc_local));
let table: Cow<'static, [u32]> = Cow::Owned((0..n as u32).collect());
self.emit_one(Instruction::BrTable(table, n as u32));
self.emit_one(Instruction::End);
for (i, arm) in arm_blocks.iter().enumerate() {
let arm_flat = &arm_flats[i];
for inst in &arm.body {
self.emit_one(inst.clone());
}
for j in (0..arm_flat.len()).rev() {
self.emit_bitcast(&cast(arm_flat[j], joined_payload[j]));
self.emit_one(Instruction::LocalSet(payload_locals[j]));
}
for j in arm_flat.len()..joined_payload.len() {
self.emit_const_zero(joined_payload[j]);
self.emit_one(Instruction::LocalSet(payload_locals[j]));
}
let depth = (n - i) as u32;
self.emit_one(Instruction::Br(depth));
self.emit_one(Instruction::End);
}
self.emit_one(Instruction::Unreachable);
self.emit_one(Instruction::End);
self.emit_one(Instruction::LocalGet(disc_local));
for idx in &payload_locals {
self.emit_one(Instruction::LocalGet(*idx));
}
}
}
fn wasm_type_to_val(wt: WasmType) -> ValType {
use WasmType::*;
match wt {
I32 | Pointer | Length => ValType::I32,
I64 | PointerOrI64 => ValType::I64,
F32 => ValType::F32,
F64 => ValType::F64,
}
}
#[derive(Clone, Copy)]
enum LoadKind {
I32Load,
I32Load8U,
I32Load8S,
I32Load16U,
I32Load16S,
I64Load,
F32Load,
F64Load,
}
impl LoadKind {
fn to_instruction(self, mem_arg: MemArg) -> Instruction<'static> {
match self {
LoadKind::I32Load => Instruction::I32Load(mem_arg),
LoadKind::I32Load8U => Instruction::I32Load8U(mem_arg),
LoadKind::I32Load8S => Instruction::I32Load8S(mem_arg),
LoadKind::I32Load16U => Instruction::I32Load16U(mem_arg),
LoadKind::I32Load16S => Instruction::I32Load16S(mem_arg),
LoadKind::I64Load => Instruction::I64Load(mem_arg),
LoadKind::F32Load => Instruction::F32Load(mem_arg),
LoadKind::F64Load => Instruction::F64Load(mem_arg),
}
}
fn natural_align_log2(self) -> u32 {
match self {
LoadKind::I32Load8U | LoadKind::I32Load8S => 0,
LoadKind::I32Load16U | LoadKind::I32Load16S => 1,
LoadKind::I32Load | LoadKind::F32Load => 2,
LoadKind::I64Load | LoadKind::F64Load => 3,
}
}
}
impl Bindgen for WasmEncoderBindgen<'_> {
type Operand = ();
fn emit(
&mut self,
_resolve: &Resolve,
inst: &AbiInst<'_>,
operands: &mut Vec<()>,
results: &mut Vec<()>,
) {
match inst {
AbiInst::I32Load { offset } => {
self.emit_load(*offset, LoadKind::I32Load);
produce_n(results, 1);
}
AbiInst::I32Load8U { offset } => {
self.emit_load(*offset, LoadKind::I32Load8U);
produce_n(results, 1);
}
AbiInst::I32Load8S { offset } => {
self.emit_load(*offset, LoadKind::I32Load8S);
produce_n(results, 1);
}
AbiInst::I32Load16U { offset } => {
self.emit_load(*offset, LoadKind::I32Load16U);
produce_n(results, 1);
}
AbiInst::I32Load16S { offset } => {
self.emit_load(*offset, LoadKind::I32Load16S);
produce_n(results, 1);
}
AbiInst::I64Load { offset } => {
self.emit_load(*offset, LoadKind::I64Load);
produce_n(results, 1);
}
AbiInst::F32Load { offset } => {
self.emit_load(*offset, LoadKind::F32Load);
produce_n(results, 1);
}
AbiInst::F64Load { offset } => {
self.emit_load(*offset, LoadKind::F64Load);
produce_n(results, 1);
}
AbiInst::PointerLoad { offset } => {
self.emit_load(*offset, LoadKind::I32Load);
produce_n(results, 1);
}
AbiInst::LengthLoad { offset } => {
self.emit_load(*offset, LoadKind::I32Load);
produce_n(results, 1);
}
AbiInst::BoolFromI32
| AbiInst::S8FromI32
| AbiInst::U8FromI32
| AbiInst::S16FromI32
| AbiInst::U16FromI32
| AbiInst::S32FromI32
| AbiInst::U32FromI32
| AbiInst::S64FromI64
| AbiInst::U64FromI64
| AbiInst::CharFromI32
| AbiInst::F32FromCoreF32
| AbiInst::F64FromCoreF64 => {
produce_n(results, 1);
}
AbiInst::Bitcasts { casts } => {
for bc in casts.iter() {
self.emit_bitcast(bc);
}
produce_n(results, operands.len());
}
AbiInst::RecordLift { .. }
| AbiInst::TupleLift { .. }
| AbiInst::HandleLift { .. }
| AbiInst::FutureLift { .. }
| AbiInst::StreamLift { .. }
| AbiInst::EnumLift { .. }
| AbiInst::FlagsLift { .. }
| AbiInst::ErrorContextLift
| AbiInst::FixedLengthListLift { .. }
| AbiInst::StringLift
| AbiInst::ListCanonLift { .. }
| AbiInst::ListLift { .. } => {
produce_n(results, 1);
}
AbiInst::VariantLift { variant, ty, .. } => {
let arms: Vec<Option<Type>> = variant.cases.iter().map(|c| c.ty).collect();
self.emit_variant_dispatch(_resolve, &Type::Id(*ty), &arms);
produce_n(results, 1);
}
AbiInst::OptionLift { payload, ty } => {
let arms = vec![None, Some(**payload)];
self.emit_variant_dispatch(_resolve, &Type::Id(*ty), &arms);
produce_n(results, 1);
}
AbiInst::ResultLift { result, ty } => {
let arms = vec![result.ok, result.err];
self.emit_variant_dispatch(_resolve, &Type::Id(*ty), &arms);
produce_n(results, 1);
}
AbiInst::IterBasePointer => {
let need_alloc = self
.block_buffers
.last()
.expect("IterBasePointer must fire inside a block")
.iter_addr_local
.is_none();
if need_alloc {
let idx = self.indices.alloc_local(ValType::I32);
self.block_buffers
.last_mut()
.expect("IterBasePointer must fire inside a block")
.iter_addr_local = Some(idx);
}
produce_n(results, 1);
}
AbiInst::FixedLengthListLiftFromMemory { element, size, .. } => {
let elem_size = self.sizes.size(element).size_wasm32() as u32;
let block = self
.completed_blocks
.pop()
.expect("FixedLengthListLiftFromMemory without a matching block");
let iter_addr = block.iter_addr_local.expect(
"fixed-size-list block must have allocated an iter_addr_local via \
IterBasePointer",
);
let parent_addr = self.current_addr_local();
self.emit_one(Instruction::LocalGet(parent_addr));
self.emit_one(Instruction::LocalSet(iter_addr));
for i in 0..*size {
if i > 0 {
self.emit_one(Instruction::LocalGet(iter_addr));
self.emit_one(Instruction::I32Const(elem_size as i32));
self.emit_one(Instruction::I32Add);
self.emit_one(Instruction::LocalSet(iter_addr));
}
for inst in &block.body {
self.emit_one(inst.clone());
}
}
produce_n(results, 1);
}
other => unimplemented!(
"WasmEncoderBindgen::emit hit unsupported instruction: {:?}. \
This path is only exercised by lift_from_memory; other entry \
points aren't supported.",
other
),
}
}
fn return_pointer(&mut self, _size: ArchitectureSize, _align: Alignment) {
unimplemented!(
"return_pointer is only called on lowering paths; \
lift_from_memory never invokes it"
);
}
fn push_block(&mut self) {
self.start_block();
}
fn finish_block(&mut self, operand: &mut Vec<()>) {
operand.clear();
self.finish_block_body();
}
fn sizes(&self) -> &SizeAlign {
self.sizes
}
fn is_list_canonical(&self, _resolve: &Resolve, _element: &Type) -> bool {
true
}
}
fn produce_n(results: &mut Vec<()>, n: usize) {
for _ in 0..n {
results.push(());
}
}
#[cfg(test)]
impl WasmEncoderBindgen<'_> {
pub(crate) fn instructions(&self) -> &[Instruction<'static>] {
&self.main
}
}
#[cfg(test)]
mod tests {
use super::*;
use wit_bindgen_core::abi::lift_from_memory;
use wit_parser::{Docs, Field, Record, Stability, TypeDef, TypeDefKind, TypeOwner};
fn new_sizes(resolve: &Resolve) -> SizeAlign {
let mut s = SizeAlign::default();
s.fill(resolve);
s
}
fn count<F: Fn(&Instruction<'static>) -> bool>(bg: &WasmEncoderBindgen<'_>, pred: F) -> usize {
bg.instructions().iter().filter(|i| pred(i)).count()
}
#[test]
fn lift_u32_emits_one_load() {
let resolve = Resolve::default();
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::U32);
assert_eq!(count(&bg, |i| matches!(i, Instruction::LocalGet(_))), 1);
assert_eq!(count(&bg, |i| matches!(i, Instruction::I32Load(_))), 1);
}
#[test]
fn lift_u64_emits_i64_load() {
let resolve = Resolve::default();
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(4);
let mut bg = WasmEncoderBindgen::new(&sizes, 3, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::U64);
assert_eq!(count(&bg, |i| matches!(i, Instruction::I64Load(_))), 1);
assert!(bg
.instructions()
.iter()
.any(|i| matches!(i, Instruction::LocalGet(3))));
}
#[test]
fn lift_record_emits_one_load_per_field() {
let mut resolve = Resolve::default();
let record_id = resolve.types.alloc(TypeDef {
name: Some("r".to_string()),
kind: TypeDefKind::Record(Record {
fields: vec![
Field {
name: "a".to_string(),
ty: Type::U32,
docs: Docs::default(),
},
Field {
name: "b".to_string(),
ty: Type::U64,
docs: Docs::default(),
},
Field {
name: "c".to_string(),
ty: Type::U8,
docs: Docs::default(),
},
],
}),
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
});
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::Id(record_id));
assert_eq!(count(&bg, |i| matches!(i, Instruction::LocalGet(_))), 3);
assert_eq!(count(&bg, |i| matches!(i, Instruction::I32Load(_))), 1);
assert_eq!(count(&bg, |i| matches!(i, Instruction::I64Load(_))), 1);
assert_eq!(count(&bg, |i| matches!(i, Instruction::I32Load8U(_))), 1);
}
#[test]
fn lift_string_emits_ptr_len_loads() {
let resolve = Resolve::default();
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::String);
assert_eq!(count(&bg, |i| matches!(i, Instruction::I32Load(_))), 2);
}
#[test]
fn lift_homogeneous_result_emits_dispatch_structure() {
let mut resolve = Resolve::default();
let result_id = resolve.types.alloc(TypeDef {
name: Some("r".to_string()),
kind: TypeDefKind::Result(wit_parser::Result_ {
ok: Some(Type::U32),
err: Some(Type::U32),
}),
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
});
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::Id(result_id));
assert_eq!(count(&bg, |i| matches!(i, Instruction::I32Load8U(_))), 1);
assert_eq!(count(&bg, |i| matches!(i, Instruction::Block(_))), 4);
assert_eq!(count(&bg, |i| matches!(i, Instruction::BrTable(_, _))), 1);
assert_eq!(count(&bg, |i| matches!(i, Instruction::Unreachable)), 1);
let _insts = bg.into_instructions();
assert_eq!(indices.into_locals(), vec![ValType::I32, ValType::I32]);
}
#[test]
fn lift_heterogeneous_result_emits_widening() {
let mut resolve = Resolve::default();
let result_id = resolve.types.alloc(TypeDef {
name: Some("r".to_string()),
kind: TypeDefKind::Result(wit_parser::Result_ {
ok: Some(Type::U8),
err: Some(Type::U64),
}),
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
});
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::Id(result_id));
assert_eq!(
count(&bg, |i| matches!(i, Instruction::I64ExtendI32U)),
1,
"ok (u8) arm should widen to i64 to match joined payload"
);
let _insts = bg.into_instructions();
assert_eq!(indices.into_locals(), vec![ValType::I32, ValType::I64]);
}
#[test]
fn lift_option_pads_none_arm_with_zero() {
let mut resolve = Resolve::default();
let opt_id = resolve.types.alloc(TypeDef {
name: Some("o".to_string()),
kind: TypeDefKind::Option(Type::U32),
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
});
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::Id(opt_id));
assert!(
bg.instructions()
.iter()
.any(|i| matches!(i, Instruction::I32Const(0))),
"option's None arm should emit i32.const 0 to pad joined payload"
);
}
#[test]
fn lift_result_string_u64_widens_pointer_to_pointer_or_i64() {
let mut resolve = Resolve::default();
let result_id = resolve.types.alloc(TypeDef {
name: Some("r".to_string()),
kind: TypeDefKind::Result(wit_parser::Result_ {
ok: Some(Type::String),
err: Some(Type::U64),
}),
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
});
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::Id(result_id));
assert_eq!(
count(&bg, |i| matches!(i, Instruction::I64ExtendI32U)),
1,
"ok (string) arm should widen Pointer to PointerOrI64"
);
let _insts = bg.into_instructions();
assert_eq!(
indices.into_locals(),
vec![ValType::I32, ValType::I64, ValType::I32]
);
}
#[test]
fn lift_fixed_size_list_unrolls_n_loads() {
let mut resolve = Resolve::default();
let list_id = resolve.types.alloc(TypeDef {
name: Some("l".to_string()),
kind: TypeDefKind::FixedSizeList(Type::U32, 4),
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
});
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::Id(list_id));
assert_eq!(count(&bg, |i| matches!(i, Instruction::I32Load(_))), 4);
assert_eq!(count(&bg, |i| matches!(i, Instruction::I32Add)), 3);
assert_eq!(count(&bg, |i| matches!(i, Instruction::I32Const(4))), 3);
let _insts = bg.into_instructions();
assert_eq!(indices.into_locals(), vec![ValType::I32]);
}
#[test]
fn lift_result_list_u64_widens_pointer_to_pointer_or_i64() {
let mut resolve = Resolve::default();
let list_id = resolve.types.alloc(TypeDef {
name: Some("l".to_string()),
kind: TypeDefKind::List(Type::U8),
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
});
let result_id = resolve.types.alloc(TypeDef {
name: Some("r".to_string()),
kind: TypeDefKind::Result(wit_parser::Result_ {
ok: Some(Type::Id(list_id)),
err: Some(Type::U64),
}),
owner: TypeOwner::None,
docs: Docs::default(),
stability: Stability::default(),
});
let sizes = new_sizes(&resolve);
let mut indices = FunctionIndices::new(1);
let mut bg = WasmEncoderBindgen::new(&sizes, 0, &mut indices);
lift_from_memory(&resolve, &mut bg, (), &Type::Id(result_id));
assert_eq!(
count(&bg, |i| matches!(i, Instruction::I64ExtendI32U)),
1,
"ok (list) arm should widen Pointer to PointerOrI64"
);
}
}