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