naga_oil/compose/
preprocess.rs

1use std::collections::{HashMap, HashSet};
2
3use indexmap::IndexMap;
4use regex::Regex;
5
6use super::{
7    comment_strip_iter::CommentReplaceExt,
8    parse_imports::{parse_imports, substitute_identifiers},
9    wgsl_directives::{DiagnosticDirective, EnableDirective, RequiresDirective, WgslDirectives},
10    ComposerErrorInner, ImportDefWithOffset, ShaderDefValue,
11};
12
13#[derive(Debug)]
14pub struct Preprocessor {
15    version_regex: Regex,
16    ifdef_regex: Regex,
17    ifndef_regex: Regex,
18    ifop_regex: Regex,
19    else_regex: Regex,
20    endif_regex: Regex,
21    def_regex: Regex,
22    def_regex_delimited: Regex,
23    import_regex: Regex,
24    define_import_path_regex: Regex,
25    define_shader_def_regex: Regex,
26    enable_regex: Regex,
27    requires_regex: Regex,
28    diagnostic_regex: Regex,
29}
30
31impl Default for Preprocessor {
32    fn default() -> Self {
33        Self {
34            version_regex: Regex::new(r"^\s*#version\s+([0-9]+)").unwrap(),
35            ifdef_regex: Regex::new(r"^\s*#\s*(else\s+)?\s*ifdef\s+([\w|\d|_]+)").unwrap(),
36            ifndef_regex: Regex::new(r"^\s*#\s*(else\s+)?\s*ifndef\s+([\w|\d|_]+)").unwrap(),
37            ifop_regex: Regex::new(
38                r"^\s*#\s*(else\s+)?\s*if\s+([\w|\d|_]+)\s*([=!<>]*)\s*([-\w|\d]+)",
39            )
40            .unwrap(),
41            else_regex: Regex::new(r"^\s*#\s*else").unwrap(),
42            endif_regex: Regex::new(r"^\s*#\s*endif").unwrap(),
43            def_regex: Regex::new(r"#\s*([\w|\d|_]+)").unwrap(),
44            def_regex_delimited: Regex::new(r"#\s*\{([\w|\d|_]+)\}").unwrap(),
45            import_regex: Regex::new(r"^\s*#\s*import\s").unwrap(),
46            define_import_path_regex: Regex::new(r"^\s*#\s*define_import_path\s+([^\s]+)").unwrap(),
47            define_shader_def_regex: Regex::new(r"^\s*#\s*define\s+([\w|\d|_]+)\s*([-\w|\d]+)?")
48                .unwrap(),
49            enable_regex: Regex::new(r"^\s*enable\s+([^;]+)\s*;").unwrap(),
50            requires_regex: Regex::new(r"^\s*requires\s+([^;]+)\s*;").unwrap(),
51            diagnostic_regex: Regex::new(r"^\s*diagnostic\s*\(\s*([^,]+)\s*,\s*([^)]+)\s*\)\s*;")
52                .unwrap(),
53        }
54    }
55}
56
57#[derive(Debug)]
58pub struct PreprocessorMetaData {
59    pub name: Option<String>,
60    pub imports: Vec<ImportDefWithOffset>,
61    pub defines: HashMap<String, ShaderDefValue>,
62    pub effective_defs: HashSet<String>,
63    pub wgsl_directives: WgslDirectives,
64    pub cleaned_source: String,
65}
66
67enum ScopeLevel {
68    Active,           // conditions have been met
69    PreviouslyActive, // conditions have previously been met
70    NotActive,        // no conditions yet met
71}
72
73struct Scope(Vec<ScopeLevel>);
74
75impl Scope {
76    fn new() -> Self {
77        Self(vec![ScopeLevel::Active])
78    }
79
80    fn branch(
81        &mut self,
82        is_else: bool,
83        condition: bool,
84        offset: usize,
85    ) -> Result<(), ComposerErrorInner> {
86        if is_else {
87            let prev_scope = self.0.pop().unwrap();
88            let parent_scope = self
89                .0
90                .last()
91                .ok_or(ComposerErrorInner::ElseWithoutCondition(offset))?;
92            let new_scope = if !matches!(parent_scope, ScopeLevel::Active) {
93                ScopeLevel::NotActive
94            } else if !matches!(prev_scope, ScopeLevel::NotActive) {
95                ScopeLevel::PreviouslyActive
96            } else if condition {
97                ScopeLevel::Active
98            } else {
99                ScopeLevel::NotActive
100            };
101
102            self.0.push(new_scope);
103        } else {
104            let parent_scope = self.0.last().unwrap_or(&ScopeLevel::Active);
105            let new_scope = if matches!(parent_scope, ScopeLevel::Active) && condition {
106                ScopeLevel::Active
107            } else {
108                ScopeLevel::NotActive
109            };
110
111            self.0.push(new_scope);
112        }
113
114        Ok(())
115    }
116
117    fn pop(&mut self, offset: usize) -> Result<(), ComposerErrorInner> {
118        self.0.pop();
119        if self.0.is_empty() {
120            Err(ComposerErrorInner::TooManyEndIfs(offset))
121        } else {
122            Ok(())
123        }
124    }
125
126    fn active(&self) -> bool {
127        matches!(self.0.last().unwrap(), ScopeLevel::Active)
128    }
129
130    fn finish(&self, offset: usize) -> Result<(), ComposerErrorInner> {
131        if self.0.len() != 1 {
132            Err(ComposerErrorInner::NotEnoughEndIfs(offset))
133        } else {
134            Ok(())
135        }
136    }
137}
138
139#[derive(Debug)]
140pub struct PreprocessOutput {
141    pub preprocessed_source: String,
142    pub imports: Vec<ImportDefWithOffset>,
143}
144
145impl Preprocessor {
146    fn parse_enable_directive(
147        &self,
148        line: &str,
149        line_idx: usize,
150    ) -> Result<Option<EnableDirective>, ComposerErrorInner> {
151        if let Some(cap) = self.enable_regex.captures(line) {
152            let extensions_str = cap.get(1).unwrap().as_str().trim();
153            let extensions: Vec<String> = extensions_str
154                .split(',')
155                .map(|s| s.trim().to_string())
156                .filter(|s| !s.is_empty())
157                .collect();
158
159            if extensions.is_empty() {
160                return Err(ComposerErrorInner::InvalidWgslDirective {
161                    directive: line.to_string(),
162                    position: line_idx,
163                    reason: "No extensions specified".to_string(),
164                });
165            }
166
167            Ok(Some(EnableDirective {
168                extensions,
169                source_location: line_idx,
170            }))
171        } else {
172            Ok(None)
173        }
174    }
175
176    fn parse_requires_directive(
177        &self,
178        line: &str,
179        line_idx: usize,
180    ) -> Result<Option<RequiresDirective>, ComposerErrorInner> {
181        if let Some(cap) = self.requires_regex.captures(line) {
182            let extensions_str = cap.get(1).unwrap().as_str().trim();
183            let extensions: Vec<String> = extensions_str
184                .split(',')
185                .map(|s| s.trim().to_string())
186                .filter(|s| !s.is_empty())
187                .collect();
188
189            if extensions.is_empty() {
190                return Err(ComposerErrorInner::InvalidWgslDirective {
191                    directive: line.to_string(),
192                    position: line_idx,
193                    reason: "No extensions specified".to_string(),
194                });
195            }
196
197            Ok(Some(RequiresDirective {
198                extensions,
199                source_location: line_idx,
200            }))
201        } else {
202            Ok(None)
203        }
204    }
205
206    fn parse_diagnostic_directive(
207        &self,
208        line: &str,
209        line_idx: usize,
210    ) -> Result<Option<DiagnosticDirective>, ComposerErrorInner> {
211        if let Some(cap) = self.diagnostic_regex.captures(line) {
212            let severity = cap.get(1).unwrap().as_str().trim().to_string();
213            let rule = cap.get(2).unwrap().as_str().trim().to_string();
214
215            match severity.as_str() {
216                "off" | "info" | "warn" | "error" => {}
217                _ => {
218                    return Err(ComposerErrorInner::InvalidWgslDirective {
219                        directive: line.to_string(),
220                        position: line_idx,
221                        reason: format!(
222                            "Invalid severity '{severity}'. Must be one of: off, info, warn, error"
223                        ),
224                    });
225                }
226            }
227
228            Ok(Some(DiagnosticDirective {
229                severity,
230                rule,
231                source_location: line_idx,
232            }))
233        } else {
234            Ok(None)
235        }
236    }
237
238    pub fn extract_wgsl_directives(
239        &self,
240        source: &str,
241    ) -> Result<(String, WgslDirectives), ComposerErrorInner> {
242        let mut directives = WgslDirectives::default();
243        let mut cleaned_lines = Vec::new();
244        let mut in_directive_section = true;
245
246        for (line_idx, line) in source.lines().enumerate() {
247            let trimmed = line.trim();
248
249            if trimmed.is_empty() || trimmed.starts_with("//") {
250                cleaned_lines.push(line);
251                continue;
252            }
253
254            if in_directive_section {
255                if let Some(enable) = self.parse_enable_directive(trimmed, line_idx)? {
256                    cleaned_lines.push("");
257                    directives.enables.push(enable);
258                    continue;
259                } else if let Some(requires) = self.parse_requires_directive(trimmed, line_idx)? {
260                    cleaned_lines.push("");
261                    directives.requires.push(requires);
262                    continue;
263                } else if let Some(diagnostic) =
264                    self.parse_diagnostic_directive(trimmed, line_idx)?
265                {
266                    cleaned_lines.push("");
267                    directives.diagnostics.push(diagnostic);
268                    continue;
269                } else if !trimmed.starts_with("enable")
270                    && !trimmed.starts_with("requires")
271                    && !trimmed.starts_with("diagnostic")
272                {
273                    in_directive_section = false;
274                }
275            }
276
277            cleaned_lines.push(line);
278        }
279
280        let cleaned_source = cleaned_lines.join("\n");
281        Ok((cleaned_source, directives))
282    }
283
284    fn check_scope<'a>(
285        &self,
286        shader_defs: &HashMap<String, ShaderDefValue>,
287        line: &'a str,
288        scope: Option<&mut Scope>,
289        offset: usize,
290    ) -> Result<(bool, Option<&'a str>), ComposerErrorInner> {
291        if let Some(cap) = self.ifdef_regex.captures(line) {
292            let is_else = cap.get(1).is_some();
293            let def = cap.get(2).unwrap().as_str();
294            let cond = shader_defs.contains_key(def);
295            scope.map_or(Ok(()), |scope| scope.branch(is_else, cond, offset))?;
296            return Ok((true, Some(def)));
297        } else if let Some(cap) = self.ifndef_regex.captures(line) {
298            let is_else = cap.get(1).is_some();
299            let def = cap.get(2).unwrap().as_str();
300            let cond = !shader_defs.contains_key(def);
301            scope.map_or(Ok(()), |scope| scope.branch(is_else, cond, offset))?;
302            return Ok((true, Some(def)));
303        } else if let Some(cap) = self.ifop_regex.captures(line) {
304            let is_else = cap.get(1).is_some();
305            let def = cap.get(2).unwrap().as_str();
306            let op = cap.get(3).unwrap();
307            let val = cap.get(4).unwrap();
308
309            if scope.is_none() {
310                // don't try to evaluate if we don't have a scope
311                return Ok((true, Some(def)));
312            }
313
314            fn act_on<T: Eq + Ord>(
315                a: T,
316                b: T,
317                op: &str,
318                pos: usize,
319            ) -> Result<bool, ComposerErrorInner> {
320                match op {
321                    "==" => Ok(a == b),
322                    "!=" => Ok(a != b),
323                    ">" => Ok(a > b),
324                    ">=" => Ok(a >= b),
325                    "<" => Ok(a < b),
326                    "<=" => Ok(a <= b),
327                    _ => Err(ComposerErrorInner::UnknownShaderDefOperator {
328                        pos,
329                        operator: op.to_string(),
330                    }),
331                }
332            }
333
334            let def_value = shader_defs
335                .get(def)
336                .ok_or(ComposerErrorInner::UnknownShaderDef {
337                    pos: offset,
338                    shader_def_name: def.to_string(),
339                })?;
340
341            let invalid_def = |ty: &str| ComposerErrorInner::InvalidShaderDefComparisonValue {
342                pos: offset,
343                shader_def_name: def.to_string(),
344                value: val.as_str().to_string(),
345                expected: ty.to_string(),
346            };
347
348            let new_scope = match def_value {
349                ShaderDefValue::Bool(def_value) => {
350                    let val = val.as_str().parse().map_err(|_| invalid_def("bool"))?;
351                    act_on(*def_value, val, op.as_str(), offset)?
352                }
353                ShaderDefValue::Int(def_value) => {
354                    let val = val.as_str().parse().map_err(|_| invalid_def("int"))?;
355                    act_on(*def_value, val, op.as_str(), offset)?
356                }
357                ShaderDefValue::UInt(def_value) => {
358                    let val = val.as_str().parse().map_err(|_| invalid_def("uint"))?;
359                    act_on(*def_value, val, op.as_str(), offset)?
360                }
361            };
362
363            scope.map_or(Ok(()), |scope| scope.branch(is_else, new_scope, offset))?;
364            return Ok((true, Some(def)));
365        } else if self.else_regex.is_match(line) {
366            scope.map_or(Ok(()), |scope| scope.branch(true, true, offset))?;
367            return Ok((true, None));
368        } else if self.endif_regex.is_match(line) {
369            scope.map_or(Ok(()), |scope| scope.pop(offset))?;
370            return Ok((true, None));
371        }
372
373        Ok((false, None))
374    }
375
376    // process #if[(n)?def]? / #else / #endif preprocessor directives,
377    // strip module name and imports
378    // also strip "#version xxx"
379    // replace items with resolved decorated names
380    pub fn preprocess(
381        &self,
382        shader_str: &str,
383        shader_defs: &HashMap<String, ShaderDefValue>,
384    ) -> Result<PreprocessOutput, ComposerErrorInner> {
385        let mut declared_imports = IndexMap::new();
386        let mut used_imports = IndexMap::new();
387        let mut scope = Scope::new();
388        let mut final_string = String::new();
389        let mut offset = 0;
390
391        // this code broadly stolen from bevy_render::ShaderProcessor
392        let mut lines = shader_str.lines();
393        let mut lines = lines.replace_comments().zip(shader_str.lines()).peekable();
394
395        while let Some((mut line, original_line)) = lines.next() {
396            let mut output = false;
397
398            if let Some(cap) = self.version_regex.captures(&line) {
399                let v = cap.get(1).unwrap().as_str();
400                if v != "440" && v != "450" {
401                    return Err(ComposerErrorInner::GlslInvalidVersion(offset));
402                }
403            } else if self
404                .check_scope(shader_defs, &line, Some(&mut scope), offset)?
405                .0
406                || self.define_import_path_regex.captures(&line).is_some()
407                || self.define_shader_def_regex.captures(&line).is_some()
408            {
409                // ignore
410            } else if scope.active() {
411                if self.import_regex.is_match(&line) {
412                    let mut import_lines = String::default();
413                    let mut open_count = 0;
414                    let initial_offset = offset;
415
416                    loop {
417                        // output spaces for removed lines to keep spans consistent (errors report against substituted_source, which is not preprocessed)
418                        final_string.extend(std::iter::repeat_n(" ", line.len()));
419                        offset += line.len() + 1;
420
421                        // PERF: Ideally we don't do multiple `match_indices` passes over `line`
422                        // in addition to the final pass for the import parse
423                        open_count += line.match_indices('{').count();
424                        open_count = open_count.saturating_sub(line.match_indices('}').count());
425
426                        // PERF: it's bad that we allocate here. ideally we would use something like
427                        //     let import_lines = &shader_str[initial_offset..offset]
428                        // but we need the comments removed, and the iterator approach doesn't make that easy
429                        import_lines.push_str(&line);
430                        import_lines.push('\n');
431
432                        if open_count == 0 || lines.peek().is_none() {
433                            break;
434                        }
435
436                        final_string.push('\n');
437                        line = lines.next().unwrap().0;
438                    }
439
440                    parse_imports(import_lines.as_str(), &mut declared_imports).map_err(
441                        |(err, line_offset)| {
442                            ComposerErrorInner::ImportParseError(
443                                err.to_owned(),
444                                initial_offset + line_offset,
445                            )
446                        },
447                    )?;
448                    output = true;
449                } else {
450                    let replaced_lines = [original_line, &line].map(|input| {
451                        let mut output = input.to_string();
452                        for capture in self.def_regex.captures_iter(input) {
453                            let def = capture.get(1).unwrap();
454                            if let Some(def) = shader_defs.get(def.as_str()) {
455                                output = self
456                                    .def_regex
457                                    .replace(&output, def.value_as_string())
458                                    .to_string();
459                            }
460                        }
461                        for capture in self.def_regex_delimited.captures_iter(input) {
462                            let def = capture.get(1).unwrap();
463                            if let Some(def) = shader_defs.get(def.as_str()) {
464                                output = self
465                                    .def_regex_delimited
466                                    .replace(&output, def.value_as_string())
467                                    .to_string();
468                            }
469                        }
470                        output
471                    });
472
473                    let original_line = &replaced_lines[0];
474                    let decommented_line = &replaced_lines[1];
475
476                    // we don't want to capture imports from comments so we run using a dummy used_imports, and disregard any errors
477                    let item_replaced_line = substitute_identifiers(
478                        original_line,
479                        offset,
480                        &declared_imports,
481                        &mut Default::default(),
482                        true,
483                    )
484                    .unwrap();
485                    // we also run against the de-commented line to replace real imports, and throw an error if appropriate
486                    let _ = substitute_identifiers(
487                        decommented_line,
488                        offset,
489                        &declared_imports,
490                        &mut used_imports,
491                        false,
492                    )
493                    .map_err(|pos| {
494                        ComposerErrorInner::ImportParseError(
495                            "Ambiguous import path for item".to_owned(),
496                            pos,
497                        )
498                    })?;
499
500                    final_string.push_str(&item_replaced_line);
501                    let diff = line.len().saturating_sub(item_replaced_line.len());
502                    final_string.extend(std::iter::repeat_n(" ", diff));
503                    offset += original_line.len() + 1;
504                    output = true;
505                }
506            }
507
508            if !output {
509                // output spaces for removed lines to keep spans consistent (errors report against substituted_source, which is not preprocessed)
510                final_string.extend(std::iter::repeat_n(" ", line.len()));
511                offset += line.len() + 1;
512            }
513            final_string.push('\n');
514        }
515
516        scope.finish(offset)?;
517
518        Ok(PreprocessOutput {
519            preprocessed_source: final_string,
520            imports: used_imports.into_values().collect(),
521        })
522    }
523
524    // extract module name and all possible imports
525    pub fn get_preprocessor_metadata(
526        &self,
527        shader_str: &str,
528        allow_defines: bool,
529    ) -> Result<PreprocessorMetaData, ComposerErrorInner> {
530        let (shader_str, wgsl_directives) = self.extract_wgsl_directives(shader_str)?;
531
532        let mut declared_imports = IndexMap::default();
533        let mut used_imports = IndexMap::default();
534        let mut name = None;
535        let mut offset = 0;
536        let mut defines = HashMap::default();
537        let mut effective_defs = HashSet::default();
538
539        let mut lines = shader_str.lines();
540        let mut lines = lines.replace_comments().peekable();
541
542        while let Some(mut line) = lines.next() {
543            let (is_scope, def) = self.check_scope(&HashMap::default(), &line, None, offset)?;
544
545            if is_scope {
546                if let Some(def) = def {
547                    effective_defs.insert(def.to_owned());
548                }
549            } else if self.import_regex.is_match(&line) {
550                let mut import_lines = String::default();
551                let mut open_count = 0;
552                let initial_offset = offset;
553
554                loop {
555                    // PERF: Ideally we don't do multiple `match_indices` passes over `line`
556                    // in addition to the final pass for the import parse
557                    open_count += line.match_indices('{').count();
558                    open_count = open_count.saturating_sub(line.match_indices('}').count());
559
560                    // PERF: it's bad that we allocate here. ideally we would use something like
561                    //     let import_lines = &shader_str[initial_offset..offset]
562                    // but we need the comments removed, and the iterator approach doesn't make that easy
563                    import_lines.push_str(&line);
564                    import_lines.push('\n');
565
566                    if open_count == 0 || lines.peek().is_none() {
567                        break;
568                    }
569
570                    // output spaces for removed lines to keep spans consistent (errors report against substituted_source, which is not preprocessed)
571                    offset += line.len() + 1;
572
573                    line = lines.next().unwrap();
574                }
575
576                parse_imports(import_lines.as_str(), &mut declared_imports).map_err(
577                    |(err, line_offset)| {
578                        ComposerErrorInner::ImportParseError(
579                            err.to_owned(),
580                            initial_offset + line_offset,
581                        )
582                    },
583                )?;
584            } else if let Some(cap) = self.define_import_path_regex.captures(&line) {
585                name = Some(cap.get(1).unwrap().as_str().to_string());
586            } else if let Some(cap) = self.define_shader_def_regex.captures(&line) {
587                if allow_defines {
588                    let def = cap.get(1).unwrap();
589                    let name = def.as_str().to_string();
590
591                    let value = if let Some(val) = cap.get(2) {
592                        if let Ok(val) = val.as_str().parse::<u32>() {
593                            ShaderDefValue::UInt(val)
594                        } else if let Ok(val) = val.as_str().parse::<i32>() {
595                            ShaderDefValue::Int(val)
596                        } else if let Ok(val) = val.as_str().parse::<bool>() {
597                            ShaderDefValue::Bool(val)
598                        } else {
599                            ShaderDefValue::Bool(false) // this error will get picked up when we fully preprocess the module
600                        }
601                    } else {
602                        ShaderDefValue::Bool(true)
603                    };
604
605                    defines.insert(name, value);
606                } else {
607                    return Err(ComposerErrorInner::DefineInModule(offset));
608                }
609            } else {
610                for cap in self
611                    .def_regex
612                    .captures_iter(&line)
613                    .chain(self.def_regex_delimited.captures_iter(&line))
614                {
615                    effective_defs.insert(cap.get(1).unwrap().as_str().to_owned());
616                }
617
618                substitute_identifiers(&line, offset, &declared_imports, &mut used_imports, true)
619                    .unwrap();
620            }
621
622            offset += line.len() + 1;
623        }
624
625        Ok(PreprocessorMetaData {
626            name,
627            imports: used_imports.into_values().collect(),
628            defines,
629            effective_defs,
630            wgsl_directives,
631            cleaned_source: shader_str.to_string(),
632        })
633    }
634}
635
636#[cfg(test)]
637mod test {
638    use super::*;
639
640    #[rustfmt::skip]
641    const WGSL_ELSE_IFDEF: &str = r"
642struct View {
643    view_proj: mat4x4<f32>,
644    world_position: vec3<f32>,
645};
646@group(0) @binding(0)
647var<uniform> view: View;
648
649#ifdef TEXTURE
650// Main texture
651@group(1) @binding(0)
652var sprite_texture: texture_2d<f32>;
653#else ifdef SECOND_TEXTURE
654// Second texture
655@group(1) @binding(0)
656var sprite_texture: texture_2d<f32>;
657#else ifdef THIRD_TEXTURE
658// Third texture
659@group(1) @binding(0)
660var sprite_texture: texture_2d<f32>;
661#else
662@group(1) @binding(0)
663var sprite_texture: texture_2d_array<f32>;
664#endif
665
666struct VertexOutput {
667    @location(0) uv: vec2<f32>,
668    @builtin(position) position: vec4<f32>,
669};
670
671@vertex
672fn vertex(
673    @location(0) vertex_position: vec3<f32>,
674    @location(1) vertex_uv: vec2<f32>
675) -> VertexOutput {
676    var out: VertexOutput;
677    out.uv = vertex_uv;
678    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
679    return out;
680}
681";
682
683    //preprocessor tests
684    #[test]
685    fn process_shader_def_unknown_operator() {
686        #[rustfmt::skip]
687        const WGSL: &str = r"
688struct View {
689    view_proj: mat4x4<f32>,
690    world_position: vec3<f32>,
691};
692@group(0) @binding(0)
693var<uniform> view: View;
694#if TEXTURE !! true
695@group(1) @binding(0)
696var sprite_texture: texture_2d<f32>;
697#endif
698struct VertexOutput {
699    @location(0) uv: vec2<f32>,
700    @builtin(position) position: vec4<f32>,
701};
702@vertex
703fn vertex(
704    @location(0) vertex_position: vec3<f32>,
705    @location(1) vertex_uv: vec2<f32>
706) -> VertexOutput {
707    var out: VertexOutput;
708    out.uv = vertex_uv;
709    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
710    return out;
711}
712";
713
714        let processor = Preprocessor::default();
715
716        let result_missing = processor.preprocess(
717            WGSL,
718            &[("TEXTURE".to_owned(), ShaderDefValue::Bool(true))].into(),
719        );
720
721        let expected: Result<Preprocessor, ComposerErrorInner> =
722            Err(ComposerErrorInner::UnknownShaderDefOperator {
723                pos: 124,
724                operator: "!!".to_string(),
725            });
726
727        assert_eq!(format!("{result_missing:?}"), format!("{expected:?}"),);
728    }
729    #[test]
730    fn process_shader_def_equal_int() {
731        #[rustfmt::skip]
732        const WGSL: &str = r"
733struct View {
734    view_proj: mat4x4<f32>,
735    world_position: vec3<f32>,
736};
737@group(0) @binding(0)
738var<uniform> view: View;
739#if TEXTURE == 3
740@group(1) @binding(0)
741var sprite_texture: texture_2d<f32>;
742#endif
743struct VertexOutput {
744    @location(0) uv: vec2<f32>,
745    @builtin(position) position: vec4<f32>,
746};
747@vertex
748fn vertex(
749    @location(0) vertex_position: vec3<f32>,
750    @location(1) vertex_uv: vec2<f32>
751) -> VertexOutput {
752    var out: VertexOutput;
753    out.uv = vertex_uv;
754    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
755    return out;
756}
757";
758
759        #[rustfmt::skip]
760        const EXPECTED_EQ: &str = r"
761struct View {
762    view_proj: mat4x4<f32>,
763    world_position: vec3<f32>,
764};
765@group(0) @binding(0)
766var<uniform> view: View;
767                
768@group(1) @binding(0)
769var sprite_texture: texture_2d<f32>;
770      
771struct VertexOutput {
772    @location(0) uv: vec2<f32>,
773    @builtin(position) position: vec4<f32>,
774};
775@vertex
776fn vertex(
777    @location(0) vertex_position: vec3<f32>,
778    @location(1) vertex_uv: vec2<f32>
779) -> VertexOutput {
780    var out: VertexOutput;
781    out.uv = vertex_uv;
782    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
783    return out;
784}
785";
786
787        #[rustfmt::skip]
788        const EXPECTED_NEQ: &str = r"
789struct View {
790    view_proj: mat4x4<f32>,
791    world_position: vec3<f32>,
792};
793@group(0) @binding(0)
794var<uniform> view: View;
795                
796                     
797                                    
798      
799struct VertexOutput {
800    @location(0) uv: vec2<f32>,
801    @builtin(position) position: vec4<f32>,
802};
803@vertex
804fn vertex(
805    @location(0) vertex_position: vec3<f32>,
806    @location(1) vertex_uv: vec2<f32>
807) -> VertexOutput {
808    var out: VertexOutput;
809    out.uv = vertex_uv;
810    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
811    return out;
812}
813";
814        let processor = Preprocessor::default();
815        let result_eq = processor
816            .preprocess(
817                WGSL,
818                &[("TEXTURE".to_string(), ShaderDefValue::Int(3))].into(),
819            )
820            .unwrap();
821        assert_eq!(result_eq.preprocessed_source, EXPECTED_EQ);
822
823        let result_neq = processor
824            .preprocess(
825                WGSL,
826                &[("TEXTURE".to_string(), ShaderDefValue::Int(7))].into(),
827            )
828            .unwrap();
829        assert_eq!(result_neq.preprocessed_source, EXPECTED_NEQ);
830
831        let result_missing = processor.preprocess(WGSL, &Default::default());
832
833        let expected_err: Result<
834            (Option<String>, String, Vec<ImportDefWithOffset>),
835            ComposerErrorInner,
836        > = Err(ComposerErrorInner::UnknownShaderDef {
837            pos: 124,
838            shader_def_name: "TEXTURE".to_string(),
839        });
840        assert_eq!(format!("{result_missing:?}"), format!("{expected_err:?}"),);
841
842        let result_wrong_type = processor.preprocess(
843            WGSL,
844            &[("TEXTURE".to_string(), ShaderDefValue::Bool(true))].into(),
845        );
846
847        let expected_err: Result<
848            (Option<String>, String, Vec<ImportDefWithOffset>),
849            ComposerErrorInner,
850        > = Err(ComposerErrorInner::InvalidShaderDefComparisonValue {
851            pos: 124,
852            shader_def_name: "TEXTURE".to_string(),
853            expected: "bool".to_string(),
854            value: "3".to_string(),
855        });
856
857        assert_eq!(
858            format!("{result_wrong_type:?}"),
859            format!("{expected_err:?}")
860        );
861    }
862
863    #[test]
864    fn process_shader_def_equal_bool() {
865        #[rustfmt::skip]
866        const WGSL: &str = r"
867struct View {
868    view_proj: mat4x4<f32>,
869    world_position: vec3<f32>,
870};
871@group(0) @binding(0)
872var<uniform> view: View;
873#if TEXTURE == true
874@group(1) @binding(0)
875var sprite_texture: texture_2d<f32>;
876#endif
877struct VertexOutput {
878    @location(0) uv: vec2<f32>,
879    @builtin(position) position: vec4<f32>,
880};
881@vertex
882fn vertex(
883    @location(0) vertex_position: vec3<f32>,
884    @location(1) vertex_uv: vec2<f32>
885) -> VertexOutput {
886    var out: VertexOutput;
887    out.uv = vertex_uv;
888    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
889    return out;
890}
891";
892
893        #[rustfmt::skip]
894        const EXPECTED_EQ: &str = r"
895struct View {
896    view_proj: mat4x4<f32>,
897    world_position: vec3<f32>,
898};
899@group(0) @binding(0)
900var<uniform> view: View;
901                   
902@group(1) @binding(0)
903var sprite_texture: texture_2d<f32>;
904      
905struct VertexOutput {
906    @location(0) uv: vec2<f32>,
907    @builtin(position) position: vec4<f32>,
908};
909@vertex
910fn vertex(
911    @location(0) vertex_position: vec3<f32>,
912    @location(1) vertex_uv: vec2<f32>
913) -> VertexOutput {
914    var out: VertexOutput;
915    out.uv = vertex_uv;
916    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
917    return out;
918}
919";
920
921        #[rustfmt::skip]
922        const EXPECTED_NEQ: &str = r"
923struct View {
924    view_proj: mat4x4<f32>,
925    world_position: vec3<f32>,
926};
927@group(0) @binding(0)
928var<uniform> view: View;
929                   
930                     
931                                    
932      
933struct VertexOutput {
934    @location(0) uv: vec2<f32>,
935    @builtin(position) position: vec4<f32>,
936};
937@vertex
938fn vertex(
939    @location(0) vertex_position: vec3<f32>,
940    @location(1) vertex_uv: vec2<f32>
941) -> VertexOutput {
942    var out: VertexOutput;
943    out.uv = vertex_uv;
944    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
945    return out;
946}
947";
948        let processor = Preprocessor::default();
949        let result_eq = processor
950            .preprocess(
951                WGSL,
952                &[("TEXTURE".to_string(), ShaderDefValue::Bool(true))].into(),
953            )
954            .unwrap();
955        assert_eq!(result_eq.preprocessed_source, EXPECTED_EQ);
956
957        let result_neq = processor
958            .preprocess(
959                WGSL,
960                &[("TEXTURE".to_string(), ShaderDefValue::Bool(false))].into(),
961            )
962            .unwrap();
963        assert_eq!(result_neq.preprocessed_source, EXPECTED_NEQ);
964    }
965
966    #[test]
967    fn process_shader_def_not_equal_bool() {
968        #[rustfmt::skip]
969        const WGSL: &str = r"
970struct View {
971    view_proj: mat4x4<f32>,
972    world_position: vec3<f32>,
973};
974@group(0) @binding(0)
975var<uniform> view: View;
976#if TEXTURE != false
977@group(1) @binding(0)
978var sprite_texture: texture_2d<f32>;
979#endif
980struct VertexOutput {
981    @location(0) uv: vec2<f32>,
982    @builtin(position) position: vec4<f32>,
983};
984@vertex
985fn vertex(
986    @location(0) vertex_position: vec3<f32>,
987    @location(1) vertex_uv: vec2<f32>
988) -> VertexOutput {
989    var out: VertexOutput;
990    out.uv = vertex_uv;
991    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
992    return out;
993}
994";
995
996        #[rustfmt::skip]
997        const EXPECTED_EQ: &str = r"
998struct View {
999    view_proj: mat4x4<f32>,
1000    world_position: vec3<f32>,
1001};
1002@group(0) @binding(0)
1003var<uniform> view: View;
1004                    
1005@group(1) @binding(0)
1006var sprite_texture: texture_2d<f32>;
1007      
1008struct VertexOutput {
1009    @location(0) uv: vec2<f32>,
1010    @builtin(position) position: vec4<f32>,
1011};
1012@vertex
1013fn vertex(
1014    @location(0) vertex_position: vec3<f32>,
1015    @location(1) vertex_uv: vec2<f32>
1016) -> VertexOutput {
1017    var out: VertexOutput;
1018    out.uv = vertex_uv;
1019    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1020    return out;
1021}
1022";
1023
1024        #[rustfmt::skip]
1025        const EXPECTED_NEQ: &str = r"
1026struct View {
1027    view_proj: mat4x4<f32>,
1028    world_position: vec3<f32>,
1029};
1030@group(0) @binding(0)
1031var<uniform> view: View;
1032                    
1033                     
1034                                    
1035      
1036struct VertexOutput {
1037    @location(0) uv: vec2<f32>,
1038    @builtin(position) position: vec4<f32>,
1039};
1040@vertex
1041fn vertex(
1042    @location(0) vertex_position: vec3<f32>,
1043    @location(1) vertex_uv: vec2<f32>
1044) -> VertexOutput {
1045    var out: VertexOutput;
1046    out.uv = vertex_uv;
1047    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1048    return out;
1049}
1050";
1051        let processor = Preprocessor::default();
1052        let result_eq = processor
1053            .preprocess(
1054                WGSL,
1055                &[("TEXTURE".to_string(), ShaderDefValue::Bool(true))].into(),
1056            )
1057            .unwrap();
1058        assert_eq!(result_eq.preprocessed_source, EXPECTED_EQ);
1059
1060        let result_neq = processor
1061            .preprocess(
1062                WGSL,
1063                &[("TEXTURE".to_string(), ShaderDefValue::Bool(false))].into(),
1064            )
1065            .unwrap();
1066        assert_eq!(result_neq.preprocessed_source, EXPECTED_NEQ);
1067
1068        let result_missing = processor.preprocess(WGSL, &[].into());
1069        let expected_err: Result<
1070            (Option<String>, String, Vec<ImportDefWithOffset>),
1071            ComposerErrorInner,
1072        > = Err(ComposerErrorInner::UnknownShaderDef {
1073            pos: 124,
1074            shader_def_name: "TEXTURE".to_string(),
1075        });
1076        assert_eq!(format!("{result_missing:?}"), format!("{expected_err:?}"),);
1077
1078        let result_wrong_type = processor.preprocess(
1079            WGSL,
1080            &[("TEXTURE".to_string(), ShaderDefValue::Int(7))].into(),
1081        );
1082
1083        let expected_err: Result<
1084            (Option<String>, String, Vec<ImportDefWithOffset>),
1085            ComposerErrorInner,
1086        > = Err(ComposerErrorInner::InvalidShaderDefComparisonValue {
1087            pos: 124,
1088            shader_def_name: "TEXTURE".to_string(),
1089            expected: "int".to_string(),
1090            value: "false".to_string(),
1091        });
1092        assert_eq!(
1093            format!("{result_wrong_type:?}"),
1094            format!("{expected_err:?}"),
1095        );
1096    }
1097
1098    #[test]
1099    fn process_shader_def_replace() {
1100        #[rustfmt::skip]
1101        const WGSL: &str = r"
1102struct View {
1103    view_proj: mat4x4<f32>,
1104    world_position: vec3<f32>,
1105};
1106@group(0) @binding(0)
1107var<uniform> view: View;
1108struct VertexOutput {
1109    @location(0) uv: vec2<f32>,
1110    @builtin(position) position: vec4<f32>,
1111};
1112@vertex
1113fn vertex(
1114    @location(0) vertex_position: vec3<f32>,
1115    @location(1) vertex_uv: vec2<f32>
1116) -> VertexOutput {
1117    var out: VertexOutput;
1118    out.uv = vertex_uv;
1119    var a: i32 = #FIRST_VALUE;
1120    var b: i32 = #FIRST_VALUE * #SECOND_VALUE;
1121    var c: i32 = #MISSING_VALUE;
1122    var d: bool = #BOOL_VALUE;
1123    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1124    return out;
1125}
1126";
1127
1128        #[rustfmt::skip]
1129        const EXPECTED_REPLACED: &str = r"
1130struct View {
1131    view_proj: mat4x4<f32>,
1132    world_position: vec3<f32>,
1133};
1134@group(0) @binding(0)
1135var<uniform> view: View;
1136struct VertexOutput {
1137    @location(0) uv: vec2<f32>,
1138    @builtin(position) position: vec4<f32>,
1139};
1140@vertex
1141fn vertex(
1142    @location(0) vertex_position: vec3<f32>,
1143    @location(1) vertex_uv: vec2<f32>
1144) -> VertexOutput {
1145    var out: VertexOutput;
1146    out.uv = vertex_uv;
1147    var a: i32 = 5;           
1148    var b: i32 = 5 * 3;                       
1149    var c: i32 = #MISSING_VALUE;
1150    var d: bool = true;       
1151    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1152    return out;
1153}
1154";
1155        let processor = Preprocessor::default();
1156        let result = processor
1157            .preprocess(
1158                WGSL,
1159                &[
1160                    ("BOOL_VALUE".to_string(), ShaderDefValue::Bool(true)),
1161                    ("FIRST_VALUE".to_string(), ShaderDefValue::Int(5)),
1162                    ("SECOND_VALUE".to_string(), ShaderDefValue::Int(3)),
1163                ]
1164                .into(),
1165            )
1166            .unwrap();
1167        assert_eq!(result.preprocessed_source, EXPECTED_REPLACED);
1168    }
1169
1170    #[test]
1171    fn process_shader_define_in_shader() {
1172        #[rustfmt::skip]
1173        const WGSL: &str = r"
1174#define NOW_DEFINED
1175#ifdef NOW_DEFINED
1176defined
1177#endif
1178";
1179
1180        #[rustfmt::skip]
1181        const EXPECTED: &str = r"
1182                   
1183                  
1184defined
1185      
1186";
1187        let processor = Preprocessor::default();
1188        let PreprocessorMetaData {
1189            defines: shader_defs,
1190            ..
1191        } = processor.get_preprocessor_metadata(WGSL, true).unwrap();
1192        println!("defines: {shader_defs:?}");
1193        let result = processor.preprocess(WGSL, &shader_defs).unwrap();
1194        assert_eq!(result.preprocessed_source, EXPECTED);
1195    }
1196
1197    #[test]
1198    fn process_shader_define_in_shader_with_value() {
1199        #[rustfmt::skip]
1200        const WGSL: &str = r"
1201#define DEFUINT 1
1202#define DEFINT -1
1203#define DEFBOOL false
1204#if DEFUINT == 1
1205uint: #DEFUINT
1206#endif
1207#if DEFINT == -1
1208int: #DEFINT
1209#endif
1210#if DEFBOOL == false
1211bool: #DEFBOOL
1212#endif
1213";
1214
1215        #[rustfmt::skip]
1216        const EXPECTED: &str = r"
1217                 
1218                 
1219                     
1220                
1221uint: 1       
1222      
1223                
1224int: -1     
1225      
1226                    
1227bool: false   
1228      
1229";
1230        let processor = Preprocessor::default();
1231        let PreprocessorMetaData {
1232            defines: shader_defs,
1233            ..
1234        } = processor.get_preprocessor_metadata(WGSL, true).unwrap();
1235        println!("defines: {shader_defs:?}");
1236        let result = processor.preprocess(WGSL, &shader_defs).unwrap();
1237        assert_eq!(result.preprocessed_source, EXPECTED);
1238    }
1239
1240    #[test]
1241    fn process_shader_def_else_ifdef_ends_up_in_else() {
1242        #[rustfmt::skip]
1243        const EXPECTED: &str = r"
1244struct View {
1245    view_proj: mat4x4<f32>,
1246    world_position: vec3<f32>,
1247};
1248@group(0) @binding(0)
1249var<uniform> view: View;
1250@group(1) @binding(0)
1251var sprite_texture: texture_2d_array<f32>;
1252struct VertexOutput {
1253    @location(0) uv: vec2<f32>,
1254    @builtin(position) position: vec4<f32>,
1255};
1256@vertex
1257fn vertex(
1258    @location(0) vertex_position: vec3<f32>,
1259    @location(1) vertex_uv: vec2<f32>
1260) -> VertexOutput {
1261    var out: VertexOutput;
1262    out.uv = vertex_uv;
1263    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1264    return out;
1265}
1266";
1267        let processor = Preprocessor::default();
1268        let result = processor.preprocess(WGSL_ELSE_IFDEF, &[].into()).unwrap();
1269        assert_eq!(
1270            result
1271                .preprocessed_source
1272                .replace(" ", "")
1273                .replace("\n", "")
1274                .replace("\r", ""),
1275            EXPECTED
1276                .replace(" ", "")
1277                .replace("\n", "")
1278                .replace("\r", "")
1279        );
1280    }
1281
1282    #[test]
1283    fn process_shader_def_else_ifdef_no_match_and_no_fallback_else() {
1284        #[rustfmt::skip]
1285        const WGSL_ELSE_IFDEF_NO_ELSE_FALLBACK: &str = r"
1286struct View {
1287    view_proj: mat4x4<f32>,
1288    world_position: vec3<f32>,
1289};
1290@group(0) @binding(0)
1291var<uniform> view: View;
1292
1293#ifdef TEXTURE
1294// Main texture
1295@group(1) @binding(0)
1296var sprite_texture: texture_2d<f32>;
1297#else ifdef OTHER_TEXTURE
1298// Other texture
1299@group(1) @binding(0)
1300var sprite_texture: texture_2d<f32>;
1301#endif
1302
1303struct VertexOutput {
1304    @location(0) uv: vec2<f32>,
1305    @builtin(position) position: vec4<f32>,
1306};
1307
1308@vertex
1309fn vertex(
1310    @location(0) vertex_position: vec3<f32>,
1311    @location(1) vertex_uv: vec2<f32>
1312) -> VertexOutput {
1313    var out: VertexOutput;
1314    out.uv = vertex_uv;
1315    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1316    return out;
1317}
1318";
1319
1320        #[rustfmt::skip]
1321    const EXPECTED: &str = r"
1322struct View {
1323    view_proj: mat4x4<f32>,
1324    world_position: vec3<f32>,
1325};
1326@group(0) @binding(0)
1327var<uniform> view: View;
1328struct VertexOutput {
1329    @location(0) uv: vec2<f32>,
1330    @builtin(position) position: vec4<f32>,
1331};
1332@vertex
1333fn vertex(
1334    @location(0) vertex_position: vec3<f32>,
1335    @location(1) vertex_uv: vec2<f32>
1336) -> VertexOutput {
1337    var out: VertexOutput;
1338    out.uv = vertex_uv;
1339    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1340    return out;
1341}
1342";
1343        let processor = Preprocessor::default();
1344        let result = processor
1345            .preprocess(WGSL_ELSE_IFDEF_NO_ELSE_FALLBACK, &[].into())
1346            .unwrap();
1347        assert_eq!(
1348            result
1349                .preprocessed_source
1350                .replace(" ", "")
1351                .replace("\n", "")
1352                .replace("\r", ""),
1353            EXPECTED
1354                .replace(" ", "")
1355                .replace("\n", "")
1356                .replace("\r", "")
1357        );
1358    }
1359
1360    #[test]
1361    fn process_shader_def_else_ifdef_ends_up_in_first_clause() {
1362        #[rustfmt::skip]
1363    const EXPECTED: &str = r"
1364struct View {
1365    view_proj: mat4x4<f32>,
1366    world_position: vec3<f32>,
1367};
1368@group(0) @binding(0)
1369var<uniform> view: View;
1370              
1371// Main texture
1372@group(1) @binding(0)
1373var sprite_texture: texture_2d<f32>;
1374                          
1375struct VertexOutput {
1376    @location(0) uv: vec2<f32>,
1377    @builtin(position) position: vec4<f32>,
1378};
1379
1380@vertex
1381fn vertex(
1382    @location(0) vertex_position: vec3<f32>,
1383    @location(1) vertex_uv: vec2<f32>
1384) -> VertexOutput {
1385    var out: VertexOutput;
1386    out.uv = vertex_uv;
1387    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1388    return out;
1389}
1390";
1391        let processor = Preprocessor::default();
1392        let result = processor
1393            .preprocess(
1394                WGSL_ELSE_IFDEF,
1395                &[("TEXTURE".to_string(), ShaderDefValue::Bool(true))].into(),
1396            )
1397            .unwrap();
1398        assert_eq!(
1399            result
1400                .preprocessed_source
1401                .replace(" ", "")
1402                .replace("\n", "")
1403                .replace("\r", ""),
1404            EXPECTED
1405                .replace(" ", "")
1406                .replace("\n", "")
1407                .replace("\r", "")
1408        );
1409    }
1410
1411    #[test]
1412    fn process_shader_def_else_ifdef_ends_up_in_second_clause() {
1413        #[rustfmt::skip]
1414    const EXPECTED: &str = r"
1415struct View {
1416    view_proj: mat4x4<f32>,
1417    world_position: vec3<f32>,
1418};
1419@group(0) @binding(0)
1420var<uniform> view: View;
1421// Second texture
1422@group(1) @binding(0)
1423var sprite_texture: texture_2d<f32>;
1424struct VertexOutput {
1425    @location(0) uv: vec2<f32>,
1426    @builtin(position) position: vec4<f32>,
1427};
1428@vertex
1429fn vertex(
1430    @location(0) vertex_position: vec3<f32>,
1431    @location(1) vertex_uv: vec2<f32>
1432) -> VertexOutput {
1433    var out: VertexOutput;
1434    out.uv = vertex_uv;
1435    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1436    return out;
1437}
1438";
1439        let processor = Preprocessor::default();
1440        let result = processor
1441            .preprocess(
1442                WGSL_ELSE_IFDEF,
1443                &[("SECOND_TEXTURE".to_string(), ShaderDefValue::Bool(true))].into(),
1444            )
1445            .unwrap();
1446        assert_eq!(
1447            result
1448                .preprocessed_source
1449                .replace(" ", "")
1450                .replace("\n", "")
1451                .replace("\r", ""),
1452            EXPECTED
1453                .replace(" ", "")
1454                .replace("\n", "")
1455                .replace("\r", "")
1456        );
1457    }
1458
1459    #[test]
1460    fn process_shader_def_else_ifdef_ends_up_in_third_clause() {
1461        #[rustfmt::skip]
1462    const EXPECTED: &str = r"
1463struct View {
1464    view_proj: mat4x4<f32>,
1465    world_position: vec3<f32>,
1466};
1467@group(0) @binding(0)
1468var<uniform> view: View;
1469// Third texture
1470@group(1) @binding(0)
1471var sprite_texture: texture_2d<f32>;
1472struct VertexOutput {
1473    @location(0) uv: vec2<f32>,
1474    @builtin(position) position: vec4<f32>,
1475};
1476@vertex
1477fn vertex(
1478    @location(0) vertex_position: vec3<f32>,
1479    @location(1) vertex_uv: vec2<f32>
1480) -> VertexOutput {
1481    var out: VertexOutput;
1482    out.uv = vertex_uv;
1483    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1484    return out;
1485}
1486";
1487        let processor = Preprocessor::default();
1488        let result = processor
1489            .preprocess(
1490                WGSL_ELSE_IFDEF,
1491                &[("THIRD_TEXTURE".to_string(), ShaderDefValue::Bool(true))].into(),
1492            )
1493            .unwrap();
1494        assert_eq!(
1495            result
1496                .preprocessed_source
1497                .replace(" ", "")
1498                .replace("\n", "")
1499                .replace("\r", ""),
1500            EXPECTED
1501                .replace(" ", "")
1502                .replace("\n", "")
1503                .replace("\r", "")
1504        );
1505    }
1506
1507    #[test]
1508    fn process_shader_def_else_ifdef_only_accepts_one_valid_else_ifdef() {
1509        #[rustfmt::skip]
1510    const EXPECTED: &str = r"
1511struct View {
1512    view_proj: mat4x4<f32>,
1513    world_position: vec3<f32>,
1514};
1515@group(0) @binding(0)
1516var<uniform> view: View;
1517// Second texture
1518@group(1) @binding(0)
1519var sprite_texture: texture_2d<f32>;
1520struct VertexOutput {
1521    @location(0) uv: vec2<f32>,
1522    @builtin(position) position: vec4<f32>,
1523};
1524@vertex
1525fn vertex(
1526    @location(0) vertex_position: vec3<f32>,
1527    @location(1) vertex_uv: vec2<f32>
1528) -> VertexOutput {
1529    var out: VertexOutput;
1530    out.uv = vertex_uv;
1531    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
1532    return out;
1533}
1534";
1535        let processor = Preprocessor::default();
1536        let result = processor
1537            .preprocess(
1538                WGSL_ELSE_IFDEF,
1539                &[
1540                    ("SECOND_TEXTURE".to_string(), ShaderDefValue::Bool(true)),
1541                    ("THIRD_TEXTURE".to_string(), ShaderDefValue::Bool(true)),
1542                ]
1543                .into(),
1544            )
1545            .unwrap();
1546        assert_eq!(
1547            result
1548                .preprocessed_source
1549                .replace(" ", "")
1550                .replace("\n", "")
1551                .replace("\r", ""),
1552            EXPECTED
1553                .replace(" ", "")
1554                .replace("\n", "")
1555                .replace("\r", "")
1556        );
1557    }
1558
1559    #[test]
1560    fn process_shader_def_else_ifdef_complicated_nesting() {
1561        // Test some nesting including #else ifdef statements
1562        // 1. Enter an #else ifdef
1563        // 2. Then enter an #else
1564        // 3. Then enter another #else ifdef
1565
1566        #[rustfmt::skip]
1567        const WGSL_COMPLICATED_ELSE_IFDEF: &str = r"
1568#ifdef NOT_DEFINED
1569// not defined
1570#else ifdef IS_DEFINED
1571// defined 1
1572#ifdef NOT_DEFINED
1573// not defined
1574#else
1575// should be here
1576#ifdef NOT_DEFINED
1577// not defined
1578#else ifdef ALSO_NOT_DEFINED
1579// not defined
1580#else ifdef IS_DEFINED
1581// defined 2
1582#endif
1583#endif
1584#endif
1585";
1586
1587        #[rustfmt::skip]
1588        const EXPECTED: &str = r"
1589// defined 1
1590// should be here
1591// defined 2
1592";
1593        let processor = Preprocessor::default();
1594        let result = processor
1595            .preprocess(
1596                WGSL_COMPLICATED_ELSE_IFDEF,
1597                &[("IS_DEFINED".to_string(), ShaderDefValue::Bool(true))].into(),
1598            )
1599            .unwrap();
1600        assert_eq!(
1601            result
1602                .preprocessed_source
1603                .replace(" ", "")
1604                .replace("\n", "")
1605                .replace("\r", ""),
1606            EXPECTED
1607                .replace(" ", "")
1608                .replace("\n", "")
1609                .replace("\r", "")
1610        );
1611    }
1612
1613    #[test]
1614    fn process_shader_def_else_ifndef() {
1615        #[rustfmt::skip]
1616        const INPUT: &str = r"
1617#ifdef NOT_DEFINED
1618fail 1
1619#else ifdef ALSO_NOT_DEFINED
1620fail 2
1621#else ifndef ALSO_ALSO_NOT_DEFINED
1622ok
1623#else
1624fail 3
1625#endif
1626";
1627
1628        const EXPECTED: &str = r"ok";
1629        let processor = Preprocessor::default();
1630        let result = processor.preprocess(INPUT, &[].into()).unwrap();
1631        assert_eq!(
1632            result
1633                .preprocessed_source
1634                .replace(" ", "")
1635                .replace("\n", "")
1636                .replace("\r", ""),
1637            EXPECTED
1638                .replace(" ", "")
1639                .replace("\n", "")
1640                .replace("\r", "")
1641        );
1642    }
1643
1644    #[test]
1645    fn process_shader_def_else_if() {
1646        #[rustfmt::skip]
1647        const INPUT: &str = r"
1648#ifdef NOT_DEFINED
1649fail 1
1650#else if x == 1
1651fail 2
1652#else if x == 2
1653ok
1654#else
1655fail 3
1656#endif
1657";
1658
1659        const EXPECTED: &str = r"ok";
1660        let processor = Preprocessor::default();
1661        let result = processor
1662            .preprocess(INPUT, &[("x".to_owned(), ShaderDefValue::Int(2))].into())
1663            .unwrap();
1664        assert_eq!(
1665            result
1666                .preprocessed_source
1667                .replace(" ", "")
1668                .replace("\n", "")
1669                .replace("\r", ""),
1670            EXPECTED
1671                .replace(" ", "")
1672                .replace("\n", "")
1673                .replace("\r", "")
1674        );
1675    }
1676}