Skip to main content

wesl/pass/
usage_analysis.rs

1use std::collections::{HashMap, hash_map::Entry};
2use wgsl_parse::{SyntaxNode, syntax::*};
3
4use crate::{
5    error::UsageError,
6    pass::{Imports, Visit, flatten_imports, imported_item_path},
7};
8
9#[derive(Clone)]
10pub struct Module {
11    pub syntax: TranslationUnit,
12    pub path: ModulePath,
13    pub imports: Imports,
14}
15
16impl Module {
17    pub fn new(path: ModulePath, syntax: TranslationUnit) -> Self {
18        let imports = flatten_imports(&syntax.imports, &path);
19        Self {
20            syntax,
21            path,
22            imports,
23        }
24    }
25}
26
27#[derive(Default, Clone, Debug)]
28pub struct UsedItems {
29    /// Module declarations used.
30    used_items: HashMap<ModulePath, HashMap<Ident, Visibility>>,
31}
32
33/// Just a convenience
34impl std::fmt::Display for UsedItems {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        use itertools::Itertools;
37        for (path, items) in self.iter() {
38            writeln!(
39                f,
40                "{path} -> {}",
41                items.keys().map(|ident| ident.to_string()).format(", ")
42            )?;
43        }
44        Ok(())
45    }
46}
47
48impl UsedItems {
49    pub fn new() -> Self {
50        Self {
51            used_items: Default::default(),
52        }
53    }
54
55    pub fn get_module(&self, path: &ModulePath) -> Option<&HashMap<Ident, Visibility>> {
56        self.used_items.get(path)
57    }
58
59    pub fn contains_module(&self, path: &ModulePath) -> bool {
60        self.used_items.contains_key(path)
61    }
62
63    pub fn get_ident(&self, path: &ModulePath, ident: &Ident) -> Option<Visibility> {
64        self.used_items
65            .get(path)
66            .and_then(|items| items.get(ident).copied())
67    }
68
69    pub fn get_name(&self, path: &ModulePath, name: &str) -> Option<(Ident, Visibility)> {
70        self.used_items.get(path).and_then(|items| {
71            items
72                .iter()
73                .find(|(ident, _vis)| &**ident.name() == name)
74                .map(|(ident, vis)| (ident.clone(), *vis))
75        })
76    }
77
78    /// Returns true if inserted.
79    pub fn insert_module(&mut self, path: ModulePath, idents: HashMap<Ident, Visibility>) -> bool {
80        match self.used_items.entry(path) {
81            Entry::Occupied(_) => false,
82            Entry::Vacant(entry) => {
83                entry.insert(idents);
84                true
85            }
86        }
87    }
88
89    /// Returns true if inserted.
90    /// Sets the visibility to max(vis, old_vis) if there was an entry.
91    pub fn insert_ident(&mut self, path: ModulePath, ident: Ident, visibility: Visibility) -> bool {
92        let entry = self.used_items.entry(path.clone()).or_default();
93
94        match entry.entry(ident) {
95            Entry::Occupied(mut entry) => {
96                if *entry.get() < visibility {
97                    entry.insert(visibility);
98                }
99                false
100            }
101            Entry::Vacant(entry) => {
102                entry.insert(visibility);
103                true
104            }
105        }
106    }
107
108    /// Returns true if deleted.
109    pub fn remove_module(&mut self, path: &ModulePath) -> bool {
110        self.used_items.remove(path).is_some()
111    }
112
113    pub fn is_empty(&mut self) -> bool {
114        self.used_items.is_empty()
115    }
116
117    pub fn iter(&self) -> impl Iterator<Item = (&ModulePath, &HashMap<Ident, Visibility>)> {
118        self.used_items.iter()
119    }
120}
121
122/// Find declarations used in external modules which this module depends on, no matter what.
123///
124/// Currently, only items referenced by module-scope `const_assert`s are always included.
125///
126/// See [`usage_analysis`].
127pub fn module_usage_analysis(
128    module: &Module,
129    already_used: &mut UsedItems,
130    to_analyze: &mut UsedItems,
131    ignore_visibility: bool,
132) -> Result<(), UsageError> {
133    if already_used.contains_module(&module.path) {
134        return Ok(());
135    }
136
137    already_used.insert_module(module.path.clone(), Default::default());
138    let const_asserts = module
139        .syntax
140        .global_declarations
141        .iter()
142        .filter(|decl| decl.is_const_assert());
143
144    for decl in const_asserts {
145        decl_usage_analysis(module, decl, already_used, to_analyze, ignore_visibility)?;
146    }
147
148    Ok(())
149}
150
151/// Find declaration names which a local declaration depends on.
152///
153/// Adds *external* referenced idents to `to_analyze` with their minimum required visibility.
154/// Perform usage analysis recursively with *local* referenced idents, and adds them to `already_used`
155/// with their declaration visibility.
156/// So at the end of the call, `to_analyze` contains incomplete usage analysis which needs to continue
157/// in a separate module. `already_used` contains finished analysis.
158///
159/// `min_vis` is the minimum visibility requirement for the declaration.
160///
161/// Returns [`UsageError::NotFound`] if the declaration is not found.
162/// Returns [`UsageError::Visibility`] if the declaration is found, but does not have the
163/// required visibility.
164pub fn usage_analysis(
165    module: &Module,
166    decl_name: &str,
167    min_vis: Visibility,
168    already_used: &mut UsedItems,
169    to_analyze: &mut UsedItems,
170    ignore_visibility: bool,
171) -> Result<(), UsageError> {
172    if let Some((decl_ident, decl_vis)) = already_used.get_name(&module.path, decl_name) {
173        if !ignore_visibility && decl_vis < min_vis {
174            return Err(UsageError::Visibility {
175                orig: None,
176                decl: (module.path.clone(), decl_ident),
177                min_vis,
178                decl_vis,
179            });
180        }
181    } else {
182        if let Some(decl) = module.syntax.global_declarations.iter().find(|decl| {
183            decl.ident()
184                .is_some_and(|ident| &**ident.name() == decl_name)
185        }) {
186            // we found a declaration with the right name, let's analyze it.
187            if !ignore_visibility && decl.visibility() < min_vis {
188                return Err(UsageError::Visibility {
189                    orig: None,
190                    decl: (
191                        module.path.clone(),
192                        decl.ident().unwrap(/* SAFETY: condition of the outer if */),
193                    ),
194                    min_vis,
195                    decl_vis: decl.visibility(),
196                });
197            } else {
198                decl_usage_analysis(module, decl, already_used, to_analyze, ignore_visibility)?;
199            }
200        } else if let Some((decl_ident, item)) = module
201            .imports
202            .iter()
203            .find(|(ident, _)| *ident.name() == decl_name)
204        {
205            // there is no declaration with this name, but there is a re-export.
206            if !ignore_visibility && item.visibility < min_vis {
207                return Err(UsageError::Visibility {
208                    orig: None,
209                    decl: (module.path.clone(), decl_ident.clone()),
210                    min_vis,
211                    decl_vis: item.visibility,
212                });
213            } else {
214                to_analyze.insert_ident(item.path.clone(), item.ident.clone(), item.visibility);
215            }
216        } else {
217            return Err(UsageError::NotFound(
218                module.path.clone(),
219                decl_name.to_string(),
220            ));
221        }
222    }
223    Ok(())
224}
225
226/// Find identifiers used by a declaration.
227fn decl_usage_analysis(
228    module: &Module,
229    decl: &GlobalDeclaration,
230    already_used: &mut UsedItems,
231    to_analyze: &mut UsedItems,
232    ignore_visibility: bool,
233) -> Result<(), UsageError> {
234    if decl.ident().is_some_and(|ident| {
235        !already_used.insert_ident(module.path.clone(), ident, decl.visibility())
236    }) {
237        // the ident has already been analyzed.
238        return Ok(());
239    }
240
241    // inside visit_rec below we can't simply exit the function so we mutate this instead.
242    let mut res = Ok(());
243
244    Visit::<TypeExpression>::visit_rec(decl, &mut |ty_expr| {
245        // if this ident refers an imported item, we add it to the list of used items.
246        if let Some((import_path, import_ident)) =
247            imported_item_path(ty_expr, &module.path, &module.imports)
248        {
249            let min_vis = match import_path.origin {
250                PathOrigin::Absolute | PathOrigin::Relative(_) => Visibility::Package,
251                PathOrigin::Package(_) => Visibility::Public,
252            };
253            if let Some((decl_ident, decl_vis)) =
254                already_used.get_name(&import_path, &ty_expr.ident.name())
255            {
256                if !ignore_visibility && decl_vis < min_vis {
257                    res = Err(UsageError::Visibility {
258                        orig: decl.ident().map(|ident| (module.path.clone(), ident)),
259                        decl: (import_path, decl_ident),
260                        min_vis,
261                        decl_vis,
262                    });
263                }
264            } else {
265                to_analyze.insert_ident(import_path, import_ident, min_vis);
266            }
267        }
268        // this ident refers a local declaration, we analyze it recursively.
269        else {
270            // look up a global decl with the same ident - not just the same name,
271            // because it could be shadowed by a local decl
272            let decl = module
273                .syntax
274                .global_declarations
275                .iter()
276                .find(|decl| decl.ident().is_some_and(|ident| ident == ty_expr.ident));
277
278            if let Some(decl) = decl
279                && let Err(err) =
280                    decl_usage_analysis(module, decl, already_used, to_analyze, ignore_visibility)
281            {
282                res = Err(err);
283            }
284        }
285    });
286
287    res
288}
289
290#[test]
291fn test_ignore_visibility() {
292    let module = Module::new(
293        "library".parse().unwrap(),
294        "private fn value() {}".parse().unwrap(),
295    );
296    for ignore_visibility in [false, true] {
297        assert_eq!(
298            usage_analysis(
299                &module,
300                "value",
301                Visibility::Public,
302                &mut UsedItems::new(),
303                &mut UsedItems::new(),
304                ignore_visibility,
305            )
306            .is_ok(),
307            ignore_visibility,
308        );
309    }
310}