Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ complex (subtyping LUB across unions).

**When to update `CONVENTIONS.md`:**

* Adding a new TS construct → add a numbered section.
* Adding a new TS construct → add a section. Sections are not
numbered; order them from simplest (primitives) to most obscure
(subtyping LUB across unions). Slot the new heading where it
naturally falls in that progression — usually next to a related
convention of similar complexity.
* Changing an existing translation rule → update its section.
* Bug fix that changes user-visible output → update if the fix changes
the documented behaviour, otherwise just add a snapshot test.
Expand Down
73 changes: 73 additions & 0 deletions CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,79 @@ primitives via `value_of()` (for `Boolean` / `Number`) or
Sync returns, arguments, and properties keep the bare-primitive
lowering.

## `Iterator<T>` / `IterableIterator<T>` map to `js_sys::Iterator<T>`

```ts
interface FormData {
entries(): IterableIterator<[string, FormDataEntryValue]>;
}
```

emits:

```rust
#[wasm_bindgen(method)]
pub fn entries(this: &FormData) -> Iterator<ArrayTuple<(JsString, JsValue)>>;
```

`Iterator<T>` and `IterableIterator<T>` describe runtime objects that
*are* iterators (have `.next()`), so they map directly to
`js_sys::Iterator<T>` with the inner type erased the same way
`Promise<T>` is — generic `T` parameters become `JsValue` unless they
resolve to a concrete named type at generation time.

`AsyncIterator<T>` and `AsyncIterableIterator<T>` map to
`js_sys::AsyncIterator<T>` analogously; wasm-bindgen models the two
iterator families separately.

## `Iterable<T>` returns synthesize a wrapper with `[Symbol.iterator]()`

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

emits:

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

// And on the original interface:
#[wasm_bindgen(method)]
pub fn list(this: &SyncKvStorage) -> SyncKvStorageList;
```

`Iterable<T>` describes the *protocol* — an object that exposes
`[Symbol.iterator](): Iterator<T>` — distinct from `Iterator<T>` (the
iterator object itself). To preserve that distinction at the binding
layer, ts-gen synthesizes a wrapper interface per top-level
`Iterable<T>` return:

* The wrapper's name is `<Parent><Method>` PascalCased, deduped
against existing names (same convention as anonymous-interface
parameter synthesis).
* The wrapper has a single method `iterator()` keyed on `Symbol.iterator`
via `js_name = "[Symbol.iterator]"`, returning the inner
`Iterator<T>`. The bracketed form matches wasm-bindgen's
computed-property `js_name` syntax.
* The original method's return type is rewritten to the wrapper's name.

`AsyncIterable<T>` synthesizes the analogous wrapper using
`[Symbol.asyncIterator]` and `AsyncIterator<T>` for the inner iterator.

Nested occurrences (inside unions, arrays, etc.) are not synthesized —
they erase to `JsValue` at codegen, matching the existing
parameter-synthesis limitation. Hoist the type manually if a wrapper is
needed in those positions.

Symbol-keyed methods drop the `[Symbol.` prefix and trailing `]` when
computing the Rust name: `[Symbol.iterator]` becomes `iterator`, not
`symboliterator`.

## `@throws` JSDoc → typed error

```ts
Expand Down
79 changes: 76 additions & 3 deletions src/codegen/classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ struct ClassConfig<'a> {
is_abstract: bool,
/// Members to generate.
members: Vec<Member>,
/// Type parameters declared on the type itself (rendered as
/// `<T: JsGeneric, …>` on the `pub type` decl and propagated to
/// every `this: &Type<T, …>` reference inside the extern block).
type_params: Vec<crate::ir::TypeParam>,
/// Codegen context for type resolution.
cgctx: Option<&'a CodegenContext<'a>>,
/// Scope for type reference resolution.
Expand Down Expand Up @@ -85,6 +89,7 @@ impl<'a> ClassConfig<'a> {
js_namespace: None,
is_abstract: decl.is_abstract,
members: decl.members.clone(),
type_params: decl.type_params.clone(),
cgctx,
scope,
}
Expand Down Expand Up @@ -117,11 +122,41 @@ impl<'a> ClassConfig<'a> {
js_namespace: None,
is_abstract: false,
members: decl.members.clone(),
type_params: decl.type_params.clone(),
cgctx,
scope,
}
}

/// Tokens for the type-level generic declaration (`<T: JsGeneric, …>`)
/// or empty when the type has no parameters.
fn type_generics_decl(&self) -> TokenStream {
if self.type_params.is_empty() {
return quote! {};
}
let idents = self
.type_params
.iter()
.map(|tp| super::typemap::make_ident(&tp.name))
.collect::<Vec<_>>();
quote! { <#(#idents: ::wasm_bindgen::JsGeneric),*> }
}

/// Tokens for the type's generic-argument list (`<T, …>`) used in
/// `this: &Type<T, …>` references inside the extern block. Empty
/// when there are no parameters.
fn type_generics_args(&self) -> TokenStream {
if self.type_params.is_empty() {
return quote! {};
}
let idents = self
.type_params
.iter()
.map(|tp| super::typemap::make_ident(&tp.name))
.collect::<Vec<_>>();
quote! { <#(#idents),*> }
}

/// Rust name to use everywhere the class identifier appears in generated
/// code (`pub type X`, `this: &X`, `static_method_of = X`, …).
///
Expand Down Expand Up @@ -976,10 +1011,12 @@ fn generate_type_decl(config: &ClassConfig) -> TokenStream {
quote! { #[wasm_bindgen(#(#wb_parts),*)] }
};

let generics_decl = config.type_generics_decl();

quote! {
#wb_attr
#[derive(Debug, Clone, PartialEq, Eq)]
pub type #rust_ident;
pub type #rust_ident #generics_decl;
}
}

Expand Down Expand Up @@ -1071,13 +1108,47 @@ fn generate_expanded_method(config: &ClassConfig, sig: &FunctionSignature) -> To
quote! {}
};

// wasm-bindgen requires every type parameter mentioned in a method
// signature to be redeclared on the method, even when the same name
// is already on the parent type — see `js_sys::Array::for_each<T:
// JsGeneric>` for the canonical pattern.
let method_generics = generic_params_for_method(config, sig);
let this_generics = config.type_generics_args();

quote! {
#doc
#[wasm_bindgen(#(#wb_parts),*)]
pub #async_kw fn #rust_ident(this: &#this_type, #params) #ret;
pub #async_kw fn #rust_ident #method_generics (this: &#this_type #this_generics, #params) #ret;
}
}

/// Generic declaration for a method, covering both type-level parameters
/// referenced in `this: &Foo<T, …>` and any method-only parameters
/// mentioned in arguments or the return type. wasm-bindgen requires the
/// redeclaration even for type-level params.
fn generic_params_for_method(config: &ClassConfig, sig: &FunctionSignature) -> TokenStream {
// Type-level params come first, in declaration order, so that
// `<T: JsGeneric, U: JsGeneric>` aligns with the type's parameter
// list when `this: &Foo<T, U>` is referenced.
let mut names: Vec<String> = config
.type_params
.iter()
.map(|tp| tp.name.clone())
.collect();
for p in &sig.params {
super::signatures::collect_type_params(&p.type_ref, &mut names);
}
super::signatures::collect_type_params(&sig.return_type, &mut names);
if names.is_empty() {
return quote! {};
}
let idents = names
.iter()
.map(|n| super::typemap::make_ident(n))
.collect::<Vec<_>>();
quote! { <#(#idents: ::wasm_bindgen::JsGeneric),*> }
}

/// Generate a static method binding from an expanded signature.
fn generate_expanded_static_method(config: &ClassConfig, sig: &FunctionSignature) -> TokenStream {
let rust_ident = super::typemap::make_ident(&sig.rust_name);
Expand Down Expand Up @@ -1124,10 +1195,12 @@ fn generate_expanded_static_method(config: &ClassConfig, sig: &FunctionSignature
quote! {}
};

let generics = generic_params_for_method(config, sig);

quote! {
#doc
#[wasm_bindgen(#(#wb_parts),*)]
pub #async_kw fn #rust_ident(#params) #ret;
pub #async_kw fn #rust_ident #generics (#params) #ret;
}
}

Expand Down
27 changes: 23 additions & 4 deletions src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ pub fn generate_with_options(
syn::parse2::<syn::File>(tokens.clone()).map_err(|e| {
anyhow::anyhow!("generated tokens are not valid syn:\n{e}\n\nTokens:\n{tokens}")
})?;
rustfmt(&tokens.to_string())
let formatted = rustfmt(&tokens.to_string())?;
// Prepend a header comment. Line comments don't survive `quote!` token
// generation, so they have to be emitted as plain text after formatting.
Ok(format!(
"// Generated by ts-gen. Do not edit.\n\n{formatted}"
))
}

/// Format Rust source via `rustfmt`.
Expand Down Expand Up @@ -120,8 +125,6 @@ fn generate_tokens(
let cgctx = CodegenContext::from_module(module, gctx);

let preamble = quote! {
// Auto-generated by ts-gen. Do not edit.

#[allow(unused_imports)]
use wasm_bindgen::prelude::*;
#[allow(unused_imports)]
Expand Down Expand Up @@ -300,9 +303,25 @@ fn generate_type_alias(
return quote! {};
}

// Type-parameter declaration so aliases that mention generics survive
// codegen: `type EmailExportedHandler<Env, Props> = …;` rather than the
// bare `EmailExportedHandler =` that would leave `Props` undeclared.
let generics = if alias.type_params.is_empty() {
quote! {}
} else {
let idents = alias
.type_params
.iter()
.map(|tp| typemap::make_ident(&tp.name))
.collect::<Vec<_>>();
// Type aliases use plain `<T, U>` without trait bounds; aliases
// are erased during monomorphisation by their use sites.
quote! { <#(#idents),*> }
};

quote! {
#[allow(dead_code)]
pub type #name = #target;
pub type #name #generics = #target;
}
}

Expand Down
Loading
Loading