1#![allow(missing_docs)] mod source_map;
61
62pub use source_map::{FileId, SourceLocation, SourceMap};
63
64use std::collections::HashMap;
65
66pub trait Resolver {
71 fn resolve(&self, requesting_file: &str, include: &Include) -> Result<String, ResolveError>;
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Include {
83 Quoted(String),
85 System(String),
87}
88
89impl Include {
90 #[must_use]
92 pub fn path(&self) -> &str {
93 match self {
94 Self::Quoted(p) | Self::System(p) => p,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ResolveError {
102 pub requested: String,
104 pub message: String,
106}
107
108#[derive(Debug, Clone, Default)]
111pub struct MemoryResolver {
112 files: HashMap<String, String>,
113}
114
115impl MemoryResolver {
116 #[must_use]
117 pub fn new() -> Self {
118 Self {
119 files: HashMap::new(),
120 }
121 }
122
123 pub fn add(&mut self, name: impl Into<String>, content: impl Into<String>) {
125 self.files.insert(name.into(), content.into());
126 }
127}
128
129impl Resolver for MemoryResolver {
130 fn resolve(&self, _requesting: &str, include: &Include) -> Result<String, ResolveError> {
131 let path = include.path();
132 self.files.get(path).cloned().ok_or_else(|| ResolveError {
133 requested: path.to_string(),
134 message: format!("file not in MemoryResolver: {path}"),
135 })
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct PragmaPrefix {
146 pub prefix: String,
148 pub file: String,
150 pub line: usize,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct PragmaKeylist {
160 pub type_name: String,
162 pub keys: Vec<String>,
164 pub file: String,
166 pub line: usize,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
178pub enum OpenSplicePragma {
179 DataType {
182 type_name: String,
183 file: String,
184 line: usize,
185 },
186 DataKey {
189 type_name: String,
190 field: String,
191 file: String,
192 line: usize,
193 },
194 Cats {
197 type_name: String,
198 keys: Vec<String>,
199 file: String,
200 line: usize,
201 },
202 GenEquality { file: String, line: usize },
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct PragmaDdsXtopics {
213 pub version: String,
215 pub file: String,
217 pub line: usize,
219}
220
221#[derive(Debug, Clone, Default)]
223pub struct ProcessedSource {
224 pub expanded: String,
226 pub source_map: SourceMap,
228 pub pragma_keylists: Vec<PragmaKeylist>,
230 pub opensplice_pragmas: Vec<OpenSplicePragma>,
233 pub pragma_prefixes: Vec<PragmaPrefix>,
237 pub pragma_dds_xtopics: Vec<PragmaDdsXtopics>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
245pub enum PreprocessError {
246 IncludeNotFound(ResolveError),
248 IncludeCycle {
250 file: String,
252 },
253 UnmatchedEndif {
255 file: String,
257 line: usize,
259 },
260 UnmatchedElse { file: String, line: usize },
262 UnclosedConditional { file: String, line: usize },
264 SyntaxError {
266 file: String,
267 line: usize,
268 message: String,
269 },
270 ErrorDirective {
272 file: String,
274 line: usize,
276 message: String,
278 },
279 TrailingBackslash {
283 file: String,
285 },
286}
287
288impl core::fmt::Display for PreprocessError {
289 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
290 match self {
291 Self::IncludeNotFound(e) => write!(f, "include not found: {}", e.requested),
292 Self::IncludeCycle { file } => write!(f, "include cycle: {file}"),
293 Self::UnmatchedEndif { file, line } => {
294 write!(f, "unmatched #endif at {file}:{line}")
295 }
296 Self::UnmatchedElse { file, line } => {
297 write!(f, "unmatched #else at {file}:{line}")
298 }
299 Self::UnclosedConditional { file, line } => {
300 write!(f, "unclosed conditional starting at {file}:{line}")
301 }
302 Self::SyntaxError {
303 file,
304 line,
305 message,
306 } => {
307 write!(f, "preprocessor syntax error at {file}:{line}: {message}")
308 }
309 Self::ErrorDirective {
310 file,
311 line,
312 message,
313 } => {
314 write!(f, "#error at {file}:{line}: {message}")
315 }
316 Self::TrailingBackslash { file } => {
317 write!(f, "trailing backslash at end of source file: {file}")
318 }
319 }
320 }
321}
322
323impl std::error::Error for PreprocessError {}
324
325pub struct Preprocessor<R: Resolver> {
330 resolver: R,
331}
332
333impl<R: Resolver> Preprocessor<R> {
334 pub fn new(resolver: R) -> Self {
335 Self { resolver }
336 }
337
338 pub fn process(
346 &self,
347 file_name: &str,
348 source: &str,
349 ) -> Result<ProcessedSource, PreprocessError> {
350 let mut state = State::new();
351 let root_id = state.source_map.add_file(file_name);
352 let mut output = String::new();
353 let spliced = splice_backslash_newlines(source);
359 if spliced.ends_with('\\') {
363 return Err(PreprocessError::TrailingBackslash {
364 file: file_name.to_string(),
365 });
366 }
367 self.expand_into(file_name, &spliced, root_id, &mut state, &mut output, 0)?;
368 Ok(ProcessedSource {
369 expanded: output,
370 source_map: state.source_map,
371 pragma_keylists: state.pragma_keylists,
372 opensplice_pragmas: state.opensplice_pragmas,
373 pragma_prefixes: state.pragma_prefixes,
374 pragma_dds_xtopics: state.pragma_dds_xtopics,
375 })
376 }
377
378 fn expand_into(
379 &self,
380 file_name: &str,
381 source: &str,
382 file_id: FileId,
383 state: &mut State,
384 output: &mut String,
385 depth: usize,
386 ) -> Result<(), PreprocessError> {
387 if state.include_stack.iter().any(|f| f == file_name) {
388 return Err(PreprocessError::IncludeCycle {
389 file: file_name.to_string(),
390 });
391 }
392 state.include_stack.push(file_name.to_string());
393
394 let mut conditional_stack: Vec<ConditionalFrame> = Vec::new();
395 let mut byte_offset = 0usize;
396
397 for (line_idx, line) in source.split_inclusive('\n').enumerate() {
398 let line_no = line_idx + 1;
399 let trimmed = line.trim_start();
400
401 let active = conditional_stack.iter().all(|f| f.active);
404
405 if let Some(directive) = parse_directive(trimmed) {
406 match directive {
407 Directive::Ifdef(name) => {
408 let parent_active = active;
409 let cond = parent_active && state.macros.contains_key(name);
410 conditional_stack.push(ConditionalFrame {
411 active: cond,
412 else_seen: false,
413 parent_active,
414 taken: cond,
415 });
416 }
417 Directive::Ifndef(name) => {
418 let parent_active = active;
419 let cond = parent_active && !state.macros.contains_key(name);
420 conditional_stack.push(ConditionalFrame {
421 active: cond,
422 else_seen: false,
423 parent_active,
424 taken: cond,
425 });
426 }
427 Directive::Else => {
428 let frame = conditional_stack.last_mut().ok_or_else(|| {
429 PreprocessError::UnmatchedElse {
430 file: file_name.to_string(),
431 line: line_no,
432 }
433 })?;
434 if frame.else_seen {
435 return Err(PreprocessError::SyntaxError {
436 file: file_name.to_string(),
437 line: line_no,
438 message: "duplicate #else".to_string(),
439 });
440 }
441 frame.else_seen = true;
442 frame.active = frame.parent_active && !frame.taken;
445 if frame.active {
446 frame.taken = true;
447 }
448 }
449 Directive::Endif => {
450 if conditional_stack.pop().is_none() {
451 return Err(PreprocessError::UnmatchedEndif {
452 file: file_name.to_string(),
453 line: line_no,
454 });
455 }
456 }
457 Directive::If(expr) => {
458 let parent_active = active;
459 let cond = parent_active && eval_if_expr(expr, &state.macros);
460 conditional_stack.push(ConditionalFrame {
461 parent_active,
462 active: cond,
463 else_seen: false,
464 taken: cond,
465 });
466 }
467 Directive::Elif(expr) => {
468 let Some(frame) = conditional_stack.last_mut() else {
469 return Err(PreprocessError::UnmatchedEndif {
470 file: file_name.to_string(),
471 line: line_no,
472 });
473 };
474 if frame.else_seen {
475 return Err(PreprocessError::SyntaxError {
476 file: file_name.to_string(),
477 line: line_no,
478 message: "#elif after #else".to_string(),
479 });
480 }
481 let cond = frame.parent_active
484 && !frame.taken
485 && eval_if_expr(expr, &state.macros);
486 frame.active = cond;
487 if cond {
488 frame.taken = true;
489 }
490 }
491 _ if !active => {
492 }
495 Directive::Define(name, def) => {
496 state.macros.insert(name.to_string(), def);
497 }
498 Directive::Undef(name) => {
499 state.macros.remove(name);
500 }
501 Directive::Include(inc) => {
502 if depth > MAX_INCLUDE_DEPTH {
503 return Err(PreprocessError::SyntaxError {
504 file: file_name.to_string(),
505 line: line_no,
506 message: format!("include depth exceeded {MAX_INCLUDE_DEPTH}"),
507 });
508 }
509 let inc_path = inc.path().to_string();
510 if state.include_stack.iter().any(|f| f == &inc_path) {
513 return Err(PreprocessError::IncludeCycle { file: inc_path });
514 }
515 let included = self
516 .resolver
517 .resolve(file_name, &inc)
518 .map_err(PreprocessError::IncludeNotFound)?;
519 let inc_id = state.source_map.add_file(&inc_path);
520 self.expand_into(&inc_path, &included, inc_id, state, output, depth + 1)?;
521 }
522 Directive::Pragma(args) => {
523 if let Some(keylist) = parse_pragma_keylist(args, file_name, line_no) {
524 state.pragma_keylists.push(keylist);
525 } else if let Some(osp) = parse_opensplice_pragma(args, file_name, line_no)
526 {
527 state.opensplice_pragmas.push(osp);
528 } else if let Some(pp) = parse_pragma_prefix(args, file_name, line_no) {
529 state.pragma_prefixes.push(pp);
530 } else if let Some(xt) = parse_pragma_dds_xtopics(args, file_name, line_no)
531 {
532 state.pragma_dds_xtopics.push(xt);
533 }
534 }
536 Directive::Error(msg) => {
537 return Err(PreprocessError::ErrorDirective {
538 file: file_name.to_string(),
539 line: line_no,
540 message: msg.trim().to_string(),
541 });
542 }
543 Directive::Warning(_msg) => {
544 }
548 Directive::Line(_args) => {
549 }
552 }
553 } else if active {
554 let expanded = expand_macros(line, &state.macros);
557 state
558 .source_map
559 .record_segment(output.len(), expanded.len(), file_id, byte_offset);
560 output.push_str(&expanded);
561 }
562
563 byte_offset += line.len();
564 }
565
566 if let Some(frame) = conditional_stack.first() {
567 let _ = frame;
568 return Err(PreprocessError::UnclosedConditional {
569 file: file_name.to_string(),
570 line: 0,
571 });
572 }
573
574 state.include_stack.pop();
575 Ok(())
576 }
577}
578
579const MAX_INCLUDE_DEPTH: usize = 64;
580
581struct State {
582 macros: HashMap<String, MacroDef>,
583 include_stack: Vec<String>,
584 source_map: SourceMap,
585 pragma_keylists: Vec<PragmaKeylist>,
586 opensplice_pragmas: Vec<OpenSplicePragma>,
587 pragma_prefixes: Vec<PragmaPrefix>,
588 pragma_dds_xtopics: Vec<PragmaDdsXtopics>,
589}
590
591impl State {
592 fn new() -> Self {
593 Self {
594 macros: HashMap::new(),
595 include_stack: Vec::new(),
596 source_map: SourceMap::new(),
597 pragma_keylists: Vec::new(),
598 opensplice_pragmas: Vec::new(),
599 pragma_prefixes: Vec::new(),
600 pragma_dds_xtopics: Vec::new(),
601 }
602 }
603}
604
605#[derive(Clone, Debug, PartialEq, Eq)]
608struct MacroDef {
609 params: Option<Vec<String>>,
612 body: String,
614}
615
616impl MacroDef {
617 fn object_like(body: &str) -> Self {
618 Self {
619 params: None,
620 body: body.to_string(),
621 }
622 }
623
624 fn function_like(params: Vec<String>, body: &str) -> Self {
625 Self {
626 params: Some(params),
627 body: body.to_string(),
628 }
629 }
630}
631
632fn splice_backslash_newlines(src: &str) -> String {
634 let bytes = src.as_bytes();
636 let mut out = Vec::with_capacity(bytes.len());
637 let mut i = 0;
638 while i < bytes.len() {
639 if bytes[i] == b'\\' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
640 i += 2;
642 continue;
643 }
644 if bytes[i] == b'\\'
645 && i + 2 < bytes.len()
646 && bytes[i + 1] == b'\r'
647 && bytes[i + 2] == b'\n'
648 {
649 i += 3;
650 continue;
651 }
652 out.push(bytes[i]);
653 i += 1;
654 }
655 String::from_utf8(out).unwrap_or_default()
657}
658
659struct ConditionalFrame {
660 active: bool,
662 else_seen: bool,
664 parent_active: bool,
667 taken: bool,
670}
671
672#[derive(Debug, PartialEq, Eq)]
674enum Directive<'a> {
675 Include(Include),
676 Define(&'a str, MacroDef),
677 Undef(&'a str),
678 Ifdef(&'a str),
679 Ifndef(&'a str),
680 If(&'a str),
684 Elif(&'a str),
686 Else,
687 Endif,
688 Pragma(&'a str),
689 Error(&'a str),
691 Warning(&'a str),
694 Line(&'a str),
697}
698
699fn parse_directive(line: &str) -> Option<Directive<'_>> {
700 let stripped = line.strip_prefix('#')?.trim_start();
701 let (head, rest) = match stripped.find(|c: char| c.is_whitespace()) {
702 Some(idx) => (&stripped[..idx], stripped[idx..].trim()),
703 None => (stripped.trim_end(), ""),
704 };
705 match head {
706 "include" => parse_include(rest).map(Directive::Include),
707 "define" => parse_define(rest),
708 "undef" => Some(Directive::Undef(rest)),
709 "ifdef" => Some(Directive::Ifdef(rest)),
710 "ifndef" => Some(Directive::Ifndef(rest)),
711 "if" => Some(Directive::If(rest)),
712 "elif" => Some(Directive::Elif(rest)),
713 "else" => Some(Directive::Else),
714 "endif" => Some(Directive::Endif),
715 "pragma" => Some(Directive::Pragma(rest)),
716 "error" => Some(Directive::Error(rest)),
717 "warning" => Some(Directive::Warning(rest)),
718 "line" => Some(Directive::Line(rest)),
719 _ => None,
720 }
721}
722
723fn eval_if_expr(expr: &str, macros: &HashMap<String, MacroDef>) -> bool {
736 let trimmed = expr.trim();
737 if trimmed.is_empty() {
738 return false;
739 }
740 let normalized = normalize_if_tokens(trimmed);
742 eval_if_tokens(&normalized, macros)
743}
744
745fn normalize_if_tokens(expr: &str) -> Vec<String> {
746 let mut out = Vec::new();
747 let mut chars = expr.chars().peekable();
748 while let Some(c) = chars.next() {
749 match c {
750 ' ' | '\t' => {}
751 '(' | ')' | '!' => out.push(c.to_string()),
752 '&' if chars.peek() == Some(&'&') => {
753 chars.next();
754 out.push("&&".into());
755 }
756 '|' if chars.peek() == Some(&'|') => {
757 chars.next();
758 out.push("||".into());
759 }
760 c if c.is_ascii_alphabetic() || c == '_' => {
761 let mut buf = String::from(c);
762 while let Some(&n) = chars.peek() {
763 if n.is_ascii_alphanumeric() || n == '_' {
764 buf.push(n);
765 chars.next();
766 } else {
767 break;
768 }
769 }
770 out.push(buf);
771 }
772 c if c.is_ascii_digit() => {
773 let mut buf = String::from(c);
774 while let Some(&n) = chars.peek() {
775 if n.is_ascii_digit() {
776 buf.push(n);
777 chars.next();
778 } else {
779 break;
780 }
781 }
782 out.push(buf);
783 }
784 _ => {} }
786 }
787 out
788}
789
790fn eval_if_tokens(tokens: &[String], macros: &HashMap<String, MacroDef>) -> bool {
792 let (val, _) = eval_or(tokens, 0, macros);
793 val
794}
795
796fn eval_or(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
797 let (mut left, mut i) = eval_and(tokens, idx, macros);
798 while tokens.get(i).map(String::as_str) == Some("||") {
799 let (right, ni) = eval_and(tokens, i + 1, macros);
800 left = left || right;
801 i = ni;
802 }
803 (left, i)
804}
805
806fn eval_and(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
807 let (mut left, mut i) = eval_not(tokens, idx, macros);
808 while tokens.get(i).map(String::as_str) == Some("&&") {
809 let (right, ni) = eval_not(tokens, i + 1, macros);
810 left = left && right;
811 i = ni;
812 }
813 (left, i)
814}
815
816fn eval_not(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
818 if tokens.get(idx).map(String::as_str) == Some("!") {
819 let (v, ni) = eval_not(tokens, idx + 1, macros);
820 return (!v, ni);
821 }
822 eval_atom(tokens, idx, macros)
823}
824
825fn eval_atom(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
826 let Some(tok) = tokens.get(idx) else {
827 return (false, idx);
828 };
829 if tok == "(" {
830 let (v, ni) = eval_or(tokens, idx + 1, macros);
831 let after = if tokens.get(ni).map(String::as_str) == Some(")") {
832 ni + 1
833 } else {
834 ni
835 };
836 return (v, after);
837 }
838 if tok == "defined" {
839 let (next_idx, ident) = if tokens.get(idx + 1).map(String::as_str) == Some("(") {
841 (
842 idx + 3,
843 tokens.get(idx + 2).map(String::as_str).unwrap_or(""),
844 )
845 } else {
846 (
847 idx + 2,
848 tokens.get(idx + 1).map(String::as_str).unwrap_or(""),
849 )
850 };
851 let v = macros.contains_key(ident);
852 let after = if tokens.get(idx + 1).map(String::as_str) == Some("(") {
853 if tokens.get(next_idx).map(String::as_str) == Some(")") {
855 next_idx + 1
856 } else {
857 next_idx
858 }
859 } else {
860 next_idx
861 };
862 return (v, after);
863 }
864 if let Ok(n) = tok.parse::<i64>() {
866 return (n != 0, idx + 1);
867 }
868 if let Some(def) = macros.get(tok) {
873 if let Ok(n) = def.body.trim().parse::<i64>() {
874 return (n != 0, idx + 1);
875 }
876 return (true, idx + 1);
877 }
878 (false, idx + 1)
880}
881
882fn parse_include(rest: &str) -> Option<Include> {
883 let rest = rest.trim();
884 if let Some(stripped) = rest.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
885 return Some(Include::Quoted(stripped.to_string()));
886 }
887 rest.strip_prefix('<')
888 .and_then(|s| s.strip_suffix('>'))
889 .map(|stripped| Include::System(stripped.to_string()))
890}
891
892fn parse_define(rest: &str) -> Option<Directive<'_>> {
893 let rest = rest.trim_end_matches('\n').trim();
894 if rest.is_empty() {
895 return None;
896 }
897 let name_end = rest
901 .find(|c: char| c.is_whitespace() || c == '(')
902 .unwrap_or(rest.len());
903 let name = &rest[..name_end];
904 if name.is_empty() {
905 return None;
906 }
907 let after_name = &rest[name_end..];
908 if let Some(after_paren) = after_name.strip_prefix('(') {
909 let close = after_paren.find(')')?;
911 let params_src = &after_paren[..close];
912 let body = after_paren[close + 1..].trim();
913 let params: Vec<String> = if params_src.trim().is_empty() {
914 Vec::new()
915 } else {
916 params_src
917 .split(',')
918 .map(|p| p.trim().to_string())
919 .collect()
920 };
921 return Some(Directive::Define(
922 name,
923 MacroDef::function_like(params, body),
924 ));
925 }
926 let body = after_name.trim();
927 Some(Directive::Define(name, MacroDef::object_like(body)))
928}
929
930fn parse_pragma_prefix(args: &str, file: &str, line: usize) -> Option<PragmaPrefix> {
935 let trimmed = args.trim();
936 let rest = trimmed.strip_prefix("prefix")?.trim_start();
937 let prefix = strip_optional_quotes(rest).trim().to_string();
938 if prefix.is_empty() {
939 return None;
940 }
941 Some(PragmaPrefix {
942 prefix,
943 file: file.to_string(),
944 line,
945 })
946}
947
948fn parse_pragma_dds_xtopics(args: &str, file: &str, line: usize) -> Option<PragmaDdsXtopics> {
952 let trimmed = args.trim();
953 let rest = trimmed.strip_prefix("dds_xtopics")?.trim_start();
954 let version = if rest.is_empty() {
956 String::new()
957 } else if let Some(v) = rest.strip_prefix("version") {
958 v.trim_start()
959 .strip_prefix('=')
960 .unwrap_or(v)
961 .trim()
962 .trim_matches('"')
963 .to_string()
964 } else {
965 rest.trim_matches('"').to_string()
966 };
967 Some(PragmaDdsXtopics {
968 version,
969 file: file.to_string(),
970 line,
971 })
972}
973
974fn parse_pragma_keylist(args: &str, file: &str, line: usize) -> Option<PragmaKeylist> {
978 let trimmed = args.trim();
979 let rest = trimmed.strip_prefix("keylist")?.trim_start();
980 let mut parts = rest.split_whitespace();
981 let type_name = parts.next()?.to_string();
982 let keys: Vec<String> = parts.map(str::to_string).collect();
983 Some(PragmaKeylist {
984 type_name,
985 keys,
986 file: file.to_string(),
987 line,
988 })
989}
990
991fn parse_opensplice_pragma(args: &str, file: &str, line: usize) -> Option<OpenSplicePragma> {
994 let trimmed = args.trim();
995 if let Some(rest) = trimmed.strip_prefix("DCPS_DATA_TYPE") {
996 let payload = rest.trim();
997 let type_name = strip_optional_quotes(payload).to_string();
999 if type_name.is_empty() {
1000 return None;
1001 }
1002 return Some(OpenSplicePragma::DataType {
1003 type_name,
1004 file: file.to_string(),
1005 line,
1006 });
1007 }
1008 if let Some(rest) = trimmed.strip_prefix("DCPS_DATA_KEY") {
1009 let payload = strip_optional_quotes(rest.trim());
1010 let dot = payload.find('.')?;
1011 let type_name = payload[..dot].trim().to_string();
1012 let field = payload[dot + 1..].trim().to_string();
1013 if type_name.is_empty() || field.is_empty() {
1014 return None;
1015 }
1016 return Some(OpenSplicePragma::DataKey {
1017 type_name,
1018 field,
1019 file: file.to_string(),
1020 line,
1021 });
1022 }
1023 if let Some(rest) = trimmed.strip_prefix("cats") {
1024 let mut parts = rest.split_whitespace();
1025 let type_name = parts.next()?.to_string();
1026 let keys: Vec<String> = parts.map(str::to_string).collect();
1027 if keys.is_empty() {
1028 return None;
1029 }
1030 return Some(OpenSplicePragma::Cats {
1031 type_name,
1032 keys,
1033 file: file.to_string(),
1034 line,
1035 });
1036 }
1037 if trimmed == "genequality" {
1038 return Some(OpenSplicePragma::GenEquality {
1039 file: file.to_string(),
1040 line,
1041 });
1042 }
1043 None
1044}
1045
1046fn strip_optional_quotes(s: &str) -> &str {
1047 let s = s.trim();
1048 s.strip_prefix('"')
1049 .and_then(|t| t.strip_suffix('"'))
1050 .unwrap_or(s)
1051}
1052
1053fn expand_macros(line: &str, macros: &HashMap<String, MacroDef>) -> String {
1057 expand_macros_rec(line, macros, 0)
1058}
1059
1060const MAX_MACRO_EXPANSION_DEPTH: usize = 32;
1065
1066fn expand_macros_rec(line: &str, macros: &HashMap<String, MacroDef>, depth: usize) -> String {
1068 if macros.is_empty() || depth >= MAX_MACRO_EXPANSION_DEPTH {
1069 return line.to_string();
1070 }
1071 let mut out = String::with_capacity(line.len());
1072 let bytes = line.as_bytes();
1073 let mut i = 0;
1074 let mut expanded_any = false;
1075 while i < bytes.len() {
1076 let c = bytes[i];
1077 if c.is_ascii_alphabetic() || c == b'_' {
1078 let start = i;
1080 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1081 i += 1;
1082 }
1083 let ident = &line[start..i];
1084 let Some(def) = macros.get(ident) else {
1085 out.push_str(ident);
1086 continue;
1087 };
1088 expanded_any = true;
1089 match &def.params {
1090 None => out.push_str(&def.body),
1091 Some(params) => {
1092 let after = skip_ascii_ws(bytes, i);
1095 if after >= bytes.len() || bytes[after] != b'(' {
1096 out.push_str(ident);
1097 continue;
1098 }
1099 let Some((args, end)) = parse_call_args(line, after) else {
1100 out.push_str(ident);
1101 continue;
1102 };
1103 let expanded = expand_function_like(params, &args, &def.body);
1104 out.push_str(&expanded);
1105 i = end;
1106 }
1107 }
1108 } else {
1109 out.push(c as char);
1110 i += 1;
1111 }
1112 }
1113 if expanded_any && out != line {
1118 return expand_macros_rec(&out, macros, depth + 1);
1119 }
1120 out
1121}
1122
1123fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
1124 while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
1125 i += 1;
1126 }
1127 i
1128}
1129
1130fn parse_call_args(line: &str, start: usize) -> Option<(Vec<String>, usize)> {
1134 let bytes = line.as_bytes();
1135 debug_assert_eq!(bytes.get(start), Some(&b'('));
1136 let mut i = start + 1;
1137 let mut depth: usize = 1;
1138 let mut args: Vec<String> = Vec::new();
1139 let mut cur = String::new();
1140 while i < bytes.len() {
1141 let c = bytes[i] as char;
1142 match c {
1143 '(' => {
1144 depth += 1;
1145 cur.push(c);
1146 i += 1;
1147 }
1148 ')' => {
1149 depth -= 1;
1150 if depth == 0 {
1151 args.push(cur.trim().to_string());
1152 return Some((args, i + 1));
1153 }
1154 cur.push(c);
1155 i += 1;
1156 }
1157 ',' if depth == 1 => {
1158 args.push(cur.trim().to_string());
1159 cur.clear();
1160 i += 1;
1161 }
1162 _ => {
1163 cur.push(c);
1164 i += 1;
1165 }
1166 }
1167 }
1168 None
1169}
1170
1171fn expand_function_like(params: &[String], args: &[String], body: &str) -> String {
1175 let arg_for = |name: &str| -> Option<&str> {
1176 params
1177 .iter()
1178 .position(|p| p == name)
1179 .and_then(|idx| args.get(idx).map(String::as_str))
1180 };
1181 let mut tokens: Vec<BodyTok> = Vec::new();
1183 let bytes = body.as_bytes();
1184 let mut i = 0;
1185 while i < bytes.len() {
1186 let c = bytes[i];
1187 if c.is_ascii_alphabetic() || c == b'_' {
1188 let start = i;
1189 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1190 i += 1;
1191 }
1192 tokens.push(BodyTok::Ident(body[start..i].to_string()));
1193 } else if c == b'#' && i + 1 < bytes.len() && bytes[i + 1] == b'#' {
1194 tokens.push(BodyTok::Paste);
1195 i += 2;
1196 } else if c == b'#' {
1197 tokens.push(BodyTok::Stringize);
1198 i += 1;
1199 } else {
1200 tokens.push(BodyTok::Other((c as char).to_string()));
1201 i += 1;
1202 }
1203 }
1204 let mut after_paste: Vec<BodyTok> = Vec::with_capacity(tokens.len());
1209 let mut k = 0;
1210 while k < tokens.len() {
1211 if matches!(tokens[k], BodyTok::Paste) {
1212 let mut lhs: Option<BodyTok> = None;
1216 while let Some(last) = after_paste.last() {
1217 if let BodyTok::Other(s) = last {
1218 if s.chars().all(char::is_whitespace) {
1219 after_paste.pop();
1220 continue;
1221 }
1222 }
1223 lhs = after_paste.pop();
1224 break;
1225 }
1226 let rhs_idx = skip_body_ws(&tokens, k + 1);
1227 match (lhs, tokens.get(rhs_idx)) {
1228 (Some(lhs_tok), Some(rhs_tok)) => {
1229 let lhs_text = render_tok(&lhs_tok, params, args);
1230 let rhs_text = render_tok(rhs_tok, params, args);
1231 after_paste.push(BodyTok::Ident(format!("{lhs_text}{rhs_text}")));
1232 k = rhs_idx + 1;
1233 }
1234 _ => {
1235 after_paste.push(BodyTok::Paste);
1237 k += 1;
1238 }
1239 }
1240 } else {
1241 after_paste.push(tokens[k].clone());
1242 k += 1;
1243 }
1244 }
1245 let mut out = String::new();
1247 let mut j = 0;
1248 while j < after_paste.len() {
1249 match &after_paste[j] {
1250 BodyTok::Stringize => {
1251 let target_idx = skip_body_ws(&after_paste, j + 1);
1252 let arg_text = match after_paste.get(target_idx) {
1253 Some(BodyTok::Ident(name)) => arg_for(name).unwrap_or(name).to_string(),
1254 _ => String::new(),
1255 };
1256 out.push('"');
1257 for ch in arg_text.chars() {
1258 if ch == '"' || ch == '\\' {
1259 out.push('\\');
1260 }
1261 out.push(ch);
1262 }
1263 out.push('"');
1264 j = target_idx + 1;
1265 }
1266 BodyTok::Ident(name) => {
1267 if let Some(text) = arg_for(name) {
1268 out.push_str(text);
1269 } else {
1270 out.push_str(name);
1271 }
1272 j += 1;
1273 }
1274 BodyTok::Other(s) => {
1275 out.push_str(s);
1276 j += 1;
1277 }
1278 BodyTok::Paste => {
1279 out.push_str("##");
1282 j += 1;
1283 }
1284 }
1285 }
1286 out
1287}
1288
1289#[derive(Clone, Debug)]
1290enum BodyTok {
1291 Ident(String),
1292 Other(String),
1293 Stringize,
1294 Paste,
1295}
1296
1297fn skip_body_ws(tokens: &[BodyTok], mut i: usize) -> usize {
1298 while let Some(BodyTok::Other(s)) = tokens.get(i) {
1299 if !s.chars().all(char::is_whitespace) {
1300 break;
1301 }
1302 i += 1;
1303 }
1304 i
1305}
1306
1307fn render_tok(tok: &BodyTok, params: &[String], args: &[String]) -> String {
1308 match tok {
1309 BodyTok::Ident(name) => params
1310 .iter()
1311 .position(|p| p == name)
1312 .and_then(|idx| args.get(idx).cloned())
1313 .unwrap_or_else(|| name.clone()),
1314 BodyTok::Other(s) => s.clone(),
1315 BodyTok::Stringize => "#".to_string(),
1316 BodyTok::Paste => "##".to_string(),
1317 }
1318}
1319
1320#[cfg(test)]
1321mod tests {
1322 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
1323 use super::*;
1324
1325 fn run(src: &str) -> String {
1326 Preprocessor::new(MemoryResolver::new())
1327 .process("main.idl", src)
1328 .expect("ok")
1329 .expanded
1330 }
1331
1332 fn run_with(resolver: MemoryResolver, src: &str) -> String {
1333 Preprocessor::new(resolver)
1334 .process("main.idl", src)
1335 .expect("ok")
1336 .expanded
1337 }
1338
1339 #[test]
1340 fn passthrough_for_source_without_directives() {
1341 let out = run("struct Foo { long x; };\n");
1342 assert!(out.contains("struct Foo"));
1343 }
1344
1345 #[test]
1346 fn pragma_is_stripped() {
1347 let out = run("#pragma keylist Foo x\nstruct Foo { long x; };\n");
1348 assert!(!out.contains("#pragma"));
1349 assert!(out.contains("struct Foo"));
1350 }
1351
1352 #[test]
1353 fn define_object_like_substitutes_in_subsequent_lines() {
1354 let out = run("#define MAX 100\nconst long L = MAX;\n");
1355 assert!(out.contains("const long L = 100;"), "{out}");
1356 assert!(!out.contains("#define"));
1357 }
1358
1359 #[test]
1360 fn ifdef_keeps_block_when_macro_defined() {
1361 let out = run("#define WITH\n#ifdef WITH\nstruct A {};\n#endif\n");
1362 assert!(out.contains("struct A"), "{out}");
1363 }
1364
1365 #[test]
1366 fn ifdef_drops_block_when_macro_not_defined() {
1367 let out = run("#ifdef WITH\nstruct A {};\n#endif\n");
1368 assert!(!out.contains("struct A"), "{out}");
1369 }
1370
1371 #[test]
1372 fn ifndef_inverse_of_ifdef() {
1373 let out = run("#ifndef WITH\nstruct B {};\n#endif\n");
1374 assert!(out.contains("struct B"), "{out}");
1375 }
1376
1377 #[test]
1378 fn else_branch_taken_when_initial_false() {
1379 let out = run("#ifdef NOPE\nstruct A {};\n#else\nstruct B {};\n#endif\n");
1380 assert!(!out.contains("struct A"), "{out}");
1381 assert!(out.contains("struct B"), "{out}");
1382 }
1383
1384 #[test]
1385 fn nested_ifdef_works() {
1386 let out = run("#define X\n\
1387 #ifdef X\n\
1388 #ifdef Y\nstruct YY {};\n#else\nstruct XnotY {};\n#endif\n\
1389 #endif\n");
1390 assert!(out.contains("struct XnotY"), "{out}");
1391 assert!(!out.contains("struct YY"));
1392 }
1393
1394 #[test]
1395 fn undef_removes_macro() {
1396 let out = run("#define M\n#undef M\n#ifdef M\nA\n#endif\n");
1397 assert!(!out.contains('A'), "{out}");
1398 }
1399
1400 #[test]
1401 fn quoted_include_resolves() {
1402 let mut r = MemoryResolver::new();
1403 r.add("inc.idl", "struct Inc {};\n");
1404 let out = run_with(r, "#include \"inc.idl\"\nstruct Main {};\n");
1405 assert!(out.contains("struct Inc"), "{out}");
1406 assert!(out.contains("struct Main"), "{out}");
1407 }
1408
1409 #[test]
1410 fn system_include_resolves() {
1411 let mut r = MemoryResolver::new();
1412 r.add("sys.idl", "struct Sys {};\n");
1413 let out = run_with(r, "#include <sys.idl>\nstruct Main {};\n");
1414 assert!(out.contains("struct Sys"), "{out}");
1415 }
1416
1417 #[test]
1418 fn missing_include_is_error() {
1419 let res = Preprocessor::new(MemoryResolver::new())
1420 .process("main.idl", "#include \"missing.idl\"\n");
1421 assert!(matches!(res, Err(PreprocessError::IncludeNotFound(_))));
1422 }
1423
1424 #[test]
1425 fn include_cycle_is_detected() {
1426 let mut r = MemoryResolver::new();
1427 r.add("a.idl", "#include \"main.idl\"\n");
1428 let res = Preprocessor::new(r).process("main.idl", "#include \"a.idl\"\n");
1429 assert!(matches!(res, Err(PreprocessError::IncludeCycle { .. })));
1430 }
1431
1432 #[test]
1433 fn unmatched_endif_is_error() {
1434 let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#endif\n");
1435 assert!(matches!(res, Err(PreprocessError::UnmatchedEndif { .. })));
1436 }
1437
1438 #[test]
1439 fn unclosed_conditional_is_error() {
1440 let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#ifdef X\n");
1441 assert!(matches!(
1442 res,
1443 Err(PreprocessError::UnclosedConditional { .. })
1444 ));
1445 }
1446
1447 #[test]
1448 fn unmatched_else_is_error() {
1449 let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#else\n");
1450 assert!(matches!(res, Err(PreprocessError::UnmatchedElse { .. })));
1451 }
1452
1453 #[test]
1454 fn macro_in_inactive_branch_does_not_take_effect() {
1455 let out = run("#ifdef NOPE\n#define M 99\n#endif\n#ifdef M\nseen\n#endif\n");
1456 assert!(!out.contains("seen"));
1457 }
1458
1459 #[test]
1460 fn source_map_records_segments() {
1461 let result = Preprocessor::new(MemoryResolver::new())
1462 .process("main.idl", "struct A {};\nstruct B {};\n")
1463 .expect("ok");
1464 assert!(
1466 result.source_map.segment_count() >= 2,
1467 "got {} segments",
1468 result.source_map.segment_count()
1469 );
1470 }
1471
1472 #[test]
1473 fn expand_macros_skips_unknown_identifiers() {
1474 let macros = HashMap::new();
1475 let out = expand_macros("foo bar baz", ¯os);
1476 assert_eq!(out, "foo bar baz");
1477 }
1478
1479 #[test]
1480 fn expand_macros_substitutes_only_full_idents() {
1481 let mut m = HashMap::new();
1482 m.insert("X".to_string(), MacroDef::object_like("100"));
1483 let out = expand_macros("X XY", &m);
1485 assert_eq!(out, "100 XY");
1486 }
1487
1488 #[test]
1493 fn if_eval_defined_macro_keeps_block() {
1494 let src = "\
1495#define FOO 1
1496#if defined(FOO)
1497struct InFoo { long x; };
1498#endif
1499struct After { long y; };
1500";
1501 let out = run(src);
1502 assert!(out.contains("struct InFoo"), "got: {out}");
1503 assert!(out.contains("struct After"), "got: {out}");
1504 }
1505
1506 #[test]
1507 fn if_eval_undefined_macro_drops_block() {
1508 let src = "\
1509#if defined(FOO)
1510struct ShouldBeGone { long x; };
1511#endif
1512struct Visible { long y; };
1513";
1514 let out = run(src);
1515 assert!(!out.contains("ShouldBeGone"), "got: {out}");
1516 assert!(out.contains("struct Visible"));
1517 }
1518
1519 #[test]
1520 fn if_eval_numeric_zero_drops_block() {
1521 let src = "#if 0\nstruct X { long x; };\n#endif\nstruct Y {};\n";
1522 let out = run(src);
1523 assert!(!out.contains("struct X"), "got: {out}");
1524 }
1525
1526 #[test]
1527 fn if_eval_numeric_nonzero_keeps_block() {
1528 let src = "#if 1\nstruct X { long x; };\n#endif\n";
1529 let out = run(src);
1530 assert!(out.contains("struct X"), "got: {out}");
1531 }
1532
1533 #[test]
1534 fn if_eval_logical_or() {
1535 let src = "\
1536#define A 1
1537#if defined(A) || defined(B)
1538struct Match { long m; };
1539#endif
1540";
1541 let out = run(src);
1542 assert!(out.contains("struct Match"), "got: {out}");
1543 }
1544
1545 #[test]
1546 fn if_eval_logical_not() {
1547 let src = "#if !defined(NOT_DEFINED)\nstruct K {};\n#endif\n";
1548 let out = run(src);
1549 assert!(out.contains("struct K"), "got: {out}");
1550 }
1551
1552 #[test]
1553 fn if_eval_logical_and_both_defined_keeps_block() {
1554 let src = "\
1556#define A 1
1557#define B 1
1558#if defined(A) && defined(B)
1559struct Both {};
1560#endif
1561";
1562 let out = run(src);
1563 assert!(out.contains("struct Both"), "got: {out}");
1564 }
1565
1566 #[test]
1567 fn if_eval_logical_and_one_undefined_drops_block() {
1568 let src = "\
1570#define A 1
1571#if defined(A) && defined(NOT_DEFINED)
1572struct OnlyA {};
1573#endif
1574";
1575 let out = run(src);
1576 assert!(!out.contains("struct OnlyA"), "got: {out}");
1577 }
1578
1579 #[test]
1580 fn if_eval_logical_and_both_undefined_drops_block() {
1581 let src = "\
1583#if defined(NOT_A) && defined(NOT_B)
1584struct Neither {};
1585#endif
1586";
1587 let out = run(src);
1588 assert!(!out.contains("struct Neither"), "got: {out}");
1589 }
1590
1591 #[test]
1592 fn if_elif_else_branches() {
1593 let src = "\
1594#if defined(NOT_DEFINED)
1595struct One {};
1596#elif defined(MODE)
1597struct WithMode {};
1598#else
1599struct Default {};
1600#endif
1601";
1602 let out = run(src);
1604 assert!(out.contains("struct Default"), "got: {out}");
1605 assert!(!out.contains("struct One"));
1606 assert!(!out.contains("struct WithMode"));
1607 }
1608
1609 #[test]
1610 fn if_elif_picks_first_true_branch() {
1611 let src = "\
1612#define MODE 1
1613#if defined(NOT_DEFINED)
1614struct A {};
1615#elif defined(MODE)
1616struct B {};
1617#elif defined(ANOTHER)
1618struct C {};
1619#else
1620struct D {};
1621#endif
1622";
1623 let out = run(src);
1624 assert!(out.contains("struct B"), "got: {out}");
1625 assert!(!out.contains("struct A"));
1626 assert!(!out.contains("struct C"));
1627 assert!(!out.contains("struct D"));
1628 }
1629
1630 #[test]
1631 fn warning_directive_does_not_abort() {
1632 let src = "#warning this is a warning\nstruct OK {};\n";
1633 let out = run(src);
1634 assert!(out.contains("struct OK"), "got: {out}");
1635 }
1636
1637 #[test]
1638 fn line_directive_does_not_abort() {
1639 let src = "#line 42 \"original.idl\"\nstruct X {};\n";
1640 let out = run(src);
1641 assert!(out.contains("struct X"), "got: {out}");
1642 }
1643
1644 #[test]
1650 fn opensplice_pragma_data_type_quoted() {
1651 let src = r#"#pragma DCPS_DATA_TYPE "Sensor"
1652struct Sensor { long id; };
1653"#;
1654 let res = Preprocessor::new(MemoryResolver::new())
1655 .process("main.idl", src)
1656 .expect("ok");
1657 assert_eq!(res.opensplice_pragmas.len(), 1);
1658 match &res.opensplice_pragmas[0] {
1659 OpenSplicePragma::DataType { type_name, .. } => {
1660 assert_eq!(type_name, "Sensor");
1661 }
1662 other => panic!("expected DataType, got {other:?}"),
1663 }
1664 }
1665
1666 #[test]
1667 fn opensplice_pragma_data_type_unquoted() {
1668 let src = "#pragma DCPS_DATA_TYPE Sensor\nstruct Sensor {};\n";
1669 let res = Preprocessor::new(MemoryResolver::new())
1670 .process("main.idl", src)
1671 .expect("ok");
1672 match &res.opensplice_pragmas[0] {
1673 OpenSplicePragma::DataType { type_name, .. } => {
1674 assert_eq!(type_name, "Sensor");
1675 }
1676 other => panic!("expected DataType, got {other:?}"),
1677 }
1678 }
1679
1680 #[test]
1681 fn opensplice_pragma_data_key() {
1682 let src = r#"#pragma DCPS_DATA_KEY "Sensor.id"
1683struct Sensor { long id; };
1684"#;
1685 let res = Preprocessor::new(MemoryResolver::new())
1686 .process("main.idl", src)
1687 .expect("ok");
1688 match &res.opensplice_pragmas[0] {
1689 OpenSplicePragma::DataKey {
1690 type_name, field, ..
1691 } => {
1692 assert_eq!(type_name, "Sensor");
1693 assert_eq!(field, "id");
1694 }
1695 other => panic!("expected DataKey, got {other:?}"),
1696 }
1697 }
1698
1699 #[test]
1700 fn opensplice_pragma_cats() {
1701 let src = "#pragma cats Sensor id sub_id\nstruct Sensor {};\n";
1702 let res = Preprocessor::new(MemoryResolver::new())
1703 .process("main.idl", src)
1704 .expect("ok");
1705 match &res.opensplice_pragmas[0] {
1706 OpenSplicePragma::Cats {
1707 type_name, keys, ..
1708 } => {
1709 assert_eq!(type_name, "Sensor");
1710 assert_eq!(keys, &vec!["id".to_string(), "sub_id".to_string()]);
1711 }
1712 other => panic!("expected Cats, got {other:?}"),
1713 }
1714 }
1715
1716 #[test]
1717 fn opensplice_pragma_genequality() {
1718 let src = "#pragma genequality\nstruct S {};\n";
1719 let res = Preprocessor::new(MemoryResolver::new())
1720 .process("main.idl", src)
1721 .expect("ok");
1722 assert!(matches!(
1723 res.opensplice_pragmas.first(),
1724 Some(OpenSplicePragma::GenEquality { .. })
1725 ));
1726 }
1727
1728 #[test]
1729 fn opensplice_legacy_full_topic_decl() {
1730 let src = r#"#pragma DCPS_DATA_TYPE "Sensor"
1733#pragma DCPS_DATA_KEY "Sensor.id"
1734#pragma genequality
1735struct Sensor {
1736 long id;
1737 double value;
1738};
1739"#;
1740 let res = Preprocessor::new(MemoryResolver::new())
1741 .process("main.idl", src)
1742 .expect("ok");
1743 assert_eq!(res.opensplice_pragmas.len(), 3);
1744 assert!(res.expanded.contains("struct Sensor"));
1745 }
1746
1747 #[test]
1748 fn nested_if_in_active_branch() {
1749 let src = "\
1750#define OUTER 1
1751#if defined(OUTER)
1752#if defined(INNER)
1753struct ShouldBeGone {};
1754#else
1755struct InnerElse {};
1756#endif
1757#endif
1758";
1759 let out = run(src);
1760 assert!(out.contains("struct InnerElse"), "got: {out}");
1761 assert!(!out.contains("ShouldBeGone"), "got: {out}");
1762 }
1763
1764 #[test]
1769 fn leading_whitespace_before_hash_accepted() {
1770 let out = run(" #define X 1\nconst long Y = X;\n");
1772 assert!(out.contains("const long Y = 1;"), "got: {out}");
1773 }
1774
1775 #[test]
1780 fn line_continuation_in_define() {
1781 let out = run("#define LONG_MACRO foo \\\nbar\nLONG_MACRO\n");
1783 assert!(out.contains("foo bar"), "got: {out}");
1786 }
1787
1788 #[test]
1789 fn line_continuation_in_idl_line() {
1790 let out = run("const long\\\nX = 1;\n");
1792 assert!(!out.contains("long\nX"), "got: {out}");
1796 }
1797
1798 #[test]
1799 fn line_continuation_with_crlf() {
1800 let out = run("#define M foo \\\r\nbar\nM\n");
1803 assert!(out.contains("foo bar"), "got: {out}");
1804 }
1805
1806 #[test]
1807 fn multi_line_continuation() {
1808 let out = run("#define M a \\\nb \\\nc\nM\n");
1810 assert!(out.contains("a b c"), "got: {out}");
1811 }
1812
1813 #[test]
1818 fn trailing_backslash_at_file_end_is_error() {
1819 let result = Preprocessor::new(MemoryResolver::new()).process("main.idl", "foo\\");
1822 assert!(
1823 matches!(result, Err(PreprocessError::TrailingBackslash { .. })),
1824 "got: {result:?}"
1825 );
1826 }
1827
1828 #[test]
1833 fn function_like_macro_substitutes_args() {
1834 let src = "#define ADD(a, b) a + b\nconst long L = ADD(1, 2);\n";
1837 let out = run(src);
1838 assert!(out.contains("1 + 2"), "got: {out}");
1839 }
1840
1841 #[test]
1842 fn stringize_param_in_function_macro() {
1843 let src = "#define STR(x) #x\nconst string S = STR(hello);\n";
1846 let out = run(src);
1847 assert!(out.contains("\"hello\""), "got: {out}");
1848 }
1849
1850 #[test]
1851 fn stringize_escapes_quotes_and_backslashes() {
1852 let src = "#define STR(x) #x\nconst string S = STR(a\"b\\c);\n";
1855 let out = run(src);
1856 assert!(out.contains("\"a\\\"b\\\\c\""), "got: {out}");
1857 }
1858
1859 #[test]
1860 fn token_paste_concatenates_idents() {
1861 let src = "#define CAT(a, b) a##b\nconst long CAT(foo, bar) = 0;\n";
1863 let out = run(src);
1864 assert!(out.contains("foobar"), "got: {out}");
1865 }
1866
1867 #[test]
1868 fn token_paste_with_macro_args_produces_single_ident() {
1869 let src = "#define CAT(a, b) a ## b\nconst long CAT(x, y) = 0;\n";
1871 let out = run(src);
1872 assert!(out.contains("xy"), "got: {out}");
1873 }
1874
1875 fn process(src: &str) -> ProcessedSource {
1878 Preprocessor::new(MemoryResolver::new())
1879 .process("main.idl", src)
1880 .expect("ok")
1881 }
1882
1883 #[test]
1884 fn pragma_dds_xtopics_version_match() {
1885 let out = process("#pragma dds_xtopics version=\"1.3\"\nstruct S { long x; };\n");
1886 assert_eq!(out.pragma_dds_xtopics.len(), 1);
1887 assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
1888 }
1889
1890 #[test]
1891 fn pragma_dds_xtopics_version_mismatch_warns() {
1892 let out = process(
1896 "#pragma dds_xtopics version=\"1.0\"\n\
1897 #pragma dds_xtopics version=\"1.3\"\n\
1898 struct S { long x; };\n",
1899 );
1900 assert_eq!(out.pragma_dds_xtopics.len(), 2);
1901 let versions: Vec<&str> = out
1902 .pragma_dds_xtopics
1903 .iter()
1904 .map(|p| p.version.as_str())
1905 .collect();
1906 assert!(versions.contains(&"1.0"));
1907 assert!(versions.contains(&"1.3"));
1908 }
1909
1910 #[test]
1911 fn pragma_dds_xtopics_nested_pragmas_handled() {
1912 let out = process(
1915 "#pragma prefix \"acme.com\"\n\
1916 #pragma dds_xtopics version=\"1.3\"\n\
1917 #pragma keylist Topic key_field\n\
1918 struct Topic { long key_field; };\n",
1919 );
1920 assert_eq!(out.pragma_dds_xtopics.len(), 1);
1921 assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
1922 assert_eq!(out.pragma_prefixes.len(), 1);
1923 assert_eq!(out.pragma_keylists.len(), 1);
1924 }
1925
1926 #[test]
1927 fn pragma_dds_xtopics_without_version_value_is_empty() {
1928 let out = process("#pragma dds_xtopics\nstruct S { long x; };\n");
1931 assert_eq!(out.pragma_dds_xtopics.len(), 1);
1932 assert_eq!(out.pragma_dds_xtopics[0].version, "");
1933 }
1934
1935 #[test]
1938 fn nested_define_two_hops() {
1939 let out = run("#define A 100\n#define B A\nconst long x = B;\n");
1942 assert!(out.contains("const long x = 100;"), "{out}");
1943 }
1944
1945 #[test]
1946 fn nested_define_three_hops() {
1947 let out = run("#define A 7\n\
1948 #define B A\n\
1949 #define C B\n\
1950 const long x = C;\n");
1951 assert!(out.contains("const long x = 7;"), "{out}");
1952 }
1953
1954 #[test]
1955 fn nested_define_with_arithmetic_expression() {
1956 let out = run("#define UNIT 8\n\
1957 #define BUF (UNIT * 4)\n\
1958 const long x = BUF;\n");
1959 assert!(out.contains("(8 * 4)"), "{out}");
1961 }
1962
1963 #[test]
1964 fn nested_define_self_recursive_terminates() {
1965 let out = run("#define A A\nconst long x = A;\n");
1969 assert!(out.contains("const long x = A;"), "{out}");
1970 }
1971
1972 #[test]
1973 fn nested_define_mutually_recursive_terminates() {
1974 let out = run("#define A B\n#define B A\nconst long x = A;\n");
1976 assert!(out.contains("const long x ="));
1978 }
1979
1980 #[test]
1981 fn pragma_dds_xtopics_unquoted_version_accepted() {
1982 let out = process("#pragma dds_xtopics version=1.3\nstruct S { long x; };\n");
1983 assert_eq!(out.pragma_dds_xtopics.len(), 1);
1984 assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
1985 }
1986}