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}