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