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] => {
1006            let lhs = static_string(a)?;
1007            let rhs = static_string(b)?;
1008            match op.tok {
1009                Token::EqEq => Some(lhs == rhs),
1010                Token::Neq => Some(lhs != rhs),
1011                _ => None,
1012            }
1013        }
1014        [a, op, b, op2, c] if matches!(op2.tok, Token::Plus) => {
1015            let lhs = static_string(a)?;
1016            let mut rhs = static_string(b)?;
1017            rhs.push_str(&static_string(c)?);
1018            match op.tok {
1019                Token::EqEq => Some(lhs == rhs),
1020                Token::Neq => Some(lhs != rhs),
1021                _ => None,
1022            }
1023        }
1024        _ => None,
1025    }
1026}
1027
1028fn trim_trailing_eof(toks: &[Spanned]) -> &[Spanned] {
1029    if matches!(toks.last().map(|s| &s.tok), Some(Token::Eof)) {
1030        &toks[..toks.len() - 1]
1031    } else {
1032        toks
1033    }
1034}
1035
1036fn static_string(s: &Spanned) -> Option<String> {
1037    match &s.tok {
1038        Token::Str(v) => Some(v.clone()),
1039        _ => None,
1040    }
1041}
1042
1043type PResult<T> = Result<T, ParseError>;
1044
1045fn is_assign_op(t: &Token) -> bool {
1046    matches!(
1047        t,
1048        Token::Eq
1049            | Token::ColonEq
1050            | Token::PlusEq
1051            | Token::MinusEq
1052            | Token::MultEq
1053            | Token::DivEq
1054            | Token::RemEq
1055    )
1056}
1057
1058struct Parser {
1059    toks: Vec<Spanned>,
1060    idx: usize,
1061}
1062
1063impl Parser {
1064    fn new(toks: Vec<Spanned>) -> Self {
1065        Parser { toks, idx: 0 }
1066    }
1067
1068    // ---- cursor helpers ----------------------------------------------------
1069
1070    fn cur(&self) -> &Token {
1071        &self.toks[self.idx].tok
1072    }
1073    fn peek(&self, n: usize) -> &Token {
1074        self.toks
1075            .get(self.idx + n)
1076            .map(|s| &s.tok)
1077            .unwrap_or(&Token::Eof)
1078    }
1079    fn at(&self, t: &Token) -> bool {
1080        self.cur() == t
1081    }
1082    fn bump(&mut self) -> Token {
1083        let t = self.toks[self.idx].tok.clone();
1084        if self.idx + 1 < self.toks.len() {
1085            self.idx += 1;
1086        }
1087        t
1088    }
1089    fn eat(&mut self, t: &Token) -> bool {
1090        if self.at(t) {
1091            self.bump();
1092            true
1093        } else {
1094            false
1095        }
1096    }
1097    fn expect(&mut self, t: &Token) -> PResult<()> {
1098        if self.eat(t) {
1099            Ok(())
1100        } else {
1101            self.err(format!("expected {t:?}, found {:?}", self.cur()))
1102        }
1103    }
1104    fn err<T>(&self, msg: impl Into<String>) -> PResult<T> {
1105        let s = &self.toks[self.idx];
1106        Err(ParseError {
1107            msg: msg.into(),
1108            line: s.line,
1109            col: s.col,
1110        })
1111    }
1112    fn at_kw(&self, k: Kw) -> bool {
1113        matches!(self.cur(), Token::Kw(x) if *x == k)
1114    }
1115    fn eat_kw(&mut self, k: Kw) -> bool {
1116        if self.at_kw(k) {
1117            self.bump();
1118            true
1119        } else {
1120            false
1121        }
1122    }
1123    fn skip_newlines(&mut self) {
1124        while self.at(&Token::Newline) {
1125            self.bump();
1126        }
1127    }
1128
1129    // ---- top level ---------------------------------------------------------
1130
1131    fn parse_picture(&mut self) -> PResult<Picture> {
1132        // `.PS`/`.PE` are treated as markers that may appear anywhere; statements
1133        // (including `animate`) are collected across them up to EOF. The first
1134        // `.PS` may carry optional width/height.
1135        let (mut width, mut height) = (None, None);
1136        let mut seen_ps = false;
1137        let mut stmts = Vec::new();
1138        loop {
1139            self.skip_newlines();
1140            match self.cur() {
1141                Token::Eof => break,
1142                Token::DotPS => {
1143                    self.bump();
1144                    if !seen_ps && self.starts_scalar() {
1145                        width = Some(self.parse_expr()?);
1146                        if self.starts_scalar() {
1147                            height = Some(self.parse_expr()?);
1148                        }
1149                    }
1150                    seen_ps = true;
1151                    while !self.at(&Token::Newline) && !self.at(&Token::Eof) {
1152                        self.bump();
1153                    }
1154                    continue;
1155                }
1156                Token::DotPE => {
1157                    self.bump();
1158                    continue;
1159                }
1160                _ => {}
1161            }
1162            stmts.push(self.parse_element()?);
1163            if !self.at(&Token::Newline)
1164                && !self.at(&Token::Eof)
1165                && !self.at(&Token::DotPE)
1166                && !self.at(&Token::DotPS)
1167            {
1168                return self.err(format!("unexpected {:?} after statement", self.cur()));
1169            }
1170        }
1171        Ok(Picture {
1172            width,
1173            height,
1174            stmts,
1175            macros: HashMap::new(),
1176            base_dir: None,
1177        })
1178    }
1179
1180    /// Parse elements until one of `terminators` (or EOF) is the current token.
1181    fn parse_elementlist(&mut self, terminators: &[Token]) -> PResult<Vec<Stmt>> {
1182        let mut stmts = Vec::new();
1183        loop {
1184            self.skip_newlines();
1185            if self.at(&Token::Eof) || terminators.iter().any(|t| self.at(t)) {
1186                break;
1187            }
1188            let s = self.parse_element()?;
1189            stmts.push(s);
1190            // a statement must end at a newline, a terminator, or EOF
1191            if !self.at(&Token::Newline)
1192                && !self.at(&Token::Eof)
1193                && !terminators.iter().any(|t| self.at(t))
1194            {
1195                return self.err(format!("unexpected {:?} after statement", self.cur()));
1196            }
1197        }
1198        Ok(stmts)
1199    }
1200
1201    // ---- statements --------------------------------------------------------
1202
1203    fn parse_element(&mut self) -> PResult<Stmt> {
1204        // A `%`-led line is a comment convention in some source documents (pic
1205        // proper uses `#`). `%` is never valid at statement start, so skip the
1206        // line as a no-op.
1207        if self.at(&Token::Percent) {
1208            while !self.at(&Token::Newline) && !self.at(&Token::Eof) {
1209                self.bump();
1210            }
1211            return Ok(Stmt::Print(PrintItem::Str(StringExpr::Lit(String::new()))));
1212        }
1213
1214        // rpic animation directive.
1215        if self.at_kw(Kw::Animate) {
1216            return Ok(Stmt::Animate(self.parse_animate()?));
1217        }
1218
1219        // control constructs
1220        match self.cur() {
1221            Token::Kw(Kw::If) => return self.parse_if(),
1222            Token::Kw(Kw::For) => return self.parse_for(),
1223            Token::Kw(Kw::Print) => return self.parse_print(),
1224            Token::Kw(Kw::Exec) => return self.parse_exec(),
1225            Token::Kw(Kw::Reset) => return self.parse_reset(),
1226            _ => {}
1227        }
1228
1229        // `define`/`undef` are handled by the macro preprocessor before parsing;
1230        // reaching here means a non-brace form we don't support.
1231        if let Token::Kw(k) = self.cur() {
1232            match k {
1233                Kw::Define | Kw::Undef => {
1234                    return self.err("only the `define name { body }` macro form is supported");
1235                }
1236                // `command`/`sh` emit raw backend text or run a shell; neither
1237                // affects SVG geometry. The lexer already skipped their raw
1238                // argument text while preserving structural delimiters.
1239                Kw::Command | Kw::Sh => {
1240                    self.bump();
1241                    return Ok(Stmt::Print(PrintItem::Str(StringExpr::Lit(String::new()))));
1242                }
1243                Kw::Copy => {
1244                    return self.err("`copy` is not supported yet (planned milestone)");
1245                }
1246                _ => {}
1247            }
1248        }
1249
1250        // `{ … }` grouping
1251        if self.eat(&Token::LeftBrace) {
1252            let stmts = self.parse_elementlist(&[Token::RightBrace])?;
1253            self.expect(&Token::RightBrace)?;
1254            return Ok(Stmt::Group(stmts));
1255        }
1256
1257        // Labelled element: `Label [suffix] : (object | position)`
1258        if matches!(self.cur(), Token::Label(_)) && self.label_colon_ahead() {
1259            let label = self.parse_label()?;
1260            self.expect(&Token::Colon)?;
1261            if self.at_object_start() {
1262                let object = self.parse_object()?;
1263                return Ok(Stmt::Object {
1264                    label: Some(label),
1265                    object,
1266                });
1267            } else {
1268                let pos = self.parse_label_position()?;
1269                return Ok(Stmt::Place { label, pos });
1270            }
1271        }
1272
1273        // Assignment: `name [suffix] op …` or `envvar op …`
1274        if self.at_assignment_start() {
1275            return Ok(Stmt::Assign(self.parse_assignlist()?));
1276        }
1277
1278        // Bare direction change.
1279        if let Token::Dir(d) = self.cur() {
1280            let d = *d;
1281            // Only a standalone direction (next token ends the statement).
1282            if matches!(self.peek(1), Token::Newline | Token::Eof) {
1283                self.bump();
1284                return Ok(Stmt::Direction(d));
1285            }
1286        }
1287
1288        // Otherwise: an unlabelled object.
1289        let object = self.parse_object()?;
1290        Ok(Stmt::Object {
1291            label: None,
1292            object,
1293        })
1294    }
1295
1296    fn parse_if(&mut self) -> PResult<Stmt> {
1297        self.expect_kw(Kw::If)?;
1298        let cond = self.parse_expr()?;
1299        self.expect_kw(Kw::Then)?;
1300        let then_body = self.capture_braced()?;
1301        // optional `else { … }`, possibly across newlines (which otherwise end
1302        // the statement)
1303        let save = self.idx;
1304        self.skip_newlines();
1305        let else_body = if self.eat_kw(Kw::Else) {
1306            Some(self.capture_braced()?)
1307        } else {
1308            self.idx = save;
1309            None
1310        };
1311        Ok(Stmt::If {
1312            cond,
1313            then_body,
1314            else_body,
1315        })
1316    }
1317
1318    fn parse_for(&mut self) -> PResult<Stmt> {
1319        self.expect_kw(Kw::For)?;
1320        let var = match self.bump() {
1321            Token::Name(s) | Token::Label(s) => s,
1322            other => return self.err(format!("expected loop variable, found {other:?}")),
1323        };
1324        let subscript = if self.eat(&Token::LeftBrack) {
1325            let e = self.parse_subscript()?;
1326            self.expect(&Token::RightBrack)?;
1327            Some(e)
1328        } else {
1329            None
1330        };
1331        match self.bump() {
1332            Token::Eq | Token::ColonEq => {}
1333            other => return self.err(format!("expected `=` in for, found {other:?}")),
1334        }
1335        let from = self.parse_expr()?;
1336        self.expect_kw(Kw::To)?;
1337        let to = self.parse_expr()?;
1338        let mut by = Expr::Num(1.0);
1339        let mut mult = false;
1340        if self.eat_kw(Kw::By) {
1341            mult = self.eat(&Token::Mult);
1342            by = self.parse_expr()?;
1343        }
1344        self.expect_kw(Kw::Do)?;
1345        let body = self.capture_braced()?;
1346        Ok(Stmt::For {
1347            var,
1348            subscript,
1349            from,
1350            to,
1351            by,
1352            mult,
1353            body,
1354        })
1355    }
1356
1357    /// Capture a brace-delimited block as raw tokens (excluding the braces),
1358    /// for deferred parsing by the evaluator. Assumes the body follows.
1359    fn capture_braced(&mut self) -> PResult<Body> {
1360        self.skip_newlines();
1361        self.expect(&Token::LeftBrace)?;
1362        let start = self.idx;
1363        let mut depth = 1i32;
1364        loop {
1365            match self.cur() {
1366                Token::LeftBrace => depth += 1,
1367                Token::RightBrace => {
1368                    depth -= 1;
1369                    if depth == 0 {
1370                        break;
1371                    }
1372                }
1373                Token::Eof => return self.err("unterminated `{` body"),
1374                _ => {}
1375            }
1376            self.bump();
1377        }
1378        let body = self.toks[start..self.idx].to_vec();
1379        self.expect(&Token::RightBrace)?;
1380        Ok(body)
1381    }
1382
1383    fn parse_print(&mut self) -> PResult<Stmt> {
1384        self.expect_kw(Kw::Print)?;
1385        let item = if self.at_string_start() {
1386            PrintItem::Str(self.parse_stringexpr()?)
1387        } else {
1388            PrintItem::Expr(self.parse_expr()?)
1389        };
1390        Ok(Stmt::Print(item))
1391    }
1392
1393    fn parse_exec(&mut self) -> PResult<Stmt> {
1394        let arg_frame = self.toks[self.idx].arg_frame.clone();
1395        self.expect_kw(Kw::Exec)?;
1396        let command = self.parse_stringexpr()?;
1397        Ok(Stmt::Exec { command, arg_frame })
1398    }
1399
1400    fn parse_reset(&mut self) -> PResult<Stmt> {
1401        self.expect_kw(Kw::Reset)?;
1402        let mut list = Vec::new();
1403        if let Token::EnvVar(v) = self.cur() {
1404            list.push(*v);
1405            self.bump();
1406            while self.eat(&Token::Comma) {
1407                match self.cur() {
1408                    Token::EnvVar(v) => {
1409                        list.push(*v);
1410                        self.bump();
1411                    }
1412                    other => {
1413                        return self.err(format!("expected environment variable, found {other:?}"));
1414                    }
1415                }
1416            }
1417        }
1418        Ok(Stmt::Reset(list))
1419    }
1420
1421    fn at_string_start(&self) -> bool {
1422        self.token_starts_string_at(0)
1423    }
1424
1425    fn parse_animate(&mut self) -> PResult<Animate> {
1426        self.expect_kw(Kw::Animate)?;
1427        let target = self.parse_place()?;
1428        self.expect_kw(Kw::With)?;
1429        let effect = self.parse_stringexpr()?;
1430        let mut duration = None;
1431        let mut timing = Timing::Sequential;
1432        let mut delay = None;
1433        loop {
1434            if self.eat_kw(Kw::For) {
1435                duration = Some(self.parse_expr()?);
1436            } else if self.eat_kw(Kw::At) {
1437                timing = Timing::At(self.parse_expr()?);
1438            } else if self.eat_kw(Kw::After) {
1439                timing = Timing::After(self.parse_place()?);
1440            } else if self.eat_kw(Kw::Delay) {
1441                delay = Some(self.parse_expr()?);
1442            } else {
1443                break;
1444            }
1445        }
1446        Ok(Animate {
1447            target,
1448            effect,
1449            duration,
1450            timing,
1451            delay,
1452        })
1453    }
1454
1455    /// True if the current `Label` is followed by `:` (allowing a `[suffix]`).
1456    fn label_colon_ahead(&self) -> bool {
1457        match self.peek(1) {
1458            Token::Colon => true,
1459            Token::LeftBrack => {
1460                // scan past a balanced [ … ] suffix to find ':'
1461                let mut depth = 0;
1462                let mut i = self.idx + 1;
1463                while i < self.toks.len() {
1464                    match &self.toks[i].tok {
1465                        Token::LeftBrack => depth += 1,
1466                        Token::RightBrack => {
1467                            depth -= 1;
1468                            if depth == 0 {
1469                                return matches!(
1470                                    self.toks.get(i + 1).map(|s| &s.tok),
1471                                    Some(Token::Colon)
1472                                );
1473                            }
1474                        }
1475                        Token::Newline | Token::Eof => return false,
1476                        _ => {}
1477                    }
1478                    i += 1;
1479                }
1480                false
1481            }
1482            _ => false,
1483        }
1484    }
1485
1486    fn at_object_start(&self) -> bool {
1487        matches!(
1488            self.cur(),
1489            Token::Prim(_)
1490                | Token::LeftBrack
1491                | Token::Block
1492                | Token::Str(_)
1493                | Token::Arg(_)
1494                | Token::Kw(Kw::Sprintf)
1495        )
1496    }
1497
1498    fn at_assignment_start(&self) -> bool {
1499        match self.cur() {
1500            Token::Name(_) | Token::Label(_) => self.assignment_op_after_var_ref(),
1501            Token::EnvVar(_) => is_assign_op(self.peek(1)),
1502            _ => false,
1503        }
1504    }
1505
1506    fn assignment_op_after_var_ref(&self) -> bool {
1507        let mut i = self.idx + 1;
1508        if matches!(self.toks.get(i).map(|s| &s.tok), Some(Token::LeftBrack)) {
1509            i += 1;
1510            let mut depth = 1i32;
1511            while let Some(tok) = self.toks.get(i).map(|s| &s.tok) {
1512                match tok {
1513                    Token::LeftBrack => depth += 1,
1514                    Token::RightBrack => {
1515                        depth -= 1;
1516                        if depth == 0 {
1517                            i += 1;
1518                            break;
1519                        }
1520                    }
1521                    Token::Eof | Token::Newline if depth > 0 => return false,
1522                    _ => {}
1523                }
1524                i += 1;
1525            }
1526            if depth != 0 {
1527                return false;
1528            }
1529        }
1530        self.toks.get(i).is_some_and(|s| is_assign_op(&s.tok))
1531    }
1532
1533    fn parse_label(&mut self) -> PResult<Label> {
1534        let name = match self.bump() {
1535            Token::Label(s) => s,
1536            other => return self.err(format!("expected label, found {other:?}")),
1537        };
1538        let subscript = if self.eat(&Token::LeftBrack) {
1539            let e = self.parse_subscript()?;
1540            self.expect(&Token::RightBrack)?;
1541            Some(e)
1542        } else {
1543            None
1544        };
1545        Ok(Label { name, subscript })
1546    }
1547
1548    fn parse_assignlist(&mut self) -> PResult<Vec<Assignment>> {
1549        let mut list = vec![self.parse_assignment()?];
1550        while self.eat(&Token::Comma) {
1551            list.push(self.parse_assignment()?);
1552        }
1553        Ok(list)
1554    }
1555
1556    fn parse_assignment(&mut self) -> PResult<Assignment> {
1557        let target = match self.cur().clone() {
1558            Token::Name(name) | Token::Label(name) => {
1559                self.bump();
1560                let sub = if self.eat(&Token::LeftBrack) {
1561                    let e = self.parse_subscript()?;
1562                    self.expect(&Token::RightBrack)?;
1563                    Some(e)
1564                } else {
1565                    None
1566                };
1567                AssignTarget::Var(name, sub)
1568            }
1569            Token::EnvVar(v) => {
1570                self.bump();
1571                AssignTarget::Env(v)
1572            }
1573            other => return self.err(format!("expected assignment target, found {other:?}")),
1574        };
1575        let op = match self.bump() {
1576            Token::Eq => AssignOp::Set,
1577            Token::ColonEq => AssignOp::ColonSet,
1578            Token::PlusEq => AssignOp::Add,
1579            Token::MinusEq => AssignOp::Sub,
1580            Token::MultEq => AssignOp::Mul,
1581            Token::DivEq => AssignOp::Div,
1582            Token::RemEq => AssignOp::Rem,
1583            other => return self.err(format!("expected assignment operator, found {other:?}")),
1584        };
1585        let value = self.parse_expr()?;
1586        Ok(Assignment { target, op, value })
1587    }
1588
1589    fn parse_subscript(&mut self) -> PResult<Expr> {
1590        let mut items = vec![self.parse_expr()?];
1591        while self.eat(&Token::Comma) {
1592            items.push(self.parse_expr()?);
1593        }
1594        if items.len() == 1 {
1595            Ok(items.pop().unwrap())
1596        } else {
1597            Ok(Expr::Index(items))
1598        }
1599    }
1600
1601    // ---- objects & attributes ---------------------------------------------
1602
1603    fn parse_object(&mut self) -> PResult<Object> {
1604        let mut attrs = Vec::new();
1605        // a bare string expression (literal, `$arg`, sprintf, concatenation)
1606        // places a text-only object.
1607        if self.at_string_start() {
1608            attrs.push(Attr::Text(self.parse_stringexpr()?));
1609            while let Some(a) = self.parse_attr()? {
1610                attrs.push(a);
1611            }
1612            return Ok(Object {
1613                kind: ObjectKind::Text,
1614                attrs,
1615            });
1616        }
1617        let kind = match self.cur().clone() {
1618            Token::Prim(p) => {
1619                self.bump();
1620                ObjectKind::Primitive(p)
1621            }
1622            Token::Block => {
1623                self.bump();
1624                ObjectKind::Empty
1625            }
1626            Token::LeftBrack => {
1627                self.bump();
1628                let stmts = self.parse_elementlist(&[Token::RightBrack])?;
1629                self.expect(&Token::RightBrack)?;
1630                ObjectKind::Block(stmts)
1631            }
1632            Token::Str(s) => {
1633                self.bump();
1634                attrs.push(Attr::Text(self.continue_string(StringExpr::Lit(s))?));
1635                ObjectKind::Text
1636            }
1637            Token::Kw(Kw::Continue) => {
1638                self.bump();
1639                ObjectKind::Continue
1640            }
1641            other => return self.err(format!("expected an object, found {other:?}")),
1642        };
1643        // `spline <expr> <linespec>`: dpic's documented exception to the bare
1644        // distance rule — the expression right after `spline` is a tension
1645        // parameter, not a length. Parse it here so the attribute loop below
1646        // doesn't read it as `Attr::Dist`.
1647        if matches!(kind, ObjectKind::Primitive(Prim::Spline)) && self.spline_tension_ahead() {
1648            attrs.push(Attr::SplineTension(self.parse_expr()?));
1649        }
1650        while let Some(a) = self.parse_attr()? {
1651            attrs.push(a);
1652        }
1653        Ok(Object { kind, attrs })
1654    }
1655
1656    /// True if the next token begins a bare scalar expression — the leading
1657    /// tension argument of `spline <expr>` — rather than a linespec keyword
1658    /// (`from`/`to`/`up`/`then`/…) or another attribute.
1659    fn spline_tension_ahead(&self) -> bool {
1660        matches!(
1661            self.cur(),
1662            Token::Float(_)
1663                | Token::Lparen
1664                | Token::EnvVar(_)
1665                | Token::Func1(_)
1666                | Token::Func2(_)
1667                | Token::Name(_)
1668                | Token::Minus
1669                | Token::Plus
1670                | Token::Kw(Kw::Rand)
1671        )
1672    }
1673
1674    fn parse_attr(&mut self) -> PResult<Option<Attr>> {
1675        // any string expression (literal, sprintf, $arg, concatenation) is text
1676        if self.at_string_start() {
1677            return Ok(Some(Attr::Text(self.parse_stringexpr()?)));
1678        }
1679        let attr = match self.cur().clone() {
1680            Token::Kw(Kw::Ht) => {
1681                self.bump();
1682                Attr::Dim(DimKind::Ht, self.parse_expr()?)
1683            }
1684            Token::Kw(Kw::Wid) => {
1685                self.bump();
1686                Attr::Dim(DimKind::Wid, self.parse_expr()?)
1687            }
1688            Token::Kw(Kw::Rad) => {
1689                self.bump();
1690                Attr::Dim(DimKind::Rad, self.parse_expr()?)
1691            }
1692            Token::Kw(Kw::Diam) => {
1693                self.bump();
1694                Attr::Dim(DimKind::Diam, self.parse_expr()?)
1695            }
1696            Token::Kw(Kw::Thick) => {
1697                self.bump();
1698                Attr::Dim(DimKind::Thick, self.parse_expr()?)
1699            }
1700            Token::Kw(Kw::Scaled) => {
1701                self.bump();
1702                Attr::Dim(DimKind::Scaled, self.parse_expr()?)
1703            }
1704            Token::Dir(d) => {
1705                self.bump();
1706                Attr::Direction(d, self.opt_expr()?)
1707            }
1708            Token::LineType(lt) => {
1709                self.bump();
1710                Attr::LineStyle(lt, self.opt_expr()?)
1711            }
1712            Token::Kw(Kw::Chop) => {
1713                self.bump();
1714                Attr::Chop(self.opt_expr()?)
1715            }
1716            Token::Kw(Kw::Fill) => {
1717                self.bump();
1718                Attr::Fill(self.opt_expr()?)
1719            }
1720            Token::Arrow(a) => {
1721                self.bump();
1722                Attr::Arrowhead(a, self.opt_expr()?)
1723            }
1724            Token::Kw(Kw::Then) => {
1725                self.bump();
1726                Attr::Then
1727            }
1728            Token::Kw(Kw::Cw) => {
1729                self.bump();
1730                Attr::Cw
1731            }
1732            Token::Kw(Kw::Ccw) => {
1733                self.bump();
1734                Attr::Ccw
1735            }
1736            Token::Kw(Kw::Same) => {
1737                self.bump();
1738                Attr::Same
1739            }
1740            Token::Kw(Kw::Continue) => {
1741                self.bump();
1742                Attr::Continue
1743            }
1744            Token::Kw(Kw::From) => {
1745                self.bump();
1746                Attr::From(self.parse_position()?)
1747            }
1748            Token::Kw(Kw::To) => {
1749                self.bump();
1750                Attr::To(self.parse_position()?)
1751            }
1752            Token::Kw(Kw::At) => {
1753                self.bump();
1754                Attr::At(self.parse_position()?)
1755            }
1756            Token::Kw(Kw::By) => {
1757                self.bump();
1758                Attr::By(self.parse_position()?)
1759            }
1760            Token::Kw(Kw::With) => {
1761                self.bump();
1762                let anchor = if self.eat(&Token::Dot) {
1763                    WithAnchor::Place(self.parse_place()?)
1764                } else if let Token::Corner(c) = self.cur() {
1765                    let c = *c;
1766                    self.bump();
1767                    WithAnchor::Corner(c)
1768                } else if self.at(&Token::Lparen) {
1769                    self.bump();
1770                    let x = self.parse_expr()?;
1771                    self.expect(&Token::Comma)?;
1772                    let y = self.parse_expr()?;
1773                    self.expect(&Token::Rparen)?;
1774                    WithAnchor::Pair(x, y)
1775                } else {
1776                    WithAnchor::Plain
1777                };
1778                self.expect_kw(Kw::At)?;
1779                Attr::With {
1780                    anchor,
1781                    at: self.parse_position()?,
1782                }
1783            }
1784            Token::TextPos(tp) => {
1785                self.bump();
1786                Attr::TextPos(tp)
1787            }
1788            Token::Color(c) => {
1789                self.bump();
1790                // a colour may be a quoted string or a bareword name (e.g.
1791                // `shaded Custom`, `outlined red`).
1792                let s = match self.cur().clone() {
1793                    Token::Name(n) | Token::Label(n) => {
1794                        self.bump();
1795                        StringExpr::Lit(n)
1796                    }
1797                    _ => self.parse_stringexpr()?,
1798                };
1799                Attr::Color(c, s)
1800            }
1801            // a bare expression distance with no direction word, e.g. `move 1`,
1802            // `move -0.1`, `spline x` (length in the prevailing direction)
1803            Token::Float(_)
1804            | Token::Lparen
1805            | Token::EnvVar(_)
1806            | Token::Func1(_)
1807            | Token::Func2(_)
1808            | Token::Name(_)
1809            | Token::Minus
1810            | Token::Plus
1811            | Token::Kw(Kw::Rand) => Attr::Dist(self.parse_expr()?),
1812            _ if self.place_is_scalar_ahead() => Attr::Dist(self.parse_expr()?),
1813            _ => return Ok(None),
1814        };
1815        Ok(Some(attr))
1816    }
1817
1818    fn expect_kw(&mut self, k: Kw) -> PResult<()> {
1819        if self.eat_kw(k) {
1820            Ok(())
1821        } else {
1822            self.err(format!("expected `{k:?}`, found {:?}", self.cur()))
1823        }
1824    }
1825
1826    // ---- string expressions ------------------------------------------------
1827
1828    fn parse_stringexpr(&mut self) -> PResult<StringExpr> {
1829        let first = self.parse_string_atom()?;
1830        self.continue_string(first)
1831    }
1832
1833    /// Continue a string expression with trailing `+ string` parts.
1834    fn continue_string(&mut self, first: StringExpr) -> PResult<StringExpr> {
1835        let mut e = first;
1836        while self.at(&Token::Plus) && self.string_after_plus() {
1837            self.bump();
1838            let rhs = self.parse_string_atom()?;
1839            e = StringExpr::Concat(Box::new(e), Box::new(rhs));
1840        }
1841        Ok(e)
1842    }
1843
1844    fn string_after_plus(&self) -> bool {
1845        self.token_starts_string_at(1)
1846    }
1847
1848    fn token_starts_string_at(&self, offset: usize) -> bool {
1849        match self.toks.get(self.idx + offset).map(|s| &s.tok) {
1850            Some(Token::Str(_) | Token::Arg(_) | Token::Kw(Kw::Sprintf)) => true,
1851            Some(Token::Name(n)) if n == "svg_font" => {
1852                matches!(
1853                    self.toks.get(self.idx + offset + 1).map(|s| &s.tok),
1854                    Some(Token::Lparen)
1855                )
1856            }
1857            _ => false,
1858        }
1859    }
1860
1861    fn parse_string_atom(&mut self) -> PResult<StringExpr> {
1862        match self.cur().clone() {
1863            Token::Str(s) => {
1864                self.bump();
1865                Ok(StringExpr::Lit(s))
1866            }
1867            Token::Arg(n) => {
1868                self.bump();
1869                Ok(StringExpr::Arg(n))
1870            }
1871            Token::Kw(Kw::Sprintf) => {
1872                self.bump();
1873                self.expect(&Token::Lparen)?;
1874                let fmt = self.parse_stringexpr()?;
1875                let mut args = Vec::new();
1876                while self.eat(&Token::Comma) {
1877                    args.push(self.parse_expr()?);
1878                }
1879                self.expect(&Token::Rparen)?;
1880                Ok(StringExpr::Sprintf(Box::new(fmt), args))
1881            }
1882            Token::Name(n) if n == "svg_font" && matches!(self.peek(1), Token::Lparen) => {
1883                self.bump();
1884                self.expect(&Token::Lparen)?;
1885                let mut args = Vec::new();
1886                if !self.at(&Token::Rparen) {
1887                    args.push(self.parse_expr()?);
1888                    while self.eat(&Token::Comma) {
1889                        args.push(self.parse_expr()?);
1890                    }
1891                }
1892                self.expect(&Token::Rparen)?;
1893                Ok(StringExpr::SvgFont(args))
1894            }
1895            other => self.err(format!("expected a string, found {other:?}")),
1896        }
1897    }
1898
1899    // ---- positions ---------------------------------------------------------
1900
1901    fn parse_label_position(&mut self) -> PResult<Position> {
1902        let save = self.idx;
1903        if let Ok(x) = self.parse_expr()
1904            && self.eat(&Token::Comma)
1905        {
1906            let y = self.parse_expr()?;
1907            return Ok(Position::Pair(x, y));
1908        }
1909        self.idx = save;
1910        self.parse_position()
1911    }
1912
1913    /// Positions support vector arithmetic; `+`/`-` are the lowest precedence.
1914    fn parse_position(&mut self) -> PResult<Position> {
1915        let mut left = self.parse_pos_mul()?;
1916        loop {
1917            let sign = if self.at(&Token::Plus) {
1918                Sign::Plus
1919            } else if self.at(&Token::Minus) {
1920                Sign::Minus
1921            } else {
1922                break;
1923            };
1924            self.bump();
1925            let right = self.parse_pos_mul()?;
1926            left = Position::Sum(sign, Box::new(left), Box::new(right));
1927        }
1928        Ok(left)
1929    }
1930
1931    /// Scaling a position by a scalar: `p * s`, `p / s` (binds tighter than ±).
1932    fn parse_pos_mul(&mut self) -> PResult<Position> {
1933        let mut left = self.parse_pos_primary()?;
1934        loop {
1935            if self.eat(&Token::Mult) {
1936                left = Position::Scale(Box::new(left), self.parse_unary()?, false);
1937            } else if self.eat(&Token::Div) {
1938                left = Position::Scale(Box::new(left), self.parse_unary()?, true);
1939            } else {
1940                break;
1941            }
1942        }
1943        Ok(left)
1944    }
1945
1946    fn parse_pos_primary(&mut self) -> PResult<Position> {
1947        // A fraction-led interpolation — `frac between A and B`, `frac <p,p>`, or
1948        // `frac of the way between A and B`. The fraction can be parenthesised
1949        // (e.g. `(X/Y) between A and B`), so try it before the `(`/place branches.
1950        if let Some(p) = self.try_fraction()? {
1951            return Ok(p);
1952        }
1953        // `( … )`: coordinate pair `(x,y)` of scalar expressions, a
1954        // parenthesised position, or `(pos, pos)` — x of the first, y of the
1955        // second. Try a scalar pair first so components that are themselves
1956        // parenthesised scalars (e.g. `((a*g)*cos(t), …)`) parse correctly.
1957        if self.eat(&Token::Lparen) {
1958            let save = self.idx;
1959            // Prefer parsing the contents as position(s) — handles `(A, B.c)`,
1960            // `(pos, pos)`, `(2,3)`, `(0.5 between A and B)`. If that fails, the
1961            // contents are scalar coordinate expressions that the position
1962            // grammar can't represent alone (e.g. `((a*g)*cos t, (a*g)*sin t)`).
1963            if let Ok(p1) = self.parse_position() {
1964                let p = if self.eat(&Token::Comma) {
1965                    let p2 = self.parse_position()?;
1966                    Position::Place(Location::ParenPair(Box::new(p1), Box::new(p2)))
1967                } else {
1968                    p1 // drop the redundant parentheses
1969                };
1970                self.expect(&Token::Rparen)?;
1971                return Ok(p);
1972            }
1973            self.idx = save;
1974            let e1 = self.parse_add()?;
1975            self.expect(&Token::Comma)?;
1976            let e2 = self.parse_add()?;
1977            self.expect(&Token::Rparen)?;
1978            return Ok(Position::Pair(e1, e2));
1979        }
1980        // A leading place is point-valued UNLESS it is a scalar accessor
1981        // (`place.x` / `.y` / `.attr`), which begins an `(expr, expr)` pair.
1982        if self.at_place_start() && !self.place_is_scalar_ahead() {
1983            return Ok(Position::Place(Location::Place(self.parse_place()?)));
1984        }
1985        // expression-led coordinate pair `x, y` (interpolation handled above)
1986        let e1 = self.parse_add()?;
1987        if self.eat(&Token::Comma) {
1988            let e2 = self.parse_add()?;
1989            return Ok(Position::Pair(e1, e2));
1990        }
1991        self.err("expected `,`, `between`, or `of the way between` in position")
1992    }
1993
1994    /// Try to parse `frac (between | <p,p> | of the way between)`; if the leading
1995    /// expression isn't followed by an interpolation, backtrack and return `None`
1996    /// so the caller can parse a plain place / pair / parenthesised position.
1997    fn try_fraction(&mut self) -> PResult<Option<Position>> {
1998        let save = self.idx;
1999        let Ok(frac) = self.parse_add() else {
2000            self.idx = save;
2001            return Ok(None);
2002        };
2003        let mk = |frac, a, b, of_the_way| {
2004            Ok(Some(Position::Between {
2005                frac: Box::new(frac),
2006                a: Box::new(a),
2007                b: Box::new(b),
2008                of_the_way,
2009            }))
2010        };
2011        if self.eat(&Token::Lt) {
2012            let a = self.parse_position()?;
2013            self.expect(&Token::Comma)?;
2014            let b = self.parse_position()?;
2015            self.expect(&Token::Gt)?;
2016            return mk(frac, a, b, false);
2017        }
2018        let of_the_way = if self.at_kw(Kw::Of) {
2019            self.eat_kw(Kw::Of);
2020            if !(self.eat_kw(Kw::The) && self.eat_kw(Kw::Way) && self.eat_kw(Kw::Between)) {
2021                self.idx = save;
2022                return Ok(None);
2023            }
2024            true
2025        } else if self.eat_kw(Kw::Between) {
2026            false
2027        } else {
2028            self.idx = save;
2029            return Ok(None);
2030        };
2031        let a = self.parse_position()?;
2032        self.expect_kw(Kw::And)?;
2033        let b = self.parse_position()?;
2034        mk(frac, a, b, of_the_way)
2035    }
2036
2037    /// Lookahead: does the upcoming place end in a scalar accessor
2038    /// (`.x` / `.y` / `.attr`)? If so it is a number, not a point. Non-consuming.
2039    fn place_is_scalar_ahead(&mut self) -> bool {
2040        let save = self.idx;
2041        let parsed = self.parse_place().is_ok();
2042        let scalar = parsed && matches!(self.cur(), Token::DotX | Token::DotY | Token::Param(_));
2043        self.idx = save;
2044        scalar
2045    }
2046
2047    fn at_place_start(&self) -> bool {
2048        match self.cur() {
2049            Token::Label(_) | Token::Block | Token::Corner(_) => true,
2050            Token::Kw(Kw::Last) | Token::Kw(Kw::Here) => true,
2051            Token::Float(_) => matches!(self.peek(1), Token::Kw(Kw::Nth)),
2052            // `{expr}th …` / `` `expr`th … `` ordinal counts (only valid as a
2053            // place in position/expression context, never a group here)
2054            Token::LeftBrace | Token::LeftQuote => true,
2055            _ => false,
2056        }
2057    }
2058
2059    fn parse_place(&mut self) -> PResult<Place> {
2060        // `corner [of] placename`
2061        if let Token::Corner(c) = self.cur() {
2062            let c = *c;
2063            self.bump();
2064            self.eat_kw(Kw::Of);
2065            let inner = self.parse_place()?;
2066            return Ok(Place::CornerOf(c, Box::new(inner)));
2067        }
2068
2069        let mut place = self.parse_place_base()?;
2070
2071        // trailing `.corner`, `.label`, `.nth primobj`
2072        loop {
2073            if let Token::Corner(c) = self.cur() {
2074                let c = *c;
2075                self.bump();
2076                place = Place::Corner(Box::new(place), c);
2077            } else if self.at(&Token::Dot) {
2078                self.bump();
2079                let rhs = self.parse_place_base()?;
2080                place = Place::Member(Box::new(place), Box::new(rhs));
2081            } else {
2082                break;
2083            }
2084        }
2085        Ok(place)
2086    }
2087
2088    fn parse_place_base(&mut self) -> PResult<Place> {
2089        match self.cur().clone() {
2090            Token::Kw(Kw::Here) => {
2091                self.bump();
2092                Ok(Place::Here)
2093            }
2094            Token::Label(name) => {
2095                self.bump();
2096                let subscript = if self.eat(&Token::LeftBrack) {
2097                    let e = self.parse_subscript()?;
2098                    self.expect(&Token::RightBrack)?;
2099                    Some(Box::new(e))
2100                } else {
2101                    None
2102                };
2103                Ok(Place::Name { name, subscript })
2104            }
2105            Token::Kw(Kw::Last) | Token::Float(_) | Token::LeftBrace | Token::LeftQuote => {
2106                let count = self.parse_nth()?;
2107                // A type keyword may follow (`last box`); without one, this is an
2108                // untyped reference to the most recent object of any kind
2109                // (`last`, `last.c`, `2nd last.n`).
2110                let obj = if self.at_primobj() {
2111                    self.parse_primobj()?
2112                } else {
2113                    PrimObj::Any
2114                };
2115                Ok(Place::Nth { count, obj })
2116            }
2117            other => self.err(format!("expected a place, found {other:?}")),
2118        }
2119    }
2120
2121    fn parse_nth(&mut self) -> PResult<Nth> {
2122        if self.eat_kw(Kw::Last) {
2123            return Ok(Nth::Last);
2124        }
2125        // ncount ordinal [last]
2126        let e = self.parse_ncount()?;
2127        self.expect_kw(Kw::Nth)?;
2128        let from_last = self.eat_kw(Kw::Last);
2129        Ok(Nth::Count(Box::new(e), from_last))
2130    }
2131
2132    /// An ordinal count: a number, `` `expr' ``, or `{ expr }` (grammar:
2133    /// `ncount`). Must NOT recurse into the general expression grammar on a
2134    /// bare number, or `2nd` would re-enter place parsing.
2135    fn parse_ncount(&mut self) -> PResult<Expr> {
2136        match self.cur().clone() {
2137            Token::Float(v) => {
2138                self.bump();
2139                Ok(Expr::Num(v))
2140            }
2141            Token::LeftBrace => {
2142                self.bump();
2143                let e = self.parse_expr()?;
2144                self.expect(&Token::RightBrace)?;
2145                Ok(e)
2146            }
2147            Token::LeftQuote => {
2148                self.bump();
2149                let e = self.parse_expr()?;
2150                self.expect(&Token::RightQuote)?;
2151                Ok(e)
2152            }
2153            other => self.err(format!("expected an ordinal count, found {other:?}")),
2154        }
2155    }
2156
2157    /// Whether the current token can begin a primitive-object type keyword
2158    /// (`box`, `[`, a string, …) — i.e. an explicit type after `last`/ordinal.
2159    fn at_primobj(&self) -> bool {
2160        matches!(
2161            self.cur(),
2162            Token::Prim(_) | Token::Block | Token::Str(_) | Token::LeftBrack
2163        )
2164    }
2165
2166    fn parse_primobj(&mut self) -> PResult<PrimObj> {
2167        match self.cur().clone() {
2168            Token::Prim(p) => {
2169                self.bump();
2170                Ok(PrimObj::Prim(p))
2171            }
2172            Token::Block => {
2173                self.bump();
2174                Ok(PrimObj::Block)
2175            }
2176            Token::Str(s) => {
2177                self.bump();
2178                Ok(PrimObj::Str(s))
2179            }
2180            Token::LeftBrack => {
2181                self.bump();
2182                self.expect(&Token::RightBrack)?;
2183                Ok(PrimObj::EmptyBrack)
2184            }
2185            other => self.err(format!("expected a primitive object, found {other:?}")),
2186        }
2187    }
2188
2189    // ---- expressions -------------------------------------------------------
2190
2191    fn opt_expr(&mut self) -> PResult<Option<Expr>> {
2192        if self.starts_scalar() || self.place_is_scalar_ahead() {
2193            Ok(Some(self.parse_expr()?))
2194        } else {
2195            Ok(None)
2196        }
2197    }
2198
2199    fn starts_scalar(&self) -> bool {
2200        matches!(
2201            self.cur(),
2202            Token::Float(_)
2203                | Token::Name(_)
2204                | Token::EnvVar(_)
2205                | Token::Lparen
2206                | Token::Minus
2207                | Token::Plus
2208                | Token::Not
2209                | Token::Func1(_)
2210                | Token::Func2(_)
2211                | Token::Kw(Kw::Rand)
2212                | Token::ArgCount
2213        )
2214    }
2215
2216    fn parse_expr(&mut self) -> PResult<Expr> {
2217        self.parse_or()
2218    }
2219
2220    fn parse_or(&mut self) -> PResult<Expr> {
2221        let mut e = self.parse_and()?;
2222        while self.eat(&Token::OrOr) {
2223            let r = self.parse_and()?;
2224            e = Expr::Bin(BinOp::Or, Box::new(e), Box::new(r));
2225        }
2226        Ok(e)
2227    }
2228
2229    fn parse_and(&mut self) -> PResult<Expr> {
2230        let mut e = self.parse_cmp()?;
2231        while self.eat(&Token::AndAnd) {
2232            let r = self.parse_cmp()?;
2233            e = Expr::Bin(BinOp::And, Box::new(e), Box::new(r));
2234        }
2235        Ok(e)
2236    }
2237
2238    fn parse_cmp(&mut self) -> PResult<Expr> {
2239        let mut e = self.parse_add()?;
2240        loop {
2241            let op = match self.cur() {
2242                Token::EqEq => BinOp::Eq,
2243                Token::Neq => BinOp::Ne,
2244                Token::Lt => BinOp::Lt,
2245                Token::Le => BinOp::Le,
2246                Token::Gt => BinOp::Gt,
2247                Token::Ge => BinOp::Ge,
2248                _ => break,
2249            };
2250            self.bump();
2251            let r = self.parse_add()?;
2252            e = Expr::Bin(op, Box::new(e), Box::new(r));
2253        }
2254        Ok(e)
2255    }
2256
2257    fn parse_add(&mut self) -> PResult<Expr> {
2258        let mut e = self.parse_mul()?;
2259        loop {
2260            let op = match self.cur() {
2261                Token::Plus => BinOp::Add,
2262                Token::Minus => BinOp::Sub,
2263                _ => break,
2264            };
2265            self.bump();
2266            let r = self.parse_mul()?;
2267            e = Expr::Bin(op, Box::new(e), Box::new(r));
2268        }
2269        Ok(e)
2270    }
2271
2272    fn parse_mul(&mut self) -> PResult<Expr> {
2273        let mut e = self.parse_unary()?;
2274        loop {
2275            let op = match self.cur() {
2276                Token::Mult => BinOp::Mul,
2277                Token::Div => BinOp::Div,
2278                Token::Percent => BinOp::Mod,
2279                _ => break,
2280            };
2281            self.bump();
2282            let r = self.parse_unary()?;
2283            e = Expr::Bin(op, Box::new(e), Box::new(r));
2284        }
2285        Ok(e)
2286    }
2287
2288    fn parse_unary(&mut self) -> PResult<Expr> {
2289        let op = match self.cur() {
2290            Token::Minus => Some(UnOp::Neg),
2291            Token::Plus => Some(UnOp::Pos),
2292            Token::Not => Some(UnOp::Not),
2293            _ => None,
2294        };
2295        if let Some(op) = op {
2296            self.bump();
2297            let e = self.parse_unary()?;
2298            Ok(Expr::Unary(op, Box::new(e)))
2299        } else {
2300            self.parse_pow()
2301        }
2302    }
2303
2304    fn parse_pow(&mut self) -> PResult<Expr> {
2305        let base = self.parse_primary()?;
2306        if self.eat(&Token::Caret) {
2307            let exp = self.parse_unary()?; // right-associative
2308            Ok(Expr::Bin(BinOp::Pow, Box::new(base), Box::new(exp)))
2309        } else {
2310            Ok(base)
2311        }
2312    }
2313
2314    /// Lookahead from just inside a `(`: is the matching `)` immediately followed
2315    /// by `.x` or `.y`? (Used to read `( position ).x` as a coordinate.)
2316    fn paren_followed_by_dot_xy(&self) -> bool {
2317        let mut depth = 1i32;
2318        let mut i = self.idx;
2319        while let Some(s) = self.toks.get(i) {
2320            match &s.tok {
2321                Token::Lparen => depth += 1,
2322                Token::Rparen => {
2323                    depth -= 1;
2324                    if depth == 0 {
2325                        return matches!(
2326                            self.toks.get(i + 1).map(|t| &t.tok),
2327                            Some(Token::DotX | Token::DotY)
2328                        );
2329                    }
2330                }
2331                Token::Eof => return false,
2332                _ => {}
2333            }
2334            i += 1;
2335        }
2336        false
2337    }
2338
2339    fn parse_primary(&mut self) -> PResult<Expr> {
2340        // place-derived scalars: location.x / location.y / place.attr
2341        if self.at_place_start() && self.place_is_scalar_ahead() {
2342            return self.parse_place_scalar();
2343        }
2344        // a string operand (only meaningful as an `==`/`!=` operand)
2345        if self.at_string_start() {
2346            return Ok(Expr::Str(self.parse_stringexpr()?));
2347        }
2348        match self.cur().clone() {
2349            Token::Float(v) => {
2350                self.bump();
2351                Ok(Expr::Num(v))
2352            }
2353            Token::Name(name) | Token::Label(name) => {
2354                self.bump();
2355                let subscript = if self.eat(&Token::LeftBrack) {
2356                    let e = self.parse_subscript()?;
2357                    self.expect(&Token::RightBrack)?;
2358                    Some(Box::new(e))
2359                } else {
2360                    None
2361                };
2362                Ok(Expr::Var(name, subscript))
2363            }
2364            Token::EnvVar(v) => {
2365                self.bump();
2366                Ok(Expr::Env(v))
2367            }
2368            Token::Lparen => {
2369                self.bump();
2370                // embedded assignment `( name = expr )` yields the assigned value
2371                if matches!(self.cur(), Token::Name(_) | Token::Label(_))
2372                    && self.assignment_op_after_var_ref()
2373                {
2374                    let name = match self.bump() {
2375                        Token::Name(n) | Token::Label(n) => n,
2376                        _ => unreachable!(),
2377                    };
2378                    let subscript = if self.eat(&Token::LeftBrack) {
2379                        let e = self.parse_subscript()?;
2380                        self.expect(&Token::RightBrack)?;
2381                        Some(Box::new(e))
2382                    } else {
2383                        None
2384                    };
2385                    self.bump(); // `=`
2386                    let v = self.parse_expr()?;
2387                    self.expect(&Token::Rparen)?;
2388                    return Ok(Expr::Assign(name, subscript, Box::new(v)));
2389                }
2390                // `( position ).x` / `.y` — a coordinate of a parenthesised
2391                // position (e.g. `(A - B).x`, `($1-($2)).y`). Chosen by lookahead
2392                // for a trailing `.x`/`.y`, since `(A - B)` alone parses as scalar
2393                // (labels read as variables).
2394                if self.paren_followed_by_dot_xy() {
2395                    let pos = self.parse_position()?;
2396                    self.expect(&Token::Rparen)?;
2397                    let loc = Location::Paren(Box::new(pos));
2398                    return Ok(if self.eat(&Token::DotX) {
2399                        Expr::DotX(loc)
2400                    } else {
2401                        self.expect(&Token::DotY)?;
2402                        Expr::DotY(loc)
2403                    });
2404                }
2405                // a plain scalar group `( expr )`
2406                let e = self.parse_expr()?;
2407                self.expect(&Token::Rparen)?;
2408                Ok(e)
2409            }
2410            // `$+` outside any macro invocation: zero arguments
2411            Token::ArgCount => {
2412                self.bump();
2413                Ok(Expr::Num(0.0))
2414            }
2415            Token::Func1(f) => {
2416                self.bump();
2417                self.expect(&Token::Lparen)?;
2418                let e = self.parse_expr()?;
2419                self.expect(&Token::Rparen)?;
2420                Ok(Expr::Func1(f, Box::new(e)))
2421            }
2422            Token::Func2(f) => {
2423                self.bump();
2424                self.expect(&Token::Lparen)?;
2425                let a = self.parse_expr()?;
2426                self.expect(&Token::Comma)?;
2427                let b = self.parse_expr()?;
2428                self.expect(&Token::Rparen)?;
2429                Ok(Expr::Func2(f, Box::new(a), Box::new(b)))
2430            }
2431            Token::Kw(Kw::Rand) => {
2432                self.bump();
2433                self.expect(&Token::Lparen)?;
2434                let arg = if self.at(&Token::Rparen) {
2435                    None
2436                } else {
2437                    Some(Box::new(self.parse_expr()?))
2438                };
2439                self.expect(&Token::Rparen)?;
2440                Ok(Expr::Rand(arg))
2441            }
2442            other => self.err(format!("expected an expression, found {other:?}")),
2443        }
2444    }
2445
2446    /// Parse a place followed by `.x` / `.y` / `.attr` to yield a scalar.
2447    fn parse_place_scalar(&mut self) -> PResult<Expr> {
2448        let place = self.parse_place()?;
2449        match self.cur().clone() {
2450            Token::DotX => {
2451                self.bump();
2452                Ok(Expr::DotX(Location::Place(place)))
2453            }
2454            Token::DotY => {
2455                self.bump();
2456                Ok(Expr::DotY(Location::Place(place)))
2457            }
2458            Token::Param(p) => {
2459                self.bump();
2460                Ok(Expr::PlaceAttr(place, p))
2461            }
2462            other => self.err(format!(
2463                "a place is not a number here; expected `.x`, `.y`, or an attribute, found {other:?}"
2464            )),
2465        }
2466    }
2467}
2468
2469#[cfg(test)]
2470mod tests {
2471    use super::*;
2472
2473    fn pic(src: &str) -> Picture {
2474        parse(src).unwrap_or_else(|e| panic!("parse error: {e}"))
2475    }
2476
2477    #[test]
2478    fn kernighan_pipeline() {
2479        let p = pic(r#".PS
2480ellipse "document"
2481arrow
2482box "PIC"
2483arrow
2484box "TBL/EQN" "(optional)" dashed
2485arrow
2486box "TROFF"
2487arrow
2488ellipse "typesetter"
2489.PE
2490"#);
2491        assert_eq!(p.stmts.len(), 9);
2492        // the dashed box with two strings
2493        if let Stmt::Object { object, .. } = &p.stmts[4] {
2494            assert_eq!(object.kind, ObjectKind::Primitive(Prim::Box));
2495            let texts = object
2496                .attrs
2497                .iter()
2498                .filter(|a| matches!(a, Attr::Text(_)))
2499                .count();
2500            assert_eq!(texts, 2);
2501            assert!(
2502                object
2503                    .attrs
2504                    .iter()
2505                    .any(|a| matches!(a, Attr::LineStyle(LineType::Dashed, _)))
2506            );
2507        } else {
2508            panic!("expected object");
2509        }
2510    }
2511
2512    #[test]
2513    fn box_with_dims_and_at() {
2514        let p = pic("box ht 0.3 wid 0.5 at 0.25,0.15");
2515        let Stmt::Object { object, .. } = &p.stmts[0] else {
2516            panic!()
2517        };
2518        assert!(matches!(object.attrs[0], Attr::Dim(DimKind::Ht, _)));
2519        assert!(matches!(object.attrs[1], Attr::Dim(DimKind::Wid, _)));
2520        assert!(matches!(object.attrs[2], Attr::At(Position::Pair(_, _))));
2521    }
2522
2523    #[test]
2524    fn labeled_and_corners() {
2525        let p = pic("B1: box\narc -> from top of B1 to last box.ne");
2526        assert!(matches!(p.stmts[0], Stmt::Object { label: Some(_), .. }));
2527        let Stmt::Object { object, .. } = &p.stmts[1] else {
2528            panic!()
2529        };
2530        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Arc));
2531        assert!(
2532            object
2533                .attrs
2534                .iter()
2535                .any(|a| matches!(a, Attr::Arrowhead(Arrow::Right, _)))
2536        );
2537        // from top of B1
2538        assert!(object.attrs.iter().any(|a| matches!(
2539            a,
2540            Attr::From(Position::Place(Location::Place(Place::CornerOf(
2541                Corner::N,
2542                _
2543            ))))
2544        )));
2545    }
2546
2547    #[test]
2548    fn with_at_and_shift() {
2549        let p = pic("ellipse \"2\" with .nw at last ellipse.se + (0.1,0)");
2550        let Stmt::Object { object, .. } = &p.stmts[0] else {
2551            panic!()
2552        };
2553        let with = object
2554            .attrs
2555            .iter()
2556            .find(|a| matches!(a, Attr::With { .. }))
2557            .unwrap();
2558        let Attr::With { anchor, at } = with else {
2559            panic!()
2560        };
2561        assert_eq!(*anchor, WithAnchor::Corner(Corner::Nw));
2562        // `last ellipse.se + (0.1,0)` is a position sum
2563        assert!(matches!(at, Position::Sum(Sign::Plus, _, _)));
2564    }
2565
2566    #[test]
2567    fn with_member_anchor_parses() {
2568        let p = pic("[ A: box ] with .A.c at Here");
2569        let Stmt::Object { object, .. } = &p.stmts[0] else {
2570            panic!()
2571        };
2572        let with = object
2573            .attrs
2574            .iter()
2575            .find(|a| matches!(a, Attr::With { .. }))
2576            .unwrap();
2577        let Attr::With { anchor, .. } = with else {
2578            panic!()
2579        };
2580        assert!(matches!(
2581            anchor,
2582            WithAnchor::Place(Place::Corner(inner, Corner::Center))
2583                if matches!(inner.as_ref(), Place::Name { name, .. } if name == "A")
2584        ));
2585    }
2586
2587    #[test]
2588    fn expression_precedence() {
2589        // 2 + 3 * 4 ^ 2  ==  2 + (3 * (4^2)) = 50
2590        let p = pic("x = 2 + 3 * 4 ^ 2");
2591        let Stmt::Assign(list) = &p.stmts[0] else {
2592            panic!()
2593        };
2594        // structure: Add(2, Mul(3, Pow(4,2)))
2595        let Expr::Bin(BinOp::Add, _, rhs) = &list[0].value else {
2596            panic!("expected top-level add")
2597        };
2598        assert!(matches!(**rhs, Expr::Bin(BinOp::Mul, _, _)));
2599    }
2600
2601    #[test]
2602    fn between_position() {
2603        let p = pic("arrow from 1/3 of the way between A.ne and A.se");
2604        let Stmt::Object { object, .. } = &p.stmts[0] else {
2605            panic!()
2606        };
2607        assert!(object.attrs.iter().any(|a| matches!(
2608            a,
2609            Attr::From(Position::Between {
2610                of_the_way: true,
2611                ..
2612            })
2613        )));
2614    }
2615
2616    #[test]
2617    fn assignment_list_and_envvar() {
2618        let p = pic("boxht = 0.3; boxwid = 2 * boxht");
2619        assert_eq!(p.stmts.len(), 2);
2620        let Stmt::Assign(a0) = &p.stmts[0] else {
2621            panic!()
2622        };
2623        assert_eq!(a0[0].target, AssignTarget::Env(EnvVar::Boxht));
2624    }
2625
2626    #[test]
2627    fn dpic_svg_font_stub_parses_as_string() {
2628        let p = pic("print svg_font(\"Times\", 12)");
2629        let Stmt::Print(PrintItem::Str(StringExpr::SvgFont(args))) = &p.stmts[0] else {
2630            panic!()
2631        };
2632        assert_eq!(args.len(), 2);
2633    }
2634
2635    #[test]
2636    fn subscripted_variable_refs_parse() {
2637        let p = pic("P[1] = 2\nx = P[1]");
2638        let Stmt::Assign(a0) = &p.stmts[0] else {
2639            panic!()
2640        };
2641        assert!(matches!(&a0[0].target, AssignTarget::Var(name, Some(_)) if name == "P"));
2642
2643        let Stmt::Assign(a1) = &p.stmts[1] else {
2644            panic!()
2645        };
2646        assert!(matches!(&a1[0].value, Expr::Var(name, Some(_)) if name == "P"));
2647    }
2648
2649    #[test]
2650    fn block_object() {
2651        let p = pic("[ box; circle ] with .nw at Here");
2652        let Stmt::Object { object, .. } = &p.stmts[0] else {
2653            panic!()
2654        };
2655        let ObjectKind::Block(inner) = &object.kind else {
2656            panic!()
2657        };
2658        assert_eq!(inner.len(), 2);
2659    }
2660
2661    #[test]
2662    fn diamond_line_with_then() {
2663        let p = pic("line up right then down right then down left then up left");
2664        let Stmt::Object { object, .. } = &p.stmts[0] else {
2665            panic!()
2666        };
2667        let thens = object
2668            .attrs
2669            .iter()
2670            .filter(|a| matches!(a, Attr::Then))
2671            .count();
2672        assert_eq!(thens, 3);
2673    }
2674
2675    #[test]
2676    fn place_scalar_in_coord_pair() {
2677        // issue #3: (A.x, expr) must parse as an (expr,expr) pair, not a place
2678        let p = pic("A: box\n\"t\" at (A.x, A.y - 0.5)");
2679        let Stmt::Object { object, .. } = &p.stmts[1] else {
2680            panic!()
2681        };
2682        assert!(
2683            object
2684                .attrs
2685                .iter()
2686                .any(|a| matches!(a, Attr::At(Position::Pair(_, _))))
2687        );
2688        // a plain point place still parses as a place position
2689        let q = pic("A: box\nbox at A.ne");
2690        let Stmt::Object { object, .. } = &q.stmts[1] else {
2691            panic!()
2692        };
2693        assert!(object.attrs.iter().any(|a| matches!(
2694            a,
2695            Attr::At(Position::Place(Location::Place(Place::Corner(_, _))))
2696        )));
2697    }
2698
2699    #[test]
2700    fn ignores_non_svg_backend_preambles() {
2701        let p = pic(r#".PS
2702verbatimtex
2703\global\def\foo#1{#1}
2704etex
2705\global\def\bar#1{#1}
2706\psset{arrowsize=4pt}
2707box
2708.PE
2709"#);
2710        assert_eq!(p.stmts.len(), 1);
2711        let Stmt::Object { object, .. } = &p.stmts[0] else {
2712            panic!()
2713        };
2714        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Box));
2715    }
2716
2717    #[test]
2718    fn backend_filter_keeps_global_lines_inside_strings() {
2719        let p = pic(
2720            "sh \"echo -n \\\"print \\\\\"\\\" > x\"\nif dpicopt==optPGF then { command \"cycle; \\\n\\global\\let\\dpicdraw=x\" } else { box }",
2721        );
2722        assert_eq!(p.stmts.len(), 2);
2723        assert!(matches!(p.stmts[1], Stmt::If { .. }));
2724    }
2725
2726    #[test]
2727    fn static_if_copy_defines_macros_before_following_statements() {
2728        let dir = std::env::temp_dir().join(format!("rpic_static_if_{}", std::process::id()));
2729        std::fs::create_dir_all(&dir).unwrap();
2730        std::fs::write(dir.join("macros.pic"), "define makebox { box wid $1 }\n").unwrap();
2731        std::fs::write(
2732            dir.join("inc.pic"),
2733            "define makecircle { circle rad 0.1 }\n",
2734        )
2735        .unwrap();
2736        let p = parse_in_dir(
2737            "if \"plotlib\" != \"1\" then { copy \"macros.pic\" }\ndefine choose { if \"$1\"==\"\" then { box } else { copy \"$1/inc.pic\" } }\nchoose(.)\nmakecircle()\nmakebox(0.4)",
2738            Some(dir.as_path()),
2739        )
2740        .unwrap_or_else(|e| panic!("parse error: {e}"));
2741        let _ = std::fs::remove_dir_all(&dir);
2742        assert_eq!(p.stmts.len(), 2);
2743        let Stmt::Object { object, .. } = &p.stmts[0] else {
2744            panic!()
2745        };
2746        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Circle));
2747        let Stmt::Object { object, .. } = &p.stmts[1] else {
2748            panic!()
2749        };
2750        assert_eq!(object.kind, ObjectKind::Primitive(Prim::Box));
2751    }
2752
2753    #[test]
2754    fn unsupported_control_is_clear() {
2755        // `copy "file"` with no filesystem context reports a clear file error
2756        let e = parse("copy \"x\"").unwrap_err();
2757        assert!(e.msg.contains("copy") && e.msg.contains("file"));
2758    }
2759
2760    #[test]
2761    fn control_constructs_parse() {
2762        assert!(parse("for i = 1 to 3 do { box }").is_ok());
2763        assert!(parse("if 1 > 0 then { box } else { circle }").is_ok());
2764        assert!(parse("reset boxht, boxwid").is_ok());
2765        // define is consumed by the preprocessor and expanded
2766        let p = parse("define e { box }\ne\ne").unwrap();
2767        assert_eq!(p.stmts.len(), 2);
2768    }
2769}