Skip to main content

rpic_core/
parser.rs

1//! Recursive-descent parser for the pic drawing core.
2//!
3//! Follows dpic's `grammar.txt`. Implemented: pictures (`.PS … .PE`), primitives
4//! with the full attribute set, positions (pairs, places, corners, ordinals,
5//! `between`, `± shifts`), expressions with proper precedence, `[ … ]` blocks,
6//! `{ … }` groups, labels, assignments, macros, includes, conditionals, loops,
7//! `print` and `exec`.
8
9use crate::ast::*;
10use crate::diagnostic::{Diagnostic, Span};
11use crate::lexer::{LexError, Spanned, lex, lex_named};
12use crate::token::*;
13
14/// A parse error with source location. `file` (via [`ParseError::span`]) is
15/// `None` for the user's own input, or the `copy` include / library name the
16/// position is relative to.
17#[derive(Debug, Clone, PartialEq)]
18pub struct ParseError {
19    pub msg: String,
20    pub line: u32,
21    pub col: u32,
22    pub end_col: u32,
23    file: Option<std::sync::Arc<str>>,
24    detail: Box<ParseErrorDetail>,
25}
26
27#[derive(Debug, Clone, PartialEq)]
28struct ParseErrorDetail {
29    kind: String,
30    found: Option<String>,
31    expected: Option<String>,
32    hint: Option<String>,
33}
34
35impl std::fmt::Display for ParseError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match &self.file {
38            Some(file) => write!(f, "{}:{}:{}: {}", file, self.line, self.col, self.msg),
39            None => write!(f, "{}:{}: {}", self.line, self.col, self.msg),
40        }
41    }
42}
43
44impl ParseError {
45    fn new(msg: impl Into<String>, span: Span) -> Self {
46        Self {
47            msg: msg.into(),
48            line: span.line,
49            col: span.col,
50            end_col: span.end_col,
51            file: span.file,
52            detail: Box::new(ParseErrorDetail {
53                kind: "parse".into(),
54                found: None,
55                expected: None,
56                hint: None,
57            }),
58        }
59    }
60
61    fn expected(expected: impl Into<String>, found: impl Into<String>, span: Span) -> Self {
62        let expected = expected.into();
63        let found = found.into();
64        Self {
65            msg: format!("expected {expected}, found {found}"),
66            line: span.line,
67            col: span.col,
68            end_col: span.end_col,
69            file: span.file,
70            detail: Box::new(ParseErrorDetail {
71                kind: "expected_token".into(),
72                found: Some(found),
73                expected: Some(expected),
74                hint: None,
75            }),
76        }
77    }
78
79    /// Override the diagnostic kind (crate-internal builder).
80    pub(crate) fn with_kind(mut self, kind: impl Into<String>) -> Self {
81        self.detail.kind = kind.into();
82        self
83    }
84
85    /// The error's source span, including which source it refers to.
86    pub fn span(&self) -> Span {
87        Span::new(self.line, self.col, self.end_col).in_file(self.file.clone())
88    }
89
90    pub fn diagnostic(&self) -> Diagnostic {
91        let mut d = Diagnostic::new(self.detail.kind.clone(), self.msg.clone()).at(self.span());
92        d.found = self.detail.found.clone();
93        d.expected = self.detail.expected.clone();
94        d.hint = self.detail.hint.clone();
95        d
96    }
97}
98
99impl From<LexError> for ParseError {
100    fn from(e: LexError) -> Self {
101        ParseError {
102            msg: e.msg,
103            line: e.line,
104            col: e.col,
105            end_col: e.end_col,
106            file: e.file,
107            detail: Box::new(ParseErrorDetail {
108                kind: e.kind,
109                found: None,
110                expected: None,
111                hint: None,
112            }),
113        }
114    }
115}
116
117/// Parse a full source string into a [`Picture`] with no filesystem context.
118/// `copy "file"` includes are unavailable (they require a base directory).
119pub fn parse(src: &str) -> Result<Picture, ParseError> {
120    parse_in_dir(src, None)
121}
122
123/// Parse pic source, resolving `copy "file"` includes relative to `base`
124/// with the default (unrestricted) include policy.
125pub fn parse_in_dir(src: &str, base: Option<&Path>) -> Result<Picture, ParseError> {
126    parse_with_prelude(
127        src,
128        IncludeCtx::unrestricted(base.map(|p| p.to_path_buf())),
129        false,
130        false,
131    )
132}
133
134/// Parse pic source with optional preludes: `circuits` loads the embedded
135/// circuit-element library and `texlabels` injects `texlabels = 1` — the
136/// library equivalents of the CLI `-c` / `-t` flags. Each prelude is lexed
137/// as its own named source unit (not text glued in front of `src`), so every
138/// diagnostic position stays relative to the source it belongs to: the user's
139/// own input reports user lines, and library problems name the library.
140pub fn parse_with_prelude(
141    src: &str,
142    includes: IncludeCtx,
143    circuits: bool,
144    texlabels: bool,
145) -> Result<Picture, ParseError> {
146    let mut toks: Vec<Spanned> = Vec::new();
147    if circuits {
148        splice_unit(&mut toks, lex_named(crate::CIRCUITS, "circuits")?);
149    }
150    if texlabels {
151        // Initializer only — the source stays sovereign (`texlabels = 0` wins).
152        splice_unit(&mut toks, lex_named("texlabels = 1\n", "<texlabels>")?);
153    }
154    let src = strip_backend_preamble(src);
155    toks.extend(lex(&src)?);
156    let (toks, macros) = preprocess(toks, &includes)?;
157    let mut pic = Parser::new(toks).parse_picture()?;
158    pic.macros = macros;
159    pic.includes = includes;
160    Ok(pic)
161}
162
163/// Append a lexed prelude unit ahead of the tokens that follow: drop its
164/// `Eof` and guarantee a trailing `Newline` so the units stay statement-
165/// separated (equivalent to the `\n` the old string-prepending inserted).
166fn splice_unit(out: &mut Vec<Spanned>, mut unit: Vec<Spanned>) {
167    if matches!(unit.last().map(|s| &s.tok), Some(Token::Eof)) {
168        unit.pop();
169    }
170    if !matches!(unit.last().map(|s| &s.tok), Some(Token::Newline)) {
171        let (line, file) = unit
172            .last()
173            .map(|s| (s.line, s.file.clone()))
174            .unwrap_or((1, None));
175        unit.push(Spanned::new(Token::Newline, line, 1).with_file(file));
176    }
177    out.extend(unit);
178}
179
180/// Parse a deferred body (the raw tokens of an `if`/`for` block) with the macro
181/// table in scope, expanding macro calls (and `copy` includes) along this
182/// executed path. Used by the evaluator so dead branches and recursive macros
183/// are never parsed.
184pub fn parse_body_tokens(
185    toks: &[Spanned],
186    macros: &mut Macros,
187    includes: &IncludeCtx,
188) -> Result<Vec<Stmt>, ParseError> {
189    let before = body_macro_frame(toks).unwrap_or_else(|| macros.clone());
190    let mut m = before.clone();
191    let mut input = toks.to_vec();
192    input.push(Spanned::new(Token::Eof, 0, 0));
193    let expanded = expand(&input, &mut m, 0, includes)?;
194    propagate_macro_changes(macros, &before, &m);
195    let mut p = Parser::new(expanded);
196    p.parse_elementlist(&[])
197}
198
199fn body_macro_frame(toks: &[Spanned]) -> Option<Macros> {
200    toks.iter()
201        .find_map(|s| s.macro_frame.as_ref().map(|m| m.as_ref().clone()))
202}
203
204fn propagate_macro_changes(macros: &mut Macros, before: &Macros, after: &Macros) {
205    for name in before.keys() {
206        if !after.contains_key(name) {
207            macros.remove(name);
208        }
209    }
210    for (name, body) in after {
211        if before.get(name) != Some(body) {
212            macros.insert(name.clone(), body.clone());
213        }
214    }
215}
216
217/// Parse pic source produced by `exec`, applying the caller's macro argument
218/// frame before normal macro expansion.
219pub(crate) fn parse_exec_source(
220    src: &str,
221    macros: &Macros,
222    includes: &IncludeCtx,
223    arg_frame: Option<&[Vec<Spanned>]>,
224) -> Result<Vec<Stmt>, ParseError> {
225    let mut toks = lex(src)?;
226    if let Some(args) = arg_frame {
227        toks = substitute(&toks, args);
228    }
229    let mut m = macros.clone();
230    let expanded = expand(&toks, &mut m, 0, includes)?;
231    let mut p = Parser::new(expanded);
232    p.parse_elementlist(&[])
233}
234
235pub(crate) fn parse_stringexpr_tokens(
236    toks: &[Spanned],
237    macros: &mut Macros,
238    includes: &IncludeCtx,
239) -> Result<StringExpr, ParseError> {
240    let mut input = toks.to_vec();
241    input.push(Spanned::new(Token::Eof, 0, 0));
242    let expanded = expand(&input, macros, 0, includes)?;
243    let mut p = Parser::new(expanded);
244    p.skip_newlines();
245    let expr = p.parse_stringexpr()?;
246    p.skip_newlines();
247    if !p.at(&Token::Eof) {
248        return p.err(format!("unexpected {:?} after string expression", p.cur()));
249    }
250    Ok(expr)
251}
252
253// ---- backend preamble filter ----------------------------------------------
254
255/// Drop non-SVG backend snippets commonly embedded in dpic examples.
256///
257/// These TeX/PSTricks preambles are meaningful to other backends, but for rpic's
258/// SVG output they should be tolerated as no-ops. Replacing ignored lines with
259/// empty lines keeps subsequent diagnostics on the original line numbers.
260fn strip_backend_preamble(src: &str) -> String {
261    let mut out = String::with_capacity(src.len());
262    let mut in_verbatimtex = false;
263    let mut in_string = false;
264    let mut in_raw_sh = false;
265
266    for line in src.lines() {
267        let trimmed = line.trim_start();
268        if in_verbatimtex {
269            out.push('\n');
270            if starts_word(trimmed, "etex") {
271                in_verbatimtex = false;
272            }
273            continue;
274        }
275
276        if in_raw_sh {
277            out.push_str(line);
278            out.push('\n');
279            in_raw_sh = line_continues(line);
280            continue;
281        }
282
283        if !in_string && starts_word(trimmed, "verbatimtex") {
284            in_verbatimtex = true;
285            out.push('\n');
286        } else if !in_string && is_ignored_backend_line(trimmed) {
287            out.push('\n');
288        } else {
289            out.push_str(line);
290            out.push('\n');
291            if starts_word(trimmed, "sh") {
292                in_raw_sh = line_continues(line);
293            } else {
294                in_string = update_string_state(line, in_string);
295            }
296        }
297    }
298    out
299}
300
301fn line_continues(line: &str) -> bool {
302    line.trim_end_matches([' ', '\t', '\r']).ends_with('\\')
303}
304
305fn update_string_state(line: &str, mut in_string: bool) -> bool {
306    let mut slashes = 0usize;
307    for c in line.chars() {
308        if c == '\\' {
309            slashes += 1;
310            continue;
311        }
312        if c == '"' && slashes.is_multiple_of(2) {
313            in_string = !in_string;
314        }
315        slashes = 0;
316    }
317    in_string
318}
319
320fn is_ignored_backend_line(trimmed: &str) -> bool {
321    trimmed.starts_with("\\global") || trimmed.starts_with("\\psset")
322}
323
324fn starts_word(s: &str, word: &str) -> bool {
325    let Some(rest) = s.strip_prefix(word) else {
326        return false;
327    };
328    !rest
329        .chars()
330        .next()
331        .is_some_and(|c| c.is_alphanumeric() || c == '_')
332}
333
334// ---- macro preprocessor ----------------------------------------------------
335//
336// Handles `define name { body }` (brace-delimited) with `$1..$9` argument
337// substitution at the token level, before parsing. Invocations `name(a, b)` (or
338// bare `name`) are replaced by the body with arguments spliced in; the result is
339// re-expanded so macros may call macros. `undef name` removes a definition.
340
341use std::collections::HashMap;
342use std::path::Path;
343
344fn preprocess(
345    input: Vec<Spanned>,
346    includes: &IncludeCtx,
347) -> Result<(Vec<Spanned>, Macros), ParseError> {
348    let mut macros: Macros = builtin_unit_macros();
349    let out = expand(&input, &mut macros, 0, includes)?;
350    Ok((out, macros))
351}
352
353/// dpic's absolute-unit suffix macros (`11bp__` → `11*(scale/72)`), predefined so
354/// examples that use them without `copy`ing dpictools still work. A user
355/// `define` of the same name overrides these.
356fn builtin_unit_macros() -> Macros {
357    let defs = [
358        ("bp__", "*(scale/72)"),       // Adobe big point
359        ("pt__", "*(scale/72.27)"),    // TeX point
360        ("pc__", "*(12*scale/72.27)"), // pica
361        ("in__", "*scale"),            // inch
362        ("cm__", "*(scale/2.54)"),     // centimetre
363        ("mm__", "*(scale/25.4)"),     // millimetre
364        ("px__", "*(scale/96)"),       // pixel (96 dpi)
365    ];
366    let mut m = Macros::new();
367    for (name, body) in defs {
368        if let Ok(toks) = lex(body) {
369            let body_toks: Vec<Spanned> =
370                toks.into_iter().filter(|s| s.tok != Token::Eof).collect();
371            m.insert(name.to_string(), body_toks);
372        }
373    }
374    m
375}
376
377fn loc(toks: &[Spanned], i: usize) -> Span {
378    toks.get(i)
379        .map(|s| s.span())
380        .unwrap_or_else(|| Span::new(0, 0, 0))
381}
382
383fn expand(
384    toks: &[Spanned],
385    macros: &mut HashMap<String, Vec<Spanned>>,
386    depth: usize,
387    includes: &IncludeCtx,
388) -> Result<Vec<Spanned>, ParseError> {
389    if depth > 64 {
390        return Err(ParseError::new(
391            "macro expansion too deep (recursive define?)",
392            Span::new(0, 0, 0),
393        ));
394    }
395    let mut out = Vec::new();
396    let mut i = 0;
397    while i < toks.len() {
398        match &toks[i].tok {
399            Token::Kw(Kw::Define) => {
400                let span = loc(toks, i);
401                i += 1;
402                let name = match toks.get(i).map(|s| &s.tok) {
403                    Some(Token::Name(n)) | Some(Token::Label(n)) => n.clone(),
404                    _ => {
405                        return Err(ParseError::new(
406                            "define: expected a macro name",
407                            span.clone(),
408                        ));
409                    }
410                };
411                i += 1;
412                // the macro body delimiter may begin on a following line
413                while toks.get(i).map(|s| &s.tok) == Some(&Token::Newline) {
414                    i += 1;
415                }
416                let Some(delim) = toks.get(i).map(|s| s.tok.clone()) else {
417                    return Err(ParseError::new(
418                        "define: expected a body delimiter",
419                        span.clone(),
420                    ));
421                };
422                let body = if delim == Token::LeftBrace {
423                    i += 1; // past `{`
424                    let start = i;
425                    let mut bd = 1;
426                    while i < toks.len() && bd > 0 {
427                        match &toks[i].tok {
428                            Token::LeftBrace => bd += 1,
429                            Token::RightBrace => {
430                                bd -= 1;
431                                if bd == 0 {
432                                    break;
433                                }
434                            }
435                            _ => {}
436                        }
437                        i += 1;
438                    }
439                    if bd != 0 {
440                        return Err(ParseError::new(
441                            "define: unterminated `{` body",
442                            span.clone(),
443                        ));
444                    }
445                    let body = trim_edge_newlines(&toks[start..i]);
446                    i += 1; // past `}`
447                    body
448                } else {
449                    if matches!(delim, Token::Eof | Token::Newline) {
450                        let span = loc(toks, i);
451                        return Err(ParseError::new(
452                            "define: expected a body delimiter",
453                            span.clone(),
454                        ));
455                    }
456                    i += 1; // past delimiter
457                    let start = i;
458                    while i < toks.len() && toks[i].tok != delim {
459                        i += 1;
460                    }
461                    if i >= toks.len() {
462                        return Err(ParseError::new(
463                            "define: unterminated delimited body",
464                            span.clone(),
465                        ));
466                    }
467                    let body = trim_edge_newlines(&toks[start..i]);
468                    i += 1; // past closing delimiter
469                    body
470                };
471                macros.insert(name, body);
472            }
473            Token::Kw(Kw::Undef) => {
474                i += 1;
475                if let Some(Token::Name(n)) | Some(Token::Label(n)) = toks.get(i).map(|s| &s.tok) {
476                    macros.remove(n);
477                }
478                i += 1;
479            }
480            // `if`/`for` bodies are copied verbatim (macro calls inside are not
481            // expanded here): they are expanded lazily, by the evaluator, only
482            // along the branch/iteration that actually runs. The condition/range
483            // is still expanded so macros there work.
484            Token::Kw(Kw::If) => {
485                let if_tok = toks[i].clone();
486                out.push(toks[i].clone());
487                i += 1;
488                let Some(te) = find_kw_depth0(toks, i, Kw::Then) else {
489                    continue; // malformed; let the parser report it
490                };
491                let cond = expand(&toks[i..te], macros, depth + 1, includes)?;
492                if let Some((then_body, after_then)) = read_braced_body(toks, te + 1)? {
493                    let mut j = after_then;
494                    while matches!(toks.get(j).map(|s| &s.tok), Some(Token::Newline)) {
495                        j += 1;
496                    }
497                    let else_body =
498                        if matches!(toks.get(j).map(|s| &s.tok), Some(Token::Kw(Kw::Else))) {
499                            read_braced_body(toks, j + 1)?
500                        } else {
501                            None
502                        };
503                    if let Some(take_then) = static_truth(&cond) {
504                        let after_static = else_body
505                            .as_ref()
506                            .map(|(_, after)| *after)
507                            .unwrap_or(after_then);
508                        out.pop(); // discard the speculative `if`
509                        if take_then {
510                            out.extend(expand(&then_body, macros, depth + 1, includes)?);
511                            i = after_static;
512                        } else if let Some((body, after)) = else_body {
513                            out.extend(expand(&body, macros, depth + 1, includes)?);
514                            i = after;
515                        } else {
516                            i = after_then;
517                        }
518                        continue;
519                    }
520                }
521                *out.last_mut().unwrap() = if_tok;
522                out.extend(cond);
523                out.push(toks[te].clone()); // `then`
524                i = copy_braced(toks, te + 1, &mut out, macros)?;
525                // optional `else { … }` (possibly across newlines)
526                let mut j = i;
527                while matches!(toks.get(j).map(|s| &s.tok), Some(Token::Newline)) {
528                    j += 1;
529                }
530                if matches!(toks.get(j).map(|s| &s.tok), Some(Token::Kw(Kw::Else))) {
531                    out.push(toks[j].clone());
532                    i = copy_braced(toks, j + 1, &mut out, macros)?;
533                }
534            }
535            Token::Kw(Kw::For) => {
536                out.push(toks[i].clone());
537                i += 1;
538                let Some(de) = find_kw_depth0(toks, i, Kw::Do) else {
539                    continue;
540                };
541                out.extend(expand(&toks[i..de], macros, depth + 1, includes)?);
542                out.push(toks[de].clone()); // `do`
543                i = copy_braced(toks, de + 1, &mut out, macros)?;
544            }
545            Token::Name(n) | Token::Label(n) if macros.contains_key(n) => {
546                let body = macros.get(n).unwrap().clone();
547                i += 1;
548                let args = if toks.get(i).map(|s| &s.tok) == Some(&Token::Lparen) {
549                    i += 1;
550                    let (a, ni) = read_args(toks, i)?;
551                    i = ni;
552                    a
553                } else {
554                    Vec::new()
555                };
556                let sub = substitute(&body, &args);
557                let expanded = expand(&sub, macros, depth + 1, includes)?;
558                out.extend(expanded);
559            }
560            // `copy "file"` splices another pic file's (expanded) tokens inline.
561            Token::Kw(Kw::Copy) => {
562                let span = loc(toks, i);
563                i += 1;
564                let Some(Token::Str(fname)) = toks.get(i).map(|s| &s.tok) else {
565                    return Err(ParseError::new(
566                        "copy: expected a quoted file name (only `copy \"file\"` is supported)",
567                        span.clone(),
568                    ));
569                };
570                let fname = fname.clone();
571                i += 1;
572                let inc = include_file(includes, &fname, macros, depth, span)?;
573                out.extend(inc);
574            }
575            _ => {
576                out.push(toks[i].clone());
577                i += 1;
578            }
579        }
580    }
581    Ok(out)
582}
583
584/// Read comma-separated argument token-lists after a `(` (index `i` is just past
585/// it), returning the args and the index after the matching `)`.
586fn read_args(toks: &[Spanned], mut i: usize) -> Result<(Vec<Vec<Spanned>>, usize), ParseError> {
587    let mut args: Vec<Vec<Spanned>> = Vec::new();
588    let mut cur: Vec<Spanned> = Vec::new();
589    let mut depth = 0i32;
590    loop {
591        let Some(s) = toks.get(i) else {
592            return Err(ParseError::new(
593                "unterminated macro arguments",
594                Span::new(0, 0, 0),
595            ));
596        };
597        match &s.tok {
598            Token::Lparen | Token::LeftBrack | Token::LeftBrace => {
599                depth += 1;
600                cur.push(s.clone());
601                i += 1;
602            }
603            Token::Rparen if depth == 0 => {
604                i += 1;
605                trim_trailing_newlines(&mut cur);
606                if !cur.is_empty() || !args.is_empty() {
607                    args.push(cur);
608                }
609                break;
610            }
611            Token::Rparen | Token::RightBrack | Token::RightBrace => {
612                depth -= 1;
613                cur.push(s.clone());
614                i += 1;
615            }
616            Token::Comma if depth == 0 => {
617                trim_trailing_newlines(&mut cur);
618                args.push(std::mem::take(&mut cur));
619                i += 1;
620            }
621            Token::Newline if depth == 0 && cur.is_empty() => {
622                i += 1;
623            }
624            _ => {
625                cur.push(s.clone());
626                i += 1;
627            }
628        }
629    }
630    Ok((args, i))
631}
632
633fn trim_trailing_newlines(toks: &mut Vec<Spanned>) {
634    while matches!(toks.last().map(|s| &s.tok), Some(Token::Newline)) {
635        toks.pop();
636    }
637}
638
639/// Drop leading and trailing `Newline` tokens (used for macro bodies, where the
640/// newlines around a multi-line `{ … }` are formatting, not structure).
641fn trim_edge_newlines(toks: &[Spanned]) -> Vec<Spanned> {
642    let mut start = 0;
643    let mut end = toks.len();
644    while start < end && toks[start].tok == Token::Newline {
645        start += 1;
646    }
647    while end > start && toks[end - 1].tok == Token::Newline {
648        end -= 1;
649    }
650    toks[start..end].to_vec()
651}
652
653/// Replace `$k` argument tokens in a macro body with the k-th argument's tokens.
654fn substitute(body: &[Spanned], args: &[Vec<Spanned>]) -> Vec<Spanned> {
655    let mut out = Vec::new();
656    let mut i = 0;
657    while i < body.len() {
658        if matches!(body[i].tok, Token::Kw(Kw::Define))
659            && let Some((define, next)) = copy_define_verbatim(body, i)
660        {
661            out.extend(define);
662            i = next;
663            continue;
664        }
665
666        if let Some((pasted, next)) = paste_adjacent_args(body, args, i) {
667            out.push(pasted.with_arg_frame(args));
668            i = next;
669            continue;
670        }
671
672        let s = &body[i];
673        match &s.tok {
674            Token::Arg(k) => {
675                if let Some(a) = args.get((*k as usize).wrapping_sub(1)) {
676                    out.extend(a.iter().cloned().map(|s| s.with_arg_frame(args)));
677                }
678            }
679            // `$+` is the number of arguments passed to this macro
680            Token::ArgCount => out.push(
681                Spanned::new(Token::Float(args.len() as f64), s.line, s.col).with_arg_frame(args),
682            ),
683            // `$n` is also substituted inside string literals (the `"$1"==""`
684            // default-argument idiom, sprintf templates like `"$2%g"`, …).
685            Token::Str(text) if text.contains('$') => {
686                out.push(
687                    Spanned::new(Token::Str(subst_in_string(text, args)), s.line, s.col)
688                        .with_arg_frame(args),
689                );
690            }
691            _ => out.push(s.clone().with_arg_frame(args)),
692        }
693        i += 1;
694    }
695    out
696}
697
698fn copy_define_verbatim(body: &[Spanned], start: usize) -> Option<(Vec<Spanned>, usize)> {
699    let mut name_idx = start + 1;
700    while matches!(body.get(name_idx).map(|s| &s.tok), Some(Token::Newline)) {
701        name_idx += 1;
702    }
703    if !matches!(
704        body.get(name_idx).map(|s| &s.tok),
705        Some(Token::Name(_)) | Some(Token::Label(_))
706    ) {
707        return None;
708    }
709
710    let mut out = Vec::new();
711    let mut i = start;
712    while let Some(s) = body.get(i) {
713        out.push(s.clone());
714        i += 1;
715        if matches!(s.tok, Token::LeftBrace) {
716            break;
717        }
718    }
719
720    if !matches!(out.last().map(|s| &s.tok), Some(Token::LeftBrace)) {
721        return None;
722    }
723
724    let mut depth = 1i32;
725    while let Some(s) = body.get(i) {
726        match &s.tok {
727            Token::LeftBrace => depth += 1,
728            Token::RightBrace => depth -= 1,
729            _ => {}
730        }
731        out.push(s.clone());
732        i += 1;
733        if depth == 0 {
734            return Some((out, i));
735        }
736    }
737    None
738}
739
740fn paste_adjacent_args(
741    body: &[Spanned],
742    args: &[Vec<Spanned>],
743    start: usize,
744) -> Option<(Spanned, usize)> {
745    let first = body.get(start)?;
746    let Token::Arg(k) = &first.tok else {
747        return None;
748    };
749
750    let mut text = arg_text(*k, args);
751    let mut count = 1usize;
752    let mut end = start + 1;
753    let mut prev = first;
754    while let Some(next) = body.get(end) {
755        let Token::Arg(k) = &next.tok else {
756            break;
757        };
758        if !adjacent_arg_tokens(prev, next) {
759            break;
760        }
761        text.push_str(&arg_text(*k, args));
762        count += 1;
763        prev = next;
764        end += 1;
765    }
766
767    if count < 2 || text.is_empty() {
768        return None;
769    }
770
771    Some((tokenize_pasted_arg_text(&text, first.line, first.col), end))
772}
773
774fn arg_text(k: u32, args: &[Vec<Spanned>]) -> String {
775    args.get((k as usize).wrapping_sub(1))
776        .map(|a| tokens_to_text(a))
777        .unwrap_or_default()
778}
779
780fn adjacent_arg_tokens(left: &Spanned, right: &Spanned) -> bool {
781    left.line == right.line && arg_end_col(left) == Some(right.col)
782}
783
784fn arg_end_col(s: &Spanned) -> Option<u32> {
785    let Token::Arg(k) = &s.tok else {
786        return None;
787    };
788    Some(s.col + 1 + k.to_string().len() as u32)
789}
790
791fn tokenize_pasted_arg_text(text: &str, line: u32, col: u32) -> Spanned {
792    if let Ok(toks) = lex(text)
793        && toks.len() == 2
794        && matches!(toks[1].tok, Token::Eof)
795    {
796        return Spanned::new(toks[0].tok.clone(), line, col);
797    }
798
799    let tok = if text.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
800        Token::Label(text.to_string())
801    } else {
802        Token::Name(text.to_string())
803    };
804    Spanned::new(tok, line, col)
805}
806
807/// Replace `$n` references inside a string literal with the textual form of the
808/// n-th macro argument (empty if missing).
809fn subst_in_string(text: &str, args: &[Vec<Spanned>]) -> String {
810    let chars: Vec<char> = text.chars().collect();
811    let mut out = String::new();
812    let mut i = 0;
813    while i < chars.len() {
814        if chars[i] == '$' && chars.get(i + 1) == Some(&'+') {
815            out.push_str(&args.len().to_string());
816            i += 2;
817        } else if chars[i] == '$' && chars.get(i + 1).is_some_and(|c| c.is_ascii_digit()) {
818            let mut j = i + 1;
819            let mut num = String::new();
820            while j < chars.len() && chars[j].is_ascii_digit() {
821                num.push(chars[j]);
822                j += 1;
823            }
824            if let Ok(k) = num.parse::<usize>()
825                && k >= 1
826                && let Some(a) = args.get(k - 1)
827            {
828                out.push_str(&tokens_to_text(a));
829            }
830            i = j;
831        } else {
832            out.push(chars[i]);
833            i += 1;
834        }
835    }
836    out
837}
838
839/// Best-effort textual rendering of an argument token list, for `$n` splicing
840/// inside string literals.
841fn tokens_to_text(toks: &[Spanned]) -> String {
842    let mut s = String::new();
843    for t in toks {
844        match &t.tok {
845            Token::Float(v) => s.push_str(&format!("{v}")),
846            Token::Str(t) => s.push_str(t),
847            Token::Name(n) | Token::Label(n) => s.push_str(n),
848            Token::Lparen => s.push('('),
849            Token::Rparen => s.push(')'),
850            Token::LeftBrack => s.push('['),
851            Token::RightBrack => s.push(']'),
852            Token::LeftBrace => s.push('{'),
853            Token::RightBrace => s.push('}'),
854            Token::Comma => s.push(','),
855            Token::Colon => s.push(':'),
856            Token::Dot => s.push('.'),
857            Token::Plus => s.push('+'),
858            Token::Minus => s.push('-'),
859            Token::Mult => s.push('*'),
860            Token::Div => s.push('/'),
861            Token::Percent => s.push('%'),
862            Token::Dollar => s.push('$'),
863            Token::Backslash => s.push('\\'),
864            Token::DotX => s.push_str(".x"),
865            Token::DotY => s.push_str(".y"),
866            Token::DotPS => s.push_str(".PS"),
867            Token::DotPE => s.push_str(".PE"),
868            Token::Corner(c) => s.push_str(corner_text(*c)),
869            Token::Param(p) => s.push_str(param_text(*p)),
870            Token::LineType(l) => s.push_str(line_type_text(*l)),
871            Token::TextPos(p) => s.push_str(text_pos_text(*p)),
872            Token::Arrow(a) => s.push_str(arrow_text(*a)),
873            Token::Dir(d) => s.push_str(dir_text(*d)),
874            Token::Prim(p) => s.push_str(prim_text(*p)),
875            Token::Color(c) => s.push_str(color_text(*c)),
876            _ => {}
877        }
878    }
879    s
880}
881
882fn corner_text(c: Corner) -> &'static str {
883    match c {
884        Corner::N => ".n",
885        Corner::S => ".s",
886        Corner::E => ".e",
887        Corner::W => ".w",
888        Corner::Ne => ".ne",
889        Corner::Se => ".se",
890        Corner::Nw => ".nw",
891        Corner::Sw => ".sw",
892        Corner::Start => ".start",
893        Corner::End => ".end",
894        Corner::Center => ".c",
895    }
896}
897
898fn param_text(p: Param) -> &'static str {
899    match p {
900        Param::Height => ".ht",
901        Param::Width => ".wid",
902        Param::Radius => ".rad",
903        Param::Diameter => ".diam",
904        Param::Thickness => ".thick",
905        Param::Length => ".len",
906    }
907}
908
909fn line_type_text(l: LineType) -> &'static str {
910    match l {
911        LineType::Solid => "solid",
912        LineType::Dotted => "dotted",
913        LineType::Dashed => "dashed",
914        LineType::Invis => "invis",
915    }
916}
917
918fn text_pos_text(p: TextPos) -> &'static str {
919    match p {
920        TextPos::Center => "center",
921        TextPos::Ljust => "ljust",
922        TextPos::Rjust => "rjust",
923        TextPos::Above => "above",
924        TextPos::Below => "below",
925    }
926}
927
928fn arrow_text(a: Arrow) -> &'static str {
929    match a {
930        Arrow::Left => "<-",
931        Arrow::Right => "->",
932        Arrow::Double => "<->",
933    }
934}
935
936fn dir_text(d: Dir) -> &'static str {
937    match d {
938        Dir::Up => "up",
939        Dir::Down => "down",
940        Dir::Right => "right",
941        Dir::Left => "left",
942    }
943}
944
945fn prim_text(p: Prim) -> &'static str {
946    match p {
947        Prim::Box => "box",
948        Prim::Circle => "circle",
949        Prim::Ellipse => "ellipse",
950        Prim::Arc => "arc",
951        Prim::Line => "line",
952        Prim::Arrow => "arrow",
953        Prim::Move => "move",
954        Prim::Spline => "spline",
955    }
956}
957
958fn color_text(c: Color) -> &'static str {
959    match c {
960        Color::Colored => "color",
961        Color::Outlined => "outlined",
962        Color::Shaded => "shaded",
963    }
964}
965
966fn kw_text(k: Kw) -> &'static str {
967    match k {
968        Kw::Ht => "ht",
969        Kw::Wid => "wid",
970        Kw::Rad => "rad",
971        Kw::Diam => "diam",
972        Kw::Thick => "thick",
973        Kw::Scaled => "scaled",
974        Kw::From => "from",
975        Kw::To => "to",
976        Kw::At => "at",
977        Kw::With => "with",
978        Kw::By => "by",
979        Kw::Then => "then",
980        Kw::Continue => "continue",
981        Kw::Chop => "chop",
982        Kw::Same => "same",
983        Kw::Cw => "cw",
984        Kw::Ccw => "ccw",
985        Kw::Of => "of",
986        Kw::The => "the",
987        Kw::Way => "way",
988        Kw::Between => "between",
989        Kw::And => "and",
990        Kw::Here => "Here",
991        Kw::Last => "last",
992        Kw::Fill => "fill",
993        Kw::Nth => "ordinal suffix",
994        Kw::Print => "print",
995        Kw::Copy => "copy",
996        Kw::Reset => "reset",
997        Kw::Exec => "exec",
998        Kw::Sh => "sh",
999        Kw::Command => "command",
1000        Kw::Define => "define",
1001        Kw::Undef => "undef",
1002        Kw::Rand => "rand",
1003        Kw::If => "if",
1004        Kw::Else => "else",
1005        Kw::For => "for",
1006        Kw::Do => "do",
1007        Kw::Sprintf => "sprintf",
1008        Kw::Animate => "animate",
1009        Kw::After => "after",
1010        Kw::Delay => "delay",
1011    }
1012}
1013
1014fn token_text(t: &Token) -> String {
1015    match t {
1016        Token::Float(v) => fmt_float(*v),
1017        Token::Str(s) => format!("\"{s}\""),
1018        Token::Name(s) | Token::Label(s) => format!("`{s}`"),
1019        Token::Arg(n) => format!("${n}"),
1020        Token::ArgCount => "$+".into(),
1021        Token::Dollar => "$".into(),
1022        Token::Backslash => "\\".into(),
1023        Token::Newline => "end of line".into(),
1024        Token::DotPS => ".PS".into(),
1025        Token::DotPE => ".PE".into(),
1026        Token::Eof => "end of input".into(),
1027        Token::Lt => "<".into(),
1028        Token::Lparen => "(".into(),
1029        Token::Rparen => ")".into(),
1030        Token::Mult => "*".into(),
1031        Token::Plus => "+".into(),
1032        Token::Minus => "-".into(),
1033        Token::Div => "/".into(),
1034        Token::Percent => "%".into(),
1035        Token::Caret => "^".into(),
1036        Token::Not => "!".into(),
1037        Token::AndAnd => "&&".into(),
1038        Token::OrOr => "||".into(),
1039        Token::Ampersand => "&".into(),
1040        Token::Comma => ",".into(),
1041        Token::Colon => ":".into(),
1042        Token::LeftBrack => "[".into(),
1043        Token::RightBrack => "]".into(),
1044        Token::LeftBrace => "{".into(),
1045        Token::RightBrace => "}".into(),
1046        Token::Dot => ".".into(),
1047        Token::Block => "[]".into(),
1048        Token::LeftQuote => "`".into(),
1049        Token::RightQuote => "'".into(),
1050        Token::Eq => "=".into(),
1051        Token::ColonEq => ":=".into(),
1052        Token::PlusEq => "+=".into(),
1053        Token::MinusEq => "-=".into(),
1054        Token::MultEq => "*=".into(),
1055        Token::DivEq => "/=".into(),
1056        Token::RemEq => "%=".into(),
1057        Token::EqEq => "==".into(),
1058        Token::Neq => "!=".into(),
1059        Token::Ge => ">=".into(),
1060        Token::Le => "<=".into(),
1061        Token::Gt => ">".into(),
1062        Token::DotX => ".x".into(),
1063        Token::DotY => ".y".into(),
1064        Token::Kw(k) => kw_text(*k).into(),
1065        Token::Corner(c) => corner_text(*c).into(),
1066        Token::Param(p) => format!(".{}", param_text(*p)),
1067        Token::Func1(f) => format!("{f:?}").to_ascii_lowercase(),
1068        Token::Func2(f) => format!("{f:?}").to_ascii_lowercase(),
1069        Token::LineType(l) => line_type_text(*l).into(),
1070        Token::TextPos(p) => text_pos_text(*p).into(),
1071        Token::Arrow(a) => arrow_text(*a).into(),
1072        Token::Dir(d) => dir_text(*d).into(),
1073        Token::Prim(p) => prim_text(*p).into(),
1074        Token::Color(c) => color_text(*c).into(),
1075        Token::EnvVar(e) => format!("{e:?}").to_ascii_lowercase(),
1076    }
1077}
1078
1079fn fmt_float(v: f64) -> String {
1080    let mut s = v.to_string();
1081    if s.ends_with(".0") {
1082        s.truncate(s.len() - 2);
1083    }
1084    s
1085}
1086
1087fn suggest_object(word: &str) -> Option<&'static str> {
1088    const WORDS: &[&str] = &[
1089        "arc", "arrow", "box", "brace", "circle", "dot", "ellipse", "line", "move", "spline",
1090    ];
1091    crate::diagnostic::closest(word, WORDS)
1092}
1093
1094/// Read and tokenize a `copy "file"` include, returning its expanded tokens
1095/// (with the trailing `Eof` removed so it splices cleanly mid-stream). The
1096/// included file resolves nested `copy`s relative to its own directory.
1097fn include_file(
1098    includes: &IncludeCtx,
1099    fname: &str,
1100    macros: &mut HashMap<String, Vec<Spanned>>,
1101    depth: usize,
1102    span: Span,
1103) -> Result<Vec<Spanned>, ParseError> {
1104    let mkerr = |msg: String| ParseError::new(msg, span.clone());
1105    let denied = |why: &str| {
1106        ParseError::new(format!("copy \"{fname}\": {why}"), span.clone())
1107            .with_kind("include_denied")
1108    };
1109    // `copy "circuits"` is a reserved target: it loads the embedded native
1110    // circuit-element library — the in-source spelling of `-c`, usable even
1111    // where file includes are not (wasm, compile_json with no base dir). It
1112    // shadows any real file literally named `circuits`. Skipped when the
1113    // library is already loaded (`-c` plus an explicit copy): `__resistor`
1114    // is one of its own defines.
1115    if fname == "circuits" {
1116        if macros.contains_key("__resistor") {
1117            return Ok(Vec::new());
1118        }
1119        let toks = lex_named(crate::CIRCUITS, "circuits")?;
1120        let mut expanded = expand(&toks, macros, depth + 1, includes)?;
1121        if matches!(expanded.last().map(|s| &s.tok), Some(Token::Eof)) {
1122            expanded.pop();
1123        }
1124        return Ok(expanded);
1125    }
1126    if includes.policy == IncludePolicy::Deny {
1127        return Err(denied(
1128            "filesystem includes are disabled by the include policy",
1129        ));
1130    }
1131    let p = Path::new(fname);
1132    if p.is_absolute() && includes.policy == IncludePolicy::SandboxedToBase {
1133        return Err(denied(
1134            "absolute paths are not allowed by the include policy",
1135        ));
1136    }
1137    let path = if p.is_absolute() {
1138        p.to_path_buf()
1139    } else {
1140        match includes.dir.as_deref() {
1141            Some(b) => b.join(p),
1142            None => {
1143                return Err(mkerr(format!(
1144                    "copy \"{fname}\": file includes require a file path (unavailable here)"
1145                )));
1146            }
1147        }
1148    };
1149    if includes.policy == IncludePolicy::SandboxedToBase {
1150        // Canonicalize (resolving `..` and symlinks) and require the result
1151        // to stay inside the fence root. A fence that failed to resolve at
1152        // setup fails closed. The error names the path as written, never the
1153        // resolved location.
1154        let inside = includes
1155            .fence
1156            .as_deref()
1157            .is_some_and(|fence| std::fs::canonicalize(&path).is_ok_and(|c| c.starts_with(fence)));
1158        if !inside {
1159            return Err(denied("path resolves outside the include base directory"));
1160        }
1161    }
1162    let content =
1163        std::fs::read_to_string(&path).map_err(|e| mkerr(format!("copy \"{fname}\": {e}")))?;
1164    let toks = lex_named(&content, fname)?;
1165    let inc_base = path.parent().map(|d| d.to_path_buf());
1166    let inc_ctx = includes.child(inc_base);
1167    let mut expanded = expand(&toks, macros, depth + 1, &inc_ctx)?;
1168    if matches!(expanded.last().map(|s| &s.tok), Some(Token::Eof)) {
1169        expanded.pop();
1170    }
1171    Ok(expanded)
1172}
1173
1174/// Find the next occurrence of keyword `kw` at bracket-depth 0 from `start`.
1175fn find_kw_depth0(toks: &[Spanned], start: usize, kw: Kw) -> Option<usize> {
1176    let mut depth = 0i32;
1177    for (off, s) in toks[start..].iter().enumerate() {
1178        match &s.tok {
1179            Token::Lparen | Token::LeftBrace | Token::LeftBrack => depth += 1,
1180            Token::Rparen | Token::RightBrace | Token::RightBrack => {
1181                depth -= 1;
1182                if depth < 0 {
1183                    return None;
1184                }
1185            }
1186            Token::Kw(k) if *k == kw && depth == 0 => return Some(start + off),
1187            _ => {}
1188        }
1189    }
1190    None
1191}
1192
1193/// Copy a brace-delimited block verbatim into `out` (including any nested
1194/// braces), skipping/copying leading newlines. Returns the index past the `}`.
1195fn copy_braced(
1196    toks: &[Spanned],
1197    mut i: usize,
1198    out: &mut Vec<Spanned>,
1199    macros: &HashMap<String, Vec<Spanned>>,
1200) -> Result<usize, ParseError> {
1201    while matches!(toks.get(i).map(|s| &s.tok), Some(Token::Newline)) {
1202        out.push(toks[i].clone());
1203        i += 1;
1204    }
1205    if !matches!(toks.get(i).map(|s| &s.tok), Some(Token::LeftBrace)) {
1206        return Ok(i); // no body; the parser will report the problem
1207    }
1208    let mut depth = 0i32;
1209    let mut tagged_body = false;
1210    while let Some(s) = toks.get(i) {
1211        let mut s = s.clone();
1212        let outer_left_brace = depth == 0 && matches!(s.tok, Token::LeftBrace);
1213        match &s.tok {
1214            Token::LeftBrace => depth += 1,
1215            Token::RightBrace => {
1216                out.push(s);
1217                i += 1;
1218                depth -= 1;
1219                if depth == 0 {
1220                    return Ok(i);
1221                }
1222                continue;
1223            }
1224            _ => {}
1225        }
1226        if depth == 1 && !outer_left_brace && !tagged_body {
1227            s = s.with_macro_frame(macros);
1228            tagged_body = true;
1229        }
1230        out.push(s);
1231        i += 1;
1232    }
1233    Err(ParseError::new("unterminated `{` body", Span::new(0, 0, 0)))
1234}
1235
1236fn read_braced_body(
1237    toks: &[Spanned],
1238    mut i: usize,
1239) -> Result<Option<(Vec<Spanned>, usize)>, ParseError> {
1240    while matches!(toks.get(i).map(|s| &s.tok), Some(Token::Newline)) {
1241        i += 1;
1242    }
1243    if !matches!(toks.get(i).map(|s| &s.tok), Some(Token::LeftBrace)) {
1244        return Ok(None);
1245    }
1246    i += 1;
1247    let start = i;
1248    let mut depth = 1i32;
1249    while let Some(s) = toks.get(i) {
1250        match &s.tok {
1251            Token::LeftBrace => depth += 1,
1252            Token::RightBrace => {
1253                depth -= 1;
1254                if depth == 0 {
1255                    return Ok(Some((toks[start..i].to_vec(), i + 1)));
1256                }
1257            }
1258            _ => {}
1259        }
1260        i += 1;
1261    }
1262    Err(ParseError::new("unterminated `{` body", Span::new(0, 0, 0)))
1263}
1264
1265fn static_truth(toks: &[Spanned]) -> Option<bool> {
1266    let toks = trim_trailing_eof(toks);
1267    match toks {
1268        [
1269            Spanned {
1270                tok: Token::Float(v),
1271                ..
1272            },
1273        ] => Some(*v != 0.0),
1274        [
1275            Spanned {
1276                tok: Token::Not, ..
1277            },
1278            Spanned {
1279                tok: Token::Float(v),
1280                ..
1281            },
1282        ] => Some(*v == 0.0),
1283        [a, op, b] => match op.tok {
1284            Token::EqEq | Token::Neq => {
1285                if let (Some(lhs), Some(rhs)) = (static_string(a), static_string(b)) {
1286                    return Some(if matches!(op.tok, Token::EqEq) {
1287                        lhs == rhs
1288                    } else {
1289                        lhs != rhs
1290                    });
1291                }
1292                if let (Some(lhs), Some(rhs)) = (static_number(a), static_number(b)) {
1293                    return Some(if matches!(op.tok, Token::EqEq) {
1294                        (lhs - rhs).abs() < f64::EPSILON
1295                    } else {
1296                        (lhs - rhs).abs() >= f64::EPSILON
1297                    });
1298                }
1299                None
1300            }
1301            _ => None,
1302        },
1303        [a, op, b, op2, c] if matches!(op2.tok, Token::Plus) => {
1304            let lhs = static_string(a)?;
1305            let mut rhs = static_string(b)?;
1306            rhs.push_str(&static_string(c)?);
1307            match op.tok {
1308                Token::EqEq => Some(lhs == rhs),
1309                Token::Neq => Some(lhs != rhs),
1310                _ => None,
1311            }
1312        }
1313        _ => None,
1314    }
1315}
1316
1317fn trim_trailing_eof(toks: &[Spanned]) -> &[Spanned] {
1318    if matches!(toks.last().map(|s| &s.tok), Some(Token::Eof)) {
1319        &toks[..toks.len() - 1]
1320    } else {
1321        toks
1322    }
1323}
1324
1325fn static_string(s: &Spanned) -> Option<String> {
1326    match &s.tok {
1327        Token::Str(v) => Some(v.clone()),
1328        _ => None,
1329    }
1330}
1331
1332fn static_number(s: &Spanned) -> Option<f64> {
1333    match &s.tok {
1334        Token::Float(v) => Some(*v),
1335        Token::Name(n) | Token::Label(n) => dpic_backend_constant(n),
1336        _ => None,
1337    }
1338}
1339
1340fn dpic_backend_constant(name: &str) -> Option<f64> {
1341    match name {
1342        "optMFpic" => Some(0.0),
1343        "optMpost" => Some(1.0),
1344        "optPDF" => Some(2.0),
1345        "optPGF" => Some(3.0),
1346        "optPict2e" => Some(4.0),
1347        "optPS" => Some(5.0),
1348        "optPSfrag" => Some(6.0),
1349        "optPSTricks" => Some(7.0),
1350        "optSVG" | "dpicopt" => Some(8.0),
1351        "optTeX" => Some(9.0),
1352        "opttTeX" => Some(10.0),
1353        "optxfig" => Some(11.0),
1354        _ => None,
1355    }
1356}
1357
1358type PResult<T> = Result<T, ParseError>;
1359
1360fn is_assign_op(t: &Token) -> bool {
1361    matches!(
1362        t,
1363        Token::Eq
1364            | Token::ColonEq
1365            | Token::PlusEq
1366            | Token::MinusEq
1367            | Token::MultEq
1368            | Token::DivEq
1369            | Token::RemEq
1370    )
1371}
1372
1373struct Parser {
1374    toks: Vec<Spanned>,
1375    idx: usize,
1376}
1377
1378impl Parser {
1379    fn new(toks: Vec<Spanned>) -> Self {
1380        Parser { toks, idx: 0 }
1381    }
1382
1383    // ---- cursor helpers ----------------------------------------------------
1384
1385    fn cur(&self) -> &Token {
1386        &self.toks[self.idx].tok
1387    }
1388    fn cur_span(&self) -> Span {
1389        self.toks[self.idx].span()
1390    }
1391    fn peek(&self, n: usize) -> &Token {
1392        self.toks
1393            .get(self.idx + n)
1394            .map(|s| &s.tok)
1395            .unwrap_or(&Token::Eof)
1396    }
1397    fn at(&self, t: &Token) -> bool {
1398        self.cur() == t
1399    }
1400    fn bump(&mut self) -> Token {
1401        let t = self.toks[self.idx].tok.clone();
1402        if self.idx + 1 < self.toks.len() {
1403            self.idx += 1;
1404        }
1405        t
1406    }
1407    fn eat(&mut self, t: &Token) -> bool {
1408        if self.at(t) {
1409            self.bump();
1410            true
1411        } else {
1412            false
1413        }
1414    }
1415    fn expect(&mut self, t: &Token) -> PResult<()> {
1416        if self.eat(t) {
1417            Ok(())
1418        } else {
1419            self.expected_here(token_text(t))
1420        }
1421    }
1422    fn err<T>(&self, msg: impl Into<String>) -> PResult<T> {
1423        let s = &self.toks[self.idx];
1424        Err(ParseError::new(msg, s.span()))
1425    }
1426    fn expected_here<T>(&self, expected: impl Into<String>) -> PResult<T> {
1427        let s = &self.toks[self.idx];
1428        Err(ParseError::expected(expected, token_text(&s.tok), s.span()))
1429    }
1430    fn expected_object<T>(&self) -> PResult<T> {
1431        let s = &self.toks[self.idx];
1432        let mut e = ParseError::expected("an object", token_text(&s.tok), s.span());
1433        if let Token::Name(name) | Token::Label(name) = &s.tok
1434            && let Some(hint) = suggest_object(name)
1435        {
1436            e.detail.hint = Some(format!("did you mean `{hint}`?"));
1437        }
1438        Err(e)
1439    }
1440    fn at_kw(&self, k: Kw) -> bool {
1441        matches!(self.cur(), Token::Kw(x) if *x == k)
1442    }
1443    fn eat_kw(&mut self, k: Kw) -> bool {
1444        if self.at_kw(k) {
1445            self.bump();
1446            true
1447        } else {
1448            false
1449        }
1450    }
1451    fn skip_newlines(&mut self) {
1452        while self.at(&Token::Newline) {
1453            self.bump();
1454        }
1455    }
1456
1457    // ---- top level ---------------------------------------------------------
1458
1459    fn parse_picture(&mut self) -> PResult<Picture> {
1460        // `.PS`/`.PE` are treated as markers that may appear anywhere; statements
1461        // (including `animate`) are collected across them up to EOF. The first
1462        // `.PS` may carry optional width/height.
1463        let (mut width, mut height) = (None, None);
1464        let mut seen_ps = false;
1465        let mut stmts = Vec::new();
1466        loop {
1467            self.skip_newlines();
1468            match self.cur() {
1469                Token::Eof => break,
1470                Token::DotPS => {
1471                    self.bump();
1472                    if !seen_ps && self.starts_scalar() {
1473                        width = Some(self.parse_expr()?);
1474                        if self.starts_scalar() {
1475                            height = Some(self.parse_expr()?);
1476                        }
1477                    }
1478                    seen_ps = true;
1479                    while !self.at(&Token::Newline) && !self.at(&Token::Eof) {
1480                        self.bump();
1481                    }
1482                    continue;
1483                }
1484                Token::DotPE => {
1485                    self.bump();
1486                    continue;
1487                }
1488                _ => {}
1489            }
1490            stmts.push(self.parse_element()?);
1491            if !self.at(&Token::Newline)
1492                && !self.at(&Token::Eof)
1493                && !self.at(&Token::DotPE)
1494                && !self.at(&Token::DotPS)
1495            {
1496                return self.err(format!("unexpected {:?} after statement", self.cur()));
1497            }
1498        }
1499        Ok(Picture {
1500            width,
1501            height,
1502            stmts,
1503            macros: HashMap::new(),
1504            includes: IncludeCtx::default(),
1505        })
1506    }
1507
1508    /// Parse elements until one of `terminators` (or EOF) is the current token.
1509    fn parse_elementlist(&mut self, terminators: &[Token]) -> PResult<Vec<Stmt>> {
1510        let mut stmts = Vec::new();
1511        loop {
1512            self.skip_newlines();
1513            if self.at(&Token::Eof) || terminators.iter().any(|t| self.at(t)) {
1514                break;
1515            }
1516            let s = self.parse_element()?;
1517            stmts.push(s);
1518            // a statement must end at a newline, a terminator, or EOF
1519            if !self.at(&Token::Newline)
1520                && !self.at(&Token::Eof)
1521                && !terminators.iter().any(|t| self.at(t))
1522            {
1523                return self.err(format!("unexpected {:?} after statement", self.cur()));
1524            }
1525        }
1526        Ok(stmts)
1527    }
1528
1529    // ---- statements --------------------------------------------------------
1530
1531    fn parse_element(&mut self) -> PResult<Stmt> {
1532        // A `%`-led line is a comment convention in some source documents (pic
1533        // proper uses `#`). `%` is never valid at statement start, so skip the
1534        // line as a no-op.
1535        if self.at(&Token::Percent) {
1536            while !self.at(&Token::Newline) && !self.at(&Token::Eof) {
1537                self.bump();
1538            }
1539            return Ok(Stmt::Print(PrintItem::Str(StringExpr::Lit(String::new()))));
1540        }
1541
1542        // rpic animation directive.
1543        if self.at_kw(Kw::Animate) {
1544            return Ok(Stmt::Animate(self.parse_animate()?));
1545        }
1546
1547        // rpic `class <place> "name"` statement (extension). Contextual:
1548        // `class = 2` stays an assignment and `class` remains usable as a
1549        // variable, mirroring how `animate` targets are referenced.
1550        if matches!(self.cur(), Token::Name(n) if n == "class")
1551            && !is_assign_op(self.peek(1))
1552            && !matches!(self.peek(1), Token::LeftBrack)
1553        {
1554            self.bump();
1555            let target = self.parse_place()?;
1556            let class = self.parse_stringexpr()?;
1557            return Ok(Stmt::Class { target, class });
1558        }
1559
1560        // rpic `canvas from <pos> to <pos>` statement (extension). Contextual
1561        // and stricter than `class`: only the exact `canvas from …` spelling
1562        // triggers, so `canvas = 2`, a `canvas(…)` macro and a plain variable
1563        // named canvas all keep their classic meaning.
1564        if matches!(self.cur(), Token::Name(n) if n == "canvas")
1565            && matches!(self.peek(1), Token::Kw(Kw::From))
1566        {
1567            self.bump();
1568            self.bump();
1569            let from = self.parse_position()?;
1570            self.expect(&Token::Kw(Kw::To))?;
1571            let to = self.parse_position()?;
1572            return Ok(Stmt::Canvas { from, to });
1573        }
1574
1575        // control constructs
1576        match self.cur() {
1577            Token::Kw(Kw::If) => return self.parse_if(),
1578            Token::Kw(Kw::For) => return self.parse_for(),
1579            Token::Kw(Kw::Print) => return self.parse_print(),
1580            Token::Kw(Kw::Exec) => return self.parse_exec(),
1581            Token::Kw(Kw::Reset) => return self.parse_reset(),
1582            _ => {}
1583        }
1584
1585        // `define`/`undef` are handled by the macro preprocessor before parsing;
1586        // reaching here means a non-brace form we don't support.
1587        if let Token::Kw(k) = self.cur() {
1588            match k {
1589                Kw::Define | Kw::Undef => {
1590                    return self.err("only the `define name { body }` macro form is supported");
1591                }
1592                // Policy (docs/raw-backend-policy in dpic-compat-audit.md):
1593                // `command` raw backend text is never injected into the SVG
1594                // output, and `sh` is never executed — both are tolerated as
1595                // true no-ops so dpic sources keep compiling. The lexer already
1596                // skipped their raw argument text.
1597                Kw::Command | Kw::Sh => {
1598                    self.bump();
1599                    return Ok(Stmt::Group(Vec::new()));
1600                }
1601                Kw::Copy => {
1602                    return self.err("`copy` is not supported yet (planned milestone)");
1603                }
1604                _ => {}
1605            }
1606        }
1607
1608        // `{ … }` grouping
1609        if self.eat(&Token::LeftBrace) {
1610            let stmts = self.parse_elementlist(&[Token::RightBrace])?;
1611            self.expect(&Token::RightBrace)?;
1612            return Ok(Stmt::Group(stmts));
1613        }
1614
1615        // Labelled element: `Label [suffix] : (object | position)`
1616        if matches!(self.cur(), Token::Label(_)) && self.label_colon_ahead() {
1617            let label = self.parse_label()?;
1618            self.expect(&Token::Colon)?;
1619            if self.at_object_start() {
1620                let object = self.parse_object()?;
1621                return Ok(Stmt::Object {
1622                    label: Some(label),
1623                    object,
1624                });
1625            } else {
1626                let pos = self.parse_label_position()?;
1627                return Ok(Stmt::Place { label, pos });
1628            }
1629        }
1630
1631        // Assignment: `name [suffix] op …` or `envvar op …`
1632        if self.at_assignment_start() {
1633            return Ok(Stmt::Assign(self.parse_assignlist()?));
1634        }
1635
1636        // Bare direction change.
1637        if let Token::Dir(d) = self.cur() {
1638            let d = *d;
1639            // Only a standalone direction (next token ends the statement).
1640            if matches!(self.peek(1), Token::Newline | Token::Eof) {
1641                self.bump();
1642                return Ok(Stmt::Direction(d));
1643            }
1644        }
1645
1646        // Otherwise: an unlabelled object.
1647        let object = self.parse_object()?;
1648        Ok(Stmt::Object {
1649            label: None,
1650            object,
1651        })
1652    }
1653
1654    fn parse_if(&mut self) -> PResult<Stmt> {
1655        self.expect_kw(Kw::If)?;
1656        let cond = self.parse_expr()?;
1657        self.expect_kw(Kw::Then)?;
1658        let then_body = self.capture_braced()?;
1659        // optional `else { … }`, possibly across newlines (which otherwise end
1660        // the statement)
1661        let save = self.idx;
1662        self.skip_newlines();
1663        let else_body = if self.eat_kw(Kw::Else) {
1664            Some(self.capture_braced()?)
1665        } else {
1666            self.idx = save;
1667            None
1668        };
1669        Ok(Stmt::If {
1670            cond,
1671            then_body,
1672            else_body,
1673        })
1674    }
1675
1676    fn parse_for(&mut self) -> PResult<Stmt> {
1677        self.expect_kw(Kw::For)?;
1678        let var = match self.bump() {
1679            Token::Name(s) | Token::Label(s) => s,
1680            other => return self.err(format!("expected loop variable, found {other:?}")),
1681        };
1682        let subscript = if self.eat(&Token::LeftBrack) {
1683            let e = self.parse_subscript()?;
1684            self.expect(&Token::RightBrack)?;
1685            Some(e)
1686        } else {
1687            None
1688        };
1689        match self.bump() {
1690            Token::Eq | Token::ColonEq => {}
1691            other => return self.err(format!("expected `=` in for, found {other:?}")),
1692        }
1693        let from = self.parse_expr()?;
1694        self.expect_kw(Kw::To)?;
1695        let to = self.parse_expr()?;
1696        let mut by = Expr::Num(1.0);
1697        let mut mult = false;
1698        if self.eat_kw(Kw::By) {
1699            mult = self.eat(&Token::Mult);
1700            by = self.parse_expr()?;
1701        }
1702        self.expect_kw(Kw::Do)?;
1703        let body = self.capture_braced()?;
1704        Ok(Stmt::For {
1705            var,
1706            subscript,
1707            from,
1708            to,
1709            by,
1710            mult,
1711            body,
1712        })
1713    }
1714
1715    /// Capture a brace-delimited block as raw tokens (excluding the braces),
1716    /// for deferred parsing by the evaluator. Assumes the body follows.
1717    fn capture_braced(&mut self) -> PResult<Body> {
1718        self.skip_newlines();
1719        self.expect(&Token::LeftBrace)?;
1720        let start = self.idx;
1721        let mut depth = 1i32;
1722        loop {
1723            match self.cur() {
1724                Token::LeftBrace => depth += 1,
1725                Token::RightBrace => {
1726                    depth -= 1;
1727                    if depth == 0 {
1728                        break;
1729                    }
1730                }
1731                Token::Eof => return self.err("unterminated `{` body"),
1732                _ => {}
1733            }
1734            self.bump();
1735        }
1736        let body = self.toks[start..self.idx].to_vec();
1737        self.expect(&Token::RightBrace)?;
1738        Ok(body)
1739    }
1740
1741    fn parse_print(&mut self) -> PResult<Stmt> {
1742        self.expect_kw(Kw::Print)?;
1743        let item = if self.at_string_start() {
1744            PrintItem::Str(self.parse_stringexpr()?)
1745        } else {
1746            PrintItem::Expr(self.parse_expr()?)
1747        };
1748        Ok(Stmt::Print(item))
1749    }
1750
1751    fn parse_exec(&mut self) -> PResult<Stmt> {
1752        let arg_frame = self.toks[self.idx].arg_frame.clone();
1753        self.expect_kw(Kw::Exec)?;
1754        let command = self.parse_stringexpr()?;
1755        Ok(Stmt::Exec { command, arg_frame })
1756    }
1757
1758    fn parse_reset(&mut self) -> PResult<Stmt> {
1759        self.expect_kw(Kw::Reset)?;
1760        let mut list = Vec::new();
1761        if let Token::EnvVar(v) = self.cur() {
1762            list.push(*v);
1763            self.bump();
1764            while self.eat(&Token::Comma) {
1765                match self.cur() {
1766                    Token::EnvVar(v) => {
1767                        list.push(*v);
1768                        self.bump();
1769                    }
1770                    other => {
1771                        return self.err(format!("expected environment variable, found {other:?}"));
1772                    }
1773                }
1774            }
1775        }
1776        Ok(Stmt::Reset(list))
1777    }
1778
1779    fn at_string_start(&self) -> bool {
1780        self.token_starts_string_at(0)
1781    }
1782
1783    fn parse_animate(&mut self) -> PResult<Animate> {
1784        self.expect_kw(Kw::Animate)?;
1785        let target = self.parse_place()?;
1786        self.expect_kw(Kw::With)?;
1787        let effect_span = Some(self.cur_span());
1788        let effect = self.parse_stringexpr()?;
1789        let mut duration = None;
1790        let mut timing = Timing::Sequential;
1791        let mut delay = None;
1792        loop {
1793            if self.eat_kw(Kw::For) {
1794                duration = Some(self.parse_expr()?);
1795            } else if self.eat_kw(Kw::At) {
1796                timing = Timing::At(self.parse_expr()?);
1797            } else if self.eat_kw(Kw::After) {
1798                timing = Timing::After(self.parse_place()?);
1799            } else if self.eat_kw(Kw::Delay) {
1800                delay = Some(self.parse_expr()?);
1801            } else {
1802                break;
1803            }
1804        }
1805        Ok(Animate {
1806            target,
1807            effect,
1808            effect_span,
1809            duration,
1810            timing,
1811            delay,
1812        })
1813    }
1814
1815    /// True if the current `Label` is followed by `:` (allowing a `[suffix]`).
1816    fn label_colon_ahead(&self) -> bool {
1817        match self.peek(1) {
1818            Token::Colon => true,
1819            Token::LeftBrack => {
1820                // scan past a balanced [ … ] suffix to find ':'
1821                let mut depth = 0;
1822                let mut i = self.idx + 1;
1823                while i < self.toks.len() {
1824                    match &self.toks[i].tok {
1825                        Token::LeftBrack => depth += 1,
1826                        Token::RightBrack => {
1827                            depth -= 1;
1828                            if depth == 0 {
1829                                return matches!(
1830                                    self.toks.get(i + 1).map(|s| &s.tok),
1831                                    Some(Token::Colon)
1832                                );
1833                            }
1834                        }
1835                        Token::Newline | Token::Eof => return false,
1836                        _ => {}
1837                    }
1838                    i += 1;
1839                }
1840                false
1841            }
1842            _ => false,
1843        }
1844    }
1845
1846    fn at_object_start(&self) -> bool {
1847        matches!(
1848            self.cur(),
1849            Token::Prim(_)
1850                | Token::LeftBrack
1851                | Token::Block
1852                | Token::Str(_)
1853                | Token::Arg(_)
1854                | Token::Kw(Kw::Sprintf)
1855        ) || matches!(self.cur(), Token::Name(n) if n == "brace" || n == "dot")
1856    }
1857
1858    fn at_assignment_start(&self) -> bool {
1859        match self.cur() {
1860            Token::Name(_) | Token::Label(_) => self.assignment_op_after_var_ref(),
1861            Token::EnvVar(_) => is_assign_op(self.peek(1)),
1862            _ => false,
1863        }
1864    }
1865
1866    fn assignment_op_after_var_ref(&self) -> bool {
1867        let mut i = self.idx + 1;
1868        if matches!(self.toks.get(i).map(|s| &s.tok), Some(Token::LeftBrack)) {
1869            i += 1;
1870            let mut depth = 1i32;
1871            while let Some(tok) = self.toks.get(i).map(|s| &s.tok) {
1872                match tok {
1873                    Token::LeftBrack => depth += 1,
1874                    Token::RightBrack => {
1875                        depth -= 1;
1876                        if depth == 0 {
1877                            i += 1;
1878                            break;
1879                        }
1880                    }
1881                    Token::Eof | Token::Newline if depth > 0 => return false,
1882                    _ => {}
1883                }
1884                i += 1;
1885            }
1886            if depth != 0 {
1887                return false;
1888            }
1889        }
1890        self.toks.get(i).is_some_and(|s| is_assign_op(&s.tok))
1891    }
1892
1893    fn parse_label(&mut self) -> PResult<Label> {
1894        let name = match self.bump() {
1895            Token::Label(s) => s,
1896            other => return self.err(format!("expected label, found {other:?}")),
1897        };
1898        let subscript = if self.eat(&Token::LeftBrack) {
1899            let e = self.parse_subscript()?;
1900            self.expect(&Token::RightBrack)?;
1901            Some(e)
1902        } else {
1903            None
1904        };
1905        Ok(Label { name, subscript })
1906    }
1907
1908    fn parse_assignlist(&mut self) -> PResult<Vec<Assignment>> {
1909        let mut list = vec![self.parse_assignment()?];
1910        while self.eat(&Token::Comma) {
1911            list.push(self.parse_assignment()?);
1912        }
1913        Ok(list)
1914    }
1915
1916    fn parse_assignment(&mut self) -> PResult<Assignment> {
1917        let target = match self.cur().clone() {
1918            Token::Name(name) | Token::Label(name) => {
1919                self.bump();
1920                let sub = if self.eat(&Token::LeftBrack) {
1921                    let e = self.parse_subscript()?;
1922                    self.expect(&Token::RightBrack)?;
1923                    Some(e)
1924                } else {
1925                    None
1926                };
1927                AssignTarget::Var(name, sub)
1928            }
1929            Token::EnvVar(v) => {
1930                self.bump();
1931                AssignTarget::Env(v)
1932            }
1933            other => return self.err(format!("expected assignment target, found {other:?}")),
1934        };
1935        let op = match self.bump() {
1936            Token::Eq => AssignOp::Set,
1937            Token::ColonEq => AssignOp::ColonSet,
1938            Token::PlusEq => AssignOp::Add,
1939            Token::MinusEq => AssignOp::Sub,
1940            Token::MultEq => AssignOp::Mul,
1941            Token::DivEq => AssignOp::Div,
1942            Token::RemEq => AssignOp::Rem,
1943            other => return self.err(format!("expected assignment operator, found {other:?}")),
1944        };
1945        let value = self.parse_expr()?;
1946        Ok(Assignment { target, op, value })
1947    }
1948
1949    fn parse_subscript(&mut self) -> PResult<Expr> {
1950        let mut items = vec![self.parse_expr()?];
1951        while self.eat(&Token::Comma) {
1952            items.push(self.parse_expr()?);
1953        }
1954        if items.len() == 1 {
1955            Ok(items.pop().unwrap())
1956        } else {
1957            Ok(Expr::Index(items))
1958        }
1959    }
1960
1961    // ---- objects & attributes ---------------------------------------------
1962
1963    fn parse_object(&mut self) -> PResult<Object> {
1964        let span = Some(self.cur_span());
1965        let mut attrs = Vec::new();
1966        // a bare string expression (literal, `$arg`, sprintf, concatenation)
1967        // places a text-only object.
1968        if self.at_string_start() {
1969            attrs.push(Attr::Text(self.parse_stringexpr()?));
1970            while let Some(a) = self.parse_attr(false, false, false, false)? {
1971                attrs.push(a);
1972            }
1973            return Ok(Object {
1974                span,
1975                kind: ObjectKind::Text,
1976                attrs,
1977            });
1978        }
1979        let kind = match self.cur().clone() {
1980            Token::Prim(p) => {
1981                self.bump();
1982                ObjectKind::Primitive(p)
1983            }
1984            Token::Block => {
1985                self.bump();
1986                ObjectKind::Empty
1987            }
1988            Token::Name(n) if n == "brace" => {
1989                self.bump();
1990                ObjectKind::Brace
1991            }
1992            Token::Name(n) if n == "dot" => {
1993                self.bump();
1994                ObjectKind::Dot
1995            }
1996            Token::LeftBrack => {
1997                self.bump();
1998                let stmts = self.parse_elementlist(&[Token::RightBrack])?;
1999                self.expect(&Token::RightBrack)?;
2000                ObjectKind::Block(stmts)
2001            }
2002            Token::Str(s) => {
2003                self.bump();
2004                attrs.push(Attr::Text(self.continue_string(StringExpr::Lit(s))?));
2005                ObjectKind::Text
2006            }
2007            Token::Kw(Kw::Continue) => {
2008                self.bump();
2009                ObjectKind::Continue
2010            }
2011            Token::Name(_) | Token::Label(_) => return self.expected_object(),
2012            _ => return self.expected_here("an object"),
2013        };
2014        // `spline <expr> <linespec>`: dpic's documented exception to the bare
2015        // distance rule — the expression right after `spline` is a tension
2016        // parameter, not a length. Parse it here so the attribute loop below
2017        // doesn't read it as `Attr::Dist`.
2018        if matches!(kind, ObjectKind::Primitive(Prim::Spline)) && self.spline_tension_ahead() {
2019            attrs.push(Attr::SplineTension(self.parse_expr()?));
2020        }
2021        let allow_fit = matches!(
2022            kind,
2023            ObjectKind::Primitive(Prim::Box | Prim::Circle | Prim::Ellipse)
2024        );
2025        let allow_brace = matches!(kind, ObjectKind::Brace);
2026        let allow_dot_fill = matches!(kind, ObjectKind::Dot);
2027        let allow_hatch = matches!(
2028            kind,
2029            ObjectKind::Primitive(
2030                Prim::Box
2031                    | Prim::Circle
2032                    | Prim::Ellipse
2033                    | Prim::Line
2034                    | Prim::Arrow
2035                    | Prim::Spline
2036                    | Prim::Arc
2037            )
2038        );
2039        let allow_close = matches!(kind, ObjectKind::Primitive(Prim::Line));
2040        while let Some(a) = self.parse_attr(
2041            allow_fit,
2042            allow_brace,
2043            allow_hatch || allow_dot_fill,
2044            allow_close,
2045        )? {
2046            attrs.push(a);
2047        }
2048        Ok(Object { kind, attrs, span })
2049    }
2050
2051    /// True if the next token begins a bare scalar expression — the leading
2052    /// tension argument of `spline <expr>` — rather than a linespec keyword
2053    /// (`from`/`to`/`up`/`then`/…) or another attribute.
2054    fn spline_tension_ahead(&self) -> bool {
2055        matches!(
2056            self.cur(),
2057            Token::Float(_)
2058                | Token::Lparen
2059                | Token::EnvVar(_)
2060                | Token::Func1(_)
2061                | Token::Func2(_)
2062                | Token::Name(_)
2063                | Token::Minus
2064                | Token::Plus
2065                | Token::Kw(Kw::Rand)
2066        )
2067    }
2068
2069    fn parse_attr(
2070        &mut self,
2071        allow_fit: bool,
2072        allow_brace: bool,
2073        allow_hatch: bool,
2074        allow_close: bool,
2075    ) -> PResult<Option<Attr>> {
2076        // any string expression (literal, sprintf, $arg, concatenation) is text
2077        if self.at_string_start() {
2078            return Ok(Some(Attr::Text(self.parse_stringexpr()?)));
2079        }
2080        let attr = match self.cur().clone() {
2081            Token::Kw(Kw::Ht) => {
2082                self.bump();
2083                Attr::Dim(DimKind::Ht, self.parse_expr()?)
2084            }
2085            Token::Kw(Kw::Wid) => {
2086                self.bump();
2087                Attr::Dim(DimKind::Wid, self.parse_expr()?)
2088            }
2089            Token::Kw(Kw::Rad) => {
2090                self.bump();
2091                Attr::Dim(DimKind::Rad, self.parse_expr()?)
2092            }
2093            Token::Kw(Kw::Diam) => {
2094                self.bump();
2095                Attr::Dim(DimKind::Diam, self.parse_expr()?)
2096            }
2097            Token::Kw(Kw::Thick) => {
2098                self.bump();
2099                Attr::Dim(DimKind::Thick, self.parse_expr()?)
2100            }
2101            Token::Kw(Kw::Scaled) => {
2102                self.bump();
2103                Attr::Dim(DimKind::Scaled, self.parse_expr()?)
2104            }
2105            Token::Dir(d) => {
2106                self.bump();
2107                Attr::Direction(
2108                    d,
2109                    self.opt_attr_expr(allow_fit, allow_brace, allow_hatch, allow_close)?,
2110                )
2111            }
2112            Token::LineType(lt) => {
2113                self.bump();
2114                Attr::LineStyle(
2115                    lt,
2116                    self.opt_attr_expr(allow_fit, allow_brace, allow_hatch, allow_close)?,
2117                )
2118            }
2119            Token::Kw(Kw::Chop) => {
2120                self.bump();
2121                Attr::Chop(self.opt_attr_expr(allow_fit, allow_brace, allow_hatch, allow_close)?)
2122            }
2123            Token::Kw(Kw::Fill) => {
2124                self.bump();
2125                Attr::Fill(self.opt_attr_expr(allow_fit, allow_brace, allow_hatch, allow_close)?)
2126            }
2127            Token::Arrow(a) => {
2128                self.bump();
2129                Attr::Arrowhead(
2130                    a,
2131                    self.opt_attr_expr(allow_fit, allow_brace, allow_hatch, allow_close)?,
2132                )
2133            }
2134            Token::Kw(Kw::Then) => {
2135                self.bump();
2136                Attr::Then
2137            }
2138            Token::Kw(Kw::Cw) => {
2139                self.bump();
2140                Attr::Cw
2141            }
2142            Token::Kw(Kw::Ccw) => {
2143                self.bump();
2144                Attr::Ccw
2145            }
2146            Token::Kw(Kw::Same) => {
2147                self.bump();
2148                Attr::Same
2149            }
2150            Token::Kw(Kw::Continue) => {
2151                self.bump();
2152                Attr::Continue
2153            }
2154            Token::Kw(Kw::From) => {
2155                self.bump();
2156                Attr::From(self.parse_position()?)
2157            }
2158            Token::Kw(Kw::To) => {
2159                self.bump();
2160                Attr::To(self.parse_position()?)
2161            }
2162            Token::Kw(Kw::At) => {
2163                self.bump();
2164                Attr::At(self.parse_position()?)
2165            }
2166            Token::Kw(Kw::By) => {
2167                self.bump();
2168                Attr::By(self.parse_position()?)
2169            }
2170            Token::Kw(Kw::With) => {
2171                self.bump();
2172                let anchor = if self.eat(&Token::Dot) {
2173                    WithAnchor::Place(self.parse_place()?)
2174                } else if let Token::Corner(c) = self.cur() {
2175                    let c = *c;
2176                    self.bump();
2177                    WithAnchor::Corner(c)
2178                } else if self.at(&Token::Lparen) {
2179                    self.bump();
2180                    let x = self.parse_expr()?;
2181                    self.expect(&Token::Comma)?;
2182                    let y = self.parse_expr()?;
2183                    self.expect(&Token::Rparen)?;
2184                    WithAnchor::Pair(x, y)
2185                } else {
2186                    WithAnchor::Plain
2187                };
2188                self.expect_kw(Kw::At)?;
2189                Attr::With {
2190                    anchor,
2191                    at: self.parse_position()?,
2192                }
2193            }
2194            Token::TextPos(tp) => {
2195                self.bump();
2196                Attr::TextPos(tp)
2197            }
2198            Token::Color(c) => {
2199                self.bump();
2200                // a colour may be a quoted string, a bareword name (`shaded
2201                // Custom`, `outlined red`), `rgb(r,g,b)` or a 0x hex literal.
2202                Attr::Color(c, self.parse_color_like()?)
2203            }
2204            Token::Name(n) if allow_fit && n == "fit" => {
2205                self.bump();
2206                Attr::Fit
2207            }
2208            Token::Name(n) if allow_hatch && n == "hatch" => {
2209                self.bump();
2210                Attr::Hatch(HatchKind::Single)
2211            }
2212            Token::Name(n) if allow_hatch && n == "crosshatch" => {
2213                self.bump();
2214                Attr::Hatch(HatchKind::Cross)
2215            }
2216            Token::Name(n) if allow_hatch && n == "hatchangle" => {
2217                self.bump();
2218                Attr::HatchAngle(self.parse_expr()?)
2219            }
2220            Token::Name(n) if allow_hatch && n == "hatchsep" => {
2221                self.bump();
2222                Attr::HatchSep(self.parse_expr()?)
2223            }
2224            Token::Name(n) if allow_hatch && (n == "hatchwid" || n == "hatchwidth") => {
2225                self.bump();
2226                Attr::HatchWidth(self.parse_expr()?)
2227            }
2228            Token::Name(n) if allow_hatch && n == "hatchcolor" => {
2229                self.bump();
2230                Attr::HatchColor(self.parse_color_like()?)
2231            }
2232            Token::Name(n) if allow_hatch && n == "gradient" => {
2233                self.bump();
2234                let from = self.parse_color_like()?;
2235                let to = self.parse_color_like()?;
2236                Attr::Gradient(from, to)
2237            }
2238            Token::Name(n) if allow_hatch && n == "gradientangle" => {
2239                self.bump();
2240                Attr::GradientAngle(self.parse_expr()?)
2241            }
2242            Token::Name(n) if n == "opacity" => {
2243                self.bump();
2244                Attr::Opacity(self.parse_expr()?)
2245            }
2246            Token::Name(n) if allow_close && n == "close" => {
2247                self.bump();
2248                Attr::Close
2249            }
2250            Token::Name(n) if allow_brace && n == "bracepos" => {
2251                self.bump();
2252                Attr::BracePos(self.parse_expr()?)
2253            }
2254            Token::Name(n) if allow_brace && n == "labeloffset" => {
2255                self.bump();
2256                Attr::BraceLabelOffset(self.parse_expr()?)
2257            }
2258            Token::Name(n) if n == "bold" => {
2259                self.bump();
2260                Attr::Bold
2261            }
2262            Token::Name(n) if n == "italic" => {
2263                self.bump();
2264                Attr::Italic
2265            }
2266            Token::Name(n) if n == "mono" => {
2267                self.bump();
2268                Attr::Mono
2269            }
2270            Token::Name(n) if n == "font" => {
2271                self.bump();
2272                // a family may be a quoted string or a bareword name, like
2273                // colours (`font "IBM Plex Mono"`, `font serif`)
2274                let s = match self.cur().clone() {
2275                    Token::Name(n) | Token::Label(n) => {
2276                        self.bump();
2277                        StringExpr::Lit(n)
2278                    }
2279                    _ => self.parse_stringexpr()?,
2280                };
2281                Attr::Font(s)
2282            }
2283            Token::Name(n) if n == "fontsize" => {
2284                self.bump();
2285                Attr::FontSize(self.parse_expr()?)
2286            }
2287            Token::Name(n) if n == "rotated" => {
2288                self.bump();
2289                Attr::Rotated(self.parse_expr()?)
2290            }
2291            Token::Name(n) if n == "behind" => {
2292                self.bump();
2293                Attr::Behind(self.parse_place()?)
2294            }
2295            Token::Name(n) if n == "class" => {
2296                self.bump();
2297                Attr::Class(self.parse_stringexpr()?)
2298            }
2299            // a bare expression distance with no direction word, e.g. `move 1`,
2300            // `move -0.1`, `spline x` (length in the prevailing direction)
2301            Token::Float(_)
2302            | Token::Lparen
2303            | Token::EnvVar(_)
2304            | Token::Func1(_)
2305            | Token::Func2(_)
2306            | Token::Name(_)
2307            | Token::Minus
2308            | Token::Plus
2309            | Token::Kw(Kw::Rand) => {
2310                let span = self.cur_span();
2311                Attr::Dist(self.parse_expr()?, Some(span))
2312            }
2313            _ if self.place_is_scalar_ahead() => {
2314                let span = self.cur_span();
2315                Attr::Dist(self.parse_expr()?, Some(span))
2316            }
2317            _ => return Ok(None),
2318        };
2319        Ok(Some(attr))
2320    }
2321
2322    fn expect_kw(&mut self, k: Kw) -> PResult<()> {
2323        if self.eat_kw(k) {
2324            Ok(())
2325        } else {
2326            self.expected_here(kw_text(k))
2327        }
2328    }
2329
2330    // ---- string expressions ------------------------------------------------
2331
2332    fn parse_stringexpr(&mut self) -> PResult<StringExpr> {
2333        let first = self.parse_string_atom()?;
2334        self.continue_string(first)
2335    }
2336
2337    /// Continue a string expression with trailing `+ string` parts.
2338    fn continue_string(&mut self, first: StringExpr) -> PResult<StringExpr> {
2339        let mut e = first;
2340        while self.at(&Token::Plus) && self.string_after_plus() {
2341            self.bump();
2342            let rhs = self.parse_string_atom()?;
2343            e = StringExpr::Concat(Box::new(e), Box::new(rhs));
2344        }
2345        Ok(e)
2346    }
2347
2348    fn string_after_plus(&self) -> bool {
2349        self.token_starts_string_at(1)
2350    }
2351
2352    fn token_starts_string_at(&self, offset: usize) -> bool {
2353        match self.toks.get(self.idx + offset).map(|s| &s.tok) {
2354            Some(Token::Str(_) | Token::Arg(_) | Token::Kw(Kw::Sprintf)) => true,
2355            Some(Token::Name(n)) if n == "svg_font" => {
2356                matches!(
2357                    self.toks.get(self.idx + offset + 1).map(|s| &s.tok),
2358                    Some(Token::Lparen)
2359                )
2360            }
2361            _ => false,
2362        }
2363    }
2364
2365    fn parse_string_atom(&mut self) -> PResult<StringExpr> {
2366        match self.cur().clone() {
2367            Token::Str(s) => {
2368                self.bump();
2369                Ok(StringExpr::Lit(s))
2370            }
2371            Token::Arg(n) => {
2372                self.bump();
2373                Ok(StringExpr::Arg(n))
2374            }
2375            Token::Kw(Kw::Sprintf) => {
2376                self.bump();
2377                self.expect(&Token::Lparen)?;
2378                let fmt = self.parse_stringexpr()?;
2379                let mut args = Vec::new();
2380                while self.eat(&Token::Comma) {
2381                    args.push(self.parse_expr()?);
2382                }
2383                self.expect(&Token::Rparen)?;
2384                Ok(StringExpr::Sprintf(Box::new(fmt), args))
2385            }
2386            Token::Name(n) if n == "svg_font" && matches!(self.peek(1), Token::Lparen) => {
2387                self.bump();
2388                self.expect(&Token::Lparen)?;
2389                let mut args = Vec::new();
2390                if !self.at(&Token::Rparen) {
2391                    args.push(self.parse_expr()?);
2392                    while self.eat(&Token::Comma) {
2393                        args.push(self.parse_expr()?);
2394                    }
2395                }
2396                self.expect(&Token::Rparen)?;
2397                Ok(StringExpr::SvgFont(args))
2398            }
2399            other => self.err(format!("expected a string, found {other:?}")),
2400        }
2401    }
2402
2403    // ---- positions ---------------------------------------------------------
2404
2405    fn parse_label_position(&mut self) -> PResult<Position> {
2406        let save = self.idx;
2407        if let Ok(x) = self.parse_expr()
2408            && self.eat(&Token::Comma)
2409        {
2410            let y = self.parse_expr()?;
2411            return Ok(Position::Pair(x, y));
2412        }
2413        self.idx = save;
2414        self.parse_position()
2415    }
2416
2417    /// Positions support vector arithmetic; `+`/`-` are the lowest precedence.
2418    fn parse_position(&mut self) -> PResult<Position> {
2419        let mut left = self.parse_pos_mul()?;
2420        loop {
2421            let sign = if self.at(&Token::Plus) {
2422                Sign::Plus
2423            } else if self.at(&Token::Minus) {
2424                Sign::Minus
2425            } else {
2426                break;
2427            };
2428            self.bump();
2429            let right = self.parse_pos_mul()?;
2430            left = Position::Sum(sign, Box::new(left), Box::new(right));
2431        }
2432        Ok(left)
2433    }
2434
2435    /// Scaling a position by a scalar: `p * s`, `p / s` (binds tighter than ±).
2436    fn parse_pos_mul(&mut self) -> PResult<Position> {
2437        let mut left = self.parse_pos_primary()?;
2438        loop {
2439            if self.eat(&Token::Mult) {
2440                left = Position::Scale(Box::new(left), self.parse_unary()?, false);
2441            } else if self.eat(&Token::Div) {
2442                left = Position::Scale(Box::new(left), self.parse_unary()?, true);
2443            } else {
2444                break;
2445            }
2446        }
2447        Ok(left)
2448    }
2449
2450    fn parse_pos_primary(&mut self) -> PResult<Position> {
2451        // A fraction-led interpolation — `frac between A and B`, `frac <p,p>`, or
2452        // `frac of the way between A and B`. The fraction can be parenthesised
2453        // (e.g. `(X/Y) between A and B`), so try it before the `(`/place branches.
2454        if let Some(p) = self.try_fraction()? {
2455            return Ok(p);
2456        }
2457        // `( … )`: coordinate pair `(x,y)` of scalar expressions, a
2458        // parenthesised position, or `(pos, pos)` — x of the first, y of the
2459        // second. Try a scalar pair first so components that are themselves
2460        // parenthesised scalars (e.g. `((a*g)*cos(t), …)`) parse correctly.
2461        if self.eat(&Token::Lparen) {
2462            let save = self.idx;
2463            // Prefer parsing the contents as position(s) — handles `(A, B.c)`,
2464            // `(pos, pos)`, `(2,3)`, `(0.5 between A and B)`. If that fails, the
2465            // contents are scalar coordinate expressions that the position
2466            // grammar can't represent alone (e.g. `((a*g)*cos t, (a*g)*sin t)`).
2467            if let Ok(p1) = self.parse_position() {
2468                let p = if self.eat(&Token::Comma) {
2469                    let p2 = self.parse_position()?;
2470                    Position::Place(Location::ParenPair(Box::new(p1), Box::new(p2)))
2471                } else {
2472                    p1 // drop the redundant parentheses
2473                };
2474                self.expect(&Token::Rparen)?;
2475                return Ok(p);
2476            }
2477            self.idx = save;
2478            let e1 = self.parse_add()?;
2479            self.expect(&Token::Comma)?;
2480            let e2 = self.parse_add()?;
2481            self.expect(&Token::Rparen)?;
2482            return Ok(Position::Pair(e1, e2));
2483        }
2484        // A leading place is point-valued UNLESS it is a scalar accessor
2485        // (`place.x` / `.y` / `.attr`), which begins an `(expr, expr)` pair.
2486        if self.at_place_start() && !self.place_is_scalar_ahead() {
2487            return Ok(Position::Place(Location::Place(self.parse_place()?)));
2488        }
2489        // expression-led coordinate pair `x, y` (interpolation handled above)
2490        let e1 = self.parse_add()?;
2491        if self.eat(&Token::Comma) {
2492            let e2 = self.parse_add()?;
2493            return Ok(Position::Pair(e1, e2));
2494        }
2495        self.err("expected `,`, `between`, or `of the way between` in position")
2496    }
2497
2498    /// Try to parse `frac (between | <p,p> | of the way between)`; if the leading
2499    /// expression isn't followed by an interpolation, backtrack and return `None`
2500    /// so the caller can parse a plain place / pair / parenthesised position.
2501    fn try_fraction(&mut self) -> PResult<Option<Position>> {
2502        let save = self.idx;
2503        let Ok(frac) = self.parse_add() else {
2504            self.idx = save;
2505            return Ok(None);
2506        };
2507        let mk = |frac, a, b, of_the_way| {
2508            Ok(Some(Position::Between {
2509                frac: Box::new(frac),
2510                a: Box::new(a),
2511                b: Box::new(b),
2512                of_the_way,
2513            }))
2514        };
2515        if self.eat(&Token::Lt) {
2516            let a = self.parse_position()?;
2517            self.expect(&Token::Comma)?;
2518            let b = self.parse_position()?;
2519            self.expect(&Token::Gt)?;
2520            return mk(frac, a, b, false);
2521        }
2522        let of_the_way = if self.at_kw(Kw::Of) {
2523            self.eat_kw(Kw::Of);
2524            if !(self.eat_kw(Kw::The) && self.eat_kw(Kw::Way) && self.eat_kw(Kw::Between)) {
2525                self.idx = save;
2526                return Ok(None);
2527            }
2528            true
2529        } else if self.eat_kw(Kw::Between) {
2530            false
2531        } else {
2532            self.idx = save;
2533            return Ok(None);
2534        };
2535        let a = self.parse_position()?;
2536        self.expect_kw(Kw::And)?;
2537        let b = self.parse_position()?;
2538        mk(frac, a, b, of_the_way)
2539    }
2540
2541    /// Lookahead: does the upcoming place end in a scalar accessor
2542    /// (`.x` / `.y` / `.attr`)? If so it is a number, not a point. Non-consuming.
2543    fn place_is_scalar_ahead(&mut self) -> bool {
2544        let save = self.idx;
2545        let parsed = self.parse_place().is_ok();
2546        let scalar = parsed && matches!(self.cur(), Token::DotX | Token::DotY | Token::Param(_));
2547        self.idx = save;
2548        scalar
2549    }
2550
2551    fn at_place_start(&self) -> bool {
2552        match self.cur() {
2553            Token::Label(_) | Token::Block | Token::Corner(_) => true,
2554            Token::Kw(Kw::Last) | Token::Kw(Kw::Here) => true,
2555            Token::Float(_) => matches!(self.peek(1), Token::Kw(Kw::Nth)),
2556            // `{expr}th …` / `` `expr`th … `` ordinal counts (only valid as a
2557            // place in position/expression context, never a group here)
2558            Token::LeftBrace | Token::LeftQuote => true,
2559            _ => false,
2560        }
2561    }
2562
2563    fn parse_place(&mut self) -> PResult<Place> {
2564        // `corner [of] placename`
2565        if let Token::Corner(c) = self.cur() {
2566            let c = *c;
2567            self.bump();
2568            self.eat_kw(Kw::Of);
2569            let inner = self.parse_place()?;
2570            return Ok(Place::CornerOf(c, Box::new(inner)));
2571        }
2572
2573        let mut place = self.parse_place_base()?;
2574
2575        // trailing `.corner`, `.label`, `.nth primobj`
2576        loop {
2577            if let Token::Corner(c) = self.cur() {
2578                let c = *c;
2579                self.bump();
2580                place = Place::Corner(Box::new(place), c);
2581            } else if self.at(&Token::Dot) {
2582                self.bump();
2583                let rhs = self.parse_place_base()?;
2584                place = Place::Member(Box::new(place), Box::new(rhs));
2585            } else {
2586                break;
2587            }
2588        }
2589        Ok(place)
2590    }
2591
2592    fn parse_place_base(&mut self) -> PResult<Place> {
2593        match self.cur().clone() {
2594            Token::Kw(Kw::Here) => {
2595                self.bump();
2596                Ok(Place::Here)
2597            }
2598            Token::Label(name) => {
2599                let span = Some(self.cur_span());
2600                self.bump();
2601                let subscript = if self.eat(&Token::LeftBrack) {
2602                    let e = self.parse_subscript()?;
2603                    self.expect(&Token::RightBrack)?;
2604                    Some(Box::new(e))
2605                } else {
2606                    None
2607                };
2608                Ok(Place::Name {
2609                    name,
2610                    subscript,
2611                    span,
2612                })
2613            }
2614            Token::Kw(Kw::Last) | Token::Float(_) | Token::LeftBrace | Token::LeftQuote => {
2615                let span = Some(self.cur_span());
2616                let count = self.parse_nth()?;
2617                // A type keyword may follow (`last box`); without one, this is an
2618                // untyped reference to the most recent object of any kind
2619                // (`last`, `last.c`, `2nd last.n`).
2620                let obj = if self.at_primobj() {
2621                    self.parse_primobj()?
2622                } else {
2623                    PrimObj::Any
2624                };
2625                Ok(Place::Nth { count, obj, span })
2626            }
2627            other => self.err(format!("expected a place, found {other:?}")),
2628        }
2629    }
2630
2631    fn parse_nth(&mut self) -> PResult<Nth> {
2632        if self.eat_kw(Kw::Last) {
2633            return Ok(Nth::Last);
2634        }
2635        // ncount ordinal [last]
2636        let e = self.parse_ncount()?;
2637        self.expect_kw(Kw::Nth)?;
2638        let from_last = self.eat_kw(Kw::Last);
2639        Ok(Nth::Count(Box::new(e), from_last))
2640    }
2641
2642    /// An ordinal count: a number, `` `expr' ``, or `{ expr }` (grammar:
2643    /// `ncount`). Must NOT recurse into the general expression grammar on a
2644    /// bare number, or `2nd` would re-enter place parsing.
2645    fn parse_ncount(&mut self) -> PResult<Expr> {
2646        match self.cur().clone() {
2647            Token::Float(v) => {
2648                self.bump();
2649                Ok(Expr::Num(v))
2650            }
2651            Token::LeftBrace => {
2652                self.bump();
2653                let e = self.parse_expr()?;
2654                self.expect(&Token::RightBrace)?;
2655                Ok(e)
2656            }
2657            Token::LeftQuote => {
2658                self.bump();
2659                let e = self.parse_expr()?;
2660                self.expect(&Token::RightQuote)?;
2661                Ok(e)
2662            }
2663            other => self.err(format!("expected an ordinal count, found {other:?}")),
2664        }
2665    }
2666
2667    /// Whether the current token can begin a primitive-object type keyword
2668    /// (`box`, `[`, a string, …) — i.e. an explicit type after `last`/ordinal.
2669    fn at_primobj(&self) -> bool {
2670        matches!(
2671            self.cur(),
2672            Token::Prim(_) | Token::Block | Token::Str(_) | Token::LeftBrack
2673        ) || matches!(self.cur(), Token::Name(n) if n == "brace")
2674    }
2675
2676    fn parse_primobj(&mut self) -> PResult<PrimObj> {
2677        match self.cur().clone() {
2678            Token::Prim(p) => {
2679                self.bump();
2680                Ok(PrimObj::Prim(p))
2681            }
2682            Token::Name(n) if n == "brace" => {
2683                self.bump();
2684                Ok(PrimObj::Brace)
2685            }
2686            Token::Block => {
2687                self.bump();
2688                Ok(PrimObj::Block)
2689            }
2690            Token::Str(s) => {
2691                self.bump();
2692                Ok(PrimObj::Str(s))
2693            }
2694            Token::LeftBrack => {
2695                self.bump();
2696                self.expect(&Token::RightBrack)?;
2697                Ok(PrimObj::EmptyBrack)
2698            }
2699            other => self.err(format!("expected a primitive object, found {other:?}")),
2700        }
2701    }
2702
2703    // ---- expressions -------------------------------------------------------
2704
2705    fn opt_expr(&mut self) -> PResult<Option<Expr>> {
2706        if self.starts_scalar() || self.place_is_scalar_ahead() {
2707            Ok(Some(self.parse_expr()?))
2708        } else {
2709            Ok(None)
2710        }
2711    }
2712
2713    /// A color argument: a bare name (`red`), a label-cased name, or any
2714    /// string expression — the same grammar `hatchcolor` accepts.
2715    fn parse_color_like(&mut self) -> PResult<StringExpr> {
2716        match self.cur().clone() {
2717            // rpic extension: `rgb(r,g,b)` colour literal
2718            Token::Name(n) if n == "rgb" && matches!(self.peek(1), Token::Lparen) => {
2719                self.bump();
2720                self.expect(&Token::Lparen)?;
2721                let r = self.parse_expr()?;
2722                self.expect(&Token::Comma)?;
2723                let g = self.parse_expr()?;
2724                self.expect(&Token::Comma)?;
2725                let b = self.parse_expr()?;
2726                self.expect(&Token::Rparen)?;
2727                Ok(StringExpr::Rgb(Box::new([r, g, b])))
2728            }
2729            Token::Name(n) | Token::Label(n) => {
2730                self.bump();
2731                Ok(StringExpr::Lit(n))
2732            }
2733            // rpic extension (pikchr-style): a numeric colour — typically a
2734            // `0xRRGGBB` hex literal (`shaded 0x1b5e20`)
2735            Token::Float(_) => Ok(StringExpr::ColorNum(Box::new(self.parse_expr()?))),
2736            _ => self.parse_stringexpr(),
2737        }
2738    }
2739
2740    fn opt_attr_expr(
2741        &mut self,
2742        allow_fit: bool,
2743        allow_brace: bool,
2744        allow_hatch: bool,
2745        allow_close: bool,
2746    ) -> PResult<Option<Expr>> {
2747        if self.contextual_attr_ahead(allow_fit, allow_brace, allow_hatch, allow_close) {
2748            Ok(None)
2749        } else {
2750            self.opt_expr()
2751        }
2752    }
2753
2754    fn contextual_attr_ahead(
2755        &self,
2756        allow_fit: bool,
2757        allow_brace: bool,
2758        allow_hatch: bool,
2759        allow_close: bool,
2760    ) -> bool {
2761        matches!(
2762            self.cur(),
2763            Token::Name(n)
2764                if (allow_fit && n == "fit")
2765                    || (allow_hatch
2766                        && matches!(
2767                            n.as_str(),
2768                            "hatch"
2769                                | "crosshatch"
2770                                | "hatchangle"
2771                                | "hatchsep"
2772                                | "hatchwid"
2773                                | "hatchwidth"
2774                                | "hatchcolor"
2775                                | "gradient"
2776                                | "gradientangle"
2777                        ))
2778                    || n == "opacity"
2779                    || (allow_brace && matches!(n.as_str(), "bracepos" | "labeloffset"))
2780                    || n == "behind"
2781                    || n == "class"
2782                    || (allow_close && n == "close")
2783        )
2784    }
2785
2786    fn starts_scalar(&self) -> bool {
2787        matches!(
2788            self.cur(),
2789            Token::Float(_)
2790                | Token::Name(_)
2791                | Token::EnvVar(_)
2792                | Token::Lparen
2793                | Token::Minus
2794                | Token::Plus
2795                | Token::Not
2796                | Token::Func1(_)
2797                | Token::Func2(_)
2798                | Token::Kw(Kw::Rand)
2799                | Token::ArgCount
2800        )
2801    }
2802
2803    fn parse_expr(&mut self) -> PResult<Expr> {
2804        self.parse_or()
2805    }
2806
2807    fn parse_or(&mut self) -> PResult<Expr> {
2808        let mut e = self.parse_and()?;
2809        while self.eat(&Token::OrOr) {
2810            let r = self.parse_and()?;
2811            e = Expr::Bin(BinOp::Or, Box::new(e), Box::new(r));
2812        }
2813        Ok(e)
2814    }
2815
2816    fn parse_and(&mut self) -> PResult<Expr> {
2817        let mut e = self.parse_cmp()?;
2818        while self.eat(&Token::AndAnd) {
2819            let r = self.parse_cmp()?;
2820            e = Expr::Bin(BinOp::And, Box::new(e), Box::new(r));
2821        }
2822        Ok(e)
2823    }
2824
2825    fn parse_cmp(&mut self) -> PResult<Expr> {
2826        let mut e = self.parse_add()?;
2827        loop {
2828            let op = match self.cur() {
2829                Token::EqEq => BinOp::Eq,
2830                Token::Neq => BinOp::Ne,
2831                Token::Lt => BinOp::Lt,
2832                Token::Le => BinOp::Le,
2833                Token::Gt => BinOp::Gt,
2834                Token::Ge => BinOp::Ge,
2835                _ => break,
2836            };
2837            self.bump();
2838            let r = self.parse_add()?;
2839            e = Expr::Bin(op, Box::new(e), Box::new(r));
2840        }
2841        Ok(e)
2842    }
2843
2844    fn parse_add(&mut self) -> PResult<Expr> {
2845        let mut e = self.parse_mul()?;
2846        loop {
2847            let op = match self.cur() {
2848                Token::Plus => BinOp::Add,
2849                Token::Minus => BinOp::Sub,
2850                _ => break,
2851            };
2852            self.bump();
2853            let r = self.parse_mul()?;
2854            e = Expr::Bin(op, Box::new(e), Box::new(r));
2855        }
2856        Ok(e)
2857    }
2858
2859    fn parse_mul(&mut self) -> PResult<Expr> {
2860        let mut e = self.parse_unary()?;
2861        loop {
2862            let op = match self.cur() {
2863                Token::Mult => BinOp::Mul,
2864                Token::Div => BinOp::Div,
2865                Token::Percent => BinOp::Mod,
2866                _ => break,
2867            };
2868            self.bump();
2869            let r = self.parse_unary()?;
2870            e = Expr::Bin(op, Box::new(e), Box::new(r));
2871        }
2872        Ok(e)
2873    }
2874
2875    fn parse_unary(&mut self) -> PResult<Expr> {
2876        let op = match self.cur() {
2877            Token::Minus => Some(UnOp::Neg),
2878            Token::Plus => Some(UnOp::Pos),
2879            Token::Not => Some(UnOp::Not),
2880            _ => None,
2881        };
2882        if let Some(op) = op {
2883            self.bump();
2884            let e = self.parse_unary()?;
2885            Ok(Expr::Unary(op, Box::new(e)))
2886        } else {
2887            self.parse_pow()
2888        }
2889    }
2890
2891    fn parse_pow(&mut self) -> PResult<Expr> {
2892        let base = self.parse_primary()?;
2893        if self.eat(&Token::Caret) {
2894            let exp = self.parse_unary()?; // right-associative
2895            Ok(Expr::Bin(BinOp::Pow, Box::new(base), Box::new(exp)))
2896        } else {
2897            Ok(base)
2898        }
2899    }
2900
2901    /// Lookahead from just inside a `(`: is the matching `)` immediately followed
2902    /// by `.x` or `.y`? (Used to read `( position ).x` as a coordinate.)
2903    fn paren_followed_by_dot_xy(&self) -> bool {
2904        let mut depth = 1i32;
2905        let mut i = self.idx;
2906        while let Some(s) = self.toks.get(i) {
2907            match &s.tok {
2908                Token::Lparen => depth += 1,
2909                Token::Rparen => {
2910                    depth -= 1;
2911                    if depth == 0 {
2912                        return matches!(
2913                            self.toks.get(i + 1).map(|t| &t.tok),
2914                            Some(Token::DotX | Token::DotY)
2915                        );
2916                    }
2917                }
2918                Token::Eof => return false,
2919                _ => {}
2920            }
2921            i += 1;
2922        }
2923        false
2924    }
2925
2926    fn parse_primary(&mut self) -> PResult<Expr> {
2927        // place-derived scalars: location.x / location.y / place.attr
2928        if self.at_place_start() && self.place_is_scalar_ahead() {
2929            return self.parse_place_scalar();
2930        }
2931        // a string operand (only meaningful as an `==`/`!=` operand)
2932        if self.at_string_start() {
2933            return Ok(Expr::Str(self.parse_stringexpr()?));
2934        }
2935        match self.cur().clone() {
2936            Token::Float(v) => {
2937                self.bump();
2938                Ok(Expr::Num(v))
2939            }
2940            Token::Name(name) | Token::Label(name) => {
2941                self.bump();
2942                let subscript = if self.eat(&Token::LeftBrack) {
2943                    let e = self.parse_subscript()?;
2944                    self.expect(&Token::RightBrack)?;
2945                    Some(Box::new(e))
2946                } else {
2947                    None
2948                };
2949                Ok(Expr::Var(name, subscript))
2950            }
2951            Token::EnvVar(v) => {
2952                self.bump();
2953                Ok(Expr::Env(v))
2954            }
2955            Token::Lparen => {
2956                self.bump();
2957                // embedded assignment `( name = expr )` yields the assigned value
2958                if matches!(self.cur(), Token::Name(_) | Token::Label(_))
2959                    && self.assignment_op_after_var_ref()
2960                {
2961                    let name = match self.bump() {
2962                        Token::Name(n) | Token::Label(n) => n,
2963                        _ => unreachable!(),
2964                    };
2965                    let subscript = if self.eat(&Token::LeftBrack) {
2966                        let e = self.parse_subscript()?;
2967                        self.expect(&Token::RightBrack)?;
2968                        Some(Box::new(e))
2969                    } else {
2970                        None
2971                    };
2972                    self.bump(); // `=`
2973                    let v = self.parse_expr()?;
2974                    self.expect(&Token::Rparen)?;
2975                    return Ok(Expr::Assign(name, subscript, Box::new(v)));
2976                }
2977                // `( position ).x` / `.y` — a coordinate of a parenthesised
2978                // position (e.g. `(A - B).x`, `($1-($2)).y`). Chosen by lookahead
2979                // for a trailing `.x`/`.y`, since `(A - B)` alone parses as scalar
2980                // (labels read as variables).
2981                if self.paren_followed_by_dot_xy() {
2982                    let pos = self.parse_position()?;
2983                    self.expect(&Token::Rparen)?;
2984                    let loc = Location::Paren(Box::new(pos));
2985                    return Ok(if self.eat(&Token::DotX) {
2986                        Expr::DotX(loc)
2987                    } else {
2988                        self.expect(&Token::DotY)?;
2989                        Expr::DotY(loc)
2990                    });
2991                }
2992                // a plain scalar group `( expr )`
2993                let e = self.parse_expr()?;
2994                self.expect(&Token::Rparen)?;
2995                Ok(e)
2996            }
2997            // `$+` outside any macro invocation: zero arguments
2998            Token::ArgCount => {
2999                self.bump();
3000                Ok(Expr::Num(0.0))
3001            }
3002            Token::Func1(f) => {
3003                self.bump();
3004                self.expect(&Token::Lparen)?;
3005                let e = self.parse_expr()?;
3006                self.expect(&Token::Rparen)?;
3007                Ok(Expr::Func1(f, Box::new(e)))
3008            }
3009            Token::Func2(f) => {
3010                self.bump();
3011                self.expect(&Token::Lparen)?;
3012                let a = self.parse_expr()?;
3013                self.expect(&Token::Comma)?;
3014                let b = self.parse_expr()?;
3015                self.expect(&Token::Rparen)?;
3016                Ok(Expr::Func2(f, Box::new(a), Box::new(b)))
3017            }
3018            Token::Kw(Kw::Rand) => {
3019                self.bump();
3020                self.expect(&Token::Lparen)?;
3021                let arg = if self.at(&Token::Rparen) {
3022                    None
3023                } else {
3024                    Some(Box::new(self.parse_expr()?))
3025                };
3026                self.expect(&Token::Rparen)?;
3027                Ok(Expr::Rand(arg))
3028            }
3029            other => self.err(format!("expected an expression, found {other:?}")),
3030        }
3031    }
3032
3033    /// Parse a place followed by `.x` / `.y` / `.attr` to yield a scalar.
3034    fn parse_place_scalar(&mut self) -> PResult<Expr> {
3035        let place = self.parse_place()?;
3036        match self.cur().clone() {
3037            Token::DotX => {
3038                self.bump();
3039                Ok(Expr::DotX(Location::Place(place)))
3040            }
3041            Token::DotY => {
3042                self.bump();
3043                Ok(Expr::DotY(Location::Place(place)))
3044            }
3045            Token::Param(p) => {
3046                self.bump();
3047                Ok(Expr::PlaceAttr(place, p))
3048            }
3049            other => self.err(format!(
3050                "a place is not a number here; expected `.x`, `.y`, or an attribute, found {other:?}"
3051            )),
3052        }
3053    }
3054}
3055
3056#[cfg(test)]
3057mod tests {
3058    use super::*;
3059
3060    fn pic(src: &str) -> Picture {
3061        parse(src).unwrap_or_else(|e| panic!("parse error: {e}"))
3062    }
3063
3064    #[test]
3065    fn kernighan_pipeline() {
3066        let p = pic(r#".PS
3067ellipse "document"
3068arrow
3069box "PIC"
3070arrow
3071box "TBL/EQN" "(optional)" dashed
3072arrow
3073box "TROFF"
3074arrow
3075ellipse "typesetter"
3076.PE
3077"#);
3078        assert_eq!(p.stmts.len(), 9);
3079        // the dashed box with two strings
3080        if let Stmt::Object { object, .. } = &p.stmts[4] {
3081            assert_eq!(object.kind, ObjectKind::Primitive(Prim::Box));
3082            let texts = object
3083                .attrs
3084                .iter()
3085                .filter(|a| matches!(a, Attr::Text(_)))
3086                .count();
3087            assert_eq!(texts, 2);
3088            assert!(
3089                object
3090                    .attrs
3091                    .iter()
3092                    .any(|a| matches!(a, Attr::LineStyle(LineType::Dashed, _)))
3093            );
3094        } else {
3095            panic!("expected object");
3096        }
3097    }
3098
3099    #[test]
3100    fn box_with_dims_and_at() {
3101        let p = pic("box ht 0.3 wid 0.5 at 0.25,0.15");
3102        let Stmt::Object { object, .. } = &p.stmts[0] else {
3103            panic!()
3104        };
3105        assert!(matches!(object.attrs[0], Attr::Dim(DimKind::Ht, _)));
3106        assert!(matches!(object.attrs[1], Attr::Dim(DimKind::Wid, _)));
3107        assert!(matches!(object.attrs[2], Attr::At(Position::Pair(_, _))));
3108    }
3109
3110    #[test]
3111    fn behind_parses_as_contextual_extension_attribute() {
3112        let p = pic("A: box\nbox behind A");
3113        let Stmt::Object { object, .. } = &p.stmts[1] else {
3114            panic!()
3115        };
3116        let Some(Attr::Behind(Place::Name {
3117            name, subscript, ..
3118        })) = object.attrs.last()
3119        else {
3120            panic!("expected behind attribute");
3121        };
3122        assert_eq!(name, "A");
3123        assert!(subscript.is_none());
3124
3125        let p = pic("behind = 2\nbox wid behind");
3126        assert_eq!(p.stmts.len(), 2);
3127        assert!(matches!(p.stmts[0], Stmt::Assign(_)));
3128    }
3129
3130    #[test]
3131    fn fit_parses_as_contextual_extension_attribute() {
3132        let p = pic("box \"long label\" fit");
3133        let Stmt::Object { object, .. } = &p.stmts[0] else {
3134            panic!()
3135        };
3136        assert!(object.attrs.iter().any(|a| matches!(a, Attr::Fit)));
3137
3138        let p = pic("fit = 2\nbox wid fit");
3139        assert_eq!(p.stmts.len(), 2);
3140        assert!(matches!(p.stmts[0], Stmt::Assign(_)));
3141
3142        let p = pic("fit = 2\nline fit");
3143        let Stmt::Object { object, .. } = &p.stmts[1] else {
3144            panic!()
3145        };
3146        assert!(matches!(object.attrs[0], Attr::Dist(_, _)));
3147    }
3148
3149    #[test]
3150    fn hatch_parses_as_contextual_extension_attribute() {
3151        let p = pic("box hatch hatchangle 30 hatchsep .05 hatchwid 1.2 hatchcolor red");
3152        let Stmt::Object { object, .. } = &p.stmts[0] else {
3153            panic!()
3154        };
3155        assert!(
3156            object
3157                .attrs
3158                .iter()
3159                .any(|a| matches!(a, Attr::Hatch(HatchKind::Single)))
3160        );
3161        assert!(
3162            object
3163                .attrs
3164                .iter()
3165                .any(|a| matches!(a, Attr::HatchAngle(_)))
3166        );
3167        assert!(object.attrs.iter().any(|a| matches!(a, Attr::HatchSep(_))));
3168        assert!(
3169            object
3170                .attrs
3171                .iter()
3172                .any(|a| matches!(a, Attr::HatchWidth(_)))
3173        );
3174        assert!(
3175            object
3176                .attrs
3177                .iter()
3178                .any(|a| matches!(a, Attr::HatchColor(_)))
3179        );
3180
3181        let p = pic("hatch = 2\nbox wid hatch");
3182        assert_eq!(p.stmts.len(), 2);
3183        assert!(matches!(p.stmts[0], Stmt::Assign(_)));
3184        let Stmt::Object { object, .. } = &p.stmts[1] else {
3185            panic!()
3186        };
3187        assert!(matches!(object.attrs[0], Attr::Dim(DimKind::Wid, _)));
3188    }
3189
3190    #[test]
3191    fn opacity_parses_as_contextual_extension_attribute() {
3192        let p = pic("box opacity 0.5");
3193        let Stmt::Object { object, .. } = &p.stmts[0] else {
3194            panic!()
3195        };
3196        assert!(object.attrs.iter().any(|a| matches!(a, Attr::Opacity(_))));
3197
3198        let p = pic("opacity = 2\nbox wid opacity");
3199        assert_eq!(p.stmts.len(), 2);
3200        assert!(matches!(p.stmts[0], Stmt::Assign(_)));
3201        let Stmt::Object { object, .. } = &p.stmts[1] else {
3202            panic!()
3203        };
3204        assert!(matches!(object.attrs[0], Attr::Dim(DimKind::Wid, _)));
3205    }
3206
3207    #[test]
3208    fn close_parses_as_contextual_line_extension_attribute() {
3209        let p = pic("line right then up close");
3210        let Stmt::Object { object, .. } = &p.stmts[0] else {
3211            panic!()
3212        };
3213        assert!(object.attrs.iter().any(|a| matches!(a, Attr::Close)));
3214
3215        let p = pic("close = 2\nbox wid close");
3216        assert_eq!(p.stmts.len(), 2);
3217        assert!(matches!(p.stmts[0], Stmt::Assign(_)));
3218        let Stmt::Object { object, .. } = &p.stmts[1] else {
3219            panic!()
3220        };
3221        assert!(matches!(object.attrs[0], Attr::Dim(DimKind::Wid, _)));
3222    }
3223
3224    #[test]
3225    fn gradient_parses_as_contextual_extension_attribute() {
3226        let p = pic("box gradient \"steelblue\" white gradientangle 45");
3227        let Stmt::Object { object, .. } = &p.stmts[0] else {
3228            panic!()
3229        };
3230        assert!(object.attrs.iter().any(|a| matches!(a, Attr::Gradient(..))));
3231        assert!(
3232            object
3233                .attrs
3234                .iter()
3235                .any(|a| matches!(a, Attr::GradientAngle(_)))
3236        );
3237
3238        // contextual fallback: `gradient` stays usable as a variable
3239        let p = pic("gradient = 2\nbox wid gradient");
3240        assert!(matches!(p.stmts[0], Stmt::Assign(_)));
3241        let Stmt::Object { object, .. } = &p.stmts[1] else {
3242            panic!()
3243        };
3244        assert!(matches!(object.attrs[0], Attr::Dim(DimKind::Wid, _)));
3245    }
3246
3247    #[test]
3248    fn class_parses_inline_and_statement_forms() {
3249        let p = pic("box class \"critical\"");
3250        let Stmt::Object { object, .. } = &p.stmts[0] else {
3251            panic!()
3252        };
3253        assert!(object.attrs.iter().any(|a| matches!(a, Attr::Class(_))));
3254
3255        let p = pic("A: box\nclass A \"hot\"\nclass last box \"cold\"");
3256        assert!(matches!(p.stmts[1], Stmt::Class { .. }));
3257        assert!(matches!(p.stmts[2], Stmt::Class { .. }));
3258
3259        // contextual fallbacks: assignment and expression use survive
3260        let p = pic("class = 2\nbox wid class");
3261        assert!(matches!(p.stmts[0], Stmt::Assign(_)));
3262        let Stmt::Object { object, .. } = &p.stmts[1] else {
3263            panic!()
3264        };
3265        assert!(matches!(object.attrs[0], Attr::Dim(DimKind::Wid, _)));
3266    }
3267
3268    #[test]
3269    fn brace_parses_as_contextual_extension_object() {
3270        let p = pic(
3271            "A: box\nB: box\nbrace from A.e to B.w down \"group\" wid .2 bracepos .4 labeloffset .1",
3272        );
3273        let Stmt::Object { object, .. } = &p.stmts[2] else {
3274            panic!()
3275        };
3276        assert_eq!(object.kind, ObjectKind::Brace);
3277        assert!(object.attrs.iter().any(|a| matches!(a, Attr::From(_))));
3278        assert!(object.attrs.iter().any(|a| matches!(a, Attr::To(_))));
3279        assert!(
3280            object
3281                .attrs
3282                .iter()
3283                .any(|a| matches!(a, Attr::Direction(Dir::Down, None)))
3284        );
3285        assert!(object.attrs.iter().any(|a| matches!(a, Attr::Text(_))));
3286        assert!(object.attrs.iter().any(|a| matches!(a, Attr::BracePos(_))));
3287        assert!(
3288            object
3289                .attrs
3290                .iter()
3291                .any(|a| matches!(a, Attr::BraceLabelOffset(_)))
3292        );
3293
3294        let p = pic("brace = 2\nline right brace");
3295        assert!(matches!(p.stmts[0], Stmt::Assign(_)));
3296        let Stmt::Object { object, .. } = &p.stmts[1] else {
3297            panic!()
3298        };
3299        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Line));
3300
3301        let p = pic("brace from 0,0 to 1,0\nline from last brace.start to last brace.end");
3302        assert_eq!(p.stmts.len(), 2);
3303    }
3304
3305    #[test]
3306    fn labeled_and_corners() {
3307        let p = pic("B1: box\narc -> from top of B1 to last box.ne");
3308        assert!(matches!(p.stmts[0], Stmt::Object { label: Some(_), .. }));
3309        let Stmt::Object { object, .. } = &p.stmts[1] else {
3310            panic!()
3311        };
3312        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Arc));
3313        assert!(
3314            object
3315                .attrs
3316                .iter()
3317                .any(|a| matches!(a, Attr::Arrowhead(Arrow::Right, _)))
3318        );
3319        // from top of B1
3320        assert!(object.attrs.iter().any(|a| matches!(
3321            a,
3322            Attr::From(Position::Place(Location::Place(Place::CornerOf(
3323                Corner::N,
3324                _
3325            ))))
3326        )));
3327    }
3328
3329    #[test]
3330    fn with_at_and_shift() {
3331        let p = pic("ellipse \"2\" with .nw at last ellipse.se + (0.1,0)");
3332        let Stmt::Object { object, .. } = &p.stmts[0] else {
3333            panic!()
3334        };
3335        let with = object
3336            .attrs
3337            .iter()
3338            .find(|a| matches!(a, Attr::With { .. }))
3339            .unwrap();
3340        let Attr::With { anchor, at } = with else {
3341            panic!()
3342        };
3343        assert_eq!(*anchor, WithAnchor::Corner(Corner::Nw));
3344        // `last ellipse.se + (0.1,0)` is a position sum
3345        assert!(matches!(at, Position::Sum(Sign::Plus, _, _)));
3346    }
3347
3348    #[test]
3349    fn with_member_anchor_parses() {
3350        let p = pic("[ A: box ] with .A.c at Here");
3351        let Stmt::Object { object, .. } = &p.stmts[0] else {
3352            panic!()
3353        };
3354        let with = object
3355            .attrs
3356            .iter()
3357            .find(|a| matches!(a, Attr::With { .. }))
3358            .unwrap();
3359        let Attr::With { anchor, .. } = with else {
3360            panic!()
3361        };
3362        assert!(matches!(
3363            anchor,
3364            WithAnchor::Place(Place::Corner(inner, Corner::Center))
3365                if matches!(inner.as_ref(), Place::Name { name, .. } if name == "A")
3366        ));
3367    }
3368
3369    #[test]
3370    fn expression_precedence() {
3371        // 2 + 3 * 4 ^ 2  ==  2 + (3 * (4^2)) = 50
3372        let p = pic("x = 2 + 3 * 4 ^ 2");
3373        let Stmt::Assign(list) = &p.stmts[0] else {
3374            panic!()
3375        };
3376        // structure: Add(2, Mul(3, Pow(4,2)))
3377        let Expr::Bin(BinOp::Add, _, rhs) = &list[0].value else {
3378            panic!("expected top-level add")
3379        };
3380        assert!(matches!(**rhs, Expr::Bin(BinOp::Mul, _, _)));
3381    }
3382
3383    #[test]
3384    fn between_position() {
3385        let p = pic("arrow from 1/3 of the way between A.ne and A.se");
3386        let Stmt::Object { object, .. } = &p.stmts[0] else {
3387            panic!()
3388        };
3389        assert!(object.attrs.iter().any(|a| matches!(
3390            a,
3391            Attr::From(Position::Between {
3392                of_the_way: true,
3393                ..
3394            })
3395        )));
3396    }
3397
3398    #[test]
3399    fn assignment_list_and_envvar() {
3400        let p = pic("boxht = 0.3; boxwid = 2 * boxht");
3401        assert_eq!(p.stmts.len(), 2);
3402        let Stmt::Assign(a0) = &p.stmts[0] else {
3403            panic!()
3404        };
3405        assert_eq!(a0[0].target, AssignTarget::Env(EnvVar::Boxht));
3406    }
3407
3408    #[test]
3409    fn dpic_svg_font_stub_parses_as_string() {
3410        let p = pic("print svg_font(\"Times\", 12)");
3411        let Stmt::Print(PrintItem::Str(StringExpr::SvgFont(args))) = &p.stmts[0] else {
3412            panic!()
3413        };
3414        assert_eq!(args.len(), 2);
3415    }
3416
3417    #[test]
3418    fn subscripted_variable_refs_parse() {
3419        let p = pic("P[1] = 2\nx = P[1]");
3420        let Stmt::Assign(a0) = &p.stmts[0] else {
3421            panic!()
3422        };
3423        assert!(matches!(&a0[0].target, AssignTarget::Var(name, Some(_)) if name == "P"));
3424
3425        let Stmt::Assign(a1) = &p.stmts[1] else {
3426            panic!()
3427        };
3428        assert!(matches!(&a1[0].value, Expr::Var(name, Some(_)) if name == "P"));
3429    }
3430
3431    #[test]
3432    fn block_object() {
3433        let p = pic("[ box; circle ] with .nw at Here");
3434        let Stmt::Object { object, .. } = &p.stmts[0] else {
3435            panic!()
3436        };
3437        let ObjectKind::Block(inner) = &object.kind else {
3438            panic!()
3439        };
3440        assert_eq!(inner.len(), 2);
3441    }
3442
3443    #[test]
3444    fn diamond_line_with_then() {
3445        let p = pic("line up right then down right then down left then up left");
3446        let Stmt::Object { object, .. } = &p.stmts[0] else {
3447            panic!()
3448        };
3449        let thens = object
3450            .attrs
3451            .iter()
3452            .filter(|a| matches!(a, Attr::Then))
3453            .count();
3454        assert_eq!(thens, 3);
3455    }
3456
3457    #[test]
3458    fn place_scalar_in_coord_pair() {
3459        // issue #3: (A.x, expr) must parse as an (expr,expr) pair, not a place
3460        let p = pic("A: box\n\"t\" at (A.x, A.y - 0.5)");
3461        let Stmt::Object { object, .. } = &p.stmts[1] else {
3462            panic!()
3463        };
3464        assert!(
3465            object
3466                .attrs
3467                .iter()
3468                .any(|a| matches!(a, Attr::At(Position::Pair(_, _))))
3469        );
3470        // a plain point place still parses as a place position
3471        let q = pic("A: box\nbox at A.ne");
3472        let Stmt::Object { object, .. } = &q.stmts[1] else {
3473            panic!()
3474        };
3475        assert!(object.attrs.iter().any(|a| matches!(
3476            a,
3477            Attr::At(Position::Place(Location::Place(Place::Corner(_, _))))
3478        )));
3479    }
3480
3481    #[test]
3482    fn ignores_non_svg_backend_preambles() {
3483        let p = pic(r#".PS
3484verbatimtex
3485\global\def\foo#1{#1}
3486etex
3487\global\def\bar#1{#1}
3488\psset{arrowsize=4pt}
3489box
3490.PE
3491"#);
3492        assert_eq!(p.stmts.len(), 1);
3493        let Stmt::Object { object, .. } = &p.stmts[0] else {
3494            panic!()
3495        };
3496        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Box));
3497    }
3498
3499    #[test]
3500    fn backend_filter_keeps_global_lines_inside_strings() {
3501        let p = pic(
3502            "sh \"echo -n \\\"print \\\\\"\\\" > x\"\nif dpicopt==optPGF then { command \"cycle; \\\n\\global\\let\\dpicdraw=x\" } else { box }",
3503        );
3504        assert_eq!(p.stmts.len(), 2);
3505        let Stmt::Object { object, .. } = &p.stmts[1] else {
3506            panic!()
3507        };
3508        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Box));
3509    }
3510
3511    #[test]
3512    fn static_if_copy_defines_macros_before_following_statements() {
3513        let dir = std::env::temp_dir().join(format!("rpic_static_if_{}", std::process::id()));
3514        std::fs::create_dir_all(&dir).unwrap();
3515        std::fs::write(dir.join("macros.pic"), "define makebox { box wid $1 }\n").unwrap();
3516        std::fs::write(
3517            dir.join("inc.pic"),
3518            "define makecircle { circle rad 0.1 }\n",
3519        )
3520        .unwrap();
3521        let p = parse_in_dir(
3522            "if \"plotlib\" != \"1\" then { copy \"macros.pic\" }\ndefine choose { if \"$1\"==\"\" then { box } else { copy \"$1/inc.pic\" } }\nchoose(.)\nmakecircle()\nmakebox(0.4)",
3523            Some(dir.as_path()),
3524        )
3525        .unwrap_or_else(|e| panic!("parse error: {e}"));
3526        let _ = std::fs::remove_dir_all(&dir);
3527        assert_eq!(p.stmts.len(), 2);
3528        let Stmt::Object { object, .. } = &p.stmts[0] else {
3529            panic!()
3530        };
3531        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Circle));
3532        let Stmt::Object { object, .. } = &p.stmts[1] else {
3533            panic!()
3534        };
3535        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Box));
3536    }
3537
3538    #[test]
3539    fn unsupported_control_is_clear() {
3540        // `copy "file"` with no filesystem context reports a clear file error
3541        let e = parse("copy \"x\"").unwrap_err();
3542        assert!(e.msg.contains("copy") && e.msg.contains("file"));
3543    }
3544
3545    #[test]
3546    fn control_constructs_parse() {
3547        assert!(parse("for i = 1 to 3 do { box }").is_ok());
3548        assert!(parse("if 1 > 0 then { box } else { circle }").is_ok());
3549        assert!(parse("reset boxht, boxwid").is_ok());
3550        // define is consumed by the preprocessor and expanded
3551        let p = parse("define e { box }\ne\ne").unwrap();
3552        assert_eq!(p.stmts.len(), 2);
3553    }
3554}