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    pub span: Span,
2747}
2748
2749/// A float refinement bound (v0.21): the parsed value plus the signed source
2750/// lexeme (for byte-stable emission). v0.40 (ADR 0073): also the source span,
2751/// for the `InRange`-swap quick-fix.
2752#[derive(Debug, Clone)]
2753pub struct FloatBound {
2754    pub value: f64,
2755    pub lexeme: String,
2756    pub span: Span,
2757}
2758
2759/// A decimal refinement bound (#2622): `InRange(0.00d, 100.00d)`. The
2760/// coefficient keeps any leading `-`; the lexeme is the signed source text.
2761#[derive(Debug, Clone)]
2762pub struct DecimalBound {
2763    pub coef: String,
2764    pub scale: u32,
2765    pub lexeme: String,
2766    pub span: Span,
2767}
2768
2769#[derive(Debug, Clone)]
2770pub struct Refinement {
2771    pub predicates: Vec<RefinementPred>,
2772    pub span: Span,
2773}
2774
2775#[derive(Debug, Clone)]
2776pub struct RefinementPred {
2777    pub kind: PredKind,
2778    pub span: Span,
2779}
2780
2781#[derive(Debug, Clone)]
2782pub enum PredKind {
2783    Matches(String),
2784    InRange(IntBound, IntBound),
2785    /// `InRange` with float bounds (v0.21) — a separate variant so every
2786    /// `Int` refinement path stays untouched. Bounds keep their source
2787    /// lexemes (including any sign) so emitted runtime checks are
2788    /// byte-stable.
2789    InRangeF(FloatBound, FloatBound),
2790    /// `InRange` with decimal bounds (#2622): `InRange(0.00d, 100.00d)`.
2791    InRangeD(DecimalBound, DecimalBound),
2792    MinLength(i64),
2793    MaxLength(i64),
2794    Length(i64),
2795    NonNegative,
2796    Positive,
2797    NonEmpty,
2798    /// #2867: a `String` that is a real calendar date, exactly `YYYY-MM-DD`
2799    /// (years 0000-9999). The same date rule `Instant.fromIso` applies.
2800    IsoDate,
2801}
2802
2803/// #2867: is `s` exactly `YYYY-MM-DD` and a date that exists? The one
2804/// compile-time statement of the rule `IsoDate` enforces; the runtime's
2805/// `__bynkIsIsoDate` (`bynk-emit/runtime/src/time.ts`) is its TypeScript twin
2806/// and shares the calendar with `Instant.fromIso`.
2807pub fn is_iso_date(s: &str) -> bool {
2808    let b = s.as_bytes();
2809    if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
2810        return false;
2811    }
2812    let digits = |r: std::ops::Range<usize>| -> Option<u32> {
2813        let part = &b[r];
2814        if part.iter().all(u8::is_ascii_digit) {
2815            Some(part.iter().fold(0, |n, d| n * 10 + u32::from(d - b'0')))
2816        } else {
2817            None
2818        }
2819    };
2820    let (Some(year), Some(month), Some(day)) = (digits(0..4), digits(5..7), digits(8..10)) else {
2821        return false;
2822    };
2823    let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
2824    let days = match month {
2825        2 if leap => 29,
2826        2 => 28,
2827        4 | 6 | 9 | 11 => 30,
2828        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2829        _ => return false,
2830    };
2831    (1..=days).contains(&day)
2832}
2833
2834impl PredKind {
2835    pub fn name(&self) -> &'static str {
2836        match self {
2837            PredKind::Matches(_) => "Matches",
2838            PredKind::InRange(..) | PredKind::InRangeF(..) | PredKind::InRangeD(..) => "InRange",
2839            PredKind::MinLength(_) => "MinLength",
2840            PredKind::MaxLength(_) => "MaxLength",
2841            PredKind::Length(_) => "Length",
2842            PredKind::NonNegative => "NonNegative",
2843            PredKind::Positive => "Positive",
2844            PredKind::NonEmpty => "NonEmpty",
2845            PredKind::IsoDate => "IsoDate",
2846        }
2847    }
2848}
2849
2850/// #1651: the order a refinement's predicates are **checked** in at runtime:
2851/// every `Matches` after every other predicate, and otherwise source order.
2852///
2853/// A `Matches` pattern may take time polynomial in the input's length, and a
2854/// `MaxLength`/`Length` bound is what caps it (`bynk.types.polynomial_regex_capped`),
2855/// so the length checks must run first wherever the author wrote them. The order
2856/// is observable only in which failure a value failing several predicates
2857/// reports. Every runtime check site (`.of`, the boundary codec, `is`) and the
2858/// checker's compile-time literal check use this order, so they agree. The
2859/// canonical form a contract fingerprint hashes sorts its own copy and is
2860/// unaffected.
2861pub fn in_check_order<T>(preds: &[T], kind: impl Fn(&T) -> &PredKind) -> Vec<&T> {
2862    let (regex, rest): (Vec<&T>, Vec<&T>) = preds
2863        .iter()
2864        .partition(|p| matches!(kind(p), PredKind::Matches(_)));
2865    rest.into_iter().chain(regex).collect()
2866}
2867
2868/// A function type parameter (v0.20a, `fn name[A, B](…)`). A struct rather
2869/// than a bare Ident so the ADR-0028 "bound-capable" promise is a later field
2870/// addition, not a representation change.
2871#[derive(Debug, Clone)]
2872pub struct TypeParam {
2873    pub name: Ident,
2874    /// Declared constraints, `T: C1 + C2` (constrained-generics slice 0). Empty
2875    /// for an unbounded parameter. Slice 0 admits only `Eq`; the checker
2876    /// reports any other name as `bynk.generics.unknown_constraint`.
2877    pub bounds: Vec<Ident>,
2878    pub span: Span,
2879}
2880
2881/// One `A: C1 + C2` entry of a method's `where` clause (constrained-generics
2882/// slice 0): bounds a type parameter the method does not declare itself — the
2883/// receiver type's (`fn Box.has(self, x: A) -> Bool where A: Eq`).
2884#[derive(Debug, Clone)]
2885pub struct WhereBound {
2886    pub param: Ident,
2887    pub bounds: Vec<Ident>,
2888    pub span: Span,
2889}
2890
2891/// A lambda expression (v0.20a): `(params) => expr` or `(params) => { … }`.
2892/// `=>` is the value arrow (shared with `match`); param annotations are
2893/// optional where an expected function type supplies them.
2894#[derive(Debug, Clone)]
2895pub struct LambdaExpr {
2896    pub params: Vec<LambdaParam>,
2897    pub body: Box<Expr>,
2898    pub span: Span,
2899    /// #2712: whether the author wrote the body as a `{ … }` block. Only read
2900    /// when a parameter is a pattern, whose desugaring wraps the body in a block
2901    /// either way; the formatter needs the source form back.
2902    pub block_body: bool,
2903}
2904
2905/// A lambda parameter. A separate type from [`Param`] because its annotation
2906/// is optional — `Param.type_ref` stays mandatory at every signature site.
2907#[derive(Debug, Clone)]
2908pub struct LambdaParam {
2909    pub name: Ident,
2910    pub type_ref: Option<TypeRef>,
2911    pub span: Span,
2912    /// #2712: the irrefutable record pattern the author wrote in place of a name
2913    /// — `(Item { qty, .. }) => …`. `name` is then the hidden
2914    /// `__lambdaArg<offset>` (empty span, at the pattern), `type_ref` is `None`,
2915    /// and the lambda's body is already desugared to
2916    /// `{ let <pattern> = __lambdaArg<offset>; <body> }` through the `else`-less
2917    /// `let` (ADR 0484, #2654). The pattern is kept here for the checker (the
2918    /// parameter's type comes from its name) and to print the source form back.
2919    pub pattern: Option<Pattern>,
2920}
2921
2922/// #2712: the prefix of every hidden lambda-parameter binder. See
2923/// [`LambdaParam::pattern`]; like [`LET_ELSE_BINDER_PREFIX`] it cannot collide
2924/// with a user identifier.
2925pub const LAMBDA_ARG_BINDER_PREFIX: &str = "__lambdaArg";
2926
2927/// #2896: the prefix of the hidden name a wildcard lambda parameter (`_`) is
2928/// given — `__wild<offset>`, with an empty span at the `_`. The parameter takes
2929/// its type from the expected function type like a named one; the unique name
2930/// keeps `(_, _) => …` from emitting duplicate TS identifiers, and no user
2931/// identifier can spell it, so nothing in the body can refer to it.
2932pub const LAMBDA_WILDCARD_PREFIX: &str = "__wild";
2933
2934impl LambdaParam {
2935    /// #2896: whether this is a `_` parameter. Keyed on the hidden prefix,
2936    /// which no user identifier can start with (identifiers do not begin with
2937    /// `_`); any new generated name must not reuse `__wild`.
2938    pub fn is_wildcard(&self) -> bool {
2939        self.pattern.is_none() && self.name.name.starts_with(LAMBDA_WILDCARD_PREFIX)
2940    }
2941}
2942
2943#[derive(Debug, Clone)]
2944pub struct FnDecl {
2945    /// v0.20a: `[A, B]` type parameters; empty for non-generic functions.
2946    pub type_params: Vec<TypeParam>,
2947    /// Free function or method (`TypeName.methodName`). See [`FnName`].
2948    pub name: FnName,
2949    pub params: Vec<Param>,
2950    pub return_type: TypeRef,
2951    /// #2868: `given C1, C2` on a context-level function, parsed straight after
2952    /// the return type: the capabilities the body may use, and that every
2953    /// caller must itself have in scope. Empty when absent.
2954    pub given: Vec<CapRef>,
2955    /// `where A: Eq, …`, parsed between the return type and the contract
2956    /// clauses; empty when absent. Bounds receiver type parameters (and may
2957    /// restate the method's own).
2958    pub where_bounds: Vec<WhereBound>,
2959    /// v0.115: preconditions (`requires <name>: <pred>`), parsed between the
2960    /// return type and the body. A contract clause is the invariant predicate
2961    /// attached to a function (ADR 0144 — one predicate surface); `requires`
2962    /// scopes over the parameters only.
2963    pub requires: Vec<Contract>,
2964    /// v0.115: postconditions (`ensures <name>: <pred>`). Scopes over the
2965    /// parameters *and* `result`, the contextual binding for the return value.
2966    pub ensures: Vec<Contract>,
2967    pub body: Block,
2968    /// True when the first parameter is the special `self` parameter. Only
2969    /// valid for method declarations.
2970    pub has_self: bool,
2971    /// Documentation block attached to this declaration (v0.3).
2972    pub documentation: Option<Doc>,
2973    /// #2618: the span of a leading `private` modifier, when the declaration
2974    /// is unit-private: visible across its own unit's files (and to that
2975    /// unit's suites), never through `uses`. `None` is the default, public.
2976    pub private: Option<Span>,
2977    pub span: Span,
2978    pub trivia: Trivia,
2979}
2980
2981/// A function-declaration name: either a free function `f` or a method
2982/// `T.method` (v0.2 §3.6).
2983#[derive(Debug, Clone)]
2984pub enum FnName {
2985    /// `fn name(...)` — a free function.
2986    Free(Ident),
2987    /// `fn TypeName.methodName(...)` — a method attached to a type.
2988    Method {
2989        type_name: Ident,
2990        method_name: Ident,
2991    },
2992}
2993
2994impl FnName {
2995    /// The function's short name for diagnostics. For methods returns the
2996    /// method portion only; the type prefix is recovered via `type_name`.
2997    pub fn ident(&self) -> &Ident {
2998        match self {
2999            FnName::Free(id) => id,
3000            FnName::Method { method_name, .. } => method_name,
3001        }
3002    }
3003
3004    /// For methods, the attached type's identifier; `None` for free fns.
3005    pub fn type_name(&self) -> Option<&Ident> {
3006        match self {
3007            FnName::Free(_) => None,
3008            FnName::Method { type_name, .. } => Some(type_name),
3009        }
3010    }
3011
3012    /// The displayed full name (e.g., `Money.add` or `parseSku`).
3013    pub fn display(&self) -> String {
3014        match self {
3015            FnName::Free(id) => id.name.clone(),
3016            FnName::Method {
3017                type_name,
3018                method_name,
3019            } => format!("{}.{}", type_name.name, method_name.name),
3020        }
3021    }
3022}
3023
3024/// A brace-delimited block of statements ending in a tail expression
3025/// whose value is the block's value (spec v0.1 §3.1).
3026#[derive(Debug, Clone)]
3027pub struct Block {
3028    pub statements: Vec<Statement>,
3029    pub tail: Box<Expr>,
3030    pub span: Span,
3031    /// Line comments that appear between the last statement (or the
3032    /// opening brace) and the tail expression. Preserved here because
3033    /// expressions do not carry trivia in v1.1.
3034    pub tail_leading_comments: Vec<Comment>,
3035    /// `true` when the block was written with no explicit tail expression and
3036    /// the parser synthesised a `()` (unit) tail (v0.146, ADR 0170). The tail
3037    /// is a real `ExprKind::UnitLit` either way; this flag records that it was
3038    /// *implicit* so the formatter can omit it (Bynk has no statement
3039    /// terminator, so a printed `()` would re-attach to the last statement on
3040    /// re-parse — `x` `()` → `x()`). The parser re-derives the implicit unit
3041    /// tail, so omitting it is loss-free.
3042    pub implicit_tail: bool,
3043}
3044
3045impl Block {
3046    /// Whether this block is a synthesised empty unit block — no statements and
3047    /// an *implicit* `()` tail (v0.146, ADR 0170). This is exactly the shape the
3048    /// parser inserts for an `if` with no `else` branch, so both the checker
3049    /// (gating the else-less form to unit) and the formatter (omitting the
3050    /// synthetic `else { () }`) recognise it here.
3051    pub fn is_synth_unit(&self) -> bool {
3052        self.statements.is_empty()
3053            && self.implicit_tail
3054            && matches!(self.tail.kind, ExprKind::UnitLit)
3055    }
3056}
3057
3058/// Block-level statement.
3059#[derive(Debug, Clone)]
3060pub enum Statement {
3061    /// `let name (: T)? = expr` — pure binding (v0.1).
3062    Let(LetStmt),
3063    /// `let name (: T)? <- expr` — effectful binding (v0.5).
3064    EffectLet(LetStmt),
3065    /// `expect expr` — verify a Bool predicate at test runtime (v0.7; renamed
3066    /// from `assert` in v0.112). Only valid inside test case bodies.
3067    Expect(ExpectStmt),
3068    /// `~> expr` — an asynchronous fire-and-forget send (v0.79). The caller does
3069    /// not await the reply; legal only when the reply is `Effect[()]`. No binder.
3070    Send(SendStmt),
3071    /// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
3072    /// Runs an `Effect[()]` and discards its (unit) result — the binder-free
3073    /// sugar for `let _ <- expr` when the awaited value is unit. Legal only in
3074    /// an effectful body; the operand MUST be `Effect[()]` (a valued reply keeps
3075    /// the explicit `let _ <- e`, so throwing away a real value stays visible).
3076    Do(DoStmt),
3077    /// `name := expr` — a `Cell` store write (v0.81, storage track). The
3078    /// unconditional write form; `.update(fn)` (a method call) is the
3079    /// read-modify-write form. ADR 0108.
3080    Assign(AssignStmt),
3081    /// `advance Clock by <Duration>` — move the harness clock forward
3082    /// (#2137, end-to-end testing track §3.4). `advance` is contextual: it
3083    /// opens a statement only before `<Ident> by`. Only valid in a `case` body.
3084    Advance(AdvanceStmt),
3085    /// `par { let a <- e1  let b <- e2 }` — a concurrent binding block (#2640,
3086    /// concurrent-effects slice 0). `par` is contextual: it opens a statement
3087    /// only before `{`. The parser admits any statements inside; the checker
3088    /// accepts only `let x <- e` and `do e`.
3089    Par(ParStmt),
3090}
3091
3092impl Statement {
3093    pub fn span(&self) -> Span {
3094        match self {
3095            Statement::Let(l) | Statement::EffectLet(l) => l.span,
3096            Statement::Expect(a) => a.span,
3097            Statement::Send(s) => s.span,
3098            Statement::Do(d) => d.span,
3099            Statement::Assign(a) => a.span,
3100            Statement::Advance(a) => a.span,
3101            Statement::Par(p) => p.span,
3102        }
3103    }
3104}
3105
3106/// #2640: a `par { … }` block. `statements` is what the author wrote between
3107/// the braces, unvalidated: the checker rejects every form but the effectful
3108/// `let` and `do` (`bynk.par.invalid_statement`), so the editor still sees the
3109/// rejected statement in place.
3110#[derive(Debug, Clone)]
3111pub struct ParStmt {
3112    /// The contextual `par` keyword.
3113    pub keyword: Span,
3114    pub statements: Vec<Statement>,
3115    /// Comments between the last statement and the closing `}`.
3116    pub closing_comments: Vec<Comment>,
3117    pub span: Span,
3118    pub trivia: Trivia,
3119}
3120
3121/// #2137: `advance <Cap> by <amount>` — moves the case's harness clock forward
3122/// by a `Duration`. `cap` must be `Clock` (the checker refuses any other).
3123#[derive(Debug, Clone)]
3124pub struct AdvanceStmt {
3125    /// The contextual `advance` keyword.
3126    pub keyword: Span,
3127    /// The capability whose time moves (`Clock`).
3128    pub cap: Ident,
3129    /// The `Duration` to move it by.
3130    pub amount: Expr,
3131    pub span: Span,
3132    pub trivia: Trivia,
3133}
3134
3135/// #2842: the `matches snapshot "<name>"` tail of an `expect`. `matches` and
3136/// `snapshot` are contextual words, matched by text like `faults`.
3137#[derive(Debug, Clone)]
3138pub struct SnapshotClause {
3139    /// The snapshot's name (the literal's text), unique within its case.
3140    pub name: String,
3141    /// The `matches` keyword through the closing quote of the name.
3142    pub span: Span,
3143    /// The name literal.
3144    pub name_span: Span,
3145}
3146
3147#[derive(Debug, Clone)]
3148pub struct ExpectStmt {
3149    pub value: Expr,
3150    /// #2842: `expect <value> matches snapshot "<name>"` compares `value`'s
3151    /// canonical JSON to a stored snapshot instead of asserting a `Bool`. `None`
3152    /// for every other `expect`.
3153    pub snapshot: Option<SnapshotClause>,
3154    pub span: Span,
3155    pub trivia: Trivia,
3156}
3157
3158/// `name := expr` — a `Cell` store write (v0.81, storage track). `target` is the
3159/// `Cell` field being written (a bare name for now; the checker resolves it to a
3160/// `store` field). `value` is the new value.
3161#[derive(Debug, Clone)]
3162pub struct AssignStmt {
3163    pub target: Ident,
3164    pub value: Expr,
3165    pub span: Span,
3166    pub trivia: Trivia,
3167}
3168
3169#[derive(Debug, Clone)]
3170pub struct LetStmt {
3171    pub name: Ident,
3172    pub type_annot: Option<TypeRef>,
3173    pub value: Expr,
3174    /// v0.182 (#664): the call-site `by <Actor>(<identity>)` clause on an
3175    /// `EffectLet` whose value addresses a test service handler. `None` on a pure
3176    /// `Let` (the `by` is parsed only in the `<-` arm) and on an effect-let with
3177    /// no principal.
3178    pub principal: Option<CallSiteActor>,
3179    /// #2010: set when this binding is the hidden one a `let <pattern> … else …`
3180    /// statement desugars into. `None` for every binding the author wrote.
3181    pub let_else: Option<LetElse>,
3182    pub span: Span,
3183    pub trivia: Trivia,
3184}
3185
3186/// #2010: what remains of a `let <pattern> <- e else …` (or `= e else …`)
3187/// statement once the parser has desugared it.
3188///
3189/// The parser rewrites `let P <- e else E` followed by the rest of the block
3190/// into a hidden binding, `let __letElse<offset> <- e`, and a `match` on it as
3191/// the block's tail: `P => { rest of the block }` first, then either
3192/// `_ => E` (an `else` block) or the `else match` arms. Everything downstream
3193/// (checker, emitter, editor) sees only that ordinary `match`. This marker,
3194/// carried on the hidden binding, is what lets the formatter print the sugar
3195/// back and the checker word its diagnostics in the author's terms.
3196#[derive(Debug, Clone)]
3197pub struct LetElse {
3198    /// Whether the `else` was a block or a `match { arms }`.
3199    pub form: LetElseForm,
3200    /// The `else` keyword (empty, at the end of the statement, for [`LetElseForm::Plain`]).
3201    pub else_span: Span,
3202    /// #2654: for a record pattern, the source ranges whose deletion leaves the
3203    /// `else`-less form — any parentheses around the pattern, and the `else`
3204    /// clause with the space before it. Empty for every other pattern, and for
3205    /// the plain form.
3206    pub remove_else: Vec<Span>,
3207}
3208
3209/// #2010: the spellings of a `let … else` fallback (and, #2654, its absence).
3210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3211pub enum LetElseForm {
3212    /// `else { … }` — desugared to a trailing `_ => { … }` arm.
3213    Block,
3214    /// `else match { arms }` — the arms follow the pattern's arm unchanged.
3215    Match,
3216    /// No `else` at all (#2654): an irrefutable record pattern, `let Item { sku, .. } = e`,
3217    /// desugared to a one-arm `match`. `else_span` is then empty, at the end of
3218    /// the statement.
3219    Plain,
3220}
3221
3222/// #2010: the prefix of every hidden `let … else` binding. User identifiers
3223/// start with a letter (the lexer's `Ident` rule), so no user name can collide
3224/// with it, and the emitter already treats `__`-prefixed names as its own.
3225pub const LET_ELSE_BINDER_PREFIX: &str = "__letElse";
3226
3227impl Expr {
3228    /// #2010: whether this is the `match` a `let <pattern> … else …` was
3229    /// desugared into — one whose scrutinee is the hidden binder. It is not an
3230    /// expression the author wrote, so source-level tools (refactorings,
3231    /// diagnostics) treat it as the statement it came from.
3232    pub fn is_let_else_match(&self) -> bool {
3233        matches!(
3234            &self.kind,
3235            ExprKind::Match { discriminant, .. }
3236                if matches!(&discriminant.kind, ExprKind::Ident(i) if i.name.starts_with(LET_ELSE_BINDER_PREFIX))
3237        )
3238    }
3239}
3240
3241/// #2010: whether `name` is a binding the compiler generated rather than one
3242/// the author wrote. Editor surfaces (inlay hints, locals, symbols) skip these.
3243pub fn is_generated_binding(name: &str) -> bool {
3244    name.starts_with("__")
3245}
3246
3247#[derive(Debug, Clone)]
3248pub struct SendStmt {
3249    /// The send target — a recipient call, e.g. `Logger.info(msg)`.
3250    pub value: Expr,
3251    pub span: Span,
3252    pub trivia: Trivia,
3253}
3254
3255/// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
3256/// `value` is the awaited effect, which MUST be `Effect[()]`.
3257#[derive(Debug, Clone)]
3258pub struct DoStmt {
3259    pub value: Expr,
3260    pub span: Span,
3261    pub trivia: Trivia,
3262}
3263
3264#[derive(Debug, Clone)]
3265pub struct Param {
3266    pub name: Ident,
3267    pub type_ref: TypeRef,
3268    pub span: Span,
3269}
3270
3271#[derive(Debug, Clone)]
3272pub enum TypeRef {
3273    Base(BaseType, Span),
3274    Named(Ident),
3275    /// `Result[T, E]` — the built-in generic Result type (v0.1).
3276    Result(Box<TypeRef>, Box<TypeRef>, Span),
3277    /// `Option[T]` — the built-in generic Option type (v0.2).
3278    Option(Box<TypeRef>, Span),
3279    /// `Effect[T]` — the built-in generic Effect type (v0.5).
3280    Effect(Box<TypeRef>, Span),
3281    /// `HttpResult[T]` — the built-in HTTP-result sum (v0.9).
3282    HttpResult(Box<TypeRef>, Span),
3283    /// `QueueResult` — the built-in queue verdict sum (`Ack | Retry`),
3284    /// non-generic; the required return of a queue handler (v0.44).
3285    QueueResult(Span),
3286    /// `Asset` — a static file declared by `asset name = "path"` (#1883,
3287    /// slice 1a, [DECISION H]). Non-generic, like `QueueResult`; its one
3288    /// field is `.url : String`.
3289    Asset(Span),
3290    /// `RequestHeaders` (#2341, decision A) — an HTTP handler's read-only view
3291    /// of the request's headers, the type of its `headers` parameter.
3292    /// Non-generic and opaque: `get`, `bearer`, `clientIp`. Never stored or
3293    /// sent (it has no wire form).
3294    RequestHeaders(Span),
3295    /// `RequestQuery` (#2854, decision B) — an HTTP handler's read-only view
3296    /// of the request's query string, the type of its `query` parameter when
3297    /// it wants raw access: `get`, `all`, `params`. Never stored or sent.
3298    RequestQuery(Span),
3299    /// `TokenClaims` (#2380, L5) — the verified claims of an `Oidc` actor's
3300    /// token, read through its binder (`who.claims`). Read-only, never stored
3301    /// or sent.
3302    TokenClaims(Span),
3303    /// `BoundaryRejection` (#2341, decision C) — why the router refused a
3304    /// request before any handler ran; the type of an `on rejected` handler's
3305    /// parameter. Opaque here (its variants are matched, never built). Never
3306    /// stored or sent.
3307    BoundaryRejection(Span),
3308    /// `List[T]` — the built-in generic immutable list type (v0.20b).
3309    List(Box<TypeRef>, Span),
3310    /// `Map[K, V]` — the built-in generic immutable map type (v0.20b).
3311    /// Keys are confined to `Key` types
3312    /// (`bynk.types.not_keyable`).
3313    Map(Box<TypeRef>, Box<TypeRef>, Span),
3314    /// `Set[T]` — the built-in generic immutable set type (standard-library
3315    /// K6a, #2596). Insertion-ordered like `Map`; elements are confined to the
3316    /// `Map`-key rule (`bynk.types.not_keyable`).
3317    Set(Box<TypeRef>, Span),
3318    /// `Query[T]` — the built-in lazy storage-read description (v0.91, ADR 0115).
3319    /// Nameable in a pure helper's return type; non-storable and non-boundary
3320    /// (like `Effect`/`Fn`).
3321    Query(Box<TypeRef>, Span),
3322    /// `Stream[T]` — the value-over-time primitive (v0.100, real-time track
3323    /// slice 0). A lazy, pull-shaped sequence produced over time; non-storable
3324    /// and non-boundary (like `Query`/`Effect`/`Fn`).
3325    Stream(Box<TypeRef>, Span),
3326    /// `Connection[F]` — a held WebSocket connection (v0.102, real-time track
3327    /// slice 2). `F` is the server→client frame type. A `Held` resource:
3328    /// non-serialisable, non-boundary, and governed by the linearity discipline
3329    /// (§2.9); storable only in `Cell[Option[Connection]]` / `Map[K, Connection]`.
3330    Connection(Box<TypeRef>, Span),
3331    /// `History[Agent]` — a generated, driven call-history of an agent (v0.119,
3332    /// testing track slice 7, ADR 0155). A test-only generator, legal only in
3333    /// `for all` binding position inside a `property`; it is not a value type,
3334    /// so it never resolves in a field/param/return position. The bound subject
3335    /// behaves as an ordinary `List[Step]`.
3336    History(Box<TypeRef>, Span),
3337    /// `ValidationError` — the built-in error type used by refined-type
3338    /// constructors (v0.1).
3339    ValidationError(Span),
3340    /// `JsonError` — the built-in JSON-decode error type (v0.22b). A
3341    /// uniform record (`kind`/`path`/`message`, all `String`) the codec
3342    /// maps `BoundaryError` variants and parse failures into.
3343    JsonError(Span),
3344    /// `()` — the unit type (v0.5).
3345    Unit(Span),
3346    /// `A -> B` / `(A, B) -> C` / `() -> B` — a function type (v0.20a).
3347    /// Right-associative; effectful iff the return type is `Effect[_]`
3348    /// (the structural rule). Confined to non-boundary positions
3349    /// (`bynk.types.function_at_boundary`).
3350    Fn(Vec<TypeRef>, Box<TypeRef>, Span),
3351    /// `Name[Arg, …]` — an application of a user-declared generic type
3352    /// (v0.157, ADR 0183). `name` is a user type name (never a built-in
3353    /// generic, which each have a dedicated variant above). Arity and the
3354    /// existence of the referenced type are checked in the resolver.
3355    App {
3356        name: Ident,
3357        args: Vec<TypeRef>,
3358        span: Span,
3359    },
3360}
3361
3362impl TypeRef {
3363    pub fn span(&self) -> Span {
3364        match self {
3365            TypeRef::Base(_, s) => *s,
3366            TypeRef::Named(id) => id.span,
3367            TypeRef::Result(_, _, s) => *s,
3368            TypeRef::Option(_, s) => *s,
3369            TypeRef::Effect(_, s) => *s,
3370            TypeRef::HttpResult(_, s) => *s,
3371            TypeRef::QueueResult(s) => *s,
3372            TypeRef::Asset(s) => *s,
3373            TypeRef::RequestHeaders(s) => *s,
3374            TypeRef::RequestQuery(s) => *s,
3375            TypeRef::TokenClaims(s) => *s,
3376            TypeRef::BoundaryRejection(s) => *s,
3377            TypeRef::List(_, s) => *s,
3378            TypeRef::Map(_, _, s) => *s,
3379            TypeRef::Set(_, s) => *s,
3380            TypeRef::Query(_, s) => *s,
3381            TypeRef::Stream(_, s) => *s,
3382            TypeRef::Connection(_, s) => *s,
3383            TypeRef::History(_, s) => *s,
3384            TypeRef::ValidationError(s) => *s,
3385            TypeRef::JsonError(s) => *s,
3386            TypeRef::Unit(s) => *s,
3387            TypeRef::Fn(_, _, s) => *s,
3388            TypeRef::App { span, .. } => *span,
3389        }
3390    }
3391}
3392
3393/// K-streams (#2623): where `t` reaches a `Stream`, as the chain of fields it
3394/// is reached through (`["Bundle.download", "Download.body"]`), or `None`. A
3395/// bare `Stream` (or one inside a wrapper) is the empty chain. A record or sum
3396/// may carry a stream; such a type is then non-boundary like the stream
3397/// itself, and this is the walk every boundary check uses to find it. It
3398/// follows named and applied types into their declarations (a generic
3399/// record's own fields too, so `Wrap[T] = { s: Stream[T] }` is found through
3400/// `Wrap[Int]`), but not into a function type, which is rejected at a
3401/// boundary on its own. Terminates via the `visited` set.
3402pub fn stream_path(
3403    t: &TypeRef,
3404    types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3405) -> Option<Vec<String>> {
3406    fn walk(
3407        t: &TypeRef,
3408        types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3409        visited: &mut std::collections::HashSet<String>,
3410    ) -> Option<Vec<String>> {
3411        match t {
3412            TypeRef::Stream(..) => Some(Vec::new()),
3413            TypeRef::Named(id) => named(&id.name, types, visited),
3414            TypeRef::App { name, args, .. } => named(&name.name, types, visited)
3415                .or_else(|| args.iter().find_map(|a| walk(a, types, visited))),
3416            TypeRef::Option(a, _)
3417            | TypeRef::List(a, _)
3418            | TypeRef::Set(a, _)
3419            | TypeRef::Effect(a, _)
3420            | TypeRef::HttpResult(a, _)
3421            | TypeRef::Query(a, _)
3422            | TypeRef::Connection(a, _)
3423            | TypeRef::History(a, _) => walk(a, types, visited),
3424            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
3425                walk(a, types, visited).or_else(|| walk(b, types, visited))
3426            }
3427            TypeRef::Fn(..)
3428            | TypeRef::Base(..)
3429            | TypeRef::QueueResult(_)
3430            | TypeRef::Asset(_)
3431            | TypeRef::RequestHeaders(_)
3432            | TypeRef::RequestQuery(_)
3433            | TypeRef::TokenClaims(_)
3434            | TypeRef::BoundaryRejection(_)
3435            | TypeRef::ValidationError(_)
3436            | TypeRef::JsonError(_)
3437            | TypeRef::Unit(_) => None,
3438        }
3439    }
3440    fn named(
3441        name: &str,
3442        types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3443        visited: &mut std::collections::HashSet<String>,
3444    ) -> Option<Vec<String>> {
3445        if !visited.insert(name.to_string()) {
3446            return None;
3447        }
3448        let decl = types.get(name)?;
3449        let fields: Vec<(String, &TypeRef)> = match &decl.body {
3450            TypeBody::Record(r) => r
3451                .fields
3452                .iter()
3453                .map(|f| (format!("{name}.{}", f.name.name), &f.type_ref))
3454                .collect(),
3455            TypeBody::Sum(s) => s
3456                .variants
3457                .iter()
3458                .flat_map(|v| {
3459                    v.payload.iter().map(move |p| {
3460                        (
3461                            format!("{name}.{}.{}", v.name.name, p.name.name),
3462                            &p.type_ref,
3463                        )
3464                    })
3465                })
3466                .collect(),
3467            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => Vec::new(),
3468        };
3469        fields.into_iter().find_map(|(at, t)| {
3470            walk(t, types, visited).map(|mut rest| {
3471                rest.insert(0, at);
3472                rest
3473            })
3474        })
3475    }
3476    walk(t, types, &mut std::collections::HashSet::new())
3477}
3478
3479/// K-streams (#2623): whether the named type `name` carries a `Stream` in a
3480/// field, at any depth ([`stream_path`]). Such a type has no codec.
3481pub fn type_carries_stream(
3482    name: &str,
3483    types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3484) -> bool {
3485    stream_path(
3486        &TypeRef::Named(Ident {
3487            name: name.to_string(),
3488            span: Span::default(),
3489        }),
3490        types,
3491    )
3492    .is_some()
3493}
3494
3495/// v0.174 (#592): does the generic record type `name` transitively contain a
3496/// reference to itself — through any field-type path, including collection and
3497/// `Option` wrappers, sum-variant payloads, and generic type arguments? Such a
3498/// type has no finite set of monomorphised boundary codecs: uniform recursion
3499/// (`Node[T] = { next: Option[Node[T]] }`) would need a self-referential codec
3500/// chain the per-instantiation model does not yet generate, and polymorphic
3501/// recursion (`Weird[T] = { next: Option[Weird[List[T]]] }`) an unbounded set of
3502/// instantiations. Both are rejected at a boundary
3503/// (`bynk.generics.recursive_generic_at_boundary`).
3504///
3505/// Detection is reachability over the type-containment graph: `name` is
3506/// recursive iff it is reachable from its own body, following every named /
3507/// applied head and descending into every wrapper, map/result pair, function
3508/// position, and generic argument. Terminates via the `visited` set.
3509pub fn generic_record_is_recursive(
3510    name: &str,
3511    types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3512) -> bool {
3513    fn heads(t: &TypeRef, out: &mut Vec<String>) {
3514        match t {
3515            TypeRef::Named(id) => out.push(id.name.clone()),
3516            TypeRef::App {
3517                name: app_name,
3518                args,
3519                ..
3520            } => {
3521                out.push(app_name.name.clone());
3522                for a in args {
3523                    heads(a, out);
3524                }
3525            }
3526            TypeRef::Option(a, _)
3527            | TypeRef::List(a, _)
3528            | TypeRef::Set(a, _)
3529            | TypeRef::Effect(a, _)
3530            | TypeRef::HttpResult(a, _)
3531            | TypeRef::Query(a, _)
3532            | TypeRef::Stream(a, _)
3533            | TypeRef::Connection(a, _)
3534            | TypeRef::History(a, _) => heads(a, out),
3535            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
3536                heads(a, out);
3537                heads(b, out);
3538            }
3539            TypeRef::Fn(ps, r, _) => {
3540                for p in ps {
3541                    heads(p, out);
3542                }
3543                heads(r, out);
3544            }
3545            TypeRef::Base(..)
3546            | TypeRef::QueueResult(_)
3547            | TypeRef::Asset(_)
3548            | TypeRef::RequestHeaders(_)
3549            | TypeRef::RequestQuery(_)
3550            | TypeRef::TokenClaims(_)
3551            | TypeRef::BoundaryRejection(_)
3552            | TypeRef::ValidationError(_)
3553            | TypeRef::JsonError(_)
3554            | TypeRef::Unit(_) => {}
3555        }
3556    }
3557    fn body_heads(decl: &TypeDecl, out: &mut Vec<String>) {
3558        match &decl.body {
3559            TypeBody::Record(r) => {
3560                for f in &r.fields {
3561                    heads(&f.type_ref, out);
3562                }
3563            }
3564            TypeBody::Sum(s) => {
3565                for v in &s.variants {
3566                    for p in &v.payload {
3567                        heads(&p.type_ref, out);
3568                    }
3569                }
3570            }
3571            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
3572        }
3573    }
3574    let Some(root) = types.get(name) else {
3575        return false;
3576    };
3577    let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
3578    let mut stack: Vec<String> = Vec::new();
3579    body_heads(root, &mut stack);
3580    while let Some(n) = stack.pop() {
3581        if n == name {
3582            return true;
3583        }
3584        if !visited.insert(n.clone()) {
3585            continue;
3586        }
3587        if let Some(decl) = types.get(&n) {
3588            body_heads(decl, &mut stack);
3589        }
3590    }
3591    false
3592}
3593
3594/// T3.4 (R2.4): a node's identity, independent of position — allocated once,
3595/// monotonically, per expression the parser constructs (`Parser::alloc_expr_id`
3596/// in `bynk-syntax/src/parser.rs`). Never derived from a `Span`, so two
3597/// expressions occupying the same byte range (a synthetic node, a
3598/// zero-width span) never collide the way a span-keyed side table could.
3599#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3600pub struct ExprId(pub u32);
3601
3602impl ExprId {
3603    /// Reserved for `Expr` nodes built outside the parser — after checking,
3604    /// during emission — that are never looked up in a checker-populated
3605    /// `expr_types`/`expr_ty` table (they are lowered directly, from
3606    /// already-typed sub-expressions they wrap or splice). A lookup against
3607    /// this id is a bug: the node was never checked and has no recorded
3608    /// type of its own.
3609    pub const SYNTHETIC: ExprId = ExprId(u32::MAX);
3610}
3611
3612#[derive(Debug, Clone)]
3613pub struct Expr {
3614    pub id: ExprId,
3615    pub kind: ExprKind,
3616    pub span: Span,
3617}
3618
3619/// Finding #31: `Expr` sets the size of every expression node in the
3620/// program — `ExprKind::Observation`'s payload and `ExprKind::Is`'s pattern
3621/// field are boxed specifically to keep it small (176 bytes unboxed, 128
3622/// boxed, measured on this target). Pinned so the next large variant added
3623/// to `ExprKind` is a compile error here rather than a silent regression.
3624/// T3.4: `id: ExprId` adds 4 bytes (padded); the budget is unchanged, so this
3625/// still fits.
3626/// T3.4 (R2.4): `id: ExprId` is a deliberate 8-byte increase (128 → 136,
3627/// alignment-padded from 4), not a silent regression — the ceiling moves
3628/// with it, once, here, so the next *accidental* growth still trips this
3629/// assertion rather than hiding under slack headroom.
3630/// T3.5 (R2.2): `file: FileId` on `Span` is a deliberate increase (136 → 160
3631/// — `Span` itself grows from 16 to 24 bytes with alignment padding, and
3632/// `ExprKind`'s largest variant carries more than one `Span`), not a silent
3633/// regression — the ceiling moves with it, once, here, exactly as T3.4 did
3634/// for `id: ExprId`.
3635const _: () = assert!(std::mem::size_of::<Expr>() <= 160);
3636
3637impl ExprKind {
3638    /// Construct an `IntLit` for a *synthesized* integer — one the compiler
3639    /// invents rather than reading from source (a default `1`, a computed bound).
3640    /// The lexeme is the canonical decimal form (no separators). Source-parsed
3641    /// literals keep their as-written lexeme instead (v0.142, ADR 0166).
3642    pub fn int_lit(value: i64) -> ExprKind {
3643        ExprKind::IntLit {
3644            value,
3645            lexeme: value.to_string(),
3646        }
3647    }
3648}
3649
3650#[derive(Debug, Clone)]
3651pub enum ExprKind {
3652    /// An integer literal (typed `Int`). The lexeme is kept alongside the parsed
3653    /// value (v0.142, ADR 0166) so formatting is byte-stable: an author's `_`
3654    /// digit separators (`1_048_576`) survive a round-trip, mirroring the
3655    /// `FloatLit` treatment. The value is separator-free; emission lowers the
3656    /// value, so emitted output is unaffected.
3657    IntLit {
3658        value: i64,
3659        lexeme: String,
3660    },
3661    /// A float literal (v0.21). The lexeme is kept alongside the parsed
3662    /// value so emission and formatting are byte-stable (`1e10` must not
3663    /// normalise to `10000000000`).
3664    FloatLit {
3665        value: f64,
3666        lexeme: String,
3667    },
3668    /// A decimal literal (#2622): `1.20d`, `5d`. Typed `Decimal`; the scale is
3669    /// the number of fraction digits as written, so `1.20d` keeps scale 2.
3670    DecimalLit {
3671        /// The coefficient: the digits with the `.` and `_` separators removed
3672        /// (`120` for `1.20d`), as an arbitrary-precision base-10 string.
3673        coef: String,
3674        /// The number of fraction digits as written (`2` for `1.20d`).
3675        scale: u32,
3676        /// The lexeme as written, `d` suffix included (formatting is verbatim).
3677        lexeme: String,
3678    },
3679    /// A duration literal `<int>.<unit>` (v0.86, ADR 0112): `5.minutes`,
3680    /// `30.days`. The parser recognises the `IntLit . <unit>` shape and records
3681    /// the magnitude, the unit, and the resolved milliseconds (the value the
3682    /// emitter lowers to). Typed `Duration`.
3683    DurationLit {
3684        /// The integer magnitude as written (`5` in `5.minutes`).
3685        value: i64,
3686        /// The unit name (`minutes`), one of the closed set.
3687        unit: DurationUnit,
3688        /// The value in milliseconds — `value * unit factor`.
3689        millis: i64,
3690    },
3691    StrLit(String),
3692    /// An interpolated string `"… \(expr) …"` (v0.43, ADR 0075). Chunks and
3693    /// holes alternate. A plain `"…"` with no holes stays [`ExprKind::StrLit`],
3694    /// so existing code and the emitter/formatter fast-path are untouched.
3695    InterpStr(Vec<InterpPart>),
3696    BoolLit(bool),
3697    Ident(Ident),
3698    Call {
3699        name: Ident,
3700        /// v0.20a: explicit type arguments (`name[T](…)`); empty when absent.
3701        type_args: Vec<TypeRef>,
3702        args: Vec<Expr>,
3703    },
3704    /// A lambda (v0.20a). See [`LambdaExpr`].
3705    Lambda(LambdaExpr),
3706    BinOp(BinOp, Box<Expr>, Box<Expr>),
3707    UnaryOp(UnaryOp, Box<Expr>),
3708    Paren(Box<Expr>),
3709    /// `{ stmts; expr }` — block expression (v0.1).
3710    Block(Block),
3711    /// `if cond { then } else { else }` (v0.1).
3712    If {
3713        cond: Box<Expr>,
3714        then_block: Box<Block>,
3715        else_block: Box<Block>,
3716    },
3717    /// `Ok(value)` — Result success constructor (v0.1).
3718    Ok(Box<Expr>),
3719    /// `Err(error)` — Result failure constructor (v0.1).
3720    Err(Box<Expr>),
3721    /// `expr?` — propagation operator (v0.1).
3722    Question(Box<Expr>),
3723    /// `TypeName { field: value, ... }` — record construction (v0.2).
3724    RecordConstruction {
3725        type_name: Ident,
3726        fields: Vec<FieldInit>,
3727    },
3728    /// `receiver.field` — field access on a record value (v0.2). v0.3 adds
3729    /// `.raw` on opaque types within the defining commons.
3730    FieldAccess {
3731        receiver: Box<Expr>,
3732        field: Ident,
3733    },
3734    /// `receiver.method(args)` — instance method call (v0.2). The
3735    /// resolver determines the receiver's type and looks up the method.
3736    MethodCall {
3737        receiver: Box<Expr>,
3738        method: Ident,
3739        /// v0.22b: explicit type arguments on a qualified static
3740        /// (`Json.decode[T](…)`); empty when absent. The same-line-`[`
3741        /// rule applies as for `Call` type application (0039).
3742        type_args: Vec<TypeRef>,
3743        args: Vec<Expr>,
3744    },
3745    /// `match disc { arm+ }` — pattern matching (v0.2).
3746    Match {
3747        discriminant: Box<Expr>,
3748        arms: Vec<MatchArm>,
3749    },
3750    /// `expr is pattern` — pattern test, returns Bool (v0.2).
3751    ///
3752    /// `pattern` is boxed (finding #31): `Pattern`'s `Variant` case carries two
3753    /// `Ident`s plus a `Vec`, inlining it into every `ExprKind` sets the size
3754    /// of every expression node in the program for the one variant that
3755    /// tests a pattern.
3756    Is {
3757        value: Box<Expr>,
3758        pattern: Box<Pattern>,
3759    },
3760    /// `Some(value)` — Option Some constructor (v0.2).
3761    Some(Box<Expr>),
3762    /// `None` — Option None constructor (v0.2).
3763    None,
3764    /// `()` — unit literal (v0.5).
3765    UnitLit,
3766    /// `TypeName { ...base, field: value, ... }` or `{ ...base, ... }` —
3767    /// record spread expression (v0.5).
3768    RecordSpread {
3769        /// Optional type prefix (`TypeName { ...base }`). Absent for the
3770        /// bare form used inside `commit`.
3771        type_name: Option<Ident>,
3772        /// The base record being spread.
3773        base: Box<Expr>,
3774        /// Field overrides (always full `name: value` form — never shorthand).
3775        overrides: Vec<FieldInit>,
3776    },
3777    /// `Effect.pure(value)` — wrap a synchronous value into `Effect[T]`
3778    /// (v0.5). Recognised in the parser as a special-form.
3779    EffectPure(Box<Expr>),
3780    /// `expect expr` — expectation as an expression of type `()` (v0.9.1;
3781    /// renamed from `assert` in v0.112). Valid only inside test bodies. Evaluates
3782    /// `expr` (must be Bool); if false, the surrounding test case fails.
3783    Expect(Box<Expr>),
3784    /// `Val[T]`, `Val[T](args)` — test-context value construction (v0.9.4).
3785    /// `args` is empty for the bare form and holds the pin arguments for
3786    /// `Val[T](...)`. The record-override form `Val[T] { ... }` is not yet
3787    /// parsed. Valid only inside test bodies; has type `T`.
3788    Val {
3789        type_ref: TypeRef,
3790        args: Vec<Expr>,
3791    },
3792    /// `Wire(<String>)` — a raw, pre-validation argument to a `system`-tier
3793    /// service address (testing-the-boundary Slice C). The inner expression is a
3794    /// `String` carrying the wire form the boundary will receive *unvalidated* —
3795    /// a body's JSON text or a path segment — so a case can drive the router with
3796    /// input the type system forbids and observe the rejection. Legal only at
3797    /// `system` (there is no wire at `unit`); the router validates it, so no
3798    /// refined value is ever minted from a `Wire` (ADR 0182 untouched).
3799    Wire(Box<Expr>),
3800    /// `[a, b, c]` — list literal (v0.20b). An empty `[]` requires an
3801    /// expected type (`bynk.types.uninferable_element_type`).
3802    ListLit(Vec<Expr>),
3803    /// An observation over a consumed capability's recorded calls (v0.117,
3804    /// testing track slice 5). The direct subject of an `expect` in a `case`
3805    /// body — `expect Cap.op called once with <pred>`, `expect Cap.op never
3806    /// called`, `expect A.op before B.op`. Types as `Bool` (the claim about the
3807    /// recorded trace), lowered to a boolean over the recorded log.
3808    /// Boxed (finding #31): at ~160 bytes, `ObservationExpr` inlined here set
3809    /// the size of every `ExprKind` for the one variant that records a
3810    /// capability-call observation.
3811    Observation(Box<ObservationExpr>),
3812    /// `<call> faults` — the claim that an effectful call **faults** (#1706).
3813    /// The direct subject of an `expect` in a `case` body —
3814    /// `expect quote.call("GBP") faults`. The claim's `call` is the call
3815    /// itself, an `Effect[_]` the claim awaits; the claim types as `Bool` and
3816    /// holds when awaiting the call throws (a capability fault, an injected
3817    /// `stub … fails`, an invariant violation) rather than returning a value.
3818    /// A fault is untyped and uncatchable by the caller, so this is a test's
3819    /// observation of the fault, not a handler for it: no production code can
3820    /// write it.
3821    ///
3822    /// #1812: the claim may carry a call-site principal (`expect
3823    /// api.POST(…) by User("bob") faults`), so an identity-carrying handler's
3824    /// fault path is testable; see [`FaultsExpr`]. Boxed, as `Observation` is,
3825    /// so the principal does not widen every `ExprKind`.
3826    Faults(Box<FaultsExpr>),
3827    /// `trace(Cap.op)` — the bound-trace escape hatch (v0.117, testing track
3828    /// slice 5). Yields the recorded calls of `Cap.op` as a `List[<CallRecord>]`
3829    /// (a synthetic record of the operation's parameters), asserted over with the
3830    /// ordinary value surface. Test-body-only, like [`ExprKind::Val`].
3831    Trace {
3832        cap: Ident,
3833        op: Ident,
3834    },
3835}
3836
3837/// Every directly-nested sub-expression of `e` — the **total** child
3838/// iterator. The match is exhaustive (no `_` arm), so adding an [`ExprKind`]
3839/// variant is a compile error here rather than a silently incomplete walk —
3840/// the trap the checker's three hand-rolled partial walkers each fell into
3841/// (block statements and match-arm bodies were skipped, so e.g. the `:=`
3842/// self-reference rule was bypassable through a match arm).
3843///
3844/// Exhaustive over [`ExprKind`] *and* over the expressions each variant holds
3845/// (#1760: match-arm guards were once missed, and every walk built on this one
3846/// missed them with it).
3847///
3848/// Descends one level: block *statements* and the tail, match-arm guards and
3849/// bodies, lambda bodies, interpolation holes, record-field values, and
3850/// observation predicates are all children. Callers recurse for a deep walk.
3851pub fn expr_children(e: &Expr) -> Vec<&Expr> {
3852    fn block_children<'a>(b: &'a Block, out: &mut Vec<&'a Expr>) {
3853        for s in &b.statements {
3854            statement_exprs(s, out);
3855        }
3856        out.push(&b.tail);
3857    }
3858    let mut out = Vec::new();
3859    match &e.kind {
3860        ExprKind::IntLit { .. }
3861        | ExprKind::FloatLit { .. }
3862        | ExprKind::DecimalLit { .. }
3863        | ExprKind::DurationLit { .. }
3864        | ExprKind::StrLit(_)
3865        | ExprKind::BoolLit(_)
3866        | ExprKind::Ident(_)
3867        | ExprKind::None
3868        | ExprKind::UnitLit
3869        | ExprKind::Trace { .. } => {}
3870        ExprKind::InterpStr(parts) => {
3871            for p in parts {
3872                if let InterpPart::Hole(h) = p {
3873                    out.push(h.as_ref());
3874                }
3875            }
3876        }
3877        ExprKind::Call { args, .. } | ExprKind::Val { args, .. } | ExprKind::ListLit(args) => {
3878            out.extend(args.iter())
3879        }
3880        ExprKind::Wire(inner) => out.push(inner.as_ref()),
3881        ExprKind::Lambda(l) => out.push(l.body.as_ref()),
3882        ExprKind::BinOp(_, l, r) => {
3883            out.push(l.as_ref());
3884            out.push(r.as_ref());
3885        }
3886        ExprKind::UnaryOp(_, inner)
3887        | ExprKind::Paren(inner)
3888        | ExprKind::Ok(inner)
3889        | ExprKind::Err(inner)
3890        | ExprKind::Question(inner)
3891        | ExprKind::Some(inner)
3892        | ExprKind::EffectPure(inner)
3893        | ExprKind::Expect(inner) => out.push(inner.as_ref()),
3894        ExprKind::Faults(f) => {
3895            // #1812: a call-site principal's identity is evaluated first, as
3896            // an argument to the claimed call — the same order as an
3897            // effect-let's in `statement_exprs`.
3898            if let Some(identity) = f.principal.as_ref().and_then(|p| p.identity.as_deref()) {
3899                out.push(identity);
3900            }
3901            if let Some(claims) = f.principal.as_ref().and_then(|p| p.claims.as_deref()) {
3902                out.push(claims);
3903            }
3904            out.push(&f.call);
3905        }
3906        ExprKind::Block(b) => block_children(b, &mut out),
3907        ExprKind::If {
3908            cond,
3909            then_block,
3910            else_block,
3911        } => {
3912            out.push(cond.as_ref());
3913            block_children(then_block, &mut out);
3914            block_children(else_block, &mut out);
3915        }
3916        ExprKind::RecordConstruction { fields, .. } => {
3917            out.extend(fields.iter().filter_map(|f| f.value.as_ref()));
3918        }
3919        ExprKind::FieldAccess { receiver, .. } => out.push(receiver.as_ref()),
3920        ExprKind::MethodCall { receiver, args, .. } => {
3921            out.push(receiver.as_ref());
3922            out.extend(args.iter());
3923        }
3924        ExprKind::Match { discriminant, arms } => {
3925            out.push(discriminant.as_ref());
3926            for arm in arms {
3927                // #1760: the guard, in evaluation order before the body. It is
3928                // an ordinary expression, checked like any other.
3929                if let Some(guard) = &arm.guard {
3930                    out.push(guard);
3931                }
3932                match &arm.body {
3933                    MatchBody::Expr(e) => out.push(e),
3934                    MatchBody::Block(b) => block_children(b, &mut out),
3935                }
3936            }
3937        }
3938        ExprKind::Is { value, .. } => out.push(value.as_ref()),
3939        ExprKind::RecordSpread {
3940            base, overrides, ..
3941        } => {
3942            out.push(base.as_ref());
3943            out.extend(overrides.iter().filter_map(|f| f.value.as_ref()));
3944        }
3945        ExprKind::Observation(obs) => match &obs.matcher {
3946            ObservationMatcher::Called { count, with_pred } => {
3947                if let Some(c) = count {
3948                    out.push(c.as_ref());
3949                }
3950                if let Some(p) = with_pred {
3951                    out.push(p.as_ref());
3952                }
3953            }
3954            ObservationMatcher::NeverCalled | ObservationMatcher::Before { .. } => {}
3955        },
3956    }
3957    out
3958}
3959
3960/// The expressions directly contained in a statement — the statement half of
3961/// [`expr_children`]'s total walk. Exhaustive over [`Statement`] for the same
3962/// reason, and over each statement's expressions: a `let`'s call-site
3963/// principal identity included.
3964pub fn statement_exprs<'a>(s: &'a Statement, out: &mut Vec<&'a Expr>) {
3965    match s {
3966        Statement::Let(l) | Statement::EffectLet(l) => {
3967            // #1766 review: a call-site principal's identity (`by User(who)`)
3968            // is a full expression. It is evaluated first, as an argument to
3969            // the addressed call.
3970            if let Some(identity) = l.principal.as_ref().and_then(|p| p.identity.as_deref()) {
3971                out.push(identity);
3972            }
3973            if let Some(claims) = l.principal.as_ref().and_then(|p| p.claims.as_deref()) {
3974                out.push(claims);
3975            }
3976            out.push(&l.value)
3977        }
3978        Statement::Expect(a) => out.push(&a.value),
3979        Statement::Send(snd) => out.push(&snd.value),
3980        Statement::Do(d) => out.push(&d.value),
3981        Statement::Assign(a) => out.push(&a.value),
3982        Statement::Advance(a) => out.push(&a.amount),
3983        Statement::Par(p) => p.statements.iter().for_each(|s| statement_exprs(s, out)),
3984    }
3985}
3986
3987/// A fault claim, `<call> [by <Actor>(<identity>)] faults` (#1706, #1812).
3988/// `call` is the effectful call the claim awaits; `principal` is the optional
3989/// call-site `by` clause, read exactly as on an effect-let (`let r <- call by
3990/// User("bob")`): it supplies the identity the addressed handler reads.
3991#[derive(Debug, Clone)]
3992pub struct FaultsExpr {
3993    pub call: Expr,
3994    pub principal: Option<CallSiteActor>,
3995}
3996
3997/// An observation of a capability operation's recorded calls (v0.117, testing
3998/// track slice 5). `cap`/`op` name the seam (`Logger.log`); `matcher` is the
3999/// claim about the recorded calls.
4000#[derive(Debug, Clone)]
4001pub struct ObservationExpr {
4002    pub cap: Ident,
4003    pub op: Ident,
4004    pub matcher: ObservationMatcher,
4005}
4006
4007/// The claim an [`ObservationExpr`] makes about a seam's recorded calls (v0.117).
4008#[derive(Debug, Clone)]
4009pub enum ObservationMatcher {
4010    /// `called` [`once` | `<n> times`]? [`with` `<pred>`]?. `count` is `None`
4011    /// for a bare `called` (at least one); `Some(expr)` is the exact-count claim
4012    /// (a literal; `once` desugars to `1`). `with_pred` matches a call whose
4013    /// arguments (in scope by the operation's parameter names) satisfy it.
4014    Called {
4015        count: Option<Box<Expr>>,
4016        with_pred: Option<Box<Expr>>,
4017    },
4018    /// `never called` — zero calls.
4019    NeverCalled,
4020    /// `before Cap.op` — the first call of the subject precedes the first call
4021    /// of the named operation (both must have occurred).
4022    Before { cap: Ident, op: Ident },
4023}
4024
4025/// One part of an interpolated string (v0.43, ADR 0075). An
4026/// [`ExprKind::InterpStr`] holds an alternating run of these.
4027#[derive(Debug, Clone)]
4028pub enum InterpPart {
4029    /// Literal text between holes, with escapes already resolved.
4030    Chunk(String),
4031    /// An interpolated expression `\(expr)`. Type-checked by the hole rule
4032    /// (base scalars only; see the checker) and lowered into a template-
4033    /// literal `${…}` slot.
4034    Hole(Box<Expr>),
4035}
4036
4037/// One field-initialiser inside a record construction expression:
4038/// either `name: expr` or the shorthand `name` (which requires a binding
4039/// of the same name in scope and uses its value).
4040#[derive(Debug, Clone)]
4041pub struct FieldInit {
4042    pub name: Ident,
4043    /// `None` means shorthand — the field's value is the same-named binding.
4044    pub value: Option<Expr>,
4045    pub span: Span,
4046}
4047
4048/// One arm of a `match` expression: `pattern => body` or, with a guard,
4049/// `pattern if guard => body` (guard added in the nested-patterns increment,
4050/// ADR 0169). A guarded arm matches only when the pattern matches **and** the
4051/// `Bool` guard evaluates true; it never contributes to exhaustiveness.
4052#[derive(Debug, Clone)]
4053pub struct MatchArm {
4054    pub pattern: Pattern,
4055    /// Optional `if <Bool-expr>` guard between the pattern and `=>`.
4056    pub guard: Option<Expr>,
4057    pub body: MatchBody,
4058    pub span: Span,
4059}
4060
4061/// The right-hand side of a match arm — either a single expression or
4062/// a block.
4063#[derive(Debug, Clone)]
4064pub enum MatchBody {
4065    Expr(Expr),
4066    Block(Block),
4067}
4068
4069impl MatchBody {
4070    pub fn span(&self) -> Span {
4071        match self {
4072            MatchBody::Expr(e) => e.span,
4073            MatchBody::Block(b) => b.span,
4074        }
4075    }
4076}
4077
4078/// A pattern (v0.2 §3.8). Patterns appear in `match` arms and as the
4079/// right-hand side of the `is` operator.
4080#[derive(Debug, Clone)]
4081pub enum Pattern {
4082    /// `_` — matches any value, no bindings.
4083    Wildcard(Span),
4084    /// A lowercase identifier — binds the whole value to `name` and matches
4085    /// anything (ADR 0169). At the top of a `match` arm it binds the scrutinee
4086    /// (`n if n > 0 => …`); inside a payload position it binds the field
4087    /// (`Some(user)`). The uppercase-led counterpart is a nullary [`Pattern::Variant`].
4088    Binding(Ident),
4089    /// A literal pattern — `31`, `"english"`, `true` (v0.130 §2.3.4). Matches a
4090    /// primitive scrutinee (`Int`/`String`/`Bool`) by value equality. The
4091    /// admitted set mirrors ADR 0001's closed literal set (integers — including
4092    /// a leading unary minus — strings, and booleans); `Float`/`()` are not
4093    /// admitted as patterns.
4094    Literal {
4095        value: LiteralValue,
4096        /// The literal as written (`-1_000`), digit separators kept; `None`
4097        /// for a pattern built without source. Only the formatter reads it,
4098        /// and it sits outside `value` so the duplicate-arm check ignores it.
4099        text: Option<String>,
4100        span: Span,
4101    },
4102    /// `lo..hi`, `lo..` or `..hi` — an inclusive integer range (patterns track,
4103    /// slice 5, #2699). Matches an `Int` scrutinee by comparison. Binds nothing.
4104    /// The one production `via schema(...)` shares ([`SchemaVersionPattern::Range`]).
4105    Range(IntRange),
4106    /// `Variant` or `Variant(bindings)` or `TypeName.Variant(bindings)`. Each
4107    /// payload binding is itself a [`Pattern`] (ADR 0169), so payloads nest:
4108    /// `Some(Ok(x))`, `Err(PollClosed)`.
4109    Variant {
4110        /// Optional qualifier: `TypeName.Variant`.
4111        type_name: Option<Ident>,
4112        /// The variant name.
4113        variant: Ident,
4114        /// Payload bindings (empty for nullary variants).
4115        bindings: Vec<PatternBinding>,
4116        span: Span,
4117    },
4118    /// `TypeName { f1, f2: pattern, .. }` — a record pattern (patterns track,
4119    /// slice 1, #2641). Always open: unlisted fields are ignored, and the
4120    /// trailing `..` is mandatory so that adding a field to the record never
4121    /// breaks a pattern. A field with no sub-pattern (`f`) binds the field to a
4122    /// local of the same name; `f: p` matches the field against `p`.
4123    Record {
4124        /// The record type's name — required, and never carries type
4125        /// arguments (a generic record's come from the scrutinee).
4126        type_name: Ident,
4127        /// The listed fields, in source order. `None` is the shorthand `f`.
4128        fields: Vec<(Ident, Option<Pattern>)>,
4129        /// The span of the mandatory trailing `..`.
4130        rest_span: Span,
4131        span: Span,
4132    },
4133    /// `[p₁, …, pₙ]` or `[p₁, …, pₙ, ..]` / `[p₁, …, pₙ, ..rest]` — a list
4134    /// pattern (patterns track, slice 4, #2680). Without `rest` it matches a
4135    /// list of exactly `n` elements, element `i` against `pᵢ`; with `rest` it
4136    /// matches `n` or more, and a named rest binds the remaining elements.
4137    List {
4138        elems: Vec<Pattern>,
4139        rest: Option<ListRest>,
4140        span: Span,
4141    },
4142    /// `p 'where' refinement-predicate` — a refinement guard on a pattern
4143    /// (#472). Matches when `inner` matches *and* the scrutinee satisfies
4144    /// `predicate` at runtime. v1 admits only `Wildcard` as `inner` (no
4145    /// binding form yet); refutable — never counts toward exhaustiveness or
4146    /// as a catch-all arm, the same treatment as an `if` guard (§2.3.4).
4147    Refined {
4148        inner: Box<Pattern>,
4149        predicate: Refinement,
4150        span: Span,
4151    },
4152    /// `p₁ | p₂ | … | pₙ` — an or-pattern (#474 §2.3.4): matches if any
4153    /// alternative matches. Left-associative `|`, flattened by the parser's
4154    /// chain fold into one `Vec` — an alternative is always a leaf
4155    /// (`Wildcard`/`Binding`/`Literal`/`Variant`), never itself an `Or`
4156    /// (there is no parenthesized-pattern syntax to nest one inside another).
4157    /// Well-typedness (checked, not parsed): every alternative binds the same
4158    /// set of names, a name shared across alternatives has the same type
4159    /// (including refinement) in each, and every alternative matches the same
4160    /// value type.
4161    Or(Vec<Pattern>, Span),
4162}
4163
4164/// An inclusive integer range, `lo..hi`, `lo..` or `..hi` (patterns track,
4165/// slice 5, #2699; events slice 4b, #990): the one production a match pattern
4166/// and a `via schema(...)` clause share. At least one bound is present — a bare
4167/// `..` is the rest marker, never a range. Each bound is an optionally negative
4168/// `Int` literal, parsed permissively: whether it is in range and whether
4169/// `lo <= hi` are checker questions.
4170#[derive(Debug, Clone, PartialEq, Eq)]
4171pub struct IntRange {
4172    pub lo: Option<i64>,
4173    pub hi: Option<i64>,
4174    /// The low bound as written (`-1_000`), digit separators kept; `None` when
4175    /// `lo` is. Only the formatter reads it; a range built without source
4176    /// (a suggested fix) leaves it `None` and prints from the value.
4177    pub lo_text: Option<String>,
4178    /// The high bound as written; see `lo_text`.
4179    pub hi_text: Option<String>,
4180    pub span: Span,
4181}
4182
4183impl IntRange {
4184    /// As [`IntRange::describe`], but each bound keeps its source spelling
4185    /// (`1_000..2_000`), so formatting does not drop digit separators.
4186    pub fn describe_source(&self) -> String {
4187        let lo = match (&self.lo_text, self.lo) {
4188            (Some(t), _) => t.clone(),
4189            (None, Some(n)) => n.to_string(),
4190            (None, None) => String::new(),
4191        };
4192        let hi = match (&self.hi_text, self.hi) {
4193            (Some(t), _) => t.clone(),
4194            (None, Some(n)) => n.to_string(),
4195            (None, None) => String::new(),
4196        };
4197        format!("{lo}..{hi}")
4198    }
4199
4200    /// The range as written, `0..9`, `18..` or `..-1`; the form the formatter
4201    /// prints and diagnostics quote.
4202    pub fn describe(&self) -> String {
4203        let lo = self.lo.map(|n| n.to_string()).unwrap_or_default();
4204        let hi = self.hi.map(|n| n.to_string()).unwrap_or_default();
4205        format!("{lo}..{hi}")
4206    }
4207}
4208
4209/// The trailing `..` of a list pattern, with the optional name that binds the
4210/// remaining elements (`..rest`).
4211#[derive(Debug, Clone)]
4212pub struct ListRest {
4213    pub binder: Option<Ident>,
4214    /// The span of the `..` token.
4215    pub span: Span,
4216}
4217
4218/// The value carried by a [`Pattern::Literal`]. A closed set (ADR 0001):
4219/// integer, string, and boolean. Kept distinct from [`ExprKind`] so patterns
4220/// carry only what they can actually match, and so it is `Eq`/`Hash` for the
4221/// duplicate-arm check.
4222#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4223pub enum LiteralValue {
4224    Int(i64),
4225    Str(String),
4226    Bool(bool),
4227}
4228
4229impl LiteralValue {
4230    /// A human-readable rendering for diagnostics (`31`, `"english"`, `true`).
4231    pub fn describe(&self) -> String {
4232        match self {
4233            LiteralValue::Int(n) => n.to_string(),
4234            LiteralValue::Str(s) => format!("{s:?}"),
4235            LiteralValue::Bool(b) => b.to_string(),
4236        }
4237    }
4238}
4239
4240impl Pattern {
4241    pub fn span(&self) -> Span {
4242        match self {
4243            Pattern::Wildcard(s) => *s,
4244            Pattern::Binding(id) => id.span,
4245            Pattern::Literal { span, .. } => *span,
4246            Pattern::Range(r) => r.span,
4247            Pattern::Variant { span, .. } => *span,
4248            Pattern::Record { span, .. } => *span,
4249            Pattern::List { span, .. } => *span,
4250            Pattern::Refined { span, .. } => *span,
4251            Pattern::Or(_, span) => *span,
4252        }
4253    }
4254
4255    /// Every identifier this pattern binds into scope, recursively (`_` and
4256    /// nullary variants bind nothing). Used by the resolver and the checker to
4257    /// populate an arm's scope, and by the guard to see the arm's bindings.
4258    ///
4259    /// For [`Pattern::Or`] this returns the *first* alternative's names — the
4260    /// checker separately verifies (#474 Rule 1) that every alternative binds
4261    /// the same set, so this is a defensive default when that rule is
4262    /// violated, not a semantic choice among alternatives.
4263    pub fn bound_names(&self) -> Vec<&Ident> {
4264        match self {
4265            Pattern::Wildcard(_) | Pattern::Literal { .. } | Pattern::Range(_) => Vec::new(),
4266            Pattern::Binding(id) => vec![id],
4267            Pattern::Variant { bindings, .. } => bindings
4268                .iter()
4269                .flat_map(|b| b.pattern().bound_names())
4270                .collect(),
4271            Pattern::Record { fields, .. } => fields
4272                .iter()
4273                .flat_map(|(name, sub)| match sub {
4274                    Some(p) => p.bound_names(),
4275                    None => vec![name],
4276                })
4277                .collect(),
4278            Pattern::List { elems, rest, .. } => elems
4279                .iter()
4280                .flat_map(Pattern::bound_names)
4281                .chain(rest.iter().filter_map(|r| r.binder.as_ref()))
4282                .collect(),
4283            Pattern::Refined { inner, .. } => inner.bound_names(),
4284            Pattern::Or(alts, _) => alts.first().map(Pattern::bound_names).unwrap_or_default(),
4285        }
4286    }
4287
4288    /// True when this pattern matches every value and binds nothing — a bare
4289    /// `_`. A [`Pattern::Binding`] also matches everything but *does* bind, so it
4290    /// is not a pure wildcard.
4291    pub fn is_wildcard(&self) -> bool {
4292        matches!(self, Pattern::Wildcard(_))
4293    }
4294
4295    /// True when this pattern matches every value (a `_` or a name binding),
4296    /// i.e. it is irrefutable and covers the position for exhaustiveness. An
4297    /// [`Pattern::Or`] is irrefutable when any alternative is — `_` in any
4298    /// position already makes the whole pattern match everything.
4299    pub fn is_irrefutable(&self) -> bool {
4300        match self {
4301            Pattern::Wildcard(_) | Pattern::Binding(_) => true,
4302            Pattern::Or(alts, _) => alts.iter().any(Pattern::is_irrefutable),
4303            Pattern::Record { fields, .. } => fields
4304                .iter()
4305                .all(|(_, sub)| sub.as_ref().is_none_or(Pattern::is_irrefutable)),
4306            // `[..]` and `[..rest]` match every list; any fixed length does not.
4307            Pattern::List { elems, rest, .. } => elems.is_empty() && rest.is_some(),
4308            _ => false,
4309        }
4310    }
4311}
4312
4313/// A single binding inside a variant pattern. Two surface forms:
4314/// `pattern` (positional — match the i-th payload field) and
4315/// `fieldName: pattern` (named — match the named payload field). The matched
4316/// sub-`pattern` is a full [`Pattern`] (ADR 0169), so a plain `name` is a
4317/// [`Pattern::Binding`], `_` a [`Pattern::Wildcard`], and `Ok(x)` a nested
4318/// [`Pattern::Variant`].
4319#[derive(Debug, Clone)]
4320pub struct PatternBinding {
4321    /// Source form: positional or named.
4322    pub kind: PatternBindingKind,
4323    pub span: Span,
4324}
4325
4326#[derive(Debug, Clone)]
4327pub enum PatternBindingKind {
4328    /// `pattern` (e.g. `x`, `_`, `Ok(v)`): match the payload field at this position.
4329    Positional { pattern: Pattern },
4330    /// `field: pattern`: match the named payload field against `pattern`.
4331    Named { field: Ident, pattern: Pattern },
4332}
4333
4334impl PatternBinding {
4335    /// The sub-pattern this binding matches its payload field against.
4336    pub fn pattern(&self) -> &Pattern {
4337        match &self.kind {
4338            PatternBindingKind::Positional { pattern } => pattern,
4339            PatternBindingKind::Named { pattern, .. } => pattern,
4340        }
4341    }
4342
4343    /// True when this binding discards its field (`_` or `field: _`) — a pure
4344    /// wildcard sub-pattern that binds nothing.
4345    pub fn is_wildcard(&self) -> bool {
4346        self.pattern().is_wildcard()
4347    }
4348}
4349
4350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4351pub enum BinOp {
4352    /// `P implies Q` — logical implication (v0.80). Desugars to `!P || Q`; sits
4353    /// at the lowest precedence (below `||`). Reads directionally (P → Q).
4354    Implies,
4355    Or,
4356    And,
4357    Eq,
4358    NotEq,
4359    Lt,
4360    LtEq,
4361    Gt,
4362    GtEq,
4363    Add,
4364    Sub,
4365    Mul,
4366    Div,
4367}
4368
4369impl BinOp {
4370    pub fn name(self) -> &'static str {
4371        match self {
4372            BinOp::Implies => "implies",
4373            BinOp::Or => "||",
4374            BinOp::And => "&&",
4375            BinOp::Eq => "==",
4376            BinOp::NotEq => "!=",
4377            BinOp::Lt => "<",
4378            BinOp::LtEq => "<=",
4379            BinOp::Gt => ">",
4380            BinOp::GtEq => ">=",
4381            BinOp::Add => "+",
4382            BinOp::Sub => "-",
4383            BinOp::Mul => "*",
4384            BinOp::Div => "/",
4385        }
4386    }
4387}
4388
4389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4390pub enum UnaryOp {
4391    Neg,
4392    Not,
4393}
4394
4395impl UnaryOp {
4396    pub fn name(self) -> &'static str {
4397        match self {
4398            UnaryOp::Neg => "-",
4399            UnaryOp::Not => "!",
4400        }
4401    }
4402}
4403
4404#[cfg(test)]
4405mod size_tests {
4406    use super::*;
4407
4408    /// Finding #31: boxing `ExprKind::Observation`'s payload and
4409    /// `ExprKind::Is`'s pattern field took `Expr` from 176 to 128 bytes on
4410    /// this target (the module-level `const _` assertion is the real pin;
4411    /// this test just makes the before/after concrete and fails loudly if a
4412    /// future change silently regresses the win rather than tripping the
4413    /// `<= 128` ceiling by enough to notice).
4414    #[test]
4415    fn expr_is_smaller_than_before_the_boxing() {
4416        assert!(
4417            std::mem::size_of::<Expr>() < 176,
4418            "Expr should be smaller than its pre-#31 size of 176 bytes"
4419        );
4420    }
4421}
4422
4423#[cfg(test)]
4424mod expr_children_tests {
4425    use super::*;
4426
4427    /// #1760: a match arm's guard is a child, between the discriminant and
4428    /// the arm's body, in evaluation order.
4429    #[test]
4430    fn a_match_arms_guard_is_a_child_in_evaluation_order() {
4431        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";
4432        let tokens = crate::lexer::tokenize(source).unwrap();
4433        let units = crate::parser::parse_units(&tokens, source).unwrap();
4434        let SourceUnit::Commons(c) = &units[0] else {
4435            panic!("a commons");
4436        };
4437        let CommonsItem::Fn(f) = &c.items[0] else {
4438            panic!("a fn");
4439        };
4440        let ExprKind::Match { .. } = &f.body.tail.kind else {
4441            panic!("a match tail");
4442        };
4443        let children: Vec<&str> = expr_children(&f.body.tail)
4444            .into_iter()
4445            .map(|e| &source[e.span.start..e.span.end])
4446            .collect();
4447        assert_eq!(children, ["n", "k > lim", "1", "0"]);
4448    }
4449
4450    /// #1766 review: a `let`'s call-site principal identity is a statement
4451    /// expression, before the value it addresses.
4452    #[test]
4453    fn a_principal_identity_is_a_statement_expression() {
4454        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";
4455        let tokens = crate::lexer::tokenize(source).unwrap();
4456        let units = crate::parser::parse_units(&tokens, source).unwrap();
4457        let SourceUnit::Suite(t) = &units[0] else {
4458            panic!("a suite");
4459        };
4460        let mut exprs = Vec::new();
4461        statement_exprs(&t.cases[0].body.statements[1], &mut exprs);
4462        let texts: Vec<&str> = exprs
4463            .into_iter()
4464            .map(|e| &source[e.span.start..e.span.end])
4465            .collect();
4466        assert_eq!(texts, ["who", "api.get()"]);
4467    }
4468}
4469
4470/// #1832: [`expr_children`] and [`statement_exprs`] reach every child slot of
4471/// every expression, so every walk built on them does too.
4472///
4473/// The guard is in two parts:
4474///
4475/// - **An oracle that cannot fall behind the AST.** [`oracle_children`] and
4476///   [`oracle_statement`] destructure every field of every `ExprKind` variant
4477///   and of every expression-bearing struct with no `..`, so adding a variant
4478///   or a field anywhere an expression can hide is a compile error here until
4479///   the oracle names the new slot.
4480/// - **A program that uses every slot.** [`COVERAGE`] is parsed (never
4481///   checked, so it only has to be syntactically valid), and two tests run
4482///   over it: every slot the oracle knows is used at least once, and at every
4483///   node `expr_children`/`statement_exprs` return exactly the oracle's
4484///   children. A new slot therefore fails the first test until the program
4485///   uses it, and the second until `expr_children` walks it.
4486///
4487/// The hand-rolled walks that cannot be built on `expr_children` (they carry
4488/// scopes, ownership or position) are tested where they live (#1832's
4489/// triage: the resolver, structure, linearity, effects and emit walks).
4490#[cfg(test)]
4491mod slot_coverage_tests {
4492    use super::*;
4493    use std::collections::BTreeSet;
4494
4495    /// Every slot, used at least once. `p` is a commons for the production
4496    /// forms; the suite holds the test-only ones (`Val`, `Wire`,
4497    /// observations, `faults`, `trace`, call-site principals).
4498    const COVERAGE: &str = r#"commons p
4499
4500fn f(x: Int) -> Int {
4501  let a = "s \(x) t"
4502  let b = g(x)
4503  let c = (y) => y
4504  let d = x + 1
4505  let e = -x
4506  let h = (x)
4507  let i = (z) => {
4508    let q = z
4509    q
4510  }
4511  let j = if x > 0 {
4512    let q = 1
4513    q
4514  } else {
4515    let w = 2
4516    w
4517  }
4518  let k = Ok(x)
4519  let l = Err(x)
4520  let n = r?
4521  let o = Some(x)
4522  let u = R { a: x }
4523  let v = r.a
4524  let w = r.m(x)
4525  let y = match x {
4526    z if z > 0 => z
4527    0 => expect x
4528    _ => {
4529      let s = 1
4530      s
4531    }
4532  }
4533  let aa = x is Some(v)
4534  let bb = R { ...r, a: x }
4535  let cc = Effect.pure(x)
4536  let dd = [x]
4537  ~> L.log(x)
4538  do s.put(x)
4539  cell := x
4540  x
4541}
4542"#;
4543
4544    const COVERAGE_SUITE: &str = r#"suite p {
4545  case "c" {
4546    let who = "alice"
4547    let a <- api.get(x) by User(who)
4548    let b = Val[T](x)
4549    let c = Wire(x)
4550    expect L.log called 2 times with msg == x
4551    expect box.call() faults
4552    expect api.post(x) by User(who) faults
4553    let t = trace(L.log)
4554    expect x
4555  }
4556}
4557"#;
4558
4559    type Slots<'a> = Vec<(&'static str, &'a Expr)>;
4560
4561    fn oracle_block<'a>(owner: &'static str, b: &'a Block, out: &mut Slots<'a>) {
4562        let Block {
4563            statements,
4564            tail,
4565            span: _,
4566            tail_leading_comments: _,
4567            implicit_tail: _,
4568        } = b;
4569        for s in statements {
4570            for (slot, e) in oracle_statement(s) {
4571                out.push((slot, e));
4572            }
4573        }
4574        out.push((owner, tail));
4575    }
4576
4577    /// A statement's expressions, each tagged with its slot.
4578    fn oracle_statement(s: &Statement) -> Slots<'_> {
4579        fn let_slots<'a>(kind: &'static str, l: &'a LetStmt, out: &mut Slots<'a>) {
4580            let LetStmt {
4581                name: _,
4582                type_annot: _,
4583                value,
4584                principal,
4585                let_else: _,
4586                span: _,
4587                trivia: _,
4588            } = l;
4589            if let Some(CallSiteActor {
4590                actor: _,
4591                identity,
4592                claims: _,
4593                span: _,
4594            }) = principal
4595                && let Some(identity) = identity
4596            {
4597                out.push((
4598                    if kind == "let" {
4599                        "let.identity"
4600                    } else {
4601                        "let<-.identity"
4602                    },
4603                    identity,
4604                ));
4605            }
4606            out.push((
4607                if kind == "let" {
4608                    "let.value"
4609                } else {
4610                    "let<-.value"
4611                },
4612                value,
4613            ));
4614        }
4615        let mut out = Vec::new();
4616        match s {
4617            Statement::Let(l) => let_slots("let", l, &mut out),
4618            Statement::EffectLet(l) => let_slots("let<-", l, &mut out),
4619            Statement::Expect(ExpectStmt {
4620                value,
4621                snapshot: _,
4622                span: _,
4623                trivia: _,
4624            }) => out.push(("expect.value", value)),
4625            Statement::Send(SendStmt {
4626                value,
4627                span: _,
4628                trivia: _,
4629            }) => out.push(("send.value", value)),
4630            Statement::Do(DoStmt {
4631                value,
4632                span: _,
4633                trivia: _,
4634            }) => out.push(("do.value", value)),
4635            Statement::Assign(AssignStmt {
4636                target: _,
4637                value,
4638                span: _,
4639                trivia: _,
4640            }) => out.push(("assign.value", value)),
4641            Statement::Advance(AdvanceStmt {
4642                keyword: _,
4643                cap: _,
4644                amount,
4645                span: _,
4646                trivia: _,
4647            }) => out.push(("advance.amount", amount)),
4648            Statement::Par(ParStmt {
4649                keyword: _,
4650                statements,
4651                closing_comments: _,
4652                span: _,
4653                trivia: _,
4654            }) => {
4655                for s in statements {
4656                    for (slot, e) in oracle_statement(s) {
4657                        out.push((slot, e));
4658                    }
4659                }
4660            }
4661        }
4662        out
4663    }
4664
4665    /// An expression's children, each tagged with its slot.
4666    fn oracle_children(e: &Expr) -> Slots<'_> {
4667        let Expr {
4668            id: _,
4669            kind,
4670            span: _,
4671        } = e;
4672        let mut out = Vec::new();
4673        match kind {
4674            ExprKind::IntLit {
4675                value: _,
4676                lexeme: _,
4677            }
4678            | ExprKind::FloatLit {
4679                value: _,
4680                lexeme: _,
4681            }
4682            | ExprKind::DecimalLit {
4683                coef: _,
4684                scale: _,
4685                lexeme: _,
4686            }
4687            | ExprKind::DurationLit {
4688                value: _,
4689                unit: _,
4690                millis: _,
4691            }
4692            | ExprKind::StrLit(_)
4693            | ExprKind::BoolLit(_)
4694            | ExprKind::Ident(_)
4695            | ExprKind::None
4696            | ExprKind::UnitLit
4697            | ExprKind::Trace { cap: _, op: _ } => {}
4698            ExprKind::InterpStr(parts) => {
4699                for p in parts {
4700                    match p {
4701                        InterpPart::Chunk(_) => {}
4702                        InterpPart::Hole(h) => out.push(("interp.hole", h.as_ref())),
4703                    }
4704                }
4705            }
4706            ExprKind::Call {
4707                name: _,
4708                type_args: _,
4709                args,
4710            } => args.iter().for_each(|a| out.push(("call.arg", a))),
4711            ExprKind::Lambda(LambdaExpr {
4712                params: _,
4713                body,
4714                span: _,
4715                block_body: _,
4716            }) => out.push(("lambda.body", body)),
4717            ExprKind::BinOp(_, l, r) => {
4718                out.push(("binop.left", l));
4719                out.push(("binop.right", r));
4720            }
4721            ExprKind::UnaryOp(_, x) => out.push(("unary.operand", x)),
4722            ExprKind::Paren(x) => out.push(("paren.inner", x)),
4723            ExprKind::Block(b) => oracle_block("block.tail", b, &mut out),
4724            ExprKind::If {
4725                cond,
4726                then_block,
4727                else_block,
4728            } => {
4729                out.push(("if.cond", cond));
4730                oracle_block("if.then.tail", then_block, &mut out);
4731                oracle_block("if.else.tail", else_block, &mut out);
4732            }
4733            ExprKind::Ok(x) => out.push(("ok.inner", x)),
4734            ExprKind::Err(x) => out.push(("err.inner", x)),
4735            ExprKind::Question(x) => out.push(("question.inner", x)),
4736            ExprKind::RecordConstruction {
4737                type_name: _,
4738                fields,
4739            } => {
4740                for FieldInit {
4741                    name: _,
4742                    value,
4743                    span: _,
4744                } in fields
4745                {
4746                    if let Some(v) = value {
4747                        out.push(("record.field", v));
4748                    }
4749                }
4750            }
4751            ExprKind::FieldAccess { receiver, field: _ } => out.push(("field.receiver", receiver)),
4752            ExprKind::MethodCall {
4753                receiver,
4754                method: _,
4755                type_args: _,
4756                args,
4757            } => {
4758                out.push(("method.receiver", receiver));
4759                args.iter().for_each(|a| out.push(("method.arg", a)));
4760            }
4761            ExprKind::Match { discriminant, arms } => {
4762                out.push(("match.discriminant", discriminant));
4763                for MatchArm {
4764                    pattern: _,
4765                    guard,
4766                    body,
4767                    span: _,
4768                } in arms
4769                {
4770                    if let Some(g) = guard {
4771                        out.push(("match.guard", g));
4772                    }
4773                    match body {
4774                        MatchBody::Expr(x) => out.push(("match.body", x)),
4775                        MatchBody::Block(b) => oracle_block("match.block.tail", b, &mut out),
4776                    }
4777                }
4778            }
4779            ExprKind::Is { value, pattern: _ } => out.push(("is.value", value)),
4780            ExprKind::Some(x) => out.push(("some.inner", x)),
4781            ExprKind::RecordSpread {
4782                type_name: _,
4783                base,
4784                overrides,
4785            } => {
4786                out.push(("spread.base", base));
4787                for FieldInit {
4788                    name: _,
4789                    value,
4790                    span: _,
4791                } in overrides
4792                {
4793                    if let Some(v) = value {
4794                        out.push(("spread.override", v));
4795                    }
4796                }
4797            }
4798            ExprKind::EffectPure(x) => out.push(("effect_pure.inner", x)),
4799            ExprKind::Expect(x) => out.push(("expect_expr.inner", x)),
4800            ExprKind::Val { type_ref: _, args } => {
4801                args.iter().for_each(|a| out.push(("val.arg", a)))
4802            }
4803            ExprKind::Wire(x) => out.push(("wire.inner", x)),
4804            ExprKind::ListLit(items) => items.iter().for_each(|i| out.push(("list.item", i))),
4805            ExprKind::Observation(obs) => {
4806                let ObservationExpr {
4807                    cap: _,
4808                    op: _,
4809                    matcher,
4810                } = obs.as_ref();
4811                match matcher {
4812                    ObservationMatcher::Called { count, with_pred } => {
4813                        if let Some(c) = count {
4814                            out.push(("observation.count", c));
4815                        }
4816                        if let Some(p) = with_pred {
4817                            out.push(("observation.with", p));
4818                        }
4819                    }
4820                    ObservationMatcher::NeverCalled
4821                    | ObservationMatcher::Before { cap: _, op: _ } => {}
4822                }
4823            }
4824            ExprKind::Faults(f) => {
4825                let FaultsExpr { call, principal } = f.as_ref();
4826                if let Some(CallSiteActor {
4827                    actor: _,
4828                    identity,
4829                    claims: _,
4830                    span: _,
4831                }) = principal
4832                    && let Some(identity) = identity
4833                {
4834                    out.push(("faults.identity", identity));
4835                }
4836                out.push(("faults.inner", call));
4837            }
4838        }
4839        out
4840    }
4841
4842    /// Every slot [`oracle_children`] and [`oracle_statement`] can name.
4843    const ALL_SLOTS: &[&str] = &[
4844        "interp.hole",
4845        "call.arg",
4846        "lambda.body",
4847        "binop.left",
4848        "binop.right",
4849        "unary.operand",
4850        "paren.inner",
4851        "block.tail",
4852        "if.cond",
4853        "if.then.tail",
4854        "if.else.tail",
4855        "ok.inner",
4856        "err.inner",
4857        "question.inner",
4858        "record.field",
4859        "field.receiver",
4860        "method.receiver",
4861        "method.arg",
4862        "match.discriminant",
4863        "match.guard",
4864        "match.body",
4865        "match.block.tail",
4866        "is.value",
4867        "some.inner",
4868        "spread.base",
4869        "spread.override",
4870        "effect_pure.inner",
4871        "expect_expr.inner",
4872        "val.arg",
4873        "wire.inner",
4874        "list.item",
4875        "observation.count",
4876        "observation.with",
4877        "faults.identity",
4878        "faults.inner",
4879        "let.value",
4880        "let<-.value",
4881        "let<-.identity",
4882        "expect.value",
4883        "send.value",
4884        "do.value",
4885        "assign.value",
4886    ];
4887
4888    /// Every top-level block of the coverage program: fn bodies and case
4889    /// bodies.
4890    fn bodies(units: &[SourceUnit]) -> Vec<&Block> {
4891        let mut out = Vec::new();
4892        for u in units {
4893            match u {
4894                SourceUnit::Commons(c) => {
4895                    for item in &c.items {
4896                        if let CommonsItem::Fn(f) = item {
4897                            out.push(&f.body);
4898                        }
4899                    }
4900                }
4901                SourceUnit::Suite(t) => out.extend(t.cases.iter().map(|c| &c.body)),
4902                _ => {}
4903            }
4904        }
4905        out
4906    }
4907
4908    fn parse(source: &str) -> Vec<SourceUnit> {
4909        let tokens = crate::lexer::tokenize(source).expect("lex");
4910        crate::parser::parse_units(&tokens, source).expect("the coverage program parses")
4911    }
4912
4913    /// Visit every expression under `b` through the oracle, depth first.
4914    fn visit<'a>(b: &'a Block, f: &mut impl FnMut(&'a Expr)) {
4915        fn go<'a>(e: &'a Expr, f: &mut impl FnMut(&'a Expr)) {
4916            f(e);
4917            for (_, c) in oracle_children(e) {
4918                go(c, f);
4919            }
4920        }
4921        let mut top = Vec::new();
4922        oracle_block("body.tail", b, &mut top);
4923        for (_, e) in top {
4924            go(e, f);
4925        }
4926    }
4927
4928    #[test]
4929    fn the_coverage_program_uses_every_slot() {
4930        let mut used = BTreeSet::new();
4931        for source in [COVERAGE, COVERAGE_SUITE] {
4932            let units = parse(source);
4933            for b in bodies(&units) {
4934                for s in &b.statements {
4935                    used.extend(oracle_statement(s).into_iter().map(|(slot, _)| slot));
4936                }
4937                visit(b, &mut |e| {
4938                    used.extend(oracle_children(e).into_iter().map(|(slot, _)| slot));
4939                    if let ExprKind::Block(b) = &e.kind {
4940                        for s in &b.statements {
4941                            used.extend(oracle_statement(s).into_iter().map(|(slot, _)| slot));
4942                        }
4943                    }
4944                });
4945            }
4946        }
4947        let missing: Vec<&str> = ALL_SLOTS
4948            .iter()
4949            .copied()
4950            .filter(|s| !used.contains(s))
4951            .collect();
4952        let unknown: Vec<&str> = used
4953            .iter()
4954            .copied()
4955            .filter(|s| !ALL_SLOTS.contains(s))
4956            .collect();
4957        assert!(
4958            missing.is_empty() && unknown.is_empty(),
4959            "slots the coverage program never uses: {missing:?}; slots missing from ALL_SLOTS: {unknown:?}"
4960        );
4961    }
4962
4963    #[test]
4964    fn expr_children_returns_exactly_the_oracle_s_children_at_every_node() {
4965        let key = |e: &Expr| (e.span.start, e.span.end, e.id);
4966        let mut checked = 0;
4967        for source in [COVERAGE, COVERAGE_SUITE] {
4968            let units = parse(source);
4969            for b in bodies(&units) {
4970                for s in &b.statements {
4971                    let mut got = Vec::new();
4972                    statement_exprs(s, &mut got);
4973                    let got: Vec<_> = got.into_iter().map(key).collect();
4974                    let want: Vec<_> = oracle_statement(s)
4975                        .into_iter()
4976                        .map(|(_, e)| key(e))
4977                        .collect();
4978                    assert_eq!(got, want, "statement_exprs at {:?}", s.span());
4979                }
4980                visit(b, &mut |e| {
4981                    let got: Vec<_> = expr_children(e).into_iter().map(key).collect();
4982                    let want: Vec<_> = oracle_children(e)
4983                        .into_iter()
4984                        .map(|(_, c)| key(c))
4985                        .collect();
4986                    assert_eq!(
4987                        got,
4988                        want,
4989                        "expr_children of `{}`",
4990                        &source[e.span.start..e.span.end]
4991                    );
4992                    checked += 1;
4993                });
4994            }
4995        }
4996        assert!(checked > 50, "only {checked} nodes checked");
4997    }
4998}
4999
5000#[cfg(test)]
5001mod stream_path_tests {
5002    use super::*;
5003
5004    fn types(source: &str) -> std::collections::HashMap<String, std::sync::Arc<TypeDecl>> {
5005        let tokens = crate::lexer::tokenize(source).unwrap();
5006        let units = crate::parser::parse_units(&tokens, source).unwrap();
5007        let SourceUnit::Commons(c) = &units[0] else {
5008            panic!("a commons");
5009        };
5010        c.items
5011            .iter()
5012            .filter_map(|i| match i {
5013                CommonsItem::Type(t) => Some((t.name.name.clone(), std::sync::Arc::new(t.clone()))),
5014                _ => None,
5015            })
5016            .collect()
5017    }
5018
5019    fn named(name: &str) -> TypeRef {
5020        TypeRef::Named(Ident {
5021            name: name.to_string(),
5022            span: Span::default(),
5023        })
5024    }
5025
5026    /// K-streams (#2623): the chain of fields a stream is reached through,
5027    /// across records, sums and a generic record's own fields, terminating on
5028    /// a recursive type.
5029    #[test]
5030    fn the_stream_path_names_each_field_it_passes_through() {
5031        let t = types(
5032            "commons d\n\
5033             type Download = { contentType: String, body: Stream[Bytes] }\n\
5034             type Shelf = { name: String, latest: Option[Download] }\n\
5035             type Pick = | Got(d: Download) | Nothing\n\
5036             type Wrap[T] = { s: Stream[T] }\n\
5037             type Tree = { kids: List[Tree], n: Int }\n\
5038             type Plain = { n: Int }\n",
5039        );
5040        let path = |n: &str| stream_path(&named(n), &t);
5041        assert_eq!(path("Download"), Some(vec!["Download.body".to_string()]));
5042        assert_eq!(
5043            path("Shelf"),
5044            Some(vec![
5045                "Shelf.latest".to_string(),
5046                "Download.body".to_string()
5047            ])
5048        );
5049        assert_eq!(
5050            path("Pick"),
5051            Some(vec!["Pick.Got.d".to_string(), "Download.body".to_string()])
5052        );
5053        assert_eq!(path("Wrap"), Some(vec!["Wrap.s".to_string()]));
5054        assert_eq!(path("Tree"), None);
5055        assert_eq!(path("Plain"), None);
5056        assert!(type_carries_stream("Shelf", &t));
5057        assert!(!type_carries_stream("Tree", &t));
5058    }
5059
5060    /// #2867: the calendar rule at its edges.
5061    #[test]
5062    fn is_iso_date_decides_the_calendar() {
5063        for ok in [
5064            "2026-11-03",
5065            "2028-02-29",
5066            "2000-02-29",
5067            "0000-01-01",
5068            "9999-12-31",
5069            "2026-04-30",
5070            "2026-12-31",
5071            "2400-02-29",
5072        ] {
5073            assert!(is_iso_date(ok), "{ok}");
5074        }
5075        for bad in [
5076            "2026-02-30",
5077            "2026-02-29",
5078            "1900-02-29",
5079            "2100-02-29",
5080            "2026-04-31",
5081            "2026-13-01",
5082            "2026-00-10",
5083            "2026-01-00",
5084            "2026-1-1",
5085            "yesterday",
5086            "",
5087            "2026-11-03 ",
5088            "20261103",
5089            "2026/11/03",
5090            "+2026-11-03",
5091            "2026-11-03T00:00:00Z",
5092            "2026-11-03",
5093            "2026-+1-03",
5094        ] {
5095            assert!(!is_iso_date(bad), "{bad:?}");
5096        }
5097    }
5098}