Skip to content
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

Implement Wasm tail-call proposal #683

Merged
merged 30 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
29bb7ff
add tail call support to Config
Robbepop Feb 16, 2023
0ca3b81
implement return_call[_indirect] for func translator
Robbepop Feb 16, 2023
759c01d
add tail-call tests from Wasm spec testsuite
Robbepop Feb 16, 2023
084780d
expect failure for tail call tests since not yet implemented
Robbepop Feb 16, 2023
c87b105
generalize CallOutcome to later support tail calls
Robbepop Feb 16, 2023
97ea69f
make StoreIdx base on NonZeroU32
Robbepop Feb 16, 2023
afc4ef4
apply rustfmt
Robbepop Feb 16, 2023
faf0611
partially implement return_call[_indirect]
Robbepop Feb 16, 2023
5802a8b
revert
Robbepop Feb 16, 2023
616ed6c
add DropKeep to return call bytecodes
Robbepop Feb 16, 2023
4ab5c24
create unique drop_keep for return calls
Robbepop Feb 16, 2023
b756bcb
fix bug in drop_keep_return_call method
Robbepop Feb 16, 2023
a20f016
wasm_return_call works now
Robbepop Feb 16, 2023
1cd11a8
apply clippy suggestion
Robbepop Feb 16, 2023
8cc5bbd
properly charge for drop_keep in return_call_indirect
Robbepop Feb 16, 2023
7fa9148
fix return_call_indirect
Robbepop Feb 16, 2023
48f7324
enable return_call_indirect Wasm spec test case
Robbepop Feb 16, 2023
ffd7248
fix performance regressions
Robbepop Feb 16, 2023
849edef
add fib_tail_recursive to benchmarks
Robbepop Feb 16, 2023
6041845
add TODO comment for resumable calls
Robbepop Feb 16, 2023
5505735
add more TODO comments for resumable calls
Robbepop Feb 16, 2023
b55bda2
comment out fib_tail_recursive
Robbepop Feb 16, 2023
b1314e0
Merge branch 'master' into rf-wasm-tail-calls
Robbepop Feb 28, 2023
51b221f
make use of return_call in benchmark .wat
Robbepop Feb 28, 2023
83c0725
refactor engine executor calls
Robbepop Feb 28, 2023
d50ac60
apply rustfmt
Robbepop Feb 28, 2023
ec3f2c2
add doc comments
Robbepop Feb 28, 2023
a93949e
revert changes as it slowed down Wasm targets
Robbepop Feb 28, 2023
12efc9b
add tests for resumable calls + tail calls
Robbepop Mar 1, 2023
f4b4612
fix resumable tail calls edge case
Robbepop Mar 1, 2023
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 crates/wasmi/benches/bench/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub fn load_wasm_from_file(file_name: &str) -> Vec<u8> {
/// Returns a [`Config`] useful for benchmarking.
fn bench_config() -> Config {
let mut config = Config::default();
config.wasm_tail_call(true);
config.set_stack_limits(StackLimits::new(1024, 1024 * 1024, 64 * 1024).unwrap());
config
}
Expand Down
4 changes: 2 additions & 2 deletions crates/wasmi/benches/wat/fibonacci.wat
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
(return (local.get $b))
)
)
(call $fib_tail_recursive
(return_call $fib_tail_recursive
(i64.sub (local.get $N) (i64.const 1))
(local.get $b)
(i64.add (local.get $a) (local.get $b))
)
)

(func (export "fibonacci_tail") (param $N i64) (result i64)
(call $fib_tail_recursive (local.get $N) (i64.const 0) (i64.const 1))
(return_call $fib_tail_recursive (local.get $N) (i64.const 0) (i64.const 1))
)

(func $fib_iterative (export "fibonacci_iter") (param $N i64) (result i64)
Expand Down
9 changes: 9 additions & 0 deletions crates/wasmi/src/engine/bytecode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ pub enum Instruction {
},
Return(DropKeep),
ReturnIfNez(DropKeep),
ReturnCall {
drop_keep: DropKeep,
func: FuncIdx,
},
ReturnCallIndirect {
drop_keep: DropKeep,
table: TableIdx,
func_type: SignatureIdx,
},
Call(FuncIdx),
CallIndirect {
table: TableIdx,
Expand Down
17 changes: 16 additions & 1 deletion crates/wasmi/src/engine/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub struct Config {
bulk_memory: bool,
/// Is `true` if the [`reference-types`] Wasm proposal is enabled.
reference_types: bool,
/// Is `true` if the [`tail-call`] Wasm proposal is enabled.
tail_call: bool,
/// Is `true` if Wasm instructions on `f32` and `f64` types are allowed.
floats: bool,
/// Is `true` if `wasmi` executions shall consume fuel.
Expand Down Expand Up @@ -94,6 +96,7 @@ impl Default for Config {
multi_value: true,
bulk_memory: true,
reference_types: true,
tail_call: false,
floats: true,
consume_fuel: false,
fuel_costs: FuelCosts::default(),
Expand Down Expand Up @@ -201,6 +204,18 @@ impl Config {
self
}

/// Enable or disable the [`tail-call`] Wasm proposal for the [`Config`].
///
/// # Note
///
/// Disabled by default.
///
/// [`tail-call`]: https://github.com/WebAssembly/tail-calls
pub fn wasm_tail_call(&mut self, enable: bool) -> &mut Self {
self.tail_call = enable;
self
}

/// Enable or disable Wasm floating point (`f32` and `f64`) instructions and types.
///
/// Enabled by default.
Expand Down Expand Up @@ -252,12 +267,12 @@ impl Config {
sign_extension: self.sign_extension,
bulk_memory: self.bulk_memory,
reference_types: self.reference_types,
tail_call: self.tail_call,
floats: self.floats,
component_model: false,
simd: false,
relaxed_simd: false,
threads: false,
tail_call: false,
multi_memory: false,
exceptions: false,
memory64: false,
Expand Down
183 changes: 142 additions & 41 deletions crates/wasmi/src/engine/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{
table::TableEntity,
Func,
FuncRef,
Instance,
StoreInner,
Table,
};
Expand All @@ -42,7 +43,7 @@ pub enum WasmOutcome {
/// The Wasm execution has ended and returns to the host side.
Return,
/// The Wasm execution calls a host function.
Call(Func),
Call { host_func: Func, instance: Instance },
}

/// The outcome of a Wasm execution.
Expand All @@ -56,7 +57,16 @@ pub enum CallOutcome {
/// The Wasm execution continues in Wasm.
Continue,
/// The Wasm execution calls a host function.
Call(Func),
Call { host_func: Func, instance: Instance },
}

/// The kind of a function call.
#[derive(Debug, Copy, Clone)]
pub enum CallKind {
/// A nested function call.
Nested,
/// A tailing function call.
Tail,
}

/// The outcome of a Wasm return statement.
Expand Down Expand Up @@ -203,16 +213,56 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
return Ok(WasmOutcome::Return);
}
}
Instr::ReturnCall { drop_keep, func } => {
if let CallOutcome::Call {
host_func,
instance,
} = self.visit_return_call(drop_keep, func)?
{
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
}
Instr::ReturnCallIndirect {
drop_keep,
table,
func_type,
} => {
if let CallOutcome::Call {
host_func,
instance,
} = self.visit_return_call_indirect(drop_keep, table, func_type)?
{
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
}
Instr::Call(func) => {
if let CallOutcome::Call(host_func) = self.visit_call(func)? {
return Ok(WasmOutcome::Call(host_func));
if let CallOutcome::Call {
host_func,
instance,
} = self.visit_call(func)?
{
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
}
Instr::CallIndirect { table, func_type } => {
if let CallOutcome::Call(host_func) =
self.visit_call_indirect(table, func_type)?
if let CallOutcome::Call {
host_func,
instance,
} = self.visit_call_indirect(table, func_type)?
{
return Ok(WasmOutcome::Call(host_func));
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
}
Instr::Drop => self.visit_drop(),
Expand Down Expand Up @@ -519,24 +569,30 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
/// the function call so that the stack and execution state is synchronized
/// with the outer structures.
#[inline(always)]
fn call_func(&mut self, func: &Func) -> Result<CallOutcome, TrapCode> {
fn call_func(&mut self, func: &Func, kind: CallKind) -> Result<CallOutcome, TrapCode> {
self.next_instr();
self.sync_stack_ptr();
self.call_stack
.push(FuncFrame::new(self.ip, self.cache.instance()))?;
let wasm_func = match self.ctx.resolve_func(func) {
FuncEntity::Wasm(wasm_func) => wasm_func,
if matches!(kind, CallKind::Nested) {
self.call_stack
.push(FuncFrame::new(self.ip, self.cache.instance()))?;
}
match self.ctx.resolve_func(func) {
FuncEntity::Wasm(wasm_func) => {
let header = self.code_map.header(wasm_func.func_body());
self.value_stack.prepare_wasm_call(header)?;
self.sp = self.value_stack.stack_ptr();
self.cache.update_instance(wasm_func.instance());
self.ip = self.code_map.instr_ptr(header.iref());
Ok(CallOutcome::Continue)
}
FuncEntity::Host(_host_func) => {
self.cache.reset();
return Ok(CallOutcome::Call(*func));
Ok(CallOutcome::Call {
host_func: *func,
instance: *self.cache.instance(),
})
}
};
let header = self.code_map.header(wasm_func.func_body());
self.value_stack.prepare_wasm_call(header)?;
self.sp = self.value_stack.stack_ptr();
self.cache.update_instance(wasm_func.instance());
self.ip = self.code_map.instr_ptr(header.iref());
Ok(CallOutcome::Continue)
}
}

/// Returns to the caller.
Expand Down Expand Up @@ -608,6 +664,48 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
fn fuel_costs(&self) -> &FuelCosts {
self.ctx.engine().config().fuel_costs()
}

/// Executes a `call` or `return_call` instruction.
#[inline(always)]
fn execute_call(
&mut self,
func_index: FuncIdx,
kind: CallKind,
) -> Result<CallOutcome, TrapCode> {
let callee = self.cache.get_func(self.ctx, func_index);
self.call_func(&callee, kind)
}

/// Executes a `call_indirect` or `return_call_indirect` instruction.
#[inline(always)]
fn execute_call_indirect(
&mut self,
table: TableIdx,
func_index: u32,
func_type: SignatureIdx,
kind: CallKind,
) -> Result<CallOutcome, TrapCode> {
let table = self.cache.get_table(self.ctx, table);
let funcref = self
.ctx
.resolve_table(&table)
.get_untyped(func_index)
.map(FuncRef::from)
.ok_or(TrapCode::TableOutOfBounds)?;
let func = funcref.func().ok_or(TrapCode::IndirectCallToNull)?;
let actual_signature = self.ctx.resolve_func(func).ty_dedup();
let expected_signature = self
.ctx
.resolve_instance(self.cache.instance())
.get_signature(func_type.into_inner())
.unwrap_or_else(|| {
panic!("missing signature for call_indirect at index: {func_type:?}")
});
if actual_signature != expected_signature {
return Err(TrapCode::BadSignature).map_err(Into::into);
}
self.call_func(func, kind)
}
}

impl<'ctx, 'engine> Executor<'ctx, 'engine> {
Expand Down Expand Up @@ -712,10 +810,32 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
self.next_instr()
}

#[inline(always)]
fn visit_return_call(
&mut self,
drop_keep: DropKeep,
func_index: FuncIdx,
) -> Result<CallOutcome, TrapCode> {
self.sp.drop_keep(drop_keep);
self.execute_call(func_index, CallKind::Tail)
}

#[inline(always)]
fn visit_return_call_indirect(
&mut self,
drop_keep: DropKeep,
table: TableIdx,
func_type: SignatureIdx,
) -> Result<CallOutcome, TrapCode> {
let func_index: u32 = self.sp.pop_as();
self.sp.drop_keep(drop_keep);
self.execute_call_indirect(table, func_index, func_type, CallKind::Tail)
}

#[inline(always)]
fn visit_call(&mut self, func_index: FuncIdx) -> Result<CallOutcome, TrapCode> {
let callee = self.cache.get_func(self.ctx, func_index);
self.call_func(&callee)
self.call_func(&callee, CallKind::Nested)
}

#[inline(always)]
Expand All @@ -725,26 +845,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
func_type: SignatureIdx,
) -> Result<CallOutcome, TrapCode> {
let func_index: u32 = self.sp.pop_as();
let table = self.cache.get_table(self.ctx, table);
let funcref = self
.ctx
.resolve_table(&table)
.get_untyped(func_index)
.map(FuncRef::from)
.ok_or(TrapCode::TableOutOfBounds)?;
let func = funcref.func().ok_or(TrapCode::IndirectCallToNull)?;
let actual_signature = self.ctx.resolve_func(func).ty_dedup();
let expected_signature = self
.ctx
.resolve_instance(self.cache.instance())
.get_signature(func_type.into_inner())
.unwrap_or_else(|| {
panic!("missing signature for call_indirect at index: {func_type:?}")
});
if actual_signature != expected_signature {
return Err(TrapCode::BadSignature).map_err(Into::into);
}
self.call_func(func)
self.execute_call_indirect(table, func_index, func_type, CallKind::Nested)
}

#[inline(always)]
Expand Down
3 changes: 3 additions & 0 deletions crates/wasmi/src/engine/func_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ macro_rules! impl_visit_operator {
( @reference_types $($rest:tt)* ) => {
impl_visit_operator!(@@supported $($rest)*);
};
( @tail_call $($rest:tt)* ) => {
impl_visit_operator!(@@supported $($rest)*);
};
( @@supported $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident $($rest:tt)* ) => {
fn $visit(&mut self $($(,$arg: $argty)*)?) -> Self::Output {
let offset = self.current_pos();
Expand Down
Loading