Skip to content

feat(macro): export generic structs via instantiate()#2

Open
rossng wants to merge 5 commits into
mainfrom
rossng/instantiate-generics-pr1
Open

feat(macro): export generic structs via instantiate()#2
rossng wants to merge 5 commits into
mainfrom
rossng/instantiate-generics-pr1

Conversation

@rossng

@rossng rossng commented Jun 11, 2026

Copy link
Copy Markdown

Adds #[wasm_bindgen(instantiate(...))] for exporting generic structs to JavaScript. This is the first of three incremental PRs; it covers the struct surface only, with impl methods/constructors and TypeScript-level type parameters to follow.

What this does

instantiate(...) monomorphizes a generic struct into one concrete JS class per listed instantiation, written as Type<Args> as JsName:

#[wasm_bindgen(instantiate(Pair<f64> as PairF64, Pair<u32> as PairU32))]
pub struct Pair<T> {
    pub first: T,
    pub second: T,
}

Each instantiation is emitted as an independent ast::Struct with the type parameter substituted throughout its fields, producing a distinct exported class with its own ABI conversions and public-field accessors. Because the struct macro is a single expansion that emits both the class and its field accessors, the monomorphized field types cannot diverge from the class they belong to.

Scope

Instances cross the boundary via free functions; public fields are read and written directly from JS. Exporting impl methods/constructors on an instantiation and propagating TypeScript type parameters are deliberately out of scope here and handled in follow-up PRs.

Validation rejects:

  • non-type (lifetime/const) parameters
  • arity mismatches between the instantiation and the struct's parameters
  • duplicate or JS-keyword class names
  • js_name combined with instantiate
  • instantiations that don't name the struct being instantiated

Tests & docs

  • crates/macro/ui-tests/instantiate-struct — compile-fail coverage of every validation path. The messages are wasm-bindgen diagnostics, so the snapshots are toolchain-stable.
  • tests/wasm/instantiate — runtime round-trips over two instantiations, field read/write from JS, and class-distinctness (instanceof) checks.
  • Guide reference page under on-rust-exports, plus updated invalid-generics/invalid-items stderr expectations.
  • cargo fmt applied.

A crates/cli/tests/reference/ case was intentionally not included: the harness compares .wat exactly, and that output is toolchain-specific. It should be added by regenerating (BLESS=1) in CI's environment so the committed .wat matches.

Verification

macro-support builds with no warnings; 21 macro-support unit tests pass; the full workspace builds; the ui-cases produce clean located errors; the runtime tests pass; changed files are rustfmt-clean.

🤖 Generated with Claude Code

rossng added 5 commits June 12, 2026 00:19
Preparation for exporting monomorphized generic structs: add an optional
`rust_generics` slot to `ast::Struct` (and the mirroring `struct_generics` on
`ast::StructField`) holding the concrete generic arguments in turbofish form,
and emit them in codegen so the generated type is `Foo #generics` rather than
the bare ident.

No behavior change: every construction site sets these to `None`, so the
emitted code for ordinary non-generic structs is byte-for-byte identical. The
next commit populates the slot from a new `instantiate(...)` attribute.
Adds `#[wasm_bindgen(instantiate(...))]`, which monomorphizes a generic struct
into one concrete JS class per listed instantiation, written as
`Type<Args> as JsName`:

    #[wasm_bindgen(instantiate(Pair<f64> as PairF64, Pair<u32> as PairU32))]
    pub struct Pair<T> { pub first: T, pub second: T }

The attribute is parsed into a list of `Instantiation`s; each is validated and
turned into an `InstantiationTarget` carrying the JS name, the concrete
turbofish arguments, and a `T -> f64` (plus `Self`) substitution map. The
struct conversion now returns one `ast::Struct` per target, substituting the
type parameter throughout every field type and populating the `rust_generics`
slot added in the previous commit. Each instantiation becomes an independent
exported class with its own ABI conversions and public-field accessors.

Because the struct macro is a single expansion that emits both the class and
its field accessors, the monomorphized field types cannot diverge from the
class they belong to.

Scope is limited to the struct surface: instances cross the boundary via free
functions, and public fields are read/written directly from JS. Validation
rejects non-type (lifetime/const) parameters, arity mismatches, duplicate or
JS-keyword class names, `js_name` combined with `instantiate`, and
instantiations that don't name the struct.
Adds a trybuild ui-test exercising every rejection path of the struct
`instantiate(...)` attribute: a non-generic struct, duplicate JS names, an
arity mismatch, a const generic parameter, `js_name` combined with
`instantiate`, an instantiation naming a different type, and a JS-keyword
class name. All assertions are wasm-bindgen's own diagnostics, so the
snapshots are stable across compiler versions.
Adds a `tests/wasm` case that builds a real Wasm module from a single
`Pair<T>` monomorphized into `PairF64` and `PairI32`. It round-trips each
instantiation through JS, reads and writes the public fields from JS, and uses
`instanceof` to confirm the two instantiations are genuinely distinct classes.
Adds a guide reference page under on-rust-exports describing `instantiate(...)`
— the syntax, the generated classes, producing/consuming instances via free
functions, and the validation rules — and links it from the summary.
@rossng rossng force-pushed the rossng/instantiate-generics-pr1 branch from 5abd092 to c82467b Compare June 11, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant