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    /// `if <cond> then { … } [else { … }]`. Bodies are deferred raw tokens.
129    If {
130        cond: Expr,
131        then_body: Body,
132        else_body: Option<Body>,
133    },
134    /// `for v = from to to [by [*] step] do { … }`. Body is deferred raw tokens.
135    For {
136        var: String,
137        subscript: Option<Expr>,
138        from: Expr,
139        to: Expr,
140        by: Expr,
141        /// `by *` multiplies instead of adds.
142        mult: bool,
143        body: Body,
144    },
145    /// `print …` (evaluated for diagnostics; no drawing effect).
146    Print(PrintItem),
147    /// `exec <string>` — evaluate generated pic source in the current state.
148    Exec {
149        command: StringExpr,
150        arg_frame: Option<Vec<Vec<Spanned>>>,
151    },
152    /// `reset` (all) or `reset a, b, …` — restore environment variables.
153    Reset(Vec<EnvVar>),
154}
155
156#[derive(Debug, Clone, PartialEq)]
157pub enum PrintItem {
158    Str(StringExpr),
159    Expr(Expr),
160}
161
162/// `animate <target> with "<effect>" [for <dur>] [at <t> | after <ref>] [delay <d>]`.
163#[derive(Debug, Clone, PartialEq)]
164pub struct Animate {
165    pub target: Place,
166    pub effect: StringExpr,
167    pub effect_span: Option<Span>,
168    pub duration: Option<Expr>,
169    pub timing: Timing,
170    pub delay: Option<Expr>,
171}
172
173/// When an animation starts.
174#[derive(Debug, Clone, PartialEq)]
175pub enum Timing {
176    /// After the previously declared animation ends (default).
177    Sequential,
178    /// At an absolute time (seconds).
179    At(Expr),
180    /// After the named object's animation ends.
181    After(Place),
182}
183
184/// A single assignment `target op value`.
185#[derive(Debug, Clone, PartialEq)]
186pub struct Assignment {
187    pub target: AssignTarget,
188    pub op: AssignOp,
189    pub value: Expr,
190}
191
192#[derive(Debug, Clone, PartialEq)]
193pub enum AssignTarget {
194    Var(String, Option<Expr>),
195    Env(EnvVar),
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum AssignOp {
200    Set,      // =
201    ColonSet, // :=
202    Add,      // +=
203    Sub,      // -=
204    Mul,      // *=
205    Div,      // /=
206    Rem,      // %=
207}
208
209/// A drawable object: a base plus a chain of attributes.
210#[derive(Debug, Clone, PartialEq)]
211pub struct Object {
212    pub kind: ObjectKind,
213    pub attrs: Vec<Attr>,
214}
215
216#[derive(Debug, Clone, PartialEq)]
217pub enum ObjectKind {
218    Primitive(Prim),
219    Block(Vec<Stmt>),
220    /// `[]` empty-block reference.
221    Empty,
222    /// A bare quoted string places a text-only object.
223    Text,
224    /// rpic extension: a curly brace annotation between two points.
225    Brace,
226    /// rpic extension: a junction dot — a tiny solid circle (`dotrad`).
227    Dot,
228    /// `continue` — extend the previous line with another segment.
229    Continue,
230}
231
232/// An object attribute (applied left to right).
233#[derive(Debug, Clone, PartialEq)]
234pub enum Attr {
235    Dim(DimKind, Expr),
236    Direction(Dir, Option<Expr>),
237    /// A bare distance with no direction word (e.g. `move 1`): advance by this
238    /// much along the prevailing direction.
239    Dist(Expr, Option<Span>),
240    /// `spline <expr> …`: the expression right after `spline` is a spline
241    /// tension parameter (dpic semantics), NOT a distance.
242    SplineTension(Expr),
243    LineStyle(LineType, Option<Expr>),
244    Chop(Option<Expr>),
245    Fill(Option<Expr>),
246    Arrowhead(Arrow, Option<Expr>),
247    Then,
248    Cw,
249    Ccw,
250    Same,
251    Continue,
252    Text(StringExpr),
253    /// rpic extension: size a closed object to text declared before `fit`.
254    Fit,
255    /// rpic extension: hatch-filled closed regions.
256    Hatch(HatchKind),
257    /// rpic extension: hatch line angle in degrees.
258    HatchAngle(Expr),
259    /// rpic extension: hatch line spacing in pic units.
260    HatchSep(Expr),
261    /// rpic extension: hatch line width in points.
262    HatchWidth(Expr),
263    /// rpic extension: hatch line color.
264    HatchColor(StringExpr),
265    /// rpic extension: fill opacity, 0 = transparent and 1 = opaque.
266    Opacity(Expr),
267    /// rpic extension: two-stop linear gradient fill (`from`, `to` colors).
268    Gradient(StringExpr, StringExpr),
269    /// rpic extension: gradient angle in degrees, pic coordinates.
270    GradientAngle(Expr),
271    /// rpic extension: close a `line` path into a polygon.
272    Close,
273    TextPos(TextPos),
274    From(Position),
275    To(Position),
276    At(Position),
277    By(Position),
278    With {
279        anchor: WithAnchor,
280        at: Position,
281    },
282    Color(Color, StringExpr),
283    /// rpic extension: draw this object below another already-placed object.
284    Behind(Place),
285    /// rpic extension: CSS class hook attached to this object's shape group.
286    Class(StringExpr),
287    /// rpic extension: relative cusp position for a `brace` object.
288    BracePos(Expr),
289    /// rpic extension: extra outward spacing between a `brace` cusp and label.
290    BraceLabelOffset(Expr),
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum HatchKind {
295    Single,
296    Cross,
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum DimKind {
301    Ht,
302    Wid,
303    Rad,
304    Diam,
305    Thick,
306    Scaled,
307}
308
309/// The reference point on an object used by `with … at …`.
310#[derive(Debug, Clone, PartialEq)]
311pub enum WithAnchor {
312    Corner(Corner),
313    Pair(Expr, Expr),
314    Place(Place),
315    Plain,
316}
317
318/// A position (point) expression. Positions support full vector arithmetic
319/// (dpic): `p + q`, `p - q`, `p * s`, `p / s` with the usual precedence.
320#[derive(Debug, Clone, PartialEq)]
321pub enum Position {
322    /// Explicit `x , y`.
323    Pair(Expr, Expr),
324    /// A bare location.
325    Place(Location),
326    /// `frac [of the way] between A and B`.
327    Between {
328        frac: Box<Expr>,
329        a: Box<Position>,
330        b: Box<Position>,
331        of_the_way: bool,
332    },
333    /// `p + q` / `p - q`.
334    Sum(Sign, Box<Position>, Box<Position>),
335    /// `p * s` (or `p / s` when `div`), scaling a position by a scalar.
336    Scale(Box<Position>, Expr, bool),
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum Sign {
341    Plus,
342    Minus,
343}
344
345/// A point-valued location.
346#[derive(Debug, Clone, PartialEq)]
347pub enum Location {
348    Place(Place),
349    /// `( position )`.
350    Paren(Box<Position>),
351    /// `( position , position )` — x of the first, y of the second.
352    ParenPair(Box<Position>, Box<Position>),
353}
354
355/// A named place in the drawing.
356#[derive(Debug, Clone, PartialEq)]
357pub enum Place {
358    /// A label, optionally subscripted.
359    Name {
360        name: String,
361        subscript: Option<Box<Expr>>,
362        /// Span of the reference (for eval-phase diagnostics).
363        span: Option<Span>,
364    },
365    /// `last box`, `2nd last circle`, etc.
366    Nth {
367        count: Nth,
368        obj: PrimObj,
369        /// Span of the reference (for eval-phase diagnostics).
370        span: Option<Span>,
371    },
372    /// `place . corner` (e.g. `A.ne`).
373    Corner(Box<Place>, Corner),
374    /// `corner of place` / `corner place` (e.g. `top of A`).
375    CornerOf(Corner, Box<Place>),
376    /// `place . place` (block sub-label, e.g. `B.A`).
377    Member(Box<Place>, Box<Place>),
378    Here,
379}
380
381#[derive(Debug, Clone, PartialEq)]
382pub enum Nth {
383    Last,
384    /// `count`-th object; `from_last` true for `… last`.
385    Count(Box<Expr>, bool),
386}
387
388#[derive(Debug, Clone, PartialEq)]
389pub enum PrimObj {
390    Prim(Prim),
391    Brace,
392    Block,
393    Str(String),
394    EmptyBrack,
395    /// Untyped `last` / `Nth last` — the most recent object of any kind
396    /// (e.g. `last.c`, `2nd last.n`). Used when no type keyword follows.
397    Any,
398}
399
400/// A string-valued expression.
401#[derive(Debug, Clone, PartialEq)]
402pub enum StringExpr {
403    Lit(String),
404    Concat(Box<StringExpr>, Box<StringExpr>),
405    Sprintf(Box<StringExpr>, Vec<Expr>),
406    /// dpic SVG-backend helper. rpic does not emit backend preamble text, so
407    /// this evaluates to a harmless empty string.
408    SvgFont(Vec<Expr>),
409    Arg(u32),
410}
411
412/// A scalar (numeric / boolean) expression. pic treats booleans as numbers.
413#[derive(Debug, Clone, PartialEq)]
414pub enum Expr {
415    Num(f64),
416    /// A string operand, valid only as an operand of `==`/`!=` (pic compares
417    /// strings for equality, e.g. the `"$1"==""` default-argument idiom).
418    Str(StringExpr),
419    /// Comma-separated array subscript, valid only inside `name[...]`.
420    Index(Vec<Expr>),
421    Var(String, Option<Box<Expr>>),
422    Env(EnvVar),
423    Unary(UnOp, Box<Expr>),
424    Bin(BinOp, Box<Expr>, Box<Expr>),
425    Func1(Func1, Box<Expr>),
426    Func2(Func2, Box<Expr>, Box<Expr>),
427    Rand(Option<Box<Expr>>),
428    /// `( name = expr )` — assign and yield the assigned value.
429    Assign(String, Option<Box<Expr>>, Box<Expr>),
430    DotX(Location),
431    DotY(Location),
432    PlaceAttr(Place, Param),
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
436pub enum UnOp {
437    Neg,
438    Pos,
439    Not,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum BinOp {
444    Add,
445    Sub,
446    Mul,
447    Div,
448    Mod,
449    Pow,
450    Eq,
451    Ne,
452    Lt,
453    Le,
454    Gt,
455    Ge,
456    And,
457    Or,
458}