Repository: bytecodealliance/wasm-tools
Pull request #2042 (2025-02-07 19:10:54)

threads: add `thread.spawn_indirect`
This propagates the upstream spec changes to the component model in [#447] here. See commit messages for more details.

[#447]: https://github.com/WebAssembly/component-model/pull/447

abrown: @alexcrichton, I rebased this on top of `main` so a lot has changed. This also incorporates the conclusions we reached in https://github.com/WebAssembly/component-model/pull/447 so there's new encoding-related implementation related to that.

Patch:
diff --git a/crates/wasm-encoder/src/component/builder.rs b/crates/wasm-encoder/src/component/builder.rs
index a2f8a7bd..488db47e 100644
--- a/crates/wasm-encoder/src/component/builder.rs
+++ b/crates/wasm-encoder/src/component/builder.rs
@@ -379,9 +379,9 @@ impl ComponentBuilder {
         inc(&mut self.core_funcs)
     }
 
-    /// Declares a new `thread.spawn` intrinsic.
-    pub fn thread_spawn(&mut self, ty: u32) -> u32 {
-        self.canonical_functions().thread_spawn(ty);
+    /// Declares a new `thread.spawn_ref` intrinsic.
+    pub fn thread_spawn_ref(&mut self, ty: u32) -> u32 {
+        self.canonical_functions().thread_spawn_ref(ty);
         inc(&mut self.core_funcs)
     }
 
diff --git a/crates/wasm-encoder/src/component/canonicals.rs b/crates/wasm-encoder/src/component/canonicals.rs
index 029fa15e..8a184214 100644
--- a/crates/wasm-encoder/src/component/canonicals.rs
+++ b/crates/wasm-encoder/src/component/canonicals.rs
@@ -158,19 +158,29 @@ impl CanonicalFunctionSection {
         self
     }
 
-    /// Defines a function which will spawns a new thread by invoking a shared
+    /// Defines a function which will spawn a new thread by invoking a shared
     /// function of type `ty_index`.
-    pub fn thread_spawn(&mut self, ty_index: u32) -> &mut Self {
-        self.bytes.push(0x05);
+    pub fn thread_spawn_ref(&mut self, ty_index: u32) -> &mut Self {
+        self.bytes.push(0x40);
         ty_index.encode(&mut self.bytes);
         self.num_added += 1;
         self
     }
 
+    /// Defines a function which will spawn a new thread by invoking a shared
+    /// function indirectly through a `funcref` table.
+    pub fn thread_spawn_indirect(&mut self, ty_index: u32, table_index: u32) -> &mut Self {
+        self.bytes.push(0x41);
+        ty_index.encode(&mut self.bytes);
+        table_index.encode(&mut self.bytes);
+        self.num_added += 1;
+        self
+    }
+
     /// Defines a function which will return the number of threads that can be
     /// expected to execute concurrently.
     pub fn thread_available_parallelism(&mut self) -> &mut Self {
-        self.bytes.push(0x06);
+        self.bytes.push(0x42);
         self.num_added += 1;
         self
     }
diff --git a/crates/wasm-encoder/src/reencode/component.rs b/crates/wasm-encoder/src/reencode/component.rs
index 7f20cbe3..f920065e 100644
--- a/crates/wasm-encoder/src/reencode/component.rs
+++ b/crates/wasm-encoder/src/reencode/component.rs
@@ -953,9 +953,17 @@ pub mod component_utils {
                 let resource = reencoder.component_type_index(resource);
                 section.resource_rep(resource);
             }
-            wasmparser::CanonicalFunction::ThreadSpawn { func_ty_index } => {
+            wasmparser::CanonicalFunction::ThreadSpawnRef { func_ty_index } => {
                 let func_ty = reencoder.type_index(func_ty_index);
-                section.thread_spawn(func_ty);
+                section.thread_spawn_ref(func_ty);
+            }
+            wasmparser::CanonicalFunction::ThreadSpawnIndirect {
+                func_ty_index,
+                table_index,
+            } => {
+                let func_ty = reencoder.type_index(func_ty_index);
+                let table_index = reencoder.table_index(table_index);
+                section.thread_spawn_indirect(func_ty, table_index);
             }
             wasmparser::CanonicalFunction::ThreadAvailableParallelism => {
                 section.thread_available_parallelism();
diff --git a/crates/wasmparser/src/readers/component/canonicals.rs b/crates/wasmparser/src/readers/component/canonicals.rs
index 4b5a0548..9ac56c28 100644
--- a/crates/wasmparser/src/readers/component/canonicals.rs
+++ b/crates/wasmparser/src/readers/component/canonicals.rs
@@ -71,10 +71,18 @@ pub enum CanonicalFunction {
         resource: u32,
     },
     /// A function which spawns a new thread by invoking the shared function.
-    ThreadSpawn {
-        /// The index of the function to spawn.
+    ThreadSpawnRef {
+        /// The index of the function type to spawn.
         func_ty_index: u32,
     },
+    /// A function which spawns a new thread by invoking the shared function
+    /// passed as an index into a `funcref` table.
+    ThreadSpawnIndirect {
+        /// The index of the function type to spawn.
+        func_ty_index: u32,
+        /// The index of the table to use for the indirect spawn.
+        table_index: u32,
+    },
     /// A function which returns the number of threads that can be expected to
     /// execute concurrently
     ThreadAvailableParallelism,
@@ -277,10 +285,6 @@ impl<'a> FromReader<'a> for CanonicalFunction {
             0x04 => CanonicalFunction::ResourceRep {
                 resource: reader.read()?,
             },
-            0x05 => CanonicalFunction::ThreadSpawn {
-                func_ty_index: reader.read()?,
-            },
-            0x06 => CanonicalFunction::ThreadAvailableParallelism,
             0x08 => CanonicalFunction::BackpressureSet,
             0x09 => CanonicalFunction::TaskReturn {
                 result: crate::read_resultlist(reader)?,
@@ -355,6 +359,14 @@ impl<'a> FromReader<'a> for CanonicalFunction {
             },
             0x22 => CanonicalFunction::WaitableSetDrop,
             0x23 => CanonicalFunction::WaitableJoin,
+            0x40 => CanonicalFunction::ThreadSpawnRef {
+                func_ty_index: reader.read()?,
+            },
+            0x41 => CanonicalFunction::ThreadSpawnIndirect {
+                func_ty_index: reader.read()?,
+                table_index: reader.read()?,
+            },
+            0x42 => CanonicalFunction::ThreadAvailableParallelism,
             x => return reader.invalid_leading_byte(x, "canonical function"),
         })
     }
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 2d248adc..2207a109 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -987,9 +987,13 @@ impl ComponentState {
             CanonicalFunction::ResourceRep { resource } => {
                 self.resource_rep(resource, types, offset)
             }
-            CanonicalFunction::ThreadSpawn { func_ty_index } => {
-                self.thread_spawn(func_ty_index, types, offset, features)
+            CanonicalFunction::ThreadSpawnRef { func_ty_index } => {
+                self.thread_spawn_ref(func_ty_index, types, offset, features)
             }
+            CanonicalFunction::ThreadSpawnIndirect {
+                func_ty_index,
+                table_index,
+            } => self.thread_spawn_indirect(func_ty_index, table_index, types, offset, features),
             CanonicalFunction::ThreadAvailableParallelism => {
                 self.thread_available_parallelism(types, offset, features)
             }
@@ -1924,7 +1928,7 @@ impl ComponentState {
         bail!(offset, "type index {} is not a resource type", idx)
     }
 
-    fn thread_spawn(
+    fn thread_spawn_ref(
         &mut self,
         func_ty_index: u32,
         types: &mut TypeAlloc,
@@ -1934,11 +1938,76 @@ impl ComponentState {
         if !features.shared_everything_threads() {
             bail!(
                 offset,
-                "`thread.spawn` requires the shared-everything-threads proposal"
+                "`thread.spawn_ref` requires the shared-everything-threads proposal"
             )
         }
+        let core_type_id = self.validate_spawn_type(func_ty_index, types, offset)?;
 
-        // Validate the type accepted by `thread.spawn`.
+        // Insert the core function.
+        let packed_index = PackedIndex::from_id(core_type_id).ok_or_else(|| {
+            format_err!(offset, "implementation limit: too many types in `TypeList`")
+        })?;
+        let start_func_ref = RefType::concrete(true, packed_index);
+        let func_ty = FuncType::new([ValType::Ref(start_func_ref), ValType::I32], [ValType::I32]);
+        let core_ty = SubType::func(func_ty, true);
+        let id = types.intern_sub_type(core_ty, offset);
+        self.core_funcs.push(id);
+
+        Ok(())
+    }
+
+    fn thread_spawn_indirect(
+        &mut self,
+        func_ty_index: u32,
+        table_index: u32,
+        types: &mut TypeAlloc,
+        offset: usize,
+        features: &WasmFeatures,
+    ) -> Result<()> {
+        if !features.shared_everything_threads() {
+            bail!(
+                offset,
+                "`thread.spawn_indirect` requires the shared-everything-threads proposal"
+            )
+        }
+        let _ = self.validate_spawn_type(func_ty_index, types, offset)?;
+
+        // Check this much like `call_indirect` (see
+        // `OperatorValidatorTemp::check_call_indirect_ty`), but loosen the
+        // table type restrictions to just a `funcref`. See the component model
+        // for more details:
+        // https://github.com/WebAssembly/component-model/blob/6e08e283/design/mvp/CanonicalABI.md#-canon-threadspawn_indirect.
+        let table = self.table_at(table_index, offset)?;
+        // TODO: check that the table is shared once components allow shared
+        // tables.
+        let expected_ty = RefType::FUNCREF
+            .shared()
+            .expect("a funcref can always be shared");
+        if table.element_type != expected_ty {
+            bail!(offset, "expected a table of shared `funcref`");
+        }
+
+        // Insert the core function.
+        let func_ty = FuncType::new([ValType::I32, ValType::I32], [ValType::I32]);
+        let core_ty = SubType::func(func_ty, true);
+        let id = types.intern_sub_type(core_ty, offset);
+        self.core_funcs.push(id);
+
+        Ok(())
+    }
+
+    /// Validates the type of a `thread.spawn*` instruction.
+    ///
+    /// This is currently limited to shared functions with the signature `[i32]
+    /// -> []`. See component model [explanation] for more details.
+    ///
+    /// [explanation]: https://github.com/WebAssembly/component-model/blob/6e08e283/design/mvp/CanonicalABI.md#-canon-threadspawn_ref
+    fn validate_spawn_type(
+        &self,
+        func_ty_index: u32,
+        types: &TypeAlloc,
+        offset: usize,
+    ) -> Result<CoreTypeId> {
         let core_type_id = match self.core_type_at(func_ty_index, offset)? {
             ComponentCoreTypeId::Sub(c) => c,
             ComponentCoreTypeId::Module(_) => bail!(offset, "expected a core function type"),
@@ -1961,18 +2030,7 @@ impl ComponentState {
             }
             _ => bail!(offset, "spawn type must be a function"),
         }
-
-        // Insert the core function.
-        let packed_index = PackedIndex::from_id(core_type_id).ok_or_else(|| {
-            format_err!(offset, "implementation limit: too many types in `TypeList`")
-        })?;
-        let start_func_ref = RefType::concrete(true, packed_index);
-        let func_ty = FuncType::new([ValType::Ref(start_func_ref), ValType::I32], [ValType::I32]);
-        let core_ty = SubType::func(func_ty, true);
-        let id = types.intern_sub_type(core_ty, offset);
-        self.core_funcs.push(id);
-
-        Ok(())
+        Ok(core_type_id)
     }
 
     fn thread_available_parallelism(
diff --git a/crates/wasmprinter/src/component.rs b/crates/wasmprinter/src/component.rs
index 08400c49..03a9d576 100644
--- a/crates/wasmprinter/src/component.rs
+++ b/crates/wasmprinter/src/component.rs
@@ -892,26 +892,27 @@ impl Printer<'_, '_> {
                         me.print_idx(&state.component.type_names, resource)
                     })?;
                 }
-                CanonicalFunction::ThreadSpawn {
-                    func_ty_index: func_index,
+                CanonicalFunction::ThreadSpawnRef { func_ty_index } => {
+                    self.print_intrinsic(state, "canon thread.spawn_ref ", &|me, state| {
+                        me.print_idx(&state.core.type_names, func_ty_index)
+                    })?;
+                }
+                CanonicalFunction::ThreadSpawnIndirect {
+                    func_ty_index,
+                    table_index,
                 } => {
-                    self.start_group("core func ")?;
-                    self.print_name(&state.core.func_names, state.core.funcs)?;
-                    self.result.write_str(" ")?;
-                    self.start_group("canon thread.spawn ")?;
-                    self.print_idx(&state.core.type_names, func_index)?;
-                    self.end_group()?;
-                    self.end_group()?;
-                    state.core.funcs += 1;
+                    self.print_intrinsic(state, "canon thread.spawn_indirect ", &|me, state| {
+                        me.print_idx(&state.core.type_names, func_ty_index)?;
+                        me.result.write_str(" ")?;
+                        me.start_group("table ")?;
+                        me.print_idx(&state.core.table_names, table_index)?;
+                        me.end_group()
+                    })?;
                 }
                 CanonicalFunction::ThreadAvailableParallelism => {
-                    self.start_group("core func ")?;
-                    self.print_name(&state.core.func_names, state.core.funcs)?;
-                    self.result.write_str(" ")?;
-                    self.start_group("canon thread.available_parallelism")?;
-                    self.end_group()?;
-                    self.end_group()?;
-                    state.core.funcs += 1;
+                    self.print_intrinsic(state, "canon thread.available_parallelism", &|_, _| {
+                        Ok(())
+                    })?;
                 }
                 CanonicalFunction::BackpressureSet => {
                     self.print_intrinsic(state, "canon backpressure.set", &|_, _| Ok(()))?;
diff --git a/crates/wast/src/component/binary.rs b/crates/wast/src/component/binary.rs
index ac2fba0a..513e0eea 100644
--- a/crates/wast/src/component/binary.rs
+++ b/crates/wast/src/component/binary.rs
@@ -353,9 +353,14 @@ impl<'a> Encoder<'a> {
                     self.core_func_names.push(name);
                     self.funcs.resource_rep(info.ty.into());
                 }
-                CoreFuncKind::ThreadSpawn(info) => {
+                CoreFuncKind::ThreadSpawnRef(info) => {
                     self.core_func_names.push(name);
-                    self.funcs.thread_spawn(info.ty.into());
+                    self.funcs.thread_spawn_ref(info.ty.into());
+                }
+                CoreFuncKind::ThreadSpawnIndirect(info) => {
+                    self.core_func_names.push(name);
+                    self.funcs
+                        .thread_spawn_indirect(info.ty.into(), info.table.idx.into());
                 }
                 CoreFuncKind::ThreadAvailableParallelism(_info) => {
                     self.core_func_names.push(name);
diff --git a/crates/wast/src/component/func.rs b/crates/wast/src/component/func.rs
index 1967ba3d..0a3f1e26 100644
--- a/crates/wast/src/component/func.rs
+++ b/crates/wast/src/component/func.rs
@@ -51,7 +51,8 @@ pub enum CoreFuncKind<'a> {
     ResourceNew(CanonResourceNew<'a>),
     ResourceDrop(CanonResourceDrop<'a>),
     ResourceRep(CanonResourceRep<'a>),
-    ThreadSpawn(CanonThreadSpawn<'a>),
+    ThreadSpawnRef(CanonThreadSpawnRef<'a>),
+    ThreadSpawnIndirect(CanonThreadSpawnIndirect<'a>),
     ThreadAvailableParallelism(CanonThreadAvailableParallelism),
     BackpressureSet,
     TaskReturn(CanonTaskReturn<'a>),
@@ -109,8 +110,10 @@ impl<'a> CoreFuncKind<'a> {
             Ok(CoreFuncKind::ResourceDrop(parser.parse()?))
         } else if l.peek::<kw::resource_rep>()? {
             Ok(CoreFuncKind::ResourceRep(parser.parse()?))
-        } else if l.peek::<kw::thread_spawn>()? {
-            Ok(CoreFuncKind::ThreadSpawn(parser.parse()?))
+        } else if l.peek::<kw::thread_spawn_ref>()? {
+            Ok(CoreFuncKind::ThreadSpawnRef(parser.parse()?))
+        } else if l.peek::<kw::thread_spawn_indirect>()? {
+            Ok(CoreFuncKind::ThreadSpawnIndirect(parser.parse()?))
         } else if l.peek::<kw::thread_available_parallelism>()? {
             Ok(CoreFuncKind::ThreadAvailableParallelism(parser.parse()?))
         } else if l.peek::<kw::backpressure_set>()? {
@@ -471,16 +474,16 @@ impl<'a> Parse<'a> for CanonResourceRep<'a> {
     }
 }
 
-/// Information relating to the `thread.spawn` intrinsic.
+/// Information relating to the `thread.spawn_ref` intrinsic.
 #[derive(Debug)]
-pub struct CanonThreadSpawn<'a> {
+pub struct CanonThreadSpawnRef<'a> {
     /// The function type that is being spawned.
     pub ty: Index<'a>,
 }
 
-impl<'a> Parse<'a> for CanonThreadSpawn<'a> {
+impl<'a> Parse<'a> for CanonThreadSpawnRef<'a> {
     fn parse(parser: Parser<'a>) -> Result<Self> {
-        parser.parse::<kw::thread_spawn>()?;
+        parser.parse::<kw::thread_spawn_ref>()?;
 
         Ok(Self {
             ty: parser.parse()?,
@@ -488,6 +491,26 @@ impl<'a> Parse<'a> for CanonThreadSpawn<'a> {
     }
 }
 
+/// Information relating to the `thread.spawn_indirect` intrinsic.
+///
+/// This should look quite similar to parsing of `CallIndirect`.
+#[derive(Debug)]
+pub struct CanonThreadSpawnIndirect<'a> {
+    /// The function type that is being spawned.
+    pub ty: Index<'a>,
+    /// The table that this spawn is going to be indexing.
+    pub table: CoreItemRef<'a, kw::table>,
+}
+
+impl<'a> Parse<'a> for CanonThreadSpawnIndirect<'a> {
+    fn parse(parser: Parser<'a>) -> Result<Self> {
+        parser.parse::<kw::thread_spawn_indirect>()?;
+        let ty = parser.parse()?;
+        let table = parser.parens(|p| p.parse())?;
+        Ok(Self { ty, table })
+    }
+}
+
 /// Information relating to the `thread.spawn` intrinsic.
 #[derive(Debug)]
 pub struct CanonThreadAvailableParallelism;
@@ -540,10 +563,7 @@ impl<'a> Parse<'a> for CanonWaitableSetWait<'a> {
     fn parse(parser: Parser<'a>) -> Result<Self> {
         parser.parse::<kw::waitable_set_wait>()?;
         let async_ = parser.parse::<Option<kw::r#async>>()?.is_some();
-        let memory = parser.parens(|parser| {
-            let span = parser.parse::<kw::memory>()?.0;
-            parse_trailing_item_ref(kw::memory(span), parser)
-        })?;
+        let memory = parser.parens(|p| p.parse())?;
 
         Ok(Self { async_, memory })
     }
@@ -563,10 +583,7 @@ impl<'a> Parse<'a> for CanonWaitableSetPoll<'a> {
     fn parse(parser: Parser<'a>) -> Result<Self> {
         parser.parse::<kw::waitable_set_poll>()?;
         let async_ = parser.parse::<Option<kw::r#async>>()?.is_some();
-        let memory = parser.parens(|parser| {
-            let span = parser.parse::<kw::memory>()?.0;
-            parse_trailing_item_ref(kw::memory(span), parser)
-        })?;
+        let memory = parser.parens(|p| p.parse())?;
 
         Ok(Self { async_, memory })
     }
@@ -929,11 +946,7 @@ impl<'a> Parse<'a> for CanonOpt<'a> {
             parser.parens(|parser| {
                 let mut l = parser.lookahead1();
                 if l.peek::<kw::memory>()? {
-                    let span = parser.parse::<kw::memory>()?.0;
-                    Ok(CanonOpt::Memory(parse_trailing_item_ref(
-                        kw::memory(span),
-                        parser,
-                    )?))
+                    Ok(CanonOpt::Memory(parser.parse()?))
                 } else if l.peek::<kw::realloc>()? {
                     parser.parse::<kw::realloc>()?;
                     Ok(CanonOpt::Realloc(
@@ -981,14 +994,6 @@ impl Peek for CanonOpt<'_> {
     }
 }
 
-fn parse_trailing_item_ref<T>(kind: T, parser: Parser) -> Result<CoreItemRef<T>> {
-    Ok(CoreItemRef {
-        kind,
-        idx: parser.parse()?,
-        export_name: parser.parse()?,
-    })
-}
-
 impl<'a> Parse<'a> for Vec<CanonOpt<'a>> {
     fn parse(parser: Parser<'a>) -> Result<Self> {
         let mut funcs = Vec::new();
diff --git a/crates/wast/src/component/resolve.rs b/crates/wast/src/component/resolve.rs
index c5855f20..a8288a22 100644
--- a/crates/wast/src/component/resolve.rs
+++ b/crates/wast/src/component/resolve.rs
@@ -387,9 +387,13 @@ impl<'a> Resolver<'a> {
                 CoreFuncKind::ResourceDrop(info) => {
                     self.resolve_ns(&mut info.ty, Ns::Type)?;
                 }
-                CoreFuncKind::ThreadSpawn(info) => {
+                CoreFuncKind::ThreadSpawnRef(info) => {
                     self.resolve_ns(&mut info.ty, Ns::CoreType)?;
                 }
+                CoreFuncKind::ThreadSpawnIndirect(info) => {
+                    self.resolve_ns(&mut info.ty, Ns::CoreType)?;
+                    self.core_item_ref(&mut info.table)?;
+                }
                 CoreFuncKind::ThreadAvailableParallelism(_)
                 | CoreFuncKind::BackpressureSet
                 | CoreFuncKind::Yield(_)
@@ -1094,6 +1098,7 @@ component_item!(kw::module, CoreModule);
 
 core_item!(kw::func, CoreFunc);
 core_item!(kw::memory, CoreMemory);
+core_item!(kw::table, CoreTable);
 core_item!(kw::r#type, CoreType);
 core_item!(kw::r#instance, CoreInstance);
 
diff --git a/crates/wast/src/lib.rs b/crates/wast/src/lib.rs
index b7e314ab..acc4d775 100644
--- a/crates/wast/src/lib.rs
+++ b/crates/wast/src/lib.rs
@@ -554,7 +554,8 @@ pub mod kw {
     custom_keyword!(export_info = "export-info");
     custom_keyword!(import_info = "import-info");
     custom_keyword!(thread);
-    custom_keyword!(thread_spawn = "thread.spawn");
+    custom_keyword!(thread_spawn_ref = "thread.spawn_ref");
+    custom_keyword!(thread_spawn_indirect = "thread.spawn_indirect");
     custom_keyword!(thread_available_parallelism = "thread.available_parallelism");
     custom_keyword!(backpressure_set = "backpressure.set");
     custom_keyword!(task_return = "task.return");
diff --git a/src/bin/wasm-tools/dump.rs b/src/bin/wasm-tools/dump.rs
index 3b461fd2..ce1d61a5 100644
--- a/src/bin/wasm-tools/dump.rs
+++ b/src/bin/wasm-tools/dump.rs
@@ -419,7 +419,8 @@ impl<'a> Dump<'a> {
                             | CanonicalFunction::ResourceDrop { .. }
                             | CanonicalFunction::ResourceDropAsync { .. }
                             | CanonicalFunction::ResourceRep { .. }
-                            | CanonicalFunction::ThreadSpawn { .. }
+                            | CanonicalFunction::ThreadSpawnRef { .. }
+                            | CanonicalFunction::ThreadSpawnIndirect { .. }
                             | CanonicalFunction::ThreadAvailableParallelism
                             | CanonicalFunction::BackpressureSet
                             | CanonicalFunction::TaskReturn { .. }


Test patch:
diff --git a/tests/cli/missing-features/component-model/shared-everything-threads-builtins.wast b/tests/cli/missing-features/component-model/shared-everything-threads-builtins.wast
index 42c2fd0f..0d159c45 100644
--- a/tests/cli/missing-features/component-model/shared-everything-threads-builtins.wast
+++ b/tests/cli/missing-features/component-model/shared-everything-threads-builtins.wast
@@ -3,10 +3,18 @@
 (assert_invalid
   (component
     ;; Refer to a non-existent function type (i.e., 0); validation for
-    ;; `thread.spawn` happens first.
-    (core func $spawn (canon thread.spawn 0))
+    ;; `thread.spawn_ref` happens first.
+    (core func $spawn (canon thread.spawn_ref 0))
   )
-  "`thread.spawn` requires the shared-everything-threads proposal")
+  "`thread.spawn_ref` requires the shared-everything-threads proposal")
+
+(assert_invalid
+  (component
+    ;; Refer to a non-existent function and table types (i.e., 0); validation
+    ;; for `thread.spawn_indirect` happens first.
+    (core func $spawn_indirect (canon thread.spawn_indirect 0 (table 0)))
+  )
+  "`thread.spawn_indirect` requires the shared-everything-threads proposal")
 
 (assert_invalid
   (component
diff --git a/tests/cli/shared-everything-threads/builtins.wast b/tests/cli/shared-everything-threads/builtins.wast
index 91826df4..80804333 100644
--- a/tests/cli/shared-everything-threads/builtins.wast
+++ b/tests/cli/shared-everything-threads/builtins.wast
@@ -4,24 +4,36 @@
 
 (component
   (core type $start (shared (func (param $context i32))))
-  (core func $spawn (canon thread.spawn $start))
+  (core func $spawn_ref (canon thread.spawn_ref $start))
+
+  (core module $libc (table (export "start-table") 1 (ref null (shared func))))
+  (core instance $libc (instantiate $libc))
+  (core func $spawn_indirect (canon thread.spawn_indirect $start (table $libc "start-table")))
+
   (core func $parallelism (canon thread.available_parallelism))
 )
 
 (component
   (core type $start (shared (func (param $context i32))))
-  (core func $spawn (canon thread.spawn $start))
+  (core func $spawn_ref (canon thread.spawn_ref $start))
+
+  (core module $libc (table (export "start-table") 1 (ref null (shared func))))
+  (core instance $libc (instantiate $libc))
+  (core func $spawn_indirect (canon thread.spawn_indirect $start (table $libc "start-table")))
+
   (core func $parallelism (canon thread.available_parallelism))
 
   (core module $m
     (type $st (shared (func (param $context i32))))
-    (import "" "spawn" (func (param (ref null $st)) (param i32) (result i32)))
+    (import "" "spawn_ref" (func (param (ref null $st)) (param i32) (result i32)))
+    (import "" "spawn_indirect" (func (param i32) (param i32) (result i32)))
     (import "" "parallelism" (func (result i32)))
   )
 
   (core instance (instantiate $m
     (with "" (instance
-      (export "spawn" (func $spawn))
+      (export "spawn_ref" (func $spawn_ref))
+      (export "spawn_indirect" (func $spawn_indirect))
       (export "parallelism" (func $parallelism))
     ))
   ))
@@ -30,7 +42,17 @@
 (assert_invalid
   (component
     (core type $start (func))
-    (core func $spawn (canon thread.spawn $start))
+    (core func $spawn_ref (canon thread.spawn_ref $start))
+  )
+  "spawn type must be shared"
+)
+
+(assert_invalid
+  (component
+    (core type $start (func))
+    ;; Refer to a non-existent table type (i.e., 0); validation
+    ;; for `thread.spawn_indirect` happens first.
+    (core func $spawn_indirect (canon thread.spawn_indirect $start (table 0)))
   )
   "spawn type must be shared"
 )
@@ -38,7 +60,36 @@
 (assert_invalid
   (component
     (core type $start (shared (func)))
-    (core func $spawn (canon thread.spawn $start))
+    (core func $spawn_ref (canon thread.spawn_ref $start))
   )
   "spawn function must take a single `i32` argument (currently)"
 )
+
+(assert_invalid
+  (component
+    (core type $start (shared (func)))
+    (core func $spawn_indirect (canon thread.spawn_indirect $start (table 0)))
+  )
+  "spawn function must take a single `i32` argument (currently)"
+)
+
+(assert_invalid
+  (component
+    (core type $start (shared (func (param i32))))
+    (core func $spawn_indirect (canon thread.spawn_indirect $start (table 0)))
+  )
+  "unknown table"
+)
+
+(assert_invalid
+  (component
+    (core type $start (shared (func (param i32))))
+    (core module $libc (table (export "start-table") 1 funcref))
+    (core instance $libc (instantiate $libc))
+    (core func $spawn_indirect (canon thread.spawn_indirect $start (table $libc "start-table")))
+  )
+  "expected a table of shared `funcref`"
+)
+
+;; TODO: add a test confirming that an unshared table is invalid once components
+;; allow shared tables; see `not-accepted.wast`.
diff --git a/tests/snapshots/cli/missing-features/component-model/shared-everything-threads-builtins.wast.json b/tests/snapshots/cli/missing-features/component-model/shared-everything-threads-builtins.wast.json
index 1973e992..6ec68759 100644
--- a/tests/snapshots/cli/missing-features/component-model/shared-everything-threads-builtins.wast.json
+++ b/tests/snapshots/cli/missing-features/component-model/shared-everything-threads-builtins.wast.json
@@ -6,13 +6,20 @@
       "line": 4,
       "filename": "shared-everything-threads-builtins.0.wasm",
       "module_type": "binary",
-      "text": "`thread.spawn` requires the shared-everything-threads proposal"
+      "text": "`thread.spawn_ref` requires the shared-everything-threads proposal"
     },
     {
       "type": "assert_invalid",
       "line": 12,
       "filename": "shared-everything-threads-builtins.1.wasm",
       "module_type": "binary",
+      "text": "`thread.spawn_indirect` requires the shared-everything-threads proposal"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 20,
+      "filename": "shared-everything-threads-builtins.2.wasm",
+      "module_type": "binary",
       "text": "`thread.available_parallelism` requires the shared-everything-threads proposal"
     }
   ]
diff --git a/tests/snapshots/cli/shared-everything-threads/builtins.wast.json b/tests/snapshots/cli/shared-everything-threads/builtins.wast.json
index f0a374c4..42e3085c 100644
--- a/tests/snapshots/cli/shared-everything-threads/builtins.wast.json
+++ b/tests/snapshots/cli/shared-everything-threads/builtins.wast.json
@@ -9,23 +9,51 @@
     },
     {
       "type": "module",
-      "line": 11,
+      "line": 16,
       "filename": "builtins.1.wasm",
       "module_type": "binary"
     },
     {
       "type": "assert_invalid",
-      "line": 31,
+      "line": 43,
       "filename": "builtins.2.wasm",
       "module_type": "binary",
       "text": "spawn type must be shared"
     },
     {
       "type": "assert_invalid",
-      "line": 39,
+      "line": 51,
       "filename": "builtins.3.wasm",
       "module_type": "binary",
+      "text": "spawn type must be shared"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 61,
+      "filename": "builtins.4.wasm",
+      "module_type": "binary",
+      "text": "spawn function must take a single `i32` argument (currently)"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 69,
+      "filename": "builtins.5.wasm",
+      "module_type": "binary",
       "text": "spawn function must take a single `i32` argument (currently)"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 77,
+      "filename": "builtins.6.wasm",
+      "module_type": "binary",
+      "text": "unknown table"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 85,
+      "filename": "builtins.7.wasm",
+      "module_type": "binary",
+      "text": "expected a table of shared `funcref`"
     }
   ]
 }
\ No newline at end of file
diff --git a/tests/snapshots/cli/shared-everything-threads/builtins.wast/0.print b/tests/snapshots/cli/shared-everything-threads/builtins.wast/0.print
index ce876bb6..0c111952 100644
--- a/tests/snapshots/cli/shared-everything-threads/builtins.wast/0.print
+++ b/tests/snapshots/cli/shared-everything-threads/builtins.wast/0.print
@@ -1,5 +1,12 @@
 (component
   (core type $start (;0;) (shared (func (param i32))))
-  (core func $spawn (;0;) (canon thread.spawn $start))
-  (core func $parallelism (;1;) (canon thread.available_parallelism))
+  (core func $spawn_ref (;0;) (canon thread.spawn_ref $start))
+  (core module $libc (;0;)
+    (table (;0;) 1 (ref null (shared func)))
+    (export "start-table" (table 0))
+  )
+  (core instance $libc (;0;) (instantiate $libc))
+  (alias core export $libc "start-table" (core table (;0;)))
+  (core func $spawn_indirect (;1;) (canon thread.spawn_indirect $start (table 0)))
+  (core func $parallelism (;2;) (canon thread.available_parallelism))
 )
diff --git a/tests/snapshots/cli/shared-everything-threads/builtins.wast/1.print b/tests/snapshots/cli/shared-everything-threads/builtins.wast/1.print
index fdd383db..6beff1ca 100644
--- a/tests/snapshots/cli/shared-everything-threads/builtins.wast/1.print
+++ b/tests/snapshots/cli/shared-everything-threads/builtins.wast/1.print
@@ -1,20 +1,30 @@
 (component
   (core type $start (;0;) (shared (func (param i32))))
-  (core func $spawn (;0;) (canon thread.spawn $start))
-  (core func $parallelism (;1;) (canon thread.available_parallelism))
-  (core module $m (;0;)
+  (core func $spawn_ref (;0;) (canon thread.spawn_ref $start))
+  (core module $libc (;0;)
+    (table (;0;) 1 (ref null (shared func)))
+    (export "start-table" (table 0))
+  )
+  (core instance $libc (;0;) (instantiate $libc))
+  (alias core export $libc "start-table" (core table (;0;)))
+  (core func $spawn_indirect (;1;) (canon thread.spawn_indirect $start (table 0)))
+  (core func $parallelism (;2;) (canon thread.available_parallelism))
+  (core module $m (;1;)
     (type $st (;0;) (shared (func (param i32))))
     (type (;1;) (func (param (ref null $st) i32) (result i32)))
-    (type (;2;) (func (result i32)))
-    (import "" "spawn" (func (;0;) (type 1)))
-    (import "" "parallelism" (func (;1;) (type 2)))
+    (type (;2;) (func (param i32 i32) (result i32)))
+    (type (;3;) (func (result i32)))
+    (import "" "spawn_ref" (func (;0;) (type 1)))
+    (import "" "spawn_indirect" (func (;1;) (type 2)))
+    (import "" "parallelism" (func (;2;) (type 3)))
   )
-  (core instance (;0;)
-    (export "spawn" (func $spawn))
+  (core instance (;1;)
+    (export "spawn_ref" (func $spawn_ref))
+    (export "spawn_indirect" (func $spawn_indirect))
     (export "parallelism" (func $parallelism))
   )
-  (core instance (;1;) (instantiate $m
-      (with "" (instance 0))
+  (core instance (;2;) (instantiate $m
+      (with "" (instance 1))
     )
   )
 )