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#[derive(Debug, Clone)]
466pub enum CommonsItem {
467    Type(TypeDecl),
468    Fn(FnDecl),
469    /// `capability Name { fn op(...) -> T ... }` (v0.5; contexts only).
470    Capability(CapabilityDecl),
471    /// `provides Cap = ProviderName { fn op(...) -> T { ... } ... }` (v0.5).
472    Provider(ProviderDecl),
473    /// `service Name { on call(...) -> T { ... } ... }` (v0.5).
474    Service(ServiceDecl),
475    /// `agent Name { key id: T; state { ... }; on call ... }` (v0.5).
476    Agent(AgentDecl),
477    /// `actor Name { auth = Scheme, identity = T }` (v0.45). A nominal boundary
478    /// contract consumed by a handler's `by` clause; not a runnable entity.
479    Actor(ActorDecl),
480}
481
482impl CommonsItem {
483    pub fn name(&self) -> &Ident {
484        match self {
485            CommonsItem::Type(t) => &t.name,
486            CommonsItem::Fn(f) => f.name.ident(),
487            CommonsItem::Capability(c) => &c.name,
488            CommonsItem::Provider(p) => &p.provider_name,
489            CommonsItem::Service(s) => &s.name,
490            CommonsItem::Agent(a) => &a.name,
491            CommonsItem::Actor(a) => &a.name,
492        }
493    }
494}
495
496/// A capability declaration (v0.5 §3.3). Capabilities are interface-like
497/// contracts for external dependencies, used inside contexts. They may only
498/// appear inside a `context` declaration.
499#[derive(Debug, Clone)]
500pub struct CapabilityDecl {
501    pub name: Ident,
502    pub ops: Vec<CapabilityOp>,
503    pub documentation: Option<String>,
504    pub span: Span,
505    pub trivia: Trivia,
506}
507
508/// One operation in a capability (signature only; no body).
509#[derive(Debug, Clone)]
510pub struct CapabilityOp {
511    pub name: Ident,
512    pub params: Vec<Param>,
513    pub return_type: TypeRef,
514    pub documentation: Option<String>,
515    pub span: Span,
516    pub trivia: Trivia,
517}
518
519/// A provider declaration (v0.5 §3.4). Supplies an implementation for a
520/// capability.
521#[derive(Debug, Clone)]
522pub struct ProviderDecl {
523    /// The capability being implemented.
524    pub capability: Ident,
525    /// The provider's identifier (used in tests/config to select impls).
526    pub provider_name: Ident,
527    /// v0.12: capabilities this provider depends on (`provides X = Impl given
528    /// Y, Z { … }`). The provider's operation bodies may use these. v0.15:
529    /// a dependency may be a cross-context capability (`given B.Cap`).
530    pub given: Vec<CapRef>,
531    pub ops: Vec<ProviderOp>,
532    /// v0.17: an *external* provider — `provides Cap = Name` with **no** brace
533    /// block — inside an adapter, supplied by the adapter's binding rather than
534    /// a Bynk body. When `true`, `ops` is empty and the emitter produces no
535    /// class. The absence of the brace block (not an empty one) is the signal.
536    pub external: bool,
537    pub documentation: Option<String>,
538    pub span: Span,
539    pub trivia: Trivia,
540}
541
542/// One operation in a provider (signature plus body).
543#[derive(Debug, Clone)]
544pub struct ProviderOp {
545    pub name: Ident,
546    pub params: Vec<Param>,
547    pub return_type: TypeRef,
548    pub body: Block,
549    pub span: Span,
550    pub trivia: Trivia,
551}
552
553/// A service declaration (v0.5 §3.5). Services are the boundary interface
554/// of a context.
555#[derive(Debug, Clone)]
556pub struct ServiceDecl {
557    pub name: Ident,
558    /// The protocol the service conforms to, from the `from <protocol>` header
559    /// clause (v0.44). `Call` when there is no clause.
560    pub protocol: ServiceProtocol,
561    /// The optional service-level `by` default (v0.155) — a `by <Actor>` clause on
562    /// the service header, `service Api from http by v: Visitor { … }`. Every
563    /// handler that omits its own `by` inherits this one (injected by the
564    /// normalization pass). `None` when absent — handlers then fall back to the
565    /// per-protocol default actor (HTTP/WebSocket have none, so `by` stays
566    /// mandatory there). The "public / bearer-authed" fact is usually a service
567    /// fact, so this removes the per-handler repetition.
568    pub default_by: Option<ByClause>,
569    /// The optional service-level `given` default (v0.155) — a `given C1, C2`
570    /// clause on the service header, following the `by` default. Every handler
571    /// that declares no `given` of its own inherits this list. Empty when absent.
572    pub default_given: Vec<CapRef>,
573    /// The optional cross-origin (CORS) policy (v0.131, ADR 0159) — a `cors { }`
574    /// section in the service body, only meaningful on a `from http` service.
575    /// `None` when absent (same-origin default, byte-for-byte unchanged output).
576    pub cors: Option<CorsPolicy>,
577    /// The optional security-headers policy (v0.141, ADR 0164) — a `security { }`
578    /// section in the service body, only meaningful on a `from http` service.
579    /// `None` when absent, but unlike `cors` the *absence* still stamps the safe
580    /// defaults (`nosniff` on) — the emitter synthesises a default policy for every
581    /// `from http` service, so `None` here means "defaults", not "no headers".
582    pub security: Option<SecurityPolicy>,
583    /// The optional request-body-size policy (v0.142, ADR 0165) — a `limits { }`
584    /// section in the service body, only meaningful on a `from http` service. It
585    /// declares a per-service `maxBody` ceiling (in bytes) for the service's
586    /// body-taking routes; a route may override it with `@limit(maxBody: …)`.
587    /// `None` when absent (no cap — byte-for-byte unchanged output, the opt-in
588    /// CORS posture, not the `security` default-on posture).
589    pub limits: Option<LimitsPolicy>,
590    pub handlers: Vec<Handler>,
591    pub documentation: Option<String>,
592    pub span: Span,
593    pub trivia: Trivia,
594}
595
596/// A cross-origin resource-sharing policy on a `from http` service (v0.131,
597/// ADR 0159): the `cors { }` section in the service body. Parsed leniently as a
598/// list of `name: value` fields (the grammar accepts any field name — an unknown
599/// one is a checker diagnostic, per the `@`-annotation precedent, ADR 0111), and
600/// interpreted through the typed accessors below.
601///
602/// `Access-Control-Allow-Methods` is deliberately **not** a field — it is derived
603/// from the service's routes at emit time (the routes already enumerate the
604/// methods; a restated list would drift). Likewise `Allow-Headers` defaults to
605/// `content-type` (+ `Authorization` when a Bearer route exists) and is only
606/// stored here when the author overrides it.
607#[derive(Debug, Clone)]
608pub struct CorsPolicy {
609    /// The `cors { }` fields as written, in source order. Field names are
610    /// validated against the closed set (`origins`/`headers`/`credentials`/
611    /// `maxAge`) by the checker, not the parser.
612    pub fields: Vec<CorsField>,
613    pub span: Span,
614    pub trivia: Trivia,
615}
616
617/// One `name: value` field inside a `cors { }` policy (v0.131).
618#[derive(Debug, Clone)]
619pub struct CorsField {
620    pub name: Ident,
621    pub value: Expr,
622    pub span: Span,
623}
624
625impl CorsPolicy {
626    /// The raw value expression for a field, by name (the last one wins if a
627    /// field is repeated — the checker flags the duplicate separately).
628    pub fn field(&self, name: &str) -> Option<&Expr> {
629        self.fields
630            .iter()
631            .rev()
632            .find(|f| f.name.name == name)
633            .map(|f| &f.value)
634    }
635
636    /// The allowed origins — the string literals of the `origins:` list. An
637    /// absent or malformed field yields an empty list (the checker has already
638    /// reported the shape error; the emitter fails closed on an empty list).
639    pub fn origins(&self) -> Vec<String> {
640        Self::str_list(self.field("origins")).unwrap_or_default()
641    }
642
643    /// `true` iff `origins` is exactly the wildcard `["*"]`.
644    pub fn is_wildcard(&self) -> bool {
645        let os = self.origins();
646        os.len() == 1 && os[0] == "*"
647    }
648
649    /// Whether credentialed requests are allowed (`credentials: true`); defaults
650    /// to `false` when the field is absent.
651    pub fn credentials(&self) -> bool {
652        matches!(
653            self.field("credentials").map(|e| &e.kind),
654            Some(ExprKind::BoolLit(true))
655        )
656    }
657
658    /// The explicit `Access-Control-Allow-Headers` override, if the author gave
659    /// a `headers:` list; `None` leaves the emitter to apply its smart default.
660    pub fn allow_headers(&self) -> Option<Vec<String>> {
661        self.field("headers").and_then(Self::str_list_of)
662    }
663
664    /// The `Access-Control-Max-Age` in whole seconds, if a `maxAge:` duration was
665    /// given; `None` leaves the header off (the browser default).
666    pub fn max_age_secs(&self) -> Option<i64> {
667        match self.field("maxAge").map(|e| &e.kind) {
668            Some(ExprKind::DurationLit { millis, .. }) => Some(millis / 1_000),
669            _ => None,
670        }
671    }
672
673    /// Interpret an expression as a list of string literals, if it is one.
674    fn str_list(expr: Option<&Expr>) -> Option<Vec<String>> {
675        expr.and_then(Self::str_list_of)
676    }
677
678    fn str_list_of(expr: &Expr) -> Option<Vec<String>> {
679        match &expr.kind {
680            ExprKind::ListLit(items) => items
681                .iter()
682                .map(|e| match &e.kind {
683                    ExprKind::StrLit(s) => Some(s.clone()),
684                    _ => None,
685                })
686                .collect(),
687            _ => None,
688        }
689    }
690}
691
692/// A security-headers policy on a `from http` service (v0.141, ADR 0164): the
693/// `security { }` section in the service body. Parsed leniently as a list of
694/// `name: value` fields (an unknown one is a checker diagnostic, per the CORS /
695/// `@`-annotation precedent) and interpreted through the typed accessors below.
696///
697/// The closed set is `nosniff` (a `Bool`, default `true` — stamps
698/// `X-Content-Type-Options: nosniff`) and `hsts` (a positive `Duration`, opt-in —
699/// stamps `Strict-Transport-Security: max-age=…`). Unlike `cors`, the *safe*
700/// header is on by default: a `from http` service with no `security { }` still
701/// stamps `nosniff`, because a security header you have to remember to switch on
702/// is the one you forget (ADR 0164 DECISION A).
703#[derive(Debug, Clone)]
704pub struct SecurityPolicy {
705    /// The `security { }` fields as written, in source order. Field names are
706    /// validated against the closed set (`hsts`/`nosniff`) by the checker, not
707    /// the parser.
708    pub fields: Vec<SecurityField>,
709    pub span: Span,
710    pub trivia: Trivia,
711}
712
713/// One `name: value` field inside a `security { }` policy (v0.141).
714#[derive(Debug, Clone)]
715pub struct SecurityField {
716    pub name: Ident,
717    pub value: Expr,
718    pub span: Span,
719}
720
721impl SecurityPolicy {
722    /// The raw value expression for a field, by name (the last one wins if a
723    /// field is repeated — the checker flags the duplicate separately).
724    pub fn field(&self, name: &str) -> Option<&Expr> {
725        self.fields
726            .iter()
727            .rev()
728            .find(|f| f.name.name == name)
729            .map(|f| &f.value)
730    }
731
732    /// Whether `X-Content-Type-Options: nosniff` is stamped. Defaults to `true`
733    /// (the safe default, ADR 0164 DECISION A); only an explicit `nosniff: false`
734    /// opts out. A malformed value has already been reported by the checker; it
735    /// falls back to the safe default here.
736    pub fn nosniff(&self) -> bool {
737        !matches!(
738            self.field("nosniff").map(|e| &e.kind),
739            Some(ExprKind::BoolLit(false))
740        )
741    }
742
743    /// The `Strict-Transport-Security` `max-age` in whole seconds, if the author
744    /// opted in with an `hsts:` duration; `None` leaves HSTS off (the default —
745    /// HSTS pins the browser to HTTPS and is a deliberate opt-in, DECISION A).
746    pub fn hsts_max_age_secs(&self) -> Option<i64> {
747        match self.field("hsts").map(|e| &e.kind) {
748            Some(ExprKind::DurationLit { millis, .. }) => Some(millis / 1_000),
749            _ => None,
750        }
751    }
752}
753
754/// A request-body-size policy on a `from http` service (v0.142, ADR 0165): the
755/// `limits { }` section in the service body. Parsed leniently as a list of
756/// `name: value` fields (an unknown one is a checker diagnostic, per the CORS /
757/// `security` / `@`-annotation precedent) and interpreted through the typed
758/// accessor below.
759///
760/// The closed set is `maxBody` — a positive `Int` byte count (there is no byte
761/// `Size` literal yet; a `1.mb`-style literal is a named follow-on, the
762/// `Duration` playbook). Unlike `security`, this is opt-in: a service with no
763/// `limits { }` (and no route `@limit`) has no cap and emits byte-for-byte
764/// unchanged output (ADR 0165 DECISION E — the CORS posture).
765#[derive(Debug, Clone)]
766pub struct LimitsPolicy {
767    /// The `limits { }` fields as written, in source order. Field names are
768    /// validated against the closed set (`maxBody`) by the checker, not the
769    /// parser.
770    pub fields: Vec<LimitsField>,
771    pub span: Span,
772    pub trivia: Trivia,
773}
774
775/// One `name: value` field inside a `limits { }` policy (v0.142).
776#[derive(Debug, Clone)]
777pub struct LimitsField {
778    pub name: Ident,
779    pub value: Expr,
780    pub span: Span,
781}
782
783impl LimitsPolicy {
784    /// The raw value expression for a field, by name (the last one wins if a
785    /// field is repeated — the checker flags the duplicate separately).
786    pub fn field(&self, name: &str) -> Option<&Expr> {
787        self.fields
788            .iter()
789            .rev()
790            .find(|f| f.name.name == name)
791            .map(|f| &f.value)
792    }
793
794    /// The service-wide maximum request-body size in bytes, if the author gave a
795    /// positive `maxBody:` `Int` literal; `None` leaves the service without a
796    /// default cap. A malformed or non-positive value has already been reported
797    /// by the checker; it falls back to `None` here (no cap).
798    pub fn max_body(&self) -> Option<i64> {
799        match self.field("maxBody").map(|e| &e.kind) {
800            Some(ExprKind::IntLit { value, .. }) if *value > 0 => Some(*value),
801            _ => None,
802        }
803    }
804}
805
806/// The protocol a service conforms to — declared on the header via
807/// `from <protocol>` (v0.44). `Call` is the default (no `from` clause): a
808/// contract-mediated internal-RPC surface, not a wire protocol. Multi-endpoint
809/// protocols (`Http`, `Cron`) carry no binding — the endpoint lives on each
810/// handler; single-binding `Queue` carries its queue name.
811#[derive(Debug, Clone)]
812pub enum ServiceProtocol {
813    /// No `from` clause: the service holds `on call` handlers only.
814    Call,
815    /// `from http` — many routes; each handler is `on <Method>("route")`.
816    Http,
817    /// `from cron` — many schedules; each handler is `on schedule("expr")`.
818    Cron,
819    /// `from queue("name")` — one bound queue; handlers are `on message(...)`.
820    Queue { name: String },
821    /// `from websocket(in: ClientFrame, out: ServerFrame)` — a held WebSocket
822    /// connection (v0.103, real-time track slice 3). `in_type` is the inbound
823    /// frame type (client→server, decoded and routed as typed agent messages);
824    /// `out_type` is the server→client frame type the held `Connection[out_type]`
825    /// carries. The service holds exactly one `on open` handler (edge auth via
826    /// `by`, then transfer of the connection to an agent).
827    WebSocket { in_type: TypeRef, out_type: TypeRef },
828}
829
830/// An agent declaration (v0.5 §3.6). Agents are state-bearing entities
831/// with their own handlers.
832#[derive(Debug, Clone)]
833pub struct AgentDecl {
834    pub name: Ident,
835    /// `key id: Type` — the identifier-typed value identifying instances.
836    pub key_name: Ident,
837    pub key_type: TypeRef,
838    /// `store` fields (v0.81, storage track) — each an access-pattern slot of a
839    /// declared storage kind (`Cell`/`Map`/…). The successor to the removed
840    /// `state { }` record (ADR 0108); every agent declares its state this way.
841    pub store_fields: Vec<StoreField>,
842    /// Invariants (v0.80 §14) — universally-quantified predicates over the
843    /// agent's `store` fields. The phase sits between the fields and the
844    /// handlers; each is checked against the state staged by a handler's writes
845    /// before it commits.
846    pub invariants: Vec<Invariant>,
847    /// Step invariants (v0.116 §, testing track slice 4) — named predicates over
848    /// the pre-/post-commit state *pair* (`old`/`new`), checked at the commit
849    /// boundary beside [`invariants`], from the second commit onward. Widen the
850    /// invariant subject from a snapshot to a step (ADR 0144 — one predicate
851    /// surface).
852    ///
853    /// [`invariants`]: AgentDecl::invariants
854    pub transitions: Vec<Transition>,
855    pub handlers: Vec<Handler>,
856    pub documentation: Option<String>,
857    pub span: Span,
858    pub trivia: Trivia,
859}
860
861/// A `store` field (v0.81, storage track). Each is an access-pattern slot of a
862/// declared storage kind: `store <name>: <Kind>[…] [@annotations] [= <init>]`.
863/// The kind and its element type are carried as an ordinary [`TypeRef`]
864/// (`Cell[Int]`, `Map[K, V]`); the checker restricts which heads are storage
865/// kinds. Access-pattern annotations (`@indexed`, …) parse into [`annotations`]
866/// (v0.85, ADR 0111); the checker validates them against the closed registry.
867///
868/// [`annotations`]: StoreField::annotations
869#[derive(Debug, Clone)]
870pub struct StoreField {
871    pub name: Ident,
872    /// The storage kind and its element type(s): `Cell[Int]`, `Map[K, V]`. A
873    /// dedicated [`StoreKind`] rather than a [`TypeRef`] — storage kinds are not
874    /// value types, and the checker dispatches kind-aware operations on the head.
875    pub kind: StoreKind,
876    /// Storage annotations on the field (v0.85, ADR 0111): `@ttl(5.minutes)`,
877    /// `@indexed(by: orderId)`. Parsed in declaration order (after the kind,
878    /// before the initialiser); the checker validates names against the closed
879    /// registry and gates each to the slice that implements it.
880    pub annotations: Vec<Annotation>,
881    /// The fresh-key initial value (`= expr`), if given — same disposition as a
882    /// `state` field's initialiser (ADRs 0003/0004 carry forward).
883    pub init: Option<Expr>,
884    pub documentation: Option<String>,
885    pub span: Span,
886    pub trivia: Trivia,
887}
888
889/// A storage annotation on a `store` field (v0.85, storage track; ADR 0111):
890/// `@<name>(<args>)`. The `name` is matched against the closed registry
891/// (`@indexed`/`@ttl`/`@retain`/`@bounded`) by the checker; the grammar accepts
892/// any identifier so an unknown name is a checker diagnostic, not a parse error.
893/// Arguments are compile-time metadata, restricted to literals (and the `by:`
894/// field-name labels of `@indexed`) by the checker per ADR 0111 D4.
895#[derive(Debug, Clone)]
896pub struct Annotation {
897    pub name: Ident,
898    pub args: Vec<AnnotationArg>,
899    pub span: Span,
900}
901
902/// A single annotation argument (v0.85; ADR 0111): an optional `label:` followed
903/// by a value expression — `by: orderId` (labelled) or `5.minutes` (positional).
904/// The value is parsed as an ordinary [`Expr`] so the duration-literal form
905/// (`5.minutes`, landing with the `Duration` slice) needs no special grammar;
906/// the checker restricts it to a literal where the annotation is functional.
907#[derive(Debug, Clone)]
908pub struct AnnotationArg {
909    pub label: Option<Ident>,
910    pub value: Expr,
911    pub span: Span,
912}
913
914/// A storage kind applied to its element type(s) (v0.81): `Cell[Int]`,
915/// `Map[ReservationId, Reservation]`. The `head` is the kind name (`Cell`,
916/// `Map`, `Set`, `Log`, `Queue`, `Cache`); the checker validates it against the
917/// closed catalogue. Element types are ordinary [`TypeRef`]s. Refined element
918/// types (`Cell[Int where NonNegative]`) ride a later slice (parse_type_ref does
919/// not yet accept an inline refinement in type-argument position).
920#[derive(Debug, Clone)]
921pub struct StoreKind {
922    pub head: Ident,
923    pub args: Vec<TypeRef>,
924    pub span: Span,
925}
926
927/// An agent invariant (v0.80 §14). A named predicate over the agent's state
928/// fields that must hold of every committed state; a commit that would violate
929/// it faults (`InvariantViolation`) before the state is persisted. The
930/// predicate references state fields by bare name, mirroring the design-notes
931/// worked examples (`status == Paid implies paymentRef.isSome()`).
932#[derive(Debug, Clone)]
933pub struct Invariant {
934    pub name: Ident,
935    /// The predicate expression — an ordinary `Bool`-typed expression over the
936    /// state fields, plus `implies` and `is`. The parsed-predicate-on-a-
937    /// declaration shape mirrors [`ActorRefinement::predicate`].
938    pub predicate: Expr,
939    pub documentation: Option<String>,
940    pub span: Span,
941    pub trivia: Trivia,
942}
943
944/// An agent step invariant (v0.116 §, testing track slice 4). A named predicate
945/// over the *pair* of committed states — the pre-commit `old` and the proposed
946/// `new`, each the agent's state record — that must hold of every state move; a
947/// commit that would violate it faults (`InvariantViolation`) before the state is
948/// persisted, exactly as a snapshot [`Invariant`] does. Widens the invariant
949/// subject from a snapshot to a step (ADR 0144 — one predicate surface); the
950/// predicate reuses the invariant surface (`implies`/`is`/pure methods) with
951/// `old`/`new` bound contextually (`old.status is Paid implies new.status is
952/// Paid`).
953#[derive(Debug, Clone)]
954pub struct Transition {
955    pub name: Ident,
956    /// The predicate expression — an ordinary `Bool`-typed expression over the
957    /// `old` and `new` state records, with `implies`/`is` and pure methods,
958    /// mirroring [`Invariant`].
959    pub predicate: Expr,
960    pub documentation: Option<String>,
961    pub span: Span,
962    pub trivia: Trivia,
963}
964
965/// A function contract clause (v0.115 §, testing track slice 3). A named
966/// predicate on a `fn` signature — a `requires` (precondition) or `ensures`
967/// (postcondition). A contract is the invariant predicate attached to a
968/// function (ADR 0144 — one predicate surface): the predicate is a pure `Bool`
969/// expression over the parameters (`requires`) or the parameters plus `result`
970/// (`ensures`), with `implies`/`is` and pure methods, mirroring [`Invariant`].
971/// The name rides the failure report and the redundant-test dedup.
972#[derive(Debug, Clone)]
973pub struct Contract {
974    pub name: Ident,
975    /// The predicate expression — an ordinary `Bool`-typed expression over the
976    /// parameters (and, for an `ensures`, the contextual `result` binding).
977    pub predicate: Expr,
978    pub span: Span,
979}
980
981/// An actor declaration (v0.45 §3.7). An actor is a nominal *contract type*
982/// describing an external party at a boundary — not a runnable entity. A
983/// handler consumes an actor on its `by` clause; the boundary verifies the
984/// declared `auth` scheme and mints a sealed identity (`name.identity`).
985#[derive(Debug, Clone)]
986pub struct ActorDecl {
987    pub name: Ident,
988    /// The authentication scheme from `auth = <Scheme>`, stored as the raw
989    /// identifier. The checker classifies it: `None`/`Internal`/`Bearer` are
990    /// admitted; `Signature` is reserved-and-rejected
991    /// (`bynk.actor.scheme_unsupported`); anything else is
992    /// `bynk.actor.unknown_scheme`. `None` for the refinement form.
993    pub auth: Option<Ident>,
994    /// The scheme's keyed config from `auth = Scheme(key = value, …)` (v0.47
995    /// `Bearer(secret = "…")`; v0.51 generalised for `Signature(secret, header,
996    /// timestamp?, tolerance?)`). Empty for schemes/forms with no config. The
997    /// checker validates which keys each scheme requires/allows.
998    pub auth_config: Vec<SchemeArg>,
999    /// The optional identity type from `, identity = <T>`. Absent ⇒ the
1000    /// scheme default (`()` for `None`; a sealed `CallerId` for the `Internal`
1001    /// `on call` channel, `()` for other `Internal` channels).
1002    pub identity: Option<TypeRef>,
1003    /// The refinement form `actor Admin = Base where <predicate>` — narrows a
1004    /// base actor by an authorisation claim (ADR 0091). The predicate is parsed
1005    /// as a full expression; a static-semantics rule restricts it to the closed
1006    /// actor-claim catalogue (`hasClaim`/`claimEquals` over a `Bearer` base;
1007    /// `bynk.actor.refinement_predicate_unsupported` / `…_base_unsupported`).
1008    pub refinement: Option<ActorRefinement>,
1009    pub documentation: Option<String>,
1010    pub span: Span,
1011    pub trivia: Trivia,
1012}
1013
1014impl ActorDecl {
1015    /// The value of a scheme config arg by key, if present (e.g. `secret`,
1016    /// `header`).
1017    pub fn scheme_arg(&self, key: &str) -> Option<&SchemeArg> {
1018        self.auth_config.iter().find(|a| a.key.name == key)
1019    }
1020}
1021
1022/// One `key = value` argument in a scheme config (`Scheme(key = value, …)`).
1023#[derive(Debug, Clone)]
1024pub struct SchemeArg {
1025    pub key: Ident,
1026    pub value: SchemeArgValue,
1027    /// Span of the value, for diagnostics.
1028    pub span: Span,
1029}
1030
1031/// A scheme config arg value — a string literal or an integer.
1032#[derive(Debug, Clone)]
1033pub enum SchemeArgValue {
1034    Str(String),
1035    Int(i64),
1036}
1037
1038impl SchemeArgValue {
1039    pub fn as_str(&self) -> Option<&str> {
1040        match self {
1041            SchemeArgValue::Str(s) => Some(s),
1042            SchemeArgValue::Int(_) => None,
1043        }
1044    }
1045    pub fn as_int(&self) -> Option<i64> {
1046        match self {
1047            SchemeArgValue::Int(n) => Some(*n),
1048            SchemeArgValue::Str(_) => None,
1049        }
1050    }
1051}
1052
1053/// The reserved refinement form `actor Admin = User where <predicate>` (Q3).
1054/// Parsed in Foundations so the grammar is fixed; admission is a later slice.
1055#[derive(Debug, Clone)]
1056pub struct ActorRefinement {
1057    /// The base actor being refined.
1058    pub base: Ident,
1059    /// The `where` predicate. Parsed but not yet checked.
1060    pub predicate: Expr,
1061    pub span: Span,
1062}
1063
1064/// The `by (<binder>:)? <Actor>` clause on a handler (v0.45; binder optional in
1065/// v0.50). Names the actor contract the handler consumes; when a `binder` is
1066/// given, the verified identity binds to it and is read as `binder.identity`.
1067/// Omitting the binder (`by <Actor>`) declares-and-verifies the contract without
1068/// capturing the identity — for anonymous or verify-and-discard handlers. Sits
1069/// after the protocol config and before the parameters.
1070#[derive(Debug, Clone)]
1071pub struct ByClause {
1072    /// The identity binder, if the handler consumes the identity. `None` for the
1073    /// binder-less `by <Actor>` form. Required when `actors` names more than one
1074    /// (a sum is resolved by matching on the bound actor).
1075    pub binder: Option<Ident>,
1076    /// The actor contract(s) referenced — each a local actor decl or a prelude
1077    /// actor. A single name is the ordinary single-actor handler; more than one
1078    /// (`by who: A | B`, v0.52) is an **ordered sum of peer actors** resolved
1079    /// first-wins, the body matching on the resolved actor. Always non-empty.
1080    pub actors: Vec<Ident>,
1081    pub span: Span,
1082}
1083
1084impl ByClause {
1085    /// The first (and, for a single-actor handler, only) actor contract named.
1086    pub fn primary(&self) -> &Ident {
1087        &self.actors[0]
1088    }
1089    /// Whether this `by` clause names an ordered sum of peer actors (`A | B`).
1090    pub fn is_sum(&self) -> bool {
1091        self.actors.len() > 1
1092    }
1093}
1094
1095/// A handler block — `on call(args) -> T given C1, C2 { body }`.
1096/// Used by both services and agents.
1097#[derive(Debug, Clone)]
1098pub struct Handler {
1099    pub kind: HandlerKind,
1100    /// Handler-position annotations (v0.140, ADR 0163): `@cache(maxAge: 5.minutes)`
1101    /// written immediately before `on <METHOD>(…)`. Reuses the [`Annotation`] AST
1102    /// shared with `store` fields (ADR 0111); the grammar accepts any `@name(args)`
1103    /// so an unknown name is a project-validation diagnostic, not a parse error. The
1104    /// first handler-position annotation surface — empty for every handler that
1105    /// carries none.
1106    pub annotations: Vec<Annotation>,
1107    /// For agent handlers, the method-style handler name (e.g.
1108    /// `on call addItem(...)`). For service handlers, this is None (just
1109    /// `on call(...)`).
1110    pub method_name: Option<Ident>,
1111    /// The `by <binder>: <Actor>` clause (v0.45), if present. Service handlers
1112    /// only; an absent clause inherits the protocol's default actor.
1113    pub by_clause: Option<ByClause>,
1114    pub params: Vec<Param>,
1115    pub return_type: TypeRef,
1116    pub given: Vec<CapRef>,
1117    pub body: Block,
1118    pub documentation: Option<String>,
1119    pub span: Span,
1120    pub trivia: Trivia,
1121}
1122
1123#[derive(Debug, Clone, PartialEq, Eq)]
1124pub enum HandlerKind {
1125    /// `on call(...)` — typed RPC (the only kind in v0.5).
1126    Call,
1127    /// `on http METHOD "path"` — external-facing HTTP route (v0.9).
1128    Http { method: HttpMethod, path: String },
1129    /// `on cron "expr"` — scheduled task; `expr` is a 5-field cron
1130    /// expression (v0.10a).
1131    Cron { expr: String },
1132    /// `on message(m: T)` — a message off the service's bound queue. The queue
1133    /// binding lives on the service's `ServiceProtocol::Queue` (v0.44).
1134    Message,
1135    /// `on open ...` — the WebSocket upgrade handler (v0.103, real-time track
1136    /// slice 3). Exactly one per `from websocket` service; carries a mandatory
1137    /// `by` clause (edge auth) and receives a fresh owned `Connection[out]`.
1138    Open,
1139    /// `on close ...` — the WebSocket close handler (v0.106, real-time track slice
1140    /// 3b-iii). Optional, ≤1 per `from websocket` service; runs when the socket
1141    /// closes. Like `on open`, edge-authenticated (`by`), with the identity/params
1142    /// recovered from the socket attachment (set at `on open`). (A `from websocket`
1143    /// `on message` reuses [`HandlerKind::Message`], disambiguated by the protocol.)
1144    Close,
1145}
1146
1147/// HTTP methods supported by `on http` handlers (v0.9).
1148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1149pub enum HttpMethod {
1150    Get,
1151    Post,
1152    Put,
1153    Patch,
1154    Delete,
1155}
1156
1157impl HttpMethod {
1158    pub fn as_str(self) -> &'static str {
1159        match self {
1160            HttpMethod::Get => "GET",
1161            HttpMethod::Post => "POST",
1162            HttpMethod::Put => "PUT",
1163            HttpMethod::Patch => "PATCH",
1164            HttpMethod::Delete => "DELETE",
1165        }
1166    }
1167
1168    pub fn from_ident(s: &str) -> Option<HttpMethod> {
1169        match s {
1170            "GET" => Some(HttpMethod::Get),
1171            "POST" => Some(HttpMethod::Post),
1172            "PUT" => Some(HttpMethod::Put),
1173            "PATCH" => Some(HttpMethod::Patch),
1174            "DELETE" => Some(HttpMethod::Delete),
1175            _ => None,
1176        }
1177    }
1178
1179    /// True if this method conventionally has no request body.
1180    pub fn forbids_body(self) -> bool {
1181        matches!(self, HttpMethod::Get | HttpMethod::Delete)
1182    }
1183}
1184
1185/// Payload shape of an `HttpResult[T]` variant (v0.9 §3.3).
1186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1187pub enum HttpVariantPayload {
1188    /// No payload (e.g. `NoContent`, `Unauthorized`).
1189    None,
1190    /// Carries a value of the `HttpResult` type parameter `T`.
1191    Value,
1192    /// Carries a `String` message (e.g. `BadRequest`, `Conflict`).
1193    Message,
1194    /// Carries a `String` target URL, emitted as a `Location` header — the
1195    /// redirect variants (`Found`, `SeeOther`, `PermanentRedirect`, …).
1196    Location,
1197    /// Carries a `Stream[String]`, emitted as an SSE (`text/event-stream`)
1198    /// streaming body — the `Streaming` (200) variant (v0.101, real-time track
1199    /// slice 1).
1200    Streamed,
1201    /// Carries `(body: Bytes, contentType: String)` — the author-owned raw body
1202    /// written straight into the response with the declared `content-type` and
1203    /// **no codec** (the typed-wire guarantee is deliberately off). The `Raw`
1204    /// (200) variant (v0.111); the first two-argument payload shape.
1205    Raw,
1206}
1207
1208/// One variant of the built-in `HttpResult[T]` sum (v0.9 §3.3).
1209#[derive(Debug, Clone, Copy)]
1210pub struct HttpVariant {
1211    pub name: &'static str,
1212    pub payload: HttpVariantPayload,
1213    pub status: u16,
1214}
1215
1216/// All `HttpResult[T]` variants, in declaration order (ascending status). The
1217/// vocabulary tracks the common, modern HTTP status codes (RFC 9110): success
1218/// and created/accepted (`Value`), redirects carrying a `Location` URL, and
1219/// the client/server failures that handlers routinely return (`Message` when
1220/// an explanation helps the caller, `None` for self-describing statuses).
1221pub const HTTP_VARIANTS: &[HttpVariant] = &[
1222    // ── 2xx success ──────────────────────────────────────────────────────
1223    HttpVariant {
1224        name: "Ok",
1225        payload: HttpVariantPayload::Value,
1226        status: 200,
1227    },
1228    // v0.101 (real-time track slice 1): a 200 whose body is a streamed
1229    // `Stream[String]`, SSE-framed. Status precedes the body, so streaming is
1230    // 200-only — pre-stream failures are ordinary variants returned instead.
1231    HttpVariant {
1232        name: "Streaming",
1233        payload: HttpVariantPayload::Streamed,
1234        status: 200,
1235    },
1236    // v0.111: a 200 whose body is an author-owned `Bytes` written straight into
1237    // the response with the declared `content-type` — no codec runs. 200-only,
1238    // like `Streaming`: it serves service-tier raw bodies (`robots.txt`,
1239    // `sitemap.xml`, feeds, a QR PNG), not custom-status error pages.
1240    HttpVariant {
1241        name: "Raw",
1242        payload: HttpVariantPayload::Raw,
1243        status: 200,
1244    },
1245    HttpVariant {
1246        name: "Created",
1247        payload: HttpVariantPayload::Value,
1248        status: 201,
1249    },
1250    HttpVariant {
1251        name: "Accepted",
1252        payload: HttpVariantPayload::Value,
1253        status: 202,
1254    },
1255    HttpVariant {
1256        name: "NoContent",
1257        payload: HttpVariantPayload::None,
1258        status: 204,
1259    },
1260    // ── 3xx redirection (carry a `Location` URL) ─────────────────────────
1261    HttpVariant {
1262        name: "MovedPermanently",
1263        payload: HttpVariantPayload::Location,
1264        status: 301,
1265    },
1266    HttpVariant {
1267        name: "Found",
1268        payload: HttpVariantPayload::Location,
1269        status: 302,
1270    },
1271    HttpVariant {
1272        name: "SeeOther",
1273        payload: HttpVariantPayload::Location,
1274        status: 303,
1275    },
1276    HttpVariant {
1277        name: "TemporaryRedirect",
1278        payload: HttpVariantPayload::Location,
1279        status: 307,
1280    },
1281    HttpVariant {
1282        name: "PermanentRedirect",
1283        payload: HttpVariantPayload::Location,
1284        status: 308,
1285    },
1286    // ── 4xx client error ─────────────────────────────────────────────────
1287    HttpVariant {
1288        name: "BadRequest",
1289        payload: HttpVariantPayload::Message,
1290        status: 400,
1291    },
1292    HttpVariant {
1293        name: "Unauthorized",
1294        payload: HttpVariantPayload::None,
1295        status: 401,
1296    },
1297    HttpVariant {
1298        name: "Forbidden",
1299        payload: HttpVariantPayload::None,
1300        status: 403,
1301    },
1302    HttpVariant {
1303        name: "NotFound",
1304        payload: HttpVariantPayload::None,
1305        status: 404,
1306    },
1307    HttpVariant {
1308        name: "MethodNotAllowed",
1309        payload: HttpVariantPayload::None,
1310        status: 405,
1311    },
1312    HttpVariant {
1313        name: "NotAcceptable",
1314        payload: HttpVariantPayload::None,
1315        status: 406,
1316    },
1317    HttpVariant {
1318        name: "RequestTimeout",
1319        payload: HttpVariantPayload::None,
1320        status: 408,
1321    },
1322    HttpVariant {
1323        name: "Conflict",
1324        payload: HttpVariantPayload::Message,
1325        status: 409,
1326    },
1327    HttpVariant {
1328        name: "Gone",
1329        payload: HttpVariantPayload::None,
1330        status: 410,
1331    },
1332    HttpVariant {
1333        name: "LengthRequired",
1334        payload: HttpVariantPayload::None,
1335        status: 411,
1336    },
1337    HttpVariant {
1338        name: "PayloadTooLarge",
1339        payload: HttpVariantPayload::Message,
1340        status: 413,
1341    },
1342    HttpVariant {
1343        name: "UnsupportedMediaType",
1344        payload: HttpVariantPayload::Message,
1345        status: 415,
1346    },
1347    HttpVariant {
1348        name: "UnprocessableEntity",
1349        payload: HttpVariantPayload::Message,
1350        status: 422,
1351    },
1352    HttpVariant {
1353        name: "TooManyRequests",
1354        payload: HttpVariantPayload::Message,
1355        status: 429,
1356    },
1357    HttpVariant {
1358        name: "UnavailableForLegalReasons",
1359        payload: HttpVariantPayload::Message,
1360        status: 451,
1361    },
1362    // ── 5xx server error ─────────────────────────────────────────────────
1363    HttpVariant {
1364        name: "ServerError",
1365        payload: HttpVariantPayload::Message,
1366        status: 500,
1367    },
1368    HttpVariant {
1369        name: "NotImplemented",
1370        payload: HttpVariantPayload::Message,
1371        status: 501,
1372    },
1373    HttpVariant {
1374        name: "BadGateway",
1375        payload: HttpVariantPayload::Message,
1376        status: 502,
1377    },
1378    HttpVariant {
1379        name: "ServiceUnavailable",
1380        payload: HttpVariantPayload::Message,
1381        status: 503,
1382    },
1383    HttpVariant {
1384        name: "GatewayTimeout",
1385        payload: HttpVariantPayload::Message,
1386        status: 504,
1387    },
1388];
1389
1390/// Find an `HttpResult[T]` variant by name. Returns the variant info or
1391/// `None` if the name doesn't match.
1392pub fn http_variant(name: &str) -> Option<HttpVariant> {
1393    HTTP_VARIANTS.iter().copied().find(|v| v.name == name)
1394}
1395
1396/// Payload shape of a `QueueResult` variant (v0.44). Non-generic — a verdict
1397/// carries no value; `Retry` carries a `String` reason for the log path.
1398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1399pub enum QueueVariantPayload {
1400    /// No payload (`Ack`).
1401    None,
1402    /// Carries a `String` reason (`Retry`).
1403    Message,
1404}
1405
1406/// One variant of the built-in `QueueResult` sum (v0.44).
1407#[derive(Debug, Clone, Copy)]
1408pub struct QueueVariant {
1409    pub name: &'static str,
1410    pub payload: QueueVariantPayload,
1411}
1412
1413/// All `QueueResult` variants, in declaration order. `Ack` confirms the
1414/// message; `Retry` redelivers it, carrying a reason for observability.
1415pub const QUEUE_VARIANTS: &[QueueVariant] = &[
1416    QueueVariant {
1417        name: "Ack",
1418        payload: QueueVariantPayload::None,
1419    },
1420    QueueVariant {
1421        name: "Retry",
1422        payload: QueueVariantPayload::Message,
1423    },
1424];
1425
1426/// Find a `QueueResult` variant by name.
1427pub fn queue_variant(name: &str) -> Option<QueueVariant> {
1428    QUEUE_VARIANTS.iter().copied().find(|v| v.name == name)
1429}
1430
1431#[derive(Debug, Clone)]
1432pub struct TypeDecl {
1433    pub name: Ident,
1434    /// `[T, U]` type parameters (v0.157, ADR 0183): empty for a non-generic
1435    /// type. A generic *record* type (`type Paginated[T] = { … }`) is the only
1436    /// generic body accepted; the checker rejects type parameters on refined /
1437    /// opaque / sum bodies. Mirrors [`FnDecl::type_params`].
1438    pub type_params: Vec<TypeParam>,
1439    pub body: TypeBody,
1440    /// Documentation block attached to this declaration (v0.3).
1441    pub documentation: Option<String>,
1442    pub span: Span,
1443    pub trivia: Trivia,
1444}
1445
1446/// The right-hand side of a `type` declaration. In v0/v0.1 only the
1447/// `Refined` variant existed; v0.2 adds records and sums; v0.3 adds opaque.
1448#[derive(Debug, Clone)]
1449pub enum TypeBody {
1450    /// Refined base type: `BaseType where refinement`.
1451    Refined {
1452        base: BaseType,
1453        base_span: Span,
1454        refinement: Option<Refinement>,
1455    },
1456    /// Record type: `{ field: T where ..., ... }`.
1457    Record(RecordBody),
1458    /// Sum type: pipe-form variants or `enum { ... }` shorthand.
1459    Sum(SumBody),
1460    /// Opaque base type: `opaque BaseType (where refinement)?` (v0.3 §3.4).
1461    /// Identity is nominal; the base type is hidden outside the defining commons.
1462    Opaque {
1463        base: BaseType,
1464        base_span: Span,
1465        refinement: Option<Refinement>,
1466    },
1467}
1468
1469/// Body of a record-type declaration (v0.2 §3.1).
1470#[derive(Debug, Clone)]
1471pub struct RecordBody {
1472    pub fields: Vec<RecordField>,
1473    pub span: Span,
1474}
1475
1476/// One field of a record type declaration. Each field may carry inline
1477/// refinement, which is enforced at construction time on the field's value.
1478#[derive(Debug, Clone)]
1479pub struct RecordField {
1480    pub name: Ident,
1481    pub type_ref: TypeRef,
1482    pub refinement: Option<Refinement>,
1483    /// v0.11: an optional initial-value expression. Only meaningful on agent
1484    /// `state` fields (the field's fresh-key value); ignored / rejected on
1485    /// record-type fields by the checker.
1486    pub init: Option<Expr>,
1487    pub span: Span,
1488}
1489
1490/// Body of a sum-type declaration (v0.2 §3.2).
1491#[derive(Debug, Clone)]
1492pub struct SumBody {
1493    pub variants: Vec<Variant>,
1494    /// v0.154 (ADR 0178): declared error embeddings — `embeds E as V, …` after
1495    /// the variants. Each says "an `E` value auto-wraps into variant `V`", which
1496    /// the `?` operator uses to convert a cross-context error without a manual
1497    /// `.mapErr`. Empty for a sum with no embeddings.
1498    pub embeds: Vec<EmbedsClause>,
1499    pub span: Span,
1500}
1501
1502/// One `embeds <source_type> as <variant>` mapping in a sum body (v0.154, ADR
1503/// 0178). Declares that a value of `source_type` can be auto-wrapped into the
1504/// named single-payload `variant` of the enclosing sum.
1505#[derive(Debug, Clone)]
1506pub struct EmbedsClause {
1507    pub source_type: TypeRef,
1508    pub variant: Ident,
1509    pub span: Span,
1510}
1511
1512/// One variant of a sum type. Variants may have payload fields; a
1513/// payload-less variant is a simple tag.
1514#[derive(Debug, Clone)]
1515pub struct Variant {
1516    pub name: Ident,
1517    pub payload: Vec<VariantField>,
1518    pub span: Span,
1519}
1520
1521/// One payload field of a sum variant. Variant payload fields use named
1522/// declarations like record fields, but do not carry refinement in v0.2.
1523#[derive(Debug, Clone)]
1524pub struct VariantField {
1525    pub name: Ident,
1526    pub type_ref: TypeRef,
1527    pub span: Span,
1528}
1529
1530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1531pub enum BaseType {
1532    Int,
1533    String,
1534    Bool,
1535    Float,
1536    /// `Duration` (v0.86, ADR 0112) — a span of time, a distinct base type
1537    /// erased to TS `number` carrying milliseconds (the `Clock` unit). Modelled
1538    /// on `Float`: Bynk-side-only, no implicit `Int` coercion (save the one
1539    /// sanctioned clock-math mix).
1540    Duration,
1541    /// `Instant` (v0.90, ADR 0114) — an absolute point in time, a distinct base
1542    /// type erased to TS `number` carrying Unix epoch milliseconds (the
1543    /// `Clock` unit). No literal (minted by `Clock.now()`); arithmetic composes
1544    /// with `Duration` (`Instant ± Duration -> Instant`, `Instant − Instant ->
1545    /// Duration`). Supersedes ADR 0112 D4's `Int`↔`Duration` clock-math mix.
1546    Instant,
1547    /// `Bytes` (v0.110, ADR 0142) — an immutable finite octet sequence, the
1548    /// seventh base type. Unlike its neighbours it does **not** erase to TS
1549    /// `number`: a `Bytes` lowers to a `Uint8Array`. No source literal
1550    /// (constructed via `Bytes.fromUtf8`/`fromBase64`/`empty`); `==` compares
1551    /// by content (real emitter codegen, not host `===`); wires as a base64
1552    /// JSON string; not `Map`-keyable and not orderable.
1553    Bytes,
1554}
1555
1556impl BaseType {
1557    pub fn name(self) -> &'static str {
1558        match self {
1559            BaseType::Int => "Int",
1560            BaseType::String => "String",
1561            BaseType::Bool => "Bool",
1562            BaseType::Float => "Float",
1563            BaseType::Duration => "Duration",
1564            BaseType::Instant => "Instant",
1565            BaseType::Bytes => "Bytes",
1566        }
1567    }
1568}
1569
1570/// A `Duration` literal unit (v0.86, ADR 0112) — the closed set of suffixes in a
1571/// `<int>.<unit>` literal. Each maps to a fixed millisecond factor (`Duration`
1572/// erases to `Int` milliseconds).
1573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1574pub enum DurationUnit {
1575    Milliseconds,
1576    Seconds,
1577    Minutes,
1578    Hours,
1579    Days,
1580}
1581
1582impl DurationUnit {
1583    /// Resolve a unit name (`minutes`) to its variant, or `None` if it is not one
1584    /// of the closed set. Used by the parser to recognise an `<int>.<unit>`
1585    /// literal; an unrecognised name leaves the expression a field access.
1586    pub fn from_name(name: &str) -> Option<Self> {
1587        Some(match name {
1588            "milliseconds" => DurationUnit::Milliseconds,
1589            "seconds" => DurationUnit::Seconds,
1590            "minutes" => DurationUnit::Minutes,
1591            "hours" => DurationUnit::Hours,
1592            "days" => DurationUnit::Days,
1593            _ => return None,
1594        })
1595    }
1596
1597    /// The unit name as written.
1598    pub fn name(self) -> &'static str {
1599        match self {
1600            DurationUnit::Milliseconds => "milliseconds",
1601            DurationUnit::Seconds => "seconds",
1602            DurationUnit::Minutes => "minutes",
1603            DurationUnit::Hours => "hours",
1604            DurationUnit::Days => "days",
1605        }
1606    }
1607
1608    /// The unit's value in milliseconds.
1609    pub fn millis(self) -> i64 {
1610        match self {
1611            DurationUnit::Milliseconds => 1,
1612            DurationUnit::Seconds => 1_000,
1613            DurationUnit::Minutes => 60_000,
1614            DurationUnit::Hours => 3_600_000,
1615            DurationUnit::Days => 86_400_000,
1616        }
1617    }
1618}
1619
1620/// An integer refinement bound (v0.40, ADR 0073): the parsed value plus the
1621/// bound's source span (covering a leading `-`). Value-only beyond the span —
1622/// ints have one canonical printed form, so the formatter stays idempotent
1623/// without a stored lexeme. The span backs the `InRange`-swap quick-fix.
1624#[derive(Debug, Clone)]
1625pub struct IntBound {
1626    pub value: i64,
1627    pub span: Span,
1628}
1629
1630/// A float refinement bound (v0.21): the parsed value plus the signed source
1631/// lexeme (for byte-stable emission). v0.40 (ADR 0073): also the source span,
1632/// for the `InRange`-swap quick-fix.
1633#[derive(Debug, Clone)]
1634pub struct FloatBound {
1635    pub value: f64,
1636    pub lexeme: String,
1637    pub span: Span,
1638}
1639
1640#[derive(Debug, Clone)]
1641pub struct Refinement {
1642    pub predicates: Vec<RefinementPred>,
1643    pub span: Span,
1644}
1645
1646#[derive(Debug, Clone)]
1647pub struct RefinementPred {
1648    pub kind: PredKind,
1649    pub span: Span,
1650}
1651
1652#[derive(Debug, Clone)]
1653pub enum PredKind {
1654    Matches(String),
1655    InRange(IntBound, IntBound),
1656    /// `InRange` with float bounds (v0.21) — a separate variant so every
1657    /// `Int` refinement path stays untouched. Bounds keep their source
1658    /// lexemes (including any sign) so emitted runtime checks are
1659    /// byte-stable.
1660    InRangeF(FloatBound, FloatBound),
1661    MinLength(i64),
1662    MaxLength(i64),
1663    Length(i64),
1664    NonNegative,
1665    Positive,
1666    NonEmpty,
1667}
1668
1669impl PredKind {
1670    pub fn name(&self) -> &'static str {
1671        match self {
1672            PredKind::Matches(_) => "Matches",
1673            PredKind::InRange(..) | PredKind::InRangeF(..) => "InRange",
1674            PredKind::MinLength(_) => "MinLength",
1675            PredKind::MaxLength(_) => "MaxLength",
1676            PredKind::Length(_) => "Length",
1677            PredKind::NonNegative => "NonNegative",
1678            PredKind::Positive => "Positive",
1679            PredKind::NonEmpty => "NonEmpty",
1680        }
1681    }
1682}
1683
1684/// A function type parameter (v0.20a, `fn name[A, B](…)`). A struct rather
1685/// than a bare Ident so the ADR-0028 "bound-capable" promise is a later field
1686/// addition, not a representation change.
1687#[derive(Debug, Clone)]
1688pub struct TypeParam {
1689    pub name: Ident,
1690    pub span: Span,
1691}
1692
1693/// A lambda expression (v0.20a): `(params) => expr` or `(params) => { … }`.
1694/// `=>` is the value arrow (shared with `match`); param annotations are
1695/// optional where an expected function type supplies them.
1696#[derive(Debug, Clone)]
1697pub struct LambdaExpr {
1698    pub params: Vec<LambdaParam>,
1699    pub body: Box<Expr>,
1700    pub span: Span,
1701}
1702
1703/// A lambda parameter. A separate type from [`Param`] because its annotation
1704/// is optional — `Param.type_ref` stays mandatory at every signature site.
1705#[derive(Debug, Clone)]
1706pub struct LambdaParam {
1707    pub name: Ident,
1708    pub type_ref: Option<TypeRef>,
1709    pub span: Span,
1710}
1711
1712#[derive(Debug, Clone)]
1713pub struct FnDecl {
1714    /// v0.20a: `[A, B]` type parameters; empty for non-generic functions.
1715    pub type_params: Vec<TypeParam>,
1716    /// Free function or method (`TypeName.methodName`). See [`FnName`].
1717    pub name: FnName,
1718    pub params: Vec<Param>,
1719    pub return_type: TypeRef,
1720    /// v0.115: preconditions (`requires <name>: <pred>`), parsed between the
1721    /// return type and the body. A contract clause is the invariant predicate
1722    /// attached to a function (ADR 0144 — one predicate surface); `requires`
1723    /// scopes over the parameters only.
1724    pub requires: Vec<Contract>,
1725    /// v0.115: postconditions (`ensures <name>: <pred>`). Scopes over the
1726    /// parameters *and* `result`, the contextual binding for the return value.
1727    pub ensures: Vec<Contract>,
1728    pub body: Block,
1729    /// True when the first parameter is the special `self` parameter. Only
1730    /// valid for method declarations.
1731    pub has_self: bool,
1732    /// Documentation block attached to this declaration (v0.3).
1733    pub documentation: Option<String>,
1734    pub span: Span,
1735    pub trivia: Trivia,
1736}
1737
1738/// A function-declaration name: either a free function `f` or a method
1739/// `T.method` (v0.2 §3.6).
1740#[derive(Debug, Clone)]
1741pub enum FnName {
1742    /// `fn name(...)` — a free function.
1743    Free(Ident),
1744    /// `fn TypeName.methodName(...)` — a method attached to a type.
1745    Method {
1746        type_name: Ident,
1747        method_name: Ident,
1748    },
1749}
1750
1751impl FnName {
1752    /// The function's short name for diagnostics. For methods returns the
1753    /// method portion only; the type prefix is recovered via `type_name`.
1754    pub fn ident(&self) -> &Ident {
1755        match self {
1756            FnName::Free(id) => id,
1757            FnName::Method { method_name, .. } => method_name,
1758        }
1759    }
1760
1761    /// For methods, the attached type's identifier; `None` for free fns.
1762    pub fn type_name(&self) -> Option<&Ident> {
1763        match self {
1764            FnName::Free(_) => None,
1765            FnName::Method { type_name, .. } => Some(type_name),
1766        }
1767    }
1768
1769    /// The displayed full name (e.g., `Money.add` or `parseSku`).
1770    pub fn display(&self) -> String {
1771        match self {
1772            FnName::Free(id) => id.name.clone(),
1773            FnName::Method {
1774                type_name,
1775                method_name,
1776            } => format!("{}.{}", type_name.name, method_name.name),
1777        }
1778    }
1779}
1780
1781/// A brace-delimited block of statements ending in a tail expression
1782/// whose value is the block's value (spec v0.1 §3.1).
1783#[derive(Debug, Clone)]
1784pub struct Block {
1785    pub statements: Vec<Statement>,
1786    pub tail: Box<Expr>,
1787    pub span: Span,
1788    /// Line comments that appear between the last statement (or the
1789    /// opening brace) and the tail expression. Preserved here because
1790    /// expressions do not carry trivia in v1.1.
1791    pub tail_leading_comments: Vec<String>,
1792    /// `true` when the block was written with no explicit tail expression and
1793    /// the parser synthesised a `()` (unit) tail (v0.146, ADR 0170). The tail
1794    /// is a real `ExprKind::UnitLit` either way; this flag records that it was
1795    /// *implicit* so the formatter can omit it (Bynk has no statement
1796    /// terminator, so a printed `()` would re-attach to the last statement on
1797    /// re-parse — `x` `()` → `x()`). The parser re-derives the implicit unit
1798    /// tail, so omitting it is loss-free.
1799    pub implicit_tail: bool,
1800}
1801
1802impl Block {
1803    /// Whether this block is a synthesised empty unit block — no statements and
1804    /// an *implicit* `()` tail (v0.146, ADR 0170). This is exactly the shape the
1805    /// parser inserts for an `if` with no `else` branch, so both the checker
1806    /// (gating the else-less form to unit) and the formatter (omitting the
1807    /// synthetic `else { () }`) recognise it here.
1808    pub fn is_synth_unit(&self) -> bool {
1809        self.statements.is_empty()
1810            && self.implicit_tail
1811            && matches!(self.tail.kind, ExprKind::UnitLit)
1812    }
1813}
1814
1815/// Block-level statement.
1816#[derive(Debug, Clone)]
1817pub enum Statement {
1818    /// `let name (: T)? = expr` — pure binding (v0.1).
1819    Let(LetStmt),
1820    /// `let name (: T)? <- expr` — effectful binding (v0.5).
1821    EffectLet(LetStmt),
1822    /// `expect expr` — verify a Bool predicate at test runtime (v0.7; renamed
1823    /// from `assert` in v0.112). Only valid inside test case bodies.
1824    Expect(ExpectStmt),
1825    /// `~> expr` — an asynchronous fire-and-forget send (v0.79). The caller does
1826    /// not await the reply; legal only when the reply is `Effect[()]`. No binder.
1827    Send(SendStmt),
1828    /// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
1829    /// Runs an `Effect[()]` and discards its (unit) result — the binder-free
1830    /// sugar for `let _ <- expr` when the awaited value is unit. Legal only in
1831    /// an effectful body; the operand MUST be `Effect[()]` (a valued reply keeps
1832    /// the explicit `let _ <- e`, so throwing away a real value stays visible).
1833    Do(DoStmt),
1834    /// `name := expr` — a `Cell` store write (v0.81, storage track). The
1835    /// unconditional write form; `.update(fn)` (a method call) is the
1836    /// read-modify-write form. ADR 0108.
1837    Assign(AssignStmt),
1838}
1839
1840impl Statement {
1841    pub fn span(&self) -> Span {
1842        match self {
1843            Statement::Let(l) | Statement::EffectLet(l) => l.span,
1844            Statement::Expect(a) => a.span,
1845            Statement::Send(s) => s.span,
1846            Statement::Do(d) => d.span,
1847            Statement::Assign(a) => a.span,
1848        }
1849    }
1850}
1851
1852#[derive(Debug, Clone)]
1853pub struct ExpectStmt {
1854    pub value: Expr,
1855    pub span: Span,
1856    pub trivia: Trivia,
1857}
1858
1859/// `name := expr` — a `Cell` store write (v0.81, storage track). `target` is the
1860/// `Cell` field being written (a bare name for now; the checker resolves it to a
1861/// `store` field). `value` is the new value.
1862#[derive(Debug, Clone)]
1863pub struct AssignStmt {
1864    pub target: Ident,
1865    pub value: Expr,
1866    pub span: Span,
1867    pub trivia: Trivia,
1868}
1869
1870#[derive(Debug, Clone)]
1871pub struct LetStmt {
1872    pub name: Ident,
1873    pub type_annot: Option<TypeRef>,
1874    pub value: Expr,
1875    pub span: Span,
1876    pub trivia: Trivia,
1877}
1878
1879#[derive(Debug, Clone)]
1880pub struct SendStmt {
1881    /// The send target — a recipient call, e.g. `Logger.info(msg)`.
1882    pub value: Expr,
1883    pub span: Span,
1884    pub trivia: Trivia,
1885}
1886
1887/// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
1888/// `value` is the awaited effect, which MUST be `Effect[()]`.
1889#[derive(Debug, Clone)]
1890pub struct DoStmt {
1891    pub value: Expr,
1892    pub span: Span,
1893    pub trivia: Trivia,
1894}
1895
1896#[derive(Debug, Clone)]
1897pub struct Param {
1898    pub name: Ident,
1899    pub type_ref: TypeRef,
1900    pub span: Span,
1901}
1902
1903#[derive(Debug, Clone)]
1904pub enum TypeRef {
1905    Base(BaseType, Span),
1906    Named(Ident),
1907    /// `Result[T, E]` — the built-in generic Result type (v0.1).
1908    Result(Box<TypeRef>, Box<TypeRef>, Span),
1909    /// `Option[T]` — the built-in generic Option type (v0.2).
1910    Option(Box<TypeRef>, Span),
1911    /// `Effect[T]` — the built-in generic Effect type (v0.5).
1912    Effect(Box<TypeRef>, Span),
1913    /// `HttpResult[T]` — the built-in HTTP-result sum (v0.9).
1914    HttpResult(Box<TypeRef>, Span),
1915    /// `QueueResult` — the built-in queue verdict sum (`Ack | Retry`),
1916    /// non-generic; the required return of a queue handler (v0.44).
1917    QueueResult(Span),
1918    /// `List[T]` — the built-in generic immutable list type (v0.20b).
1919    List(Box<TypeRef>, Span),
1920    /// `Map[K, V]` — the built-in generic immutable map type (v0.20b).
1921    /// Keys are confined to value-keyable types
1922    /// (`bynk.types.unkeyable_map_key`).
1923    Map(Box<TypeRef>, Box<TypeRef>, Span),
1924    /// `Query[T]` — the built-in lazy storage-read description (v0.91, ADR 0115).
1925    /// Nameable in a pure helper's return type; non-storable and non-boundary
1926    /// (like `Effect`/`Fn`).
1927    Query(Box<TypeRef>, Span),
1928    /// `Stream[T]` — the value-over-time primitive (v0.100, real-time track
1929    /// slice 0). A lazy, pull-shaped sequence produced over time; non-storable
1930    /// and non-boundary (like `Query`/`Effect`/`Fn`).
1931    Stream(Box<TypeRef>, Span),
1932    /// `Connection[F]` — a held WebSocket connection (v0.102, real-time track
1933    /// slice 2). `F` is the server→client frame type. A `Held` resource:
1934    /// non-serialisable, non-boundary, and governed by the linearity discipline
1935    /// (§2.9); storable only in `Cell[Option[Connection]]` / `Map[K, Connection]`.
1936    Connection(Box<TypeRef>, Span),
1937    /// `History[Agent]` — a generated, driven call-history of an agent (v0.119,
1938    /// testing track slice 7, ADR 0155). A test-only generator, legal only in
1939    /// `for all` binding position inside a `property`; it is not a value type,
1940    /// so it never resolves in a field/param/return position. The bound subject
1941    /// behaves as an ordinary `List[Step]`.
1942    History(Box<TypeRef>, Span),
1943    /// `ValidationError` — the built-in error type used by refined-type
1944    /// constructors (v0.1).
1945    ValidationError(Span),
1946    /// `JsonError` — the built-in JSON-decode error type (v0.22b). A
1947    /// uniform record (`kind`/`path`/`message`, all `String`) the codec
1948    /// maps `BoundaryError` variants and parse failures into.
1949    JsonError(Span),
1950    /// `()` — the unit type (v0.5).
1951    Unit(Span),
1952    /// `A -> B` / `(A, B) -> C` / `() -> B` — a function type (v0.20a).
1953    /// Right-associative; effectful iff the return type is `Effect[_]`
1954    /// (the structural rule). Confined to non-boundary positions
1955    /// (`bynk.types.function_at_boundary`).
1956    Fn(Vec<TypeRef>, Box<TypeRef>, Span),
1957    /// `Name[Arg, …]` — an application of a user-declared generic type
1958    /// (v0.157, ADR 0183). `name` is a user type name (never a built-in
1959    /// generic, which each have a dedicated variant above). Arity and the
1960    /// existence of the referenced type are checked in the resolver.
1961    App {
1962        name: Ident,
1963        args: Vec<TypeRef>,
1964        span: Span,
1965    },
1966}
1967
1968impl TypeRef {
1969    pub fn span(&self) -> Span {
1970        match self {
1971            TypeRef::Base(_, s) => *s,
1972            TypeRef::Named(id) => id.span,
1973            TypeRef::Result(_, _, s) => *s,
1974            TypeRef::Option(_, s) => *s,
1975            TypeRef::Effect(_, s) => *s,
1976            TypeRef::HttpResult(_, s) => *s,
1977            TypeRef::QueueResult(s) => *s,
1978            TypeRef::List(_, s) => *s,
1979            TypeRef::Map(_, _, s) => *s,
1980            TypeRef::Query(_, s) => *s,
1981            TypeRef::Stream(_, s) => *s,
1982            TypeRef::Connection(_, s) => *s,
1983            TypeRef::History(_, s) => *s,
1984            TypeRef::ValidationError(s) => *s,
1985            TypeRef::JsonError(s) => *s,
1986            TypeRef::Unit(s) => *s,
1987            TypeRef::Fn(_, _, s) => *s,
1988            TypeRef::App { span, .. } => *span,
1989        }
1990    }
1991}
1992
1993#[derive(Debug, Clone)]
1994pub struct Expr {
1995    pub kind: ExprKind,
1996    pub span: Span,
1997}
1998
1999impl ExprKind {
2000    /// Construct an `IntLit` for a *synthesized* integer — one the compiler
2001    /// invents rather than reading from source (a default `1`, a computed bound).
2002    /// The lexeme is the canonical decimal form (no separators). Source-parsed
2003    /// literals keep their as-written lexeme instead (v0.142, ADR 0166).
2004    pub fn int_lit(value: i64) -> ExprKind {
2005        ExprKind::IntLit {
2006            value,
2007            lexeme: value.to_string(),
2008        }
2009    }
2010}
2011
2012#[derive(Debug, Clone)]
2013pub enum ExprKind {
2014    /// An integer literal (typed `Int`). The lexeme is kept alongside the parsed
2015    /// value (v0.142, ADR 0166) so formatting is byte-stable: an author's `_`
2016    /// digit separators (`1_048_576`) survive a round-trip, mirroring the
2017    /// `FloatLit` treatment. The value is separator-free; emission lowers the
2018    /// value, so emitted output is unaffected.
2019    IntLit {
2020        value: i64,
2021        lexeme: String,
2022    },
2023    /// A float literal (v0.21). The lexeme is kept alongside the parsed
2024    /// value so emission and formatting are byte-stable (`1e10` must not
2025    /// normalise to `10000000000`).
2026    FloatLit {
2027        value: f64,
2028        lexeme: String,
2029    },
2030    /// A duration literal `<int>.<unit>` (v0.86, ADR 0112): `5.minutes`,
2031    /// `30.days`. The parser recognises the `IntLit . <unit>` shape and records
2032    /// the magnitude, the unit, and the resolved milliseconds (the value the
2033    /// emitter lowers to). Typed `Duration`.
2034    DurationLit {
2035        /// The integer magnitude as written (`5` in `5.minutes`).
2036        value: i64,
2037        /// The unit name (`minutes`), one of the closed set.
2038        unit: DurationUnit,
2039        /// The value in milliseconds — `value * unit factor`.
2040        millis: i64,
2041    },
2042    StrLit(String),
2043    /// An interpolated string `"… \(expr) …"` (v0.43, ADR 0075). Chunks and
2044    /// holes alternate. A plain `"…"` with no holes stays [`ExprKind::StrLit`],
2045    /// so existing code and the emitter/formatter fast-path are untouched.
2046    InterpStr(Vec<InterpPart>),
2047    BoolLit(bool),
2048    Ident(Ident),
2049    Call {
2050        name: Ident,
2051        /// v0.20a: explicit type arguments (`name[T](…)`); empty when absent.
2052        type_args: Vec<TypeRef>,
2053        args: Vec<Expr>,
2054    },
2055    /// A lambda (v0.20a). See [`LambdaExpr`].
2056    Lambda(LambdaExpr),
2057    BinOp(BinOp, Box<Expr>, Box<Expr>),
2058    UnaryOp(UnaryOp, Box<Expr>),
2059    Paren(Box<Expr>),
2060    /// `{ stmts; expr }` — block expression (v0.1).
2061    Block(Block),
2062    /// `if cond { then } else { else }` (v0.1).
2063    If {
2064        cond: Box<Expr>,
2065        then_block: Box<Block>,
2066        else_block: Box<Block>,
2067    },
2068    /// `Ok(value)` — Result success constructor (v0.1).
2069    Ok(Box<Expr>),
2070    /// `Err(error)` — Result failure constructor (v0.1).
2071    Err(Box<Expr>),
2072    /// `expr?` — propagation operator (v0.1).
2073    Question(Box<Expr>),
2074    /// `TypeName.method(args)` — qualified static call on a type
2075    /// (v0.1: only refined-type `of`; v0.2: any static method or variant
2076    /// constructor for sum types). The resolver decides which.
2077    ConstructorCall {
2078        type_name: Ident,
2079        method: Ident,
2080        args: Vec<Expr>,
2081    },
2082    /// `TypeName { field: value, ... }` — record construction (v0.2).
2083    RecordConstruction {
2084        type_name: Ident,
2085        fields: Vec<FieldInit>,
2086    },
2087    /// `receiver.field` — field access on a record value (v0.2). v0.3 adds
2088    /// `.raw` on opaque types within the defining commons.
2089    FieldAccess {
2090        receiver: Box<Expr>,
2091        field: Ident,
2092    },
2093    /// `receiver.method(args)` — instance method call (v0.2). The
2094    /// resolver determines the receiver's type and looks up the method.
2095    MethodCall {
2096        receiver: Box<Expr>,
2097        method: Ident,
2098        /// v0.22b: explicit type arguments on a qualified static
2099        /// (`Json.decode[T](…)`); empty when absent. The same-line-`[`
2100        /// rule applies as for `Call` type application (0039).
2101        type_args: Vec<TypeRef>,
2102        args: Vec<Expr>,
2103    },
2104    /// `match disc { arm+ }` — pattern matching (v0.2).
2105    Match {
2106        discriminant: Box<Expr>,
2107        arms: Vec<MatchArm>,
2108    },
2109    /// `expr is pattern` — pattern test, returns Bool (v0.2).
2110    Is {
2111        value: Box<Expr>,
2112        pattern: Pattern,
2113    },
2114    /// `Some(value)` — Option Some constructor (v0.2).
2115    Some(Box<Expr>),
2116    /// `None` — Option None constructor (v0.2).
2117    None,
2118    /// `()` — unit literal (v0.5).
2119    UnitLit,
2120    /// `TypeName { ...base, field: value, ... }` or `{ ...base, ... }` —
2121    /// record spread expression (v0.5).
2122    RecordSpread {
2123        /// Optional type prefix (`TypeName { ...base }`). Absent for the
2124        /// bare form used inside `commit`.
2125        type_name: Option<Ident>,
2126        /// The base record being spread.
2127        base: Box<Expr>,
2128        /// Field overrides (always full `name: value` form — never shorthand).
2129        overrides: Vec<FieldInit>,
2130    },
2131    /// `Effect.pure(value)` — wrap a synchronous value into `Effect[T]`
2132    /// (v0.5). Recognised in the parser as a special-form.
2133    EffectPure(Box<Expr>),
2134    /// `expect expr` — expectation as an expression of type `()` (v0.9.1;
2135    /// renamed from `assert` in v0.112). Valid only inside test bodies. Evaluates
2136    /// `expr` (must be Bool); if false, the surrounding test case fails.
2137    Expect(Box<Expr>),
2138    /// `Val[T]`, `Val[T](args)` — test-context value construction (v0.9.4).
2139    /// `args` is empty for the bare form and holds the pin arguments for
2140    /// `Val[T](...)`. The record-override form `Val[T] { ... }` is not yet
2141    /// parsed. Valid only inside test bodies; has type `T`.
2142    Val {
2143        type_ref: TypeRef,
2144        args: Vec<Expr>,
2145    },
2146    /// `[a, b, c]` — list literal (v0.20b). An empty `[]` requires an
2147    /// expected type (`bynk.types.uninferable_element_type`).
2148    ListLit(Vec<Expr>),
2149    /// An observation over a consumed capability's recorded calls (v0.117,
2150    /// testing track slice 5). The direct subject of an `expect` in a `case`
2151    /// body — `expect Cap.op called once with <pred>`, `expect Cap.op never
2152    /// called`, `expect A.op before B.op`. Types as `Bool` (the claim about the
2153    /// recorded trace), lowered to a boolean over the recorded log.
2154    Observation(ObservationExpr),
2155    /// `trace(Cap.op)` — the bound-trace escape hatch (v0.117, testing track
2156    /// slice 5). Yields the recorded calls of `Cap.op` as a `List[<CallRecord>]`
2157    /// (a synthetic record of the operation's parameters), asserted over with the
2158    /// ordinary value surface. Test-body-only, like [`ExprKind::Val`].
2159    Trace {
2160        cap: Ident,
2161        op: Ident,
2162    },
2163}
2164
2165/// Every directly-nested sub-expression of `e` — the **total** child
2166/// iterator. The match is exhaustive (no `_` arm), so adding an [`ExprKind`]
2167/// variant is a compile error here rather than a silently incomplete walk —
2168/// the trap the checker's three hand-rolled partial walkers each fell into
2169/// (block statements and match-arm bodies were skipped, so e.g. the `:=`
2170/// self-reference rule was bypassable through a match arm).
2171///
2172/// Descends one level: block *statements* and the tail, match-arm bodies,
2173/// lambda bodies, interpolation holes, record-field values, and observation
2174/// predicates are all children. Callers recurse for a deep walk.
2175pub fn expr_children(e: &Expr) -> Vec<&Expr> {
2176    fn block_children<'a>(b: &'a Block, out: &mut Vec<&'a Expr>) {
2177        for s in &b.statements {
2178            statement_exprs(s, out);
2179        }
2180        out.push(&b.tail);
2181    }
2182    let mut out = Vec::new();
2183    match &e.kind {
2184        ExprKind::IntLit { .. }
2185        | ExprKind::FloatLit { .. }
2186        | ExprKind::DurationLit { .. }
2187        | ExprKind::StrLit(_)
2188        | ExprKind::BoolLit(_)
2189        | ExprKind::Ident(_)
2190        | ExprKind::None
2191        | ExprKind::UnitLit
2192        | ExprKind::Trace { .. } => {}
2193        ExprKind::InterpStr(parts) => {
2194            for p in parts {
2195                if let InterpPart::Hole(h) = p {
2196                    out.push(h.as_ref());
2197                }
2198            }
2199        }
2200        ExprKind::Call { args, .. }
2201        | ExprKind::ConstructorCall { args, .. }
2202        | ExprKind::Val { args, .. }
2203        | ExprKind::ListLit(args) => out.extend(args.iter()),
2204        ExprKind::Lambda(l) => out.push(l.body.as_ref()),
2205        ExprKind::BinOp(_, l, r) => {
2206            out.push(l.as_ref());
2207            out.push(r.as_ref());
2208        }
2209        ExprKind::UnaryOp(_, inner)
2210        | ExprKind::Paren(inner)
2211        | ExprKind::Ok(inner)
2212        | ExprKind::Err(inner)
2213        | ExprKind::Question(inner)
2214        | ExprKind::Some(inner)
2215        | ExprKind::EffectPure(inner)
2216        | ExprKind::Expect(inner) => out.push(inner.as_ref()),
2217        ExprKind::Block(b) => block_children(b, &mut out),
2218        ExprKind::If {
2219            cond,
2220            then_block,
2221            else_block,
2222        } => {
2223            out.push(cond.as_ref());
2224            block_children(then_block, &mut out);
2225            block_children(else_block, &mut out);
2226        }
2227        ExprKind::RecordConstruction { fields, .. } => {
2228            out.extend(fields.iter().filter_map(|f| f.value.as_ref()));
2229        }
2230        ExprKind::FieldAccess { receiver, .. } => out.push(receiver.as_ref()),
2231        ExprKind::MethodCall { receiver, args, .. } => {
2232            out.push(receiver.as_ref());
2233            out.extend(args.iter());
2234        }
2235        ExprKind::Match { discriminant, arms } => {
2236            out.push(discriminant.as_ref());
2237            for arm in arms {
2238                match &arm.body {
2239                    MatchBody::Expr(e) => out.push(e),
2240                    MatchBody::Block(b) => block_children(b, &mut out),
2241                }
2242            }
2243        }
2244        ExprKind::Is { value, .. } => out.push(value.as_ref()),
2245        ExprKind::RecordSpread {
2246            base, overrides, ..
2247        } => {
2248            out.push(base.as_ref());
2249            out.extend(overrides.iter().filter_map(|f| f.value.as_ref()));
2250        }
2251        ExprKind::Observation(obs) => match &obs.matcher {
2252            ObservationMatcher::Called { count, with_pred } => {
2253                if let Some(c) = count {
2254                    out.push(c.as_ref());
2255                }
2256                if let Some(p) = with_pred {
2257                    out.push(p.as_ref());
2258                }
2259            }
2260            ObservationMatcher::NeverCalled | ObservationMatcher::Before { .. } => {}
2261        },
2262    }
2263    out
2264}
2265
2266/// The expressions directly contained in a statement — the statement half of
2267/// [`expr_children`]'s total walk. Exhaustive over [`Statement`] for the same
2268/// reason.
2269pub fn statement_exprs<'a>(s: &'a Statement, out: &mut Vec<&'a Expr>) {
2270    match s {
2271        Statement::Let(l) | Statement::EffectLet(l) => out.push(&l.value),
2272        Statement::Expect(a) => out.push(&a.value),
2273        Statement::Send(snd) => out.push(&snd.value),
2274        Statement::Do(d) => out.push(&d.value),
2275        Statement::Assign(a) => out.push(&a.value),
2276    }
2277}
2278
2279/// An observation of a capability operation's recorded calls (v0.117, testing
2280/// track slice 5). `cap`/`op` name the seam (`Logger.log`); `matcher` is the
2281/// claim about the recorded calls.
2282#[derive(Debug, Clone)]
2283pub struct ObservationExpr {
2284    pub cap: Ident,
2285    pub op: Ident,
2286    pub matcher: ObservationMatcher,
2287}
2288
2289/// The claim an [`ObservationExpr`] makes about a seam's recorded calls (v0.117).
2290#[derive(Debug, Clone)]
2291pub enum ObservationMatcher {
2292    /// `called` [`once` | `<n> times`]? [`with` `<pred>`]?. `count` is `None`
2293    /// for a bare `called` (at least one); `Some(expr)` is the exact-count claim
2294    /// (a literal; `once` desugars to `1`). `with_pred` matches a call whose
2295    /// arguments (in scope by the operation's parameter names) satisfy it.
2296    Called {
2297        count: Option<Box<Expr>>,
2298        with_pred: Option<Box<Expr>>,
2299    },
2300    /// `never called` — zero calls.
2301    NeverCalled,
2302    /// `before Cap.op` — the first call of the subject precedes the first call
2303    /// of the named operation (both must have occurred).
2304    Before { cap: Ident, op: Ident },
2305}
2306
2307/// One part of an interpolated string (v0.43, ADR 0075). An
2308/// [`ExprKind::InterpStr`] holds an alternating run of these.
2309#[derive(Debug, Clone)]
2310pub enum InterpPart {
2311    /// Literal text between holes, with escapes already resolved.
2312    Chunk(String),
2313    /// An interpolated expression `\(expr)`. Type-checked by the hole rule
2314    /// (base scalars only; see the checker) and lowered into a template-
2315    /// literal `${…}` slot.
2316    Hole(Box<Expr>),
2317}
2318
2319/// One field-initialiser inside a record construction expression:
2320/// either `name: expr` or the shorthand `name` (which requires a binding
2321/// of the same name in scope and uses its value).
2322#[derive(Debug, Clone)]
2323pub struct FieldInit {
2324    pub name: Ident,
2325    /// `None` means shorthand — the field's value is the same-named binding.
2326    pub value: Option<Expr>,
2327    pub span: Span,
2328}
2329
2330/// One arm of a `match` expression: `pattern => body` or, with a guard,
2331/// `pattern if guard => body` (guard added in the nested-patterns increment,
2332/// ADR 0169). A guarded arm matches only when the pattern matches **and** the
2333/// `Bool` guard evaluates true; it never contributes to exhaustiveness.
2334#[derive(Debug, Clone)]
2335pub struct MatchArm {
2336    pub pattern: Pattern,
2337    /// Optional `if <Bool-expr>` guard between the pattern and `=>`.
2338    pub guard: Option<Expr>,
2339    pub body: MatchBody,
2340    pub span: Span,
2341}
2342
2343/// The right-hand side of a match arm — either a single expression or
2344/// a block.
2345#[derive(Debug, Clone)]
2346pub enum MatchBody {
2347    Expr(Expr),
2348    Block(Block),
2349}
2350
2351impl MatchBody {
2352    pub fn span(&self) -> Span {
2353        match self {
2354            MatchBody::Expr(e) => e.span,
2355            MatchBody::Block(b) => b.span,
2356        }
2357    }
2358}
2359
2360/// A pattern (v0.2 §3.8). Patterns appear in `match` arms and as the
2361/// right-hand side of the `is` operator.
2362#[derive(Debug, Clone)]
2363pub enum Pattern {
2364    /// `_` — matches any value, no bindings.
2365    Wildcard(Span),
2366    /// A lowercase identifier — binds the whole value to `name` and matches
2367    /// anything (ADR 0169). At the top of a `match` arm it binds the scrutinee
2368    /// (`n if n > 0 => …`); inside a payload position it binds the field
2369    /// (`Some(user)`). The uppercase-led counterpart is a nullary [`Pattern::Variant`].
2370    Binding(Ident),
2371    /// A literal pattern — `31`, `"english"`, `true` (v0.130 §2.3.4). Matches a
2372    /// primitive scrutinee (`Int`/`String`/`Bool`) by value equality. The
2373    /// admitted set mirrors ADR 0001's closed literal set (integers — including
2374    /// a leading unary minus — strings, and booleans); `Float`/`()` are not
2375    /// admitted as patterns.
2376    Literal { value: LiteralValue, span: Span },
2377    /// `Variant` or `Variant(bindings)` or `TypeName.Variant(bindings)`. Each
2378    /// payload binding is itself a [`Pattern`] (ADR 0169), so payloads nest:
2379    /// `Some(Ok(x))`, `Err(PollClosed)`.
2380    Variant {
2381        /// Optional qualifier: `TypeName.Variant`.
2382        type_name: Option<Ident>,
2383        /// The variant name.
2384        variant: Ident,
2385        /// Payload bindings (empty for nullary variants).
2386        bindings: Vec<PatternBinding>,
2387        span: Span,
2388    },
2389}
2390
2391/// The value carried by a [`Pattern::Literal`]. A closed set (ADR 0001):
2392/// integer, string, and boolean. Kept distinct from [`ExprKind`] so patterns
2393/// carry only what they can actually match, and so it is `Eq`/`Hash` for the
2394/// duplicate-arm check.
2395#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2396pub enum LiteralValue {
2397    Int(i64),
2398    Str(String),
2399    Bool(bool),
2400}
2401
2402impl LiteralValue {
2403    /// A human-readable rendering for diagnostics (`31`, `"english"`, `true`).
2404    pub fn describe(&self) -> String {
2405        match self {
2406            LiteralValue::Int(n) => n.to_string(),
2407            LiteralValue::Str(s) => format!("{s:?}"),
2408            LiteralValue::Bool(b) => b.to_string(),
2409        }
2410    }
2411}
2412
2413impl Pattern {
2414    pub fn span(&self) -> Span {
2415        match self {
2416            Pattern::Wildcard(s) => *s,
2417            Pattern::Binding(id) => id.span,
2418            Pattern::Literal { span, .. } => *span,
2419            Pattern::Variant { span, .. } => *span,
2420        }
2421    }
2422
2423    /// Every identifier this pattern binds into scope, recursively (`_` and
2424    /// nullary variants bind nothing). Used by the resolver and the checker to
2425    /// populate an arm's scope, and by the guard to see the arm's bindings.
2426    pub fn bound_names(&self) -> Vec<&Ident> {
2427        match self {
2428            Pattern::Wildcard(_) | Pattern::Literal { .. } => Vec::new(),
2429            Pattern::Binding(id) => vec![id],
2430            Pattern::Variant { bindings, .. } => bindings
2431                .iter()
2432                .flat_map(|b| b.pattern().bound_names())
2433                .collect(),
2434        }
2435    }
2436
2437    /// True when this pattern matches every value and binds nothing — a bare
2438    /// `_`. A [`Pattern::Binding`] also matches everything but *does* bind, so it
2439    /// is not a pure wildcard.
2440    pub fn is_wildcard(&self) -> bool {
2441        matches!(self, Pattern::Wildcard(_))
2442    }
2443
2444    /// True when this pattern matches every value (a `_` or a name binding),
2445    /// i.e. it is irrefutable and covers the position for exhaustiveness.
2446    pub fn is_irrefutable(&self) -> bool {
2447        matches!(self, Pattern::Wildcard(_) | Pattern::Binding(_))
2448    }
2449}
2450
2451/// A single binding inside a variant pattern. Two surface forms:
2452/// `pattern` (positional — match the i-th payload field) and
2453/// `fieldName: pattern` (named — match the named payload field). The matched
2454/// sub-`pattern` is a full [`Pattern`] (ADR 0169), so a plain `name` is a
2455/// [`Pattern::Binding`], `_` a [`Pattern::Wildcard`], and `Ok(x)` a nested
2456/// [`Pattern::Variant`].
2457#[derive(Debug, Clone)]
2458pub struct PatternBinding {
2459    /// Source form: positional or named.
2460    pub kind: PatternBindingKind,
2461    pub span: Span,
2462}
2463
2464#[derive(Debug, Clone)]
2465pub enum PatternBindingKind {
2466    /// `pattern` (e.g. `x`, `_`, `Ok(v)`): match the payload field at this position.
2467    Positional { pattern: Pattern },
2468    /// `field: pattern`: match the named payload field against `pattern`.
2469    Named { field: Ident, pattern: Pattern },
2470}
2471
2472impl PatternBinding {
2473    /// The sub-pattern this binding matches its payload field against.
2474    pub fn pattern(&self) -> &Pattern {
2475        match &self.kind {
2476            PatternBindingKind::Positional { pattern } => pattern,
2477            PatternBindingKind::Named { pattern, .. } => pattern,
2478        }
2479    }
2480
2481    /// True when this binding discards its field (`_` or `field: _`) — a pure
2482    /// wildcard sub-pattern that binds nothing.
2483    pub fn is_wildcard(&self) -> bool {
2484        self.pattern().is_wildcard()
2485    }
2486}
2487
2488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2489pub enum BinOp {
2490    /// `P implies Q` — logical implication (v0.80). Desugars to `!P || Q`; sits
2491    /// at the lowest precedence (below `||`). Reads directionally (P → Q).
2492    Implies,
2493    Or,
2494    And,
2495    Eq,
2496    NotEq,
2497    Lt,
2498    LtEq,
2499    Gt,
2500    GtEq,
2501    Add,
2502    Sub,
2503    Mul,
2504    Div,
2505}
2506
2507impl BinOp {
2508    pub fn name(self) -> &'static str {
2509        match self {
2510            BinOp::Implies => "implies",
2511            BinOp::Or => "||",
2512            BinOp::And => "&&",
2513            BinOp::Eq => "==",
2514            BinOp::NotEq => "!=",
2515            BinOp::Lt => "<",
2516            BinOp::LtEq => "<=",
2517            BinOp::Gt => ">",
2518            BinOp::GtEq => ">=",
2519            BinOp::Add => "+",
2520            BinOp::Sub => "-",
2521            BinOp::Mul => "*",
2522            BinOp::Div => "/",
2523        }
2524    }
2525}
2526
2527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2528pub enum UnaryOp {
2529    Neg,
2530    Not,
2531}
2532
2533impl UnaryOp {
2534    pub fn name(self) -> &'static str {
2535        match self {
2536            UnaryOp::Neg => "-",
2537            UnaryOp::Not => "!",
2538        }
2539    }
2540}