use ::std::collections::{HashMap, HashSet};
pub use wgsl_rs_macros::{wgsl, wgsl_allow, wgsl_ignore};
pub mod extension;
pub mod linkage;
pub mod source_error;
pub use extension::{WgslExtension, any_ext_claims, contains_str};
pub use source_error::SourceError;
pub use wgsl_rs_ir as ir;
#[derive(Clone, Debug)]
pub struct Source {
pub id: u64,
pub name: &'static str,
pub imports: &'static [&'static Source],
pub ir_constructor: fn() -> ir::Module,
pub templates: &'static [GenericTemplate],
pub instantiations: &'static [TemplateInstantiation],
pub module_type_params: &'static [&'static str],
}
#[derive(Debug)]
pub struct GenericTemplate {
pub name: &'static str,
pub type_params: &'static [&'static str],
pub const_params: &'static [&'static str],
pub ir_constructor: fn() -> Vec<ir::Item>,
pub dependencies: &'static [TemplateDependency],
}
#[derive(Clone, Debug)]
pub struct TemplateDependency {
pub callee: &'static str,
pub type_param_mapping: &'static [usize],
}
#[derive(Debug)]
pub struct TemplateInstantiation {
pub modules: &'static [&'static Source],
pub template_name: &'static str,
pub type_args_constructor: fn() -> Vec<ir::Type>,
pub mangled_type_args: &'static [&'static str],
pub const_args_constructor: fn() -> Vec<u32>,
pub mangled_const_args: &'static [&'static str],
}
impl Source {
pub fn is_template(&self) -> bool {
!self.module_type_params.is_empty()
}
pub fn wgsl_source(&self) -> Result<String, SourceError<'_>> {
if self.is_template() {
return Err(SourceError::TemplateWgsl {
uninstantiated_source: self,
});
}
let mut out = String::new();
let mut visited_sources: HashSet<u64> = HashSet::new();
let mut seen: HashSet<(u64, String, Vec<String>, Vec<String>)> = HashSet::new();
let mut needs_tier1 = false;
let mut imports = ir::TypeImports::default();
self.collect(
&mut out,
&mut visited_sources,
&mut seen,
None,
&mut needs_tier1,
&mut imports,
)?;
if needs_tier1 {
out.insert_str(0, "enable texture_formats_tier1;\n\n");
}
Ok(out)
}
fn collect(
&self,
out: &mut String,
visited_sources: &mut HashSet<u64>,
seen: &mut HashSet<(u64, String, Vec<String>, Vec<String>)>,
subst: Option<&HashMap<String, ir::Type>>,
needs_tier1: &mut bool,
imports: &mut ir::TypeImports,
) -> Result<(), SourceError<'_>> {
for m in self.imports {
if visited_sources.insert(m.id) {
m.collect(out, visited_sources, seen, None, needs_tier1, imports)?;
}
}
let mut ir_module = (self.ir_constructor)();
if let Some(s) = subst {
ir::substitute_types(&mut ir_module, s);
}
ir::deshadow_module(&mut ir_module);
for inst in self.instantiations {
let mangled: Vec<String> = inst
.mangled_type_args
.iter()
.map(|s| (*s).to_string())
.collect();
let type_args = (inst.type_args_constructor)();
let mangled_consts: Vec<String> = inst
.mangled_const_args
.iter()
.map(|s| (*s).to_string())
.collect();
let const_args = (inst.const_args_constructor)();
imports.extend(instance_type_imports(
inst.modules,
inst.template_name,
&type_args,
&const_args,
&mangled,
&mangled_consts,
));
}
imports.extend(ir::type_imports(&ir_module));
ir::suffix_module_with_type_imports(&mut ir_module, imports);
if ir::items_need_tier1_extension(&ir_module.items) {
*needs_tier1 = true;
}
out.push_str(&ir::render_module(&ir_module));
for inst in self.instantiations {
let mangled: Vec<String> = inst
.mangled_type_args
.iter()
.map(|s| (*s).to_string())
.collect();
let type_args = (inst.type_args_constructor)();
let mangled_consts: Vec<String> = inst
.mangled_const_args
.iter()
.map(|s| (*s).to_string())
.collect();
let const_args = (inst.const_args_constructor)();
instantiate_template_into(
inst.modules,
inst.template_name,
&mangled,
&type_args,
&mangled_consts,
&const_args,
out,
seen,
needs_tier1,
imports,
)?;
}
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
fn instantiate_template_into<'a>(
sources: &[&'a Source],
template_name: &str,
mangled_type_args: &[String],
type_args: &[ir::Type],
mangled_const_args: &[String],
const_args: &[u32],
out: &mut String,
seen: &mut HashSet<(u64, String, Vec<String>, Vec<String>)>,
needs_tier1: &mut bool,
imports: &mut ir::TypeImports,
) -> Result<(), SourceError<'a>> {
let (source, template) = resolve_template(sources, template_name, mangled_type_args)?;
let key = (
source.id,
template_name.to_string(),
mangled_type_args.to_vec(),
mangled_const_args.to_vec(),
);
if !seen.insert(key) {
return Ok(()); }
for dep in template.dependencies {
let dep_mangled: Vec<String> = dep
.type_param_mapping
.iter()
.map(|&idx| mangled_type_args[idx].clone())
.collect();
let dep_args: Vec<ir::Type> = dep
.type_param_mapping
.iter()
.map(|&idx| type_args[idx].clone())
.collect();
instantiate_template_into(
&[source],
dep.callee,
&dep_mangled,
&dep_args,
mangled_const_args,
const_args,
out,
seen,
needs_tier1,
imports,
)?;
}
let mut items = build_instance_items(
template,
type_args,
const_args,
mangled_type_args,
mangled_const_args,
);
ir::deshadow_items(&mut items);
ir::suffix_items_with_type_imports(&mut items, imports);
imports.extend(ir::type_imports_in_items(&items));
if ir::items_need_tier1_extension(&items) {
*needs_tier1 = true;
}
out.push_str(&ir::render_items(&items));
Ok(())
}
fn resolve_template<'a>(
sources: &[&'a Source],
template_name: &str,
mangled_type_args: &[String],
) -> Result<(&'a Source, &'a GenericTemplate), SourceError<'a>> {
let available_templates: Vec<String> = sources
.iter()
.copied()
.flat_map(|m| m.templates.iter().map(|t| t.name.to_string()))
.collect();
let mut matching_sources: Vec<&Source> = sources
.iter()
.copied()
.filter(|m| m.templates.iter().any(|t| t.name == template_name))
.collect();
if matching_sources.is_empty() {
return Err(SourceError::TemplateNotFound {
template_name: template_name.to_string(),
mangled_type_args: mangled_type_args.to_vec(),
available_templates,
});
}
if matching_sources.len() > 1 {
let source_names: Vec<String> = matching_sources
.iter()
.map(|m| m.name.to_string())
.collect();
return Err(SourceError::AmbiguousTemplate {
template_name: template_name.to_string(),
mangled_type_args: mangled_type_args.to_vec(),
matching_source_names: source_names,
available_templates,
});
}
let source = matching_sources
.pop()
.expect("matching_sources is guaranteed to be non-empty after checks");
let Some(template) = source.templates.iter().find(|t| t.name == template_name) else {
unreachable!(
"resolved source '{}' does not contain template '{}'; available templates: {:?}",
source.name, template_name, available_templates
);
};
Ok((source, template))
}
fn build_instance_items(
template: &GenericTemplate,
type_args: &[ir::Type],
const_args: &[u32],
mangled_type_args: &[String],
mangled_const_args: &[String],
) -> Vec<ir::Item> {
let mut subst: HashMap<String, ir::Type> = HashMap::new();
for (param, arg) in template.type_params.iter().zip(type_args.iter()) {
subst.insert((*param).to_string(), arg.clone());
}
let mut consts: HashMap<String, u32> = HashMap::new();
for (param, arg) in template.const_params.iter().zip(const_args.iter()) {
consts.insert((*param).to_string(), *arg);
}
let mut items = (template.ir_constructor)();
ir::substitute_items(&mut items, &subst);
ir::substitute_consts_in_items(&mut items, &consts);
let instance_name = if mangled_type_args.is_empty() && mangled_const_args.is_empty() {
template.name.to_string()
} else {
let mut components: Vec<String> =
Vec::with_capacity(1 + mangled_type_args.len() + mangled_const_args.len());
components.push(template.name.to_string());
for s in mangled_type_args {
components.push(s.clone());
}
for s in mangled_const_args {
components.push(s.clone());
}
let str_components: Vec<&str> = components.iter().map(|s| s.as_str()).collect();
ir::mangle(&str_components)
};
if instance_name != template.name {
ir::rename_items(&mut items, template.name, &instance_name);
}
items
}
fn instance_type_imports(
sources: &[&Source],
template_name: &str,
type_args: &[ir::Type],
const_args: &[u32],
mangled_type_args: &[String],
mangled_const_args: &[String],
) -> ir::TypeImports {
let Ok((_source, template)) = resolve_template(sources, template_name, mangled_type_args)
else {
return ir::TypeImports::default();
};
let items = build_instance_items(
template,
type_args,
const_args,
mangled_type_args,
mangled_const_args,
);
ir::type_imports_in_items(&items)
}
#[cfg(feature = "validation")]
impl Source {
pub fn validate(&self) -> Result<(), String> {
if self.is_template() {
return Err(
"cannot validate a template source — call instantiate(...) first".to_string(),
);
}
validate_wgsl_source(&self.wgsl_source().expect("concrete source"))
}
}
#[cfg(feature = "validation")]
pub fn validate_wgsl_source(source: &str) -> Result<(), String> {
let module = naga::front::wgsl::parse_str(source).map_err(|e| e.emit_to_string(source))?;
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.map_err(|e| e.emit_to_string(source))?;
Ok(())
}
pub mod std;
#[cfg(test)]
#[allow(dead_code)]
mod test {
use crate::{GenericTemplate, Source, TemplateDependency, TemplateInstantiation, ir, wgsl};
#[wgsl(crate_path = crate)]
pub mod a {
pub const THREE: u32 = 3;
}
#[wgsl(crate_path = crate)]
pub mod b {
use super::a::*;
pub fn add_three_to_x_minus_y(x: u32, y: u32) -> u32 {
let i: u32 = (x - y) + THREE;
i
}
}
#[wgsl(crate_path = crate)]
pub mod c {
use super::b::*;
pub fn main() {
let _u = add_three_to_x_minus_y(1337, 666);
}
}
#[wgsl(crate_path = crate)]
pub mod vecs {
use crate::std::*;
const _VEC3F_ONE: Vec4f = vec4f(0.0, 1.0, 2.0, 3.0);
}
#[test]
fn module_source() {
let source = c::WGSL_SOURCE.wgsl_source().unwrap();
c::main();
assert!(source.contains("const THREE: u32 = 3u;"), "got:\n{source}");
assert!(
source.contains("fn add_three_to_x_minus_y(x: u32, y: u32) -> u32"),
"got:\n{source}"
);
assert!(source.contains("fn main()"), "got:\n{source}");
assert!(
source.contains("add_three_to_x_minus_y(1337u, 666u)"),
"got:\n{source}"
);
assert_eq!(
source.matches("const THREE:").count(),
1,
"THREE should appear exactly once (no duplicates from transitive imports), \
got:\n{source}"
);
}
#[wgsl(crate_path = crate)]
pub mod trait_provider {
pub trait Scalable {
fn scale(val: Self, factor: f32) -> Self;
}
impl Scalable for f32 {
fn scale(val: f32, factor: f32) -> f32 {
val * factor
}
}
}
#[wgsl(crate_path = crate)]
pub mod trait_consumer {
use super::trait_provider::*;
pub fn apply_scale<T: Scalable>(val: T, factor: f32) -> T {
T::scale(val, factor)
}
pub fn go() -> f32 {
apply_scale::<f32>(2.0, 3.0)
}
}
#[test]
fn cross_module_trait_impl() {
let provider_src = trait_provider::WGSL_SOURCE.wgsl_source().unwrap();
assert!(
provider_src.contains("fn f32_scale("),
"trait_provider should contain f32_scale, got:\n{provider_src}"
);
let consumer_src = trait_consumer::WGSL_SOURCE.wgsl_source().unwrap();
assert!(
consumer_src.contains("fn _1apply_scale_f32("),
"trait_consumer should contain _1apply_scale_f32, got:\n{consumer_src}"
);
assert!(
consumer_src.contains("f32_scale("),
"_1apply_scale_f32 should call f32_scale, got:\n{consumer_src}"
);
assert!(
consumer_src.contains("fn f32_scale("),
"concatenated source should have f32_scale (from import), got:\n{consumer_src}"
);
let result = trait_consumer::go();
assert!(
(result - 6.0).abs() < f32::EPSILON,
"2.0 * 3.0 should be 6.0, got {result}"
);
}
#[wgsl(crate_path = crate)]
mod dedupe_provider {
pub fn id<T: Copy>(v: T) -> T {
v
}
}
#[wgsl(crate_path = crate)]
mod dedupe_left {
use super::dedupe_provider::*;
pub fn left() -> f32 {
id::<f32>(1.0)
}
}
#[wgsl(crate_path = crate)]
mod dedupe_right {
use super::dedupe_provider::*;
pub fn right() -> f32 {
id::<f32>(2.0)
}
}
#[expect(dead_code)]
#[wgsl(crate_path = crate)]
mod dedupe_root {
#[rustfmt::skip]
use super::dedupe_left::*;
#[rustfmt::skip]
use super::dedupe_right::*;
pub fn root() -> f32 {
left() + right()
}
}
#[test]
fn cross_module_instantiations_are_deduped_globally() {
let full_src = dedupe_root::WGSL_SOURCE.wgsl_source().unwrap();
assert_eq!(
full_src.matches("fn id_f32(").count(),
1,
"expected id_f32 to appear once, got:\n{full_src}"
);
}
#[test]
fn ambiguous_template_provider_errors() {
fn empty_source_ir() -> ir::Module {
ir::Module {
name: "stub",
items: vec![],
attrs: vec![],
}
}
fn shared_template_items() -> Vec<ir::Item> {
vec![ir::Item::Fn(ir::ItemFn {
type_params: vec![],
const_params: vec![],
fn_attrs: ir::FnAttrs::None,
name: "shared".to_string().into(),
inputs: vec![ir::FnArg {
inter_stage_io: vec![],
name: "x".to_string(),
ty: ir::Type::TypeParam {
name: "T".to_string(),
},
attrs: vec![],
}],
return_type: ir::ReturnType::Type {
annotation: ir::ReturnTypeAnnotation::None,
ty: ir::Type::TypeParam {
name: "T".to_string(),
},
},
block: ir::Block {
stmts: vec![ir::Stmt::Return(Some(ir::Expr::Ident("x".to_string())))],
},
attrs: vec![],
})]
}
fn f32_args() -> Vec<ir::Type> {
vec![ir::Type::Scalar(ir::ScalarType::F32)]
}
static DEP: [TemplateDependency; 0] = [];
static A_TEMPLATES: [GenericTemplate; 1] = [GenericTemplate {
name: "shared",
type_params: &["T"],
const_params: &[],
ir_constructor: shared_template_items,
dependencies: &DEP,
}];
static B_TEMPLATES: [GenericTemplate; 1] = [GenericTemplate {
name: "shared",
type_params: &["T"],
const_params: &[],
ir_constructor: shared_template_items,
dependencies: &DEP,
}];
static EMPTY_SOURCES: [&Source; 0] = [];
static EMPTY_INSTS: [TemplateInstantiation; 0] = [];
static EMPTY_TYPE_PARAMS: [&str; 0] = [];
static MOD_A: Source = Source {
id: 0,
name: "a",
imports: &EMPTY_SOURCES,
ir_constructor: empty_source_ir,
templates: &A_TEMPLATES,
instantiations: &EMPTY_INSTS,
module_type_params: &EMPTY_TYPE_PARAMS,
};
static MOD_B: Source = Source {
id: 1,
name: "b",
imports: &EMPTY_SOURCES,
ir_constructor: empty_source_ir,
templates: &B_TEMPLATES,
instantiations: &EMPTY_INSTS,
module_type_params: &EMPTY_TYPE_PARAMS,
};
static ROOT_INSTS: [TemplateInstantiation; 1] = [TemplateInstantiation {
modules: &[&MOD_A, &MOD_B],
template_name: "shared",
type_args_constructor: f32_args,
mangled_type_args: &["f32"],
const_args_constructor: || vec![],
mangled_const_args: &[],
}];
static ROOT: Source = Source {
id: 2,
name: "root",
imports: &EMPTY_SOURCES,
ir_constructor: empty_source_ir,
templates: &[],
instantiations: &ROOT_INSTS,
module_type_params: &EMPTY_TYPE_PARAMS,
};
let err = ROOT.wgsl_source().unwrap_err();
match &err {
crate::SourceError::AmbiguousTemplate {
template_name,
matching_source_names,
..
} => {
assert_eq!(template_name, "shared");
assert!(
matching_source_names.contains(&"a".to_string())
&& matching_source_names.contains(&"b".to_string()),
"expected matching sources [a, b], got {matching_source_names:?}"
);
}
other => panic!("expected AmbiguousTemplate, got {other:?}"),
}
let msg = err.to_string();
assert!(msg.contains("ambiguous"), "got: {msg}");
assert!(msg.contains("shared"), "got: {msg}");
}
#[wgsl(crate_path = crate)]
mod struct_provider {
pub struct Pair<T: Copy> {
pub a: T,
pub b: T,
}
impl<T: Copy + std::ops::Add<Output = T>> Pair<T> {
pub fn sum(p: Pair<T>) -> T {
p.a + p.b
}
}
}
#[wgsl(crate_path = crate)]
mod struct_consumer {
use super::struct_provider::*;
pub fn use_pair() -> f32 {
let p: Pair<f32> = Pair::<f32> { a: 1.0, b: 2.0 };
Pair::<f32>::sum(p)
}
}
#[test]
fn cross_module_generic_struct() {
assert!(
struct_provider::WGSL_SOURCE
.templates
.iter()
.any(|t| t.name == "Pair"),
"struct_provider should have a template named 'Pair', templates: {:?}",
struct_provider::WGSL_SOURCE
.templates
.iter()
.map(|t| t.name)
.collect::<Vec<_>>()
);
let full_src = struct_consumer::WGSL_SOURCE.wgsl_source().unwrap();
assert!(
full_src.contains("Pair_f32"),
"Expected Pair_f32 in assembled WGSL, got:\n{full_src}"
);
assert!(
full_src.contains("Pair_f32_sum"),
"Expected Pair_f32_sum in assembled WGSL, got:\n{full_src}"
);
let result = struct_consumer::use_pair();
assert!(
(result - 3.0).abs() < f32::EPSILON,
"1.0 + 2.0 should be 3.0, got {result}"
);
}
#[wgsl(crate_path = crate, validate_with_instantiation_types(f32, f32))]
pub mod generic_shader {
use crate::std::*;
uniform!(group(0), binding(0), FRAME: impl Convert<f32>);
#[fragment]
pub fn frag_main<T: Convert<f32> + Wgsl + Clone>() -> Vec4f {
vec4f(1.0, sin(f32(get!(FRAME, T)) / 128.0), 0.0, 1.0)
}
}
#[test]
fn generic_module_is_template() {
let m = &generic_shader::WGSL_SOURCE;
assert!(m.is_template());
assert_eq!(m.module_type_params, &["FRAME", "frag_main_0"]);
}
#[test]
fn generic_module_template_source_wgsl_source_errors() {
assert!(generic_shader::WGSL_SOURCE.wgsl_source().is_err());
}
#[test]
fn generic_module_instantiate_substitutes_placeholders() {
let m = generic_shader::instantiate::<f32, f32>();
let src = ir::render_module(&m);
assert!(
!src.contains("__TPFRAME__") && !src.contains("__TPfrag_main_0__"),
"expected no placeholders after instantiation, got:\n{src}"
);
assert!(
src.contains("FRAME") && src.contains("f32"),
"expected FRAME with f32 type, got:\n{src}"
);
}
#[test]
fn generic_uniform_is_type_erased_static() {
generic_shader::FRAME.set_typed::<f32>(42.0f32);
let v = *generic_shader::FRAME.get_typed::<f32>();
assert_eq!(v, 42.0);
generic_shader::FRAME.set_typed::<u32>(7u32);
let u = *generic_shader::FRAME.get_typed::<u32>();
assert_eq!(u, 7);
let v2 = *generic_shader::FRAME.get_typed::<f32>();
assert_eq!(v2, 42.0);
}
#[cfg(feature = "validation")]
#[test]
fn generic_module_validate_rejects_template() {
assert!(
generic_shader::WGSL_SOURCE.validate().is_err(),
"template sources should fail validation"
);
}
#[test]
fn generic_module_instantiate_produces_concrete_ir() {
let m = generic_shader::instantiate::<f32, f32>();
let src = ir::render_module(&m);
assert!(!src.contains("__TPFRAME__"));
assert!(!src.contains("__TPfrag_main_0__"));
assert!(src.contains("f32"));
}
#[cfg(feature = "validation")]
#[test]
fn generic_module_instantiated_source_is_valid_wgsl() {
let m = generic_shader::instantiate::<f32, f32>();
let src = ir::render_module(&m);
crate::validate_wgsl_source(&src)
.expect("instantiated generic_shader should produce valid WGSL");
}
#[wgsl(crate_path = crate, validate_with_instantiation_types(f32, u32))]
pub mod generic_compute {
use crate::std::*;
storage!(group(0), binding(0), read_write, INPUT: impl Wgsl + Convert<f32> + Clone);
storage!(group(0), binding(1), read_write, OUTPUT: impl Wgsl + Convert<f32> + Clone);
#[compute]
#[workgroup_size(1)]
pub fn cs_main() {}
}
#[test]
fn generic_compute_has_two_type_params() {
let m = &generic_compute::WGSL_SOURCE;
assert!(m.is_template());
assert_eq!(m.module_type_params, &["INPUT", "OUTPUT"]);
}
#[test]
fn generic_compute_storages_are_type_erased() {
generic_compute::INPUT.set_typed::<f32>(1.5f32);
generic_compute::OUTPUT.set_typed::<u32>(42u32);
let a = *generic_compute::INPUT.get_typed::<f32>();
let b = *generic_compute::OUTPUT.get_typed::<u32>();
assert_eq!(a, 1.5);
assert_eq!(b, 42);
}
#[test]
fn generic_compute_template_source_errors() {
let err = generic_compute::WGSL_SOURCE.wgsl_source().unwrap_err();
match &err {
crate::SourceError::TemplateWgsl {
uninstantiated_source,
} => {
assert_eq!(uninstantiated_source.name, "generic_compute");
assert_eq!(
uninstantiated_source.module_type_params,
&["INPUT", "OUTPUT"]
);
}
_ => unreachable!("expected TemplateWgsl for generic_compute"),
}
let msg = err.to_string();
assert!(msg.contains("generic_compute"), "got: {msg}");
assert!(
msg.contains("INPUT") && msg.contains("OUTPUT"),
"got: {msg}"
);
assert!(msg.contains("instantiate"), "got: {msg}");
}
#[test]
fn generic_compute_instantiate_substitutes_both() {
let m = generic_compute::instantiate::<f32, u32>();
let src = ir::render_module(&m);
assert!(!src.contains("__TPINPUT__") && !src.contains("__TPOUTPUT__"));
assert!(src.contains("INPUT") && src.contains("OUTPUT"));
assert!(src.contains("f32") && src.contains("u32"));
}
#[test]
fn generic_compute_instantiate_binds_both_storages() {
let m = generic_compute::instantiate::<f32, u32>();
let src = ir::render_module(&m);
assert!(!src.contains("__TPINPUT__"));
assert!(!src.contains("__TPOUTPUT__"));
assert!(src.contains("f32") && src.contains("u32"));
}
#[wgsl(crate_path = crate, validate_with_instantiation_types(f32, f32, f32))]
pub mod multi_ep {
use crate::std::*;
storage!(group(0), binding(0), read_write, DATA: impl std::any::Any);
#[compute]
#[workgroup_size(1)]
pub fn read_as<T: Wgsl + Convert<f32>>(#[builtin(global_invocation_id)] _gid: Vec3u) {
let _v = get!(DATA, T);
}
#[compute]
#[workgroup_size(1)]
pub fn write_as<T: Wgsl + Convert<f32>>(#[builtin(global_invocation_id)] _gid: Vec3u) {
let mut _v = get_mut!(DATA, T);
}
}
#[test]
fn multi_ep_instantiate_substitutes_correctly() {
let m = multi_ep::instantiate::<f32, f32, f32>();
let src = ir::render_module(&m);
assert!(
!src.contains("__TPDATA__"),
"expected no placeholder for DATA, got:\n{src}"
);
assert!(
!src.contains("__TPread_as_0__") && !src.contains("__TPwrite_as_0__"),
"expected no entry-point placeholders, got:\n{src}"
);
assert!(
src.contains("f32"),
"expected f32 type in output, got:\n{src}"
);
}
#[test]
fn multi_ep_disambiguated_type_params_are_independent() {
let m = multi_ep::instantiate::<f32, f32, f32>();
let src = ir::render_module(&m);
assert!(!src.contains("__TPDATA__"));
assert!(src.contains("DATA"));
}
}