Skip to main content

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}