1use ::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
29pub use wgsl_rs_ir as ir;
33
34#[derive(Clone, Debug)]
62pub struct Source {
63 pub id: u64,
72
73 pub name: &'static str,
75
76 pub imports: &'static [&'static Source],
78
79 pub ir_constructor: fn() -> ir::Module,
85
86 pub templates: &'static [GenericTemplate],
92
93 pub instantiations: &'static [TemplateInstantiation],
99
100 pub module_type_params: &'static [&'static str],
123}
124
125#[derive(Debug)]
133pub struct GenericTemplate {
134 pub name: &'static str,
136 pub type_params: &'static [&'static str],
138 pub const_params: &'static [&'static str],
140 pub ir_constructor: fn() -> Vec<ir::Item>,
142 pub dependencies: &'static [TemplateDependency],
147}
148
149#[derive(Clone, Debug)]
151pub struct TemplateDependency {
152 pub callee: &'static str,
154 pub type_param_mapping: &'static [usize],
158}
159
160#[derive(Debug)]
162pub struct TemplateInstantiation {
163 pub modules: &'static [&'static Source],
168 pub template_name: &'static str,
170 pub type_args_constructor: fn() -> Vec<ir::Type>,
173 pub mangled_type_args: &'static [&'static str],
176 pub const_args_constructor: fn() -> Vec<u32>,
179 pub mangled_const_args: &'static [&'static str],
181}
182
183impl Source {
184 pub fn is_template(&self) -> bool {
190 !self.module_type_params.is_empty()
191 }
192
193 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 if needs_tier1 {
224 out.insert_str(0, "enable texture_formats_tier1;\n\n");
225 }
226 Ok(out)
227 }
228
229 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 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 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 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 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 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#[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(()); }
372
373 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 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 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
424fn 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 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
481fn 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 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
529fn 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 pub fn validate(&self) -> Result<(), String> {
588 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#[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 assert!(
675 source.contains("add_three_to_x_minus_y(1337u, 666u)"),
676 "got:\n{source}"
677 );
678
679 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 #[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 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 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 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 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 #[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 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 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 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 #[wgsl(crate_path = crate, validate_with_instantiation_types(f32, f32))]
983 pub mod generic_shader {
984 use crate::std::*;
985
986 uniform!(group(0), binding(0), FRAME: impl Convert<f32>);
990
991 #[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 assert_eq!(m.module_type_params, &["FRAME", "frag_main_0"]);
1005 }
1006
1007 #[test]
1008 fn generic_module_template_source_wgsl_source_errors() {
1009 assert!(generic_shader::WGSL_SOURCE.wgsl_source().is_err());
1012 }
1013
1014 #[test]
1015 fn generic_module_instantiate_substitutes_placeholders() {
1016 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 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 generic_shader::FRAME.set_typed::<u32>(7u32);
1040 let u = *generic_shader::FRAME.get_typed::<u32>();
1041 assert_eq!(u, 7);
1042 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 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 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 #[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 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 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 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 #[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 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 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}