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`]).
19/// - `trailing` holds a single comment that appears on the same source
20///   line as the node's final token (e.g. `expr  -- note`).
21#[derive(Debug, Clone, Default)]
22pub struct Trivia {
23    pub leading: Vec<Comment>,
24    pub trailing: Option<String>,
25}
26
27/// One entry of comment trivia.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Comment {
30    /// A `--` line comment: the text after the marker, with its original
31    /// inline whitespace preserved.
32    Line(String),
33    /// #1756: a `---` doc block that attaches to no declaration (a blank line
34    /// separates it from the next one, or nothing follows it). The parser warns
35    /// `bynk.parse.orphan_doc_block` and keeps the block here, so the formatter
36    /// can print it where it was. Its content is normalised as an attached
37    /// doc's is.
38    OrphanDoc(String),
39}
40
41impl Trivia {
42    pub fn is_empty(&self) -> bool {
43        self.leading.is_empty() && self.trailing.is_none()
44    }
45}
46
47/// A whole parsed commons source file.
48///
49/// In v0.3 a commons may be split across multiple files in a directory; the
50/// resolver merges them into one logical commons. Each parsed AST instance
51/// represents the contribution from a single source file.
52#[derive(Debug, Clone)]
53pub struct Commons {
54    pub name: QualifiedName,
55    pub items: Vec<CommonsItem>,
56    /// `uses` clauses declared in this file.
57    pub uses: Vec<UsesDecl>,
58    /// Optional documentation block attached to the commons declaration.
59    pub documentation: Option<String>,
60    /// Surface form of the file: brace-delimited body or headerless fragment.
61    pub form: CommonsForm,
62    pub span: Span,
63    /// Trivia attached to the commons declaration itself — leading comments
64    /// before the `commons` keyword and a trailing comment after the header
65    /// or closing brace.
66    pub trivia: Trivia,
67    /// Comments appearing after the last item but before the file ends
68    /// (or the closing brace, for brace form). One entry per `--` line.
69    pub trailing_comments: Vec<Comment>,
70}
71
72/// The two surface forms in which a commons body may be parsed (v0.3 §3.1).
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CommonsForm {
75    /// `commons name { ... }`
76    Brace,
77    /// `commons name` followed by top-level declarations to EOF.
78    Fragment,
79}
80
81/// A `uses other.commons` declaration (v0.3 §3.3).
82#[derive(Debug, Clone)]
83pub struct UsesDecl {
84    pub target: QualifiedName,
85    pub span: Span,
86    pub trivia: Trivia,
87}
88
89/// A whole parsed context source file (v0.4 §3.1).
90///
91/// Contexts are the architectural-layer declaration kind. Like commons, a
92/// context may be split across multiple files in a directory.
93#[derive(Debug, Clone)]
94pub struct Context {
95    pub name: QualifiedName,
96    pub items: Vec<CommonsItem>,
97    /// `uses` clauses declared in this file.
98    pub uses: Vec<UsesDecl>,
99    /// `consumes` clauses declared in this file.
100    pub consumes: Vec<ConsumesDecl>,
101    /// `exports` clauses declared in this file.
102    pub exports: Vec<ExportsDecl>,
103    /// Optional documentation block attached to the context declaration.
104    pub documentation: Option<String>,
105    /// Surface form of the file: brace-delimited body or headerless fragment.
106    pub form: CommonsForm,
107    pub span: Span,
108    /// Trivia attached to the context declaration itself — leading comments
109    /// before the `context` keyword.
110    pub trivia: Trivia,
111    /// Comments appearing after the last item but before the file ends
112    /// (or the closing brace, for brace form). One entry per `--` line.
113    pub trailing_comments: Vec<Comment>,
114}
115
116/// A `consumes other.context` declaration (v0.4 §3.2). May optionally carry
117/// an alias introduced by `consumes other.context as Alias` (v0.6 §3.1).
118#[derive(Debug, Clone)]
119pub struct ConsumesDecl {
120    pub target: QualifiedName,
121    pub alias: Option<Ident>,
122    /// v0.17: `consumes U { Cap, … }` — selected capabilities flattened into
123    /// the consumer's local capability namespace under their bare names (§3.3).
124    /// `None` for the whole-unit forms; `Some` (possibly empty) for the braced
125    /// form. Mutually exclusive with `alias`.
126    pub selected: Option<Vec<Ident>>,
127    pub span: Span,
128    pub trivia: Trivia,
129}
130
131/// An `exports visibility { names }` clause (v0.4 §3.3) or, v0.15, an
132/// `exports capability { names }` clause.
133#[derive(Debug, Clone)]
134pub struct ExportsDecl {
135    pub kind: ExportKind,
136    pub names: Vec<ExportName>,
137    pub span: Span,
138    pub trivia: Trivia,
139    /// #1797: comments before the closing `}`.
140    pub trailing_comments: Vec<Comment>,
141}
142
143/// One name in an `exports` list, with its comments (#1797).
144#[derive(Debug, Clone)]
145pub struct ExportName {
146    pub name: Ident,
147    /// #1797: comments above the name and at the end of its line. A comment
148    /// on the list's `{` line leads the first name.
149    pub trivia: Trivia,
150}
151
152/// What an `exports` clause exposes: types (with a visibility) or, v0.15,
153/// capabilities offered for cross-context consumption.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum ExportKind {
156    /// `exports opaque { ... }` / `exports transparent { ... }` — type exports.
157    Type(Visibility),
158    /// `exports capability { ... }` — capabilities offered to consumers (v0.15).
159    Capability,
160}
161
162/// Visibility level for an exports clause (v0.4 §3.3).
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum Visibility {
165    /// Token-only outside the context: hold, pass, compare; no inspect, no construct.
166    Opaque,
167    /// Readable shape outside the context: inspect fields, match variants; no construct.
168    Transparent,
169}
170
171/// An `adapter qualified.name { … }` declaration (v0.17 §3.1). An adapter
172/// co-locates a capability contract with a non-Bynk binding: it may declare
173/// capabilities, the boundary types they reference, inline pure helper
174/// `type`/`fn` (and `uses`), external (bodiless) providers, `exports
175/// capability`, and exactly one `binding` clause. It may *not* declare
176/// services, agents, or bodied providers. Like commons/contexts it may be
177/// split across files in a directory.
178#[derive(Debug, Clone)]
179pub struct AdapterDecl {
180    pub name: QualifiedName,
181    pub items: Vec<CommonsItem>,
182    /// `uses` clauses declared in this file (pure-vocabulary mixin; allowed
183    /// because helpers cannot pierce containment — spec [DECISION B]).
184    pub uses: Vec<UsesDecl>,
185    /// `exports capability { … }` clauses (adapters export capabilities and
186    /// boundary types, never services).
187    pub exports: Vec<ExportsDecl>,
188    /// v0.18: `consumes U { Cap, … }` clauses — adapter-to-adapter capability
189    /// dependencies (spec §4.5, \[N\]). Braced form only; adapter targets only
190    /// (both enforced semantically, not in the parser).
191    pub consumes: Vec<ConsumesDecl>,
192    /// The `binding "<module>" requires { … }` clause, if present. Required
193    /// when the adapter declares any external provider (`bynk.adapter.no_binding`).
194    pub binding: Option<BindingDecl>,
195    pub documentation: Option<String>,
196    pub form: CommonsForm,
197    pub span: Span,
198    pub trivia: Trivia,
199    pub trailing_comments: Vec<Comment>,
200}
201
202/// A `binding "<module>" requires { "pkg": "range", … }` clause inside an
203/// adapter (v0.17 §3.5). `module` is the TypeScript module supplying the
204/// adapter's external provider symbols, resolved relative to the adapter's
205/// source file. `requires` declares npm dependencies folded into the
206/// generated `package.json`.
207#[derive(Debug, Clone)]
208pub struct BindingDecl {
209    /// The module path as written (the string-literal contents, no quotes).
210    pub module: String,
211    pub module_span: Span,
212    pub requires: Vec<RequiresDep>,
213    pub span: Span,
214    pub trivia: Trivia,
215}
216
217/// One `"pkg": "range"` entry in a binding's `requires { … }` map.
218#[derive(Debug, Clone)]
219pub struct RequiresDep {
220    pub package: String,
221    pub range: String,
222    pub span: Span,
223}
224
225/// Either a commons or a context — the two declaration kinds at the file
226/// level (v0.4 §3.1). v0.7 adds the test declaration kind; v0.17 the adapter.
227#[derive(Debug, Clone)]
228pub enum SourceUnit {
229    Commons(Commons),
230    Context(Context),
231    Suite(SuiteDecl),
232    /// v0.17: an `adapter` unit — the host boundary (capability contract +
233    /// external binding).
234    Adapter(AdapterDecl),
235}
236
237impl SourceUnit {
238    pub fn name(&self) -> &QualifiedName {
239        match self {
240            SourceUnit::Commons(c) => &c.name,
241            SourceUnit::Context(c) => &c.name,
242            SourceUnit::Suite(t) => &t.target,
243            SourceUnit::Adapter(a) => &a.name,
244        }
245    }
246
247    pub fn span(&self) -> Span {
248        match self {
249            SourceUnit::Commons(c) => c.span,
250            SourceUnit::Context(c) => c.span,
251            SourceUnit::Suite(t) => t.span,
252            SourceUnit::Adapter(a) => a.span,
253        }
254    }
255
256    pub fn kind_name(&self) -> &'static str {
257        match self {
258            SourceUnit::Commons(_) => "commons",
259            SourceUnit::Context(_) => "context",
260            SourceUnit::Suite(_) => "suite",
261            SourceUnit::Adapter(_) => "adapter",
262        }
263    }
264}
265
266/// A `test <qualified-name> { ... }` declaration (v0.7 §3.1).
267///
268/// A test targets a commons or context by qualified name and bundles a set of
269/// test cases plus optional mock declarations. As with commons and contexts, a
270/// test may be split across multiple files (fragment form).
271#[derive(Debug, Clone)]
272pub struct SuiteDecl {
273    /// The targeted commons or context.
274    pub target: QualifiedName,
275    /// `uses` clauses brought in by this test fragment.
276    pub uses: Vec<UsesDecl>,
277    /// v0.118: suite-scoped `stub` clauses — per-seam provider overrides
278    /// applied to every case (a case-scoped `stub` takes precedence). Formerly
279    /// the punned `provides` stub; renamed to `stub` in the keyword-hygiene
280    /// batch (#548).
281    pub stubs: Vec<StubClause>,
282    /// The individual test cases.
283    pub cases: Vec<Case>,
284    /// v0.114: generative `property` blocks (testing track slice 2).
285    pub properties: Vec<PropertyDecl>,
286    /// v0.118: the suite-level tier default (`suite … as integration`). `None`
287    /// means the `unit` default; a `case`'s own tier overrides it. A `property`
288    /// ignores a suite tier (tiers are a `case`-only affordance).
289    pub tier: Option<TestTier>,
290    /// Surface form: brace-delimited body or headerless fragment.
291    pub form: CommonsForm,
292    /// Optional documentation block attached to the test declaration.
293    pub documentation: Option<String>,
294    pub span: Span,
295    pub trivia: Trivia,
296    pub trailing_comments: Vec<Comment>,
297}
298
299/// v0.118: the tier a `case` runs at (testing track slice 6, ADR 0153). One
300/// body promoted across the testing pyramid; `unit` is the default and elided.
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum TestTier {
303    /// Collaborators stubbed (the default).
304    Unit,
305    /// Real collaborators within one context, no serialisation wire.
306    Integration,
307    /// Contexts wired across the real serialise → JSON → deserialise boundary.
308    System,
309}
310
311impl TestTier {
312    pub fn as_str(self) -> &'static str {
313        match self {
314            TestTier::Unit => "unit",
315            TestTier::Integration => "integration",
316            TestTier::System => "system",
317        }
318    }
319}
320
321/// v0.118: a per-seam provider override `stub Cap.method(<args>) returns <v>
322/// | fails` (testing track slice 6, ADR 0154; keyword `stub` since #548).
323/// Substitutes one capability method's provision under test; the right-hand
324/// side is a value or a fault, never a computed body.
325#[derive(Debug, Clone)]
326pub struct StubClause {
327    /// The capability being overridden (a consumed seam of the unit).
328    pub capability: Ident,
329    /// The overridden method.
330    pub method: Ident,
331    /// One argument pattern per parameter (`_` or a value the arg must equal).
332    pub args: Vec<ArgPattern>,
333    /// The provision: a value, a fault, or a per-call sequence.
334    pub rhs: StubRhs,
335    pub documentation: Option<String>,
336    pub span: Span,
337    pub trivia: Trivia,
338}
339
340/// v0.118: one argument pattern in a `stub` call pattern. Patterns for the
341/// same method are tried top-to-bottom, first match wins.
342#[derive(Debug, Clone)]
343pub enum ArgPattern {
344    /// `_` — matches any argument.
345    Any(Span),
346    /// A value the recorded argument must equal (a literal or pure value expr).
347    Value(Expr),
348}
349
350/// v0.118: the right-hand side of a `stub` clause.
351#[derive(Debug, Clone)]
352pub enum StubRhs {
353    /// `returns <value>` — a single success value, repeated for every call.
354    Returns(Expr),
355    /// `fails` — inject a capability fault (Principle 3).
356    Fails(Span),
357    /// `returns each [<outcome>, …]` — one outcome per call, in order; the last
358    /// outcome repeats once the sequence is exhausted (DECISION V).
359    ReturnsEach(Vec<SeqOutcome>, Span),
360}
361
362impl StubRhs {
363    pub fn span(&self) -> Span {
364        match self {
365            StubRhs::Returns(e) => e.span,
366            StubRhs::Fails(s) => *s,
367            StubRhs::ReturnsEach(_, s) => *s,
368        }
369    }
370}
371
372/// v0.118: one outcome in a sequenced (`returns each`) `stub`.
373#[derive(Debug, Clone)]
374pub enum SeqOutcome {
375    /// A success value.
376    Value(Expr),
377    /// A fault.
378    Fails(Span),
379}
380
381/// A `case "name" [as <tier>] { [stub …] body }` block inside a suite
382/// (v0.7 §3.3; v0.118 adds the tier clause and case-scoped stubs).
383#[derive(Debug, Clone)]
384pub struct Case {
385    /// The test name, taken from the string literal.
386    pub name: String,
387    /// The span of the string literal — used for diagnostics and runtime
388    /// failure reports.
389    pub name_span: Span,
390    /// v0.118: the case's own tier, if written (`as integration` / `as system`).
391    /// `None` means inherit the suite default (itself `unit` when unset).
392    pub tier: Option<TestTier>,
393    /// v0.118: case-scoped `stub` clauses (override the suite's, and the
394    /// tier default).
395    pub stubs: Vec<StubClause>,
396    pub body: Block,
397    pub documentation: Option<String>,
398    pub span: Span,
399    pub trivia: Trivia,
400}
401
402/// A `property "name" { for all <bindings> [where <pred>] { body } }` block
403/// inside a suite (v0.114, testing track slice 2, ADR 0149). The generative
404/// sibling of [`Case`]: the runner draws inhabitants of each binding's type from
405/// its refinement domain and evaluates the body's `expect`s over them.
406#[derive(Debug, Clone)]
407pub struct PropertyDecl {
408    /// The property name, taken from the string literal.
409    pub name: String,
410    /// The span of the string literal — used for diagnostics and reports.
411    pub name_span: Span,
412    /// The `for all` binder: the generated bindings, an optional `where` filter,
413    /// and the predicate body.
414    pub forall: ForAll,
415    pub documentation: Option<String>,
416    pub span: Span,
417    pub trivia: Trivia,
418}
419
420/// The `for all x: T, … [where <pred>] { … }` binder inside a [`PropertyDecl`].
421#[derive(Debug, Clone)]
422pub struct ForAll {
423    /// The generated bindings, `x: T` (one or more).
424    pub bindings: Vec<ForAllBinding>,
425    /// An optional `where <pred>` filter (a pure `Bool`) applied to generated
426    /// tuples before the body runs.
427    pub where_pred: Option<Expr>,
428    /// The body — one or more statements, typically `expect`s.
429    pub body: Block,
430    pub span: Span,
431}
432
433/// One `for all` binding: `name: T`, where the runner generates inhabitants of
434/// `T` from its refinements.
435#[derive(Debug, Clone)]
436pub struct ForAllBinding {
437    pub name: Ident,
438    pub type_ref: TypeRef,
439}
440
441/// A capability reference in a `given` clause (v0.15 §3.2). A bare name is a
442/// local capability (`given Cap`); a dotted name refers to a capability a
443/// consumed context provides (`given B.Cap` / `given Alias.Cap`).
444#[derive(Debug, Clone)]
445pub struct CapRef {
446    /// `None` for a local capability; `Some(prefix)` for a cross-context
447    /// reference where `prefix` is a consumed-context qualified name or alias.
448    pub context: Option<QualifiedName>,
449    /// The capability's simple name (also the local deps key).
450    pub name: Ident,
451    pub span: Span,
452}
453
454impl CapRef {
455    /// The local deps key / capability simple name (e.g. `Clock`).
456    pub fn key(&self) -> &str {
457        &self.name.name
458    }
459
460    /// True when this references a capability provided by a consumed context.
461    pub fn is_cross_context(&self) -> bool {
462        self.context.is_some()
463    }
464
465    /// The cross-context prefix (consumed-context qualified name or alias) as
466    /// a dotted string, if any.
467    pub fn prefix(&self) -> Option<String> {
468        self.context.as_ref().map(|q| q.joined())
469    }
470}
471
472/// A dotted name like `fitness.units`.
473#[derive(Debug, Clone)]
474pub struct QualifiedName {
475    pub parts: Vec<Ident>,
476    pub span: Span,
477}
478
479impl QualifiedName {
480    pub fn joined(&self) -> String {
481        self.parts
482            .iter()
483            .map(|p| p.name.as_str())
484            .collect::<Vec<_>>()
485            .join(".")
486    }
487}
488
489// Finding #31 shrank `Expr`/`ExprKind` enough that clippy's variance check
490// between this enum's smallest and largest variants (`Service`/`Actor` vs.
491// `Type`/`Fn`) now crosses its threshold — a pre-existing size profile made
492// newly visible, not something #31 itself is scoped to fix. Boxing
493// `ServiceDecl`/`ActorDecl` here is a separate, unscoped refactor (its own
494// blast radius across every `CommonsItem::Service`/`Actor` construction and
495// match site) left for a future finding.
496#[allow(clippy::large_enum_variant)]
497#[derive(Debug, Clone)]
498pub enum CommonsItem {
499    Type(TypeDecl),
500    Fn(FnDecl),
501    /// `capability Name { fn op(...) -> T ... }` (v0.5; contexts only).
502    Capability(CapabilityDecl),
503    /// `provides Cap = ProviderName { fn op(...) -> T { ... } ... }` (v0.5).
504    Provider(ProviderDecl),
505    /// `service Name { on call(...) -> T { ... } ... }` (v0.5).
506    Service(ServiceDecl),
507    /// `agent Name { key id: T; state { ... }; on call ... }` (v0.5).
508    Agent(AgentDecl),
509    /// `actor Name { auth = Scheme, identity = T }` (v0.45). A nominal boundary
510    /// contract consumed by a handler's `by` clause; not a runnable entity.
511    Actor(ActorDecl),
512    /// `messages <tag> @reference { "code" => "template" ... }` — a message
513    /// bundle for one locale. Commons-only (checker-enforced, not grammar);
514    /// legal syntactically wherever any `CommonsItem` is, per the existing
515    /// `Service`/`Agent`-in-`adapter` precedent.
516    Messages(MessagesDecl),
517    /// `event Name = { fields }` (Events track, slice 0, spine #936).
518    /// Context-only (checker-enforced, not grammar) — the mirror image of
519    /// `Messages`' commons-only restriction, same mechanism.
520    Event(EventDecl),
521}
522
523impl CommonsItem {
524    /// The declaring identifier, when the item is named by one. `Messages` is
525    /// the sole `None`: its locale tag is a `LocaleTag` string literal
526    /// (`"pt-BR"`), not an identifier, and synthesising an `Ident` from it
527    /// would be a lie any identifier-shaped consumer (rename, go-to-def) would
528    /// eventually surface.
529    pub fn name(&self) -> Option<&Ident> {
530        match self {
531            CommonsItem::Type(t) => Some(&t.name),
532            CommonsItem::Fn(f) => Some(f.name.ident()),
533            CommonsItem::Capability(c) => Some(&c.name),
534            CommonsItem::Provider(p) => Some(&p.provider_name),
535            CommonsItem::Service(s) => Some(&s.name),
536            CommonsItem::Agent(a) => Some(&a.name),
537            CommonsItem::Actor(a) => Some(&a.name),
538            CommonsItem::Messages(_) => None,
539            CommonsItem::Event(e) => Some(&e.name),
540        }
541    }
542
543    /// The whole declaration's span, from its first token to its last.
544    pub fn span(&self) -> Span {
545        match self {
546            CommonsItem::Type(t) => t.span,
547            CommonsItem::Fn(f) => f.span,
548            CommonsItem::Capability(c) => c.span,
549            CommonsItem::Provider(p) => p.span,
550            CommonsItem::Service(s) => s.span,
551            CommonsItem::Agent(a) => a.span,
552            CommonsItem::Actor(a) => a.span,
553            CommonsItem::Messages(m) => m.span,
554            CommonsItem::Event(e) => e.span,
555        }
556    }
557}
558
559/// One locale's message bundle (v0.222+): `messages "<tag>" @reference { ... }`.
560/// `tag` is a `LocaleTag` string literal (like an entry's `code`/`template`);
561/// its refinement (`bynk.locale.types`) is checked by `check_messages_bundles`,
562/// which reports `bynk.messages.invalid_locale_tag` for a tag the pattern
563/// rejects.
564#[derive(Debug, Clone)]
565pub struct MessagesDecl {
566    pub tag: String,
567    pub tag_span: Span,
568    /// Every `@`-annotation attached to this block. The parser stays
569    /// permissive (zero or more, same as `store` field annotations); cardinality
570    /// (exactly one `@reference` per bundle, counted across every `Messages`
571    /// item in the commons) is a checker concern, not a parse error.
572    pub annotations: Vec<Annotation>,
573    pub entries: Vec<MessageEntry>,
574    pub documentation: Option<String>,
575    pub span: Span,
576    pub trivia: Trivia,
577}
578
579/// One `"code" => "template"` entry inside a `messages` block. Both sides are
580/// plain string literals — a template's `{name}` placeholders are resolved by
581/// a compile-time string scan during lowering, not parsed as expressions.
582#[derive(Debug, Clone)]
583pub struct MessageEntry {
584    pub code: String,
585    pub code_span: Span,
586    pub template: String,
587    pub template_span: Span,
588    pub span: Span,
589}
590
591/// A capability declaration (v0.5 §3.3). Capabilities are interface-like
592/// contracts for external dependencies, used inside contexts. They may only
593/// appear inside a `context` declaration.
594#[derive(Debug, Clone)]
595pub struct CapabilityDecl {
596    pub name: Ident,
597    pub ops: Vec<CapabilityOp>,
598    pub documentation: Option<String>,
599    pub span: Span,
600    /// #1756: comments before the closing `}`, an orphaned doc block among
601    /// them, so the formatter keeps them.
602    pub trailing_comments: Vec<Comment>,
603    pub trivia: Trivia,
604}
605
606/// One operation in a capability (signature only; no body).
607#[derive(Debug, Clone)]
608pub struct CapabilityOp {
609    pub name: Ident,
610    /// #926: `[T, …]` type parameters on the op itself; empty for a
611    /// non-generic op. Resolved only from an explicit type argument at the
612    /// call site (`Cap.op[Some](…)`) — never inferred.
613    pub type_params: Vec<TypeParam>,
614    pub params: Vec<Param>,
615    pub return_type: TypeRef,
616    pub documentation: Option<String>,
617    pub span: Span,
618    pub trivia: Trivia,
619}
620
621/// A provider declaration (v0.5 §3.4). Supplies an implementation for a
622/// capability.
623#[derive(Debug, Clone)]
624pub struct ProviderDecl {
625    /// The capability being implemented.
626    pub capability: Ident,
627    /// The provider's identifier (used in tests/config to select impls).
628    pub provider_name: Ident,
629    /// v0.12: capabilities this provider depends on (`provides X = Impl given
630    /// Y, Z { … }`). The provider's operation bodies may use these. v0.15:
631    /// a dependency may be a cross-context capability (`given B.Cap`).
632    pub given: Vec<CapRef>,
633    pub ops: Vec<ProviderOp>,
634    /// v0.17: an *external* provider — `provides Cap = Name` with **no** brace
635    /// block — inside an adapter, supplied by the adapter's binding rather than
636    /// a Bynk body. When `true`, `ops` is empty and the emitter produces no
637    /// class. The absence of the brace block (not an empty one) is the signal.
638    pub external: bool,
639    pub documentation: Option<String>,
640    pub span: Span,
641    pub trivia: Trivia,
642}
643
644/// One operation in a provider (signature plus body).
645#[derive(Debug, Clone)]
646pub struct ProviderOp {
647    pub name: Ident,
648    pub params: Vec<Param>,
649    pub return_type: TypeRef,
650    pub body: Block,
651    pub span: Span,
652    pub trivia: Trivia,
653}
654
655/// A service declaration (v0.5 §3.5). Services are the boundary interface
656/// of a context.
657#[derive(Debug, Clone)]
658pub struct ServiceDecl {
659    pub name: Ident,
660    /// The protocol the service conforms to, from the `from <protocol>` header
661    /// clause (v0.44). `Call` when there is no clause.
662    pub protocol: ServiceProtocol,
663    /// The optional service-level `by` default (v0.155) — a `by <Actor>` clause on
664    /// the service header, `service Api from http by v: Visitor { … }`. Every
665    /// handler that omits its own `by` inherits this one (injected by the
666    /// normalization pass). `None` when absent — handlers then fall back to the
667    /// per-protocol default actor (HTTP/WebSocket have none, so `by` stays
668    /// mandatory there). The "public / bearer-authed" fact is usually a service
669    /// fact, so this removes the per-handler repetition.
670    pub default_by: Option<ByClause>,
671    /// The optional service-level `given` default (v0.155) — a `given C1, C2`
672    /// clause on the service header, following the `by` default. Every handler
673    /// that declares no `given` of its own inherits this list. Empty when absent.
674    pub default_given: Vec<CapRef>,
675    /// The optional cross-origin (CORS) policy (v0.131, ADR 0159) — a `cors { }`
676    /// section in the service body, only meaningful on a `from http` service.
677    /// `None` when absent (same-origin default, byte-for-byte unchanged output).
678    pub cors: Option<CorsPolicy>,
679    /// The optional security-headers policy (v0.141, ADR 0164) — a `security { }`
680    /// section in the service body, only meaningful on a `from http` service.
681    /// `None` when absent, but unlike `cors` the *absence* still stamps the safe
682    /// defaults (`nosniff` on) — the emitter synthesises a default policy for every
683    /// `from http` service, so `None` here means "defaults", not "no headers".
684    pub security: Option<SecurityPolicy>,
685    /// The optional request-body-size policy (v0.142, ADR 0165) — a `limits { }`
686    /// section in the service body, only meaningful on a `from http` service. It
687    /// declares a per-service `maxBody` ceiling (in bytes) for the service's
688    /// body-taking routes; a route may override it with `@limit(maxBody: …)`.
689    /// `None` when absent (no cap — byte-for-byte unchanged output, the opt-in
690    /// CORS posture, not the `security` default-on posture).
691    pub limits: Option<LimitsPolicy>,
692    pub handlers: Vec<Handler>,
693    pub documentation: Option<String>,
694    pub span: Span,
695    /// #1756: comments before the closing `}`, an orphaned doc block among
696    /// them, so the formatter keeps them.
697    pub trailing_comments: Vec<Comment>,
698    pub trivia: Trivia,
699}
700
701/// A cross-origin resource-sharing policy on a `from http` service (v0.131,
702/// ADR 0159): the `cors { }` section in the service body. Parsed leniently as a
703/// list of `name: value` fields (the grammar accepts any field name — an unknown
704/// one is a checker diagnostic, per the `@`-annotation precedent, ADR 0111), and
705/// interpreted through the typed accessors below.
706///
707/// `Access-Control-Allow-Methods` is deliberately **not** a field — it is derived
708/// from the service's routes at emit time (the routes already enumerate the
709/// methods; a restated list would drift). Likewise `Allow-Headers` defaults to
710/// `content-type` (+ `Authorization` when a Bearer route exists) and is only
711/// stored here when the author overrides it.
712#[derive(Debug, Clone)]
713pub struct CorsPolicy {
714    /// The `cors { }` fields as written, in source order. Field names are
715    /// validated against the closed set (`origins`/`headers`/`credentials`/
716    /// `maxAge`) by the checker, not the parser.
717    pub fields: Vec<CorsField>,
718    pub span: Span,
719    /// #1786: comments before the closing `}`, an orphaned doc block among
720    /// them.
721    pub trailing_comments: Vec<Comment>,
722    pub trivia: Trivia,
723}
724
725/// One `name: value` field inside a `cors { }` policy (v0.131).
726#[derive(Debug, Clone)]
727pub struct CorsField {
728    pub name: Ident,
729    pub value: Expr,
730    pub span: Span,
731    /// #1786: the field's own comments, above it and at the end of its line.
732    pub trivia: Trivia,
733}
734
735impl CorsPolicy {
736    /// The raw value expression for a field, by name (the last one wins if a
737    /// field is repeated — the checker flags the duplicate separately).
738    pub fn field(&self, name: &str) -> Option<&Expr> {
739        self.fields
740            .iter()
741            .rev()
742            .find(|f| f.name.name == name)
743            .map(|f| &f.value)
744    }
745
746    /// The allowed origins — the string literals of the `origins:` list. An
747    /// absent or malformed field yields an empty list (the checker has already
748    /// reported the shape error; the emitter fails closed on an empty list).
749    pub fn origins(&self) -> Vec<String> {
750        Self::str_list(self.field("origins")).unwrap_or_default()
751    }
752
753    /// `true` iff `origins` is exactly the wildcard `["*"]`.
754    pub fn is_wildcard(&self) -> bool {
755        let os = self.origins();
756        os.len() == 1 && os[0] == "*"
757    }
758
759    /// Whether credentialed requests are allowed (`credentials: true`); defaults
760    /// to `false` when the field is absent.
761    pub fn credentials(&self) -> bool {
762        matches!(
763            self.field("credentials").map(|e| &e.kind),
764            Some(ExprKind::BoolLit(true))
765        )
766    }
767
768    /// The explicit `Access-Control-Allow-Headers` override, if the author gave
769    /// a `headers:` list; `None` leaves the emitter to apply its smart default.
770    pub fn allow_headers(&self) -> Option<Vec<String>> {
771        self.field("headers").and_then(Self::str_list_of)
772    }
773
774    /// The `Access-Control-Max-Age` in whole seconds, if a `maxAge:` duration was
775    /// given; `None` leaves the header off (the browser default).
776    pub fn max_age_secs(&self) -> Option<i64> {
777        match self.field("maxAge").map(|e| &e.kind) {
778            Some(ExprKind::DurationLit { millis, .. }) => Some(millis / 1_000),
779            _ => None,
780        }
781    }
782
783    /// Interpret an expression as a list of string literals, if it is one.
784    fn str_list(expr: Option<&Expr>) -> Option<Vec<String>> {
785        expr.and_then(Self::str_list_of)
786    }
787
788    fn str_list_of(expr: &Expr) -> Option<Vec<String>> {
789        match &expr.kind {
790            ExprKind::ListLit(items) => items
791                .iter()
792                .map(|e| match &e.kind {
793                    ExprKind::StrLit(s) => Some(s.clone()),
794                    _ => None,
795                })
796                .collect(),
797            _ => None,
798        }
799    }
800}
801
802/// A security-headers policy on a `from http` service (v0.141, ADR 0164): the
803/// `security { }` section in the service body. Parsed leniently as a list of
804/// `name: value` fields (an unknown one is a checker diagnostic, per the CORS /
805/// `@`-annotation precedent) and interpreted through the typed accessors below.
806///
807/// The closed set is `nosniff` (a `Bool`, default `true` — stamps
808/// `X-Content-Type-Options: nosniff`) and `hsts` (a positive `Duration`, opt-in —
809/// stamps `Strict-Transport-Security: max-age=…`). Unlike `cors`, the *safe*
810/// header is on by default: a `from http` service with no `security { }` still
811/// stamps `nosniff`, because a security header you have to remember to switch on
812/// is the one you forget (ADR 0164 DECISION A).
813#[derive(Debug, Clone)]
814pub struct SecurityPolicy {
815    /// The `security { }` fields as written, in source order. Field names are
816    /// validated against the closed set (`hsts`/`nosniff`) by the checker, not
817    /// the parser.
818    pub fields: Vec<SecurityField>,
819    pub span: Span,
820    /// #1786: comments before the closing `}`, an orphaned doc block among
821    /// them.
822    pub trailing_comments: Vec<Comment>,
823    pub trivia: Trivia,
824}
825
826/// One `name: value` field inside a `security { }` policy (v0.141).
827#[derive(Debug, Clone)]
828pub struct SecurityField {
829    pub name: Ident,
830    pub value: Expr,
831    pub span: Span,
832    /// #1786: the field's own comments, above it and at the end of its line.
833    pub trivia: Trivia,
834}
835
836impl SecurityPolicy {
837    /// The raw value expression for a field, by name (the last one wins if a
838    /// field is repeated — the checker flags the duplicate separately).
839    pub fn field(&self, name: &str) -> Option<&Expr> {
840        self.fields
841            .iter()
842            .rev()
843            .find(|f| f.name.name == name)
844            .map(|f| &f.value)
845    }
846
847    /// Whether `X-Content-Type-Options: nosniff` is stamped. Defaults to `true`
848    /// (the safe default, ADR 0164 DECISION A); only an explicit `nosniff: false`
849    /// opts out. A malformed value has already been reported by the checker; it
850    /// falls back to the safe default here.
851    pub fn nosniff(&self) -> bool {
852        !matches!(
853            self.field("nosniff").map(|e| &e.kind),
854            Some(ExprKind::BoolLit(false))
855        )
856    }
857
858    /// The `Strict-Transport-Security` `max-age` in whole seconds, if the author
859    /// opted in with an `hsts:` duration; `None` leaves HSTS off (the default —
860    /// HSTS pins the browser to HTTPS and is a deliberate opt-in, DECISION A).
861    pub fn hsts_max_age_secs(&self) -> Option<i64> {
862        match self.field("hsts").map(|e| &e.kind) {
863            Some(ExprKind::DurationLit { millis, .. }) => Some(millis / 1_000),
864            _ => None,
865        }
866    }
867}
868
869/// A request-body-size policy on a `from http` service (v0.142, ADR 0165): the
870/// `limits { }` section in the service body. Parsed leniently as a list of
871/// `name: value` fields (an unknown one is a checker diagnostic, per the CORS /
872/// `security` / `@`-annotation precedent) and interpreted through the typed
873/// accessor below.
874///
875/// The closed set is `maxBody` — a positive `Int` byte count (there is no byte
876/// `Size` literal yet; a `1.mb`-style literal is a named follow-on, the
877/// `Duration` playbook). Unlike `security`, this is opt-in: a service with no
878/// `limits { }` (and no route `@limit`) has no cap and emits byte-for-byte
879/// unchanged output (ADR 0165 DECISION E — the CORS posture).
880#[derive(Debug, Clone)]
881pub struct LimitsPolicy {
882    /// The `limits { }` fields as written, in source order. Field names are
883    /// validated against the closed set (`maxBody`) by the checker, not the
884    /// parser.
885    pub fields: Vec<LimitsField>,
886    pub span: Span,
887    /// #1786: comments before the closing `}`, an orphaned doc block among
888    /// them.
889    pub trailing_comments: Vec<Comment>,
890    pub trivia: Trivia,
891}
892
893/// One `name: value` field inside a `limits { }` policy (v0.142).
894#[derive(Debug, Clone)]
895pub struct LimitsField {
896    pub name: Ident,
897    pub value: Expr,
898    pub span: Span,
899    /// #1786: the field's own comments, above it and at the end of its line.
900    pub trivia: Trivia,
901}
902
903impl LimitsPolicy {
904    /// The raw value expression for a field, by name (the last one wins if a
905    /// field is repeated — the checker flags the duplicate separately).
906    pub fn field(&self, name: &str) -> Option<&Expr> {
907        self.fields
908            .iter()
909            .rev()
910            .find(|f| f.name.name == name)
911            .map(|f| &f.value)
912    }
913
914    /// The service-wide maximum request-body size in bytes, if the author gave a
915    /// positive `maxBody:` `Int` literal; `None` leaves the service without a
916    /// default cap. A malformed or non-positive value has already been reported
917    /// by the checker; it falls back to `None` here (no cap).
918    pub fn max_body(&self) -> Option<i64> {
919        match self.field("maxBody").map(|e| &e.kind) {
920            Some(ExprKind::IntLit { value, .. }) if *value > 0 => Some(*value),
921            _ => None,
922        }
923    }
924}
925
926/// The protocol a service conforms to — declared on the header via
927/// `from <protocol>` (v0.44). `Call` is the default (no `from` clause): a
928/// contract-mediated internal-RPC surface, not a wire protocol. Multi-endpoint
929/// protocols (`Http`, `Cron`) carry no binding — the endpoint lives on each
930/// handler; single-binding `Queue` carries its queue name.
931#[derive(Debug, Clone)]
932pub enum ServiceProtocol {
933    /// No `from` clause: the service holds `on call` handlers only.
934    Call,
935    /// `from http` — many routes; each handler is `on <Method>("route")`.
936    Http,
937    /// `from cron` — many schedules; each handler is `on schedule("expr")`.
938    Cron,
939    /// `from queue("name")` — one bound queue; handlers are `on message(...)`.
940    Queue { name: String },
941    /// `from websocket(in: ClientFrame, out: ServerFrame)` — a held WebSocket
942    /// connection (v0.103, real-time track slice 3). `in_type` is the inbound
943    /// frame type (client→server, decoded and routed as typed agent messages);
944    /// `out_type` is the server→client frame type the held `Connection[out_type]`
945    /// carries. The service holds exactly one `on open` handler (edge auth via
946    /// `by`, then transfer of the connection to an agent).
947    WebSocket { in_type: TypeRef, out_type: TypeRef },
948    /// `from Events(E)` or `from Events(E { field: value, .. })`, optionally
949    /// followed by `via schema(N)` — a subscriber to event type `E`,
950    /// optionally filtered by a structural payload pattern (Events track,
951    /// slice 0 spine #936; the pattern is slice 1) and/or the envelope's
952    /// `schemaVersion` (slice 4). `Events`, capitalised, is matched as plain
953    /// `Ident` text the same way `websocket` is — it names the `Events`
954    /// capability directly (every first-party capability is already an
955    /// unreserved PascalCase identifier), not a built-in type name, so no
956    /// lexer reservation. `pattern` and `schema_dispatch` are independent:
957    /// a service may carry either, both, or neither.
958    Events {
959        event_type: TypeRef,
960        pattern: Option<EventPattern>,
961        schema_dispatch: Option<SchemaDispatch>,
962    },
963}
964
965/// An agent declaration (v0.5 §3.6). Agents are state-bearing entities
966/// with their own handlers.
967#[derive(Debug, Clone)]
968pub struct AgentDecl {
969    pub name: Ident,
970    /// `key id: Type` — the identifier-typed value identifying instances.
971    pub key_name: Ident,
972    pub key_type: TypeRef,
973    /// #1788: comments above the `key` line and at its end. A comment on the
974    /// agent's `{` line leads the key too.
975    pub key_trivia: Trivia,
976    /// `store` fields (v0.81, storage track) — each an access-pattern slot of a
977    /// declared storage kind (`Cell`/`Map`/…). The successor to the removed
978    /// `state { }` record (ADR 0108); every agent declares its state this way.
979    pub store_fields: Vec<StoreField>,
980    /// Invariants (v0.80 §14) — universally-quantified predicates over the
981    /// agent's `store` fields. The phase sits between the fields and the
982    /// handlers; each is checked against the state staged by a handler's writes
983    /// before it commits.
984    pub invariants: Vec<Invariant>,
985    /// Step invariants (v0.116 §, testing track slice 4) — named predicates over
986    /// the pre-/post-commit state *pair* (`old`/`new`), checked at the commit
987    /// boundary beside [`invariants`], from the second commit onward. Widen the
988    /// invariant subject from a snapshot to a step (ADR 0144 — one predicate
989    /// surface).
990    ///
991    /// [`invariants`]: AgentDecl::invariants
992    pub transitions: Vec<Transition>,
993    pub handlers: Vec<Handler>,
994    pub documentation: Option<String>,
995    pub span: Span,
996    /// #1756: comments before the closing `}`, an orphaned doc block among
997    /// them, so the formatter keeps them.
998    pub trailing_comments: Vec<Comment>,
999    pub trivia: Trivia,
1000}
1001
1002/// A `store` field (v0.81, storage track). Each is an access-pattern slot of a
1003/// declared storage kind: `store <name>: <Kind>[…] [@annotations] [= <init>]`.
1004/// The kind and its element type are carried as an ordinary [`TypeRef`]
1005/// (`Cell[Int]`, `Map[K, V]`); the checker restricts which heads are storage
1006/// kinds. Access-pattern annotations (`@indexed`, …) parse into [`annotations`]
1007/// (v0.85, ADR 0111); the checker validates them against the closed registry.
1008///
1009/// [`annotations`]: StoreField::annotations
1010#[derive(Debug, Clone)]
1011pub struct StoreField {
1012    pub name: Ident,
1013    /// The storage kind and its element type(s): `Cell[Int]`, `Map[K, V]`. A
1014    /// dedicated [`StoreKind`] rather than a [`TypeRef`] — storage kinds are not
1015    /// value types, and the checker dispatches kind-aware operations on the head.
1016    pub kind: StoreKind,
1017    /// Storage annotations on the field (v0.85, ADR 0111): `@ttl(5.minutes)`,
1018    /// `@indexed(by: orderId)`. Parsed in declaration order (after the kind,
1019    /// before the initialiser); the checker validates names against the closed
1020    /// registry and gates each to the slice that implements it.
1021    pub annotations: Vec<Annotation>,
1022    /// The fresh-key initial value (`= expr`), if given — same disposition as a
1023    /// `state` field's initialiser (ADRs 0003/0004 carry forward).
1024    pub init: Option<Expr>,
1025    pub documentation: Option<String>,
1026    pub span: Span,
1027    pub trivia: Trivia,
1028}
1029
1030/// A storage annotation on a `store` field (v0.85, storage track; ADR 0111):
1031/// `@<name>(<args>)`. The `name` is matched against the closed registry
1032/// (`@indexed`/`@ttl`/`@retain`/`@bounded`) by the checker; the grammar accepts
1033/// any identifier so an unknown name is a checker diagnostic, not a parse error.
1034/// Arguments are compile-time metadata, restricted to literals (and the `by:`
1035/// field-name labels of `@indexed`) by the checker per ADR 0111 D4.
1036#[derive(Debug, Clone)]
1037pub struct Annotation {
1038    pub name: Ident,
1039    pub args: Vec<AnnotationArg>,
1040    pub span: Span,
1041}
1042
1043/// A single annotation argument (v0.85; ADR 0111): an optional `label:` followed
1044/// by a value expression — `by: orderId` (labelled) or `5.minutes` (positional).
1045/// The value is parsed as an ordinary [`Expr`] so the duration-literal form
1046/// (`5.minutes`, landing with the `Duration` slice) needs no special grammar;
1047/// the checker restricts it to a literal where the annotation is functional.
1048#[derive(Debug, Clone)]
1049pub struct AnnotationArg {
1050    pub label: Option<Ident>,
1051    pub value: Expr,
1052    pub span: Span,
1053}
1054
1055/// A storage kind applied to its element type(s) (v0.81): `Cell[Int]`,
1056/// `Map[ReservationId, Reservation]`. The `head` is the kind name (`Cell`,
1057/// `Map`, `Set`, `Log`, `Queue`, `Cache`); the checker validates it against the
1058/// closed catalogue. Element types are ordinary [`TypeRef`]s. Refined element
1059/// types (`Cell[Int where NonNegative]`) ride a later slice (parse_type_ref does
1060/// not yet accept an inline refinement in type-argument position).
1061#[derive(Debug, Clone)]
1062pub struct StoreKind {
1063    pub head: Ident,
1064    pub args: Vec<TypeRef>,
1065    pub span: Span,
1066}
1067
1068/// An agent invariant (v0.80 §14). A named predicate over the agent's state
1069/// fields that must hold of every committed state; a commit that would violate
1070/// it faults (`InvariantViolation`) before the state is persisted. The
1071/// predicate references state fields by bare name, mirroring the design-notes
1072/// worked examples (`status == Paid implies paymentRef.isSome()`).
1073#[derive(Debug, Clone)]
1074pub struct Invariant {
1075    pub name: Ident,
1076    /// The predicate expression — an ordinary `Bool`-typed expression over the
1077    /// state fields, plus `implies` and `is`. The parsed-predicate-on-a-
1078    /// declaration shape mirrors [`ActorRefinement::predicate`].
1079    pub predicate: Expr,
1080    pub documentation: Option<String>,
1081    pub span: Span,
1082    pub trivia: Trivia,
1083}
1084
1085/// An agent step invariant (v0.116 §, testing track slice 4). A named predicate
1086/// over the *pair* of committed states — the pre-commit `old` and the proposed
1087/// `new`, each the agent's state record — that must hold of every state move; a
1088/// commit that would violate it faults (`InvariantViolation`) before the state is
1089/// persisted, exactly as a snapshot [`Invariant`] does. Widens the invariant
1090/// subject from a snapshot to a step (ADR 0144 — one predicate surface); the
1091/// predicate reuses the invariant surface (`implies`/`is`/pure methods) with
1092/// `old`/`new` bound contextually (`old.status is Paid implies new.status is
1093/// Paid`).
1094#[derive(Debug, Clone)]
1095pub struct Transition {
1096    pub name: Ident,
1097    /// The predicate expression — an ordinary `Bool`-typed expression over the
1098    /// `old` and `new` state records, with `implies`/`is` and pure methods,
1099    /// mirroring [`Invariant`].
1100    pub predicate: Expr,
1101    pub documentation: Option<String>,
1102    pub span: Span,
1103    pub trivia: Trivia,
1104}
1105
1106/// A function contract clause (v0.115 §, testing track slice 3). A named
1107/// predicate on a `fn` signature — a `requires` (precondition) or `ensures`
1108/// (postcondition). A contract is the invariant predicate attached to a
1109/// function (ADR 0144 — one predicate surface): the predicate is a pure `Bool`
1110/// expression over the parameters (`requires`) or the parameters plus `result`
1111/// (`ensures`), with `implies`/`is` and pure methods, mirroring [`Invariant`].
1112/// The name rides the failure report and the redundant-test dedup.
1113#[derive(Debug, Clone)]
1114pub struct Contract {
1115    pub name: Ident,
1116    /// The predicate expression — an ordinary `Bool`-typed expression over the
1117    /// parameters (and, for an `ensures`, the contextual `result` binding).
1118    pub predicate: Expr,
1119    pub span: Span,
1120}
1121
1122/// An actor declaration (v0.45 §3.7). An actor is a nominal *contract type*
1123/// describing an external party at a boundary — not a runnable entity. A
1124/// handler consumes an actor on its `by` clause; the boundary verifies the
1125/// declared `auth` scheme and mints a sealed identity (`name.identity`).
1126#[derive(Debug, Clone)]
1127pub struct ActorDecl {
1128    pub name: Ident,
1129    /// The authentication scheme from `auth = <Scheme>`, stored as the raw
1130    /// identifier. The checker classifies it: `None`/`Internal`/`Bearer` are
1131    /// admitted; `Signature` is reserved-and-rejected
1132    /// (`bynk.actor.scheme_unsupported`); anything else is
1133    /// `bynk.actor.unknown_scheme`. `None` for the refinement form.
1134    pub auth: Option<Ident>,
1135    /// The scheme's keyed config from `auth = Scheme(key = value, …)` (v0.47
1136    /// `Bearer(secret = "…")`; v0.51 generalised for `Signature(secret, header,
1137    /// timestamp?, tolerance?)`). Empty for schemes/forms with no config. The
1138    /// checker validates which keys each scheme requires/allows.
1139    pub auth_config: Vec<SchemeArg>,
1140    /// The optional identity type from `, identity = <T>`. Absent ⇒ the
1141    /// scheme default (`()` for `None`; a sealed `CallerId` for the `Internal`
1142    /// `on call` channel, `()` for other `Internal` channels).
1143    pub identity: Option<TypeRef>,
1144    /// The refinement form `actor Admin = Base where <predicate>` — narrows a
1145    /// base actor by an authorisation claim (ADR 0091). The predicate is parsed
1146    /// as a full expression; a static-semantics rule restricts it to the closed
1147    /// actor-claim catalogue (`hasClaim`/`claimEquals` over a `Bearer` base;
1148    /// `bynk.actor.refinement_predicate_unsupported` / `…_base_unsupported`).
1149    pub refinement: Option<ActorRefinement>,
1150    pub documentation: Option<String>,
1151    pub span: Span,
1152    pub trivia: Trivia,
1153    /// #1797: comments above the `auth` entry and at the end of its line. A
1154    /// comment on the actor's `{` line leads `auth` too. Empty for the
1155    /// refinement form.
1156    pub auth_trivia: Trivia,
1157    /// #1797: comments above the `identity` entry and at the end of its line.
1158    /// Empty when there is no `identity`.
1159    pub identity_trivia: Trivia,
1160    /// #1797: comments before the actor body's closing `}`.
1161    pub trailing_comments: Vec<Comment>,
1162}
1163
1164impl ActorDecl {
1165    /// The value of a scheme config arg by key, if present (e.g. `secret`,
1166    /// `header`).
1167    pub fn scheme_arg(&self, key: &str) -> Option<&SchemeArg> {
1168        self.auth_config.iter().find(|a| a.key.name == key)
1169    }
1170}
1171
1172/// One `key = value` argument in a scheme config (`Scheme(key = value, …)`).
1173#[derive(Debug, Clone)]
1174pub struct SchemeArg {
1175    pub key: Ident,
1176    pub value: SchemeArgValue,
1177    /// Span of the value, for diagnostics.
1178    pub span: Span,
1179}
1180
1181/// A scheme config arg value — a string literal or an integer.
1182#[derive(Debug, Clone)]
1183pub enum SchemeArgValue {
1184    Str(String),
1185    Int(i64),
1186}
1187
1188impl SchemeArgValue {
1189    pub fn as_str(&self) -> Option<&str> {
1190        match self {
1191            SchemeArgValue::Str(s) => Some(s),
1192            SchemeArgValue::Int(_) => None,
1193        }
1194    }
1195    pub fn as_int(&self) -> Option<i64> {
1196        match self {
1197            SchemeArgValue::Int(n) => Some(*n),
1198            SchemeArgValue::Str(_) => None,
1199        }
1200    }
1201}
1202
1203/// The reserved refinement form `actor Admin = User where <predicate>` (Q3).
1204/// Parsed in Foundations so the grammar is fixed; admission is a later slice.
1205#[derive(Debug, Clone)]
1206pub struct ActorRefinement {
1207    /// The base actor being refined.
1208    pub base: Ident,
1209    /// The `where` predicate. Parsed but not yet checked.
1210    pub predicate: Expr,
1211    pub span: Span,
1212}
1213
1214/// The `by (<binder>:)? <Actor>` clause on a handler (v0.45; binder optional in
1215/// v0.50). Names the actor contract the handler consumes; when a `binder` is
1216/// given, the verified identity binds to it and is read as `binder.identity`.
1217/// Omitting the binder (`by <Actor>`) declares-and-verifies the contract without
1218/// capturing the identity — for anonymous or verify-and-discard handlers. Sits
1219/// after the protocol config and before the parameters.
1220#[derive(Debug, Clone)]
1221pub struct ByClause {
1222    /// The identity binder, if the handler consumes the identity. `None` for the
1223    /// binder-less `by <Actor>` form. Required when `actors` names more than one
1224    /// (a sum is resolved by matching on the bound actor).
1225    pub binder: Option<Ident>,
1226    /// The actor contract(s) referenced — each a local actor decl or a prelude
1227    /// actor. A single name is the ordinary single-actor handler; more than one
1228    /// (`by who: A | B`, v0.52) is an **ordered sum of peer actors** resolved
1229    /// first-wins, the body matching on the resolved actor. Always non-empty.
1230    pub actors: Vec<Ident>,
1231    pub span: Span,
1232}
1233
1234impl ByClause {
1235    /// The first (and, for a single-actor handler, only) actor contract named.
1236    pub fn primary(&self) -> &Ident {
1237        &self.actors[0]
1238    }
1239    /// Whether this `by` clause names an ordered sum of peer actors (`A | B`).
1240    pub fn is_sum(&self) -> bool {
1241        self.actors.len() > 1
1242    }
1243}
1244
1245/// v0.182 (testing-the-boundary Slice A, #664): a call-site actor clause on a
1246/// test-body `let x <- <service address> by <Actor>(<identity>)`. Distinct from
1247/// [`ByClause`] (the handler/header form): the *declaration* names which actor
1248/// may call and binds the verified identity, whereas the *call site* names the
1249/// actor the case is acting as and supplies the identity value. A unit-identity
1250/// actor (`Visitor`, and cron/queue's internal actors) carries no `identity`.
1251#[derive(Debug, Clone)]
1252pub struct CallSiteActor {
1253    /// The actor the case acts as — a local actor decl or a prelude actor.
1254    pub actor: Ident,
1255    /// The supplied identity value (`"bob"` in `by User("bob")`), or `None` for a
1256    /// unit-identity actor written `by Visitor` with no argument.
1257    pub identity: Option<Box<Expr>>,
1258    pub span: Span,
1259}
1260
1261/// A handler block — `on call(args) -> T given C1, C2 { body }`.
1262/// Used by both services and agents.
1263#[derive(Debug, Clone)]
1264pub struct Handler {
1265    pub kind: HandlerKind,
1266    /// Handler-position annotations (v0.140, ADR 0163): `@cache(maxAge: 5.minutes)`
1267    /// written immediately before `on <METHOD>(…)`. Reuses the [`Annotation`] AST
1268    /// shared with `store` fields (ADR 0111); the grammar accepts any `@name(args)`
1269    /// so an unknown name is a project-validation diagnostic, not a parse error. The
1270    /// first handler-position annotation surface — empty for every handler that
1271    /// carries none.
1272    pub annotations: Vec<Annotation>,
1273    /// For agent handlers, the method-style handler name (e.g.
1274    /// `on call addItem(...)`). For service handlers, this is None (just
1275    /// `on call(...)`).
1276    pub method_name: Option<Ident>,
1277    /// The `by <binder>: <Actor>` clause (v0.45), if present. Service handlers
1278    /// only; an absent clause inherits the protocol's default actor.
1279    pub by_clause: Option<ByClause>,
1280    pub params: Vec<Param>,
1281    pub return_type: TypeRef,
1282    pub given: Vec<CapRef>,
1283    pub body: Block,
1284    pub documentation: Option<String>,
1285    pub span: Span,
1286    pub trivia: Trivia,
1287}
1288
1289/// `Hash` (P8.5, #1516): a `HandlerKind` value is a body-free, span-free
1290/// discriminant, added so a `DefId`-keyed handler identity could carry it
1291/// (`bynk-check`'s former `queries::HandlerDefId`, deleted by #1537 — kept
1292/// here because the derive is harmless, and a rebuild against ADR 0417's
1293/// shape would need it again). Purely additive; no existing
1294/// caller matches on hashing behaviour.
1295#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1296pub enum HandlerKind {
1297    /// `on call(...)` — typed RPC (the only kind in v0.5).
1298    Call,
1299    /// `on http METHOD "path"` — external-facing HTTP route (v0.9).
1300    Http { method: HttpMethod, path: String },
1301    /// `on cron "expr"` — scheduled task; `expr` is a 5-field cron
1302    /// expression (v0.10a).
1303    Cron { expr: String },
1304    /// `on message(m: T)` — a message off the service's bound queue. The queue
1305    /// binding lives on the service's `ServiceProtocol::Queue` (v0.44).
1306    Message,
1307    /// `on open ...` — the WebSocket upgrade handler (v0.103, real-time track
1308    /// slice 3). Exactly one per `from websocket` service; carries a mandatory
1309    /// `by` clause (edge auth) and receives a fresh owned `Connection[out]`.
1310    Open,
1311    /// `on close ...` — the WebSocket close handler (v0.106, real-time track slice
1312    /// 3b-iii). Optional, ≤1 per `from websocket` service; runs when the socket
1313    /// closes. Like `on open`, edge-authenticated (`by`), with the identity/params
1314    /// recovered from the socket attachment (set at `on open`). (A `from websocket`
1315    /// `on message` reuses [`HandlerKind::Message`], disambiguated by the protocol.)
1316    Close,
1317    /// `on event(e: E)` — one emission of a `from Events(E)` service's
1318    /// subscribed event type (Events track, slice 0, spine #936). No
1319    /// envelope parameter yet (slice 2). `event`, like `message`/`open`/
1320    /// `close`/`schedule`, is matched by plain ident text at the fixed
1321    /// position right after `on`, with no lexer reservation — an ordinary
1322    /// identifier everywhere else in the grammar.
1323    Event,
1324}
1325
1326/// HTTP methods supported by `on http` handlers (v0.9).
1327#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1328pub enum HttpMethod {
1329    Get,
1330    Post,
1331    Put,
1332    Patch,
1333    Delete,
1334}
1335
1336impl HttpMethod {
1337    pub fn as_str(self) -> &'static str {
1338        match self {
1339            HttpMethod::Get => "GET",
1340            HttpMethod::Post => "POST",
1341            HttpMethod::Put => "PUT",
1342            HttpMethod::Patch => "PATCH",
1343            HttpMethod::Delete => "DELETE",
1344        }
1345    }
1346
1347    pub fn from_ident(s: &str) -> Option<HttpMethod> {
1348        match s {
1349            "GET" => Some(HttpMethod::Get),
1350            "POST" => Some(HttpMethod::Post),
1351            "PUT" => Some(HttpMethod::Put),
1352            "PATCH" => Some(HttpMethod::Patch),
1353            "DELETE" => Some(HttpMethod::Delete),
1354            _ => None,
1355        }
1356    }
1357
1358    /// True if this method conventionally has no request body.
1359    pub fn forbids_body(self) -> bool {
1360        matches!(self, HttpMethod::Get | HttpMethod::Delete)
1361    }
1362}
1363
1364/// Payload shape of an `HttpResult[T]` variant (v0.9 §3.3).
1365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1366pub enum HttpVariantPayload {
1367    /// No payload (e.g. `NoContent`, `Unauthorized`).
1368    None,
1369    /// Carries a value of the `HttpResult` type parameter `T`.
1370    Value,
1371    /// Carries a `String` message (e.g. `BadRequest`, `Conflict`).
1372    Message,
1373    /// Carries a `String` target URL, emitted as a `Location` header — the
1374    /// redirect variants (`Found`, `SeeOther`, `PermanentRedirect`, …).
1375    Location,
1376    /// Carries a `Stream[String]`, emitted as an SSE (`text/event-stream`)
1377    /// streaming body — the `Streaming` (200) variant (v0.101, real-time track
1378    /// slice 1).
1379    Streamed,
1380    /// Carries `(body: Bytes, contentType: String)` — the author-owned raw body
1381    /// written straight into the response with the declared `content-type` and
1382    /// **no codec** (the typed-wire guarantee is deliberately off). The `Raw`
1383    /// (200) variant (v0.111); the first two-argument payload shape.
1384    Raw,
1385}
1386
1387/// One variant of the built-in `HttpResult[T]` sum (v0.9 §3.3).
1388#[derive(Debug, Clone, Copy)]
1389pub struct HttpVariant {
1390    pub name: &'static str,
1391    pub payload: HttpVariantPayload,
1392    pub status: u16,
1393}
1394
1395/// All `HttpResult[T]` variants, in declaration order (ascending status). The
1396/// vocabulary tracks the common, modern HTTP status codes (RFC 9110): success
1397/// and created/accepted (`Value`), redirects carrying a `Location` URL, and
1398/// the client/server failures that handlers routinely return (`Message` when
1399/// an explanation helps the caller, `None` for self-describing statuses).
1400pub const HTTP_VARIANTS: &[HttpVariant] = &[
1401    // ── 2xx success ──────────────────────────────────────────────────────
1402    HttpVariant {
1403        name: "Ok",
1404        payload: HttpVariantPayload::Value,
1405        status: 200,
1406    },
1407    // v0.101 (real-time track slice 1): a 200 whose body is a streamed
1408    // `Stream[String]`, SSE-framed. Status precedes the body, so streaming is
1409    // 200-only — pre-stream failures are ordinary variants returned instead.
1410    HttpVariant {
1411        name: "Streaming",
1412        payload: HttpVariantPayload::Streamed,
1413        status: 200,
1414    },
1415    // v0.111: a 200 whose body is an author-owned `Bytes` written straight into
1416    // the response with the declared `content-type` — no codec runs. 200-only,
1417    // like `Streaming`: it serves service-tier raw bodies (`robots.txt`,
1418    // `sitemap.xml`, feeds, a QR PNG), not custom-status error pages.
1419    HttpVariant {
1420        name: "Raw",
1421        payload: HttpVariantPayload::Raw,
1422        status: 200,
1423    },
1424    HttpVariant {
1425        name: "Created",
1426        payload: HttpVariantPayload::Value,
1427        status: 201,
1428    },
1429    HttpVariant {
1430        name: "Accepted",
1431        payload: HttpVariantPayload::Value,
1432        status: 202,
1433    },
1434    HttpVariant {
1435        name: "NoContent",
1436        payload: HttpVariantPayload::None,
1437        status: 204,
1438    },
1439    // ── 3xx redirection (carry a `Location` URL) ─────────────────────────
1440    HttpVariant {
1441        name: "MovedPermanently",
1442        payload: HttpVariantPayload::Location,
1443        status: 301,
1444    },
1445    HttpVariant {
1446        name: "Found",
1447        payload: HttpVariantPayload::Location,
1448        status: 302,
1449    },
1450    HttpVariant {
1451        name: "SeeOther",
1452        payload: HttpVariantPayload::Location,
1453        status: 303,
1454    },
1455    HttpVariant {
1456        name: "TemporaryRedirect",
1457        payload: HttpVariantPayload::Location,
1458        status: 307,
1459    },
1460    HttpVariant {
1461        name: "PermanentRedirect",
1462        payload: HttpVariantPayload::Location,
1463        status: 308,
1464    },
1465    // ── 4xx client error ─────────────────────────────────────────────────
1466    HttpVariant {
1467        name: "BadRequest",
1468        payload: HttpVariantPayload::Message,
1469        status: 400,
1470    },
1471    HttpVariant {
1472        name: "Unauthorized",
1473        payload: HttpVariantPayload::None,
1474        status: 401,
1475    },
1476    HttpVariant {
1477        name: "Forbidden",
1478        payload: HttpVariantPayload::None,
1479        status: 403,
1480    },
1481    HttpVariant {
1482        name: "NotFound",
1483        payload: HttpVariantPayload::None,
1484        status: 404,
1485    },
1486    HttpVariant {
1487        name: "MethodNotAllowed",
1488        payload: HttpVariantPayload::None,
1489        status: 405,
1490    },
1491    HttpVariant {
1492        name: "NotAcceptable",
1493        payload: HttpVariantPayload::None,
1494        status: 406,
1495    },
1496    HttpVariant {
1497        name: "RequestTimeout",
1498        payload: HttpVariantPayload::None,
1499        status: 408,
1500    },
1501    HttpVariant {
1502        name: "Conflict",
1503        payload: HttpVariantPayload::Message,
1504        status: 409,
1505    },
1506    HttpVariant {
1507        name: "Gone",
1508        payload: HttpVariantPayload::None,
1509        status: 410,
1510    },
1511    HttpVariant {
1512        name: "LengthRequired",
1513        payload: HttpVariantPayload::None,
1514        status: 411,
1515    },
1516    HttpVariant {
1517        name: "PayloadTooLarge",
1518        payload: HttpVariantPayload::Message,
1519        status: 413,
1520    },
1521    HttpVariant {
1522        name: "UnsupportedMediaType",
1523        payload: HttpVariantPayload::Message,
1524        status: 415,
1525    },
1526    HttpVariant {
1527        name: "UnprocessableEntity",
1528        payload: HttpVariantPayload::Message,
1529        status: 422,
1530    },
1531    HttpVariant {
1532        name: "TooManyRequests",
1533        payload: HttpVariantPayload::Message,
1534        status: 429,
1535    },
1536    HttpVariant {
1537        name: "UnavailableForLegalReasons",
1538        payload: HttpVariantPayload::Message,
1539        status: 451,
1540    },
1541    // ── 5xx server error ─────────────────────────────────────────────────
1542    HttpVariant {
1543        name: "ServerError",
1544        payload: HttpVariantPayload::Message,
1545        status: 500,
1546    },
1547    HttpVariant {
1548        name: "NotImplemented",
1549        payload: HttpVariantPayload::Message,
1550        status: 501,
1551    },
1552    HttpVariant {
1553        name: "BadGateway",
1554        payload: HttpVariantPayload::Message,
1555        status: 502,
1556    },
1557    HttpVariant {
1558        name: "ServiceUnavailable",
1559        payload: HttpVariantPayload::Message,
1560        status: 503,
1561    },
1562    HttpVariant {
1563        name: "GatewayTimeout",
1564        payload: HttpVariantPayload::Message,
1565        status: 504,
1566    },
1567];
1568
1569/// Find an `HttpResult[T]` variant by name. Returns the variant info or
1570/// `None` if the name doesn't match.
1571pub fn http_variant(name: &str) -> Option<HttpVariant> {
1572    HTTP_VARIANTS.iter().copied().find(|v| v.name == name)
1573}
1574
1575/// Payload shape of a `QueueResult` variant (v0.44). Non-generic — a verdict
1576/// carries no value; `Retry` carries a `String` reason for the log path.
1577#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1578pub enum QueueVariantPayload {
1579    /// No payload (`Ack`).
1580    None,
1581    /// Carries a `String` reason (`Retry`).
1582    Message,
1583}
1584
1585/// One variant of the built-in `QueueResult` sum (v0.44).
1586#[derive(Debug, Clone, Copy)]
1587pub struct QueueVariant {
1588    pub name: &'static str,
1589    pub payload: QueueVariantPayload,
1590}
1591
1592/// All `QueueResult` variants, in declaration order. `Ack` confirms the
1593/// message; `Retry` redelivers it, carrying a reason for observability.
1594pub const QUEUE_VARIANTS: &[QueueVariant] = &[
1595    QueueVariant {
1596        name: "Ack",
1597        payload: QueueVariantPayload::None,
1598    },
1599    QueueVariant {
1600        name: "Retry",
1601        payload: QueueVariantPayload::Message,
1602    },
1603];
1604
1605/// Find a `QueueResult` variant by name.
1606pub fn queue_variant(name: &str) -> Option<QueueVariant> {
1607    QUEUE_VARIANTS.iter().copied().find(|v| v.name == name)
1608}
1609
1610#[derive(Debug, Clone)]
1611pub struct TypeDecl {
1612    pub name: Ident,
1613    /// `[T, U]` type parameters (v0.157, ADR 0183): empty for a non-generic
1614    /// type. A generic *record* type (`type Paginated[T] = { … }`) is the only
1615    /// generic body accepted; the checker rejects type parameters on refined /
1616    /// opaque / sum bodies. Mirrors [`FnDecl::type_params`].
1617    pub type_params: Vec<TypeParam>,
1618    pub body: TypeBody,
1619    /// Documentation block attached to this declaration (v0.3).
1620    pub documentation: Option<String>,
1621    pub span: Span,
1622    pub trivia: Trivia,
1623}
1624
1625/// `event Name = { fields }` — a typed fact a context may emit and other
1626/// contexts' subscriber services may receive (Events track, slice 0, spine
1627/// #936). Record body only in slice 0 — pattern refinement (subscription
1628/// side, slice 1) and default-valued fields for additive versioning (slice
1629/// 3a) both extend a record body, so nothing here forecloses them. An
1630/// optional `@schema(N)` annotation (slice 3b) asserts the event's current
1631/// wire schema version, embedded into `env.schemaVersion` at emission — see
1632/// [`EventDecl::schema_version`]. Legal only inside a `context` —
1633/// checker-enforced (`bynk.event.outside_context`), not grammar, mirroring
1634/// how `capability`/`provides` are commons-rejected at the parser while
1635/// `event` instead follows `messages`' precedent (ADR 0272) of parsing
1636/// uniformly and letting the checker place it, since unlike
1637/// `capability`/`provides` an `event` has no meaning to reject early inside
1638/// an `adapter` either.
1639#[derive(Debug, Clone)]
1640pub struct EventDecl {
1641    pub name: Ident,
1642    /// Every `@`-annotation attached to this declaration. The parser stays
1643    /// permissive (zero or more, same as `store` field / `messages`
1644    /// annotations); the closed registry (today: `@schema` alone) and its
1645    /// argument shape are a checker concern (`bynk.event.unknown_annotation`
1646    /// / `bynk.event.bad_schema_version`), not a parse error.
1647    pub annotations: Vec<Annotation>,
1648    pub body: RecordBody,
1649    /// Documentation block attached to this declaration.
1650    pub documentation: Option<String>,
1651    pub span: Span,
1652    pub trivia: Trivia,
1653}
1654
1655impl EventDecl {
1656    /// A synthetic `TypeDecl` with this event's name and record body, so an
1657    /// event registers into the ordinary `types` symbol table and reuses
1658    /// every existing type-reference/exports/consumes/construction check —
1659    /// no non-generic type parameters, no separate resolution path. Callers
1660    /// that need to know a name is specifically an *event* (owner-only
1661    /// emission, `from Events(E)`/`Events.emit[E]`'s "must be an event, not
1662    /// just any type" gate) track that separately, alongside this.
1663    ///
1664    /// Deliberately lossy: a `TypeDecl` has no `annotations`, so `@schema(N)`
1665    /// does not survive this conversion. Nothing downstream of this
1666    /// synthesis needs the event's schema version — only the emitter's own
1667    /// `Events.emit` lowering does, and it reads [`EventDecl::schema_version`]
1668    /// directly off the real declaration instead.
1669    pub fn as_type_decl(&self) -> TypeDecl {
1670        TypeDecl {
1671            name: self.name.clone(),
1672            type_params: Vec::new(),
1673            body: TypeBody::Record(self.body.clone()),
1674            documentation: self.documentation.clone(),
1675            span: self.span,
1676            trivia: self.trivia.clone(),
1677        }
1678    }
1679
1680    /// This event's declared wire schema version (Events slice 3b, #978):
1681    /// the positive `Int` literal argument of its sole `@schema(N)`
1682    /// annotation, or `1` if the annotation is absent — identical to every
1683    /// event's behaviour before this annotation existed. A malformed
1684    /// `@schema` (non-positive, non-literal, wrong arity, labelled, or
1685    /// duplicated) has already been reported by the checker
1686    /// (`bynk.event.bad_schema_version`); this falls back to `1` rather than
1687    /// re-deriving that diagnostic.
1688    pub fn schema_version(&self) -> i64 {
1689        self.annotations
1690            .iter()
1691            .find(|a| a.name.name == "schema")
1692            .and_then(|a| a.args.first())
1693            .and_then(|arg| match &arg.value.kind {
1694                ExprKind::IntLit { value, .. } if *value > 0 => Some(*value),
1695                _ => None,
1696            })
1697            .unwrap_or(1)
1698    }
1699}
1700
1701/// The structural filter on a `from Events(E { field: value, .. })`
1702/// subscription header (Events track, slice 1, spine #936) — deliver-and-filter:
1703/// every emission still reaches the fan-out mechanism, and the subscriber's
1704/// own generated handler evaluates this as a boolean guard before running the
1705/// body. Deliberately **not** a [`Pattern`] — an event is a plain record, not
1706/// a sum, so it has no tag for [`Pattern::Variant`] to test; extending the
1707/// shared `Pattern` enum to fit would touch parser/checker/emitter/fmt/
1708/// tree-sitter/LSP sites and drag in match-exhaustiveness semantics a
1709/// delivery filter does not need. This amends
1710/// [ADR 0286](../decisions/0286-events-pattern-dispatch-deliver-and-filter.md)'s
1711/// "no bespoke matching engine is introduced for Events" claim; its
1712/// deliver-and-filter decision is unchanged. No static narrowing: a matching
1713/// handler body still sees its parameter at its own declared type, never
1714/// narrowed to a listed field's specific value (deferred — narrowing needs a
1715/// singleton-variant type the checker does not have, and waits on the
1716/// refinement-propagation design question `design/bynk-type-system.md`
1717/// §2.5.4 names as still open).
1718#[derive(Debug, Clone)]
1719pub struct EventPattern {
1720    /// The listed fields, in source order. Never empty — a pattern with no
1721    /// fields has no shape (`from Events(E)`, no braces, is the pattern-less
1722    /// form; `from Events(E { })` is a parse error pointing at it).
1723    pub fields: Vec<EventPatternField>,
1724    /// The span of the required trailing `..` — every listed field leaves
1725    /// the rest of the record's fields unconstrained, and that must be
1726    /// written explicitly rather than implied.
1727    pub rest_span: Span,
1728    pub span: Span,
1729}
1730
1731/// One `name: value` entry in an [`EventPattern`].
1732#[derive(Debug, Clone)]
1733pub struct EventPatternField {
1734    pub name: Ident,
1735    pub value: EventPatternValue,
1736    pub span: Span,
1737}
1738
1739/// The value a pattern field is matched against. A closed set, mirroring
1740/// [`Pattern::Literal`]'s closed literal kinds plus a nullary sum-variant
1741/// reference — no nested record sub-patterns in v1 (slice 1 filters on
1742/// top-level fields only).
1743#[derive(Debug, Clone)]
1744pub enum EventPatternValue {
1745    /// An `Int`/`String`/`Bool` literal — matches the field by value equality.
1746    Literal { value: LiteralValue, span: Span },
1747    /// A nullary sum-type variant, optionally qualified: `Region.Domestic` or
1748    /// bare `Domestic` — both resolve against the field's declared sum type.
1749    /// A variant that carries a payload is rejected (`bynk.event.
1750    /// pattern_variant_payload`): testing only the tag while ignoring a
1751    /// payload would silently over-broaden the filter.
1752    Variant {
1753        /// `Some(Region)` for the qualified form, `None` for bare.
1754        type_name: Option<Ident>,
1755        variant: Ident,
1756        span: Span,
1757    },
1758}
1759
1760impl EventPattern {
1761    pub fn span(&self) -> Span {
1762        self.span
1763    }
1764}
1765
1766impl EventPatternValue {
1767    pub fn span(&self) -> Span {
1768        match self {
1769            EventPatternValue::Literal { span, .. } => *span,
1770            EventPatternValue::Variant { span, .. } => *span,
1771        }
1772    }
1773}
1774
1775/// A `via schema(...)` dispatch clause on a `from Events(...)` header
1776/// (Events track, slice 4, spine #936): filters delivery by the envelope's
1777/// `schemaVersion`, parallel to [`EventPattern`] but matched against the
1778/// envelope rather than the payload, and written after the `Events(...)`
1779/// header's closing `)` rather than inside it. Delivery is still
1780/// deliver-and-filter (unchanged from slice 1's ADR 0286): the fan-out
1781/// mechanism delivers every emission to every subscriber regardless, and
1782/// this becomes one more independently-evaluated runtime guard in the
1783/// subscriber's own generated handler — no cross-subscriber ambiguity
1784/// check (two sibling subscribers with the same or overlapping version
1785/// coverage are both legal, undiagnosed).
1786#[derive(Debug, Clone)]
1787pub struct SchemaDispatch {
1788    pub pattern: SchemaVersionPattern,
1789    pub span: Span,
1790}
1791
1792/// The pattern a `via schema(...)` clause matches `env.schemaVersion`
1793/// against. A closed set of one variant today — literal only, mirroring
1794/// `@schema(N)`'s own permissive-parse-then-checker-validate split (a
1795/// non-positive value is a checker error, not a parse error, for the same
1796/// diagnostic style). A future slice's range patterns (`via schema(2..)`)
1797/// are additive to this enum, not a breaking rename of every match site
1798/// this slice creates.
1799#[derive(Debug, Clone)]
1800pub enum SchemaVersionPattern {
1801    Literal(i64),
1802}
1803
1804/// The right-hand side of a `type` declaration. In v0/v0.1 only the
1805/// `Refined` variant existed; v0.2 adds records and sums; v0.3 adds opaque.
1806#[derive(Debug, Clone)]
1807pub enum TypeBody {
1808    /// Refined base type: `BaseType where refinement`.
1809    Refined {
1810        base: BaseType,
1811        base_span: Span,
1812        refinement: Option<Refinement>,
1813    },
1814    /// Record type: `{ field: T where ..., ... }`.
1815    Record(RecordBody),
1816    /// Sum type: pipe-form variants or `enum { ... }` shorthand.
1817    Sum(SumBody),
1818    /// Opaque base type: `opaque BaseType (where refinement)?` (v0.3 §3.4).
1819    /// Identity is nominal; the base type is hidden outside the defining commons.
1820    Opaque {
1821        base: BaseType,
1822        base_span: Span,
1823        refinement: Option<Refinement>,
1824    },
1825}
1826
1827/// Body of a record-type declaration (v0.2 §3.1).
1828#[derive(Debug, Clone)]
1829pub struct RecordBody {
1830    pub fields: Vec<RecordField>,
1831    pub span: Span,
1832    /// #1788: comments before the closing `}`.
1833    pub trailing_comments: Vec<Comment>,
1834}
1835
1836/// One field of a record type declaration. Each field may carry inline
1837/// refinement, which is enforced at construction time on the field's value.
1838#[derive(Debug, Clone)]
1839pub struct RecordField {
1840    pub name: Ident,
1841    pub type_ref: TypeRef,
1842    pub refinement: Option<Refinement>,
1843    /// v0.11: an optional initial-value expression. Only meaningful on agent
1844    /// `state` fields (the field's fresh-key value); ignored / rejected on
1845    /// record-type fields by the checker.
1846    pub init: Option<Expr>,
1847    pub span: Span,
1848    /// #1788: comments above the field and at the end of its line.
1849    pub trivia: Trivia,
1850}
1851
1852/// Body of a sum-type declaration (v0.2 §3.2).
1853#[derive(Debug, Clone)]
1854pub struct SumBody {
1855    pub variants: Vec<Variant>,
1856    /// v0.154 (ADR 0178): declared error embeddings — `embeds E as V, …` after
1857    /// the variants. Each says "an `E` value auto-wraps into variant `V`", which
1858    /// the `?` operator uses to convert a cross-context error without a manual
1859    /// `.mapErr`. Empty for a sum with no embeddings.
1860    pub embeds: Vec<EmbedsClause>,
1861    pub span: Span,
1862    /// #1794: comments before the closing `}` of an `enum { … }` body, or, in
1863    /// the pipe form, on their own lines between the last variant and its
1864    /// `embeds` clause.
1865    pub trailing_comments: Vec<Comment>,
1866}
1867
1868/// One `embeds <source_type> as <variant>` mapping in a sum body (v0.154, ADR
1869/// 0178). Declares that a value of `source_type` can be auto-wrapped into the
1870/// named single-payload `variant` of the enclosing sum.
1871#[derive(Debug, Clone)]
1872pub struct EmbedsClause {
1873    pub source_type: TypeRef,
1874    pub variant: Ident,
1875    pub span: Span,
1876}
1877
1878/// One variant of a sum type. Variants may have payload fields; a
1879/// payload-less variant is a simple tag.
1880#[derive(Debug, Clone)]
1881pub struct Variant {
1882    pub name: Ident,
1883    pub payload: Vec<VariantField>,
1884    pub span: Span,
1885    /// #1794: comments above the variant and at the end of its line. A comment
1886    /// on an `enum {` line leads the first variant, and so does one on the `=`
1887    /// line of a pipe-form sum. The last pipe-form variant's end-of-line
1888    /// comment is the type's own trailing comment unless an `embeds` clause
1889    /// follows it.
1890    pub trivia: Trivia,
1891}
1892
1893/// One payload field of a sum variant. Variant payload fields use named
1894/// declarations like record fields, but do not carry refinement in v0.2.
1895#[derive(Debug, Clone)]
1896pub struct VariantField {
1897    pub name: Ident,
1898    pub type_ref: TypeRef,
1899    pub span: Span,
1900}
1901
1902#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1903pub enum BaseType {
1904    Int,
1905    String,
1906    Bool,
1907    Float,
1908    /// `Duration` (v0.86, ADR 0112) — a span of time, a distinct base type
1909    /// erased to TS `number` carrying milliseconds (the `Clock` unit). Modelled
1910    /// on `Float`: Bynk-side-only, no implicit `Int` coercion (save the one
1911    /// sanctioned clock-math mix).
1912    Duration,
1913    /// `Instant` (v0.90, ADR 0114) — an absolute point in time, a distinct base
1914    /// type erased to TS `number` carrying Unix epoch milliseconds (the
1915    /// `Clock` unit). No literal (minted by `Clock.now()`); arithmetic composes
1916    /// with `Duration` (`Instant ± Duration -> Instant`, `Instant − Instant ->
1917    /// Duration`). Supersedes ADR 0112 D4's `Int`↔`Duration` clock-math mix.
1918    Instant,
1919    /// `Bytes` (v0.110, ADR 0142) — an immutable finite octet sequence, the
1920    /// seventh base type. Unlike its neighbours it does **not** erase to TS
1921    /// `number`: a `Bytes` lowers to a `Uint8Array`. No source literal
1922    /// (constructed via `Bytes.fromUtf8`/`fromBase64`/`empty`); `==` compares
1923    /// by content (real emitter codegen, not host `===`); wires as a base64
1924    /// JSON string; not `Map`-keyable and not orderable.
1925    Bytes,
1926}
1927
1928impl BaseType {
1929    pub fn name(self) -> &'static str {
1930        match self {
1931            BaseType::Int => "Int",
1932            BaseType::String => "String",
1933            BaseType::Bool => "Bool",
1934            BaseType::Float => "Float",
1935            BaseType::Duration => "Duration",
1936            BaseType::Instant => "Instant",
1937            BaseType::Bytes => "Bytes",
1938        }
1939    }
1940}
1941
1942/// A `Duration` literal unit (v0.86, ADR 0112) — the closed set of suffixes in a
1943/// `<int>.<unit>` literal. Each maps to a fixed millisecond factor (`Duration`
1944/// erases to `Int` milliseconds).
1945#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1946pub enum DurationUnit {
1947    Milliseconds,
1948    Seconds,
1949    Minutes,
1950    Hours,
1951    Days,
1952}
1953
1954impl DurationUnit {
1955    /// Resolve a unit name (`minutes`) to its variant, or `None` if it is not one
1956    /// of the closed set. Used by the parser to recognise an `<int>.<unit>`
1957    /// literal; an unrecognised name leaves the expression a field access.
1958    pub fn from_name(name: &str) -> Option<Self> {
1959        Some(match name {
1960            "milliseconds" => DurationUnit::Milliseconds,
1961            "seconds" => DurationUnit::Seconds,
1962            "minutes" => DurationUnit::Minutes,
1963            "hours" => DurationUnit::Hours,
1964            "days" => DurationUnit::Days,
1965            _ => return None,
1966        })
1967    }
1968
1969    /// The unit name as written.
1970    pub fn name(self) -> &'static str {
1971        match self {
1972            DurationUnit::Milliseconds => "milliseconds",
1973            DurationUnit::Seconds => "seconds",
1974            DurationUnit::Minutes => "minutes",
1975            DurationUnit::Hours => "hours",
1976            DurationUnit::Days => "days",
1977        }
1978    }
1979
1980    /// The unit's value in milliseconds.
1981    pub fn millis(self) -> i64 {
1982        match self {
1983            DurationUnit::Milliseconds => 1,
1984            DurationUnit::Seconds => 1_000,
1985            DurationUnit::Minutes => 60_000,
1986            DurationUnit::Hours => 3_600_000,
1987            DurationUnit::Days => 86_400_000,
1988        }
1989    }
1990}
1991
1992/// An integer refinement bound (v0.40, ADR 0073): the parsed value plus the
1993/// bound's source span (covering a leading `-`). Value-only beyond the span —
1994/// ints have one canonical printed form, so the formatter stays idempotent
1995/// without a stored lexeme. The span backs the `InRange`-swap quick-fix.
1996#[derive(Debug, Clone)]
1997pub struct IntBound {
1998    pub value: i64,
1999    pub span: Span,
2000}
2001
2002/// A float refinement bound (v0.21): the parsed value plus the signed source
2003/// lexeme (for byte-stable emission). v0.40 (ADR 0073): also the source span,
2004/// for the `InRange`-swap quick-fix.
2005#[derive(Debug, Clone)]
2006pub struct FloatBound {
2007    pub value: f64,
2008    pub lexeme: String,
2009    pub span: Span,
2010}
2011
2012#[derive(Debug, Clone)]
2013pub struct Refinement {
2014    pub predicates: Vec<RefinementPred>,
2015    pub span: Span,
2016}
2017
2018#[derive(Debug, Clone)]
2019pub struct RefinementPred {
2020    pub kind: PredKind,
2021    pub span: Span,
2022}
2023
2024#[derive(Debug, Clone)]
2025pub enum PredKind {
2026    Matches(String),
2027    InRange(IntBound, IntBound),
2028    /// `InRange` with float bounds (v0.21) — a separate variant so every
2029    /// `Int` refinement path stays untouched. Bounds keep their source
2030    /// lexemes (including any sign) so emitted runtime checks are
2031    /// byte-stable.
2032    InRangeF(FloatBound, FloatBound),
2033    MinLength(i64),
2034    MaxLength(i64),
2035    Length(i64),
2036    NonNegative,
2037    Positive,
2038    NonEmpty,
2039}
2040
2041impl PredKind {
2042    pub fn name(&self) -> &'static str {
2043        match self {
2044            PredKind::Matches(_) => "Matches",
2045            PredKind::InRange(..) | PredKind::InRangeF(..) => "InRange",
2046            PredKind::MinLength(_) => "MinLength",
2047            PredKind::MaxLength(_) => "MaxLength",
2048            PredKind::Length(_) => "Length",
2049            PredKind::NonNegative => "NonNegative",
2050            PredKind::Positive => "Positive",
2051            PredKind::NonEmpty => "NonEmpty",
2052        }
2053    }
2054}
2055
2056/// #1651: the order a refinement's predicates are **checked** in at runtime:
2057/// every `Matches` after every other predicate, and otherwise source order.
2058///
2059/// A `Matches` pattern may take time polynomial in the input's length, and a
2060/// `MaxLength`/`Length` bound is what caps it (`bynk.types.polynomial_regex_capped`),
2061/// so the length checks must run first wherever the author wrote them. The order
2062/// is observable only in which failure a value failing several predicates
2063/// reports. Every runtime check site (`.of`, the boundary codec, `is`) and the
2064/// checker's compile-time literal check use this order, so they agree. The
2065/// canonical form a contract fingerprint hashes sorts its own copy and is
2066/// unaffected.
2067pub fn in_check_order<T>(preds: &[T], kind: impl Fn(&T) -> &PredKind) -> Vec<&T> {
2068    let (regex, rest): (Vec<&T>, Vec<&T>) = preds
2069        .iter()
2070        .partition(|p| matches!(kind(p), PredKind::Matches(_)));
2071    rest.into_iter().chain(regex).collect()
2072}
2073
2074/// A function type parameter (v0.20a, `fn name[A, B](…)`). A struct rather
2075/// than a bare Ident so the ADR-0028 "bound-capable" promise is a later field
2076/// addition, not a representation change.
2077#[derive(Debug, Clone)]
2078pub struct TypeParam {
2079    pub name: Ident,
2080    pub span: Span,
2081}
2082
2083/// A lambda expression (v0.20a): `(params) => expr` or `(params) => { … }`.
2084/// `=>` is the value arrow (shared with `match`); param annotations are
2085/// optional where an expected function type supplies them.
2086#[derive(Debug, Clone)]
2087pub struct LambdaExpr {
2088    pub params: Vec<LambdaParam>,
2089    pub body: Box<Expr>,
2090    pub span: Span,
2091}
2092
2093/// A lambda parameter. A separate type from [`Param`] because its annotation
2094/// is optional — `Param.type_ref` stays mandatory at every signature site.
2095#[derive(Debug, Clone)]
2096pub struct LambdaParam {
2097    pub name: Ident,
2098    pub type_ref: Option<TypeRef>,
2099    pub span: Span,
2100}
2101
2102#[derive(Debug, Clone)]
2103pub struct FnDecl {
2104    /// v0.20a: `[A, B]` type parameters; empty for non-generic functions.
2105    pub type_params: Vec<TypeParam>,
2106    /// Free function or method (`TypeName.methodName`). See [`FnName`].
2107    pub name: FnName,
2108    pub params: Vec<Param>,
2109    pub return_type: TypeRef,
2110    /// v0.115: preconditions (`requires <name>: <pred>`), parsed between the
2111    /// return type and the body. A contract clause is the invariant predicate
2112    /// attached to a function (ADR 0144 — one predicate surface); `requires`
2113    /// scopes over the parameters only.
2114    pub requires: Vec<Contract>,
2115    /// v0.115: postconditions (`ensures <name>: <pred>`). Scopes over the
2116    /// parameters *and* `result`, the contextual binding for the return value.
2117    pub ensures: Vec<Contract>,
2118    pub body: Block,
2119    /// True when the first parameter is the special `self` parameter. Only
2120    /// valid for method declarations.
2121    pub has_self: bool,
2122    /// Documentation block attached to this declaration (v0.3).
2123    pub documentation: Option<String>,
2124    pub span: Span,
2125    pub trivia: Trivia,
2126}
2127
2128/// A function-declaration name: either a free function `f` or a method
2129/// `T.method` (v0.2 §3.6).
2130#[derive(Debug, Clone)]
2131pub enum FnName {
2132    /// `fn name(...)` — a free function.
2133    Free(Ident),
2134    /// `fn TypeName.methodName(...)` — a method attached to a type.
2135    Method {
2136        type_name: Ident,
2137        method_name: Ident,
2138    },
2139}
2140
2141impl FnName {
2142    /// The function's short name for diagnostics. For methods returns the
2143    /// method portion only; the type prefix is recovered via `type_name`.
2144    pub fn ident(&self) -> &Ident {
2145        match self {
2146            FnName::Free(id) => id,
2147            FnName::Method { method_name, .. } => method_name,
2148        }
2149    }
2150
2151    /// For methods, the attached type's identifier; `None` for free fns.
2152    pub fn type_name(&self) -> Option<&Ident> {
2153        match self {
2154            FnName::Free(_) => None,
2155            FnName::Method { type_name, .. } => Some(type_name),
2156        }
2157    }
2158
2159    /// The displayed full name (e.g., `Money.add` or `parseSku`).
2160    pub fn display(&self) -> String {
2161        match self {
2162            FnName::Free(id) => id.name.clone(),
2163            FnName::Method {
2164                type_name,
2165                method_name,
2166            } => format!("{}.{}", type_name.name, method_name.name),
2167        }
2168    }
2169}
2170
2171/// A brace-delimited block of statements ending in a tail expression
2172/// whose value is the block's value (spec v0.1 §3.1).
2173#[derive(Debug, Clone)]
2174pub struct Block {
2175    pub statements: Vec<Statement>,
2176    pub tail: Box<Expr>,
2177    pub span: Span,
2178    /// Line comments that appear between the last statement (or the
2179    /// opening brace) and the tail expression. Preserved here because
2180    /// expressions do not carry trivia in v1.1.
2181    pub tail_leading_comments: Vec<Comment>,
2182    /// `true` when the block was written with no explicit tail expression and
2183    /// the parser synthesised a `()` (unit) tail (v0.146, ADR 0170). The tail
2184    /// is a real `ExprKind::UnitLit` either way; this flag records that it was
2185    /// *implicit* so the formatter can omit it (Bynk has no statement
2186    /// terminator, so a printed `()` would re-attach to the last statement on
2187    /// re-parse — `x` `()` → `x()`). The parser re-derives the implicit unit
2188    /// tail, so omitting it is loss-free.
2189    pub implicit_tail: bool,
2190}
2191
2192impl Block {
2193    /// Whether this block is a synthesised empty unit block — no statements and
2194    /// an *implicit* `()` tail (v0.146, ADR 0170). This is exactly the shape the
2195    /// parser inserts for an `if` with no `else` branch, so both the checker
2196    /// (gating the else-less form to unit) and the formatter (omitting the
2197    /// synthetic `else { () }`) recognise it here.
2198    pub fn is_synth_unit(&self) -> bool {
2199        self.statements.is_empty()
2200            && self.implicit_tail
2201            && matches!(self.tail.kind, ExprKind::UnitLit)
2202    }
2203}
2204
2205/// Block-level statement.
2206#[derive(Debug, Clone)]
2207pub enum Statement {
2208    /// `let name (: T)? = expr` — pure binding (v0.1).
2209    Let(LetStmt),
2210    /// `let name (: T)? <- expr` — effectful binding (v0.5).
2211    EffectLet(LetStmt),
2212    /// `expect expr` — verify a Bool predicate at test runtime (v0.7; renamed
2213    /// from `assert` in v0.112). Only valid inside test case bodies.
2214    Expect(ExpectStmt),
2215    /// `~> expr` — an asynchronous fire-and-forget send (v0.79). The caller does
2216    /// not await the reply; legal only when the reply is `Effect[()]`. No binder.
2217    Send(SendStmt),
2218    /// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
2219    /// Runs an `Effect[()]` and discards its (unit) result — the binder-free
2220    /// sugar for `let _ <- expr` when the awaited value is unit. Legal only in
2221    /// an effectful body; the operand MUST be `Effect[()]` (a valued reply keeps
2222    /// the explicit `let _ <- e`, so throwing away a real value stays visible).
2223    Do(DoStmt),
2224    /// `name := expr` — a `Cell` store write (v0.81, storage track). The
2225    /// unconditional write form; `.update(fn)` (a method call) is the
2226    /// read-modify-write form. ADR 0108.
2227    Assign(AssignStmt),
2228}
2229
2230impl Statement {
2231    pub fn span(&self) -> Span {
2232        match self {
2233            Statement::Let(l) | Statement::EffectLet(l) => l.span,
2234            Statement::Expect(a) => a.span,
2235            Statement::Send(s) => s.span,
2236            Statement::Do(d) => d.span,
2237            Statement::Assign(a) => a.span,
2238        }
2239    }
2240}
2241
2242#[derive(Debug, Clone)]
2243pub struct ExpectStmt {
2244    pub value: Expr,
2245    pub span: Span,
2246    pub trivia: Trivia,
2247}
2248
2249/// `name := expr` — a `Cell` store write (v0.81, storage track). `target` is the
2250/// `Cell` field being written (a bare name for now; the checker resolves it to a
2251/// `store` field). `value` is the new value.
2252#[derive(Debug, Clone)]
2253pub struct AssignStmt {
2254    pub target: Ident,
2255    pub value: Expr,
2256    pub span: Span,
2257    pub trivia: Trivia,
2258}
2259
2260#[derive(Debug, Clone)]
2261pub struct LetStmt {
2262    pub name: Ident,
2263    pub type_annot: Option<TypeRef>,
2264    pub value: Expr,
2265    /// v0.182 (#664): the call-site `by <Actor>(<identity>)` clause on an
2266    /// `EffectLet` whose value addresses a test service handler. `None` on a pure
2267    /// `Let` (the `by` is parsed only in the `<-` arm) and on an effect-let with
2268    /// no principal.
2269    pub principal: Option<CallSiteActor>,
2270    pub span: Span,
2271    pub trivia: Trivia,
2272}
2273
2274#[derive(Debug, Clone)]
2275pub struct SendStmt {
2276    /// The send target — a recipient call, e.g. `Logger.info(msg)`.
2277    pub value: Expr,
2278    pub span: Span,
2279    pub trivia: Trivia,
2280}
2281
2282/// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
2283/// `value` is the awaited effect, which MUST be `Effect[()]`.
2284#[derive(Debug, Clone)]
2285pub struct DoStmt {
2286    pub value: Expr,
2287    pub span: Span,
2288    pub trivia: Trivia,
2289}
2290
2291#[derive(Debug, Clone)]
2292pub struct Param {
2293    pub name: Ident,
2294    pub type_ref: TypeRef,
2295    pub span: Span,
2296}
2297
2298#[derive(Debug, Clone)]
2299pub enum TypeRef {
2300    Base(BaseType, Span),
2301    Named(Ident),
2302    /// `Result[T, E]` — the built-in generic Result type (v0.1).
2303    Result(Box<TypeRef>, Box<TypeRef>, Span),
2304    /// `Option[T]` — the built-in generic Option type (v0.2).
2305    Option(Box<TypeRef>, Span),
2306    /// `Effect[T]` — the built-in generic Effect type (v0.5).
2307    Effect(Box<TypeRef>, Span),
2308    /// `HttpResult[T]` — the built-in HTTP-result sum (v0.9).
2309    HttpResult(Box<TypeRef>, Span),
2310    /// `QueueResult` — the built-in queue verdict sum (`Ack | Retry`),
2311    /// non-generic; the required return of a queue handler (v0.44).
2312    QueueResult(Span),
2313    /// `List[T]` — the built-in generic immutable list type (v0.20b).
2314    List(Box<TypeRef>, Span),
2315    /// `Map[K, V]` — the built-in generic immutable map type (v0.20b).
2316    /// Keys are confined to value-keyable types
2317    /// (`bynk.types.unkeyable_map_key`).
2318    Map(Box<TypeRef>, Box<TypeRef>, Span),
2319    /// `Query[T]` — the built-in lazy storage-read description (v0.91, ADR 0115).
2320    /// Nameable in a pure helper's return type; non-storable and non-boundary
2321    /// (like `Effect`/`Fn`).
2322    Query(Box<TypeRef>, Span),
2323    /// `Stream[T]` — the value-over-time primitive (v0.100, real-time track
2324    /// slice 0). A lazy, pull-shaped sequence produced over time; non-storable
2325    /// and non-boundary (like `Query`/`Effect`/`Fn`).
2326    Stream(Box<TypeRef>, Span),
2327    /// `Connection[F]` — a held WebSocket connection (v0.102, real-time track
2328    /// slice 2). `F` is the server→client frame type. A `Held` resource:
2329    /// non-serialisable, non-boundary, and governed by the linearity discipline
2330    /// (§2.9); storable only in `Cell[Option[Connection]]` / `Map[K, Connection]`.
2331    Connection(Box<TypeRef>, Span),
2332    /// `History[Agent]` — a generated, driven call-history of an agent (v0.119,
2333    /// testing track slice 7, ADR 0155). A test-only generator, legal only in
2334    /// `for all` binding position inside a `property`; it is not a value type,
2335    /// so it never resolves in a field/param/return position. The bound subject
2336    /// behaves as an ordinary `List[Step]`.
2337    History(Box<TypeRef>, Span),
2338    /// `ValidationError` — the built-in error type used by refined-type
2339    /// constructors (v0.1).
2340    ValidationError(Span),
2341    /// `JsonError` — the built-in JSON-decode error type (v0.22b). A
2342    /// uniform record (`kind`/`path`/`message`, all `String`) the codec
2343    /// maps `BoundaryError` variants and parse failures into.
2344    JsonError(Span),
2345    /// `()` — the unit type (v0.5).
2346    Unit(Span),
2347    /// `A -> B` / `(A, B) -> C` / `() -> B` — a function type (v0.20a).
2348    /// Right-associative; effectful iff the return type is `Effect[_]`
2349    /// (the structural rule). Confined to non-boundary positions
2350    /// (`bynk.types.function_at_boundary`).
2351    Fn(Vec<TypeRef>, Box<TypeRef>, Span),
2352    /// `Name[Arg, …]` — an application of a user-declared generic type
2353    /// (v0.157, ADR 0183). `name` is a user type name (never a built-in
2354    /// generic, which each have a dedicated variant above). Arity and the
2355    /// existence of the referenced type are checked in the resolver.
2356    App {
2357        name: Ident,
2358        args: Vec<TypeRef>,
2359        span: Span,
2360    },
2361}
2362
2363impl TypeRef {
2364    pub fn span(&self) -> Span {
2365        match self {
2366            TypeRef::Base(_, s) => *s,
2367            TypeRef::Named(id) => id.span,
2368            TypeRef::Result(_, _, s) => *s,
2369            TypeRef::Option(_, s) => *s,
2370            TypeRef::Effect(_, s) => *s,
2371            TypeRef::HttpResult(_, s) => *s,
2372            TypeRef::QueueResult(s) => *s,
2373            TypeRef::List(_, s) => *s,
2374            TypeRef::Map(_, _, s) => *s,
2375            TypeRef::Query(_, s) => *s,
2376            TypeRef::Stream(_, s) => *s,
2377            TypeRef::Connection(_, s) => *s,
2378            TypeRef::History(_, s) => *s,
2379            TypeRef::ValidationError(s) => *s,
2380            TypeRef::JsonError(s) => *s,
2381            TypeRef::Unit(s) => *s,
2382            TypeRef::Fn(_, _, s) => *s,
2383            TypeRef::App { span, .. } => *span,
2384        }
2385    }
2386}
2387
2388/// v0.174 (#592): does the generic record type `name` transitively contain a
2389/// reference to itself — through any field-type path, including collection and
2390/// `Option` wrappers, sum-variant payloads, and generic type arguments? Such a
2391/// type has no finite set of monomorphised boundary codecs: uniform recursion
2392/// (`Node[T] = { next: Option[Node[T]] }`) would need a self-referential codec
2393/// chain the per-instantiation model does not yet generate, and polymorphic
2394/// recursion (`Weird[T] = { next: Option[Weird[List[T]]] }`) an unbounded set of
2395/// instantiations. Both are rejected at a boundary
2396/// (`bynk.generics.recursive_generic_at_boundary`).
2397///
2398/// Detection is reachability over the type-containment graph: `name` is
2399/// recursive iff it is reachable from its own body, following every named /
2400/// applied head and descending into every wrapper, map/result pair, function
2401/// position, and generic argument. Terminates via the `visited` set.
2402pub fn generic_record_is_recursive(
2403    name: &str,
2404    types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
2405) -> bool {
2406    fn heads(t: &TypeRef, out: &mut Vec<String>) {
2407        match t {
2408            TypeRef::Named(id) => out.push(id.name.clone()),
2409            TypeRef::App {
2410                name: app_name,
2411                args,
2412                ..
2413            } => {
2414                out.push(app_name.name.clone());
2415                for a in args {
2416                    heads(a, out);
2417                }
2418            }
2419            TypeRef::Option(a, _)
2420            | TypeRef::List(a, _)
2421            | TypeRef::Effect(a, _)
2422            | TypeRef::HttpResult(a, _)
2423            | TypeRef::Query(a, _)
2424            | TypeRef::Stream(a, _)
2425            | TypeRef::Connection(a, _)
2426            | TypeRef::History(a, _) => heads(a, out),
2427            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
2428                heads(a, out);
2429                heads(b, out);
2430            }
2431            TypeRef::Fn(ps, r, _) => {
2432                for p in ps {
2433                    heads(p, out);
2434                }
2435                heads(r, out);
2436            }
2437            TypeRef::Base(..)
2438            | TypeRef::QueueResult(_)
2439            | TypeRef::ValidationError(_)
2440            | TypeRef::JsonError(_)
2441            | TypeRef::Unit(_) => {}
2442        }
2443    }
2444    fn body_heads(decl: &TypeDecl, out: &mut Vec<String>) {
2445        match &decl.body {
2446            TypeBody::Record(r) => {
2447                for f in &r.fields {
2448                    heads(&f.type_ref, out);
2449                }
2450            }
2451            TypeBody::Sum(s) => {
2452                for v in &s.variants {
2453                    for p in &v.payload {
2454                        heads(&p.type_ref, out);
2455                    }
2456                }
2457            }
2458            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
2459        }
2460    }
2461    let Some(root) = types.get(name) else {
2462        return false;
2463    };
2464    let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
2465    let mut stack: Vec<String> = Vec::new();
2466    body_heads(root, &mut stack);
2467    while let Some(n) = stack.pop() {
2468        if n == name {
2469            return true;
2470        }
2471        if !visited.insert(n.clone()) {
2472            continue;
2473        }
2474        if let Some(decl) = types.get(&n) {
2475            body_heads(decl, &mut stack);
2476        }
2477    }
2478    false
2479}
2480
2481/// T3.4 (R2.4): a node's identity, independent of position — allocated once,
2482/// monotonically, per expression the parser constructs (`Parser::alloc_expr_id`
2483/// in `bynk-syntax/src/parser.rs`). Never derived from a `Span`, so two
2484/// expressions occupying the same byte range (a synthetic node, a
2485/// zero-width span) never collide the way a span-keyed side table could.
2486#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
2487pub struct ExprId(pub u32);
2488
2489impl ExprId {
2490    /// Reserved for `Expr` nodes built outside the parser — after checking,
2491    /// during emission — that are never looked up in a checker-populated
2492    /// `expr_types`/`expr_ty` table (they are lowered directly, from
2493    /// already-typed sub-expressions they wrap or splice). A lookup against
2494    /// this id is a bug: the node was never checked and has no recorded
2495    /// type of its own.
2496    pub const SYNTHETIC: ExprId = ExprId(u32::MAX);
2497}
2498
2499#[derive(Debug, Clone)]
2500pub struct Expr {
2501    pub id: ExprId,
2502    pub kind: ExprKind,
2503    pub span: Span,
2504}
2505
2506/// Finding #31: `Expr` sets the size of every expression node in the
2507/// program — `ExprKind::Observation`'s payload and `ExprKind::Is`'s pattern
2508/// field are boxed specifically to keep it small (176 bytes unboxed, 128
2509/// boxed, measured on this target). Pinned so the next large variant added
2510/// to `ExprKind` is a compile error here rather than a silent regression.
2511/// T3.4: `id: ExprId` adds 4 bytes (padded); the budget is unchanged, so this
2512/// still fits.
2513/// T3.4 (R2.4): `id: ExprId` is a deliberate 8-byte increase (128 → 136,
2514/// alignment-padded from 4), not a silent regression — the ceiling moves
2515/// with it, once, here, so the next *accidental* growth still trips this
2516/// assertion rather than hiding under slack headroom.
2517/// T3.5 (R2.2): `file: FileId` on `Span` is a deliberate increase (136 → 160
2518/// — `Span` itself grows from 16 to 24 bytes with alignment padding, and
2519/// `ExprKind`'s largest variant carries more than one `Span`), not a silent
2520/// regression — the ceiling moves with it, once, here, exactly as T3.4 did
2521/// for `id: ExprId`.
2522const _: () = assert!(std::mem::size_of::<Expr>() <= 160);
2523
2524impl ExprKind {
2525    /// Construct an `IntLit` for a *synthesized* integer — one the compiler
2526    /// invents rather than reading from source (a default `1`, a computed bound).
2527    /// The lexeme is the canonical decimal form (no separators). Source-parsed
2528    /// literals keep their as-written lexeme instead (v0.142, ADR 0166).
2529    pub fn int_lit(value: i64) -> ExprKind {
2530        ExprKind::IntLit {
2531            value,
2532            lexeme: value.to_string(),
2533        }
2534    }
2535}
2536
2537#[derive(Debug, Clone)]
2538pub enum ExprKind {
2539    /// An integer literal (typed `Int`). The lexeme is kept alongside the parsed
2540    /// value (v0.142, ADR 0166) so formatting is byte-stable: an author's `_`
2541    /// digit separators (`1_048_576`) survive a round-trip, mirroring the
2542    /// `FloatLit` treatment. The value is separator-free; emission lowers the
2543    /// value, so emitted output is unaffected.
2544    IntLit {
2545        value: i64,
2546        lexeme: String,
2547    },
2548    /// A float literal (v0.21). The lexeme is kept alongside the parsed
2549    /// value so emission and formatting are byte-stable (`1e10` must not
2550    /// normalise to `10000000000`).
2551    FloatLit {
2552        value: f64,
2553        lexeme: String,
2554    },
2555    /// A duration literal `<int>.<unit>` (v0.86, ADR 0112): `5.minutes`,
2556    /// `30.days`. The parser recognises the `IntLit . <unit>` shape and records
2557    /// the magnitude, the unit, and the resolved milliseconds (the value the
2558    /// emitter lowers to). Typed `Duration`.
2559    DurationLit {
2560        /// The integer magnitude as written (`5` in `5.minutes`).
2561        value: i64,
2562        /// The unit name (`minutes`), one of the closed set.
2563        unit: DurationUnit,
2564        /// The value in milliseconds — `value * unit factor`.
2565        millis: i64,
2566    },
2567    StrLit(String),
2568    /// An interpolated string `"… \(expr) …"` (v0.43, ADR 0075). Chunks and
2569    /// holes alternate. A plain `"…"` with no holes stays [`ExprKind::StrLit`],
2570    /// so existing code and the emitter/formatter fast-path are untouched.
2571    InterpStr(Vec<InterpPart>),
2572    BoolLit(bool),
2573    Ident(Ident),
2574    Call {
2575        name: Ident,
2576        /// v0.20a: explicit type arguments (`name[T](…)`); empty when absent.
2577        type_args: Vec<TypeRef>,
2578        args: Vec<Expr>,
2579    },
2580    /// A lambda (v0.20a). See [`LambdaExpr`].
2581    Lambda(LambdaExpr),
2582    BinOp(BinOp, Box<Expr>, Box<Expr>),
2583    UnaryOp(UnaryOp, Box<Expr>),
2584    Paren(Box<Expr>),
2585    /// `{ stmts; expr }` — block expression (v0.1).
2586    Block(Block),
2587    /// `if cond { then } else { else }` (v0.1).
2588    If {
2589        cond: Box<Expr>,
2590        then_block: Box<Block>,
2591        else_block: Box<Block>,
2592    },
2593    /// `Ok(value)` — Result success constructor (v0.1).
2594    Ok(Box<Expr>),
2595    /// `Err(error)` — Result failure constructor (v0.1).
2596    Err(Box<Expr>),
2597    /// `expr?` — propagation operator (v0.1).
2598    Question(Box<Expr>),
2599    /// `TypeName.method(args)` — qualified static call on a type
2600    /// (v0.1: only refined-type `of`; v0.2: any static method or variant
2601    /// constructor for sum types). The resolver decides which.
2602    ConstructorCall {
2603        type_name: Ident,
2604        method: Ident,
2605        args: Vec<Expr>,
2606    },
2607    /// `TypeName { field: value, ... }` — record construction (v0.2).
2608    RecordConstruction {
2609        type_name: Ident,
2610        fields: Vec<FieldInit>,
2611    },
2612    /// `receiver.field` — field access on a record value (v0.2). v0.3 adds
2613    /// `.raw` on opaque types within the defining commons.
2614    FieldAccess {
2615        receiver: Box<Expr>,
2616        field: Ident,
2617    },
2618    /// `receiver.method(args)` — instance method call (v0.2). The
2619    /// resolver determines the receiver's type and looks up the method.
2620    MethodCall {
2621        receiver: Box<Expr>,
2622        method: Ident,
2623        /// v0.22b: explicit type arguments on a qualified static
2624        /// (`Json.decode[T](…)`); empty when absent. The same-line-`[`
2625        /// rule applies as for `Call` type application (0039).
2626        type_args: Vec<TypeRef>,
2627        args: Vec<Expr>,
2628    },
2629    /// `match disc { arm+ }` — pattern matching (v0.2).
2630    Match {
2631        discriminant: Box<Expr>,
2632        arms: Vec<MatchArm>,
2633    },
2634    /// `expr is pattern` — pattern test, returns Bool (v0.2).
2635    ///
2636    /// `pattern` is boxed (finding #31): `Pattern`'s `Variant` case carries two
2637    /// `Ident`s plus a `Vec`, inlining it into every `ExprKind` sets the size
2638    /// of every expression node in the program for the one variant that
2639    /// tests a pattern.
2640    Is {
2641        value: Box<Expr>,
2642        pattern: Box<Pattern>,
2643    },
2644    /// `Some(value)` — Option Some constructor (v0.2).
2645    Some(Box<Expr>),
2646    /// `None` — Option None constructor (v0.2).
2647    None,
2648    /// `()` — unit literal (v0.5).
2649    UnitLit,
2650    /// `TypeName { ...base, field: value, ... }` or `{ ...base, ... }` —
2651    /// record spread expression (v0.5).
2652    RecordSpread {
2653        /// Optional type prefix (`TypeName { ...base }`). Absent for the
2654        /// bare form used inside `commit`.
2655        type_name: Option<Ident>,
2656        /// The base record being spread.
2657        base: Box<Expr>,
2658        /// Field overrides (always full `name: value` form — never shorthand).
2659        overrides: Vec<FieldInit>,
2660    },
2661    /// `Effect.pure(value)` — wrap a synchronous value into `Effect[T]`
2662    /// (v0.5). Recognised in the parser as a special-form.
2663    EffectPure(Box<Expr>),
2664    /// `expect expr` — expectation as an expression of type `()` (v0.9.1;
2665    /// renamed from `assert` in v0.112). Valid only inside test bodies. Evaluates
2666    /// `expr` (must be Bool); if false, the surrounding test case fails.
2667    Expect(Box<Expr>),
2668    /// `Val[T]`, `Val[T](args)` — test-context value construction (v0.9.4).
2669    /// `args` is empty for the bare form and holds the pin arguments for
2670    /// `Val[T](...)`. The record-override form `Val[T] { ... }` is not yet
2671    /// parsed. Valid only inside test bodies; has type `T`.
2672    Val {
2673        type_ref: TypeRef,
2674        args: Vec<Expr>,
2675    },
2676    /// `Wire(<String>)` — a raw, pre-validation argument to a `system`-tier
2677    /// service address (testing-the-boundary Slice C). The inner expression is a
2678    /// `String` carrying the wire form the boundary will receive *unvalidated* —
2679    /// a body's JSON text or a path segment — so a case can drive the router with
2680    /// input the type system forbids and observe the rejection. Legal only at
2681    /// `system` (there is no wire at `unit`); the router validates it, so no
2682    /// refined value is ever minted from a `Wire` (ADR 0182 untouched).
2683    Wire(Box<Expr>),
2684    /// `[a, b, c]` — list literal (v0.20b). An empty `[]` requires an
2685    /// expected type (`bynk.types.uninferable_element_type`).
2686    ListLit(Vec<Expr>),
2687    /// An observation over a consumed capability's recorded calls (v0.117,
2688    /// testing track slice 5). The direct subject of an `expect` in a `case`
2689    /// body — `expect Cap.op called once with <pred>`, `expect Cap.op never
2690    /// called`, `expect A.op before B.op`. Types as `Bool` (the claim about the
2691    /// recorded trace), lowered to a boolean over the recorded log.
2692    /// Boxed (finding #31): at ~160 bytes, `ObservationExpr` inlined here set
2693    /// the size of every `ExprKind` for the one variant that records a
2694    /// capability-call observation.
2695    Observation(Box<ObservationExpr>),
2696    /// `<call> faults` — the claim that an effectful call **faults** (#1706).
2697    /// The direct subject of an `expect` in a `case` body —
2698    /// `expect quote.call("GBP") faults`. The boxed expression is the call
2699    /// itself, an `Effect[_]` the claim awaits; the claim types as `Bool` and
2700    /// holds when awaiting the call throws (a capability fault, an injected
2701    /// `stub … fails`, an invariant violation) rather than returning a value.
2702    /// A fault is untyped and uncatchable by the caller, so this is a test's
2703    /// observation of the fault, not a handler for it: no production code can
2704    /// write it.
2705    Faults(Box<Expr>),
2706    /// `trace(Cap.op)` — the bound-trace escape hatch (v0.117, testing track
2707    /// slice 5). Yields the recorded calls of `Cap.op` as a `List[<CallRecord>]`
2708    /// (a synthetic record of the operation's parameters), asserted over with the
2709    /// ordinary value surface. Test-body-only, like [`ExprKind::Val`].
2710    Trace {
2711        cap: Ident,
2712        op: Ident,
2713    },
2714}
2715
2716/// Every directly-nested sub-expression of `e` — the **total** child
2717/// iterator. The match is exhaustive (no `_` arm), so adding an [`ExprKind`]
2718/// variant is a compile error here rather than a silently incomplete walk —
2719/// the trap the checker's three hand-rolled partial walkers each fell into
2720/// (block statements and match-arm bodies were skipped, so e.g. the `:=`
2721/// self-reference rule was bypassable through a match arm).
2722///
2723/// Exhaustive over [`ExprKind`] *and* over the expressions each variant holds
2724/// (#1760: match-arm guards were once missed, and every walk built on this one
2725/// missed them with it).
2726///
2727/// Descends one level: block *statements* and the tail, match-arm guards and
2728/// bodies, lambda bodies, interpolation holes, record-field values, and
2729/// observation predicates are all children. Callers recurse for a deep walk.
2730pub fn expr_children(e: &Expr) -> Vec<&Expr> {
2731    fn block_children<'a>(b: &'a Block, out: &mut Vec<&'a Expr>) {
2732        for s in &b.statements {
2733            statement_exprs(s, out);
2734        }
2735        out.push(&b.tail);
2736    }
2737    let mut out = Vec::new();
2738    match &e.kind {
2739        ExprKind::IntLit { .. }
2740        | ExprKind::FloatLit { .. }
2741        | ExprKind::DurationLit { .. }
2742        | ExprKind::StrLit(_)
2743        | ExprKind::BoolLit(_)
2744        | ExprKind::Ident(_)
2745        | ExprKind::None
2746        | ExprKind::UnitLit
2747        | ExprKind::Trace { .. } => {}
2748        ExprKind::InterpStr(parts) => {
2749            for p in parts {
2750                if let InterpPart::Hole(h) = p {
2751                    out.push(h.as_ref());
2752                }
2753            }
2754        }
2755        ExprKind::Call { args, .. }
2756        | ExprKind::ConstructorCall { args, .. }
2757        | ExprKind::Val { args, .. }
2758        | ExprKind::ListLit(args) => out.extend(args.iter()),
2759        ExprKind::Wire(inner) => out.push(inner.as_ref()),
2760        ExprKind::Lambda(l) => out.push(l.body.as_ref()),
2761        ExprKind::BinOp(_, l, r) => {
2762            out.push(l.as_ref());
2763            out.push(r.as_ref());
2764        }
2765        ExprKind::UnaryOp(_, inner)
2766        | ExprKind::Paren(inner)
2767        | ExprKind::Ok(inner)
2768        | ExprKind::Err(inner)
2769        | ExprKind::Question(inner)
2770        | ExprKind::Some(inner)
2771        | ExprKind::EffectPure(inner)
2772        | ExprKind::Expect(inner)
2773        | ExprKind::Faults(inner) => out.push(inner.as_ref()),
2774        ExprKind::Block(b) => block_children(b, &mut out),
2775        ExprKind::If {
2776            cond,
2777            then_block,
2778            else_block,
2779        } => {
2780            out.push(cond.as_ref());
2781            block_children(then_block, &mut out);
2782            block_children(else_block, &mut out);
2783        }
2784        ExprKind::RecordConstruction { fields, .. } => {
2785            out.extend(fields.iter().filter_map(|f| f.value.as_ref()));
2786        }
2787        ExprKind::FieldAccess { receiver, .. } => out.push(receiver.as_ref()),
2788        ExprKind::MethodCall { receiver, args, .. } => {
2789            out.push(receiver.as_ref());
2790            out.extend(args.iter());
2791        }
2792        ExprKind::Match { discriminant, arms } => {
2793            out.push(discriminant.as_ref());
2794            for arm in arms {
2795                // #1760: the guard, in evaluation order before the body. It is
2796                // an ordinary expression, checked like any other.
2797                if let Some(guard) = &arm.guard {
2798                    out.push(guard);
2799                }
2800                match &arm.body {
2801                    MatchBody::Expr(e) => out.push(e),
2802                    MatchBody::Block(b) => block_children(b, &mut out),
2803                }
2804            }
2805        }
2806        ExprKind::Is { value, .. } => out.push(value.as_ref()),
2807        ExprKind::RecordSpread {
2808            base, overrides, ..
2809        } => {
2810            out.push(base.as_ref());
2811            out.extend(overrides.iter().filter_map(|f| f.value.as_ref()));
2812        }
2813        ExprKind::Observation(obs) => match &obs.matcher {
2814            ObservationMatcher::Called { count, with_pred } => {
2815                if let Some(c) = count {
2816                    out.push(c.as_ref());
2817                }
2818                if let Some(p) = with_pred {
2819                    out.push(p.as_ref());
2820                }
2821            }
2822            ObservationMatcher::NeverCalled | ObservationMatcher::Before { .. } => {}
2823        },
2824    }
2825    out
2826}
2827
2828/// The expressions directly contained in a statement — the statement half of
2829/// [`expr_children`]'s total walk. Exhaustive over [`Statement`] for the same
2830/// reason, and over each statement's expressions: a `let`'s call-site
2831/// principal identity included.
2832pub fn statement_exprs<'a>(s: &'a Statement, out: &mut Vec<&'a Expr>) {
2833    match s {
2834        Statement::Let(l) | Statement::EffectLet(l) => {
2835            // #1766 review: a call-site principal's identity (`by User(who)`)
2836            // is a full expression. It is evaluated first, as an argument to
2837            // the addressed call.
2838            if let Some(identity) = l.principal.as_ref().and_then(|p| p.identity.as_deref()) {
2839                out.push(identity);
2840            }
2841            out.push(&l.value)
2842        }
2843        Statement::Expect(a) => out.push(&a.value),
2844        Statement::Send(snd) => out.push(&snd.value),
2845        Statement::Do(d) => out.push(&d.value),
2846        Statement::Assign(a) => out.push(&a.value),
2847    }
2848}
2849
2850/// An observation of a capability operation's recorded calls (v0.117, testing
2851/// track slice 5). `cap`/`op` name the seam (`Logger.log`); `matcher` is the
2852/// claim about the recorded calls.
2853#[derive(Debug, Clone)]
2854pub struct ObservationExpr {
2855    pub cap: Ident,
2856    pub op: Ident,
2857    pub matcher: ObservationMatcher,
2858}
2859
2860/// The claim an [`ObservationExpr`] makes about a seam's recorded calls (v0.117).
2861#[derive(Debug, Clone)]
2862pub enum ObservationMatcher {
2863    /// `called` [`once` | `<n> times`]? [`with` `<pred>`]?. `count` is `None`
2864    /// for a bare `called` (at least one); `Some(expr)` is the exact-count claim
2865    /// (a literal; `once` desugars to `1`). `with_pred` matches a call whose
2866    /// arguments (in scope by the operation's parameter names) satisfy it.
2867    Called {
2868        count: Option<Box<Expr>>,
2869        with_pred: Option<Box<Expr>>,
2870    },
2871    /// `never called` — zero calls.
2872    NeverCalled,
2873    /// `before Cap.op` — the first call of the subject precedes the first call
2874    /// of the named operation (both must have occurred).
2875    Before { cap: Ident, op: Ident },
2876}
2877
2878/// One part of an interpolated string (v0.43, ADR 0075). An
2879/// [`ExprKind::InterpStr`] holds an alternating run of these.
2880#[derive(Debug, Clone)]
2881pub enum InterpPart {
2882    /// Literal text between holes, with escapes already resolved.
2883    Chunk(String),
2884    /// An interpolated expression `\(expr)`. Type-checked by the hole rule
2885    /// (base scalars only; see the checker) and lowered into a template-
2886    /// literal `${…}` slot.
2887    Hole(Box<Expr>),
2888}
2889
2890/// One field-initialiser inside a record construction expression:
2891/// either `name: expr` or the shorthand `name` (which requires a binding
2892/// of the same name in scope and uses its value).
2893#[derive(Debug, Clone)]
2894pub struct FieldInit {
2895    pub name: Ident,
2896    /// `None` means shorthand — the field's value is the same-named binding.
2897    pub value: Option<Expr>,
2898    pub span: Span,
2899}
2900
2901/// One arm of a `match` expression: `pattern => body` or, with a guard,
2902/// `pattern if guard => body` (guard added in the nested-patterns increment,
2903/// ADR 0169). A guarded arm matches only when the pattern matches **and** the
2904/// `Bool` guard evaluates true; it never contributes to exhaustiveness.
2905#[derive(Debug, Clone)]
2906pub struct MatchArm {
2907    pub pattern: Pattern,
2908    /// Optional `if <Bool-expr>` guard between the pattern and `=>`.
2909    pub guard: Option<Expr>,
2910    pub body: MatchBody,
2911    pub span: Span,
2912}
2913
2914/// The right-hand side of a match arm — either a single expression or
2915/// a block.
2916#[derive(Debug, Clone)]
2917pub enum MatchBody {
2918    Expr(Expr),
2919    Block(Block),
2920}
2921
2922impl MatchBody {
2923    pub fn span(&self) -> Span {
2924        match self {
2925            MatchBody::Expr(e) => e.span,
2926            MatchBody::Block(b) => b.span,
2927        }
2928    }
2929}
2930
2931/// A pattern (v0.2 §3.8). Patterns appear in `match` arms and as the
2932/// right-hand side of the `is` operator.
2933#[derive(Debug, Clone)]
2934pub enum Pattern {
2935    /// `_` — matches any value, no bindings.
2936    Wildcard(Span),
2937    /// A lowercase identifier — binds the whole value to `name` and matches
2938    /// anything (ADR 0169). At the top of a `match` arm it binds the scrutinee
2939    /// (`n if n > 0 => …`); inside a payload position it binds the field
2940    /// (`Some(user)`). The uppercase-led counterpart is a nullary [`Pattern::Variant`].
2941    Binding(Ident),
2942    /// A literal pattern — `31`, `"english"`, `true` (v0.130 §2.3.4). Matches a
2943    /// primitive scrutinee (`Int`/`String`/`Bool`) by value equality. The
2944    /// admitted set mirrors ADR 0001's closed literal set (integers — including
2945    /// a leading unary minus — strings, and booleans); `Float`/`()` are not
2946    /// admitted as patterns.
2947    Literal { value: LiteralValue, span: Span },
2948    /// `Variant` or `Variant(bindings)` or `TypeName.Variant(bindings)`. Each
2949    /// payload binding is itself a [`Pattern`] (ADR 0169), so payloads nest:
2950    /// `Some(Ok(x))`, `Err(PollClosed)`.
2951    Variant {
2952        /// Optional qualifier: `TypeName.Variant`.
2953        type_name: Option<Ident>,
2954        /// The variant name.
2955        variant: Ident,
2956        /// Payload bindings (empty for nullary variants).
2957        bindings: Vec<PatternBinding>,
2958        span: Span,
2959    },
2960    /// `p 'where' refinement-predicate` — a refinement guard on a pattern
2961    /// (#472). Matches when `inner` matches *and* the scrutinee satisfies
2962    /// `predicate` at runtime. v1 admits only `Wildcard` as `inner` (no
2963    /// binding form yet); refutable — never counts toward exhaustiveness or
2964    /// as a catch-all arm, the same treatment as an `if` guard (§2.3.4).
2965    Refined {
2966        inner: Box<Pattern>,
2967        predicate: Refinement,
2968        span: Span,
2969    },
2970    /// `p₁ | p₂ | … | pₙ` — an or-pattern (#474 §2.3.4): matches if any
2971    /// alternative matches. Left-associative `|`, flattened by the parser's
2972    /// chain fold into one `Vec` — an alternative is always a leaf
2973    /// (`Wildcard`/`Binding`/`Literal`/`Variant`), never itself an `Or`
2974    /// (there is no parenthesized-pattern syntax to nest one inside another).
2975    /// Well-typedness (checked, not parsed): every alternative binds the same
2976    /// set of names, a name shared across alternatives has the same type
2977    /// (including refinement) in each, and every alternative matches the same
2978    /// value type.
2979    Or(Vec<Pattern>, Span),
2980}
2981
2982/// The value carried by a [`Pattern::Literal`]. A closed set (ADR 0001):
2983/// integer, string, and boolean. Kept distinct from [`ExprKind`] so patterns
2984/// carry only what they can actually match, and so it is `Eq`/`Hash` for the
2985/// duplicate-arm check.
2986#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2987pub enum LiteralValue {
2988    Int(i64),
2989    Str(String),
2990    Bool(bool),
2991}
2992
2993impl LiteralValue {
2994    /// A human-readable rendering for diagnostics (`31`, `"english"`, `true`).
2995    pub fn describe(&self) -> String {
2996        match self {
2997            LiteralValue::Int(n) => n.to_string(),
2998            LiteralValue::Str(s) => format!("{s:?}"),
2999            LiteralValue::Bool(b) => b.to_string(),
3000        }
3001    }
3002}
3003
3004impl Pattern {
3005    pub fn span(&self) -> Span {
3006        match self {
3007            Pattern::Wildcard(s) => *s,
3008            Pattern::Binding(id) => id.span,
3009            Pattern::Literal { span, .. } => *span,
3010            Pattern::Variant { span, .. } => *span,
3011            Pattern::Refined { span, .. } => *span,
3012            Pattern::Or(_, span) => *span,
3013        }
3014    }
3015
3016    /// Every identifier this pattern binds into scope, recursively (`_` and
3017    /// nullary variants bind nothing). Used by the resolver and the checker to
3018    /// populate an arm's scope, and by the guard to see the arm's bindings.
3019    ///
3020    /// For [`Pattern::Or`] this returns the *first* alternative's names — the
3021    /// checker separately verifies (#474 Rule 1) that every alternative binds
3022    /// the same set, so this is a defensive default when that rule is
3023    /// violated, not a semantic choice among alternatives.
3024    pub fn bound_names(&self) -> Vec<&Ident> {
3025        match self {
3026            Pattern::Wildcard(_) | Pattern::Literal { .. } => Vec::new(),
3027            Pattern::Binding(id) => vec![id],
3028            Pattern::Variant { bindings, .. } => bindings
3029                .iter()
3030                .flat_map(|b| b.pattern().bound_names())
3031                .collect(),
3032            Pattern::Refined { inner, .. } => inner.bound_names(),
3033            Pattern::Or(alts, _) => alts.first().map(Pattern::bound_names).unwrap_or_default(),
3034        }
3035    }
3036
3037    /// True when this pattern matches every value and binds nothing — a bare
3038    /// `_`. A [`Pattern::Binding`] also matches everything but *does* bind, so it
3039    /// is not a pure wildcard.
3040    pub fn is_wildcard(&self) -> bool {
3041        matches!(self, Pattern::Wildcard(_))
3042    }
3043
3044    /// True when this pattern matches every value (a `_` or a name binding),
3045    /// i.e. it is irrefutable and covers the position for exhaustiveness. An
3046    /// [`Pattern::Or`] is irrefutable when any alternative is — `_` in any
3047    /// position already makes the whole pattern match everything.
3048    pub fn is_irrefutable(&self) -> bool {
3049        match self {
3050            Pattern::Wildcard(_) | Pattern::Binding(_) => true,
3051            Pattern::Or(alts, _) => alts.iter().any(Pattern::is_irrefutable),
3052            _ => false,
3053        }
3054    }
3055}
3056
3057/// A single binding inside a variant pattern. Two surface forms:
3058/// `pattern` (positional — match the i-th payload field) and
3059/// `fieldName: pattern` (named — match the named payload field). The matched
3060/// sub-`pattern` is a full [`Pattern`] (ADR 0169), so a plain `name` is a
3061/// [`Pattern::Binding`], `_` a [`Pattern::Wildcard`], and `Ok(x)` a nested
3062/// [`Pattern::Variant`].
3063#[derive(Debug, Clone)]
3064pub struct PatternBinding {
3065    /// Source form: positional or named.
3066    pub kind: PatternBindingKind,
3067    pub span: Span,
3068}
3069
3070#[derive(Debug, Clone)]
3071pub enum PatternBindingKind {
3072    /// `pattern` (e.g. `x`, `_`, `Ok(v)`): match the payload field at this position.
3073    Positional { pattern: Pattern },
3074    /// `field: pattern`: match the named payload field against `pattern`.
3075    Named { field: Ident, pattern: Pattern },
3076}
3077
3078impl PatternBinding {
3079    /// The sub-pattern this binding matches its payload field against.
3080    pub fn pattern(&self) -> &Pattern {
3081        match &self.kind {
3082            PatternBindingKind::Positional { pattern } => pattern,
3083            PatternBindingKind::Named { pattern, .. } => pattern,
3084        }
3085    }
3086
3087    /// True when this binding discards its field (`_` or `field: _`) — a pure
3088    /// wildcard sub-pattern that binds nothing.
3089    pub fn is_wildcard(&self) -> bool {
3090        self.pattern().is_wildcard()
3091    }
3092}
3093
3094#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3095pub enum BinOp {
3096    /// `P implies Q` — logical implication (v0.80). Desugars to `!P || Q`; sits
3097    /// at the lowest precedence (below `||`). Reads directionally (P → Q).
3098    Implies,
3099    Or,
3100    And,
3101    Eq,
3102    NotEq,
3103    Lt,
3104    LtEq,
3105    Gt,
3106    GtEq,
3107    Add,
3108    Sub,
3109    Mul,
3110    Div,
3111}
3112
3113impl BinOp {
3114    pub fn name(self) -> &'static str {
3115        match self {
3116            BinOp::Implies => "implies",
3117            BinOp::Or => "||",
3118            BinOp::And => "&&",
3119            BinOp::Eq => "==",
3120            BinOp::NotEq => "!=",
3121            BinOp::Lt => "<",
3122            BinOp::LtEq => "<=",
3123            BinOp::Gt => ">",
3124            BinOp::GtEq => ">=",
3125            BinOp::Add => "+",
3126            BinOp::Sub => "-",
3127            BinOp::Mul => "*",
3128            BinOp::Div => "/",
3129        }
3130    }
3131}
3132
3133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3134pub enum UnaryOp {
3135    Neg,
3136    Not,
3137}
3138
3139impl UnaryOp {
3140    pub fn name(self) -> &'static str {
3141        match self {
3142            UnaryOp::Neg => "-",
3143            UnaryOp::Not => "!",
3144        }
3145    }
3146}
3147
3148#[cfg(test)]
3149mod size_tests {
3150    use super::*;
3151
3152    /// Finding #31: boxing `ExprKind::Observation`'s payload and
3153    /// `ExprKind::Is`'s pattern field took `Expr` from 176 to 128 bytes on
3154    /// this target (the module-level `const _` assertion is the real pin;
3155    /// this test just makes the before/after concrete and fails loudly if a
3156    /// future change silently regresses the win rather than tripping the
3157    /// `<= 128` ceiling by enough to notice).
3158    #[test]
3159    fn expr_is_smaller_than_before_the_boxing() {
3160        assert!(
3161            std::mem::size_of::<Expr>() < 176,
3162            "Expr should be smaller than its pre-#31 size of 176 bytes"
3163        );
3164    }
3165}
3166
3167#[cfg(test)]
3168mod expr_children_tests {
3169    use super::*;
3170
3171    /// #1760: a match arm's guard is a child, between the discriminant and
3172    /// the arm's body, in evaluation order.
3173    #[test]
3174    fn a_match_arms_guard_is_a_child_in_evaluation_order() {
3175        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";
3176        let tokens = crate::lexer::tokenize(source).unwrap();
3177        let units = crate::parser::parse_units(&tokens, source).unwrap();
3178        let SourceUnit::Commons(c) = &units[0] else {
3179            panic!("a commons");
3180        };
3181        let CommonsItem::Fn(f) = &c.items[0] else {
3182            panic!("a fn");
3183        };
3184        let ExprKind::Match { .. } = &f.body.tail.kind else {
3185            panic!("a match tail");
3186        };
3187        let children: Vec<&str> = expr_children(&f.body.tail)
3188            .into_iter()
3189            .map(|e| &source[e.span.start..e.span.end])
3190            .collect();
3191        assert_eq!(children, ["n", "k > lim", "1", "0"]);
3192    }
3193
3194    /// #1766 review: a `let`'s call-site principal identity is a statement
3195    /// expression, before the value it addresses.
3196    #[test]
3197    fn a_principal_identity_is_a_statement_expression() {
3198        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";
3199        let tokens = crate::lexer::tokenize(source).unwrap();
3200        let units = crate::parser::parse_units(&tokens, source).unwrap();
3201        let SourceUnit::Suite(t) = &units[0] else {
3202            panic!("a suite");
3203        };
3204        let mut exprs = Vec::new();
3205        statement_exprs(&t.cases[0].body.statements[1], &mut exprs);
3206        let texts: Vec<&str> = exprs
3207            .into_iter()
3208            .map(|e| &source[e.span.start..e.span.end])
3209            .collect();
3210        assert_eq!(texts, ["who", "api.get()"]);
3211    }
3212}