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    // Boxed: `Animate` carries several `Place`s and is far larger than the
125    // other variants (clippy::large_enum_variant).
126    Animate(Box<Animate>),
127    /// rpic extension: `animate scroll` — a timeline-level hint that the host
128    /// should scrub the animation on scroll instead of autoplaying.
129    AnimateScroll,
130    /// rpic extension: `draggable <place> [inertia] [bounds <place>] [x|y]` —
131    /// mark an object grabbable in the browser (GSAP Draggable). Interaction,
132    /// not a timeline effect, so it is its own directive.
133    Draggable(Draggable),
134    /// rpic extension: `class <place> "name"` — append a CSS class to an
135    /// already-drawn object's shape group (labels and ordinals both work).
136    Class { target: Place, class: StringExpr },
137    /// rpic extension: `link <place> "<url>"` — make an already-drawn object a
138    /// hyperlink (its SVG shape group is wrapped in `<a href>`).
139    Link { target: Place, url: StringExpr },
140    /// rpic extension: `canvas from <pos> to <pos>` — fix the output page to
141    /// the rectangle spanned by the two corners, independent of content, so
142    /// the viewBox stays stable while objects move (visual editors).
143    Canvas { from: Position, to: Position },
144    /// `if <cond> then { … } [else { … }]`. Bodies are deferred raw tokens.
145    If {
146        cond: Expr,
147        then_body: Body,
148        else_body: Option<Body>,
149    },
150    /// `for v = from to to [by [*] step] do { … }`. Body is deferred raw tokens.
151    For {
152        var: String,
153        subscript: Option<Expr>,
154        from: Expr,
155        to: Expr,
156        by: Expr,
157        /// `by *` multiplies instead of adds.
158        mult: bool,
159        body: Body,
160    },
161    /// `print …` (evaluated for diagnostics; no drawing effect).
162    Print(PrintItem),
163    /// `exec <string>` — evaluate generated pic source in the current state.
164    Exec {
165        command: StringExpr,
166        arg_frame: Option<std::sync::Arc<Vec<Vec<Spanned>>>>,
167    },
168    /// `reset` (all) or `reset a, b, …` — restore environment variables.
169    Reset(Vec<EnvVar>),
170}
171
172#[derive(Debug, Clone, PartialEq)]
173pub enum PrintItem {
174    Str(StringExpr),
175    Expr(Expr),
176}
177
178/// A `draggable` interaction directive (rpic extension).
179#[derive(Debug, Clone, PartialEq)]
180pub struct Draggable {
181    pub target: Place,
182    /// `inertia` — throw with momentum (GSAP InertiaPlugin).
183    pub inertia: bool,
184    /// `bounds <place>` — constrain dragging to another object's box.
185    pub bounds: Option<Place>,
186    /// `x` / `y` — lock dragging to one axis (`None` = free).
187    pub axis: Option<DragAxis>,
188}
189
190/// Axis lock for a `draggable` directive.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum DragAxis {
193    X,
194    Y,
195}
196
197/// Granularity of the `type` effect's staggered reveal.
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum TypeUnit {
200    /// One tspan per character (default).
201    Char,
202    /// One tspan per whitespace-separated word.
203    Word,
204}
205
206/// `animate <target> with "<effect>" [along <path>] [to <colour>] [for <dur>]
207///   [at <t> | after <ref>] [delay <d>] [repeat <n>] [yoyo] [ease "<name>"]`.
208#[derive(Debug, Clone, PartialEq)]
209pub struct Animate {
210    pub target: Place,
211    pub effect: StringExpr,
212    pub effect_span: Option<Span>,
213    pub duration: Option<Expr>,
214    pub timing: Timing,
215    pub delay: Option<Expr>,
216    /// Object whose geometry the `move` effect follows (GSAP MotionPath).
217    pub along: Option<Place>,
218    /// Number of repeats after the first play (GSAP `repeat`): `-1` loops
219    /// forever, `0`/absent plays once.
220    pub repeat: Option<Expr>,
221    /// Alternate direction on each repeat (GSAP `yoyo`).
222    pub yoyo: bool,
223    /// GSAP easing name overriding the per-effect default (e.g. `"elastic.out"`).
224    pub ease: Option<StringExpr>,
225    /// Target colour for the `highlight` effect (`to <colour>`); any rpic
226    /// colour form (name, `rgb()`, `0xRRGGBB`).
227    pub color: Option<StringExpr>,
228    /// When the target is a block, fan the effect across its children with this
229    /// per-child start offset in seconds (GSAP-style `stagger`).
230    pub stagger: Option<Expr>,
231    /// Play the effect as an exit (reverse: `.to()` the hidden state) rather
232    /// than an entrance.
233    pub out: bool,
234    /// Direction the `slide` effect enters from (`from left`, …).
235    pub slide_from: Option<Dir>,
236    /// Target object the `morph` effect morphs into (`into B`).
237    pub morph_into: Option<Place>,
238    /// Split granularity for the `type` effect (`by word` / `by char`); `None`
239    /// defaults to `Char`.
240    pub type_unit: Option<TypeUnit>,
241    /// Custom scramble charset for the `scramble` effect (`by "01"`); `None`
242    /// defaults to the plugin's `upperCase`.
243    pub scramble_chars: Option<StringExpr>,
244    /// Oscillation count for the `wiggle` effect (`wiggles <n>`); `None`
245    /// defaults to a few.
246    pub wiggles: Option<Expr>,
247    /// Start of the revealed segment for the `draw` effect, as a fraction of the
248    /// stroke (`from 40%` → `0.4`); `None` starts at the beginning.
249    pub draw_from: Option<Expr>,
250    /// End of the revealed segment for the `draw` effect, as a fraction of the
251    /// stroke (`to 60%` → `0.6`); `None` reaches the end.
252    pub draw_to: Option<Expr>,
253}
254
255/// When an animation starts.
256#[derive(Debug, Clone, PartialEq)]
257pub enum Timing {
258    /// After the previously declared animation ends (default).
259    Sequential,
260    /// At an absolute time (seconds).
261    At(Expr),
262    /// After the named object's animation ends.
263    After(Place),
264}
265
266/// A single assignment `target op value`.
267#[derive(Debug, Clone, PartialEq)]
268pub struct Assignment {
269    pub target: AssignTarget,
270    pub op: AssignOp,
271    pub value: Expr,
272}
273
274#[derive(Debug, Clone, PartialEq)]
275pub enum AssignTarget {
276    Var(String, Option<Expr>),
277    Env(EnvVar),
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub enum AssignOp {
282    Set,      // =
283    ColonSet, // :=
284    Add,      // +=
285    Sub,      // -=
286    Mul,      // *=
287    Div,      // /=
288    Rem,      // %=
289}
290
291/// A drawable object: a base plus a chain of attributes.
292#[derive(Debug, Clone, PartialEq)]
293pub struct Object {
294    pub kind: ObjectKind,
295    pub attrs: Vec<Attr>,
296    /// Span of the statement's leading token (for per-object geometry export
297    /// and future diagnostics).
298    pub span: Option<Span>,
299}
300
301#[derive(Debug, Clone, PartialEq)]
302pub enum ObjectKind {
303    Primitive(Prim),
304    Block(Vec<Stmt>),
305    /// `[]` empty-block reference.
306    Empty,
307    /// A bare quoted string places a text-only object.
308    Text,
309    /// rpic extension: a curly brace annotation between two points.
310    Brace,
311    /// rpic extension: a junction dot — a tiny solid circle (`dotrad`).
312    Dot,
313    /// `continue` — extend the previous line with another segment.
314    Continue,
315}
316
317/// An object attribute (applied left to right).
318#[derive(Debug, Clone, PartialEq)]
319pub enum Attr {
320    Dim(DimKind, Expr),
321    Direction(Dir, Option<Expr>),
322    /// A bare distance with no direction word (e.g. `move 1`): advance by this
323    /// much along the prevailing direction.
324    Dist(Expr, Option<Span>),
325    /// `spline <expr> …`: the expression right after `spline` is a spline
326    /// tension parameter (dpic semantics), NOT a distance.
327    SplineTension(Expr),
328    LineStyle(LineType, Option<Expr>),
329    Chop(Option<Expr>),
330    Fill(Option<Expr>),
331    Arrowhead(Arrow, Option<Expr>),
332    Then,
333    Cw,
334    Ccw,
335    Same,
336    Continue,
337    Text(StringExpr),
338    /// rpic extension: size a closed object to text declared before `fit`.
339    Fit,
340    /// rpic extension: hatch-filled closed regions.
341    Hatch(HatchKind),
342    /// rpic extension: hatch line angle in degrees.
343    HatchAngle(Expr),
344    /// rpic extension: hatch line spacing in pic units.
345    HatchSep(Expr),
346    /// rpic extension: hatch line width in points.
347    HatchWidth(Expr),
348    /// rpic extension: hatch line color (with its source span for diagnostics).
349    HatchColor(StringExpr, Option<Span>),
350    /// rpic extension: fill opacity, 0 = transparent and 1 = opaque.
351    Opacity(Expr),
352    /// rpic extension: two-stop linear gradient fill (`from`, `to` colors,
353    /// each with its source span for diagnostics).
354    Gradient(StringExpr, Option<Span>, StringExpr, Option<Span>),
355    /// rpic extension: gradient angle in degrees, pic coordinates.
356    GradientAngle(Expr),
357    /// rpic extension: close a `line` path into a polygon.
358    Close,
359    TextPos(TextPos),
360    From(Position),
361    To(Position),
362    At(Position),
363    By(Position),
364    With {
365        anchor: WithAnchor,
366        at: Position,
367    },
368    /// A colour attribute (`outlined`/`shaded`/`color`) with its source span.
369    Color(Color, StringExpr, Option<Span>),
370    /// rpic extension: draw this object below another already-placed object.
371    Behind(Place),
372    /// rpic extension: CSS class hook attached to this object's shape group.
373    Class(StringExpr),
374    /// rpic extension: hyperlink — the object's SVG shape group is wrapped in
375    /// `<a href>` so clicking it navigates to the URL.
376    Link(StringExpr),
377    /// rpic extension (pikchr-flavoured): a lighter stroke — sets the object's
378    /// line thickness to ⅔ of the current `linethick` (no value; the sugar for
379    /// `thick (linethick*2/3)`, mirroring `thick <n>`).
380    Thin,
381    /// rpic extension: bold face for the preceding text string.
382    Bold,
383    /// rpic extension: italic face for the preceding text string.
384    Italic,
385    /// rpic extension: monospace family for the preceding text string.
386    Mono,
387    /// rpic extension: font family for the preceding text string.
388    Font(StringExpr),
389    /// rpic extension: font size in points for the preceding text string.
390    FontSize(Expr),
391    /// rpic extension: rotation in degrees (CCW) for the preceding string.
392    Rotated(Expr),
393    /// rpic extension: align the preceding string to the host segment's angle
394    /// (pikchr `aligned`). Only linear objects have a segment; elsewhere inert.
395    Aligned,
396    /// rpic extension: pikchr `big`/`small` text size (sugar over `fontsize`);
397    /// `true` = big (1.5×), `false` = small (0.7×) of the classic 11 pt.
398    Sized(bool),
399    /// rpic extension: relative cusp position for a `brace` object.
400    BracePos(Expr),
401    /// rpic extension: extra outward spacing between a `brace` cusp and label.
402    BraceLabelOffset(Expr),
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub enum HatchKind {
407    Single,
408    Cross,
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub enum DimKind {
413    Ht,
414    Wid,
415    Rad,
416    Diam,
417    Thick,
418    Scaled,
419}
420
421/// The reference point on an object used by `with … at …`.
422#[derive(Debug, Clone, PartialEq)]
423pub enum WithAnchor {
424    Corner(Corner),
425    Pair(Expr, Expr),
426    Place(Place),
427    Plain,
428}
429
430/// A position (point) expression. Positions support full vector arithmetic
431/// (dpic): `p + q`, `p - q`, `p * s`, `p / s` with the usual precedence.
432#[derive(Debug, Clone, PartialEq)]
433pub enum Position {
434    /// Explicit `x , y`.
435    Pair(Expr, Expr),
436    /// A bare location.
437    Place(Location),
438    /// `frac [of the way] between A and B`.
439    Between {
440        frac: Box<Expr>,
441        a: Box<Position>,
442        b: Box<Position>,
443        of_the_way: bool,
444    },
445    /// `p + q` / `p - q`.
446    Sum(Sign, Box<Position>, Box<Position>),
447    /// `p * s` (or `p / s` when `div`), scaling a position by a scalar.
448    Scale(Box<Position>, Expr, bool),
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452pub enum Sign {
453    Plus,
454    Minus,
455}
456
457/// A point-valued location.
458#[derive(Debug, Clone, PartialEq)]
459pub enum Location {
460    Place(Place),
461    /// `( position )`.
462    Paren(Box<Position>),
463    /// `( position , position )` — x of the first, y of the second.
464    ParenPair(Box<Position>, Box<Position>),
465}
466
467/// A named place in the drawing.
468#[derive(Debug, Clone, PartialEq)]
469pub enum Place {
470    /// A label, optionally subscripted.
471    Name {
472        name: String,
473        subscript: Option<Box<Expr>>,
474        /// Span of the reference (for eval-phase diagnostics).
475        span: Option<Span>,
476    },
477    /// `last box`, `2nd last circle`, etc.
478    Nth {
479        count: Nth,
480        obj: PrimObj,
481        /// Span of the reference (for eval-phase diagnostics).
482        span: Option<Span>,
483    },
484    /// `place . corner` (e.g. `A.ne`).
485    Corner(Box<Place>, Corner),
486    /// `corner of place` / `corner place` (e.g. `top of A`).
487    CornerOf(Corner, Box<Place>),
488    /// `place . place` (block sub-label, e.g. `B.A`).
489    Member(Box<Place>, Box<Place>),
490    Here,
491}
492
493#[derive(Debug, Clone, PartialEq)]
494pub enum Nth {
495    Last,
496    /// `count`-th object; `from_last` true for `… last`.
497    Count(Box<Expr>, bool),
498}
499
500#[derive(Debug, Clone, PartialEq)]
501pub enum PrimObj {
502    Prim(Prim),
503    Brace,
504    Block,
505    Str(String),
506    EmptyBrack,
507    /// Untyped `last` / `Nth last` — the most recent object of any kind
508    /// (e.g. `last.c`, `2nd last.n`). Used when no type keyword follows.
509    Any,
510}
511
512/// A string-valued expression.
513#[derive(Debug, Clone, PartialEq)]
514pub enum StringExpr {
515    Lit(String),
516    Concat(Box<StringExpr>, Box<StringExpr>),
517    Sprintf(Box<StringExpr>, Vec<Expr>),
518    /// rpic extension: `rgb(r,g,b)` colour literal (components 0–255);
519    /// evaluates to `#rrggbb`.
520    Rgb(Box<[Expr; 3]>),
521    /// rpic extension: a numeric colour in colour position (`shaded
522    /// 0x1b5e20`, pikchr-style); evaluates to `#rrggbb`.
523    ColorNum(Box<Expr>),
524    /// dpic SVG-backend helper. rpic does not emit backend preamble text, so
525    /// this evaluates to a harmless empty string.
526    SvgFont(Vec<Expr>),
527    Arg(u32),
528}
529
530/// A scalar (numeric / boolean) expression. pic treats booleans as numbers.
531#[derive(Debug, Clone, PartialEq)]
532pub enum Expr {
533    Num(f64),
534    /// A string operand, valid only as an operand of `==`/`!=` (pic compares
535    /// strings for equality, e.g. the `"$1"==""` default-argument idiom).
536    Str(StringExpr),
537    /// Comma-separated array subscript, valid only inside `name[...]`.
538    Index(Vec<Expr>),
539    Var(String, Option<Box<Expr>>),
540    Env(EnvVar),
541    Unary(UnOp, Box<Expr>),
542    Bin(BinOp, Box<Expr>, Box<Expr>),
543    Func1(Func1, Box<Expr>),
544    Func2(Func2, Box<Expr>, Box<Expr>),
545    Rand(Option<Box<Expr>>),
546    /// `( name = expr )` — assign and yield the assigned value.
547    Assign(String, Option<Box<Expr>>, Box<Expr>),
548    DotX(Location),
549    DotY(Location),
550    PlaceAttr(Place, Param),
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
554pub enum UnOp {
555    Neg,
556    Pos,
557    Not,
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum BinOp {
562    Add,
563    Sub,
564    Mul,
565    Div,
566    Mod,
567    Pow,
568    Eq,
569    Ne,
570    Lt,
571    Le,
572    Gt,
573    Ge,
574    And,
575    Or,
576}