Skip to main content

rpic_core/
ast.rs

1//! Abstract syntax tree for the pic language.
2//!
3//! Mirrors the structure of dpic's `grammar.txt`. The parser populates the
4//! drawing core (pictures, primitives + attributes, positions, expressions,
5//! blocks, assignments) plus the control constructs (`if`/`for`/`define`/
6//! `print`/`exec`) used by the evaluator and macro preprocessor.
7
8use crate::diagnostic::Span;
9use crate::lexer::Spanned;
10use crate::token::{
11    Arrow, Color, Corner, Dir, EnvVar, Func1, Func2, LineType, Param, Prim, TextPos,
12};
13
14/// Macro definitions (name → body tokens), carried from parse to eval so that
15/// `if`/`for` bodies can be expanded lazily along the executed path.
16pub type Macros = std::collections::HashMap<String, Vec<Spanned>>;
17
18/// A deferred block of statements, kept as raw tokens until the branch/iteration
19/// that contains it actually runs (so dead branches and recursive macro calls
20/// are never parsed). Parsed on demand by the evaluator.
21pub type Body = Vec<Spanned>;
22
23/// A complete picture: optional `.PS <w> <h>` dimensions plus a list of elements.
24#[derive(Debug, Clone, PartialEq)]
25pub struct Picture {
26    pub width: Option<Expr>,
27    pub height: Option<Expr>,
28    pub stmts: Vec<Stmt>,
29    pub macros: Macros,
30    /// Filesystem context for `copy "file"` includes (directory + policy);
31    /// carried so eval-time deferred parsing resolves includes the same way.
32    pub includes: IncludeCtx,
33}
34
35/// Policy for `copy "file"` filesystem includes. The default matches the CLI:
36/// full access, absolute paths allowed. Embedders compiling untrusted source
37/// should pick a restrictive policy. The embedded `copy "circuits"` library
38/// is always available — it never touches the filesystem.
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
40pub enum IncludePolicy {
41    /// Resolve like the CLI: absolute paths allowed, no fence. (Default.)
42    #[default]
43    Unrestricted,
44    /// Only files inside the base directory (canonicalized prefix check, so
45    /// `..` and symlink escapes are rejected); absolute paths are errors.
46    SandboxedToBase,
47    /// No filesystem includes at all (the wasm behavior everywhere).
48    Deny,
49}
50
51/// Where and how `copy "file"` includes resolve: the current directory (the
52/// including file's own dir, which varies as includes nest) plus the fixed
53/// [`IncludePolicy`] and its canonicalized fence root.
54#[derive(Debug, Clone, Default, PartialEq)]
55pub struct IncludeCtx {
56    /// Directory relative includes resolve against; `None` when parsing has
57    /// no filesystem context (WASM/bindings without `base`).
58    pub dir: Option<std::path::PathBuf>,
59    /// Policy applied to every filesystem include in this compilation.
60    pub policy: IncludePolicy,
61    /// Canonicalized sandbox root. Set when the policy is `SandboxedToBase`
62    /// and the base directory canonicalizes; `None` under that policy means
63    /// every filesystem include is denied (a fence that failed to resolve
64    /// must fail closed).
65    pub(crate) fence: Option<std::path::PathBuf>,
66}
67
68impl IncludeCtx {
69    /// The CLI behavior: resolve against `dir`, no restrictions.
70    pub fn unrestricted(dir: Option<std::path::PathBuf>) -> Self {
71        Self {
72            dir,
73            policy: IncludePolicy::Unrestricted,
74            fence: None,
75        }
76    }
77
78    /// Build a context for `dir` under `policy`, canonicalizing the fence
79    /// root for `SandboxedToBase` (fails closed if it cannot resolve).
80    pub fn with_policy(dir: Option<std::path::PathBuf>, policy: IncludePolicy) -> Self {
81        let fence = match policy {
82            IncludePolicy::SandboxedToBase => {
83                dir.as_deref().and_then(|d| std::fs::canonicalize(d).ok())
84            }
85            _ => None,
86        };
87        Self { dir, policy, fence }
88    }
89
90    /// A nested include's context: its own directory, same policy and fence.
91    pub(crate) fn child(&self, dir: Option<std::path::PathBuf>) -> Self {
92        Self {
93            dir,
94            policy: self.policy,
95            fence: self.fence.clone(),
96        }
97    }
98}
99
100/// A label with an optional `[subscript]` suffix.
101#[derive(Debug, Clone, PartialEq)]
102pub struct Label {
103    pub name: String,
104    pub subscript: Option<Expr>,
105}
106
107/// A top-level element.
108#[derive(Debug, Clone, PartialEq)]
109pub enum Stmt {
110    /// A drawn object, optionally labelled (`Start: box …`).
111    Object {
112        label: Option<Label>,
113        object: Object,
114    },
115    /// `Label: position` — names a point without drawing.
116    Place { label: Label, pos: Position },
117    /// One or more comma-separated assignments.
118    Assign(Vec<Assignment>),
119    /// A bare direction change (`up` / `down` / `left` / `right`).
120    Direction(Dir),
121    /// `{ … }` grouping block (local scope, no bounding object).
122    Group(Vec<Stmt>),
123    /// rpic animation directive (extension; see [`Animate`]).
124    Animate(Animate),
125    /// rpic extension: `class <place> "name"` — append a CSS class to an
126    /// already-drawn object's shape group (labels and ordinals both work).
127    Class { target: Place, class: StringExpr },
128    /// rpic extension: `canvas from <pos> to <pos>` — fix the output page to
129    /// the rectangle spanned by the two corners, independent of content, so
130    /// the viewBox stays stable while objects move (visual editors).
131    Canvas { from: Position, to: Position },
132    /// `if <cond> then { … } [else { … }]`. Bodies are deferred raw tokens.
133    If {
134        cond: Expr,
135        then_body: Body,
136        else_body: Option<Body>,
137    },
138    /// `for v = from to to [by [*] step] do { … }`. Body is deferred raw tokens.
139    For {
140        var: String,
141        subscript: Option<Expr>,
142        from: Expr,
143        to: Expr,
144        by: Expr,
145        /// `by *` multiplies instead of adds.
146        mult: bool,
147        body: Body,
148    },
149    /// `print …` (evaluated for diagnostics; no drawing effect).
150    Print(PrintItem),
151    /// `exec <string>` — evaluate generated pic source in the current state.
152    Exec {
153        command: StringExpr,
154        arg_frame: Option<Vec<Vec<Spanned>>>,
155    },
156    /// `reset` (all) or `reset a, b, …` — restore environment variables.
157    Reset(Vec<EnvVar>),
158}
159
160#[derive(Debug, Clone, PartialEq)]
161pub enum PrintItem {
162    Str(StringExpr),
163    Expr(Expr),
164}
165
166/// `animate <target> with "<effect>" [for <dur>] [at <t> | after <ref>] [delay <d>]`.
167#[derive(Debug, Clone, PartialEq)]
168pub struct Animate {
169    pub target: Place,
170    pub effect: StringExpr,
171    pub effect_span: Option<Span>,
172    pub duration: Option<Expr>,
173    pub timing: Timing,
174    pub delay: Option<Expr>,
175}
176
177/// When an animation starts.
178#[derive(Debug, Clone, PartialEq)]
179pub enum Timing {
180    /// After the previously declared animation ends (default).
181    Sequential,
182    /// At an absolute time (seconds).
183    At(Expr),
184    /// After the named object's animation ends.
185    After(Place),
186}
187
188/// A single assignment `target op value`.
189#[derive(Debug, Clone, PartialEq)]
190pub struct Assignment {
191    pub target: AssignTarget,
192    pub op: AssignOp,
193    pub value: Expr,
194}
195
196#[derive(Debug, Clone, PartialEq)]
197pub enum AssignTarget {
198    Var(String, Option<Expr>),
199    Env(EnvVar),
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum AssignOp {
204    Set,      // =
205    ColonSet, // :=
206    Add,      // +=
207    Sub,      // -=
208    Mul,      // *=
209    Div,      // /=
210    Rem,      // %=
211}
212
213/// A drawable object: a base plus a chain of attributes.
214#[derive(Debug, Clone, PartialEq)]
215pub struct Object {
216    pub kind: ObjectKind,
217    pub attrs: Vec<Attr>,
218    /// Span of the statement's leading token (for per-object geometry export
219    /// and future diagnostics).
220    pub span: Option<Span>,
221}
222
223#[derive(Debug, Clone, PartialEq)]
224pub enum ObjectKind {
225    Primitive(Prim),
226    Block(Vec<Stmt>),
227    /// `[]` empty-block reference.
228    Empty,
229    /// A bare quoted string places a text-only object.
230    Text,
231    /// rpic extension: a curly brace annotation between two points.
232    Brace,
233    /// rpic extension: a junction dot — a tiny solid circle (`dotrad`).
234    Dot,
235    /// `continue` — extend the previous line with another segment.
236    Continue,
237}
238
239/// An object attribute (applied left to right).
240#[derive(Debug, Clone, PartialEq)]
241pub enum Attr {
242    Dim(DimKind, Expr),
243    Direction(Dir, Option<Expr>),
244    /// A bare distance with no direction word (e.g. `move 1`): advance by this
245    /// much along the prevailing direction.
246    Dist(Expr, Option<Span>),
247    /// `spline <expr> …`: the expression right after `spline` is a spline
248    /// tension parameter (dpic semantics), NOT a distance.
249    SplineTension(Expr),
250    LineStyle(LineType, Option<Expr>),
251    Chop(Option<Expr>),
252    Fill(Option<Expr>),
253    Arrowhead(Arrow, Option<Expr>),
254    Then,
255    Cw,
256    Ccw,
257    Same,
258    Continue,
259    Text(StringExpr),
260    /// rpic extension: size a closed object to text declared before `fit`.
261    Fit,
262    /// rpic extension: hatch-filled closed regions.
263    Hatch(HatchKind),
264    /// rpic extension: hatch line angle in degrees.
265    HatchAngle(Expr),
266    /// rpic extension: hatch line spacing in pic units.
267    HatchSep(Expr),
268    /// rpic extension: hatch line width in points.
269    HatchWidth(Expr),
270    /// rpic extension: hatch line color.
271    HatchColor(StringExpr),
272    /// rpic extension: fill opacity, 0 = transparent and 1 = opaque.
273    Opacity(Expr),
274    /// rpic extension: two-stop linear gradient fill (`from`, `to` colors).
275    Gradient(StringExpr, StringExpr),
276    /// rpic extension: gradient angle in degrees, pic coordinates.
277    GradientAngle(Expr),
278    /// rpic extension: close a `line` path into a polygon.
279    Close,
280    TextPos(TextPos),
281    From(Position),
282    To(Position),
283    At(Position),
284    By(Position),
285    With {
286        anchor: WithAnchor,
287        at: Position,
288    },
289    Color(Color, StringExpr),
290    /// rpic extension: draw this object below another already-placed object.
291    Behind(Place),
292    /// rpic extension: CSS class hook attached to this object's shape group.
293    Class(StringExpr),
294    /// rpic extension: bold face for the preceding text string.
295    Bold,
296    /// rpic extension: italic face for the preceding text string.
297    Italic,
298    /// rpic extension: monospace family for the preceding text string.
299    Mono,
300    /// rpic extension: font family for the preceding text string.
301    Font(StringExpr),
302    /// rpic extension: font size in points for the preceding text string.
303    FontSize(Expr),
304    /// rpic extension: rotation in degrees (CCW) for the preceding string.
305    Rotated(Expr),
306    /// rpic extension: relative cusp position for a `brace` object.
307    BracePos(Expr),
308    /// rpic extension: extra outward spacing between a `brace` cusp and label.
309    BraceLabelOffset(Expr),
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum HatchKind {
314    Single,
315    Cross,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum DimKind {
320    Ht,
321    Wid,
322    Rad,
323    Diam,
324    Thick,
325    Scaled,
326}
327
328/// The reference point on an object used by `with … at …`.
329#[derive(Debug, Clone, PartialEq)]
330pub enum WithAnchor {
331    Corner(Corner),
332    Pair(Expr, Expr),
333    Place(Place),
334    Plain,
335}
336
337/// A position (point) expression. Positions support full vector arithmetic
338/// (dpic): `p + q`, `p - q`, `p * s`, `p / s` with the usual precedence.
339#[derive(Debug, Clone, PartialEq)]
340pub enum Position {
341    /// Explicit `x , y`.
342    Pair(Expr, Expr),
343    /// A bare location.
344    Place(Location),
345    /// `frac [of the way] between A and B`.
346    Between {
347        frac: Box<Expr>,
348        a: Box<Position>,
349        b: Box<Position>,
350        of_the_way: bool,
351    },
352    /// `p + q` / `p - q`.
353    Sum(Sign, Box<Position>, Box<Position>),
354    /// `p * s` (or `p / s` when `div`), scaling a position by a scalar.
355    Scale(Box<Position>, Expr, bool),
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum Sign {
360    Plus,
361    Minus,
362}
363
364/// A point-valued location.
365#[derive(Debug, Clone, PartialEq)]
366pub enum Location {
367    Place(Place),
368    /// `( position )`.
369    Paren(Box<Position>),
370    /// `( position , position )` — x of the first, y of the second.
371    ParenPair(Box<Position>, Box<Position>),
372}
373
374/// A named place in the drawing.
375#[derive(Debug, Clone, PartialEq)]
376pub enum Place {
377    /// A label, optionally subscripted.
378    Name {
379        name: String,
380        subscript: Option<Box<Expr>>,
381        /// Span of the reference (for eval-phase diagnostics).
382        span: Option<Span>,
383    },
384    /// `last box`, `2nd last circle`, etc.
385    Nth {
386        count: Nth,
387        obj: PrimObj,
388        /// Span of the reference (for eval-phase diagnostics).
389        span: Option<Span>,
390    },
391    /// `place . corner` (e.g. `A.ne`).
392    Corner(Box<Place>, Corner),
393    /// `corner of place` / `corner place` (e.g. `top of A`).
394    CornerOf(Corner, Box<Place>),
395    /// `place . place` (block sub-label, e.g. `B.A`).
396    Member(Box<Place>, Box<Place>),
397    Here,
398}
399
400#[derive(Debug, Clone, PartialEq)]
401pub enum Nth {
402    Last,
403    /// `count`-th object; `from_last` true for `… last`.
404    Count(Box<Expr>, bool),
405}
406
407#[derive(Debug, Clone, PartialEq)]
408pub enum PrimObj {
409    Prim(Prim),
410    Brace,
411    Block,
412    Str(String),
413    EmptyBrack,
414    /// Untyped `last` / `Nth last` — the most recent object of any kind
415    /// (e.g. `last.c`, `2nd last.n`). Used when no type keyword follows.
416    Any,
417}
418
419/// A string-valued expression.
420#[derive(Debug, Clone, PartialEq)]
421pub enum StringExpr {
422    Lit(String),
423    Concat(Box<StringExpr>, Box<StringExpr>),
424    Sprintf(Box<StringExpr>, Vec<Expr>),
425    /// rpic extension: `rgb(r,g,b)` colour literal (components 0–255);
426    /// evaluates to `#rrggbb`.
427    Rgb(Box<[Expr; 3]>),
428    /// rpic extension: a numeric colour in colour position (`shaded
429    /// 0x1b5e20`, pikchr-style); evaluates to `#rrggbb`.
430    ColorNum(Box<Expr>),
431    /// dpic SVG-backend helper. rpic does not emit backend preamble text, so
432    /// this evaluates to a harmless empty string.
433    SvgFont(Vec<Expr>),
434    Arg(u32),
435}
436
437/// A scalar (numeric / boolean) expression. pic treats booleans as numbers.
438#[derive(Debug, Clone, PartialEq)]
439pub enum Expr {
440    Num(f64),
441    /// A string operand, valid only as an operand of `==`/`!=` (pic compares
442    /// strings for equality, e.g. the `"$1"==""` default-argument idiom).
443    Str(StringExpr),
444    /// Comma-separated array subscript, valid only inside `name[...]`.
445    Index(Vec<Expr>),
446    Var(String, Option<Box<Expr>>),
447    Env(EnvVar),
448    Unary(UnOp, Box<Expr>),
449    Bin(BinOp, Box<Expr>, Box<Expr>),
450    Func1(Func1, Box<Expr>),
451    Func2(Func2, Box<Expr>, Box<Expr>),
452    Rand(Option<Box<Expr>>),
453    /// `( name = expr )` — assign and yield the assigned value.
454    Assign(String, Option<Box<Expr>>, Box<Expr>),
455    DotX(Location),
456    DotY(Location),
457    PlaceAttr(Place, Param),
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461pub enum UnOp {
462    Neg,
463    Pos,
464    Not,
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub enum BinOp {
469    Add,
470    Sub,
471    Mul,
472    Div,
473    Mod,
474    Pow,
475    Eq,
476    Ne,
477    Lt,
478    Le,
479    Gt,
480    Ge,
481    And,
482    Or,
483}