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