polydat_grammar/ast.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Abstract syntax tree for the Polydat DSL.
5
6use crate::lexer::Span;
7
8/// A complete `.polydat` file.
9#[derive(Debug, Clone)]
10pub struct PolydatFile {
11 /// The statements, in document order.
12 pub statements: Vec<Statement>,
13}
14
15/// A top-level statement.
16#[derive(Debug, Clone)]
17pub enum Statement {
18 /// `input name[: type]` — declares one per-cycle kernel input slot.
19 /// The name becomes both an input slot (settable via `set_input`)
20 /// and a passthrough output (readable via `get_constant`/`pull`).
21 ///
22 /// Surface forms (parser desugars the tuple into N `InputDecl`s,
23 /// mirroring the module-signature param-list shape from
24 /// a host-provided cycle module):
25 /// ```text
26 /// input cycle: u64
27 /// input (cycle: u64, q: f64)
28 /// ```
29 InputDecl(InputDecl),
30 /// A name-to-expression binding. The modifier on the
31 /// binding determines its lifecycle:
32 ///
33 /// - **no modifier** — per-cycle: re-evaluated every cycle.
34 /// - **`const`** — effectively-const for the scope's
35 /// lifetime: materialized at the earliest opportunity
36 /// (compile-time fold if the RHS is fold-eligible,
37 /// otherwise scope-init pull after materialize-wiring
38 /// has populated extern slots). Authors don't need to
39 /// know which path the runtime takes — the contract is
40 /// "fixed once, then immutable."
41 /// - **`shared`** — cell-backed, mutable across kernel
42 /// instances in the same lineage. See
43 /// `crates/polydat/docs/design/scope_model.md` §6
44 /// "Shared mutable bindings".
45 /// - **`volatile`** — re-evaluated on every pull and never
46 /// constant-folded. The program's canonical hash
47 /// (`scope_model.md` §8) covers the binding's expression
48 /// and its `volatile` flag, never a value it produced.
49 ///
50 /// Surface forms:
51 /// ```text
52 /// x := mul(cycle, 2) // per-cycle
53 /// const pi := 3.14 // const, folds at compile
54 /// const ann_opts := str_concat(...) // const, materializes at scope-init
55 /// shared budget := 100 // shared cell
56 /// (a, b) := split_pair(...) // tuple destructuring
57 /// ```
58 Binding(Binding),
59 /// `name(param: type, ...) -> (output: type, ...) := { body }`
60 ModuleDef(ModuleDef),
61 /// `extern name: type = default`
62 ExternPort(ExternPort),
63 /// `cursor name = Cursor()` or `cursor name = constructor_expr`
64 Cursor(CursorDecl),
65 /// `pragma <name>` — a directive opting the scope it is written
66 /// in, and every scope nested in it, into a compile-time check
67 /// (polydat_grammar.md §14).
68 /// First-class grammar, distinct from line
69 /// comments. Recognised pragmas trigger
70 /// `CompileEvent::PragmaAcknowledged`; unknown names trigger
71 /// `CompileEvent::UnknownPragma` and are otherwise ignored
72 /// (forward-compatible).
73 Pragma {
74 /// The pragma's name, after the `pragma` keyword.
75 name: String,
76 /// Where the pragma appears.
77 span: Span,
78 },
79 /// `for <source> { body }` — a traversal scope (for_traversal.md §3.2).
80 /// One child scope activates per tuple of the source; the
81 /// comprehension's element names are wires inside the body.
82 For(ForStmt),
83 /// `tile name : encoding (options) := body` — a compiled variate
84 /// template (polytile.md §2).
85 Tile(TileDef),
86}
87
88/// Per-tile template options (polytile.md §2.3): the hole delimiters, the
89/// directive sigil, and strictness.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct TileOptions {
92 /// The text that opens a hole, `${` by default.
93 pub open: String,
94 /// The text that closes a hole, `}` by default.
95 pub close: String,
96 /// The directive sigil, `@` by default.
97 pub sigil: String,
98 /// Whether an unknown hole or directive is an error rather than text.
99 pub strict: bool,
100 /// The body begins inside a JSON string literal (`instring`): holes
101 /// encode as escaped text from the first byte. The compiler sets
102 /// this on the tile it makes for a projection nested in a string
103 /// position; authors rarely need it.
104 pub in_string: bool,
105}
106
107impl Default for TileOptions {
108 fn default() -> Self {
109 Self {
110 open: "${".into(),
111 close: "}".into(),
112 sigil: "@".into(),
113 strict: false,
114 in_string: false,
115 }
116 }
117}
118
119/// How a tile body was written, so the printer can reproduce it.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum TileBodyKind {
122 /// A brace- or bracket-balanced block, as `json` templates are written.
123 Block,
124 /// Text between `<<<` and `>>>`.
125 Heredoc,
126 /// An ordinary string literal.
127 Literal,
128}
129
130/// A tile definition: its header, its raw body, and the parsed template.
131#[derive(Debug, Clone)]
132pub struct TileDef {
133 /// The tile's name: the wire it binds.
134 pub name: String,
135 /// The declared encoding, such as `json` or `csv`, if any.
136 pub encoding: Option<String>,
137 /// The delimiter and strictness options.
138 pub options: TileOptions,
139 /// How the body was written: heredoc, block, or string literal.
140 /// Presentation, not content: it decides how the body is delimited
141 /// when the tile is printed.
142 pub body_kind: TileBodyKind,
143 /// The template: static runs, holes, projections, and branches.
144 /// This is the tile's body — the only representation of it. Text
145 /// admitted from source is parsed into pieces under the options in
146 /// force at that moment and is not kept beside them.
147 pub pieces: Vec<TilePiece>,
148 /// Where the definition appears.
149 pub span: Span,
150}
151
152impl TileDef {
153 /// A tile whose body is admitted as **text**, parsed into pieces
154 /// under the tile's own options.
155 ///
156 /// The text is consumed by this call: what the tile holds
157 /// afterwards is the template it parsed to. Reading the body back
158 /// renders from those pieces ([`Self::body_text`]), so the body a
159 /// tile prints is always the body it renders (polytile.md §3).
160 pub fn from_body(
161 name: impl Into<String>,
162 encoding: Option<String>,
163 options: TileOptions,
164 body_kind: TileBodyKind,
165 body: impl Into<String>,
166 span: Span,
167 ) -> Result<Self, String> {
168 let pieces = crate::tile::parse_template(&body.into(), &options, span)?;
169 Ok(Self::from_pieces(
170 name, encoding, options, body_kind, pieces, span,
171 ))
172 }
173
174 /// A tile whose body is admitted as **pieces**, built directly.
175 ///
176 /// The counterpart of [`Self::from_body`], and the same tile: a
177 /// template composed programmatically and one parsed from text that
178 /// renders the same pieces are equal as tiles, because the pieces
179 /// are what a tile is. Pieces from either source compose by
180 /// concatenation, so a template may be assembled from parsed
181 /// fragments, hand-built pieces, or any mixture, in any order.
182 pub fn from_pieces(
183 name: impl Into<String>,
184 encoding: Option<String>,
185 options: TileOptions,
186 body_kind: TileBodyKind,
187 pieces: Vec<TilePiece>,
188 span: Span,
189 ) -> Self {
190 TileDef {
191 name: name.into(),
192 encoding,
193 options,
194 body_kind,
195 pieces,
196 span,
197 }
198 }
199
200 /// The body as template text, rendered from the pieces under this
201 /// tile's own delimiters and sigil.
202 ///
203 /// Canonical rather than verbatim: a tile parsed from source does
204 /// not keep the author's spacing, the same way the projector does
205 /// not keep the spacing around a binary operator. Re-parsing this
206 /// text under the same options yields the same pieces.
207 pub fn body_text(&self) -> String {
208 crate::tile::render_template(&self.pieces, &self.options)
209 }
210}
211
212/// One element of a parsed template.
213#[derive(Debug, Clone)]
214pub enum TilePiece {
215 /// Bytes copied as written.
216 Static(String),
217 /// `${expr : type | format !}`.
218 Hole(TileHole),
219 /// `@for <source> [sep "..."] { body }`.
220 Projection {
221 /// What the projection iterates.
222 source: ForSource,
223 /// The separator emitted between tuples, if any.
224 sep: Option<String>,
225 /// The template rendered once per tuple.
226 body: Vec<TilePiece>,
227 /// Where the directive appears.
228 span: Span,
229 },
230 /// `@if cond { body } [@else { body }]`.
231 Branch {
232 /// The condition, a boolean expression.
233 cond: Expr,
234 /// The template rendered when the condition holds.
235 then: Vec<TilePiece>,
236 /// The template rendered otherwise, if an `@else` was written.
237 otherwise: Option<Vec<TilePiece>>,
238 /// Where the directive appears.
239 span: Span,
240 },
241}
242
243/// A hole: an expression with an optional declared type, format spec,
244/// and raw flag.
245#[derive(Debug, Clone)]
246pub struct TileHole {
247 /// The expression the hole evaluates.
248 pub expr: Expr,
249 /// The declared type after the colon, if any.
250 pub decl_type: Option<String>,
251 /// The format after the bar, if any.
252 pub format: Option<String>,
253 /// Whether the value is emitted without the encoding's escaping.
254 pub raw: bool,
255 /// Where the hole appears.
256 pub span: Span,
257}
258
259impl TileHole {
260 /// The hole's body as template text, between the delimiters: the
261 /// expression, then the declared type, the format, and the raw
262 /// marker where each is present.
263 ///
264 /// This is the hole's only textual form. A hole parsed from source
265 /// does not keep what was written, because the parts are what the
266 /// renderer and the compiler read, and a second copy of the same
267 /// thing is a second thing to disagree. Spacing the author used
268 /// inside the delimiters is not reproduced, exactly as the
269 /// projector does not reproduce the spacing around a binary
270 /// operator.
271 pub fn to_text(&self) -> String {
272 let mut out = crate::pprint::pp_expr(&self.expr);
273 if let Some(ty) = &self.decl_type {
274 out.push_str(": ");
275 out.push_str(ty);
276 }
277 if let Some(fmt) = &self.format {
278 out.push_str(" | ");
279 out.push_str(fmt);
280 }
281 if self.raw {
282 out.push('!');
283 }
284 out
285 }
286}
287
288/// What a `for` iterates: inline comprehension text, or the name of a
289/// bound producer wire.
290#[derive(Debug, Clone)]
291#[non_exhaustive]
292pub struct ForSource {
293 /// What the source denotes. This is the source: its text is
294 /// rendered from here ([`ForSource::to_text`]), never stored
295 /// beside it, so a source cannot read as one thing and traverse
296 /// another.
297 pub kind: ForSourceKind,
298 /// Where the source appears, for diagnostics. Position is
299 /// provenance and belongs on the node; a copy of the text the
300 /// author wrote is not provenance, it is a second answer to a
301 /// question that has one.
302 pub span: Span,
303}
304
305#[derive(Debug, Clone)]
306/// The parsed form of a `for` source.
307pub enum ForSourceKind {
308 /// A bare identifier naming a `Streamer` wire bound by a `for`
309 /// expression elsewhere in scope.
310 Producer(String),
311 /// Comprehension text, parsed to the algebra AST.
312 Comprehension(crate::comprehension::Comprehension),
313 /// A derivation of a bound producer: `base where <pred>`,
314 /// `base order <spec>`, or both (for_traversal.md §3.1). Resolved against
315 /// the producer at compile time.
316 Derived {
317 /// The producer wire the derivation starts from.
318 base: String,
319 /// The `where` predicate text, if any.
320 filter: Option<String>,
321 /// The `order` specification text, if any.
322 order: Option<String>,
323 },
324}
325
326impl ForSource {
327 /// A traversal source over a comprehension, its text the tree's
328 /// canonical text
329 /// ([`Comprehension::to_text`](crate::comprehension::Comprehension::to_text)).
330 /// `None` when the
331 /// tree is outside the text grammar, so a source never carries a
332 /// text that reads back as a different comprehension.
333 pub fn comprehension(tree: crate::comprehension::Comprehension, span: Span) -> Option<Self> {
334 // The guarantee `to_text` rests on, and it is the round trip,
335 // not merely the existence of a text: this tree must write as
336 // text that reads back as this tree. A tree that writes
337 // something meaning anything else — or nothing the parser
338 // accepts — has no source, so the shape can never reach a
339 // program and be projected as one comprehension while
340 // traversing another.
341 let text = tree.to_text()?;
342 if crate::comprehension::spec::parse_comprehension_algebra(&text).ok()? != tree {
343 return None;
344 }
345 Some(ForSource {
346 kind: ForSourceKind::Comprehension(tree),
347 span,
348 })
349 }
350
351 /// A traversal source naming a producer bound in the same scope.
352 pub fn producer(name: impl Into<String>, span: Span) -> Self {
353 ForSource {
354 kind: ForSourceKind::Producer(name.into()),
355 span,
356 }
357 }
358
359 /// A traversal source deriving from a bound producer: the base
360 /// with an optional filter and order, written as the text writes
361 /// them.
362 pub fn derived(
363 base: impl Into<String>,
364 filter: Option<String>,
365 order: Option<String>,
366 span: Span,
367 ) -> Self {
368 ForSource {
369 kind: ForSourceKind::Derived {
370 base: base.into(),
371 filter,
372 order,
373 },
374 span,
375 }
376 }
377
378 /// The source as the text after `for` writes it, rendered from
379 /// what it denotes.
380 ///
381 /// A comprehension renders through
382 /// [`Comprehension::to_text`](crate::comprehension::Comprehension::to_text),
383 /// a producer is its name, and a derivation is its base with the
384 /// `where` and `order` it carries. Re-reading the result yields
385 /// the same source, so nothing downstream can traverse one thing
386 /// and report another.
387 ///
388 /// Total for every source that exists: [`Self::comprehension`]
389 /// refuses a tree the text cannot write, so the fallback below is
390 /// unreachable through the constructors. It renders a form no
391 /// parser accepts rather than a plausible one, so a source that
392 /// somehow evaded them fails loudly at the next read instead of
393 /// quietly meaning something else.
394 pub fn to_text(&self) -> String {
395 match &self.kind {
396 ForSourceKind::Producer(name) => name.clone(),
397 ForSourceKind::Comprehension(tree) => tree
398 .to_text()
399 .unwrap_or_else(|| "«comprehension with no text form»".to_string()),
400 ForSourceKind::Derived {
401 base,
402 filter,
403 order,
404 } => {
405 let mut text = base.clone();
406 if let Some(predicate) = filter {
407 text.push_str(&format!(" where {predicate}"));
408 }
409 if let Some(spec) = order {
410 text.push_str(&format!(" order {spec}"));
411 }
412 text
413 }
414 }
415 }
416
417 /// The element names the source dispenses, when known statically.
418 /// A producer reference or derivation resolves its names at compile
419 /// time.
420 pub fn element_names(&self) -> Vec<String> {
421 match &self.kind {
422 ForSourceKind::Producer(_) | ForSourceKind::Derived { .. } => Vec::new(),
423 ForSourceKind::Comprehension(c) => c.coordinate_names(),
424 }
425 }
426}
427
428/// A traversal statement: `for <source> { statements }`.
429#[derive(Debug, Clone)]
430pub struct ForStmt {
431 /// What the traversal iterates.
432 pub source: ForSource,
433 /// The body: one child scope per tuple.
434 pub body: Vec<Statement>,
435 /// Where the statement appears.
436 pub span: Span,
437}
438
439/// An external input port declaration.
440///
441/// Ports persist across `set_inputs()` calls within a stanza.
442/// Written by capture extraction, read by Polydat nodes.
443///
444/// ```text
445/// extern balance: f64 = 0.0
446/// extern session_id: u64 = 0
447/// ```
448#[derive(Debug, Clone)]
449pub struct ExternPort {
450 /// The port's name.
451 pub name: String,
452 /// The declared type keyword.
453 pub typ: String,
454 /// The default value, if one was written; without one the port is `None` until set.
455 pub default: Option<Expr>,
456 /// Where the declaration appears.
457 pub span: Span,
458}
459
460/// One per-cycle kernel input slot.
461///
462/// Declared by `input <name>[: <type>]` (single) or
463/// `input (<name>[: <type>], ...)` (tuple, sugar for N decls).
464/// The name participates in the kernel's input-port wiring just
465/// like `extern` participates in its port set, but inputs are
466/// driven by the runtime cycle pump (cursors, captures, etc.)
467/// rather than by external port writes.
468///
469/// `ty` is `None` when the author omitted the annotation; typed
470/// downstream by inference. Authors are encouraged to declare
471/// the type for clarity and editor support.
472#[derive(Debug, Clone)]
473pub struct InputDecl {
474 /// The input's name.
475 pub name: String,
476 /// The declared type keyword, if one was written.
477 pub ty: Option<String>,
478 /// Where the declaration appears.
479 pub span: Span,
480}
481
482/// One wire-coloring keyword. The single enum that names every
483/// modifier the grammar recognises before a binding name.
484/// Future modifiers are new variants here.
485///
486/// Each variant maps to a token the lexer emits and a parser
487/// branch in `parse_modified_binding`. A binding can carry zero
488/// or more of these, stored as a [`BindingModifier`] set.
489#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
490pub enum WireModifier {
491 /// `const` — effectively-const for the scope's lifetime.
492 /// Materialized at the earliest opportunity: compile-time
493 /// const-fold when the RHS is fold-eligible, otherwise the
494 /// scope-init pull pass after materialize-wiring has
495 /// populated extern slots. The runtime contract is "fixed
496 /// once per scope activation, then immutable for the rest of
497 /// the scope's lifetime." Replaces the former `final` /
498 /// `init` distinction — the two were redundant axes of the
499 /// same lifecycle, and the surface now collapses to one
500 /// keyword whose materialization timing is an internal
501 /// optimization.
502 Const,
503 /// `shared` — mutable cell visible across kernel instances.
504 /// The runtime propagates iteration N's end state into
505 /// iteration N+1's start state.
506 Shared,
507 /// `volatile` — the binding is re-evaluated on every pull and
508 /// never constant-folded, even where its inputs would allow a
509 /// fold. The program's canonical hash (`scope_model.md` §8)
510 /// covers the binding's expression and the `volatile` flag; no
511 /// value the binding produces is hashed.
512 Volatile,
513}
514
515/// Set of wire modifiers carried by one binding declaration.
516/// Stored as a bitset under the hood; consumers use
517/// [`Self::has`] to test for individual modifiers and
518/// [`Self::insert`] / `Self::from_iter` to build instances.
519///
520/// **Validity:** the combinations `const` + `volatile` and
521/// `const` + `shared` are rejected at parse time as contradictory
522/// (`Self::try_from_iter` is the validating builder). All other
523/// combinations are representable.
524///
525/// Lives on every [`Statement::Binding`] — the modifier set
526/// determines the binding's lifecycle. Other statement kinds
527/// (`ExternPort`, `InputDecl`, etc.) don't carry modifiers
528/// because their semantics are fixed by their statement form.
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
530pub struct BindingModifier {
531 bits: u8,
532}
533
534impl BindingModifier {
535 /// All-modifiers-off; the default state of an unannotated
536 /// binding (per-cycle).
537 pub const NONE: Self = Self { bits: 0 };
538
539 /// Single-modifier convenience constants. Tests reach for
540 /// these to express their intent compactly.
541 pub const CONST: Self = Self {
542 bits: Self::bit(WireModifier::Const),
543 };
544 /// The `shared` modifier alone.
545 pub const SHARED: Self = Self {
546 bits: Self::bit(WireModifier::Shared),
547 };
548 /// The `volatile` modifier alone.
549 pub const VOLATILE: Self = Self {
550 bits: Self::bit(WireModifier::Volatile),
551 };
552
553 /// `true` iff `m` is set.
554 pub const fn has(&self, m: WireModifier) -> bool {
555 self.bits & Self::bit(m) != 0
556 }
557
558 /// `true` iff at least one modifier is set.
559 pub const fn has_any(&self) -> bool {
560 self.bits != 0
561 }
562
563 /// Add `m` to the set.
564 pub fn insert(&mut self, m: WireModifier) {
565 self.bits |= Self::bit(m);
566 }
567
568 /// Build a modifier set from an iterator of variants. The
569 /// parser uses this after collecting tokens. Rejects the
570 /// contradictory `const` + `volatile` combo with a clear
571 /// error.
572 pub fn try_from_iter<I: IntoIterator<Item = WireModifier>>(
573 items: I,
574 ) -> Result<Self, &'static str> {
575 let mut out = Self::NONE;
576 for m in items {
577 out.insert(m);
578 }
579 if out.has(WireModifier::Const) && out.has(WireModifier::Volatile) {
580 return Err(
581 "modifier conflict: `const` and `volatile` are contradictory \
582 — `const` materializes the value once and freezes it; \
583 `volatile` excludes the wire from const-fold and signals \
584 per-cycle variability. Drop one.",
585 );
586 }
587 if out.has(WireModifier::Const) && out.has(WireModifier::Shared) {
588 return Err("modifier conflict: `const` and `shared` are contradictory \
589 — `const` fixes the value for the kernel's life; `shared` \
590 makes the binding a register other scopes write. Drop one: \
591 `shared` alone takes a computed starting value.");
592 }
593 Ok(out)
594 }
595
596 /// Iterate the modifiers in the set, in fixed declaration
597 /// order (`Const`, `Shared`, `Volatile`). Used for
598 /// re-emission and stable hash output.
599 pub fn iter(&self) -> impl Iterator<Item = WireModifier> + '_ {
600 const ORDER: &[WireModifier] = &[
601 WireModifier::Const,
602 WireModifier::Shared,
603 WireModifier::Volatile,
604 ];
605 ORDER.iter().copied().filter(move |m| self.has(*m))
606 }
607
608 /// Direct field-style accessors retained for sites that
609 /// pattern-match on individual flags. Mechanically derive
610 /// from `has(...)` so adding a new modifier is one variant
611 /// + one bit assignment + (optionally) one accessor.
612 #[inline]
613 pub const fn is_const(&self) -> bool {
614 self.has(WireModifier::Const)
615 }
616 #[inline]
617 /// `true` iff `shared` is set.
618 pub const fn is_shared(&self) -> bool {
619 self.has(WireModifier::Shared)
620 }
621 #[inline]
622 /// `true` iff `volatile` is set.
623 pub const fn is_volatile(&self) -> bool {
624 self.has(WireModifier::Volatile)
625 }
626
627 /// Compile-time bit index for a modifier.
628 const fn bit(m: WireModifier) -> u8 {
629 match m {
630 WireModifier::Const => 1 << 0,
631 WireModifier::Shared => 1 << 1,
632 WireModifier::Volatile => 1 << 2,
633 }
634 }
635}
636
637/// A name-to-expression binding (per-cycle by default;
638/// `const`/`shared`/`volatile` modifier changes the
639/// lifecycle). Replaces the former `CycleBinding` and
640/// `InitBinding` AST variants — the surface unified to one
641/// shape `name := expr` (or `(a, b, c) := expr` for tuple
642/// destructuring), with the modifier driving runtime
643/// lifecycle.
644#[derive(Debug, Clone)]
645pub struct Binding {
646 /// The bound names: one, or several for tuple destructuring.
647 pub targets: Vec<String>,
648 /// The right-hand side.
649 pub value: Expr,
650 /// The wire-coloring keywords written before the names.
651 pub modifier: BindingModifier,
652 /// Optional explicit type annotation — `shared name: f64 := 1`.
653 /// Only meaningful on `shared` bindings (scope_model.md §"Type
654 /// stability"): it pins the CELL's PortType for life, winning over
655 /// literal inference (so `1` vs `1.0` stops being load-bearing).
656 /// The parser rejects annotations on non-shared bindings.
657 pub type_annotation: Option<String>,
658 /// Where the binding appears.
659 pub span: Span,
660}
661
662/// A cursor declaration: `cursor name = Cursor() [over partition_source]`
663///
664/// Declares a named positional cursor. The cursor's extent is
665/// discovered at init time by interrogating its downstream consumers
666/// for cardinality. The runtime advances the cursor to drive
667/// phase iteration.
668///
669/// The optional `over` clause (cursor_partitions.md §7.2) names a partition source
670/// — an in-scope wire that resolves to a `Partition` or a
671/// `PartitionList`. When bound, the cursor's effective extent
672/// narrows to the named partition's `[start_ord, end_ord)`
673/// range; without it, the cursor uses its full declared extent.
674#[derive(Debug, Clone)]
675pub struct CursorDecl {
676 /// The cursor's name.
677 pub name: String,
678 /// The constructor call, such as `range(0, 100)`.
679 pub constructor: Expr,
680 /// The `over <expr>` clause (cursor_partitions.md §7.2). The expression is parsed
681 /// the same way as any other Polydat expression so authors can
682 /// name a workload parameter's `.partitions` projection
683 /// (e.g. `cursor.partitions`), an iter-var bound by an
684 /// enclosing `for:`, or a sibling cursor's `.cursor`
685 /// projection (`q1.cursor`). `None` means no narrowing —
686 /// the cursor uses its full declared extent.
687 pub over: Option<Expr>,
688 /// Where the declaration appears.
689 pub span: Span,
690}
691
692/// An expression (right-hand side of a binding).
693#[derive(Debug, Clone)]
694pub enum Expr {
695 /// A bare identifier referencing a wire or init binding: `cycle`, `lut`
696 Ident(String, Span),
697 /// An integer literal: `1000`
698 IntLit(u64, Span),
699 /// A float literal: `72.0`
700 FloatLit(f64, Span),
701 /// A string literal (may contain `{name}` interpolation): `"hello {name}"`
702 StringLit(String, Span),
703 /// An array literal: `[60.0, 20.0, 15.0]`
704 ArrayLit(Vec<Expr>, Span),
705 /// A function call: `hash(cycle)`, `dist_normal(mean: 72.0, stddev: 5.0)`
706 Call(CallExpr),
707 /// A binary arithmetic operation: `a + b`, `x * 0.25`.
708 /// Desugared by the compiler into the equivalent function call.
709 BinOp(Box<Expr>, BinOpKind, Box<Expr>),
710 /// Unary negation: `-x`.
711 /// Desugared to `f64_sub(0.0, x)`.
712 UnaryNeg(Box<Expr>, Span),
713 /// Unary bitwise NOT: `!x`.
714 /// Desugared to `u64_not(x)`.
715 UnaryBitNot(Box<Expr>, Span),
716 /// Source field projection: `base.ordinal`, `base.vector`.
717 /// Resolved by the compiler to a node that reads from the source item.
718 FieldAccess {
719 /// The wire the field is read from.
720 source: String,
721 /// The field's name.
722 field: String,
723 /// Where the projection appears.
724 span: Span,
725 },
726 /// `<expr> as <type>` — a type-coercion cast (polydat_grammar.md
727 /// §10). An *optional, alignment-only* type-fusion infill: a no-op
728 /// when the inner expression's type already matches the target,
729 /// otherwise the compiler inserts the catalog adapter
730 /// (type_system.md §3), or errors if the catalog has none. The
731 /// cast's type is its target.
732 Cast(Box<Expr>, crate::PortType, Span),
733 /// `for <comprehension>` in expression position — a comprehension
734 /// producer (for_traversal.md §3.1). Binds a `Streamer` wire.
735 For(Box<ForSource>),
736}
737
738/// Binary arithmetic operator kind.
739#[derive(Debug, Clone, Copy, PartialEq, Eq)]
740pub enum BinOpKind {
741 /// `+` — desugars to `u64_add` or `f64_add` based on operand types
742 Add,
743 /// `-` — desugars to `u64_sub` or `f64_sub` based on operand types
744 Sub,
745 /// `*` — desugars to `u64_mul` or `f64_mul` based on operand types
746 Mul,
747 /// `/` — desugars to `u64_div` or `f64_div` based on operand types
748 Div,
749 /// `%` — desugars to `u64_mod` or `f64_mod` based on operand types
750 Mod,
751 /// `**` — desugars to `pow(a, b)` (always f64)
752 Pow,
753 /// `&` — desugars to `u64_and(a, b)`
754 BitAnd,
755 /// `|` — desugars to `u64_or(a, b)`
756 BitOr,
757 /// `^` — desugars to `u64_xor(a, b)`
758 BitXor,
759 /// `<<` — desugars to `u64_shl(a, b)`
760 Shl,
761 /// `>>` — desugars to `u64_shr(a, b)`
762 Shr,
763 /// `==` — desugars to `u64_eq` / `f64_eq`. Output type is `u64`
764 /// (0 = false, 1 = true).
765 Eq,
766 /// `!=` — desugars to `u64_ne` / `f64_ne`. Output type is `u64`.
767 Ne,
768 /// `<` — desugars to `u64_lt` / `f64_lt`. Output type is `u64`.
769 Lt,
770 /// `>` — desugars to `u64_gt` / `f64_gt`. Output type is `u64`.
771 Gt,
772 /// `<=` — desugars to `u64_le` / `f64_le`. Output type is `u64`.
773 Le,
774 /// `>=` — desugars to `u64_ge` / `f64_ge`. Output type is `u64`.
775 Ge,
776 /// `&&` — eager logical-and (polydat_grammar.md §7). Desugars to
777 /// `u64_and(a != 0, b != 0)`: both operands evaluate, each is
778 /// normalised to truthiness (`0`/`1`), and the bitwise-and of two
779 /// truthiness values is logical-and. Output type is `u64` (`0`/`1`).
780 /// Lowest precedence, below comparison.
781 And,
782 /// `||` — eager logical-or (polydat_grammar.md §7). Desugars to
783 /// `u64_or(a != 0, b != 0)`. Output type is `u64` (`0`/`1`). Binds
784 /// looser than `&&`.
785 Or,
786}
787
788/// A typed parameter in a module signature.
789#[derive(Debug, Clone)]
790pub struct TypedParam {
791 /// The parameter's name.
792 pub name: String,
793 /// The declared type keyword.
794 pub typ: String, // "u64", "f64", "String", "bytes", etc.
795}
796
797/// A formal module definition with typed interface.
798///
799/// ```text
800/// hash_range(input: u64, max: u64) -> (value: u64) := {
801/// h := hash(input)
802/// value := mod(h, max)
803/// }
804/// ```
805#[derive(Debug, Clone)]
806pub struct ModuleDef {
807 /// The module's name.
808 pub name: String,
809 /// The typed inputs, in signature order.
810 pub params: Vec<TypedParam>,
811 /// The typed outputs, in signature order.
812 pub outputs: Vec<TypedParam>,
813 /// The module body.
814 pub body: Vec<Statement>,
815 /// Where the definition appears.
816 pub span: Span,
817}
818
819/// A function call expression.
820#[derive(Debug, Clone)]
821pub struct CallExpr {
822 /// The function's name.
823 pub func: String,
824 /// The arguments, in call order.
825 pub args: Vec<Arg>,
826 /// Where the call appears.
827 pub span: Span,
828}
829
830/// A function argument: positional or named.
831#[derive(Debug, Clone)]
832pub enum Arg {
833 /// Positional: just an expression
834 Positional(Expr),
835 /// Named: `name: expr`
836 Named(String, Expr),
837}
838
839#[cfg(test)]
840mod modifier_tests {
841 use super::*;
842
843 #[test]
844 fn empty_set_has_no_modifiers() {
845 let m = BindingModifier::NONE;
846 assert!(!m.has_any());
847 assert!(!m.is_const() && !m.is_shared() && !m.is_volatile());
848 }
849
850 #[test]
851 fn single_modifier_consts_match_expected_flags() {
852 assert!(BindingModifier::CONST.is_const());
853 assert!(!BindingModifier::CONST.is_shared());
854 assert!(!BindingModifier::CONST.is_volatile());
855
856 assert!(BindingModifier::SHARED.is_shared());
857 assert!(!BindingModifier::SHARED.is_const());
858
859 assert!(BindingModifier::VOLATILE.is_volatile());
860 assert!(!BindingModifier::VOLATILE.is_const());
861 }
862
863 #[test]
864 fn from_iter_collects_combinations() {
865 let m = BindingModifier::try_from_iter([WireModifier::Shared, WireModifier::Volatile])
866 .expect("shared+volatile is valid");
867 assert!(m.is_shared() && m.is_volatile());
868 assert!(!m.is_const());
869 }
870
871 #[test]
872 fn from_iter_rejects_const_plus_shared() {
873 let err = BindingModifier::try_from_iter([WireModifier::Shared, WireModifier::Const])
874 .expect_err("const+shared must be rejected");
875 assert!(
876 err.contains("const") && err.contains("shared") && err.contains("register"),
877 "error should name both keywords and why: {err}"
878 );
879 }
880
881 #[test]
882 fn from_iter_rejects_const_plus_volatile() {
883 let err = BindingModifier::try_from_iter([WireModifier::Const, WireModifier::Volatile])
884 .expect_err("const+volatile must be rejected");
885 assert!(
886 err.contains("const") && err.contains("volatile"),
887 "error should name both keywords: {err}"
888 );
889 }
890
891 #[test]
892 fn from_iter_rejects_const_shared_volatile() {
893 // Triple combination subsumes the contradiction.
894 let err = BindingModifier::try_from_iter([
895 WireModifier::Const,
896 WireModifier::Shared,
897 WireModifier::Volatile,
898 ])
899 .expect_err("triple combo includes the contradictory pair");
900 assert!(err.contains("const") && err.contains("volatile"));
901 }
902
903 #[test]
904 fn iter_yields_modifiers_in_stable_order() {
905 let m =
906 BindingModifier::try_from_iter([WireModifier::Volatile, WireModifier::Shared]).unwrap();
907 // Insertion order was Volatile, Shared — but iter yields
908 // in fixed declaration order: Const, Shared, Volatile.
909 let collected: Vec<_> = m.iter().collect();
910 assert_eq!(
911 collected,
912 vec![WireModifier::Shared, WireModifier::Volatile]
913 );
914 }
915
916 #[test]
917 fn equality_distinguishes_combinations() {
918 let shared_only = BindingModifier::SHARED;
919 let shared_volatile =
920 BindingModifier::try_from_iter([WireModifier::Shared, WireModifier::Volatile]).unwrap();
921 assert_ne!(
922 shared_only, shared_volatile,
923 "shared-only must not equal shared+volatile"
924 );
925 }
926}