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}