Skip to content

Iterable / iterator support and generic type parameter propagation#12

Closed
guybedford wants to merge 1 commit into
wasm-bindgen:mainfrom
guybedford:iterables-and-generics
Closed

Iterable / iterator support and generic type parameter propagation#12
guybedford wants to merge 1 commit into
wasm-bindgen:mainfrom
guybedford:iterables-and-generics

Conversation

@guybedford
Copy link
Copy Markdown
Contributor

@guybedford guybedford commented Apr 29, 2026

Adds first-class handling for the TS iterability protocol and bare generic type parameters in method signatures, so APIs like SyncKvStorage.put<T>(key: string, value: T) and SyncKvStorage.list<T>(): Iterable<[string, T]> round-trip into typed wasm-bindgen externs without erasing the generics.

Iterators

Iterator<T> and IterableIterator<T> map directly to js_sys::Iterator<T>; AsyncIterator<T> and AsyncIterableIterator<T> map to js_sys::AsyncIterator<T>. New IR variants TypeRef::Iterator and TypeRef::AsyncIterator emit through the existing generic container machinery alongside Promise<T>.

Iterable wrapper synthesis

Iterable<T> describes the protocol — an object exposing [Symbol.iterator](): Iterator<T> — distinct from Iterator<T> itself. To preserve that at the binding layer, top-level Iterable<T> returns now synthesize a wrapper extern type:

interface SyncKvStorage {
  list<T>(): Iterable<[string, T]>;
}

becomes:

pub type SyncKvStorageList<T: ::wasm_bindgen::JsGeneric>;
#[wasm_bindgen(method, js_name = "[Symbol.iterator]")]
pub fn iterator<T: ::wasm_bindgen::JsGeneric>(
    this: &SyncKvStorageList<T>,
) -> Iterator<ArrayTuple<(JsString, T)>>;

#[wasm_bindgen(method)]
pub fn list<T: ::wasm_bindgen::JsGeneric>(this: &SyncKvStorage) -> SyncKvStorageList<T>;

Naming follows the existing <Parent><Member> convention with dedup, mirroring anonymous-interface parameter synthesis. AsyncIterable<T> synthesizes the analogous wrapper keyed on [Symbol.asyncIterator]. The bracketed js_name matches wasm-bindgen's computed-property syntax for symbol-keyed methods. Nested occurrences inside unions or arrays are not synthesized — they erase to JsValue, matching the existing parameter-synthesis limitation.

Generic type parameter propagation

Bare TS generics on methods now survive codegen as <T: ::wasm_bindgen::JsGeneric> declarations rather than being erased to JsValue:

  • New TypeRef::TypeParam(String) IR variant for in-scope generic type parameter references.
  • convert_ts_type_scoped returns TypeParam(name) instead of Any for names in the method's type-parameter scope.
  • convert_formal_params_with_synthesis threads scope through to parameter type conversion (previously called convert_ts_type, which ignored scope — a pre-existing bug that erased every generic argument type to JsValue and emitted dummy use JsValue as T; aliases).
  • Method codegen collects every TypeParam reachable from the signature and emits the bound declaration. Synthesized iterable wrappers propagate any TypeParam from their item type onto the wrapper type itself.
  • pub type X<T, …> declarations carry their generics so type aliases that mention generic parameters compile.

Before:

#[allow(dead_code)]
use JsValue as T;
#[wasm_bindgen(method)]
pub fn put(this: &SyncKvStorage, key: &str, value: &T);

After:

#[wasm_bindgen(method)]
pub fn put<T: ::wasm_bindgen::JsGeneric>(this: &SyncKvStorage, key: &str, value: &T);

External non-generic type protection

GenericInstantiation codegen now consults the codegen context's recorded type-parameter arity per local type. References like ReadableStream<Uint8Array> whose target accepts zero generics (non-generic web-sys / external types) drop the args and emit the bare base type, preventing E0107 at the use site.

Symbol-keyed methods

Symbol JS names like [Symbol.iterator] previously snake_cased to symboliterator. base_rust_name now strips the [Symbol. prefix and trailing ] when computing the Rust name, so the synthesized iterator methods read as iterator / async_iterator while js_name keeps the bracketed [Symbol.iterator] for wasm-bindgen.

Dedup numbering

dedupe_name now starts collision suffixes at _2 instead of _1 — the unsuffixed candidate is the implicit _1, so the first collision becomes _2. Matches the convention already used by parse::members::unique_type_name (Foo, Foo2, Foo3, …). User-visible: Response::json_1 (instance method colliding with the static json factory) becomes json_2, etc.

Header comment

Generated files now begin with // Generated by ts-gen. Do not edit. — prepended after rustfmt since quote! strips line comments from token streams.

Tests

  • New tests/fixtures/iterables.d.ts exercises every iterability variant plus generic propagation through put<T> / get<T>.
  • New tests/snapshots/iterables.rs blesses the expected output.
  • Four unit tests in parse::members::tests cover happy path (sync), async, non-iterable rejection, and dedup.
  • Existing snapshots updated: workers-types now produces typed Iterator<...>, AsyncIterator<...>, and per-method generics on put / get style methods that previously aliased T to JsValue.

Conventions

Two new sections, slotted next to Promise<T> since all three share the generic-container shape:

  • Iterator<T> / IterableIterator<T> map to js_sys::Iterator<T>
  • Iterable<T> returns synthesize a wrapper with [Symbol.iterator]()

AGENTS.md updated to clarify that CONVENTIONS sections are not numbered and are ordered from simplest to most obscure.

Verification

  • just test — 123 unit + 1 snapshot pass
  • just clippy — clean with -D warnings
  • just fmt-check — clean

@guybedford guybedford force-pushed the iterables-and-generics branch 2 times, most recently from e9a73d3 to 414b1cf Compare April 29, 2026 22:23
Adds first-class handling for the TS iterability protocol and bare
generic type parameters in method signatures, so APIs like
`SyncKvStorage.put<T>(key: string, value: T)` and
`SyncKvStorage.list<T>(): Iterable<[string, T]>` round-trip into
typed wasm-bindgen externs without erasing the generics.

`Iterator<T>` and `IterableIterator<T>` map directly to
`js_sys::Iterator<T>`; `AsyncIterator<T>` and
`AsyncIterableIterator<T>` map to `js_sys::AsyncIterator<T>`. These
are added as new IR variants (`TypeRef::Iterator` and
`TypeRef::AsyncIterator`) and emit through the existing generic
container machinery alongside `Promise<T>`.

`Iterable<T>` describes the protocol — an object exposing
`[Symbol.iterator](): Iterator<T>` — distinct from `Iterator<T>`
itself. To preserve that at the binding layer, top-level
`Iterable<T>` returns now synthesize a wrapper extern type with a
single `[Symbol.iterator]()` method:

```ts
interface SyncKvStorage {
  list<T>(): Iterable<[string, T]>;
}
```

becomes:

```rust
pub type SyncKvStorageList<T: ::wasm_bindgen::JsGeneric>;
pub fn iterator<T: ::wasm_bindgen::JsGeneric>(
    this: &SyncKvStorageList<T>,
) -> Iterator<ArrayTuple<(JsString, T)>>;

pub fn list<T: ::wasm_bindgen::JsGeneric>(this: &SyncKvStorage) -> SyncKvStorageList<T>;
```

The wrapper's name follows the existing `<Parent><Member>`
convention with dedup, mirroring anonymous-interface parameter
synthesis. `AsyncIterable<T>` synthesizes the analogous wrapper
keyed on `[Symbol.asyncIterator]`. The bracketed `js_name`
matches wasm-bindgen's computed-property syntax for symbol-keyed
methods. Nested occurrences inside unions or arrays are not
synthesized — they erase to `JsValue`, matching the existing
parameter-synthesis limitation.

Bare TS generics on methods now survive codegen as
`<T: ::wasm_bindgen::JsGeneric>` declarations rather than being
erased to `JsValue`. Implementation:

* New `TypeRef::TypeParam(String)` IR variant for in-scope generic
  type parameter references.
* `convert_ts_type_scoped` returns `TypeParam(name)` instead of
  `Any` for names in the method's type-parameter scope.
* `convert_formal_params_with_synthesis` now threads scope through
  to parameter type conversion (previously called `convert_ts_type`,
  which ignored scope).
* Method codegen collects every `TypeParam` reachable from the
  signature and emits a generic decl bounded by
  `::wasm_bindgen::JsGeneric`. Synthesized iterable wrappers
  propagate any `TypeParam` from their item type onto the wrapper
  type itself, so `SyncKvStorageList` is `SyncKvStorageList<T>`.
* `pub type X<T, …>` declarations carry their generics so type
  aliases that mention generic parameters compile.

`GenericInstantiation` codegen now consults the codegen context's
recorded type-parameter arity per local type. References like
`ReadableStream<Uint8Array>` whose target accepts zero generics
(non-generic web-sys / external types) drop the args and emit the
bare base type, preventing `E0107`.

Symbol JS names like `[Symbol.iterator]` previously snake_cased to
`symboliterator`. `base_rust_name` now strips the `[Symbol.`
prefix and trailing `]` when computing the Rust name, so the
synthesized iterator methods read as `iterator` /
`async_iterator` while `js_name` keeps the bracketed
`[Symbol.iterator]` for wasm-bindgen.

Generated files now begin with `// Generated by ts-gen. Do not
edit.` (prepended after rustfmt — `quote!` doesn't preserve line
comments through token streams).

* New `tests/fixtures/iterables.d.ts` exercises every iterability
  variant plus generic propagation through `put<T>` / `get<T>`.
* New `tests/snapshots/iterables.rs` blesses the expected output.
* Four unit tests in `parse::members::tests` cover happy path
  (sync), async, non-iterable rejection, and dedup.
* Existing snapshots updated: workers-types now produces typed
  `Iterator<...>`, `AsyncIterator<...>`, and per-method generics
  on `put` / `get` style methods that previously aliased `T` to
  `JsValue`.

Two new sections (slotted next to `Promise<T>` since all three
share the generic-container shape):

* `Iterator<T>` / `IterableIterator<T>` map to `js_sys::Iterator<T>`
* `Iterable<T>` returns synthesize a wrapper with `[Symbol.iterator]()`

`AGENTS.md` updated to clarify that CONVENTIONS sections are not
numbered and are ordered from simplest to most obscure.
@guybedford guybedford force-pushed the iterables-and-generics branch from 414b1cf to bbda773 Compare April 30, 2026 00:53
@guybedford
Copy link
Copy Markdown
Contributor Author

Replaced by #24 on the new IR model.

@guybedford guybedford closed this May 7, 2026
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