Skip to main content

ts_gen/codegen/
mod.rs

1//! Code generation: IR → Rust source code.
2//!
3//! The main entry point is [`generate`], which takes a parsed IR [`Module`]
4//! and produces formatted Rust source code as a string.
5
6pub mod classes;
7pub mod enums;
8pub mod functions;
9pub mod signatures;
10pub mod typemap;
11
12use proc_macro2::TokenStream;
13use quote::quote;
14
15use crate::ir::{InterfaceClassification, Module, TypeDeclaration, TypeKind};
16use crate::parse::scope::ScopeId;
17
18use typemap::CodegenContext;
19
20/// Convert an optional doc string into `/// ...` doc-comment attributes.
21///
22/// Returns an empty `TokenStream` if `doc` is `None`.
23pub(crate) fn doc_tokens(doc: &Option<String>) -> TokenStream {
24    match doc {
25        Some(text) => {
26            let lines: Vec<TokenStream> = text
27                .lines()
28                .map(|line| {
29                    let line = format!(" {line}");
30                    quote! { #[doc = #line] }
31                })
32                .collect();
33            quote! { #(#lines)* }
34        }
35        None => quote! {},
36    }
37}
38
39/// Options for controlling code generation output.
40#[derive(Debug, Clone, Default)]
41pub struct GenerateOptions {
42    /// When `true`, omit the `PromiseExt` trait and its `impl` block from
43    /// the generated preamble. Useful when multiple generated files are
44    /// compiled in the same crate and only one copy of the trait is needed.
45    pub skip_promise_ext: bool,
46}
47
48/// Generate Rust source code from a parsed IR module + global context.
49///
50/// Returns the formatted source as a string, ready to be written to a file.
51pub fn generate(module: &Module, gctx: &crate::context::GlobalContext) -> anyhow::Result<String> {
52    generate_with_options(module, gctx, &GenerateOptions::default())
53}
54
55/// Generate Rust source code with explicit options.
56pub fn generate_with_options(
57    module: &Module,
58    gctx: &crate::context::GlobalContext,
59    options: &GenerateOptions,
60) -> anyhow::Result<String> {
61    let tokens = generate_tokens(module, gctx, options);
62    let file = syn::parse2::<syn::File>(tokens.clone()).map_err(|e| {
63        anyhow::anyhow!("generated tokens are not valid syn:\n{e}\n\nTokens:\n{tokens}")
64    })?;
65    Ok(prettyplease::unparse(&file))
66}
67
68/// Generate the token stream for a full module.
69fn generate_tokens(
70    module: &Module,
71    gctx: &crate::context::GlobalContext,
72    options: &GenerateOptions,
73) -> TokenStream {
74    let cgctx = CodegenContext::from_module(module, gctx);
75
76    let promise_ext = if options.skip_promise_ext {
77        quote! {}
78    } else {
79        quote! {
80            /// Extension trait for awaiting `js_sys::Promise<T>`.
81            ///
82            /// Since `IntoFuture` can't be implemented for `js_sys::Promise` from
83            /// generated code (orphan rule), use `.into_future().await` instead:
84            /// ```ignore
85            /// use bindings::PromiseExt;
86            /// let data: ArrayBuffer = promise.into_future().await?;
87            /// ```
88            pub trait PromiseExt {
89                type Output;
90                fn into_future(self) -> wasm_bindgen_futures::JsFuture<Self::Output>;
91            }
92
93            impl<T: 'static + wasm_bindgen::convert::FromWasmAbi> PromiseExt for js_sys::Promise<T> {
94                type Output = T;
95                fn into_future(self) -> wasm_bindgen_futures::JsFuture<T> {
96                    wasm_bindgen_futures::JsFuture::from(self)
97                }
98            }
99        }
100    };
101
102    let preamble = quote! {
103        // Auto-generated by ts-gen. Do not edit.
104
105        #[allow(unused_imports)]
106        use wasm_bindgen::prelude::*;
107        #[allow(unused_imports)]
108        use js_sys::*;
109
110        #promise_ext
111    };
112
113    // Group declarations by module context.
114    // Global declarations go at the top level.
115    // Module declarations go inside `mod <name> { ... }` blocks.
116    let mut global_items = Vec::new();
117    let mut module_items: std::collections::BTreeMap<std::rc::Rc<str>, Vec<TokenStream>> =
118        std::collections::BTreeMap::new();
119
120    for &type_id in &module.types {
121        let decl = gctx.get_type(type_id);
122        if let Some(tokens) = generate_declaration(decl, &cgctx) {
123            match &decl.module_context {
124                crate::ir::ModuleContext::Global => {
125                    global_items.push(tokens);
126                }
127                crate::ir::ModuleContext::Module(m) => {
128                    module_items.entry(m.clone()).or_default().push(tokens);
129                }
130            }
131        }
132    }
133
134    // Wrap each module's items in a `mod` block
135    let mod_blocks: Vec<TokenStream> = module_items
136        .into_iter()
137        .map(|(mod_specifier, items)| {
138            // Strip protocol prefix: "cloudflare:email" → "email", "node:url" → "url"
139            let short_name = mod_specifier
140                .rsplit_once(':')
141                .map(|(_, rest)| rest)
142                .unwrap_or(&mod_specifier);
143            let mod_name = typemap::make_ident(&crate::util::naming::to_snake_case(
144                &short_name.replace('/', "_").replace('*', "star"),
145            ));
146            quote! {
147                pub mod #mod_name {
148                    use wasm_bindgen::prelude::*;
149                    use js_sys::*;
150                    use super::*;
151                    #(#items)*
152                }
153            }
154        })
155        .collect();
156
157    // External type use aliases (collected during codegen above)
158    let external_uses = cgctx.external_use_tokens();
159
160    // Emit codegen diagnostics
161    cgctx.take_diagnostics().emit();
162
163    quote! {
164        #preamble
165        #external_uses
166        #(#global_items)*
167        #(#mod_blocks)*
168    }
169}
170
171/// Generate tokens for a single declaration.
172fn generate_declaration(decl: &TypeDeclaration, cgctx: &CodegenContext) -> Option<TokenStream> {
173    match &decl.kind {
174        TypeKind::Class(c) => Some(classes::generate_class(
175            c,
176            &decl.module_context,
177            Some(cgctx),
178            decl.scope_id,
179        )),
180        TypeKind::Interface(i) => match i.classification {
181            InterfaceClassification::ClassLike | InterfaceClassification::Unclassified => {
182                Some(classes::generate_class_like_interface(
183                    i,
184                    &decl.module_context,
185                    Some(cgctx),
186                    None,
187                    decl.scope_id,
188                ))
189            }
190            InterfaceClassification::Dictionary => Some(classes::generate_dictionary_extern(
191                i,
192                &decl.module_context,
193                Some(cgctx),
194                None,
195                decl.scope_id,
196            )),
197        },
198        TypeKind::StringEnum(e) => Some(enums::generate_string_enum(e)),
199        TypeKind::NumericEnum(e) => Some(enums::generate_numeric_enum(e)),
200        TypeKind::Function(f) => Some(functions::generate_function(
201            f,
202            &decl.module_context,
203            Some(cgctx),
204            &decl.doc,
205            decl.scope_id,
206        )),
207        TypeKind::Variable(v) => Some(functions::generate_variable(
208            v,
209            &decl.module_context,
210            Some(cgctx),
211            &decl.doc,
212            None,
213            decl.scope_id,
214        )),
215        TypeKind::TypeAlias(alias) => Some(generate_type_alias(alias, cgctx, decl.scope_id)),
216        TypeKind::Namespace(ns) => Some(generate_namespace(ns, &decl.module_context, cgctx)),
217    }
218}
219
220/// Generate output for a TypeAlias declaration.
221///
222/// - Local alias: `pub type WritableStream = Writable;`
223/// - External re-export: `pub use external_crate::Foo;`
224fn generate_type_alias(
225    alias: &crate::ir::TypeAliasDecl,
226    cgctx: &CodegenContext,
227    scope: ScopeId,
228) -> TokenStream {
229    if let Some(ref module) = alias.from_module {
230        // External re-export: resolve through external map.
231        let type_name = match &alias.target {
232            crate::ir::TypeRef::Named(n) => n.as_str(),
233            _ => &alias.name,
234        };
235        if let Some(rust_path) = cgctx.resolve_external(type_name, module) {
236            let path: syn::Path = syn::parse_str(&rust_path.path).unwrap_or_else(|_| {
237                syn::Path::from(syn::Ident::new("JsValue", proc_macro2::Span::call_site()))
238            });
239            let name = typemap::make_ident(&alias.name);
240            if alias.name == type_name {
241                return quote! { pub use #path; };
242            } else {
243                return quote! { pub use #path as #name; };
244            }
245        }
246        cgctx.warn(format!(
247            "No external mapping for `{}` from \"{module}\" — emitting JsValue alias",
248            type_name,
249        ));
250        let name = typemap::make_ident(&alias.name);
251        return quote! {
252            #[allow(dead_code)]
253            pub type #name = JsValue;
254        };
255    }
256
257    // Local alias — only emit if the target resolves to a known type.
258    if let crate::ir::TypeRef::Named(ref target_name) = alias.target {
259        if !cgctx.local_types.contains(target_name)
260            && !crate::codegen::typemap::JS_SYS_RESERVED.contains(&target_name.as_str())
261        {
262            cgctx.warn(format!(
263                "Type alias `{}` targets unknown type `{target_name}`, skipping",
264                alias.name
265            ));
266            return quote! {};
267        }
268    }
269
270    let target = typemap::to_syn_type(
271        &alias.target,
272        typemap::TypePosition::ARGUMENT.to_inner(),
273        Some(cgctx),
274        scope,
275    );
276    let name = typemap::make_ident(&alias.name);
277
278    // Identity — skip.
279    if target.to_string() == alias.name {
280        return quote! {};
281    }
282
283    quote! {
284        #[allow(dead_code)]
285        pub type #name = #target;
286    }
287}
288
289/// Generate a Rust `mod` block for a namespace, with all nested declarations.
290fn generate_namespace(
291    ns: &crate::ir::NamespaceDecl,
292    _parent_ctx: &crate::ir::ModuleContext,
293    cgctx: &CodegenContext,
294) -> TokenStream {
295    let mod_name = typemap::make_ident(&crate::util::naming::to_snake_case(&ns.name));
296    let js_name = &ns.name;
297
298    let items: Vec<TokenStream> = ns
299        .declarations
300        .iter()
301        .filter_map(|decl| generate_ns_declaration(decl, js_name, cgctx))
302        .collect();
303
304    quote! {
305        pub mod #mod_name {
306            use wasm_bindgen::prelude::*;
307            #(#items)*
308        }
309    }
310}
311
312/// Generate tokens for a declaration inside a namespace.
313/// Adds `js_namespace` attribute to extern blocks so wasm_bindgen emits
314/// the correct JS access (e.g., `WebAssembly.Module`).
315fn generate_ns_declaration(
316    decl: &TypeDeclaration,
317    ns_js_name: &str,
318    cgctx: &CodegenContext,
319) -> Option<TokenStream> {
320    match &decl.kind {
321        TypeKind::Class(c) => Some(classes::generate_class_with_js_namespace(
322            c,
323            &decl.module_context,
324            ns_js_name,
325            Some(cgctx),
326            decl.scope_id,
327        )),
328        TypeKind::Interface(i) => match i.classification {
329            InterfaceClassification::ClassLike | InterfaceClassification::Unclassified => {
330                Some(classes::generate_class_like_interface(
331                    i,
332                    &decl.module_context,
333                    Some(cgctx),
334                    Some(ns_js_name),
335                    decl.scope_id,
336                ))
337            }
338            InterfaceClassification::Dictionary => Some(classes::generate_dictionary_extern(
339                i,
340                &decl.module_context,
341                Some(cgctx),
342                Some(ns_js_name),
343                decl.scope_id,
344            )),
345        },
346        TypeKind::Function(f) => Some(functions::generate_function_with_js_namespace(
347            f,
348            &decl.module_context,
349            ns_js_name,
350            Some(cgctx),
351            &decl.doc,
352            decl.scope_id,
353        )),
354        TypeKind::StringEnum(e) => Some(enums::generate_string_enum(e)),
355        TypeKind::NumericEnum(e) => Some(enums::generate_numeric_enum(e)),
356        TypeKind::Variable(v) => Some(functions::generate_variable(
357            v,
358            &decl.module_context,
359            Some(cgctx),
360            &decl.doc,
361            Some(ns_js_name),
362            decl.scope_id,
363        )),
364        TypeKind::TypeAlias(_) => None,
365        TypeKind::Namespace(ns) => Some(generate_namespace(ns, &decl.module_context, cgctx)),
366    }
367}