Skip to main content

blues_lsp/syntax/preprocessor/
vpp.rs

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    // We know this will run to completion so we can take `inp` by value
209    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        // TODO: Probably shouldn't allow cross-origin here?
236        let span = Span::cross_origin(range.start, range.end, &self.ctx.origins);
237        if span.origin != ROOT_ORIGIN {
238            // Not visible in source file, skip.
239            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        // character escape sequences (`" and `/)
252        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        // Skip whitespace:
324        //         V
325        // `include MACRO_NAME
326        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                // TODO: macro expansion inside path specifier
340                // eg.: `include `SOME_PATH
341                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            // TODO: a better error recovery?
372            // This will only fail on EOF, meaning `include will scan the entire file as
373            // a filepath, which is not ideal...
374            // Ideally, we would just bail on newline, but that's not what bsc does.
375            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        // TODO: review against bsc
413
414        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        // 'line directives...
437        // Just do the bare minimum to skip them and move on.
438        // TODO: We could lint their arguments...
439        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        // Skip whitespace:
498        //        V
499        // `define MACRO_NAME
500        inp.skip_while(bsv_whitespace);
501
502        // Read identifier:
503        //         .--------.
504        // `define MACRO_NAME
505        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        // https://github.com/B-Lang-org/bsc/blob/a798d8a94d54197d2b4755afdbb460cdc559c151/src/comp/SystemVerilogPreprocess.lhs#L229
518        #[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        // TODO: also validate first character of name
534
535        // Skip whitespace after ID:
536        //                   V                         V
537        // `define MACRO_NAME ()  |  `define MACRO_NAME BODY
538        inp.skip_while(bsv_horizontal_whitespace);
539
540        // Parse param names:
541        let params: Vec<Substr> = match inp.peek() {
542            // `define MACRO_NAME(a, b, c) BODY
543            Some('(') => {
544                // Consume leading '('
545                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            // `define MACRO_NAME BODY
565            _ => Vec::new(),
566        };
567
568        let parts = self.read_macro_body(inp, &params);
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        // Immediate ')' - empty param list
584        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            // If we have both, we know it won't bind to anything
699            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        // TODO: this should be split into smaller functions...
716        while let Some(c) = sub.peek() {
717            match c {
718                // Possible start of comment
719                '/' => {
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                // Possible escaped newline
745                '\\' => {
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                    // TODO: bsc handling of this is a bit weird,
761                    // weird line endings could do with a lint.
762                    while let Some('\r') = sub.peek() {
763                        sub.skip();
764                    }
765                    sub.check('\n');
766                    sub.advance();
767                }
768
769                // Terminating newline
770                '\n' => {
771                    sub.advance();
772                    break;
773                }
774
775                // Possible arg expansions
776                c if bsv_ident(c) => {
777                    if !self.handle_possible_arg_inst(&mut sub, params, &output, bsv_ident) {
778                        // Include ident chars in output if not a param
779                        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                    // If not a param, check for existance of ``
788                    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                // General case, advance one char
814                _ => 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        // Find a matching param name
832        if let Some((i, _)) = params.iter().enumerate().find(|e| e.1 == &name) {
833            // Param found, push it to macro body
834            sub.flush();
835            let start = sub.pos();
836            // Omit name use from plain output
837            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            // Param not found, leave it to user
848            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        // Have we passed an `else
888        let mut post_else = false;
889        // Have we found a positive branch
890        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        // There is a difference between `FOO and `FOO(), so we represent them differently
1057        let mut has_arg_list = false;
1058        // TODO: This is certainly a place where some sort of SSO vec would be nice
1059        let args: Vec<Vec<TextChunk>> = match inp.peek() {
1060            Some('(') => {
1061                has_arg_list = true;
1062                // TODO: We currently do not run argument lists through preprocessor before
1063                // expansion. This will produce different ouput from bsc in some cases.
1064                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            // Inline `Self::diag` call to apease borrowchecking gods.
1080            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                        // bsc currently does not perform catenation if macro is invoked
1122                        // without an arg list (`FOO).
1123                        has_concat = true;
1124                        state.push(
1125                            origins,
1126                            TextChunk {
1127                                pos: *pos,
1128                                text: literal_substr!("``"),
1129                            },
1130                        );
1131                        continue;
1132                    }
1133
1134                    // Trim whitespace before concat operator
1135                    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        // Run expanded macro through preprocessor again.
1162        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    /// Returns `true` if terminating newline was escaped
1251    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                            // bsc currently does not handle non-terminating line
1264                            // comments on non-LF line endings
1265                            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                // We only handle LF and CRLF line endings to follow bsc
1278                '\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
1309// Ad hoc state for filling in expanded macro text
1310struct MacroExpansionState {
1311    // Span in parent origin
1312    parent_span: Span,
1313
1314    // Mapping from underlying origins to new, augmented ones
1315    // TODO: techicaly we only need a mapping file -> new origin,
1316    // but this is easier for now.
1317    origins: Vec<(OriginId, OriginId)>, // old -> new
1318
1319    // Do we need to trim whitespace (after concat operator)?
1320    need_trim: bool,
1321
1322    // Chunks for the expanded macro contents
1323    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                // Chunk is empty after trimming
1376                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}