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