Skip to main content

nwnrs_nwscript/
preprocess.rs

1use std::{
2    collections::{HashMap, HashSet},
3    error::Error,
4    fmt,
5};
6
7use crate::{
8    Keyword, LexerError, ScriptResolver, SourceError, SourceFile, SourceId, SourceLoadOptions,
9    SourceMap, Token, TokenKind, lex_source,
10};
11
12/// One include relationship discovered while traversing source files.
13#[derive(#[automatically_derived]
impl ::core::fmt::Debug for IncludeEdge {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f, "IncludeEdge",
            "from", &self.from, "to", &self.to, "include_name",
            &&self.include_name)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for IncludeEdge {
    #[inline]
    fn clone(&self) -> IncludeEdge {
        IncludeEdge {
            from: ::core::clone::Clone::clone(&self.from),
            to: ::core::clone::Clone::clone(&self.to),
            include_name: ::core::clone::Clone::clone(&self.include_name),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for IncludeEdge {
    #[inline]
    fn eq(&self, other: &IncludeEdge) -> bool {
        self.from == other.from && self.to == other.to &&
            self.include_name == other.include_name
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for IncludeEdge {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<SourceId>;
        let _: ::core::cmp::AssertParamIsEq<String>;
    }
}Eq)]
14pub struct IncludeEdge {
15    /// Including file.
16    pub from:         SourceId,
17    /// Included file.
18    pub to:           SourceId,
19    /// Include string as it appeared in the source file.
20    pub include_name: String,
21}
22
23/// One loaded root script plus all transitively discovered include files.
24#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SourceBundle {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field4_finish(f, "SourceBundle",
            "source_map", &self.source_map, "root_id", &self.root_id,
            "source_order", &self.source_order, "include_edges",
            &&self.include_edges)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SourceBundle {
    #[inline]
    fn clone(&self) -> SourceBundle {
        SourceBundle {
            source_map: ::core::clone::Clone::clone(&self.source_map),
            root_id: ::core::clone::Clone::clone(&self.root_id),
            source_order: ::core::clone::Clone::clone(&self.source_order),
            include_edges: ::core::clone::Clone::clone(&self.include_edges),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for SourceBundle {
    #[inline]
    fn eq(&self, other: &SourceBundle) -> bool {
        self.source_map == other.source_map && self.root_id == other.root_id
                && self.source_order == other.source_order &&
            self.include_edges == other.include_edges
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SourceBundle {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<SourceMap>;
        let _: ::core::cmp::AssertParamIsEq<SourceId>;
        let _: ::core::cmp::AssertParamIsEq<Vec<SourceId>>;
        let _: ::core::cmp::AssertParamIsEq<Vec<IncludeEdge>>;
    }
}Eq)]
25pub struct SourceBundle {
26    /// All loaded source files.
27    pub source_map:    SourceMap,
28    /// The root script file requested by the caller.
29    pub root_id:       SourceId,
30    /// Source ids in first-load order.
31    pub source_order:  Vec<SourceId>,
32    /// Include relationships observed during scanning.
33    pub include_edges: Vec<IncludeEdge>,
34}
35
36/// One object-like `#define` captured during preprocessing.
37#[derive(#[automatically_derived]
impl ::core::fmt::Debug for MacroDefinition {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field4_finish(f,
            "MacroDefinition", "name", &self.name, "replacement",
            &self.replacement, "source_id", &self.source_id, "line",
            &&self.line)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for MacroDefinition {
    #[inline]
    fn clone(&self) -> MacroDefinition {
        MacroDefinition {
            name: ::core::clone::Clone::clone(&self.name),
            replacement: ::core::clone::Clone::clone(&self.replacement),
            source_id: ::core::clone::Clone::clone(&self.source_id),
            line: ::core::clone::Clone::clone(&self.line),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for MacroDefinition {
    #[inline]
    fn eq(&self, other: &MacroDefinition) -> bool {
        self.name == other.name && self.replacement == other.replacement &&
                self.source_id == other.source_id && self.line == other.line
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for MacroDefinition {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<String>;
        let _: ::core::cmp::AssertParamIsEq<Vec<Token>>;
        let _: ::core::cmp::AssertParamIsEq<SourceId>;
        let _: ::core::cmp::AssertParamIsEq<usize>;
    }
}Eq)]
38pub struct MacroDefinition {
39    /// Macro name.
40    pub name:        String,
41    /// Replacement tokens captured from the define line.
42    pub replacement: Vec<Token>,
43    /// File where the macro was defined.
44    pub source_id:   SourceId,
45    /// One-based source line of the define.
46    pub line:        usize,
47}
48
49/// One preprocessed token stream plus the macros captured while producing it.
50#[derive(#[automatically_derived]
impl ::core::fmt::Debug for PreprocessedSource {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "PreprocessedSource", "tokens", &self.tokens, "defines",
            &&self.defines)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for PreprocessedSource {
    #[inline]
    fn clone(&self) -> PreprocessedSource {
        PreprocessedSource {
            tokens: ::core::clone::Clone::clone(&self.tokens),
            defines: ::core::clone::Clone::clone(&self.defines),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for PreprocessedSource {
    #[inline]
    fn eq(&self, other: &PreprocessedSource) -> bool {
        self.tokens == other.tokens && self.defines == other.defines
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for PreprocessedSource {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<Vec<Token>>;
        let _: ::core::cmp::AssertParamIsEq<Vec<MacroDefinition>>;
    }
}Eq)]
51pub struct PreprocessedSource {
52    /// Tokens after include traversal and object-like macro expansion.
53    pub tokens:  Vec<Token>,
54    /// Macro definitions in encounter order, with later redefinitions included.
55    pub defines: Vec<MacroDefinition>,
56}
57
58/// Errors returned while scanning include directives across multiple files.
59#[derive(#[automatically_derived]
impl ::core::fmt::Debug for PreprocessError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            PreprocessError::Source(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Source",
                    &__self_0),
            PreprocessError::Lex(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Lex",
                    &__self_0),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for PreprocessError {
    #[inline]
    fn clone(&self) -> PreprocessError {
        match self {
            PreprocessError::Source(__self_0) =>
                PreprocessError::Source(::core::clone::Clone::clone(__self_0)),
            PreprocessError::Lex(__self_0) =>
                PreprocessError::Lex(::core::clone::Clone::clone(__self_0)),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for PreprocessError {
    #[inline]
    fn eq(&self, other: &PreprocessError) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr &&
            match (self, other) {
                (PreprocessError::Source(__self_0),
                    PreprocessError::Source(__arg1_0)) => __self_0 == __arg1_0,
                (PreprocessError::Lex(__self_0),
                    PreprocessError::Lex(__arg1_0)) => __self_0 == __arg1_0,
                _ => unsafe { ::core::intrinsics::unreachable() }
            }
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for PreprocessError {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<SourceError>;
        let _: ::core::cmp::AssertParamIsEq<LexerError>;
    }
}Eq)]
60pub enum PreprocessError {
61    /// Source resolution or load failure.
62    Source(SourceError),
63    /// Lexing failure while scanning include directives.
64    Lex(LexerError),
65}
66
67impl fmt::Display for PreprocessError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            Self::Source(error) => error.fmt(f),
71            Self::Lex(error) => error.fmt(f),
72        }
73    }
74}
75
76impl Error for PreprocessError {}
77
78impl From<SourceError> for PreprocessError {
79    fn from(value: SourceError) -> Self {
80        Self::Source(value)
81    }
82}
83
84impl From<LexerError> for PreprocessError {
85    fn from(value: LexerError) -> Self {
86        Self::Lex(value)
87    }
88}
89
90/// Loads a root script and recursively discovers its `#include` dependencies.
91///
92/// # Errors
93///
94/// Returns [`PreprocessError`] if any script cannot be resolved or loaded.
95pub fn load_source_bundle<R: ScriptResolver + ?Sized>(
96    resolver: &R,
97    root_name: &str,
98    options: SourceLoadOptions,
99) -> Result<SourceBundle, PreprocessError> {
100    let mut loader = SourceBundleLoader::new(resolver, options);
101    let root_id = loader.load_script(root_name)?;
102    Ok(SourceBundle {
103        source_map: loader.source_map,
104        root_id,
105        source_order: loader.source_order,
106        include_edges: loader.include_edges,
107    })
108}
109
110/// Preprocesses one already-loaded source bundle into a token stream.
111///
112/// # Errors
113///
114/// Returns [`PreprocessError`] if macro expansion or include resolution fails.
115pub fn preprocess_source_bundle(
116    bundle: &SourceBundle,
117) -> Result<PreprocessedSource, PreprocessError> {
118    let mut preprocessor = BundlePreprocessor::new(bundle);
119    preprocessor.expand_source(bundle.root_id)?;
120    preprocessor.finish(bundle.root_id)
121}
122
123struct SourceBundleLoader<'a, R: ScriptResolver + ?Sized> {
124    resolver:      &'a R,
125    options:       SourceLoadOptions,
126    source_map:    SourceMap,
127    source_order:  Vec<SourceId>,
128    include_edges: Vec<IncludeEdge>,
129    active_stack:  Vec<String>,
130}
131
132impl<'a, R: ScriptResolver + ?Sized> SourceBundleLoader<'a, R> {
133    fn new(resolver: &'a R, options: SourceLoadOptions) -> Self {
134        Self {
135            resolver,
136            options,
137            source_map: SourceMap::new(),
138            source_order: Vec::new(),
139            include_edges: Vec::new(),
140            active_stack: Vec::new(),
141        }
142    }
143
144    fn load_script(&mut self, script_name: &str) -> Result<SourceId, PreprocessError> {
145        if let Some(existing) = self.source_map.get_by_name(script_name) {
146            return Ok(existing.id);
147        }
148        if self.active_stack.len() >= self.options.max_include_depth {
149            return Err(SourceError::include_too_many_levels(
150                script_name,
151                self.options.max_include_depth,
152            )
153            .into());
154        }
155
156        let contents = self
157            .resolver
158            .resolve_script_bytes(script_name, self.options.res_type)?
159            .filter(|bytes| !bytes.is_empty())
160            .ok_or_else(|| SourceError::file_not_found(script_name))?;
161
162        let normalized = script_name.to_ascii_lowercase();
163        if self.active_stack.contains(&normalized) {
164            return Err(SourceError::include_recursive(script_name).into());
165        }
166
167        let source_id = self.source_map.next_id();
168        let source_file = SourceFile::new(source_id, script_name, contents);
169        let include_names = scan_include_names(&source_file)?;
170        self.source_map.insert_file(source_file);
171        self.source_order.push(source_id);
172        self.active_stack.push(normalized);
173
174        for include_name in include_names {
175            let child_id = self.load_script(&include_name)?;
176            self.include_edges.push(IncludeEdge {
177                from: source_id,
178                to: child_id,
179                include_name,
180            });
181        }
182
183        self.active_stack.pop();
184        Ok(source_id)
185    }
186}
187
188fn scan_include_names(source_file: &SourceFile) -> Result<Vec<String>, LexerError> {
189    let tokens = lex_source(source_file)?;
190    let mut includes = Vec::new();
191    let mut index = 0;
192    while index < tokens.len() {
193        if tokens
194            .get(index)
195            .is_some_and(|token| token.kind == TokenKind::Keyword(Keyword::Include))
196            && let Some(argument) = tokens.get(index + 1)
197            && argument.kind == TokenKind::String
198        {
199            includes.push(argument.text.clone());
200            index += 2;
201            continue;
202        }
203        index += 1;
204    }
205    Ok(includes)
206}
207
208struct BundlePreprocessor<'a> {
209    bundle:         &'a SourceBundle,
210    defines:        HashMap<String, MacroDefinition>,
211    define_order:   Vec<MacroDefinition>,
212    expanded_files: HashSet<SourceId>,
213    tokens:         Vec<Token>,
214}
215
216impl<'a> BundlePreprocessor<'a> {
217    fn new(bundle: &'a SourceBundle) -> Self {
218        Self {
219            bundle,
220            defines: HashMap::new(),
221            define_order: Vec::new(),
222            expanded_files: HashSet::new(),
223            tokens: Vec::new(),
224        }
225    }
226
227    fn finish(mut self, root_id: SourceId) -> Result<PreprocessedSource, PreprocessError> {
228        let root = self
229            .bundle
230            .source_map
231            .get(root_id)
232            .ok_or_else(|| SourceError::file_not_found("root"))?;
233        self.tokens.push(Token::new(
234            TokenKind::Eof,
235            crate::Span::new(root_id, root.len(), root.len()),
236            "",
237        ));
238        Ok(PreprocessedSource {
239            tokens:  self.tokens,
240            defines: self.define_order,
241        })
242    }
243
244    fn expand_source(&mut self, source_id: SourceId) -> Result<(), PreprocessError> {
245        if !self.expanded_files.insert(source_id) {
246            return Ok(());
247        }
248
249        let source = self
250            .bundle
251            .source_map
252            .get(source_id)
253            .ok_or_else(|| SourceError::file_not_found(source_id.to_string()))?;
254        let tokens = lex_source(source)?;
255        let mut index = 0;
256
257        while index < tokens.len() {
258            let Some(token) = tokens.get(index) else {
259                break;
260            };
261            if token.kind == TokenKind::Eof {
262                break;
263            }
264
265            let line = token_line(source, token).ok_or_else(|| LexerError {
266                code:    crate::CompilerErrorCode::UnknownStateInCompiler,
267                span:    crate::Span::new(source.id, token.span.start, token.span.end),
268                message: "failed to resolve token line during preprocessing".to_string(),
269            })?;
270            let line_end = next_line_index(source, &tokens, index, line);
271
272            if token.kind == TokenKind::Keyword(Keyword::Include)
273                && let Some(argument) = tokens.get(index + 1)
274                && argument.kind == TokenKind::String
275                && token_line(source, argument) == Some(line)
276                && let Some(include) = self.bundle.source_map.get_by_name(&argument.text)
277            {
278                self.expand_source(include.id)?;
279                index = line_end;
280                continue;
281            }
282
283            if token.kind == TokenKind::Keyword(Keyword::Define) {
284                let line_tokens = tokens.get(index..line_end).unwrap_or(&[]);
285                self.capture_define(source, line_tokens, line);
286                index = line_end;
287                continue;
288            }
289
290            for token in tokens.get(index..line_end).unwrap_or(&[]) {
291                self.expand_token(token, &mut Vec::new());
292            }
293            index = line_end;
294        }
295
296        Ok(())
297    }
298
299    fn capture_define(&mut self, source: &SourceFile, line_tokens: &[Token], line: usize) {
300        let Some(name_token) = line_tokens.get(1) else {
301            return;
302        };
303        if name_token.kind != TokenKind::Identifier {
304            return;
305        }
306
307        let replacement = line_tokens
308            .iter()
309            .skip(2)
310            .filter(|token| token.kind != TokenKind::Eof)
311            .cloned()
312            .collect::<Vec<_>>();
313        let definition = MacroDefinition {
314            name: name_token.text.clone(),
315            replacement,
316            source_id: source.id,
317            line,
318        };
319        self.defines
320            .insert(definition.name.clone(), definition.clone());
321        self.define_order.push(definition);
322    }
323
324    fn expand_token(&mut self, token: &Token, active: &mut Vec<String>) {
325        if token.kind == TokenKind::Identifier
326            && let Some(definition) = self.defines.get(&token.text).cloned()
327            && !active.iter().any(|name| name == &definition.name)
328        {
329            active.push(definition.name.clone());
330            for replacement in definition.replacement {
331                // Mirror the upstream identifier rewrite path: once a define is
332                // recognized, the replacement keeps its own typed token kind
333                // but is attributed to the call site for diagnostics.
334                let rewritten = Token::new(replacement.kind, token.span, replacement.text);
335                self.expand_token(&rewritten, active);
336            }
337            active.pop();
338            return;
339        }
340
341        self.tokens.push(token.clone());
342    }
343}
344
345fn token_line(source: &SourceFile, token: &Token) -> Option<usize> {
346    source
347        .location(token.span.start)
348        .map(|location| location.line)
349}
350
351fn next_line_index(source: &SourceFile, tokens: &[Token], start: usize, line: usize) -> usize {
352    let mut index = start;
353    while let Some(token) = tokens.get(index) {
354        if token.kind == TokenKind::Eof {
355            break;
356        }
357        if token_line(source, token) != Some(line) {
358            break;
359        }
360        index += 1;
361    }
362    index
363}
364
365#[cfg(test)]
366mod tests {
367    use super::{load_source_bundle, preprocess_source_bundle};
368    use crate::{CompilerErrorCode, InMemoryScriptResolver, Keyword, SourceLoadOptions, TokenKind};
369
370    fn token_pairs(preprocessed: super::PreprocessedSource) -> Vec<(TokenKind, String)> {
371        preprocessed
372            .tokens
373            .into_iter()
374            .map(|token| (token.kind, token.text))
375            .collect::<Vec<_>>()
376    }
377
378    #[test]
379    fn loads_transitive_includes_and_ignores_duplicate_files() {
380        let mut resolver = InMemoryScriptResolver::new();
381        resolver.insert_source(
382            "root",
383            r#"#include "util"
384#include "common"
385#include "util"
386void main() {}"#,
387        );
388        resolver.insert_source(
389            "util",
390            r#"#include "common"
391int UTIL = 1;"#,
392        );
393        resolver.insert_source("common", "int COMMON = 2;");
394
395        let bundle = load_source_bundle(&resolver, "root", SourceLoadOptions::default());
396        let names = bundle.ok().map(|bundle| {
397            bundle
398                .source_order
399                .iter()
400                .filter_map(|id| bundle.source_map.get(*id).map(|file| file.name.clone()))
401                .collect::<Vec<_>>()
402        });
403
404        assert_eq!(
405            names,
406            Some(vec![
407                "root".to_string(),
408                "util".to_string(),
409                "common".to_string(),
410            ])
411        );
412    }
413
414    #[test]
415    fn treats_empty_source_as_file_not_found() {
416        let mut resolver = InMemoryScriptResolver::new();
417        resolver.insert_source("root", r#"#include "empty""#);
418        resolver.insert_source("empty", "");
419
420        let error = load_source_bundle(&resolver, "root", SourceLoadOptions::default()).err();
421        let code = error.and_then(|error| match error {
422            super::PreprocessError::Source(source) => source.code(),
423            super::PreprocessError::Lex(_) => None,
424        });
425
426        assert_eq!(code, Some(CompilerErrorCode::FileNotFound));
427    }
428
429    #[test]
430    fn enforces_include_depth_limit() {
431        let mut resolver = InMemoryScriptResolver::new();
432        resolver.insert_source("root", r#"#include "a""#);
433        resolver.insert_source("a", r#"#include "b""#);
434        resolver.insert_source("b", r#"#include "c""#);
435        resolver.insert_source("c", "void c() {}");
436
437        let error = load_source_bundle(
438            &resolver,
439            "root",
440            SourceLoadOptions {
441                max_include_depth: 2,
442                ..SourceLoadOptions::default()
443            },
444        )
445        .err();
446        let code = error.and_then(|error| match error {
447            super::PreprocessError::Source(source) => source.code(),
448            super::PreprocessError::Lex(_) => None,
449        });
450
451        assert_eq!(code, Some(CompilerErrorCode::IncludeTooManyLevels));
452    }
453
454    #[test]
455    fn resolver_matches_include_names_case_insensitively() {
456        let mut resolver = InMemoryScriptResolver::new();
457        resolver.insert_source("ROOT", r#"#include "Util""#);
458        resolver.insert_source("util", "void util() {}");
459
460        let bundle = load_source_bundle(&resolver, "root", SourceLoadOptions::default());
461        let count = bundle.ok().map(|bundle| bundle.source_map.len());
462
463        assert_eq!(count, Some(2));
464    }
465
466    #[test]
467    fn preprocesses_object_like_defines_with_include_order()
468    -> Result<(), Box<dyn std::error::Error>> {
469        let mut resolver = InMemoryScriptResolver::new();
470        resolver.insert_source(
471            "root",
472            br#"#define VALUE 7
473#include "util"
474int x = VALUE;
475"#,
476        );
477        resolver.insert_source(
478            "util",
479            br#"#define PLUS +
480int y = VALUE PLUS 1;
481"#,
482        );
483
484        let bundle = load_source_bundle(&resolver, "root", SourceLoadOptions::default())?;
485        let pairs = token_pairs(preprocess_source_bundle(&bundle)?);
486
487        assert_eq!(
488            pairs,
489            vec![
490                (TokenKind::Keyword(Keyword::Int), "int".to_string()),
491                (TokenKind::Identifier, "y".to_string()),
492                (TokenKind::Assign, "=".to_string()),
493                (TokenKind::Integer, "7".to_string()),
494                (TokenKind::Plus, "+".to_string()),
495                (TokenKind::Integer, "1".to_string()),
496                (TokenKind::Semicolon, ";".to_string()),
497                (TokenKind::Keyword(Keyword::Int), "int".to_string()),
498                (TokenKind::Identifier, "x".to_string()),
499                (TokenKind::Assign, "=".to_string()),
500                (TokenKind::Integer, "7".to_string()),
501                (TokenKind::Semicolon, ";".to_string()),
502                (TokenKind::Eof, "".to_string()),
503            ]
504        );
505        Ok(())
506    }
507
508    #[test]
509    fn preprocess_define_redefinitions_use_latest_value() -> Result<(), Box<dyn std::error::Error>>
510    {
511        let mut resolver = InMemoryScriptResolver::new();
512        resolver.insert_source(
513            "root",
514            br#"#define VALUE 1
515#define VALUE 2
516int x = VALUE;
517"#,
518        );
519
520        let bundle = load_source_bundle(&resolver, "root", SourceLoadOptions::default())?;
521        let preprocessed = preprocess_source_bundle(&bundle)?;
522        let integers = preprocessed
523            .tokens
524            .into_iter()
525            .filter(|token| token.kind == TokenKind::Integer)
526            .map(|token| token.text)
527            .collect::<Vec<_>>();
528
529        assert_eq!(integers, vec!["2".to_string()]);
530        Ok(())
531    }
532
533    #[test]
534    fn chained_define_expansion_preserves_upstream_literal_token_kinds()
535    -> Result<(), Box<dyn std::error::Error>> {
536        let mut resolver = InMemoryScriptResolver::new();
537        resolver.insert_source(
538            "root",
539            br#"#define BASE 4
540#define VALUE BASE
541int x = VALUE;
542"#,
543        );
544
545        let bundle = load_source_bundle(&resolver, "root", SourceLoadOptions::default())?;
546        let pairs = token_pairs(preprocess_source_bundle(&bundle)?);
547
548        assert_eq!(
549            pairs,
550            vec![
551                (TokenKind::Keyword(Keyword::Int), "int".to_string()),
552                (TokenKind::Identifier, "x".to_string()),
553                (TokenKind::Assign, "=".to_string()),
554                (TokenKind::Integer, "4".to_string()),
555                (TokenKind::Semicolon, ";".to_string()),
556                (TokenKind::Eof, "".to_string()),
557            ]
558        );
559        Ok(())
560    }
561
562    #[test]
563    fn define_visibility_tracks_include_encounter_order() -> Result<(), Box<dyn std::error::Error>>
564    {
565        let mut resolver = InMemoryScriptResolver::new();
566        resolver.insert_source(
567            "root",
568            br#"#define VALUE 1
569#include "util"
570#define VALUE 2
571int root_value = VALUE;
572"#,
573        );
574        resolver.insert_source("util", br#"int util_value = VALUE;"#);
575
576        let bundle = load_source_bundle(&resolver, "root", SourceLoadOptions::default())?;
577        let pairs = token_pairs(preprocess_source_bundle(&bundle)?);
578
579        assert_eq!(
580            pairs,
581            vec![
582                (TokenKind::Keyword(Keyword::Int), "int".to_string()),
583                (TokenKind::Identifier, "util_value".to_string()),
584                (TokenKind::Assign, "=".to_string()),
585                (TokenKind::Integer, "1".to_string()),
586                (TokenKind::Semicolon, ";".to_string()),
587                (TokenKind::Keyword(Keyword::Int), "int".to_string()),
588                (TokenKind::Identifier, "root_value".to_string()),
589                (TokenKind::Assign, "=".to_string()),
590                (TokenKind::Integer, "2".to_string()),
591                (TokenKind::Semicolon, ";".to_string()),
592                (TokenKind::Eof, "".to_string()),
593            ]
594        );
595        Ok(())
596    }
597
598    #[test]
599    fn define_expansion_preserves_keyword_token_kinds() -> Result<(), Box<dyn std::error::Error>> {
600        let mut resolver = InMemoryScriptResolver::new();
601        resolver.insert_source(
602            "root",
603            br#"#define BAD_OBJECT OBJECT_INVALID
604object value = BAD_OBJECT;
605"#,
606        );
607
608        let bundle = load_source_bundle(&resolver, "root", SourceLoadOptions::default())?;
609        let pairs = token_pairs(preprocess_source_bundle(&bundle)?);
610
611        assert_eq!(
612            pairs,
613            vec![
614                (TokenKind::Keyword(Keyword::Object), "object".to_string()),
615                (TokenKind::Identifier, "value".to_string()),
616                (TokenKind::Assign, "=".to_string()),
617                (
618                    TokenKind::Keyword(Keyword::ObjectInvalid),
619                    "OBJECT_INVALID".to_string(),
620                ),
621                (TokenKind::Semicolon, ";".to_string()),
622                (TokenKind::Eof, "".to_string()),
623            ]
624        );
625        Ok(())
626    }
627}