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