Skip to content

feat: export generic structs/impls to TypeScript via instantiate(...)#1

Open
rossng wants to merge 3 commits into
mainfrom
rossng/instantiate-generics-v2
Open

feat: export generic structs/impls to TypeScript via instantiate(...)#1
rossng wants to merge 3 commits into
mainfrom
rossng/instantiate-generics-v2

Conversation

@rossng

@rossng rossng commented Jun 11, 2026

Copy link
Copy Markdown

Summary

Adds #[wasm_bindgen(instantiate(...))]definition-site monomorphization that lets #[wasm_bindgen] be applied to generic structs and impls, which stock wasm-bindgen rejects outright. One generic definition stamps out one concrete exported JS class per listed instantiation.

This is the upstream-side of removing a large class of hand-monomorphization / type-erasure boilerplate in our Rust→WASM crates (the instantiate-generics work).

What's in it

Three composable capabilities, built as three commits on top of current upstream main (0.2.123):

  1. instantiate(Foo<A> as FooA, Foo<B> as FooB) attribute — export a generic struct + generic impl as one concrete JS class per instantiation. Type-parameter-independent fields become shared auto-getters; #[wasm_bindgen(skip)] opts a field out.

  2. Per-instantiation concrete impl blocks#[wasm_bindgen(js_class = "FooB")] impl Foo<B> { ... } attaches methods to a single instantiation's exported class (Self is rewritten to the concrete type). Unblocks typestate generics: methods that only type-check for one instantiation live on concrete impls while shared surface stays generic.

  3. TS-level type parametersinstantiate(Foo<JsValue> as Foo<P>) exports one erased runtime class whose generated .d.ts is the generic class Foo<P = any>: type-parameter positions are typed P, Self positions Foo<P>. typescript_type_params = "Q" composes with unchecked_return_type / unchecked_param_type for cases like map<Q>(f: (p: P) => Q): Foo<Q>. Unblocks JS-side type erasure with a TS declaration generated from the Rust signatures instead of hand-maintained.

Schema bump

This changes the bindgen schema, so the CLI must be built from this branch — the stock CLI rejects the instantiate schema with a version mismatch. (crates/shared schema hash updated accordingly.)

Validation

wbg-generics-test/ is a standalone end-to-end crate (detached from the workspace) exercising all three capabilities from a Node runtime:

  • Holder<P> / Pair<A, B> — basic monomorphized exports (capability 1)
  • Check<D> / Plan<D> with a GAT-based EvalMode — typestate generics (capability 2)
  • Frame<P> erased to JsValue with a generic .d.ts — TS-level type params (capability 3)

Verified locally: build the CLI from this branch, build the crate to wasm32-unknown-unknown, run wasm-bindgen, node test.mjs → all assertion groups pass.

Base

Targets this fork's main (now fast-forwarded to current upstream 4f17390b5), not rustwasm upstream.

🤖 Generated with Claude Code

rossng and others added 3 commits June 10, 2026 17:30
Adds a `#[wasm_bindgen(instantiate(...))]` attribute that monomorphizes
generic structs and their impls into concrete instantiations exported to
JS. Tracks rust_generics on Struct/StructField/Export, threads
generics/instantiate params through ClassMarker, and emits concrete type
params (e.g. Tr::<f64>) in codegen instead of bare idents.

Updates invalid-generics/invalid-items ui-test stderr expectations.
…ructs

#[wasm_bindgen(js_class = "FooDebug")] impl Foo<Evaluated> { ... } attaches
methods to one declared instantiation's exported class. Self is rewritten to
the concrete type and shims carry the instantiation's generic arguments.
This unblocks typestate generics (uniform impls that cannot type-check for
every instantiation) by letting per-state methods live on concrete impls.

Also restores a loud error for concrete-args impls without js_class (the
instantiate feature had made them silently strip their arguments).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
instantiate(Tr<JsValue> as Tr<P>) exports one erased runtime class whose
generated .d.ts is generic: class Tr<P = any> with P in the signature
positions whose original Rust type was the corresponding type parameter
(or Self / Tr<P>). Statics redeclare used class params method-level
(TS statics cannot reference class type params). A new free-form
typescript_type_params = "Q" attribute declares method-level TS params,
composing with unchecked_return_type/unchecked_param_type for cases like
map<Q>(f: (p: P) => Q): Tr<Q>.

Schema: Struct.ts_params + Function.ts_params; SCHEMA_VERSION bumped to
0.2.122-mnml.1 so a stock CLI fails loudly instead of misdecoding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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