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