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}