Skip to main content

wesl/pass/
validate.rs

1use std::collections::HashSet;
2
3use wesl_macros::query;
4use wgsl_parse::SyntaxNode;
5use wgsl_parse::syntax::{
6    Expression, ExpressionNode, FunctionCall, GlobalDeclaration, Ident, ImportContent,
7    TranslationUnit, TypeExpression,
8};
9use wgsl_types::idents::{BUILTIN_CONSTRUCTOR_NAMES, BUILTIN_FUNCTION_NAMES};
10
11use crate::{
12    error::{Diagnostic, Error, ValidateError},
13    idents::builtin_ident,
14    pass::Visit,
15};
16
17type E = ValidateError;
18
19/// An identifier is linked to a declaration if:
20/// * its use-count is greater than 1
21/// * OR it is a built-in name
22///
23/// Note that this function could be simplified if we didn't care about the diagnostics metadata (declaration and expression)
24fn check_defined_symbols(module: &TranslationUnit) -> Result<(), Diagnostic<Error>> {
25    fn check_ty(ty: &TypeExpression) -> Result<(), Diagnostic<Error>> {
26        if ty.path.is_none()
27            && ty.ident.use_count() == 1
28            && builtin_ident(&ty.ident.name()).is_none()
29            // `_` is only valid for phony assignments
30            && *ty.ident.name() != "_"
31        {
32            Err(E::UndefinedSymbol(ty.ident.to_string()).into())
33        } else {
34            for arg in ty.template_args.iter().flatten() {
35                check_expr(&arg.expression)?;
36            }
37            Ok(())
38        }
39    }
40    fn check_expr(expr: &ExpressionNode) -> Result<(), Diagnostic<Error>> {
41        if let Expression::TypeOrIdentifier(ty) = expr.node() {
42            check_ty(ty).map_err(|d| d.with_span(expr.span()))
43        } else if let Expression::FunctionCall(call) = expr.node() {
44            check_ty(&call.ty).map_err(|d| d.with_span(expr.span()))?;
45            for expr in &call.arguments {
46                check_expr(expr)?;
47            }
48            Ok(())
49        } else {
50            for expr in Visit::<ExpressionNode>::visit(expr.node()) {
51                check_expr(expr)?;
52            }
53            Ok(())
54        }
55    }
56    fn check_decl(decl: &GlobalDeclaration) -> Result<(), Diagnostic<Error>> {
57        let decl_name = decl.ident().map(|ident| ident.name().to_string());
58        for expr in Visit::<ExpressionNode>::visit(decl) {
59            check_expr(expr).map_err(|mut d| {
60                d.detail.declaration = decl_name.clone();
61                d
62            })?;
63        }
64
65        // those are the attributes that don't have an expression as parent.
66        // unfortunately the diagnostic won't have a span :(
67        for ty in query!(decl.{
68            GlobalDeclaration::Declaration.ty.[],
69            GlobalDeclaration::TypeAlias.ty,
70            GlobalDeclaration::Struct.members.[].ty,
71            GlobalDeclaration::Function.{ parameters.[].ty, return_type.[] }
72        }) {
73            check_ty(ty).map_err(|mut d| {
74                d.detail.declaration = decl_name.clone();
75                d
76            })?;
77        }
78        Ok(())
79    }
80
81    for decl in &module.global_declarations {
82        check_decl(decl)?;
83    }
84    Ok(())
85}
86
87fn check_function_calls(module: &TranslationUnit) -> Result<(), Diagnostic<Error>> {
88    fn check_call(call: &FunctionCall, ident: &Ident, module: &TranslationUnit) -> Result<(), E> {
89        let decl = module
90            .global_declarations
91            .iter()
92            .find(|decl| decl.ident().is_some_and(|id| &id == ident))
93            .map(|decl| decl.node());
94
95        match decl {
96            Some(GlobalDeclaration::Function(decl)) => {
97                if call.arguments.len() != decl.parameters.len() {
98                    return Err(E::ParamCount(
99                        ident.to_string(),
100                        decl.parameters.len(),
101                        call.arguments.len(),
102                    ));
103                }
104            }
105            Some(GlobalDeclaration::Struct(decl)) => {
106                if call.arguments.len() != decl.members.len() && !call.arguments.is_empty() {
107                    return Err(E::ParamCount(
108                        ident.to_string(),
109                        decl.members.len(),
110                        call.arguments.len(),
111                    ));
112                }
113            }
114            Some(GlobalDeclaration::TypeAlias(decl)) => {
115                if decl.ty.template_args.is_some() {
116                    // TODO: check args for builtin functions
117                } else {
118                    check_call(call, &decl.ty.ident, module)?;
119                }
120            }
121            Some(_) => return Err(E::NotCallable(ident.to_string())),
122            None => {
123                if BUILTIN_FUNCTION_NAMES
124                    .iter()
125                    .any(|name| name == &*ident.name())
126                {
127                    // TODO: check args for builtin functions
128                } else if BUILTIN_CONSTRUCTOR_NAMES
129                    .iter()
130                    .any(|name| name == &*ident.name())
131                {
132                    // TODO: check args for builtin constructors
133                } else {
134                    // the ident is not a global declaration, it must be a local variable.
135                    return Err(E::NotCallable(ident.to_string()));
136                }
137            }
138        };
139        Ok(())
140    }
141    fn check_expr(expr: &Expression, module: &TranslationUnit) -> Result<(), E> {
142        if let Expression::FunctionCall(call) = expr {
143            check_call(call, &call.ty.ident, module)?;
144        }
145        Ok(())
146    }
147    for decl in &module.global_declarations {
148        for expr in Visit::<ExpressionNode>::visit(decl.node()) {
149            check_expr(expr, module).map_err(|e| {
150                let mut err = Diagnostic::from(e);
151                err.detail.span = Some(expr.span());
152                err.detail.declaration = decl.ident().map(|id| id.name().to_string());
153                err
154            })?;
155        }
156    }
157    Ok(())
158}
159
160fn check_duplicate_decl(module: &TranslationUnit) -> Result<(), Diagnostic<Error>> {
161    let mut unique = HashSet::new();
162
163    fn check_ident(id: &Ident, unique: &mut HashSet<String>) -> Result<(), Diagnostic<Error>> {
164        if unique.contains(id.name().as_str()) {
165            Err(Diagnostic::from(E::Duplicate(id.to_string())).with_declaration(id.to_string()))
166        } else {
167            unique.insert(id.to_string());
168            Ok(())
169        }
170    }
171
172    fn check_import_content(
173        cont: &ImportContent,
174        unique: &mut HashSet<String>,
175    ) -> Result<(), Diagnostic<Error>> {
176        match cont {
177            ImportContent::Item(item) => {
178                let id = item.rename.as_ref().unwrap_or(&item.ident);
179                check_ident(id, unique)?;
180            }
181            ImportContent::Collection(coll) => {
182                for item in coll {
183                    check_import_content(&item.content, unique)?;
184                }
185            }
186        }
187        Ok(())
188    }
189
190    for import in &module.imports {
191        if import
192            .attributes()
193            .iter()
194            .any(|attr| attr.is_if() || attr.is_elif() || attr.is_else())
195        {
196            // we skip checking declarations that have conditional compilation flags.
197            continue;
198        }
199        check_import_content(&import.content, &mut unique)?;
200    }
201
202    for decl in &module.global_declarations {
203        if decl
204            .attributes()
205            .iter()
206            .any(|attr| attr.is_if() || attr.is_elif() || attr.is_else())
207        {
208            // we skip checking declarations that have conditional compilation flags.
209            continue;
210        }
211        if let Some(id) = decl.ident() {
212            check_ident(&id, &mut unique)?;
213        }
214    }
215    Ok(())
216}
217
218fn check_cycles(module: &TranslationUnit) -> Result<(), Diagnostic<Error>> {
219    fn check_decl(
220        id: &Ident,
221        decl: &GlobalDeclaration,
222        unique: &mut HashSet<Ident>,
223        module: &TranslationUnit,
224    ) -> Result<(), E> {
225        let mut res = Ok(());
226
227        Visit::<TypeExpression>::visit_rec(decl, &mut |ty| {
228            if res.is_ok() {
229                if ty.ident == *id {
230                    res = Err(E::Cycle(id.to_string(), decl.ident().unwrap().to_string()));
231                } else if unique.insert(ty.ident.clone())
232                    && let Some(decl) = module
233                        .global_declarations
234                        .iter()
235                        .find(|decl| decl.ident().as_ref() == Some(&ty.ident))
236                {
237                    res = check_decl(id, decl, unique, module);
238                }
239            }
240        });
241
242        res
243    }
244    for decl in &module.global_declarations {
245        if let Some(id) = decl.ident() {
246            let mut unique = HashSet::new();
247            check_decl(&id, decl, &mut unique, module)
248                .map_err(|e| Diagnostic::from(e).with_declaration(id.to_string()))?;
249        }
250    }
251    Ok(())
252}
253
254/// Validate an intermediate WESL module.
255///
256/// This function only checks that a WESL module is valid on its own, without looking at
257/// external modules (imports).
258///
259/// It currently does not validate a lot. It checks for:
260/// * Defined declarations: all identifiers refer to a user declaration, import or
261///   built-in name.
262/// * Duplicate declarations: declarations in the same scope cannot have the same name.
263///   (except for unresolved conditional compilation)
264/// * Cyclic declarations: no cycles are allowed in declarations.
265pub fn validate_wesl(module: &TranslationUnit) -> Result<(), Diagnostic<Error>> {
266    check_defined_symbols(module)?;
267    check_duplicate_decl(module)?;
268    check_cycles(module)?;
269    Ok(())
270}
271
272/// Validate the final output (valid WGSL).
273///
274/// It currently does not validate a lot. It checks for:
275/// * Defined declarations: all identifiers refer to a user declaration or built-in name.
276/// * Duplicate declarations: declarations in the same scope cannot have the same name.
277/// * Cyclic declarations: no cycles are allowed in declarations.
278/// * Function calls: call expressions must refer to a function or a type constructor.
279///   Check the number of arguments but not their type.
280pub fn validate_wgsl(module: &TranslationUnit) -> Result<(), Diagnostic<Error>> {
281    check_defined_symbols(module)?;
282    check_duplicate_decl(module)?;
283    check_cycles(module)?;
284    check_function_calls(module)?;
285    Ok(())
286}