Skip to main content

drawlang_syntax/
parser.rs

1//! Recursive-descent parser with error recovery: a malformed statement is
2//! reported and skipped, and parsing continues, so one pass reports every
3//! error in the file.
4
5use crate::ast::*;
6use crate::diag::Diagnostic;
7use crate::lexer::{Token, TokenKind};
8use crate::span::Span;
9
10pub struct ParseOutput {
11    pub file: File,
12    pub diagnostics: Vec<Diagnostic>,
13}
14
15pub fn parse(src: &str, tokens: Vec<Token>) -> ParseOutput {
16    let mut p = Parser {
17        src,
18        tokens,
19        pos: 0,
20        diags: Vec::new(),
21    };
22    let file = p.parse_file();
23    ParseOutput {
24        file,
25        diagnostics: p.diags,
26    }
27}
28
29const STMT_KEYWORDS: &[&str] = &[
30    "canvas",
31    "def",
32    "group",
33    "class",
34    "constrain",
35    "pin",
36    "for",
37    "port",
38];
39
40struct Parser<'a> {
41    src: &'a str,
42    tokens: Vec<Token>,
43    pos: usize,
44    diags: Vec<Diagnostic>,
45}
46
47/// Internal error marker; the diagnostic is already pushed when this is raised.
48struct Bail;
49type PResult<T> = Result<T, Bail>;
50
51impl<'a> Parser<'a> {
52    // ------------------------------------------------------------- cursors
53
54    fn peek(&self) -> &TokenKind {
55        &self.tokens[self.pos].kind
56    }
57
58    fn peek_at(&self, ahead: usize) -> &TokenKind {
59        let idx = (self.pos + ahead).min(self.tokens.len() - 1);
60        &self.tokens[idx].kind
61    }
62
63    fn span(&self) -> Span {
64        self.tokens[self.pos].span
65    }
66
67    fn prev_span(&self) -> Span {
68        self.tokens[self.pos.saturating_sub(1)].span
69    }
70
71    fn bump(&mut self) -> Token {
72        let t = self.tokens[self.pos].clone();
73        if self.pos < self.tokens.len() - 1 {
74            self.pos += 1;
75        }
76        t
77    }
78
79    fn at_eof(&self) -> bool {
80        matches!(self.peek(), TokenKind::Eof)
81    }
82
83    fn eat(&mut self, kind: &TokenKind) -> bool {
84        if self.peek() == kind {
85            self.bump();
86            true
87        } else {
88            false
89        }
90    }
91
92    /// Skip newlines and comments without recording them (used inside
93    /// parenthesized lists where trivia placement doesn't matter).
94    fn skip_trivia(&mut self) {
95        while matches!(self.peek(), TokenKind::Newline | TokenKind::Comment(_)) {
96            self.bump();
97        }
98    }
99
100    fn expect(&mut self, kind: TokenKind, ctx: &str) -> PResult<Token> {
101        if self.peek() == &kind {
102            return Ok(self.bump());
103        }
104        let found = self.peek().describe();
105        self.diags.push(
106            Diagnostic::error(
107                "E0103",
108                format!("expected {} {ctx}, found {found}", kind.describe()),
109            )
110            .with_label(self.span(), format!("expected {} here", kind.describe())),
111        );
112        Err(Bail)
113    }
114
115    fn expect_ident(&mut self, ctx: &str) -> PResult<Ident> {
116        if let TokenKind::Ident(name) = self.peek() {
117            let name = name.clone();
118            let t = self.bump();
119            return Ok(Ident { name, span: t.span });
120        }
121        let found = self.peek().describe();
122        self.diags.push(
123            Diagnostic::error("E0103", format!("expected a name {ctx}, found {found}"))
124                .with_label(self.span(), "expected an identifier here"),
125        );
126        Err(Bail)
127    }
128
129    /// Is the current token an identifier with this exact text?
130    fn at_kw(&self, kw: &str) -> bool {
131        matches!(self.peek(), TokenKind::Ident(n) if n == kw)
132    }
133
134    /// After a failed statement: skip to the next plausible statement start —
135    /// past the end of the current line, consuming balanced braces so we
136    /// don't resynchronize in the middle of a block we were inside.
137    fn recover(&mut self) {
138        let mut depth = 0usize;
139        loop {
140            match self.peek() {
141                TokenKind::Eof => return,
142                TokenKind::LBrace => {
143                    depth += 1;
144                    self.bump();
145                }
146                TokenKind::RBrace => {
147                    if depth == 0 {
148                        return; // let the enclosing block see it
149                    }
150                    depth -= 1;
151                    self.bump();
152                }
153                TokenKind::Newline | TokenKind::Semi if depth == 0 => {
154                    self.bump();
155                    return;
156                }
157                _ => {
158                    self.bump();
159                }
160            }
161        }
162    }
163
164    // ---------------------------------------------------------------- file
165
166    fn parse_file(&mut self) -> File {
167        let header = self.parse_header();
168        let stmts = self.parse_stmt_list(true);
169        File { header, stmts }
170    }
171
172    fn parse_header(&mut self) -> Option<Header> {
173        self.skip_trivia();
174        if !self.at_kw("drawl") {
175            let span = self.span();
176            self.diags.push(
177                Diagnostic::warning("W0101", "missing `drawl` version header")
178                    .with_label(
179                        Span::new(span.start, span.start),
180                        "file should start with a version header",
181                    )
182                    .with_help("add `drawl 0.1` as the first line"),
183            );
184            return None;
185        }
186        let kw = self.bump();
187        let (version, vspan) = match self.peek().clone() {
188            TokenKind::Float(v) => {
189                let t = self.bump();
190                (format!("{v}"), t.span)
191            }
192            TokenKind::Int(v) => {
193                let t = self.bump();
194                (format!("{v}"), t.span)
195            }
196            other => {
197                self.diags.push(
198                    Diagnostic::error(
199                        "E0103",
200                        format!(
201                            "expected a version number after `drawl`, found {}",
202                            other.describe()
203                        ),
204                    )
205                    .with_label(self.span(), "expected something like `0.1`"),
206                );
207                self.recover();
208                return Some(Header {
209                    version: "0.1".into(),
210                    span: kw.span,
211                });
212            }
213        };
214        if version != "0.1" {
215            self.diags.push(
216                Diagnostic::error("E0106", format!("unsupported drawl version `{version}`"))
217                    .with_label(vspan, "this tool understands version `0.1`")
218                    .with_help("change the header to `drawl 0.1`"),
219            );
220        }
221        Some(Header {
222            version,
223            span: kw.span.to(vspan),
224        })
225    }
226
227    /// Parse statements until `}` (or EOF when `top_level`).
228    fn parse_stmt_list(&mut self, top_level: bool) -> Vec<Stmt> {
229        let mut stmts = Vec::new();
230        loop {
231            let trivia = self.collect_leading_trivia();
232            match self.peek() {
233                TokenKind::Eof => {
234                    self.flush_orphan_comments(trivia, &mut stmts);
235                    return stmts;
236                }
237                TokenKind::RBrace => {
238                    if top_level {
239                        self.diags.push(
240                            Diagnostic::error("E0110", "unmatched `}`")
241                                .with_label(self.span(), "no open block to close here"),
242                        );
243                        self.bump();
244                        continue;
245                    }
246                    self.flush_orphan_comments(trivia, &mut stmts);
247                    return stmts;
248                }
249                _ => {}
250            }
251            let start = self.span();
252            match self.parse_stmt() {
253                Ok(mut stmt) => {
254                    stmt.trivia = trivia;
255                    self.attach_trailing_comment(&mut stmt);
256                    stmts.push(stmt);
257                }
258                Err(Bail) => {
259                    self.recover();
260                    // Guard against zero-progress loops.
261                    if self.span() == start && !self.at_eof() {
262                        self.bump();
263                    }
264                }
265            }
266        }
267    }
268
269    fn collect_leading_trivia(&mut self) -> Trivia {
270        let mut trivia = Trivia::default();
271        let mut newline_run = 0usize;
272        loop {
273            match self.peek() {
274                // `;` separates statements on one line, like a newline.
275                TokenKind::Semi => {
276                    self.bump();
277                }
278                TokenKind::Newline => {
279                    newline_run += 1;
280                    if newline_run >= 2 && !trivia.leading.is_empty() {
281                        // Blank line after comments: those comments are
282                        // orphans, not attached to the next statement. Keep
283                        // them anyway (better than dropping); fmt prints them.
284                    }
285                    if newline_run >= 2 {
286                        trivia.blank_before = true;
287                    }
288                    self.bump();
289                }
290                TokenKind::Comment(text) => {
291                    let text = text.clone();
292                    trivia.leading.push(text);
293                    newline_run = 0;
294                    self.bump();
295                }
296                _ => return trivia,
297            }
298        }
299    }
300
301    /// Comments at the end of a block with no following statement: keep them
302    /// as a no-op empty statement so fmt round-trips them.
303    fn flush_orphan_comments(&mut self, trivia: Trivia, stmts: &mut Vec<Stmt>) {
304        if !trivia.leading.is_empty() {
305            stmts.push(Stmt {
306                kind: StmtKind::Prop(Prop {
307                    key: Vec::new(),
308                    value: Value::Num(0.0, Span::DUMMY),
309                    span: Span::DUMMY,
310                }),
311                span: Span::DUMMY,
312                trivia,
313            });
314        }
315    }
316
317    fn attach_trailing_comment(&mut self, stmt: &mut Stmt) {
318        if let TokenKind::Comment(text) = self.peek() {
319            stmt.trivia.trailing = Some(text.clone());
320            self.bump();
321        }
322    }
323
324    // ----------------------------------------------------------- statements
325
326    fn parse_stmt(&mut self) -> PResult<Stmt> {
327        let start = self.span();
328        // Keywords only act as keywords when the next token fits their shape,
329        // so `class: bus` (a property) coexists with `class bus { ... }`.
330        let next_is_ident = matches!(self.peek_at(1), TokenKind::Ident(_));
331        let next_is_lbrace = matches!(self.peek_at(1), TokenKind::LBrace);
332        let kind = if self.at_kw("canvas") && next_is_lbrace {
333            self.bump();
334            StmtKind::Canvas(self.parse_block()?)
335        } else if self.at_kw("def") && next_is_ident {
336            self.bump();
337            StmtKind::Def(self.parse_def()?)
338        } else if self.at_kw("group")
339            && (next_is_ident || matches!(self.peek_at(1), TokenKind::Str(_)))
340        {
341            self.bump();
342            StmtKind::Group(self.parse_group()?)
343        } else if self.at_kw("class") && next_is_ident {
344            self.bump();
345            let name = self.expect_ident("for the class")?;
346            let body = self.parse_block()?;
347            StmtKind::Class(Class { name, body })
348        } else if self.at_kw("constrain") && next_is_lbrace {
349            self.bump();
350            StmtKind::Constrain(self.parse_constrain_block()?)
351        } else if self.at_kw("pin") && next_is_ident {
352            self.bump();
353            StmtKind::Pin(self.parse_pin()?)
354        } else if self.at_kw("for") && next_is_ident {
355            self.bump();
356            StmtKind::For(self.parse_for()?)
357        } else if self.at_kw("port") && next_is_ident {
358            self.bump();
359            let name = self.expect_ident("for the port")?;
360            let body = if matches!(self.peek(), TokenKind::LBrace) {
361                self.parse_block()?
362            } else {
363                Block {
364                    stmts: Vec::new(),
365                    span: self.prev_span(),
366                }
367            };
368            StmtKind::Port(Port { name, body })
369        } else if matches!(self.peek(), TokenKind::Ident(_)) {
370            self.parse_ident_stmt()?
371        } else {
372            let found = self.peek().describe();
373            let mut d = Diagnostic::error("E0110", format!("expected a statement, found {found}"))
374                .with_label(self.span(), "not the start of any drawlang statement");
375            if let TokenKind::Ident(name) = self.peek() {
376                if let Some(s) = crate::diag::suggest(name, STMT_KEYWORDS.iter().copied()) {
377                    d = d.with_help(format!("did you mean `{s}`?"));
378                }
379            }
380            d = d.with_help(
381                "statements are nodes (`id { ... }`), edges (`a -> b`), properties \
382                 (`key: value`), or the keywords canvas/def/group/class/constrain/pin/for/port",
383            );
384            self.diags.push(d);
385            return Err(Bail);
386        };
387        let span = start.to(self.prev_span());
388        Ok(Stmt {
389            kind,
390            span,
391            trivia: Trivia::default(),
392        })
393    }
394
395    /// Statements that begin with a plain identifier: node declarations,
396    /// containers, instantiations, properties, and edges.
397    fn parse_ident_stmt(&mut self) -> PResult<StmtKind> {
398        // Try keyword suggestions for common typos at statement position:
399        // an ident directly followed by another ident is never valid.
400        if let (TokenKind::Ident(first), TokenKind::Ident(_)) = (self.peek(), self.peek_at(1)) {
401            if let Some(s) = crate::diag::suggest(first, STMT_KEYWORDS.iter().copied()) {
402                let first = first.clone();
403                self.diags.push(
404                    Diagnostic::error("E0110", format!("unknown statement `{first}`"))
405                        .with_label(self.span(), "not a drawlang keyword")
406                        .with_help(format!("did you mean `{s}`?")),
407                );
408                return Err(Bail);
409            }
410        }
411
412        let path = self.parse_path(false)?;
413
414        match self.peek() {
415            // Edges -------------------------------------------------------
416            TokenKind::Arrow | TokenKind::BidiArrow | TokenKind::BackArrow => {
417                self.parse_edge_rest(path)
418            }
419            // Node with body ------------------------------------------------
420            TokenKind::LBrace => {
421                let name = self.path_as_single_name(path, "node")?;
422                let body = self.parse_block()?;
423                Ok(StmtKind::Node(Node {
424                    name: Some(name),
425                    kind: NodeKind::Plain { body },
426                }))
427            }
428            // Bare instantiation -------------------------------------------
429            TokenKind::LParen => {
430                let callee = self.path_as_single_name(path, "component")?;
431                let args = self.parse_call_args()?;
432                let body = if matches!(self.peek(), TokenKind::LBrace) {
433                    Some(self.parse_block()?)
434                } else {
435                    None
436                };
437                Ok(StmtKind::Node(Node {
438                    name: None,
439                    kind: NodeKind::Call { callee, args, body },
440                }))
441            }
442            // `name: ...` — container, named call, or property ---------------
443            TokenKind::Colon => {
444                self.bump();
445                self.parse_after_colon(path)
446            }
447            // Bare node ----------------------------------------------------
448            TokenKind::Newline
449            | TokenKind::Semi
450            | TokenKind::Comment(_)
451            | TokenKind::RBrace
452            | TokenKind::Eof => {
453                let name = self.path_as_single_name(path, "node")?;
454                let span = name.span;
455                Ok(StmtKind::Node(Node {
456                    name: Some(name),
457                    kind: NodeKind::Plain {
458                        body: Block {
459                            stmts: Vec::new(),
460                            span,
461                        },
462                    },
463                }))
464            }
465            other => {
466                let found = other.describe();
467                self.diags.push(
468                    Diagnostic::error("E0103", format!(
469                        "expected `{{`, `:`, `(`, or an edge arrow after `{}`, found {found}",
470                        path.display()
471                    ))
472                    .with_label(self.span(), "unexpected here")
473                    .with_help("write `id { ... }` for a node, `id: value` for a property, or `a -> b` for an edge"),
474                );
475                Err(Bail)
476            }
477        }
478    }
479
480    fn parse_after_colon(&mut self, key_path: PathRef) -> PResult<StmtKind> {
481        // Containers: `row {`, `column {`, `grid 2x4 {`
482        if (self.at_kw("row") || self.at_kw("column"))
483            && matches!(self.peek_at(1), TokenKind::LBrace)
484        {
485            let name = self.path_as_single_name(key_path, "container")?;
486            let kw = self.bump();
487            let ctype = if let TokenKind::Ident(k) = &kw.kind {
488                if k == "row" {
489                    ContainerType::Row
490                } else {
491                    ContainerType::Column
492                }
493            } else {
494                unreachable!()
495            };
496            let body = self.parse_block()?;
497            return Ok(StmtKind::Node(Node {
498                name: Some(name),
499                kind: NodeKind::Container {
500                    ctype,
501                    ctype_span: kw.span,
502                    body,
503                },
504            }));
505        }
506        if self.at_kw("grid") {
507            let name = self.path_as_single_name(key_path, "container")?;
508            let kw = self.bump();
509            let (cols, rows, dspan) = match self.peek().clone() {
510                TokenKind::Dimension(c, r) => {
511                    let t = self.bump();
512                    (c, r, t.span)
513                }
514                other => {
515                    self.diags.push(
516                        Diagnostic::error(
517                            "E0103",
518                            format!(
519                                "expected grid dimensions after `grid`, found {}",
520                                other.describe()
521                            ),
522                        )
523                        .with_label(
524                            self.span(),
525                            "expected something like `2x4` (columns x rows)",
526                        ),
527                    );
528                    return Err(Bail);
529                }
530            };
531            if cols == 0 || rows == 0 {
532                self.diags.push(
533                    Diagnostic::error("E0105", "grid dimensions must be at least 1x1")
534                        .with_label(dspan, "zero-sized grid"),
535                );
536            }
537            let body = self.parse_block()?;
538            return Ok(StmtKind::Node(Node {
539                name: Some(name),
540                kind: NodeKind::Container {
541                    ctype: ContainerType::Grid { cols, rows },
542                    ctype_span: kw.span.to(dspan),
543                    body,
544                },
545            }));
546        }
547        // Named instantiation: `g0: gpu(0)`
548        if matches!(self.peek(), TokenKind::Ident(_))
549            && matches!(self.peek_at(1), TokenKind::LParen)
550        {
551            let name = self.path_as_single_name(key_path, "node")?;
552            let callee = self.expect_ident("for the component")?;
553            let args = self.parse_call_args()?;
554            let body = if matches!(self.peek(), TokenKind::LBrace) {
555                Some(self.parse_block()?)
556            } else {
557                None
558            };
559            return Ok(StmtKind::Node(Node {
560                name: Some(name),
561                kind: NodeKind::Call { callee, args, body },
562            }));
563        }
564        // Property: `key: value` (key may be dotted: `label.wrap`).
565        let key = self.path_as_prop_key(key_path)?;
566        let value = self.parse_value()?;
567        let span = key
568            .first()
569            .map(|k| k.span)
570            .unwrap_or(Span::DUMMY)
571            .to(value.span());
572        Ok(StmtKind::Prop(Prop { key, value, span }))
573    }
574
575    fn parse_edge_rest(&mut self, from: PathRef) -> PResult<StmtKind> {
576        let op_tok = self.bump();
577        let to = self.parse_path(false)?;
578        // `a <- b` is stored as `b -> a` so downstream code sees one direction.
579        let (from, op, to) = match op_tok.kind {
580            TokenKind::Arrow => (from, EdgeOp::Forward, to),
581            TokenKind::BidiArrow => (from, EdgeOp::Bidirectional, to),
582            TokenKind::BackArrow => (to, EdgeOp::Forward, from),
583            _ => unreachable!(),
584        };
585        let label = if self.eat(&TokenKind::Colon) {
586            match self.peek().clone() {
587                TokenKind::Str(_) => Some(self.parse_strlit()?),
588                other => {
589                    self.diags.push(
590                        Diagnostic::error(
591                            "E0103",
592                            format!(
593                                "expected a string label after `:`, found {}",
594                                other.describe()
595                            ),
596                        )
597                        .with_label(
598                            self.span(),
599                            r#"edge labels are strings, like `: "PCIe 5.0 x16"`"#,
600                        ),
601                    );
602                    return Err(Bail);
603                }
604            }
605        } else {
606            None
607        };
608        let props = if matches!(self.peek(), TokenKind::LBrace) {
609            Some(self.parse_block()?)
610        } else {
611            None
612        };
613        Ok(StmtKind::Edge(Edge {
614            from,
615            op,
616            op_span: op_tok.span,
617            to,
618            label,
619            props,
620        }))
621    }
622
623    fn parse_def(&mut self) -> PResult<Def> {
624        let name = self.expect_ident("for the component")?;
625        self.expect(TokenKind::LParen, "to open the parameter list")?;
626        let mut params = Vec::new();
627        self.skip_trivia();
628        while !matches!(self.peek(), TokenKind::RParen | TokenKind::Eof) {
629            params.push(self.expect_ident("for the parameter")?);
630            self.skip_trivia();
631            if !self.eat(&TokenKind::Comma) {
632                break;
633            }
634            self.skip_trivia();
635        }
636        self.expect(TokenKind::RParen, "to close the parameter list")?;
637        let body = self.parse_block()?;
638        Ok(Def { name, params, body })
639    }
640
641    fn parse_group(&mut self) -> PResult<Group> {
642        let name = self.expect_ident("for the group")?;
643        let label = if matches!(self.peek(), TokenKind::Str(_)) {
644            Some(self.parse_strlit()?)
645        } else {
646            None
647        };
648        let body = self.parse_block()?;
649        Ok(Group { name, label, body })
650    }
651
652    fn parse_pin(&mut self) -> PResult<Pin> {
653        let target = self.parse_path(false)?;
654        if !self.at_kw("at") {
655            self.diags.push(
656                Diagnostic::error(
657                    "E0103",
658                    format!(
659                        "expected `at` after the pin target, found {}",
660                        self.peek().describe()
661                    ),
662                )
663                .with_label(self.span(), "write `pin <element> at (x, y)`"),
664            );
665            return Err(Bail);
666        }
667        self.bump();
668        self.expect(TokenKind::LParen, "to open the position")?;
669        let x = self.parse_expr()?;
670        self.expect(TokenKind::Comma, "between the x and y coordinates")?;
671        let y = self.parse_expr()?;
672        self.expect(TokenKind::RParen, "to close the position")?;
673        Ok(Pin { target, x, y })
674    }
675
676    fn parse_for(&mut self) -> PResult<For> {
677        let var = self.expect_ident("for the loop variable")?;
678        if !self.at_kw("in") {
679            self.diags.push(
680                Diagnostic::error(
681                    "E0103",
682                    format!(
683                        "expected `in` after the loop variable, found {}",
684                        self.peek().describe()
685                    ),
686                )
687                .with_label(self.span(), "write `for i in 0..4 { ... }`"),
688            );
689            return Err(Bail);
690        }
691        self.bump();
692        let start = self.parse_expr()?;
693        self.expect(TokenKind::DotDot, "in the loop range")?;
694        let end = self.parse_expr()?;
695        let body = self.parse_block()?;
696        Ok(For {
697            var,
698            start,
699            end,
700            body,
701        })
702    }
703
704    fn parse_constrain_block(&mut self) -> PResult<Vec<Constraint>> {
705        self.expect(TokenKind::LBrace, "to open the constrain block")?;
706        let mut constraints = Vec::new();
707        loop {
708            let trivia = self.collect_leading_trivia();
709            match self.peek() {
710                TokenKind::RBrace => {
711                    self.bump();
712                    return Ok(constraints);
713                }
714                TokenKind::Eof => {
715                    self.diags.push(
716                        Diagnostic::error("E0103", "unclosed `constrain` block")
717                            .with_label(self.span(), "expected `}` before end of file"),
718                    );
719                    return Ok(constraints);
720                }
721                _ => {}
722            }
723            let before = self.pos;
724            match self.parse_constraint(trivia) {
725                Ok(c) => constraints.push(c),
726                Err(Bail) => {
727                    self.recover();
728                    if self.pos == before && !self.at_eof() {
729                        self.bump();
730                    }
731                }
732            }
733        }
734    }
735
736    fn parse_constraint(&mut self, trivia: Trivia) -> PResult<Constraint> {
737        let start = self.span();
738        // Constraint names may be hyphenated (`left-of`): join contiguous
739        // ident `-` ident sequences.
740        let mut name = self.expect_ident("for the constraint")?;
741        while matches!(self.peek(), TokenKind::Minus)
742            && self.span().start == name.span.end
743            && matches!(self.peek_at(1), TokenKind::Ident(_))
744        {
745            self.bump(); // -
746            let part = self.expect_ident("after `-`")?;
747            name = Ident {
748                name: format!("{}-{}", name.name, part.name),
749                span: name.span.to(part.span),
750            };
751        }
752        self.expect(TokenKind::LParen, "to open the constraint arguments")?;
753        let mut args = Vec::new();
754        self.skip_trivia();
755        while !matches!(self.peek(), TokenKind::RParen | TokenKind::Eof) {
756            args.push(self.parse_constraint_arg()?);
757            self.skip_trivia();
758            if !self.eat(&TokenKind::Comma) {
759                break;
760            }
761            self.skip_trivia();
762        }
763        self.expect(TokenKind::RParen, "to close the constraint arguments")?;
764        let mut c = Constraint {
765            name,
766            args,
767            span: start.to(self.prev_span()),
768            trivia: Trivia::default(),
769        };
770        c.trivia = trivia;
771        // Trailing comment on the same line.
772        if let TokenKind::Comment(text) = self.peek() {
773            c.trivia.trailing = Some(text.clone());
774            self.bump();
775        }
776        Ok(c)
777    }
778
779    fn parse_constraint_arg(&mut self) -> PResult<ConstraintArg> {
780        match self.peek().clone() {
781            TokenKind::Int(v) => {
782                let t = self.bump();
783                Ok(ConstraintArg::Num(v as f64, t.span))
784            }
785            TokenKind::Float(v) => {
786                let t = self.bump();
787                Ok(ConstraintArg::Num(v, t.span))
788            }
789            TokenKind::Minus => {
790                let start = self.bump().span;
791                match self.peek().clone() {
792                    TokenKind::Int(v) => {
793                        let t = self.bump();
794                        Ok(ConstraintArg::Num(-(v as f64), start.to(t.span)))
795                    }
796                    TokenKind::Float(v) => {
797                        let t = self.bump();
798                        Ok(ConstraintArg::Num(-v, start.to(t.span)))
799                    }
800                    other => {
801                        self.diags.push(
802                            Diagnostic::error(
803                                "E0103",
804                                format!("expected a number after `-`, found {}", other.describe()),
805                            )
806                            .with_label(self.span(), "expected a number"),
807                        );
808                        Err(Bail)
809                    }
810                }
811            }
812            TokenKind::Ident(_) => Ok(ConstraintArg::Path(self.parse_path(true)?)),
813            other => {
814                self.diags.push(
815                    Diagnostic::error(
816                        "E0103",
817                        format!(
818                            "expected an element path or number, found {}",
819                            other.describe()
820                        ),
821                    )
822                    .with_label(
823                        self.span(),
824                        "constraint arguments are element paths, keywords, or numbers",
825                    ),
826                );
827                Err(Bail)
828            }
829        }
830    }
831
832    // ------------------------------------------------------------- helpers
833
834    fn parse_block(&mut self) -> PResult<Block> {
835        let open = self.expect(TokenKind::LBrace, "to open the block")?;
836        let stmts = self.parse_stmt_list(false);
837        let close = if matches!(self.peek(), TokenKind::RBrace) {
838            self.bump().span
839        } else {
840            self.diags.push(
841                Diagnostic::error("E0103", "unclosed block")
842                    .with_label(open.span, "this `{` is never closed")
843                    .with_label(self.span(), "expected `}` before this point"),
844            );
845            self.span()
846        };
847        Ok(Block {
848            stmts,
849            span: open.span.to(close),
850        })
851    }
852
853    fn parse_call_args(&mut self) -> PResult<Vec<Expr>> {
854        self.expect(TokenKind::LParen, "to open the arguments")?;
855        let mut args = Vec::new();
856        self.skip_trivia();
857        while !matches!(self.peek(), TokenKind::RParen | TokenKind::Eof) {
858            args.push(self.parse_expr()?);
859            self.skip_trivia();
860            if !self.eat(&TokenKind::Comma) {
861                break;
862            }
863            self.skip_trivia();
864        }
865        self.expect(TokenKind::RParen, "to close the arguments")?;
866        Ok(args)
867    }
868
869    fn path_as_single_name(&mut self, path: PathRef, what: &str) -> PResult<Ident> {
870        match path.segments.as_slice() {
871            [PathSeg::Name(id)] => Ok(id.clone()),
872            _ => {
873                self.diags.push(
874                    Diagnostic::error("E0107", format!("{what} names must be a single identifier"))
875                        .with_label(
876                            path.span,
877                            format!("`{}` is a path, not a name", path.display()),
878                        )
879                        .with_help(
880                            "dots and indices are for *referring* to elements, not declaring them",
881                        ),
882                );
883                Err(Bail)
884            }
885        }
886    }
887
888    fn path_as_prop_key(&mut self, path: PathRef) -> PResult<Vec<Ident>> {
889        let mut key = Vec::new();
890        for seg in &path.segments {
891            match seg {
892                PathSeg::Name(id) => key.push(id.clone()),
893                _ => {
894                    self.diags.push(
895                        Diagnostic::error("E0107", "property keys cannot contain indices")
896                            .with_label(path.span, "expected a key like `label.wrap`"),
897                    );
898                    return Err(Bail);
899                }
900            }
901        }
902        Ok(key)
903    }
904
905    fn parse_path(&mut self, allow_wildcard: bool) -> PResult<PathRef> {
906        let first = self.expect_ident("to start an element path")?;
907        let start = first.span;
908        let mut segments = vec![PathSeg::Name(first)];
909        loop {
910            match self.peek() {
911                TokenKind::Dot => {
912                    self.bump();
913                    let id = self.expect_ident("after `.`")?;
914                    segments.push(PathSeg::Name(id));
915                }
916                TokenKind::LBracket => {
917                    self.bump();
918                    if matches!(self.peek(), TokenKind::Star) {
919                        let star = self.bump();
920                        if !allow_wildcard {
921                            self.diags.push(
922                                Diagnostic::error(
923                                    "E0108",
924                                    "`[*]` is only allowed in constraint arguments",
925                                )
926                                .with_label(star.span, "wildcard not allowed here")
927                                .with_help("name a specific index, like `[0]`"),
928                            );
929                        }
930                        segments.push(PathSeg::Wildcard(star.span));
931                    } else {
932                        let expr = self.parse_expr()?;
933                        segments.push(PathSeg::Index(expr));
934                    }
935                    self.expect(TokenKind::RBracket, "to close the index")?;
936                }
937                _ => break,
938            }
939        }
940        let span = start.to(self.prev_span());
941        Ok(PathRef { segments, span })
942    }
943
944    fn parse_value(&mut self) -> PResult<Value> {
945        match self.peek().clone() {
946            TokenKind::Str(_) => Ok(Value::Str(self.parse_strlit()?)),
947            TokenKind::Int(v) => {
948                let t = self.bump();
949                Ok(Value::Num(v as f64, t.span))
950            }
951            TokenKind::Float(v) => {
952                let t = self.bump();
953                Ok(Value::Num(v, t.span))
954            }
955            TokenKind::Minus => {
956                let start = self.bump().span;
957                match self.peek().clone() {
958                    TokenKind::Int(v) => {
959                        let t = self.bump();
960                        Ok(Value::Num(-(v as f64), start.to(t.span)))
961                    }
962                    TokenKind::Float(v) => {
963                        let t = self.bump();
964                        Ok(Value::Num(-v, start.to(t.span)))
965                    }
966                    other => {
967                        self.diags.push(
968                            Diagnostic::error(
969                                "E0103",
970                                format!("expected a number after `-`, found {}", other.describe()),
971                            )
972                            .with_label(self.span(), "expected a number"),
973                        );
974                        Err(Bail)
975                    }
976                }
977            }
978            TokenKind::Ident(name) => {
979                let t = self.bump();
980                Ok(Value::Word(Ident { name, span: t.span }))
981            }
982            TokenKind::AtIdent(name) => {
983                let t = self.bump();
984                Ok(Value::ThemeToken(Ident { name, span: t.span }))
985            }
986            TokenKind::HexColor(hex) => {
987                let t = self.bump();
988                Ok(Value::Color(hex, t.span))
989            }
990            other => {
991                self.diags.push(
992                    Diagnostic::error(
993                        "E0103",
994                        format!("expected a property value, found {}", other.describe()),
995                    )
996                    .with_label(
997                        self.span(),
998                        "expected a string, number, word, `@token`, or `#color`",
999                    ),
1000                );
1001                Err(Bail)
1002            }
1003        }
1004    }
1005
1006    // --------------------------------------------------------- expressions
1007
1008    fn parse_expr(&mut self) -> PResult<Expr> {
1009        self.parse_additive()
1010    }
1011
1012    fn parse_additive(&mut self) -> PResult<Expr> {
1013        let mut lhs = self.parse_multiplicative()?;
1014        loop {
1015            let op = match self.peek() {
1016                TokenKind::Plus => BinOp::Add,
1017                TokenKind::Minus => BinOp::Sub,
1018                _ => return Ok(lhs),
1019            };
1020            self.bump();
1021            let rhs = self.parse_multiplicative()?;
1022            let span = lhs.span.to(rhs.span);
1023            lhs = Expr {
1024                kind: ExprKind::Binary(op, Box::new(lhs), Box::new(rhs)),
1025                span,
1026            };
1027        }
1028    }
1029
1030    fn parse_multiplicative(&mut self) -> PResult<Expr> {
1031        let mut lhs = self.parse_unary()?;
1032        loop {
1033            let op = match self.peek() {
1034                TokenKind::Star => BinOp::Mul,
1035                TokenKind::Slash => BinOp::Div,
1036                TokenKind::Percent => BinOp::Mod,
1037                _ => return Ok(lhs),
1038            };
1039            self.bump();
1040            let rhs = self.parse_unary()?;
1041            let span = lhs.span.to(rhs.span);
1042            lhs = Expr {
1043                kind: ExprKind::Binary(op, Box::new(lhs), Box::new(rhs)),
1044                span,
1045            };
1046        }
1047    }
1048
1049    fn parse_unary(&mut self) -> PResult<Expr> {
1050        if matches!(self.peek(), TokenKind::Minus) {
1051            let start = self.bump().span;
1052            let inner = self.parse_unary()?;
1053            let span = start.to(inner.span);
1054            return Ok(Expr {
1055                kind: ExprKind::Unary(UnOp::Neg, Box::new(inner)),
1056                span,
1057            });
1058        }
1059        self.parse_primary()
1060    }
1061
1062    fn parse_primary(&mut self) -> PResult<Expr> {
1063        match self.peek().clone() {
1064            TokenKind::Int(v) => {
1065                let t = self.bump();
1066                Ok(Expr {
1067                    kind: ExprKind::Num(v as f64),
1068                    span: t.span,
1069                })
1070            }
1071            TokenKind::Float(v) => {
1072                let t = self.bump();
1073                Ok(Expr {
1074                    kind: ExprKind::Num(v),
1075                    span: t.span,
1076                })
1077            }
1078            TokenKind::Str(_) => {
1079                let s = self.parse_strlit()?;
1080                let span = s.span;
1081                Ok(Expr {
1082                    kind: ExprKind::Str(Box::new(s)),
1083                    span,
1084                })
1085            }
1086            TokenKind::Ident(name) => {
1087                let t = self.bump();
1088                Ok(Expr {
1089                    kind: ExprKind::Var(Ident { name, span: t.span }),
1090                    span: t.span,
1091                })
1092            }
1093            TokenKind::LParen => {
1094                self.bump();
1095                let inner = self.parse_expr()?;
1096                self.expect(TokenKind::RParen, "to close the parenthesized expression")?;
1097                Ok(inner)
1098            }
1099            other => {
1100                self.diags.push(
1101                    Diagnostic::error(
1102                        "E0103",
1103                        format!("expected an expression, found {}", other.describe()),
1104                    )
1105                    .with_label(self.span(), "expected a number, variable, or `(expr)`"),
1106                );
1107                Err(Bail)
1108            }
1109        }
1110    }
1111
1112    // ------------------------------------------------------------- strings
1113
1114    /// Parse the current Str token into literal/interpolated parts.
1115    /// Interpolated expressions are re-lexed from the original source so
1116    /// their spans point at the real file location.
1117    fn parse_strlit(&mut self) -> PResult<StrLit> {
1118        let tok = self.bump();
1119        let TokenKind::Str(_) = &tok.kind else {
1120            unreachable!("caller checked")
1121        };
1122        let span = tok.span;
1123        // Scan the raw source between the quotes so offsets stay exact.
1124        let inner_start = span.start + 1;
1125        let inner_end = span.end.saturating_sub(1).max(inner_start);
1126        let raw = &self.src[inner_start.min(self.src.len())..inner_end.min(self.src.len())];
1127
1128        let mut parts = Vec::new();
1129        let mut text = String::new();
1130        let bytes = raw.as_bytes();
1131        let mut i = 0;
1132        while i < bytes.len() {
1133            match bytes[i] {
1134                b'\\' if i + 1 < bytes.len() => {
1135                    match bytes[i + 1] {
1136                        b'n' => text.push('\n'),
1137                        b't' => text.push('\t'),
1138                        b'"' => text.push('"'),
1139                        b'\\' => text.push('\\'),
1140                        b'{' => text.push('{'),
1141                        b'}' => text.push('}'),
1142                        other => text.push(other as char), // already diagnosed by lexer
1143                    }
1144                    i += 2;
1145                }
1146                b'{' => {
1147                    // Find the matching close brace.
1148                    let expr_start = i + 1;
1149                    let mut depth = 1;
1150                    let mut j = expr_start;
1151                    while j < bytes.len() && depth > 0 {
1152                        match bytes[j] {
1153                            b'{' => depth += 1,
1154                            b'}' => depth -= 1,
1155                            _ => {}
1156                        }
1157                        j += 1;
1158                    }
1159                    let expr_end = j - 1; // index of the closing `}` (or end)
1160                    if depth > 0 {
1161                        // Lexer already reported E0104; treat rest as text.
1162                        text.push_str(&raw[i..]);
1163                        i = bytes.len();
1164                        continue;
1165                    }
1166                    if !text.is_empty() {
1167                        parts.push(StrPart::Text(std::mem::take(&mut text)));
1168                    }
1169                    let expr_src = &raw[expr_start..expr_end];
1170                    let abs_offset = inner_start + expr_start;
1171                    parts.push(StrPart::Expr(
1172                        self.parse_embedded_expr(expr_src, abs_offset)?,
1173                    ));
1174                    i = j;
1175                }
1176                _ => {
1177                    let ch = raw[i..].chars().next().unwrap();
1178                    text.push(ch);
1179                    i += ch.len_utf8();
1180                }
1181            }
1182        }
1183        if !text.is_empty() {
1184            parts.push(StrPart::Text(text));
1185        }
1186        Ok(StrLit { parts, span })
1187    }
1188
1189    fn parse_embedded_expr(&mut self, expr_src: &str, abs_offset: usize) -> PResult<Expr> {
1190        if expr_src.trim().is_empty() {
1191            self.diags.push(
1192                Diagnostic::error("E0104", "empty interpolation `{}` in string")
1193                    .with_label(
1194                        Span::new(abs_offset - 1, abs_offset + expr_src.len() + 1),
1195                        "nothing to interpolate",
1196                    )
1197                    .with_help(r#"put an expression inside the braces, like `"GPU {i}"`"#),
1198            );
1199            return Err(Bail);
1200        }
1201        let lexed = crate::lexer::lex(expr_src);
1202        // Shift sub-lexer diagnostics and token spans to absolute positions.
1203        for mut d in lexed.diagnostics {
1204            for l in &mut d.labels {
1205                l.span = Span::new(l.span.start + abs_offset, l.span.end + abs_offset);
1206            }
1207            self.diags.push(d);
1208        }
1209        let tokens: Vec<Token> = lexed
1210            .tokens
1211            .into_iter()
1212            .map(|t| Token {
1213                kind: t.kind,
1214                span: Span::new(t.span.start + abs_offset, t.span.end + abs_offset),
1215            })
1216            .collect();
1217        let mut sub = Parser {
1218            src: self.src,
1219            tokens,
1220            pos: 0,
1221            diags: Vec::new(),
1222        };
1223        let result = sub.parse_expr();
1224        let leftover = !matches!(sub.peek(), TokenKind::Newline | TokenKind::Eof);
1225        self.diags.extend(sub.diags);
1226        match result {
1227            Ok(expr) => {
1228                if leftover {
1229                    self.diags.push(
1230                        Diagnostic::error("E0104", "unexpected trailing tokens in interpolation")
1231                            .with_label(
1232                                Span::new(abs_offset, abs_offset + expr_src.len()),
1233                                "only a single expression is allowed inside `{...}`",
1234                            ),
1235                    );
1236                    return Err(Bail);
1237                }
1238                Ok(expr)
1239            }
1240            Err(Bail) => Err(Bail),
1241        }
1242    }
1243}