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}