Skip to main content

wgsl_rs/
lib.rs

1//! Keep your WGSL in Rust.
2//!
3//! This crate answers the following questions:
4//! 1. How do I write shaders?
5//!   * By writing Rust code in a module annotated by the `#[wgsl_rs]` macro.
6//!   * It looks like this <https://renderling.xyz/wgsl-rs/manual/examples/hello-triangle.html#rust-source>.
7//! 2. How do I package my shaders?
8//!   * They are already packaged in that same module, automatically. No need to
9//!     deal with shader source files. Instead, you access the module at
10//!     runtime.
11//! 3. How do I run my shaders?
12//!   * That's still up to you, but if your runtime is [`wgpu`](https://crates.io/crates/wgpu), then with the
13//!     `linkage-wgpu` feature provided by this crate you can avoid a lot of boiler plate by having your [linkage
14//!     auto-generated](https://renderling.xyz/wgsl-rs/manual/linkage/overview.html?highlight=generate_linkage#high-level-workflow).
15//!
16//! For more info see the [Operator's Manual](https://renderling.xyz/wgsl-rs/manual/introduction.html).
17use ::std::collections::{HashMap, HashSet};
18
19pub use wgsl_rs_macros::{wgsl, wgsl_allow, wgsl_ignore};
20
21pub mod extension;
22pub mod linkage;
23pub mod source_error;
24
25pub use extension::{WgslExtension, any_ext_claims, contains_str};
26
27pub use source_error::SourceError;
28
29/// Re-export of the IR crate so the proc-macro and consuming crates can
30/// reference IR types via `wgsl_rs::ir::...` without needing to depend on
31/// `wgsl-rs-ir` directly.
32pub use wgsl_rs_ir as ir;
33
34/// A WGSL source.
35///
36/// A `Source` is the public-facing spec for a `#[wgsl] mod foo { ... }`
37/// block: the macro emits one `pub static foo::WGSL_SOURCE: Source` per
38/// module. WGSL itself doesn't support importing modules, but `wgsl-rs`
39/// does (glob imports only), so a `Source` can import other `Source`s.
40///
41/// Concrete and template sources share the same type. Templates are
42/// detected at runtime via [`Source::is_template`]; a template source's
43/// [`Source::wgsl_source`] returns
44/// [`SourceError::TemplateWgsl`] and the caller must use the
45/// macro-emitted `instantiate::<…>()` to obtain a concrete
46/// [`ir::Module`] first.
47///
48/// ```rust, ignore
49/// #[wgsl]
50/// pub mod constants {
51///     pub const NUMBER: u32 = 1234;
52/// }
53/// #[wgsl]
54/// pub mod main {
55///     use super::constants::*;
56///
57///     fn add_to_number(n: u32) -> u32 {
58///         n + NUMBER
59///     }
60/// }
61#[derive(Clone, Debug)]
62pub struct Source {
63    /// Unique identifier for this source, assigned at proc-macro expansion
64    /// time from the consuming compilation unit's identity (Cargo
65    /// package name + version, plus crate/bin target names) and a
66    /// per-unit counter (wgsl-rs#165). Exactly unique within one
67    /// compilation unit; across units separated by a 64-bit hash, so
68    /// collisions are astronomically unlikely but not impossible. Used
69    /// to deduplicate sources during source assembly (diamond import
70    /// graphs, same-named sources from different paths).
71    pub id: u64,
72
73    /// Name of the source.
74    pub name: &'static str,
75
76    /// Imports of other WGSL sources.
77    pub imports: &'static [&'static Source],
78
79    /// Constructor function that builds this module's IR. Called lazily by
80    /// `wgsl_source()` / `instantiate()`.
81    ///
82    /// The returned IR is owned and freshly constructed on each call; the
83    /// caller is free to clone, mutate, or substitute over it.
84    pub ir_constructor: fn() -> ir::Module,
85
86    /// Generic function templates defined in this module.
87    ///
88    /// Consuming modules reference these via `instantiations` to produce
89    /// monomorphized functions at source-assembly time. The IR is held as
90    /// a constructor, just like the parent module's IR.
91    pub templates: &'static [GenericTemplate],
92
93    /// Cross-module template instantiations.
94    ///
95    /// Each entry references a template from an imported module and provides
96    /// the concrete IR type arguments to substitute for the type parameters.
97    /// These are resolved at source-assembly time in `wgsl_source()`.
98    pub instantiations: &'static [TemplateInstantiation],
99
100    /// Module-level type parameters, in instantiation order.
101    ///
102    /// Two distinct kinds of declaration contribute to this list:
103    ///
104    /// * `impl Trait` linkage variables, e.g. `uniform!(group(0), binding(0),
105    ///   FRAME: impl Convert<f32>)`. Each such declaration contributes the
106    ///   variable's identifier (`"FRAME"`).
107    /// * Generic shader entry points, e.g. `pub fn frag_main<T: Convert<f32>>()
108    ///   -> Vec4f`. Each one of an entry point's type parameters contributes a
109    ///   positional name of the form `"<fn_name>_<index>"` (e.g.
110    ///   `"frag_main_0"`). Source names like `T` are intentionally not used so
111    ///   that two entry points sharing a letter don't collide.
112    ///
113    /// The two groups appear in declaration order: linkage variables
114    /// first, then entry points.
115    ///
116    /// When this slice is empty, the module is concrete and
117    /// [`Self::wgsl_source`] produces a directly usable WGSL source.
118    /// When it is non-empty, callers should use the `instantiate`
119    /// function generated alongside the source, which uses
120    /// `wgsl_rs::linkage::Type<Is = ...>` constraints to check at
121    /// compile time that every parameter has been bound.
122    pub module_type_params: &'static [&'static str],
123}
124
125/// A generic function (or struct) template defined within a module.
126///
127/// The IR is built lazily by calling `ir_constructor`. The returned items
128/// reference unresolved [`ir::Type::TypeParam`] nodes whose names match
129/// entries in `type_params` and `Expr::Ident` nodes whose names match
130/// entries in `const_params`; substitution at instantiation time replaces
131/// them with concrete types and values.
132#[derive(Debug)]
133pub struct GenericTemplate {
134    /// The generic function or struct name (e.g., `"id"`, `"Pair"`).
135    pub name: &'static str,
136    /// Type parameter names (e.g., `["T"]`, `["M", "L", "N"]`).
137    pub type_params: &'static [&'static str],
138    /// `const` parameter names (e.g., `["N"]` for `const N: u32`).
139    pub const_params: &'static [&'static str],
140    /// Constructor that builds the un-instantiated IR for this template.
141    pub ir_constructor: fn() -> Vec<ir::Item>,
142    /// Transitive generic function calls within this template.
143    ///
144    /// Each entry records a call to another generic function, with a mapping
145    /// from the callee's type params back to this template's type params.
146    pub dependencies: &'static [TemplateDependency],
147}
148
149/// A transitive dependency from one generic template to another.
150#[derive(Clone, Debug)]
151pub struct TemplateDependency {
152    /// Name of the generic function being called.
153    pub callee: &'static str,
154    /// Maps each of the callee's type params to one of the caller's type
155    /// params, by index. For example, if `quadruple<T>` calls `double::<T>()`,
156    /// the mapping is `&[0]` (double's 0th param = quadruple's 0th param).
157    pub type_param_mapping: &'static [usize],
158}
159
160/// A request to instantiate a generic template with concrete types.
161#[derive(Debug)]
162pub struct TemplateInstantiation {
163    /// Candidate imported sources that may contain the template.
164    ///
165    /// Resolution must find exactly one matching source; missing or ambiguous
166    /// matches are treated as hard failures during `wgsl_source()` assembly.
167    pub modules: &'static [&'static Source],
168    /// The generic function name to instantiate.
169    pub template_name: &'static str,
170    /// Constructor that builds the concrete IR types to substitute for the
171    /// template's type parameters.
172    pub type_args_constructor: fn() -> Vec<ir::Type>,
173    /// Identifier-safe mangled type argument names, used for deduplication
174    /// keys and the `seen` set (e.g. `"array_f32_4"`, `"ptr_function_f32"`).
175    pub mangled_type_args: &'static [&'static str],
176    /// Constructor that builds the concrete `u32` const values to
177    /// substitute for the template's const parameters.
178    pub const_args_constructor: fn() -> Vec<u32>,
179    /// Identifier-safe mangled const argument names (e.g. `["4"]`).
180    pub mangled_const_args: &'static [&'static str],
181}
182
183impl Source {
184    /// Returns whether this source is a template — i.e. has unresolved
185    /// module-level type parameters.
186    ///
187    /// Template sources cannot be used as shader sources directly; call
188    /// `instantiate(...)` to produce a concrete WGSL source.
189    pub fn is_template(&self) -> bool {
190        !self.module_type_params.is_empty()
191    }
192
193    /// Returns the assembled WGSL source for this source and all its
194    /// imports, including cross-source template instantiations.
195    ///
196    /// If this source is a template, the returned error is
197    /// [`SourceError::TemplateWgsl`]; the caller must use the
198    /// macro-emitted `instantiate::<…>()` to obtain a concrete
199    /// [`ir::Module`] first and then call [`ir::Module::wgsl_source`].
200    pub fn wgsl_source(&self) -> Result<String, SourceError<'_>> {
201        if self.is_template() {
202            return Err(SourceError::TemplateWgsl {
203                uninstantiated_source: self,
204            });
205        }
206        let mut out = String::new();
207        let mut visited_sources: HashSet<u64> = HashSet::new();
208        let mut seen: HashSet<(u64, String, Vec<String>, Vec<String>)> = HashSet::new();
209        let mut needs_tier1 = false;
210        let mut imports = ir::TypeImports::default();
211        self.collect(
212            &mut out,
213            &mut visited_sources,
214            &mut seen,
215            None,
216            &mut needs_tier1,
217            &mut imports,
218        )?;
219        // WGSL requires `enable` directives at the very start of the
220        // translation unit, before any other declarations. Since `collect`
221        // assembles multiple modules / instantiations by concatenation, we
222        // prepend the directive here after all chunks have been rendered.
223        if needs_tier1 {
224            out.insert_str(0, "enable texture_formats_tier1;\n\n");
225        }
226        Ok(out)
227    }
228
229    /// Recursively collect this source's WGSL source (and that of its
230    /// imports / instantiations) into `out`. When `subst` is `Some`, the
231    /// caller's substitution map is applied to *this* source's IR (but not
232    /// to imported sources — imports are concrete by construction).
233    ///
234    /// `visited_sources` tracks source IDs that have already been emitted,
235    /// preventing duplicate definitions in diamond import graphs.
236    /// `seen` tracks `(source_id, template_name, mangled_type_args,
237    /// mangled_const_args)` 4-tuples to deduplicate cross-source template
238    /// instantiations. Including const args ensures `foo::<4>` and
239    /// `foo::<8>` aren't incorrectly deduplicated.
240    /// `imports` accumulates every rendered source's harvested type
241    /// environment — globals (consts, linkage declarations), struct
242    /// definitions, and user-fn signatures — so later sources anchor
243    /// from imported names too (calls, consts, linkage accesses,
244    /// struct constructors) via
245    /// [`ir::suffix_module_with_type_imports`], with the caller's own
246    /// declarations shadowing imported ones.
247    fn collect(
248        &self,
249        out: &mut String,
250        visited_sources: &mut HashSet<u64>,
251        seen: &mut HashSet<(u64, String, Vec<String>, Vec<String>)>,
252        subst: Option<&HashMap<String, ir::Type>>,
253        needs_tier1: &mut bool,
254        imports: &mut ir::TypeImports,
255    ) -> Result<(), SourceError<'_>> {
256        // 1. Imports first (depth-first, deduplicated by source ID).
257        for m in self.imports {
258            if visited_sources.insert(m.id) {
259                m.collect(out, visited_sources, seen, None, needs_tier1, imports)?;
260            }
261        }
262
263        // 2. Build this source's IR, optionally substituting type params.
264        let mut ir_module = (self.ir_constructor)();
265        if let Some(s) = subst {
266            ir::substitute_types(&mut ir_module, s);
267        }
268        ir::deshadow_module(&mut ir_module);
269        // Pre-seed with the types of cross-source template instances
270        // this source calls: instance chunks are appended (and publish
271        // their types) after this chunk is suffixed, so a call like
272        // `choose::<u32>(select(0, 1, data))` must find the
273        // instance's concrete parameter types now.
274        for inst in self.instantiations {
275            let mangled: Vec<String> = inst
276                .mangled_type_args
277                .iter()
278                .map(|s| (*s).to_string())
279                .collect();
280            let type_args = (inst.type_args_constructor)();
281            let mangled_consts: Vec<String> = inst
282                .mangled_const_args
283                .iter()
284                .map(|s| (*s).to_string())
285                .collect();
286            let const_args = (inst.const_args_constructor)();
287            imports.extend(instance_type_imports(
288                inst.modules,
289                inst.template_name,
290                &type_args,
291                &const_args,
292                &mangled,
293                &mangled_consts,
294            ));
295        }
296        // Make this source's own globals, structs, and signatures
297        // available to later sources, then suffix with every
298        // ancestor's types seeded — references to imported names
299        // anchor (calls, consts, linkage accesses, struct
300        // constructors), and a caller's own declarations shadow
301        // imported ones (matching Rust name resolution).
302        imports.extend(ir::type_imports(&ir_module));
303        ir::suffix_module_with_type_imports(&mut ir_module, imports);
304        if ir::items_need_tier1_extension(&ir_module.items) {
305            *needs_tier1 = true;
306        }
307        out.push_str(&ir::render_module(&ir_module));
308
309        // 3. Cross-source template instantiations.
310        for inst in self.instantiations {
311            let mangled: Vec<String> = inst
312                .mangled_type_args
313                .iter()
314                .map(|s| (*s).to_string())
315                .collect();
316            let type_args = (inst.type_args_constructor)();
317            let mangled_consts: Vec<String> = inst
318                .mangled_const_args
319                .iter()
320                .map(|s| (*s).to_string())
321                .collect();
322            let const_args = (inst.const_args_constructor)();
323            instantiate_template_into(
324                inst.modules,
325                inst.template_name,
326                &mangled,
327                &type_args,
328                &mangled_consts,
329                &const_args,
330                out,
331                seen,
332                needs_tier1,
333                imports,
334            )?;
335        }
336        Ok(())
337    }
338}
339
340/// Resolve `template_name` from the given candidate sources, recursively
341/// instantiate its dependencies, then render its substituted IR into
342/// `out`. Tracks `(source_id, name, mangled_type_args)` triples in `seen`
343/// to avoid duplicate emission.
344///
345/// Returns `Err` with a structured [`SourceError`] for user-facing
346/// configuration errors (template not found, ambiguous match). Returns
347/// `Ok` for a successful instantiation, or for the early-return dedup case.
348#[allow(clippy::too_many_arguments)]
349fn instantiate_template_into<'a>(
350    sources: &[&'a Source],
351    template_name: &str,
352    mangled_type_args: &[String],
353    type_args: &[ir::Type],
354    mangled_const_args: &[String],
355    const_args: &[u32],
356    out: &mut String,
357    seen: &mut HashSet<(u64, String, Vec<String>, Vec<String>)>,
358    needs_tier1: &mut bool,
359    imports: &mut ir::TypeImports,
360) -> Result<(), SourceError<'a>> {
361    let (source, template) = resolve_template(sources, template_name, mangled_type_args)?;
362
363    let key = (
364        source.id,
365        template_name.to_string(),
366        mangled_type_args.to_vec(),
367        mangled_const_args.to_vec(),
368    );
369    if !seen.insert(key) {
370        return Ok(()); // Already instantiated
371    }
372
373    // Recursively instantiate dependencies first.
374    for dep in template.dependencies {
375        let dep_mangled: Vec<String> = dep
376            .type_param_mapping
377            .iter()
378            .map(|&idx| mangled_type_args[idx].clone())
379            .collect();
380        let dep_args: Vec<ir::Type> = dep
381            .type_param_mapping
382            .iter()
383            .map(|&idx| type_args[idx].clone())
384            .collect();
385        // Dependencies currently share the caller's const args by
386        // reference; a full const-param-mapping parallel to
387        // `type_param_mapping` is a future extension. For now we pass
388        // the const args through unchanged.
389        instantiate_template_into(
390            &[source],
391            dep.callee,
392            &dep_mangled,
393            &dep_args,
394            mangled_const_args,
395            const_args,
396            out,
397            seen,
398            needs_tier1,
399            imports,
400        )?;
401    }
402
403    let mut items = build_instance_items(
404        template,
405        type_args,
406        const_args,
407        mangled_type_args,
408        mangled_const_args,
409    );
410
411    ir::deshadow_items(&mut items);
412    // Seed with every ancestor's types (imports and already-
413    // instantiated dependency templates), then publish this instance's
414    // own (renamed) types for any later chunk.
415    ir::suffix_items_with_type_imports(&mut items, imports);
416    imports.extend(ir::type_imports_in_items(&items));
417    if ir::items_need_tier1_extension(&items) {
418        *needs_tier1 = true;
419    }
420    out.push_str(&ir::render_items(&items));
421    Ok(())
422}
423
424/// Resolve `template_name` to exactly one template among `sources`,
425/// producing the structured errors `instantiate_template_into` reports.
426fn resolve_template<'a>(
427    sources: &[&'a Source],
428    template_name: &str,
429    mangled_type_args: &[String],
430) -> Result<(&'a Source, &'a GenericTemplate), SourceError<'a>> {
431    let available_templates: Vec<String> = sources
432        .iter()
433        .copied()
434        .flat_map(|m| m.templates.iter().map(|t| t.name.to_string()))
435        .collect();
436
437    let mut matching_sources: Vec<&Source> = sources
438        .iter()
439        .copied()
440        .filter(|m| m.templates.iter().any(|t| t.name == template_name))
441        .collect();
442
443    if matching_sources.is_empty() {
444        return Err(SourceError::TemplateNotFound {
445            template_name: template_name.to_string(),
446            mangled_type_args: mangled_type_args.to_vec(),
447            available_templates,
448        });
449    }
450
451    if matching_sources.len() > 1 {
452        let source_names: Vec<String> = matching_sources
453            .iter()
454            .map(|m| m.name.to_string())
455            .collect();
456        return Err(SourceError::AmbiguousTemplate {
457            template_name: template_name.to_string(),
458            mangled_type_args: mangled_type_args.to_vec(),
459            matching_source_names: source_names,
460            available_templates,
461        });
462    }
463
464    let source = matching_sources
465        .pop()
466        .expect("matching_sources is guaranteed to be non-empty after checks");
467
468    let Some(template) = source.templates.iter().find(|t| t.name == template_name) else {
469        // Invariant: `matching_sources` was built by filtering on
470        // `templates.iter().any(|t| t.name == template_name)`, so the
471        // sole surviving source must contain the template.
472        unreachable!(
473            "resolved source '{}' does not contain template '{}'; available templates: {:?}",
474            source.name, template_name, available_templates
475        );
476    };
477
478    Ok((source, template))
479}
480
481/// Build a template instance's items: run the template's IR
482/// constructor, substitute its type and const parameters, and rename
483/// the instance to its mangled name. Uses `ir::mangle` so that names
484/// with underscores in either the template name or the type-arg-mangled
485/// strings are escaped unambiguously (see issue #112).
486fn build_instance_items(
487    template: &GenericTemplate,
488    type_args: &[ir::Type],
489    const_args: &[u32],
490    mangled_type_args: &[String],
491    mangled_const_args: &[String],
492) -> Vec<ir::Item> {
493    // Build substitution maps: type params -> concrete types, const
494    // params -> concrete u32 values.
495    let mut subst: HashMap<String, ir::Type> = HashMap::new();
496    for (param, arg) in template.type_params.iter().zip(type_args.iter()) {
497        subst.insert((*param).to_string(), arg.clone());
498    }
499    let mut consts: HashMap<String, u32> = HashMap::new();
500    for (param, arg) in template.const_params.iter().zip(const_args.iter()) {
501        consts.insert((*param).to_string(), *arg);
502    }
503
504    let mut items = (template.ir_constructor)();
505    ir::substitute_items(&mut items, &subst);
506    ir::substitute_consts_in_items(&mut items, &consts);
507
508    let instance_name = if mangled_type_args.is_empty() && mangled_const_args.is_empty() {
509        template.name.to_string()
510    } else {
511        let mut components: Vec<String> =
512            Vec::with_capacity(1 + mangled_type_args.len() + mangled_const_args.len());
513        components.push(template.name.to_string());
514        for s in mangled_type_args {
515            components.push(s.clone());
516        }
517        for s in mangled_const_args {
518            components.push(s.clone());
519        }
520        let str_components: Vec<&str> = components.iter().map(|s| s.as_str()).collect();
521        ir::mangle(&str_components)
522    };
523    if instance_name != template.name {
524        ir::rename_items(&mut items, template.name, &instance_name);
525    }
526    items
527}
528
529/// Harvest the types a template instance would export, for
530/// pre-seeding the type-import accumulator before a calling source's
531/// chunk is suffixed — instance chunks are appended (and publish their
532/// types) after the caller is suffixed, so a call like
533/// `choose::<u32>(select(0, 1, data))` must find the instance's
534/// concrete parameter types now.
535///
536/// Resolution failures are ignored here:
537/// [`instantiate_template_into`] reports them with full context when
538/// the instance is actually rendered.
539fn instance_type_imports(
540    sources: &[&Source],
541    template_name: &str,
542    type_args: &[ir::Type],
543    const_args: &[u32],
544    mangled_type_args: &[String],
545    mangled_const_args: &[String],
546) -> ir::TypeImports {
547    let Ok((_source, template)) = resolve_template(sources, template_name, mangled_type_args)
548    else {
549        return ir::TypeImports::default();
550    };
551    let items = build_instance_items(
552        template,
553        type_args,
554        const_args,
555        mangled_type_args,
556        mangled_const_args,
557    );
558    ir::type_imports_in_items(&items)
559}
560
561#[cfg(feature = "validation")]
562impl Source {
563    /// Validates the concatenated WGSL source of this source and its imports.
564    ///
565    /// This method concatenates the WGSL source from this source and all its
566    /// imports (recursively), then validates the result using naga.
567    ///
568    /// # Returns
569    ///
570    /// Returns `Ok(())` if validation succeeds, or an error message
571    /// describing the validation failure. Template sources return an
572    /// `Err(String)` instructing the caller to `instantiate::<…>()`
573    /// first (they cannot be validated standalone — their IR contains
574    /// `__TP{name}__` placeholders that aren't valid WGSL).
575    ///
576    /// # Example
577    ///
578    /// ```rust,ignore
579    /// #[wgsl]
580    /// pub mod my_shader {
581    ///     // ...
582    /// }
583    ///
584    /// // Validate at runtime
585    /// my_shader::WGSL_SOURCE.validate().expect("WGSL validation failed");
586    /// ```
587    pub fn validate(&self) -> Result<(), String> {
588        // Template sources contain unresolved `__TP{name}__` placeholders
589        // that aren't valid WGSL identifiers. They cannot be validated
590        // standalone — the user must call `instantiate(...)` first.
591        if self.is_template() {
592            return Err(
593                "cannot validate a template source — call instantiate(...) first".to_string(),
594            );
595        }
596
597        validate_wgsl_source(&self.wgsl_source().expect("concrete source"))
598    }
599}
600
601/// Validates arbitrary WGSL source text using naga.
602///
603/// Parses the source, then runs naga's semantic validation. This is useful
604/// for validating the output of `instantiate::<...>()` + `ir::render_module()`
605/// for generic (template) sources.
606///
607/// # Returns
608///
609/// Returns `Ok(())` if validation succeeds, or an error message describing
610/// the validation failure.
611#[cfg(feature = "validation")]
612pub fn validate_wgsl_source(source: &str) -> Result<(), String> {
613    let module = naga::front::wgsl::parse_str(source).map_err(|e| e.emit_to_string(source))?;
614    naga::valid::Validator::new(
615        naga::valid::ValidationFlags::all(),
616        naga::valid::Capabilities::all(),
617    )
618    .validate(&module)
619    .map_err(|e| e.emit_to_string(source))?;
620    Ok(())
621}
622
623pub mod std;
624
625#[cfg(test)]
626#[allow(dead_code)]
627mod test {
628    use crate::{GenericTemplate, Source, TemplateDependency, TemplateInstantiation, ir, wgsl};
629
630    #[wgsl(crate_path = crate)]
631    pub mod a {
632        pub const THREE: u32 = 3;
633    }
634
635    #[wgsl(crate_path = crate)]
636    pub mod b {
637        use super::a::*;
638
639        pub fn add_three_to_x_minus_y(x: u32, y: u32) -> u32 {
640            let i: u32 = (x - y) + THREE;
641            i
642        }
643    }
644
645    #[wgsl(crate_path = crate)]
646    pub mod c {
647        use super::b::*;
648
649        pub fn main() {
650            let _u = add_three_to_x_minus_y(1337, 666);
651        }
652    }
653
654    #[wgsl(crate_path = crate)]
655    pub mod vecs {
656        use crate::std::*;
657
658        const _VEC3F_ONE: Vec4f = vec4f(0.0, 1.0, 2.0, 3.0);
659    }
660
661    #[test]
662    fn module_source() {
663        let source = c::WGSL_SOURCE.wgsl_source().unwrap();
664        c::main();
665        assert!(source.contains("const THREE: u32 = 3u;"), "got:\n{source}");
666        assert!(
667            source.contains("fn add_three_to_x_minus_y(x: u32, y: u32) -> u32"),
668            "got:\n{source}"
669        );
670        assert!(source.contains("fn main()"), "got:\n{source}");
671        // Cross-source call arguments are suffixed via the fn-signature
672        // accumulator threaded through `collect` (module `c` imports
673        // `add_three_to_x_minus_y` from module `b`).
674        assert!(
675            source.contains("add_three_to_x_minus_y(1337u, 666u)"),
676            "got:\n{source}"
677        );
678
679        // Verify that imported source outputs are not duplicated.
680        // Source C imports B, which imports A. Source A's `THREE` const
681        // should appear exactly once in the concatenated output.
682        assert_eq!(
683            source.matches("const THREE:").count(),
684            1,
685            "THREE should appear exactly once (no duplicates from transitive imports), \
686             got:\n{source}"
687        );
688    }
689
690    // Cross-module trait impl test:
691    // - trait_provider defines a trait and implements it for f32
692    // - trait_consumer imports it and uses the impl via a generic function
693
694    #[wgsl(crate_path = crate)]
695    pub mod trait_provider {
696        pub trait Scalable {
697            fn scale(val: Self, factor: f32) -> Self;
698        }
699
700        impl Scalable for f32 {
701            fn scale(val: f32, factor: f32) -> f32 {
702                val * factor
703            }
704        }
705    }
706
707    #[wgsl(crate_path = crate)]
708    pub mod trait_consumer {
709        use super::trait_provider::*;
710
711        pub fn apply_scale<T: Scalable>(val: T, factor: f32) -> T {
712            T::scale(val, factor)
713        }
714
715        pub fn go() -> f32 {
716            apply_scale::<f32>(2.0, 3.0)
717        }
718    }
719
720    #[test]
721    fn cross_module_trait_impl() {
722        // Verify provider source generates f32_scale
723        let provider_src = trait_provider::WGSL_SOURCE.wgsl_source().unwrap();
724        assert!(
725            provider_src.contains("fn f32_scale("),
726            "trait_provider should contain f32_scale, got:\n{provider_src}"
727        );
728
729        // Verify consumer source generates the monomorphized `apply_scale<f32>`
730        // and that it calls `f32_scale`. Under the robust mangling scheme
731        // (issue #112), `apply_scale` has an underscore so the joined name
732        // is `_1apply_scale_f32`, not `apply_scale_f32`.
733        let consumer_src = trait_consumer::WGSL_SOURCE.wgsl_source().unwrap();
734        assert!(
735            consumer_src.contains("fn _1apply_scale_f32("),
736            "trait_consumer should contain _1apply_scale_f32, got:\n{consumer_src}"
737        );
738        assert!(
739            consumer_src.contains("f32_scale("),
740            "_1apply_scale_f32 should call f32_scale, got:\n{consumer_src}"
741        );
742        assert!(
743            consumer_src.contains("fn f32_scale("),
744            "concatenated source should have f32_scale (from import), got:\n{consumer_src}"
745        );
746
747        // Verify Rust-side execution works too
748        let result = trait_consumer::go();
749        assert!(
750            (result - 6.0).abs() < f32::EPSILON,
751            "2.0 * 3.0 should be 6.0, got {result}"
752        );
753    }
754
755    #[wgsl(crate_path = crate)]
756    mod dedupe_provider {
757        pub fn id<T: Copy>(v: T) -> T {
758            v
759        }
760    }
761
762    #[wgsl(crate_path = crate)]
763    mod dedupe_left {
764        use super::dedupe_provider::*;
765
766        pub fn left() -> f32 {
767            id::<f32>(1.0)
768        }
769    }
770
771    #[wgsl(crate_path = crate)]
772    mod dedupe_right {
773        use super::dedupe_provider::*;
774
775        pub fn right() -> f32 {
776            id::<f32>(2.0)
777        }
778    }
779
780    #[expect(dead_code)]
781    #[wgsl(crate_path = crate)]
782    mod dedupe_root {
783        #[rustfmt::skip]
784        use super::dedupe_left::*;
785        #[rustfmt::skip]
786        use super::dedupe_right::*;
787
788        pub fn root() -> f32 {
789            left() + right()
790        }
791    }
792
793    #[test]
794    fn cross_module_instantiations_are_deduped_globally() {
795        let full_src = dedupe_root::WGSL_SOURCE.wgsl_source().unwrap();
796        assert_eq!(
797            full_src.matches("fn id_f32(").count(),
798            1,
799            "expected id_f32 to appear once, got:\n{full_src}"
800        );
801    }
802
803    #[test]
804    fn ambiguous_template_provider_errors() {
805        // Build hand-crafted sources where two distinct provider sources
806        // both define a generic template named "shared". Resolution must
807        // return `Err(SourceError::AmbiguousTemplate { .. })`.
808        fn empty_source_ir() -> ir::Module {
809            ir::Module {
810                name: "stub",
811                items: vec![],
812                attrs: vec![],
813            }
814        }
815        fn shared_template_items() -> Vec<ir::Item> {
816            vec![ir::Item::Fn(ir::ItemFn {
817                type_params: vec![],
818                const_params: vec![],
819                fn_attrs: ir::FnAttrs::None,
820                name: "shared".to_string().into(),
821                inputs: vec![ir::FnArg {
822                    inter_stage_io: vec![],
823                    name: "x".to_string(),
824                    ty: ir::Type::TypeParam {
825                        name: "T".to_string(),
826                    },
827                    attrs: vec![],
828                }],
829                return_type: ir::ReturnType::Type {
830                    annotation: ir::ReturnTypeAnnotation::None,
831                    ty: ir::Type::TypeParam {
832                        name: "T".to_string(),
833                    },
834                },
835                block: ir::Block {
836                    stmts: vec![ir::Stmt::Return(Some(ir::Expr::Ident("x".to_string())))],
837                },
838                attrs: vec![],
839            })]
840        }
841        fn f32_args() -> Vec<ir::Type> {
842            vec![ir::Type::Scalar(ir::ScalarType::F32)]
843        }
844        static DEP: [TemplateDependency; 0] = [];
845        static A_TEMPLATES: [GenericTemplate; 1] = [GenericTemplate {
846            name: "shared",
847            type_params: &["T"],
848            const_params: &[],
849            ir_constructor: shared_template_items,
850            dependencies: &DEP,
851        }];
852        static B_TEMPLATES: [GenericTemplate; 1] = [GenericTemplate {
853            name: "shared",
854            type_params: &["T"],
855            const_params: &[],
856            ir_constructor: shared_template_items,
857            dependencies: &DEP,
858        }];
859        static EMPTY_SOURCES: [&Source; 0] = [];
860        static EMPTY_INSTS: [TemplateInstantiation; 0] = [];
861        static EMPTY_TYPE_PARAMS: [&str; 0] = [];
862        static MOD_A: Source = Source {
863            id: 0,
864            name: "a",
865            imports: &EMPTY_SOURCES,
866            ir_constructor: empty_source_ir,
867            templates: &A_TEMPLATES,
868            instantiations: &EMPTY_INSTS,
869            module_type_params: &EMPTY_TYPE_PARAMS,
870        };
871        static MOD_B: Source = Source {
872            id: 1,
873            name: "b",
874            imports: &EMPTY_SOURCES,
875            ir_constructor: empty_source_ir,
876            templates: &B_TEMPLATES,
877            instantiations: &EMPTY_INSTS,
878            module_type_params: &EMPTY_TYPE_PARAMS,
879        };
880        static ROOT_INSTS: [TemplateInstantiation; 1] = [TemplateInstantiation {
881            modules: &[&MOD_A, &MOD_B],
882            template_name: "shared",
883            type_args_constructor: f32_args,
884            mangled_type_args: &["f32"],
885            const_args_constructor: || vec![],
886            mangled_const_args: &[],
887        }];
888        static ROOT: Source = Source {
889            id: 2,
890            name: "root",
891            imports: &EMPTY_SOURCES,
892            ir_constructor: empty_source_ir,
893            templates: &[],
894            instantiations: &ROOT_INSTS,
895            module_type_params: &EMPTY_TYPE_PARAMS,
896        };
897
898        let err = ROOT.wgsl_source().unwrap_err();
899        match &err {
900            crate::SourceError::AmbiguousTemplate {
901                template_name,
902                matching_source_names,
903                ..
904            } => {
905                assert_eq!(template_name, "shared");
906                assert!(
907                    matching_source_names.contains(&"a".to_string())
908                        && matching_source_names.contains(&"b".to_string()),
909                    "expected matching sources [a, b], got {matching_source_names:?}"
910                );
911            }
912            other => panic!("expected AmbiguousTemplate, got {other:?}"),
913        }
914        let msg = err.to_string();
915        assert!(msg.contains("ambiguous"), "got: {msg}");
916        assert!(msg.contains("shared"), "got: {msg}");
917    }
918
919    // --- Cross-module generic struct tests ---
920
921    #[wgsl(crate_path = crate)]
922    mod struct_provider {
923        pub struct Pair<T: Copy> {
924            pub a: T,
925            pub b: T,
926        }
927
928        impl<T: Copy + std::ops::Add<Output = T>> Pair<T> {
929            pub fn sum(p: Pair<T>) -> T {
930                p.a + p.b
931            }
932        }
933    }
934
935    #[wgsl(crate_path = crate)]
936    mod struct_consumer {
937        use super::struct_provider::*;
938
939        pub fn use_pair() -> f32 {
940            let p: Pair<f32> = Pair::<f32> { a: 1.0, b: 2.0 };
941            Pair::<f32>::sum(p)
942        }
943    }
944
945    #[test]
946    fn cross_module_generic_struct() {
947        // The provider should have a template for "Pair"
948        assert!(
949            struct_provider::WGSL_SOURCE
950                .templates
951                .iter()
952                .any(|t| t.name == "Pair"),
953            "struct_provider should have a template named 'Pair', templates: {:?}",
954            struct_provider::WGSL_SOURCE
955                .templates
956                .iter()
957                .map(|t| t.name)
958                .collect::<Vec<_>>()
959        );
960
961        // The consumer should produce WGSL with Pair_f32 and Pair_f32_sum
962        let full_src = struct_consumer::WGSL_SOURCE.wgsl_source().unwrap();
963        assert!(
964            full_src.contains("Pair_f32"),
965            "Expected Pair_f32 in assembled WGSL, got:\n{full_src}"
966        );
967        assert!(
968            full_src.contains("Pair_f32_sum"),
969            "Expected Pair_f32_sum in assembled WGSL, got:\n{full_src}"
970        );
971
972        // Verify Rust-side execution
973        let result = struct_consumer::use_pair();
974        assert!(
975            (result - 3.0).abs() < f32::EPSILON,
976            "1.0 + 2.0 should be 3.0, got {result}"
977        );
978    }
979
980    // --- Generic linkage / generic entry-point tests ---
981
982    #[wgsl(crate_path = crate, validate_with_instantiation_types(f32, f32))]
983    pub mod generic_shader {
984        use crate::std::*;
985
986        // The linkage variable is generic via `impl Trait` syntax. Its
987        // module-level type-parameter name is `FRAME` (the variable's
988        // identifier), independent of any entry point's type params.
989        uniform!(group(0), binding(0), FRAME: impl Convert<f32>);
990
991        // The fragment entry point has its own type parameter `T`. In the
992        // emitted IR this becomes the positional name `frag_main_0`.
993        #[fragment]
994        pub fn frag_main<T: Convert<f32> + Wgsl + Clone>() -> Vec4f {
995            vec4f(1.0, sin(f32(get!(FRAME, T)) / 128.0), 0.0, 1.0)
996        }
997    }
998
999    #[test]
1000    fn generic_module_is_template() {
1001        let m = &generic_shader::WGSL_SOURCE;
1002        assert!(m.is_template());
1003        // FRAME first (linkage var), then frag_main_0 (entry-point param 0).
1004        assert_eq!(m.module_type_params, &["FRAME", "frag_main_0"]);
1005    }
1006
1007    #[test]
1008    fn generic_module_template_source_wgsl_source_errors() {
1009        // After C2: `Source::wgsl_source` returns Err(TemplateWgsl) for a
1010        // template source. To get placeholder text, instantiate first.
1011        assert!(generic_shader::WGSL_SOURCE.wgsl_source().is_err());
1012    }
1013
1014    #[test]
1015    fn generic_module_instantiate_substitutes_placeholders() {
1016        // Order matches `module_type_params`: FRAME first, then frag_main_0.
1017        let m = generic_shader::instantiate::<f32, f32>();
1018        let src = ir::render_module(&m);
1019        assert!(
1020            !src.contains("__TPFRAME__") && !src.contains("__TPfrag_main_0__"),
1021            "expected no placeholders after instantiation, got:\n{src}"
1022        );
1023        assert!(
1024            src.contains("FRAME") && src.contains("f32"),
1025            "expected FRAME with f32 type, got:\n{src}"
1026        );
1027    }
1028
1029    #[test]
1030    fn generic_uniform_is_type_erased_static() {
1031        // The Rust-side static is `Uniform` (with default
1032        // WgslTypeVariable), not `Uniform<T>`. Setting and getting via the
1033        // typed accessors should round-trip.
1034        generic_shader::FRAME.set_typed::<f32>(42.0f32);
1035        let v = *generic_shader::FRAME.get_typed::<f32>();
1036        assert_eq!(v, 42.0);
1037
1038        // A different concrete type can also be stored simultaneously.
1039        generic_shader::FRAME.set_typed::<u32>(7u32);
1040        let u = *generic_shader::FRAME.get_typed::<u32>();
1041        assert_eq!(u, 7);
1042        // The f32 value is unaffected.
1043        let v2 = *generic_shader::FRAME.get_typed::<f32>();
1044        assert_eq!(v2, 42.0);
1045    }
1046
1047    #[cfg(feature = "validation")]
1048    #[test]
1049    fn generic_module_validate_rejects_template() {
1050        // `validate()` returns an error for template sources — they have
1051        // unresolved placeholders that aren't valid WGSL.
1052        assert!(
1053            generic_shader::WGSL_SOURCE.validate().is_err(),
1054            "template sources should fail validation"
1055        );
1056    }
1057
1058    #[test]
1059    fn generic_module_instantiate_produces_concrete_ir() {
1060        // The `instantiate` function produces an `ir::Module` with all type
1061        // params resolved. Its rendering should be free of placeholders.
1062        let m = generic_shader::instantiate::<f32, f32>();
1063        let src = ir::render_module(&m);
1064        assert!(!src.contains("__TPFRAME__"));
1065        assert!(!src.contains("__TPfrag_main_0__"));
1066        assert!(src.contains("f32"));
1067    }
1068
1069    #[cfg(feature = "validation")]
1070    #[test]
1071    fn generic_module_instantiated_source_is_valid_wgsl() {
1072        let m = generic_shader::instantiate::<f32, f32>();
1073        let src = ir::render_module(&m);
1074        crate::validate_wgsl_source(&src)
1075            .expect("instantiated generic_shader should produce valid WGSL");
1076    }
1077
1078    // Generic compute shader exercising multiple linkage and entry-point
1079    // type parameters using `impl Trait` syntax.
1080    #[wgsl(crate_path = crate, validate_with_instantiation_types(f32, u32))]
1081    pub mod generic_compute {
1082        use crate::std::*;
1083
1084        storage!(group(0), binding(0), read_write, INPUT: impl Wgsl + Convert<f32> + Clone);
1085        storage!(group(0), binding(1), read_write, OUTPUT: impl Wgsl + Convert<f32> + Clone);
1086
1087        #[compute]
1088        #[workgroup_size(1)]
1089        pub fn cs_main() {}
1090    }
1091
1092    #[test]
1093    fn generic_compute_has_two_type_params() {
1094        let m = &generic_compute::WGSL_SOURCE;
1095        assert!(m.is_template());
1096        // Linkage variables in declaration order: INPUT, OUTPUT.
1097        assert_eq!(m.module_type_params, &["INPUT", "OUTPUT"]);
1098    }
1099
1100    #[test]
1101    fn generic_compute_storages_are_type_erased() {
1102        generic_compute::INPUT.set_typed::<f32>(1.5f32);
1103        generic_compute::OUTPUT.set_typed::<u32>(42u32);
1104        let a = *generic_compute::INPUT.get_typed::<f32>();
1105        let b = *generic_compute::OUTPUT.get_typed::<u32>();
1106        assert_eq!(a, 1.5);
1107        assert_eq!(b, 42);
1108    }
1109
1110    #[test]
1111    fn generic_compute_template_source_errors() {
1112        // After C2: Source::wgsl_source on a template returns
1113        // Err(SourceError::TemplateWgsl { .. }).
1114        let err = generic_compute::WGSL_SOURCE.wgsl_source().unwrap_err();
1115        match &err {
1116            crate::SourceError::TemplateWgsl {
1117                uninstantiated_source,
1118            } => {
1119                assert_eq!(uninstantiated_source.name, "generic_compute");
1120                assert_eq!(
1121                    uninstantiated_source.module_type_params,
1122                    &["INPUT", "OUTPUT"]
1123                );
1124            }
1125            _ => unreachable!("expected TemplateWgsl for generic_compute"),
1126        }
1127        // The Display impl lists the source name and the type params so
1128        // users can quickly see what to instantiate.
1129        let msg = err.to_string();
1130        assert!(msg.contains("generic_compute"), "got: {msg}");
1131        assert!(
1132            msg.contains("INPUT") && msg.contains("OUTPUT"),
1133            "got: {msg}"
1134        );
1135        assert!(msg.contains("instantiate"), "got: {msg}");
1136    }
1137
1138    #[test]
1139    fn generic_compute_instantiate_substitutes_both() {
1140        let m = generic_compute::instantiate::<f32, u32>();
1141        let src = ir::render_module(&m);
1142        assert!(!src.contains("__TPINPUT__") && !src.contains("__TPOUTPUT__"));
1143        assert!(src.contains("INPUT") && src.contains("OUTPUT"));
1144        assert!(src.contains("f32") && src.contains("u32"));
1145    }
1146
1147    #[test]
1148    fn generic_compute_instantiate_binds_both_storages() {
1149        let m = generic_compute::instantiate::<f32, u32>();
1150        let src = ir::render_module(&m);
1151        assert!(!src.contains("__TPINPUT__"));
1152        assert!(!src.contains("__TPOUTPUT__"));
1153        assert!(src.contains("f32") && src.contains("u32"));
1154    }
1155
1156    // --- Multi-entry-point type param disambiguation tests ---
1157
1158    // Module with two entry points that both use the letter `T` and
1159    // both access the same linkage variable DATA via `get!`. This tests
1160    // that the `instantiate` function correctly disambiguates the type
1161    // params and generates the right `Type<Is = ...>` constraints.
1162    #[wgsl(crate_path = crate, validate_with_instantiation_types(f32, f32, f32))]
1163    pub mod multi_ep {
1164        use crate::std::*;
1165
1166        storage!(group(0), binding(0), read_write, DATA: impl std::any::Any);
1167
1168        #[compute]
1169        #[workgroup_size(1)]
1170        pub fn read_as<T: Wgsl + Convert<f32>>(#[builtin(global_invocation_id)] _gid: Vec3u) {
1171            let _v = get!(DATA, T);
1172        }
1173
1174        #[compute]
1175        #[workgroup_size(1)]
1176        pub fn write_as<T: Wgsl + Convert<f32>>(#[builtin(global_invocation_id)] _gid: Vec3u) {
1177            let mut _v = get_mut!(DATA, T);
1178        }
1179    }
1180
1181    #[test]
1182    fn multi_ep_instantiate_substitutes_correctly() {
1183        // Three type params: DATA, T_read_as, T_write_as.
1184        // Both entry-point type params are substituted with f32.
1185        let m = multi_ep::instantiate::<f32, f32, f32>();
1186        let src = ir::render_module(&m);
1187        assert!(
1188            !src.contains("__TPDATA__"),
1189            "expected no placeholder for DATA, got:\n{src}"
1190        );
1191        assert!(
1192            !src.contains("__TPread_as_0__") && !src.contains("__TPwrite_as_0__"),
1193            "expected no entry-point placeholders, got:\n{src}"
1194        );
1195        assert!(
1196            src.contains("f32"),
1197            "expected f32 type in output, got:\n{src}"
1198        );
1199    }
1200
1201    #[test]
1202    fn multi_ep_disambiguated_type_params_are_independent() {
1203        // Even though both entry points use the letter `T`, the
1204        // `instantiate` function should have two independent type params
1205        // (T_read_as and T_write_as). This test verifies the constraints
1206        // are correct by using different concrete types: DATA = f32 for
1207        // the read path and DATA = f32 for the write path (they must agree
1208        // since DATA: Type<Is = T_read_as> and DATA: Type<Is = T_write_as>,
1209        // so T_read_as must equal T_write_as).
1210        let m = multi_ep::instantiate::<f32, f32, f32>();
1211        let src = ir::render_module(&m);
1212        assert!(!src.contains("__TPDATA__"));
1213        assert!(src.contains("DATA"));
1214    }
1215}