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