Skip to main content

bynk_syntax/
ast.rs

1//! Abstract syntax tree types for Bynk v0 (spec §9.2).
2
3use crate::span::Span;
4
5/// An identifier with its source span.
6#[derive(Debug, Clone)]
7pub struct Ident {
8    pub name: String,
9    pub span: Span,
10}
11
12/// Comment trivia attached to a declaration or statement (v1.1 LSP spec
13/// §3.5). The parser collects line comments from the token stream and
14/// attaches them to nearby AST nodes so the formatter can re-emit them.
15///
16/// - `leading` holds comments that appear immediately above the node,
17///   ordered top-to-bottom: each a `--` line, or an orphaned `---` doc block
18///   (see [`Comment`]). Above a unit-level declaration it also records where a
19///   blank line separated them ([`Comment::Blank`]).
20/// - `trailing` holds a single comment that appears on the same source
21///   line as the node's final token (e.g. `expr  -- note`).
22#[derive(Debug, Clone, Default)]
23pub struct Trivia {
24    pub leading: Vec<Comment>,
25    pub trailing: Option<String>,
26}
27
28/// One entry of comment trivia.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum Comment {
31    /// A `--` line comment: the text after the marker, with its original
32    /// inline whitespace preserved.
33    Line(String),
34    /// #1756: a doc (a `---` block, or since #1888 a run of `--|` lines) that
35    /// attaches to no declaration (a blank line separates it from the next
36    /// one, or nothing follows it). The parser warns
37    /// `bynk.parse.orphan_doc_block` and keeps the block here, so the formatter
38    /// can print it where it was. Its content is normalised as an attached
39    /// doc's is.
40    OrphanDoc(Doc),
41    /// #1884: a blank line after a `--` comment, before the next comment or
42    /// the declaration. Recorded only where canonical style sets declarations
43    /// apart with blank lines: in the leading and closing comments of a
44    /// unit-level declaration (an item, a `uses`/`consumes`/`exports` clause,
45    /// the unit header), and of a service's sections and handlers or an agent's
46    /// invariants, transitions and handlers. There it keeps a comment that is
47    /// separated from the declaration below from reading as describing it.
48    /// Elsewhere (statements, record fields, capability ops, policy fields,
49    /// store fields) style has no blank lines, so the parser drops it. Several
50    /// blank lines are one entry.
51    Blank,
52}
53
54/// #1888: a documentation comment as written: its normalised text and the
55/// form it was written in. Every `documentation` field holds one. It derefs to
56/// its text, so a reader that only wants the Markdown treats it as a `str`;
57/// only the formatter reads [`Doc::form`], to print each doc in the form its
58/// author chose (DECISION F: it never converts between them).
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Doc {
61    /// The Markdown text: a `---` block's content with its common indent
62    /// stripped, or the `--|` lines' text joined with newlines.
63    pub text: String,
64    pub form: DocForm,
65}
66
67/// #1888: which of the two doc forms a [`Doc`] was written in.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
69pub enum DocForm {
70    /// A `---` fenced block (ADR 0188; matched-length fences since #1885).
71    #[default]
72    Block,
73    /// One or more consecutive `--|` lines.
74    Lines,
75}
76
77impl Doc {
78    pub fn new(text: impl Into<String>, form: DocForm) -> Self {
79        Self {
80            text: text.into(),
81            form,
82        }
83    }
84
85    pub fn as_str(&self) -> &str {
86        &self.text
87    }
88}
89
90impl std::ops::Deref for Doc {
91    type Target = str;
92    fn deref(&self) -> &str {
93        &self.text
94    }
95}
96
97/// A doc built from bare text is a `---` block, the form every doc had before
98/// #1888.
99impl From<String> for Doc {
100    fn from(text: String) -> Self {
101        Self::new(text, DocForm::Block)
102    }
103}
104
105impl From<&str> for Doc {
106    fn from(text: &str) -> Self {
107        Self::new(text, DocForm::Block)
108    }
109}
110
111impl std::fmt::Display for Doc {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        f.write_str(&self.text)
114    }
115}
116
117impl Trivia {
118    pub fn is_empty(&self) -> bool {
119        self.leading.is_empty() && self.trailing.is_none()
120    }
121}
122
123/// A whole parsed commons source file.
124///
125/// In v0.3 a commons may be split across multiple files in a directory; the
126/// resolver merges them into one logical commons. Each parsed AST instance
127/// represents the contribution from a single source file.
128#[derive(Debug, Clone)]
129pub struct Commons {
130    pub name: QualifiedName,
131    pub items: Vec<CommonsItem>,
132    /// `uses` clauses declared in this file.
133    pub uses: Vec<UsesDecl>,
134    /// Optional documentation block attached to the commons declaration.
135    pub documentation: Option<Doc>,
136    /// Surface form of the file: brace-delimited body or headerless fragment.
137    pub form: CommonsForm,
138    pub span: Span,
139    /// Trivia attached to the commons declaration itself — leading comments
140    /// before the `commons` keyword and a trailing comment after the header
141    /// or closing brace.
142    pub trivia: Trivia,
143    /// Comments appearing after the last item but before the file ends
144    /// (or the closing brace, for brace form). One entry per `--` line.
145    pub trailing_comments: Vec<Comment>,
146}
147
148/// The two surface forms in which a commons body may be parsed (v0.3 §3.1).
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum CommonsForm {
151    /// `commons name { ... }`
152    Brace,
153    /// `commons name` followed by top-level declarations to EOF.
154    Fragment,
155}
156
157/// A `uses other.commons` declaration (v0.3 §3.3).
158#[derive(Debug, Clone)]
159pub struct UsesDecl {
160    pub target: QualifiedName,
161    pub span: Span,
162    pub trivia: Trivia,
163    /// A blank line separated this header line's leading comment from the
164    /// line above (#2893). The formatter keeps it, so the comment still reads
165    /// as belonging to this line. Never set without a leading comment.
166    pub blank_before: bool,
167}
168
169/// A whole parsed context source file (v0.4 §3.1).
170///
171/// Contexts are the architectural-layer declaration kind. Like commons, a
172/// context may be split across multiple files in a directory.
173#[derive(Debug, Clone)]
174pub struct Context {
175    pub name: QualifiedName,
176    pub items: Vec<CommonsItem>,
177    /// `uses` clauses declared in this file.
178    pub uses: Vec<UsesDecl>,
179    /// `consumes` clauses declared in this file.
180    pub consumes: Vec<ConsumesDecl>,
181    /// `exports` clauses declared in this file.
182    pub exports: Vec<ExportsDecl>,
183    /// Optional documentation block attached to the context declaration.
184    pub documentation: Option<Doc>,
185    /// Surface form of the file: brace-delimited body or headerless fragment.
186    pub form: CommonsForm,
187    pub span: Span,
188    /// Trivia attached to the context declaration itself — leading comments
189    /// before the `context` keyword.
190    pub trivia: Trivia,
191    /// Comments appearing after the last item but before the file ends
192    /// (or the closing brace, for brace form). One entry per `--` line.
193    pub trailing_comments: Vec<Comment>,
194    /// State-migrations S3 (#2248): `removed agent Old` declarations. Kept
195    /// beside `items` rather than as a `CommonsItem`: they declare a class's
196    /// *absence* (its stored data is deleted), so nothing that walks the
197    /// unit's items has anything to do with them.
198    pub removed_agents: Vec<RemovedAgentDecl>,
199}
200
201/// `removed agent Old` (state-migrations S3, #2248): the agent class `Old`
202/// is gone, and its stored data is deleted through an `exports` tombstone.
203#[derive(Debug, Clone)]
204pub struct RemovedAgentDecl {
205    /// The contextual `removed` keyword.
206    pub keyword: Span,
207    pub name: Ident,
208    pub documentation: Option<Doc>,
209    pub span: Span,
210    pub trivia: Trivia,
211}
212
213/// A `consumes other.context` declaration (v0.4 §3.2). May optionally carry
214/// an alias introduced by `consumes other.context as Alias` (v0.6 §3.1).
215#[derive(Debug, Clone)]
216pub struct ConsumesDecl {
217    pub target: QualifiedName,
218    pub alias: Option<Ident>,
219    /// v0.17: `consumes U { Cap, … }` — selected capabilities flattened into
220    /// the consumer's local capability namespace under their bare names (§3.3).
221    /// `None` for the whole-unit forms; `Some` (possibly empty) for the braced
222    /// form. Mutually exclusive with `alias`.
223    pub selected: Option<Vec<ConsumesItem>>,
224    pub span: Span,
225    pub trivia: Trivia,
226    /// A blank line separated this header line's leading comment from the
227    /// line above (#2893). The formatter keeps it, so the comment still reads
228    /// as belonging to this line. Never set without a leading comment.
229    pub blank_before: bool,
230}
231
232/// One name in a braced `consumes U { … }` list: a capability or exported type,
233/// optionally renamed for this context with `Name as Alias` (#2887).
234#[derive(Debug, Clone)]
235pub struct ConsumesItem {
236    pub name: Ident,
237    /// `Digest as CryptoDigest`: the exported type is in scope here only as the
238    /// alias, which sidesteps a clash with a local or `uses` type of that name.
239    pub alias: Option<Ident>,
240}
241
242/// An `exports visibility { names }` clause (v0.4 §3.3) or, v0.15, an
243/// `exports capability { names }` clause.
244#[derive(Debug, Clone)]
245pub struct ExportsDecl {
246    pub kind: ExportKind,
247    pub names: Vec<ExportName>,
248    pub span: Span,
249    pub trivia: Trivia,
250    /// A blank line separated this header line's leading comment from the
251    /// line above (#2893). The formatter keeps it, so the comment still reads
252    /// as belonging to this line. Never set without a leading comment.
253    pub blank_before: bool,
254    /// #1797: comments before the closing `}`.
255    pub trailing_comments: Vec<Comment>,
256}
257
258/// One name in an `exports` list, with its comments (#1797).
259#[derive(Debug, Clone)]
260pub struct ExportName {
261    pub name: Ident,
262    /// #1797: comments above the name and at the end of its line. A comment
263    /// on the list's `{` line leads the first name.
264    pub trivia: Trivia,
265}
266
267/// What an `exports` clause exposes: types (with a visibility) or, v0.15,
268/// capabilities offered for cross-context consumption.
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub enum ExportKind {
271    /// `exports opaque { ... }` / `exports transparent { ... }` — type exports.
272    Type(Visibility),
273    /// `exports capability { ... }` — capabilities offered to consumers (v0.15).
274    Capability,
275}
276
277/// Visibility level for an exports clause (v0.4 §3.3).
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum Visibility {
280    /// Token-only outside the context: hold, pass, compare; no inspect, no construct.
281    Opaque,
282    /// Readable shape outside the context: inspect fields, match variants; no construct.
283    Transparent,
284}
285
286/// An `adapter qualified.name { … }` declaration (v0.17 §3.1). An adapter
287/// co-locates a capability contract with a non-Bynk binding: it may declare
288/// capabilities, the boundary types they reference, inline pure helper
289/// `type`/`fn` (and `uses`), external (bodiless) providers, `exports
290/// capability`, and exactly one `binding` clause. It may *not* declare
291/// services, agents, or bodied providers. Like commons/contexts it may be
292/// split across files in a directory.
293#[derive(Debug, Clone)]
294pub struct AdapterDecl {
295    pub name: QualifiedName,
296    pub items: Vec<CommonsItem>,
297    /// `uses` clauses declared in this file (pure-vocabulary mixin; allowed
298    /// because helpers cannot pierce containment — spec [DECISION B]).
299    pub uses: Vec<UsesDecl>,
300    /// `exports capability { … }` clauses (adapters export capabilities and
301    /// boundary types, never services).
302    pub exports: Vec<ExportsDecl>,
303    /// v0.18: `consumes U { Cap, … }` clauses — adapter-to-adapter capability
304    /// dependencies (spec §4.5, \[N\]). Braced form only; adapter targets only
305    /// (both enforced semantically, not in the parser).
306    pub consumes: Vec<ConsumesDecl>,
307    /// The `binding "<module>" requires { … }` clause, if present. Required
308    /// when the adapter declares any external provider (`bynk.adapter.no_binding`).
309    pub binding: Option<BindingDecl>,
310    pub documentation: Option<Doc>,
311    pub form: CommonsForm,
312    pub span: Span,
313    pub trivia: Trivia,
314    pub trailing_comments: Vec<Comment>,
315}
316
317/// A `binding "<module>" [includes [ … ]] requires { "pkg": "range", … }` clause inside an
318/// adapter (v0.17 §3.5). `module` is the TypeScript module supplying the
319/// adapter's external provider symbols, resolved relative to the adapter's
320/// source file. `requires` declares npm dependencies folded into the
321/// generated `package.json`.
322#[derive(Debug, Clone)]
323pub struct BindingDecl {
324    /// The module path as written (the string-literal contents, no quotes).
325    pub module: String,
326    pub module_span: Span,
327    /// #2321: the `includes ["./x.wasm", …]` files shipped beside the binding,
328    /// as written (string-literal contents, no quotes) with their spans.
329    pub includes: Vec<IncludeEntry>,
330    pub requires: Vec<RequiresDep>,
331    pub span: Span,
332    pub trivia: Trivia,
333}
334
335/// One path in a binding's `includes [ … ]` list (#2321).
336#[derive(Debug, Clone)]
337pub struct IncludeEntry {
338    pub path: String,
339    pub span: Span,
340}
341
342/// One `"pkg": "range"` entry in a binding's `requires { … }` map.
343#[derive(Debug, Clone)]
344pub struct RequiresDep {
345    pub package: String,
346    pub range: String,
347    pub span: Span,
348}
349
350/// Either a commons or a context — the two declaration kinds at the file
351/// level (v0.4 §3.1). v0.7 adds the test declaration kind; v0.17 the adapter.
352#[derive(Debug, Clone)]
353pub enum SourceUnit {
354    Commons(Commons),
355    Context(Context),
356    Suite(SuiteDecl),
357    /// v0.17: an `adapter` unit — the host boundary (capability contract +
358    /// external binding).
359    Adapter(AdapterDecl),
360}
361
362impl SourceUnit {
363    pub fn name(&self) -> &QualifiedName {
364        match self {
365            SourceUnit::Commons(c) => &c.name,
366            SourceUnit::Context(c) => &c.name,
367            SourceUnit::Suite(t) => &t.target,
368            SourceUnit::Adapter(a) => &a.name,
369        }
370    }
371
372    pub fn span(&self) -> Span {
373        match self {
374            SourceUnit::Commons(c) => c.span,
375            SourceUnit::Context(c) => c.span,
376            SourceUnit::Suite(t) => t.span,
377            SourceUnit::Adapter(a) => a.span,
378        }
379    }
380
381    /// The doc-block above this file's unit header, if it carries one. For a
382    /// `commons`/`context`/`adapter` this is the unit's *module doc* (#1885);
383    /// for a `suite` it is the suite's own doc.
384    pub fn documentation(&self) -> Option<&str> {
385        match self {
386            SourceUnit::Commons(c) => c.documentation.as_deref(),
387            SourceUnit::Context(c) => c.documentation.as_deref(),
388            SourceUnit::Suite(t) => t.documentation.as_deref(),
389            SourceUnit::Adapter(a) => a.documentation.as_deref(),
390        }
391    }
392
393    /// The doc-block above this file's unit header when it carries prose:
394    /// [`SourceUnit::documentation`], with an empty or all-whitespace block
395    /// treated as absent. This is what "a file carries a module doc" means
396    /// for the one-module-doc-per-unit rule and for the tooling that shows the
397    /// doc (#1885): an empty `---`/`---` pair documents nothing, so it neither
398    /// counts against the rule nor shadows a sibling file's real doc.
399    pub fn module_doc(&self) -> Option<&str> {
400        self.documentation().filter(|d| !d.trim().is_empty())
401    }
402
403    pub fn kind_name(&self) -> &'static str {
404        match self {
405            SourceUnit::Commons(_) => "commons",
406            SourceUnit::Context(_) => "context",
407            SourceUnit::Suite(_) => "suite",
408            SourceUnit::Adapter(_) => "adapter",
409        }
410    }
411}
412
413/// A `test <qualified-name> { ... }` declaration (v0.7 §3.1).
414///
415/// A test targets a commons or context by qualified name and bundles a set of
416/// test cases plus optional mock declarations. As with commons and contexts, a
417/// test may be split across multiple files (fragment form).
418#[derive(Debug, Clone)]
419pub struct SuiteDecl {
420    /// The targeted commons or context.
421    pub target: QualifiedName,
422    /// `uses` clauses brought in by this test fragment.
423    pub uses: Vec<UsesDecl>,
424    /// v0.118: suite-scoped `stub` clauses — per-seam provider overrides
425    /// applied to every case (a case-scoped `stub` takes precedence). Formerly
426    /// the punned `provides` stub; renamed to `stub` in the keyword-hygiene
427    /// batch (#548).
428    pub stubs: Vec<StubClause>,
429    /// #2134: suite-scoped `env { … }` clauses — the variables and secrets
430    /// every participant sees, for every case (a case's own `env` layers over
431    /// them name by name).
432    pub envs: Vec<EnvClause>,
433    /// The individual test cases.
434    pub cases: Vec<Case>,
435    /// v0.114: generative `property` blocks (testing track slice 2).
436    pub properties: Vec<PropertyDecl>,
437    /// #2841: `fixture name = "fixtures/…"` declarations — project files whose
438    /// text is a `String` constant in this suite's cases, never deployed.
439    pub fixtures: Vec<FixtureDecl>,
440    /// #2834: suite-local helper `fn`s — pure, private to this suite, called
441    /// from its `case` and `property` bodies and emitted only into test output.
442    pub fns: Vec<FnDecl>,
443    /// v0.118: the suite-level tier default (`suite … as integration`). `None`
444    /// means the `unit` default; a `case`'s own tier overrides it. A `property`
445    /// ignores a suite tier (tiers are a `case`-only affordance).
446    pub tier: Option<TestTier>,
447    /// Surface form: brace-delimited body or headerless fragment.
448    pub form: CommonsForm,
449    /// Optional documentation block attached to the test declaration.
450    pub documentation: Option<Doc>,
451    pub span: Span,
452    pub trivia: Trivia,
453    pub trailing_comments: Vec<Comment>,
454}
455
456/// v0.118: the tier a `case` runs at (testing track slice 6, ADR 0153). One
457/// body promoted across the testing pyramid; `unit` is the default and elided.
458#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459pub enum TestTier {
460    /// Collaborators stubbed (the default).
461    Unit,
462    /// Real collaborators within one context, no serialisation wire.
463    Integration,
464    /// Contexts wired across the real serialise → JSON → deserialise boundary.
465    System,
466}
467
468impl TestTier {
469    pub fn as_str(self) -> &'static str {
470        match self {
471            TestTier::Unit => "unit",
472            TestTier::Integration => "integration",
473            TestTier::System => "system",
474        }
475    }
476}
477
478/// v0.118: a per-seam provider override `stub Cap.method(<args>) returns <v>
479/// | fails` (testing track slice 6, ADR 0154; keyword `stub` since #548).
480/// Substitutes one capability method's provision under test; the right-hand
481/// side is a value or a fault, never a computed body.
482#[derive(Debug, Clone)]
483pub struct StubClause {
484    /// The capability being overridden (a consumed seam of the unit).
485    pub capability: Ident,
486    /// The overridden method.
487    pub method: Ident,
488    /// One argument pattern per parameter (`_` or a value the arg must equal).
489    pub args: Vec<ArgPattern>,
490    /// The provision: a value, a fault, a per-call sequence, or none at all.
491    pub rhs: StubRhs,
492    /// #2683: `returns <v> after <d>` — the call completes only once the
493    /// harness clock has advanced by `d` from the call. Only on `returns`.
494    pub after: Option<Expr>,
495    pub documentation: Option<Doc>,
496    pub span: Span,
497    pub trivia: Trivia,
498}
499
500/// #2134: a suite- or case-scoped `env { NAME = "value" … }` clause — the
501/// variables and secrets a suite's participants see (ADR for #2134). `env` is
502/// contextual: a keyword only at the head of a suite or case body member.
503#[derive(Debug, Clone)]
504pub struct EnvClause {
505    /// The entries, in source order.
506    pub entries: Vec<EnvEntry>,
507    pub documentation: Option<Doc>,
508    pub span: Span,
509    pub trivia: Trivia,
510}
511
512/// #2134: one `NAME = <value>` entry of an [`EnvClause`]. The value is parsed
513/// as an expression; the checker requires a `String` literal.
514#[derive(Debug, Clone)]
515pub struct EnvEntry {
516    pub name: Ident,
517    pub value: Expr,
518    pub span: Span,
519}
520
521/// v0.118: one argument pattern in a `stub` call pattern. Patterns for the
522/// same method are tried top-to-bottom, first match wins.
523#[derive(Debug, Clone)]
524pub enum ArgPattern {
525    /// `_` — matches any argument.
526    Any(Span),
527    /// A value the recorded argument must equal (a literal or pure value expr).
528    Value(Expr),
529}
530
531/// v0.118: the right-hand side of a `stub` clause.
532#[derive(Debug, Clone)]
533pub enum StubRhs {
534    /// `returns <value>` — a single success value, repeated for every call.
535    Returns(Expr),
536    /// `fails` — inject a capability fault (Principle 3).
537    Fails(Span),
538    /// `returns each [<outcome>, …]` — one outcome per call, in order; the last
539    /// outcome repeats once the sequence is exhausted (DECISION V).
540    ReturnsEach(Vec<SeqOutcome>, Span),
541    /// #2683: `never` — the call never completes. It exists to test a
542    /// `within` against an effect that does not finish.
543    Never(Span),
544}
545
546impl StubRhs {
547    pub fn span(&self) -> Span {
548        match self {
549            StubRhs::Returns(e) => e.span,
550            StubRhs::Fails(s) => *s,
551            StubRhs::ReturnsEach(_, s) => *s,
552            StubRhs::Never(s) => *s,
553        }
554    }
555}
556
557/// v0.118: one outcome in a sequenced (`returns each`) `stub`.
558#[derive(Debug, Clone)]
559pub enum SeqOutcome {
560    /// A success value.
561    Value(Expr),
562    /// A fault.
563    Fails(Span),
564}
565
566/// A `case "name" [as <tier>] { [stub …] body }` block inside a suite
567/// (v0.7 §3.3; v0.118 adds the tier clause and case-scoped stubs).
568#[derive(Debug, Clone)]
569pub struct Case {
570    /// The test name, taken from the string literal.
571    pub name: String,
572    /// The span of the string literal — used for diagnostics and runtime
573    /// failure reports.
574    pub name_span: Span,
575    /// v0.118: the case's own tier, if written (`as integration` / `as system`).
576    /// `None` means inherit the suite default (itself `unit` when unset).
577    pub tier: Option<TestTier>,
578    /// v0.118: case-scoped `stub` clauses (override the suite's, and the
579    /// tier default).
580    pub stubs: Vec<StubClause>,
581    /// #2134: case-scoped `env { … }` clauses, layered over the suite's.
582    pub envs: Vec<EnvClause>,
583    pub body: Block,
584    pub documentation: Option<Doc>,
585    pub span: Span,
586    pub trivia: Trivia,
587}
588
589/// A `property "name" { for all <bindings> [where <pred>] { body } }` block
590/// inside a suite (v0.114, testing track slice 2, ADR 0149). The generative
591/// sibling of [`Case`]: the runner draws inhabitants of each binding's type from
592/// its refinement domain and evaluates the body's `expect`s over them.
593#[derive(Debug, Clone)]
594pub struct PropertyDecl {
595    /// The property name, taken from the string literal.
596    pub name: String,
597    /// The span of the string literal — used for diagnostics and reports.
598    pub name_span: Span,
599    /// The `for all` binder: the generated bindings, an optional `where` filter,
600    /// and the predicate body.
601    pub forall: ForAll,
602    pub documentation: Option<Doc>,
603    pub span: Span,
604    pub trivia: Trivia,
605}
606
607/// The `for all x: T, … [where <pred>] { … }` binder inside a [`PropertyDecl`].
608#[derive(Debug, Clone)]
609pub struct ForAll {
610    /// The generated bindings, `x: T` (one or more).
611    pub bindings: Vec<ForAllBinding>,
612    /// An optional `where <pred>` filter (a pure `Bool`) applied to generated
613    /// tuples before the body runs.
614    pub where_pred: Option<Expr>,
615    /// The body — one or more statements, typically `expect`s.
616    pub body: Block,
617    pub span: Span,
618}
619
620/// One `for all` binding: `name: T`, where the runner generates inhabitants of
621/// `T` from its refinements.
622#[derive(Debug, Clone)]
623pub struct ForAllBinding {
624    pub name: Ident,
625    pub type_ref: TypeRef,
626}
627
628/// A capability reference in a `given` clause (v0.15 §3.2). A bare name is a
629/// local capability (`given Cap`); a dotted name refers to a capability a
630/// consumed context provides (`given B.Cap` / `given Alias.Cap`).
631#[derive(Debug, Clone)]
632pub struct CapRef {
633    /// `None` for a local capability; `Some(prefix)` for a cross-context
634    /// reference where `prefix` is a consumed-context qualified name or alias.
635    pub context: Option<QualifiedName>,
636    /// The capability's simple name (also the local deps key).
637    pub name: Ident,
638    pub span: Span,
639}
640
641impl CapRef {
642    /// The local deps key / capability simple name (e.g. `Clock`).
643    pub fn key(&self) -> &str {
644        &self.name.name
645    }
646
647    /// True when this references a capability provided by a consumed context.
648    pub fn is_cross_context(&self) -> bool {
649        self.context.is_some()
650    }
651
652    /// How the reference is written: `Cap`, or `B.Cap` for a consumed context's.
653    pub fn display_name(&self) -> String {
654        match self.prefix() {
655            Some(p) => format!("{p}.{}", self.key()),
656            None => self.key().to_string(),
657        }
658    }
659
660    /// The cross-context prefix (consumed-context qualified name or alias) as
661    /// a dotted string, if any.
662    pub fn prefix(&self) -> Option<String> {
663        self.context.as_ref().map(|q| q.joined())
664    }
665}
666
667/// A dotted name like `fitness.units`.
668#[derive(Debug, Clone)]
669pub struct QualifiedName {
670    pub parts: Vec<Ident>,
671    pub span: Span,
672}
673
674impl QualifiedName {
675    pub fn joined(&self) -> String {
676        self.parts
677            .iter()
678            .map(|p| p.name.as_str())
679            .collect::<Vec<_>>()
680            .join(".")
681    }
682}
683
684// Finding #31 shrank `Expr`/`ExprKind` enough that clippy's variance check
685// between this enum's smallest and largest variants (`Service`/`Actor` vs.
686// `Type`/`Fn`) now crosses its threshold — a pre-existing size profile made
687// newly visible, not something #31 itself is scoped to fix. Boxing
688// `ServiceDecl`/`ActorDecl` here is a separate, unscoped refactor (its own
689// blast radius across every `CommonsItem::Service`/`Actor` construction and
690// match site) left for a future finding.
691#[allow(clippy::large_enum_variant)]
692#[derive(Debug, Clone)]
693pub enum CommonsItem {
694    Type(TypeDecl),
695    Fn(FnDecl),
696    /// `capability Name { fn op(...) -> T ... }` (v0.5; contexts only).
697    Capability(CapabilityDecl),
698    /// `provides Cap = ProviderName { fn op(...) -> T { ... } ... }` (v0.5).
699    Provider(ProviderDecl),
700    /// `service Name { on call(...) -> T { ... } ... }` (v0.5).
701    Service(ServiceDecl),
702    /// `agent Name { key id: T; state { ... }; on call ... }` (v0.5).
703    Agent(AgentDecl),
704    /// `actor Name { auth = Scheme, identity = T }` (v0.45). A nominal boundary
705    /// contract consumed by a handler's `by` clause; not a runnable entity.
706    Actor(ActorDecl),
707    /// `messages <tag> @reference { "code" => "template" ... }` — a message
708    /// bundle for one locale. Commons-only (checker-enforced, not grammar);
709    /// legal syntactically wherever any `CommonsItem` is, per the existing
710    /// `Service`/`Agent`-in-`adapter` precedent.
711    Messages(MessagesDecl),
712    /// `event Name = { fields }` (Events track, slice 0, spine #936).
713    /// Context-only (checker-enforced, not grammar) — the mirror image of
714    /// `Messages`' commons-only restriction, same mechanism.
715    Event(EventDecl),
716    /// `asset name = "path"` (#1883, slice 1a). A static file the platform
717    /// serves beside the Worker. Context or commons ([DECISION G]); an
718    /// adapter is rejected by the checker, not the grammar, like `Event`.
719    Asset(AssetDecl),
720    /// `embed name = "path"` (#2387, slice 1). A project file's text as a
721    /// `String` constant. Context or commons; an adapter is rejected by the
722    /// checker, not the grammar, like `Asset`.
723    Embed(EmbedDecl),
724    /// `const Name: Type = <static expression>` (#2617). A named value of
725    /// the declared type, usable wherever an expression of that type is.
726    /// Context or commons; an adapter is rejected by the checker, not the
727    /// grammar, like `Embed`.
728    Const(ConstDecl),
729}
730
731impl CommonsItem {
732    /// The declaring identifier, when the item is named by one. `Messages` is
733    /// the sole `None`: its locale tag is a `LocaleTag` string literal
734    /// (`"pt-BR"`), not an identifier, and synthesising an `Ident` from it
735    /// would be a lie any identifier-shaped consumer (rename, go-to-def) would
736    /// eventually surface.
737    pub fn name(&self) -> Option<&Ident> {
738        match self {
739            CommonsItem::Type(t) => Some(&t.name),
740            CommonsItem::Fn(f) => Some(f.name.ident()),
741            CommonsItem::Capability(c) => Some(&c.name),
742            CommonsItem::Provider(p) => Some(&p.provider_name),
743            CommonsItem::Service(s) => Some(&s.name),
744            CommonsItem::Agent(a) => Some(&a.name),
745            CommonsItem::Actor(a) => Some(&a.name),
746            CommonsItem::Messages(_) => None,
747            CommonsItem::Event(e) => Some(&e.name),
748            CommonsItem::Asset(a) => Some(&a.name),
749            CommonsItem::Embed(e) => Some(&e.name),
750            CommonsItem::Const(c) => Some(&c.name),
751        }
752    }
753
754    /// #2618: the span of the item's `private` modifier, when it has one.
755    /// Only the kinds that may carry the modifier (`fn`, `type`, `messages`,
756    /// `asset`, `embed`, `const`) ever answer `Some`.
757    pub fn private_span(&self) -> Option<Span> {
758        match self {
759            CommonsItem::Type(t) => t.private,
760            CommonsItem::Fn(f) => f.private,
761            CommonsItem::Messages(m) => m.private,
762            CommonsItem::Asset(a) => a.private,
763            CommonsItem::Embed(e) => e.private,
764            CommonsItem::Const(c) => c.private,
765            CommonsItem::Capability(_)
766            | CommonsItem::Provider(_)
767            | CommonsItem::Service(_)
768            | CommonsItem::Agent(_)
769            | CommonsItem::Actor(_)
770            | CommonsItem::Event(_) => None,
771        }
772    }
773
774    /// #2618: is the item unit-private?
775    pub fn is_private(&self) -> bool {
776        self.private_span().is_some()
777    }
778
779    /// The whole declaration's span, from its first token to its last.
780    pub fn span(&self) -> Span {
781        match self {
782            CommonsItem::Type(t) => t.span,
783            CommonsItem::Fn(f) => f.span,
784            CommonsItem::Capability(c) => c.span,
785            CommonsItem::Provider(p) => p.span,
786            CommonsItem::Service(s) => s.span,
787            CommonsItem::Agent(a) => a.span,
788            CommonsItem::Actor(a) => a.span,
789            CommonsItem::Messages(m) => m.span,
790            CommonsItem::Event(e) => e.span,
791            CommonsItem::Asset(a) => a.span,
792            CommonsItem::Embed(e) => e.span,
793            CommonsItem::Const(c) => c.span,
794        }
795    }
796}
797
798/// `asset name = "path"` (#1883, slice 1a): one static file, served by the
799/// platform at a content-hashed URL and never reaching the Worker, so public
800/// by construction. `name` is a value of the built-in type `Asset`
801/// ([DECISION H]), whose `.url` is that URL.
802///
803/// `path` is the literal's value with escapes applied, relative to the
804/// project root. The checker rejects a path that is absolute or contains a
805/// `..` segment (`bynk.asset.unsafe_path`) or names a file outside the
806/// project's `assets/` folder (`bynk.asset.outside_assets_dir`), and one
807/// whose file does not exist there (`bynk.asset.missing_file`).
808#[derive(Debug, Clone)]
809pub struct AssetDecl {
810    pub name: Ident,
811    pub path: String,
812    pub path_span: Span,
813    /// `at "/favicon.ico"` (#1883, slice 2, [DECISION C]): a fixed URL path
814    /// to serve the file at, for clients that fetch a file by name without
815    /// reading a page (`favicon.ico`, `robots.txt`). `None` serves it at a
816    /// content-hashed path under `/_assets/`. With its span, for diagnostics.
817    pub at: Option<(String, Span)>,
818    pub documentation: Option<Doc>,
819    /// #2618: the span of a leading `private` modifier, when the declaration
820    /// is unit-private: visible across its own unit's files (and to that
821    /// unit's suites), never through `uses`. `None` is the default, public.
822    pub private: Option<Span>,
823    pub span: Span,
824    pub trivia: Trivia,
825}
826
827/// `embed name = "path"` (#2387, slice 1): one project file whose text is the
828/// value of `name`, a `String` constant compiled into the code. Unlike an
829/// [`AssetDecl`] it is never served, so it is private and comes from the
830/// project's `embeds/` folder rather than `assets/`.
831///
832/// `path` is the literal's value with escapes applied, relative to the
833/// project root. The checker rejects a path that is absolute or contains a
834/// `..` segment (`bynk.embed.unsafe_path`) or names a file outside the
835/// project's `embeds/` folder (`bynk.embed.outside_embeds_dir`).
836#[derive(Debug, Clone)]
837pub struct EmbedDecl {
838    pub name: Ident,
839    pub path: String,
840    pub path_span: Span,
841    pub documentation: Option<Doc>,
842    /// #2618: the span of a leading `private` modifier, when the declaration
843    /// is unit-private: visible across its own unit's files (and to that
844    /// unit's suites), never through `uses`. `None` is the default, public.
845    pub private: Option<Span>,
846    pub span: Span,
847    pub trivia: Trivia,
848}
849
850/// `fixture name = "path"` (#2841): a project file's text as a `String`
851/// constant visible to the declaring suite's `case` and `property` bodies. A
852/// fixture is test-only: its text goes into test output, never into a deployable.
853///
854/// `path` is the literal's value with escapes applied, relative to the project
855/// root. The checker rejects a path that is absolute or contains a `..` segment
856/// (`bynk.fixture.unsafe_path`) or names a file outside the project's
857/// `fixtures/` folder (`bynk.fixture.outside_fixtures_dir`).
858#[derive(Debug, Clone)]
859pub struct FixtureDecl {
860    pub name: Ident,
861    pub path: String,
862    pub path_span: Span,
863    pub documentation: Option<Doc>,
864    pub span: Span,
865    pub trivia: Trivia,
866}
867
868/// `const Name: Type = <static expression>` (#2617): a named value, typed by
869/// its required annotation `ty`. The initialiser `init` is a *static
870/// expression* (ADR 0004) or a reference to another constant; the checker
871/// rejects anything else (`bynk.resolve.bad_const_initialiser`), a name that
872/// is not upper-camel (`bynk.resolve.const_name_case`) and constants whose
873/// initialisers refer to each other in a cycle (`bynk.resolve.const_cycle`).
874/// Emitted as an exported, hoisted TypeScript function that caches the value;
875/// each reference is a call.
876#[derive(Debug, Clone)]
877pub struct ConstDecl {
878    pub name: Ident,
879    pub ty: TypeRef,
880    pub init: Expr,
881    pub documentation: Option<Doc>,
882    /// The span of a leading `private` modifier, when the constant is
883    /// unit-private: visible across its own unit's files (and to that unit's
884    /// suites), never through `uses`. `None` is the default, public.
885    pub private: Option<Span>,
886    pub span: Span,
887    pub trivia: Trivia,
888}
889
890/// One locale's message bundle (v0.222+): `messages "<tag>" @reference { ... }`.
891/// `tag` is a `LocaleTag` string literal (like an entry's `code`/`template`);
892/// its refinement (`bynk.locale.types`) is checked by `check_messages_bundles`,
893/// which reports `bynk.messages.invalid_locale_tag` for a tag the pattern
894/// rejects.
895#[derive(Debug, Clone)]
896pub struct MessagesDecl {
897    pub tag: String,
898    pub tag_span: Span,
899    /// Every `@`-annotation attached to this block. The parser stays
900    /// permissive (zero or more, same as `store` field annotations); cardinality
901    /// (exactly one `@reference` per bundle, counted across every `Messages`
902    /// item in the commons) is a checker concern, not a parse error.
903    pub annotations: Vec<Annotation>,
904    pub entries: Vec<MessageEntry>,
905    pub documentation: Option<Doc>,
906    /// #2618: the span of a leading `private` modifier, when the declaration
907    /// is unit-private: visible across its own unit's files (and to that
908    /// unit's suites), never through `uses`. `None` is the default, public.
909    pub private: Option<Span>,
910    pub span: Span,
911    pub trivia: Trivia,
912}
913
914/// One `"code" => "template"` entry inside a `messages` block. Both sides are
915/// plain string literals — a template's `{name}` placeholders are resolved by
916/// a compile-time string scan during lowering, not parsed as expressions.
917#[derive(Debug, Clone)]
918pub struct MessageEntry {
919    pub code: String,
920    pub code_span: Span,
921    pub template: String,
922    pub template_span: Span,
923    pub span: Span,
924    /// #1888: the doc above the entry, in either form.
925    pub documentation: Option<Doc>,
926}
927
928/// A capability declaration (v0.5 §3.3). Capabilities are interface-like
929/// contracts for external dependencies, used inside contexts. They may only
930/// appear inside a `context` declaration.
931#[derive(Debug, Clone)]
932pub struct CapabilityDecl {
933    pub name: Ident,
934    /// Empty for a derived capability ([`CapabilityDecl::derived`]), whose
935    /// operations are its base's; the checker resolves them.
936    pub ops: Vec<CapabilityOp>,
937    /// Standard-library S4a (#1966): `capability Sessions = Kv @option(…)`, a
938    /// distinct instance of a platform capability the unit consumes. `None` for
939    /// the braced `capability X { fn … }` form.
940    pub derived: Option<DerivedCapability>,
941    pub documentation: Option<Doc>,
942    pub span: Span,
943    /// #1756: comments before the closing `}`, an orphaned doc block among
944    /// them, so the formatter keeps them.
945    pub trailing_comments: Vec<Comment>,
946    pub trivia: Trivia,
947}
948
949/// The `= Base @option(…)*` of a derived capability (S4a, #1966): a new
950/// capability name whose operations are those of `base`, a platform capability
951/// the unit selects with `consumes U { Base }`, provided by the platform
952/// adapter's own provider bound to this instance.
953#[derive(Debug, Clone)]
954pub struct DerivedCapability {
955    /// The base capability, by its flattened (bare) name.
956    pub base: Ident,
957    /// Creation options for the instance, checked against the base kind's
958    /// closed set.
959    pub annotations: Vec<Annotation>,
960}
961
962/// One operation in a capability (signature only; no body).
963#[derive(Debug, Clone)]
964pub struct CapabilityOp {
965    pub name: Ident,
966    /// #926: `[T, …]` type parameters on the op itself; empty for a
967    /// non-generic op. Resolved only from an explicit type argument at the
968    /// call site (`Cap.op[Some](…)`) — never inferred.
969    pub type_params: Vec<TypeParam>,
970    pub params: Vec<Param>,
971    pub return_type: TypeRef,
972    pub documentation: Option<Doc>,
973    pub span: Span,
974    pub trivia: Trivia,
975}
976
977/// A provider declaration (v0.5 §3.4). Supplies an implementation for a
978/// capability.
979#[derive(Debug, Clone)]
980pub struct ProviderDecl {
981    /// The capability being implemented.
982    pub capability: Ident,
983    /// The provider's identifier (used in tests/config to select impls).
984    pub provider_name: Ident,
985    /// v0.12: capabilities this provider depends on (`provides X = Impl given
986    /// Y, Z { … }`). The provider's operation bodies may use these. v0.15:
987    /// a dependency may be a cross-context capability (`given B.Cap`).
988    pub given: Vec<CapRef>,
989    pub ops: Vec<ProviderOp>,
990    /// v0.17: an *external* provider — `provides Cap = Name` with **no** brace
991    /// block — inside an adapter, supplied by the adapter's binding rather than
992    /// a Bynk body. When `true`, `ops` is empty and the emitter produces no
993    /// class. The absence of the brace block (not an empty one) is the signal.
994    pub external: bool,
995    pub documentation: Option<Doc>,
996    pub span: Span,
997    pub trivia: Trivia,
998}
999
1000/// One operation in a provider (signature plus body).
1001#[derive(Debug, Clone)]
1002pub struct ProviderOp {
1003    pub name: Ident,
1004    pub params: Vec<Param>,
1005    pub return_type: TypeRef,
1006    pub body: Block,
1007    pub span: Span,
1008    pub trivia: Trivia,
1009}
1010
1011/// A service declaration (v0.5 §3.5). Services are the boundary interface
1012/// of a context.
1013#[derive(Debug, Clone)]
1014pub struct ServiceDecl {
1015    pub name: Ident,
1016    /// The protocol the service conforms to, from the `from <protocol>` header
1017    /// clause (v0.44). `Call` when there is no clause.
1018    pub protocol: ServiceProtocol,
1019    /// The optional service-level `by` default (v0.155) — a `by <Actor>` clause on
1020    /// the service header, `service Api from http by v: Visitor { … }`. Every
1021    /// handler that omits its own `by` inherits this one (injected by the
1022    /// normalization pass). `None` when absent — handlers then fall back to the
1023    /// per-protocol default actor (HTTP/WebSocket have none, so `by` stays
1024    /// mandatory there). The "public / bearer-authed" fact is usually a service
1025    /// fact, so this removes the per-handler repetition.
1026    pub default_by: Option<ByClause>,
1027    /// The optional service-level `given` default (v0.155) — a `given C1, C2`
1028    /// clause on the service header, following the `by` default. Every handler
1029    /// that declares no `given` of its own inherits this list. Empty when absent.
1030    pub default_given: Vec<CapRef>,
1031    /// The optional cross-origin (CORS) policy (v0.131, ADR 0159) — a `cors { }`
1032    /// section in the service body, only meaningful on a `from http` service.
1033    /// `None` when absent (same-origin default, byte-for-byte unchanged output).
1034    pub cors: Option<CorsPolicy>,
1035    /// The optional security-headers policy (v0.141, ADR 0164) — a `security { }`
1036    /// section in the service body, only meaningful on a `from http` service.
1037    /// `None` when absent, but unlike `cors` the *absence* still stamps the safe
1038    /// defaults (`nosniff` on) — the emitter synthesises a default policy for every
1039    /// `from http` service, so `None` here means "defaults", not "no headers".
1040    pub security: Option<SecurityPolicy>,
1041    /// The optional request-body-size policy (v0.142, ADR 0165) — a `limits { }`
1042    /// section in the service body, only meaningful on a `from http` service. It
1043    /// declares a per-service `maxBody` ceiling (in bytes) for the service's
1044    /// body-taking routes; a route may override it with `@limit(maxBody: …)`.
1045    /// `None` when absent (no cap — byte-for-byte unchanged output, the opt-in
1046    /// CORS posture, not the `security` default-on posture).
1047    pub limits: Option<LimitsPolicy>,
1048    /// The optional response-JSON policy (#2858) — a `json { options: plain }`
1049    /// section in the service body, only meaningful on a `from http` service.
1050    /// `None` (or `options: tagged`) keeps Bynk's tagged `Option` encoding in
1051    /// response bodies, byte-for-byte unchanged; a route's `@json(options:)`
1052    /// overrides it.
1053    pub json: Option<JsonPolicy>,
1054    pub handlers: Vec<Handler>,
1055    pub documentation: Option<Doc>,
1056    pub span: Span,
1057    /// #1756: comments before the closing `}`, an orphaned doc block among
1058    /// them, so the formatter keeps them.
1059    pub trailing_comments: Vec<Comment>,
1060    pub trivia: Trivia,
1061}
1062
1063/// A cross-origin resource-sharing policy on a `from http` service (v0.131,
1064/// ADR 0159): the `cors { }` section in the service body. Parsed leniently as a
1065/// list of `name: value` fields (the grammar accepts any field name — an unknown
1066/// one is a checker diagnostic, per the `@`-annotation precedent, ADR 0111), and
1067/// interpreted through the typed accessors below.
1068///
1069/// `Access-Control-Allow-Methods` is deliberately **not** a field — it is derived
1070/// from the service's routes at emit time (the routes already enumerate the
1071/// methods; a restated list would drift). Likewise `Allow-Headers` defaults to
1072/// `content-type` (+ `Authorization` when a Bearer route exists) and is only
1073/// stored here when the author overrides it.
1074#[derive(Debug, Clone)]
1075pub struct CorsPolicy {
1076    /// The `cors { }` fields as written, in source order. Field names are
1077    /// validated against the closed set (`origins`/`headers`/`credentials`/
1078    /// `maxAge`) by the checker, not the parser.
1079    pub fields: Vec<CorsField>,
1080    pub span: Span,
1081    /// #1786: comments before the closing `}`, an orphaned doc block among
1082    /// them.
1083    pub trailing_comments: Vec<Comment>,
1084    pub trivia: Trivia,
1085}
1086
1087/// One `name: value` field inside a `cors { }` policy (v0.131).
1088#[derive(Debug, Clone)]
1089pub struct CorsField {
1090    pub name: Ident,
1091    pub value: Expr,
1092    pub span: Span,
1093    /// #1786: the field's own comments, above it and at the end of its line.
1094    pub trivia: Trivia,
1095}
1096
1097impl CorsPolicy {
1098    /// The raw value expression for a field, by name (the last one wins if a
1099    /// field is repeated — the checker flags the duplicate separately).
1100    pub fn field(&self, name: &str) -> Option<&Expr> {
1101        self.fields
1102            .iter()
1103            .rev()
1104            .find(|f| f.name.name == name)
1105            .map(|f| &f.value)
1106    }
1107
1108    /// The allowed origins — the string literals of the `origins:` list. An
1109    /// absent or malformed field yields an empty list (the checker has already
1110    /// reported the shape error; the emitter fails closed on an empty list).
1111    pub fn origins(&self) -> Vec<String> {
1112        Self::str_list(self.field("origins")).unwrap_or_default()
1113    }
1114
1115    /// `true` iff `origins` is exactly the wildcard `["*"]`.
1116    pub fn is_wildcard(&self) -> bool {
1117        let os = self.origins();
1118        os.len() == 1 && os[0] == "*"
1119    }
1120
1121    /// Whether credentialed requests are allowed (`credentials: true`); defaults
1122    /// to `false` when the field is absent.
1123    pub fn credentials(&self) -> bool {
1124        matches!(
1125            self.field("credentials").map(|e| &e.kind),
1126            Some(ExprKind::BoolLit(true))
1127        )
1128    }
1129
1130    /// The explicit `Access-Control-Allow-Headers` override, if the author gave
1131    /// a `headers:` list; `None` leaves the emitter to apply its smart default.
1132    pub fn allow_headers(&self) -> Option<Vec<String>> {
1133        self.field("headers").and_then(Self::str_list_of)
1134    }
1135
1136    /// The `Access-Control-Max-Age` in whole seconds, if a `maxAge:` duration was
1137    /// given; `None` leaves the header off (the browser default).
1138    pub fn max_age_secs(&self) -> Option<i64> {
1139        match self.field("maxAge").map(|e| &e.kind) {
1140            Some(ExprKind::DurationLit { millis, .. }) => Some(millis / 1_000),
1141            _ => None,
1142        }
1143    }
1144
1145    /// Interpret an expression as a list of string literals, if it is one.
1146    fn str_list(expr: Option<&Expr>) -> Option<Vec<String>> {
1147        expr.and_then(Self::str_list_of)
1148    }
1149
1150    fn str_list_of(expr: &Expr) -> Option<Vec<String>> {
1151        match &expr.kind {
1152            ExprKind::ListLit(items) => items
1153                .iter()
1154                .map(|e| match &e.kind {
1155                    ExprKind::StrLit(s) => Some(s.clone()),
1156                    _ => None,
1157                })
1158                .collect(),
1159            _ => None,
1160        }
1161    }
1162}
1163
1164/// A security-headers policy on a `from http` service (v0.141, ADR 0164): the
1165/// `security { }` section in the service body. Parsed leniently as a list of
1166/// `name: value` fields (an unknown one is a checker diagnostic, per the CORS /
1167/// `@`-annotation precedent) and interpreted through the typed accessors below.
1168///
1169/// The closed set is `nosniff` (a `Bool`, default `true` — stamps
1170/// `X-Content-Type-Options: nosniff`) and `hsts` (a positive `Duration`, opt-in —
1171/// stamps `Strict-Transport-Security: max-age=…`). Unlike `cors`, the *safe*
1172/// header is on by default: a `from http` service with no `security { }` still
1173/// stamps `nosniff`, because a security header you have to remember to switch on
1174/// is the one you forget (ADR 0164 DECISION A).
1175#[derive(Debug, Clone)]
1176pub struct SecurityPolicy {
1177    /// The `security { }` fields as written, in source order. Field names are
1178    /// validated against the closed set (`hsts`/`nosniff`) by the checker, not
1179    /// the parser.
1180    pub fields: Vec<SecurityField>,
1181    pub span: Span,
1182    /// #1786: comments before the closing `}`, an orphaned doc block among
1183    /// them.
1184    pub trailing_comments: Vec<Comment>,
1185    pub trivia: Trivia,
1186}
1187
1188/// One `name: value` field inside a `security { }` policy (v0.141).
1189#[derive(Debug, Clone)]
1190pub struct SecurityField {
1191    pub name: Ident,
1192    pub value: Expr,
1193    pub span: Span,
1194    /// #1786: the field's own comments, above it and at the end of its line.
1195    pub trivia: Trivia,
1196}
1197
1198impl SecurityPolicy {
1199    /// The raw value expression for a field, by name (the last one wins if a
1200    /// field is repeated — the checker flags the duplicate separately).
1201    pub fn field(&self, name: &str) -> Option<&Expr> {
1202        self.fields
1203            .iter()
1204            .rev()
1205            .find(|f| f.name.name == name)
1206            .map(|f| &f.value)
1207    }
1208
1209    /// Whether `X-Content-Type-Options: nosniff` is stamped. Defaults to `true`
1210    /// (the safe default, ADR 0164 DECISION A); only an explicit `nosniff: false`
1211    /// opts out. A malformed value has already been reported by the checker; it
1212    /// falls back to the safe default here.
1213    pub fn nosniff(&self) -> bool {
1214        !matches!(
1215            self.field("nosniff").map(|e| &e.kind),
1216            Some(ExprKind::BoolLit(false))
1217        )
1218    }
1219
1220    /// The `Strict-Transport-Security` `max-age` in whole seconds, if the author
1221    /// opted in with an `hsts:` duration; `None` leaves HSTS off (the default —
1222    /// HSTS pins the browser to HTTPS and is a deliberate opt-in, DECISION A).
1223    pub fn hsts_max_age_secs(&self) -> Option<i64> {
1224        match self.field("hsts").map(|e| &e.kind) {
1225            Some(ExprKind::DurationLit { millis, .. }) => Some(millis / 1_000),
1226            _ => None,
1227        }
1228    }
1229}
1230
1231/// A request-body-size policy on a `from http` service (v0.142, ADR 0165): the
1232/// `limits { }` section in the service body. Parsed leniently as a list of
1233/// `name: value` fields (an unknown one is a checker diagnostic, per the CORS /
1234/// `security` / `@`-annotation precedent) and interpreted through the typed
1235/// accessor below.
1236///
1237/// The closed set is `maxBody` — a positive `Int` byte count (there is no byte
1238/// `Size` literal yet; a `1.mb`-style literal is a named follow-on, the
1239/// `Duration` playbook). Unlike `security`, this is opt-in: a service with no
1240/// `limits { }` (and no route `@limit`) has no cap and emits byte-for-byte
1241/// unchanged output (ADR 0165 DECISION E — the CORS posture).
1242#[derive(Debug, Clone)]
1243pub struct LimitsPolicy {
1244    /// The `limits { }` fields as written, in source order. Field names are
1245    /// validated against the closed set (`maxBody`) by the checker, not the
1246    /// parser.
1247    pub fields: Vec<LimitsField>,
1248    pub span: Span,
1249    /// #1786: comments before the closing `}`, an orphaned doc block among
1250    /// them.
1251    pub trailing_comments: Vec<Comment>,
1252    pub trivia: Trivia,
1253}
1254
1255/// One `name: value` field inside a `limits { }` policy (v0.142).
1256#[derive(Debug, Clone)]
1257pub struct LimitsField {
1258    pub name: Ident,
1259    pub value: Expr,
1260    pub span: Span,
1261    /// #1786: the field's own comments, above it and at the end of its line.
1262    pub trivia: Trivia,
1263}
1264
1265impl LimitsPolicy {
1266    /// The raw value expression for a field, by name (the last one wins if a
1267    /// field is repeated — the checker flags the duplicate separately).
1268    pub fn field(&self, name: &str) -> Option<&Expr> {
1269        self.fields
1270            .iter()
1271            .rev()
1272            .find(|f| f.name.name == name)
1273            .map(|f| &f.value)
1274    }
1275
1276    /// The service-wide maximum request-body size in bytes, if the author gave a
1277    /// positive `maxBody:` `Int` literal; `None` leaves the service without a
1278    /// default cap. A malformed or non-positive value has already been reported
1279    /// by the checker; it falls back to `None` here (no cap).
1280    pub fn max_body(&self) -> Option<i64> {
1281        match self.field("maxBody").map(|e| &e.kind) {
1282            Some(ExprKind::IntLit { value, .. }) if *value > 0 => Some(*value),
1283            _ => None,
1284        }
1285    }
1286}
1287
1288/// How an `Option` is written in an HTTP response body (#2858).
1289#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1290pub enum JsonOptions {
1291    /// Bynk's tagged form: `{"kind": "Some", "value": v}` / `{"kind": "None"}`.
1292    #[default]
1293    Tagged,
1294    /// Plain JSON: `Some(v)` is `v` and `None` is `null`.
1295    Plain,
1296}
1297
1298impl JsonOptions {
1299    /// Read a bare `tagged` / `plain` identifier, the value of `options:`.
1300    pub fn from_expr(e: &Expr) -> Option<JsonOptions> {
1301        match &e.kind {
1302            ExprKind::Ident(n) if n.name == "tagged" => Some(JsonOptions::Tagged),
1303            ExprKind::Ident(n) if n.name == "plain" => Some(JsonOptions::Plain),
1304            _ => None,
1305        }
1306    }
1307}
1308
1309/// A response-JSON policy on a `from http` service (#2858): the
1310/// `json { }` section in the service body. Parsed leniently as a list of
1311/// `name: value` fields (as `limits { }`); the closed set is `options`, whose
1312/// value is the bare identifier `tagged` or `plain`.
1313#[derive(Debug, Clone)]
1314pub struct JsonPolicy {
1315    pub fields: Vec<JsonField>,
1316    pub span: Span,
1317    pub trailing_comments: Vec<Comment>,
1318    pub trivia: Trivia,
1319}
1320
1321/// One `name: value` field inside a `json { }` policy.
1322#[derive(Debug, Clone)]
1323pub struct JsonField {
1324    pub name: Ident,
1325    pub value: Expr,
1326    pub span: Span,
1327    pub trivia: Trivia,
1328}
1329
1330impl JsonPolicy {
1331    pub fn field(&self, name: &str) -> Option<&Expr> {
1332        self.fields
1333            .iter()
1334            .rev()
1335            .find(|f| f.name.name == name)
1336            .map(|f| &f.value)
1337    }
1338
1339    /// The service-wide `options:` mode; a malformed value (already reported by
1340    /// the checker) reads as the default, tagged.
1341    pub fn options(&self) -> JsonOptions {
1342        self.field("options")
1343            .and_then(JsonOptions::from_expr)
1344            .unwrap_or_default()
1345    }
1346}
1347
1348/// The mode a route's response body is written in: its own `@json(options:)`
1349/// wins over the service's `json { options: }`, which wins over the tagged
1350/// default.
1351pub fn effective_json_options(service: Option<&JsonPolicy>, h: &Handler) -> JsonOptions {
1352    h.json_options()
1353        .or_else(|| service.map(JsonPolicy::options))
1354        .unwrap_or_default()
1355}
1356
1357/// The protocol a service conforms to — declared on the header via
1358/// `from <protocol>` (v0.44). `Call` is the default (no `from` clause): a
1359/// contract-mediated internal-RPC surface, not a wire protocol. Multi-endpoint
1360/// protocols (`Http`, `Cron`) carry no binding — the endpoint lives on each
1361/// handler; single-binding `Queue` carries its queue name.
1362#[derive(Debug, Clone)]
1363pub enum ServiceProtocol {
1364    /// No `from` clause: the service holds `on call` handlers only.
1365    Call,
1366    /// `from http` — many routes; each handler is `on <Method>("route")`.
1367    Http,
1368    /// `from cron` — many schedules; each handler is `on schedule("expr")`.
1369    Cron,
1370    /// `from queue("name")` — one bound queue; handlers are `on message(...)`.
1371    Queue { name: String },
1372    /// `from websocket(in: ClientFrame, out: ServerFrame)` — a held WebSocket
1373    /// connection (v0.103, real-time track slice 3). `in_type` is the inbound
1374    /// frame type (client→server, decoded and routed as typed agent messages);
1375    /// `out_type` is the server→client frame type the held `Connection[out_type]`
1376    /// carries. The service holds exactly one `on open` handler (edge auth via
1377    /// `by`, then transfer of the connection to an agent).
1378    WebSocket { in_type: TypeRef, out_type: TypeRef },
1379    /// `from Events(E)` or `from Events(E { field: value, .. })`, optionally
1380    /// followed by `via schema(N)` — a subscriber to event type `E`,
1381    /// optionally filtered by a structural payload pattern (Events track,
1382    /// slice 0 spine #936; the pattern is slice 1) and/or the envelope's
1383    /// `schemaVersion` (slice 4). `Events`, capitalised, is matched as plain
1384    /// `Ident` text the same way `websocket` is — it names the `Events`
1385    /// capability directly (every first-party capability is already an
1386    /// unreserved PascalCase identifier), not a built-in type name, so no
1387    /// lexer reservation. `pattern` and `schema_dispatch` are independent:
1388    /// a service may carry either, both, or neither.
1389    Events {
1390        event_type: TypeRef,
1391        pattern: Option<EventPattern>,
1392        schema_dispatch: Option<SchemaDispatch>,
1393    },
1394}
1395
1396/// An agent declaration (v0.5 §3.6). Agents are state-bearing entities
1397/// with their own handlers.
1398#[derive(Debug, Clone)]
1399pub struct AgentDecl {
1400    pub name: Ident,
1401    /// State-migrations S3 (#2248): `agent New replaces Old` — the class
1402    /// `Old` is renamed to this one, its stored data moving with it.
1403    pub replaces: Option<Ident>,
1404    /// `key id: Type` — the identifier-typed value identifying instances.
1405    pub key_name: Ident,
1406    pub key_type: TypeRef,
1407    /// #1788: comments above the `key` line and at its end. A comment on the
1408    /// agent's `{` line leads the key too.
1409    pub key_trivia: Trivia,
1410    /// `store` fields (v0.81, storage track) — each an access-pattern slot of a
1411    /// declared storage kind (`Cell`/`Map`/…). The successor to the removed
1412    /// `state { }` record (ADR 0108); every agent declares its state this way.
1413    pub store_fields: Vec<StoreField>,
1414    /// Invariants (v0.80 §14) — universally-quantified predicates over the
1415    /// agent's `store` fields. The phase sits between the fields and the
1416    /// handlers; each is checked against the state staged by a handler's writes
1417    /// before it commits.
1418    pub invariants: Vec<Invariant>,
1419    /// Step invariants (v0.116 §, testing track slice 4) — named predicates over
1420    /// the pre-/post-commit state *pair* (`old`/`new`), checked at the commit
1421    /// boundary beside [`invariants`], from the second commit onward. Widen the
1422    /// invariant subject from a snapshot to a step (ADR 0144 — one predicate
1423    /// surface).
1424    ///
1425    /// [`invariants`]: AgentDecl::invariants
1426    pub transitions: Vec<Transition>,
1427    /// State-migrations S2 (#2224, track §3.3): `migrate from N (old: T) { … }`
1428    /// steps, each carrying a stored record from lock version `N` to the
1429    /// current shape. Declared in the phase beside invariants and transitions.
1430    pub migrations: Vec<MigrateDecl>,
1431    pub handlers: Vec<Handler>,
1432    pub documentation: Option<Doc>,
1433    pub span: Span,
1434    /// #1756: comments before the closing `}`, an orphaned doc block among
1435    /// them, so the formatter keeps them.
1436    pub trailing_comments: Vec<Comment>,
1437    pub trivia: Trivia,
1438}
1439
1440/// A migration step (state-migrations S2, #2224): `migrate from N (old: T) {
1441/// … }`. `old` names a record type declaring the version-`N` fields the step
1442/// reads; the body assigns current `store` fields with record-literal
1443/// semantics (each at most once, order-free, a bare field reads its carried
1444/// value) and acknowledges removed or uncovered fields with `drop`.
1445#[derive(Debug, Clone)]
1446pub struct MigrateDecl {
1447    /// The `migrate` keyword.
1448    pub keyword: Span,
1449    /// The lock version `N` this step reads, and its literal's span.
1450    pub from: i64,
1451    pub from_span: Span,
1452    /// `(old: T)` — absent for a step that reads nothing.
1453    pub old: Option<MigrateOld>,
1454    pub items: Vec<MigrateItem>,
1455    /// #1756-style: comments before the body's closing `}`.
1456    pub trailing_comments: Vec<Comment>,
1457    pub documentation: Option<Doc>,
1458    pub span: Span,
1459    pub trivia: Trivia,
1460}
1461
1462/// A step's `(old: T)` parameter.
1463#[derive(Debug, Clone)]
1464pub struct MigrateOld {
1465    pub name: Ident,
1466    pub type_ref: TypeRef,
1467}
1468
1469/// One item of a migration step's body.
1470// A step holds a handful of items, so the size gap between `Let`/`Assign` and
1471// the small `Drop` costs nothing worth boxing every consumer's match for.
1472#[allow(clippy::large_enum_variant)]
1473#[derive(Debug, Clone)]
1474pub enum MigrateItem {
1475    /// `let x = e` — a pure local binding, visible to later items.
1476    Let(LetStmt),
1477    /// `f := e` — the current `store` field `f` takes `e`.
1478    Assign(AssignStmt),
1479    /// `drop f` — the stored value of `f` is discarded on purpose.
1480    Drop(MigrateDrop),
1481}
1482
1483/// `drop f` inside a migration step.
1484#[derive(Debug, Clone)]
1485pub struct MigrateDrop {
1486    pub keyword: Span,
1487    pub field: Ident,
1488    pub span: Span,
1489    pub trivia: Trivia,
1490}
1491
1492/// A `store` field (v0.81, storage track). Each is an access-pattern slot of a
1493/// declared storage kind: `store <name>: <Kind>[…] [@annotations] [= <init>]`.
1494/// The kind and its element type are carried as an ordinary [`TypeRef`]
1495/// (`Cell[Int]`, `Map[K, V]`); the checker restricts which heads are storage
1496/// kinds. Access-pattern annotations (`@indexed`, …) parse into [`annotations`]
1497/// (v0.85, ADR 0111); the checker validates them against the closed registry.
1498///
1499/// [`annotations`]: StoreField::annotations
1500#[derive(Debug, Clone)]
1501pub struct StoreField {
1502    pub name: Ident,
1503    /// The storage kind and its element type(s): `Cell[Int]`, `Map[K, V]`. A
1504    /// dedicated [`StoreKind`] rather than a [`TypeRef`] — storage kinds are not
1505    /// value types, and the checker dispatches kind-aware operations on the head.
1506    pub kind: StoreKind,
1507    /// Storage annotations on the field (v0.85, ADR 0111): `@ttl(5.minutes)`,
1508    /// `@indexed(by: orderId)`. Parsed in declaration order (after the kind,
1509    /// before the initialiser); the checker validates names against the closed
1510    /// registry and gates each to the slice that implements it.
1511    pub annotations: Vec<Annotation>,
1512    /// The fresh-key initial value (`= expr`), if given — same disposition as a
1513    /// `state` field's initialiser (ADRs 0003/0004 carry forward).
1514    pub init: Option<Expr>,
1515    pub documentation: Option<Doc>,
1516    pub span: Span,
1517    pub trivia: Trivia,
1518}
1519
1520/// A storage annotation on a `store` field (v0.85, storage track; ADR 0111):
1521/// `@<name>(<args>)`. The `name` is matched against the closed registry
1522/// (`@indexed`/`@ttl`/`@retain`/`@bounded`) by the checker; the grammar accepts
1523/// any identifier so an unknown name is a checker diagnostic, not a parse error.
1524/// Arguments are compile-time metadata, restricted to literals (and the `by:`
1525/// field-name labels of `@indexed`) by the checker per ADR 0111 D4.
1526#[derive(Debug, Clone)]
1527pub struct Annotation {
1528    pub name: Ident,
1529    pub args: Vec<AnnotationArg>,
1530    pub span: Span,
1531}
1532
1533/// A single annotation argument (v0.85; ADR 0111): an optional `label:` followed
1534/// by a value expression — `by: orderId` (labelled) or `5.minutes` (positional).
1535/// The value is parsed as an ordinary [`Expr`] so the duration-literal form
1536/// (`5.minutes`, landing with the `Duration` slice) needs no special grammar;
1537/// the checker restricts it to a literal where the annotation is functional.
1538#[derive(Debug, Clone)]
1539pub struct AnnotationArg {
1540    pub label: Option<Ident>,
1541    pub value: Expr,
1542    pub span: Span,
1543}
1544
1545/// A storage kind applied to its element type(s) (v0.81): `Cell[Int]`,
1546/// `Map[ReservationId, Reservation]`. The `head` is the kind name (`Cell`,
1547/// `Map`, `Set`, `Log`, `Queue`, `Cache`); the checker validates it against the
1548/// closed catalogue. Element types are ordinary [`TypeRef`]s. Refined element
1549/// types (`Cell[Int where NonNegative]`) ride a later slice (parse_type_ref does
1550/// not yet accept an inline refinement in type-argument position).
1551#[derive(Debug, Clone)]
1552pub struct StoreKind {
1553    pub head: Ident,
1554    pub args: Vec<TypeRef>,
1555    pub span: Span,
1556}
1557
1558/// An agent invariant (v0.80 §14). A named predicate over the agent's state
1559/// fields that must hold of every committed state; a commit that would violate
1560/// it faults (`InvariantViolation`) before the state is persisted. The
1561/// predicate references state fields by bare name, mirroring the design-notes
1562/// worked examples (`status == Paid implies paymentRef.isSome()`).
1563#[derive(Debug, Clone)]
1564pub struct Invariant {
1565    pub name: Ident,
1566    /// The predicate expression — an ordinary `Bool`-typed expression over the
1567    /// state fields, plus `implies` and `is`. The parsed-predicate-on-a-
1568    /// declaration shape mirrors [`ActorRefinement::predicate`].
1569    pub predicate: Expr,
1570    pub documentation: Option<Doc>,
1571    pub span: Span,
1572    pub trivia: Trivia,
1573}
1574
1575/// An agent step invariant (v0.116 §, testing track slice 4). A named predicate
1576/// over the *pair* of committed states — the pre-commit `old` and the proposed
1577/// `new`, each the agent's state record — that must hold of every state move; a
1578/// commit that would violate it faults (`InvariantViolation`) before the state is
1579/// persisted, exactly as a snapshot [`Invariant`] does. Widens the invariant
1580/// subject from a snapshot to a step (ADR 0144 — one predicate surface); the
1581/// predicate reuses the invariant surface (`implies`/`is`/pure methods) with
1582/// `old`/`new` bound contextually (`old.status is Paid implies new.status is
1583/// Paid`).
1584#[derive(Debug, Clone)]
1585pub struct Transition {
1586    pub name: Ident,
1587    /// The predicate expression — an ordinary `Bool`-typed expression over the
1588    /// `old` and `new` state records, with `implies`/`is` and pure methods,
1589    /// mirroring [`Invariant`].
1590    pub predicate: Expr,
1591    pub documentation: Option<Doc>,
1592    pub span: Span,
1593    pub trivia: Trivia,
1594}
1595
1596/// A function contract clause (v0.115 §, testing track slice 3). A named
1597/// predicate on a `fn` signature — a `requires` (precondition) or `ensures`
1598/// (postcondition). A contract is the invariant predicate attached to a
1599/// function (ADR 0144 — one predicate surface): the predicate is a pure `Bool`
1600/// expression over the parameters (`requires`) or the parameters plus `result`
1601/// (`ensures`), with `implies`/`is` and pure methods, mirroring [`Invariant`].
1602/// The name rides the failure report and the redundant-test dedup.
1603#[derive(Debug, Clone)]
1604pub struct Contract {
1605    pub name: Ident,
1606    /// The predicate expression — an ordinary `Bool`-typed expression over the
1607    /// parameters (and, for an `ensures`, the contextual `result` binding).
1608    pub predicate: Expr,
1609    pub span: Span,
1610}
1611
1612/// An actor declaration (v0.45 §3.7). An actor is a nominal *contract type*
1613/// describing an external party at a boundary — not a runnable entity. A
1614/// handler consumes an actor on its `by` clause; the boundary verifies the
1615/// declared `auth` scheme and mints a sealed identity (`name.identity`).
1616#[derive(Debug, Clone)]
1617pub struct ActorDecl {
1618    pub name: Ident,
1619    /// The authentication scheme from `auth = <Scheme>`, stored as the raw
1620    /// identifier. The checker classifies it: `None`/`Internal`/`Bearer` are
1621    /// admitted; `Signature` is reserved-and-rejected
1622    /// (`bynk.actor.scheme_unsupported`); anything else is
1623    /// `bynk.actor.unknown_scheme`. `None` for the refinement form.
1624    pub auth: Option<Ident>,
1625    /// The scheme's keyed config from `auth = Scheme(key = value, …)` (v0.47
1626    /// `Bearer(secret = "…")`; v0.51 generalised for `Signature(secret, header,
1627    /// timestamp?, tolerance?)`). Empty for schemes/forms with no config. The
1628    /// checker validates which keys each scheme requires/allows.
1629    pub auth_config: Vec<SchemeArg>,
1630    /// The optional identity type from `, identity = <T>`. Absent ⇒ the
1631    /// scheme default (`()` for `None`; a sealed `CallerId` for the `Internal`
1632    /// `on call` channel, `()` for other `Internal` channels).
1633    pub identity: Option<TypeRef>,
1634    /// The refinement form `actor Admin = Base where <predicate>` — narrows a
1635    /// base actor by an authorisation claim (ADR 0091). The predicate is parsed
1636    /// as a full expression; a static-semantics rule restricts it to the closed
1637    /// actor-claim catalogue (`hasClaim`/`claimEquals` over a `Bearer` base;
1638    /// `bynk.actor.refinement_predicate_unsupported` / `…_base_unsupported`).
1639    pub refinement: Option<ActorRefinement>,
1640    pub documentation: Option<Doc>,
1641    pub span: Span,
1642    pub trivia: Trivia,
1643    /// #1797: comments above the `auth` entry and at the end of its line. A
1644    /// comment on the actor's `{` line leads `auth` too. Empty for the
1645    /// refinement form.
1646    pub auth_trivia: Trivia,
1647    /// #1797: comments above the `identity` entry and at the end of its line.
1648    /// Empty when there is no `identity`.
1649    pub identity_trivia: Trivia,
1650    /// #1797: comments before the actor body's closing `}`.
1651    pub trailing_comments: Vec<Comment>,
1652}
1653
1654impl ActorDecl {
1655    /// The value of a scheme config arg by key, if present (e.g. `secret`,
1656    /// `header`).
1657    pub fn scheme_arg(&self, key: &str) -> Option<&SchemeArg> {
1658        self.auth_config.iter().find(|a| a.key.name == key)
1659    }
1660}
1661
1662/// One `key = value` argument in a scheme config (`Scheme(key = value, …)`).
1663#[derive(Debug, Clone)]
1664pub struct SchemeArg {
1665    pub key: Ident,
1666    pub value: SchemeArgValue,
1667    /// Span of the value, for diagnostics.
1668    pub span: Span,
1669}
1670
1671/// A scheme config arg value — a string literal or an integer.
1672#[derive(Debug, Clone)]
1673pub enum SchemeArgValue {
1674    Str(String),
1675    Int(i64),
1676}
1677
1678impl SchemeArgValue {
1679    pub fn as_str(&self) -> Option<&str> {
1680        match self {
1681            SchemeArgValue::Str(s) => Some(s),
1682            SchemeArgValue::Int(_) => None,
1683        }
1684    }
1685    pub fn as_int(&self) -> Option<i64> {
1686        match self {
1687            SchemeArgValue::Int(n) => Some(*n),
1688            SchemeArgValue::Str(_) => None,
1689        }
1690    }
1691}
1692
1693/// The reserved refinement form `actor Admin = User where <predicate>` (Q3).
1694/// Parsed in Foundations so the grammar is fixed; admission is a later slice.
1695#[derive(Debug, Clone)]
1696pub struct ActorRefinement {
1697    /// The base actor being refined.
1698    pub base: Ident,
1699    /// The `where` predicate. Parsed but not yet checked.
1700    pub predicate: Expr,
1701    pub span: Span,
1702}
1703
1704/// The `by (<binder>:)? <Actor>` clause on a handler (v0.45; binder optional in
1705/// v0.50). Names the actor contract the handler consumes; when a `binder` is
1706/// given, the verified identity binds to it and is read as `binder.identity`.
1707/// Omitting the binder (`by <Actor>`) declares-and-verifies the contract without
1708/// capturing the identity — for anonymous or verify-and-discard handlers. Sits
1709/// after the protocol config and before the parameters.
1710#[derive(Debug, Clone)]
1711pub struct ByClause {
1712    /// The identity binder, if the handler consumes the identity. `None` for the
1713    /// binder-less `by <Actor>` form. Required when `actors` names more than one
1714    /// (a sum is resolved by matching on the bound actor).
1715    pub binder: Option<Ident>,
1716    /// The actor contract(s) referenced — each a local actor decl or a prelude
1717    /// actor. A single name is the ordinary single-actor handler; more than one
1718    /// (`by who: A | B`, v0.52) is an **ordered sum of peer actors** resolved
1719    /// first-wins, the body matching on the resolved actor. Always non-empty.
1720    pub actors: Vec<Ident>,
1721    pub span: Span,
1722}
1723
1724impl ByClause {
1725    /// The first (and, for a single-actor handler, only) actor contract named.
1726    pub fn primary(&self) -> &Ident {
1727        &self.actors[0]
1728    }
1729    /// Whether this `by` clause names an ordered sum of peer actors (`A | B`).
1730    pub fn is_sum(&self) -> bool {
1731        self.actors.len() > 1
1732    }
1733}
1734
1735/// v0.182 (testing-the-boundary Slice A, #664): a call-site actor clause on a
1736/// test-body `let x <- <service address> by <Actor>(<identity>)`. Distinct from
1737/// [`ByClause`] (the handler/header form): the *declaration* names which actor
1738/// may call and binds the verified identity, whereas the *call site* names the
1739/// actor the case is acting as and supplies the identity value. A unit-identity
1740/// actor (`Visitor`, and cron/queue's internal actors) carries no `identity`.
1741#[derive(Debug, Clone)]
1742pub struct CallSiteActor {
1743    /// The actor the case acts as — a local actor decl or a prelude actor.
1744    pub actor: Ident,
1745    /// The supplied identity value (`"bob"` in `by User("bob")`), or `None` for a
1746    /// unit-identity actor written `by Visitor` with no argument.
1747    pub identity: Option<Box<Expr>>,
1748    /// #2380 (L5): the verified-token claims an `Oidc` actor's handler reads
1749    /// (`by Workload("id", TokenClaims.of([…]))`), at the in-process tiers.
1750    pub claims: Option<Box<Expr>>,
1751    pub span: Span,
1752}
1753
1754/// A handler block — `on call(args) -> T given C1, C2 { body }`.
1755/// Used by both services and agents.
1756#[derive(Debug, Clone)]
1757pub struct Handler {
1758    pub kind: HandlerKind,
1759    /// Handler-position annotations (v0.140, ADR 0163): `@cache(maxAge: 5.minutes)`
1760    /// written immediately before `on <METHOD>(…)`. Reuses the [`Annotation`] AST
1761    /// shared with `store` fields (ADR 0111); the grammar accepts any `@name(args)`
1762    /// so an unknown name is a project-validation diagnostic, not a parse error. The
1763    /// first handler-position annotation surface — empty for every handler that
1764    /// carries none.
1765    pub annotations: Vec<Annotation>,
1766    /// For agent handlers, the method-style handler name (e.g.
1767    /// `on call addItem(...)`). For service handlers, this is None (just
1768    /// `on call(...)`).
1769    pub method_name: Option<Ident>,
1770    /// The `by <binder>: <Actor>` clause (v0.45), if present. Service handlers
1771    /// only; an absent clause inherits the protocol's default actor.
1772    pub by_clause: Option<ByClause>,
1773    pub params: Vec<Param>,
1774    pub return_type: TypeRef,
1775    pub given: Vec<CapRef>,
1776    pub body: Block,
1777    pub documentation: Option<Doc>,
1778    pub span: Span,
1779    pub trivia: Trivia,
1780}
1781
1782impl Handler {
1783    /// The handler's own `@limit(maxBody:)` byte cap, if it carries a well-formed
1784    /// one (ADR 0165). The checker rejects a malformed `@limit`, so a bad value
1785    /// reads as no cap here.
1786    pub fn limit_max_body(&self) -> Option<i64> {
1787        let ann = self.annotations.iter().find(|a| a.name.name == "limit")?;
1788        ann.args
1789            .iter()
1790            .find_map(|arg| match (&arg.label, &arg.value.kind) {
1791                (Some(l), ExprKind::IntLit { value, .. }) if l.name == "maxBody" && *value > 0 => {
1792                    Some(*value)
1793                }
1794                _ => None,
1795            })
1796    }
1797
1798    /// The handler's own `@json(options:)` mode, if it carries a well-formed one
1799    /// (#2858). The checker rejects a malformed `@json`, so a bad value reads as
1800    /// no override here.
1801    pub fn json_options(&self) -> Option<JsonOptions> {
1802        let ann = self.annotations.iter().find(|a| a.name.name == "json")?;
1803        ann.args.iter().find_map(|arg| match &arg.label {
1804            Some(l) if l.name == "options" => JsonOptions::from_expr(&arg.value),
1805            _ => None,
1806        })
1807    }
1808
1809    /// The media type a `@raw(contentType:)` handler declares: `Some` marks the
1810    /// handler's `body: Bytes` as the raw request body (request-side counterpart of
1811    /// ADR 0143). `"*/*"` accepts any content type. A malformed `@raw` is a checker
1812    /// error, so a missing or non-literal `contentType` reads as not raw here.
1813    pub fn raw_content_type(&self) -> Option<&str> {
1814        let ann = self.annotations.iter().find(|a| a.name.name == "raw")?;
1815        ann.args
1816            .iter()
1817            .find_map(|arg| match (&arg.label, &arg.value.kind) {
1818                (Some(l), ExprKind::StrLit(s)) if l.name == "contentType" && is_media_type(s) => {
1819                    Some(s.as_str())
1820                }
1821                _ => None,
1822            })
1823    }
1824}
1825
1826/// Whether `s` is a media type a `@raw(contentType:)` may declare: `type/subtype`
1827/// of RFC 7230 token characters, or the wildcard `*/*`. Parameters (`; charset=…`)
1828/// are not part of the declaration.
1829pub fn is_media_type(s: &str) -> bool {
1830    fn token(t: &str) -> bool {
1831        !t.is_empty()
1832            && t.bytes()
1833                .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
1834    }
1835    s == "*/*"
1836        || s.split_once('/')
1837            .is_some_and(|(t, sub)| token(t) && token(sub) && t != "*" && sub != "*")
1838}
1839
1840/// `Hash` (P8.5, #1516): a `HandlerKind` value is a body-free, span-free
1841/// discriminant, added so a `DefId`-keyed handler identity could carry it
1842/// (`bynk-check`'s former `queries::HandlerDefId`, deleted by #1537 — kept
1843/// here because the derive is harmless, and a rebuild against ADR 0417's
1844/// shape would need it again). Purely additive; no existing
1845/// caller matches on hashing behaviour.
1846#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1847pub enum HandlerKind {
1848    /// `on call(...)` — typed RPC (the only kind in v0.5).
1849    Call,
1850    /// `on http METHOD "path"` — external-facing HTTP route (v0.9).
1851    Http { method: HttpMethod, path: String },
1852    /// `on cron "expr"` — scheduled task; `expr` is a 5-field cron
1853    /// expression (v0.10a).
1854    Cron { expr: String },
1855    /// `on message(m: T)` — a message off the service's bound queue. The queue
1856    /// binding lives on the service's `ServiceProtocol::Queue` (v0.44).
1857    Message,
1858    /// `on open ...` — the WebSocket upgrade handler (v0.103, real-time track
1859    /// slice 3). Exactly one per `from websocket` service; carries a mandatory
1860    /// `by` clause (edge auth) and receives a fresh owned `Connection[out]`.
1861    Open,
1862    /// `on close ...` — the WebSocket close handler (v0.106, real-time track slice
1863    /// 3b-iii). Optional, ≤1 per `from websocket` service; runs when the socket
1864    /// closes. Like `on open`, edge-authenticated (`by`), with the identity/params
1865    /// recovered from the socket attachment (set at `on open`). (A `from websocket`
1866    /// `on message` reuses [`HandlerKind::Message`], disambiguated by the protocol.)
1867    Close,
1868    /// `on event(e: E)` — one emission of a `from Events(E)` service's
1869    /// subscribed event type (Events track, slice 0, spine #936). No
1870    /// envelope parameter yet (slice 2). `event`, like `message`/`open`/
1871    /// `close`/`schedule`, is matched by plain ident text at the fixed
1872    /// position right after `on`, with no lexer reservation — an ordinary
1873    /// identifier everywhere else in the grammar.
1874    Event,
1875    /// `on rejected(why: BoundaryRejection)` — renders the router's own
1876    /// rejections (`400`, `405`, `413`, `415`) of a `from http` service in the
1877    /// service's shape, keeping their status (#2341, decision C). At most one
1878    /// per service; no `by`, no `given`.
1879    Rejected,
1880}
1881
1882/// HTTP methods supported by `on http` handlers (v0.9).
1883#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1884pub enum HttpMethod {
1885    Get,
1886    Post,
1887    Put,
1888    Patch,
1889    Delete,
1890}
1891
1892impl HttpMethod {
1893    pub fn as_str(self) -> &'static str {
1894        match self {
1895            HttpMethod::Get => "GET",
1896            HttpMethod::Post => "POST",
1897            HttpMethod::Put => "PUT",
1898            HttpMethod::Patch => "PATCH",
1899            HttpMethod::Delete => "DELETE",
1900        }
1901    }
1902
1903    pub fn from_ident(s: &str) -> Option<HttpMethod> {
1904        match s {
1905            "GET" => Some(HttpMethod::Get),
1906            "POST" => Some(HttpMethod::Post),
1907            "PUT" => Some(HttpMethod::Put),
1908            "PATCH" => Some(HttpMethod::Patch),
1909            "DELETE" => Some(HttpMethod::Delete),
1910            _ => None,
1911        }
1912    }
1913
1914    /// True if this method conventionally has no request body.
1915    pub fn forbids_body(self) -> bool {
1916        matches!(self, HttpMethod::Get | HttpMethod::Delete)
1917    }
1918}
1919
1920/// Payload shape of an `HttpResult[T]` variant (v0.9 §3.3).
1921#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1922pub enum HttpVariantPayload {
1923    /// No payload (e.g. `NoContent`, `Unauthorized`).
1924    None,
1925    /// Carries a value of the `HttpResult` type parameter `T`.
1926    Value,
1927    /// Carries a `String` message (e.g. `BadRequest`, `Conflict`).
1928    Message,
1929    /// Carries a `String` target URL, emitted as a `Location` header — the
1930    /// redirect variants (`Found`, `SeeOther`, `PermanentRedirect`, …).
1931    Location,
1932    /// Carries a `Stream[String]`, emitted as an SSE (`text/event-stream`)
1933    /// streaming body — the `Streaming` (200) variant (v0.101, real-time track
1934    /// slice 1).
1935    Streamed,
1936    /// Carries `(body: Bytes, contentType: String)` — the author-owned raw body
1937    /// written straight into the response with the declared `content-type` and
1938    /// **no codec** (the typed-wire guarantee is deliberately off). The `Raw`
1939    /// (200) variant (v0.111), the first two-argument payload shape, and
1940    /// `RawCreated` (201, #2403).
1941    Raw,
1942    /// Carries `(body: Bytes, contentType: String, etag: String)` — a raw body
1943    /// with the handler's own entity tag, sent as the response's `ETag` and
1944    /// validated at run time. The `RawTagged` (200) variant (caching raw
1945    /// responses), for a body too large to hash or a validator the handler has
1946    /// for free (a content hash, an object store's `etag`).
1947    RawTagged,
1948    /// Carries `(body: Stream[Bytes], contentType: String)` — a raw body
1949    /// streamed into the response as its chunks arrive, with no codec and no
1950    /// validator: the bytes can't be hashed without buffering them. The
1951    /// `RawStreamed` (200) variant (K-streams, #2623). Its status is sent
1952    /// before the first chunk, so a fault mid-body aborts the response.
1953    RawStreamed,
1954    /// Carries `(status: ErrorStatus, body: Bytes, contentType: String)` — an
1955    /// error status the author chooses, with an author-owned body and no codec
1956    /// (#2341, L4). The `RawError` variant; `status` is one of the vocabulary's
1957    /// 4xx/5xx variant names ([`error_status`]), resolved at compile time.
1958    RawError,
1959}
1960
1961/// One variant of the built-in `HttpResult[T]` sum (v0.9 §3.3).
1962#[derive(Debug, Clone, Copy)]
1963pub struct HttpVariant {
1964    pub name: &'static str,
1965    pub payload: HttpVariantPayload,
1966    pub status: u16,
1967}
1968
1969/// All `HttpResult[T]` variants, in declaration order (ascending status). The
1970/// vocabulary tracks the common, modern HTTP status codes (RFC 9110): success
1971/// and created/accepted (`Value`), redirects carrying a `Location` URL, and
1972/// the client/server failures that handlers routinely return (`Message` when
1973/// an explanation helps the caller, `None` for self-describing statuses).
1974pub const HTTP_VARIANTS: &[HttpVariant] = &[
1975    // ── 2xx success ──────────────────────────────────────────────────────
1976    HttpVariant {
1977        name: "Ok",
1978        payload: HttpVariantPayload::Value,
1979        status: 200,
1980    },
1981    // v0.101 (real-time track slice 1): a 200 whose body is a streamed
1982    // `Stream[String]`, SSE-framed. Status precedes the body, so streaming is
1983    // 200-only — pre-stream failures are ordinary variants returned instead.
1984    HttpVariant {
1985        name: "Streaming",
1986        payload: HttpVariantPayload::Streamed,
1987        status: 200,
1988    },
1989    // v0.111: a 200 whose body is an author-owned `Bytes` written straight into
1990    // the response with the declared `content-type` — no codec runs. 200-only,
1991    // like `Streaming`: it serves service-tier raw bodies (`robots.txt`,
1992    // `sitemap.xml`, feeds, a QR PNG), not custom-status error pages.
1993    HttpVariant {
1994        name: "Raw",
1995        payload: HttpVariantPayload::Raw,
1996        status: 200,
1997    },
1998    // Caching raw responses: a `Raw` body plus the handler-supplied entity tag
1999    // its `ETag` carries. A separate variant, so every `Raw(…)` call is unchanged.
2000    HttpVariant {
2001        name: "RawTagged",
2002        payload: HttpVariantPayload::RawTagged,
2003        status: 200,
2004    },
2005    // K-streams (#2623): a `Raw` body streamed as a `Stream[Bytes]`, chunk by
2006    // chunk, for a body too large to buffer. 200-only, like `Streaming`: the
2007    // status is sent before the body.
2008    HttpVariant {
2009        name: "RawStreamed",
2010        payload: HttpVariantPayload::RawStreamed,
2011        status: 200,
2012    },
2013    // #2403 (L6): a `Raw` body answered `201` — for an external contract that
2014    // fixes the bytes of a creation's answer. `Raw`'s payload shape, so every
2015    // `Raw` check, binding and emit arm covers it; no validator (a 201 answers a
2016    // write and is not cached).
2017    HttpVariant {
2018        name: "RawCreated",
2019        payload: HttpVariantPayload::Raw,
2020        status: 201,
2021    },
2022    HttpVariant {
2023        name: "Created",
2024        payload: HttpVariantPayload::Value,
2025        status: 201,
2026    },
2027    HttpVariant {
2028        name: "Accepted",
2029        payload: HttpVariantPayload::Value,
2030        status: 202,
2031    },
2032    HttpVariant {
2033        name: "NoContent",
2034        payload: HttpVariantPayload::None,
2035        status: 204,
2036    },
2037    // ── 3xx redirection (carry a `Location` URL) ─────────────────────────
2038    HttpVariant {
2039        name: "MovedPermanently",
2040        payload: HttpVariantPayload::Location,
2041        status: 301,
2042    },
2043    HttpVariant {
2044        name: "Found",
2045        payload: HttpVariantPayload::Location,
2046        status: 302,
2047    },
2048    HttpVariant {
2049        name: "SeeOther",
2050        payload: HttpVariantPayload::Location,
2051        status: 303,
2052    },
2053    HttpVariant {
2054        name: "TemporaryRedirect",
2055        payload: HttpVariantPayload::Location,
2056        status: 307,
2057    },
2058    HttpVariant {
2059        name: "PermanentRedirect",
2060        payload: HttpVariantPayload::Location,
2061        status: 308,
2062    },
2063    // ── 4xx client error ─────────────────────────────────────────────────
2064    HttpVariant {
2065        name: "BadRequest",
2066        payload: HttpVariantPayload::Message,
2067        status: 400,
2068    },
2069    HttpVariant {
2070        name: "Unauthorized",
2071        payload: HttpVariantPayload::None,
2072        status: 401,
2073    },
2074    HttpVariant {
2075        name: "Forbidden",
2076        payload: HttpVariantPayload::None,
2077        status: 403,
2078    },
2079    HttpVariant {
2080        name: "NotFound",
2081        payload: HttpVariantPayload::None,
2082        status: 404,
2083    },
2084    HttpVariant {
2085        name: "MethodNotAllowed",
2086        payload: HttpVariantPayload::None,
2087        status: 405,
2088    },
2089    HttpVariant {
2090        name: "NotAcceptable",
2091        payload: HttpVariantPayload::None,
2092        status: 406,
2093    },
2094    HttpVariant {
2095        name: "RequestTimeout",
2096        payload: HttpVariantPayload::None,
2097        status: 408,
2098    },
2099    HttpVariant {
2100        name: "Conflict",
2101        payload: HttpVariantPayload::Message,
2102        status: 409,
2103    },
2104    HttpVariant {
2105        name: "Gone",
2106        payload: HttpVariantPayload::None,
2107        status: 410,
2108    },
2109    HttpVariant {
2110        name: "LengthRequired",
2111        payload: HttpVariantPayload::None,
2112        status: 411,
2113    },
2114    HttpVariant {
2115        name: "PayloadTooLarge",
2116        payload: HttpVariantPayload::Message,
2117        status: 413,
2118    },
2119    HttpVariant {
2120        name: "UnsupportedMediaType",
2121        payload: HttpVariantPayload::Message,
2122        status: 415,
2123    },
2124    HttpVariant {
2125        name: "UnprocessableEntity",
2126        payload: HttpVariantPayload::Message,
2127        status: 422,
2128    },
2129    HttpVariant {
2130        name: "TooManyRequests",
2131        payload: HttpVariantPayload::Message,
2132        status: 429,
2133    },
2134    HttpVariant {
2135        name: "UnavailableForLegalReasons",
2136        payload: HttpVariantPayload::Message,
2137        status: 451,
2138    },
2139    // ── 5xx server error ─────────────────────────────────────────────────
2140    HttpVariant {
2141        name: "ServerError",
2142        payload: HttpVariantPayload::Message,
2143        status: 500,
2144    },
2145    HttpVariant {
2146        name: "NotImplemented",
2147        payload: HttpVariantPayload::Message,
2148        status: 501,
2149    },
2150    HttpVariant {
2151        name: "BadGateway",
2152        payload: HttpVariantPayload::Message,
2153        status: 502,
2154    },
2155    HttpVariant {
2156        name: "ServiceUnavailable",
2157        payload: HttpVariantPayload::Message,
2158        status: 503,
2159    },
2160    HttpVariant {
2161        name: "GatewayTimeout",
2162        payload: HttpVariantPayload::Message,
2163        status: 504,
2164    },
2165    // #2341 (L4): an error status the author chooses, with an author-owned
2166    // body — the form for an external API's error contract. Its status is
2167    // its first argument, so the row's own is [`AUTHOR_CHOSEN_STATUS`].
2168    HttpVariant {
2169        name: "RawError",
2170        payload: HttpVariantPayload::RawError,
2171        status: AUTHOR_CHOSEN_STATUS,
2172    },
2173];
2174
2175/// The `status` of an [`HttpVariant`] whose status is chosen per construction
2176/// (`RawError`, #2341) rather than fixed by the variant.
2177pub const AUTHOR_CHOSEN_STATUS: u16 = 0;
2178
2179/// The status an `ErrorStatus` name stands for in `RawError`'s first argument
2180/// (#2341, L4): one of the vocabulary's own 4xx/5xx variant names
2181/// (`Forbidden` → 403). `None` for any other name.
2182pub fn error_status(name: &str) -> Option<u16> {
2183    HTTP_VARIANTS
2184        .iter()
2185        .find(|v| v.name == name && (400..600).contains(&v.status))
2186        .map(|v| v.status)
2187}
2188
2189/// Find an `HttpResult[T]` variant by name. Returns the variant info or
2190/// `None` if the name doesn't match.
2191pub fn http_variant(name: &str) -> Option<HttpVariant> {
2192    HTTP_VARIANTS.iter().copied().find(|v| v.name == name)
2193}
2194
2195/// #2341 (L4): the status a `RawError`'s first argument names — a bare 4xx/5xx
2196/// variant name (`Forbidden`) or one qualified by `ErrorStatus`
2197/// (`ErrorStatus.Forbidden`). `None` for any other expression.
2198pub fn raw_error_status(arg: &Expr) -> Option<u16> {
2199    match &arg.kind {
2200        ExprKind::Ident(id) => error_status(&id.name),
2201        ExprKind::FieldAccess { receiver, field } => match &receiver.kind {
2202            ExprKind::Ident(r) if r.name == "ErrorStatus" => error_status(&field.name),
2203            _ => None,
2204        },
2205        _ => None,
2206    }
2207}
2208
2209/// A `Decimal` rounding mode (#2622) — the closed set `round`/`div` accept.
2210/// Written `RoundingMode.HalfEven`; read syntactically at that argument (it is
2211/// not a first-class value in this slice), so it lowers to its name.
2212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2213pub enum RoundingMode {
2214    /// Toward zero (truncate).
2215    Down,
2216    /// Away from zero.
2217    Up,
2218    /// Toward negative infinity.
2219    Floor,
2220    /// Toward positive infinity.
2221    Ceiling,
2222    /// To nearest; a tie goes away from zero.
2223    HalfUp,
2224    /// To nearest; a tie goes toward zero.
2225    HalfDown,
2226    /// To nearest; a tie goes to the even neighbour (banker's rounding).
2227    HalfEven,
2228}
2229
2230impl RoundingMode {
2231    pub const ALL: [RoundingMode; 7] = [
2232        RoundingMode::Down,
2233        RoundingMode::Up,
2234        RoundingMode::Floor,
2235        RoundingMode::Ceiling,
2236        RoundingMode::HalfUp,
2237        RoundingMode::HalfDown,
2238        RoundingMode::HalfEven,
2239    ];
2240
2241    pub fn name(self) -> &'static str {
2242        match self {
2243            RoundingMode::Down => "Down",
2244            RoundingMode::Up => "Up",
2245            RoundingMode::Floor => "Floor",
2246            RoundingMode::Ceiling => "Ceiling",
2247            RoundingMode::HalfUp => "HalfUp",
2248            RoundingMode::HalfDown => "HalfDown",
2249            RoundingMode::HalfEven => "HalfEven",
2250        }
2251    }
2252
2253    pub fn from_name(name: &str) -> Option<Self> {
2254        Self::ALL.into_iter().find(|m| m.name() == name)
2255    }
2256}
2257
2258/// #2622: the rounding mode a `Decimal` `round`/`div` argument names —
2259/// `RoundingMode.HalfEven`. `None` for any other expression.
2260pub fn rounding_mode(arg: &Expr) -> Option<RoundingMode> {
2261    match &arg.kind {
2262        ExprKind::FieldAccess { receiver, field } => match &receiver.kind {
2263            ExprKind::Ident(r) if r.name == "RoundingMode" => RoundingMode::from_name(&field.name),
2264            _ => None,
2265        },
2266        _ => None,
2267    }
2268}
2269
2270/// Payload shape of a `QueueResult` variant (v0.44). Non-generic — a verdict
2271/// carries no value; `Retry` carries a `String` reason for the log path.
2272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2273pub enum QueueVariantPayload {
2274    /// No payload (`Ack`).
2275    None,
2276    /// Carries a `String` reason (`Retry`).
2277    Message,
2278}
2279
2280/// One variant of the built-in `QueueResult` sum (v0.44).
2281#[derive(Debug, Clone, Copy)]
2282pub struct QueueVariant {
2283    pub name: &'static str,
2284    pub payload: QueueVariantPayload,
2285}
2286
2287/// All `QueueResult` variants, in declaration order. `Ack` confirms the
2288/// message; `Retry` redelivers it, carrying a reason for observability.
2289pub const QUEUE_VARIANTS: &[QueueVariant] = &[
2290    QueueVariant {
2291        name: "Ack",
2292        payload: QueueVariantPayload::None,
2293    },
2294    QueueVariant {
2295        name: "Retry",
2296        payload: QueueVariantPayload::Message,
2297    },
2298];
2299
2300/// Find a `QueueResult` variant by name.
2301pub fn queue_variant(name: &str) -> Option<QueueVariant> {
2302    QUEUE_VARIANTS.iter().copied().find(|v| v.name == name)
2303}
2304
2305#[derive(Debug, Clone)]
2306pub struct TypeDecl {
2307    pub name: Ident,
2308    /// `[T, U]` type parameters (v0.157, ADR 0183): empty for a non-generic
2309    /// type. A generic *record* type (`type Paginated[T] = { … }`) is the only
2310    /// generic body accepted; the checker rejects type parameters on refined /
2311    /// opaque / sum bodies. Mirrors [`FnDecl::type_params`].
2312    pub type_params: Vec<TypeParam>,
2313    pub body: TypeBody,
2314    /// `derives Ord` (constrained-generics slice 1): the constraints the type
2315    /// opts into, in source order. Empty when there is no clause. `Eq`, `Key` and
2316    /// `Codable` are automatic, so `Ord` is the only name the checker admits.
2317    pub derives: Vec<Ident>,
2318    /// The span of the `derives` keyword, when there is a clause.
2319    pub derives_kw: Option<Span>,
2320    /// Documentation block attached to this declaration (v0.3).
2321    pub documentation: Option<Doc>,
2322    /// #2618: the span of a leading `private` modifier, when the declaration
2323    /// is unit-private: visible across its own unit's files (and to that
2324    /// unit's suites), never through `uses`. `None` is the default, public.
2325    pub private: Option<Span>,
2326    pub span: Span,
2327    pub trivia: Trivia,
2328}
2329
2330/// `event Name = { fields }` — a typed fact a context may emit and other
2331/// contexts' subscriber services may receive (Events track, slice 0, spine
2332/// #936). Record body only in slice 0 — pattern refinement (subscription
2333/// side, slice 1) and default-valued fields for additive versioning (slice
2334/// 3a) both extend a record body, so nothing here forecloses them. An
2335/// optional `@schema(N)` annotation (slice 3b) asserts the event's current
2336/// wire schema version, embedded into `env.schemaVersion` at emission — see
2337/// [`EventDecl::schema_version`]. Legal only inside a `context` —
2338/// checker-enforced (`bynk.event.outside_context`), not grammar, mirroring
2339/// how `capability`/`provides` are commons-rejected at the parser while
2340/// `event` instead follows `messages`' precedent (ADR 0272) of parsing
2341/// uniformly and letting the checker place it, since unlike
2342/// `capability`/`provides` an `event` has no meaning to reject early inside
2343/// an `adapter` either.
2344#[derive(Debug, Clone)]
2345pub struct EventDecl {
2346    pub name: Ident,
2347    /// Every `@`-annotation attached to this declaration. The parser stays
2348    /// permissive (zero or more, same as `store` field / `messages`
2349    /// annotations); the closed registry (today: `@schema` alone) and its
2350    /// argument shape are a checker concern (`bynk.event.unknown_annotation`
2351    /// / `bynk.event.bad_schema_version`), not a parse error.
2352    pub annotations: Vec<Annotation>,
2353    pub body: RecordBody,
2354    /// Documentation block attached to this declaration.
2355    pub documentation: Option<Doc>,
2356    pub span: Span,
2357    pub trivia: Trivia,
2358}
2359
2360impl EventDecl {
2361    /// A synthetic `TypeDecl` with this event's name and record body, so an
2362    /// event registers into the ordinary `types` symbol table and reuses
2363    /// every existing type-reference/exports/consumes/construction check —
2364    /// no non-generic type parameters, no separate resolution path. Callers
2365    /// that need to know a name is specifically an *event* (owner-only
2366    /// emission, `from Events(E)`/`Events.emit[E]`'s "must be an event, not
2367    /// just any type" gate) track that separately, alongside this.
2368    ///
2369    /// Deliberately lossy: a `TypeDecl` has no `annotations`, so `@schema(N)`
2370    /// does not survive this conversion. Nothing downstream of this
2371    /// synthesis needs the event's schema version — only the emitter's own
2372    /// `Events.emit` lowering does, and it reads [`EventDecl::schema_version`]
2373    /// directly off the real declaration instead.
2374    pub fn as_type_decl(&self) -> TypeDecl {
2375        TypeDecl {
2376            derives: Vec::new(),
2377            derives_kw: None,
2378            name: self.name.clone(),
2379            type_params: Vec::new(),
2380            body: TypeBody::Record(self.body.clone()),
2381            documentation: self.documentation.clone(),
2382            span: self.span,
2383            trivia: self.trivia.clone(),
2384            private: None,
2385        }
2386    }
2387
2388    /// This event's declared wire schema version (Events slice 3b, #978):
2389    /// the positive `Int` literal argument of its sole `@schema(N)`
2390    /// annotation, or `1` if the annotation is absent — identical to every
2391    /// event's behaviour before this annotation existed. A malformed
2392    /// `@schema` (non-positive, non-literal, wrong arity, labelled, or
2393    /// duplicated) has already been reported by the checker
2394    /// (`bynk.event.bad_schema_version`); this falls back to `1` rather than
2395    /// re-deriving that diagnostic.
2396    pub fn schema_version(&self) -> i64 {
2397        self.annotations
2398            .iter()
2399            .find(|a| a.name.name == "schema")
2400            .and_then(|a| a.args.first())
2401            .and_then(|arg| match &arg.value.kind {
2402                ExprKind::IntLit { value, .. } if *value > 0 => Some(*value),
2403                _ => None,
2404            })
2405            .unwrap_or(1)
2406    }
2407}
2408
2409/// The structural filter on a `from Events(E { field: value, .. })`
2410/// subscription header (Events track, slice 1, spine #936) — deliver-and-filter:
2411/// every emission still reaches the fan-out mechanism, and the subscriber's
2412/// own generated handler evaluates this as a boolean guard before running the
2413/// body. Deliberately **not** a [`Pattern`] — an event is a plain record, not
2414/// a sum, so it has no tag for [`Pattern::Variant`] to test; extending the
2415/// shared `Pattern` enum to fit would touch parser/checker/emitter/fmt/
2416/// tree-sitter/LSP sites and drag in match-exhaustiveness semantics a
2417/// delivery filter does not need. This amends
2418/// [ADR 0286](../decisions/0286-events-pattern-dispatch-deliver-and-filter.md)'s
2419/// "no bespoke matching engine is introduced for Events" claim; its
2420/// deliver-and-filter decision is unchanged. No static narrowing: a matching
2421/// handler body still sees its parameter at its own declared type, never
2422/// narrowed to a listed field's specific value (deferred — narrowing needs a
2423/// singleton-variant type the checker does not have, and waits on the
2424/// refinement-propagation design question `design/bynk-type-system.md`
2425/// §2.5.4 names as still open).
2426#[derive(Debug, Clone)]
2427pub struct EventPattern {
2428    /// The listed fields, in source order. Never empty — a pattern with no
2429    /// fields has no shape (`from Events(E)`, no braces, is the pattern-less
2430    /// form; `from Events(E { })` is a parse error pointing at it).
2431    pub fields: Vec<EventPatternField>,
2432    /// The span of the required trailing `..` — every listed field leaves
2433    /// the rest of the record's fields unconstrained, and that must be
2434    /// written explicitly rather than implied.
2435    pub rest_span: Span,
2436    pub span: Span,
2437}
2438
2439/// One `name: value` entry in an [`EventPattern`].
2440#[derive(Debug, Clone)]
2441pub struct EventPatternField {
2442    pub name: Ident,
2443    pub value: EventPatternValue,
2444    pub span: Span,
2445}
2446
2447/// The value a pattern field is matched against. A closed set, mirroring
2448/// [`Pattern::Literal`]'s closed literal kinds plus a nullary sum-variant
2449/// reference — no nested record sub-patterns in v1 (slice 1 filters on
2450/// top-level fields only).
2451#[derive(Debug, Clone)]
2452pub enum EventPatternValue {
2453    /// An `Int`/`String`/`Bool` literal — matches the field by value equality.
2454    Literal {
2455        value: LiteralValue,
2456        /// The literal as written (`-1_000`), digit separators kept; `None`
2457        /// for a pattern built without source. Only the formatter reads it,
2458        /// and it sits outside `value` so the duplicate-arm check ignores it.
2459        text: Option<String>,
2460        span: Span,
2461    },
2462    /// A nullary sum-type variant, optionally qualified: `Region.Domestic` or
2463    /// bare `Domestic` — both resolve against the field's declared sum type.
2464    /// A variant that carries a payload is rejected (`bynk.event.
2465    /// pattern_variant_payload`): testing only the tag while ignoring a
2466    /// payload would silently over-broaden the filter.
2467    Variant {
2468        /// `Some(Region)` for the qualified form, `None` for bare.
2469        type_name: Option<Ident>,
2470        variant: Ident,
2471        span: Span,
2472    },
2473}
2474
2475impl EventPattern {
2476    pub fn span(&self) -> Span {
2477        self.span
2478    }
2479}
2480
2481impl EventPatternValue {
2482    pub fn span(&self) -> Span {
2483        match self {
2484            EventPatternValue::Literal { span, .. } => *span,
2485            EventPatternValue::Variant { span, .. } => *span,
2486        }
2487    }
2488}
2489
2490/// A `via schema(...)` dispatch clause on a `from Events(...)` header
2491/// (Events track, slice 4, spine #936): filters delivery by the envelope's
2492/// `schemaVersion`, parallel to [`EventPattern`] but matched against the
2493/// envelope rather than the payload, and written after the `Events(...)`
2494/// header's closing `)` rather than inside it. Delivery is still
2495/// deliver-and-filter (unchanged from slice 1's ADR 0286): the fan-out
2496/// mechanism delivers every emission to every subscriber regardless, and
2497/// this becomes one more independently-evaluated runtime guard in the
2498/// subscriber's own generated handler — no cross-subscriber ambiguity
2499/// check (two sibling subscribers with the same or overlapping version
2500/// coverage are both legal, undiagnosed).
2501#[derive(Debug, Clone)]
2502pub struct SchemaDispatch {
2503    pub pattern: SchemaVersionPattern,
2504    pub span: Span,
2505}
2506
2507/// The pattern a `via schema(...)` clause matches `env.schemaVersion`
2508/// against: a literal, an inclusive integer range, or `_`. Mirrors
2509/// `@schema(N)`'s own permissive-parse-then-checker-validate split (a
2510/// non-positive bound is a checker error, not a parse error, for the same
2511/// diagnostic style). The range is the same [`IntRange`] a match pattern uses
2512/// (events slice 4b, #990, with patterns slice 5, #2699).
2513#[derive(Debug, Clone)]
2514pub enum SchemaVersionPattern {
2515    Literal(i64),
2516    /// `via schema(2..4)`, `(2..)`, `(..4)`; bounds are inclusive.
2517    Range(IntRange),
2518    /// `via schema(_)`: any version, the same as omitting the clause.
2519    Wildcard,
2520}
2521
2522/// The right-hand side of a `type` declaration. In v0/v0.1 only the
2523/// `Refined` variant existed; v0.2 adds records and sums; v0.3 adds opaque.
2524#[derive(Debug, Clone)]
2525pub enum TypeBody {
2526    /// Refined base type: `BaseType where refinement`.
2527    Refined {
2528        base: BaseType,
2529        base_span: Span,
2530        refinement: Option<Refinement>,
2531    },
2532    /// Record type: `{ field: T where ..., ... }`.
2533    Record(RecordBody),
2534    /// Sum type: pipe-form variants or `enum { ... }` shorthand.
2535    Sum(SumBody),
2536    /// Opaque base type: `opaque BaseType (where refinement)?` (v0.3 §3.4).
2537    /// Identity is nominal; the base type is hidden outside the defining commons.
2538    Opaque {
2539        base: BaseType,
2540        base_span: Span,
2541        refinement: Option<Refinement>,
2542    },
2543}
2544
2545/// Body of a record-type declaration (v0.2 §3.1).
2546#[derive(Debug, Clone)]
2547pub struct RecordBody {
2548    pub fields: Vec<RecordField>,
2549    pub span: Span,
2550    /// #1788: comments before the closing `}`.
2551    pub trailing_comments: Vec<Comment>,
2552}
2553
2554/// One field of a record type declaration. Each field may carry inline
2555/// refinement, which is enforced at construction time on the field's value.
2556#[derive(Debug, Clone)]
2557pub struct RecordField {
2558    pub name: Ident,
2559    pub type_ref: TypeRef,
2560    pub refinement: Option<Refinement>,
2561    /// v0.11: an optional initial-value expression. Only meaningful on agent
2562    /// `state` fields (the field's fresh-key value); ignored / rejected on
2563    /// record-type fields by the checker.
2564    pub init: Option<Expr>,
2565    pub span: Span,
2566    /// #1888: the doc above the field, in either form. Hover, completion and
2567    /// the documentation page show it.
2568    pub documentation: Option<Doc>,
2569    /// #1788: comments above the field and at the end of its line.
2570    pub trivia: Trivia,
2571}
2572
2573/// Body of a sum-type declaration (v0.2 §3.2).
2574#[derive(Debug, Clone)]
2575pub struct SumBody {
2576    pub variants: Vec<Variant>,
2577    /// v0.154 (ADR 0178): declared error embeddings — `embeds E as V, …` after
2578    /// the variants. Each says "an `E` value auto-wraps into variant `V`", which
2579    /// the `?` operator uses to convert a cross-context error without a manual
2580    /// `.mapErr`. Empty for a sum with no embeddings.
2581    pub embeds: Vec<EmbedsClause>,
2582    pub span: Span,
2583    /// #1794: comments before the closing `}` of an `enum { … }` body, or, in
2584    /// the pipe form, on their own lines between the last variant and its
2585    /// `embeds` clause.
2586    pub trailing_comments: Vec<Comment>,
2587}
2588
2589/// One `embeds <source_type> as <variant>` mapping in a sum body (v0.154, ADR
2590/// 0178). Declares that a value of `source_type` can be auto-wrapped into the
2591/// named single-payload `variant` of the enclosing sum.
2592#[derive(Debug, Clone)]
2593pub struct EmbedsClause {
2594    pub source_type: TypeRef,
2595    pub variant: Ident,
2596    pub span: Span,
2597}
2598
2599/// One variant of a sum type. Variants may have payload fields; a
2600/// payload-less variant is a simple tag.
2601#[derive(Debug, Clone)]
2602pub struct Variant {
2603    pub name: Ident,
2604    pub payload: Vec<VariantField>,
2605    pub span: Span,
2606    /// #1888: the doc above the variant, in either form.
2607    pub documentation: Option<Doc>,
2608    /// #1794: comments above the variant and at the end of its line. A comment
2609    /// on an `enum {` line leads the first variant, and so does one on the `=`
2610    /// line of a pipe-form sum. The last pipe-form variant's end-of-line
2611    /// comment is the type's own trailing comment unless an `embeds` clause
2612    /// follows it.
2613    pub trivia: Trivia,
2614}
2615
2616/// One payload field of a sum variant. Variant payload fields use named
2617/// declarations like record fields, but do not carry refinement in v0.2.
2618#[derive(Debug, Clone)]
2619pub struct VariantField {
2620    pub name: Ident,
2621    pub type_ref: TypeRef,
2622    pub span: Span,
2623    /// #1888: the doc above the payload field, in either form.
2624    pub documentation: Option<Doc>,
2625}
2626
2627#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
2628pub enum BaseType {
2629    Int,
2630    String,
2631    Bool,
2632    Float,
2633    /// `Duration` (v0.86, ADR 0112) — a span of time, a distinct base type
2634    /// erased to TS `number` carrying milliseconds (the `Clock` unit). Modelled
2635    /// on `Float`: Bynk-side-only, no implicit `Int` coercion (save the one
2636    /// sanctioned clock-math mix).
2637    Duration,
2638    /// `Instant` (v0.90, ADR 0114) — an absolute point in time, a distinct base
2639    /// type erased to TS `number` carrying Unix epoch milliseconds (the
2640    /// `Clock` unit). No literal (minted by `Clock.now()`); arithmetic composes
2641    /// with `Duration` (`Instant ± Duration -> Instant`, `Instant − Instant ->
2642    /// Duration`). Supersedes ADR 0112 D4's `Int`↔`Duration` clock-math mix.
2643    Instant,
2644    /// `Bytes` (v0.110, ADR 0142) — an immutable finite octet sequence, the
2645    /// seventh base type. Unlike its neighbours it does **not** erase to TS
2646    /// `number`: a `Bytes` lowers to a `Uint8Array`. No source literal
2647    /// (constructed via `Bytes.fromUtf8`/`fromBase64`/`empty`); `==` compares
2648    /// by content (real emitter codegen, not host `===`); wires as a base64
2649    /// JSON string; not `Map`-keyable and not orderable.
2650    Bytes,
2651    /// `Decimal` (#2622) — an exact base-10 number: an arbitrary-precision
2652    /// integer coefficient with a non-negative scale. Like `Bytes` it does not
2653    /// erase to a JS primitive (it lowers to a frozen `{ coefficient, scale }`
2654    /// object over `BigInt`); `==`/`<` compare by numeric value; it wires as a
2655    /// canonical JSON string (`"1.20"`).
2656    Decimal,
2657}
2658
2659impl BaseType {
2660    pub fn name(self) -> &'static str {
2661        match self {
2662            BaseType::Int => "Int",
2663            BaseType::String => "String",
2664            BaseType::Bool => "Bool",
2665            BaseType::Float => "Float",
2666            BaseType::Duration => "Duration",
2667            BaseType::Instant => "Instant",
2668            BaseType::Bytes => "Bytes",
2669            BaseType::Decimal => "Decimal",
2670        }
2671    }
2672
2673    /// The base type written `name`, or `None` for anything else.
2674    pub fn from_name(name: &str) -> Option<Self> {
2675        Some(match name {
2676            "Int" => BaseType::Int,
2677            "String" => BaseType::String,
2678            "Bool" => BaseType::Bool,
2679            "Float" => BaseType::Float,
2680            "Duration" => BaseType::Duration,
2681            "Instant" => BaseType::Instant,
2682            "Bytes" => BaseType::Bytes,
2683            "Decimal" => BaseType::Decimal,
2684            _ => return None,
2685        })
2686    }
2687}
2688
2689/// A `Duration` literal unit (v0.86, ADR 0112) — the closed set of suffixes in a
2690/// `<int>.<unit>` literal. Each maps to a fixed millisecond factor (`Duration`
2691/// erases to `Int` milliseconds).
2692#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2693pub enum DurationUnit {
2694    Milliseconds,
2695    Seconds,
2696    Minutes,
2697    Hours,
2698    Days,
2699}
2700
2701impl DurationUnit {
2702    /// Resolve a unit name (`minutes`) to its variant, or `None` if it is not one
2703    /// of the closed set. Used by the parser to recognise an `<int>.<unit>`
2704    /// literal; an unrecognised name leaves the expression a field access.
2705    pub fn from_name(name: &str) -> Option<Self> {
2706        Some(match name {
2707            "milliseconds" => DurationUnit::Milliseconds,
2708            "seconds" => DurationUnit::Seconds,
2709            "minutes" => DurationUnit::Minutes,
2710            "hours" => DurationUnit::Hours,
2711            "days" => DurationUnit::Days,
2712            _ => return None,
2713        })
2714    }
2715
2716    /// The unit name as written.
2717    pub fn name(self) -> &'static str {
2718        match self {
2719            DurationUnit::Milliseconds => "milliseconds",
2720            DurationUnit::Seconds => "seconds",
2721            DurationUnit::Minutes => "minutes",
2722            DurationUnit::Hours => "hours",
2723            DurationUnit::Days => "days",
2724        }
2725    }
2726
2727    /// The unit's value in milliseconds.
2728    pub fn millis(self) -> i64 {
2729        match self {
2730            DurationUnit::Milliseconds => 1,
2731            DurationUnit::Seconds => 1_000,
2732            DurationUnit::Minutes => 60_000,
2733            DurationUnit::Hours => 3_600_000,
2734            DurationUnit::Days => 86_400_000,
2735        }
2736    }
2737}
2738
2739/// An integer refinement bound (v0.40, ADR 0073): the parsed value plus the
2740/// bound's source span (covering a leading `-`). Value-only beyond the span —
2741/// ints have one canonical printed form, so the formatter stays idempotent
2742/// without a stored lexeme. The span backs the `InRange`-swap quick-fix.
2743#[derive(Debug, Clone)]
2744pub struct IntBound {
2745    pub value: i64,
2746    /// The bound as written (`-20_000`), digit separators kept (#2993). Only
2747    /// the formatter reads it; `value` is what the checker uses.
2748    pub lexeme: String,
2749    pub span: Span,
2750}
2751
2752/// A float refinement bound (v0.21): the parsed value plus the signed source
2753/// lexeme (for byte-stable emission). v0.40 (ADR 0073): also the source span,
2754/// for the `InRange`-swap quick-fix.
2755#[derive(Debug, Clone)]
2756pub struct FloatBound {
2757    pub value: f64,
2758    pub lexeme: String,
2759    pub span: Span,
2760}
2761
2762/// A decimal refinement bound (#2622): `InRange(0.00d, 100.00d)`. The
2763/// coefficient keeps any leading `-`; the lexeme is the signed source text.
2764#[derive(Debug, Clone)]
2765pub struct DecimalBound {
2766    pub coef: String,
2767    pub scale: u32,
2768    pub lexeme: String,
2769    pub span: Span,
2770}
2771
2772#[derive(Debug, Clone)]
2773pub struct Refinement {
2774    pub predicates: Vec<RefinementPred>,
2775    pub span: Span,
2776}
2777
2778#[derive(Debug, Clone)]
2779pub struct RefinementPred {
2780    pub kind: PredKind,
2781    /// The integer argument of `MinLength`/`MaxLength`/`Length` as written
2782    /// (`1_000`), digit separators kept (#2993); `None` for other predicates
2783    /// and for a predicate built without source. Only the formatter reads it.
2784    pub arg_text: Option<String>,
2785    pub span: Span,
2786}
2787
2788#[derive(Debug, Clone)]
2789pub enum PredKind {
2790    Matches(String),
2791    InRange(IntBound, IntBound),
2792    /// `InRange` with float bounds (v0.21) — a separate variant so every
2793    /// `Int` refinement path stays untouched. Bounds keep their source
2794    /// lexemes (including any sign) so emitted runtime checks are
2795    /// byte-stable.
2796    InRangeF(FloatBound, FloatBound),
2797    /// `InRange` with decimal bounds (#2622): `InRange(0.00d, 100.00d)`.
2798    InRangeD(DecimalBound, DecimalBound),
2799    MinLength(i64),
2800    MaxLength(i64),
2801    Length(i64),
2802    NonNegative,
2803    Positive,
2804    NonEmpty,
2805    /// #2867: a `String` that is a real calendar date, exactly `YYYY-MM-DD`
2806    /// (years 0000-9999). The same date rule `Instant.fromIso` applies.
2807    IsoDate,
2808}
2809
2810/// #2867: is `s` exactly `YYYY-MM-DD` and a date that exists? The one
2811/// compile-time statement of the rule `IsoDate` enforces; the runtime's
2812/// `__bynkIsIsoDate` (`bynk-emit/runtime/src/time.ts`) is its TypeScript twin
2813/// and shares the calendar with `Instant.fromIso`.
2814pub fn is_iso_date(s: &str) -> bool {
2815    let b = s.as_bytes();
2816    if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
2817        return false;
2818    }
2819    let digits = |r: std::ops::Range<usize>| -> Option<u32> {
2820        let part = &b[r];
2821        if part.iter().all(u8::is_ascii_digit) {
2822            Some(part.iter().fold(0, |n, d| n * 10 + u32::from(d - b'0')))
2823        } else {
2824            None
2825        }
2826    };
2827    let (Some(year), Some(month), Some(day)) = (digits(0..4), digits(5..7), digits(8..10)) else {
2828        return false;
2829    };
2830    let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
2831    let days = match month {
2832        2 if leap => 29,
2833        2 => 28,
2834        4 | 6 | 9 | 11 => 30,
2835        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2836        _ => return false,
2837    };
2838    (1..=days).contains(&day)
2839}
2840
2841impl PredKind {
2842    pub fn name(&self) -> &'static str {
2843        match self {
2844            PredKind::Matches(_) => "Matches",
2845            PredKind::InRange(..) | PredKind::InRangeF(..) | PredKind::InRangeD(..) => "InRange",
2846            PredKind::MinLength(_) => "MinLength",
2847            PredKind::MaxLength(_) => "MaxLength",
2848            PredKind::Length(_) => "Length",
2849            PredKind::NonNegative => "NonNegative",
2850            PredKind::Positive => "Positive",
2851            PredKind::NonEmpty => "NonEmpty",
2852            PredKind::IsoDate => "IsoDate",
2853        }
2854    }
2855}
2856
2857/// #1651: the order a refinement's predicates are **checked** in at runtime:
2858/// every `Matches` after every other predicate, and otherwise source order.
2859///
2860/// A `Matches` pattern may take time polynomial in the input's length, and a
2861/// `MaxLength`/`Length` bound is what caps it (`bynk.types.polynomial_regex_capped`),
2862/// so the length checks must run first wherever the author wrote them. The order
2863/// is observable only in which failure a value failing several predicates
2864/// reports. Every runtime check site (`.of`, the boundary codec, `is`) and the
2865/// checker's compile-time literal check use this order, so they agree. The
2866/// canonical form a contract fingerprint hashes sorts its own copy and is
2867/// unaffected.
2868pub fn in_check_order<T>(preds: &[T], kind: impl Fn(&T) -> &PredKind) -> Vec<&T> {
2869    let (regex, rest): (Vec<&T>, Vec<&T>) = preds
2870        .iter()
2871        .partition(|p| matches!(kind(p), PredKind::Matches(_)));
2872    rest.into_iter().chain(regex).collect()
2873}
2874
2875/// A function type parameter (v0.20a, `fn name[A, B](…)`). A struct rather
2876/// than a bare Ident so the ADR-0028 "bound-capable" promise is a later field
2877/// addition, not a representation change.
2878#[derive(Debug, Clone)]
2879pub struct TypeParam {
2880    pub name: Ident,
2881    /// Declared constraints, `T: C1 + C2` (constrained-generics slice 0). Empty
2882    /// for an unbounded parameter. Slice 0 admits only `Eq`; the checker
2883    /// reports any other name as `bynk.generics.unknown_constraint`.
2884    pub bounds: Vec<Ident>,
2885    pub span: Span,
2886}
2887
2888/// One `A: C1 + C2` entry of a method's `where` clause (constrained-generics
2889/// slice 0): bounds a type parameter the method does not declare itself — the
2890/// receiver type's (`fn Box.has(self, x: A) -> Bool where A: Eq`).
2891#[derive(Debug, Clone)]
2892pub struct WhereBound {
2893    pub param: Ident,
2894    pub bounds: Vec<Ident>,
2895    pub span: Span,
2896}
2897
2898/// A lambda expression (v0.20a): `(params) => expr` or `(params) => { … }`.
2899/// `=>` is the value arrow (shared with `match`); param annotations are
2900/// optional where an expected function type supplies them.
2901#[derive(Debug, Clone)]
2902pub struct LambdaExpr {
2903    pub params: Vec<LambdaParam>,
2904    pub body: Box<Expr>,
2905    pub span: Span,
2906    /// #2712: whether the author wrote the body as a `{ … }` block. Only read
2907    /// when a parameter is a pattern, whose desugaring wraps the body in a block
2908    /// either way; the formatter needs the source form back.
2909    pub block_body: bool,
2910}
2911
2912/// A lambda parameter. A separate type from [`Param`] because its annotation
2913/// is optional — `Param.type_ref` stays mandatory at every signature site.
2914#[derive(Debug, Clone)]
2915pub struct LambdaParam {
2916    pub name: Ident,
2917    pub type_ref: Option<TypeRef>,
2918    pub span: Span,
2919    /// #2712: the irrefutable record pattern the author wrote in place of a name
2920    /// — `(Item { qty, .. }) => …`. `name` is then the hidden
2921    /// `__lambdaArg<offset>` (empty span, at the pattern), `type_ref` is `None`,
2922    /// and the lambda's body is already desugared to
2923    /// `{ let <pattern> = __lambdaArg<offset>; <body> }` through the `else`-less
2924    /// `let` (ADR 0484, #2654). The pattern is kept here for the checker (the
2925    /// parameter's type comes from its name) and to print the source form back.
2926    pub pattern: Option<Pattern>,
2927}
2928
2929/// #2712: the prefix of every hidden lambda-parameter binder. See
2930/// [`LambdaParam::pattern`]; like [`LET_ELSE_BINDER_PREFIX`] it cannot collide
2931/// with a user identifier.
2932pub const LAMBDA_ARG_BINDER_PREFIX: &str = "__lambdaArg";
2933
2934/// #2896: the prefix of the hidden name a wildcard lambda parameter (`_`) is
2935/// given — `__wild<offset>`, with an empty span at the `_`. The parameter takes
2936/// its type from the expected function type like a named one; the unique name
2937/// keeps `(_, _) => …` from emitting duplicate TS identifiers, and no user
2938/// identifier can spell it, so nothing in the body can refer to it.
2939pub const LAMBDA_WILDCARD_PREFIX: &str = "__wild";
2940
2941impl LambdaParam {
2942    /// #2896: whether this is a `_` parameter. Keyed on the hidden prefix,
2943    /// which no user identifier can start with (identifiers do not begin with
2944    /// `_`); any new generated name must not reuse `__wild`.
2945    pub fn is_wildcard(&self) -> bool {
2946        self.pattern.is_none() && self.name.name.starts_with(LAMBDA_WILDCARD_PREFIX)
2947    }
2948}
2949
2950#[derive(Debug, Clone)]
2951pub struct FnDecl {
2952    /// v0.20a: `[A, B]` type parameters; empty for non-generic functions.
2953    pub type_params: Vec<TypeParam>,
2954    /// Free function or method (`TypeName.methodName`). See [`FnName`].
2955    pub name: FnName,
2956    pub params: Vec<Param>,
2957    pub return_type: TypeRef,
2958    /// #2868: `given C1, C2` on a context-level function, parsed straight after
2959    /// the return type: the capabilities the body may use, and that every
2960    /// caller must itself have in scope. Empty when absent.
2961    pub given: Vec<CapRef>,
2962    /// `where A: Eq, …`, parsed between the return type and the contract
2963    /// clauses; empty when absent. Bounds receiver type parameters (and may
2964    /// restate the method's own).
2965    pub where_bounds: Vec<WhereBound>,
2966    /// v0.115: preconditions (`requires <name>: <pred>`), parsed between the
2967    /// return type and the body. A contract clause is the invariant predicate
2968    /// attached to a function (ADR 0144 — one predicate surface); `requires`
2969    /// scopes over the parameters only.
2970    pub requires: Vec<Contract>,
2971    /// v0.115: postconditions (`ensures <name>: <pred>`). Scopes over the
2972    /// parameters *and* `result`, the contextual binding for the return value.
2973    pub ensures: Vec<Contract>,
2974    pub body: Block,
2975    /// True when the first parameter is the special `self` parameter. Only
2976    /// valid for method declarations.
2977    pub has_self: bool,
2978    /// Documentation block attached to this declaration (v0.3).
2979    pub documentation: Option<Doc>,
2980    /// #2618: the span of a leading `private` modifier, when the declaration
2981    /// is unit-private: visible across its own unit's files (and to that
2982    /// unit's suites), never through `uses`. `None` is the default, public.
2983    pub private: Option<Span>,
2984    pub span: Span,
2985    pub trivia: Trivia,
2986}
2987
2988/// A function-declaration name: either a free function `f` or a method
2989/// `T.method` (v0.2 §3.6).
2990#[derive(Debug, Clone)]
2991pub enum FnName {
2992    /// `fn name(...)` — a free function.
2993    Free(Ident),
2994    /// `fn TypeName.methodName(...)` — a method attached to a type.
2995    Method {
2996        type_name: Ident,
2997        method_name: Ident,
2998    },
2999}
3000
3001impl FnName {
3002    /// The function's short name for diagnostics. For methods returns the
3003    /// method portion only; the type prefix is recovered via `type_name`.
3004    pub fn ident(&self) -> &Ident {
3005        match self {
3006            FnName::Free(id) => id,
3007            FnName::Method { method_name, .. } => method_name,
3008        }
3009    }
3010
3011    /// For methods, the attached type's identifier; `None` for free fns.
3012    pub fn type_name(&self) -> Option<&Ident> {
3013        match self {
3014            FnName::Free(_) => None,
3015            FnName::Method { type_name, .. } => Some(type_name),
3016        }
3017    }
3018
3019    /// The displayed full name (e.g., `Money.add` or `parseSku`).
3020    pub fn display(&self) -> String {
3021        match self {
3022            FnName::Free(id) => id.name.clone(),
3023            FnName::Method {
3024                type_name,
3025                method_name,
3026            } => format!("{}.{}", type_name.name, method_name.name),
3027        }
3028    }
3029}
3030
3031/// A brace-delimited block of statements ending in a tail expression
3032/// whose value is the block's value (spec v0.1 §3.1).
3033#[derive(Debug, Clone)]
3034pub struct Block {
3035    pub statements: Vec<Statement>,
3036    pub tail: Box<Expr>,
3037    pub span: Span,
3038    /// Line comments that appear between the last statement (or the
3039    /// opening brace) and the tail expression. Preserved here because
3040    /// expressions do not carry trivia in v1.1.
3041    pub tail_leading_comments: Vec<Comment>,
3042    /// `true` when the block was written with no explicit tail expression and
3043    /// the parser synthesised a `()` (unit) tail (v0.146, ADR 0170). The tail
3044    /// is a real `ExprKind::UnitLit` either way; this flag records that it was
3045    /// *implicit* so the formatter can omit it (Bynk has no statement
3046    /// terminator, so a printed `()` would re-attach to the last statement on
3047    /// re-parse — `x` `()` → `x()`). The parser re-derives the implicit unit
3048    /// tail, so omitting it is loss-free.
3049    pub implicit_tail: bool,
3050}
3051
3052impl Block {
3053    /// Whether this block is a synthesised empty unit block — no statements and
3054    /// an *implicit* `()` tail (v0.146, ADR 0170). This is exactly the shape the
3055    /// parser inserts for an `if` with no `else` branch, so both the checker
3056    /// (gating the else-less form to unit) and the formatter (omitting the
3057    /// synthetic `else { () }`) recognise it here.
3058    pub fn is_synth_unit(&self) -> bool {
3059        self.statements.is_empty()
3060            && self.implicit_tail
3061            && matches!(self.tail.kind, ExprKind::UnitLit)
3062    }
3063}
3064
3065/// Block-level statement.
3066#[derive(Debug, Clone)]
3067pub enum Statement {
3068    /// `let name (: T)? = expr` — pure binding (v0.1).
3069    Let(LetStmt),
3070    /// `let name (: T)? <- expr` — effectful binding (v0.5).
3071    EffectLet(LetStmt),
3072    /// `expect expr` — verify a Bool predicate at test runtime (v0.7; renamed
3073    /// from `assert` in v0.112). Only valid inside test case bodies.
3074    Expect(ExpectStmt),
3075    /// `~> expr` — an asynchronous fire-and-forget send (v0.79). The caller does
3076    /// not await the reply; legal only when the reply is `Effect[()]`. No binder.
3077    Send(SendStmt),
3078    /// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
3079    /// Runs an `Effect[()]` and discards its (unit) result — the binder-free
3080    /// sugar for `let _ <- expr` when the awaited value is unit. Legal only in
3081    /// an effectful body; the operand MUST be `Effect[()]` (a valued reply keeps
3082    /// the explicit `let _ <- e`, so throwing away a real value stays visible).
3083    Do(DoStmt),
3084    /// `name := expr` — a `Cell` store write (v0.81, storage track). The
3085    /// unconditional write form; `.update(fn)` (a method call) is the
3086    /// read-modify-write form. ADR 0108.
3087    Assign(AssignStmt),
3088    /// `advance Clock by <Duration>` — move the harness clock forward
3089    /// (#2137, end-to-end testing track §3.4). `advance` is contextual: it
3090    /// opens a statement only before `<Ident> by`. Only valid in a `case` body.
3091    Advance(AdvanceStmt),
3092    /// `par { let a <- e1  let b <- e2 }` — a concurrent binding block (#2640,
3093    /// concurrent-effects slice 0). `par` is contextual: it opens a statement
3094    /// only before `{`. The parser admits any statements inside; the checker
3095    /// accepts only `let x <- e` and `do e`.
3096    Par(ParStmt),
3097}
3098
3099impl Statement {
3100    pub fn span(&self) -> Span {
3101        match self {
3102            Statement::Let(l) | Statement::EffectLet(l) => l.span,
3103            Statement::Expect(a) => a.span,
3104            Statement::Send(s) => s.span,
3105            Statement::Do(d) => d.span,
3106            Statement::Assign(a) => a.span,
3107            Statement::Advance(a) => a.span,
3108            Statement::Par(p) => p.span,
3109        }
3110    }
3111}
3112
3113/// #2640: a `par { … }` block. `statements` is what the author wrote between
3114/// the braces, unvalidated: the checker rejects every form but the effectful
3115/// `let` and `do` (`bynk.par.invalid_statement`), so the editor still sees the
3116/// rejected statement in place.
3117#[derive(Debug, Clone)]
3118pub struct ParStmt {
3119    /// The contextual `par` keyword.
3120    pub keyword: Span,
3121    pub statements: Vec<Statement>,
3122    /// Comments between the last statement and the closing `}`.
3123    pub closing_comments: Vec<Comment>,
3124    pub span: Span,
3125    pub trivia: Trivia,
3126}
3127
3128/// #2137: `advance <Cap> by <amount>` — moves the case's harness clock forward
3129/// by a `Duration`. `cap` must be `Clock` (the checker refuses any other).
3130#[derive(Debug, Clone)]
3131pub struct AdvanceStmt {
3132    /// The contextual `advance` keyword.
3133    pub keyword: Span,
3134    /// The capability whose time moves (`Clock`).
3135    pub cap: Ident,
3136    /// The `Duration` to move it by.
3137    pub amount: Expr,
3138    pub span: Span,
3139    pub trivia: Trivia,
3140}
3141
3142/// #2842: the `matches snapshot "<name>"` tail of an `expect`. `matches` and
3143/// `snapshot` are contextual words, matched by text like `faults`.
3144#[derive(Debug, Clone)]
3145pub struct SnapshotClause {
3146    /// The snapshot's name (the literal's text), unique within its case.
3147    pub name: String,
3148    /// The `matches` keyword through the closing quote of the name.
3149    pub span: Span,
3150    /// The name literal.
3151    pub name_span: Span,
3152}
3153
3154#[derive(Debug, Clone)]
3155pub struct ExpectStmt {
3156    pub value: Expr,
3157    /// #2842: `expect <value> matches snapshot "<name>"` compares `value`'s
3158    /// canonical JSON to a stored snapshot instead of asserting a `Bool`. `None`
3159    /// for every other `expect`.
3160    pub snapshot: Option<SnapshotClause>,
3161    pub span: Span,
3162    pub trivia: Trivia,
3163}
3164
3165/// `name := expr` — a `Cell` store write (v0.81, storage track). `target` is the
3166/// `Cell` field being written (a bare name for now; the checker resolves it to a
3167/// `store` field). `value` is the new value.
3168#[derive(Debug, Clone)]
3169pub struct AssignStmt {
3170    pub target: Ident,
3171    pub value: Expr,
3172    pub span: Span,
3173    pub trivia: Trivia,
3174}
3175
3176#[derive(Debug, Clone)]
3177pub struct LetStmt {
3178    pub name: Ident,
3179    pub type_annot: Option<TypeRef>,
3180    pub value: Expr,
3181    /// v0.182 (#664): the call-site `by <Actor>(<identity>)` clause on an
3182    /// `EffectLet` whose value addresses a test service handler. `None` on a pure
3183    /// `Let` (the `by` is parsed only in the `<-` arm) and on an effect-let with
3184    /// no principal.
3185    pub principal: Option<CallSiteActor>,
3186    /// #2010: set when this binding is the hidden one a `let <pattern> … else …`
3187    /// statement desugars into. `None` for every binding the author wrote.
3188    pub let_else: Option<LetElse>,
3189    pub span: Span,
3190    pub trivia: Trivia,
3191}
3192
3193/// #2010: what remains of a `let <pattern> <- e else …` (or `= e else …`)
3194/// statement once the parser has desugared it.
3195///
3196/// The parser rewrites `let P <- e else E` followed by the rest of the block
3197/// into a hidden binding, `let __letElse<offset> <- e`, and a `match` on it as
3198/// the block's tail: `P => { rest of the block }` first, then either
3199/// `_ => E` (an `else` block) or the `else match` arms. Everything downstream
3200/// (checker, emitter, editor) sees only that ordinary `match`. This marker,
3201/// carried on the hidden binding, is what lets the formatter print the sugar
3202/// back and the checker word its diagnostics in the author's terms.
3203#[derive(Debug, Clone)]
3204pub struct LetElse {
3205    /// Whether the `else` was a block or a `match { arms }`.
3206    pub form: LetElseForm,
3207    /// The `else` keyword (empty, at the end of the statement, for [`LetElseForm::Plain`]).
3208    pub else_span: Span,
3209    /// #2654: for a record pattern, the source ranges whose deletion leaves the
3210    /// `else`-less form — any parentheses around the pattern, and the `else`
3211    /// clause with the space before it. Empty for every other pattern, and for
3212    /// the plain form.
3213    pub remove_else: Vec<Span>,
3214}
3215
3216/// #2010: the spellings of a `let … else` fallback (and, #2654, its absence).
3217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3218pub enum LetElseForm {
3219    /// `else { … }` — desugared to a trailing `_ => { … }` arm.
3220    Block,
3221    /// `else match { arms }` — the arms follow the pattern's arm unchanged.
3222    Match,
3223    /// No `else` at all (#2654): an irrefutable record pattern, `let Item { sku, .. } = e`,
3224    /// desugared to a one-arm `match`. `else_span` is then empty, at the end of
3225    /// the statement.
3226    Plain,
3227}
3228
3229/// #2010: the prefix of every hidden `let … else` binding. User identifiers
3230/// start with a letter (the lexer's `Ident` rule), so no user name can collide
3231/// with it, and the emitter already treats `__`-prefixed names as its own.
3232pub const LET_ELSE_BINDER_PREFIX: &str = "__letElse";
3233
3234impl Expr {
3235    /// #2010: whether this is the `match` a `let <pattern> … else …` was
3236    /// desugared into — one whose scrutinee is the hidden binder. It is not an
3237    /// expression the author wrote, so source-level tools (refactorings,
3238    /// diagnostics) treat it as the statement it came from.
3239    pub fn is_let_else_match(&self) -> bool {
3240        matches!(
3241            &self.kind,
3242            ExprKind::Match { discriminant, .. }
3243                if matches!(&discriminant.kind, ExprKind::Ident(i) if i.name.starts_with(LET_ELSE_BINDER_PREFIX))
3244        )
3245    }
3246}
3247
3248/// #2010: whether `name` is a binding the compiler generated rather than one
3249/// the author wrote. Editor surfaces (inlay hints, locals, symbols) skip these.
3250pub fn is_generated_binding(name: &str) -> bool {
3251    name.starts_with("__")
3252}
3253
3254#[derive(Debug, Clone)]
3255pub struct SendStmt {
3256    /// The send target — a recipient call, e.g. `Logger.info(msg)`.
3257    pub value: Expr,
3258    pub span: Span,
3259    pub trivia: Trivia,
3260}
3261
3262/// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
3263/// `value` is the awaited effect, which MUST be `Effect[()]`.
3264#[derive(Debug, Clone)]
3265pub struct DoStmt {
3266    pub value: Expr,
3267    pub span: Span,
3268    pub trivia: Trivia,
3269}
3270
3271#[derive(Debug, Clone)]
3272pub struct Param {
3273    pub name: Ident,
3274    pub type_ref: TypeRef,
3275    pub span: Span,
3276}
3277
3278#[derive(Debug, Clone)]
3279pub enum TypeRef {
3280    Base(BaseType, Span),
3281    Named(Ident),
3282    /// `Result[T, E]` — the built-in generic Result type (v0.1).
3283    Result(Box<TypeRef>, Box<TypeRef>, Span),
3284    /// `Option[T]` — the built-in generic Option type (v0.2).
3285    Option(Box<TypeRef>, Span),
3286    /// `Effect[T]` — the built-in generic Effect type (v0.5).
3287    Effect(Box<TypeRef>, Span),
3288    /// `HttpResult[T]` — the built-in HTTP-result sum (v0.9).
3289    HttpResult(Box<TypeRef>, Span),
3290    /// `QueueResult` — the built-in queue verdict sum (`Ack | Retry`),
3291    /// non-generic; the required return of a queue handler (v0.44).
3292    QueueResult(Span),
3293    /// `Asset` — a static file declared by `asset name = "path"` (#1883,
3294    /// slice 1a, [DECISION H]). Non-generic, like `QueueResult`; its one
3295    /// field is `.url : String`.
3296    Asset(Span),
3297    /// `RequestHeaders` (#2341, decision A) — an HTTP handler's read-only view
3298    /// of the request's headers, the type of its `headers` parameter.
3299    /// Non-generic and opaque: `get`, `bearer`, `clientIp`. Never stored or
3300    /// sent (it has no wire form).
3301    RequestHeaders(Span),
3302    /// `RequestQuery` (#2854, decision B) — an HTTP handler's read-only view
3303    /// of the request's query string, the type of its `query` parameter when
3304    /// it wants raw access: `get`, `all`, `params`. Never stored or sent.
3305    RequestQuery(Span),
3306    /// `TokenClaims` (#2380, L5) — the verified claims of an `Oidc` actor's
3307    /// token, read through its binder (`who.claims`). Read-only, never stored
3308    /// or sent.
3309    TokenClaims(Span),
3310    /// `BoundaryRejection` (#2341, decision C) — why the router refused a
3311    /// request before any handler ran; the type of an `on rejected` handler's
3312    /// parameter. Opaque here (its variants are matched, never built). Never
3313    /// stored or sent.
3314    BoundaryRejection(Span),
3315    /// `List[T]` — the built-in generic immutable list type (v0.20b).
3316    List(Box<TypeRef>, Span),
3317    /// `Map[K, V]` — the built-in generic immutable map type (v0.20b).
3318    /// Keys are confined to `Key` types
3319    /// (`bynk.types.not_keyable`).
3320    Map(Box<TypeRef>, Box<TypeRef>, Span),
3321    /// `Set[T]` — the built-in generic immutable set type (standard-library
3322    /// K6a, #2596). Insertion-ordered like `Map`; elements are confined to the
3323    /// `Map`-key rule (`bynk.types.not_keyable`).
3324    Set(Box<TypeRef>, Span),
3325    /// `Query[T]` — the built-in lazy storage-read description (v0.91, ADR 0115).
3326    /// Nameable in a pure helper's return type; non-storable and non-boundary
3327    /// (like `Effect`/`Fn`).
3328    Query(Box<TypeRef>, Span),
3329    /// `Stream[T]` — the value-over-time primitive (v0.100, real-time track
3330    /// slice 0). A lazy, pull-shaped sequence produced over time; non-storable
3331    /// and non-boundary (like `Query`/`Effect`/`Fn`).
3332    Stream(Box<TypeRef>, Span),
3333    /// `Connection[F]` — a held WebSocket connection (v0.102, real-time track
3334    /// slice 2). `F` is the server→client frame type. A `Held` resource:
3335    /// non-serialisable, non-boundary, and governed by the linearity discipline
3336    /// (§2.9); storable only in `Cell[Option[Connection]]` / `Map[K, Connection]`.
3337    Connection(Box<TypeRef>, Span),
3338    /// `History[Agent]` — a generated, driven call-history of an agent (v0.119,
3339    /// testing track slice 7, ADR 0155). A test-only generator, legal only in
3340    /// `for all` binding position inside a `property`; it is not a value type,
3341    /// so it never resolves in a field/param/return position. The bound subject
3342    /// behaves as an ordinary `List[Step]`.
3343    History(Box<TypeRef>, Span),
3344    /// `ValidationError` — the built-in error type used by refined-type
3345    /// constructors (v0.1).
3346    ValidationError(Span),
3347    /// `JsonError` — the built-in JSON-decode error type (v0.22b). A
3348    /// uniform record (`kind`/`path`/`message`, all `String`) the codec
3349    /// maps `BoundaryError` variants and parse failures into.
3350    JsonError(Span),
3351    /// `()` — the unit type (v0.5).
3352    Unit(Span),
3353    /// `A -> B` / `(A, B) -> C` / `() -> B` — a function type (v0.20a).
3354    /// Right-associative; effectful iff the return type is `Effect[_]`
3355    /// (the structural rule). Confined to non-boundary positions
3356    /// (`bynk.types.function_at_boundary`).
3357    Fn(Vec<TypeRef>, Box<TypeRef>, Span),
3358    /// `Name[Arg, …]` — an application of a user-declared generic type
3359    /// (v0.157, ADR 0183). `name` is a user type name (never a built-in
3360    /// generic, which each have a dedicated variant above). Arity and the
3361    /// existence of the referenced type are checked in the resolver.
3362    App {
3363        name: Ident,
3364        args: Vec<TypeRef>,
3365        span: Span,
3366    },
3367}
3368
3369impl TypeRef {
3370    pub fn span(&self) -> Span {
3371        match self {
3372            TypeRef::Base(_, s) => *s,
3373            TypeRef::Named(id) => id.span,
3374            TypeRef::Result(_, _, s) => *s,
3375            TypeRef::Option(_, s) => *s,
3376            TypeRef::Effect(_, s) => *s,
3377            TypeRef::HttpResult(_, s) => *s,
3378            TypeRef::QueueResult(s) => *s,
3379            TypeRef::Asset(s) => *s,
3380            TypeRef::RequestHeaders(s) => *s,
3381            TypeRef::RequestQuery(s) => *s,
3382            TypeRef::TokenClaims(s) => *s,
3383            TypeRef::BoundaryRejection(s) => *s,
3384            TypeRef::List(_, s) => *s,
3385            TypeRef::Map(_, _, s) => *s,
3386            TypeRef::Set(_, s) => *s,
3387            TypeRef::Query(_, s) => *s,
3388            TypeRef::Stream(_, s) => *s,
3389            TypeRef::Connection(_, s) => *s,
3390            TypeRef::History(_, s) => *s,
3391            TypeRef::ValidationError(s) => *s,
3392            TypeRef::JsonError(s) => *s,
3393            TypeRef::Unit(s) => *s,
3394            TypeRef::Fn(_, _, s) => *s,
3395            TypeRef::App { span, .. } => *span,
3396        }
3397    }
3398}
3399
3400/// K-streams (#2623): where `t` reaches a `Stream`, as the chain of fields it
3401/// is reached through (`["Bundle.download", "Download.body"]`), or `None`. A
3402/// bare `Stream` (or one inside a wrapper) is the empty chain. A record or sum
3403/// may carry a stream; such a type is then non-boundary like the stream
3404/// itself, and this is the walk every boundary check uses to find it. It
3405/// follows named and applied types into their declarations (a generic
3406/// record's own fields too, so `Wrap[T] = { s: Stream[T] }` is found through
3407/// `Wrap[Int]`), but not into a function type, which is rejected at a
3408/// boundary on its own. Terminates via the `visited` set.
3409pub fn stream_path(
3410    t: &TypeRef,
3411    types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3412) -> Option<Vec<String>> {
3413    fn walk(
3414        t: &TypeRef,
3415        types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3416        visited: &mut std::collections::HashSet<String>,
3417    ) -> Option<Vec<String>> {
3418        match t {
3419            TypeRef::Stream(..) => Some(Vec::new()),
3420            TypeRef::Named(id) => named(&id.name, types, visited),
3421            TypeRef::App { name, args, .. } => named(&name.name, types, visited)
3422                .or_else(|| args.iter().find_map(|a| walk(a, types, visited))),
3423            TypeRef::Option(a, _)
3424            | TypeRef::List(a, _)
3425            | TypeRef::Set(a, _)
3426            | TypeRef::Effect(a, _)
3427            | TypeRef::HttpResult(a, _)
3428            | TypeRef::Query(a, _)
3429            | TypeRef::Connection(a, _)
3430            | TypeRef::History(a, _) => walk(a, types, visited),
3431            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
3432                walk(a, types, visited).or_else(|| walk(b, types, visited))
3433            }
3434            TypeRef::Fn(..)
3435            | TypeRef::Base(..)
3436            | TypeRef::QueueResult(_)
3437            | TypeRef::Asset(_)
3438            | TypeRef::RequestHeaders(_)
3439            | TypeRef::RequestQuery(_)
3440            | TypeRef::TokenClaims(_)
3441            | TypeRef::BoundaryRejection(_)
3442            | TypeRef::ValidationError(_)
3443            | TypeRef::JsonError(_)
3444            | TypeRef::Unit(_) => None,
3445        }
3446    }
3447    fn named(
3448        name: &str,
3449        types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3450        visited: &mut std::collections::HashSet<String>,
3451    ) -> Option<Vec<String>> {
3452        if !visited.insert(name.to_string()) {
3453            return None;
3454        }
3455        let decl = types.get(name)?;
3456        let fields: Vec<(String, &TypeRef)> = match &decl.body {
3457            TypeBody::Record(r) => r
3458                .fields
3459                .iter()
3460                .map(|f| (format!("{name}.{}", f.name.name), &f.type_ref))
3461                .collect(),
3462            TypeBody::Sum(s) => s
3463                .variants
3464                .iter()
3465                .flat_map(|v| {
3466                    v.payload.iter().map(move |p| {
3467                        (
3468                            format!("{name}.{}.{}", v.name.name, p.name.name),
3469                            &p.type_ref,
3470                        )
3471                    })
3472                })
3473                .collect(),
3474            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => Vec::new(),
3475        };
3476        fields.into_iter().find_map(|(at, t)| {
3477            walk(t, types, visited).map(|mut rest| {
3478                rest.insert(0, at);
3479                rest
3480            })
3481        })
3482    }
3483    walk(t, types, &mut std::collections::HashSet::new())
3484}
3485
3486/// K-streams (#2623): whether the named type `name` carries a `Stream` in a
3487/// field, at any depth ([`stream_path`]). Such a type has no codec.
3488pub fn type_carries_stream(
3489    name: &str,
3490    types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3491) -> bool {
3492    stream_path(
3493        &TypeRef::Named(Ident {
3494            name: name.to_string(),
3495            span: Span::default(),
3496        }),
3497        types,
3498    )
3499    .is_some()
3500}
3501
3502/// v0.174 (#592): does the generic record type `name` transitively contain a
3503/// reference to itself — through any field-type path, including collection and
3504/// `Option` wrappers, sum-variant payloads, and generic type arguments? Such a
3505/// type has no finite set of monomorphised boundary codecs: uniform recursion
3506/// (`Node[T] = { next: Option[Node[T]] }`) would need a self-referential codec
3507/// chain the per-instantiation model does not yet generate, and polymorphic
3508/// recursion (`Weird[T] = { next: Option[Weird[List[T]]] }`) an unbounded set of
3509/// instantiations. Both are rejected at a boundary
3510/// (`bynk.generics.recursive_generic_at_boundary`).
3511///
3512/// Detection is reachability over the type-containment graph: `name` is
3513/// recursive iff it is reachable from its own body, following every named /
3514/// applied head and descending into every wrapper, map/result pair, function
3515/// position, and generic argument. Terminates via the `visited` set.
3516pub fn generic_record_is_recursive(
3517    name: &str,
3518    types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3519) -> bool {
3520    fn heads(t: &TypeRef, out: &mut Vec<String>) {
3521        match t {
3522            TypeRef::Named(id) => out.push(id.name.clone()),
3523            TypeRef::App {
3524                name: app_name,
3525                args,
3526                ..
3527            } => {
3528                out.push(app_name.name.clone());
3529                for a in args {
3530                    heads(a, out);
3531                }
3532            }
3533            TypeRef::Option(a, _)
3534            | TypeRef::List(a, _)
3535            | TypeRef::Set(a, _)
3536            | TypeRef::Effect(a, _)
3537            | TypeRef::HttpResult(a, _)
3538            | TypeRef::Query(a, _)
3539            | TypeRef::Stream(a, _)
3540            | TypeRef::Connection(a, _)
3541            | TypeRef::History(a, _) => heads(a, out),
3542            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
3543                heads(a, out);
3544                heads(b, out);
3545            }
3546            TypeRef::Fn(ps, r, _) => {
3547                for p in ps {
3548                    heads(p, out);
3549                }
3550                heads(r, out);
3551            }
3552            TypeRef::Base(..)
3553            | TypeRef::QueueResult(_)
3554            | TypeRef::Asset(_)
3555            | TypeRef::RequestHeaders(_)
3556            | TypeRef::RequestQuery(_)
3557            | TypeRef::TokenClaims(_)
3558            | TypeRef::BoundaryRejection(_)
3559            | TypeRef::ValidationError(_)
3560            | TypeRef::JsonError(_)
3561            | TypeRef::Unit(_) => {}
3562        }
3563    }
3564    fn body_heads(decl: &TypeDecl, out: &mut Vec<String>) {
3565        match &decl.body {
3566            TypeBody::Record(r) => {
3567                for f in &r.fields {
3568                    heads(&f.type_ref, out);
3569                }
3570            }
3571            TypeBody::Sum(s) => {
3572                for v in &s.variants {
3573                    for p in &v.payload {
3574                        heads(&p.type_ref, out);
3575                    }
3576                }
3577            }
3578            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
3579        }
3580    }
3581    let Some(root) = types.get(name) else {
3582        return false;
3583    };
3584    let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
3585    let mut stack: Vec<String> = Vec::new();
3586    body_heads(root, &mut stack);
3587    while let Some(n) = stack.pop() {
3588        if n == name {
3589            return true;
3590        }
3591        if !visited.insert(n.clone()) {
3592            continue;
3593        }
3594        if let Some(decl) = types.get(&n) {
3595            body_heads(decl, &mut stack);
3596        }
3597    }
3598    false
3599}
3600
3601/// T3.4 (R2.4): a node's identity, independent of position — allocated once,
3602/// monotonically, per expression the parser constructs (`Parser::alloc_expr_id`
3603/// in `bynk-syntax/src/parser.rs`). Never derived from a `Span`, so two
3604/// expressions occupying the same byte range (a synthetic node, a
3605/// zero-width span) never collide the way a span-keyed side table could.
3606#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3607pub struct ExprId(pub u32);
3608
3609impl ExprId {
3610    /// Reserved for `Expr` nodes built outside the parser — after checking,
3611    /// during emission — that are never looked up in a checker-populated
3612    /// `expr_types`/`expr_ty` table (they are lowered directly, from
3613    /// already-typed sub-expressions they wrap or splice). A lookup against
3614    /// this id is a bug: the node was never checked and has no recorded
3615    /// type of its own.
3616    pub const SYNTHETIC: ExprId = ExprId(u32::MAX);
3617}
3618
3619#[derive(Debug, Clone)]
3620pub struct Expr {
3621    pub id: ExprId,
3622    pub kind: ExprKind,
3623    pub span: Span,
3624}
3625
3626/// Finding #31: `Expr` sets the size of every expression node in the
3627/// program — `ExprKind::Observation`'s payload and `ExprKind::Is`'s pattern
3628/// field are boxed specifically to keep it small (176 bytes unboxed, 128
3629/// boxed, measured on this target). Pinned so the next large variant added
3630/// to `ExprKind` is a compile error here rather than a silent regression.
3631/// T3.4: `id: ExprId` adds 4 bytes (padded); the budget is unchanged, so this
3632/// still fits.
3633/// T3.4 (R2.4): `id: ExprId` is a deliberate 8-byte increase (128 → 136,
3634/// alignment-padded from 4), not a silent regression — the ceiling moves
3635/// with it, once, here, so the next *accidental* growth still trips this
3636/// assertion rather than hiding under slack headroom.
3637/// T3.5 (R2.2): `file: FileId` on `Span` is a deliberate increase (136 → 160
3638/// — `Span` itself grows from 16 to 24 bytes with alignment padding, and
3639/// `ExprKind`'s largest variant carries more than one `Span`), not a silent
3640/// regression — the ceiling moves with it, once, here, exactly as T3.4 did
3641/// for `id: ExprId`.
3642const _: () = assert!(std::mem::size_of::<Expr>() <= 160);
3643
3644impl ExprKind {
3645    /// Construct an `IntLit` for a *synthesized* integer — one the compiler
3646    /// invents rather than reading from source (a default `1`, a computed bound).
3647    /// The lexeme is the canonical decimal form (no separators). Source-parsed
3648    /// literals keep their as-written lexeme instead (v0.142, ADR 0166).
3649    pub fn int_lit(value: i64) -> ExprKind {
3650        ExprKind::IntLit {
3651            value,
3652            lexeme: value.to_string(),
3653        }
3654    }
3655}
3656
3657#[derive(Debug, Clone)]
3658pub enum ExprKind {
3659    /// An integer literal (typed `Int`). The lexeme is kept alongside the parsed
3660    /// value (v0.142, ADR 0166) so formatting is byte-stable: an author's `_`
3661    /// digit separators (`1_048_576`) survive a round-trip, mirroring the
3662    /// `FloatLit` treatment. The value is separator-free; emission lowers the
3663    /// value, so emitted output is unaffected.
3664    IntLit {
3665        value: i64,
3666        lexeme: String,
3667    },
3668    /// A float literal (v0.21). The lexeme is kept alongside the parsed
3669    /// value so emission and formatting are byte-stable (`1e10` must not
3670    /// normalise to `10000000000`).
3671    FloatLit {
3672        value: f64,
3673        lexeme: String,
3674    },
3675    /// A decimal literal (#2622): `1.20d`, `5d`. Typed `Decimal`; the scale is
3676    /// the number of fraction digits as written, so `1.20d` keeps scale 2.
3677    DecimalLit {
3678        /// The coefficient: the digits with the `.` and `_` separators removed
3679        /// (`120` for `1.20d`), as an arbitrary-precision base-10 string.
3680        coef: String,
3681        /// The number of fraction digits as written (`2` for `1.20d`).
3682        scale: u32,
3683        /// The lexeme as written, `d` suffix included (formatting is verbatim).
3684        lexeme: String,
3685    },
3686    /// A duration literal `<int>.<unit>` (v0.86, ADR 0112): `5.minutes`,
3687    /// `30.days`. The parser recognises the `IntLit . <unit>` shape and records
3688    /// the magnitude, the unit, and the resolved milliseconds (the value the
3689    /// emitter lowers to). Typed `Duration`.
3690    DurationLit {
3691        /// The integer magnitude as written (`5` in `5.minutes`).
3692        value: i64,
3693        /// The unit name (`minutes`), one of the closed set.
3694        unit: DurationUnit,
3695        /// The value in milliseconds — `value * unit factor`.
3696        millis: i64,
3697    },
3698    StrLit(String),
3699    /// An interpolated string `"… \(expr) …"` (v0.43, ADR 0075). Chunks and
3700    /// holes alternate. A plain `"…"` with no holes stays [`ExprKind::StrLit`],
3701    /// so existing code and the emitter/formatter fast-path are untouched.
3702    InterpStr(Vec<InterpPart>),
3703    BoolLit(bool),
3704    Ident(Ident),
3705    Call {
3706        name: Ident,
3707        /// v0.20a: explicit type arguments (`name[T](…)`); empty when absent.
3708        type_args: Vec<TypeRef>,
3709        args: Vec<Expr>,
3710    },
3711    /// A lambda (v0.20a). See [`LambdaExpr`].
3712    Lambda(LambdaExpr),
3713    BinOp(BinOp, Box<Expr>, Box<Expr>),
3714    UnaryOp(UnaryOp, Box<Expr>),
3715    Paren(Box<Expr>),
3716    /// `{ stmts; expr }` — block expression (v0.1).
3717    Block(Block),
3718    /// `if cond { then } else { else }` (v0.1).
3719    If {
3720        cond: Box<Expr>,
3721        then_block: Box<Block>,
3722        else_block: Box<Block>,
3723    },
3724    /// `Ok(value)` — Result success constructor (v0.1).
3725    Ok(Box<Expr>),
3726    /// `Err(error)` — Result failure constructor (v0.1).
3727    Err(Box<Expr>),
3728    /// `expr?` — propagation operator (v0.1).
3729    Question(Box<Expr>),
3730    /// `TypeName { field: value, ... }` — record construction (v0.2).
3731    RecordConstruction {
3732        type_name: Ident,
3733        fields: Vec<FieldInit>,
3734    },
3735    /// `receiver.field` — field access on a record value (v0.2). v0.3 adds
3736    /// `.raw` on opaque types within the defining commons.
3737    FieldAccess {
3738        receiver: Box<Expr>,
3739        field: Ident,
3740    },
3741    /// `receiver.method(args)` — instance method call (v0.2). The
3742    /// resolver determines the receiver's type and looks up the method.
3743    MethodCall {
3744        receiver: Box<Expr>,
3745        method: Ident,
3746        /// v0.22b: explicit type arguments on a qualified static
3747        /// (`Json.decode[T](…)`); empty when absent. The same-line-`[`
3748        /// rule applies as for `Call` type application (0039).
3749        type_args: Vec<TypeRef>,
3750        args: Vec<Expr>,
3751    },
3752    /// `match disc { arm+ }` — pattern matching (v0.2).
3753    Match {
3754        discriminant: Box<Expr>,
3755        arms: Vec<MatchArm>,
3756    },
3757    /// `expr is pattern` — pattern test, returns Bool (v0.2).
3758    ///
3759    /// `pattern` is boxed (finding #31): `Pattern`'s `Variant` case carries two
3760    /// `Ident`s plus a `Vec`, inlining it into every `ExprKind` sets the size
3761    /// of every expression node in the program for the one variant that
3762    /// tests a pattern.
3763    Is {
3764        value: Box<Expr>,
3765        pattern: Box<Pattern>,
3766    },
3767    /// `Some(value)` — Option Some constructor (v0.2).
3768    Some(Box<Expr>),
3769    /// `None` — Option None constructor (v0.2).
3770    None,
3771    /// `()` — unit literal (v0.5).
3772    UnitLit,
3773    /// `TypeName { ...base, field: value, ... }` or `{ ...base, ... }` —
3774    /// record spread expression (v0.5).
3775    RecordSpread {
3776        /// Optional type prefix (`TypeName { ...base }`). Absent for the
3777        /// bare form used inside `commit`.
3778        type_name: Option<Ident>,
3779        /// The base record being spread.
3780        base: Box<Expr>,
3781        /// Field overrides (always full `name: value` form — never shorthand).
3782        overrides: Vec<FieldInit>,
3783    },
3784    /// `Effect.pure(value)` — wrap a synchronous value into `Effect[T]`
3785    /// (v0.5). Recognised in the parser as a special-form.
3786    EffectPure(Box<Expr>),
3787    /// `expect expr` — expectation as an expression of type `()` (v0.9.1;
3788    /// renamed from `assert` in v0.112). Valid only inside test bodies. Evaluates
3789    /// `expr` (must be Bool); if false, the surrounding test case fails.
3790    Expect(Box<Expr>),
3791    /// `Val[T]`, `Val[T](args)` — test-context value construction (v0.9.4).
3792    /// `args` is empty for the bare form and holds the pin arguments for
3793    /// `Val[T](...)`. The record-override form `Val[T] { ... }` is not yet
3794    /// parsed. Valid only inside test bodies; has type `T`.
3795    Val {
3796        type_ref: TypeRef,
3797        args: Vec<Expr>,
3798    },
3799    /// `Wire(<String>)` — a raw, pre-validation argument to a `system`-tier
3800    /// service address (testing-the-boundary Slice C). The inner expression is a
3801    /// `String` carrying the wire form the boundary will receive *unvalidated* —
3802    /// a body's JSON text or a path segment — so a case can drive the router with
3803    /// input the type system forbids and observe the rejection. Legal only at
3804    /// `system` (there is no wire at `unit`); the router validates it, so no
3805    /// refined value is ever minted from a `Wire` (ADR 0182 untouched).
3806    Wire(Box<Expr>),
3807    /// `[a, b, c]` — list literal (v0.20b). An empty `[]` requires an
3808    /// expected type (`bynk.types.uninferable_element_type`).
3809    ListLit(Vec<Expr>),
3810    /// An observation over a consumed capability's recorded calls (v0.117,
3811    /// testing track slice 5). The direct subject of an `expect` in a `case`
3812    /// body — `expect Cap.op called once with <pred>`, `expect Cap.op never
3813    /// called`, `expect A.op before B.op`. Types as `Bool` (the claim about the
3814    /// recorded trace), lowered to a boolean over the recorded log.
3815    /// Boxed (finding #31): at ~160 bytes, `ObservationExpr` inlined here set
3816    /// the size of every `ExprKind` for the one variant that records a
3817    /// capability-call observation.
3818    Observation(Box<ObservationExpr>),
3819    /// `<call> faults` — the claim that an effectful call **faults** (#1706).
3820    /// The direct subject of an `expect` in a `case` body —
3821    /// `expect quote.call("GBP") faults`. The claim's `call` is the call
3822    /// itself, an `Effect[_]` the claim awaits; the claim types as `Bool` and
3823    /// holds when awaiting the call throws (a capability fault, an injected
3824    /// `stub … fails`, an invariant violation) rather than returning a value.
3825    /// A fault is untyped and uncatchable by the caller, so this is a test's
3826    /// observation of the fault, not a handler for it: no production code can
3827    /// write it.
3828    ///
3829    /// #1812: the claim may carry a call-site principal (`expect
3830    /// api.POST(…) by User("bob") faults`), so an identity-carrying handler's
3831    /// fault path is testable; see [`FaultsExpr`]. Boxed, as `Observation` is,
3832    /// so the principal does not widen every `ExprKind`.
3833    Faults(Box<FaultsExpr>),
3834    /// `trace(Cap.op)` — the bound-trace escape hatch (v0.117, testing track
3835    /// slice 5). Yields the recorded calls of `Cap.op` as a `List[<CallRecord>]`
3836    /// (a synthetic record of the operation's parameters), asserted over with the
3837    /// ordinary value surface. Test-body-only, like [`ExprKind::Val`].
3838    Trace {
3839        cap: Ident,
3840        op: Ident,
3841    },
3842}
3843
3844/// Every directly-nested sub-expression of `e` — the **total** child
3845/// iterator. The match is exhaustive (no `_` arm), so adding an [`ExprKind`]
3846/// variant is a compile error here rather than a silently incomplete walk —
3847/// the trap the checker's three hand-rolled partial walkers each fell into
3848/// (block statements and match-arm bodies were skipped, so e.g. the `:=`
3849/// self-reference rule was bypassable through a match arm).
3850///
3851/// Exhaustive over [`ExprKind`] *and* over the expressions each variant holds
3852/// (#1760: match-arm guards were once missed, and every walk built on this one
3853/// missed them with it).
3854///
3855/// Descends one level: block *statements* and the tail, match-arm guards and
3856/// bodies, lambda bodies, interpolation holes, record-field values, and
3857/// observation predicates are all children. Callers recurse for a deep walk.
3858pub fn expr_children(e: &Expr) -> Vec<&Expr> {
3859    fn block_children<'a>(b: &'a Block, out: &mut Vec<&'a Expr>) {
3860        for s in &b.statements {
3861            statement_exprs(s, out);
3862        }
3863        out.push(&b.tail);
3864    }
3865    let mut out = Vec::new();
3866    match &e.kind {
3867        ExprKind::IntLit { .. }
3868        | ExprKind::FloatLit { .. }
3869        | ExprKind::DecimalLit { .. }
3870        | ExprKind::DurationLit { .. }
3871        | ExprKind::StrLit(_)
3872        | ExprKind::BoolLit(_)
3873        | ExprKind::Ident(_)
3874        | ExprKind::None
3875        | ExprKind::UnitLit
3876        | ExprKind::Trace { .. } => {}
3877        ExprKind::InterpStr(parts) => {
3878            for p in parts {
3879                if let InterpPart::Hole(h) = p {
3880                    out.push(h.as_ref());
3881                }
3882            }
3883        }
3884        ExprKind::Call { args, .. } | ExprKind::Val { args, .. } | ExprKind::ListLit(args) => {
3885            out.extend(args.iter())
3886        }
3887        ExprKind::Wire(inner) => out.push(inner.as_ref()),
3888        ExprKind::Lambda(l) => out.push(l.body.as_ref()),
3889        ExprKind::BinOp(_, l, r) => {
3890            out.push(l.as_ref());
3891            out.push(r.as_ref());
3892        }
3893        ExprKind::UnaryOp(_, inner)
3894        | ExprKind::Paren(inner)
3895        | ExprKind::Ok(inner)
3896        | ExprKind::Err(inner)
3897        | ExprKind::Question(inner)
3898        | ExprKind::Some(inner)
3899        | ExprKind::EffectPure(inner)
3900        | ExprKind::Expect(inner) => out.push(inner.as_ref()),
3901        ExprKind::Faults(f) => {
3902            // #1812: a call-site principal's identity is evaluated first, as
3903            // an argument to the claimed call — the same order as an
3904            // effect-let's in `statement_exprs`.
3905            if let Some(identity) = f.principal.as_ref().and_then(|p| p.identity.as_deref()) {
3906                out.push(identity);
3907            }
3908            if let Some(claims) = f.principal.as_ref().and_then(|p| p.claims.as_deref()) {
3909                out.push(claims);
3910            }
3911            out.push(&f.call);
3912        }
3913        ExprKind::Block(b) => block_children(b, &mut out),
3914        ExprKind::If {
3915            cond,
3916            then_block,
3917            else_block,
3918        } => {
3919            out.push(cond.as_ref());
3920            block_children(then_block, &mut out);
3921            block_children(else_block, &mut out);
3922        }
3923        ExprKind::RecordConstruction { fields, .. } => {
3924            out.extend(fields.iter().filter_map(|f| f.value.as_ref()));
3925        }
3926        ExprKind::FieldAccess { receiver, .. } => out.push(receiver.as_ref()),
3927        ExprKind::MethodCall { receiver, args, .. } => {
3928            out.push(receiver.as_ref());
3929            out.extend(args.iter());
3930        }
3931        ExprKind::Match { discriminant, arms } => {
3932            out.push(discriminant.as_ref());
3933            for arm in arms {
3934                // #1760: the guard, in evaluation order before the body. It is
3935                // an ordinary expression, checked like any other.
3936                if let Some(guard) = &arm.guard {
3937                    out.push(guard);
3938                }
3939                match &arm.body {
3940                    MatchBody::Expr(e) => out.push(e),
3941                    MatchBody::Block(b) => block_children(b, &mut out),
3942                }
3943            }
3944        }
3945        ExprKind::Is { value, .. } => out.push(value.as_ref()),
3946        ExprKind::RecordSpread {
3947            base, overrides, ..
3948        } => {
3949            out.push(base.as_ref());
3950            out.extend(overrides.iter().filter_map(|f| f.value.as_ref()));
3951        }
3952        ExprKind::Observation(obs) => match &obs.matcher {
3953            ObservationMatcher::Called { count, with_pred } => {
3954                if let Some(c) = count {
3955                    out.push(c.as_ref());
3956                }
3957                if let Some(p) = with_pred {
3958                    out.push(p.as_ref());
3959                }
3960            }
3961            ObservationMatcher::NeverCalled | ObservationMatcher::Before { .. } => {}
3962        },
3963    }
3964    out
3965}
3966
3967/// The expressions directly contained in a statement — the statement half of
3968/// [`expr_children`]'s total walk. Exhaustive over [`Statement`] for the same
3969/// reason, and over each statement's expressions: a `let`'s call-site
3970/// principal identity included.
3971pub fn statement_exprs<'a>(s: &'a Statement, out: &mut Vec<&'a Expr>) {
3972    match s {
3973        Statement::Let(l) | Statement::EffectLet(l) => {
3974            // #1766 review: a call-site principal's identity (`by User(who)`)
3975            // is a full expression. It is evaluated first, as an argument to
3976            // the addressed call.
3977            if let Some(identity) = l.principal.as_ref().and_then(|p| p.identity.as_deref()) {
3978                out.push(identity);
3979            }
3980            if let Some(claims) = l.principal.as_ref().and_then(|p| p.claims.as_deref()) {
3981                out.push(claims);
3982            }
3983            out.push(&l.value)
3984        }
3985        Statement::Expect(a) => out.push(&a.value),
3986        Statement::Send(snd) => out.push(&snd.value),
3987        Statement::Do(d) => out.push(&d.value),
3988        Statement::Assign(a) => out.push(&a.value),
3989        Statement::Advance(a) => out.push(&a.amount),
3990        Statement::Par(p) => p.statements.iter().for_each(|s| statement_exprs(s, out)),
3991    }
3992}
3993
3994/// A fault claim, `<call> [by <Actor>(<identity>)] faults` (#1706, #1812).
3995/// `call` is the effectful call the claim awaits; `principal` is the optional
3996/// call-site `by` clause, read exactly as on an effect-let (`let r <- call by
3997/// User("bob")`): it supplies the identity the addressed handler reads.
3998#[derive(Debug, Clone)]
3999pub struct FaultsExpr {
4000    pub call: Expr,
4001    pub principal: Option<CallSiteActor>,
4002}
4003
4004/// An observation of a capability operation's recorded calls (v0.117, testing
4005/// track slice 5). `cap`/`op` name the seam (`Logger.log`); `matcher` is the
4006/// claim about the recorded calls.
4007#[derive(Debug, Clone)]
4008pub struct ObservationExpr {
4009    pub cap: Ident,
4010    pub op: Ident,
4011    pub matcher: ObservationMatcher,
4012}
4013
4014/// The claim an [`ObservationExpr`] makes about a seam's recorded calls (v0.117).
4015#[derive(Debug, Clone)]
4016pub enum ObservationMatcher {
4017    /// `called` [`once` | `<n> times`]? [`with` `<pred>`]?. `count` is `None`
4018    /// for a bare `called` (at least one); `Some(expr)` is the exact-count claim
4019    /// (a literal; `once` desugars to `1`). `with_pred` matches a call whose
4020    /// arguments (in scope by the operation's parameter names) satisfy it.
4021    Called {
4022        count: Option<Box<Expr>>,
4023        with_pred: Option<Box<Expr>>,
4024    },
4025    /// `never called` — zero calls.
4026    NeverCalled,
4027    /// `before Cap.op` — the first call of the subject precedes the first call
4028    /// of the named operation (both must have occurred).
4029    Before { cap: Ident, op: Ident },
4030}
4031
4032/// One part of an interpolated string (v0.43, ADR 0075). An
4033/// [`ExprKind::InterpStr`] holds an alternating run of these.
4034#[derive(Debug, Clone)]
4035pub enum InterpPart {
4036    /// Literal text between holes, with escapes already resolved.
4037    Chunk(String),
4038    /// An interpolated expression `\(expr)`. Type-checked by the hole rule
4039    /// (base scalars only; see the checker) and lowered into a template-
4040    /// literal `${…}` slot.
4041    Hole(Box<Expr>),
4042}
4043
4044/// One field-initialiser inside a record construction expression:
4045/// either `name: expr` or the shorthand `name` (which requires a binding
4046/// of the same name in scope and uses its value).
4047#[derive(Debug, Clone)]
4048pub struct FieldInit {
4049    pub name: Ident,
4050    /// `None` means shorthand — the field's value is the same-named binding.
4051    pub value: Option<Expr>,
4052    pub span: Span,
4053}
4054
4055/// One arm of a `match` expression: `pattern => body` or, with a guard,
4056/// `pattern if guard => body` (guard added in the nested-patterns increment,
4057/// ADR 0169). A guarded arm matches only when the pattern matches **and** the
4058/// `Bool` guard evaluates true; it never contributes to exhaustiveness.
4059#[derive(Debug, Clone)]
4060pub struct MatchArm {
4061    pub pattern: Pattern,
4062    /// Optional `if <Bool-expr>` guard between the pattern and `=>`.
4063    pub guard: Option<Expr>,
4064    pub body: MatchBody,
4065    pub span: Span,
4066}
4067
4068/// The right-hand side of a match arm — either a single expression or
4069/// a block.
4070#[derive(Debug, Clone)]
4071pub enum MatchBody {
4072    Expr(Expr),
4073    Block(Block),
4074}
4075
4076impl MatchBody {
4077    pub fn span(&self) -> Span {
4078        match self {
4079            MatchBody::Expr(e) => e.span,
4080            MatchBody::Block(b) => b.span,
4081        }
4082    }
4083}
4084
4085/// A pattern (v0.2 §3.8). Patterns appear in `match` arms and as the
4086/// right-hand side of the `is` operator.
4087#[derive(Debug, Clone)]
4088pub enum Pattern {
4089    /// `_` — matches any value, no bindings.
4090    Wildcard(Span),
4091    /// A lowercase identifier — binds the whole value to `name` and matches
4092    /// anything (ADR 0169). At the top of a `match` arm it binds the scrutinee
4093    /// (`n if n > 0 => …`); inside a payload position it binds the field
4094    /// (`Some(user)`). The uppercase-led counterpart is a nullary [`Pattern::Variant`].
4095    Binding(Ident),
4096    /// A literal pattern — `31`, `"english"`, `true` (v0.130 §2.3.4). Matches a
4097    /// primitive scrutinee (`Int`/`String`/`Bool`) by value equality. The
4098    /// admitted set mirrors ADR 0001's closed literal set (integers — including
4099    /// a leading unary minus — strings, and booleans); `Float`/`()` are not
4100    /// admitted as patterns.
4101    Literal {
4102        value: LiteralValue,
4103        /// The literal as written (`-1_000`), digit separators kept; `None`
4104        /// for a pattern built without source. Only the formatter reads it,
4105        /// and it sits outside `value` so the duplicate-arm check ignores it.
4106        text: Option<String>,
4107        span: Span,
4108    },
4109    /// `lo..hi`, `lo..` or `..hi` — an inclusive integer range (patterns track,
4110    /// slice 5, #2699). Matches an `Int` scrutinee by comparison. Binds nothing.
4111    /// The one production `via schema(...)` shares ([`SchemaVersionPattern::Range`]).
4112    Range(IntRange),
4113    /// `Variant` or `Variant(bindings)` or `TypeName.Variant(bindings)`. Each
4114    /// payload binding is itself a [`Pattern`] (ADR 0169), so payloads nest:
4115    /// `Some(Ok(x))`, `Err(PollClosed)`.
4116    Variant {
4117        /// Optional qualifier: `TypeName.Variant`.
4118        type_name: Option<Ident>,
4119        /// The variant name.
4120        variant: Ident,
4121        /// Payload bindings (empty for nullary variants).
4122        bindings: Vec<PatternBinding>,
4123        span: Span,
4124    },
4125    /// `TypeName { f1, f2: pattern, .. }` — a record pattern (patterns track,
4126    /// slice 1, #2641). Always open: unlisted fields are ignored, and the
4127    /// trailing `..` is mandatory so that adding a field to the record never
4128    /// breaks a pattern. A field with no sub-pattern (`f`) binds the field to a
4129    /// local of the same name; `f: p` matches the field against `p`.
4130    Record {
4131        /// The record type's name — required, and never carries type
4132        /// arguments (a generic record's come from the scrutinee).
4133        type_name: Ident,
4134        /// The listed fields, in source order. `None` is the shorthand `f`.
4135        fields: Vec<(Ident, Option<Pattern>)>,
4136        /// The span of the mandatory trailing `..`.
4137        rest_span: Span,
4138        span: Span,
4139    },
4140    /// `[p₁, …, pₙ]` or `[p₁, …, pₙ, ..]` / `[p₁, …, pₙ, ..rest]` — a list
4141    /// pattern (patterns track, slice 4, #2680). Without `rest` it matches a
4142    /// list of exactly `n` elements, element `i` against `pᵢ`; with `rest` it
4143    /// matches `n` or more, and a named rest binds the remaining elements.
4144    List {
4145        elems: Vec<Pattern>,
4146        rest: Option<ListRest>,
4147        span: Span,
4148    },
4149    /// `p 'where' refinement-predicate` — a refinement guard on a pattern
4150    /// (#472). Matches when `inner` matches *and* the scrutinee satisfies
4151    /// `predicate` at runtime. v1 admits only `Wildcard` as `inner` (no
4152    /// binding form yet); refutable — never counts toward exhaustiveness or
4153    /// as a catch-all arm, the same treatment as an `if` guard (§2.3.4).
4154    Refined {
4155        inner: Box<Pattern>,
4156        predicate: Refinement,
4157        span: Span,
4158    },
4159    /// `p₁ | p₂ | … | pₙ` — an or-pattern (#474 §2.3.4): matches if any
4160    /// alternative matches. Left-associative `|`, flattened by the parser's
4161    /// chain fold into one `Vec` — an alternative is always a leaf
4162    /// (`Wildcard`/`Binding`/`Literal`/`Variant`), never itself an `Or`
4163    /// (there is no parenthesized-pattern syntax to nest one inside another).
4164    /// Well-typedness (checked, not parsed): every alternative binds the same
4165    /// set of names, a name shared across alternatives has the same type
4166    /// (including refinement) in each, and every alternative matches the same
4167    /// value type.
4168    Or(Vec<Pattern>, Span),
4169}
4170
4171/// An inclusive integer range, `lo..hi`, `lo..` or `..hi` (patterns track,
4172/// slice 5, #2699; events slice 4b, #990): the one production a match pattern
4173/// and a `via schema(...)` clause share. At least one bound is present — a bare
4174/// `..` is the rest marker, never a range. Each bound is an optionally negative
4175/// `Int` literal, parsed permissively: whether it is in range and whether
4176/// `lo <= hi` are checker questions.
4177#[derive(Debug, Clone, PartialEq, Eq)]
4178pub struct IntRange {
4179    pub lo: Option<i64>,
4180    pub hi: Option<i64>,
4181    /// The low bound as written (`-1_000`), digit separators kept; `None` when
4182    /// `lo` is. Only the formatter reads it; a range built without source
4183    /// (a suggested fix) leaves it `None` and prints from the value.
4184    pub lo_text: Option<String>,
4185    /// The high bound as written; see `lo_text`.
4186    pub hi_text: Option<String>,
4187    pub span: Span,
4188}
4189
4190impl IntRange {
4191    /// As [`IntRange::describe`], but each bound keeps its source spelling
4192    /// (`1_000..2_000`), so formatting does not drop digit separators.
4193    pub fn describe_source(&self) -> String {
4194        let lo = match (&self.lo_text, self.lo) {
4195            (Some(t), _) => t.clone(),
4196            (None, Some(n)) => n.to_string(),
4197            (None, None) => String::new(),
4198        };
4199        let hi = match (&self.hi_text, self.hi) {
4200            (Some(t), _) => t.clone(),
4201            (None, Some(n)) => n.to_string(),
4202            (None, None) => String::new(),
4203        };
4204        format!("{lo}..{hi}")
4205    }
4206
4207    /// The range as written, `0..9`, `18..` or `..-1`; the form the formatter
4208    /// prints and diagnostics quote.
4209    pub fn describe(&self) -> String {
4210        let lo = self.lo.map(|n| n.to_string()).unwrap_or_default();
4211        let hi = self.hi.map(|n| n.to_string()).unwrap_or_default();
4212        format!("{lo}..{hi}")
4213    }
4214}
4215
4216/// The trailing `..` of a list pattern, with the optional name that binds the
4217/// remaining elements (`..rest`).
4218#[derive(Debug, Clone)]
4219pub struct ListRest {
4220    pub binder: Option<Ident>,
4221    /// The span of the `..` token.
4222    pub span: Span,
4223}
4224
4225/// The value carried by a [`Pattern::Literal`]. A closed set (ADR 0001):
4226/// integer, string, and boolean. Kept distinct from [`ExprKind`] so patterns
4227/// carry only what they can actually match, and so it is `Eq`/`Hash` for the
4228/// duplicate-arm check.
4229#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4230pub enum LiteralValue {
4231    Int(i64),
4232    Str(String),
4233    Bool(bool),
4234}
4235
4236impl LiteralValue {
4237    /// A human-readable rendering for diagnostics (`31`, `"english"`, `true`).
4238    pub fn describe(&self) -> String {
4239        match self {
4240            LiteralValue::Int(n) => n.to_string(),
4241            LiteralValue::Str(s) => format!("{s:?}"),
4242            LiteralValue::Bool(b) => b.to_string(),
4243        }
4244    }
4245}
4246
4247impl Pattern {
4248    pub fn span(&self) -> Span {
4249        match self {
4250            Pattern::Wildcard(s) => *s,
4251            Pattern::Binding(id) => id.span,
4252            Pattern::Literal { span, .. } => *span,
4253            Pattern::Range(r) => r.span,
4254            Pattern::Variant { span, .. } => *span,
4255            Pattern::Record { span, .. } => *span,
4256            Pattern::List { span, .. } => *span,
4257            Pattern::Refined { span, .. } => *span,
4258            Pattern::Or(_, span) => *span,
4259        }
4260    }
4261
4262    /// Every identifier this pattern binds into scope, recursively (`_` and
4263    /// nullary variants bind nothing). Used by the resolver and the checker to
4264    /// populate an arm's scope, and by the guard to see the arm's bindings.
4265    ///
4266    /// For [`Pattern::Or`] this returns the *first* alternative's names — the
4267    /// checker separately verifies (#474 Rule 1) that every alternative binds
4268    /// the same set, so this is a defensive default when that rule is
4269    /// violated, not a semantic choice among alternatives.
4270    pub fn bound_names(&self) -> Vec<&Ident> {
4271        match self {
4272            Pattern::Wildcard(_) | Pattern::Literal { .. } | Pattern::Range(_) => Vec::new(),
4273            Pattern::Binding(id) => vec![id],
4274            Pattern::Variant { bindings, .. } => bindings
4275                .iter()
4276                .flat_map(|b| b.pattern().bound_names())
4277                .collect(),
4278            Pattern::Record { fields, .. } => fields
4279                .iter()
4280                .flat_map(|(name, sub)| match sub {
4281                    Some(p) => p.bound_names(),
4282                    None => vec![name],
4283                })
4284                .collect(),
4285            Pattern::List { elems, rest, .. } => elems
4286                .iter()
4287                .flat_map(Pattern::bound_names)
4288                .chain(rest.iter().filter_map(|r| r.binder.as_ref()))
4289                .collect(),
4290            Pattern::Refined { inner, .. } => inner.bound_names(),
4291            Pattern::Or(alts, _) => alts.first().map(Pattern::bound_names).unwrap_or_default(),
4292        }
4293    }
4294
4295    /// True when this pattern matches every value and binds nothing — a bare
4296    /// `_`. A [`Pattern::Binding`] also matches everything but *does* bind, so it
4297    /// is not a pure wildcard.
4298    pub fn is_wildcard(&self) -> bool {
4299        matches!(self, Pattern::Wildcard(_))
4300    }
4301
4302    /// True when this pattern matches every value (a `_` or a name binding),
4303    /// i.e. it is irrefutable and covers the position for exhaustiveness. An
4304    /// [`Pattern::Or`] is irrefutable when any alternative is — `_` in any
4305    /// position already makes the whole pattern match everything.
4306    pub fn is_irrefutable(&self) -> bool {
4307        match self {
4308            Pattern::Wildcard(_) | Pattern::Binding(_) => true,
4309            Pattern::Or(alts, _) => alts.iter().any(Pattern::is_irrefutable),
4310            Pattern::Record { fields, .. } => fields
4311                .iter()
4312                .all(|(_, sub)| sub.as_ref().is_none_or(Pattern::is_irrefutable)),
4313            // `[..]` and `[..rest]` match every list; any fixed length does not.
4314            Pattern::List { elems, rest, .. } => elems.is_empty() && rest.is_some(),
4315            _ => false,
4316        }
4317    }
4318}
4319
4320/// A single binding inside a variant pattern. Two surface forms:
4321/// `pattern` (positional — match the i-th payload field) and
4322/// `fieldName: pattern` (named — match the named payload field). The matched
4323/// sub-`pattern` is a full [`Pattern`] (ADR 0169), so a plain `name` is a
4324/// [`Pattern::Binding`], `_` a [`Pattern::Wildcard`], and `Ok(x)` a nested
4325/// [`Pattern::Variant`].
4326#[derive(Debug, Clone)]
4327pub struct PatternBinding {
4328    /// Source form: positional or named.
4329    pub kind: PatternBindingKind,
4330    pub span: Span,
4331}
4332
4333#[derive(Debug, Clone)]
4334pub enum PatternBindingKind {
4335    /// `pattern` (e.g. `x`, `_`, `Ok(v)`): match the payload field at this position.
4336    Positional { pattern: Pattern },
4337    /// `field: pattern`: match the named payload field against `pattern`.
4338    Named { field: Ident, pattern: Pattern },
4339}
4340
4341impl PatternBinding {
4342    /// The sub-pattern this binding matches its payload field against.
4343    pub fn pattern(&self) -> &Pattern {
4344        match &self.kind {
4345            PatternBindingKind::Positional { pattern } => pattern,
4346            PatternBindingKind::Named { pattern, .. } => pattern,
4347        }
4348    }
4349
4350    /// True when this binding discards its field (`_` or `field: _`) — a pure
4351    /// wildcard sub-pattern that binds nothing.
4352    pub fn is_wildcard(&self) -> bool {
4353        self.pattern().is_wildcard()
4354    }
4355}
4356
4357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4358pub enum BinOp {
4359    /// `P implies Q` — logical implication (v0.80). Desugars to `!P || Q`; sits
4360    /// at the lowest precedence (below `||`). Reads directionally (P → Q).
4361    Implies,
4362    Or,
4363    And,
4364    Eq,
4365    NotEq,
4366    Lt,
4367    LtEq,
4368    Gt,
4369    GtEq,
4370    Add,
4371    Sub,
4372    Mul,
4373    Div,
4374}
4375
4376impl BinOp {
4377    pub fn name(self) -> &'static str {
4378        match self {
4379            BinOp::Implies => "implies",
4380            BinOp::Or => "||",
4381            BinOp::And => "&&",
4382            BinOp::Eq => "==",
4383            BinOp::NotEq => "!=",
4384            BinOp::Lt => "<",
4385            BinOp::LtEq => "<=",
4386            BinOp::Gt => ">",
4387            BinOp::GtEq => ">=",
4388            BinOp::Add => "+",
4389            BinOp::Sub => "-",
4390            BinOp::Mul => "*",
4391            BinOp::Div => "/",
4392        }
4393    }
4394}
4395
4396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4397pub enum UnaryOp {
4398    Neg,
4399    Not,
4400}
4401
4402impl UnaryOp {
4403    pub fn name(self) -> &'static str {
4404        match self {
4405            UnaryOp::Neg => "-",
4406            UnaryOp::Not => "!",
4407        }
4408    }
4409}
4410
4411#[cfg(test)]
4412mod size_tests {
4413    use super::*;
4414
4415    /// Finding #31: boxing `ExprKind::Observation`'s payload and
4416    /// `ExprKind::Is`'s pattern field took `Expr` from 176 to 128 bytes on
4417    /// this target (the module-level `const _` assertion is the real pin;
4418    /// this test just makes the before/after concrete and fails loudly if a
4419    /// future change silently regresses the win rather than tripping the
4420    /// `<= 128` ceiling by enough to notice).
4421    #[test]
4422    fn expr_is_smaller_than_before_the_boxing() {
4423        assert!(
4424            std::mem::size_of::<Expr>() < 176,
4425            "Expr should be smaller than its pre-#31 size of 176 bytes"
4426        );
4427    }
4428}
4429
4430#[cfg(test)]
4431mod expr_children_tests {
4432    use super::*;
4433
4434    /// #1760: a match arm's guard is a child, between the discriminant and
4435    /// the arm's body, in evaluation order.
4436    #[test]
4437    fn a_match_arms_guard_is_a_child_in_evaluation_order() {
4438        let source = "commons d\n\nfn f(n: Int, lim: Int) -> Int {\n  match n {\n    k if k > lim => 1\n    _ => 0\n  }\n}\n";
4439        let tokens = crate::lexer::tokenize(source).unwrap();
4440        let units = crate::parser::parse_units(&tokens, source).unwrap();
4441        let SourceUnit::Commons(c) = &units[0] else {
4442            panic!("a commons");
4443        };
4444        let CommonsItem::Fn(f) = &c.items[0] else {
4445            panic!("a fn");
4446        };
4447        let ExprKind::Match { .. } = &f.body.tail.kind else {
4448            panic!("a match tail");
4449        };
4450        let children: Vec<&str> = expr_children(&f.body.tail)
4451            .into_iter()
4452            .map(|e| &source[e.span.start..e.span.end])
4453            .collect();
4454        assert_eq!(children, ["n", "k > lim", "1", "0"]);
4455    }
4456
4457    /// #1766 review: a `let`'s call-site principal identity is a statement
4458    /// expression, before the value it addresses.
4459    #[test]
4460    fn a_principal_identity_is_a_statement_expression() {
4461        let source = "suite demo.s {\n  case \"c\" {\n    let who = \"alice\"\n    let item <- api.get() by User(who)\n    expect item\n  }\n}\n";
4462        let tokens = crate::lexer::tokenize(source).unwrap();
4463        let units = crate::parser::parse_units(&tokens, source).unwrap();
4464        let SourceUnit::Suite(t) = &units[0] else {
4465            panic!("a suite");
4466        };
4467        let mut exprs = Vec::new();
4468        statement_exprs(&t.cases[0].body.statements[1], &mut exprs);
4469        let texts: Vec<&str> = exprs
4470            .into_iter()
4471            .map(|e| &source[e.span.start..e.span.end])
4472            .collect();
4473        assert_eq!(texts, ["who", "api.get()"]);
4474    }
4475}
4476
4477/// #1832: [`expr_children`] and [`statement_exprs`] reach every child slot of
4478/// every expression, so every walk built on them does too.
4479///
4480/// The guard is in two parts:
4481///
4482/// - **An oracle that cannot fall behind the AST.** [`oracle_children`] and
4483///   [`oracle_statement`] destructure every field of every `ExprKind` variant
4484///   and of every expression-bearing struct with no `..`, so adding a variant
4485///   or a field anywhere an expression can hide is a compile error here until
4486///   the oracle names the new slot.
4487/// - **A program that uses every slot.** [`COVERAGE`] is parsed (never
4488///   checked, so it only has to be syntactically valid), and two tests run
4489///   over it: every slot the oracle knows is used at least once, and at every
4490///   node `expr_children`/`statement_exprs` return exactly the oracle's
4491///   children. A new slot therefore fails the first test until the program
4492///   uses it, and the second until `expr_children` walks it.
4493///
4494/// The hand-rolled walks that cannot be built on `expr_children` (they carry
4495/// scopes, ownership or position) are tested where they live (#1832's
4496/// triage: the resolver, structure, linearity, effects and emit walks).
4497#[cfg(test)]
4498mod slot_coverage_tests {
4499    use super::*;
4500    use std::collections::BTreeSet;
4501
4502    /// Every slot, used at least once. `p` is a commons for the production
4503    /// forms; the suite holds the test-only ones (`Val`, `Wire`,
4504    /// observations, `faults`, `trace`, call-site principals).
4505    const COVERAGE: &str = r#"commons p
4506
4507fn f(x: Int) -> Int {
4508  let a = "s \(x) t"
4509  let b = g(x)
4510  let c = (y) => y
4511  let d = x + 1
4512  let e = -x
4513  let h = (x)
4514  let i = (z) => {
4515    let q = z
4516    q
4517  }
4518  let j = if x > 0 {
4519    let q = 1
4520    q
4521  } else {
4522    let w = 2
4523    w
4524  }
4525  let k = Ok(x)
4526  let l = Err(x)
4527  let n = r?
4528  let o = Some(x)
4529  let u = R { a: x }
4530  let v = r.a
4531  let w = r.m(x)
4532  let y = match x {
4533    z if z > 0 => z
4534    0 => expect x
4535    _ => {
4536      let s = 1
4537      s
4538    }
4539  }
4540  let aa = x is Some(v)
4541  let bb = R { ...r, a: x }
4542  let cc = Effect.pure(x)
4543  let dd = [x]
4544  ~> L.log(x)
4545  do s.put(x)
4546  cell := x
4547  x
4548}
4549"#;
4550
4551    const COVERAGE_SUITE: &str = r#"suite p {
4552  case "c" {
4553    let who = "alice"
4554    let a <- api.get(x) by User(who)
4555    let b = Val[T](x)
4556    let c = Wire(x)
4557    expect L.log called 2 times with msg == x
4558    expect box.call() faults
4559    expect api.post(x) by User(who) faults
4560    let t = trace(L.log)
4561    expect x
4562  }
4563}
4564"#;
4565
4566    type Slots<'a> = Vec<(&'static str, &'a Expr)>;
4567
4568    fn oracle_block<'a>(owner: &'static str, b: &'a Block, out: &mut Slots<'a>) {
4569        let Block {
4570            statements,
4571            tail,
4572            span: _,
4573            tail_leading_comments: _,
4574            implicit_tail: _,
4575        } = b;
4576        for s in statements {
4577            for (slot, e) in oracle_statement(s) {
4578                out.push((slot, e));
4579            }
4580        }
4581        out.push((owner, tail));
4582    }
4583
4584    /// A statement's expressions, each tagged with its slot.
4585    fn oracle_statement(s: &Statement) -> Slots<'_> {
4586        fn let_slots<'a>(kind: &'static str, l: &'a LetStmt, out: &mut Slots<'a>) {
4587            let LetStmt {
4588                name: _,
4589                type_annot: _,
4590                value,
4591                principal,
4592                let_else: _,
4593                span: _,
4594                trivia: _,
4595            } = l;
4596            if let Some(CallSiteActor {
4597                actor: _,
4598                identity,
4599                claims: _,
4600                span: _,
4601            }) = principal
4602                && let Some(identity) = identity
4603            {
4604                out.push((
4605                    if kind == "let" {
4606                        "let.identity"
4607                    } else {
4608                        "let<-.identity"
4609                    },
4610                    identity,
4611                ));
4612            }
4613            out.push((
4614                if kind == "let" {
4615                    "let.value"
4616                } else {
4617                    "let<-.value"
4618                },
4619                value,
4620            ));
4621        }
4622        let mut out = Vec::new();
4623        match s {
4624            Statement::Let(l) => let_slots("let", l, &mut out),
4625            Statement::EffectLet(l) => let_slots("let<-", l, &mut out),
4626            Statement::Expect(ExpectStmt {
4627                value,
4628                snapshot: _,
4629                span: _,
4630                trivia: _,
4631            }) => out.push(("expect.value", value)),
4632            Statement::Send(SendStmt {
4633                value,
4634                span: _,
4635                trivia: _,
4636            }) => out.push(("send.value", value)),
4637            Statement::Do(DoStmt {
4638                value,
4639                span: _,
4640                trivia: _,
4641            }) => out.push(("do.value", value)),
4642            Statement::Assign(AssignStmt {
4643                target: _,
4644                value,
4645                span: _,
4646                trivia: _,
4647            }) => out.push(("assign.value", value)),
4648            Statement::Advance(AdvanceStmt {
4649                keyword: _,
4650                cap: _,
4651                amount,
4652                span: _,
4653                trivia: _,
4654            }) => out.push(("advance.amount", amount)),
4655            Statement::Par(ParStmt {
4656                keyword: _,
4657                statements,
4658                closing_comments: _,
4659                span: _,
4660                trivia: _,
4661            }) => {
4662                for s in statements {
4663                    for (slot, e) in oracle_statement(s) {
4664                        out.push((slot, e));
4665                    }
4666                }
4667            }
4668        }
4669        out
4670    }
4671
4672    /// An expression's children, each tagged with its slot.
4673    fn oracle_children(e: &Expr) -> Slots<'_> {
4674        let Expr {
4675            id: _,
4676            kind,
4677            span: _,
4678        } = e;
4679        let mut out = Vec::new();
4680        match kind {
4681            ExprKind::IntLit {
4682                value: _,
4683                lexeme: _,
4684            }
4685            | ExprKind::FloatLit {
4686                value: _,
4687                lexeme: _,
4688            }
4689            | ExprKind::DecimalLit {
4690                coef: _,
4691                scale: _,
4692                lexeme: _,
4693            }
4694            | ExprKind::DurationLit {
4695                value: _,
4696                unit: _,
4697                millis: _,
4698            }
4699            | ExprKind::StrLit(_)
4700            | ExprKind::BoolLit(_)
4701            | ExprKind::Ident(_)
4702            | ExprKind::None
4703            | ExprKind::UnitLit
4704            | ExprKind::Trace { cap: _, op: _ } => {}
4705            ExprKind::InterpStr(parts) => {
4706                for p in parts {
4707                    match p {
4708                        InterpPart::Chunk(_) => {}
4709                        InterpPart::Hole(h) => out.push(("interp.hole", h.as_ref())),
4710                    }
4711                }
4712            }
4713            ExprKind::Call {
4714                name: _,
4715                type_args: _,
4716                args,
4717            } => args.iter().for_each(|a| out.push(("call.arg", a))),
4718            ExprKind::Lambda(LambdaExpr {
4719                params: _,
4720                body,
4721                span: _,
4722                block_body: _,
4723            }) => out.push(("lambda.body", body)),
4724            ExprKind::BinOp(_, l, r) => {
4725                out.push(("binop.left", l));
4726                out.push(("binop.right", r));
4727            }
4728            ExprKind::UnaryOp(_, x) => out.push(("unary.operand", x)),
4729            ExprKind::Paren(x) => out.push(("paren.inner", x)),
4730            ExprKind::Block(b) => oracle_block("block.tail", b, &mut out),
4731            ExprKind::If {
4732                cond,
4733                then_block,
4734                else_block,
4735            } => {
4736                out.push(("if.cond", cond));
4737                oracle_block("if.then.tail", then_block, &mut out);
4738                oracle_block("if.else.tail", else_block, &mut out);
4739            }
4740            ExprKind::Ok(x) => out.push(("ok.inner", x)),
4741            ExprKind::Err(x) => out.push(("err.inner", x)),
4742            ExprKind::Question(x) => out.push(("question.inner", x)),
4743            ExprKind::RecordConstruction {
4744                type_name: _,
4745                fields,
4746            } => {
4747                for FieldInit {
4748                    name: _,
4749                    value,
4750                    span: _,
4751                } in fields
4752                {
4753                    if let Some(v) = value {
4754                        out.push(("record.field", v));
4755                    }
4756                }
4757            }
4758            ExprKind::FieldAccess { receiver, field: _ } => out.push(("field.receiver", receiver)),
4759            ExprKind::MethodCall {
4760                receiver,
4761                method: _,
4762                type_args: _,
4763                args,
4764            } => {
4765                out.push(("method.receiver", receiver));
4766                args.iter().for_each(|a| out.push(("method.arg", a)));
4767            }
4768            ExprKind::Match { discriminant, arms } => {
4769                out.push(("match.discriminant", discriminant));
4770                for MatchArm {
4771                    pattern: _,
4772                    guard,
4773                    body,
4774                    span: _,
4775                } in arms
4776                {
4777                    if let Some(g) = guard {
4778                        out.push(("match.guard", g));
4779                    }
4780                    match body {
4781                        MatchBody::Expr(x) => out.push(("match.body", x)),
4782                        MatchBody::Block(b) => oracle_block("match.block.tail", b, &mut out),
4783                    }
4784                }
4785            }
4786            ExprKind::Is { value, pattern: _ } => out.push(("is.value", value)),
4787            ExprKind::Some(x) => out.push(("some.inner", x)),
4788            ExprKind::RecordSpread {
4789                type_name: _,
4790                base,
4791                overrides,
4792            } => {
4793                out.push(("spread.base", base));
4794                for FieldInit {
4795                    name: _,
4796                    value,
4797                    span: _,
4798                } in overrides
4799                {
4800                    if let Some(v) = value {
4801                        out.push(("spread.override", v));
4802                    }
4803                }
4804            }
4805            ExprKind::EffectPure(x) => out.push(("effect_pure.inner", x)),
4806            ExprKind::Expect(x) => out.push(("expect_expr.inner", x)),
4807            ExprKind::Val { type_ref: _, args } => {
4808                args.iter().for_each(|a| out.push(("val.arg", a)))
4809            }
4810            ExprKind::Wire(x) => out.push(("wire.inner", x)),
4811            ExprKind::ListLit(items) => items.iter().for_each(|i| out.push(("list.item", i))),
4812            ExprKind::Observation(obs) => {
4813                let ObservationExpr {
4814                    cap: _,
4815                    op: _,
4816                    matcher,
4817                } = obs.as_ref();
4818                match matcher {
4819                    ObservationMatcher::Called { count, with_pred } => {
4820                        if let Some(c) = count {
4821                            out.push(("observation.count", c));
4822                        }
4823                        if let Some(p) = with_pred {
4824                            out.push(("observation.with", p));
4825                        }
4826                    }
4827                    ObservationMatcher::NeverCalled
4828                    | ObservationMatcher::Before { cap: _, op: _ } => {}
4829                }
4830            }
4831            ExprKind::Faults(f) => {
4832                let FaultsExpr { call, principal } = f.as_ref();
4833                if let Some(CallSiteActor {
4834                    actor: _,
4835                    identity,
4836                    claims: _,
4837                    span: _,
4838                }) = principal
4839                    && let Some(identity) = identity
4840                {
4841                    out.push(("faults.identity", identity));
4842                }
4843                out.push(("faults.inner", call));
4844            }
4845        }
4846        out
4847    }
4848
4849    /// Every slot [`oracle_children`] and [`oracle_statement`] can name.
4850    const ALL_SLOTS: &[&str] = &[
4851        "interp.hole",
4852        "call.arg",
4853        "lambda.body",
4854        "binop.left",
4855        "binop.right",
4856        "unary.operand",
4857        "paren.inner",
4858        "block.tail",
4859        "if.cond",
4860        "if.then.tail",
4861        "if.else.tail",
4862        "ok.inner",
4863        "err.inner",
4864        "question.inner",
4865        "record.field",
4866        "field.receiver",
4867        "method.receiver",
4868        "method.arg",
4869        "match.discriminant",
4870        "match.guard",
4871        "match.body",
4872        "match.block.tail",
4873        "is.value",
4874        "some.inner",
4875        "spread.base",
4876        "spread.override",
4877        "effect_pure.inner",
4878        "expect_expr.inner",
4879        "val.arg",
4880        "wire.inner",
4881        "list.item",
4882        "observation.count",
4883        "observation.with",
4884        "faults.identity",
4885        "faults.inner",
4886        "let.value",
4887        "let<-.value",
4888        "let<-.identity",
4889        "expect.value",
4890        "send.value",
4891        "do.value",
4892        "assign.value",
4893    ];
4894
4895    /// Every top-level block of the coverage program: fn bodies and case
4896    /// bodies.
4897    fn bodies(units: &[SourceUnit]) -> Vec<&Block> {
4898        let mut out = Vec::new();
4899        for u in units {
4900            match u {
4901                SourceUnit::Commons(c) => {
4902                    for item in &c.items {
4903                        if let CommonsItem::Fn(f) = item {
4904                            out.push(&f.body);
4905                        }
4906                    }
4907                }
4908                SourceUnit::Suite(t) => out.extend(t.cases.iter().map(|c| &c.body)),
4909                _ => {}
4910            }
4911        }
4912        out
4913    }
4914
4915    fn parse(source: &str) -> Vec<SourceUnit> {
4916        let tokens = crate::lexer::tokenize(source).expect("lex");
4917        crate::parser::parse_units(&tokens, source).expect("the coverage program parses")
4918    }
4919
4920    /// Visit every expression under `b` through the oracle, depth first.
4921    fn visit<'a>(b: &'a Block, f: &mut impl FnMut(&'a Expr)) {
4922        fn go<'a>(e: &'a Expr, f: &mut impl FnMut(&'a Expr)) {
4923            f(e);
4924            for (_, c) in oracle_children(e) {
4925                go(c, f);
4926            }
4927        }
4928        let mut top = Vec::new();
4929        oracle_block("body.tail", b, &mut top);
4930        for (_, e) in top {
4931            go(e, f);
4932        }
4933    }
4934
4935    #[test]
4936    fn the_coverage_program_uses_every_slot() {
4937        let mut used = BTreeSet::new();
4938        for source in [COVERAGE, COVERAGE_SUITE] {
4939            let units = parse(source);
4940            for b in bodies(&units) {
4941                for s in &b.statements {
4942                    used.extend(oracle_statement(s).into_iter().map(|(slot, _)| slot));
4943                }
4944                visit(b, &mut |e| {
4945                    used.extend(oracle_children(e).into_iter().map(|(slot, _)| slot));
4946                    if let ExprKind::Block(b) = &e.kind {
4947                        for s in &b.statements {
4948                            used.extend(oracle_statement(s).into_iter().map(|(slot, _)| slot));
4949                        }
4950                    }
4951                });
4952            }
4953        }
4954        let missing: Vec<&str> = ALL_SLOTS
4955            .iter()
4956            .copied()
4957            .filter(|s| !used.contains(s))
4958            .collect();
4959        let unknown: Vec<&str> = used
4960            .iter()
4961            .copied()
4962            .filter(|s| !ALL_SLOTS.contains(s))
4963            .collect();
4964        assert!(
4965            missing.is_empty() && unknown.is_empty(),
4966            "slots the coverage program never uses: {missing:?}; slots missing from ALL_SLOTS: {unknown:?}"
4967        );
4968    }
4969
4970    #[test]
4971    fn expr_children_returns_exactly_the_oracle_s_children_at_every_node() {
4972        let key = |e: &Expr| (e.span.start, e.span.end, e.id);
4973        let mut checked = 0;
4974        for source in [COVERAGE, COVERAGE_SUITE] {
4975            let units = parse(source);
4976            for b in bodies(&units) {
4977                for s in &b.statements {
4978                    let mut got = Vec::new();
4979                    statement_exprs(s, &mut got);
4980                    let got: Vec<_> = got.into_iter().map(key).collect();
4981                    let want: Vec<_> = oracle_statement(s)
4982                        .into_iter()
4983                        .map(|(_, e)| key(e))
4984                        .collect();
4985                    assert_eq!(got, want, "statement_exprs at {:?}", s.span());
4986                }
4987                visit(b, &mut |e| {
4988                    let got: Vec<_> = expr_children(e).into_iter().map(key).collect();
4989                    let want: Vec<_> = oracle_children(e)
4990                        .into_iter()
4991                        .map(|(_, c)| key(c))
4992                        .collect();
4993                    assert_eq!(
4994                        got,
4995                        want,
4996                        "expr_children of `{}`",
4997                        &source[e.span.start..e.span.end]
4998                    );
4999                    checked += 1;
5000                });
5001            }
5002        }
5003        assert!(checked > 50, "only {checked} nodes checked");
5004    }
5005}
5006
5007#[cfg(test)]
5008mod stream_path_tests {
5009    use super::*;
5010
5011    fn types(source: &str) -> std::collections::HashMap<String, std::sync::Arc<TypeDecl>> {
5012        let tokens = crate::lexer::tokenize(source).unwrap();
5013        let units = crate::parser::parse_units(&tokens, source).unwrap();
5014        let SourceUnit::Commons(c) = &units[0] else {
5015            panic!("a commons");
5016        };
5017        c.items
5018            .iter()
5019            .filter_map(|i| match i {
5020                CommonsItem::Type(t) => Some((t.name.name.clone(), std::sync::Arc::new(t.clone()))),
5021                _ => None,
5022            })
5023            .collect()
5024    }
5025
5026    fn named(name: &str) -> TypeRef {
5027        TypeRef::Named(Ident {
5028            name: name.to_string(),
5029            span: Span::default(),
5030        })
5031    }
5032
5033    /// K-streams (#2623): the chain of fields a stream is reached through,
5034    /// across records, sums and a generic record's own fields, terminating on
5035    /// a recursive type.
5036    #[test]
5037    fn the_stream_path_names_each_field_it_passes_through() {
5038        let t = types(
5039            "commons d\n\
5040             type Download = { contentType: String, body: Stream[Bytes] }\n\
5041             type Shelf = { name: String, latest: Option[Download] }\n\
5042             type Pick = | Got(d: Download) | Nothing\n\
5043             type Wrap[T] = { s: Stream[T] }\n\
5044             type Tree = { kids: List[Tree], n: Int }\n\
5045             type Plain = { n: Int }\n",
5046        );
5047        let path = |n: &str| stream_path(&named(n), &t);
5048        assert_eq!(path("Download"), Some(vec!["Download.body".to_string()]));
5049        assert_eq!(
5050            path("Shelf"),
5051            Some(vec![
5052                "Shelf.latest".to_string(),
5053                "Download.body".to_string()
5054            ])
5055        );
5056        assert_eq!(
5057            path("Pick"),
5058            Some(vec!["Pick.Got.d".to_string(), "Download.body".to_string()])
5059        );
5060        assert_eq!(path("Wrap"), Some(vec!["Wrap.s".to_string()]));
5061        assert_eq!(path("Tree"), None);
5062        assert_eq!(path("Plain"), None);
5063        assert!(type_carries_stream("Shelf", &t));
5064        assert!(!type_carries_stream("Tree", &t));
5065    }
5066
5067    /// #2867: the calendar rule at its edges.
5068    #[test]
5069    fn is_iso_date_decides_the_calendar() {
5070        for ok in [
5071            "2026-11-03",
5072            "2028-02-29",
5073            "2000-02-29",
5074            "0000-01-01",
5075            "9999-12-31",
5076            "2026-04-30",
5077            "2026-12-31",
5078            "2400-02-29",
5079        ] {
5080            assert!(is_iso_date(ok), "{ok}");
5081        }
5082        for bad in [
5083            "2026-02-30",
5084            "2026-02-29",
5085            "1900-02-29",
5086            "2100-02-29",
5087            "2026-04-31",
5088            "2026-13-01",
5089            "2026-00-10",
5090            "2026-01-00",
5091            "2026-1-1",
5092            "yesterday",
5093            "",
5094            "2026-11-03 ",
5095            "20261103",
5096            "2026/11/03",
5097            "+2026-11-03",
5098            "2026-11-03T00:00:00Z",
5099            "2026-11-03",
5100            "2026-+1-03",
5101        ] {
5102            assert!(!is_iso_date(bad), "{bad:?}");
5103        }
5104    }
5105}