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