Skip to content

feat: support the tail call proposal #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Support for the custom memory page sizes proposal ([#22](https://github.com/explodingcamera/tinywasm/pull/22) by [@danielstuart14](https://github.com/danielstuart14))
- Support for the `tail_call` proposal

### Breaking Changes

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ TinyWasm passes all WebAssembly MVP tests from the [WebAssembly core testsuite](
| [**Reference Types**](https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md) | 🟢 | 0.7.0 |
| [**Multiple Memories**](https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md) | 🟢 | 0.8.0 |
| [**Custom Page Sizes**](https://github.com/WebAssembly/custom-page-sizes/blob/main/proposals/custom-page-sizes/Overview.md) | 🟢 | `next` |
| [**Tail Call**](https://github.com/WebAssembly/tail-call/blob/main/proposals/tail-call/Overview.md) | 🟢 | `next` |
| [**Memory64**](https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md) | 🚧 | N/A |
| [**Fixed-Width SIMD**](https://github.com/webassembly/simd) | 🌑 | N/A |
| [**Fixed-Width SIMD**](https://github.com/webassembly/simd) | 🚧 | N/A |

## Usage

Expand Down
6 changes: 5 additions & 1 deletion crates/parser/src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ macro_rules! impl_visit_operator {
(@@saturating_float_to_int $($rest:tt)* ) => {};
(@@bulk_memory $($rest:tt)* ) => {};
(@@simd $($rest:tt)* ) => {};
(@@tail_call $($rest:tt)* ) => {};

(@@$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*)) => {
#[cold]
Expand All @@ -181,7 +182,7 @@ impl<'a, R: WasmModuleResources> wasmparser::VisitOperator<'a> for FunctionBuild

define_operands! {
// basic instructions
visit_br(Br, u32), visit_br_if(BrIf, u32), visit_global_get(GlobalGet, u32), visit_i32_const(I32Const, i32), visit_i64_const(I64Const, i64), visit_call(Call, u32), visit_memory_size(MemorySize, u32), visit_memory_grow(MemoryGrow, u32), visit_unreachable(Unreachable), visit_nop(Nop), visit_return(Return), visit_i32_eqz(I32Eqz), visit_i32_eq(I32Eq), visit_i32_ne(I32Ne), visit_i32_lt_s(I32LtS), visit_i32_lt_u(I32LtU), visit_i32_gt_s(I32GtS), visit_i32_gt_u(I32GtU), visit_i32_le_s(I32LeS), visit_i32_le_u(I32LeU), visit_i32_ge_s(I32GeS), visit_i32_ge_u(I32GeU), visit_i64_eqz(I64Eqz), visit_i64_eq(I64Eq), visit_i64_ne(I64Ne), visit_i64_lt_s(I64LtS), visit_i64_lt_u(I64LtU), visit_i64_gt_s(I64GtS), visit_i64_gt_u(I64GtU), visit_i64_le_s(I64LeS), visit_i64_le_u(I64LeU), visit_i64_ge_s(I64GeS), visit_i64_ge_u(I64GeU), visit_f32_eq(F32Eq), visit_f32_ne(F32Ne), visit_f32_lt(F32Lt), visit_f32_gt(F32Gt), visit_f32_le(F32Le), visit_f32_ge(F32Ge), visit_f64_eq(F64Eq), visit_f64_ne(F64Ne), visit_f64_lt(F64Lt), visit_f64_gt(F64Gt), visit_f64_le(F64Le), visit_f64_ge(F64Ge), visit_i32_clz(I32Clz), visit_i32_ctz(I32Ctz), visit_i32_popcnt(I32Popcnt), visit_i32_add(I32Add), visit_i32_sub(I32Sub), visit_i32_mul(I32Mul), visit_i32_div_s(I32DivS), visit_i32_div_u(I32DivU), visit_i32_rem_s(I32RemS), visit_i32_rem_u(I32RemU), visit_i32_and(I32And), visit_i32_or(I32Or), visit_i32_xor(I32Xor), visit_i32_shl(I32Shl), visit_i32_shr_s(I32ShrS), visit_i32_shr_u(I32ShrU), visit_i32_rotl(I32Rotl), visit_i32_rotr(I32Rotr), visit_i64_clz(I64Clz), visit_i64_ctz(I64Ctz), visit_i64_popcnt(I64Popcnt), visit_i64_add(I64Add), visit_i64_sub(I64Sub), visit_i64_mul(I64Mul), visit_i64_div_s(I64DivS), visit_i64_div_u(I64DivU), visit_i64_rem_s(I64RemS), visit_i64_rem_u(I64RemU), visit_i64_and(I64And), visit_i64_or(I64Or), visit_i64_xor(I64Xor), visit_i64_shl(I64Shl), visit_i64_shr_s(I64ShrS), visit_i64_shr_u(I64ShrU), visit_i64_rotl(I64Rotl), visit_i64_rotr(I64Rotr), visit_f32_abs(F32Abs), visit_f32_neg(F32Neg), visit_f32_ceil(F32Ceil), visit_f32_floor(F32Floor), visit_f32_trunc(F32Trunc), visit_f32_nearest(F32Nearest), visit_f32_sqrt(F32Sqrt), visit_f32_add(F32Add), visit_f32_sub(F32Sub), visit_f32_mul(F32Mul), visit_f32_div(F32Div), visit_f32_min(F32Min), visit_f32_max(F32Max), visit_f32_copysign(F32Copysign), visit_f64_abs(F64Abs), visit_f64_neg(F64Neg), visit_f64_ceil(F64Ceil), visit_f64_floor(F64Floor), visit_f64_trunc(F64Trunc), visit_f64_nearest(F64Nearest), visit_f64_sqrt(F64Sqrt), visit_f64_add(F64Add), visit_f64_sub(F64Sub), visit_f64_mul(F64Mul), visit_f64_div(F64Div), visit_f64_min(F64Min), visit_f64_max(F64Max), visit_f64_copysign(F64Copysign), visit_i32_wrap_i64(I32WrapI64), visit_i32_trunc_f32_s(I32TruncF32S), visit_i32_trunc_f32_u(I32TruncF32U), visit_i32_trunc_f64_s(I32TruncF64S), visit_i32_trunc_f64_u(I32TruncF64U), visit_i64_extend_i32_s(I64ExtendI32S), visit_i64_extend_i32_u(I64ExtendI32U), visit_i64_trunc_f32_s(I64TruncF32S), visit_i64_trunc_f32_u(I64TruncF32U), visit_i64_trunc_f64_s(I64TruncF64S), visit_i64_trunc_f64_u(I64TruncF64U), visit_f32_convert_i32_s(F32ConvertI32S), visit_f32_convert_i32_u(F32ConvertI32U), visit_f32_convert_i64_s(F32ConvertI64S), visit_f32_convert_i64_u(F32ConvertI64U), visit_f32_demote_f64(F32DemoteF64), visit_f64_convert_i32_s(F64ConvertI32S), visit_f64_convert_i32_u(F64ConvertI32U), visit_f64_convert_i64_s(F64ConvertI64S), visit_f64_convert_i64_u(F64ConvertI64U), visit_f64_promote_f32(F64PromoteF32), visit_i32_reinterpret_f32(I32ReinterpretF32), visit_i64_reinterpret_f64(I64ReinterpretF64), visit_f32_reinterpret_i32(F32ReinterpretI32), visit_f64_reinterpret_i64(F64ReinterpretI64),
visit_br(Br, u32), visit_br_if(BrIf, u32), visit_global_get(GlobalGet, u32), visit_i32_const(I32Const, i32), visit_i64_const(I64Const, i64), visit_call(Call, u32), visit_return_call(ReturnCall, u32), visit_memory_size(MemorySize, u32), visit_memory_grow(MemoryGrow, u32), visit_unreachable(Unreachable), visit_nop(Nop), visit_return(Return), visit_i32_eqz(I32Eqz), visit_i32_eq(I32Eq), visit_i32_ne(I32Ne), visit_i32_lt_s(I32LtS), visit_i32_lt_u(I32LtU), visit_i32_gt_s(I32GtS), visit_i32_gt_u(I32GtU), visit_i32_le_s(I32LeS), visit_i32_le_u(I32LeU), visit_i32_ge_s(I32GeS), visit_i32_ge_u(I32GeU), visit_i64_eqz(I64Eqz), visit_i64_eq(I64Eq), visit_i64_ne(I64Ne), visit_i64_lt_s(I64LtS), visit_i64_lt_u(I64LtU), visit_i64_gt_s(I64GtS), visit_i64_gt_u(I64GtU), visit_i64_le_s(I64LeS), visit_i64_le_u(I64LeU), visit_i64_ge_s(I64GeS), visit_i64_ge_u(I64GeU), visit_f32_eq(F32Eq), visit_f32_ne(F32Ne), visit_f32_lt(F32Lt), visit_f32_gt(F32Gt), visit_f32_le(F32Le), visit_f32_ge(F32Ge), visit_f64_eq(F64Eq), visit_f64_ne(F64Ne), visit_f64_lt(F64Lt), visit_f64_gt(F64Gt), visit_f64_le(F64Le), visit_f64_ge(F64Ge), visit_i32_clz(I32Clz), visit_i32_ctz(I32Ctz), visit_i32_popcnt(I32Popcnt), visit_i32_add(I32Add), visit_i32_sub(I32Sub), visit_i32_mul(I32Mul), visit_i32_div_s(I32DivS), visit_i32_div_u(I32DivU), visit_i32_rem_s(I32RemS), visit_i32_rem_u(I32RemU), visit_i32_and(I32And), visit_i32_or(I32Or), visit_i32_xor(I32Xor), visit_i32_shl(I32Shl), visit_i32_shr_s(I32ShrS), visit_i32_shr_u(I32ShrU), visit_i32_rotl(I32Rotl), visit_i32_rotr(I32Rotr), visit_i64_clz(I64Clz), visit_i64_ctz(I64Ctz), visit_i64_popcnt(I64Popcnt), visit_i64_add(I64Add), visit_i64_sub(I64Sub), visit_i64_mul(I64Mul), visit_i64_div_s(I64DivS), visit_i64_div_u(I64DivU), visit_i64_rem_s(I64RemS), visit_i64_rem_u(I64RemU), visit_i64_and(I64And), visit_i64_or(I64Or), visit_i64_xor(I64Xor), visit_i64_shl(I64Shl), visit_i64_shr_s(I64ShrS), visit_i64_shr_u(I64ShrU), visit_i64_rotl(I64Rotl), visit_i64_rotr(I64Rotr), visit_f32_abs(F32Abs), visit_f32_neg(F32Neg), visit_f32_ceil(F32Ceil), visit_f32_floor(F32Floor), visit_f32_trunc(F32Trunc), visit_f32_nearest(F32Nearest), visit_f32_sqrt(F32Sqrt), visit_f32_add(F32Add), visit_f32_sub(F32Sub), visit_f32_mul(F32Mul), visit_f32_div(F32Div), visit_f32_min(F32Min), visit_f32_max(F32Max), visit_f32_copysign(F32Copysign), visit_f64_abs(F64Abs), visit_f64_neg(F64Neg), visit_f64_ceil(F64Ceil), visit_f64_floor(F64Floor), visit_f64_trunc(F64Trunc), visit_f64_nearest(F64Nearest), visit_f64_sqrt(F64Sqrt), visit_f64_add(F64Add), visit_f64_sub(F64Sub), visit_f64_mul(F64Mul), visit_f64_div(F64Div), visit_f64_min(F64Min), visit_f64_max(F64Max), visit_f64_copysign(F64Copysign), visit_i32_wrap_i64(I32WrapI64), visit_i32_trunc_f32_s(I32TruncF32S), visit_i32_trunc_f32_u(I32TruncF32U), visit_i32_trunc_f64_s(I32TruncF64S), visit_i32_trunc_f64_u(I32TruncF64U), visit_i64_extend_i32_s(I64ExtendI32S), visit_i64_extend_i32_u(I64ExtendI32U), visit_i64_trunc_f32_s(I64TruncF32S), visit_i64_trunc_f32_u(I64TruncF32U), visit_i64_trunc_f64_s(I64TruncF64S), visit_i64_trunc_f64_u(I64TruncF64U), visit_f32_convert_i32_s(F32ConvertI32S), visit_f32_convert_i32_u(F32ConvertI32U), visit_f32_convert_i64_s(F32ConvertI64S), visit_f32_convert_i64_u(F32ConvertI64U), visit_f32_demote_f64(F32DemoteF64), visit_f64_convert_i32_s(F64ConvertI32S), visit_f64_convert_i32_u(F64ConvertI32U), visit_f64_convert_i64_s(F64ConvertI64S), visit_f64_convert_i64_u(F64ConvertI64U), visit_f64_promote_f32(F64PromoteF32), visit_i32_reinterpret_f32(I32ReinterpretF32), visit_i64_reinterpret_f64(I64ReinterpretF64), visit_f32_reinterpret_i32(F32ReinterpretI32), visit_f64_reinterpret_i64(F64ReinterpretI64),

// sign_extension
visit_i32_extend8_s(I32Extend8S), visit_i32_extend16_s(I32Extend16S), visit_i64_extend8_s(I64Extend8S), visit_i64_extend16_s(I64Extend16S), visit_i64_extend32_s(I64Extend32S),
Expand Down Expand Up @@ -431,6 +432,9 @@ impl<'a, R: WasmModuleResources> wasmparser::VisitOperator<'a> for FunctionBuild
fn visit_call_indirect(&mut self, ty: u32, table: u32) -> Self::Output {
self.instructions.push(Instruction::CallIndirect(ty, table));
}
fn visit_return_call_indirect(&mut self, ty: u32, table: u32) -> Self::Output {
self.instructions.push(Instruction::ReturnCallIndirect(ty, table));
}

fn visit_f32_const(&mut self, val: wasmparser::Ieee32) -> Self::Output {
self.instructions.push(Instruction::F32Const(f32::from_bits(val.bits())));
Expand Down
4 changes: 4 additions & 0 deletions crates/tinywasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ harness=false
name="test-wasm-custom-page-sizes"
harness=false

[[test]]
name="test-wasm-tail-call"
harness=false

[[test]]
name="test-wasm-memory64"
harness=false
Expand Down
110 changes: 58 additions & 52 deletions crates/tinywasm/src/interpreter/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ impl<'store, 'stack> Executor<'store, 'stack> {
Select128 => self.stack.values.select::<Value128>(),
SelectRef => self.stack.values.select::<ValueRef>(),

Call(v) => return self.exec_call_direct(*v),
CallIndirect(ty, table) => return self.exec_call_indirect(*ty, *table),
Call(v) => return self.exec_call_direct::<false>(*v),
CallIndirect(ty, table) => return self.exec_call_indirect::<false>(*ty, *table),

ReturnCall(v) => return self.exec_call_direct::<true>(*v),
ReturnCallIndirect(ty, table) => return self.exec_call_indirect::<true>(*ty, *table),

If(end, el) => self.exec_if(*end, *el, (StackHeight::default(), StackHeight::default())),
IfWithType(ty, end, el) => self.exec_if(*end, *el, (StackHeight::default(), (*ty).into())),
Expand Down Expand Up @@ -314,50 +317,71 @@ impl<'store, 'stack> Executor<'store, 'stack> {
ControlFlow::Break(Some(Trap::Unreachable.into()))
}

fn exec_call(&mut self, wasm_func: Rc<WasmFunction>, owner: ModuleInstanceAddr) -> ControlFlow<Option<Error>> {
let locals = self.stack.values.pop_locals(wasm_func.params, wasm_func.locals);
let new_call_frame = CallFrame::new_raw(wasm_func, owner, locals, self.stack.blocks.len() as u32);
self.cf.incr_instr_ptr(); // skip the call instruction
self.stack.call_stack.push(core::mem::replace(&mut self.cf, new_call_frame))?;
self.module.swap_with(self.cf.module_addr(), self.store);
fn exec_call<const IS_RETURN_CALL: bool>(
&mut self,
wasm_func: Rc<WasmFunction>,
owner: ModuleInstanceAddr,
) -> ControlFlow<Option<Error>> {
if !IS_RETURN_CALL {
let locals = self.stack.values.pop_locals(wasm_func.params, wasm_func.locals);
let new_call_frame = CallFrame::new_raw(wasm_func, owner, locals, self.stack.blocks.len() as u32);
self.cf.incr_instr_ptr(); // skip the call instruction
self.stack.call_stack.push(core::mem::replace(&mut self.cf, new_call_frame))?;
self.module.swap_with(self.cf.module_addr(), self.store);
} else {
let locals = self.stack.values.pop_locals(wasm_func.params, wasm_func.locals);
self.cf.reuse_for(wasm_func, locals, self.stack.blocks.len() as u32, owner);
self.module.swap_with(self.cf.module_addr(), self.store);
}

ControlFlow::Continue(())
}
fn exec_call_direct(&mut self, v: u32) -> ControlFlow<Option<Error>> {
fn exec_call_host(&mut self, host_func: Rc<imports::HostFunction>) -> ControlFlow<Option<Error>> {
let params = self.stack.values.pop_params(&host_func.ty.params);
let res = host_func
.clone()
.call(FuncContext { store: self.store, module_addr: self.module.id() }, &params)
.to_cf()?;
self.stack.values.extend_from_wasmvalues(&res);
self.cf.incr_instr_ptr();
ControlFlow::Continue(())
}
fn exec_call_direct<const IS_RETURN_CALL: bool>(&mut self, v: u32) -> ControlFlow<Option<Error>> {
let func_inst = self.store.get_func(self.module.resolve_func_addr(v));
let wasm_func = match &func_inst.func {
crate::Function::Wasm(wasm_func) => wasm_func,
crate::Function::Host(host_func) => {
let func = &host_func.clone();
let params = self.stack.values.pop_params(&host_func.ty.params);
let res =
func.call(FuncContext { store: self.store, module_addr: self.module.id() }, &params).to_cf()?;
self.stack.values.extend_from_wasmvalues(&res);
self.cf.incr_instr_ptr();
return ControlFlow::Continue(());
}
};

self.exec_call(wasm_func.clone(), func_inst.owner)
match func_inst.func.clone() {
crate::Function::Wasm(wasm_func) => self.exec_call::<IS_RETURN_CALL>(wasm_func, func_inst.owner),
crate::Function::Host(host_func) => self.exec_call_host(host_func),
}
}
fn exec_call_indirect(&mut self, type_addr: u32, table_addr: u32) -> ControlFlow<Option<Error>> {
fn exec_call_indirect<const IS_RETURN_CALL: bool>(
&mut self,
type_addr: u32,
table_addr: u32,
) -> ControlFlow<Option<Error>> {
// verify that the table is of the right type, this should be validated by the parser already
let func_ref = {
let table = self.store.get_table(self.module.resolve_table_addr(table_addr));
let table_idx: u32 = self.stack.values.pop::<i32>() as u32;
assert!(table.kind.element_type == ValType::RefFunc, "table is not of type funcref");
table
.get(table_idx)
.map_err(|_| Error::Trap(Trap::UndefinedElement { index: table_idx as usize }))
.to_cf()?
.addr()
.ok_or(Error::Trap(Trap::UninitializedElement { index: table_idx as usize }))
.to_cf()?
let table = table.get(table_idx).map_err(|_| Trap::UndefinedElement { index: table_idx as usize }.into());
let table = table.to_cf()?;
table.addr().ok_or(Trap::UninitializedElement { index: table_idx as usize }.into()).to_cf()?
};

let func_inst = self.store.get_func(func_ref);
let call_ty = self.module.func_ty(type_addr);
let wasm_func = match &func_inst.func {
crate::Function::Wasm(f) => f,

match func_inst.func.clone() {
crate::Function::Wasm(wasm_func) => {
if unlikely(wasm_func.ty != *call_ty) {
return ControlFlow::Break(Some(
Trap::IndirectCallTypeMismatch { actual: wasm_func.ty.clone(), expected: call_ty.clone() }
.into(),
));
}

self.exec_call::<IS_RETURN_CALL>(wasm_func, func_inst.owner)
}
crate::Function::Host(host_func) => {
if unlikely(host_func.ty != *call_ty) {
return ControlFlow::Break(Some(
Expand All @@ -366,27 +390,9 @@ impl<'store, 'stack> Executor<'store, 'stack> {
));
}

let host_func = host_func.clone();
let params = self.stack.values.pop_params(&host_func.ty.params);
let res =
match host_func.call(FuncContext { store: self.store, module_addr: self.module.id() }, &params) {
Ok(res) => res,
Err(e) => return ControlFlow::Break(Some(e)),
};

self.stack.values.extend_from_wasmvalues(&res);
self.cf.incr_instr_ptr();
return ControlFlow::Continue(());
self.exec_call_host(host_func)
}
};

if unlikely(wasm_func.ty != *call_ty) {
return ControlFlow::Break(Some(
Trap::IndirectCallTypeMismatch { actual: wasm_func.ty.clone(), expected: call_ty.clone() }.into(),
));
}

self.exec_call(wasm_func.clone(), func_inst.owner)
}

fn exec_if(&mut self, else_offset: u32, end_offset: u32, (params, results): (StackHeight, StackHeight)) {
Expand Down
14 changes: 14 additions & 0 deletions crates/tinywasm/src/interpreter/stack/call_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ impl CallFrame {
}
}

pub(crate) fn reuse_for(
&mut self,
func: Rc<WasmFunction>,
locals: Locals,
block_depth: u32,
module_addr: ModuleInstanceAddr,
) {
self.func_instance = func;
self.module_addr = module_addr;
self.locals = locals;
self.block_ptr = block_depth;
self.instr_ptr = 0; // Reset to function entry
}

/// Break to a block at the given index (relative to the current frame)
/// Returns `None` if there is no block at the given index (e.g. if we need to return, this is handled by the caller)
#[inline]
Expand Down
Loading
Loading