Rust · 74365 bytes Raw Blame History
1 //! Fortran-aware C-style preprocessor.
2 //!
3 //! Text-to-text transformation that runs before lexing. Handles #define,
4 //! #ifdef/#ifndef/#if/#elif/#else/#endif, #include, #undef, #error, #warning.
5 //! Aware of Fortran string literals and comments — won't expand macros inside them.
6
7 use std::collections::HashMap;
8 use std::fmt;
9 use std::path::{Path, PathBuf};
10
11 /// Configuration for the preprocessor.
12 #[derive(Debug, Clone)]
13 pub struct PreprocConfig {
14 /// Predefined macros from -D flags and built-in definitions.
15 pub defines: HashMap<String, MacroDef>,
16 /// Include search paths from -I flags.
17 pub include_paths: Vec<PathBuf>,
18 /// The source filename (for __FILE__ and error messages).
19 pub filename: String,
20 /// If true, source is fixed-form Fortran (C/* in column 1 = comment).
21 pub fixed_form: bool,
22 }
23
24 impl Default for PreprocConfig {
25 fn default() -> Self {
26 let mut defines = HashMap::new();
27 // Built-in predefined macros.
28 defines.insert("__ARMFORTAS__".into(), MacroDef::object("1"));
29 defines.insert("__ARMFORTAS_MAJOR__".into(), MacroDef::object("0"));
30 defines.insert("__ARMFORTAS_MINOR__".into(), MacroDef::object("1"));
31 defines.insert("__aarch64__".into(), MacroDef::object("1"));
32 defines.insert("__arm64__".into(), MacroDef::object("1"));
33 #[cfg(target_os = "macos")]
34 defines.insert("__APPLE__".into(), MacroDef::object("1"));
35 #[cfg(target_os = "linux")]
36 defines.insert("__linux__".into(), MacroDef::object("1"));
37
38 Self {
39 defines,
40 include_paths: Vec::new(),
41 filename: "<input>".into(),
42 fixed_form: false,
43 }
44 }
45 }
46
47 /// A macro definition.
48 #[derive(Debug, Clone)]
49 pub struct MacroDef {
50 /// For object-like macros: the replacement text.
51 /// For function-like macros: the replacement text with parameter placeholders.
52 pub body: String,
53 /// Parameter names (empty for object-like macros).
54 pub params: Vec<String>,
55 /// Whether this is a function-like macro.
56 pub is_function: bool,
57 /// Whether this is a variadic macro (accepts `...` / `__VA_ARGS__`).
58 pub is_variadic: bool,
59 }
60
61 impl MacroDef {
62 pub fn object(body: &str) -> Self {
63 Self {
64 body: body.into(),
65 params: Vec::new(),
66 is_function: false,
67 is_variadic: false,
68 }
69 }
70
71 pub fn function(params: Vec<String>, body: &str) -> Self {
72 Self {
73 body: body.into(),
74 params,
75 is_function: true,
76 is_variadic: false,
77 }
78 }
79 }
80
81 /// Preprocessor output.
82 #[derive(Debug, Clone)]
83 pub struct PreprocOutput {
84 /// The preprocessed text.
85 pub text: String,
86 /// Maps output line numbers (1-based) to original (filename, line) pairs.
87 pub source_map: Vec<SourceLoc>,
88 }
89
90 /// A source location before preprocessing.
91 #[derive(Debug, Clone)]
92 pub struct SourceLoc {
93 pub filename: String,
94 pub line: u32,
95 }
96
97 /// Preprocessor error.
98 #[derive(Debug, Clone)]
99 pub struct PreprocError {
100 pub filename: String,
101 pub line: u32,
102 pub msg: String,
103 }
104
105 impl fmt::Display for PreprocError {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 write!(f, "{}:{}: error: {}", self.filename, self.line, self.msg)
108 }
109 }
110
111 impl std::error::Error for PreprocError {}
112
113 /// Preprocess Fortran source text with given configuration.
114 pub fn preprocess(source: &str, config: &PreprocConfig) -> Result<PreprocOutput, PreprocError> {
115 let mut pp = Preprocessor::new(config);
116 pp.process(source, &config.filename)
117 }
118
119 /// Condition stack state for nested #if/#ifdef blocks.
120 #[derive(Debug, Clone, Copy)]
121 enum CondState {
122 /// Currently in a true branch, emitting output.
123 Active,
124 /// In a false branch, skipping output. Saw the directive at this level.
125 Skipping,
126 /// Already found a true branch at this level, skip rest (including #else).
127 Done,
128 /// Parent was skipping, so everything at this level is skipped regardless.
129 ParentSkipping,
130 }
131
132 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
133 enum MacroExpandMode {
134 SourceLine,
135 ConditionExpr,
136 }
137
138 struct Preprocessor {
139 defines: HashMap<String, MacroDef>,
140 include_paths: Vec<PathBuf>,
141 cond_stack: Vec<CondState>,
142 /// O(1) counter: number of non-Active levels on the stack.
143 /// `is_emitting()` is just `skip_depth == 0`.
144 skip_depth: u32,
145 /// Include depth for recursion guard.
146 include_depth: u32,
147 /// Fixed-form source mode.
148 fixed_form: bool,
149 /// #line overrides for source map reporting.
150 line_override: Option<(u32, String)>,
151 }
152
153 impl Preprocessor {
154 fn new(config: &PreprocConfig) -> Self {
155 Self {
156 defines: config.defines.clone(),
157 include_paths: config.include_paths.clone(),
158 fixed_form: config.fixed_form,
159 line_override: None,
160 cond_stack: Vec::new(),
161 skip_depth: 0,
162 include_depth: 0,
163 }
164 }
165
166 fn make_source_loc(&self, filename: &str, line: u32) -> SourceLoc {
167 if let Some((override_line, ref override_file)) = self.line_override {
168 let fname = if override_file.is_empty() {
169 filename
170 } else {
171 override_file.as_str()
172 };
173 SourceLoc {
174 filename: fname.into(),
175 line: override_line,
176 }
177 } else {
178 SourceLoc {
179 filename: filename.into(),
180 line,
181 }
182 }
183 }
184
185 fn is_emitting(&self) -> bool {
186 self.skip_depth == 0
187 }
188
189 fn process(&mut self, source: &str, filename: &str) -> Result<PreprocOutput, PreprocError> {
190 let mut output = String::new();
191 let mut source_map = Vec::new();
192 self.process_into(source, filename, &mut output, &mut source_map)?;
193
194 // Check for unterminated conditionals (only at top level).
195 if self.include_depth == 0 && !self.cond_stack.is_empty() {
196 return Err(PreprocError {
197 filename: filename.into(),
198 line: source.lines().count() as u32,
199 msg: format!(
200 "unterminated #if/#ifdef ({} level(s) still open)",
201 self.cond_stack.len()
202 ),
203 });
204 }
205
206 Ok(PreprocOutput {
207 text: output,
208 source_map,
209 })
210 }
211
212 fn process_into(
213 &mut self,
214 source: &str,
215 filename: &str,
216 output: &mut String,
217 source_map: &mut Vec<SourceLoc>,
218 ) -> Result<(), PreprocError> {
219 // Set dynamic macros.
220 self.defines.insert(
221 "__FILE__".into(),
222 MacroDef::object(&format!("\"{}\"", filename)),
223 );
224
225 let now = current_datetime();
226 self.defines.insert(
227 "__DATE__".into(),
228 MacroDef::object(&format!("\"{}\"", now.0)),
229 );
230 self.defines.insert(
231 "__TIME__".into(),
232 MacroDef::object(&format!("\"{}\"", now.1)),
233 );
234
235 // Process lines with inline backslash continuation joining,
236 // tracking original line numbers so __LINE__ and source_map are correct.
237 let raw_lines: Vec<&str> = source.lines().collect();
238 let mut i = 0;
239 while i < raw_lines.len() {
240 let orig_line_num = (i + 1) as u32; // 1-based, tracks original source line
241 let mut logical_line = String::new();
242
243 // Join backslash-continued lines (C-style).
244 while i < raw_lines.len() && raw_lines[i].ends_with('\\') {
245 logical_line.push_str(&raw_lines[i][..raw_lines[i].len() - 1]);
246 i += 1;
247 }
248 if i < raw_lines.len() {
249 logical_line.push_str(raw_lines[i]);
250 i += 1;
251 }
252
253 // Also join Fortran &-continued lines (free-form).
254 // A line ending with & in the code portion (not inside strings or after !)
255 // continues on the next line.
256 // Skip for preprocessor directives (#if, #define, etc.) where ! and &
257 // have C semantics, not Fortran semantics.
258 if !self.fixed_form && !logical_line.trim_start().starts_with('#') {
259 while has_trailing_continuation(&logical_line) {
260 if i < raw_lines.len() {
261 let next = raw_lines[i].trim_start();
262 if next.starts_with('#') {
263 break;
264 }
265 // F2018 6.3.2.4: comment lines and blank lines
266 // may appear between a continuation line and
267 // its successor. Skip them without breaking
268 // the continuation.
269 if next.starts_with('!') || next.is_empty() {
270 i += 1;
271 continue;
272 }
273 let amp_pos = find_code_trailing_ampersand(&logical_line).unwrap();
274 logical_line.truncate(amp_pos);
275 let next = next.strip_prefix('&').unwrap_or(next);
276 logical_line.push_str(next);
277 i += 1;
278 } else {
279 break;
280 }
281 }
282 }
283
284 // Update __LINE__ to the original starting line of this logical line.
285 self.defines.insert(
286 "__LINE__".into(),
287 MacroDef::object(&orig_line_num.to_string()),
288 );
289
290 // Fixed-form: C, c, or * in column 1 is a comment line.
291 if self.fixed_form {
292 let first = logical_line.as_bytes().first().copied().unwrap_or(0);
293 if first == b'C' || first == b'c' || first == b'*' {
294 // Comment line — emit as-is without expansion.
295 if self.is_emitting() {
296 output.push_str(&logical_line);
297 }
298 output.push('\n');
299 source_map.push(self.make_source_loc(filename, orig_line_num));
300 continue;
301 }
302 }
303
304 let trimmed = logical_line.trim_start();
305
306 // Preprocessor directives: # in column 1 (or after whitespace in free-form).
307 if trimmed.starts_with('#') {
308 self.process_directive(trimmed, filename, orig_line_num, output, source_map)?;
309 output.push('\n');
310 source_map.push(self.make_source_loc(filename, orig_line_num));
311 continue;
312 }
313
314 if self.is_emitting() {
315 let expanded = self.expand_macros(&logical_line);
316 output.push_str(&expanded);
317 }
318 output.push('\n');
319 source_map.push(self.make_source_loc(filename, orig_line_num));
320 }
321
322 Ok(())
323 }
324
325 fn process_directive(
326 &mut self,
327 line: &str,
328 filename: &str,
329 line_num: u32,
330 output: &mut String,
331 source_map: &mut Vec<SourceLoc>,
332 ) -> Result<(), PreprocError> {
333 let rest = line[1..].trim_start(); // skip '#' and whitespace
334 let (directive, args) = split_first_word(rest);
335
336 // Conditionals must be processed even when skipping.
337 match directive {
338 "ifdef" => return self.do_ifdef(args, false),
339 "ifndef" => return self.do_ifdef(args, true),
340 "if" => return self.do_if(args, filename, line_num),
341 "elif" => return self.do_elif(args, filename, line_num),
342 "else" => return self.do_else(filename, line_num),
343 "endif" => return self.do_endif(filename, line_num),
344 _ => {}
345 }
346
347 // All other directives are only processed when emitting.
348 if !self.is_emitting() {
349 return Ok(());
350 }
351
352 match directive {
353 "define" => self.do_define(args),
354 "undef" => self.do_undef(args),
355 "include" => self.do_include(args, filename, line_num, output, source_map),
356 "error" => Err(PreprocError {
357 filename: filename.into(),
358 line: line_num,
359 msg: format!("#error {}", args),
360 }),
361 "warning" => {
362 eprintln!("{}:{}: warning: #warning {}", filename, line_num, args);
363 Ok(())
364 }
365 "line" => self.do_line(args, filename, line_num),
366 "" => Ok(()), // bare # is allowed (null directive)
367 _ => Ok(()), // unknown directives are ignored (like #pragma)
368 }
369 }
370
371 // ---- Conditional directive helpers (maintain skip_depth counter) ----
372
373 fn push_cond(&mut self, state: CondState) {
374 if !matches!(state, CondState::Active) {
375 self.skip_depth += 1;
376 }
377 self.cond_stack.push(state);
378 }
379
380 fn set_top_cond(&mut self, new: CondState) {
381 let old = *self.cond_stack.last().unwrap();
382 let was_skip = !matches!(old, CondState::Active);
383 let now_skip = !matches!(new, CondState::Active);
384 match (was_skip, now_skip) {
385 (false, true) => self.skip_depth += 1,
386 (true, false) => self.skip_depth -= 1,
387 _ => {}
388 }
389 *self.cond_stack.last_mut().unwrap() = new;
390 }
391
392 fn pop_cond(&mut self) -> Option<CondState> {
393 let popped = self.cond_stack.pop()?;
394 if !matches!(popped, CondState::Active) {
395 self.skip_depth -= 1;
396 }
397 Some(popped)
398 }
399
400 // ---- Conditional directives ----
401
402 fn do_ifdef(&mut self, args: &str, negate: bool) -> Result<(), PreprocError> {
403 let name = args.split_whitespace().next().unwrap_or("");
404 if !self.is_emitting() {
405 self.push_cond(CondState::ParentSkipping);
406 return Ok(());
407 }
408 let defined = self.defines.contains_key(name);
409 let condition = if negate { !defined } else { defined };
410 self.push_cond(if condition {
411 CondState::Active
412 } else {
413 CondState::Skipping
414 });
415 Ok(())
416 }
417
418 fn do_if(&mut self, args: &str, filename: &str, line_num: u32) -> Result<(), PreprocError> {
419 if !self.is_emitting() {
420 self.push_cond(CondState::ParentSkipping);
421 return Ok(());
422 }
423 let val = self.eval_condition(args, filename, line_num)?;
424 self.push_cond(if val {
425 CondState::Active
426 } else {
427 CondState::Skipping
428 });
429 Ok(())
430 }
431
432 fn do_elif(&mut self, args: &str, filename: &str, line_num: u32) -> Result<(), PreprocError> {
433 match self.cond_stack.last().copied() {
434 None => Err(PreprocError {
435 filename: filename.into(),
436 line: line_num,
437 msg: "#elif without matching #if".into(),
438 }),
439 Some(CondState::ParentSkipping) => Ok(()),
440 Some(CondState::Active) => {
441 self.set_top_cond(CondState::Done);
442 Ok(())
443 }
444 Some(CondState::Done) => Ok(()),
445 Some(CondState::Skipping) => {
446 let val = self.eval_condition(args, filename, line_num)?;
447 self.set_top_cond(if val {
448 CondState::Active
449 } else {
450 CondState::Skipping
451 });
452 Ok(())
453 }
454 }
455 }
456
457 fn do_else(&mut self, filename: &str, line_num: u32) -> Result<(), PreprocError> {
458 match self.cond_stack.last().copied() {
459 None => Err(PreprocError {
460 filename: filename.into(),
461 line: line_num,
462 msg: "#else without matching #if".into(),
463 }),
464 Some(CondState::ParentSkipping) => Ok(()),
465 Some(CondState::Active) => {
466 self.set_top_cond(CondState::Done);
467 Ok(())
468 }
469 Some(CondState::Done) => Ok(()),
470 Some(CondState::Skipping) => {
471 self.set_top_cond(CondState::Active);
472 Ok(())
473 }
474 }
475 }
476
477 fn do_endif(&mut self, filename: &str, line_num: u32) -> Result<(), PreprocError> {
478 if self.pop_cond().is_none() {
479 return Err(PreprocError {
480 filename: filename.into(),
481 line: line_num,
482 msg: "#endif without matching #if".into(),
483 });
484 }
485 Ok(())
486 }
487
488 // ---- #define / #undef ----
489
490 fn do_define(&mut self, args: &str) -> Result<(), PreprocError> {
491 let args = args.trim();
492 if args.is_empty() {
493 return Ok(());
494 }
495
496 // Check for function-like macro: NAME(params...) body
497 if let Some(paren_pos) = args.find('(') {
498 let name = &args[..paren_pos];
499 if !name.contains(' ') {
500 // Function-like macro.
501 let rest = &args[paren_pos + 1..];
502 if let Some(close) = rest.find(')') {
503 let params_str = &rest[..close];
504 let mut params: Vec<String> = params_str
505 .split(',')
506 .map(|p| p.trim().to_string())
507 .filter(|p| !p.is_empty())
508 .collect();
509 // Handle variadic: last param is "..." → replace with __VA_ARGS__
510 let is_variadic = params.last().is_some_and(|p| p == "...");
511 if is_variadic {
512 params.pop();
513 }
514 let body = rest[close + 1..].trim();
515 let mut def = MacroDef::function(params, body);
516 def.is_variadic = is_variadic;
517 self.defines.insert(name.into(), def);
518 return Ok(());
519 }
520 }
521 }
522
523 // Object-like macro: NAME body or NAME (empty body = "1")
524 let (name, body) = split_first_word(args);
525 // Empty #define has empty body (not "1"). #ifdef uses contains_key, not body value.
526 self.defines.insert(name.into(), MacroDef::object(body));
527 Ok(())
528 }
529
530 fn do_undef(&mut self, args: &str) -> Result<(), PreprocError> {
531 let name = args.split_whitespace().next().unwrap_or("");
532 self.defines.remove(name);
533 Ok(())
534 }
535
536 // ---- #line ----
537
538 fn do_line(&mut self, args: &str, _filename: &str, _line_num: u32) -> Result<(), PreprocError> {
539 let args = args.trim();
540 let (line_str, rest) = split_first_word(args);
541 if let Ok(line_num) = line_str.parse::<u32>() {
542 let filename = if !rest.is_empty() {
543 // Strip quotes from filename.
544 rest.trim_matches('"').to_string()
545 } else {
546 String::new()
547 };
548 self.line_override = Some((line_num, filename));
549 }
550 Ok(())
551 }
552
553 // ---- #include ----
554
555 fn do_include(
556 &mut self,
557 args: &str,
558 filename: &str,
559 line_num: u32,
560 output: &mut String,
561 source_map: &mut Vec<SourceLoc>,
562 ) -> Result<(), PreprocError> {
563 let args = args.trim();
564 let (path_str, search_system) = if let Some(rest) = args.strip_prefix('"') {
565 let end = rest.find('"').ok_or_else(|| PreprocError {
566 filename: filename.into(),
567 line: line_num,
568 msg: "unterminated #include string".into(),
569 })?;
570 (&rest[..end], false)
571 } else if let Some(rest) = args.strip_prefix('<') {
572 let end = rest.find('>').ok_or_else(|| PreprocError {
573 filename: filename.into(),
574 line: line_num,
575 msg: "unterminated #include <path>".into(),
576 })?;
577 (&rest[..end], true)
578 } else {
579 return Err(PreprocError {
580 filename: filename.into(),
581 line: line_num,
582 msg: format!("expected \"file\" or <file> after #include, got: {}", args),
583 });
584 };
585
586 if self.include_depth >= 64 {
587 return Err(PreprocError {
588 filename: filename.into(),
589 line: line_num,
590 msg: "include depth limit exceeded (possible recursion)".into(),
591 });
592 }
593
594 // Search for the file.
595 let resolved = self
596 .resolve_include(path_str, filename, search_system)
597 .ok_or_else(|| PreprocError {
598 filename: filename.into(),
599 line: line_num,
600 msg: format!("cannot find include file: {}", path_str),
601 })?;
602
603 let content = std::fs::read_to_string(&resolved).map_err(|e| PreprocError {
604 filename: filename.into(),
605 line: line_num,
606 msg: format!("reading {}: {}", resolved.display(), e),
607 })?;
608
609 // Save __FILE__ so it's restored after the include returns.
610 let saved_file = self.defines.get("__FILE__").cloned();
611
612 self.include_depth += 1;
613 let inc_filename = resolved.to_string_lossy().into_owned();
614 self.process_into(&content, &inc_filename, output, source_map)?;
615 self.include_depth -= 1;
616
617 // Restore __FILE__ to the parent's filename.
618 if let Some(saved) = saved_file {
619 self.defines.insert("__FILE__".into(), saved);
620 }
621 Ok(())
622 }
623
624 fn resolve_include(&self, path: &str, current_file: &str, system: bool) -> Option<PathBuf> {
625 // #include "file" — search relative to current file first, then include paths.
626 // #include <file> — search include paths only (system = true).
627 if !system {
628 let current_dir = Path::new(current_file).parent().unwrap_or(Path::new("."));
629 let candidate = current_dir.join(path);
630 if candidate.exists() {
631 return Some(candidate);
632 }
633 }
634
635 // Search include paths.
636 for dir in &self.include_paths {
637 let candidate = dir.join(path);
638 if candidate.exists() {
639 return Some(candidate);
640 }
641 }
642
643 None
644 }
645
646 // ---- Condition expression evaluator ----
647
648 fn eval_condition(
649 &self,
650 expr: &str,
651 filename: &str,
652 line_num: u32,
653 ) -> Result<bool, PreprocError> {
654 // Expand macros in the expression first.
655 let expanded = self.expand_condition_macros(expr);
656 // Parse and evaluate the expression.
657 eval_expr(&expanded).map_err(|msg| PreprocError {
658 filename: filename.into(),
659 line: line_num,
660 msg: format!("in #if expression: {}", msg),
661 })
662 }
663
664 /// Expand macros and `defined(NAME)` / `defined NAME` in a condition expression.
665 ///
666 /// Condition expressions share the same recursive macro engine as
667 /// ordinary source lines, but apply condition-specific semantics:
668 /// `defined` is resolved during the walk and any remaining identifiers
669 /// are rewritten to `0` at the end.
670 fn expand_condition_macros(&self, expr: &str) -> String {
671 let expanding = std::collections::HashSet::new();
672 let expanded = self.expand_macros_inner(expr, &expanding, MacroExpandMode::ConditionExpr);
673 replace_undefined_idents(&expanded)
674 }
675
676 // ---- Macro expansion in source lines ----
677
678 fn expand_macros(&self, line: &str) -> String {
679 let expanding = std::collections::HashSet::new();
680 self.expand_macros_inner(line, &expanding, MacroExpandMode::SourceLine)
681 }
682
683 fn expand_macros_inner(
684 &self,
685 line: &str,
686 expanding: &std::collections::HashSet<String>,
687 mode: MacroExpandMode,
688 ) -> String {
689 if self.defines.is_empty() {
690 return line.to_string();
691 }
692
693 let mut result = String::with_capacity(line.len());
694 let bytes = line.as_bytes();
695 let mut i = 0;
696
697 while i < bytes.len() {
698 // Skip Fortran comment (! to end of line) in source mode.
699 if mode == MacroExpandMode::SourceLine && bytes[i] == b'!' {
700 result.push_str(&line[i..]);
701 break;
702 }
703
704 // Skip string literals in source mode.
705 if mode == MacroExpandMode::SourceLine && (bytes[i] == b'\'' || bytes[i] == b'"') {
706 let quote = bytes[i];
707 result.push(quote as char);
708 i += 1;
709 while i < bytes.len() {
710 if bytes[i] == quote {
711 if i + 1 < bytes.len() && bytes[i + 1] == quote {
712 result.push(quote as char);
713 result.push(quote as char);
714 i += 2;
715 } else {
716 break;
717 }
718 } else {
719 push_utf8_char(&mut result, bytes, &mut i);
720 }
721 }
722 if i < bytes.len() {
723 result.push(quote as char);
724 i += 1;
725 }
726 continue;
727 }
728
729 // Try to match an identifier.
730 if bytes[i].is_ascii_alphabetic() || bytes[i] == b'_' {
731 let start = i;
732 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
733 i += 1;
734 }
735 let ident = &line[start..i];
736
737 if mode == MacroExpandMode::ConditionExpr && ident == "defined" {
738 let (name, new_i) = parse_defined_operand(line, i);
739 result.push_str(if self.defines.contains_key(name) {
740 "1"
741 } else {
742 "0"
743 });
744 i = new_i;
745 continue;
746 }
747
748 // Skip if this macro is currently being expanded (blue paint).
749 if expanding.contains(ident) {
750 result.push_str(ident);
751 continue;
752 }
753
754 if let Some(def) = self.defines.get(ident) {
755 if def.is_function {
756 if i < bytes.len() && bytes[i] == b'(' {
757 if let Some((expanded, new_i)) =
758 self.expand_function_macro(def, line, i)
759 {
760 // Re-expand the result with this macro marked as expanding.
761 let mut next_expanding = expanding.clone();
762 next_expanding.insert(ident.to_string());
763 result.push_str(&self.expand_macros_inner(
764 &expanded,
765 &next_expanding,
766 mode,
767 ));
768 i = new_i;
769 continue;
770 }
771 }
772 result.push_str(ident);
773 } else {
774 // Re-expand object macro body with this macro marked as expanding.
775 let mut next_expanding = expanding.clone();
776 next_expanding.insert(ident.to_string());
777 result.push_str(&self.expand_macros_inner(
778 &def.body,
779 &next_expanding,
780 mode,
781 ));
782 }
783 } else {
784 result.push_str(ident);
785 }
786 continue;
787 }
788
789 push_utf8_char(&mut result, bytes, &mut i);
790 }
791
792 result
793 }
794
795 fn expand_function_macro(
796 &self,
797 def: &MacroDef,
798 line: &str,
799 paren_start: usize,
800 ) -> Option<(String, usize)> {
801 let bytes = line.as_bytes();
802 let mut i = paren_start + 1; // skip '('
803 let mut args: Vec<String> = Vec::new();
804 let mut current_arg = String::new();
805 let mut depth = 1;
806
807 while i < bytes.len() && depth > 0 {
808 match bytes[i] {
809 b'(' => {
810 depth += 1;
811 current_arg.push('(');
812 }
813 b')' => {
814 depth -= 1;
815 if depth > 0 {
816 current_arg.push(')');
817 }
818 }
819 b',' if depth == 1 => {
820 args.push(current_arg.trim().to_string());
821 current_arg = String::new();
822 }
823 _ => current_arg.push(bytes[i] as char),
824 }
825 i += 1;
826 }
827
828 if depth != 0 {
829 return None;
830 }
831 args.push(current_arg.trim().to_string());
832
833 // Build parameter lookup table.
834 let mut param_map: HashMap<&str, usize> = HashMap::new();
835 for (pi, param) in def.params.iter().enumerate() {
836 param_map.insert(param.as_str(), pi);
837 }
838
839 let va_args_str = if def.is_variadic {
840 let va_start = def.params.len();
841 args.get(va_start..).unwrap_or(&[]).join(", ")
842 } else {
843 String::new()
844 };
845
846 // Single-pass word-boundary-aware substitution over the body.
847 let body_bytes = def.body.as_bytes();
848 let mut body = String::new();
849 let mut bi = 0;
850
851 while bi < body_bytes.len() {
852 // Stringification: # followed by identifier (whitespace between # and name is allowed).
853 if body_bytes[bi] == b'#' && bi + 1 < body_bytes.len() && body_bytes[bi + 1] != b'#' {
854 let mut id_start = bi + 1;
855 // Skip whitespace between # and the parameter name.
856 while id_start < body_bytes.len() && body_bytes[id_start] == b' ' {
857 id_start += 1;
858 }
859 let mut id_end = id_start;
860 while id_end < body_bytes.len()
861 && (body_bytes[id_end].is_ascii_alphanumeric() || body_bytes[id_end] == b'_')
862 {
863 id_end += 1;
864 }
865 if id_end > id_start {
866 let id = std::str::from_utf8(&body_bytes[id_start..id_end]).unwrap_or("");
867 if let Some(&pi) = param_map.get(id) {
868 body.push_str(&format!(
869 "\"{}\"",
870 args.get(pi).map(|s| s.as_str()).unwrap_or("")
871 ));
872 bi = id_end;
873 continue;
874 }
875 }
876 }
877
878 // Token pasting: ##
879 if bi + 1 < body_bytes.len() && body_bytes[bi] == b'#' && body_bytes[bi + 1] == b'#' {
880 // Trim trailing whitespace from what we've built, skip ## and leading whitespace.
881 let trimmed = body.trim_end().to_string();
882 body = trimmed;
883 bi += 2;
884 while bi < body_bytes.len() && body_bytes[bi] == b' ' {
885 bi += 1;
886 }
887 continue;
888 }
889
890 // Identifier: check if it's a parameter name.
891 if body_bytes[bi].is_ascii_alphabetic() || body_bytes[bi] == b'_' {
892 let id_start = bi;
893 while bi < body_bytes.len()
894 && (body_bytes[bi].is_ascii_alphanumeric() || body_bytes[bi] == b'_')
895 {
896 bi += 1;
897 }
898 let id = std::str::from_utf8(&body_bytes[id_start..bi]).unwrap_or("");
899
900 if id == "__VA_ARGS__" && def.is_variadic {
901 body.push_str(&va_args_str);
902 } else if let Some(&pi) = param_map.get(id) {
903 body.push_str(args.get(pi).map(|s| s.as_str()).unwrap_or(""));
904 } else {
905 body.push_str(id);
906 }
907 continue;
908 }
909
910 body.push(body_bytes[bi] as char);
911 bi += 1;
912 }
913
914 Some((body, i))
915 }
916 }
917
918 fn parse_defined_operand(expr: &str, start: usize) -> (&str, usize) {
919 let bytes = expr.as_bytes();
920 let mut i = start;
921
922 while i < bytes.len() && bytes[i] == b' ' {
923 i += 1;
924 }
925
926 let has_paren = i < bytes.len() && bytes[i] == b'(';
927 if has_paren {
928 i += 1;
929 while i < bytes.len() && bytes[i] == b' ' {
930 i += 1;
931 }
932 }
933
934 let name_start = i;
935 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
936 i += 1;
937 }
938 let name = &expr[name_start..i];
939
940 if has_paren {
941 while i < bytes.len() && bytes[i] == b' ' {
942 i += 1;
943 }
944 if i < bytes.len() && bytes[i] == b')' {
945 i += 1;
946 }
947 }
948
949 (name, i)
950 }
951
952 // ---- Expression evaluator for #if ----
953
954 fn eval_expr(expr: &str) -> Result<bool, String> {
955 let trimmed = expr.trim();
956 if trimmed.is_empty() {
957 return Err("empty expression".into());
958 }
959 let val = eval_or(trimmed)?;
960 Ok(val != 0)
961 }
962
963 fn eval_or(expr: &str) -> Result<i64, String> {
964 // Split on || (lowest precedence).
965 if let Some(pos) = find_op(expr, "||") {
966 let left = eval_or(&expr[..pos])?;
967 let right = eval_or(&expr[pos + 2..])?;
968 return Ok(if left != 0 || right != 0 { 1 } else { 0 });
969 }
970 eval_and(expr)
971 }
972
973 fn eval_and(expr: &str) -> Result<i64, String> {
974 if let Some(pos) = find_op(expr, "&&") {
975 let left = eval_and(&expr[..pos])?;
976 let right = eval_and(&expr[pos + 2..])?;
977 return Ok(if left != 0 && right != 0 { 1 } else { 0 });
978 }
979 eval_comparison(expr)
980 }
981
982 fn eval_comparison(expr: &str) -> Result<i64, String> {
983 // Scan right-to-left for left-associative evaluation.
984 for (op, op_len) in [
985 ("==", 2),
986 ("!=", 2),
987 (">=", 2),
988 ("<=", 2),
989 (">", 1),
990 ("<", 1),
991 ] {
992 if let Some(pos) = find_op_right(expr, op) {
993 let left = eval_comparison(&expr[..pos])?;
994 let right = eval_additive(&expr[pos + op_len..])?;
995 let result = match op {
996 "==" => left == right,
997 "!=" => left != right,
998 ">=" => left >= right,
999 "<=" => left <= right,
1000 ">" => left > right,
1001 "<" => left < right,
1002 _ => unreachable!(),
1003 };
1004 return Ok(if result { 1 } else { 0 });
1005 }
1006 }
1007 eval_additive(expr)
1008 }
1009
1010 fn eval_additive(expr: &str) -> Result<i64, String> {
1011 // Scan right-to-left for left-associative + and -.
1012 // But be careful: don't match unary minus (no left operand).
1013 if let Some(pos) = find_op_right(expr, "+") {
1014 let left = eval_additive(&expr[..pos])?;
1015 let right = eval_multiplicative(&expr[pos + 1..])?;
1016 return Ok(left + right);
1017 }
1018 // For minus, only match if there's a non-empty left side (not unary).
1019 if let Some(pos) = find_op_right(expr, "-") {
1020 if pos > 0 && !expr[..pos].trim().is_empty() {
1021 let left = eval_additive(&expr[..pos])?;
1022 let right = eval_multiplicative(&expr[pos + 1..])?;
1023 return Ok(left - right);
1024 }
1025 }
1026 eval_multiplicative(expr)
1027 }
1028
1029 fn eval_multiplicative(expr: &str) -> Result<i64, String> {
1030 if let Some(pos) = find_op_right(expr, "*") {
1031 let left = eval_multiplicative(&expr[..pos])?;
1032 let right = eval_unary(&expr[pos + 1..])?;
1033 return Ok(left * right);
1034 }
1035 if let Some(pos) = find_op_right(expr, "/") {
1036 let left = eval_multiplicative(&expr[..pos])?;
1037 let right = eval_unary(&expr[pos + 1..])?;
1038 if right == 0 {
1039 return Err("division by zero in #if expression".into());
1040 }
1041 return Ok(left / right);
1042 }
1043 if let Some(pos) = find_op_right(expr, "%") {
1044 let left = eval_multiplicative(&expr[..pos])?;
1045 let right = eval_unary(&expr[pos + 1..])?;
1046 if right == 0 {
1047 return Err("modulo by zero in #if expression".into());
1048 }
1049 return Ok(left % right);
1050 }
1051 eval_unary(expr)
1052 }
1053
1054 fn eval_unary(expr: &str) -> Result<i64, String> {
1055 let trimmed = expr.trim();
1056 if let Some(rest) = trimmed.strip_prefix('!') {
1057 let val = eval_unary(rest)?;
1058 return Ok(if val == 0 { 1 } else { 0 });
1059 }
1060 if let Some(rest) = trimmed.strip_prefix('-') {
1061 let val = eval_unary(rest)?;
1062 return Ok(-val);
1063 }
1064 if let Some(rest) = trimmed.strip_prefix('+') {
1065 return eval_unary(rest);
1066 }
1067 eval_primary(trimmed)
1068 }
1069
1070 fn eval_primary(expr: &str) -> Result<i64, String> {
1071 let trimmed = expr.trim();
1072
1073 if trimmed.is_empty() {
1074 return Err("unexpected end of expression".into());
1075 }
1076
1077 // Parenthesized expression.
1078 if trimmed.starts_with('(') {
1079 let close =
1080 find_matching_paren(trimmed).ok_or("unmatched parenthesis in #if expression")?;
1081 return eval_or(&trimmed[1..close]);
1082 }
1083
1084 // Integer literal.
1085 if trimmed.starts_with("0x") || trimmed.starts_with("0X") {
1086 return i64::from_str_radix(&trimmed[2..], 16)
1087 .map_err(|e| format!("invalid hex in #if: {}", e));
1088 }
1089 if let Ok(val) = trimmed.parse::<i64>() {
1090 return Ok(val);
1091 }
1092
1093 // Identifier — should have been expanded already. Treat as 0.
1094 if trimmed.chars().all(|c| c.is_alphanumeric() || c == '_') {
1095 return Ok(0);
1096 }
1097
1098 Err(format!("unexpected token in #if expression: '{}'", trimmed))
1099 }
1100
1101 /// Find an operator at the top level (not inside parentheses).
1102 fn find_op(expr: &str, op: &str) -> Option<usize> {
1103 let bytes = expr.as_bytes();
1104 let op_bytes = op.as_bytes();
1105 let mut depth = 0i32;
1106 let mut i = 0;
1107 while i + op_bytes.len() <= bytes.len() {
1108 match bytes[i] {
1109 b'(' => depth += 1,
1110 b')' => depth -= 1,
1111 _ => {
1112 if depth == 0 && &bytes[i..i + op_bytes.len()] == op_bytes {
1113 // Make sure we don't match inside a longer operator.
1114 return Some(i);
1115 }
1116 }
1117 }
1118 i += 1;
1119 }
1120 None
1121 }
1122
1123 /// Find the rightmost occurrence of an operator at the top level (not inside parentheses).
1124 /// Used for left-associative binary operators.
1125 fn find_op_right(expr: &str, op: &str) -> Option<usize> {
1126 let bytes = expr.as_bytes();
1127 let op_bytes = op.as_bytes();
1128 let mut depth = 0i32;
1129 let mut last_match = None;
1130 let mut i = 0;
1131 while i + op_bytes.len() <= bytes.len() {
1132 match bytes[i] {
1133 b'(' => depth += 1,
1134 b')' => depth -= 1,
1135 _ => {
1136 if depth == 0 && &bytes[i..i + op_bytes.len()] == op_bytes {
1137 // Don't match multi-char operators as part of longer ones.
1138 // e.g., don't match ">" inside ">=" or "!" inside "!=".
1139 let after = i + op_bytes.len();
1140 let is_part_of_longer = match op {
1141 ">" => after < bytes.len() && bytes[after] == b'=',
1142 "<" => after < bytes.len() && bytes[after] == b'=',
1143 "!" => after < bytes.len() && bytes[after] == b'=',
1144 _ => false,
1145 };
1146 if !is_part_of_longer {
1147 last_match = Some(i);
1148 }
1149 }
1150 }
1151 }
1152 i += 1;
1153 }
1154 last_match
1155 }
1156
1157 fn find_matching_paren(s: &str) -> Option<usize> {
1158 let mut depth = 0;
1159 for (i, ch) in s.chars().enumerate() {
1160 match ch {
1161 '(' => depth += 1,
1162 ')' => {
1163 depth -= 1;
1164 if depth == 0 {
1165 return Some(i);
1166 }
1167 }
1168 _ => {}
1169 }
1170 }
1171 None
1172 }
1173
1174 /// Replace remaining identifiers with "0" in a #if expression.
1175 /// After macro expansion, any identifier that's still present is undefined
1176 /// and evaluates to 0 per the cpp standard.
1177 fn replace_undefined_idents(expr: &str) -> String {
1178 let mut result = String::new();
1179 let bytes = expr.as_bytes();
1180 let mut i = 0;
1181 while i < bytes.len() {
1182 // Preserve numeric literals (including hex).
1183 if bytes[i].is_ascii_digit() {
1184 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1185 result.push(bytes[i] as char);
1186 i += 1;
1187 }
1188 continue;
1189 }
1190 // Identifiers -> "0".
1191 if bytes[i].is_ascii_alphabetic() || bytes[i] == b'_' {
1192 let start = i;
1193 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1194 i += 1;
1195 }
1196 let _ident = &expr[start..i];
1197 result.push('0');
1198 continue;
1199 }
1200 result.push(bytes[i] as char);
1201 i += 1;
1202 }
1203 result
1204 }
1205
1206 /// Check if a line has a trailing `&` in the code portion (not inside a string or comment).
1207 fn has_trailing_continuation(line: &str) -> bool {
1208 find_code_trailing_ampersand(line).is_some()
1209 }
1210
1211 /// Find the position of a trailing `&` continuation marker on this line.
1212 /// Recognises both code continuations (`&` outside strings) and the string
1213 /// continuation case where the `&` sits inside an unterminated literal —
1214 /// `'hello &\n &world'` is one logical literal and the line still
1215 /// needs to be joined to the next. Returns None if no `&` qualifies.
1216 fn find_code_trailing_ampersand(line: &str) -> Option<usize> {
1217 let bytes = line.as_bytes();
1218 let mut in_string: Option<u8> = None;
1219 let mut last_amp: Option<usize> = None;
1220
1221 let mut i = 0;
1222 while i < bytes.len() {
1223 let ch = bytes[i];
1224
1225 // Track string state. Inside a string, `&` followed only by
1226 // whitespace until end-of-line is also a continuation, and `!`
1227 // is a literal character (not the start of a comment).
1228 if let Some(quote) = in_string {
1229 if ch == quote {
1230 if i + 1 < bytes.len() && bytes[i + 1] == quote {
1231 i += 2; // doubled quote escape
1232 continue;
1233 }
1234 in_string = None;
1235 last_amp = None; // any `&` we saw was inside the now-closed string
1236 i += 1;
1237 continue;
1238 }
1239 if ch == b'&' {
1240 last_amp = Some(i);
1241 } else if !ch.is_ascii_whitespace() {
1242 last_amp = None;
1243 }
1244 i += 1;
1245 continue;
1246 }
1247
1248 if ch == b'\'' || ch == b'"' {
1249 in_string = Some(ch);
1250 i += 1;
1251 continue;
1252 }
1253
1254 // Comment — everything after ! is not code.
1255 if ch == b'!' {
1256 break;
1257 }
1258
1259 if ch == b'&' {
1260 last_amp = Some(i);
1261 } else if !ch.is_ascii_whitespace() {
1262 // Non-whitespace after the & means it's not trailing.
1263 last_amp = None;
1264 }
1265
1266 i += 1;
1267 }
1268
1269 last_amp
1270 }
1271
1272 fn split_first_word(s: &str) -> (&str, &str) {
1273 let s = s.trim();
1274 if let Some(pos) = s.find(|c: char| c.is_whitespace()) {
1275 (&s[..pos], s[pos..].trim_start())
1276 } else {
1277 (s, "")
1278 }
1279 }
1280
1281 fn push_utf8_char(result: &mut String, bytes: &[u8], i: &mut usize) {
1282 if bytes[*i].is_ascii() {
1283 result.push(bytes[*i] as char);
1284 *i += 1;
1285 return;
1286 }
1287 if let Ok(rest) = std::str::from_utf8(&bytes[*i..]) {
1288 if let Some(ch) = rest.chars().next() {
1289 result.push(ch);
1290 *i += ch.len_utf8();
1291 return;
1292 }
1293 }
1294 result.push(bytes[*i] as char);
1295 *i += 1;
1296 }
1297
1298 /// Get current date and time strings for __DATE__ and __TIME__.
1299 fn current_datetime() -> (String, String) {
1300 use std::time::SystemTime;
1301
1302 let now = SystemTime::now()
1303 .duration_since(SystemTime::UNIX_EPOCH)
1304 .unwrap_or_default()
1305 .as_secs();
1306
1307 // Convert epoch seconds to date/time components.
1308 // Simple conversion without external crates.
1309 let secs_per_day = 86400u64;
1310 let days = now / secs_per_day;
1311 let time_of_day = now % secs_per_day;
1312
1313 let hours = time_of_day / 3600;
1314 let minutes = (time_of_day % 3600) / 60;
1315 let seconds = time_of_day % 60;
1316
1317 // Days since epoch to year/month/day (simplified Gregorian).
1318 let (year, month, day) = epoch_days_to_date(days);
1319
1320 let months = [
1321 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
1322 ];
1323 let month_name = months.get(month as usize).unwrap_or(&"???");
1324
1325 let date = format!("{} {:2} {}", month_name, day, year);
1326 let time = format!("{:02}:{:02}:{:02}", hours, minutes, seconds);
1327 (date, time)
1328 }
1329
1330 fn epoch_days_to_date(mut days: u64) -> (u64, u64, u64) {
1331 // Algorithm from Howard Hinnant's date library (public domain).
1332 days += 719468;
1333 let era = days / 146097;
1334 let doe = days - era * 146097;
1335 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1336 let y = yoe + era * 400;
1337 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1338 let mp = (5 * doy + 2) / 153;
1339 let d = doy - (153 * mp + 2) / 5 + 1;
1340 let m = if mp < 10 { mp + 3 } else { mp - 9 };
1341 let y = if m <= 2 { y + 1 } else { y };
1342 (y, m - 1, d) // month is 0-based for array indexing
1343 }
1344
1345 #[cfg(test)]
1346 mod tests {
1347 use super::*;
1348
1349 fn pp(src: &str) -> String {
1350 let config = PreprocConfig::default();
1351 preprocess(src, &config).unwrap().text
1352 }
1353
1354 fn pp_with(src: &str, defines: &[(&str, &str)]) -> String {
1355 let mut config = PreprocConfig::default();
1356 for (k, v) in defines {
1357 config.defines.insert(k.to_string(), MacroDef::object(v));
1358 }
1359 preprocess(src, &config).unwrap().text
1360 }
1361
1362 fn pp_err(src: &str) -> PreprocError {
1363 let config = PreprocConfig::default();
1364 preprocess(src, &config).unwrap_err()
1365 }
1366
1367 fn lines(s: &str) -> Vec<&str> {
1368 s.lines().filter(|l| !l.is_empty()).collect()
1369 }
1370
1371 // ---- Object-like macros ----
1372
1373 #[test]
1374 fn define_and_expand_object_macro() {
1375 let out = pp("#define FOO 42\nx = FOO\n");
1376 assert!(out.contains("x = 42"));
1377 }
1378
1379 #[test]
1380 fn define_empty_macro() {
1381 let out = pp("#define ENABLED\n#ifdef ENABLED\nyes\n#endif\n");
1382 assert!(lines(&out).contains(&"yes"));
1383 }
1384
1385 #[test]
1386 fn define_empty_macro_expands_to_nothing() {
1387 // Per cpp standard: #define GUARD with no body expands to empty, not "1".
1388 let out = pp("#define GUARD\nx = GUARD end\n");
1389 assert!(out.contains("x = end"), "got: {:?}", out);
1390 }
1391
1392 #[test]
1393 fn undef_removes_macro() {
1394 let out = pp("#define FOO 1\n#undef FOO\n#ifdef FOO\nyes\n#else\nno\n#endif\n");
1395 assert!(lines(&out).contains(&"no"));
1396 }
1397
1398 #[test]
1399 fn macro_expands_to_macro() {
1400 // A expands to B, B expands to 42. Recursive expansion required.
1401 let out = pp("#define A B\n#define B 42\nx = A\n");
1402 assert!(out.contains("x = 42"), "got: {:?}", out);
1403 }
1404
1405 #[test]
1406 fn recursive_expansion_three_levels() {
1407 let out = pp("#define X Y\n#define Y Z\n#define Z 99\nval = X\n");
1408 assert!(out.contains("val = 99"), "got: {:?}", out);
1409 }
1410
1411 #[test]
1412 fn self_referencing_macro_stops() {
1413 // A macro referencing itself must not infinite-loop.
1414 // Blue paint: FOO is marked as expanding, so inner FOO is not re-expanded.
1415 let out = pp("#define FOO FOO + 1\nx = FOO\n");
1416 assert!(out.contains("x = FOO + 1"), "got: {:?}", out);
1417 }
1418
1419 #[test]
1420 fn mutual_recursion_stops() {
1421 // A → B, B → A. Must not infinite-loop.
1422 let out = pp("#define A B\n#define B A\nx = A\n");
1423 // A→B→A(blocked), so result is "A"
1424 assert!(out.contains("x = A"), "got: {:?}", out);
1425 }
1426
1427 // ---- Function-like macros ----
1428
1429 #[test]
1430 fn define_and_expand_function_macro() {
1431 let out = pp("#define MAX(a, b) merge((a), (b), (a) > (b))\nx = MAX(foo, bar)\n");
1432 assert!(out.contains("x = merge((foo), (bar), (foo) > (bar))"));
1433 }
1434
1435 #[test]
1436 fn function_macro_no_parens_not_expanded() {
1437 let out = pp("#define FOO(x) (x+1)\ny = FOO\n");
1438 // No parens after FOO — should not expand.
1439 assert!(out.contains("y = FOO"));
1440 }
1441
1442 #[test]
1443 fn function_macro_nested_parens() {
1444 let out = pp("#define F(x) (x)\ny = F(a(b, c))\n");
1445 assert!(out.contains("y = (a(b, c))"));
1446 }
1447
1448 #[test]
1449 fn param_substitution_word_boundary() {
1450 // Parameter "a" must not match inside "abcdef".
1451 let out = pp("#define F(a) abcdef + a\ny = F(99)\n");
1452 assert!(out.contains("y = abcdef + 99"), "got: {:?}", out);
1453 }
1454
1455 #[test]
1456 fn param_no_double_substitution() {
1457 // F(a, b) with body "a + b": calling F(b, a) should give "b + a", not "a + a".
1458 let out = pp("#define F(a, b) a + b\ny = F(b, a)\n");
1459 assert!(out.contains("y = b + a"), "got: {:?}", out);
1460 }
1461
1462 #[test]
1463 fn param_name_as_substring_of_another() {
1464 // Parameter "x" should not match inside "x_extra".
1465 let out = pp("#define F(x) x_extra + x\ny = F(42)\n");
1466 assert!(out.contains("y = x_extra + 42"), "got: {:?}", out);
1467 }
1468
1469 // ---- Conditionals ----
1470
1471 #[test]
1472 fn ifdef_true() {
1473 let out = pp_with("#ifdef FEAT\nyes\n#endif\n", &[("FEAT", "1")]);
1474 assert!(lines(&out).contains(&"yes"));
1475 }
1476
1477 #[test]
1478 fn ifdef_false() {
1479 let out = pp("#ifdef FEAT\nyes\n#endif\n");
1480 assert!(!lines(&out).contains(&"yes"));
1481 }
1482
1483 #[test]
1484 fn ifdef_else() {
1485 let out = pp("#ifdef FEAT\nyes\n#else\nno\n#endif\n");
1486 assert!(lines(&out).contains(&"no"));
1487 assert!(!lines(&out).contains(&"yes"));
1488 }
1489
1490 #[test]
1491 fn ifndef() {
1492 let out = pp("#ifndef FEAT\nyes\n#endif\n");
1493 assert!(lines(&out).contains(&"yes"));
1494 }
1495
1496 #[test]
1497 fn ifndef_false() {
1498 let out = pp_with("#ifndef FEAT\nyes\n#endif\n", &[("FEAT", "1")]);
1499 assert!(!lines(&out).contains(&"yes"));
1500 }
1501
1502 #[test]
1503 fn nested_ifdef() {
1504 let out = pp_with(
1505 "#ifdef A\n#ifdef B\nboth\n#else\nonly_a\n#endif\n#endif\n",
1506 &[("A", "1"), ("B", "1")],
1507 );
1508 assert!(lines(&out).contains(&"both"));
1509 }
1510
1511 #[test]
1512 fn nested_ifdef_outer_false() {
1513 let out = pp("#ifdef A\n#ifdef B\nboth\n#endif\n#endif\n");
1514 assert!(!lines(&out).contains(&"both"));
1515 }
1516
1517 #[test]
1518 fn if_true() {
1519 let out = pp("#if 1\nyes\n#endif\n");
1520 assert!(lines(&out).contains(&"yes"));
1521 }
1522
1523 #[test]
1524 fn if_false() {
1525 let out = pp("#if 0\nyes\n#endif\n");
1526 assert!(!lines(&out).contains(&"yes"));
1527 }
1528
1529 #[test]
1530 fn if_defined() {
1531 let out = pp_with("#if defined(FEAT)\nyes\n#endif\n", &[("FEAT", "1")]);
1532 assert!(lines(&out).contains(&"yes"));
1533 }
1534
1535 #[test]
1536 fn if_not_defined() {
1537 let out = pp("#if defined(FEAT)\nyes\n#else\nno\n#endif\n");
1538 assert!(lines(&out).contains(&"no"));
1539 }
1540
1541 #[test]
1542 fn if_arithmetic() {
1543 let out = pp_with(
1544 "#if MAX > 512\nbig\n#else\nsmall\n#endif\n",
1545 &[("MAX", "1024")],
1546 );
1547 assert!(lines(&out).contains(&"big"));
1548 }
1549
1550 #[test]
1551 fn if_and() {
1552 let out = pp_with(
1553 "#if defined(A) && defined(B)\nboth\n#endif\n",
1554 &[("A", "1"), ("B", "1")],
1555 );
1556 assert!(lines(&out).contains(&"both"));
1557 }
1558
1559 #[test]
1560 fn elif() {
1561 let out = pp_with(
1562 "#ifdef LINUX\nlinux\n#elif defined(__APPLE__)\napple\n#else\nother\n#endif\n",
1563 &[], // __APPLE__ is predefined on macOS
1564 );
1565 // On macOS, should get "apple".
1566 #[cfg(target_os = "macos")]
1567 assert!(lines(&out).contains(&"apple"));
1568 }
1569
1570 #[test]
1571 fn if_zero_skips_content() {
1572 let out = pp("#if 0\nskipped code\n#define SHOULD_NOT_EXIST\n#endif\nx\n");
1573 assert!(!lines(&out).contains(&"skipped code"));
1574 assert!(lines(&out).contains(&"x"));
1575 }
1576
1577 // ---- Predefined macros ----
1578
1579 #[test]
1580 fn predefined_armfortas() {
1581 let out = pp("x = __ARMFORTAS__\n");
1582 assert!(out.contains("x = 1"));
1583 }
1584
1585 #[test]
1586 fn predefined_aarch64() {
1587 let out = pp("x = __aarch64__\n");
1588 assert!(out.contains("x = 1"));
1589 }
1590
1591 #[test]
1592 #[cfg(target_os = "macos")]
1593 fn predefined_apple() {
1594 let out = pp("#ifdef __APPLE__\nyes\n#endif\n");
1595 assert!(lines(&out).contains(&"yes"));
1596 }
1597
1598 #[test]
1599 fn predefined_line() {
1600 let out = pp("a\nb\nc = __LINE__\n");
1601 assert!(out.contains("c = 3"));
1602 }
1603
1604 // ---- Fortran-aware behavior ----
1605
1606 #[test]
1607 fn no_expansion_in_string_single() {
1608 let out = pp_with("x = 'FOO is great'\n", &[("FOO", "BAR")]);
1609 assert!(out.contains("'FOO is great'"));
1610 }
1611
1612 #[test]
1613 fn no_expansion_in_string_double() {
1614 let out = pp_with("x = \"FOO is great\"\n", &[("FOO", "BAR")]);
1615 assert!(out.contains("\"FOO is great\""));
1616 }
1617
1618 #[test]
1619 fn no_expansion_in_doubled_quote_string() {
1620 // Fortran doubled-quote escape: 'it''s' should be preserved intact.
1621 let out = pp_with("x = 'it''s a FOO'\n", &[("FOO", "BAR")]);
1622 assert!(out.contains("'it''s a FOO'"), "got: {:?}", out);
1623 }
1624
1625 #[test]
1626 fn doubled_quote_does_not_end_string_early() {
1627 // Regression test: the '' must not cause early string termination.
1628 let out = pp_with("x = 'he said ''hello'' there' + FOO\n", &[("FOO", "1")]);
1629 assert!(out.contains("'he said ''hello'' there'"), "got: {:?}", out);
1630 assert!(
1631 out.contains("+ 1"),
1632 "FOO after string should expand, got: {:?}",
1633 out
1634 );
1635 }
1636
1637 #[test]
1638 fn no_expansion_in_comment() {
1639 let out = pp_with("x = 1 ! FOO comment\n", &[("FOO", "BAR")]);
1640 assert!(out.contains("! FOO comment"));
1641 }
1642
1643 #[test]
1644 fn expansion_before_comment() {
1645 let out = pp_with("x = FOO ! comment\n", &[("FOO", "42")]);
1646 assert!(out.contains("x = 42 ! comment"));
1647 }
1648
1649 // ---- Error cases ----
1650
1651 #[test]
1652 fn error_unterminated_if() {
1653 let err = pp_err("#ifdef FOO\nstuff\n");
1654 assert!(err.msg.contains("unterminated"));
1655 }
1656
1657 #[test]
1658 fn error_else_without_if() {
1659 let err = pp_err("#else\n");
1660 assert!(err.msg.contains("without matching"));
1661 }
1662
1663 #[test]
1664 fn error_endif_without_if() {
1665 let err = pp_err("#endif\n");
1666 assert!(err.msg.contains("without matching"));
1667 }
1668
1669 #[test]
1670 fn error_directive() {
1671 let err = pp_err("#error something went wrong\n");
1672 assert!(err.msg.contains("something went wrong"));
1673 }
1674
1675 // ---- Practical Fortran patterns ----
1676
1677 #[test]
1678 fn fortsh_style_ifdef() {
1679 let src = "\
1680 module test
1681 #ifdef USE_C_STRINGS
1682 use c_string_module
1683 #else
1684 ! pure Fortran strings
1685 #endif
1686 implicit none
1687 end module
1688 ";
1689 let out = pp(src);
1690 // USE_C_STRINGS not defined, should get the else branch.
1691 assert!(out.contains("! pure Fortran strings"));
1692 assert!(!out.contains("use c_string_module"));
1693 }
1694
1695 #[test]
1696 fn fortsh_style_apple_guard() {
1697 let src = "\
1698 #ifdef __APPLE__
1699 call macos_specific()
1700 #else
1701 call linux_specific()
1702 #endif
1703 ";
1704 let out = pp(src);
1705 #[cfg(target_os = "macos")]
1706 {
1707 assert!(out.contains("call macos_specific()"));
1708 assert!(!out.contains("call linux_specific()"));
1709 }
1710 }
1711
1712 #[test]
1713 fn directive_between_free_form_continuations_stays_a_directive() {
1714 let src = "\
1715 program p
1716 integer :: x
1717 x = 1 + &
1718 #if FLAG
1719 2 + &
1720 #endif
1721 3
1722 end program
1723 ";
1724 let out = pp(src);
1725 assert!(
1726 out.contains("x = 1 + &"),
1727 "continued line head should remain intact: {:?}",
1728 out
1729 );
1730 assert!(
1731 out.contains(" 3"),
1732 "continued line tail should remain after the stripped directive block: {:?}",
1733 out
1734 );
1735 assert!(
1736 !out.contains("#if FLAG") && !out.contains("2 + &") && !out.contains("#endif"),
1737 "false branch should remain removed: {:?}",
1738 out
1739 );
1740 }
1741
1742 #[test]
1743 fn source_map_preserves_line_numbers() {
1744 let config = PreprocConfig::default();
1745 let result = preprocess("a\n#define X 1\nb\nc\n", &config).unwrap();
1746 // Line 1 is "a", line 2 is #define (blank), line 3 is "b", line 4 is "c".
1747 assert_eq!(result.source_map.len(), 4);
1748 assert_eq!(result.source_map[0].line, 1);
1749 assert_eq!(result.source_map[2].line, 3);
1750 }
1751
1752 // ---- Expression evaluator ----
1753
1754 #[test]
1755 fn eval_simple_true() {
1756 assert!(eval_expr("1").unwrap());
1757 }
1758
1759 #[test]
1760 fn eval_simple_false() {
1761 assert!(!eval_expr("0").unwrap());
1762 }
1763
1764 #[test]
1765 fn eval_comparison_gt() {
1766 assert!(eval_expr("1024 > 512").unwrap());
1767 }
1768
1769 #[test]
1770 fn eval_comparison_eq() {
1771 assert!(eval_expr("42 == 42").unwrap());
1772 }
1773
1774 #[test]
1775 fn eval_logical_and() {
1776 assert!(eval_expr("1 && 1").unwrap());
1777 assert!(!eval_expr("1 && 0").unwrap());
1778 }
1779
1780 #[test]
1781 fn eval_logical_or() {
1782 assert!(eval_expr("0 || 1").unwrap());
1783 assert!(!eval_expr("0 || 0").unwrap());
1784 }
1785
1786 #[test]
1787 fn eval_not() {
1788 assert!(eval_expr("!0").unwrap());
1789 assert!(!eval_expr("!1").unwrap());
1790 }
1791
1792 #[test]
1793 fn eval_parenthesized() {
1794 assert!(eval_expr("(1 || 0) && 1").unwrap());
1795 assert!(!eval_expr("1 && (0 || 0)").unwrap());
1796 }
1797
1798 #[test]
1799 fn eval_hex() {
1800 assert!(eval_expr("0xFF > 200").unwrap());
1801 }
1802
1803 // Arithmetic
1804 #[test]
1805 fn eval_addition() {
1806 assert_eq!(eval_expr("3 + 4").unwrap(), true); // 7 != 0
1807 assert!(eval_expr("100 + 200 > 250").unwrap());
1808 }
1809
1810 #[test]
1811 fn eval_subtraction() {
1812 assert!(eval_expr("10 - 5 > 0").unwrap());
1813 assert!(!eval_expr("5 - 10 > 0").unwrap());
1814 }
1815
1816 #[test]
1817 fn eval_multiplication() {
1818 assert!(eval_expr("6 * 7 == 42").unwrap());
1819 }
1820
1821 #[test]
1822 fn eval_division() {
1823 assert!(eval_expr("42 / 6 == 7").unwrap());
1824 }
1825
1826 #[test]
1827 fn eval_modulo() {
1828 assert!(eval_expr("10 % 3 == 1").unwrap());
1829 }
1830
1831 #[test]
1832 fn eval_unary_minus() {
1833 assert!(eval_expr("-1 < 0").unwrap());
1834 assert!(eval_expr("-(-1) > 0").unwrap());
1835 }
1836
1837 #[test]
1838 fn eval_complex_arithmetic() {
1839 // (1024 + 1) > 512
1840 assert!(eval_expr("1024 + 1 > 512").unwrap());
1841 }
1842
1843 #[test]
1844 fn eval_precedence() {
1845 // 2 + 3 * 4 = 14 (not 20)
1846 assert!(eval_expr("2 + 3 * 4 == 14").unwrap());
1847 }
1848
1849 #[test]
1850 fn if_with_arithmetic() {
1851 let out = pp_with(
1852 "#if MAX + 1 > 512\nbig\n#else\nsmall\n#endif\n",
1853 &[("MAX", "1024")],
1854 );
1855 assert!(lines(&out).contains(&"big"));
1856 }
1857
1858 #[test]
1859 fn if_chained_macros() {
1860 // A -> 1, B -> A. #if B should expand B->A->1, evaluating to true.
1861 let out = pp_with("#if B\nyes\n#endif\n", &[("A", "1"), ("B", "A")]);
1862 assert!(
1863 lines(&out).contains(&"yes"),
1864 "chained macro in #if failed, got: {:?}",
1865 lines(&out)
1866 );
1867 }
1868
1869 #[test]
1870 fn if_chained_three_levels() {
1871 let out = pp_with(
1872 "#if C > 10\nyes\n#endif\n",
1873 &[("A", "42"), ("B", "A"), ("C", "B")],
1874 );
1875 assert!(
1876 lines(&out).contains(&"yes"),
1877 "3-level chain in #if failed, got: {:?}",
1878 lines(&out)
1879 );
1880 }
1881
1882 #[test]
1883 fn if_defined_and_value() {
1884 // Common real-world pattern: #if defined(FOO) && FOO > 5
1885 let out = pp_with(
1886 "#if defined(FOO) && FOO > 5\nyes\n#endif\n",
1887 &[("FOO", "10")],
1888 );
1889 assert!(lines(&out).contains(&"yes"));
1890 }
1891
1892 #[test]
1893 fn if_function_macro_expands() {
1894 let out = pp("#define INC(x) ((x) + 1)\n#if INC(41) > 41\nyes\n#endif\n");
1895 assert!(
1896 lines(&out).contains(&"yes"),
1897 "function macro in #if failed, got: {:?}",
1898 lines(&out)
1899 );
1900 }
1901
1902 #[test]
1903 fn if_object_macro_can_expand_into_function_macro() {
1904 let out = pp(
1905 "#define INC(x) ((x) + 1)\n#define WRAP(x) INC(x)\n#if WRAP(41) > 41\nyes\n#endif\n",
1906 );
1907 assert!(
1908 lines(&out).contains(&"yes"),
1909 "object->function macro chain in #if failed, got: {:?}",
1910 lines(&out)
1911 );
1912 }
1913
1914 #[test]
1915 fn if_not_defined_with_bang() {
1916 let out = pp("#if !defined(NOPE)\nyes\n#endif\n");
1917 assert!(lines(&out).contains(&"yes"));
1918 }
1919
1920 // ---- Variadic macros ----
1921
1922 #[test]
1923 fn variadic_macro() {
1924 let out = pp("#define DBG(fmt, ...) write(0, fmt) __VA_ARGS__\nx = DBG(a, b, c)\n");
1925 assert!(out.contains("x = write(0, a) b, c"));
1926 }
1927
1928 #[test]
1929 fn variadic_macro_no_extra_args() {
1930 let out = pp("#define DBG(fmt, ...) write(0, fmt) __VA_ARGS__\nx = DBG(a)\n");
1931 assert!(out.contains("x = write(0, a) "));
1932 }
1933
1934 // ---- Stringification ----
1935
1936 #[test]
1937 fn stringification() {
1938 let out = pp("#define STR(x) #x\ny = STR(hello)\n");
1939 assert!(out.contains("y = \"hello\""));
1940 }
1941
1942 #[test]
1943 fn stringification_with_spaces() {
1944 let out = pp("#define STR(x) #x\ny = STR(a + b)\n");
1945 assert!(out.contains("y = \"a + b\""));
1946 }
1947
1948 // ---- Token pasting ----
1949
1950 #[test]
1951 fn token_pasting() {
1952 let out = pp("#define PASTE(a, b) a ## b\nx = PASTE(foo, bar)\n");
1953 assert!(out.contains("x = foobar"));
1954 }
1955
1956 #[test]
1957 fn token_pasting_with_numbers() {
1958 let out = pp("#define VAR(n) var_ ## n\nx = VAR(42)\n");
1959 assert!(out.contains("x = var_42"));
1960 }
1961
1962 // ---- Backslash continuation ----
1963
1964 #[test]
1965 fn backslash_continuation_in_define() {
1966 let out = pp("#define LONG_MACRO \\\n 42\nx = LONG_MACRO\n");
1967 assert!(out.contains("x = 42"));
1968 }
1969
1970 #[test]
1971 fn backslash_continuation_multiline() {
1972 let out = pp("#define M(a, b) \\\n ((a) + \\\n (b))\nx = M(1, 2)\n");
1973 // After continuation joining, the define body is " ((a) + (b))"
1974 // with leading/trailing whitespace from the continuation lines.
1975 // The body gets trimmed during define processing, so it becomes "((a) + (b))".
1976 assert!(
1977 out.contains("((1) +"),
1978 "got: {:?}",
1979 out.lines().collect::<Vec<_>>()
1980 );
1981 assert!(out.contains("(2))"));
1982 }
1983
1984 #[test]
1985 fn line_number_correct_after_continuation() {
1986 // Lines 1-3 are a continued #define. Line 4 should report __LINE__ = 4.
1987 let out = pp("#define M \\\n 42\na\nb = __LINE__\n");
1988 assert!(
1989 out.contains("b = 4"),
1990 "got: {:?}",
1991 out.lines().collect::<Vec<_>>()
1992 );
1993 }
1994
1995 // ---- Self-referencing macro (no infinite loop) ----
1996
1997 #[test]
1998 fn self_referencing_macro_no_loop() {
1999 // A macro that expands to its own name should not infinite-loop.
2000 // cpp standard: a macro is not re-expanded during its own expansion.
2001 // Our simple implementation does one pass, so this works naturally.
2002 let out = pp("#define FOO FOO + 1\nx = FOO\n");
2003 assert!(out.contains("x = FOO + 1") || out.contains("x = FOO"));
2004 }
2005
2006 // ---- #if 0 does not process directives inside ----
2007
2008 #[test]
2009 fn if_zero_does_not_include() {
2010 // #include inside #if 0 must not try to open the file.
2011 let out = pp("#if 0\n#include \"nonexistent_file.h\"\n#endif\nok\n");
2012 assert!(lines(&out).contains(&"ok"));
2013 }
2014
2015 #[test]
2016 fn if_zero_does_not_define() {
2017 let out = pp("#if 0\n#define SECRET 42\n#endif\n#ifdef SECRET\nyes\n#else\nno\n#endif\n");
2018 assert!(lines(&out).contains(&"no"));
2019 }
2020
2021 // ---- Deeply nested conditionals ----
2022
2023 #[test]
2024 fn deeply_nested_conditionals() {
2025 let src = "\
2026 #if 1
2027 #if 1
2028 #if 1
2029 #if 1
2030 deep
2031 #endif
2032 #endif
2033 #endif
2034 #endif
2035 ";
2036 let out = pp(src);
2037 assert!(lines(&out).contains(&"deep"));
2038 }
2039
2040 // ---- Null directive ----
2041
2042 #[test]
2043 fn null_directive() {
2044 // Bare # on a line is valid (null directive).
2045 let out = pp("#\nok\n");
2046 assert!(lines(&out).contains(&"ok"));
2047 }
2048
2049 // ---- #include with actual file content ----
2050
2051 #[test]
2052 fn include_injects_file_content() {
2053 use std::io::Write;
2054 let dir = std::env::temp_dir();
2055 let inc_path = dir.join("test_pp_include.inc");
2056 let mut f = std::fs::File::create(&inc_path).unwrap();
2057 writeln!(f, "integer :: included_var").unwrap();
2058 drop(f);
2059
2060 let mut config = PreprocConfig::default();
2061 config.include_paths.push(dir);
2062 let src = "#include \"test_pp_include.inc\"\nreal :: x\n";
2063 let result = preprocess(src, &config).unwrap();
2064 assert!(
2065 result.text.contains("integer :: included_var"),
2066 "got: {:?}",
2067 result.text
2068 );
2069 assert!(result.text.contains("real :: x"));
2070 }
2071
2072 #[test]
2073 fn include_defines_propagate() {
2074 use std::io::Write;
2075 let dir = std::env::temp_dir();
2076 let inc_path = dir.join("test_pp_define.inc");
2077 let mut f = std::fs::File::create(&inc_path).unwrap();
2078 writeln!(f, "#define INCLUDED_VAL 99").unwrap();
2079 drop(f);
2080
2081 let mut config = PreprocConfig::default();
2082 config.include_paths.push(dir);
2083 let src = "#include \"test_pp_define.inc\"\nx = INCLUDED_VAL\n";
2084 let result = preprocess(src, &config).unwrap();
2085 assert!(result.text.contains("x = 99"), "got: {:?}", result.text);
2086 }
2087
2088 #[test]
2089 fn file_macro_restored_after_include() {
2090 use std::io::Write;
2091 let dir = std::env::temp_dir();
2092 let inc_path = dir.join("test_pp_file_restore.inc");
2093 let mut f = std::fs::File::create(&inc_path).unwrap();
2094 writeln!(f, "! included").unwrap();
2095 drop(f);
2096
2097 let mut config = PreprocConfig::default();
2098 config.include_paths.push(dir);
2099 config.filename = "parent.f90".into();
2100 let src = "before = __FILE__\n#include \"test_pp_file_restore.inc\"\nafter = __FILE__\n";
2101 let result = preprocess(src, &config).unwrap();
2102 assert!(
2103 result.text.contains("before = \"parent.f90\""),
2104 "got: {:?}",
2105 result.text
2106 );
2107 assert!(
2108 result.text.contains("after = \"parent.f90\""),
2109 "__FILE__ not restored, got: {:?}",
2110 result.text
2111 );
2112 }
2113
2114 // ---- Fixed-form awareness ----
2115
2116 #[test]
2117 fn fixed_form_comment_not_expanded() {
2118 let mut config = PreprocConfig::default();
2119 config.fixed_form = true;
2120 config.defines.insert("FOO".into(), MacroDef::object("BAR"));
2121 let result = preprocess("C FOO is a comment\n x = FOO\n", &config).unwrap();
2122 // C-line should not have FOO expanded.
2123 assert!(
2124 result.text.contains("C FOO is a comment"),
2125 "got: {:?}",
2126 result.text
2127 );
2128 // Continuation line should expand FOO.
2129 assert!(result.text.contains("x = BAR"), "got: {:?}", result.text);
2130 }
2131
2132 #[test]
2133 fn fixed_form_star_comment() {
2134 let mut config = PreprocConfig::default();
2135 config.fixed_form = true;
2136 config.defines.insert("FOO".into(), MacroDef::object("BAR"));
2137 let result = preprocess("* FOO is a comment\n", &config).unwrap();
2138 assert!(
2139 result.text.contains("* FOO is a comment"),
2140 "got: {:?}",
2141 result.text
2142 );
2143 }
2144
2145 // ---- Fortran & continuation ----
2146
2147 #[test]
2148 fn fortran_ampersand_continuation() {
2149 let out = pp_with("x = FOO + &\n BAR\n", &[("FOO", "1"), ("BAR", "2")]);
2150 assert!(
2151 out.contains("x = 1 + 2"),
2152 "got: {:?}",
2153 out.lines().collect::<Vec<_>>()
2154 );
2155 }
2156
2157 #[test]
2158 fn ampersand_in_string_not_continued() {
2159 // & inside a string literal must NOT trigger continuation.
2160 let out = pp("x = 'hello &'\ny = 2\n");
2161 assert!(
2162 out.contains("'hello &'"),
2163 "string corrupted, got: {:?}",
2164 out.lines().collect::<Vec<_>>()
2165 );
2166 assert!(
2167 out.contains("y = 2"),
2168 "next line missing, got: {:?}",
2169 out.lines().collect::<Vec<_>>()
2170 );
2171 }
2172
2173 #[test]
2174 fn ampersand_in_comment_not_continued() {
2175 // & after ! comment must NOT trigger continuation.
2176 let out = pp("x = 1 ! comment &\ny = 2\n");
2177 assert!(
2178 out.contains("! comment &"),
2179 "comment corrupted, got: {:?}",
2180 out.lines().collect::<Vec<_>>()
2181 );
2182 assert!(
2183 out.contains("y = 2"),
2184 "next line missing, got: {:?}",
2185 out.lines().collect::<Vec<_>>()
2186 );
2187 }
2188
2189 // ---- __DATE__ and __TIME__ ----
2190
2191 #[test]
2192 fn date_macro_not_empty() {
2193 let out = pp("x = __DATE__\n");
2194 // Should contain a quoted date string, not empty.
2195 assert!(out.contains("\""), "got: {:?}", out);
2196 assert!(!out.contains("__DATE__"), "macro not expanded: {:?}", out);
2197 }
2198
2199 #[test]
2200 fn time_macro_has_colons() {
2201 let out = pp("x = __TIME__\n");
2202 assert!(out.contains(":"), "got: {:?}", out);
2203 }
2204
2205 // ---- __FILE__ ----
2206
2207 #[test]
2208 fn file_macro() {
2209 let mut config = PreprocConfig::default();
2210 config.filename = "test.f90".into();
2211 let result = preprocess("x = __FILE__\n", &config).unwrap();
2212 assert!(
2213 result.text.contains("\"test.f90\""),
2214 "got: {:?}",
2215 result.text
2216 );
2217 }
2218
2219 // ---- defined without parens ----
2220
2221 #[test]
2222 fn defined_without_parens() {
2223 let out = pp_with("#if defined FEAT\nyes\n#endif\n", &[("FEAT", "1")]);
2224 assert!(lines(&out).contains(&"yes"));
2225 }
2226
2227 // ---- Multi-elif chain ----
2228
2229 #[test]
2230 fn multi_elif_chain() {
2231 let out = pp_with(
2232 "#if X == 1\nfirst\n#elif X == 2\nsecond\n#elif X == 3\nthird\n#else\nother\n#endif\n",
2233 &[("X", "2")],
2234 );
2235 assert!(lines(&out).contains(&"second"));
2236 assert!(!lines(&out).contains(&"first"));
2237 assert!(!lines(&out).contains(&"third"));
2238 assert!(!lines(&out).contains(&"other"));
2239 }
2240
2241 // ---- #error inside #if 0 should not trigger ----
2242
2243 #[test]
2244 fn error_inside_if_zero_does_not_trigger() {
2245 let out = pp("#if 0\n#error this should not fire\n#endif\nok\n");
2246 assert!(lines(&out).contains(&"ok"));
2247 }
2248
2249 // ---- #if with hex ----
2250
2251 #[test]
2252 fn if_hex_comparison() {
2253 let out = pp("#if 0xFF > 200\nyes\n#endif\n");
2254 assert!(lines(&out).contains(&"yes"));
2255 }
2256
2257 // ---- Object macro with space before paren is not function-like ----
2258
2259 #[test]
2260 fn define_with_space_before_paren_is_object() {
2261 // #define FOO (x) should be object-like with body "(x)", not function-like.
2262 let out = pp("#define FOO (x)\ny = FOO\n");
2263 assert!(out.contains("y = (x)"), "got: {:?}", out);
2264 }
2265
2266 // ---- #line directive ----
2267
2268 #[test]
2269 fn line_directive_updates_source_map() {
2270 let config = PreprocConfig::default();
2271 let result = preprocess("a\n#line 100 \"other.f90\"\nb\n", &config).unwrap();
2272 // Line 3 (b) should have source map entry pointing to other.f90:100.
2273 assert_eq!(result.source_map[2].line, 100);
2274 assert_eq!(result.source_map[2].filename, "other.f90");
2275 }
2276
2277 #[test]
2278 fn line_directive_without_filename() {
2279 let config = PreprocConfig::default();
2280 let result = preprocess("a\n#line 50\nb\n", &config).unwrap();
2281 assert_eq!(result.source_map[2].line, 50);
2282 }
2283
2284 // ---- Stringify with space ----
2285
2286 #[test]
2287 fn stringify_with_space() {
2288 // # x (with space) should still stringify.
2289 let out = pp("#define STR(x) # x\ny = STR(hello)\n");
2290 assert!(out.contains("y = \"hello\""), "got: {:?}", out);
2291 }
2292
2293 // ---- Variadic edge cases ----
2294
2295 #[test]
2296 fn variadic_zero_args() {
2297 let out = pp("#define M(...) [__VA_ARGS__]\ny = M()\n");
2298 assert!(out.contains("y = []"), "got: {:?}", out);
2299 }
2300
2301 // ---- #warning doesn't error ----
2302
2303 #[test]
2304 fn warning_continues_processing() {
2305 let out = pp("#warning test warning\nok\n");
2306 assert!(lines(&out).contains(&"ok"));
2307 }
2308
2309 // ---- Include recursion guard ----
2310
2311 #[test]
2312 fn include_recursion_guard() {
2313 use std::io::Write;
2314 let dir = std::env::temp_dir();
2315 let path = dir.join("test_pp_recurse.inc");
2316 let mut f = std::fs::File::create(&path).unwrap();
2317 writeln!(f, "#include \"test_pp_recurse.inc\"").unwrap();
2318 drop(f);
2319
2320 let mut config = PreprocConfig::default();
2321 config.include_paths.push(dir);
2322 let result = preprocess("#include \"test_pp_recurse.inc\"\n", &config);
2323 assert!(result.is_err());
2324 let err = result.unwrap_err();
2325 assert!(
2326 err.msg.contains("depth") || err.msg.contains("recursion"),
2327 "got: {}",
2328 err.msg
2329 );
2330 }
2331 }
2332