Skip to main content

wgsl_rs/linkage/
wgpu.rs

1//! Runtime wgpu linkage analysis for WGSL modules.
2//!
3//! Given an [`ir::Module`] (produced by a `#[wgsl]` source's
4//! `WGSL_SOURCE.ir_constructor`, or by the macro-emitted
5//! `instantiate::<...>()` function for templates), this module extracts
6//! the binding and entry-point information needed to build wgpu
7//! pipelines.
8//!
9//! # Why at runtime?
10//!
11//! The pre-[#120](https://github.com/schell/wgsl-rs/issues/120)
12//! implementation generated all of this at proc-macro expansion time by walking
13//! the parse tree in `wgsl-rs-macros/src/linkage.rs`. That had two drawbacks:
14//!
15//! 1. Template modules (`#[wgsl]` modules with type parameters) couldn't get
16//!    any linkage — the generated WGSL was a template with `__TP{name}__`
17//!    placeholders, so a `wgpu::ShaderModule` couldn't be built from it.
18//! 2. The proc-macro walked a parse tree that duplicated information already
19//!    present in `wgsl-rs-ir`.
20//!
21//! Walking the runtime IR unifies the two cases. After the macro-emitted
22//! `instantiate::<...>()` function produces a concrete `ir::Module`, the
23//! same analyzer used for non-template sources works on the result.
24//!
25//! # Example
26//!
27//! ```ignore
28//! use wgsl_rs::linkage::wgpu;
29//!
30//! let linkage = wgpu::analyze_wgsl_module(&hello_triangle::WGSL_SOURCE).unwrap();
31//! let shader_module = linkage.shader_module(&device);
32//! let bg_layout = linkage.bind_group(0).unwrap().layout(&device);
33//! let bg = linkage.bind_group(0).unwrap().create(
34//!     &device, &bg_layout, &[frame_uniform.as_entire_binding()],
35//! );
36//! let vtx = linkage.vertex_entries.iter().find(|e| e.name == "vtx_main").unwrap()
37//!     .vertex_state(&shader_module);
38//! ```
39
40use std::{
41    borrow::Cow,
42    collections::{BTreeMap, HashMap},
43};
44
45use snafu::prelude::*;
46use wgsl_rs_ir as ir;
47
48use crate::Source;
49
50/// Errors thrown by the WGPU linkage module.
51#[derive(Snafu, Debug)]
52pub enum Error {
53    #[snafu(display(
54        "Unable to resolve template '{template_name}' for type args {:?}; available templates: \
55         {:?}",
56        mangled_type_args,
57        available
58    ))]
59    TemplateResolution {
60        template_name: String,
61        mangled_type_args: Vec<String>,
62        available: Vec<String>,
63    },
64
65    #[snafu(display(
66        "{module}::bind_group_{group}: missing binding resource for declared name {name:?}"
67    ))]
68    MissingBinding {
69        module: &'static str,
70        group: u32,
71        name: String,
72    },
73
74    #[snafu(display(
75        "{module}::bind_group_{group}: resource provided for unknown binding name {name:?} \
76         (declared names: {declared:?})"
77    ))]
78    UnknownBindingName {
79        module: &'static str,
80        group: u32,
81        name: String,
82        declared: Vec<String>,
83    },
84
85    #[snafu(display(
86        "{module}::bind_group_{group}: binding name {name:?} appears more than once in resources"
87    ))]
88    DuplicateBindingName {
89        module: &'static str,
90        group: u32,
91        name: String,
92    },
93
94    #[snafu(display(
95        "{module}: no bind group @group({group}) declared; available groups: {available:?}"
96    ))]
97    NoSuchBindGroup {
98        module: &'static str,
99        group: u32,
100        available: Vec<u32>,
101    },
102}
103
104impl From<crate::SourceError<'_>> for Error {
105    fn from(err: crate::SourceError<'_>) -> Self {
106        match err {
107            crate::SourceError::TemplateWgsl {
108                uninstantiated_source,
109            } => Error::TemplateResolution {
110                template_name: format!("<source {}>", uninstantiated_source.name),
111                mangled_type_args: uninstantiated_source
112                    .module_type_params
113                    .iter()
114                    .map(|s| (*s).to_string())
115                    .collect(),
116                available: Vec::new(),
117            },
118            crate::SourceError::TemplateNotFound {
119                template_name,
120                mangled_type_args,
121                available_templates,
122            } => Error::TemplateResolution {
123                template_name,
124                mangled_type_args,
125                available: available_templates,
126            },
127            crate::SourceError::AmbiguousTemplate {
128                template_name,
129                mangled_type_args,
130                matching_source_names,
131                available_templates: _,
132            } => Error::TemplateResolution {
133                template_name,
134                mangled_type_args,
135                available: matching_source_names,
136            },
137        }
138    }
139}
140
141/// All the wgpu linkage information extracted from a WGSL module.
142///
143/// Produced by [`analyze_ir_module`] or [`analyze_wgsl_module`]. Owns the
144/// string labels that the various `wgpu::*Descriptor` types borrow, so
145/// hold this struct for the lifetime of any borrowed descriptor.
146#[derive(Clone, Debug)]
147pub struct WgpuLinkage {
148    /// The module's name (used as a default label for descriptors).
149    pub module_label: &'static str,
150
151    /// The concrete, fully-assembled IR the linkage was built from. This
152    /// is the IR rendered by [`Self::wgsl_source`] to produce the shader
153    /// source's WGSL text. For sources built from [`crate::Source`] it
154    /// is the result of flattening imports and template instantiations;
155    /// for modules built from [`ir::Module`] (e.g. an instantiated
156    /// template) it is that module directly. Always concrete: no
157    /// `Type::TypeParam` nodes.
158    ir: ir::Module,
159
160    /// Bind groups keyed by `@group(N)` index. Each group is sorted by
161    /// binding number.
162    pub bind_groups: BTreeMap<u32, BindGroupInfo>,
163
164    /// Lazily-built `wgpu::BindGroupLayout` per `@group(N)` index. Empty
165    /// until a cache-populating method is called. Populating one does
166    /// not populate the others; calling
167    /// [`WgpuLinkage::pipeline_layout`] populates all groups at once.
168    bind_group_layouts: HashMap<u32, wgpu::BindGroupLayout>,
169
170    /// Lazily-built `wgpu::PipelineLayout` referencing all bind group
171    /// layouts. `None` until [`WgpuLinkage::pipeline_layout`] is called.
172    pipeline_layout: Option<wgpu::PipelineLayout>,
173
174    /// Vertex shader entry points declared in the module.
175    pub vertex_entries: Vec<EntryPointInfo>,
176
177    /// Fragment shader entry points declared in the module.
178    pub fragment_entries: Vec<EntryPointInfo>,
179
180    /// Compute shader entry points declared in the module.
181    pub compute_entries: Vec<ComputeEntryInfo>,
182
183    /// Buffer descriptors for `uniform!` and `storage!` declarations, in
184    /// declaration order. Indexed by binding name via [`Self::buffer`].
185    pub buffers: Vec<BufferDescriptorInfo>,
186}
187
188impl core::fmt::Display for WgpuLinkage {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        let WgpuLinkage {
191            module_label,
192            ir,
193            bind_groups,
194            bind_group_layouts,
195            pipeline_layout,
196            vertex_entries,
197            fragment_entries,
198            compute_entries,
199            buffers,
200        } = self;
201
202        writeln!(
203            f,
204            "linkage for '{module_label}' ({} IR items)",
205            ir.items.len()
206        )?;
207
208        writeln!(f, "  entry points:")?;
209        if vertex_entries.is_empty() && fragment_entries.is_empty() && compute_entries.is_empty() {
210            writeln!(f, "    (none)")?;
211        }
212        for e in vertex_entries {
213            writeln!(f, "    vertex: {}", e.name)?;
214        }
215        for e in fragment_entries {
216            writeln!(f, "    fragment: {}", e.name)?;
217        }
218        for e in compute_entries {
219            let (x, y, z) = e.workgroup_size;
220            writeln!(f, "    compute: {} @workgroup_size({x}, {y}, {z})", e.name)?;
221        }
222
223        writeln!(f, "  bind groups:")?;
224        if bind_groups.is_empty() {
225            writeln!(f, "    (none)")?;
226        }
227        for (group, info) in bind_groups {
228            writeln!(f, "    @group({group}) {}", info.label)?;
229            for meta in &info.bindings {
230                writeln!(
231                    f,
232                    "      @binding({}) {} ({})",
233                    meta.binding,
234                    meta.name,
235                    binding_kind_label(&meta.kind)
236                )?;
237            }
238        }
239
240        writeln!(f, "  buffers:")?;
241        if buffers.is_empty() {
242            writeln!(f, "    (none)")?;
243        }
244        for b in buffers {
245            // `size` is 0 for runtime-sized storage buffers; the buffer's
246            // length is then whatever the caller chooses to allocate.
247            let size = if b.size == 0 {
248                "runtime-sized".to_string()
249            } else {
250                format!("{} bytes", b.size)
251            };
252            writeln!(
253                f,
254                "      @group({}) @binding({}): {} ({}, {size}, {ty})",
255                b.group,
256                b.binding,
257                b.binding_name,
258                buffer_kind_label(&b.kind),
259                ty = b.ty
260            )?;
261        }
262
263        // The layouts are built lazily; show how much of the cache is
264        // populated. Calling `pipeline_layout` fills every group at once.
265        write!(
266            f,
267            "  cache: {} of {} bind group layouts built, pipeline layout {}",
268            bind_group_layouts.len(),
269            bind_groups.len(),
270            if pipeline_layout.is_some() {
271                "built"
272            } else {
273                "not built"
274            }
275        )?;
276
277        Ok(())
278    }
279}
280
281/// A human-readable label for a [`BindingKind`].
282fn binding_kind_label(kind: &BindingKind) -> &'static str {
283    match kind {
284        BindingKind::Uniform => "uniform",
285        BindingKind::Storage { read_only: true } => "storage, read-only",
286        BindingKind::Storage { read_only: false } => "storage, read-write",
287        BindingKind::Sampler { comparison: true } => "comparison sampler",
288        BindingKind::Sampler { comparison: false } => "sampler",
289        BindingKind::Texture => "texture",
290        BindingKind::DepthTexture => "depth texture",
291        BindingKind::StorageTexture { read_write: true } => "storage texture, read-write",
292        BindingKind::StorageTexture { read_write: false } => "storage texture",
293    }
294}
295
296/// A human-readable label for a [`BufferKind`].
297fn buffer_kind_label(kind: &BufferKind) -> &'static str {
298    match kind {
299        BufferKind::Uniform => "uniform",
300        BufferKind::Storage { read_only: true } => "storage, read-only",
301        BufferKind::Storage { read_only: false } => "storage, read-write",
302    }
303}
304
305impl WgpuLinkage {
306    /// Returns the bind group for the given `@group(N)` index, if any.
307    pub fn bind_group(&self, group: u32) -> Option<&BindGroupInfo> {
308        self.bind_groups.get(&group)
309    }
310
311    /// Returns the buffer descriptor for a `uniform!` or `storage!`
312    /// binding by its declared name, if any.
313    pub fn buffer(&self, name: &str) -> Option<&BufferDescriptorInfo> {
314        self.buffers.iter().find(|b| b.binding_name == name)
315    }
316
317    /// Returns the buffer descriptor for a `uniform!` or `storage!`
318    /// binding by its declared name, if any - removing it from the linkage.
319    ///
320    /// Use this to ensure that you provide data for all buffers in your
321    /// program.
322    pub fn take_buffer(&mut self, name: &str) -> Option<BufferDescriptorInfo> {
323        let mut info = None;
324        self.buffers.retain(|bi| {
325            if bi.binding_name == name {
326                info = Some(bi.clone());
327                false
328            } else {
329                true
330            }
331        });
332        info
333    }
334
335    /// Returns the WGSL source for this linkage's source, rendered from
336    /// the concrete IR the linkage was built from.
337    ///
338    /// The linkage's IR is concrete (no `Type::TypeParam`s), so the
339    /// rendered text is free of `__TP{name}__` placeholders. For a
340    /// linkage built from a template `Source` via
341    /// [`analyze_ir_module`], this is the instantiated source.
342    pub fn wgsl_source(&self) -> String {
343        ir::render_module(&self.ir)
344    }
345
346    /// Builds a `wgpu::ShaderModuleDescriptor` for this module using
347    /// the concrete IR the linkage was built from. The source string
348    /// is owned by the descriptor, so no borrow of `self` is required
349    /// to build a wgpu pipeline.
350    pub fn shader_module_descriptor(&self) -> wgpu::ShaderModuleDescriptor<'static> {
351        wgpu::ShaderModuleDescriptor {
352            label: Some(self.module_label),
353            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(self.wgsl_source())),
354        }
355    }
356
357    /// Creates a `wgpu::ShaderModule` from the concrete IR the linkage
358    /// was built from.
359    pub fn shader_module(&self, device: &wgpu::Device) -> wgpu::ShaderModule {
360        device.create_shader_module(self.shader_module_descriptor())
361    }
362
363    /// Returns the first vertex entry matching `name`, or `None`.
364    pub fn vertex_entry(&self, name: &str) -> Option<&EntryPointInfo> {
365        self.vertex_entries.iter().find(|e| e.name == name)
366    }
367
368    /// Returns the first fragment entry matching `name`, or `None`.
369    pub fn fragment_entry(&self, name: &str) -> Option<&EntryPointInfo> {
370        self.fragment_entries.iter().find(|e| e.name == name)
371    }
372
373    /// Returns the first compute entry matching `name`, or `None`.
374    pub fn compute_entry(&self, name: &str) -> Option<&ComputeEntryInfo> {
375        self.compute_entries.iter().find(|e| e.name == name)
376    }
377
378    /// Returns the cached `wgpu::BindGroupLayout` for the given
379    /// `@group(N)` index, building and caching it on first access.
380    /// Returns `None` if the linkage has no such bind group.
381    ///
382    /// The returned layout is an owned clone of the cached value; the
383    /// cache is not invalidated. Multiple calls are cheap after the
384    /// first.
385    pub fn bind_group_layout(
386        &mut self,
387        group: u32,
388        device: &wgpu::Device,
389    ) -> Option<wgpu::BindGroupLayout> {
390        if let Some(layout) = self.bind_group_layouts.get(&group) {
391            return Some(layout.clone());
392        }
393        let info = self.bind_groups.get(&group)?;
394        let layout = info.layout(device);
395        self.bind_group_layouts.insert(group, layout.clone());
396        Some(layout)
397    }
398
399    /// Creates a bind group for `@group(N)` by matching each binding's
400    /// declared name to a resource. The order of `resources` does not
401    /// have to match the binding declaration order; the produced
402    /// `BindGroupEntry` list is sorted by binding number, matching the
403    /// cached layout.
404    ///
405    /// Looks up (and builds+caches on first access) the bind group
406    /// layout for `group`, then delegates to
407    /// [`BindGroupInfo::create_named`].
408    ///
409    /// # Errors
410    ///
411    /// - [`Error::NoSuchBindGroup`] if no bind group with the given index is
412    ///   declared in this module.
413    /// - [`Error::DuplicateBindingName`] if a name appears twice in
414    ///   `resources`.
415    /// - [`Error::MissingBinding`] if a declared binding has no resource in
416    ///   `resources`.
417    /// - [`Error::UnknownBindingName`] if `resources` contains a name that
418    ///   isn't declared in this bind group.
419    pub fn create_bind_group_named<'a>(
420        &mut self,
421        group: u32,
422        device: &wgpu::Device,
423        resources: &[(&str, wgpu::BindingResource<'a>)],
424    ) -> Result<wgpu::BindGroup, Error> {
425        // Build (or reuse cached) the layout for this group first.
426        // The cache lookup is cheap and avoids a redundant layout
427        // rebuild if the caller has already populated it.
428        if !self.bind_group_layouts.contains_key(&group) {
429            let Some(info) = self.bind_groups.get(&group) else {
430                let mut available: Vec<u32> = self.bind_groups.keys().copied().collect();
431                available.sort_unstable();
432                return NoSuchBindGroupSnafu {
433                    module: self.module_label,
434                    group,
435                    available,
436                }
437                .fail();
438            };
439            self.bind_group_layouts.insert(group, info.layout(device));
440        }
441        let info = self
442            .bind_groups
443            .get(&group)
444            .expect("just inserted or found");
445        let layout = self
446            .bind_group_layouts
447            .get(&group)
448            .expect("just inserted or found");
449        info.create_named(self.module_label, device, layout, resources)
450    }
451
452    /// Returns the cached `wgpu::PipelineLayout` for this module,
453    /// building and caching it (and all bind group layouts it
454    /// references) on first access. Subsequent calls return a cheap
455    /// Arc-backed clone.
456    ///
457    /// For modules with no bind groups (e.g. depth-only pipelines), the
458    /// cached layout references an empty `bind_group_layouts` slice.
459    ///
460    /// **Infallible.** The bind groups are already analyzed; this only
461    /// drives `wgpu::Device` constructors.
462    pub fn pipeline_layout(
463        &mut self,
464        device: &wgpu::Device,
465        label: Option<&str>,
466    ) -> wgpu::PipelineLayout {
467        if let Some(layout) = &self.pipeline_layout {
468            return layout.clone();
469        }
470        // Build (and cache) every bind group layout we don't already have.
471        for (&group, info) in &self.bind_groups {
472            self.bind_group_layouts
473                .entry(group)
474                .or_insert_with(|| info.layout(device));
475        }
476        // `wgpu::PipelineLayoutDescriptor::bind_group_layouts` is indexed
477        // by `@group(N)`: position N must hold group N's layout. Sort the
478        // cached layouts by group index so the slice lines up. Gaps (e.g.
479        // a module declaring only group 0 and group 2) are filled with
480        // `None`, which wgpu accepts as "no bind group at this index".
481        let max_group = self.bind_group_layouts.keys().copied().max().unwrap_or(0);
482        let mut bg_layouts: Vec<Option<&wgpu::BindGroupLayout>> = (0..=max_group)
483            .map(|g| self.bind_group_layouts.get(&g))
484            .collect();
485        // Trim trailing `None`s so we don't pass unnecessary slots.
486        while bg_layouts.last() == Some(&None) {
487            bg_layouts.pop();
488        }
489        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
490            label,
491            bind_group_layouts: &bg_layouts,
492            immediate_size: 0,
493        });
494        self.pipeline_layout = Some(layout.clone());
495        layout
496    }
497}
498
499// ===== ir::Module extension trait =====
500
501/// Extension trait that adds `wgsl-rs` operations to [`wgsl_rs_ir::Module`].
502///
503/// [`wgsl_rs_ir::Module`] lives in a separate crate and so cannot have
504/// inherent methods that reference `wgsl-rs` types. The methods that
505/// need `WgpuLinkage` or naga validation live here instead. The trait
506/// is re-exported from `wgsl_rs::*` so `m.generate_linkage()` works
507/// after `use wgsl_rs;` without an explicit import.
508pub trait IrModuleExt {
509    /// Builds a [`WgpuLinkage`] from this concrete IR module.
510    ///
511    /// Equivalent to [`analyze_ir_module`](self).
512    fn generate_linkage(&self) -> WgpuLinkage;
513
514    /// Validates the WGSL source rendered from this IR module using naga.
515    ///
516    /// Equivalent to the free function [`crate::validate_wgsl_source`]
517    /// called on the output of [`wgsl_rs_ir::Module::wgsl_source`].
518    ///
519    /// Only available with the `validation` feature enabled.
520    #[cfg(feature = "validation")]
521    fn validate(&self) -> Result<(), String>;
522}
523
524impl IrModuleExt for wgsl_rs_ir::Module {
525    fn generate_linkage(&self) -> WgpuLinkage {
526        analyze_ir_module(self.clone())
527    }
528
529    #[cfg(feature = "validation")]
530    fn validate(&self) -> Result<(), String> {
531        crate::validate_wgsl_source(&self.wgsl_source())
532    }
533}
534
535// ===== Bind groups =====
536
537/// A single bind group, including its layout entries and metadata for
538/// resource construction.
539#[derive(Clone, Debug)]
540pub struct BindGroupInfo {
541    /// The `@group(N)` index.
542    pub group: u32,
543    /// The bind group layout entries, sorted by binding number.
544    pub entries: Vec<wgpu::BindGroupLayoutEntry>,
545    /// Per-binding metadata: the declared name, the binding number, and
546    /// the kind of resource the binding expects.
547    pub bindings: Vec<BindingMeta>,
548    /// A default label for descriptors (`"<module>::bind_group_<N>"`).
549    pub label: String,
550}
551
552/// Metadata for a single binding inside a bind group.
553#[derive(Clone, Debug)]
554pub struct BindingMeta {
555    /// The declared Rust name (e.g. `"FRAME"`, `"DIFFUSE_TEX"`).
556    pub name: String,
557    /// The `@binding(N)` index.
558    pub binding: u32,
559    /// What kind of resource the binding expects.
560    pub kind: BindingKind,
561}
562
563/// The kind of resource a binding expects.
564#[derive(Clone, Copy, Debug, PartialEq, Eq)]
565pub enum BindingKind {
566    /// A uniform buffer.
567    Uniform,
568    /// A storage buffer. `read_only` mirrors the WGSL access mode.
569    Storage { read_only: bool },
570    /// A (filtering) sampler.
571    Sampler { comparison: bool },
572    /// A sampled texture.
573    Texture,
574    /// A depth texture.
575    DepthTexture,
576    /// A storage texture. The `read_write` flag distinguishes
577    /// `read_write` access from `read`-only or `write`-only.
578    StorageTexture { read_write: bool },
579}
580
581impl BindGroupInfo {
582    /// Builds a `wgpu::BindGroupLayoutDescriptor` borrowing this struct.
583    /// The returned descriptor borrows from `self`, so `self` must
584    /// outlive the descriptor and any wgpu object built from it.
585    pub fn layout_descriptor<'a>(&'a self) -> wgpu::BindGroupLayoutDescriptor<'a> {
586        wgpu::BindGroupLayoutDescriptor {
587            label: Some(&self.label),
588            entries: &self.entries,
589        }
590    }
591
592    /// Creates the bind group layout on the given device.
593    pub fn layout(&self, device: &wgpu::Device) -> wgpu::BindGroupLayout {
594        device.create_bind_group_layout(&self.layout_descriptor())
595    }
596
597    /// Creates a bind group with one entry per binding, in `self.entries`
598    /// order — which is sorted by binding number (see
599    /// [`BindGroupInfo::entries`]). The caller must pass exactly one
600    /// `BindingResource` per entry, in that same (binding-number) order.
601    pub fn create<'a>(
602        &self,
603        device: &wgpu::Device,
604        layout: &wgpu::BindGroupLayout,
605        resources: &[wgpu::BindingResource<'a>],
606    ) -> wgpu::BindGroup {
607        assert_eq!(
608            resources.len(),
609            self.entries.len(),
610            "{}: expected {} binding resource(s), got {}",
611            self.label,
612            self.entries.len(),
613            resources.len(),
614        );
615        let entries: Vec<wgpu::BindGroupEntry> = self
616            .entries
617            .iter()
618            .zip(resources.iter())
619            .map(|(layout_entry, resource)| wgpu::BindGroupEntry {
620                binding: layout_entry.binding,
621                resource: resource.clone(),
622            })
623            .collect();
624        device.create_bind_group(&wgpu::BindGroupDescriptor {
625            label: Some(&self.label),
626            layout,
627            entries: &entries,
628        })
629    }
630
631    /// Creates a bind group by matching each binding's declared name
632    /// to a resource. The order of `resources` does not have to match
633    /// the binding declaration order; the produced `BindGroupEntry`
634    /// list is sorted by binding number, matching the layout.
635    ///
636    /// `module_label` is the linkage's `module_label`, used to build
637    /// helpful error messages. It is not borrowed into the returned
638    /// bind group.
639    ///
640    /// # Errors
641    ///
642    /// - [`Error::DuplicateBindingName`] if a name appears twice in
643    ///   `resources`.
644    /// - [`Error::MissingBinding`] if a declared name has no resource in
645    ///   `resources`.
646    /// - [`Error::UnknownBindingName`] if `resources` contains a name that
647    ///   isn't declared in this bind group.
648    pub fn create_named<'a>(
649        &self,
650        module_label: &'static str,
651        device: &wgpu::Device,
652        layout: &wgpu::BindGroupLayout,
653        resources: &[(&str, wgpu::BindingResource<'a>)],
654    ) -> Result<wgpu::BindGroup, Error> {
655        use std::collections::HashMap;
656
657        // Build a name → resource map. Detect duplicates as we go.
658        let mut by_name: HashMap<&str, wgpu::BindingResource<'a>> =
659            HashMap::with_capacity(resources.len());
660        for &(name, ref resource) in resources {
661            if by_name.insert(name, resource.clone()).is_some() {
662                return DuplicateBindingNameSnafu {
663                    module: module_label,
664                    group: self.group,
665                    name: name.to_string(),
666                }
667                .fail();
668            }
669        }
670
671        // Every declared binding must be present.
672        for binding in &self.bindings {
673            if !by_name.contains_key(binding.name.as_str()) {
674                return MissingBindingSnafu {
675                    module: module_label,
676                    group: self.group,
677                    name: binding.name.clone(),
678                }
679                .fail();
680            }
681        }
682
683        // Every supplied name must be declared. We check this by
684        // building the entries from the declared list (not from the
685        // supplied slice) and tracking which supplied names we hit.
686        let mut hit: HashMap<&str, ()> = HashMap::with_capacity(by_name.len());
687        let mut entries: Vec<wgpu::BindGroupEntry> = Vec::with_capacity(self.bindings.len());
688        for (layout_entry, binding) in self.entries.iter().zip(self.bindings.iter()) {
689            let resource = by_name
690                .get(binding.name.as_str())
691                .expect("checked above")
692                .clone();
693            hit.insert(binding.name.as_str(), ());
694            entries.push(wgpu::BindGroupEntry {
695                binding: layout_entry.binding,
696                resource,
697            });
698        }
699        for name in by_name.keys() {
700            if !hit.contains_key(name) {
701                let declared: Vec<String> = self.bindings.iter().map(|b| b.name.clone()).collect();
702                return UnknownBindingNameSnafu {
703                    module: module_label,
704                    group: self.group,
705                    name: (*name).to_string(),
706                    declared,
707                }
708                .fail();
709            }
710        }
711
712        Ok(device.create_bind_group(&wgpu::BindGroupDescriptor {
713            label: Some(&self.label),
714            layout,
715            entries: &entries,
716        }))
717    }
718}
719
720// ===== Entry points =====
721
722/// A vertex or fragment shader entry point.
723#[derive(Clone, Debug)]
724pub struct EntryPointInfo {
725    /// The entry point name. For non-monomorphized functions this is a
726    /// `Cow::Borrowed` of a `stringify!`-emitted `'static` literal;
727    /// for monomorphized instances (e.g. `id` → `id_f32`) it is a
728    /// `Cow::Owned` holding the runtime-computed name.
729    pub name: Cow<'static, str>,
730}
731
732impl EntryPointInfo {
733    /// Builds a `wgpu::VertexState` borrowing this entry's name.
734    pub fn vertex_state<'a>(&'a self, module: &'a wgpu::ShaderModule) -> wgpu::VertexState<'a> {
735        wgpu::VertexState {
736            module,
737            entry_point: Some(&self.name),
738            buffers: &[],
739            compilation_options: Default::default(),
740        }
741    }
742
743    /// Builds a `wgpu::FragmentState` borrowing this entry's name.
744    pub fn fragment_state<'a>(
745        &'a self,
746        module: &'a wgpu::ShaderModule,
747        targets: &'a [Option<wgpu::ColorTargetState>],
748    ) -> wgpu::FragmentState<'a> {
749        wgpu::FragmentState {
750            module,
751            entry_point: Some(&self.name),
752            targets,
753            compilation_options: Default::default(),
754        }
755    }
756}
757
758/// A compute shader entry point.
759#[derive(Clone, Debug)]
760pub struct ComputeEntryInfo {
761    /// The entry point name (see [`EntryPointInfo::name`] for the
762    /// `Cow<'static, str>` rationale).
763    pub name: Cow<'static, str>,
764    /// The `@workgroup_size(X, Y, Z)` dimensions.
765    pub workgroup_size: (u32, u32, u32),
766    /// A default label for descriptors (`"<module>::<name>"`).
767    pub label: String,
768}
769
770// `ComputeEntryInfo` currently exposes only public fields. Helper
771// methods for building compute pipelines live in caller code (the
772// `wgpu::ComputePipelineDescriptor` shape is simple enough to assemble
773// at the call site, and the previous `compute_pipeline_descriptor` /
774// `compute_pipeline` helpers had zero in-tree call sites).
775
776// ===== Buffer descriptors =====
777
778/// A pre-built `wgpu::BufferDescriptor` for a `uniform!` or `storage!`
779/// declaration. Sizing follows WGSL §14.4.1 ("Alignment and Size").
780///
781/// `size` is `0` for runtime-sized storage buffers, which signals the
782/// caller to choose a buffer size appropriate for the workload (the GPU
783/// side stores a runtime array whose length is determined by the buffer
784/// size).
785#[derive(Clone, Debug)]
786pub struct BufferDescriptorInfo {
787    /// The declared Rust name of the linkage variable.
788    pub binding_name: String,
789    /// The `@group(N)` index.
790    pub group: u32,
791    /// The `@binding(N)` index.
792    pub binding: u32,
793    /// What kind of buffer this is.
794    pub kind: BufferKind,
795    /// The byte size, computed per WGSL §14.4.1. `0` for runtime arrays.
796    pub size: u64,
797    /// The pre-built descriptor's usage flags, ready to plug into a
798    /// `wgpu::BufferDescriptor` along with the borrow of
799    /// [`Self::binding_name`].
800    pub usage: wgpu::BufferUsages,
801    /// The buffer's IR type.
802    ///
803    /// Here because it's useful for users to see the type from the runtime/CPU
804    /// side.
805    pub ty: crate::ir::Type,
806}
807
808impl BufferDescriptorInfo {
809    /// Builds a `wgpu::BufferDescriptor` borrowing from
810    /// [`Self::binding_name`]. The caller must keep this
811    /// `BufferDescriptorInfo` alive for the duration of any wgpu call
812    /// using the returned descriptor.
813    pub fn descriptor(&self) -> wgpu::BufferDescriptor<'_> {
814        wgpu::BufferDescriptor {
815            label: Some(&self.binding_name),
816            size: self.size,
817            usage: self.usage,
818            mapped_at_creation: false,
819        }
820    }
821
822    /// Creates an empty buffer on the given device. The caller is
823    /// responsible for populating the buffer (typically via
824    /// `queue.write_buffer`).
825    pub fn create_buffer(&self, device: &wgpu::Device) -> wgpu::Buffer {
826        device.create_buffer(&self.descriptor())
827    }
828}
829
830/// What kind of GPU buffer a [`BufferDescriptorInfo`] describes.
831#[derive(Clone, Copy, Debug, PartialEq, Eq)]
832pub enum BufferKind {
833    /// A uniform buffer (`var<uniform>`).
834    Uniform,
835    /// A storage buffer (`var<storage, read[_write]>`).
836    Storage { read_only: bool },
837}
838
839/// The wgpu shader stage of an entry-point fn, or `None` for helper fns.
840fn stage_of(fn_attrs: &ir::FnAttrs) -> Option<wgpu::ShaderStages> {
841    match fn_attrs {
842        ir::FnAttrs::Vertex => Some(wgpu::ShaderStages::VERTEX),
843        ir::FnAttrs::Fragment => Some(wgpu::ShaderStages::FRAGMENT),
844        ir::FnAttrs::Compute { .. } => Some(wgpu::ShaderStages::COMPUTE),
845        ir::FnAttrs::None => None,
846    }
847}
848
849/// Value identifiers and call edges collected from a fn body, kept
850/// separate so a local (or parameter) that shadows a helper-fn name is
851/// not mistaken for a call (PR #190 review finding).
852#[derive(Default)]
853struct FnRefs {
854    /// `Expr::Ident` references: bindings, locals, params.
855    idents: std::collections::HashSet<String>,
856    /// `Expr::FnCall` callee paths; method calls are mangled
857    /// (`Type_method`). Only these are traversed as call-graph edges.
858    callees: std::collections::HashSet<String>,
859}
860
861/// Records a function's call-graph entry: its (possibly mangled) name
862/// and the value identifiers and call edges its body references (see
863/// [`collect_refs_in_block`]).
864fn record_fn_idents(
865    f: &ir::ItemFn,
866    name: String,
867    fn_names: &mut std::collections::HashSet<String>,
868    fn_refs: &mut std::collections::HashMap<String, FnRefs>,
869) {
870    let mut refs = FnRefs::default();
871    collect_refs_in_block(&mut refs, &f.block);
872    fn_names.insert(name.clone());
873    fn_refs.insert(name, refs);
874}
875
876/// Analyzes an IR module and returns its wgpu linkage.
877///
878/// This is the core entry point. It expects a concrete IR module (no
879/// `Type::TypeParam` nodes); for template modules, call the
880/// macro-emitted `instantiate::<...>()` function first and then pass the
881/// result here.
882///
883/// The passed IR is consumed and stored on the returned linkage so that
884/// [`WgpuLinkage::wgsl_source`] and [`WgpuLinkage::shader_module`] can
885/// render the same source the linkage was analyzed from — even when
886/// the linkage was built from an instantiated template.
887///
888/// **Infallible.** The input must be concrete (no `Type::TypeParam`
889/// nodes); the proc-macro's `instantiate` enforces this at the type
890/// level. The infallibility is honest: every failure mode would be a
891/// bug in IR construction, not a user-facing error.
892pub fn analyze_ir_module(mut ir_module: wgsl_rs_ir::Module) -> WgpuLinkage {
893    let module_label = ir_module.name;
894    ir::deshadow_module(&mut ir_module);
895    ir::suffix_module(&mut ir_module);
896    let mut linkage = WgpuLinkage {
897        module_label,
898        ir: ir_module,
899        bind_groups: BTreeMap::new(),
900        bind_group_layouts: HashMap::new(),
901        pipeline_layout: None,
902        vertex_entries: Vec::new(),
903        fragment_entries: Vec::new(),
904        compute_entries: Vec::new(),
905        buffers: Vec::new(),
906    };
907
908    // First pass: collect every binding declaration, and record the
909    // identifiers used by every function — entry points and helpers
910    // alike. Per-binding `ShaderStages` visibility is then computed
911    // transitively in a second pass by walking each entry point's call
912    // graph, so bindings referenced only inside helper functions
913    // called from an entry point still carry that entry point's stage
914    // (wgsl-rs#177).
915    let mut binding_names: std::collections::HashSet<String> = std::collections::HashSet::new();
916    // Every function name in the module, including impl methods under
917    // their mangled render names (`Type_method`).
918    let mut fn_names: std::collections::HashSet<String> = std::collections::HashSet::new();
919    // Per-fn reference sets: value identifiers and call edges, kept
920    // separate (see [`FnRefs`]) so a local shadowing a helper-fn name
921    // is not mistaken for a call.
922    let mut fn_refs: std::collections::HashMap<String, FnRefs> = std::collections::HashMap::new();
923    let mut entry_points: Vec<(String, wgpu::ShaderStages)> = Vec::new();
924
925    for item in &linkage.ir.items {
926        match item {
927            ir::Item::Uniform(u) => {
928                binding_names.insert(u.name.clone());
929            }
930            ir::Item::Storage(s) => {
931                binding_names.insert(s.name.clone());
932            }
933            ir::Item::Sampler(s) => {
934                binding_names.insert(s.name.clone());
935            }
936            ir::Item::Texture(t) => {
937                binding_names.insert(t.name.clone());
938            }
939            ir::Item::Fn(f) => {
940                record_fn_idents(f, f.name.to_string(), &mut fn_names, &mut fn_refs);
941                if let Some(stage) = stage_of(&f.fn_attrs) {
942                    entry_points.push((f.name.to_string(), stage));
943                }
944            }
945            ir::Item::Impl(imp) => {
946                for impl_item in &imp.items {
947                    let ir::ImplItem::Fn(f) = impl_item else {
948                        continue;
949                    };
950                    // Impl methods are invoked via their mangled render
951                    // names (`Type_method`), so key the call graph by the
952                    // same mangled name.
953                    let mangled = ir::mangle::mangle(&[&imp.self_ty, &f.name]);
954                    record_fn_idents(f, mangled.clone(), &mut fn_names, &mut fn_refs);
955                    if let Some(stage) = stage_of(&f.fn_attrs) {
956                        entry_points.push((mangled, stage));
957                    }
958                }
959            }
960            _ => {}
961        }
962    }
963
964    // Second pass: resolve per-binding stage visibility transitively.
965    // Starting from each entry point, walk the module call graph and
966    // union every reachable function's referenced bindings into the
967    // entry point's stage. The visited set guards against (spec-
968    // illegal) recursive call cycles.
969    let mut referenced: std::collections::HashMap<String, wgpu::ShaderStages> =
970        std::collections::HashMap::new();
971    for (entry_name, stage) in &entry_points {
972        let mut visited: std::collections::HashSet<&String> = std::collections::HashSet::new();
973        let mut stack: Vec<&String> = vec![entry_name];
974        while let Some(fname) = stack.pop() {
975            if !visited.insert(fname) {
976                continue;
977            }
978            let Some(refs) = fn_refs.get(fname) else {
979                continue;
980            };
981            // Bindings are attributed from value identifiers only; call
982            // edges come from `callees` only, so a same-named local can
983            // never masquerade as a call.
984            for name in &refs.idents {
985                if binding_names.contains(name) {
986                    referenced
987                        .entry(name.clone())
988                        .and_modify(|s| *s |= *stage)
989                        .or_insert(*stage);
990                }
991            }
992            for callee in &refs.callees {
993                if fn_names.contains(callee) {
994                    stack.push(callee);
995                }
996            }
997        }
998    }
999
1000    for item in &linkage.ir.items {
1001        match item {
1002            ir::Item::Uniform(u) => {
1003                let size = type_byte_size_or_zero(&u.ty, &linkage.ir);
1004                let entry = wgpu::BindGroupLayoutEntry {
1005                    binding: u.binding,
1006                    visibility: visibility_for(&referenced, &u.name),
1007                    ty: wgpu::BindingType::Buffer {
1008                        ty: wgpu::BufferBindingType::Uniform,
1009                        has_dynamic_offset: false,
1010                        min_binding_size: None,
1011                    },
1012                    count: None,
1013                };
1014                linkage
1015                    .bind_groups
1016                    .entry(u.group)
1017                    .or_insert_with(|| BindGroupInfo {
1018                        group: u.group,
1019                        entries: Vec::new(),
1020                        bindings: Vec::new(),
1021                        label: format!("{}::bind_group_{}", linkage.module_label, u.group),
1022                    })
1023                    .entries
1024                    .push(entry);
1025                linkage
1026                    .bind_groups
1027                    .get_mut(&u.group)
1028                    .unwrap()
1029                    .bindings
1030                    .push(BindingMeta {
1031                        name: u.name.clone(),
1032                        binding: u.binding,
1033                        kind: BindingKind::Uniform,
1034                    });
1035                linkage.buffers.push(BufferDescriptorInfo {
1036                    binding_name: u.name.clone(),
1037                    group: u.group,
1038                    binding: u.binding,
1039                    kind: BufferKind::Uniform,
1040                    size,
1041                    usage: wgpu::BufferUsages::UNIFORM
1042                        | wgpu::BufferUsages::COPY_DST
1043                        | wgpu::BufferUsages::COPY_SRC,
1044                    ty: u.ty.clone(),
1045                });
1046            }
1047            ir::Item::Storage(s) => {
1048                let read_only = matches!(s.access, ir::StorageAccess::Read);
1049                let size = type_byte_size_or_zero(&s.ty, &linkage.ir);
1050                let entry = wgpu::BindGroupLayoutEntry {
1051                    binding: s.binding,
1052                    visibility: visibility_for(&referenced, &s.name),
1053                    ty: wgpu::BindingType::Buffer {
1054                        ty: wgpu::BufferBindingType::Storage { read_only },
1055                        has_dynamic_offset: false,
1056                        min_binding_size: None,
1057                    },
1058                    count: None,
1059                };
1060                linkage
1061                    .bind_groups
1062                    .entry(s.group)
1063                    .or_insert_with(|| BindGroupInfo {
1064                        group: s.group,
1065                        entries: Vec::new(),
1066                        bindings: Vec::new(),
1067                        label: format!("{}::bind_group_{}", linkage.module_label, s.group),
1068                    })
1069                    .entries
1070                    .push(entry);
1071                linkage
1072                    .bind_groups
1073                    .get_mut(&s.group)
1074                    .unwrap()
1075                    .bindings
1076                    .push(BindingMeta {
1077                        name: s.name.clone(),
1078                        binding: s.binding,
1079                        kind: BindingKind::Storage { read_only },
1080                    });
1081                linkage.buffers.push(BufferDescriptorInfo {
1082                    binding_name: s.name.clone(),
1083                    group: s.group,
1084                    binding: s.binding,
1085                    kind: BufferKind::Storage { read_only },
1086                    size,
1087                    usage: wgpu::BufferUsages::STORAGE
1088                        | wgpu::BufferUsages::COPY_DST
1089                        | wgpu::BufferUsages::COPY_SRC,
1090                    ty: s.ty.clone(),
1091                });
1092            }
1093            ir::Item::Sampler(s) => {
1094                let comparison = matches!(s.ty, ir::Type::SamplerComparison);
1095                let sampler_ty = if comparison {
1096                    wgpu::SamplerBindingType::Comparison
1097                } else if !s.filterable {
1098                    wgpu::SamplerBindingType::NonFiltering
1099                } else {
1100                    wgpu::SamplerBindingType::Filtering
1101                };
1102                let entry = wgpu::BindGroupLayoutEntry {
1103                    binding: s.binding,
1104                    visibility: visibility_for(&referenced, &s.name),
1105                    ty: wgpu::BindingType::Sampler(sampler_ty),
1106                    count: None,
1107                };
1108                linkage
1109                    .bind_groups
1110                    .entry(s.group)
1111                    .or_insert_with(|| BindGroupInfo {
1112                        group: s.group,
1113                        entries: Vec::new(),
1114                        bindings: Vec::new(),
1115                        label: format!("{}::bind_group_{}", linkage.module_label, s.group),
1116                    })
1117                    .entries
1118                    .push(entry);
1119                linkage
1120                    .bind_groups
1121                    .get_mut(&s.group)
1122                    .unwrap()
1123                    .bindings
1124                    .push(BindingMeta {
1125                        name: s.name.clone(),
1126                        binding: s.binding,
1127                        kind: BindingKind::Sampler { comparison },
1128                    });
1129            }
1130            ir::Item::Texture(t) => {
1131                let (mut entry, kind) = texture_layout_entry(t);
1132                entry.visibility = visibility_for(&referenced, &t.name);
1133                linkage
1134                    .bind_groups
1135                    .entry(t.group)
1136                    .or_insert_with(|| BindGroupInfo {
1137                        group: t.group,
1138                        entries: Vec::new(),
1139                        bindings: Vec::new(),
1140                        label: format!("{}::bind_group_{}", linkage.module_label, t.group),
1141                    })
1142                    .entries
1143                    .push(entry);
1144                linkage
1145                    .bind_groups
1146                    .get_mut(&t.group)
1147                    .unwrap()
1148                    .bindings
1149                    .push(BindingMeta {
1150                        name: t.name.clone(),
1151                        binding: t.binding,
1152                        kind,
1153                    });
1154            }
1155            ir::Item::Fn(f) => match &f.fn_attrs {
1156                ir::FnAttrs::Vertex => {
1157                    linkage.vertex_entries.push(EntryPointInfo {
1158                        name: f.name.clone(),
1159                    });
1160                }
1161                ir::FnAttrs::Fragment => {
1162                    linkage.fragment_entries.push(EntryPointInfo {
1163                        name: f.name.clone(),
1164                    });
1165                }
1166                ir::FnAttrs::Compute { workgroup_size } => {
1167                    let x = workgroup_size.x;
1168                    let y = workgroup_size.y.unwrap_or(1);
1169                    let z = workgroup_size.z.unwrap_or(1);
1170                    linkage.compute_entries.push(ComputeEntryInfo {
1171                        name: f.name.clone(),
1172                        workgroup_size: (x, y, z),
1173                        label: format!("{}::{}", linkage.module_label, f.name),
1174                    });
1175                }
1176                ir::FnAttrs::None => {}
1177            },
1178            // Structs, impls, enums, consts, workgroup vars don't directly
1179            // produce wgpu objects.
1180            ir::Item::Struct(_)
1181            | ir::Item::Impl(_)
1182            | ir::Item::Enum(_)
1183            | ir::Item::Const(_)
1184            | ir::Item::Workgroup(_) => {}
1185        }
1186    }
1187
1188    // Sort each bind group's entries by binding number for determinism
1189    // (matches the compile-time codegen behavior).
1190    for bg in linkage.bind_groups.values_mut() {
1191        let mut paired: Vec<_> = bg.entries.drain(..).zip(bg.bindings.drain(..)).collect();
1192        paired.sort_by_key(|(e, _m)| e.binding);
1193        bg.entries = paired.iter().map(|(e, _)| *e).collect();
1194        bg.bindings = paired.into_iter().map(|(_, m)| m).collect();
1195    }
1196
1197    linkage
1198}
1199
1200/// Returns the `ShaderStages` visibility for a binding given the
1201/// per-binding reference map. If no entry-point function references
1202/// the binding (e.g. the binding is only declared but unused, or the
1203/// reference walker missed it), we default to `COMPUTE` — the most
1204/// common stage for the wgsl-rs roundtrip-test use case. The previous
1205/// `ShaderStages::all()` default would silently demand the
1206/// `VERTEX_WRITABLE_STORAGE` feature for any read_write storage, which
1207/// is almost never what the user wants.
1208fn visibility_for(
1209    referenced: &std::collections::HashMap<String, wgpu::ShaderStages>,
1210    name: &str,
1211) -> wgpu::ShaderStages {
1212    referenced
1213        .get(name)
1214        .copied()
1215        .unwrap_or(wgpu::ShaderStages::COMPUTE)
1216}
1217
1218/// Walks an `ir::Block`, collecting value identifiers and call edges
1219/// into `refs` (see [`FnRefs`]).
1220fn collect_refs_in_block(refs: &mut FnRefs, block: &ir::Block) {
1221    for stmt in &block.stmts {
1222        collect_refs_in_stmt(refs, stmt);
1223    }
1224}
1225
1226fn collect_refs_in_stmt(refs: &mut FnRefs, stmt: &ir::Stmt) {
1227    match stmt {
1228        ir::Stmt::Local(l) => {
1229            if let Some(init) = &l.init {
1230                collect_refs_in_expr(refs, init);
1231            }
1232        }
1233        ir::Stmt::Const(c) => {
1234            collect_refs_in_expr(refs, &c.expr);
1235        }
1236        ir::Stmt::Assignment { lhs, rhs } => {
1237            collect_refs_in_expr(refs, lhs);
1238            collect_refs_in_expr(refs, rhs);
1239        }
1240        ir::Stmt::CompoundAssignment { lhs, rhs, .. } => {
1241            collect_refs_in_expr(refs, lhs);
1242            collect_refs_in_expr(refs, rhs);
1243        }
1244        ir::Stmt::While { condition, body } => {
1245            collect_refs_in_expr(refs, condition);
1246            collect_refs_in_block(refs, body);
1247        }
1248        ir::Stmt::Loop { body } => {
1249            collect_refs_in_block(refs, body);
1250        }
1251        ir::Stmt::Expr { expr, .. } => {
1252            collect_refs_in_expr(refs, expr);
1253        }
1254        ir::Stmt::If(s) => {
1255            collect_refs_in_expr(refs, &s.condition);
1256            collect_refs_in_block(refs, &s.then_block);
1257            if let Some(else_branch) = &s.else_branch {
1258                match else_branch {
1259                    ir::ElseBranch::Block(b) => collect_refs_in_block(refs, b),
1260                    ir::ElseBranch::If(i) => {
1261                        collect_refs_in_expr(refs, &i.condition);
1262                        collect_refs_in_block(refs, &i.then_block);
1263                        if let Some(else_branch) = &i.else_branch {
1264                            match else_branch {
1265                                ir::ElseBranch::Block(b) => collect_refs_in_block(refs, b),
1266                                ir::ElseBranch::If(_) => {
1267                                    // Shouldn't recurse infinitely; the IR is
1268                                    // finite.
1269                                }
1270                            }
1271                        }
1272                    }
1273                }
1274            }
1275        }
1276        ir::Stmt::Return(Some(e)) => collect_refs_in_expr(refs, e),
1277        ir::Stmt::For(f) => {
1278            collect_refs_in_expr(refs, &f.from);
1279            collect_refs_in_expr(refs, &f.to);
1280            collect_refs_in_block(refs, &f.body);
1281        }
1282        ir::Stmt::Switch(s) => {
1283            collect_refs_in_expr(refs, &s.selector);
1284            for arm in &s.arms {
1285                for sel in &arm.selectors {
1286                    match sel {
1287                        ir::CaseSelector::Literal(_) => {}
1288                        ir::CaseSelector::Expr(e) => collect_refs_in_expr(refs, e),
1289                        ir::CaseSelector::Default => {}
1290                    }
1291                }
1292                collect_refs_in_block(refs, &arm.body);
1293            }
1294        }
1295        ir::Stmt::Block(b) => collect_refs_in_block(refs, b),
1296        ir::Stmt::SlabCopy {
1297            src,
1298            src_offset,
1299            dest,
1300            dest_offset,
1301            size,
1302        } => {
1303            collect_refs_in_expr(refs, src);
1304            collect_refs_in_expr(refs, src_offset);
1305            collect_refs_in_expr(refs, dest);
1306            collect_refs_in_expr(refs, dest_offset);
1307            collect_refs_in_expr(refs, size);
1308        }
1309        ir::Stmt::Break
1310        | ir::Stmt::Continue
1311        | ir::Stmt::Discard
1312        | ir::Stmt::Return(None)
1313        | ir::Stmt::Macro { .. } => {}
1314    }
1315}
1316
1317fn collect_refs_in_expr(refs: &mut FnRefs, expr: &ir::Expr) {
1318    match expr {
1319        ir::Expr::Ident(name) => {
1320            refs.idents.insert(name.clone());
1321        }
1322        ir::Expr::Lit(_) => {}
1323        ir::Expr::Array { elems } => {
1324            for e in elems {
1325                collect_refs_in_expr(refs, e);
1326            }
1327        }
1328        ir::Expr::Paren(e) => collect_refs_in_expr(refs, e),
1329        ir::Expr::Binary { lhs, rhs, .. } => {
1330            collect_refs_in_expr(refs, lhs);
1331            collect_refs_in_expr(refs, rhs);
1332        }
1333        ir::Expr::Unary { expr, .. } => collect_refs_in_expr(refs, expr),
1334        ir::Expr::ArrayIndexing { lhs, index } => {
1335            collect_refs_in_expr(refs, lhs);
1336            collect_refs_in_expr(refs, index);
1337        }
1338        ir::Expr::Swizzle { lhs, params, .. } => {
1339            collect_refs_in_expr(refs, lhs);
1340            if let Some(args) = params {
1341                for a in args {
1342                    collect_refs_in_expr(refs, a);
1343                }
1344            }
1345        }
1346        ir::Expr::Cast { lhs, .. } => collect_refs_in_expr(refs, lhs),
1347        ir::Expr::FnCall { path, params, .. } => {
1348            // Record the callee so the linkage analysis can resolve
1349            // transitive binding use through helper functions
1350            // (wgsl-rs#177). Method calls are recorded under their
1351            // mangled render names, matching how impl methods are
1352            // keyed in the call graph.
1353            match path {
1354                ir::FnPath::Ident(name) => {
1355                    refs.callees.insert(name.clone());
1356                }
1357                ir::FnPath::TypeMethod { ty, method } => {
1358                    refs.callees.insert(ir::mangle::mangle(&[ty, method]));
1359                }
1360            }
1361            for p in params {
1362                collect_refs_in_expr(refs, p);
1363            }
1364        }
1365        ir::Expr::Struct { fields, .. } => {
1366            for fv in fields {
1367                collect_refs_in_expr(refs, &fv.expr);
1368            }
1369        }
1370        ir::Expr::FieldAccess { base, .. } => collect_refs_in_expr(refs, base),
1371        ir::Expr::TypePath { .. } => {}
1372        ir::Expr::Reference(e) => collect_refs_in_expr(refs, e),
1373        ir::Expr::ZeroValueArray { len, .. } => collect_refs_in_expr(refs, len),
1374    }
1375}
1376
1377/// Analyzes a `wgsl_rs::Source` by first assembling its full IR
1378/// (including imports and template instantiations), then delegating to
1379/// [`analyze_ir_module`].
1380///
1381/// Errors if the source is a template (has unresolved
1382/// `Type::TypeParam`s). For templates, call the macro-emitted
1383/// `instantiate::<...>()` function first and pass the resulting
1384/// `ir::Module` to [`analyze_ir_module`].
1385///
1386/// **Fallible.** Returns
1387/// [`SourceError::TemplateWgsl`](crate::SourceError::TemplateWgsl)-style errors
1388/// for template sources (caller must `instantiate` first) and
1389/// [`Error::TemplateResolution`] if a cross-source template instantiation
1390/// is ambiguous. Callers can `?` this on a possibly-template source, or
1391/// `unwrap` on a known concrete one.
1392pub fn analyze_wgsl_module(wgsl_source: &Source) -> Result<WgpuLinkage, Error> {
1393    if wgsl_source.is_template() {
1394        return Err(crate::SourceError::TemplateWgsl {
1395            uninstantiated_source: wgsl_source,
1396        }
1397        .into());
1398    }
1399    let ir_module = assemble_ir(wgsl_source)?;
1400    Ok(analyze_ir_module(ir_module))
1401}
1402
1403// ===== IR assembly (flattens imports) =====
1404
1405/// Build a single owned `ir::Module` from a `wgsl_rs::Source` and all
1406/// its transitive imports and template instantiations. The result is
1407/// concrete (no `Type::TypeParam`s) and self-contained: every struct
1408/// referenced by a binding is defined within the assembled source.
1409///
1410/// This is the same traversal that `Source::wgsl_source()` performs,
1411/// but it stops at IR rather than rendering. It exists so the runtime
1412/// linkage analyzer can correctly size buffers whose types are defined
1413/// in imported sources.
1414pub(crate) fn assemble_ir(wgsl_source: &Source) -> Result<ir::Module, Error> {
1415    let mut visited: std::collections::HashSet<u64> = std::collections::HashSet::new();
1416    let mut seen: std::collections::HashSet<(u64, String, Vec<String>)> =
1417        std::collections::HashSet::new();
1418    let mut items: Vec<ir::Item> = Vec::new();
1419    collect_items(wgsl_source, &mut visited, &mut seen, &mut items, None)?;
1420    // Note: deshadowing is deferred to `analyze_ir_module`, which is the
1421    // sole consumer of this assembled IR. Deshadowing here too would be
1422    // redundant (the pass is idempotent but wastes work).
1423    Ok(ir::Module {
1424        name: wgsl_source.name,
1425        items,
1426        attrs: Vec::new(),
1427    })
1428}
1429
1430fn collect_items(
1431    wgsl_source: &Source,
1432    visited: &mut std::collections::HashSet<u64>,
1433    seen: &mut std::collections::HashSet<(u64, String, Vec<String>)>,
1434    out: &mut Vec<ir::Item>,
1435    subst: Option<&std::collections::HashMap<String, ir::Type>>,
1436) -> Result<(), Error> {
1437    // Imports first (depth-first, deduplicated by source id).
1438    for m in wgsl_source.imports {
1439        if visited.insert(m.id) {
1440            collect_items(m, visited, seen, out, None)?;
1441        }
1442    }
1443
1444    // This source's own items, optionally substituted.
1445    let mut ir_module = (wgsl_source.ir_constructor)();
1446    if let Some(s) = subst {
1447        ir::substitute_types(&mut ir_module, s);
1448    }
1449    out.extend(ir_module.items);
1450
1451    // Cross-source template instantiations.
1452    for inst in wgsl_source.instantiations {
1453        let mangled: Vec<String> = inst
1454            .mangled_type_args
1455            .iter()
1456            .map(|s| (*s).to_string())
1457            .collect();
1458        let type_args = (inst.type_args_constructor)();
1459        collect_instantiation(
1460            inst.modules,
1461            inst.template_name,
1462            &mangled,
1463            &type_args,
1464            out,
1465            seen,
1466        )?;
1467    }
1468    Ok(())
1469}
1470
1471fn collect_instantiation(
1472    sources: &[&Source],
1473    template_name: &str,
1474    mangled_type_args: &[String],
1475    type_args: &[ir::Type],
1476    out: &mut Vec<ir::Item>,
1477    seen: &mut std::collections::HashSet<(u64, String, Vec<String>)>,
1478) -> Result<(), Error> {
1479    let available: Vec<String> = sources
1480        .iter()
1481        .copied()
1482        .flat_map(|m| m.templates.iter().map(|t| t.name.to_string()))
1483        .collect();
1484
1485    let mut matching: Vec<&Source> = sources
1486        .iter()
1487        .copied()
1488        .filter(|m| m.templates.iter().any(|t| t.name == template_name))
1489        .collect();
1490
1491    snafu::ensure!(
1492        !matching.is_empty(),
1493        TemplateResolutionSnafu {
1494            template_name: template_name.to_string(),
1495            mangled_type_args: mangled_type_args.to_vec(),
1496            available,
1497        }
1498    );
1499
1500    if matching.len() > 1 {
1501        let names: Vec<String> = matching.iter().map(|m| m.name.to_string()).collect();
1502        return TemplateResolutionSnafu {
1503            template_name: template_name.to_string(),
1504            mangled_type_args: mangled_type_args.to_vec(),
1505            available: names,
1506        }
1507        .fail();
1508    }
1509
1510    let source = matching
1511        .pop()
1512        .expect("matching is non-empty after the check above");
1513    let Some(template) = source.templates.iter().find(|t| t.name == template_name) else {
1514        // Invariant: `matching` was built by filtering on
1515        // `templates.iter().any(|t| t.name == template_name)`, so the
1516        // sole surviving source must contain the template.
1517        unreachable!(
1518            "resolved source '{}' has no template '{}'; available: {:?}",
1519            source.name, template_name, available
1520        );
1521    };
1522
1523    let key = (
1524        source.id,
1525        template_name.to_string(),
1526        mangled_type_args.to_vec(),
1527    );
1528    snafu::ensure!(
1529        seen.insert(key),
1530        TemplateResolutionSnafu {
1531            template_name: template_name.to_owned(),
1532            mangled_type_args: mangled_type_args.to_vec(),
1533            available
1534        }
1535    );
1536
1537    // Recurse into dependencies first.
1538    for dep in template.dependencies {
1539        let dep_mangled: Vec<String> = dep
1540            .type_param_mapping
1541            .iter()
1542            .map(|&idx| mangled_type_args[idx].clone())
1543            .collect();
1544        let dep_args: Vec<ir::Type> = dep
1545            .type_param_mapping
1546            .iter()
1547            .map(|&idx| type_args[idx].clone())
1548            .collect();
1549        collect_instantiation(&[source], dep.callee, &dep_mangled, &dep_args, out, seen)?;
1550    }
1551
1552    let mut subst: std::collections::HashMap<String, ir::Type> = std::collections::HashMap::new();
1553    for (param, arg) in template.type_params.iter().zip(type_args.iter()) {
1554        subst.insert((*param).to_string(), arg.clone());
1555    }
1556
1557    let mut items = (template.ir_constructor)();
1558    ir::substitute_items(&mut items, &subst);
1559
1560    let instance_name = if mangled_type_args.is_empty() {
1561        template.name.to_string()
1562    } else {
1563        let mut components: Vec<&str> = Vec::with_capacity(1 + mangled_type_args.len());
1564        components.push(template.name);
1565        for s in mangled_type_args {
1566            components.push(s.as_str());
1567        }
1568        ir::mangle(&components)
1569    };
1570    if instance_name != template.name {
1571        ir::rename_items(&mut items, template.name, &instance_name);
1572    }
1573    out.extend(items);
1574
1575    Ok(())
1576}
1577
1578// ===== Texture helpers =====
1579
1580fn texture_layout_entry(item: &ir::ItemTexture) -> (wgpu::BindGroupLayoutEntry, BindingKind) {
1581    let view_dimension = match &item.ty {
1582        ir::Type::Texture { kind, .. } => texture_kind_view_dimension(*kind),
1583        ir::Type::TextureDepth { kind } => texture_depth_view_dimension(*kind),
1584        ir::Type::TextureStorage { kind, .. } => texture_storage_kind_view_dimension(*kind),
1585        _ => unreachable!("ItemTexture validated to be a texture type"),
1586    };
1587    let multisampled = match &item.ty {
1588        ir::Type::Texture { kind, .. } => matches!(kind, ir::TextureKind::TextureMultisampled2D),
1589        ir::Type::TextureDepth { kind } => {
1590            matches!(kind, ir::TextureDepthKind::DepthMultisampled2D)
1591        }
1592        ir::Type::TextureStorage { .. } => false,
1593        _ => unreachable!(),
1594    };
1595    match &item.ty {
1596        ir::Type::Texture { sampled_type, .. } => {
1597            let sample = match sampled_type {
1598                ir::ScalarType::F32 => wgpu::TextureSampleType::Float {
1599                    filterable: item.filterable,
1600                },
1601                ir::ScalarType::I32 => wgpu::TextureSampleType::Sint,
1602                ir::ScalarType::U32 => wgpu::TextureSampleType::Uint,
1603                ir::ScalarType::Bool => unreachable!("textures can't be bool-sampled"),
1604            };
1605            (
1606                wgpu::BindGroupLayoutEntry {
1607                    binding: item.binding,
1608                    visibility: wgpu::ShaderStages::all(),
1609                    ty: wgpu::BindingType::Texture {
1610                        sample_type: sample,
1611                        view_dimension,
1612                        multisampled,
1613                    },
1614                    count: None,
1615                },
1616                BindingKind::Texture,
1617            )
1618        }
1619        ir::Type::TextureDepth { .. } => (
1620            wgpu::BindGroupLayoutEntry {
1621                binding: item.binding,
1622                visibility: wgpu::ShaderStages::all(),
1623                ty: wgpu::BindingType::Texture {
1624                    sample_type: wgpu::TextureSampleType::Depth,
1625                    view_dimension,
1626                    multisampled,
1627                },
1628                count: None,
1629            },
1630            BindingKind::DepthTexture,
1631        ),
1632        ir::Type::TextureStorage { format, access, .. } => {
1633            let wgpu_format = texel_format_to_wgpu(*format);
1634            let wgpu_access = match access {
1635                ir::StorageTextureAccess::Read => wgpu::StorageTextureAccess::ReadOnly,
1636                ir::StorageTextureAccess::Write => wgpu::StorageTextureAccess::WriteOnly,
1637                ir::StorageTextureAccess::ReadWrite => wgpu::StorageTextureAccess::ReadWrite,
1638            };
1639            let read_write = matches!(access, ir::StorageTextureAccess::ReadWrite);
1640            (
1641                wgpu::BindGroupLayoutEntry {
1642                    binding: item.binding,
1643                    visibility: wgpu::ShaderStages::all(),
1644                    ty: wgpu::BindingType::StorageTexture {
1645                        access: wgpu_access,
1646                        format: wgpu_format,
1647                        view_dimension,
1648                    },
1649                    count: None,
1650                },
1651                BindingKind::StorageTexture { read_write },
1652            )
1653        }
1654        _ => unreachable!(),
1655    }
1656}
1657
1658fn texture_storage_kind_view_dimension(kind: ir::TextureStorageKind) -> wgpu::TextureViewDimension {
1659    match kind {
1660        ir::TextureStorageKind::Storage1D => wgpu::TextureViewDimension::D1,
1661        ir::TextureStorageKind::Storage2D => wgpu::TextureViewDimension::D2,
1662        ir::TextureStorageKind::Storage2DArray => wgpu::TextureViewDimension::D2Array,
1663        ir::TextureStorageKind::Storage3D => wgpu::TextureViewDimension::D3,
1664    }
1665}
1666
1667/// Map an IR texel format to the corresponding wgpu texture format.
1668fn texel_format_to_wgpu(f: ir::TexelFormat) -> wgpu::TextureFormat {
1669    use ir::TexelFormat::*;
1670    match f {
1671        Rgba8unorm => wgpu::TextureFormat::Rgba8Unorm,
1672        Rgba8snorm => wgpu::TextureFormat::Rgba8Snorm,
1673        Rgba8uint => wgpu::TextureFormat::Rgba8Uint,
1674        Rgba8sint => wgpu::TextureFormat::Rgba8Sint,
1675        Rgba16uint => wgpu::TextureFormat::Rgba16Uint,
1676        Rgba16sint => wgpu::TextureFormat::Rgba16Sint,
1677        Rgba16float => wgpu::TextureFormat::Rgba16Float,
1678        R32uint => wgpu::TextureFormat::R32Uint,
1679        R32sint => wgpu::TextureFormat::R32Sint,
1680        R32float => wgpu::TextureFormat::R32Float,
1681        Rg32uint => wgpu::TextureFormat::Rg32Uint,
1682        Rg32sint => wgpu::TextureFormat::Rg32Sint,
1683        Rg32float => wgpu::TextureFormat::Rg32Float,
1684        Rgba32uint => wgpu::TextureFormat::Rgba32Uint,
1685        Rgba32sint => wgpu::TextureFormat::Rgba32Sint,
1686        Rgba32float => wgpu::TextureFormat::Rgba32Float,
1687        Bgra8unorm => wgpu::TextureFormat::Bgra8Unorm,
1688        Rgba16unorm => wgpu::TextureFormat::Rgba16Unorm,
1689        Rgba16snorm => wgpu::TextureFormat::Rgba16Snorm,
1690        Rg8unorm => wgpu::TextureFormat::Rg8Unorm,
1691        Rg8snorm => wgpu::TextureFormat::Rg8Snorm,
1692        Rg8uint => wgpu::TextureFormat::Rg8Uint,
1693        Rg8sint => wgpu::TextureFormat::Rg8Sint,
1694        Rg16unorm => wgpu::TextureFormat::Rg16Unorm,
1695        Rg16snorm => wgpu::TextureFormat::Rg16Snorm,
1696        Rg16uint => wgpu::TextureFormat::Rg16Uint,
1697        Rg16sint => wgpu::TextureFormat::Rg16Sint,
1698        Rg16float => wgpu::TextureFormat::Rg16Float,
1699        R8unorm => wgpu::TextureFormat::R8Unorm,
1700        R8snorm => wgpu::TextureFormat::R8Snorm,
1701        R8uint => wgpu::TextureFormat::R8Uint,
1702        R8sint => wgpu::TextureFormat::R8Sint,
1703        R16unorm => wgpu::TextureFormat::R16Unorm,
1704        R16snorm => wgpu::TextureFormat::R16Snorm,
1705        R16uint => wgpu::TextureFormat::R16Uint,
1706        R16sint => wgpu::TextureFormat::R16Sint,
1707        R16float => wgpu::TextureFormat::R16Float,
1708        Rgb10a2unorm => wgpu::TextureFormat::Rgb10a2Unorm,
1709        Rgb10a2uint => wgpu::TextureFormat::Rgb10a2Uint,
1710        Rg11b10ufloat => wgpu::TextureFormat::Rg11b10Ufloat,
1711    }
1712}
1713
1714fn texture_kind_view_dimension(kind: ir::TextureKind) -> wgpu::TextureViewDimension {
1715    match kind {
1716        ir::TextureKind::Texture1D => wgpu::TextureViewDimension::D1,
1717        ir::TextureKind::Texture2D | ir::TextureKind::TextureMultisampled2D => {
1718            wgpu::TextureViewDimension::D2
1719        }
1720        ir::TextureKind::Texture2DArray => wgpu::TextureViewDimension::D2Array,
1721        ir::TextureKind::Texture3D => wgpu::TextureViewDimension::D3,
1722        ir::TextureKind::TextureCube => wgpu::TextureViewDimension::Cube,
1723        ir::TextureKind::TextureCubeArray => wgpu::TextureViewDimension::CubeArray,
1724    }
1725}
1726
1727fn texture_depth_view_dimension(kind: ir::TextureDepthKind) -> wgpu::TextureViewDimension {
1728    match kind {
1729        ir::TextureDepthKind::Depth2D | ir::TextureDepthKind::DepthMultisampled2D => {
1730            wgpu::TextureViewDimension::D2
1731        }
1732        ir::TextureDepthKind::Depth2DArray => wgpu::TextureViewDimension::D2Array,
1733        ir::TextureDepthKind::DepthCube => wgpu::TextureViewDimension::Cube,
1734        ir::TextureDepthKind::DepthCubeArray => wgpu::TextureViewDimension::CubeArray,
1735    }
1736}
1737
1738// ===== WGSL §14.4.1 type sizing =====
1739
1740/// Returns the byte size of a host-shareable WGSL type per §14.4.1.
1741/// `RuntimeArray<T>` returns `0` (caller chooses the buffer size based
1742/// on workload). User struct lookups require the defining module.
1743///
1744/// Returns `0` for non-shareable types (samplers, textures, pointers) and
1745/// for unresolvable cases (e.g. an array length that isn't a literal or
1746/// same-module const). In the latter case a panic with location info
1747/// would be more user-friendly, but we err on the side of "do something
1748/// reasonable" so that callers can still build pipelines.
1749pub fn type_byte_size(ty: &ir::Type, module: &ir::Module) -> u64 {
1750    let layout = type_layout(ty, module);
1751    layout.size
1752}
1753
1754fn type_byte_size_or_zero(ty: &ir::Type, module: &ir::Module) -> u64 {
1755    type_byte_size(ty, module)
1756}
1757
1758/// Layout for a single type (size + align) per WGSL §14.4.1.
1759#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1760pub struct TypeLayout {
1761    pub size: u64,
1762    pub align: u64,
1763}
1764
1765fn align_up(val: u64, align: u64) -> u64 {
1766    if align == 0 {
1767        val
1768    } else {
1769        (val + (align - 1)) & !(align - 1)
1770    }
1771}
1772
1773fn type_layout(ty: &ir::Type, module: &ir::Module) -> TypeLayout {
1774    match ty {
1775        ir::Type::Scalar(s) => scalar_layout(*s),
1776        ir::Type::Vector { elements, .. } => vector_layout(*elements),
1777        ir::Type::Matrix { columns, rows, .. } => matrix_layout(*columns, *rows),
1778        ir::Type::Array { elem, len } => {
1779            let inner = type_layout(elem, module);
1780            let n = eval_array_len(len, module).unwrap_or(0);
1781            TypeLayout {
1782                size: (n as u64) * align_up(inner.size, inner.align.max(1)),
1783                align: inner.align,
1784            }
1785        }
1786        ir::Type::RuntimeArray { elem } => {
1787            let inner = type_layout(elem, module);
1788            TypeLayout {
1789                size: 0,
1790                align: inner.align,
1791            }
1792        }
1793        ir::Type::Atomic { elem } => type_layout(elem, module),
1794        ir::Type::Struct { name, type_args } => struct_layout(name, type_args, module),
1795        ir::Type::Ptr { .. } => TypeLayout { size: 0, align: 1 },
1796        // Samplers, textures, and type parameters aren't host-shareable.
1797        // Return a zero layout so the analyzer doesn't choke.
1798        ir::Type::Sampler
1799        | ir::Type::SamplerComparison
1800        | ir::Type::Texture { .. }
1801        | ir::Type::TextureDepth { .. }
1802        | ir::Type::TextureStorage { .. }
1803        | ir::Type::TypeParam { .. }
1804        | ir::Type::Phantom { .. }
1805        | ir::Type::AssocType { .. } => TypeLayout { size: 0, align: 1 },
1806    }
1807}
1808
1809fn scalar_layout(s: ir::ScalarType) -> TypeLayout {
1810    match s {
1811        ir::ScalarType::F32 | ir::ScalarType::I32 | ir::ScalarType::U32 | ir::ScalarType::Bool => {
1812            TypeLayout { size: 4, align: 4 }
1813        }
1814    }
1815}
1816
1817fn vector_layout(elements: u8) -> TypeLayout {
1818    match elements {
1819        2 => TypeLayout { size: 8, align: 8 },
1820        3 => TypeLayout {
1821            size: 12,
1822            align: 16,
1823        },
1824        4 => TypeLayout {
1825            size: 16,
1826            align: 16,
1827        },
1828        _ => TypeLayout { size: 0, align: 1 },
1829    }
1830}
1831
1832fn matrix_layout(columns: u8, rows: u8) -> TypeLayout {
1833    // WGSL §14.4.1: matCxR<T> has:
1834    //   Align = AlignOf(vecR<T>)
1835    //   Size  = SizeOf(array<vecR<T>, C>)
1836    // Each column is stored as a vecR<T> with stride roundUp(SizeOf(vecR<T>),
1837    // AlignOf(vecR<T>)).
1838    let row_vec = vector_layout(rows);
1839    let stride = align_up(row_vec.size, row_vec.align);
1840    TypeLayout {
1841        size: (columns as u64) * stride,
1842        align: row_vec.align,
1843    }
1844}
1845
1846fn struct_layout(name: &str, type_args: &[ir::Type], module: &ir::Module) -> TypeLayout {
1847    let Some(s) = find_struct(name, type_args, module) else {
1848        return TypeLayout { size: 0, align: 1 };
1849    };
1850    let mut offset: u64 = 0;
1851    let mut struct_align: u64 = 1;
1852    for field in &s.fields {
1853        let fl = type_layout(&field.ty, module);
1854        struct_align = struct_align.max(fl.align);
1855        offset = align_up(offset, fl.align);
1856        offset += fl.size;
1857    }
1858    let size = align_up(offset, struct_align);
1859    TypeLayout {
1860        size,
1861        align: struct_align,
1862    }
1863}
1864
1865/// Look up a struct by name (and optional type args) in the module. The
1866/// struct is matched by base name; type args are currently ignored for
1867/// lookup (we look up the generic template, then resolve via the
1868/// `type_args` substitution if any). Generic structs share a base name
1869/// across instantiations, so callers that need a specific instantiation
1870/// must pre-substitute the IR before calling.
1871fn find_struct<'a>(
1872    name: &str,
1873    _type_args: &[ir::Type],
1874    module: &'a ir::Module,
1875) -> Option<&'a ir::ItemStruct> {
1876    module.items.iter().find_map(|i| match i {
1877        ir::Item::Struct(s) if s.name == name => Some(s),
1878        _ => None,
1879    })
1880}
1881
1882/// Evaluate a fixed-size array length expression. Supports integer
1883/// literals and named integer constants defined at module scope. Other
1884/// expressions (binary ops, casts, etc.) return `None` and the caller
1885/// falls back to size 0 with a clearly-too-small buffer.
1886fn eval_array_len(expr: &ir::Expr, module: &ir::Module) -> Option<usize> {
1887    match expr {
1888        ir::Expr::Lit(ir::Lit::Int { digits, .. }) => digits.parse::<usize>().ok(),
1889        // WGSL array lengths must be non-negative const expressions, so a
1890        // unary negation is unresolvable — returning `None` falls back to
1891        // size 0 rather than producing a huge (wraparound) length.
1892        ir::Expr::Unary {
1893            op: ir::UnOp::Neg, ..
1894        } => None,
1895        ir::Expr::Ident(name) => {
1896            let val = module.items.iter().find_map(|i| match i {
1897                ir::Item::Const(c) if c.name == *name => Some(eval_const_int(&c.expr, module)),
1898                _ => None,
1899            });
1900            val.flatten()
1901        }
1902        _ => None,
1903    }
1904}
1905
1906fn eval_const_int(expr: &ir::Expr, module: &ir::Module) -> Option<usize> {
1907    eval_array_len(expr, module)
1908}
1909
1910#[cfg(test)]
1911mod binding_visibility {
1912    //! Tests for per-binding `ShaderStages` visibility, including the
1913    //! transitive helper-function case from wgsl-rs#177.
1914
1915    use std::borrow::Cow;
1916
1917    use super::*;
1918
1919    fn f32_ty() -> ir::Type {
1920        ir::Type::Scalar(ir::ScalarType::F32)
1921    }
1922
1923    fn zero() -> ir::Expr {
1924        ir::Expr::Lit(ir::Lit::Float {
1925            text: "0.0".to_string(),
1926        })
1927    }
1928
1929    fn uniform(name: &str, group: u32, binding: u32) -> ir::Item {
1930        ir::Item::Uniform(ir::ItemUniform {
1931            group,
1932            binding,
1933            name: name.to_string(),
1934            ty: f32_ty(),
1935            attrs: Vec::new(),
1936        })
1937    }
1938
1939    fn call(name: &str, params: Vec<ir::Expr>) -> ir::Expr {
1940        ir::Expr::FnCall {
1941            path: ir::FnPath::Ident(name.to_string()),
1942            type_args: Vec::new(),
1943            params,
1944        }
1945    }
1946
1947    fn ident(name: &str) -> ir::Expr {
1948        ir::Expr::Ident(name.to_string())
1949    }
1950
1951    fn fn_item(name: &str, fn_attrs: ir::FnAttrs, stmts: Vec<ir::Stmt>) -> ir::Item {
1952        ir::Item::Fn(ir::ItemFn {
1953            type_params: Vec::new(),
1954            const_params: Vec::new(),
1955            fn_attrs,
1956            name: Cow::Owned(name.to_string()),
1957            inputs: Vec::new(),
1958            return_type: ir::ReturnType::Default,
1959            block: ir::Block { stmts },
1960            attrs: Vec::new(),
1961        })
1962    }
1963
1964    fn returns(expr: ir::Expr) -> ir::Stmt {
1965        ir::Stmt::Return(Some(expr))
1966    }
1967
1968    fn analyze(items: Vec<ir::Item>) -> WgpuLinkage {
1969        analyze_ir_module(ir::Module {
1970            name: "test_module",
1971            items,
1972            attrs: Vec::new(),
1973        })
1974    }
1975
1976    /// Looks up the analyzed visibility for the named binding.
1977    fn visibility_of(linkage: &WgpuLinkage, name: &str) -> wgpu::ShaderStages {
1978        for bind_group in linkage.bind_groups.values() {
1979            if let Some(meta) = bind_group.bindings.iter().find(|b| b.name == name) {
1980                let entry = bind_group
1981                    .entries
1982                    .iter()
1983                    .find(|e| e.binding == meta.binding)
1984                    .expect("binding layout entry exists");
1985                return entry.visibility;
1986            }
1987        }
1988        panic!("binding '{name}' not found in any bind group");
1989    }
1990
1991    #[test]
1992    fn direct_use_gets_entry_stage() {
1993        let linkage = analyze(vec![
1994            uniform("MY_UNIFORM", 0, 0),
1995            fn_item(
1996                "vert",
1997                ir::FnAttrs::Vertex,
1998                vec![returns(ident("MY_UNIFORM"))],
1999            ),
2000        ]);
2001        assert_eq!(
2002            visibility_of(&linkage, "MY_UNIFORM"),
2003            wgpu::ShaderStages::VERTEX
2004        );
2005    }
2006
2007    /// The wgsl-rs#177 repro: the binding is used only inside a helper
2008    /// called from both entry points, so it must carry both stages.
2009    #[test]
2010    fn helper_only_use_carries_entry_point_stages() {
2011        let linkage = analyze(vec![
2012            uniform("MY_UNIFORM", 0, 0),
2013            fn_item(
2014                "read_uniform",
2015                ir::FnAttrs::None,
2016                vec![returns(ident("MY_UNIFORM"))],
2017            ),
2018            fn_item(
2019                "vert",
2020                ir::FnAttrs::Vertex,
2021                vec![returns(call("read_uniform", vec![]))],
2022            ),
2023            fn_item(
2024                "frag",
2025                ir::FnAttrs::Fragment,
2026                vec![returns(call("read_uniform", vec![]))],
2027            ),
2028        ]);
2029        assert_eq!(
2030            visibility_of(&linkage, "MY_UNIFORM"),
2031            wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT
2032        );
2033    }
2034
2035    /// A helper called only from `frag` must not leak vertex visibility.
2036    #[test]
2037    fn frag_only_helper_stays_fragment() {
2038        let linkage = analyze(vec![
2039            uniform("MY_UNIFORM", 0, 0),
2040            fn_item(
2041                "read_uniform",
2042                ir::FnAttrs::None,
2043                vec![returns(ident("MY_UNIFORM"))],
2044            ),
2045            fn_item(
2046                "frag",
2047                ir::FnAttrs::Fragment,
2048                vec![returns(call("read_uniform", vec![]))],
2049            ),
2050            fn_item("vert", ir::FnAttrs::Vertex, vec![returns(zero())]),
2051        ]);
2052        assert_eq!(
2053            visibility_of(&linkage, "MY_UNIFORM"),
2054            wgpu::ShaderStages::FRAGMENT
2055        );
2056    }
2057
2058    /// Mixed use: direct reference in one entry point, helper-only in
2059    /// the other.
2060    #[test]
2061    fn mixed_direct_and_helper_use() {
2062        let linkage = analyze(vec![
2063            uniform("MY_UNIFORM", 0, 0),
2064            fn_item(
2065                "read_uniform",
2066                ir::FnAttrs::None,
2067                vec![returns(ident("MY_UNIFORM"))],
2068            ),
2069            fn_item(
2070                "vert",
2071                ir::FnAttrs::Vertex,
2072                vec![returns(ident("MY_UNIFORM"))],
2073            ),
2074            fn_item(
2075                "frag",
2076                ir::FnAttrs::Fragment,
2077                vec![returns(call("read_uniform", vec![]))],
2078            ),
2079        ]);
2080        assert_eq!(
2081            visibility_of(&linkage, "MY_UNIFORM"),
2082            wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT
2083        );
2084    }
2085
2086    /// Reachability must be transitive: entry point -> helper -> helper
2087    /// -> binding.
2088    #[test]
2089    fn helper_chain_is_transitive() {
2090        let linkage = analyze(vec![
2091            uniform("MY_UNIFORM", 0, 0),
2092            fn_item(
2093                "inner",
2094                ir::FnAttrs::None,
2095                vec![returns(ident("MY_UNIFORM"))],
2096            ),
2097            fn_item(
2098                "outer",
2099                ir::FnAttrs::None,
2100                vec![returns(call("inner", vec![]))],
2101            ),
2102            fn_item(
2103                "vert",
2104                ir::FnAttrs::Vertex,
2105                vec![returns(call("outer", vec![]))],
2106            ),
2107        ]);
2108        assert_eq!(
2109            visibility_of(&linkage, "MY_UNIFORM"),
2110            wgpu::ShaderStages::VERTEX
2111        );
2112    }
2113
2114    /// A declared-but-unused binding keeps the documented COMPUTE
2115    /// default.
2116    #[test]
2117    fn unused_binding_defaults_to_compute() {
2118        let linkage = analyze(vec![
2119            uniform("MY_UNIFORM", 0, 0),
2120            fn_item("vert", ir::FnAttrs::Vertex, vec![returns(zero())]),
2121        ]);
2122        assert_eq!(
2123            visibility_of(&linkage, "MY_UNIFORM"),
2124            wgpu::ShaderStages::COMPUTE
2125        );
2126    }
2127
2128    /// A local (or parameter) that shadows a helper-fn name is a value
2129    /// reference, not a call edge (PR #190 review finding): the helper
2130    /// is never called, so its bindings must not gain this entry
2131    /// point's stage.
2132    #[test]
2133    fn local_shadowing_fn_name_is_not_a_call_edge() {
2134        let linkage = analyze(vec![
2135            uniform("DATA", 0, 0),
2136            fn_item(
2137                "read_uniform",
2138                ir::FnAttrs::None,
2139                vec![returns(ident("DATA"))],
2140            ),
2141            fn_item(
2142                "vert",
2143                ir::FnAttrs::Vertex,
2144                vec![
2145                    ir::Stmt::Local(ir::Local {
2146                        mutable: false,
2147                        name: "read_uniform".to_string(),
2148                        ty: None,
2149                        init: Some(zero()),
2150                    }),
2151                    returns(ident("read_uniform")),
2152                ],
2153            ),
2154        ]);
2155        // The helper is unreachable, so DATA falls back to the COMPUTE
2156        // default rather than spuriously gaining VERTEX.
2157        assert_eq!(visibility_of(&linkage, "DATA"), wgpu::ShaderStages::COMPUTE);
2158    }
2159
2160    /// Same-shaped control: a real call from frag still creates the
2161    /// edge (FRAGMENT), while vert's same-named local grants nothing.
2162    #[test]
2163    fn call_edge_survives_local_shadowing_elsewhere() {
2164        let linkage = analyze(vec![
2165            uniform("DATA", 0, 0),
2166            fn_item(
2167                "read_uniform",
2168                ir::FnAttrs::None,
2169                vec![returns(ident("DATA"))],
2170            ),
2171            fn_item(
2172                "vert",
2173                ir::FnAttrs::Vertex,
2174                vec![
2175                    ir::Stmt::Local(ir::Local {
2176                        mutable: false,
2177                        name: "read_uniform".to_string(),
2178                        ty: None,
2179                        init: Some(zero()),
2180                    }),
2181                    returns(ident("read_uniform")),
2182                ],
2183            ),
2184            fn_item(
2185                "frag",
2186                ir::FnAttrs::Fragment,
2187                vec![returns(call("read_uniform", vec![]))],
2188            ),
2189        ]);
2190        assert_eq!(
2191            visibility_of(&linkage, "DATA"),
2192            wgpu::ShaderStages::FRAGMENT
2193        );
2194    }
2195
2196    /// Impl methods are called via mangled `Type_method` names; bindings
2197    /// used inside them must be reachable from entry points that call
2198    /// them.
2199    #[test]
2200    fn impl_method_helper_is_reachable() {
2201        let method = ir::ItemFn {
2202            type_params: Vec::new(),
2203            const_params: Vec::new(),
2204            fn_attrs: ir::FnAttrs::None,
2205            name: Cow::Owned("read_uniform".to_string()),
2206            inputs: Vec::new(),
2207            return_type: ir::ReturnType::Default,
2208            block: ir::Block {
2209                stmts: vec![returns(ident("MY_UNIFORM"))],
2210            },
2211            attrs: Vec::new(),
2212        };
2213        let linkage = analyze(vec![
2214            uniform("MY_UNIFORM", 0, 0),
2215            ir::Item::Impl(ir::ItemImpl {
2216                type_params: Vec::new(),
2217                const_params: Vec::new(),
2218                self_ty: "Foo".to_string(),
2219                items: vec![ir::ImplItem::Fn(method)],
2220                attrs: Vec::new(),
2221            }),
2222            fn_item(
2223                "vert",
2224                ir::FnAttrs::Vertex,
2225                vec![returns(ir::Expr::FnCall {
2226                    path: ir::FnPath::TypeMethod {
2227                        ty: "Foo".to_string(),
2228                        method: "read_uniform".to_string(),
2229                    },
2230                    type_args: Vec::new(),
2231                    params: Vec::new(),
2232                })],
2233            ),
2234        ]);
2235        assert_eq!(
2236            visibility_of(&linkage, "MY_UNIFORM"),
2237            wgpu::ShaderStages::VERTEX
2238        );
2239    }
2240}