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