1use std::{borrow::Borrow, cell::Cell, ops::Range, path::PathBuf};
2
3use arcstr::{Substr, literal_substr};
4
5use crate::{
6 bsc::args::BscArgs,
7 project::Project,
8 syntax::{
9 TextChunk,
10 buildctx::BuildCtx,
11 diagnostic::Severity,
12 location::{
13 BUILTIN_ORIGIN, CLI_ORIGIN, FilePos, FileSpan, LineColCounter, Origin, OriginId,
14 OriginReason, OriginTable, Pos, ROOT_ORIGIN, Span,
15 },
16 streams::{CharStream, ChunkWriter, TextCharStream},
17 util::{bsv_horizontal_whitespace, bsv_ident, bsv_whitespace, vpp_malformed_param},
18 },
19 util::{
20 ext::{
21 arcstr::{EMPTY_SUBSTR, SubStrExt},
22 cell::CellUpdateExt,
23 },
24 path::maybe_canonicalize,
25 },
26};
27
28use super::Preprocessor;
29
30#[derive(Debug)]
31struct VppMacroDef {
32 name: Substr,
33 num_args: u32,
34 parts: Vec<VppMacroPart>,
35}
36
37#[derive(Debug)]
38enum VppMacroPart {
39 Arg(usize, Span),
40 Chunk(TextChunk),
41 Concat(Pos),
42}
43
44#[derive(Debug)]
45enum VppDefStmtKind {
46 Def(VppMacroDef),
47 Undef(Substr),
48 UndefAll,
49}
50
51#[derive(Debug)]
52struct VppDefStmt {
53 span: Span,
54 kind: VppDefStmtKind,
55}
56
57#[derive(Debug)]
58pub struct VppDefs {
59 items: Vec<VppDefStmt>,
60}
61
62impl VppDefs {
63 fn populate(cli: &BscArgs) -> Self {
64 let mut items = Vec::new();
65
66 items.push(VppDefStmt {
67 span: Span {
68 origin: BUILTIN_ORIGIN,
69 text_span: FileSpan::default(),
70 },
71 kind: VppDefStmtKind::Def(VppMacroDef {
72 name: "bluespec".into(),
73 num_args: 0,
74 parts: Vec::new(),
75 }),
76 });
77
78 items.push(VppDefStmt {
79 span: Span {
80 origin: BUILTIN_ORIGIN,
81 text_span: FileSpan::default(),
82 },
83 kind: VppDefStmtKind::Def(VppMacroDef {
84 name: "BLUESPEC".into(),
85 num_args: 0,
86 parts: Vec::new(),
87 }),
88 });
89
90 for (name, val) in &cli.defines {
91 let parts = if val.is_empty() {
92 Vec::new()
93 } else {
94 vec![VppMacroPart::Chunk(TextChunk {
95 pos: Pos {
96 origin: CLI_ORIGIN,
97 text_pos: FilePos::default(),
98 },
99 text: val.into(),
100 })]
101 };
102
103 items.push(VppDefStmt {
104 span: Span {
105 origin: CLI_ORIGIN,
106 text_span: FileSpan::default(),
107 },
108 kind: VppDefStmtKind::Def(VppMacroDef {
109 name: name.into(),
110 num_args: 0,
111 parts,
112 }),
113 });
114 }
115
116 Self { items }
117 }
118
119 fn get(&self, name: &str) -> Option<(&Span, &VppMacroDef)> {
120 for item in self.items.iter().rev() {
121 match &item.kind {
122 VppDefStmtKind::Def(def) if def.name == name => return Some((&item.span, def)),
123 VppDefStmtKind::Undef(undef) if undef == name => return None,
124 VppDefStmtKind::UndefAll => return None,
125 _ => (),
126 }
127 }
128 None
129 }
130
131 fn define(&mut self, def: VppMacroDef, span: Span) {
132 self.items.push(VppDefStmt {
133 span,
134 kind: VppDefStmtKind::Def(def),
135 });
136 }
137
138 fn undef(&mut self, name: Substr, span: Span) {
139 self.items.push(VppDefStmt {
140 span,
141 kind: VppDefStmtKind::Undef(name),
142 });
143 }
144
145 fn undef_all(&mut self, span: Span) {
146 self.items.push(VppDefStmt {
147 span,
148 kind: VppDefStmtKind::UndefAll,
149 });
150 }
151}
152
153pub struct VppSpan {
154 pub span: FileSpan,
155 pub kind: VppSpanKind,
156}
157
158pub enum VppSpanKind {
159 Escape,
160
161 Include,
162 IncludePath,
163
164 Line,
165 LineArgs,
166
167 Define,
168 DefineName,
169 DefineParam,
170 DefineBodyParam,
171 DefineBodyConcat,
172 DefineBodyEscape,
173 DefineBodyComment,
174
175 Undef,
176 UndefName,
177
178 ResetAll,
179
180 Conditional,
181 ConditionalArgTrue,
182 ConditionalArgFalse,
183 ConditionalDisabled,
184
185 MacroCall,
186}
187
188pub struct VppPreprocessor<'ctx> {
189 proj: &'ctx Project,
190 ctx: &'ctx mut BuildCtx,
191 pub spans: Vec<VppSpan>,
192
193 defs: VppDefs,
194}
195
196impl<'ctx> VppPreprocessor<'ctx> {
197 pub fn new(proj: &'ctx Project, ctx: &'ctx mut BuildCtx) -> Self {
198 let defs = VppDefs::populate(&proj.get_pkg(ctx.pkg).cli);
199
200 Self {
201 proj,
202 ctx,
203 spans: Vec::new(),
204 defs,
205 }
206 }
207
208 fn run<'a>(&mut self, mut inp: impl CharStream<'a>, out: &mut Vec<TextChunk>) {
210 while let Some(c) = inp.peek() {
211 match c {
212 '`' => self.handle_directive(&mut inp, out),
213 _ => self.handle_plain_text(&mut inp, out),
214 }
215 }
216 }
217
218 fn emit_str(&mut self, out: &mut Vec<TextChunk>, text: Substr, pos: Pos) {
219 out.push(TextChunk { pos, text });
220 }
221
222 fn diag(&mut self, range: Range<Pos>, severity: Severity, message: String) {
223 self.ctx.diagnostics.push(
224 Span::cross_origin(range.start, range.end, &self.ctx.origins),
225 severity,
226 "VPP",
227 message,
228 );
229 }
230
231 fn span(&mut self, range: Range<Pos>, kind: VppSpanKind) {
232 if range.start == range.end {
233 return;
234 }
235 let span = Span::cross_origin(range.start, range.end, &self.ctx.origins);
237 if span.origin != ROOT_ORIGIN {
238 return;
240 }
241 self.spans.push(VppSpan {
242 span: span.text_span,
243 kind,
244 });
245 }
246
247 fn handle_directive<'a>(&mut self, inp: &mut impl CharStream<'a>, out: &mut Vec<TextChunk>) {
248 let pos = inp.pos();
249 inp.expect('`');
250
251 match inp.peek() {
253 Some('"') => {
254 inp.expect_any();
255 self.span(pos..inp.pos(), VppSpanKind::Escape);
256 self.emit_str(out, literal_substr!("\""), pos);
257 return;
258 }
259 Some('\\') => {
260 inp.expect_any();
261 self.span(pos..inp.pos(), VppSpanKind::Escape);
262 self.emit_str(out, literal_substr!("\\"), pos);
263 return;
264 }
265 Some('`') => {
266 inp.expect_any();
267 self.diag(
268 pos..inp.pos(),
269 Severity::Error,
270 "macro catenation operators may only be used inside a macro definition.".into(),
271 );
272 return;
273 }
274 _ => (),
275 }
276
277 let directive = inp.read_while(bsv_ident);
278
279 if directive.is_empty() {
280 self.diag(
281 pos..inp.pos(),
282 Severity::Error,
283 "missing preprocessor directive after '`'".into(),
284 );
285 return;
286 }
287
288 match directive.borrow() {
289 "include" => self.handle_include(inp, out, pos),
290 "line" => self.handle_line(inp, pos),
291 "define" => self.handle_define(inp, pos),
292 "undef" => self.handle_undef(inp, pos),
293 "resetall" => self.handle_resetall(inp, pos),
294 "ifdef" => self.handle_conditional(inp, out, pos, false),
295 "ifndef" => self.handle_conditional(inp, out, pos, true),
296 "elsif" => self.diag(
297 pos..inp.pos(),
298 Severity::Error,
299 "unexpected '`elsif' directive".into(),
300 ),
301 "else" => self.diag(
302 pos..inp.pos(),
303 Severity::Error,
304 "unexpected '`else' directive".into(),
305 ),
306 "endif" => self.diag(
307 pos..inp.pos(),
308 Severity::Error,
309 "unexpected '`endif' directive".into(),
310 ),
311 _name => self.handle_expansion(inp, out, pos, &directive),
312 }
313 }
314
315 fn handle_include<'a>(
316 &mut self,
317 inp: &mut impl CharStream<'a>,
318 out: &mut Vec<TextChunk>,
319 pos: Pos,
320 ) {
321 self.span(pos..inp.pos(), VppSpanKind::Include);
322
323 let ws = inp.skip_while(bsv_whitespace);
327 if ws == 0 {
328 self.diag(
329 pos..inp.pos(),
330 Severity::Error,
331 "'`inlcude' directive must be followed by whitespace".into(),
332 );
333 }
334
335 let closing_delim = match inp.peek() {
336 Some('"') => '"',
337 Some('<') => '>',
338 Some('`') => {
339 let pos = inp.pos();
342 inp.read_while(bsv_ident);
343 self.diag(
344 pos..inp.pos(),
345 Severity::Error,
346 "macro expansion inside '`include' arguments is currently not supported in blues-lsp"
347 .into(),
348 );
349 return;
350 }
351 _ => {
352 self.diag(
353 pos..inp.pos(),
354 Severity::Error,
355 "'`inlcude' directive is missing filename field".into(),
356 );
357 return;
358 }
359 };
360 let open_delim_start = inp.pos();
361 inp.next();
362 let open_delim_range = open_delim_start..inp.pos();
363
364 let path_start = inp.pos();
365 let path = inp.read_while(|c| c != closing_delim);
366 let path_range = path_start..inp.pos();
367
368 let close_delim = inp.next();
369 self.span(open_delim_start..inp.pos(), VppSpanKind::IncludePath);
370 if close_delim != Some(closing_delim) {
371 self.diag(
376 open_delim_range,
377 Severity::Error,
378 format!(
379 "missing '{closing_delim}' closing delimeter for '`include' filename argument"
380 ),
381 );
382 return;
383 }
384
385 let Some(path) = self.resolve_include_path(&path) else {
386 self.diag(
387 path_range,
388 Severity::Error,
389 format!("include file '{}' not found", path),
390 );
391 return;
392 };
393
394 let file = self.proj.lookup_file(&path);
395 let file = self.proj.file_contents(file, self.ctx.pkg);
396
397 let span = Span::cross_origin(pos, inp.pos(), &self.ctx.origins);
398 let include_origin = self.ctx.origins.add(Origin {
399 origin: span.origin,
400 span: span.text_span,
401 file: file.clone(),
402 reason: OriginReason::Include,
403 });
404
405 let chunks = file.chunks(include_origin);
406 let chars = TextCharStream::new(chunks.iter());
407
408 self.run(chars, out);
409 }
410
411 fn resolve_include_path(&mut self, path: &str) -> Option<PathBuf> {
412 let path = PathBuf::from(path);
415
416 if path.is_absolute() {
417 return Some(path);
418 }
419
420 let pkg = self.proj.get_pkg(self.ctx.pkg);
421
422 for dir in pkg.cli.include_dirs_all() {
423 let path = dir.join(&path);
424
425 if path.is_file() {
426 return Some(maybe_canonicalize(&path));
427 }
428 }
429
430 None
431 }
432
433 fn handle_line<'a>(&mut self, inp: &mut impl CharStream<'a>, pos: Pos) {
434 self.span(pos..inp.pos(), VppSpanKind::Line);
435 let args_start = inp.pos();
436 match inp.peek() {
440 Some('(') => {
441 inp.skip_while(|c| c != ')');
442 if inp.peek() == Some(')') {
443 inp.next();
444 } else {
445 self.diag(
446 pos..inp.pos(),
447 Severity::Error,
448 "'`line' arguments are missing a closing ')'".into(),
449 );
450 }
451 }
452 Some(c) if bsv_whitespace(c) => {
453 inp.skip_while(|c| c != '\n');
454 if inp.peek() == Some('\n') {
455 inp.next();
456 }
457 }
458 _ => {
459 self.diag(
460 pos..inp.pos(),
461 Severity::Error,
462 "'`line' must be directly followed by '(' or whitespace".into(),
463 );
464 }
465 }
466 self.span(args_start..inp.pos(), VppSpanKind::LineArgs);
467 }
468
469 fn handle_undef<'a>(&mut self, inp: &mut impl CharStream<'a>, pos: Pos) {
470 self.span(pos..inp.pos(), VppSpanKind::Undef);
471 inp.skip_while(bsv_whitespace);
472
473 let name_start = inp.pos();
474 let name = inp.read_while(bsv_ident);
475 self.span(name_start..inp.pos(), VppSpanKind::UndefName);
476
477 if name.is_empty() {
478 self.diag(
479 pos..inp.pos(),
480 Severity::Error,
481 "'`undef' directive is missing a macro name".into(),
482 );
483 return;
484 }
485 let span = Span::cross_origin(pos, inp.pos(), &self.ctx.origins);
486 self.defs.undef(name, span);
487 }
488
489 fn handle_resetall<'a>(&mut self, inp: &impl CharStream<'a>, pos: Pos) {
490 self.span(pos..inp.pos(), VppSpanKind::ResetAll);
491 let span = Span::cross_origin(pos, inp.pos(), &self.ctx.origins);
492 self.defs.undef_all(span);
493 }
494
495 fn handle_define<'a>(&mut self, inp: &mut impl CharStream<'a>, pos: Pos) {
496 self.span(pos..inp.pos(), VppSpanKind::Define);
497 inp.skip_while(bsv_whitespace);
501
502 let id_start = inp.pos();
506 let id = inp.read_while(bsv_ident);
507 let id_range = id_start..inp.pos();
508 self.span(id_range.clone(), VppSpanKind::DefineName);
509 if id.is_empty() {
510 self.diag(
511 pos..inp.pos(),
512 Severity::Error,
513 "'`define' directive is missing a name".into(),
514 );
515 }
516
517 #[rustfmt::skip]
519 const RESERVED_KWS: &[&str] = &[
520 "celldefine", "default_nettype", "define", "else", "elsif", "endcelldefine",
521 "endif", "ifdef", "ifndef", "include", "line", "nounconnected_drive", "resetall",
522 "timescale", "unconnected_drive", "undef", "bluespec", "BLUESPEC"
523 ];
524
525 if RESERVED_KWS.iter().any(|kw| *kw == id.as_str()) {
526 self.diag(
527 id_range,
528 Severity::Error,
529 format!("reserved keyword '{id}' cannot be used as a macro name"),
530 );
531 }
532
533 inp.skip_while(bsv_horizontal_whitespace);
539
540 let params: Vec<Substr> = match inp.peek() {
542 Some('(') => {
544 let open_delim_start = inp.pos();
546 inp.expect_any();
547 let open_delim_range = open_delim_start..inp.pos();
548 let params = self.read_param_list(inp);
549
550 match inp.peek() {
551 Some(')') => inp.expect_any(),
552 None => {
553 self.diag(
554 open_delim_range,
555 Severity::Error,
556 "macro parameter list is missing a closing ')'".into(),
557 );
558 }
559 _ => unreachable!("read_define_params should only terminate on ')' or EOF"),
560 }
561
562 params
563 }
564 _ => Vec::new(),
566 };
567
568 let parts = self.read_macro_body(inp, ¶ms);
569
570 let span = Span::cross_origin(pos, inp.pos(), &self.ctx.origins);
571
572 self.defs.define(
573 VppMacroDef {
574 name: id,
575 num_args: params.len() as u32,
576 parts,
577 },
578 span,
579 );
580 }
581
582 fn read_param_list<'a>(&mut self, inp: &mut impl CharStream<'a>) -> Vec<Substr> {
583 if let Some(')') = inp.peek() {
585 return Vec::new();
586 }
587
588 let mut params = Vec::new();
589 let mut empty_field = true;
590
591 while let Some(c) = inp.peek() {
592 match c {
593 ',' | ')' => {
594 if empty_field {
595 self.diag(
596 inp.pos()..inp.pos(),
597 Severity::Warning,
598 "empty macro parameter fields are not counted as additional parameters"
599 .into(),
600 );
601 }
602
603 if c == ',' {
604 empty_field = true;
605 inp.expect_any();
606 } else if c == ')' {
607 break;
608 }
609 }
610 _ => {
611 empty_field = false;
612 params.push(self.read_param(inp))
613 }
614 }
615 }
616
617 params
618 }
619
620 fn read_param<'a>(&mut self, inp: &mut impl CharStream<'a>) -> Substr {
621 let field_start = inp.pos();
622 inp.skip_while(bsv_whitespace);
623 let name_start = inp.pos();
624 let mut name_end = None;
625
626 let mut has_id_chars = false;
627 let mut has_non_id_chars = false;
628 let mut has_omited_chars = false;
629 let mut name = EMPTY_SUBSTR;
630
631 while let Some(c) = inp.peek() {
632 match c {
633 ',' | ')' => break,
634 '\\' => {
635 has_omited_chars = true;
636 inp.expect_any();
637 }
638 c if bsv_whitespace(c) => {
639 name_end = Some(inp.pos());
640 inp.skip_while(bsv_whitespace);
641 if !matches!(inp.peek(), None | Some(',') | Some(')')) {
642 name_end = None;
643 has_omited_chars = true;
644 }
645 }
646 c if bsv_ident(c) => {
647 has_id_chars = true;
648
649 if has_id_chars && has_non_id_chars {
650 inp.skip_while(bsv_ident);
651 } else {
652 name.append(inp.read_while(bsv_ident))
653 }
654 }
655 _ => {
656 has_non_id_chars = true;
657
658 if has_id_chars && has_non_id_chars {
659 inp.skip_while(vpp_malformed_param);
660 } else {
661 name.append(inp.read_while(vpp_malformed_param))
662 }
663 }
664 }
665 }
666
667 let name_end = name_end.unwrap_or(inp.pos());
668
669 let name_range = name_start..name_end;
670 self.span(name_range.clone(), VppSpanKind::DefineParam);
671
672 if has_omited_chars {
673 self.diag(
674 name_range.clone(),
675 Severity::Warning,
676 format!(
677 "whitespace and '\\' characters are ommited from parameter names.
678This parameter name will be seen as '{name}' by the preprocessor."
679 ),
680 );
681 }
682 if has_non_id_chars {
683 self.diag(
684 name_range,
685 Severity::Warning,
686 format!("macro parameter '{name}' contains non-id characters"),
687 );
688 }
689 if name.is_empty() {
690 self.diag(
691 field_start..inp.pos(),
692 Severity::Warning,
693 "empty macro parameter name".to_string(),
694 );
695 }
696
697 if has_id_chars && has_non_id_chars {
698 EMPTY_SUBSTR
700 } else {
701 name
702 }
703 }
704
705 fn read_macro_body<'a>(
706 &mut self,
707 inp: &mut impl CharStream<'a>,
708 params: &[Substr],
709 ) -> Vec<VppMacroPart> {
710 let output = Cell::new(Vec::new());
711 let mut sub = inp
712 .chunk_state()
713 .writer(|c| output.update(|v: &mut Vec<VppMacroPart>| v.push(VppMacroPart::Chunk(c))));
714
715 while let Some(c) = sub.peek() {
717 match c {
718 '/' => {
720 let start = sub.pos();
721 let mut la = sub.chars().clone();
722 la.next();
723
724 match la.peek() {
725 Some('/') => {
726 sub.skip();
727 if !self.scan_line_comment(&mut sub.skipped(), true) {
728 continue;
729 }
730 self.span(start..sub.pos(), VppSpanKind::DefineBodyComment);
731 if let Some('\n') = sub.peek() {
732 sub.advance();
733 }
734 }
735 Some('*') => {
736 sub.skip();
737 self.scan_block_comment(&mut sub.skipped());
738 self.span(start..sub.pos(), VppSpanKind::DefineBodyComment);
739 }
740 _ => sub.advance(),
741 }
742 }
743
744 '\\' => {
746 let start = sub.pos();
747 let mut la = sub.chars().clone();
748 la.next();
749
750 while let Some('\r') = sub.peek() {
751 la.next();
752 }
753 let Some('\n') = la.peek() else {
754 sub.advance();
755 continue;
756 };
757
758 sub.skip();
759 self.span(start..sub.pos(), VppSpanKind::DefineBodyEscape);
760 while let Some('\r') = sub.peek() {
763 sub.skip();
764 }
765 sub.check('\n');
766 sub.advance();
767 }
768
769 '\n' => {
771 sub.advance();
772 break;
773 }
774
775 c if bsv_ident(c) => {
777 if !self.handle_possible_arg_inst(&mut sub, params, &output, bsv_ident) {
778 sub.advance_while(bsv_ident);
780 }
781 }
782 c if vpp_malformed_param(c) => {
783 if self.handle_possible_arg_inst(&mut sub, params, &output, vpp_malformed_param)
784 {
785 continue;
786 }
787 while let Some(c) = sub.peek() {
789 if !vpp_malformed_param(c) {
790 break;
791 }
792 if c != '`' {
793 sub.advance();
794 continue;
795 }
796 let mut la = sub.chars().clone();
797 la.next();
798 if let Some('`') = la.peek() {
799 sub.flush();
800 let pos = sub.pos();
801 output.update(|v| v.push(VppMacroPart::Concat(pos)));
802 sub.check('`');
803 sub.skip();
804 sub.check('`');
805 sub.skip();
806 self.span(pos..sub.pos(), VppSpanKind::DefineBodyConcat);
807 } else {
808 sub.advance();
809 }
810 }
811 }
812
813 _ => sub.advance(),
815 }
816 }
817
818 sub.flush();
819 output.into_inner()
820 }
821
822 fn handle_possible_arg_inst<'a>(
823 &mut self,
824 sub: &mut impl ChunkWriter<'a>,
825 params: &[Substr],
826 output: &Cell<Vec<VppMacroPart>>,
827 name_pred: impl Fn(char) -> bool,
828 ) -> bool {
829 let mut la = sub.chars().clone();
830 let name = la.read_while(&name_pred);
831 if let Some((i, _)) = params.iter().enumerate().find(|e| e.1 == &name) {
833 sub.flush();
835 let start = sub.pos();
836 sub.skip_while(&name_pred);
838 self.span(start..sub.pos(), VppSpanKind::DefineBodyParam);
839 output.update(|v| {
840 v.push(VppMacroPart::Arg(
841 i,
842 Span::cross_origin(start, sub.pos(), &self.ctx.origins),
843 ))
844 });
845 true
846 } else {
847 false
849 }
850 }
851
852 fn handle_conditional<'a>(
853 &mut self,
854 inp: &mut impl CharStream<'a>,
855 out: &mut Vec<TextChunk>,
856 pos: Pos,
857 inverted: bool,
858 ) {
859 self.span(pos..inp.pos(), VppSpanKind::Conditional);
860 inp.skip_while(bsv_whitespace);
861
862 let name_start = inp.pos();
863 let name = inp.read_while(bsv_ident);
864 let mut condition = if name.is_empty() {
865 self.diag(
866 pos..inp.pos(),
867 Severity::Error,
868 "'`ifdef'/'`ifdef' directive is missing a macro name".into(),
869 );
870 false
871 } else {
872 self.defs.get(&name).is_some()
873 };
874 self.span(
875 name_start..inp.pos(),
876 if condition {
877 VppSpanKind::ConditionalArgTrue
878 } else {
879 VppSpanKind::ConditionalArgFalse
880 },
881 );
882
883 if inverted {
884 condition = !condition;
885 }
886
887 let mut post_else = false;
889 let mut found = false;
891
892 let mut body = Vec::new();
893
894 while inp.peek().is_some() {
895 let body_start = inp.pos();
896 self.scan_conditional_body(inp, &mut body);
897 if condition && !found {
898 self.run(TextCharStream::new(body.iter()), out);
899 found = true;
900 } else {
901 self.span(body_start..inp.pos(), VppSpanKind::ConditionalDisabled);
902 }
903 body.clear();
904
905 if inp.peek().is_none() {
906 break;
907 }
908
909 let directive_start = inp.pos();
910 inp.expect('`');
911 let directive = inp.read_while(bsv_ident);
912 let directive_range = directive_start..inp.pos();
913 self.span(directive_range.clone(), VppSpanKind::Conditional);
914
915 match directive.as_str() {
916 "elseif" => {
917 if post_else {
918 self.diag(
919 directive_range,
920 Severity::Error,
921 "'`elseif' following a '`else' directive".into(),
922 );
923 }
924
925 inp.skip_while(bsv_whitespace);
926 let name_start = inp.pos();
927 let name = inp.read_while(bsv_ident);
928
929 if name.is_empty() {
930 self.diag(
931 pos..inp.pos(),
932 Severity::Error,
933 "'`elseif' directive is missing a macro name".into(),
934 );
935 condition = false;
936 } else {
937 condition = self.defs.get(&name).is_some();
938 }
939 self.span(
940 name_start..inp.pos(),
941 if condition {
942 VppSpanKind::ConditionalArgTrue
943 } else {
944 VppSpanKind::ConditionalArgFalse
945 },
946 );
947 }
948 "else" => {
949 if post_else {
950 self.diag(
951 directive_range,
952 Severity::Error,
953 "'`else' following a '`else' directive".into(),
954 );
955 }
956
957 condition = true;
958 post_else = true;
959 }
960 "endif" => {
961 break;
962 }
963 _ => unreachable!(),
964 }
965 }
966 }
967
968 fn scan_conditional_body<'a>(
969 &mut self,
970 inp: &mut impl CharStream<'a>,
971 out: &mut Vec<TextChunk>,
972 ) {
973 let mut depth = 0;
974 let mut sub = inp.chunk_state().writer(|c| out.push(c));
975
976 while let Some(c) = sub.peek() {
977 match c {
978 '`' => {
979 let mut la = sub.chars().clone();
980 let directive_start = la.pos();
981 la.expect('`');
982 let name = la.read_while(bsv_ident);
983 let directive_range = directive_start..la.pos();
984 match name.as_str() {
985 "ifdef" | "ifndef" => {
986 sub.advance();
987 sub.advance_while(bsv_ident);
988 if sub.peek().is_some_and(bsv_whitespace) {
989 depth += 1;
990 } else {
991 self.diag(
992 directive_range,
993 Severity::Warning,
994 "nested branching directives must be followed by whitespace to be recognized".into()
995 );
996 }
997 }
998 "elseif" | "else" | "endif" if depth == 0 => {
999 if la.peek().is_some_and(bsv_whitespace) {
1000 break;
1001 } else {
1002 sub.advance();
1003 sub.advance_while(bsv_ident);
1004 self.diag(
1005 directive_range,
1006 Severity::Warning,
1007 "nested branching directives must be followed by whitespace to be recognized".into()
1008 );
1009 }
1010 }
1011 "else" => {
1012 sub.advance();
1013 sub.advance_while(bsv_ident);
1014 if !sub.peek().is_some_and(bsv_whitespace) {
1015 self.diag(
1016 directive_range,
1017 Severity::Warning,
1018 "nested branching directives must be followed by whitespace to be recognized".into()
1019 );
1020 }
1021 }
1022 "elseif" | "endif" => {
1023 sub.advance();
1024 sub.advance_while(bsv_ident);
1025 if sub.peek().is_some_and(bsv_whitespace) {
1026 depth -= 1;
1027 } else {
1028 self.diag(
1029 directive_range,
1030 Severity::Warning,
1031 "nested branching directives must be followed by whitespace to be recognized".into()
1032 );
1033 }
1034 }
1035 _ => {
1036 sub.advance();
1037 sub.advance_while(bsv_ident);
1038 }
1039 };
1040 }
1041 _ => sub.advance(),
1042 }
1043 }
1044
1045 sub.flush();
1046 }
1047
1048 fn handle_expansion<'a>(
1049 &mut self,
1050 inp: &mut impl CharStream<'a>,
1051 out: &mut Vec<TextChunk>,
1052 pos: Pos,
1053 name: &str,
1054 ) {
1055 self.span(pos..inp.pos(), VppSpanKind::MacroCall);
1056 let mut has_arg_list = false;
1058 let args: Vec<Vec<TextChunk>> = match inp.peek() {
1060 Some('(') => {
1061 has_arg_list = true;
1062 self.read_expansion_args(inp)
1065 }
1066 _ => Vec::new(),
1067 };
1068
1069 let Some((_, def)) = self.defs.get(name) else {
1070 self.diag(
1071 pos..inp.pos(),
1072 Severity::Error,
1073 format!("missing macro definition for {name}"),
1074 );
1075 return;
1076 };
1077
1078 if args.len() != def.num_args as usize {
1079 self.ctx.diagnostics.push(
1081 Span::cross_origin(pos, inp.pos(), &self.ctx.origins),
1082 Severity::Error,
1083 "VPP",
1084 format!(
1085 "macro argument count mismatch, expected {}, got {}",
1086 def.num_args,
1087 args.len()
1088 ),
1089 );
1090 }
1091
1092 let origins = &mut self.ctx.origins;
1093 let mut state = MacroExpansionState::new(
1094 Span::cross_origin(pos, inp.pos(), origins),
1095 OriginReason::Macro,
1096 );
1097
1098 let mut has_concat = false;
1099
1100 for part in &def.parts {
1101 match part {
1102 VppMacroPart::Arg(arg, span) => {
1103 let Some(arg) = args.get(*arg) else {
1104 continue;
1105 };
1106
1107 let mut span = *span;
1108 span.origin = state.augment(origins, span.origin);
1109 let mut interp_state =
1110 MacroExpansionState::new(span, OriginReason::MacroInterp);
1111
1112 for chunk in arg {
1113 interp_state.push(origins, chunk.clone());
1114 }
1115 }
1116 VppMacroPart::Chunk(chunk) => {
1117 state.push(origins, chunk.clone());
1118 }
1119 VppMacroPart::Concat(pos) => {
1120 if !has_arg_list {
1121 has_concat = true;
1124 state.push(
1125 origins,
1126 TextChunk {
1127 pos: *pos,
1128 text: literal_substr!("``"),
1129 },
1130 );
1131 continue;
1132 }
1133
1134 while let Some(chunk) = state.expanded.last_mut() {
1136 let trimmed = chunk
1137 .text
1138 .substr_using(|s| s.trim_end_matches(bsv_whitespace));
1139 if trimmed.is_empty() {
1140 state.expanded.pop();
1141 } else {
1142 chunk.text = trimmed;
1143 break;
1144 }
1145 }
1146 state.need_trim = true;
1147 }
1148 }
1149 }
1150
1151 if !has_arg_list && has_concat {
1152 self.diag(
1153 pos..inp.pos(),
1154 Severity::Hint,
1155 format!(
1156 "macro catenation operators '``' only work if the macro is called with an empty argument list: '{name}()'"
1157 )
1158 );
1159 }
1160
1161 self.run(TextCharStream::new(state.expanded.iter()), out);
1163 }
1164
1165 fn read_expansion_args<'a>(&mut self, inp: &mut impl CharStream<'a>) -> Vec<Vec<TextChunk>> {
1166 let pos = inp.pos();
1167 let mut args = Vec::new();
1168 inp.expect('(');
1169
1170 while let Some(c) = inp.peek() {
1171 if c == ')' {
1172 break;
1173 }
1174
1175 let mut depth = 0;
1176 let mut arg = Vec::new();
1177 let mut sub = inp.chunk_state().writer(|c| arg.push(c));
1178
1179 while let Some(c) = sub.peek() {
1180 match c {
1181 ',' if depth == 0 => {
1182 sub.flush();
1183 inp.next();
1184 break;
1185 }
1186 ')' if depth == 0 => {
1187 sub.flush();
1188 break;
1189 }
1190 '(' => {
1191 depth += 1;
1192 sub.advance();
1193 }
1194 ')' => {
1195 depth -= 1;
1196 sub.advance();
1197 }
1198 '`' => {
1199 let pos = sub.pos();
1200 sub.advance();
1201 sub.advance_while(bsv_ident);
1202 self.diag(
1203 pos..sub.pos(),
1204 Severity::Information,
1205 "macro expansion inside macro argument lists is currently not performed by blues-lsp, behavior may diverge from bsc".into()
1206 );
1207 }
1208 _ => sub.advance(),
1209 }
1210 }
1211
1212 args.push(arg);
1213 }
1214
1215 if inp.peek() != Some(')') {
1216 self.diag(
1217 pos..inp.pos(),
1218 Severity::Error,
1219 "macro argument list is missing a closing ')'".into(),
1220 );
1221 }
1222 inp.next();
1223
1224 args
1225 }
1226
1227 fn handle_plain_text<'a>(&mut self, inp: &mut impl CharStream<'a>, out: &mut Vec<TextChunk>) {
1228 let mut sub = inp.chunk_state().writer(|c| out.push(c));
1229
1230 while let Some(c) = sub.peek() {
1231 match c {
1232 '`' => break,
1233 '/' => {
1234 sub.advance();
1235 match sub.peek() {
1236 Some('/') => {
1237 self.scan_line_comment(&mut sub, false);
1238 }
1239 Some('*') => self.scan_block_comment(&mut sub),
1240 _ => continue,
1241 }
1242 }
1243 _ => sub.advance(),
1244 }
1245 }
1246
1247 sub.flush();
1248 }
1249
1250 fn scan_line_comment<'a>(
1252 &mut self,
1253 sub: &mut impl ChunkWriter<'a>,
1254 handle_lf_escape: bool,
1255 ) -> bool {
1256 while let Some(c) = sub.peek() {
1257 match c {
1258 '\\' if handle_lf_escape => {
1259 let pos = sub.pos();
1260 sub.advance();
1261 match sub.peek() {
1262 Some('\r') => {
1263 self.diag(
1266 pos..sub.pos(),
1267 Severity::Warning,
1268 "escaping line endings inside a line comment, inside a macro definition does not work with CRLF or CR line endings".into()
1269 );
1270 }
1271 Some('\n') => {
1272 return true;
1273 }
1274 _ => (),
1275 }
1276 }
1277 '\n' => return false,
1279 _ => sub.advance(),
1280 }
1281 }
1282 false
1283 }
1284
1285 fn scan_block_comment<'a>(&self, sub: &mut impl ChunkWriter<'a>) {
1286 sub.advance();
1287
1288 while let Some(c) = sub.peek() {
1289 match c {
1290 '*' => {
1291 sub.advance();
1292 if let Some('/') = sub.peek() {
1293 sub.advance();
1294 break;
1295 }
1296 }
1297 _ => sub.advance(),
1298 }
1299 }
1300 }
1301}
1302
1303impl<'src> Preprocessor<'src> for VppPreprocessor<'_> {
1304 fn process<I: CharStream<'src>>(&mut self, input: I, output: &mut Vec<TextChunk>) {
1305 self.run(input, output);
1306 }
1307}
1308
1309struct MacroExpansionState {
1311 parent_span: Span,
1313
1314 origins: Vec<(OriginId, OriginId)>, need_trim: bool,
1321
1322 expanded: Vec<TextChunk>,
1324
1325 reason: OriginReason,
1326}
1327
1328impl MacroExpansionState {
1329 fn new(parent_span: Span, reason: OriginReason) -> Self {
1330 Self {
1331 parent_span,
1332 origins: Vec::new(),
1333 need_trim: false,
1334 expanded: Vec::new(),
1335 reason,
1336 }
1337 }
1338
1339 fn augment(&mut self, origins: &mut OriginTable, origin: OriginId) -> OriginId {
1340 match self.origins.iter().find(|o| o.0 == origin) {
1341 Some((_, new_origin)) => *new_origin,
1342 None => {
1343 let new_origin = origins.add(Origin {
1344 origin: self.parent_span.origin,
1345 span: self.parent_span.text_span,
1346 file: origins.get(origin).file.clone(),
1347 reason: self.reason,
1348 });
1349 self.origins.push((origin, new_origin));
1350 new_origin
1351 }
1352 }
1353 }
1354
1355 fn push(&mut self, origins: &mut OriginTable, chunk: TextChunk) {
1356 let mut pos = chunk.pos.text_pos;
1357 let mut text = chunk.text;
1358
1359 if self.need_trim {
1360 let mut counter = LineColCounter::start_at(pos);
1361 text = text.substr_using(|s| {
1362 s.trim_matches(|c| {
1363 if bsv_whitespace(c) {
1364 counter.advance(c);
1365 true
1366 } else {
1367 false
1368 }
1369 })
1370 });
1371 if let Some(c) = text.chars().next() {
1372 pos = counter.pos(Some(c));
1373 self.need_trim = false;
1374 } else {
1375 return;
1377 }
1378 }
1379
1380 let new_origin = self.augment(origins, chunk.pos.origin);
1381
1382 self.expanded.push(TextChunk {
1383 pos: Pos {
1384 origin: new_origin,
1385 text_pos: pos,
1386 },
1387 text,
1388 });
1389 }
1390}