diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index b32ae9a203b2..65cb19b599e5 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -1189,7 +1189,7 @@ impl<'a> TrampolineCompiler<'a> { } if self.compiler.tunables.concurrency_support { - Some(self.enter_sync_call_inline(instance, def.instance)) + Some(self.enter_sync_call_inline(def.instance)) } else { None } @@ -1286,14 +1286,9 @@ impl<'a> TrampolineCompiler<'a> { /// otherwise do eagerly. fn enter_sync_call_inline( &mut self, - caller_instance: RuntimeComponentInstanceIndex, callee_instance: RuntimeComponentInstanceIndex, ) -> ir::StackSlot { let vmctx = self.caller_vmctx(); - let caller_instance = self - .builder - .ins() - .iconst(ir::types::I32, i64::from(caller_instance.as_u32())); let callee_async = self.builder.ins().iconst(ir::types::I32, 0); let callee_instance = self .builder @@ -1304,7 +1299,6 @@ impl<'a> TrampolineCompiler<'a> { &mut self.alias_regions, vmctx, crate::component_sync_call::EnterArgs { - caller_instance, callee_async, callee_instance, }, diff --git a/crates/cranelift/src/component_sync_call.rs b/crates/cranelift/src/component_sync_call.rs index 831603afcf65..60d87843bc8c 100644 --- a/crates/cranelift/src/component_sync_call.rs +++ b/crates/cranelift/src/component_sync_call.rs @@ -29,8 +29,6 @@ use wasmtime_environ::{GetPtrSize, NUM_COMPONENT_CONTEXT_SLOTS, PtrSize}; /// `VMDeferredThread`, to be replayed by the host if it ever has to promote the /// deferred thread into a real one. pub struct EnterArgs { - /// The component instance performing the call. - pub caller_instance: ir::Value, /// Whether the callee is async-lifted, as an `i32` boolean. pub callee_async: ir::Value, /// The component instance being called into. @@ -79,11 +77,6 @@ where .store(&mut builder.cursor(), slot_addr, parent); // Record the deferred `enter_sync_call` arguments. - alias_regions.vm_deferred_thread().caller_instance().store( - &mut builder.cursor(), - slot_addr, - args.caller_instance, - ); alias_regions.vm_deferred_thread().callee_async().store( &mut builder.cursor(), slot_addr, diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index a617dc06c194..7d5ff73bee28 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -2076,8 +2076,8 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { /// defers the heavyweight task bookkeeping the `enter_sync_call` libcall /// would otherwise do eagerly. /// - /// `real_call_args` is `[callee_vmctx, caller_vmctx, caller_instance, - /// callee_async, callee_instance]`. + /// `real_call_args` is `[callee_vmctx, caller_vmctx, callee_async, + /// callee_instance]`. fn lower_fact_enter_sync_call(&mut self, real_call_args: &[ir::Value]) -> CallRets { let vmctx = self.env.vmctx_val(&mut self.builder.cursor()); let slot = crate::component_sync_call::enter( @@ -2085,9 +2085,8 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { &mut self.env.alias_regions, vmctx, crate::component_sync_call::EnterArgs { - caller_instance: real_call_args[2], - callee_async: real_call_args[3], - callee_instance: real_call_args[4], + callee_async: real_call_args[2], + callee_instance: real_call_args[3], }, ); diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 9e3233209d66..53d7ced09e5f 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -97,7 +97,7 @@ macro_rules! foreach_builtin_component_function { resource_transfer_own(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64; resource_transfer_borrow(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64; - enter_sync_call(vmctx: vmctx, caller_instance: u32, callee_async: u32, callee_instance: u32) -> bool; + enter_sync_call(vmctx: vmctx, callee_async: u32, callee_instance: u32) -> bool; exit_sync_call(vmctx: vmctx) -> bool; #[cfg(feature = "component-model-async")] diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index 69e456038884..35a67a999cf2 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -150,6 +150,9 @@ pub struct ComponentDfg { /// Interned map of id-to-`CanonicalOptions`, or all sets-of-options used by /// this component. pub options: Intern, + + /// The thread-transparency analysis for this component. + pub transparency: ThreadTransparency, } /// Possible side effects that are possible with instantiating this component. diff --git a/crates/environ/src/component/translate.rs b/crates/environ/src/component/translate.rs index 3cbf3d02643d..0b22334df618 100644 --- a/crates/environ/src/component/translate.rs +++ b/crates/environ/src/component/translate.rs @@ -23,6 +23,8 @@ use wasmparser::{Chunk, ComponentExternName, Encoding, Parser, Payload, Validato mod adapt; pub use self::adapt::*; mod inline; +mod thread_transparency; +pub use self::thread_transparency::ThreadTransparency; /// Structure used to translate a component and parse it. pub struct Translator<'a, 'data> { diff --git a/crates/environ/src/component/translate/adapt.rs b/crates/environ/src/component/translate/adapt.rs index 9555adcb7a6c..09d9b59dd1fd 100644 --- a/crates/environ/src/component/translate/adapt.rs +++ b/crates/environ/src/component/translate/adapt.rs @@ -211,7 +211,14 @@ impl<'data> Translator<'_, 'data> { let mut names = Vec::with_capacity(adapter_module.adapters.len()); for adapter in adapter_module.adapters.iter() { let name = format!("adapter{}", adapter.as_u32()); - module.adapt(&name, &component.adapters[*adapter]); + let adapter = &component.adapters[*adapter]; + module.adapt( + &name, + adapter, + component + .transparency + .adapter_is_transparent(self.types.types(), adapter), + ); names.push(name); } let wasm = module.encode(); diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 4fd2f4a55e9b..70a7793a079e 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -130,6 +130,7 @@ pub(super) fn run( // the root frame which are then used for recording the exports of the // component. inliner.result.num_runtime_component_instances += 1; + inliner.result.transparency.push_root_instance(index); let frame = InlinerFrame::new(index, result, ComponentClosure::default(), args, None); let resources_snapshot = types.resources_mut().clone(); let mut frames = vec![(frame, resources_snapshot)]; @@ -185,8 +186,8 @@ struct Inliner<'a> { /// incrementally processed via the `initializers` list here. Note that the /// inliner frames are stored on the heap to avoid recursion based on user /// input. -struct InlinerFrame<'a> { - instance: RuntimeComponentInstanceIndex, +pub(super) struct InlinerFrame<'a> { + pub(super) instance: RuntimeComponentInstanceIndex, /// The remaining initializers to process when instantiating this component. initializers: std::slice::Iter<'a, LocalInitializer<'a>>, @@ -215,7 +216,7 @@ struct InlinerFrame<'a> { modules: PrimaryMap>, // component model index spaces - component_funcs: PrimaryMap>, + pub(super) component_funcs: PrimaryMap>, module_instances: PrimaryMap>, component_instances: PrimaryMap>, components: PrimaryMap>, @@ -258,7 +259,7 @@ struct ComponentClosure<'a> { /// values and so this is used to ensure that we primarily only deal with /// individual functions and modules instead of synthetic instances. #[derive(Clone, PartialEq, Hash, Eq)] -struct ImportPath<'a> { +pub(super) struct ImportPath<'a> { index: ImportIndex, path: Vec>, } @@ -268,7 +269,7 @@ struct ImportPath<'a> { /// This is the "value" of an item defined within a component and is used to /// represent both imports and exports. #[derive(Clone)] -enum ComponentItemDef<'a> { +pub(super) enum ComponentItemDef<'a> { Component(ComponentDef<'a>), Instance(ComponentInstanceDef<'a>), Func(ComponentFuncDef<'a>), @@ -277,7 +278,7 @@ enum ComponentItemDef<'a> { } #[derive(Clone)] -enum ModuleDef<'a> { +pub(super) enum ModuleDef<'a> { /// A core wasm module statically defined within the original component. /// /// The `StaticModuleIndex` indexes into the `static_modules` map in the @@ -309,7 +310,7 @@ enum ModuleInstanceDef<'a> { } #[derive(Clone)] -enum ComponentFuncDef<'a> { +pub(super) enum ComponentFuncDef<'a> { /// A compile-time builtin intrinsic. UnsafeIntrinsic(UnsafeIntrinsic), @@ -328,7 +329,7 @@ enum ComponentFuncDef<'a> { } #[derive(Clone)] -enum ComponentInstanceDef<'a> { +pub(super) enum ComponentInstanceDef<'a> { /// The `__wasmtime_intrinsics` instance that exports all of our /// compile-time builtin intrinsics. Intrinsics, @@ -356,7 +357,7 @@ enum ComponentInstanceDef<'a> { } #[derive(Clone)] -struct ComponentDef<'a> { +pub(super) struct ComponentDef<'a> { index: StaticComponentIndex, closure: ComponentClosure<'a>, } @@ -437,6 +438,11 @@ impl<'a> Inliner<'a> { use LocalInitializer::*; let (frame, _) = frames.last_mut().unwrap(); + + self.result + .transparency + .process_initializer(types, frame, initializer); + match initializer { // When a component imports an item the actual definition of the // item is looked up here (not at runtime) via its name. The @@ -1297,13 +1303,18 @@ impl<'a> Inliner<'a> { self.result.num_runtime_component_instances, ); self.result.num_runtime_component_instances += 1; + let args = args + .iter() + .map(|(name, item)| Ok((*name, frame.item(*item, types)?))) + .collect::>>()?; + + self.result.transparency.push_instance(index, &args); + let frame = InlinerFrame::new( index, &self.nested_components[component.index], component.closure.clone(), - args.iter() - .map(|(name, item)| Ok((*name, frame.item(*item, types)?))) - .collect::>()?, + args, Some(*ty), ); return Ok(Some(frame)); diff --git a/crates/environ/src/component/translate/thread_transparency.rs b/crates/environ/src/component/translate/thread_transparency.rs new file mode 100644 index 000000000000..4ee713f1782a --- /dev/null +++ b/crates/environ/src/component/translate/thread_transparency.rs @@ -0,0 +1,231 @@ +//! Static analysis of "thread transparency" for fused adapters. +//! +//! Every fused adapter brackets its callee with `enter-sync-call`/ +//! `exit-sync-call` calls, which preserve the caller's task state and create +//! the callee's task state (`current_thread`, context slots, etc...). However, +//! we call an adapter that statically cannot call anything that accesses this +//! task state *thread-transparent*, and we can entirely avoid the task state +//! save/creation/restore for these adapters. +//! +//! The analysis is performed at the granularity of component instances and +//! their `canon lower`s: if an instance doesn't `canon lower` anything that +//! reads or writes that task state, then it cannot access that task state and +//! adapters calling into it are transparent. + +use crate::component::translate::inline::{ + ComponentFuncDef, ComponentInstanceDef, ComponentItemDef, InlinerFrame, +}; +use crate::component::translate::*; +use cranelift_entity::EntitySet; + +/// Results of the analysis, built by the inliner and queried by +/// `ThreadTransparency::adapter_is_transparent` when adapter modules are generated. +#[derive(Default)] +pub struct ThreadTransparency { + /// The instances known to be transparent; an absent instance is implicitly + /// opaque. + instances: EntitySet, +} + +impl ThreadTransparency { + /// Registers the root component instance as `instance`. + /// + /// The root starts out transparent: its host imports only disqualify it at + /// the point where they are actually `canon lower`ed, which + /// `process_initializer` takes care of. + pub(super) fn push_root_instance(&mut self, instance: RuntimeComponentInstanceIndex) { + self.instances.insert(instance); + } + + /// Registers as `instance` a component instance being instantiated with `args`. + /// + /// Such an instance is only transparent if all of its instantiation + /// arguments are; anything else it does that's disqualifying is noticed as + /// its own initializers are processed. + pub(super) fn push_instance( + &mut self, + instance: RuntimeComponentInstanceIndex, + args: &HashMap<&str, ComponentItemDef<'_>>, + ) { + if args.values().all(|item| self.item_def_is_transparent(item)) { + self.instances.insert(instance); + } + } + + /// Records the effect of `frame`'s component instance processing `init`. + pub(super) fn process_initializer( + &mut self, + types: &ComponentTypesBuilder, + frame: &InlinerFrame<'_>, + init: &LocalInitializer<'_>, + ) { + if self.initializer_is_opaque(types, frame, init) { + self.instances.remove(frame.instance); + } + } + + /// Returns whether `adapter` can omit its `{enter,exit}-sync-call` window. + pub fn adapter_is_transparent(&self, types: &ComponentTypesBuilder, adapter: &Adapter) -> bool { + Self::signature_is_transparent(types, adapter) + && self.instances.contains(adapter.lift_options.instance) + } + + fn signature_is_transparent(types: &ComponentTypesBuilder, adapter: &Adapter) -> bool { + // Async adapters really do need task state. + if adapter.lift_options.async_ + || adapter.lower_options.async_ + || types[adapter.lift_ty].async_ + || types[adapter.lower_ty].async_ + { + return false; + } + + // Transferring handles across instances unconditionally requires task + // state for now. + if types.func_contains_any_handle(adapter.lift_ty) + || types.func_contains_any_handle(adapter.lower_ty) + { + return false; + } + + true + } + + fn initializer_is_opaque( + &self, + types: &ComponentTypesBuilder, + frame: &InlinerFrame<'_>, + init: &LocalInitializer<'_>, + ) -> bool { + use LocalInitializer::*; + // NB: This `match` is deliberately exhaustive so that a new canon must + // be classified here rather than silently defaulting to transparent. + match init { + Lower { func, .. } => { + let def = &frame.component_funcs[*func]; + + // At least as opaque as the function being made callable. + !self.func_def_is_transparent(def) + // Calling an `async`-typed lift from a sync-typed call + // needs the callee's thread state: the callee may block, + // in which case the scheduler has to find the sync-typed + // call in progress on this instance. + || matches!(def, ComponentFuncDef::Lifted { ty, .. } if types[*ty].async_) + } + + // These make no intrinsic callable from this instance's core Wasm. + Import(..) + | IntrinsicsImport + | Lift(..) + | ModuleStatic(..) + | ModuleInstantiate(..) + | ModuleSynthetic(..) + | ComponentStatic(..) + | ComponentSynthetic(..) + | AliasExportFunc(..) + | AliasExportTable(..) + | AliasExportGlobal(..) + | AliasExportMemory(..) + | AliasExportTag(..) + | AliasComponentExport(..) + | AliasModule(..) + | AliasComponent(..) + | Resource(..) + | Export(..) => false, + + // `ComponentInstantiate` also does not make any intrinsic directly + // callable from this instance's core Wasm: it is reachable only + // through a separate adapter, which may or may not itself be + // transparent, but it doesn't affect this instance's transparency. + ComponentInstantiate(..) => false, + + // Each of these `canon` intrinsics forces the `VMDeferredThread`, + // mutates backpressure, or touches the current context slots. + ResourceNew(..) + | ResourceRep(..) + | ResourceDrop(..) + | BackpressureInc { .. } + | BackpressureDec { .. } + | TaskReturn { .. } + | TaskCancel { .. } + | WaitableSetNew { .. } + | WaitableSetWait { .. } + | WaitableSetPoll { .. } + | WaitableSetDrop { .. } + | WaitableJoin { .. } + | SubtaskDrop { .. } + | SubtaskCancel { .. } + | StreamNew { .. } + | StreamRead { .. } + | StreamWrite { .. } + | StreamCancelRead { .. } + | StreamCancelWrite { .. } + | StreamDropReadable { .. } + | StreamDropWritable { .. } + | FutureNew { .. } + | FutureRead { .. } + | FutureWrite { .. } + | FutureCancelRead { .. } + | FutureCancelWrite { .. } + | FutureDropReadable { .. } + | FutureDropWritable { .. } + | ErrorContextNew { .. } + | ErrorContextDebugMessage { .. } + | ErrorContextDrop { .. } + | ContextGet { .. } + | ContextSet { .. } + | ThreadIndex { .. } + | ThreadNewIndirect { .. } + | ThreadResumeLater { .. } + | ThreadSuspend { .. } + | ThreadYield { .. } + | ThreadSuspendThenResume { .. } + | ThreadYieldThenResume { .. } + | ThreadSuspendThenPromote { .. } + | ThreadYieldThenPromote { .. } => true, + } + } + + fn item_def_is_transparent(&self, def: &ComponentItemDef<'_>) -> bool { + match def { + ComponentItemDef::Func(func) => self.func_def_is_transparent(func), + ComponentItemDef::Instance(instance) => self.instance_def_is_transparent(instance), + ComponentItemDef::Module(_) + | ComponentItemDef::Type(_) + | ComponentItemDef::Component(_) => true, + } + } + + fn func_def_is_transparent(&self, def: &ComponentFuncDef<'_>) -> bool { + match def { + // Goes through an adapter, which saves/restores its own state if + // needed, but doesn't affect this adapter. + ComponentFuncDef::Lifted { .. } => true, + + // A host import can do anything at all. + ComponentFuncDef::Import(_) => false, + + // Transparent except `context.{get,set}`. + ComponentFuncDef::UnsafeIntrinsic(intrinsic) => !matches!( + intrinsic, + UnsafeIntrinsic::ContextGetI32_0 + | UnsafeIntrinsic::ContextSetI32_0 + | UnsafeIntrinsic::ContextGetI32_1 + | UnsafeIntrinsic::ContextSetI32_1 + ), + } + } + + fn instance_def_is_transparent(&self, def: &ComponentInstanceDef<'_>) -> bool { + match def { + // A host import can do anything at all. + ComponentInstanceDef::Import(..) => false, + + ComponentInstanceDef::Intrinsics => true, + + ComponentInstanceDef::Items(items, _) => items + .values() + .all(|(def, _)| self.item_def_is_transparent(def)), + } + } +} diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index cb36353aeb61..940f7e95f471 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -828,6 +828,24 @@ impl ComponentTypesBuilder { self.type_information(ty).has_borrow } + /// Returns whether the type specified contains any handle within it, where + /// "handle" means `own`, `borrow`, `future`, `stream`, or `error-context`. + fn ty_contains_any_handle(&self, ty: &InterfaceType) -> bool { + self.type_information(ty).has_handle + } + + /// Returns whether the signature of `ty` mentions any handle, in either its + /// parameters or its results. + pub fn func_contains_any_handle(&self, ty: TypeFuncIndex) -> bool { + let ty = &self[ty]; + let params = &self[ty.params].types; + let results = &self[ty.results].types; + params + .iter() + .chain(results.iter()) + .any(|ty| self.ty_contains_any_handle(ty)) + } + fn type_information(&self, ty: &InterfaceType) -> &TypeInformation { match ty { InterfaceType::U8 @@ -837,18 +855,26 @@ impl ComponentTypesBuilder { | InterfaceType::S16 | InterfaceType::U32 | InterfaceType::S32 - | InterfaceType::Char - | InterfaceType::Own(_) + | InterfaceType::Char => { + static INFO: TypeInformation = TypeInformation::primitive(FlatType::I32); + &INFO + } + InterfaceType::Own(_) | InterfaceType::Future(_) | InterfaceType::Stream(_) | InterfaceType::ErrorContext(_) => { - static INFO: TypeInformation = TypeInformation::primitive(FlatType::I32); + static INFO: TypeInformation = { + let mut info = TypeInformation::primitive(FlatType::I32); + info.has_handle = true; + info + }; &INFO } InterfaceType::Borrow(_) => { static INFO: TypeInformation = { let mut info = TypeInformation::primitive(FlatType::I32); info.has_borrow = true; + info.has_handle = true; info }; &INFO @@ -995,7 +1021,14 @@ struct TypeInformationCache { struct TypeInformation { flat: FlatTypesStorage, + + /// Whether this type contains a borrow anywhere within it. has_borrow: bool, + + /// Whether this type contains any handle (`own`, `borrow`, `future`, + /// `stream`, or `error-context`) anywhere within it. Note that this is a + /// superset of `has_borrow`. + has_handle: bool, } impl TypeInformation { @@ -1003,6 +1036,7 @@ impl TypeInformation { TypeInformation { flat: FlatTypesStorage::new(), has_borrow: false, + has_handle: false, } } @@ -1029,6 +1063,7 @@ impl TypeInformation { fn build_record<'a>(&mut self, types: impl Iterator) { for info in types { self.has_borrow = self.has_borrow || info.has_borrow; + self.has_handle = self.has_handle || info.has_handle; match info.flat.as_flat_types() { Some(types) => { for (t32, t64) in types.memory32.iter().zip(types.memory64) { @@ -1070,6 +1105,7 @@ impl TypeInformation { None => continue, }; self.has_borrow = self.has_borrow || info.has_borrow; + self.has_handle = self.has_handle || info.has_handle; // If this variant is already unrepresentable in a flat // representation then this can be skipped. @@ -1135,6 +1171,7 @@ impl TypeInformation { fn fixed_length_lists(&mut self, types: &ComponentTypesBuilder, ty: &TypeFixedLengthList) { let element_info = types.type_information(&ty.element); self.has_borrow = element_info.has_borrow; + self.has_handle = element_info.has_handle; match element_info.flat.as_flat_types() { Some(types) => { 'outer: for _ in 0..ty.size { @@ -1190,6 +1227,7 @@ impl TypeInformation { *self = TypeInformation::string(); let info = types.type_information(&ty.element); self.has_borrow = info.has_borrow; + self.has_handle = info.has_handle; } fn maps(&mut self, types: &ComponentTypesBuilder, ty: &TypeMap) { @@ -1199,5 +1237,6 @@ impl TypeInformation { let key_info = types.type_information(&ty.key); let value_info = types.type_information(&ty.value); self.has_borrow = key_info.has_borrow || value_info.has_borrow; + self.has_handle = key_info.has_handle || value_info.has_handle; } } diff --git a/crates/environ/src/fact.rs b/crates/environ/src/fact.rs index 650647decc0a..10614dbb7489 100644 --- a/crates/environ/src/fact.rs +++ b/crates/environ/src/fact.rs @@ -125,6 +125,12 @@ struct AdapterData { /// The core wasm function that this adapter will be calling (the original /// function that was `canon lift`'d) callee: FuncIndex, + /// Whether nothing this adapter can reach is able to observe or mutate the + /// thread state that `enter-sync-call`/`exit-sync-call` maintain, meaning + /// that pair can be omitted entirely. + /// + /// See `component::translate::ThreadTransparency` for details. + thread_transparent: bool, } /// Configuration options which apply at the "global adapter" level. @@ -300,7 +306,11 @@ impl<'a> Module<'a> { /// /// The `name` provided is the export name of the adapter from the final /// module, and `adapter` contains all metadata necessary for compilation. - pub fn adapt(&mut self, name: &str, adapter: &Adapter) { + /// + /// The `thread_transparent` flag indicates that nothing this adapter can + /// reach is able to observe the thread state that `enter-sync-call` and + /// `exit-sync-call` maintain, allowing that pair to be skipped. + pub fn adapt(&mut self, name: &str, adapter: &Adapter, thread_transparent: bool) { // Import any items required by the various canonical options // (memories, reallocs, etc) let mut lift = self.import_options(adapter.lift_ty, &adapter.lift_options); @@ -335,6 +345,7 @@ impl<'a> Module<'a> { lift, lower, callee, + thread_transparent, }, ); @@ -722,7 +733,7 @@ impl<'a> Module<'a> { self.import_simple( "async", "enter-sync-call", - &[ValType::I32; 3], + &[ValType::I32; 2], &[], Import::EnterSyncCall, |me| &mut me.imported_enter_sync_call, diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index f55b4ccf09ed..17139d41f340 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -756,7 +756,19 @@ impl<'a, 'b> Compiler<'a, 'b> { let saved_lower_may_leave = self.trap_if_not_may_leave(adapter.lower.flags, Trap::CannotLeaveComponent); - if self.module.tunables.concurrency_support { + // If nothing that this adapter can reach is able to observe or mutate + // the thread state that `enter-sync-call`/`exit-sync-call` maintain then + // none of its bookkeeping is necessary. + // + // See `component::translate::ThreadTransparency` for details. + debug_assert!( + !(adapter.thread_transparent && self.emit_resource_call), + "resources are not thread transparent", + ); + let needs_thread_state = + self.module.tunables.concurrency_support && !adapter.thread_transparent; + + if needs_thread_state { // Push a task onto the current task stack. // // Note that for sync-to-sync calls, we replace this call with @@ -765,9 +777,6 @@ impl<'a, 'b> Compiler<'a, 'b> { // adapter for most sync-to-sync calls, since most sync-to-sync // calls do not do anything to force the task's creation // (e.g. adjust backpressure). - self.instruction(I32Const( - i32::try_from(adapter.lower.instance.as_u32()).unwrap(), - )); self.instruction(I32Const(if self.types[adapter.lift.ty].async_ { 1 } else { @@ -780,9 +789,6 @@ impl<'a, 'b> Compiler<'a, 'b> { self.instruction(Call(enter_sync_call.as_u32())); } else if self.emit_resource_call { assert!(!self.types[adapter.lift.ty].async_); - self.instruction(I32Const( - i32::try_from(adapter.lower.instance.as_u32()).unwrap(), - )); self.instruction(I32Const(0)); self.instruction(I32Const( i32::try_from(adapter.lift.instance.as_u32()).unwrap(), @@ -875,7 +881,7 @@ impl<'a, 'b> Compiler<'a, 'b> { // Note that for sync-to-sync calls, we will emit inline code during // translation to CLIF to avoid actually calling out to a libcall when // the deferred task's allocation was never forced. - if self.emit_resource_call || self.module.tunables.concurrency_support { + if self.emit_resource_call || needs_thread_state { let exit_sync_call = self.module.import_exit_sync_call(); self.instruction(Call(exit_sync_call.as_u32())); } diff --git a/crates/environ/src/vmtypes.rs b/crates/environ/src/vmtypes.rs index e6eadf1d4a83..441e2c32bd85 100644 --- a/crates/environ/src/vmtypes.rs +++ b/crates/environ/src/vmtypes.rs @@ -534,8 +534,6 @@ macro_rules! for_each_vm_type { pub struct VMDeferredThread { /// The previous value of `VMStoreContext::current_thread`. pub parent: VMLazyThread, - /// The caller component instance (a deferred `enter_sync_call` argument). - pub caller_instance: u32, /// Whether the callee is async-lifted (a deferred `enter_sync_call` arg). pub callee_async: u32, /// The callee component instance (a deferred `enter_sync_call` argument). diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index ce8e46eb6488..16a5327e67ac 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1640,6 +1640,18 @@ impl StoreContextMut<'_, T> { /// Tasks are yielded "youngest first" where the first item in the iterator /// is the current task, and the last item in the iterator is the original /// call. + /// + /// # Omitted Tasks + /// + /// This stack might not contain an entry for every guest-to-guest component + /// call that is currently executing: if those guest-to-guest calls are not + /// asynchronous and provably cannot access async task state, then Wasmtime + /// can avoid materializing the guest task state for that call, and such + /// tasks will not have a `GuestTaskId` and will not appear in this stack + /// trace. + /// + /// The `GuestTaskId`s for host-to-guest and guest-to-host calls, however, + /// will always appear in this stack trace. pub fn async_call_stack(&mut self) -> Result> { let mut cur = Some(self.0.current_thread()?); let state = self.0.concurrent_state_mut()?; @@ -1767,7 +1779,7 @@ impl StoreOpaque { instance: id, index: RuntimeComponentInstanceIndex::from_u32(callee_instance), }; - self.enter_guest_sync_call(None, callee_async, callee)?; + self.enter_guest_sync_call(callee_async, callee)?; } // Replaying done; restore the current context. @@ -1846,7 +1858,6 @@ impl StoreOpaque { /// sync! pub(crate) fn enter_guest_sync_call( &mut self, - guest_caller: Option, callee_async_typed: bool, callee: RuntimeInstance, ) -> Result<()> { @@ -1857,14 +1868,6 @@ impl StoreOpaque { let thread = self.current_thread()?; let state = self.concurrent_state_mut()?; - let instance = if let Some(task) = thread.guest_task() { - Some(state.get_mut(task)?.instance) - } else { - None - }; - if guest_caller.is_some() { - debug_assert_eq!(instance, guest_caller); - } let guest_thread = GuestTask::new( state, Box::new(move |_, _| bail_bug!("cannot lower params in sync call")), diff --git a/crates/wasmtime/src/runtime/component/concurrent_disabled.rs b/crates/wasmtime/src/runtime/component/concurrent_disabled.rs index 9be79a482b0b..2ad8e26a4c74 100644 --- a/crates/wasmtime/src/runtime/component/concurrent_disabled.rs +++ b/crates/wasmtime/src/runtime/component/concurrent_disabled.rs @@ -151,7 +151,6 @@ unsafe impl Lower for StreamAny { impl StoreOpaque { pub(crate) fn enter_guest_sync_call( &mut self, - _guest_caller: Option, _callee_async: bool, _callee: RuntimeInstance, ) -> Result<()> { diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index a9cf79707feb..8d0eb34cf598 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -469,7 +469,7 @@ impl Func { bail!(crate::Trap::CannotEnterComponent); } - store.0.enter_guest_sync_call(None, async_, instance)?; + store.0.enter_guest_sync_call(async_, instance)?; #[repr(C)] union Union { diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 8bf24920e9fb..548024c22b84 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -817,7 +817,6 @@ impl<'a> Instantiator<'a> { let exit = if let Some(component_instance) = *component_instance { store.0.enter_guest_sync_call( - None, false, RuntimeInstance { instance, diff --git a/crates/wasmtime/src/runtime/component/resources/any.rs b/crates/wasmtime/src/runtime/component/resources/any.rs index 0cfb1220f1ed..8dda3dc2d408 100644 --- a/crates/wasmtime/src/runtime/component/resources/any.rs +++ b/crates/wasmtime/src/runtime/component/resources/any.rs @@ -218,7 +218,7 @@ impl ResourceAny { // means that this is a host resource being destroyed by the host. In // that case restrictions around blocking and such are exempt. if let Some(instance) = slot.instance { - store.0.enter_guest_sync_call(None, false, instance)?; + store.0.enter_guest_sync_call(false, instance)?; } // This should be safe because `dtor` has been checked to belong to the diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 18b881a2e27c..295b253b7595 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -671,12 +671,10 @@ fn resource_transfer_borrow( fn enter_sync_call( store: &mut dyn VMStore, instance: Instance, - caller_instance: u32, callee_async: u32, callee_instance: u32, ) -> Result<()> { store.enter_guest_sync_call( - Some(instance.runtime_instance(RuntimeComponentInstanceIndex::from_u32(caller_instance))), callee_async != 0, instance.runtime_instance(RuntimeComponentInstanceIndex::from_u32(callee_instance)), ) diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs index b3ad7b4f8973..446d0b31f20a 100644 --- a/tests/all/component_model/async.rs +++ b/tests/all/component_model/async.rs @@ -1063,6 +1063,103 @@ async fn async_call_stack() -> Result<()> { Ok(()) } +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn async_call_stack_omits_transparent_adapters() -> Result<()> { + async fn call_stack(taint: bool) -> Result<(Vec, GuestTaskId)> { + let mut config = Config::new(); + config.wasm_component_model_async(true); + let engine = Engine::new(&config)?; + + let taint = if taint { + "(core func $unused (canon context.get i32 0))" + } else { + "" + }; + + let component = Component::new( + &engine, + &format!( + r#" + (component + (import "a" (func $a)) + + ;; Lowers the host import, so this is never thread-transparent. + (component $Deep + (import "a" (func $a)) + (core func $a (canon lower (func $a))) + (core module $m + (import "" "a" (func $a)) + (func (export "a") call $a)) + (core instance $m (instantiate $m + (with "" (instance (export "a" (func $a)))))) + (func (export "a") (canon lift (core func $m "a"))) + ) + + ;; Declares nothing but a `canon lower` of an imported *lifted*, + ;; non-`async` function, so an instance of this component is + ;; thread-transparent -- unless the taint below is present. + (component $Mid + (import "a" (func $a)) + {taint} + (core func $a (canon lower (func $a))) + (core module $m + (import "" "a" (func $a)) + (func (export "a") call $a)) + (core instance $m (instantiate $m + (with "" (instance (export "a" (func $a)))))) + (func (export "a") (canon lift (core func $m "a"))) + ) + + (instance $deep (instantiate $Deep (with "a" (func $a)))) + (instance $mid (instantiate $Mid (with "a" (func $deep "a")))) + (instance $top (instantiate $Mid (with "a" (func $mid "a")))) + (export "a" (func $top "a")) + ) + "# + ), + )?; + + let mut linker = Linker::new(&engine); + linker.root().func_wrap( + "a", + |mut store: StoreContextMut>>, (): ()| { + let stack = store.async_call_stack()?.collect::>(); + *store.data_mut() = Some(stack); + Ok(()) + }, + )?; + + let mut store = Store::new(&engine, None); + let instance = linker.instantiate_async(&mut store, &component).await?; + let func = instance.get_typed_func::<(), ()>(&mut store, "a")?; + + let call = func.start_call_concurrent(&mut store, ())?; + let root = call.task(); + store + .run_concurrent(async |store| func.finish_call_concurrent(store, call).await) + .await??; + + Ok((store.data_mut().take().unwrap(), root)) + } + + // With the intermediate instances tainted into opacity, all three + // guest-to-guest calls materialize a task and all three show up. + let (stack, root) = call_stack(true).await?; + assert_eq!(stack.len(), 3); + assert_eq!(stack.last(), Some(&root)); + + // Without the taint, `$top -> $mid` is a thread-transparent call, its task + // is never materialized, and so it is omitted from the stack. The root + // host-to-guest call is always materialized, so it is still reported, and + // `$mid -> $deep` still is too since `$deep` is opaque. + let (stack, root) = call_stack(false).await?; + assert_eq!(stack.len(), 2); + assert_eq!(stack.last(), Some(&root)); + + Ok(()) +} + #[test] #[cfg_attr(miri, ignore)] fn inter_component_stream_is_not_intra_component() -> Result<()> { diff --git a/tests/all/component_model/sync_call_inline.rs b/tests/all/component_model/sync_call_inline.rs index fe2130b8be1d..c807db21db57 100644 --- a/tests/all/component_model/sync_call_inline.rs +++ b/tests/all/component_model/sync_call_inline.rs @@ -267,6 +267,287 @@ async fn repeated_calls_have_no_state_leak() -> Result<()> { Ok(()) } +#[tokio::test] +async fn transparent_frame_nested_inside_opaque_one() -> Result<()> { + let component = r#" +(component + (import "poke" (func $poke)) + + (component $Leaf + (core module $M + (func (export "leaf'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 1)))) + (core instance $m (instantiate $M)) + (func (export "leaf") (param "x" u32) (result u32) + (canon lift (core func $m "leaf'")))) + + (component $Mid + (import "leaf" (func $leaf (param "x" u32) (result u32))) + (import "poke" (func $poke)) + (core func $leaf' (canon lower (func $leaf))) + (core func $poke' (canon lower (func $poke))) + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + (core module $M + (import "" "leaf'" (func $leaf' (param i32) (result i32))) + (import "" "poke" (func $poke')) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func (export "mid'") (param i32) (result i32) (local $r i32) + ;; Freshly entered deferred thread: context starts zeroed. + (if (i32.ne (call $cget) (i32.const 0)) (then unreachable)) + (call $cset (i32.const 0x0d00d100)) + ;; Call through the transparent adapter, which pushes no frame. + (local.set $r (call $leaf' (local.get 0))) + ;; Only now force the deferred thread, after the omitted frame has + ;; come and gone. + (call $poke') + (if (i32.ne (call $cget) (i32.const 0x0d00d100)) (then unreachable)) + (i32.add (local.get $r) (i32.const 10)))) + (core instance $m (instantiate $M (with "" (instance + (export "leaf'" (func $leaf')) + (export "poke" (func $poke')) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + (func (export "mid") (param "x" u32) (result u32) + (canon lift (core func $m "mid'")))) + + (component $Root + (import "mid" (func $mid (param "x" u32) (result u32))) + (core func $mid' (canon lower (func $mid))) + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + (core module $M + (import "" "mid'" (func $mid' (param i32) (result i32))) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func (export "root'") (result i32) (local $r i32) + (call $cset (i32.const 0x0badf00d)) + (local.set $r (call $mid' (i32.const 100))) + (if (i32.ne (call $cget) (i32.const 0x0badf00d)) (then unreachable)) + (i32.add (local.get $r) (i32.const 1000)))) + (core instance $m (instantiate $M (with "" (instance + (export "mid'" (func $mid')) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + (func (export "root") (result u32) + (canon lift (core func $m "root'")))) + + (instance $leaf (instantiate $Leaf)) + (instance $mid (instantiate $Mid + (with "leaf" (func $leaf "leaf")) + (with "poke" (func $poke)))) + (instance $root (instantiate $Root (with "mid" (func $mid "mid")))) + (export "root" (func $root "root")) +) + "#; + + let engine = engine(); + let component = Component::new(&engine, component)?; + let mut store = Store::new(&engine, 0u32); + let mut linker = Linker::new(&engine); + linker + .root() + .func_wrap("poke", |mut cx: StoreContextMut, (): ()| { + *cx.data_mut() += 1; + Ok(()) + })?; + let instance = linker.instantiate_async(&mut store, &component).await?; + let root = instance.get_typed_func::<(), (u32,)>(&mut store, "root")?; + + // Call more than once so that any state the omitted frame failed to clean + // up would be visible on a later call. + for i in 1..=3 { + let (result,) = root.call_async(&mut store, ()).await?; + assert_eq!(result, 1111); + assert_eq!(*store.data(), i); + } + Ok(()) +} + +#[tokio::test] +async fn opaque_frame_nested_inside_transparent_one() -> Result<()> { + let component = r#" +(component + (import "poke" (func $poke)) + + (component $Leaf + (import "poke" (func $poke)) + (core func $poke' (canon lower (func $poke))) + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + (core module $M + (import "" "poke" (func $poke')) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func (export "leaf'") (param i32) (result i32) + ;; The maintained frame zeroed the context on the way in. + (if (i32.ne (call $cget) (i32.const 0)) (then unreachable)) + (call $cset (i32.const 0x0feed000)) + ;; Force the deferred thread from the innermost frame. + (call $poke') + (if (i32.ne (call $cget) (i32.const 0x0feed000)) (then unreachable)) + (i32.add (local.get 0) (i32.const 1)))) + (core instance $m (instantiate $M (with "" (instance + (export "poke" (func $poke')) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + (func (export "leaf") (param "x" u32) (result u32) + (canon lift (core func $m "leaf'")))) + + ;; Nothing in here can observe or mutate thread state, so the adapter that + ;; lifts `mid` is thread-transparent. + (component $Mid + (import "leaf" (func $leaf (param "x" u32) (result u32))) + (core func $leaf' (canon lower (func $leaf))) + (core module $M + (import "" "leaf'" (func $leaf' (param i32) (result i32))) + (func (export "mid'") (param i32) (result i32) + (i32.add (call $leaf' (local.get 0)) (i32.const 10)))) + (core instance $m (instantiate $M (with "" (instance + (export "leaf'" (func $leaf')))))) + (func (export "mid") (param "x" u32) (result u32) + (canon lift (core func $m "mid'")))) + + (component $Root + (import "mid" (func $mid (param "x" u32) (result u32))) + (core func $mid' (canon lower (func $mid))) + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + (core module $M + (import "" "mid'" (func $mid' (param i32) (result i32))) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func (export "root'") (result i32) (local $r i32) + (call $cset (i32.const 0x0badf00d)) + (local.set $r (call $mid' (i32.const 100))) + ;; Neither `$Mid` nor `$Leaf` disturbed our slot. + (if (i32.ne (call $cget) (i32.const 0x0badf00d)) (then unreachable)) + (i32.add (local.get $r) (i32.const 1000)))) + (core instance $m (instantiate $M (with "" (instance + (export "mid'" (func $mid')) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + (func (export "root") (result u32) + (canon lift (core func $m "root'")))) + + (instance $leaf (instantiate $Leaf (with "poke" (func $poke)))) + (instance $mid (instantiate $Mid (with "leaf" (func $leaf "leaf")))) + (instance $root (instantiate $Root (with "mid" (func $mid "mid")))) + (export "root" (func $root "root")) +) + "#; + + let engine = engine(); + let component = Component::new(&engine, component)?; + let mut store = Store::new(&engine, 0u32); + let mut linker = Linker::new(&engine); + linker + .root() + .func_wrap("poke", |mut cx: StoreContextMut, (): ()| { + *cx.data_mut() += 1; + Ok(()) + })?; + let instance = linker.instantiate_async(&mut store, &component).await?; + let root = instance.get_typed_func::<(), (u32,)>(&mut store, "root")?; + + for i in 1..=3 { + let (result,) = root.call_async(&mut store, ()).await?; + assert_eq!(result, 1111); + assert_eq!(*store.data(), i); + } + Ok(()) +} + +#[tokio::test] +async fn trap_unwinds_through_transparent_frames() -> Result<()> { + let component = r#" +(component + (import "poke" (func $poke)) + + (component $Leaf + (core module $M + (func (export "leaf'") (param i32) (result i32) unreachable)) + (core instance $m (instantiate $M)) + (func (export "leaf") (param "x" u32) (result u32) + (canon lift (core func $m "leaf'")))) + + (component $Mid + (import "leaf" (func $leaf (param "x" u32) (result u32))) + (import "poke" (func $poke)) + (core func $leaf' (canon lower (func $leaf))) + (core func $poke' (canon lower (func $poke))) + (core func $cset (canon context.set i32 0)) + (core module $M + (import "" "leaf'" (func $leaf' (param i32) (result i32))) + (import "" "poke" (func $poke')) + (import "" "cset" (func $cset (param i32))) + (func (export "mid'") (param i32) (result i32) + (call $cset (i32.const 0x0d00d100)) + ;; Force the deferred thread first so that the unwind below has a real + ;; task to tear down rather than just a stack slot. + (call $poke') + (call $leaf' (local.get 0)))) + (core instance $m (instantiate $M (with "" (instance + (export "leaf'" (func $leaf')) + (export "poke" (func $poke')) + (export "cset" (func $cset)))))) + (func (export "mid") (param "x" u32) (result u32) + (canon lift (core func $m "mid'")))) + + (component $Root + (import "mid" (func $mid (param "x" u32) (result u32))) + (core func $mid' (canon lower (func $mid))) + (core module $M + (import "" "mid'" (func $mid' (param i32) (result i32))) + (func (export "root'") (result i32) (call $mid' (i32.const 1)))) + (core instance $m (instantiate $M (with "" (instance + (export "mid'" (func $mid')))))) + (func (export "root") (result u32) + (canon lift (core func $m "root'")))) + + (instance $leaf (instantiate $Leaf)) + (instance $mid (instantiate $Mid + (with "leaf" (func $leaf "leaf")) + (with "poke" (func $poke)))) + (instance $root (instantiate $Root (with "mid" (func $mid "mid")))) + (export "root" (func $root "root")) +) + "#; + + let other = r#" +(component + (core module $m (func (export "x"))) + (core instance (instantiate $m)) +) + "#; + + let engine = engine(); + let component = Component::new(&engine, component)?; + let other = Component::new(&engine, other)?; + let mut store = Store::new(&engine, 0u32); + let mut linker = Linker::new(&engine); + linker + .root() + .func_wrap("poke", |mut cx: StoreContextMut, (): ()| { + *cx.data_mut() += 1; + Ok(()) + })?; + let instance = linker.instantiate_async(&mut store, &component).await?; + let root = instance.get_typed_func::<(), (u32,)>(&mut store, "root")?; + + let err = root.call_async(&mut store, ()).await.unwrap_err(); + assert!( + err.downcast_ref::().is_some(), + "expected a trap, got: {err:?}" + ); + assert_eq!(*store.data(), 1); + + // The store is still usable afterwards. + let _ = linker.instantiate_async(&mut store, &other).await?; + Ok(()) +} + #[tokio::test] async fn trap_then_instantiate_uses_freed_deferred_thread() -> Result<()> { let trapping = [ @@ -456,3 +737,167 @@ async fn trap_then_instantiate_uses_freed_deferred_thread() -> Result<()> { } Ok(()) } + +#[tokio::test] +async fn shared_table_does_not_leak_context() -> Result<()> { + // The `$Victim`/`$Evil` pair, parameterized over which core module owns the + // shared table and what else `$Victim` imports. + fn component(inner: &str) -> String { + format!( + r#" +(component + (component $Inner {inner}) + + (component $Outer + (import "f" (func $f (result u32))) + (core func $f' (canon lower (func $f))) + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + (core module $N + (import "" "f" (func $f' (result i32))) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + ;; What the callee saw in *our* slot. + (func (export "g") (result i32) + (call $cset (i32.const 0x1234)) + (call $f')) + ;; Our slot after the call returns. + (func (export "h") (result i32) + (call $cset (i32.const 0x1234)) + (drop (call $f')) + (call $cget))) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + (func (export "h") (result u32) (canon lift (core func $n "h")))) + + (instance $inner (instantiate $Inner)) + (instance $outer (instantiate $Outer (with "f" (func $inner "f")))) + (export "g" (func $outer "g")) + (export "h" (func $outer "h")) +) + "# + ) + } + + // `$Evil`'s body: read the caller's slot, clobber it, return what was read. + // The element segment writes into the *imported* table, so this happens + // during instantiation with no start function or host call involved. + const EVIL: &str = r#" + (core module $Evil + (import "" "t" (table 1 funcref)) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func $leak (result i32) (local $t i32) + (local.set $t (call $cget)) + (call $cset (i32.const 0x9999)) + (local.get $t)) + (elem (table 0) (i32.const 0) func $leak)) + "#; + + let cases = [ + // The table is defined by a third module that imports nothing at all. + component(&format!( + r#" + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + + (core module $Shared (table (export "t") 1 funcref)) + (core instance $shared (instantiate $Shared)) + + {EVIL} + (core instance $evil (instantiate $Evil (with "" (instance + (export "t" (table $shared "t")) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + + (core module $Victim + (import "" "t" (table 1 funcref)) + (type $sig (func (result i32))) + (func (export "f") (result i32) + (call_indirect (type $sig) (i32.const 0)))) + (core instance $victim (instantiate $Victim (with "" (instance + (export "t" (table $shared "t")))))) + + (func (export "f") (result u32) (canon lift (core func $victim "f"))) + "# + )), + // Same thing with only two core modules: the table is defined and + // exported by `$Victim` itself, which still imports nothing. + component(&format!( + r#" + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + + (core module $Victim + (table (export "t") 1 funcref) + (type $sig (func (result i32))) + (func (export "f") (result i32) + (call_indirect (type $sig) (i32.const 0)))) + (core instance $victim (instantiate $Victim)) + + {EVIL} + (core instance $evil (instantiate $Evil (with "" (instance + (export "t" (table $victim "t")) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + + (func (export "f") (result u32) (canon lift (core func $victim "f"))) + "# + )), + // Control: `$Victim` also directly imports `context.get`, which even a + // core-instance-granularity analysis would have seen. + component(&format!( + r#" + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + + (core module $Shared (table (export "t") 1 funcref)) + (core instance $shared (instantiate $Shared)) + + {EVIL} + (core instance $evil (instantiate $Evil (with "" (instance + (export "t" (table $shared "t")) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + + (core module $Victim + (import "" "t" (table 1 funcref)) + (import "" "cget" (func (result i32))) + (type $sig (func (result i32))) + (func (export "f") (result i32) + (call_indirect (type $sig) (i32.const 0)))) + (core instance $victim (instantiate $Victim (with "" (instance + (export "t" (table $shared "t")) + (export "cget" (func $cget)))))) + + (func (export "f") (result u32) (canon lift (core func $victim "f"))) + "# + )), + ]; + + let engine = engine(); + for (i, wat) in cases.iter().enumerate() { + let component = Component::new(&engine, wat)?; + let mut store = Store::new(&engine, ()); + let linker = Linker::new(&engine); + let instance = linker.instantiate_async(&mut store, &component).await?; + + let g = instance.get_typed_func::<(), (u32,)>(&mut store, "g")?; + let (leaked,) = g.call_async(&mut store, ()).await?; + assert_eq!( + leaked, 0, + "case {i}: callee read the caller's private context slot: {leaked:#x}", + ); + + let h = instance.get_typed_func::<(), (u32,)>(&mut store, "h")?; + let (after,) = h.call_async(&mut store, ()).await?; + assert_eq!( + after, 0x1234, + "case {i}: callee clobbered the caller's context slot: {after:#x}", + ); + } + Ok(()) +} diff --git a/tests/disas/component-model/sync-adapter-calls-x64.wat b/tests/disas/component-model/sync-adapter-calls-x64.wat index 49e9ec05debd..1511868ce15c 100644 --- a/tests/disas/component-model/sync-adapter-calls-x64.wat +++ b/tests/disas/component-model/sync-adapter-calls-x64.wat @@ -54,39 +54,13 @@ ;; wasm[1]::function[1]: ;; pushq %rbp ;; movq %rsp, %rbp -;; movq 8(%rdi), %r10 -;; movq 0x18(%r10), %r10 -;; addq $0x20, %r10 -;; cmpq %rsp, %r10 -;; ja 0xd2 -;; 39: subq $0x20, %rsp -;; movq 0x48(%rdi), %rdx -;; movq 0xe8(%rdx), %rax -;; movl (%rax), %ecx -;; testl %ecx, %ecx -;; je 0xd4 -;; 52: movq 8(%rdx), %rdx -;; movq 0x88(%rdx), %rsi -;; leaq (%rsp), %r8 -;; movq %rsi, (%rsp) -;; movl $2, 8(%rsp) -;; movl $0, 0xc(%rsp) -;; movl $1, 0x10(%rsp) -;; movl 0x80(%rdx), %edi -;; movl %edi, 0x14(%rsp) -;; movl $0, 0x80(%rdx) -;; movl 0x84(%rdx), %r9d -;; movl %r9d, 0x18(%rsp) -;; movl $0, 0x84(%rdx) -;; movq %r8, 0x88(%rdx) -;; movq %rsi, 0x88(%rdx) -;; movl %edi, 0x80(%rdx) -;; movl %r9d, 0x84(%rdx) -;; movl %ecx, (%rax) -;; movl $0x4fc, %eax -;; addq $0x20, %rsp +;; movq 0x48(%rdi), %rdi +;; movq 0xa8(%rdi), %rdi +;; movl (%rdi), %edi +;; testl %edi, %edi +;; je 0x43 +;; 39: movl $0x4fc, %eax ;; movq %rbp, %rsp ;; popq %rbp ;; retq -;; d2: ud2 -;; d4: ud2 +;; 43: ud2 diff --git a/tests/disas/component-model/sync-adapter-calls.wat b/tests/disas/component-model/sync-adapter-calls.wat index cb42b80d9e02..455a417ec808 100644 --- a/tests/disas/component-model/sync-adapter-calls.wat +++ b/tests/disas/component-model/sync-adapter-calls.wat @@ -52,23 +52,12 @@ ) ;; function u1:0(i64 vmctx, i64) -> i32 tail { -;; ss0 = explicit_slot 32, align = 8 ;; region0 = 8 "VMContext+0x8" ;; region1 = 67108888 "VMStoreContext+0x18" ;; region2 = 1207959576 "VMFunctionImport+0x18" ;; region3 = 1476395008 "VMGlobalImport+0x0" ;; region4 = 738197568 "VMComponentContext+0x40" -;; region5 = 67109000 "VMStoreContext+0x88" -;; region6 = 1006632960 "VMDeferredThread+0x0" -;; region7 = 1006632968 "VMDeferredThread+0x8" -;; region8 = 1006632972 "VMDeferredThread+0xc" -;; region9 = 1006632976 "VMDeferredThread+0x10" -;; region10 = 67108992 "VMStoreContext+0x80" -;; region11 = 1006632980 "VMDeferredThread+0x14" -;; region12 = 67108996 "VMStoreContext+0x84" -;; region13 = 1006632984 "VMDeferredThread+0x18" -;; region14 = 738197552 "VMComponentContext+0x30" -;; region15 = 1207959560 "VMFunctionImport+0x8" +;; region5 = 738197552 "VMComponentContext+0x30" ;; gv0 = vmctx ;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 ;; gv2 = load.i64 notrap aligned region1 gv1+24 @@ -80,10 +69,9 @@ ;; gv8 = load.i64 notrap aligned region1 gv7+24 ;; sig0 = (i64 vmctx, i64, i32) -> i32 tail ;; sig1 = (i64 vmctx, i64) tail -;; sig2 = (i64 vmctx, i64, i32, i32, i32) tail -;; sig3 = (i64 vmctx, i64, i32) -> i32 tail +;; sig2 = (i64 vmctx, i64, i32) -> i32 tail ;; fn0 = colocated u2:0 sig0 -;; fn1 = colocated u0:0 sig3 +;; fn1 = colocated u0:0 sig2 ;; stack_limit = gv2 ;; ;; block0(v0: i64, v1: i64): @@ -94,50 +82,23 @@ ;; ;; block6: ;; @00ee v3 = load.i64 notrap aligned readonly can_move region2 v0+72 -;; v9 = load.i64 notrap aligned readonly can_move region3 v3+232 +;; v9 = load.i64 notrap aligned readonly can_move region3 v3+168 ;; v10 = load.i32 notrap aligned region4 v9 ;; trapz v10, user26 ;; jump block9 ;; ;; block9: -;; v16 = load.i64 notrap aligned readonly can_move region0 v3+8 -;; v17 = load.i64 notrap aligned region5 v16+136 -;; v15 = stack_addr.i64 ss0 -;; store notrap aligned region6 v17, v15 -;; v11 = iconst.i32 2 -;; store notrap aligned region7 v11, v15+8 ; v11 = 2 -;; v8 = iconst.i32 0 -;; store notrap aligned region8 v8, v15+12 ; v8 = 0 -;; v13 = iconst.i32 1 -;; store notrap aligned region9 v13, v15+16 ; v13 = 1 -;; v18 = load.i32 notrap aligned region10 v16+128 -;; store notrap aligned region11 v18, v15+20 -;; store notrap aligned region10 v8, v16+128 ; v8 = 0 -;; v20 = load.i32 notrap aligned region12 v16+132 -;; store notrap aligned region13 v20, v15+24 -;; store notrap aligned region12 v8, v16+132 ; v8 = 0 -;; store notrap aligned region5 v15, v16+136 -;; v22 = load.i64 notrap aligned readonly can_move region3 v3+208 -;; v23 = load.i32 notrap aligned region14 v22 -;; jump block16 +;; v11 = load.i64 notrap aligned readonly can_move region3 v3+144 +;; v12 = load.i32 notrap aligned region5 v11 +;; jump block12 ;; -;; block16: -;; jump block17 +;; block12: +;; jump block13 ;; -;; block17: +;; block13: ;; jump block11 ;; ;; block11: -;; jump block12 -;; -;; block12: -;; store.i64 notrap aligned region5 v17, v16+136 -;; store.i32 notrap aligned region10 v18, v16+128 -;; store.i32 notrap aligned region12 v20, v16+132 -;; jump block14 -;; -;; block14: -;; store.i32 notrap aligned region4 v10, v9 ;; jump block7 ;; ;; block7: @@ -147,12 +108,12 @@ ;; jump block3 ;; ;; block3: -;; jump block18 +;; jump block14 ;; -;; block18: +;; block14: ;; @00f0 jump block1 ;; ;; block1: -;; v45 = iconst.i32 1276 -;; @00f0 return v45 ; v45 = 1276 +;; v23 = iconst.i32 1276 +;; @00f0 return v23 ; v23 = 1276 ;; } diff --git a/tests/disas/component-model/thread-transparency/aggregate.wat b/tests/disas/component-model/thread-transparency/aggregate.wat new file mode 100644 index 000000000000..8f926f334b35 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/aggregate.wat @@ -0,0 +1,147 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[1]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; Only handles disqualify an adapter's signature, so an aggregate parameter is +;; still thread-transparent and the `enter-sync-call`/`exit-sync-call` calls are +;; omitted below. + +(component + (component $A + (core module $M + ;; The lifted signature flattens to a discriminant plus the tuple's fields. + (func (export "f'") (param i32 i32 i32) (result i32) + (if (result i32) (local.get 0) + (then (i32.add (local.get 1) (local.get 2))) + (else (i32.const 0))) + ) + ) + + (core instance $m (instantiate $M)) + + (func (export "f") (param "x" (option (tuple u32 u32))) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $B + (import "f" (func $f (param "x" (option (tuple u32 u32))) (result u32))) + + (core func $f' (canon lower (func $f))) + + (core module $N + (import "" "f'" (func $f' (param i32 i32 i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1) (i32.const 1200) (i32.const 34)) + ) + ) + + (core instance $n + (instantiate $N + (with "" (instance (export "f'" (func $f')))) + ) + ) + + (func (export "g") (result u32) + (canon lift (core func $n "g'")) + ) + ) + + (instance $a (instantiate $A)) + (instance $b + (instantiate $B + (with "f" (func $a "f")) + ) + ) + + (export "g" (func $b "g")) +) +;; function u1:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197568 "VMComponentContext+0x40" +;; region5 = 738197552 "VMComponentContext+0x30" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64, i32, i32, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32, i32, i32) -> i32 tail +;; fn0 = colocated u2:0 sig0 +;; fn1 = colocated u0:0 sig2 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @010a jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @010a v5 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v15 = load.i64 notrap aligned readonly can_move region3 v5+200 +;; v16 = load.i32 notrap aligned region4 v15 +;; trapz v16, user26 +;; jump block9 +;; +;; block9: +;; v17 = load.i64 notrap aligned readonly can_move region3 v5+176 +;; v18 = load.i32 notrap aligned region5 v17 +;; v14 = iconst.i32 0 +;; store notrap aligned region5 v14, v17 ; v14 = 0 +;; jump block12 +;; +;; block12: +;; @0103 v2 = iconst.i32 1 +;; @0105 v3 = iconst.i32 1200 +;; @0108 v4 = iconst.i32 34 +;; jump block11(v2, v3, v4) ; v2 = 1, v3 = 1200, v4 = 34 +;; +;; block11(v8: i32, v9: i32, v10: i32): +;; store.i32 notrap aligned region5 v18, v17 +;; jump block16 +;; +;; block16: +;; brif.i32 v8, block18, block20 +;; +;; block18: +;; v27 = iadd.i32 v9, v10 +;; jump block19(v27) +;; +;; block20: +;; v34 = iconst.i32 0 +;; jump block19(v34) ; v34 = 0 +;; +;; block19(v12: i32): +;; jump block17 +;; +;; block17: +;; jump block15(v12) +;; +;; block15(v11: i32): +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block21(v11) +;; +;; block21(v13: i32): +;; @010c jump block1 +;; +;; block1: +;; @010c return v13 +;; } diff --git a/tests/disas/component-model/thread-transparency/async-type.wat b/tests/disas/component-model/thread-transparency/async-type.wat new file mode 100644 index 000000000000..21344943fb10 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/async-type.wat @@ -0,0 +1,158 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[1]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y -Wcomponent-model-async=y" + +;; An `async` function type is disqualifying even when both sides of the adapter +;; are lifted and lowered synchronously, so the `{enter,exit}-sync-call` calls +;; are kept. + +(component + (component $A + (core module $M + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 42)) + ) + ) + + (core instance $m (instantiate $M)) + + (func (export "f") async (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $B + (import "f" (func $f async (param "x" u32) (result u32))) + + (core func $f' (canon lower (func $f))) + + (core module $N + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1234)) + ) + ) + + (core instance $n + (instantiate $N + (with "" (instance (export "f'" (func $f')))) + ) + ) + + (func (export "g") (result u32) + (canon lift (core func $n "g'")) + ) + ) + + (instance $a (instantiate $A)) + (instance $b + (instantiate $B + (with "f" (func $a "f")) + ) + ) + + (export "g" (func $b "g")) +) +;; function u1:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197568 "VMComponentContext+0x40" +;; region5 = 67109000 "VMStoreContext+0x88" +;; region6 = 1006632960 "VMDeferredThread+0x0" +;; region7 = 1006632968 "VMDeferredThread+0x8" +;; region8 = 1006632972 "VMDeferredThread+0xc" +;; region9 = 67108992 "VMStoreContext+0x80" +;; region10 = 1006632976 "VMDeferredThread+0x10" +;; region11 = 67108996 "VMStoreContext+0x84" +;; region12 = 1006632980 "VMDeferredThread+0x14" +;; region13 = 738197552 "VMComponentContext+0x30" +;; region14 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32, i32) tail +;; sig3 = (i64 vmctx, i64, i32) -> i32 tail +;; fn0 = colocated u2:0 sig0 +;; fn1 = colocated u0:0 sig3 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @00ee jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @00ee v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v9 = load.i64 notrap aligned readonly can_move region3 v3+232 +;; v10 = load.i32 notrap aligned region4 v9 +;; trapz v10, user26 +;; jump block9 +;; +;; block9: +;; v15 = load.i64 notrap aligned readonly can_move region0 v3+8 +;; v16 = load.i64 notrap aligned region5 v15+136 +;; v14 = stack_addr.i64 ss0 +;; store notrap aligned region6 v16, v14 +;; v11 = iconst.i32 1 +;; store notrap aligned region7 v11, v14+8 ; v11 = 1 +;; store notrap aligned region8 v11, v14+12 ; v11 = 1 +;; v17 = load.i32 notrap aligned region9 v15+128 +;; store notrap aligned region10 v17, v14+16 +;; v8 = iconst.i32 0 +;; store notrap aligned region9 v8, v15+128 ; v8 = 0 +;; v19 = load.i32 notrap aligned region11 v15+132 +;; store notrap aligned region12 v19, v14+20 +;; store notrap aligned region11 v8, v15+132 ; v8 = 0 +;; store notrap aligned region5 v14, v15+136 +;; v21 = load.i64 notrap aligned readonly can_move region3 v3+208 +;; v22 = load.i32 notrap aligned region13 v21 +;; jump block16 +;; +;; block16: +;; jump block17 +;; +;; block17: +;; jump block11 +;; +;; block11: +;; jump block12 +;; +;; block12: +;; store.i64 notrap aligned region5 v16, v15+136 +;; store.i32 notrap aligned region9 v17, v15+128 +;; store.i32 notrap aligned region11 v19, v15+132 +;; jump block14 +;; +;; block14: +;; store.i32 notrap aligned region4 v10, v9 +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block18 +;; +;; block18: +;; @00f0 jump block1 +;; +;; block1: +;; v44 = iconst.i32 1276 +;; @00f0 return v44 ; v44 = 1276 +;; } diff --git a/tests/disas/component-model/thread-transparency/chain-opaque-inner.wat b/tests/disas/component-model/thread-transparency/chain-opaque-inner.wat new file mode 100644 index 000000000000..4e922a7ae284 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/chain-opaque-inner.wat @@ -0,0 +1,240 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[2]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; Opacity does not propagate outwards along a call chain: `canon +;; context.{get,set}` in $Inner makes the $Mid -> $Inner adapter opaque, but the +;; $Outer -> $Mid adapter above it stays transparent. Both adapters are inlined +;; into the function below, so we only have one `explicit_slot 24` for the +;; `VMDeferredThread`, not multiple slots. + +(component + (component $Inner + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + (core module $M + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func (export "f'") (param i32) (result i32) + (call $cset (i32.const 0x5555)) + (i32.add (local.get 0) (call $cget)) + ) + ) + (core instance $m + (instantiate $M + (with "" (instance + (export "cget" (func $cget)) + (export "cset" (func $cset)) + )) + ) + ) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $Mid + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core module $M + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "f'") (param i32) (result i32) + (i32.add (call $f' (local.get 0)) (i32.const 20)) + ) + ) + (core instance $m + (instantiate $M + (with "" (instance (export "f'" (func $f')))) + ) + ) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $Outer + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core module $M + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1200)) + ) + ) + (core instance $m + (instantiate $M + (with "" (instance (export "f'" (func $f')))) + ) + ) + (func (export "g") (result u32) + (canon lift (core func $m "g'")) + ) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "f" (func $inner "f")))) + (instance $outer (instantiate $Outer (with "f" (func $mid "f")))) + + (export "g" (func $outer "g")) +) +;; function u2:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197584 "VMComponentContext+0x50" +;; region5 = 738197568 "VMComponentContext+0x40" +;; region6 = 67109000 "VMStoreContext+0x88" +;; region7 = 1006632960 "VMDeferredThread+0x0" +;; region8 = 1006632968 "VMDeferredThread+0x8" +;; region9 = 1006632972 "VMDeferredThread+0xc" +;; region10 = 67108992 "VMStoreContext+0x80" +;; region11 = 1006632976 "VMDeferredThread+0x10" +;; region12 = 67108996 "VMStoreContext+0x84" +;; region13 = 1006632980 "VMDeferredThread+0x14" +;; region14 = 738197552 "VMComponentContext+0x30" +;; region15 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; gv9 = vmctx +;; gv10 = load.i64 notrap aligned readonly can_move region0 gv9+8 +;; gv11 = load.i64 notrap aligned region1 gv10+24 +;; gv12 = vmctx +;; gv13 = load.i64 notrap aligned readonly can_move region0 gv12+8 +;; gv14 = load.i64 notrap aligned region1 gv13+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32) -> i32 tail +;; sig3 = (i64 vmctx, i64, i32) -> i32 tail +;; sig4 = (i64 vmctx, i64) tail +;; sig5 = (i64 vmctx, i64, i32, i32) tail +;; sig6 = (i64 vmctx, i64, i32) -> i32 tail +;; sig7 = (i64 vmctx, i64, i32) tail +;; sig8 = (i64 vmctx, i64) -> i32 tail +;; fn0 = colocated u4:0 sig0 +;; fn1 = colocated u1:0 sig2 +;; fn2 = colocated u3:0 sig3 +;; fn3 = colocated u0:0 sig6 +;; fn4 = colocated u2147483648:18 sig7 +;; fn5 = colocated u2147483648:17 sig8 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0225 jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @0225 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v12 = load.i64 notrap aligned readonly can_move region3 v3+168 +;; v13 = load.i32 notrap aligned region4 v12 +;; trapz v13, user26 +;; jump block9 +;; +;; block9: +;; v14 = load.i64 notrap aligned readonly can_move region3 v3+144 +;; v15 = load.i32 notrap aligned region5 v14 +;; jump block12 +;; +;; block12: +;; jump block14 +;; +;; block14: +;; jump block18 +;; +;; block18: +;; v19 = load.i64 notrap aligned readonly can_move region2 v3+72 +;; v20 = load.i64 notrap aligned readonly can_move region2 v19+72 +;; v22 = load.i64 notrap aligned readonly can_move region3 v20+232 +;; v23 = load.i32 notrap aligned region5 v22 +;; trapz v23, user26 +;; jump block21 +;; +;; block21: +;; v28 = load.i64 notrap aligned readonly can_move region0 v20+8 +;; v29 = load.i64 notrap aligned region6 v28+136 +;; v27 = stack_addr.i64 ss0 +;; store notrap aligned region7 v29, v27 +;; v11 = iconst.i32 0 +;; store notrap aligned region8 v11, v27+8 ; v11 = 0 +;; v25 = iconst.i32 1 +;; store notrap aligned region9 v25, v27+12 ; v25 = 1 +;; v30 = load.i32 notrap aligned region10 v28+128 +;; store notrap aligned region11 v30, v27+16 +;; store notrap aligned region10 v11, v28+128 ; v11 = 0 +;; v32 = load.i32 notrap aligned region12 v28+132 +;; store notrap aligned region13 v32, v27+20 +;; store notrap aligned region12 v11, v28+132 ; v11 = 0 +;; store notrap aligned region6 v27, v28+136 +;; v34 = load.i64 notrap aligned readonly can_move region3 v20+208 +;; v35 = load.i32 notrap aligned region14 v34 +;; jump block28 +;; +;; block28: +;; v40 = iconst.i32 0x5555 +;; v39 = load.i64 notrap aligned readonly can_move region2 v20+72 +;; v42 = load.i64 notrap aligned readonly can_move region0 v39+8 +;; store notrap aligned region10 v40, v42+128 ; v40 = 0x5555 +;; jump block29 +;; +;; block29: +;; jump block23 +;; +;; block23: +;; jump block24 +;; +;; block24: +;; store.i64 notrap aligned region6 v29, v28+136 +;; store.i32 notrap aligned region10 v30, v28+128 +;; store.i32 notrap aligned region12 v32, v28+132 +;; jump block26 +;; +;; block26: +;; store.i32 notrap aligned region5 v23, v22 +;; jump block19 +;; +;; block19: +;; jump block16 +;; +;; block16: +;; jump block15 +;; +;; block15: +;; jump block30 +;; +;; block30: +;; jump block13 +;; +;; block13: +;; jump block11 +;; +;; block11: +;; store.i32 notrap aligned region4 v13, v12 +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block31 +;; +;; block31: +;; @0227 jump block1 +;; +;; block1: +;; v77 = iconst.i32 0x5a19 +;; @0227 return v77 ; v77 = 0x5a19 +;; } diff --git a/tests/disas/component-model/thread-transparency/chain.wat b/tests/disas/component-model/thread-transparency/chain.wat new file mode 100644 index 000000000000..934f14f5d37c --- /dev/null +++ b/tests/disas/component-model/thread-transparency/chain.wat @@ -0,0 +1,181 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[2]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; Transparency composes along a call chain: with $Outer -> $Mid -> $Inner all +;; clean, both adapters are thread-transparent and the fully-inlined function +;; below has no `enter-sync-call`/`exit-sync-call` calls. + +(component + (component $Inner + (core module $M + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 2)) + ) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $Mid + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core module $M + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "f'") (param i32) (result i32) + (i32.add (call $f' (local.get 0)) (i32.const 20)) + ) + ) + (core instance $m + (instantiate $M + (with "" (instance (export "f'" (func $f')))) + ) + ) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $Outer + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core module $M + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1200)) + ) + ) + (core instance $m + (instantiate $M + (with "" (instance (export "f'" (func $f')))) + ) + ) + (func (export "g") (result u32) + (canon lift (core func $m "g'")) + ) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "f" (func $inner "f")))) + (instance $outer (instantiate $Outer (with "f" (func $mid "f")))) + + (export "g" (func $outer "g")) +) +;; function u2:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197584 "VMComponentContext+0x50" +;; region5 = 738197568 "VMComponentContext+0x40" +;; region6 = 738197552 "VMComponentContext+0x30" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; gv9 = vmctx +;; gv10 = load.i64 notrap aligned readonly can_move region0 gv9+8 +;; gv11 = load.i64 notrap aligned region1 gv10+24 +;; gv12 = vmctx +;; gv13 = load.i64 notrap aligned readonly can_move region0 gv12+8 +;; gv14 = load.i64 notrap aligned region1 gv13+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32) -> i32 tail +;; sig3 = (i64 vmctx, i64, i32) -> i32 tail +;; sig4 = (i64 vmctx, i64) tail +;; sig5 = (i64 vmctx, i64, i32) -> i32 tail +;; fn0 = colocated u4:0 sig0 +;; fn1 = colocated u1:0 sig2 +;; fn2 = colocated u3:0 sig3 +;; fn3 = colocated u0:0 sig5 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @01c8 jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @01c8 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v12 = load.i64 notrap aligned readonly can_move region3 v3+168 +;; v13 = load.i32 notrap aligned region4 v12 +;; trapz v13, user26 +;; jump block9 +;; +;; block9: +;; v14 = load.i64 notrap aligned readonly can_move region3 v3+144 +;; v15 = load.i32 notrap aligned region5 v14 +;; jump block12 +;; +;; block12: +;; jump block14 +;; +;; block14: +;; jump block18 +;; +;; block18: +;; v19 = load.i64 notrap aligned readonly can_move region2 v3+72 +;; v20 = load.i64 notrap aligned readonly can_move region2 v19+72 +;; v22 = load.i64 notrap aligned readonly can_move region3 v20+168 +;; v23 = load.i32 notrap aligned region5 v22 +;; trapz v23, user26 +;; jump block21 +;; +;; block21: +;; v24 = load.i64 notrap aligned readonly can_move region3 v20+144 +;; v25 = load.i32 notrap aligned region6 v24 +;; jump block24 +;; +;; block24: +;; jump block25 +;; +;; block25: +;; jump block23 +;; +;; block23: +;; jump block19 +;; +;; block19: +;; jump block16 +;; +;; block16: +;; jump block15 +;; +;; block15: +;; jump block26 +;; +;; block26: +;; jump block13 +;; +;; block13: +;; jump block11 +;; +;; block11: +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block27 +;; +;; block27: +;; @01ca jump block1 +;; +;; block1: +;; v48 = iconst.i32 1222 +;; @01ca return v48 ; v48 = 1222 +;; } diff --git a/tests/disas/component-model/thread-transparency/handle.wat b/tests/disas/component-model/thread-transparency/handle.wat new file mode 100644 index 000000000000..bed396ed5eab --- /dev/null +++ b/tests/disas/component-model/thread-transparency/handle.wat @@ -0,0 +1,191 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[1]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; A handle anywhere in either signature is makes the adapter opaque. + +(component + (component $A + (type $t (resource (rep i32))) + (export $t' "t" (type $t)) + + (core module $M + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 42)) + ) + ) + + (core instance $m (instantiate $M)) + + (func (export "f") (param "h" (own $t')) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $B + (import "t" (type $t (sub resource))) + (import "f" (func $f (param "h" (own $t)) (result u32))) + + (core func $f' (canon lower (func $f))) + + (core module $N + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1234)) + ) + ) + + (core instance $n + (instantiate $N + (with "" (instance (export "f'" (func $f')))) + ) + ) + + (func (export "g") (result u32) + (canon lift (core func $n "g'")) + ) + ) + + (instance $a (instantiate $A)) + (instance $b + (instantiate $B + (with "t" (type $a "t")) + (with "f" (func $a "f")) + ) + ) + + (export "g" (func $b "g")) +) +;; function u1:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197568 "VMComponentContext+0x40" +;; region5 = 67109000 "VMStoreContext+0x88" +;; region6 = 1006632960 "VMDeferredThread+0x0" +;; region7 = 1006632968 "VMDeferredThread+0x8" +;; region8 = 1006632972 "VMDeferredThread+0xc" +;; region9 = 67108992 "VMStoreContext+0x80" +;; region10 = 1006632976 "VMDeferredThread+0x10" +;; region11 = 67108996 "VMStoreContext+0x84" +;; region12 = 1006632980 "VMDeferredThread+0x14" +;; region13 = 738197552 "VMComponentContext+0x30" +;; region14 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32, i32) tail +;; sig3 = (i64 vmctx, i64, i32, i32, i32) -> i32 tail +;; sig4 = (i64 vmctx, i64, i32) -> i32 tail +;; fn0 = colocated u2:0 sig0 +;; fn1 = colocated u0:0 sig4 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0114 jump block2 +;; +;; block2: +;; jump block6 +;; +;; block8(v5: i64): +;; jump block5 +;; +;; block6: +;; @0114 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v10 = load.i64 notrap aligned readonly can_move region3 v3+264 +;; v11 = load.i32 notrap aligned region4 v10 +;; trapz v11, user26 +;; jump block9 +;; +;; block9: +;; v16 = load.i64 notrap aligned readonly can_move region0 v3+8 +;; v17 = load.i64 notrap aligned region5 v16+136 +;; v15 = stack_addr.i64 ss0 +;; store notrap aligned region6 v17, v15 +;; v9 = iconst.i32 0 +;; store notrap aligned region7 v9, v15+8 ; v9 = 0 +;; v13 = iconst.i32 1 +;; store notrap aligned region8 v13, v15+12 ; v13 = 1 +;; v18 = load.i32 notrap aligned region9 v16+128 +;; store notrap aligned region10 v18, v15+16 +;; store notrap aligned region9 v9, v16+128 ; v9 = 0 +;; v20 = load.i32 notrap aligned region11 v16+132 +;; store notrap aligned region12 v20, v15+20 +;; store notrap aligned region11 v9, v16+132 ; v9 = 0 +;; store notrap aligned region5 v15, v16+136 +;; v22 = load.i64 notrap aligned readonly can_move region3 v3+240 +;; v23 = load.i32 notrap aligned region13 v22 +;; store notrap aligned region13 v9, v22 ; v9 = 0 +;; v29 = load.i64 notrap aligned readonly can_move region14 v3+152 +;; v28 = load.i64 notrap aligned readonly can_move region2 v3+168 +;; @0111 v2 = iconst.i32 1234 +;; v26 = iconst.i32 2 +;; try_call_indirect v29(v28, v3, v2, v26, v9), sig3, block11(ret0), [ context v3, default: block8(exn0) ] ; v2 = 1234, v26 = 2, v9 = 0 +;; +;; block11(v6: i32): +;; store.i32 notrap aligned region13 v23, v22 +;; jump block17 +;; +;; block17: +;; jump block18 +;; +;; block18: +;; jump block12 +;; +;; block12: +;; v39 = load.i64 notrap aligned region5 v16+136 +;; v40 = icmp eq v39, v15 +;; brif v40, block13, block14 +;; +;; block13: +;; v41 = load.i64 notrap aligned region6 v15 +;; store notrap aligned region5 v41, v16+136 +;; v42 = load.i32 notrap aligned region10 v15+16 +;; store notrap aligned region9 v42, v16+128 +;; v43 = load.i32 notrap aligned region12 v15+20 +;; store notrap aligned region11 v43, v16+132 +;; jump block15 +;; +;; block14: +;; v47 = load.i64 notrap aligned readonly can_move region14 v3+184 +;; v36 = load.i64 notrap aligned readonly can_move region2 v3+200 +;; try_call_indirect v47(v36, v3), sig1, block16, [ context v3, default: block8(exn0) ] +;; +;; block16: +;; jump block15 +;; +;; block15: +;; store.i32 notrap aligned region4 v11, v10 +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block5: +;; trap user52 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block19 +;; +;; block19: +;; @0116 jump block1 +;; +;; block1: +;; v34 = iconst.i32 42 +;; v35 = iadd.i32 v6, v34 ; v34 = 42 +;; @0116 return v35 +;; } diff --git a/tests/disas/component-model/thread-transparency/may-block.wat b/tests/disas/component-model/thread-transparency/may-block.wat new file mode 100644 index 000000000000..6482bda6c204 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/may-block.wat @@ -0,0 +1,171 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[2]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y -Wcomponent-model-async=y" + +;; $Mid lowers an `async`-typed function, which requires a `task_may_block` +;; check, so the $Outer -> $Mid adapter keeps its `{enter,exit}-sync-call` +;; calls. + +(component + (component $Inner + (core module $M + (func (export "g'") (result i32) unreachable) + ) + (core instance $m (instantiate $M)) + (func (export "g") async (result u32) + (canon lift (core func $m "g'")) + ) + ) + + (component $Mid + (import "g" (func $g async (result u32))) + (core func $g' (canon lower (func $g))) + (core module $M + (import "" "g'" (func $g' (result i32))) + ;; What $Outer calls; it never reaches the `async`-typed lowering. + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 34)) + ) + ;; The lowering is only reachable from this other export. + (func (export "blocking") (result i32) + (call $g') + ) + ) + (core instance $m + (instantiate $M + (with "" (instance (export "g'" (func $g')))) + ) + ) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $Outer + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core module $M + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1200)) + ) + ) + (core instance $m + (instantiate $M + (with "" (instance (export "f'" (func $f')))) + ) + ) + (func (export "g") (result u32) + (canon lift (core func $m "g'")) + ) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "g" (func $inner "g")))) + (instance $outer (instantiate $Outer (with "f" (func $mid "f")))) + + (export "g" (func $outer "g")) +) +;; function u2:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197584 "VMComponentContext+0x50" +;; region5 = 67109000 "VMStoreContext+0x88" +;; region6 = 1006632960 "VMDeferredThread+0x0" +;; region7 = 1006632968 "VMDeferredThread+0x8" +;; region8 = 1006632972 "VMDeferredThread+0xc" +;; region9 = 67108992 "VMStoreContext+0x80" +;; region10 = 1006632976 "VMDeferredThread+0x10" +;; region11 = 67108996 "VMStoreContext+0x84" +;; region12 = 1006632980 "VMDeferredThread+0x14" +;; region13 = 738197568 "VMComponentContext+0x40" +;; region14 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32, i32) tail +;; sig3 = (i64 vmctx, i64, i32) -> i32 tail +;; fn0 = colocated u4:0 sig0 +;; fn1 = colocated u1:0 sig3 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @01d0 jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @01d0 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v9 = load.i64 notrap aligned readonly can_move region3 v3+232 +;; v10 = load.i32 notrap aligned region4 v9 +;; trapz v10, user26 +;; jump block9 +;; +;; block9: +;; v15 = load.i64 notrap aligned readonly can_move region0 v3+8 +;; v16 = load.i64 notrap aligned region5 v15+136 +;; v14 = stack_addr.i64 ss0 +;; store notrap aligned region6 v16, v14 +;; v8 = iconst.i32 0 +;; store notrap aligned region7 v8, v14+8 ; v8 = 0 +;; v12 = iconst.i32 2 +;; store notrap aligned region8 v12, v14+12 ; v12 = 2 +;; v17 = load.i32 notrap aligned region9 v15+128 +;; store notrap aligned region10 v17, v14+16 +;; store notrap aligned region9 v8, v15+128 ; v8 = 0 +;; v19 = load.i32 notrap aligned region11 v15+132 +;; store notrap aligned region12 v19, v14+20 +;; store notrap aligned region11 v8, v15+132 ; v8 = 0 +;; store notrap aligned region5 v14, v15+136 +;; v21 = load.i64 notrap aligned readonly can_move region3 v3+208 +;; v22 = load.i32 notrap aligned region13 v21 +;; jump block16 +;; +;; block16: +;; jump block17 +;; +;; block17: +;; jump block11 +;; +;; block11: +;; jump block12 +;; +;; block12: +;; store.i64 notrap aligned region5 v16, v15+136 +;; store.i32 notrap aligned region9 v17, v15+128 +;; store.i32 notrap aligned region11 v19, v15+132 +;; jump block14 +;; +;; block14: +;; store.i32 notrap aligned region4 v10, v9 +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block18 +;; +;; block18: +;; @01d2 jump block1 +;; +;; block1: +;; v44 = iconst.i32 1234 +;; @01d2 return v44 ; v44 = 1234 +;; } diff --git a/tests/disas/component-model/thread-transparency/opaque-core-instance.wat b/tests/disas/component-model/thread-transparency/opaque-core-instance.wat new file mode 100644 index 000000000000..023104190089 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/opaque-core-instance.wat @@ -0,0 +1,164 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[2]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; The analysis is at the component instance granularity, so $A's sibling helper +;; module importing `canon context.set` makes the adapter keep its +;; `{enter,exit}-sync-call` calls, even though `context.set` isn't actually used +;; by anything this adapter calls. + +(component + (component $A + (core func $cset (canon context.set i32 0)) + + (core module $Helpers + (import "" "cset" (func $cset (param i32))) + (func (export "helper") (param i32) + (call $cset (local.get 0)) + ) + ) + (core instance $helpers + (instantiate $Helpers + (with "" (instance (export "cset" (func $cset)))) + ) + ) + + (core module $M + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 42)) + ) + ) + (core instance $m (instantiate $M)) + + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core module $N + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1234)) + ) + ) + (core instance $n + (instantiate $N + (with "" (instance (export "f'" (func $f')))) + ) + ) + (func (export "g") (result u32) + (canon lift (core func $n "g'")) + ) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + + (export "g" (func $b "g")) +) +;; function u2:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197568 "VMComponentContext+0x40" +;; region5 = 67109000 "VMStoreContext+0x88" +;; region6 = 1006632960 "VMDeferredThread+0x0" +;; region7 = 1006632968 "VMDeferredThread+0x8" +;; region8 = 1006632972 "VMDeferredThread+0xc" +;; region9 = 67108992 "VMStoreContext+0x80" +;; region10 = 1006632976 "VMDeferredThread+0x10" +;; region11 = 67108996 "VMStoreContext+0x84" +;; region12 = 1006632980 "VMDeferredThread+0x14" +;; region13 = 738197552 "VMComponentContext+0x30" +;; region14 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32, i32) tail +;; sig3 = (i64 vmctx, i64, i32) -> i32 tail +;; fn0 = colocated u3:0 sig0 +;; fn1 = colocated u1:0 sig3 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0173 jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @0173 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v9 = load.i64 notrap aligned readonly can_move region3 v3+232 +;; v10 = load.i32 notrap aligned region4 v9 +;; trapz v10, user26 +;; jump block9 +;; +;; block9: +;; v15 = load.i64 notrap aligned readonly can_move region0 v3+8 +;; v16 = load.i64 notrap aligned region5 v15+136 +;; v14 = stack_addr.i64 ss0 +;; store notrap aligned region6 v16, v14 +;; v8 = iconst.i32 0 +;; store notrap aligned region7 v8, v14+8 ; v8 = 0 +;; v12 = iconst.i32 1 +;; store notrap aligned region8 v12, v14+12 ; v12 = 1 +;; v17 = load.i32 notrap aligned region9 v15+128 +;; store notrap aligned region10 v17, v14+16 +;; store notrap aligned region9 v8, v15+128 ; v8 = 0 +;; v19 = load.i32 notrap aligned region11 v15+132 +;; store notrap aligned region12 v19, v14+20 +;; store notrap aligned region11 v8, v15+132 ; v8 = 0 +;; store notrap aligned region5 v14, v15+136 +;; v21 = load.i64 notrap aligned readonly can_move region3 v3+208 +;; v22 = load.i32 notrap aligned region13 v21 +;; jump block16 +;; +;; block16: +;; jump block17 +;; +;; block17: +;; jump block11 +;; +;; block11: +;; jump block12 +;; +;; block12: +;; store.i64 notrap aligned region5 v16, v15+136 +;; store.i32 notrap aligned region9 v17, v15+128 +;; store.i32 notrap aligned region11 v19, v15+132 +;; jump block14 +;; +;; block14: +;; store.i32 notrap aligned region4 v10, v9 +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block18 +;; +;; block18: +;; @0175 jump block1 +;; +;; block1: +;; v44 = iconst.i32 1276 +;; @0175 return v44 ; v44 = 1276 +;; } diff --git a/tests/disas/component-model/thread-transparency/opaque-lowering.wat b/tests/disas/component-model/thread-transparency/opaque-lowering.wat new file mode 100644 index 000000000000..51dc560c2e4d --- /dev/null +++ b/tests/disas/component-model/thread-transparency/opaque-lowering.wat @@ -0,0 +1,131 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[1]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; Only the *lift* side of an adapter is judged, since FACT emits +;; `exit-sync-call` before translating results. The caller below declares and +;; uses `canon context.{get,set}`, yet its adapter into the clean callee is +;; still transparent and drops the `{enter,exit}-sync-call` calls. + +(component + (component $A + (core module $M + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 42)) + ) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + (core module $N + (import "" "f'" (func $f' (param i32) (result i32))) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func (export "g'") (result i32) (local $r i32) + (call $cset (i32.const 0x1234)) + (local.set $r (call $f' (i32.const 1234))) + (i32.add (local.get $r) (call $cget)) + ) + ) + (core instance $n + (instantiate $N + (with "" (instance + (export "f'" (func $f')) + (export "cget" (func $cget)) + (export "cset" (func $cset)) + )) + ) + ) + (func (export "g") (result u32) + (canon lift (core func $n "g'")) + ) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + + (export "g" (func $b "g")) +) +;; function u1:0(i64 vmctx, i64) -> i32 tail { +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 67108992 "VMStoreContext+0x80" +;; region4 = 1476395008 "VMGlobalImport+0x0" +;; region5 = 738197568 "VMComponentContext+0x40" +;; region6 = 738197552 "VMComponentContext+0x30" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64, i32) tail +;; sig1 = (i64 vmctx, i64, i32) -> i32 tail +;; sig2 = (i64 vmctx, i64) -> i32 tail +;; sig3 = (i64 vmctx, i64) tail +;; sig4 = (i64 vmctx, i64, i32) -> i32 tail +;; fn0 = colocated u2147483648:18 sig0 +;; fn1 = colocated u2:0 sig1 +;; fn2 = colocated u2147483648:17 sig2 +;; fn3 = colocated u0:0 sig4 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0107 v3 = iconst.i32 4660 +;; @010a v5 = load.i64 notrap aligned readonly can_move region0 v0+8 +;; @010a store notrap aligned region3 v3, v5+128 ; v3 = 4660 +;; @010f jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @010f v7 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v17 = load.i64 notrap aligned readonly can_move region4 v7+168 +;; v18 = load.i32 notrap aligned region5 v17 +;; trapz v18, user26 +;; jump block9 +;; +;; block9: +;; v19 = load.i64 notrap aligned readonly can_move region4 v7+144 +;; v20 = load.i32 notrap aligned region6 v19 +;; jump block12 +;; +;; block12: +;; jump block13 +;; +;; block13: +;; jump block11 +;; +;; block11: +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block14 +;; +;; block14: +;; @0118 jump block1 +;; +;; block1: +;; v37 = iconst.i32 5936 +;; @0118 return v37 ; v37 = 5936 +;; } diff --git a/tests/disas/component-model/thread-transparency/opaque-sibling.wat b/tests/disas/component-model/thread-transparency/opaque-sibling.wat new file mode 100644 index 000000000000..7c787857c868 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/opaque-sibling.wat @@ -0,0 +1,233 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[2]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; Opacity is per-adapter, not per-caller: $Caller calls both a clean component +;; instance and one declaring `canon context.{get,set}` from the same core +;; function, and only the latter adapter keeps its window, so exactly one +;; `explicit_slot 24` (the frame-local `VMDeferredThread`) appears below. + +(component + (component $Clean + (core module $M + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 2)) + ) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $Dirty + (core func $cset (canon context.set i32 0)) + (core module $M + (import "" "cset" (func $cset (param i32))) + (func (export "f'") (param i32) (result i32) + (call $cset (i32.const 0x5555)) + (i32.add (local.get 0) (i32.const 20)) + ) + ) + (core instance $m + (instantiate $M + (with "" (instance (export "cset" (func $cset)))) + ) + ) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $Caller + (import "clean" (func $clean (param "x" u32) (result u32))) + (import "dirty" (func $dirty (param "x" u32) (result u32))) + (core func $clean' (canon lower (func $clean))) + (core func $dirty' (canon lower (func $dirty))) + (core module $M + (import "" "clean" (func $clean (param i32) (result i32))) + (import "" "dirty" (func $dirty (param i32) (result i32))) + (func (export "g'") (result i32) + (i32.add (call $clean (i32.const 1200)) (call $dirty (i32.const 0))) + ) + ) + (core instance $m + (instantiate $M + (with "" (instance + (export "clean" (func $clean')) + (export "dirty" (func $dirty')) + )) + ) + ) + (func (export "g") (result u32) + (canon lift (core func $m "g'")) + ) + ) + + (instance $clean (instantiate $Clean)) + (instance $dirty (instantiate $Dirty)) + (instance $caller + (instantiate $Caller + (with "clean" (func $clean "f")) + (with "dirty" (func $dirty "f")) + ) + ) + + (export "g" (func $caller "g")) +) +;; function u2:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197584 "VMComponentContext+0x50" +;; region5 = 738197552 "VMComponentContext+0x30" +;; region6 = 67109000 "VMStoreContext+0x88" +;; region7 = 1006632960 "VMDeferredThread+0x0" +;; region8 = 1006632968 "VMDeferredThread+0x8" +;; region9 = 1006632972 "VMDeferredThread+0xc" +;; region10 = 67108992 "VMStoreContext+0x80" +;; region11 = 1006632976 "VMDeferredThread+0x10" +;; region12 = 67108996 "VMStoreContext+0x84" +;; region13 = 1006632980 "VMDeferredThread+0x14" +;; region14 = 738197568 "VMComponentContext+0x40" +;; region15 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; gv9 = vmctx +;; gv10 = load.i64 notrap aligned readonly can_move region0 gv9+8 +;; gv11 = load.i64 notrap aligned region1 gv10+24 +;; gv12 = vmctx +;; gv13 = load.i64 notrap aligned readonly can_move region0 gv12+8 +;; gv14 = load.i64 notrap aligned region1 gv13+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32) -> i32 tail +;; sig3 = (i64 vmctx, i64) tail +;; sig4 = (i64 vmctx, i64, i32, i32) tail +;; sig5 = (i64 vmctx, i64, i32) -> i32 tail +;; sig6 = (i64 vmctx, i64, i32) tail +;; fn0 = colocated u3:0 sig0 +;; fn1 = colocated u3:1 sig0 +;; fn2 = colocated u0:0 sig2 +;; fn3 = colocated u1:0 sig5 +;; fn4 = colocated u2147483648:18 sig6 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @01ea jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @01ea v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v13 = load.i64 notrap aligned readonly can_move region3 v3+264 +;; v14 = load.i32 notrap aligned region4 v13 +;; trapz v14, user26 +;; jump block9 +;; +;; block9: +;; v15 = load.i64 notrap aligned readonly can_move region3 v3+240 +;; v16 = load.i32 notrap aligned region5 v15 +;; jump block12 +;; +;; block12: +;; jump block13 +;; +;; block13: +;; jump block11 +;; +;; block11: +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block14 +;; +;; block14: +;; @01ee jump block15 +;; +;; block15: +;; jump block19 +;; +;; block19: +;; @01ee v6 = load.i64 notrap aligned readonly can_move region2 v0+104 +;; v31 = load.i64 notrap aligned readonly can_move region3 v6+264 +;; v32 = load.i32 notrap aligned region4 v31 +;; trapz v32, user26 +;; jump block22 +;; +;; block22: +;; v37 = load.i64 notrap aligned readonly can_move region0 v6+8 +;; v38 = load.i64 notrap aligned region6 v37+136 +;; v36 = stack_addr.i64 ss0 +;; store notrap aligned region7 v38, v36 +;; v12 = iconst.i32 0 +;; store notrap aligned region8 v12, v36+8 ; v12 = 0 +;; v21 = iconst.i32 2 +;; store notrap aligned region9 v21, v36+12 ; v21 = 2 +;; v39 = load.i32 notrap aligned region10 v37+128 +;; store notrap aligned region11 v39, v36+16 +;; store notrap aligned region10 v12, v37+128 ; v12 = 0 +;; v41 = load.i32 notrap aligned region12 v37+132 +;; store notrap aligned region13 v41, v36+20 +;; store notrap aligned region12 v12, v37+132 ; v12 = 0 +;; store notrap aligned region6 v36, v37+136 +;; v43 = load.i64 notrap aligned readonly can_move region3 v6+288 +;; v44 = load.i32 notrap aligned region14 v43 +;; jump block29 +;; +;; block29: +;; v49 = iconst.i32 0x5555 +;; v48 = load.i64 notrap aligned readonly can_move region2 v6+168 +;; v51 = load.i64 notrap aligned readonly can_move region0 v48+8 +;; store notrap aligned region10 v49, v51+128 ; v49 = 0x5555 +;; jump block30 +;; +;; block30: +;; jump block24 +;; +;; block24: +;; jump block25 +;; +;; block25: +;; store.i64 notrap aligned region6 v38, v37+136 +;; store.i32 notrap aligned region10 v39, v37+128 +;; store.i32 notrap aligned region12 v41, v37+132 +;; jump block27 +;; +;; block27: +;; store.i32 notrap aligned region4 v32, v31 +;; jump block20 +;; +;; block20: +;; jump block17 +;; +;; block17: +;; jump block16 +;; +;; block16: +;; jump block31 +;; +;; block31: +;; @01f1 jump block1 +;; +;; block1: +;; v81 = iconst.i32 1222 +;; @01f1 return v81 ; v81 = 1222 +;; } diff --git a/tests/disas/component-model/thread-transparency/opaque.wat b/tests/disas/component-model/thread-transparency/opaque.wat new file mode 100644 index 000000000000..bb5dd26052c0 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/opaque.wat @@ -0,0 +1,171 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[1]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; The callee here imports `canon context.get`, so the adapter is *not* +;; thread-transparent and the `enter-sync-call`/`exit-sync-call` calls must +;; still be emitted. + +(component + (component $A + (core func $cget (canon context.get i32 0)) + + (core module $M + (import "" "cget" (func $cget (result i32))) + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (call $cget)) + ) + ) + + (core instance $m + (instantiate $M + (with "" (instance (export "cget" (func $cget)))) + ) + ) + + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + + (core func $f' (canon lower (func $f))) + + (core module $N + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1234)) + ) + ) + + (core instance $n + (instantiate $N + (with "" (instance (export "f'" (func $f')))) + ) + ) + + (func (export "g") (result u32) + (canon lift (core func $n "g'")) + ) + ) + + (instance $a (instantiate $A)) + (instance $b + (instantiate $B + (with "f" (func $a "f")) + ) + ) + + (export "g" (func $b "g")) +) +;; function u1:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197568 "VMComponentContext+0x40" +;; region5 = 67109000 "VMStoreContext+0x88" +;; region6 = 1006632960 "VMDeferredThread+0x0" +;; region7 = 1006632968 "VMDeferredThread+0x8" +;; region8 = 1006632972 "VMDeferredThread+0xc" +;; region9 = 67108992 "VMStoreContext+0x80" +;; region10 = 1006632976 "VMDeferredThread+0x10" +;; region11 = 67108996 "VMStoreContext+0x84" +;; region12 = 1006632980 "VMDeferredThread+0x14" +;; region13 = 738197552 "VMComponentContext+0x30" +;; region14 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32, i32) tail +;; sig3 = (i64 vmctx, i64, i32) -> i32 tail +;; sig4 = (i64 vmctx, i64) -> i32 tail +;; fn0 = colocated u2:0 sig0 +;; fn1 = colocated u0:0 sig3 +;; fn2 = colocated u2147483648:17 sig4 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0123 jump block2 +;; +;; block2: +;; jump block6 +;; +;; block6: +;; @0123 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v9 = load.i64 notrap aligned readonly can_move region3 v3+232 +;; v10 = load.i32 notrap aligned region4 v9 +;; trapz v10, user26 +;; jump block9 +;; +;; block9: +;; v15 = load.i64 notrap aligned readonly can_move region0 v3+8 +;; v16 = load.i64 notrap aligned region5 v15+136 +;; v14 = stack_addr.i64 ss0 +;; store notrap aligned region6 v16, v14 +;; v8 = iconst.i32 0 +;; store notrap aligned region7 v8, v14+8 ; v8 = 0 +;; v12 = iconst.i32 1 +;; store notrap aligned region8 v12, v14+12 ; v12 = 1 +;; v17 = load.i32 notrap aligned region9 v15+128 +;; store notrap aligned region10 v17, v14+16 +;; store notrap aligned region9 v8, v15+128 ; v8 = 0 +;; v19 = load.i32 notrap aligned region11 v15+132 +;; store notrap aligned region12 v19, v14+20 +;; store notrap aligned region11 v8, v15+132 ; v8 = 0 +;; store notrap aligned region5 v14, v15+136 +;; v21 = load.i64 notrap aligned readonly can_move region3 v3+208 +;; v22 = load.i32 notrap aligned region13 v21 +;; jump block16 +;; +;; block16: +;; v26 = load.i64 notrap aligned readonly can_move region2 v3+72 +;; v28 = load.i64 notrap aligned readonly can_move region0 v26+8 +;; v29 = load.i32 notrap aligned region9 v28+128 +;; jump block17 +;; +;; block17: +;; jump block11 +;; +;; block11: +;; jump block12 +;; +;; block12: +;; store.i64 notrap aligned region5 v16, v15+136 +;; store.i32 notrap aligned region9 v17, v15+128 +;; store.i32 notrap aligned region11 v19, v15+132 +;; jump block14 +;; +;; block14: +;; store.i32 notrap aligned region4 v10, v9 +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block18 +;; +;; block18: +;; @0125 jump block1 +;; +;; block1: +;; @0120 v2 = iconst.i32 1234 +;; v46 = iadd.i32 v29, v2 ; v2 = 1234 +;; @0125 return v46 +;; } diff --git a/tests/disas/component-model/thread-transparency/resource-canon.wat b/tests/disas/component-model/thread-transparency/resource-canon.wat new file mode 100644 index 000000000000..021e8dd36ca2 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/resource-canon.wat @@ -0,0 +1,193 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[1]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; Any canonical built-in (here `resource.new`) makes the callee's instance +;; opaque. + +(component + (component $A + (type $t (resource (rep i32))) + (core func $new (canon resource.new $t)) + + (core module $M + (import "" "new" (func $new (param i32) (result i32))) + (func (export "f'") (param i32) (result i32) + (i32.add (local.get 0) (call $new (local.get 0))) + ) + ) + + (core instance $m + (instantiate $M + (with "" (instance (export "new" (func $new)))) + ) + ) + + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f'")) + ) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + + (core func $f' (canon lower (func $f))) + + (core module $N + (import "" "f'" (func $f' (param i32) (result i32))) + (func (export "g'") (result i32) + (call $f' (i32.const 1234)) + ) + ) + + (core instance $n + (instantiate $N + (with "" (instance (export "f'" (func $f')))) + ) + ) + + (func (export "g") (result u32) + (canon lift (core func $n "g'")) + ) + ) + + (instance $a (instantiate $A)) + (instance $b + (instantiate $B + (with "f" (func $a "f")) + ) + ) + + (export "g" (func $b "g")) +) +;; function u1:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197568 "VMComponentContext+0x40" +;; region5 = 67109000 "VMStoreContext+0x88" +;; region6 = 1006632960 "VMDeferredThread+0x0" +;; region7 = 1006632968 "VMDeferredThread+0x8" +;; region8 = 1006632972 "VMDeferredThread+0xc" +;; region9 = 67108992 "VMStoreContext+0x80" +;; region10 = 1006632976 "VMDeferredThread+0x10" +;; region11 = 67108996 "VMStoreContext+0x84" +;; region12 = 1006632980 "VMDeferredThread+0x14" +;; region13 = 738197552 "VMComponentContext+0x30" +;; region14 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64, i32) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32, i32) tail +;; sig3 = (i64 vmctx, i64, i32) -> i32 tail +;; sig4 = (i64 vmctx, i64, i32) -> i32 tail +;; fn0 = colocated u2:0 sig0 +;; fn1 = colocated u0:0 sig3 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0129 jump block2 +;; +;; block2: +;; jump block6 +;; +;; block8(v5: i64): +;; jump block5 +;; +;; block6: +;; @0129 v3 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v10 = load.i64 notrap aligned readonly can_move region3 v3+232 +;; v11 = load.i32 notrap aligned region4 v10 +;; trapz v11, user26 +;; jump block9 +;; +;; block9: +;; v16 = load.i64 notrap aligned readonly can_move region0 v3+8 +;; v17 = load.i64 notrap aligned region5 v16+136 +;; v15 = stack_addr.i64 ss0 +;; store notrap aligned region6 v17, v15 +;; v9 = iconst.i32 0 +;; store notrap aligned region7 v9, v15+8 ; v9 = 0 +;; v13 = iconst.i32 1 +;; store notrap aligned region8 v13, v15+12 ; v13 = 1 +;; v18 = load.i32 notrap aligned region9 v16+128 +;; store notrap aligned region10 v18, v15+16 +;; store notrap aligned region9 v9, v16+128 ; v9 = 0 +;; v20 = load.i32 notrap aligned region11 v16+132 +;; store notrap aligned region12 v20, v15+20 +;; store notrap aligned region11 v9, v16+132 ; v9 = 0 +;; store notrap aligned region5 v15, v16+136 +;; v22 = load.i64 notrap aligned readonly can_move region3 v3+208 +;; v23 = load.i32 notrap aligned region13 v22 +;; jump block16 +;; +;; block16: +;; v27 = load.i64 notrap aligned readonly can_move region2 v3+72 +;; v29 = load.i64 notrap aligned readonly can_move region14 v27+56 +;; v28 = load.i64 notrap aligned readonly can_move region2 v27+72 +;; @0126 v2 = iconst.i32 1234 +;; try_call_indirect v29(v28, v27, v2), sig4, block18(ret0), [ context v3, default: block8(exn0) ] ; v2 = 1234 +;; +;; block18(v7: i32): +;; jump block17 +;; +;; block17: +;; jump block11 +;; +;; block11: +;; v36 = load.i64 notrap aligned region5 v16+136 +;; v37 = icmp eq v36, v15 +;; brif v37, block12, block13 +;; +;; block12: +;; v38 = load.i64 notrap aligned region6 v15 +;; store notrap aligned region5 v38, v16+136 +;; v39 = load.i32 notrap aligned region10 v15+16 +;; store notrap aligned region9 v39, v16+128 +;; v40 = load.i32 notrap aligned region12 v15+20 +;; store notrap aligned region11 v40, v16+132 +;; jump block14 +;; +;; block13: +;; v44 = load.i64 notrap aligned readonly can_move region14 v3+152 +;; v33 = load.i64 notrap aligned readonly can_move region2 v3+168 +;; try_call_indirect v44(v33, v3), sig1, block15, [ context v3, default: block8(exn0) ] +;; +;; block15: +;; jump block14 +;; +;; block14: +;; store.i32 notrap aligned region4 v11, v10 +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block5: +;; trap user52 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block19 +;; +;; block19: +;; @012b jump block1 +;; +;; block1: +;; v48 = iconst.i32 1234 +;; v49 = iadd.i32 v7, v48 ; v48 = 1234 +;; @012b return v49 +;; } diff --git a/tests/disas/component-model/thread-transparency/shared-table.wat b/tests/disas/component-model/thread-transparency/shared-table.wat new file mode 100644 index 000000000000..2cc2e992bc49 --- /dev/null +++ b/tests/disas/component-model/thread-transparency/shared-table.wat @@ -0,0 +1,244 @@ +;;! target = "x86_64" +;;! test = "optimize" +;;! filter = "wasm[3]--function" +;;! flags = "-C inlining=y -Wconcurrency-support=y" + +;; The analysis works at component-instance rather than core-instance +;; granularity: $Inner's lifted callee imports nothing but a core table, but a +;; sibling core instance planted a `context`-poking function in it that +;; `call_indirect` reaches. $Inner declares `canon context.{get,set}`, so the +;; $Outer -> $Inner adapter is opaque. + +(component + (component $Inner + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + + (core module $Shared (table (export "t") 1 funcref)) + (core instance $shared (instantiate $Shared)) + + (core module $Evil + (import "" "t" (table 1 funcref)) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func $leak (result i32) + (call $cset (i32.const 0x5555)) + (call $cget)) + (elem (table 0) (i32.const 0) func $leak) + ) + (core instance $evil + (instantiate $Evil + (with "" (instance + (export "t" (table $shared "t")) + (export "cget" (func $cget)) + (export "cset" (func $cset)) + )) + ) + ) + + (core module $Victim + (import "" "t" (table 1 funcref)) + (type $sig (func (result i32))) + (func (export "f'") (result i32) + (call_indirect (type $sig) (i32.const 0))) + ) + (core instance $victim + (instantiate $Victim + (with "" (instance (export "t" (table $shared "t")))) + ) + ) + + (func (export "f") (result u32) + (canon lift (core func $victim "f'")) + ) + ) + + (component $Outer + (import "f" (func $f (result u32))) + (core func $f' (canon lower (func $f))) + (core module $M + (import "" "f'" (func $f' (result i32))) + (func (export "g'") (result i32) + (call $f') + ) + ) + (core instance $m + (instantiate $M + (with "" (instance (export "f'" (func $f')))) + ) + ) + (func (export "g") (result u32) + (canon lift (core func $m "g'")) + ) + ) + + (instance $inner (instantiate $Inner)) + (instance $outer (instantiate $Outer (with "f" (func $inner "f")))) + + (export "g" (func $outer "g")) +) +;; function u3:0(i64 vmctx, i64) -> i32 tail { +;; ss0 = explicit_slot 24, align = 8 +;; region0 = 8 "VMContext+0x8" +;; region1 = 67108888 "VMStoreContext+0x18" +;; region2 = 1207959576 "VMFunctionImport+0x18" +;; region3 = 1476395008 "VMGlobalImport+0x0" +;; region4 = 738197568 "VMComponentContext+0x40" +;; region5 = 67109000 "VMStoreContext+0x88" +;; region6 = 1006632960 "VMDeferredThread+0x0" +;; region7 = 1006632968 "VMDeferredThread+0x8" +;; region8 = 1006632972 "VMDeferredThread+0xc" +;; region9 = 67108992 "VMStoreContext+0x80" +;; region10 = 1006632976 "VMDeferredThread+0x10" +;; region11 = 67108996 "VMStoreContext+0x84" +;; region12 = 1006632980 "VMDeferredThread+0x14" +;; region13 = 738197552 "VMComponentContext+0x30" +;; region14 = 1342177280 "VMTableImport+0x0" +;; region15 = 671088648 "VMTableDefinition+0x8" +;; region16 = 671088640 "VMTableDefinition+0x0" +;; region17 = 335544320 "DefinedTable(StaticModuleIndex(0), DefinedTableIndex(0))" +;; region18 = 40 "VMContext+0x28" +;; region19 = 1677721600 "TypeIdsArray+0x0" +;; region20 = 1610612752 "VMFuncRef+0x10" +;; region21 = 1610612744 "VMFuncRef+0x8" +;; region22 = 1610612760 "VMFuncRef+0x18" +;; region23 = 1207959560 "VMFunctionImport+0x8" +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8 +;; gv2 = load.i64 notrap aligned region1 gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move region0 gv3+8 +;; gv5 = load.i64 notrap aligned region1 gv4+24 +;; gv6 = vmctx +;; gv7 = load.i64 notrap aligned readonly can_move region0 gv6+8 +;; gv8 = load.i64 notrap aligned region1 gv7+24 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i64) tail +;; sig2 = (i64 vmctx, i64, i32, i32) tail +;; sig3 = (i64 vmctx, i64) -> i32 tail +;; sig4 = (i64 vmctx, i64) -> i32 tail +;; sig5 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u4:0 sig0 +;; fn1 = colocated u2:0 sig3 +;; fn2 = colocated u805306368:7 sig5 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @020e jump block2 +;; +;; block2: +;; jump block6 +;; +;; block8(v4: i64): +;; jump block5 +;; +;; block6: +;; @020e v2 = load.i64 notrap aligned readonly can_move region2 v0+72 +;; v11 = load.i64 notrap aligned readonly can_move region3 v2+232 +;; v12 = load.i32 notrap aligned region4 v11 +;; trapz v12, user26 +;; jump block9 +;; +;; block9: +;; v17 = load.i64 notrap aligned readonly can_move region0 v2+8 +;; v18 = load.i64 notrap aligned region5 v17+136 +;; v16 = stack_addr.i64 ss0 +;; store notrap aligned region6 v18, v16 +;; v10 = iconst.i32 0 +;; store notrap aligned region7 v10, v16+8 ; v10 = 0 +;; v14 = iconst.i32 1 +;; store notrap aligned region8 v14, v16+12 ; v14 = 1 +;; v19 = load.i32 notrap aligned region9 v17+128 +;; store notrap aligned region10 v19, v16+16 +;; store notrap aligned region9 v10, v17+128 ; v10 = 0 +;; v21 = load.i32 notrap aligned region11 v17+132 +;; store notrap aligned region12 v21, v16+20 +;; store notrap aligned region11 v10, v17+132 ; v10 = 0 +;; store notrap aligned region5 v16, v17+136 +;; v23 = load.i64 notrap aligned readonly can_move region3 v2+208 +;; v24 = load.i32 notrap aligned region13 v23 +;; jump block16 +;; +;; block16: +;; v28 = load.i64 notrap aligned readonly can_move region2 v2+72 +;; v30 = load.i64 notrap aligned readonly can_move region14 v28+48 +;; v31 = load.i64 notrap aligned region15 v30+8 +;; v36 = load.i64 notrap aligned region16 v30 +;; v32 = ireduce.i32 v31 +;; v74 = iconst.i32 0 +;; v75 = icmp eq v32, v74 ; v74 = 0 +;; v73 = iconst.i64 0 +;; v41 = select_spectre_guard v75, v73, v36 ; v73 = 0 +;; v42 = load.i64 user6 aligned region17 v41 +;; v43 = iconst.i64 -2 +;; v44 = band v42, v43 ; v43 = -2 +;; brif v42, block19(v44), block18 +;; +;; block18 cold: +;; v76 = iconst.i32 0 +;; v77 = iconst.i64 0 +;; try_call fn2(v28, v76, v77), sig5, block21(ret0), [ context v2, default: block8(exn0) ] ; v76 = 0, v77 = 0 +;; +;; block21(v8: i64): +;; jump block19(v8) +;; +;; block19(v6: i64): +;; v47 = load.i32 user7 aligned readonly region20 v6+16 +;; v45 = load.i64 notrap aligned readonly can_move region18 v28+40 +;; v46 = load.i32 notrap aligned readonly can_move region19 v45 +;; v48 = icmp eq v47, v46 +;; trapz v48, user8 +;; v50 = load.i64 notrap aligned readonly region21 v6+8 +;; v51 = load.i64 notrap aligned readonly region22 v6+24 +;; try_call_indirect v50(v51, v28), sig4, block20(ret0), [ context v2, default: block8(exn0) ] +;; +;; block20(v7: i32): +;; jump block17 +;; +;; block17: +;; jump block11(v7) +;; +;; block11(v5: i32): +;; v57 = load.i64 notrap aligned region5 v17+136 +;; v58 = icmp eq v57, v16 +;; brif v58, block12, block13 +;; +;; block12: +;; v59 = load.i64 notrap aligned region6 v16 +;; store notrap aligned region5 v59, v17+136 +;; v60 = load.i32 notrap aligned region10 v16+16 +;; store notrap aligned region9 v60, v17+128 +;; v61 = load.i32 notrap aligned region12 v16+20 +;; store notrap aligned region11 v61, v17+132 +;; jump block14 +;; +;; block13: +;; v65 = load.i64 notrap aligned readonly can_move region23 v2+152 +;; v54 = load.i64 notrap aligned readonly can_move region2 v2+168 +;; try_call_indirect v65(v54, v2), sig1, block15, [ context v2, default: block8(exn0) ] +;; +;; block15: +;; jump block14 +;; +;; block14: +;; store.i32 notrap aligned region4 v12, v11 +;; jump block7 +;; +;; block7: +;; jump block4 +;; +;; block5: +;; trap user52 +;; +;; block4: +;; jump block3 +;; +;; block3: +;; jump block22(v5) +;; +;; block22(v9: i32): +;; @0210 jump block1 +;; +;; block1: +;; @0210 return v9 +;; } diff --git a/tests/misc_testsuite/component-model/thread-transparency/blocks.wast b/tests/misc_testsuite/component-model/thread-transparency/blocks.wast new file mode 100644 index 000000000000..fc2fa94d7c31 --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/blocks.wast @@ -0,0 +1,69 @@ +;;! component_model_async = true + +;; $Inner's synchronously-lifted `async`-typed export really blocks, on a +;; `waitable-set.wait` over a fresh, empty waitable set. If the +;; thread-transparent $Outer -> $Mid adapter left `task_may_block` set, this +;; synchronous call chain would suspend instead of trapping. + +(component definition $Tester + (component $Inner + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $M + (import "" "waitable-set.new" (func $ws-new (result i32))) + (import "" "waitable-set.wait" (func $ws-wait (param i32 i32) (result i32))) + (func (export "g") + (drop (call $ws-wait (call $ws-new) (i32.const 0))) + unreachable) + ) + (core func $ws-new (canon waitable-set.new)) + (core func $ws-wait + (canon waitable-set.wait (memory (core memory $memory "mem")))) + (core instance $m (instantiate $M + (with "" (instance + (export "waitable-set.new" (func $ws-new)) + (export "waitable-set.wait" (func $ws-wait)))))) + (func (export "g") async (canon lift (core func $m "g"))) + ) + + (component $Mid + (import "inner" (instance $inner (export "g" (func async)))) + (core module $M + (import "" "g" (func $g)) + (func (export "f") (call $g)) + ) + (canon lower (func $inner "g") (core func $g)) + (core instance $m (instantiate $M + (with "" (instance (export "g" (func $g)))))) + (func (export "f") (canon lift (core func $m "f"))) + ) + + (component $Outer + (import "mid" (instance $mid (export "f" (func)))) + (core module $M + (import "" "task.return" (func $task-return)) + (import "" "f" (func $f)) + (func (export "run") (result i32) + (call $f) + (call $task-return) + (i32.const 0 (; EXIT ;))) + (func (export "cb") (param i32 i32 i32) (result i32) unreachable) + ) + (canon task.return (core func $task-return)) + (canon lower (func $mid "f") (core func $f)) + (core instance $m (instantiate $M + (with "" (instance + (export "task.return" (func $task-return)) + (export "f" (func $f)))))) + (func (export "run") async + (canon lift (core func $m "run") async (callback (core func $m "cb")))) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "inner" (instance $inner)))) + (instance $outer (instantiate $Outer (with "mid" (instance $mid)))) + (func (export "run") (alias export $outer "run")) +) + +(component instance $i $Tester) +(assert_trap (invoke "run") "cannot block a synchronous task before returning") diff --git a/tests/misc_testsuite/component-model/thread-transparency/chain.wast b/tests/misc_testsuite/component-model/thread-transparency/chain.wast new file mode 100644 index 000000000000..b5fac8f30d29 --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/chain.wast @@ -0,0 +1,209 @@ +;;! component_model_async = true + +;; Opacity does not propagate in either direction along a call chain: each +;; adapter is judged purely on its own lift instance. Every case below is the +;; same $Outer -> $Mid -> $Inner chain with a different link made opaque by a +;; `context` canon, with $Outer driving a slot across the whole chain and +;; checking that it survived. + +;; Opaque innermost link, transparent outer one. +(component + (component $Inner + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "f") (param i32) (result i32) + (if (call $get) (then unreachable)) + (call $set (i32.const 0x5555ffff)) + (i32.add (local.get 0) (i32.const 2))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $Mid + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core module $M + (import "" "f" (func $f (param i32) (result i32))) + (func (export "f") (param i32) (result i32) + (i32.add (call $f (local.get 0)) (i32.const 20))) + ) + (core instance $m (instantiate $M + (with "" (instance (export "f" (func $f')))))) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $Outer + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 20))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $m (instantiate $M (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $m "g"))) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "f" (func $inner "f")))) + (instance $outer (instantiate $Outer (with "f" (func $mid "f")))) + (export "g" (func $outer "g")) +) +(assert_return (invoke "g") (u32.const 42)) + +;; Transparent innermost link, opaque outer one. +(component + (component $Inner + (core module $M + (func (export "f") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 2))) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $Mid + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "f") (param i32) (result i32) (local $r i32) + (if (call $get) (then unreachable)) + (call $set (i32.const 0x5555ffff)) + (local.set $r (call $f (local.get 0))) + ;; The transparent callee below cannot have disturbed our slot. + (if (i32.ne (call $get) (i32.const 0x5555ffff)) (then unreachable)) + (i32.add (local.get $r) (i32.const 20))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $Outer + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 20))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $m (instantiate $M (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $m "g"))) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "f" (func $inner "f")))) + (instance $outer (instantiate $Outer (with "f" (func $mid "f")))) + (export "g" (func $outer "g")) +) +(assert_return (invoke "g") (u32.const 42)) + +;; Both links opaque, each with its own slot value to keep straight. +(component + (component $Inner + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "f") (param i32) (result i32) + (if (call $get) (then unreachable)) + (call $set (i32.const 0x7777ffff)) + (i32.add (local.get 0) (i32.const 2))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $Mid + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "f") (param i32) (result i32) (local $r i32) + (if (call $get) (then unreachable)) + (call $set (i32.const 0x5555ffff)) + (local.set $r (call $f (local.get 0))) + (if (i32.ne (call $get) (i32.const 0x5555ffff)) (then unreachable)) + (i32.add (local.get $r) (i32.const 20))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $Outer + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 20))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $m (instantiate $M (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $m "g"))) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "f" (func $inner "f")))) + (instance $outer (instantiate $Outer (with "f" (func $mid "f")))) + (export "g" (func $outer "g")) +) +(assert_return (invoke "g") (u32.const 42)) diff --git a/tests/misc_testsuite/component-model/thread-transparency/handles.wast b/tests/misc_testsuite/component-model/thread-transparency/handles.wast new file mode 100644 index 000000000000..ede7529a91c6 --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/handles.wast @@ -0,0 +1,251 @@ +;;! component_model_async = true +;;! component_model_more_async_builtins = true +;;! component_model_error_context = true + +;; A handle anywhere in either of an adapter's signatures makes it opaque. + +;; An `own` handle as a parameter, and again nested inside an aggregate. +(component + (component $A + (type $t (resource (rep i32))) + (export $t' "t" (type $t)) + (core func $rep (canon resource.rep $t)) + (core func $drop (canon resource.drop $t)) + (core func $new (canon resource.new $t)) + (core module $M + (import "" "rep" (func $rep (param i32) (result i32))) + (import "" "drop" (func $drop (param i32))) + ;; Takes ownership, reads through it, then drops it. + (func $take (export "take") (param i32) (result i32) + (local $r i32) + (local.set $r (call $rep (local.get 0))) + (call $drop (local.get 0)) + (local.get $r)) + ;; Same, but the handle arrived as the second field of a tuple. + (func (export "take-nested") (param i32 i32) (result i32) + (i32.add (local.get 0) (call $take (local.get 1)))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "rep" (func $rep)) + (export "drop" (func $drop)))))) + (core module $Ctor + (import "" "new" (func $new (param i32) (result i32))) + (func (export "make") (param i32) (result i32) + (call $new (local.get 0))) + ) + (core instance $ctor (instantiate $Ctor + (with "" (instance (export "new" (func $new)))))) + (func (export "make") (param "rep" u32) (result (own $t')) + (canon lift (core func $ctor "make"))) + (func (export "take") (param "h" (own $t')) (result u32) + (canon lift (core func $m "take"))) + (func (export "take-nested") (param "x" (tuple u32 (own $t'))) (result u32) + (canon lift (core func $m "take-nested"))) + ) + + (component $B + (import "t" (type $t (sub resource))) + (import "make" (func $make (param "rep" u32) (result (own $t)))) + (import "take" (func $take (param "h" (own $t)) (result u32))) + (import "take-nested" + (func $take-nested (param "x" (tuple u32 (own $t))) (result u32))) + (core func $make' (canon lower (func $make))) + (core func $take' (canon lower (func $take))) + (core func $take-nested' (canon lower (func $take-nested))) + (core module $N + (import "" "make" (func $make (param i32) (result i32))) + (import "" "take" (func $take (param i32) (result i32))) + (import "" "take-nested" (func $take-nested (param i32 i32) (result i32))) + (func (export "g") (result i32) + ;; An `own` result on the way out, an `own` param on the way back in, + ;; and once more nested inside a tuple. + (i32.add + (call $take (call $make (i32.const 100))) + (call $take-nested (i32.const 5) (call $make (i32.const 200))))) + ) + (core instance $n (instantiate $N (with "" (instance + (export "make" (func $make')) + (export "take" (func $take')) + (export "take-nested" (func $take-nested')))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B + (with "t" (type $a "t")) + (with "make" (func $a "make")) + (with "take" (func $a "take")) + (with "take-nested" (func $a "take-nested")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 305)) + +;; A `borrow` handle as a parameter: the caller keeps ownership across the call. +(component + (component $A + (type $t (resource (rep i32))) + (export $t' "t" (type $t)) + (core func $new (canon resource.new $t)) + (core func $drop (canon resource.drop $t)) + (core module $M + (import "" "new" (func $new (param i32) (result i32))) + (import "" "drop" (func $drop (param i32))) + (func (export "make") (param i32) (result i32) + (call $new (local.get 0))) + ;; A `borrow` lifted into the resource's own defining component arrives + ;; as the representation itself rather than as a table index. + (func (export "peek") (param i32) (result i32) + (local.get 0)) + (func (export "release") (param i32) + (call $drop (local.get 0))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "new" (func $new)) + (export "drop" (func $drop)))))) + (func (export "make") (param "rep" u32) (result (own $t')) + (canon lift (core func $m "make"))) + (func (export "peek") (param "h" (borrow $t')) (result u32) + (canon lift (core func $m "peek"))) + (func (export "release") (param "h" (own $t')) + (canon lift (core func $m "release"))) + ) + + (component $B + (import "t" (type $t (sub resource))) + (import "make" (func $make (param "rep" u32) (result (own $t)))) + (import "peek" (func $peek (param "h" (borrow $t)) (result u32))) + (import "release" (func $release (param "h" (own $t)))) + (core func $make' (canon lower (func $make))) + (core func $peek' (canon lower (func $peek))) + (core func $release' (canon lower (func $release))) + (core module $N + (import "" "make" (func $make (param i32) (result i32))) + (import "" "peek" (func $peek (param i32) (result i32))) + (import "" "release" (func $release (param i32))) + (func (export "g") (result i32) + (local $h i32) (local $sum i32) + (local.set $h (call $make (i32.const 7))) + ;; Borrowing twice is fine; we still own it afterwards. + (local.set $sum (call $peek (local.get $h))) + (local.set $sum + (i32.add (local.get $sum) (call $peek (local.get $h)))) + (call $release (local.get $h)) + (local.get $sum)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "make" (func $make')) + (export "peek" (func $peek')) + (export "release" (func $release')))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B + (with "t" (type $a "t")) + (with "make" (func $a "make")) + (with "peek" (func $a "peek")) + (with "release" (func $a "release")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 14)) + +;; `future`, `stream`, and `error-context` are handles for this purpose too. +;; The caller hands the readable end over and the callee just receives and drops +;; it, which is enough to run the transfer. +(component + (type $fut (future u8)) + (type $str (stream u8)) + + (component $A + (type $fut (future u8)) + (type $str (stream u8)) + (core func $future-drop (canon future.drop-readable $fut)) + (core func $stream-drop (canon stream.drop-readable $str)) + (core func $ctx-drop (canon error-context.drop)) + (core module $M + (import "" "future.drop-readable" (func $future-drop (param i32))) + (import "" "stream.drop-readable" (func $stream-drop (param i32))) + (import "" "error-context.drop" (func $ctx-drop (param i32))) + (func (export "take-future") (param i32) (result i32) + (call $future-drop (local.get 0)) + (i32.const 1)) + (func (export "take-stream") (param i32) (result i32) + (call $stream-drop (local.get 0)) + (i32.const 2)) + (func (export "take-error-context") (param i32) (result i32) + (call $ctx-drop (local.get 0)) + (i32.const 4)) + ) + (core instance $m (instantiate $M (with "" (instance + (export "future.drop-readable" (func $future-drop)) + (export "stream.drop-readable" (func $stream-drop)) + (export "error-context.drop" (func $ctx-drop)))))) + (func (export "take-future") (param "x" (future u8)) (result u32) + (canon lift (core func $m "take-future"))) + (func (export "take-stream") (param "x" (stream u8)) (result u32) + (canon lift (core func $m "take-stream"))) + (func (export "take-error-context") (param "x" error-context) (result u32) + (canon lift (core func $m "take-error-context"))) + ) + + (component $B + (type $fut (future u8)) + (type $str (stream u8)) + (import "take-future" (func $take-future (param "x" (future u8)) (result u32))) + (import "take-stream" (func $take-stream (param "x" (stream u8)) (result u32))) + (import "take-error-context" + (func $take-error-context (param "x" error-context) (result u32))) + (core module $Libc + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) unreachable) + ) + (core instance $libc (instantiate $Libc)) + (core func $take-future' (canon lower (func $take-future))) + (core func $take-stream' (canon lower (func $take-stream))) + (core func $take-error-context' (canon lower (func $take-error-context))) + (core func $future-new (canon future.new $fut)) + (core func $stream-new (canon stream.new $str)) + (core func $ctx-new + (canon error-context.new (memory $libc "memory"))) + (core module $N + (import "" "take-future" (func $take-future (param i32) (result i32))) + (import "" "take-stream" (func $take-stream (param i32) (result i32))) + (import "" "take-error-context" + (func $take-error-context (param i32) (result i32))) + (import "" "future.new" (func $future-new (result i64))) + (import "" "stream.new" (func $stream-new (result i64))) + (import "" "error-context.new" (func $ctx-new (param i32 i32) (result i32))) + ;; The writable ends stay live: dropping one without writing traps, and + ;; all this test needs is for the readable ends to make it across. + (func (export "g") (result i32) + (local $pair i64) (local $sum i32) + (local.set $pair (call $future-new)) + (local.set $sum + (call $take-future (i32.wrap_i64 (local.get $pair)))) + + (local.set $pair (call $stream-new)) + (local.set $sum (i32.add (local.get $sum) + (call $take-stream (i32.wrap_i64 (local.get $pair))))) + + (i32.add (local.get $sum) + (call $take-error-context + (call $ctx-new (i32.const 0) (i32.const 0))))) + ) + (core instance $n (instantiate $N (with "" (instance + (export "take-future" (func $take-future')) + (export "take-stream" (func $take-stream')) + (export "take-error-context" (func $take-error-context')) + (export "future.new" (func $future-new)) + (export "stream.new" (func $stream-new)) + (export "error-context.new" (func $ctx-new)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B + (with "take-future" (func $a "take-future")) + (with "take-stream" (func $a "take-stream")) + (with "take-error-context" (func $a "take-error-context")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 7)) diff --git a/tests/misc_testsuite/component-model/thread-transparency/may-block.wast b/tests/misc_testsuite/component-model/thread-transparency/may-block.wast new file mode 100644 index 000000000000..a579b6d2211b --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/may-block.wast @@ -0,0 +1,77 @@ +;;! component_model_async = true + +;; A sync-typed call reaching an `async`-typed export, through a chain that +;; includes a guest-to-guest adapter: +;; +;; host --async-lift--> $Outer --adapter--> $Mid --async-typed--> $Inner +;; +;; Blocking in a sync-typed call is enforced lazily: making the call is allowed, +;; and only actually blocking traps (see `blocks.wast`, where $Inner really does +;; block). $Inner's body here never blocks, so the call goes through and its +;; `unreachable` is what surfaces. +;; +;; The point of this file is that that outcome does not depend on how the +;; $Outer -> $Mid adapter is classified: `opaque-mid.wast` is this same +;; component with one extra `canon context.get` in $Mid, which forces the +;; adapter to keep its `{enter,exit}-sync-call` window, and asserts the same +;; result. + +(component definition $Tester + ;; $Inner's `async`-typed export is lifted *synchronously*. Its body traps + ;; rather than blocking, so the sync-typed call into it is permitted and this + ;; `unreachable` is what the caller observes. + (component $Inner + (core module $M + (func (export "g") unreachable) + ) + (core instance $m (instantiate $M)) + (func (export "g") async (canon lift (core func $m "g"))) + ) + + ;; $Mid declares no canonical built-in beyond a `canon lower` of an imported + ;; lift. That lift is `async`-typed, which is itself disqualifying, so the + ;; $Outer -> $Mid adapter keeps its window here; `opaque-mid.wast` is the same + ;; component with a second, independent reason to keep it. + (component $Mid + (import "inner" (instance $inner (export "g" (func async)))) + (core module $M + (import "" "g" (func $g)) + (func (export "f") (call $g)) + ) + (canon lower (func $inner "g") (core func $g)) + (core instance $m (instantiate $M + (with "" (instance (export "g" (func $g)))))) + (func (export "f") (canon lift (core func $m "f"))) + ) + + ;; $Outer's export is async-lifted, so its task is an "async function" and is + ;; itself allowed to block while it runs. + (component $Outer + (import "mid" (instance $mid (export "f" (func)))) + (core module $M + (import "" "task.return" (func $task-return)) + (import "" "f" (func $f)) + (func (export "run") (result i32) + (call $f) + (call $task-return) + (i32.const 0 (; EXIT ;))) + (func (export "cb") (param i32 i32 i32) (result i32) unreachable) + ) + (canon task.return (core func $task-return)) + (canon lower (func $mid "f") (core func $f)) + (core instance $m (instantiate $M + (with "" (instance + (export "task.return" (func $task-return)) + (export "f" (func $f)))))) + (func (export "run") async + (canon lift (core func $m "run") async (callback (core func $m "cb")))) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "inner" (instance $inner)))) + (instance $outer (instantiate $Outer (with "mid" (instance $mid)))) + (func (export "run") (alias export $outer "run")) +) + +(component instance $i $Tester) +(assert_trap (invoke "run") "wasm `unreachable` instruction executed") diff --git a/tests/misc_testsuite/component-model/thread-transparency/opaque-mid.wast b/tests/misc_testsuite/component-model/thread-transparency/opaque-mid.wast new file mode 100644 index 000000000000..3778a7ee0961 --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/opaque-mid.wast @@ -0,0 +1,68 @@ +;;! component_model_async = true + +;; Like `may-block.wast` except that $Mid declares one extra `canon +;; context.get`, which makes it opaque on its own account, so the $Outer -> $Mid +;; adapter keeps its `{enter,exit}-sync-call` calls. The result must be the same +;; as `may-block.wast`'s: how the adapter is classified is not observable. + +(component definition $Tester + ;; $Inner's `async`-typed export is lifted *synchronously*. Its body traps + ;; rather than blocking, so the sync-typed call into it is permitted and this + ;; `unreachable` is what the caller observes. + (component $Inner + (core module $M + (func (export "g") unreachable) + ) + (core instance $m (instantiate $M)) + (func (export "g") async (canon lift (core func $m "g"))) + ) + + ;; $Mid would otherwise be thread-transparent: it declares no canonical + ;; built-in beyond a `canon lower` of an imported lift, and its own export `f` + ;; is sync on both sides and mentions no handles. + (component $Mid + (import "inner" (instance $inner (export "g" (func async)))) + (core module $M + (import "" "g" (func $g)) + (func (export "f") (call $g)) + ) + (canon lower (func $inner "g") (core func $g)) + ;; The only difference from `may-block.wast`: + ;; this canon makes $Mid opaque. + (canon context.get i32 0 (core func $ctx)) + (core instance $m (instantiate $M + (with "" (instance (export "g" (func $g)))))) + (func (export "f") (canon lift (core func $m "f"))) + ) + + ;; $Outer's export is async-lifted, so its task is an "async function" and is + ;; itself allowed to block while it runs. + (component $Outer + (import "mid" (instance $mid (export "f" (func)))) + (core module $M + (import "" "task.return" (func $task-return)) + (import "" "f" (func $f)) + (func (export "run") (result i32) + (call $f) + (call $task-return) + (i32.const 0 (; EXIT ;))) + (func (export "cb") (param i32 i32 i32) (result i32) unreachable) + ) + (canon task.return (core func $task-return)) + (canon lower (func $mid "f") (core func $f)) + (core instance $m (instantiate $M + (with "" (instance + (export "task.return" (func $task-return)) + (export "f" (func $f)))))) + (func (export "run") async + (canon lift (core func $m "run") async (callback (core func $m "cb")))) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "inner" (instance $inner)))) + (instance $outer (instantiate $Outer (with "mid" (instance $mid)))) + (func (export "run") (alias export $outer "run")) +) + +(component instance $i $Tester) +(assert_trap (invoke "run") "wasm `unreachable` instruction executed") diff --git a/tests/misc_testsuite/component-model/thread-transparency/opaque.wast b/tests/misc_testsuite/component-model/thread-transparency/opaque.wast new file mode 100644 index 000000000000..223df9205d6b --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/opaque.wast @@ -0,0 +1,251 @@ +;;! component_model_async = true +;;! multi_memory = true + +;; Guest-to-guest sync calls whose adapters the thread-transparency analysis +;; classifies as opaque, so they keep their `enter-sync-call`/`exit-sync-call` +;; calls. +;; +;; Wherever the disqualifying canon can actually run, the callee writes a +;; `context` slot and the caller checks that its own slot survived, which would +;; break if one of these cases was misclassified as transparent. + +;; The callee's core module imports `canon context.get`/`context.set`. +(component + (component $A + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "f") (param i32) (result i32) + ;; A fresh task, so a fresh, zeroed slot. + (if (call $get) (then unreachable)) + (call $set (i32.const 0x5555ffff)) + (if (i32.ne (call $get) (i32.const 0x5555ffff)) (then unreachable)) + (i32.add (local.get 0) (i32.const 42))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $N + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 1))) + ;; The callee clobbered slot 0; the window must have restored ours. + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 43)) + +;; The disqualifying canon need not be reachable from the callee: here only the +;; `realloc` helper touches `context`, but it shares the callee's component +;; instance, which is the granularity the analysis works at. +(component + (component $A + (core func $set (canon context.set i32 0)) + (core module $Helpers + (import "" "set" (func $set (param i32))) + (memory (export "memory") 1) + (global $next (mut i32) (i32.const 16)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + (local $ret i32) + (call $set (i32.const 0x5555ffff)) + (local.set $ret (global.get $next)) + (global.set $next (i32.add (global.get $next) (local.get 3))) + (local.get $ret)) + ) + (core instance $helpers (instantiate $Helpers + (with "" (instance (export "set" (func $set)))))) + (core module $M + (import "" "memory" (memory 1)) + (func (export "f") (param $ptr i32) (param $len i32) (result i32) + (local $sum i32) + (loop $l + (if (local.get $len) (then + (local.set $sum + (i32.add (local.get $sum) (i32.load8_u (local.get $ptr)))) + (local.set $ptr (i32.add (local.get $ptr) (i32.const 1))) + (local.set $len (i32.sub (local.get $len) (i32.const 1))) + (br $l)))) + (local.get $sum)) + ) + (core instance $m (instantiate $M + (with "" (instance (export "memory" (memory $helpers "memory")))))) + (func (export "f") (param "x" (list u8)) (result u32) + (canon lift (core func $m "f") + (memory $helpers "memory") + (realloc (func $helpers "realloc")))) + ) + + (component $B + (import "f" (func $f (param "x" (list u8)) (result u32))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $Helpers + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + unreachable) + (data (i32.const 8) "\01\02\03\04") + ) + (core instance $helpers (instantiate $Helpers)) + (core func $f' (canon lower (func $f) + (memory $helpers "memory") + (realloc (func $helpers "realloc")))) + (core module $N + (import "" "f" (func $f (param i32 i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 8) (i32.const 4))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 10)) + +;; Importing any other canonical built-in (here `resource.{new,drop}`) is +;; disqualifying. The resource's destructor writes `context`, so this one is +;; observable as well. +(component + (component $A + (core func $set (canon context.set i32 0)) + (core module $Dtor + (import "" "set" (func $set (param i32))) + (global $drops (mut i32) (i32.const 0)) + (func (export "dtor") (param i32) + (call $set (i32.const 0x5555ffff)) + (global.set $drops (i32.add (global.get $drops) (i32.const 1)))) + (func (export "drops") (result i32) global.get $drops) + ) + (core instance $dtor (instantiate $Dtor + (with "" (instance (export "set" (func $set)))))) + (type $t (resource (rep i32) (dtor (core func $dtor "dtor")))) + (core func $new (canon resource.new $t)) + (core func $drop (canon resource.drop $t)) + (core module $M + (import "" "new" (func $new (param i32) (result i32))) + (import "" "drop" (func $drop (param i32))) + (import "" "drops" (func $drops (result i32))) + (func (export "f") (param i32) (result i32) + (call $drop (call $new (local.get 0))) + ;; One destructor ran. + (if (i32.ne (call $drops) (i32.const 1)) (then unreachable)) + (i32.add (local.get 0) (i32.const 42))) + ) + (core instance $m (instantiate $M (with "" (instance + (export "new" (func $new)) + (export "drop" (func $drop)) + (export "drops" (func $dtor "drops")))))) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $N + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 1))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 43)) + +;; An `async` function *type* is disqualifying even when the function is both +;; lifted and lowered synchronously: a sync-typed call into an `async`-typed +;; callee may block, and if it does the scheduler has to be able to find the +;; sync-typed call in progress on this instance. +;; +;; The callee here never blocks, so the call itself is allowed -- blocking is +;; only enforced if and when it actually happens -- and the caller-side +;; `context` assertions do run, checking that the window this adapter kept +;; restored $B's slot. +(component + (component $A + (core module $M + (func (export "f") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 42))) + ) + (core instance $m (instantiate $M)) + (func (export "f") async (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $B + (import "f" (func $f async (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $N + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 1))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 43)) diff --git a/tests/misc_testsuite/component-model/thread-transparency/reentrancy.wast b/tests/misc_testsuite/component-model/thread-transparency/reentrancy.wast new file mode 100644 index 000000000000..9db5a41bbff1 --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/reentrancy.wast @@ -0,0 +1,114 @@ +;;! component_model_async = true +;;! component_model_more_async_builtins = true +;;! component_model_async_stackful = true +;;! component_model_threading = true + +;; Sync-to-sync adapters between sibling instances have no dynamic `may_enter` +;; check, so a guest that cares about reentrance has to guard against it itself. +;; This exercises the case where it actually happens: +;; +;; host --async-lift--> $Outer --adapter--> $Mid --async-typed--> $Inner +;; +;; $Inner blocks on a fresh, empty waitable set. Blocking in a sync-typed call +;; no longer traps outright: the scheduler first looks for another eligible +;; thread to run, and $Outer has started a second one. That second thread calls +;; $Mid.f while the first call's frame is still live, reentrance now being +;; permitted, so $Mid's own `$inside` guard is what fires. +;; +;; This is not an artifact of dropping the `{enter,exit}-sync-call` window: the +;; $Outer -> $Mid adapter is opaque here anyway, because $Mid lowers an +;; `async`-typed lift. + +(component definition $Tester + (component $Inner + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $M + (import "" "waitable-set.new" (func $ws-new (result i32))) + (import "" "waitable-set.wait" (func $ws-wait (param i32 i32) (result i32))) + (func (export "g") + (drop (call $ws-wait (call $ws-new) (i32.const 0))) + unreachable) + ) + (core func $ws-new (canon waitable-set.new)) + (core func $ws-wait + (canon waitable-set.wait (memory (core memory $memory "mem")))) + (core instance $m (instantiate $M + (with "" (instance + (export "waitable-set.new" (func $ws-new)) + (export "waitable-set.wait" (func $ws-wait)))))) + (func (export "g") async (canon lift (core func $m "g"))) + ) + + ;; $Mid traps if it is ever re-entered while a previous call is still in + ;; progress. + (component $Mid + (import "inner" (instance $inner (export "g" (func async)))) + (core module $M + (import "" "g" (func $g)) + (global $inside (mut i32) (i32.const 0)) + (func (export "f") + (if (global.get $inside) + (then unreachable (; REENTERED ;))) + (global.set $inside (i32.const 1)) + (call $g) + (global.set $inside (i32.const 0))) + ) + (canon lower (func $inner "g") (core func $g)) + (core instance $m (instantiate $M + (with "" (instance (export "g" (func $g)))))) + (func (export "f") (canon lift (core func $m "f"))) + ) + + (component $Outer + (import "mid" (instance $mid (export "f" (func)))) + (core module $Table + (table (export "__indirect_function_table") 1 funcref)) + (core instance $table (instantiate $Table)) + (core module $M + (import "" "task.return" (func $task-return)) + (import "" "f" (func $f)) + (import "" "thread.new-indirect" (func $thread-new (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $resume-later (param i32))) + (import "" "__indirect_function_table" (table $tbl 1 funcref)) + + ;; Runs on the second thread, once the first one has blocked. + (func $second (param i32) + (call $f)) + (elem (table $tbl) (i32.const 0) func $second) + + (func (export "run") (result i32) + (call $resume-later (call $thread-new (i32.const 0) (i32.const 0))) + (call $f) + (call $task-return) + (i32.const 0 (; EXIT ;))) + (func (export "cb") (param i32 i32 i32) (result i32) unreachable) + ) + (core type $start-func-ty (func (param i32))) + (alias core export $table "__indirect_function_table" + (core table $indirect-function-table)) + (core func $thread-new + (canon thread.new-indirect $start-func-ty + (core table $indirect-function-table))) + (core func $resume-later (canon thread.resume-later)) + (canon task.return (core func $task-return)) + (canon lower (func $mid "f") (core func $f)) + (core instance $m (instantiate $M + (with "" (instance + (export "task.return" (func $task-return)) + (export "f" (func $f)) + (export "thread.new-indirect" (func $thread-new)) + (export "thread.resume-later" (func $resume-later)) + (export "__indirect_function_table" (table $indirect-function-table)))))) + (func (export "run") async + (canon lift (core func $m "run") async (callback (core func $m "cb")))) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "inner" (instance $inner)))) + (instance $outer (instantiate $Outer (with "mid" (instance $mid)))) + (func (export "run") (alias export $outer "run")) +) + +(component instance $i $Tester) +(assert_trap (invoke "run") "wasm `unreachable` instruction executed") diff --git a/tests/misc_testsuite/component-model/thread-transparency/shared-table.wast b/tests/misc_testsuite/component-model/thread-transparency/shared-table.wast new file mode 100644 index 000000000000..1667160a9ce5 --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/shared-table.wast @@ -0,0 +1,72 @@ +;;! component_model_async = true + +;; The transparency analysis works at component-instance, rather than +;; core-instance, granularity: $Inner's lifted callee imports nothing but a core +;; table, but a sibling core instance planted a `context`-poking function in it +;; that `call_indirect` reaches. Since $Inner declares `context.{get,set}`, the +;; $Outer -> $Inner adapter is opaque and keeps its `{enter,exit}-sync-call` +;; calls, so the value $Evil writes cannot land in $Outer's slot. + +(component + (component $Inner + (core func $cget (canon context.get i32 0)) + (core func $cset (canon context.set i32 0)) + + (core module $Shared (table (export "t") 1 funcref)) + (core instance $shared (instantiate $Shared)) + + (core module $Evil + (import "" "t" (table 1 funcref)) + (import "" "cget" (func $cget (result i32))) + (import "" "cset" (func $cset (param i32))) + (func $leak (result i32) + (call $cset (i32.const 0x5555)) + (call $cget)) + (elem (table 0) (i32.const 0) func $leak) + ) + (core instance $evil (instantiate $Evil (with "" (instance + (export "t" (table $shared "t")) + (export "cget" (func $cget)) + (export "cset" (func $cset)))))) + + (core module $Victim + (import "" "t" (table 1 funcref)) + (type $sig (func (result i32))) + (func (export "f") (result i32) + (call_indirect (type $sig) (i32.const 0))) + ) + (core instance $victim (instantiate $Victim (with "" (instance + (export "t" (table $shared "t")))))) + + (func (export "f") (result u32) (canon lift (core func $victim "f"))) + ) + + (component $Outer + (import "f" (func $f (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "f" (func $f (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f)) + ;; $Evil ran and saw its own write; ours must be untouched. + (if (i32.ne (local.get $r) (i32.const 0x5555)) (then unreachable)) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $m (instantiate $M (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $m "g"))) + ) + + (instance $inner (instantiate $Inner)) + (instance $outer (instantiate $Outer (with "f" (func $inner "f")))) + (export "g" (func $outer "g")) +) +(assert_return (invoke "g") (u32.const 0x5555)) diff --git a/tests/misc_testsuite/component-model/thread-transparency/transparent.wast b/tests/misc_testsuite/component-model/thread-transparency/transparent.wast new file mode 100644 index 000000000000..8856ec3a74a5 --- /dev/null +++ b/tests/misc_testsuite/component-model/thread-transparency/transparent.wast @@ -0,0 +1,265 @@ +;;! component_model_async = true +;;! multi_memory = true + +;; Guest-to-guest sync calls that are thread transparent, so they drop their +;; `enter-sync-call`/`exit-sync-call` window. +;; +;; A transparent adapter is only correct if the caller cannot tell, so every +;; case below has the caller drive a `context` slot across the call and check +;; that it is still intact afterwards. + +;; A plain `u32 -> u32` adapter whose callee imports nothing. +(component + (component $A + (core module $M + (func (export "f") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 42))) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $N + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 1))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 43)) + +;; Aggregates stay transparent; only handles disqualify a signature. +(component + (component $A + (core module $M + ;; `(option (tuple u32 u32))` flattens to a discriminant plus two fields. + (func (export "f") (param i32 i32 i32) (result i32) + (if (i32.eqz (local.get 0)) (then unreachable)) + (i32.add (local.get 1) (local.get 2))) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" (option (tuple u32 u32))) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $B + (import "f" (func $f (param "x" (option (tuple u32 u32))) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $N + (import "" "f" (func $f (param i32 i32 i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r + (call $f (i32.const 1) (i32.const 20) (i32.const 22))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 42)) + +;; A `(list u8)` argument, so the adapter copies through both memories and +;; calls the callee's `realloc`, all still within a transparent adapter. +(component + (component $A + (core module $M + (memory (export "memory") 1) + (global $next (mut i32) (i32.const 16)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + (local $ret i32) + (local.set $ret (global.get $next)) + (global.set $next (i32.add (global.get $next) (local.get 3))) + (local.get $ret)) + ;; Sum the bytes the adapter copied in for us. + (func (export "f") (param $ptr i32) (param $len i32) (result i32) + (local $sum i32) + (loop $l + (if (local.get $len) (then + (local.set $sum + (i32.add (local.get $sum) (i32.load8_u (local.get $ptr)))) + (local.set $ptr (i32.add (local.get $ptr) (i32.const 1))) + (local.set $len (i32.sub (local.get $len) (i32.const 1))) + (br $l)))) + (local.get $sum)) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" (list u8)) (result u32) + (canon lift (core func $m "f") + (memory $m "memory") + (realloc (func $m "realloc")))) + ) + + (component $B + (import "f" (func $f (param "x" (list u8)) (result u32))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $Helpers + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + unreachable) + (data (i32.const 8) "\01\02\03\04") + ) + (core instance $helpers (instantiate $Helpers)) + (core func $f' (canon lower (func $f) + (memory $helpers "memory") + (realloc (func $helpers "realloc")))) + (core module $N + (import "" "f" (func $f (param i32 i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 8) (i32.const 4))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 10)) + +;; An unrelated *sibling* component instance declaring a disqualifying canon +;; does not taint anyone else: the $B -> $A adapter is still transparent. +(component + (component $Dirty + (core func $get (canon context.get i32 0)) + (core module $M (import "" "get" (func (result i32)))) + (core instance $m (instantiate $M + (with "" (instance (export "get" (func $get)))))) + ) + + (component $A + (core module $M + (func (export "f") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 42))) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $B + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $N + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 1))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $n (instantiate $N (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $n "g"))) + ) + + (instance $dirty (instantiate $Dirty)) + (instance $a (instantiate $A)) + (instance $b (instantiate $B (with "f" (func $a "f")))) + (export "g" (func $b "g")) +) +(assert_return (invoke "g") (u32.const 43)) + +;; A three-deep chain $Outer -> $Mid -> $Inner where every link is clean, so +;; both adapters are transparent and nest inside one another. +(component + (component $Inner + (core module $M + (func (export "f") (param i32) (result i32) + (i32.add (local.get 0) (i32.const 2))) + ) + (core instance $m (instantiate $M)) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $Mid + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core module $M + (import "" "f" (func $f (param i32) (result i32))) + (func (export "f") (param i32) (result i32) + (i32.add (call $f (local.get 0)) (i32.const 20))) + ) + (core instance $m (instantiate $M + (with "" (instance (export "f" (func $f')))))) + (func (export "f") (param "x" u32) (result u32) + (canon lift (core func $m "f"))) + ) + + (component $Outer + (import "f" (func $f (param "x" u32) (result u32))) + (core func $f' (canon lower (func $f))) + (core func $get (canon context.get i32 0)) + (core func $set (canon context.set i32 0)) + (core module $M + (import "" "f" (func $f (param i32) (result i32))) + (import "" "get" (func $get (result i32))) + (import "" "set" (func $set (param i32))) + (func (export "g") (result i32) (local $r i32) + (call $set (i32.const 0x1234abcd)) + (local.set $r (call $f (i32.const 20))) + (if (i32.ne (call $get) (i32.const 0x1234abcd)) (then unreachable)) + (local.get $r)) + ) + (core instance $m (instantiate $M (with "" (instance + (export "f" (func $f')) + (export "get" (func $get)) + (export "set" (func $set)))))) + (func (export "g") (result u32) (canon lift (core func $m "g"))) + ) + + (instance $inner (instantiate $Inner)) + (instance $mid (instantiate $Mid (with "f" (func $inner "f")))) + (instance $outer (instantiate $Outer (with "f" (func $mid "f")))) + (export "g" (func $outer "g")) +) +(assert_return (invoke "g") (u32.const 42))