diff --git a/crates/continuations/src/lib.rs b/crates/continuations/src/lib.rs index d63ec47b4b89..b92c0f17ef98 100644 --- a/crates/continuations/src/lib.rs +++ b/crates/continuations/src/lib.rs @@ -77,13 +77,30 @@ pub struct CommonStackInformation { /// Otherwise, the list may remain allocated, but its `length` must be 0. /// /// Represents the handlers that this stack installed when resume-ing a - /// continuation with - /// (resume $ct handler_clause_1 ... handler_clause_n) - /// The actual entries are tag identifiers (i.e., *mut VMTagDefinition). The - /// order of entries is relevant: The tag in the i-th entry in the list is - /// handled by the block mentioned in the i-th handler clause in the resume - /// instruction above. + /// continuation. + /// + /// Note that for any resume instruction, we can re-order the handler + /// clauses without changing behavior such that all the suspend handlers + /// come first, followed by all the switch handler (while maintaining the + /// original ordering within the two groups). + /// Thus, we assume that the given resume instruction has the following + /// shape: + /// + /// (resume $ct + /// (on $tag_0 $block_0) ... (on $tag_{n-1} $block_{n-1}) + /// (on $tag_n switch) ... (on $tag_m switch) + /// ) + /// + /// On resume, the handler list is then filled with m + 1 (i.e., one per + /// handler clause) entries such that the i-th entry, using 0-based + /// indexing, is the identifier of $tag_i (represented as *mut + /// VMTagDefinition). + /// Further, `first_switch_handler_index` (see below) is set to n (i.e., the + /// 0-based index of the first switch handler). pub handlers: HandlerList, + + /// Only used when state is `Parent`. See documentation of `handlers` above. + pub first_switch_handler_index: u32, } impl CommonStackInformation { @@ -92,6 +109,7 @@ impl CommonStackInformation { limits: StackLimits::default(), state: State::Running, handlers: HandlerList::new(INITIAL_HANDLER_LIST_CAPACITY as u32), + first_switch_handler_index: 0, } } } @@ -208,7 +226,7 @@ pub const STACK_CHAIN_CONTINUATION_DISCRIMINANT: usize = 2; #[derive(Debug, Clone, Copy, PartialEq)] #[repr(i32)] pub enum State { - /// The `VMContRef` has been created, but `resume` has never been + /// The `VMContRef` has been created, but neither `resume` or `switch` has ever been /// called on it. During this stage, we may add arguments using `cont.bind`. Fresh, /// The continuation is running, meaning that it is the one currently @@ -219,7 +237,7 @@ pub enum State { /// another continuation (which may itself be `Running`, a `Parent`, or /// `Suspended`). Parent, - /// The continuation was suspended by a `suspend` instruction. + /// The continuation was suspended by a `suspend` or `switch` instruction. Suspended, /// The function originally passed to `cont.new` has returned normally. /// Note that there is no guarantee that a VMContRef will ever @@ -295,6 +313,8 @@ pub mod offsets { pub const LIMITS: usize = offset_of!(CommonStackInformation, limits); pub const STATE: usize = offset_of!(CommonStackInformation, state); pub const HANDLERS: usize = offset_of!(CommonStackInformation, handlers); + pub const FIRST_SWITCH_HANDLER_INDEX: usize = + offset_of!(CommonStackInformation, first_switch_handler_index); } /// Size of wasmtime_runtime::continuation::FiberStack. @@ -354,6 +374,9 @@ pub const CONTROL_EFFECT_RESUME_DISCRIMINANT: u32 = 1; /// Discriminant of variant `Suspend` in /// `ControlEffect`. pub const CONTROL_EFFECT_SUSPEND_DISCRIMINANT: u32 = 2; +/// Discriminant of variant `Switch` in +/// `ControlEffect`. +pub const CONTROL_EFFECT_SWITCH_DISCRIMINANT: u32 = 3; /// Universal control effect. This structure encodes return signal, /// resume signal, suspension signal, and the handler to suspend to in a single variant type. @@ -365,6 +388,7 @@ pub enum ControlEffect { Return = CONTROL_EFFECT_RETURN_DISCRIMINANT, Resume = CONTROL_EFFECT_RESUME_DISCRIMINANT, Suspend { handler_index: u32 } = CONTROL_EFFECT_SUSPEND_DISCRIMINANT, + Switch = CONTROL_EFFECT_SWITCH_DISCRIMINANT, } // TODO(frank-emrich) This conversion assumes little-endian data layout. diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 2cbcd46247d2..d2e7ade3a717 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -3261,8 +3261,8 @@ impl<'module_environment> crate::translate::FuncEnvironment type_index: u32, contobj: ir::Value, resume_args: &[ir::Value], - resumetable: &[(u32, ir::Block)], - ) -> Vec { + resumetable: &[(u32, Option)], + ) -> WasmResult> { wasmfx_impl::translate_resume(self, builder, type_index, contobj, resume_args, resumetable) } @@ -3286,6 +3286,18 @@ impl<'module_environment> crate::translate::FuncEnvironment wasmfx_impl::translate_suspend(self, builder, tag_index, suspend_args, tag_return_types) } + /// Translates switch instructions. + fn translate_switch( + &mut self, + builder: &mut FunctionBuilder, + tag_index: u32, + contobj: ir::Value, + switch_args: &[ir::Value], + return_types: &[WasmValType], + ) -> WasmResult> { + wasmfx_impl::translate_switch(self, builder, tag_index, contobj, switch_args, return_types) + } + fn continuation_arguments(&self, index: u32) -> &[WasmValType] { let idx = self.module.types[TypeIndex::from_u32(index)]; self.types[self.types[idx].unwrap_cont().clone().interned_type_index()] diff --git a/crates/cranelift/src/translate/code_translator.rs b/crates/cranelift/src/translate/code_translator.rs index 2dfd6874084f..d65bab2a3311 100644 --- a/crates/cranelift/src/translate/code_translator.rs +++ b/crates/cranelift/src/translate/code_translator.rs @@ -93,7 +93,7 @@ use std::vec::Vec; use wasmparser::{FuncValidator, MemArg, Operator, WasmModuleResources}; use wasmtime_environ::{ wasm_unsupported, DataIndex, ElemIndex, FuncIndex, GlobalIndex, MemoryIndex, Signed, - TableIndex, TypeIndex, Unsigned, WasmRefType, WasmResult, + TableIndex, TypeIndex, Unsigned, WasmHeapType, WasmRefType, WasmResult, }; /// Given a `Reachability`, unwrap the inner `T` or, when unreachable, set @@ -2905,9 +2905,11 @@ pub fn translate_operator( let frame = &mut state.control_stack[i]; // This is side-effecting! frame.set_branched_to_exit(); - resumetable.push((*tag, frame.br_destination())); + resumetable.push((*tag, Some(frame.br_destination()))); + } + wasmparser::Handle::OnSwitch { tag } => { + resumetable.push((*tag, None)); } - wasmparser::Handle::OnSwitch { tag: _ } => unimplemented!(), } } @@ -2920,7 +2922,7 @@ pub fn translate_operator( *contobj, call_args, resumetable.as_slice(), - ); + )?; state.popn(arity + 1); // arguments + continuation state.pushn(&cont_return_vals); @@ -2929,11 +2931,48 @@ pub fn translate_operator( cont_type_index: _, tag_index: _, resume_table: _, + } => todo!("unimplemented stack switching instruction"), + Operator::Switch { + cont_type_index, + tag_index, + } => { + // Arguments of the continuation we are going to switch to + let continuation_argument_types = + environ.continuation_arguments(*cont_type_index).to_vec(); + // Arity includes the continuation argument + let arity = continuation_argument_types.len(); + let (contobj, switch_args) = state.peekn(arity).split_last().unwrap(); + + // Type of the continuation we are going to create by suspending the + // currently running stack + let current_continuation_type = continuation_argument_types.last().unwrap(); + let current_continuation_type = current_continuation_type.unwrap_ref_type(); + + // Argument types of current_continuation_type. These will in turn + // be the types of the arguments we receive when someone switches + // back to this switch instruction + let current_continuation_arg_types = match current_continuation_type.heap_type { + WasmHeapType::ConcreteCont(index) => { + let mti = index + .as_module_type_index() + .expect("Only supporting module type indices on switch for now"); + + environ.continuation_arguments(mti.as_u32()).to_vec() + } + _ => panic!("Invalid type on switch"), + }; + + let switch_return_values = environ.translate_switch( + builder, + *tag_index, + *contobj, + switch_args, + ¤t_continuation_arg_types, + )?; + + state.popn(arity); + state.pushn(&switch_return_values) } - | Operator::Switch { - cont_type_index: _, - tag_index: _, - } => todo!("unimplemented stack switching instructions"), Operator::GlobalAtomicGet { .. } | Operator::GlobalAtomicSet { .. } diff --git a/crates/cranelift/src/translate/environ/spec.rs b/crates/cranelift/src/translate/environ/spec.rs index 5961d3707e4d..590be6cc150c 100644 --- a/crates/cranelift/src/translate/environ/spec.rs +++ b/crates/cranelift/src/translate/environ/spec.rs @@ -845,8 +845,18 @@ pub trait FuncEnvironment: TargetEnvironment { type_index: u32, contobj: ir::Value, resume_args: &[ir::Value], - resumetable: &[(u32, ir::Block)], - ) -> Vec; + resumetable: &[(u32, Option)], + ) -> WasmResult>; + + /// Translates switch instructions. + fn translate_switch( + &mut self, + builder: &mut FunctionBuilder, + tag_index: u32, + contobj: ir::Value, + switch_args: &[ir::Value], + return_types: &[WasmValType], + ) -> WasmResult>; /// TODO(dhil): write documentation. #[allow(unused, reason = "TODO")] diff --git a/crates/cranelift/src/wasmfx/baseline.rs b/crates/cranelift/src/wasmfx/baseline.rs index e07fe3641538..423d2331517c 100644 --- a/crates/cranelift/src/wasmfx/baseline.rs +++ b/crates/cranelift/src/wasmfx/baseline.rs @@ -8,7 +8,7 @@ use cranelift_codegen::ir::types::*; use cranelift_codegen::ir::InstBuilder; use cranelift_frontend::{FunctionBuilder, Switch}; use wasmtime_environ::PtrSize; -use wasmtime_environ::{WasmResult, WasmValType}; +use wasmtime_environ::{WasmError, WasmResult, WasmValType}; #[cfg_attr( not(feature = "wasmfx_baseline"), @@ -182,8 +182,8 @@ pub(crate) fn translate_resume<'a>( type_index: u32, resumee_obj: ir::Value, resume_args: &[ir::Value], - resumetable: &[(u32, ir::Block)], -) -> Vec { + resumetable: &[(u32, Option)], +) -> WasmResult> { // The resume instruction is by far the most involved // instruction to compile as it is responsible for both // continuation application and effect dispatch. @@ -311,11 +311,16 @@ pub(crate) fn translate_resume<'a>( // Second, we consume the resume table entry-wise. let mut case_blocks = vec![]; let mut tag_seen = std::collections::HashSet::new(); // Used to keep track of tags - for &(tag, label) in resumetable { + for &(tag, label_opt) in resumetable { // Skip if this `tag` has been seen previously. if !tag_seen.insert(tag) { continue; } + let label = label_opt.ok_or_else(|| { + WasmError::Unsupported(String::from( + "switch handlers not supported in the baseline implementation", + )) + })?; let case = builder.create_block(); switch.set_entry(tag as u128, case); builder.switch_to_block(case); @@ -401,7 +406,7 @@ pub(crate) fn translate_resume<'a>( tc_baseline_drop_continuation_reference(resumee_fiber) ); - return values; + Ok(values) } } @@ -453,3 +458,16 @@ pub(crate) fn translate_suspend<'a>( return_values } + +pub(crate) fn translate_switch<'a>( + _env: &mut crate::func_environ::FuncEnvironment<'a>, + _builder: &mut FunctionBuilder, + _tag_index: u32, + _switchee_contobj: ir::Value, + _switch_args: &[ir::Value], + _return_types: &[WasmValType], +) -> WasmResult> { + WasmResult::Err(WasmError::Unsupported(String::from( + "switch instructions are unsuported", + ))) +} diff --git a/crates/cranelift/src/wasmfx/optimized.rs b/crates/cranelift/src/wasmfx/optimized.rs index 8998d52492b5..34e30676d9e2 100644 --- a/crates/cranelift/src/wasmfx/optimized.rs +++ b/crates/cranelift/src/wasmfx/optimized.rs @@ -1,17 +1,22 @@ use super::shared; +use itertools::{Either, Itertools}; use crate::translate::{FuncEnvironment, FuncTranslationState}; use crate::wasmfx::shared::call_builtin; -use cranelift_codegen::ir; use cranelift_codegen::ir::condcodes::*; use cranelift_codegen::ir::types::*; -use cranelift_codegen::ir::{BlockCall, InstBuilder, JumpTableData}; +use cranelift_codegen::ir::{self, MemFlags}; +use cranelift_codegen::ir::{Block, BlockCall, InstBuilder, JumpTableData}; use cranelift_frontend::FunctionBuilder; use wasmtime_environ::PtrSize; use wasmtime_environ::{WasmResult, WasmValType}; pub const DEBUG_ASSERT_TRAP_CODE: crate::TrapCode = crate::TRAP_DEBUG_ASSERTION; +// TODO(frank-emrich) This is the size for x64 Linux. Once we support different +// platforms for stack switching, must select appropriate value for target. +pub const CONTROL_CONTEXT_SIZE: usize = 24; + #[cfg_attr(feature = "wasmfx_baseline", allow(unused_imports, reason = "TODO"))] pub(crate) use shared::{assemble_contobj, disassemble_contobj, vm_contobj_type, ControlEffect}; @@ -258,7 +263,9 @@ pub(crate) mod typed_continuation_helpers { std::line!(), "\n" ); - tc::emit_debug_assert_generic($env, $builder, $condition, msg); + // This makes the borrow checker happy if $condition uses env or builder. + let c = $condition; + tc::emit_debug_assert_generic($env, $builder, c, msg); }; } @@ -1044,9 +1051,9 @@ pub(crate) mod typed_continuation_helpers { ) -> ir::Value { if cfg!(debug_assertions) { let continuation_discriminant = - wasmtime_continuations::STACK_CHAIN_ABSENT_DISCRIMINANT; + wasmtime_continuations::STACK_CHAIN_CONTINUATION_DISCRIMINANT; let is_continuation = builder.ins().icmp_imm( - IntCC::NotEqual, + IntCC::Equal, self.discriminant, continuation_discriminant as i64, ); @@ -1149,17 +1156,32 @@ pub(crate) mod typed_continuation_helpers { builder.ins().store(mem_flags, discriminant, state_ptr, 0); } - pub fn has_state<'a>( + pub fn has_state_any_of<'a>( &self, env: &mut crate::func_environ::FuncEnvironment<'a>, builder: &mut FunctionBuilder, - state: wasmtime_continuations::State, + states: &[wasmtime_continuations::State], ) -> ir::Value { let actual_state = self.load_state(env, builder); + let zero = builder.ins().iconst(I8, 0); + let mut res = zero; + for state in states { + let eq = + builder + .ins() + .icmp_imm(IntCC::Equal, actual_state, state.discriminant() as i64); + res = builder.ins().bor(res, eq); + } + res + } - builder - .ins() - .icmp_imm(IntCC::Equal, actual_state, state.discriminant() as i64) + pub fn has_state<'a>( + &self, + env: &mut crate::func_environ::FuncEnvironment<'a>, + builder: &mut FunctionBuilder, + state: wasmtime_continuations::State, + ) -> ir::Value { + self.has_state_any_of(env, builder, &[state]) } /// Checks whether the `State` reflects that the stack has ever been @@ -1182,6 +1204,35 @@ pub(crate) mod typed_continuation_helpers { HandlerList::new(self.address, offset as i32) } + #[allow(clippy::cast_possible_truncation, reason = "TODO")] + pub fn get_first_switch_handler_index<'a>( + &self, + _env: &mut crate::func_environ::FuncEnvironment<'a>, + builder: &mut FunctionBuilder, + ) -> ir::Value { + // Field first_switch_handler_index has type u32 + let memflags = ir::MemFlags::trusted(); + let offset = wasmtime_continuations::offsets::common_stack_information::FIRST_SWITCH_HANDLER_INDEX; + builder + .ins() + .load(I32, memflags, self.address, offset as i32) + } + + #[allow(clippy::cast_possible_truncation, reason = "TODO")] + pub fn set_first_switch_handler_index<'a>( + &self, + _env: &mut crate::func_environ::FuncEnvironment<'a>, + builder: &mut FunctionBuilder, + value: ir::Value, + ) { + // Field first_switch_handler_index has type u32 + let memflags = ir::MemFlags::trusted(); + let offset = wasmtime_continuations::offsets::common_stack_information::FIRST_SWITCH_HANDLER_INDEX; + builder + .ins() + .store(memflags, value, self.address, offset as i32); + } + /// Sets `last_wasm_entry_sp` and `stack_limit` fields in /// `VMRuntimelimits` using the values from the `StackLimits` of this /// object. @@ -1327,6 +1378,9 @@ pub(crate) mod typed_continuation_helpers { use crate::wasmfx::optimized::tc::StackChain; use typed_continuation_helpers as tc; +use wasmtime_continuations::{ + State, CONTROL_EFFECT_RESUME_DISCRIMINANT, CONTROL_EFFECT_SWITCH_DISCRIMINANT, +}; #[allow(clippy::cast_possible_truncation, reason = "TODO")] fn vmcontref_load_return_values<'a>( @@ -1515,10 +1569,18 @@ pub(crate) fn vmctx_store_payloads<'a>( /// which must be a `*mut VMTagDefinition`. The search walks up the chain of /// continuations beginning at `start`. /// +/// The flag `search_suspend_handlers` determines whether we search for a +/// suspend or switch handler. Concretely, this influences which part of each +/// handler list we will search. +/// +/// We trap if no handler was found. +/// /// The returned values are: -/// 1. The continuation whose *parent* (another continuation or the -/// main stack) handles `tag_address`. -/// 2. The index of the handler in the parent's HandlerList. +/// 1. The stack (continuation or main stack, represented as a StackChain) in +/// whose handler list we found the tag (i.e., the stack that performed the +/// resume instruction that installed handler for the tag). +/// 2. The continuation whose parent is the stack mentioned in 1. +/// 3. The index of the handler in the handler list. /// /// In pseudo-code, the generated code's behavior can be expressed as /// follows: @@ -1529,8 +1591,12 @@ pub(crate) fn vmctx_store_payloads<'a>( /// parent_link = contref.parent /// parent_csi = parent_link.get_common_stack_information(); /// handlers = parent_csi.handlers; -/// len = handlers.length; -/// for index in 0..len { +/// (begin_range, end_range) = if search_suspend_handlers { +/// (0, parent_csi.first_switch_handler_index) +/// } else { +/// (parent_csi.first_switch_handler_index, handlers.length) +/// }; +/// for index in begin_range..end_range { /// if handlers[index] == tag_address { /// goto on_match(contref, index) /// } @@ -1547,7 +1613,8 @@ fn search_handler<'a>( builder: &mut FunctionBuilder, start: &tc::StackChain, tag_address: ir::Value, -) -> (ir::Value, ir::Value) { + search_suspend_handlers: bool, +) -> (StackChain, ir::Value, ir::Value) { let handle_link = builder.create_block(); let begin_search_handler_list = builder.create_block(); let try_index = builder.create_block(); @@ -1579,7 +1646,7 @@ fn search_handler<'a>( }; // Block begin_search_handler_list - let (contref, parent_link, handler_list_data_ptr, len) = { + let (contref, parent_link, handler_list_data_ptr, end_range) = { builder.switch_to_block(begin_search_handler_list); let contref = chain_link.unchecked_get_continuation(env, builder); let contref = tc::VMContRef::new(contref); @@ -1597,12 +1664,25 @@ fn search_handler<'a>( let handlers = parent_csi.get_handler_list(); let handler_list_data_ptr = handlers.get_data(env, builder); - let len = handlers.get_length(env, builder); - let zero = builder.ins().iconst(I32, 0); - builder.ins().jump(try_index, &[zero]); + let first_switch_handler_index = parent_csi.get_first_switch_handler_index(env, builder); + + // Note that these indices are inclusive-exclusive, i.e. [begin_range, end_range). + let (begin_range, end_range) = if search_suspend_handlers { + let zero = builder.ins().iconst(I32, 0); + if cfg!(debug_assertions) { + let length = handlers.get_length(env, builder); + emit_debug_assert_ule!(env, builder, first_switch_handler_index, length); + } + (zero, first_switch_handler_index) + } else { + let length = handlers.get_length(env, builder); + (first_switch_handler_index, length) + }; + + builder.ins().jump(try_index, &[begin_range]); - (contref, parent_link, handler_list_data_ptr, len) + (contref, parent_link, handler_list_data_ptr, end_range) }; // Block try_index @@ -1611,7 +1691,9 @@ fn search_handler<'a>( builder.switch_to_block(try_index); let index = builder.block_params(try_index)[0]; - let in_bounds = builder.ins().icmp(IntCC::UnsignedLessThan, index, len); + let in_bounds = builder + .ins() + .icmp(IntCC::UnsignedLessThan, index, end_range); builder.ins().brif( in_bounds, compare_tags, @@ -1662,7 +1744,17 @@ fn search_handler<'a>( // final block: on_match builder.switch_to_block(on_match); - (contref.address, index) + emit_debug_println!( + env, + builder, + "[search_handler] found handler at stack chain ({}, {:p}), whose child continuation is {:p}, index is {}", + parent_link.to_raw_parts()[0], + parent_link.to_raw_parts()[1], + contref.address, + index + ); + + (parent_link, contref.address, index) } pub(crate) fn translate_cont_bind<'a>( @@ -1722,8 +1814,8 @@ pub(crate) fn translate_resume<'a>( type_index: u32, resume_contobj: ir::Value, resume_args: &[ir::Value], - resumetable: &[(u32, ir::Block)], -) -> Vec { + resumetable: &[(u32, Option)], +) -> WasmResult> { // The resume instruction is the most involved instruction to // compile as it is responsible for both continuation application // and control tag dispatch. @@ -1765,6 +1857,16 @@ pub(crate) fn translate_resume<'a>( let vmctx = tc::VMContext::new(env.vmctx_val(&mut builder.cursor()), env.pointer_type()); + // Split the resumetable into suspend handlers (each represented by the tag + // index and handler block) and the switch handlers (represented just by the + // tag index). Note that we currently don't remove duplicate tags. + let (suspend_handlers, switch_tags): (Vec<(u32, Block)>, Vec) = resumetable + .iter() + .partition_map(|(tag_index, block_opt)| match block_opt { + Some(block) => Either::Left((*tag_index, *block)), + None => Either::Right(*tag_index), + }); + // Technically, there is no need to have a dedicated resume block, we could // just put all of its contents into the current block. builder.ins().jump(resume_block, &[]); @@ -1897,16 +1999,34 @@ pub(crate) fn translate_resume<'a>( let handler_list = parent_csi.get_handler_list(); if resumetable.len() > 0 { + // Total number of handlers (suspend and switch). + let handler_count = builder.ins().iconst(I32, resumetable.len() as i64); + // If the existing list is too small, reallocate (in runtime). - let resumetable_len = builder.ins().iconst(I32, resumetable.len() as i64); - handler_list.ensure_capacity(env, builder, resumetable_len); + handler_list.ensure_capacity(env, builder, handler_count); + + let suspend_handler_count = suspend_handlers.len(); - // Note that we currently don't remove duplicate tags. - let tag_addresses: Vec = resumetable + // All handlers, represented by the indices of the tags they handle. + // All the suspend handlers come first, followed by all the switch handlers. + let all_handlers = suspend_handlers .iter() - .map(|(tag_index, _)| shared::tag_address(env, builder, *tag_index)) + .map(|(tag_index, _block)| *tag_index) + .chain(switch_tags); + + // Translate all tag indices to tag addresses (i.e., the corresponding *mut VMTagDefinition). + let all_tag_addresses: Vec = all_handlers + .map(|tag_index| shared::tag_address(env, builder, tag_index)) .collect(); - handler_list.store_data_entries(env, builder, &tag_addresses, false); + + // Store all tag addresess in the handler list. + handler_list.store_data_entries(env, builder, &all_tag_addresses, false); + + // To enable distinguishing switch and suspend handlers when searching the handler list: + // Store at which index the switch handlers start. + let first_switch_handler_index = + builder.ins().iconst(I32, suspend_handler_count as i64); + parent_csi.set_first_switch_handler_index(env, builder, first_switch_handler_index); } let resume_payload = ControlEffect::make_resume(env, builder).to_u64(); @@ -1947,9 +2067,9 @@ pub(crate) fn translate_resume<'a>( vmctx.store_stack_chain(env, builder, &original_stack_chain); parent_csi.set_state(env, builder, wasmtime_continuations::State::Running); - // Just for consistency: Reset the length of the handler list to 0, - // these handlers are no longer active. + // Just for consistency: Reset the handler list. handler_list.clear(builder); + parent_csi.set_first_switch_handler_index(env, builder, zero); // Extract the result and signal bit. let result = ControlEffect::from_u64(result); @@ -2047,7 +2167,7 @@ pub(crate) fn translate_resume<'a>( let target_preamble_blocks = { let mut preamble_blocks = vec![]; - for &(handle_tag, target_block) in resumetable { + for &(handle_tag, target_block) in &suspend_handlers { let preamble_block = builder.create_block(); preamble_blocks.push(preamble_block); builder.switch_to_block(preamble_block); @@ -2120,7 +2240,7 @@ pub(crate) fn translate_resume<'a>( // repurposed). shared::typed_continuations_drop_cont_ref(env, builder, returned_contref.address); - return values; + Ok(values) } } @@ -2140,8 +2260,8 @@ pub(crate) fn translate_suspend<'a>( let vmctx = tc::VMContext::new(vmctx, env.pointer_type()); let active_stack_chain = vmctx.load_stack_chain(env, builder); - let (end_of_chain_contref, handler_index) = - search_handler(env, builder, &active_stack_chain, tag_addr); + let (_, end_of_chain_contref, handler_index) = + search_handler(env, builder, &active_stack_chain, tag_addr, true); emit_debug_println!( env, @@ -2191,3 +2311,300 @@ pub(crate) fn translate_suspend<'a>( return_values } + +#[allow(clippy::cast_possible_truncation, reason = "TODO")] +pub(crate) fn translate_switch<'a>( + env: &mut crate::func_environ::FuncEnvironment<'a>, + builder: &mut FunctionBuilder, + tag_index: u32, + switchee_contobj: ir::Value, + switch_args: &[ir::Value], + return_types: &[WasmValType], +) -> WasmResult> { + let vmctx = tc::VMContext::new(env.vmctx_val(&mut builder.cursor()), env.pointer_type()); + + // Check and increment revision on switchee continuation object (i.e., the + // one being switched to). Logically, the switchee continuation extends from + // `switchee_contref` to `switchee_contref.last_ancestor` (i.e., the end of + // the parent chain starting at `switchee_contref`). + let switchee_contref = { + let (witness, target_contref) = shared::disassemble_contobj(env, builder, switchee_contobj); + let mut target_contref = tc::VMContRef::new(target_contref); + + let revision = target_contref.get_revision(env, builder); + let evidence = builder.ins().icmp(IntCC::Equal, revision, witness); + emit_debug_println!( + env, + builder, + "[switch] target_contref = {:p} witness = {}, revision = {}, evidence = {}", + target_contref.address, + witness, + revision, + evidence + ); + builder + .ins() + .trapz(evidence, crate::TRAP_CONTINUATION_ALREADY_CONSUMED); + let _next_revision = target_contref.incr_revision(env, builder, revision); + target_contref + }; + + // We create the "switcher continuation" (i.e., the one executing switch) + // from the current execution context: Logically, it extends from the + // continuation reference executing `switch` (subsequently called + // `switcher_contref`) to the immediate child (called + // `switcher_contref_last_ancestor`) of the stack with the corresponding + // handler (saved in `handler_stack_chain`). + let ( + switcher_contref, + switcher_contobj, + switcher_contref_last_ancestor, + handler_stack_chain, + vm_runtime_limits_ptr, + ) = { + let tag_addr = shared::tag_address(env, builder, tag_index); + let active_stack_chain = vmctx.load_stack_chain(env, builder); + let (handler_stack_chain, last_ancestor, _handler_index) = + search_handler(env, builder, &active_stack_chain, tag_addr, false); + let mut last_ancestor = tc::VMContRef::new(last_ancestor); + + // If we get here, the search_handler logic succeeded (i.e., did not trap). + // Thus, there is at least one parent, so we are not on the main stack. + // Can therefore extract continuation directly. + let switcher_contref = active_stack_chain.unchecked_get_continuation(env, builder); + let mut switcher_contref = tc::VMContRef::new(switcher_contref); + + switcher_contref.set_last_ancestor(env, builder, last_ancestor.address); + + let switcher_contref_csi = switcher_contref.common_stack_information(env, builder); + emit_debug_assert!( + env, + builder, + switcher_contref_csi.has_state(env, builder, State::Running) + ); + switcher_contref_csi.set_state(env, builder, State::Suspended); + // We break off `switcher_contref` from the chain of active + // continuations, by separating the link between `last_ancestor` and its + // parent stack. + let absent = StackChain::absent(builder, env.pointer_type()); + last_ancestor.set_parent_stack_chain(env, builder, &absent); + + // Load current runtime limits from `VMContext` and store in the + // switcher continuation. + let vm_runtime_limits_ptr = vmctx.load_vm_runtime_limits_ptr(env, builder); + switcher_contref_csi.load_limits_from_vmcontext( + env, + builder, + vm_runtime_limits_ptr, + false, + None, + None, + ); + + let revision = switcher_contref.get_revision(env, builder); + let new_contobj = + shared::assemble_contobj(env, builder, revision, switcher_contref.address); + + emit_debug_println!( + env, + builder, + "[switch] created new contref = {:p}, revision = {}", + switcher_contref.address, + revision + ); + + ( + switcher_contref, + new_contobj, + last_ancestor, + handler_stack_chain, + vm_runtime_limits_ptr, + ) + }; + + // Prepare switchee continuation: + // - Store "ordinary" switch arguments as well as the contobj just + // synthesized from the current context (i.e., `switcher_contobj`) in the + // switchee continuation's payload buffer. + // - Splice switchee's continuation chain with handler stack to form new + // overall chain of active continuations. + let (switchee_contref_csi, switchee_contref_last_ancestor) = { + let mut combined_payloads = switch_args.to_vec(); + combined_payloads.push(switcher_contobj); + let count = builder.ins().iconst(I32, combined_payloads.len() as i64); + vmcontref_store_payloads( + env, + builder, + &combined_payloads, + count, + switchee_contref.address, + ); + + let switchee_contref_csi = switchee_contref.common_stack_information(env, builder); + + emit_debug_assert!( + env, + builder, + switchee_contref_csi.has_state_any_of(env, builder, &[State::Fresh, State::Suspended]) + ); + switchee_contref_csi.set_state(env, builder, State::Running); + + let switchee_contref_last_ancestor = switchee_contref.get_last_ancestor(env, builder); + let mut switchee_contref_last_ancestor = tc::VMContRef::new(switchee_contref_last_ancestor); + + switchee_contref_last_ancestor.set_parent_stack_chain(env, builder, &handler_stack_chain); + + (switchee_contref_csi, switchee_contref_last_ancestor) + }; + + // Update VMContext/Store: Update active continuation and `VMRuntimeLimits`. + { + vmctx.set_active_continuation(env, builder, switchee_contref.address); + + switchee_contref_csi.write_limits_to_vmcontext(env, builder, vm_runtime_limits_ptr); + } + + // Perform actual stack switch + { + let switcher_last_ancestor_fs = + switcher_contref_last_ancestor.get_fiber_stack(env, builder); + let switcher_last_ancestor_cc = + switcher_last_ancestor_fs.load_control_context(env, builder); + + let switchee_last_ancestor_fs = + switchee_contref_last_ancestor.get_fiber_stack(env, builder); + let switchee_last_ancestor_cc = + switchee_last_ancestor_fs.load_control_context(env, builder); + + // The stack switch involves the following control contexts (e.g., IP, + // SP, FP, ...): + // - `switchee_last_ancestor_cc` contains the information to continue + // execution in the switchee/target continuation. + // - `switcher_last_ancestor_cc` contains the information about how to + // continue execution once we suspend/return to the stack with the + // switch handler. + // + // In total, the following needs to happen: + // 1. Load control context at `switchee_last_ancestor_cc` to perform + // stack switch. + // 2. Move control context at `switcher_last_ancestor_cc` over to + // `switchee_last_ancestor_cc`. + // 3. Upon actual switch, save current control context at + // `switcher_last_ancestor_cc`. + // + // We implement this as follows: + // 1. We copy `switchee_last_ancestor_cc` to a temporary area on the + // stack (`tmp_control_context`). + // 2. We copy `switcher_last_ancestor_cc` over to + // `switchee_last_ancestor_cc`. + // 3. We invoke the stack switch instruction such that it reads from the + // temporary area, and writes to `switcher_last_ancestor_cc`. + // + // Note that the temporary area is only accessed once by the + // `stack_switch` instruction emitted later in this block, meaning that we + // don't have to worry about its lifetime. + // + // NOTE(frank-emrich) The implementation below results in one stack slot + // being created per switch instruction, even though multiple switch + // instructions in the same function could safely re-use the same stack + // slot. Thus, we could implement logic for sharing the stack slot by + // adding an appropriate field to `FuncEnvironment`. + // + // NOTE(frank-emrich) We could avoid the copying to a temporary area by + // making `stack_switch` do all of the necessary moving itself. However, + // that would be a rather ad-hoc change to how the instruction uses the + // two pointers given to it. + + let slot_size = ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + CONTROL_CONTEXT_SIZE as u32, + env.pointer_type().bytes() as u8, + ); + let slot = builder.create_sized_stack_slot(slot_size); + let tmp_control_context = builder.ins().stack_addr(env.pointer_type(), slot, 0); + + let flags = MemFlags::trusted(); + let mut offset: i32 = 0; + while offset < CONTROL_CONTEXT_SIZE as i32 { + // switchee_last_ancestor_cc -> tmp control context + let tmp1 = + builder + .ins() + .load(env.pointer_type(), flags, switchee_last_ancestor_cc, offset); + builder + .ins() + .store(flags, tmp1, tmp_control_context, offset); + + // switcher_last_ancestor_cc -> switchee_last_ancestor_cc + let tmp2 = + builder + .ins() + .load(env.pointer_type(), flags, switcher_last_ancestor_cc, offset); + builder + .ins() + .store(flags, tmp2, switchee_last_ancestor_cc, offset); + + offset += env.pointer_type().bytes() as i32; + } + + let switch_payload = ControlEffect::make_switch(env, builder).to_u64(); + + emit_debug_println!( + env, + builder, + "[switch] about to execute stack_switch, store_control_context_ptr is {:p}, load_control_context_ptr {:p}, tmp_control_context is {:p}", + switcher_last_ancestor_cc, + switchee_last_ancestor_cc, + tmp_control_context + ); + + let result = builder.ins().stack_switch( + switcher_last_ancestor_cc, + tmp_control_context, + switch_payload, + ); + + emit_debug_println!( + env, + builder, + "[switch] continuing after stack_switch in frame with stack chain ({}, {:p}), result is {:p}", + handler_stack_chain.to_raw_parts()[0], + handler_stack_chain.to_raw_parts()[1], + result + ); + + if cfg!(debug_assertions) { + // The only way to switch back to this point is by using resume or switch instructions. + let result_control_effect = ControlEffect::from_u64(result); + let result_discriminant = result_control_effect.signal(env, builder); + let is_resume = builder.ins().icmp_imm( + IntCC::Equal, + result_discriminant, + CONTROL_EFFECT_RESUME_DISCRIMINANT as i64, + ); + let is_switch = builder.ins().icmp_imm( + IntCC::Equal, + result_discriminant, + CONTROL_EFFECT_SWITCH_DISCRIMINANT as i64, + ); + let is_switch_or_resume = builder.ins().bor(is_switch, is_resume); + emit_debug_assert!(env, builder, is_switch_or_resume); + } + } + + // After switching back to the original stack: Load return values, they are + // stored on the switcher continuation. + let return_values = { + if cfg!(debug_assertions) { + // The originally active continuation (before the switch) should be active again. + let active_stack_chain = vmctx.load_stack_chain(env, builder); + // This has a debug assertion that also checks that the `active_stack_chain` is indeed a continuation. + let active_contref = active_stack_chain.unchecked_get_continuation(env, builder); + emit_debug_assert_eq!(env, builder, switcher_contref.address, active_contref); + } + + vmcontref_load_values(env, builder, switcher_contref.address, return_types) + }; + + Ok(return_values) +} diff --git a/crates/cranelift/src/wasmfx/shared.rs b/crates/cranelift/src/wasmfx/shared.rs index 688e67d8df15..bfbafb4b0e31 100644 --- a/crates/cranelift/src/wasmfx/shared.rs +++ b/crates/cranelift/src/wasmfx/shared.rs @@ -205,6 +205,19 @@ impl ControlEffect { Self(val) } + pub fn make_switch<'a>( + _env: &mut crate::func_environ::FuncEnvironment<'a>, + builder: &mut FunctionBuilder, + ) -> Self { + let discriminant = builder.ins().iconst( + I64, + wasmtime_continuations::CONTROL_EFFECT_SWITCH_DISCRIMINANT as i64, + ); + let val = builder.ins().ishl_imm(discriminant, 32); + + Self(val) + } + pub fn make_suspend<'a>( _env: &mut crate::func_environ::FuncEnvironment<'a>, builder: &mut FunctionBuilder, diff --git a/crates/environ/src/types.rs b/crates/environ/src/types.rs index c9a28b720ef9..bf9a98edf302 100644 --- a/crates/environ/src/types.rs +++ b/crates/environ/src/types.rs @@ -221,6 +221,14 @@ impl WasmValType { | WasmValType::V128 => *self, } } + + /// TODO + pub fn unwrap_ref_type(&self) -> WasmRefType { + match self { + WasmValType::Ref(ref_type) => *ref_type, + _ => panic!("Called WasmValType::unwrap_ref_type on non-reference type"), + } + } } /// WebAssembly reference type -- equivalent of `wasmparser`'s RefType diff --git a/crates/wasmtime/src/runtime/vm/continuation.rs b/crates/wasmtime/src/runtime/vm/continuation.rs index 9ce5f58de661..9a16bc157290 100644 --- a/crates/wasmtime/src/runtime/vm/continuation.rs +++ b/crates/wasmtime/src/runtime/vm/continuation.rs @@ -108,10 +108,11 @@ pub mod optimized { /// Note that this is *not* used for tag payloads. pub args: Payloads, - /// Once a continuation is suspended, this buffer is used to hold - /// payloads provided by cont.bind and resume and received at the - /// suspend site. In particular, this may not be used while the - /// continuation's state is `Fresh`. + /// Once a continuation has been suspended (using suspend or switch), + /// this buffer is used to hold payloads provided by cont.bind, resume, + /// and switch. They are received at the suspend site (i.e., the + /// corrsponding suspend or switch instruction). In particular, this may + /// not be used while the continuation's state is `Fresh`. pub values: Payloads, /// Revision counter. @@ -143,6 +144,7 @@ pub mod optimized { limits, state, handlers, + first_switch_handler_index: 0, }; let parent_chain = StackChain::Absent; let last_ancestor = std::ptr::null_mut(); diff --git a/crates/wast-util/src/lib.rs b/crates/wast-util/src/lib.rs index 248ffa7a325f..d54af2de732c 100644 --- a/crates/wast-util/src/lib.rs +++ b/crates/wast-util/src/lib.rs @@ -287,12 +287,25 @@ impl WastTest { /// Returns whether this test should fail under the specified extra /// configuration. pub fn should_fail(&self, config: &WastConfig) -> bool { - // The stack-switching baseline does not support proper linking of tags yet. - if cfg!(feature = "wasmfx_baseline") - && cfg!(not(feature = "wasmfx_no_baseline")) - && self.path.ends_with("linking_tags2.wast") - { - return true; + if cfg!(feature = "wasmfx_baseline") && cfg!(not(feature = "wasmfx_no_baseline")) { + // The stack-switching baseline does not support proper linking of tags yet. + if self.path.ends_with("linking_tags2.wast") { + return true; + } + + // The stack-switching baseline does not support switch instructions + // and switch handlers yet. + if self.path.parent().unwrap().ends_with("stack-switching") + && self + .path + .file_name() + .unwrap() + .to_str() + .unwrap() + .starts_with("switch") + { + return true; + } } // Winch only supports x86_64 at this time. diff --git a/tests/all/stack_switching.rs b/tests/all/stack_switching.rs index d2becc859882..20d074b5a5be 100644 --- a/tests/all/stack_switching.rs +++ b/tests/all/stack_switching.rs @@ -18,6 +18,8 @@ mod test_utils { config.wasm_function_references(true); config.wasm_exceptions(true); config.wasm_stack_switching(true); + // Required in order to use recursive types. + config.wasm_gc(true); let engine = Engine::new(&config).unwrap(); @@ -1040,6 +1042,75 @@ mod traps { Ok(()) } + #[test] + /// Tests that we get correct backtraces after switch. + /// We first create the a stack with the following shape: + /// entry -> a -> b, then switch, leading to + /// entry -> c -> d, at which point we resume the a -> b continuation: + /// entry -> c -> d -> a -> b + /// We trap at that point. + #[cfg_attr(feature = "wasmfx_baseline", ignore)] + fn trap_switch_and_resume() -> Result<()> { + let wat = r#" + (module + (rec + (type $ft0 (func (param (ref null $ct0)))) + (type $ct0 (cont $ft0))) + + (type $ft1 (func)) + (type $ct1 (cont $ft1)) + + (tag $t) + + (func $a (type $ft1) + (cont.new $ct1 (ref.func $b)) + (resume $ct1) + ) + (elem declare func $a) + + (func $b (type $ft1) + (cont.new $ct0 (ref.func $c)) + (switch $ct0 $t) + + ;; we want a backtrace here + (unreachable) + ) + (elem declare func $b) + + (func $c (type $ft0) + (local.get 0) + (cont.new $ct0 (ref.func $d)) + (resume $ct0) + ) + (elem declare func $c) + + (func $d (type $ft0) + (block $handler (result (ref $ct1)) + (ref.null $ct0) ;; passed as payload + (local.get 0) ;; resumed + (resume $ct0 (on $t $handler)) + (unreachable) ;; f1 will suspend after the switch + ) + (resume $ct1) + ) + (elem declare func $d) + + (func $entry (export "entry") + (cont.new $ct1 (ref.func $a)) + (resume $ct1 (on $t switch)) + ) + ) + "#; + + run_test_expect_trap_backtrace( + wat, + Trap::UnreachableCodeReached, + &["entry", "c", "d", "a", "b"], + ); + + Ok(()) + } + #[test] /// Tests that we get correct panic payloads if we panic deep inside multiple /// continuations. Note that wasmtime does not create its own backtraces for panics. diff --git a/tests/misc_testsuite/stack-switching/switch1.wast b/tests/misc_testsuite/stack-switching/switch1.wast new file mode 100644 index 000000000000..5a4047482627 --- /dev/null +++ b/tests/misc_testsuite/stack-switching/switch1.wast @@ -0,0 +1,35 @@ +;;! stack_switching = true + +;; smoke test for switching: Only a single switch to a cotinuation created with +;; cont.new. +(module + + (type $ft0 (func)) + (type $ct0 (cont $ft0)) + + (type $ft1 (func (param (ref $ct0)))) + (type $ct1 (cont $ft1)) + + (func $print (import "spectest" "print_i32") (param i32)) + (tag $t) + + + (func $f + (cont.new $ct1 (ref.func $g)) + (switch $ct1 $t) + ) + (elem declare func $f) + + (func $g (type $ft1) + (call $print (i32.const 123)) + ) + (elem declare func $g) + + (func $entry (export "entry") (result i32) + (cont.new $ct0 (ref.func $f)) + (resume $ct0 (on $t switch)) + (i32.const 0) + ) +) + +(assert_return (invoke "entry" ) (i32.const 0)) diff --git a/tests/misc_testsuite/stack-switching/switch2.wast b/tests/misc_testsuite/stack-switching/switch2.wast new file mode 100644 index 000000000000..2bf84718fcf1 --- /dev/null +++ b/tests/misc_testsuite/stack-switching/switch2.wast @@ -0,0 +1,71 @@ +;;! stack_switching = true +;;! gc = true + +;; switch to continuation created by switch +(module + + (rec + (type $ft (func (param i32 (ref null $ct)) (result i32))) + (type $ct (cont $ft))) + + (tag $t (result i32)) + + (func $f0 (type $ft) + ;; Just a wrapper around $f1 to make sure that the involved continuation + ;; chains consist of more than one element. + + (local.get 0) + (local.get 1) + (cont.new $ct (ref.func $f1)) + (resume $ct) + ) + (elem declare func $f0) + + (func $f1 (type $ft) + ;; add 1 to argument and pass to $g0 on switch + (local.get 0) + (i32.const 1) + (i32.add) + ;; prepare continuation + (cont.new $ct (ref.func $g0)) + (switch $ct $t) + (drop) ;; we won't run $g to completion + + ;; add 1 to payload received from $g1 + (i32.const 1) + (i32.add) + ) + (elem declare func $f1) + + (func $g0 (type $ft) + ;; Just a wrapper around $g1 to make sure that the involved continuation + ;; chains consist of more than one element. + + (local.get 0) + (local.get 1) + (cont.new $ct (ref.func $g1)) + (resume $ct) + ) + (elem declare func $g0) + + (func $g1 (type $ft) + ;; add 1 to argument received from $f1 + (local.get 0) + (i32.const 1) + (i32.add) + (local.get 1) + (switch $ct $t) + + ;; $f never switches back to us + (unreachable) + ) + (elem declare func $g1) + + (func $entry (export "entry") (result i32) + (i32.const 100) + (ref.null $ct) + (cont.new $ct (ref.func $f0)) + (resume $ct (on $t switch)) + ) +) +(assert_return (invoke "entry" ) (i32.const 103)) diff --git a/tests/misc_testsuite/stack-switching/switch3.wast b/tests/misc_testsuite/stack-switching/switch3.wast new file mode 100644 index 000000000000..58df4aa337b1 --- /dev/null +++ b/tests/misc_testsuite/stack-switching/switch3.wast @@ -0,0 +1,95 @@ +;;! stack_switching = true +;;! gc = true + +;; resume continuation created by switch +(module + + (rec + (type $ft0 (func (param i32 (ref null $ct0)) (result i32))) + (type $ct0 (cont $ft0))) + + (type $ft1 (func (param i32) (result i32))) + (type $ct1 (cont $ft1)) + + (tag $t_switch (result i32)) + (tag $t_suspend (param i32) (result i32)) + + (func $f0 (type $ft0) + ;; Just a wrapper around $f1 to make sure that the involved continuation + ;; chains consist of more than one element. + + (local.get 0) + (local.get 1) + (cont.new $ct0 (ref.func $f1)) + (resume $ct0) + ) + (elem declare func $f0) + + (func $f1 (type $ft0) + ;; add 1 to argument and pass to $g on switch + (local.get 0) + (i32.const 1) + (i32.add) + ;; prepare continuation + (cont.new $ct0 (ref.func $g0)) + ;; switch to $g0 + (switch $ct0 $t_switch) + ;; g1 resumed us, installed suspend handler for t_suspend) + ;; drop null continuation and increment argument. + (drop) + (i32.const 1) + (i32.add) + (suspend $t_suspend) + + ;; add 1 to tag return value + (i32.const 1) + (i32.add) + ) + (elem declare func $f1) + + (func $g0 (type $ft0) + ;; Just a wrapper around $g1 to make sure that the involved continuation + ;; chains consist of more than one element. + + (local.get 0) + (local.get 1) + (cont.new $ct0 (ref.func $g1)) + (resume $ct0) + ) + (elem declare func $g0) + + (func $g1 (type $ft0) + (local $c (ref $ct1)) + + (block $handler (result i32 (ref $ct1)) + ;; add 1 to argument received from f1 on switch + (local.get 0) + (i32.const 1) + (i32.add) + (ref.null $ct0) ;; passed as payload + (local.get 1) ;; resumed + (resume $ct0 (on $t_suspend $handler)) + (unreachable) ;; f1 will suspend after the switch + ) + ;; stash continuation created by suspend in $f1 aside + (local.set $c) + ;; increment value received from suspend in $f1 + (i32.const 1) + (i32.add) + ;; ... and pass back to $f1 + (local.get $c) + (resume $ct1) + ;; increment $f1's return value + (i32.const 1) + (i32.add) + ) + (elem declare func $g1) + + (func $entry (export "entry") (result i32) + (i32.const 100) + (ref.null $ct0) + (cont.new $ct0 (ref.func $f0)) + (resume $ct0 (on $t_switch switch)) + ) +) +(assert_return (invoke "entry" ) (i32.const 106)) diff --git a/tests/misc_testsuite/stack-switching/switch4.wast b/tests/misc_testsuite/stack-switching/switch4.wast new file mode 100644 index 000000000000..4cf1d6b51bd9 --- /dev/null +++ b/tests/misc_testsuite/stack-switching/switch4.wast @@ -0,0 +1,52 @@ +;;! stack_switching = true +;;! gc = true + +;; use cont.bind on continuation created by switch +(module + + (rec + (type $ft0 (func (param (ref null $ct1)) (result i32))) + (type $ct0 (cont $ft0)) + (type $ft1 (func (param i32 (ref null $ct1)) (result i32))) + (type $ct1 (cont $ft1))) + + (tag $t (result i32)) + + (func $f (type $ft1) + ;; add 1 to argument and pass to $g on switch + (local.get 0) + (i32.const 1) + (i32.add) + ;; prepare continuation + (cont.new $ct1 (ref.func $g)) + (cont.bind $ct1 $ct0) + (switch $ct0 $t) + (drop) ;; we won't run $g to completion + + ;; add 1 to payload received from $g + (i32.const 1) + (i32.add) + ) + (elem declare func $f) + + (func $g (type $ft1) + ;; add 1 to argument received from $f + (local.get 0) + (i32.const 1) + (i32.add) + (local.get 1) + (cont.bind $ct1 $ct0) + (switch $ct0 $t) + ;; $f never switches back to us + (unreachable) + ) + (elem declare func $g) + + (func $entry (export "entry") (result i32) + (i32.const 100) + (ref.null $ct1) + (cont.new $ct1 (ref.func $f)) + (resume $ct1 (on $t switch)) + ) +) +(assert_return (invoke "entry" ) (i32.const 103)) diff --git a/tests/misc_testsuite/stack-switching/switch5.wast b/tests/misc_testsuite/stack-switching/switch5.wast new file mode 100644 index 000000000000..207b0cef0533 --- /dev/null +++ b/tests/misc_testsuite/stack-switching/switch5.wast @@ -0,0 +1,54 @@ +;;! stack_switching = true + +;; switch to continuation created by suspend +(module + + (type $ft0 (func (result i32))) + (type $ct0 (cont $ft0)) + + (type $ft1 (func (result i32))) + (type $ct1 (cont $ft1)) + + (type $ft2 (func (param i32 (ref $ct0)) (result i32))) + (type $ct2 (cont $ft2)) + + (tag $t_suspend (result i32 (ref $ct0))) + (tag $t_switch (result i32)) + + (global $c (mut (ref null $ct2)) (ref.null $ct2)) + + + (func $f (result i32) + (suspend $t_suspend) + (drop) ;; drops continuation created by switch without running to completion + + ;; We increment the switch payload and return it to our handler + (i32.const 1) + (i32.add) + ) + (elem declare func $f) + + (func $g (result i32) + (i32.const 100) + (global.get $c) + (switch $ct2 $t_switch) + ;; we never switch back here + (unreachable) + ) + + (elem declare func $g) + + (func $entry (export "entry") (result i32) + (block $handler (result (ref $ct2)) + (cont.new $ct0 (ref.func $f)) + (resume $ct0 (on $t_suspend $handler)) + (unreachable) + ) + (global.set $c) + + (cont.new $ct1 (ref.func $g)) + (resume $ct1 (on $t_switch switch)) + + ) +) +(assert_return (invoke "entry" ) (i32.const 101)) diff --git a/tests/misc_testsuite/stack-switching/switch6.wast b/tests/misc_testsuite/stack-switching/switch6.wast new file mode 100644 index 000000000000..c634adcae9b4 --- /dev/null +++ b/tests/misc_testsuite/stack-switching/switch6.wast @@ -0,0 +1,37 @@ +;;! stack_switching = true + +;; suspend past a switch handler for same tag +(module + + (type $ft (func)) + (type $ct (cont $ft)) + + (tag $t) + + (func $f + (cont.new $ct (ref.func $g)) + (resume $ct (on $t switch)) + ;; $g will suspend and we will not come back here + (unreachable) + ) + (elem declare func $f) + + (func $g (type $ft) + (suspend $t) + ;; we will not come back here + (unreachable) + ) + (elem declare func $g) + + (func $entry (export "entry") (result i32) + (block $handler (result (ref $ct)) + (cont.new $ct (ref.func $f)) + (resume $ct (on $t switch) (on $t $handler)) + ;; we will have a suspension + (unreachable) + ) + (drop) + (i32.const 100) + ) +) +(assert_return (invoke "entry" ) (i32.const 100)) diff --git a/tests/misc_testsuite/stack-switching/switch7.wast b/tests/misc_testsuite/stack-switching/switch7.wast new file mode 100644 index 000000000000..1520286597b0 --- /dev/null +++ b/tests/misc_testsuite/stack-switching/switch7.wast @@ -0,0 +1,52 @@ +;;! stack_switching = true + +;; switch past a suspend handler for same tag +(module + + (type $ft0 (func (result i32))) + (type $ct0 (cont $ft0)) + + (type $ft1 (func (param i32) (result i32))) + (type $ct1 (cont $ft1)) + + (type $ft2 (func (param (ref $ct0)) (result i32))) + (type $ct2 (cont $ft2)) + + (tag $t (result i32)) + + (func $f (result i32) + (block $handler (result (ref $ct1)) + (cont.new $ct0 (ref.func $g)) + (resume $ct0 (on $t $handler)) + ;; $g will switch, we won't come back here + (unreachable) + ) + ;; we will not suspend + (unreachable) + ) + (elem declare func $f) + + (func $g (result i32) + (cont.new $ct2 (ref.func $h)) + (switch $ct2 $t) + ;; we won't come back here + (unreachable) + ) + (elem declare func $g) + + (func $h (type $ft2) + (i32.const 100) + ) + (elem declare func $h) + + (func $entry (export "entry") (result i32) + (block $handler (result (ref $ct1)) + (cont.new $ct0 (ref.func $f)) + (resume $ct0 (on $t switch) (on $t $handler)) + (return) + ) + ;; we will not suspend + (unreachable) + ) +) +(assert_return (invoke "entry" ) (i32.const 100)) diff --git a/tests/misc_testsuite/stack-switching/switch8.wast b/tests/misc_testsuite/stack-switching/switch8.wast new file mode 100644 index 000000000000..b92ab8b25352 --- /dev/null +++ b/tests/misc_testsuite/stack-switching/switch8.wast @@ -0,0 +1,36 @@ +;;! stack_switching = true + +;; try to switch to an already consumed continuation +(module + (type $ft0 (func)) + (type $ct0 (cont $ft0)) + + (type $ft1 (func (param (ref null $ct0)))) + (type $ct1 (cont $ft1)) + + (func $print (import "spectest" "print_i32") (param i32)) + (tag $t) + + + (func $f + (local $c (ref $ct1)) + (ref.null $ct0) ;; argument to $g + (cont.new $ct1 (ref.func $g)) + (local.tee $c) + (resume $ct1) + + ;; this should fail, we already used the continuation + (local.get $c) + (switch $ct1 $t) + ) + (elem declare func $f) + + (func $g (type $ft1)) + (elem declare func $g) + + (func $entry (export "entry") + (cont.new $ct0 (ref.func $f)) + (resume $ct0 (on $t switch)) + ) +) +(assert_trap (invoke "entry") "continuation already consumed")