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