Stream: git-wasmtime

Topic: wasmtime / issue #10176 Add embedder APIs for defining re...


view this post on Zulip Wasmtime GitHub notifications bot (Feb 03 2025 at 17:54):

fitzgen added the wasmtime:api label to Issue #10176.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 03 2025 at 17:54):

fitzgen added the wasm-proposal:gc label to Issue #10176.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 03 2025 at 17:54):

fitzgen opened issue #10176:

While we provide embedder APIs for creating one-off function/array/struct types, we don't currently have a way to define a set of types inside a rec group. This means the embedder cannot define a type that has a reference to itself, or any mutually recursive types. It also means that if a Wasm module uses such a type in its imports or exports, the embedder must find it and pluck it out via Module::{imports,exports} rather than simply create it themselves when they are trying to define functions that take/return those types.

This shouldn't be hard to implement, but it will require a bit of API design work that we want to make sure we are happy with, and that we think will extend well for Wasm's likely future type system extensions, before we commit to anything.

Random, half-baked thoughts:

view this post on Zulip Wasmtime GitHub notifications bot (Jun 18 2026 at 11:14):

somdoron commented on issue #10176:

I would like to try and tackle this, see proposed design below:

Proposed design: RecGroupBuilder + minimal "template" types

The core idea follows the "labels in a compiler" approach suggested above: you declare a type to get a Copy, kind-typed handle (a label), use that handle as a forward reference while
defining other types, then define it later. Everything is validated in one place when the rec group is finished.

A key goal is keeping the existing runtime types (StructType, RefType, HeapType, ValType, …) exactly as they are: always valid, always registered. Forward references live in a small,
build-time-only "template" family that mirrors the real type hierarchy one-to-one, so it's a compile-time error to use an unfinished type where a finished one is required (no runtime
"pending" state that panics).

Kind-typed labels

Labels carry their kind, which makes a whole class of mistakes unrepresentable:

#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct PendingStructId(/* .. */);
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct PendingArrayId(/* .. */);
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct PendingFuncId(/* .. */);

The kind is preserved everywhere — definition, ref target, supertype, getter — and checked by the compiler.

The template family mirrors the type hierarchy

A template can express exactly one thing a concrete type can't: a ref to a sibling being defined in the same rec group. The leaf is a "pending heap type" that mirrors HeapType's concrete
variants, with one extra local variant per kind:

/// Mirror of `HeapType`, but a concrete target may be a not-yet-defined sibling.
pub enum HeapTypeTemplate {
    Type(HeapType),                  // abstract (`any`, `eq`, …) or already-registered concrete
    LocalStruct(PendingStructId),
    LocalArray(PendingArrayId),
    LocalFunc(PendingFuncId),
}

/// Mirror of `ValType`.
pub enum ValTypeTemplate {
    Type(ValType),                                   // i32/.../v128 or a *concrete* ref
    Ref { nullable: bool, heap: HeapTypeTemplate },  // ref whose target may be local
}

/// Mirror of `StorageType` + a field's mutability.
pub struct FieldTemplate { /* mutability + element */ }
pub enum StorageTypeTemplate {
    Type(StorageType),                               // i8/i16 or concrete StorageType
    Ref { nullable: bool, heap: HeapTypeTemplate },
}

Concrete, already-known types flow in transparently via From/Into, so the no-forward-reference path looks identical to today's API, and a label drops straight into a ref target:

impl From<HeapType>       for HeapTypeTemplate {}   // abstract or concrete
impl From<StructType>     for HeapTypeTemplate {}   // convenience: -> Type(ConcreteStruct)
impl From<ArrayType>      for HeapTypeTemplate {}
impl From<FuncType>       for HeapTypeTemplate {}
impl From<PendingStructId> for HeapTypeTemplate {}  // -> LocalStruct
impl From<PendingArrayId>  for HeapTypeTemplate {}
impl From<PendingFuncId>   for HeapTypeTemplate {}

impl From<ValType>        for ValTypeTemplate {}
impl From<StorageType>    for StorageTypeTemplate {}
impl From<ValType>        for StorageTypeTemplate {}
impl From<FieldType>      for FieldTemplate {}       // pass plain FieldTypes when no fwd refs

impl FieldTemplate {
    pub fn new(m: Mutability, e: impl Into<StorageTypeTemplate>) -> Self;
    // ref target may be a label, an abstract heap type, or a registered type:
    pub fn ref_(m: Mutability, nullable: bool, heap: impl Into<HeapTypeTemplate>) -> Self;
}

The rule for embedders is simple: have the type already? .into() it. Don't have it yet? use a typed label.

Supertypes are kind-specific

A supertype is constrained to the same kind as the type being defined, and may itself be either a sibling label or an already-registered type:

pub enum StructSuperType { Local(PendingStructId), Type(StructType) }
pub enum ArraySuperType  { Local(PendingArrayId),  Type(ArrayType)  }
pub enum FuncSuperType   { Local(PendingFuncId),   Type(FuncType)   }

impl From<PendingStructId> for StructSuperType {}
impl From<StructType>      for StructSuperType {}
// ... and likewise for Array / Func

A struct's supertype can only ever be a struct (label or registered) — passing a PendingArrayId or an ArrayType is a compile error.

The builder

pub struct RecGroupBuilder { /* engine + members */ }

impl RecGroupBuilder {
    pub fn new(engine: &Engine) -> Self;

    // forward declarations (kind-typed):
    pub fn declare_struct(&mut self) -> PendingStructId;
    pub fn declare_array(&mut self)  -> PendingArrayId;
    pub fn declare_func(&mut self)   -> PendingFuncId;

    // declare + define sugar for the non-recursive case:
    pub fn add_struct(&mut self, fields) -> PendingStructId;

    // define a previously-declared label; the id's type fixes the kind:
    pub fn define_struct(&mut self, id: PendingStructId, fields: impl IntoIterator<Item = impl Into<FieldTemplate>>) -> &mut Self;
    pub fn define_array (&mut self, id: PendingArrayId,  field:  impl Into<FieldTemplate>) -> &mut Self;
    pub fn define_func  (&mut self, id: PendingFuncId,   params, results) -> &mut Self;

    // finality + supertype, constrained to the matching kind via `impl Into`:
    pub fn define_struct_with_finality_and_supertype(
        &mut self,
        id: PendingStructId,
        finality: Finality,
        supertype: Option<impl Into<StructSuperType>>,   // StructType or PendingStructId
        fields: impl IntoIterator<Item = impl Into<FieldTemplate>>,
    ) -> &mut Self;
    // ... array/func equivalents

    pub fn build(self) -> Result<RecGroup>;        // single point of validation
}

pub struct RecGroup { /* registered members */ }
impl RecGroup {
    // infallible getters — the label's type proves the kind:
    pub fn struct_(&self, id: PendingStructId) -> StructType;
    pub fn array  (&self, id: PendingArrayId)  -> ArrayType;
    pub fn func   (&self, id: PendingFuncId)   -> FuncType;
    pub fn types(&self) -> impl ExactSizeIterator<Item = CompositeType> + '_;  // {Struct, Array, Func}
}

build() is the only fallible point. It validates that every declared label was defined, field/param counts are within limits, supertypes are non-final and structurally matched, and all
referenced types belong to this engine. (Using a label from a different builder on a RecGroup's getter is a programmer error and panics like an out-of-bounds index.)

Example: mutual recursion

let mut b = RecGroupBuilder::new(&engine);

let s1 = b.declare_struct();
let s2 = b.declare_struct();

b.define_struct(s1, [
    FieldTemplate::ref_(Mutability::Var, /* nullable */ true, s2),   // (ref null s2)
]);
b.define_struct(s2, [
    FieldTemplate::ref_(Mutability::Const, /* nullable */ false, s1), // (ref s1)
]);

let group = b.build()?;                       // validates + registers
let s1: StructType = group.struct_(s1);       // infallible
let s2: StructType = group.struct_(s2);

Example: mixing a forward reference with an already-registered type

let mut b = RecGroupBuilder::new(&engine);

let node = b.declare_struct();                 // sibling, defined below
let existing: StructType = /* e.g. plucked from a module's exports */;

b.define_struct(node, [
    // forward ref to the sibling `node`     ->  (ref null <node>)
    FieldTemplate::ref_(Mutability::Var, true, node),

    // ref to an already-registered type     ->  (ref <existing>)
    FieldTemplate::ref_(Mutability::Const, false, existing.clone()),

    // a plain scalar field
    FieldTemplate::new(Mutability::Var, StorageType::I64),
]);

let group = b.build()?;
let node: StructType = group.struct_(node);

Example: a supertype that is a sibling or an already-registered type

let mut b = RecGroupBuilder::new(&engine);

// supertype is a sibling defined in this same group:
let base = b.declare_struct();
let derived = b.declare_struct();
b.define_struct(base, [FieldTemplate::new(Mutability::Const, StorageType::I32)]);
b.define_struct_with_finality_and_supertype(
    derived, Finality::Final, Some(base),                     // PendingStructId
    [FieldTemplate::new(Mutability::Const, StorageType::I32)],
);

// supertype is an already-registered struct:
let existing: StructType = /* ... */;
let sub = b.declare_struct();
b.define_struct_with_finality_and_supertype(
    sub, Finality::Final, Some(existing),                     // StructType
    /* fields matching `existing` ... */ [],
);

let group = b.build()?;

Under the hood, a concrete reference (existing) is an inter-group edge and is reference-counted to stay registered while building; a label reference is an intra-group edge. That
distinction is invisible to embedders.

Notes / addressing the wishlist

view this post on Zulip Wasmtime GitHub notifications bot (Jun 18 2026 at 16:36):

fitzgen commented on issue #10176:

@somdoron please do not dump AI text into our issues. You can review our AI tool policy here: https://github.com/bytecodealliance/governance/blob/main/AI_TOOL_POLICY.md

view this post on Zulip Wasmtime GitHub notifications bot (Jun 18 2026 at 17:02):

somdoron edited a comment on issue #10176:

I would like to try and tackle this, see proposed design below:

Proposed: RecGroupBuilder

Declare typed labels, define them, then build() validates and registers the whole group at once. Existing types (StructType, …) stay unchanged. Labels are Copy and kind-typed, so passing an array where a struct is expected is a compile error. Concrete types flow in via Into; forward references use a label.

impl RecGroupBuilder {
    pub fn new(engine: &Engine) -> Self;
    pub fn declare_struct(&mut self) -> PendingStructId;   // + array/func
    pub fn define_struct(&mut self, id: PendingStructId,
        fields: impl IntoIterator<Item = impl Into<FieldTemplate>>) -> &mut Self;
    pub fn build(self) -> Result<RecGroup>;                // only fallible point
}

impl RecGroup {
    pub fn struct_(&self, id: PendingStructId) -> StructType;   // infallible
}

let mut b = RecGroupBuilder::new(&engine);
let (s1, s2) = (b.declare_struct(), b.declare_struct());
b.define_struct(s1, [FieldTemplate::ref_(Mutability::Var, true, s2)]);    // (ref null s2)
b.define_struct(s2, [FieldTemplate::ref_(Mutability::Const, false, s1)]); // (ref s1)
let g = b.build()?;
let (s1, s2) = (g.struct_(s1), g.struct_(s2));

view this post on Zulip Wasmtime GitHub notifications bot (Jun 18 2026 at 17:04):

somdoron commented on issue #10176:

@fitzgen sorry for that, I edited the message with the proposed API and with AI only proofing.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 18 2026 at 17:04):

somdoron edited a comment on issue #10176:

I would like to try and tackle this, see proposed design below:

Proposed: RecGroupBuilder

Declare typed labels, define them, then build() validates and registers the whole group at once. Existing types (StructType, …) stay unchanged. Labels are Copy and kind-typed, so passing an array where a struct is expected is a compile error. Concrete types flow in via Into. Forward references use a label.

impl RecGroupBuilder {
    pub fn new(engine: &Engine) -> Self;
    pub fn declare_struct(&mut self) -> PendingStructId;   // + array/func
    pub fn define_struct(&mut self, id: PendingStructId,
        fields: impl IntoIterator<Item = impl Into<FieldTemplate>>) -> &mut Self;
    pub fn build(self) -> Result<RecGroup>;                // only fallible point
}

impl RecGroup {
    pub fn struct_(&self, id: PendingStructId) -> StructType;   // infallible
}

let mut b = RecGroupBuilder::new(&engine);
let (s1, s2) = (b.declare_struct(), b.declare_struct());
b.define_struct(s1, [FieldTemplate::ref_(Mutability::Var, true, s2)]);    // (ref null s2)
b.define_struct(s2, [FieldTemplate::ref_(Mutability::Const, false, s1)]); // (ref s1)
let g = b.build()?;
let (s1, s2) = (g.struct_(s1), g.struct_(s2));

view this post on Zulip Wasmtime GitHub notifications bot (Jun 18 2026 at 17:06):

somdoron edited a comment on issue #10176:

I would like to try and tackle this, see proposed design below:

Proposed: RecGroupBuilder

Declare labels for the types you want, define them (referencing each other freely), then build() validates and registers the whole group at once.

impl RecGroupBuilder {
    pub fn new(engine: &Engine) -> Self;
    pub fn declare_struct(&mut self) -> PendingStructId;   // + array/func
    pub fn define_struct(&mut self, id: PendingStructId,
        fields: impl IntoIterator<Item = impl Into<FieldTemplate>>) -> &mut Self;
    pub fn build(self) -> Result<RecGroup>;                // only fallible point
}

impl RecGroup {
    pub fn struct_(&self, id: PendingStructId) -> StructType;   // infallible
}

let mut b = RecGroupBuilder::new(&engine);
let (s1, s2) = (b.declare_struct(), b.declare_struct());
b.define_struct(s1, [FieldTemplate::ref_(Mutability::Var, true, s2)]);    // (ref null s2)
b.define_struct(s2, [FieldTemplate::ref_(Mutability::Const, false, s1)]); // (ref s1)
let g = b.build()?;
let (s1, s2) = (g.struct_(s1), g.struct_(s2));


Last updated: Jul 29 2026 at 05:03 UTC