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