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    Test(TestDecl),
208    /// v0.16: a `test integration "name" { wires … }` multi-Worker integration
209    /// test. Its `name()` is synthesised from the suite name.
210    Integration(IntegrationDecl),
211    /// v0.17: an `adapter` unit — the host boundary (capability contract +
212    /// external binding).
213    Adapter(AdapterDecl),
214}
215
216impl SourceUnit {
217    pub fn name(&self) -> &QualifiedName {
218        match self {
219            SourceUnit::Commons(c) => &c.name,
220            SourceUnit::Context(c) => &c.name,
221            SourceUnit::Test(t) => &t.target,
222            SourceUnit::Integration(i) => &i.name,
223            SourceUnit::Adapter(a) => &a.name,
224        }
225    }
226
227    pub fn span(&self) -> Span {
228        match self {
229            SourceUnit::Commons(c) => c.span,
230            SourceUnit::Context(c) => c.span,
231            SourceUnit::Test(t) => t.span,
232            SourceUnit::Integration(i) => i.span,
233            SourceUnit::Adapter(a) => a.span,
234        }
235    }
236
237    pub fn kind_name(&self) -> &'static str {
238        match self {
239            SourceUnit::Commons(_) => "commons",
240            SourceUnit::Context(_) => "context",
241            SourceUnit::Test(_) => "test",
242            SourceUnit::Integration(_) => "integration test",
243            SourceUnit::Adapter(_) => "adapter",
244        }
245    }
246}
247
248/// A `test <qualified-name> { ... }` declaration (v0.7 §3.1).
249///
250/// A test targets a commons or context by qualified name and bundles a set of
251/// test cases plus optional mock declarations. As with commons and contexts, a
252/// test may be split across multiple files (fragment form).
253#[derive(Debug, Clone)]
254pub struct TestDecl {
255    /// The targeted commons or context.
256    pub target: QualifiedName,
257    /// `uses` clauses brought in by this test fragment.
258    pub uses: Vec<UsesDecl>,
259    /// Provider or consumed-context mocks declared for the test.
260    pub mocks: Vec<MockDecl>,
261    /// The individual test cases.
262    pub cases: Vec<TestCase>,
263    /// Surface form: brace-delimited body or headerless fragment.
264    pub form: CommonsForm,
265    /// Optional documentation block attached to the test declaration.
266    pub documentation: Option<String>,
267    pub span: Span,
268    pub trivia: Trivia,
269    pub trailing_comments: Vec<String>,
270}
271
272/// A `mocks Name = Impl { ops }` declaration inside a test body (v0.7 §3.2).
273#[derive(Debug, Clone)]
274pub struct MockDecl {
275    /// The capability or consumed-context alias being mocked.
276    pub target_name: Ident,
277    /// The implementation identifier (used as the TypeScript class name).
278    pub impl_name: Ident,
279    /// One operation per mock body entry.
280    pub ops: Vec<MockOp>,
281    pub documentation: Option<String>,
282    pub span: Span,
283    pub trivia: Trivia,
284}
285
286/// One operation inside a mock declaration: `fn name(params) -> T { body }`.
287#[derive(Debug, Clone)]
288pub struct MockOp {
289    pub name: Ident,
290    pub params: Vec<Param>,
291    pub return_type: TypeRef,
292    pub body: Block,
293    pub span: Span,
294    pub trivia: Trivia,
295}
296
297/// A `test "name" { body }` block inside a test declaration (v0.7 §3.3).
298#[derive(Debug, Clone)]
299pub struct TestCase {
300    /// The test name, taken from the string literal.
301    pub name: String,
302    /// The span of the string literal — used for diagnostics and runtime
303    /// failure reports.
304    pub name_span: Span,
305    pub body: Block,
306    pub documentation: Option<String>,
307    pub span: Span,
308    pub trivia: Trivia,
309}
310
311/// A `test integration "name" { wires C1, C2, … ; cases }` declaration
312/// (v0.16 §3.1). Unlike a unit test, an integration test names a *set* of
313/// participating contexts (`wires`), stands each up as its own Worker, and
314/// exercises a flow across the real Worker boundary. It carries no `mocks`.
315#[derive(Debug, Clone)]
316pub struct IntegrationDecl {
317    /// The suite name, taken from the string literal after `integration`.
318    pub suite: String,
319    /// The span of the suite-name literal — used in diagnostics and reports.
320    pub suite_span: Span,
321    /// A synthesised qualified name (`integration <suite>`), so the unit shares
322    /// the `SourceUnit::name()` shape. Not user-written.
323    pub name: QualifiedName,
324    /// The participating contexts, in declaration order (≥ 2, validated later).
325    pub participants: Vec<QualifiedName>,
326    /// `uses` clauses bringing commons into the case bodies.
327    pub uses: Vec<UsesDecl>,
328    /// The individual test cases.
329    pub cases: Vec<TestCase>,
330    /// Surface form: brace-delimited body or headerless fragment.
331    pub form: CommonsForm,
332    pub documentation: Option<String>,
333    pub span: Span,
334    pub trivia: Trivia,
335    pub trailing_comments: Vec<String>,
336}
337
338/// A capability reference in a `given` clause (v0.15 §3.2). A bare name is a
339/// local capability (`given Cap`); a dotted name refers to a capability a
340/// consumed context provides (`given B.Cap` / `given Alias.Cap`).
341#[derive(Debug, Clone)]
342pub struct CapRef {
343    /// `None` for a local capability; `Some(prefix)` for a cross-context
344    /// reference where `prefix` is a consumed-context qualified name or alias.
345    pub context: Option<QualifiedName>,
346    /// The capability's simple name (also the local deps key).
347    pub name: Ident,
348    pub span: Span,
349}
350
351impl CapRef {
352    /// The local deps key / capability simple name (e.g. `Clock`).
353    pub fn key(&self) -> &str {
354        &self.name.name
355    }
356
357    /// True when this references a capability provided by a consumed context.
358    pub fn is_cross_context(&self) -> bool {
359        self.context.is_some()
360    }
361
362    /// The cross-context prefix (consumed-context qualified name or alias) as
363    /// a dotted string, if any.
364    pub fn prefix(&self) -> Option<String> {
365        self.context.as_ref().map(|q| q.joined())
366    }
367}
368
369/// A dotted name like `fitness.units`.
370#[derive(Debug, Clone)]
371pub struct QualifiedName {
372    pub parts: Vec<Ident>,
373    pub span: Span,
374}
375
376impl QualifiedName {
377    pub fn joined(&self) -> String {
378        self.parts
379            .iter()
380            .map(|p| p.name.as_str())
381            .collect::<Vec<_>>()
382            .join(".")
383    }
384}
385
386#[derive(Debug, Clone)]
387pub enum CommonsItem {
388    Type(TypeDecl),
389    Fn(FnDecl),
390    /// `capability Name { fn op(...) -> T ... }` (v0.5; contexts only).
391    Capability(CapabilityDecl),
392    /// `provides Cap = ProviderName { fn op(...) -> T { ... } ... }` (v0.5).
393    Provider(ProviderDecl),
394    /// `service Name { on call(...) -> T { ... } ... }` (v0.5).
395    Service(ServiceDecl),
396    /// `agent Name { key id: T; state { ... }; on call ... }` (v0.5).
397    Agent(AgentDecl),
398    /// `actor Name { auth = Scheme, identity = T }` (v0.45). A nominal boundary
399    /// contract consumed by a handler's `by` clause; not a runnable entity.
400    Actor(ActorDecl),
401}
402
403impl CommonsItem {
404    pub fn name(&self) -> &Ident {
405        match self {
406            CommonsItem::Type(t) => &t.name,
407            CommonsItem::Fn(f) => f.name.ident(),
408            CommonsItem::Capability(c) => &c.name,
409            CommonsItem::Provider(p) => &p.provider_name,
410            CommonsItem::Service(s) => &s.name,
411            CommonsItem::Agent(a) => &a.name,
412            CommonsItem::Actor(a) => &a.name,
413        }
414    }
415}
416
417/// A capability declaration (v0.5 §3.3). Capabilities are interface-like
418/// contracts for external dependencies, used inside contexts. They may only
419/// appear inside a `context` declaration.
420#[derive(Debug, Clone)]
421pub struct CapabilityDecl {
422    pub name: Ident,
423    pub ops: Vec<CapabilityOp>,
424    pub documentation: Option<String>,
425    pub span: Span,
426    pub trivia: Trivia,
427}
428
429/// One operation in a capability (signature only; no body).
430#[derive(Debug, Clone)]
431pub struct CapabilityOp {
432    pub name: Ident,
433    pub params: Vec<Param>,
434    pub return_type: TypeRef,
435    pub documentation: Option<String>,
436    pub span: Span,
437    pub trivia: Trivia,
438}
439
440/// A provider declaration (v0.5 §3.4). Supplies an implementation for a
441/// capability.
442#[derive(Debug, Clone)]
443pub struct ProviderDecl {
444    /// The capability being implemented.
445    pub capability: Ident,
446    /// The provider's identifier (used in tests/config to select impls).
447    pub provider_name: Ident,
448    /// v0.12: capabilities this provider depends on (`provides X = Impl given
449    /// Y, Z { … }`). The provider's operation bodies may use these. v0.15:
450    /// a dependency may be a cross-context capability (`given B.Cap`).
451    pub given: Vec<CapRef>,
452    pub ops: Vec<ProviderOp>,
453    /// v0.17: an *external* provider — `provides Cap = Name` with **no** brace
454    /// block — inside an adapter, supplied by the adapter's binding rather than
455    /// a Bynk body. When `true`, `ops` is empty and the emitter produces no
456    /// class. The absence of the brace block (not an empty one) is the signal.
457    pub external: bool,
458    pub documentation: Option<String>,
459    pub span: Span,
460    pub trivia: Trivia,
461}
462
463/// One operation in a provider (signature plus body).
464#[derive(Debug, Clone)]
465pub struct ProviderOp {
466    pub name: Ident,
467    pub params: Vec<Param>,
468    pub return_type: TypeRef,
469    pub body: Block,
470    pub span: Span,
471    pub trivia: Trivia,
472}
473
474/// A service declaration (v0.5 §3.5). Services are the boundary interface
475/// of a context.
476#[derive(Debug, Clone)]
477pub struct ServiceDecl {
478    pub name: Ident,
479    /// The protocol the service conforms to, from the `from <protocol>` header
480    /// clause (v0.44). `Call` when there is no clause.
481    pub protocol: ServiceProtocol,
482    pub handlers: Vec<Handler>,
483    pub documentation: Option<String>,
484    pub span: Span,
485    pub trivia: Trivia,
486}
487
488/// The protocol a service conforms to — declared on the header via
489/// `from <protocol>` (v0.44). `Call` is the default (no `from` clause): a
490/// contract-mediated internal-RPC surface, not a wire protocol. Multi-endpoint
491/// protocols (`Http`, `Cron`) carry no binding — the endpoint lives on each
492/// handler; single-binding `Queue` carries its queue name.
493#[derive(Debug, Clone)]
494pub enum ServiceProtocol {
495    /// No `from` clause: the service holds `on call` handlers only.
496    Call,
497    /// `from http` — many routes; each handler is `on <Method>("route")`.
498    Http,
499    /// `from cron` — many schedules; each handler is `on schedule("expr")`.
500    Cron,
501    /// `from queue("name")` — one bound queue; handlers are `on message(...)`.
502    Queue { name: String },
503    /// `from WebSocket(in: ClientFrame, out: ServerFrame)` — a held WebSocket
504    /// connection (v0.103, real-time track slice 3). `in_type` is the inbound
505    /// frame type (client→server, decoded and routed as typed agent messages);
506    /// `out_type` is the server→client frame type the held `Connection[out_type]`
507    /// carries. The service holds exactly one `on open` handler (edge auth via
508    /// `by`, then transfer of the connection to an agent).
509    WebSocket { in_type: TypeRef, out_type: TypeRef },
510}
511
512/// An agent declaration (v0.5 §3.6). Agents are state-bearing entities
513/// with their own handlers.
514#[derive(Debug, Clone)]
515pub struct AgentDecl {
516    pub name: Ident,
517    /// `key id: Type` — the identifier-typed value identifying instances.
518    pub key_name: Ident,
519    pub key_type: TypeRef,
520    /// `store` fields (v0.81, storage track) — each an access-pattern slot of a
521    /// declared storage kind (`Cell`/`Map`/…). The successor to the removed
522    /// `state { }` record (ADR 0108); every agent declares its state this way.
523    pub store_fields: Vec<StoreField>,
524    /// Invariants (v0.80 §14) — universally-quantified predicates over the
525    /// agent's `store` fields. The phase sits between the fields and the
526    /// handlers; each is checked against the state staged by a handler's writes
527    /// before it commits.
528    pub invariants: Vec<Invariant>,
529    pub handlers: Vec<Handler>,
530    pub documentation: Option<String>,
531    pub span: Span,
532    pub trivia: Trivia,
533}
534
535/// A `store` field (v0.81, storage track). Each is an access-pattern slot of a
536/// declared storage kind: `store <name>: <Kind>[…] [@annotations] [= <init>]`.
537/// The kind and its element type are carried as an ordinary [`TypeRef`]
538/// (`Cell[Int]`, `Map[K, V]`); the checker restricts which heads are storage
539/// kinds. Access-pattern annotations (`@indexed`, …) parse into [`annotations`]
540/// (v0.85, ADR 0111); the checker validates them against the closed registry.
541///
542/// [`annotations`]: StoreField::annotations
543#[derive(Debug, Clone)]
544pub struct StoreField {
545    pub name: Ident,
546    /// The storage kind and its element type(s): `Cell[Int]`, `Map[K, V]`. A
547    /// dedicated [`StoreKind`] rather than a [`TypeRef`] — storage kinds are not
548    /// value types, and the checker dispatches kind-aware operations on the head.
549    pub kind: StoreKind,
550    /// Storage annotations on the field (v0.85, ADR 0111): `@ttl(5.minutes)`,
551    /// `@indexed(by: orderId)`. Parsed in declaration order (after the kind,
552    /// before the initialiser); the checker validates names against the closed
553    /// registry and gates each to the slice that implements it.
554    pub annotations: Vec<Annotation>,
555    /// The fresh-key initial value (`= expr`), if given — same disposition as a
556    /// `state` field's initialiser (ADRs 0003/0004 carry forward).
557    pub init: Option<Expr>,
558    pub documentation: Option<String>,
559    pub span: Span,
560    pub trivia: Trivia,
561}
562
563/// A storage annotation on a `store` field (v0.85, storage track; ADR 0111):
564/// `@<name>(<args>)`. The `name` is matched against the closed registry
565/// (`@indexed`/`@ttl`/`@retain`/`@bounded`) by the checker; the grammar accepts
566/// any identifier so an unknown name is a checker diagnostic, not a parse error.
567/// Arguments are compile-time metadata, restricted to literals (and the `by:`
568/// field-name labels of `@indexed`) by the checker per ADR 0111 D4.
569#[derive(Debug, Clone)]
570pub struct Annotation {
571    pub name: Ident,
572    pub args: Vec<AnnotationArg>,
573    pub span: Span,
574}
575
576/// A single annotation argument (v0.85; ADR 0111): an optional `label:` followed
577/// by a value expression — `by: orderId` (labelled) or `5.minutes` (positional).
578/// The value is parsed as an ordinary [`Expr`] so the duration-literal form
579/// (`5.minutes`, landing with the `Duration` slice) needs no special grammar;
580/// the checker restricts it to a literal where the annotation is functional.
581#[derive(Debug, Clone)]
582pub struct AnnotationArg {
583    pub label: Option<Ident>,
584    pub value: Expr,
585    pub span: Span,
586}
587
588/// A storage kind applied to its element type(s) (v0.81): `Cell[Int]`,
589/// `Map[ReservationId, Reservation]`. The `head` is the kind name (`Cell`,
590/// `Map`, `Set`, `Log`, `Queue`, `Cache`); the checker validates it against the
591/// closed catalogue. Element types are ordinary [`TypeRef`]s. Refined element
592/// types (`Cell[Int where NonNegative]`) ride a later slice (parse_type_ref does
593/// not yet accept an inline refinement in type-argument position).
594#[derive(Debug, Clone)]
595pub struct StoreKind {
596    pub head: Ident,
597    pub args: Vec<TypeRef>,
598    pub span: Span,
599}
600
601/// An agent invariant (v0.80 §14). A named predicate over the agent's state
602/// fields that must hold of every committed state; a commit that would violate
603/// it faults (`InvariantViolation`) before the state is persisted. The
604/// predicate references state fields by bare name, mirroring the design-notes
605/// worked examples (`status == Paid implies paymentRef.isSome()`).
606#[derive(Debug, Clone)]
607pub struct Invariant {
608    pub name: Ident,
609    /// The predicate expression — an ordinary `Bool`-typed expression over the
610    /// state fields, plus `implies` and `is`. The parsed-predicate-on-a-
611    /// declaration shape mirrors [`ActorRefinement::predicate`].
612    pub predicate: Expr,
613    pub documentation: Option<String>,
614    pub span: Span,
615    pub trivia: Trivia,
616}
617
618/// An actor declaration (v0.45 §3.7). An actor is a nominal *contract type*
619/// describing an external party at a boundary — not a runnable entity. A
620/// handler consumes an actor on its `by` clause; the boundary verifies the
621/// declared `auth` scheme and mints a sealed identity (`name.identity`).
622#[derive(Debug, Clone)]
623pub struct ActorDecl {
624    pub name: Ident,
625    /// The authentication scheme from `auth = <Scheme>`, stored as the raw
626    /// identifier. The checker classifies it: `None`/`Internal`/`Bearer` are
627    /// admitted; `Signature` is reserved-and-rejected
628    /// (`bynk.actor.scheme_unsupported`); anything else is
629    /// `bynk.actor.unknown_scheme`. `None` for the refinement form.
630    pub auth: Option<Ident>,
631    /// The scheme's keyed config from `auth = Scheme(key = value, …)` (v0.47
632    /// `Bearer(secret = "…")`; v0.51 generalised for `Signature(secret, header,
633    /// timestamp?, tolerance?)`). Empty for schemes/forms with no config. The
634    /// checker validates which keys each scheme requires/allows.
635    pub auth_config: Vec<SchemeArg>,
636    /// The optional identity type from `, identity = <T>`. Absent ⇒ the
637    /// scheme default (`()` for `None`; a sealed `CallerId` for the `Internal`
638    /// `on call` channel, `()` for other `Internal` channels).
639    pub identity: Option<TypeRef>,
640    /// The reserved-and-rejected refinement form `actor Admin = Base where p`
641    /// (Q3). Parsed so the grammar is fixed now; the checker emits
642    /// `bynk.actor.refinement_unsupported`.
643    pub refinement: Option<ActorRefinement>,
644    pub documentation: Option<String>,
645    pub span: Span,
646    pub trivia: Trivia,
647}
648
649impl ActorDecl {
650    /// The value of a scheme config arg by key, if present (e.g. `secret`,
651    /// `header`).
652    pub fn scheme_arg(&self, key: &str) -> Option<&SchemeArg> {
653        self.auth_config.iter().find(|a| a.key.name == key)
654    }
655}
656
657/// One `key = value` argument in a scheme config (`Scheme(key = value, …)`).
658#[derive(Debug, Clone)]
659pub struct SchemeArg {
660    pub key: Ident,
661    pub value: SchemeArgValue,
662    /// Span of the value, for diagnostics.
663    pub span: Span,
664}
665
666/// A scheme config arg value — a string literal or an integer.
667#[derive(Debug, Clone)]
668pub enum SchemeArgValue {
669    Str(String),
670    Int(i64),
671}
672
673impl SchemeArgValue {
674    pub fn as_str(&self) -> Option<&str> {
675        match self {
676            SchemeArgValue::Str(s) => Some(s),
677            SchemeArgValue::Int(_) => None,
678        }
679    }
680    pub fn as_int(&self) -> Option<i64> {
681        match self {
682            SchemeArgValue::Int(n) => Some(*n),
683            SchemeArgValue::Str(_) => None,
684        }
685    }
686}
687
688/// The reserved refinement form `actor Admin = User where <predicate>` (Q3).
689/// Parsed in Foundations so the grammar is fixed; admission is a later slice.
690#[derive(Debug, Clone)]
691pub struct ActorRefinement {
692    /// The base actor being refined.
693    pub base: Ident,
694    /// The `where` predicate. Parsed but not yet checked.
695    pub predicate: Expr,
696    pub span: Span,
697}
698
699/// The `by (<binder>:)? <Actor>` clause on a handler (v0.45; binder optional in
700/// v0.50). Names the actor contract the handler consumes; when a `binder` is
701/// given, the verified identity binds to it and is read as `binder.identity`.
702/// Omitting the binder (`by <Actor>`) declares-and-verifies the contract without
703/// capturing the identity — for anonymous or verify-and-discard handlers. Sits
704/// after the protocol config and before the parameters.
705#[derive(Debug, Clone)]
706pub struct ByClause {
707    /// The identity binder, if the handler consumes the identity. `None` for the
708    /// binder-less `by <Actor>` form. Required when `actors` names more than one
709    /// (a sum is resolved by matching on the bound actor).
710    pub binder: Option<Ident>,
711    /// The actor contract(s) referenced — each a local actor decl or a prelude
712    /// actor. A single name is the ordinary single-actor handler; more than one
713    /// (`by who: A | B`, v0.52) is an **ordered sum of peer actors** resolved
714    /// first-wins, the body matching on the resolved actor. Always non-empty.
715    pub actors: Vec<Ident>,
716    pub span: Span,
717}
718
719impl ByClause {
720    /// The first (and, for a single-actor handler, only) actor contract named.
721    pub fn primary(&self) -> &Ident {
722        &self.actors[0]
723    }
724    /// Whether this `by` clause names an ordered sum of peer actors (`A | B`).
725    pub fn is_sum(&self) -> bool {
726        self.actors.len() > 1
727    }
728}
729
730/// A handler block — `on call(args) -> T given C1, C2 { body }`.
731/// Used by both services and agents.
732#[derive(Debug, Clone)]
733pub struct Handler {
734    pub kind: HandlerKind,
735    /// For agent handlers, the method-style handler name (e.g.
736    /// `on call addItem(...)`). For service handlers, this is None (just
737    /// `on call(...)`).
738    pub method_name: Option<Ident>,
739    /// The `by <binder>: <Actor>` clause (v0.45), if present. Service handlers
740    /// only; an absent clause inherits the protocol's default actor.
741    pub by_clause: Option<ByClause>,
742    pub params: Vec<Param>,
743    pub return_type: TypeRef,
744    pub given: Vec<CapRef>,
745    pub body: Block,
746    pub documentation: Option<String>,
747    pub span: Span,
748    pub trivia: Trivia,
749}
750
751#[derive(Debug, Clone, PartialEq, Eq)]
752pub enum HandlerKind {
753    /// `on call(...)` — typed RPC (the only kind in v0.5).
754    Call,
755    /// `on http METHOD "path"` — external-facing HTTP route (v0.9).
756    Http { method: HttpMethod, path: String },
757    /// `on cron "expr"` — scheduled task; `expr` is a 5-field cron
758    /// expression (v0.10a).
759    Cron { expr: String },
760    /// `on message(m: T)` — a message off the service's bound queue. The queue
761    /// binding lives on the service's `ServiceProtocol::Queue` (v0.44).
762    Message,
763    /// `on open ...` — the WebSocket upgrade handler (v0.103, real-time track
764    /// slice 3). Exactly one per `from WebSocket` service; carries a mandatory
765    /// `by` clause (edge auth) and receives a fresh owned `Connection[out]`.
766    Open,
767    /// `on close ...` — the WebSocket close handler (v0.106, real-time track slice
768    /// 3b-iii). Optional, ≤1 per `from WebSocket` service; runs when the socket
769    /// closes. Like `on open`, edge-authenticated (`by`), with the identity/params
770    /// recovered from the socket attachment (set at `on open`). (A `from WebSocket`
771    /// `on message` reuses [`HandlerKind::Message`], disambiguated by the protocol.)
772    Close,
773}
774
775/// HTTP methods supported by `on http` handlers (v0.9).
776#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
777pub enum HttpMethod {
778    Get,
779    Post,
780    Put,
781    Patch,
782    Delete,
783}
784
785impl HttpMethod {
786    pub fn as_str(self) -> &'static str {
787        match self {
788            HttpMethod::Get => "GET",
789            HttpMethod::Post => "POST",
790            HttpMethod::Put => "PUT",
791            HttpMethod::Patch => "PATCH",
792            HttpMethod::Delete => "DELETE",
793        }
794    }
795
796    pub fn from_ident(s: &str) -> Option<HttpMethod> {
797        match s {
798            "GET" => Some(HttpMethod::Get),
799            "POST" => Some(HttpMethod::Post),
800            "PUT" => Some(HttpMethod::Put),
801            "PATCH" => Some(HttpMethod::Patch),
802            "DELETE" => Some(HttpMethod::Delete),
803            _ => None,
804        }
805    }
806
807    /// True if this method conventionally has no request body.
808    pub fn forbids_body(self) -> bool {
809        matches!(self, HttpMethod::Get | HttpMethod::Delete)
810    }
811}
812
813/// Payload shape of an `HttpResult[T]` variant (v0.9 §3.3).
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
815pub enum HttpVariantPayload {
816    /// No payload (e.g. `NoContent`, `Unauthorized`).
817    None,
818    /// Carries a value of the `HttpResult` type parameter `T`.
819    Value,
820    /// Carries a `String` message (e.g. `BadRequest`, `Conflict`).
821    Message,
822    /// Carries a `String` target URL, emitted as a `Location` header — the
823    /// redirect variants (`Found`, `SeeOther`, `PermanentRedirect`, …).
824    Location,
825    /// Carries a `Stream[String]`, emitted as an SSE (`text/event-stream`)
826    /// streaming body — the `Streaming` (200) variant (v0.101, real-time track
827    /// slice 1).
828    Streamed,
829}
830
831/// One variant of the built-in `HttpResult[T]` sum (v0.9 §3.3).
832#[derive(Debug, Clone, Copy)]
833pub struct HttpVariant {
834    pub name: &'static str,
835    pub payload: HttpVariantPayload,
836    pub status: u16,
837}
838
839/// All `HttpResult[T]` variants, in declaration order (ascending status). The
840/// vocabulary tracks the common, modern HTTP status codes (RFC 9110): success
841/// and created/accepted (`Value`), redirects carrying a `Location` URL, and
842/// the client/server failures that handlers routinely return (`Message` when
843/// an explanation helps the caller, `None` for self-describing statuses).
844pub const HTTP_VARIANTS: &[HttpVariant] = &[
845    // ── 2xx success ──────────────────────────────────────────────────────
846    HttpVariant {
847        name: "Ok",
848        payload: HttpVariantPayload::Value,
849        status: 200,
850    },
851    // v0.101 (real-time track slice 1): a 200 whose body is a streamed
852    // `Stream[String]`, SSE-framed. Status precedes the body, so streaming is
853    // 200-only — pre-stream failures are ordinary variants returned instead.
854    HttpVariant {
855        name: "Streaming",
856        payload: HttpVariantPayload::Streamed,
857        status: 200,
858    },
859    HttpVariant {
860        name: "Created",
861        payload: HttpVariantPayload::Value,
862        status: 201,
863    },
864    HttpVariant {
865        name: "Accepted",
866        payload: HttpVariantPayload::Value,
867        status: 202,
868    },
869    HttpVariant {
870        name: "NoContent",
871        payload: HttpVariantPayload::None,
872        status: 204,
873    },
874    // ── 3xx redirection (carry a `Location` URL) ─────────────────────────
875    HttpVariant {
876        name: "MovedPermanently",
877        payload: HttpVariantPayload::Location,
878        status: 301,
879    },
880    HttpVariant {
881        name: "Found",
882        payload: HttpVariantPayload::Location,
883        status: 302,
884    },
885    HttpVariant {
886        name: "SeeOther",
887        payload: HttpVariantPayload::Location,
888        status: 303,
889    },
890    HttpVariant {
891        name: "TemporaryRedirect",
892        payload: HttpVariantPayload::Location,
893        status: 307,
894    },
895    HttpVariant {
896        name: "PermanentRedirect",
897        payload: HttpVariantPayload::Location,
898        status: 308,
899    },
900    // ── 4xx client error ─────────────────────────────────────────────────
901    HttpVariant {
902        name: "BadRequest",
903        payload: HttpVariantPayload::Message,
904        status: 400,
905    },
906    HttpVariant {
907        name: "Unauthorized",
908        payload: HttpVariantPayload::None,
909        status: 401,
910    },
911    HttpVariant {
912        name: "Forbidden",
913        payload: HttpVariantPayload::None,
914        status: 403,
915    },
916    HttpVariant {
917        name: "NotFound",
918        payload: HttpVariantPayload::None,
919        status: 404,
920    },
921    HttpVariant {
922        name: "MethodNotAllowed",
923        payload: HttpVariantPayload::None,
924        status: 405,
925    },
926    HttpVariant {
927        name: "NotAcceptable",
928        payload: HttpVariantPayload::None,
929        status: 406,
930    },
931    HttpVariant {
932        name: "RequestTimeout",
933        payload: HttpVariantPayload::None,
934        status: 408,
935    },
936    HttpVariant {
937        name: "Conflict",
938        payload: HttpVariantPayload::Message,
939        status: 409,
940    },
941    HttpVariant {
942        name: "Gone",
943        payload: HttpVariantPayload::None,
944        status: 410,
945    },
946    HttpVariant {
947        name: "LengthRequired",
948        payload: HttpVariantPayload::None,
949        status: 411,
950    },
951    HttpVariant {
952        name: "PayloadTooLarge",
953        payload: HttpVariantPayload::Message,
954        status: 413,
955    },
956    HttpVariant {
957        name: "UnsupportedMediaType",
958        payload: HttpVariantPayload::Message,
959        status: 415,
960    },
961    HttpVariant {
962        name: "UnprocessableEntity",
963        payload: HttpVariantPayload::Message,
964        status: 422,
965    },
966    HttpVariant {
967        name: "TooManyRequests",
968        payload: HttpVariantPayload::Message,
969        status: 429,
970    },
971    HttpVariant {
972        name: "UnavailableForLegalReasons",
973        payload: HttpVariantPayload::Message,
974        status: 451,
975    },
976    // ── 5xx server error ─────────────────────────────────────────────────
977    HttpVariant {
978        name: "ServerError",
979        payload: HttpVariantPayload::Message,
980        status: 500,
981    },
982    HttpVariant {
983        name: "NotImplemented",
984        payload: HttpVariantPayload::Message,
985        status: 501,
986    },
987    HttpVariant {
988        name: "BadGateway",
989        payload: HttpVariantPayload::Message,
990        status: 502,
991    },
992    HttpVariant {
993        name: "ServiceUnavailable",
994        payload: HttpVariantPayload::Message,
995        status: 503,
996    },
997    HttpVariant {
998        name: "GatewayTimeout",
999        payload: HttpVariantPayload::Message,
1000        status: 504,
1001    },
1002];
1003
1004/// Find an `HttpResult[T]` variant by name. Returns the variant info or
1005/// `None` if the name doesn't match.
1006pub fn http_variant(name: &str) -> Option<HttpVariant> {
1007    HTTP_VARIANTS.iter().copied().find(|v| v.name == name)
1008}
1009
1010/// Payload shape of a `QueueResult` variant (v0.44). Non-generic — a verdict
1011/// carries no value; `Retry` carries a `String` reason for the log path.
1012#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1013pub enum QueueVariantPayload {
1014    /// No payload (`Ack`).
1015    None,
1016    /// Carries a `String` reason (`Retry`).
1017    Message,
1018}
1019
1020/// One variant of the built-in `QueueResult` sum (v0.44).
1021#[derive(Debug, Clone, Copy)]
1022pub struct QueueVariant {
1023    pub name: &'static str,
1024    pub payload: QueueVariantPayload,
1025}
1026
1027/// All `QueueResult` variants, in declaration order. `Ack` confirms the
1028/// message; `Retry` redelivers it, carrying a reason for observability.
1029pub const QUEUE_VARIANTS: &[QueueVariant] = &[
1030    QueueVariant {
1031        name: "Ack",
1032        payload: QueueVariantPayload::None,
1033    },
1034    QueueVariant {
1035        name: "Retry",
1036        payload: QueueVariantPayload::Message,
1037    },
1038];
1039
1040/// Find a `QueueResult` variant by name.
1041pub fn queue_variant(name: &str) -> Option<QueueVariant> {
1042    QUEUE_VARIANTS.iter().copied().find(|v| v.name == name)
1043}
1044
1045#[derive(Debug, Clone)]
1046pub struct TypeDecl {
1047    pub name: Ident,
1048    pub body: TypeBody,
1049    /// Documentation block attached to this declaration (v0.3).
1050    pub documentation: Option<String>,
1051    pub span: Span,
1052    pub trivia: Trivia,
1053}
1054
1055/// The right-hand side of a `type` declaration. In v0/v0.1 only the
1056/// `Refined` variant existed; v0.2 adds records and sums; v0.3 adds opaque.
1057#[derive(Debug, Clone)]
1058pub enum TypeBody {
1059    /// Refined base type: `BaseType where refinement`.
1060    Refined {
1061        base: BaseType,
1062        base_span: Span,
1063        refinement: Option<Refinement>,
1064    },
1065    /// Record type: `{ field: T where ..., ... }`.
1066    Record(RecordBody),
1067    /// Sum type: pipe-form variants or `enum { ... }` shorthand.
1068    Sum(SumBody),
1069    /// Opaque base type: `opaque BaseType (where refinement)?` (v0.3 §3.4).
1070    /// Identity is nominal; the base type is hidden outside the defining commons.
1071    Opaque {
1072        base: BaseType,
1073        base_span: Span,
1074        refinement: Option<Refinement>,
1075    },
1076}
1077
1078/// Body of a record-type declaration (v0.2 §3.1).
1079#[derive(Debug, Clone)]
1080pub struct RecordBody {
1081    pub fields: Vec<RecordField>,
1082    pub span: Span,
1083}
1084
1085/// One field of a record type declaration. Each field may carry inline
1086/// refinement, which is enforced at construction time on the field's value.
1087#[derive(Debug, Clone)]
1088pub struct RecordField {
1089    pub name: Ident,
1090    pub type_ref: TypeRef,
1091    pub refinement: Option<Refinement>,
1092    /// v0.11: an optional initial-value expression. Only meaningful on agent
1093    /// `state` fields (the field's fresh-key value); ignored / rejected on
1094    /// record-type fields by the checker.
1095    pub init: Option<Expr>,
1096    pub span: Span,
1097}
1098
1099/// Body of a sum-type declaration (v0.2 §3.2).
1100#[derive(Debug, Clone)]
1101pub struct SumBody {
1102    pub variants: Vec<Variant>,
1103    pub span: Span,
1104}
1105
1106/// One variant of a sum type. Variants may have payload fields; a
1107/// payload-less variant is a simple tag.
1108#[derive(Debug, Clone)]
1109pub struct Variant {
1110    pub name: Ident,
1111    pub payload: Vec<VariantField>,
1112    pub span: Span,
1113}
1114
1115/// One payload field of a sum variant. Variant payload fields use named
1116/// declarations like record fields, but do not carry refinement in v0.2.
1117#[derive(Debug, Clone)]
1118pub struct VariantField {
1119    pub name: Ident,
1120    pub type_ref: TypeRef,
1121    pub span: Span,
1122}
1123
1124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1125pub enum BaseType {
1126    Int,
1127    String,
1128    Bool,
1129    Float,
1130    /// `Duration` (v0.86, ADR 0112) — a span of time, a distinct base type
1131    /// erased to TS `number` carrying milliseconds (the `Clock` unit). Modelled
1132    /// on `Float`: Bynk-side-only, no implicit `Int` coercion (save the one
1133    /// sanctioned clock-math mix).
1134    Duration,
1135    /// `Instant` (v0.90, ADR 0114) — an absolute point in time, a distinct base
1136    /// type erased to TS `number` carrying Unix epoch milliseconds (the
1137    /// `Clock` unit). No literal (minted by `Clock.now()`); arithmetic composes
1138    /// with `Duration` (`Instant ± Duration -> Instant`, `Instant − Instant ->
1139    /// Duration`). Supersedes ADR 0112 D4's `Int`↔`Duration` clock-math mix.
1140    Instant,
1141    /// `Bytes` (v0.110, ADR 0142) — an immutable finite octet sequence, the
1142    /// seventh base type. Unlike its neighbours it does **not** erase to TS
1143    /// `number`: a `Bytes` lowers to a `Uint8Array`. No source literal
1144    /// (constructed via `Bytes.fromUtf8`/`fromBase64`/`empty`); `==` compares
1145    /// by content (real emitter codegen, not host `===`); wires as a base64
1146    /// JSON string; not `Map`-keyable and not orderable.
1147    Bytes,
1148}
1149
1150impl BaseType {
1151    pub fn name(self) -> &'static str {
1152        match self {
1153            BaseType::Int => "Int",
1154            BaseType::String => "String",
1155            BaseType::Bool => "Bool",
1156            BaseType::Float => "Float",
1157            BaseType::Duration => "Duration",
1158            BaseType::Instant => "Instant",
1159            BaseType::Bytes => "Bytes",
1160        }
1161    }
1162}
1163
1164/// A `Duration` literal unit (v0.86, ADR 0112) — the closed set of suffixes in a
1165/// `<int>.<unit>` literal. Each maps to a fixed millisecond factor (`Duration`
1166/// erases to `Int` milliseconds).
1167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1168pub enum DurationUnit {
1169    Milliseconds,
1170    Seconds,
1171    Minutes,
1172    Hours,
1173    Days,
1174}
1175
1176impl DurationUnit {
1177    /// Resolve a unit name (`minutes`) to its variant, or `None` if it is not one
1178    /// of the closed set. Used by the parser to recognise an `<int>.<unit>`
1179    /// literal; an unrecognised name leaves the expression a field access.
1180    pub fn from_name(name: &str) -> Option<Self> {
1181        Some(match name {
1182            "milliseconds" => DurationUnit::Milliseconds,
1183            "seconds" => DurationUnit::Seconds,
1184            "minutes" => DurationUnit::Minutes,
1185            "hours" => DurationUnit::Hours,
1186            "days" => DurationUnit::Days,
1187            _ => return None,
1188        })
1189    }
1190
1191    /// The unit name as written.
1192    pub fn name(self) -> &'static str {
1193        match self {
1194            DurationUnit::Milliseconds => "milliseconds",
1195            DurationUnit::Seconds => "seconds",
1196            DurationUnit::Minutes => "minutes",
1197            DurationUnit::Hours => "hours",
1198            DurationUnit::Days => "days",
1199        }
1200    }
1201
1202    /// The unit's value in milliseconds.
1203    pub fn millis(self) -> i64 {
1204        match self {
1205            DurationUnit::Milliseconds => 1,
1206            DurationUnit::Seconds => 1_000,
1207            DurationUnit::Minutes => 60_000,
1208            DurationUnit::Hours => 3_600_000,
1209            DurationUnit::Days => 86_400_000,
1210        }
1211    }
1212}
1213
1214/// An integer refinement bound (v0.40, ADR 0073): the parsed value plus the
1215/// bound's source span (covering a leading `-`). Value-only beyond the span —
1216/// ints have one canonical printed form, so the formatter stays idempotent
1217/// without a stored lexeme. The span backs the `InRange`-swap quick-fix.
1218#[derive(Debug, Clone)]
1219pub struct IntBound {
1220    pub value: i64,
1221    pub span: Span,
1222}
1223
1224/// A float refinement bound (v0.21): the parsed value plus the signed source
1225/// lexeme (for byte-stable emission). v0.40 (ADR 0073): also the source span,
1226/// for the `InRange`-swap quick-fix.
1227#[derive(Debug, Clone)]
1228pub struct FloatBound {
1229    pub value: f64,
1230    pub lexeme: String,
1231    pub span: Span,
1232}
1233
1234#[derive(Debug, Clone)]
1235pub struct Refinement {
1236    pub predicates: Vec<RefinementPred>,
1237    pub span: Span,
1238}
1239
1240#[derive(Debug, Clone)]
1241pub struct RefinementPred {
1242    pub kind: PredKind,
1243    pub span: Span,
1244}
1245
1246#[derive(Debug, Clone)]
1247pub enum PredKind {
1248    Matches(String),
1249    InRange(IntBound, IntBound),
1250    /// `InRange` with float bounds (v0.21) — a separate variant so every
1251    /// `Int` refinement path stays untouched. Bounds keep their source
1252    /// lexemes (including any sign) so emitted runtime checks are
1253    /// byte-stable.
1254    InRangeF(FloatBound, FloatBound),
1255    MinLength(i64),
1256    MaxLength(i64),
1257    Length(i64),
1258    NonNegative,
1259    Positive,
1260    NonEmpty,
1261}
1262
1263impl PredKind {
1264    pub fn name(&self) -> &'static str {
1265        match self {
1266            PredKind::Matches(_) => "Matches",
1267            PredKind::InRange(..) | PredKind::InRangeF(..) => "InRange",
1268            PredKind::MinLength(_) => "MinLength",
1269            PredKind::MaxLength(_) => "MaxLength",
1270            PredKind::Length(_) => "Length",
1271            PredKind::NonNegative => "NonNegative",
1272            PredKind::Positive => "Positive",
1273            PredKind::NonEmpty => "NonEmpty",
1274        }
1275    }
1276}
1277
1278/// A function type parameter (v0.20a, `fn name[A, B](…)`). A struct rather
1279/// than a bare Ident so the ADR-0028 "bound-capable" promise is a later field
1280/// addition, not a representation change.
1281#[derive(Debug, Clone)]
1282pub struct TypeParam {
1283    pub name: Ident,
1284    pub span: Span,
1285}
1286
1287/// A lambda expression (v0.20a): `(params) => expr` or `(params) => { … }`.
1288/// `=>` is the value arrow (shared with `match`); param annotations are
1289/// optional where an expected function type supplies them.
1290#[derive(Debug, Clone)]
1291pub struct LambdaExpr {
1292    pub params: Vec<LambdaParam>,
1293    pub body: Box<Expr>,
1294    pub span: Span,
1295}
1296
1297/// A lambda parameter. A separate type from [`Param`] because its annotation
1298/// is optional — `Param.type_ref` stays mandatory at every signature site.
1299#[derive(Debug, Clone)]
1300pub struct LambdaParam {
1301    pub name: Ident,
1302    pub type_ref: Option<TypeRef>,
1303    pub span: Span,
1304}
1305
1306#[derive(Debug, Clone)]
1307pub struct FnDecl {
1308    /// v0.20a: `[A, B]` type parameters; empty for non-generic functions.
1309    pub type_params: Vec<TypeParam>,
1310    /// Free function or method (`TypeName.methodName`). See [`FnName`].
1311    pub name: FnName,
1312    pub params: Vec<Param>,
1313    pub return_type: TypeRef,
1314    pub body: Block,
1315    /// True when the first parameter is the special `self` parameter. Only
1316    /// valid for method declarations.
1317    pub has_self: bool,
1318    /// Documentation block attached to this declaration (v0.3).
1319    pub documentation: Option<String>,
1320    pub span: Span,
1321    pub trivia: Trivia,
1322}
1323
1324/// A function-declaration name: either a free function `f` or a method
1325/// `T.method` (v0.2 §3.6).
1326#[derive(Debug, Clone)]
1327pub enum FnName {
1328    /// `fn name(...)` — a free function.
1329    Free(Ident),
1330    /// `fn TypeName.methodName(...)` — a method attached to a type.
1331    Method {
1332        type_name: Ident,
1333        method_name: Ident,
1334    },
1335}
1336
1337impl FnName {
1338    /// The function's short name for diagnostics. For methods returns the
1339    /// method portion only; the type prefix is recovered via `type_name`.
1340    pub fn ident(&self) -> &Ident {
1341        match self {
1342            FnName::Free(id) => id,
1343            FnName::Method { method_name, .. } => method_name,
1344        }
1345    }
1346
1347    /// For methods, the attached type's identifier; `None` for free fns.
1348    pub fn type_name(&self) -> Option<&Ident> {
1349        match self {
1350            FnName::Free(_) => None,
1351            FnName::Method { type_name, .. } => Some(type_name),
1352        }
1353    }
1354
1355    /// The displayed full name (e.g., `Money.add` or `parseSku`).
1356    pub fn display(&self) -> String {
1357        match self {
1358            FnName::Free(id) => id.name.clone(),
1359            FnName::Method {
1360                type_name,
1361                method_name,
1362            } => format!("{}.{}", type_name.name, method_name.name),
1363        }
1364    }
1365}
1366
1367/// A brace-delimited block of statements ending in a tail expression
1368/// whose value is the block's value (spec v0.1 §3.1).
1369#[derive(Debug, Clone)]
1370pub struct Block {
1371    pub statements: Vec<Statement>,
1372    pub tail: Box<Expr>,
1373    pub span: Span,
1374    /// Line comments that appear between the last statement (or the
1375    /// opening brace) and the tail expression. Preserved here because
1376    /// expressions do not carry trivia in v1.1.
1377    pub tail_leading_comments: Vec<String>,
1378}
1379
1380/// Block-level statement.
1381#[derive(Debug, Clone)]
1382pub enum Statement {
1383    /// `let name (: T)? = expr` — pure binding (v0.1).
1384    Let(LetStmt),
1385    /// `let name (: T)? <- expr` — effectful binding (v0.5).
1386    EffectLet(LetStmt),
1387    /// `assert expr` — verify a Bool expression at test runtime (v0.7).
1388    /// Only valid inside test case bodies.
1389    Assert(AssertStmt),
1390    /// `~> expr` — an asynchronous fire-and-forget send (v0.79). The caller does
1391    /// not await the reply; legal only when the reply is `Effect[()]`. No binder.
1392    Send(SendStmt),
1393    /// `name := expr` — a `Cell` store write (v0.81, storage track). The
1394    /// unconditional write form; `.update(fn)` (a method call) is the
1395    /// read-modify-write form. ADR 0108.
1396    Assign(AssignStmt),
1397}
1398
1399impl Statement {
1400    pub fn span(&self) -> Span {
1401        match self {
1402            Statement::Let(l) | Statement::EffectLet(l) => l.span,
1403            Statement::Assert(a) => a.span,
1404            Statement::Send(s) => s.span,
1405            Statement::Assign(a) => a.span,
1406        }
1407    }
1408}
1409
1410#[derive(Debug, Clone)]
1411pub struct AssertStmt {
1412    pub value: Expr,
1413    pub span: Span,
1414    pub trivia: Trivia,
1415}
1416
1417/// `name := expr` — a `Cell` store write (v0.81, storage track). `target` is the
1418/// `Cell` field being written (a bare name for now; the checker resolves it to a
1419/// `store` field). `value` is the new value.
1420#[derive(Debug, Clone)]
1421pub struct AssignStmt {
1422    pub target: Ident,
1423    pub value: Expr,
1424    pub span: Span,
1425    pub trivia: Trivia,
1426}
1427
1428#[derive(Debug, Clone)]
1429pub struct LetStmt {
1430    pub name: Ident,
1431    pub type_annot: Option<TypeRef>,
1432    pub value: Expr,
1433    pub span: Span,
1434    pub trivia: Trivia,
1435}
1436
1437#[derive(Debug, Clone)]
1438pub struct SendStmt {
1439    /// The send target — a recipient call, e.g. `Logger.info(msg)`.
1440    pub value: Expr,
1441    pub span: Span,
1442    pub trivia: Trivia,
1443}
1444
1445#[derive(Debug, Clone)]
1446pub struct Param {
1447    pub name: Ident,
1448    pub type_ref: TypeRef,
1449    pub span: Span,
1450}
1451
1452#[derive(Debug, Clone)]
1453pub enum TypeRef {
1454    Base(BaseType, Span),
1455    Named(Ident),
1456    /// `Result[T, E]` — the built-in generic Result type (v0.1).
1457    Result(Box<TypeRef>, Box<TypeRef>, Span),
1458    /// `Option[T]` — the built-in generic Option type (v0.2).
1459    Option(Box<TypeRef>, Span),
1460    /// `Effect[T]` — the built-in generic Effect type (v0.5).
1461    Effect(Box<TypeRef>, Span),
1462    /// `HttpResult[T]` — the built-in HTTP-result sum (v0.9).
1463    HttpResult(Box<TypeRef>, Span),
1464    /// `QueueResult` — the built-in queue verdict sum (`Ack | Retry`),
1465    /// non-generic; the required return of a queue handler (v0.44).
1466    QueueResult(Span),
1467    /// `List[T]` — the built-in generic immutable list type (v0.20b).
1468    List(Box<TypeRef>, Span),
1469    /// `Map[K, V]` — the built-in generic immutable map type (v0.20b).
1470    /// Keys are confined to value-keyable types
1471    /// (`bynk.types.unkeyable_map_key`).
1472    Map(Box<TypeRef>, Box<TypeRef>, Span),
1473    /// `Query[T]` — the built-in lazy storage-read description (v0.91, ADR 0115).
1474    /// Nameable in a pure helper's return type; non-storable and non-boundary
1475    /// (like `Effect`/`Fn`).
1476    Query(Box<TypeRef>, Span),
1477    /// `Stream[T]` — the value-over-time primitive (v0.100, real-time track
1478    /// slice 0). A lazy, pull-shaped sequence produced over time; non-storable
1479    /// and non-boundary (like `Query`/`Effect`/`Fn`).
1480    Stream(Box<TypeRef>, Span),
1481    /// `Connection[F]` — a held WebSocket connection (v0.102, real-time track
1482    /// slice 2). `F` is the server→client frame type. A `Held` resource:
1483    /// non-serialisable, non-boundary, and governed by the linearity discipline
1484    /// (§2.9); storable only in `Cell[Option[Connection]]` / `Map[K, Connection]`.
1485    Connection(Box<TypeRef>, Span),
1486    /// `ValidationError` — the built-in error type used by refined-type
1487    /// constructors (v0.1).
1488    ValidationError(Span),
1489    /// `JsonError` — the built-in JSON-decode error type (v0.22b). A
1490    /// uniform record (`kind`/`path`/`message`, all `String`) the codec
1491    /// maps `BoundaryError` variants and parse failures into.
1492    JsonError(Span),
1493    /// `()` — the unit type (v0.5).
1494    Unit(Span),
1495    /// `A -> B` / `(A, B) -> C` / `() -> B` — a function type (v0.20a).
1496    /// Right-associative; effectful iff the return type is `Effect[_]`
1497    /// (the structural rule). Confined to non-boundary positions
1498    /// (`bynk.types.function_at_boundary`).
1499    Fn(Vec<TypeRef>, Box<TypeRef>, Span),
1500}
1501
1502impl TypeRef {
1503    pub fn span(&self) -> Span {
1504        match self {
1505            TypeRef::Base(_, s) => *s,
1506            TypeRef::Named(id) => id.span,
1507            TypeRef::Result(_, _, s) => *s,
1508            TypeRef::Option(_, s) => *s,
1509            TypeRef::Effect(_, s) => *s,
1510            TypeRef::HttpResult(_, s) => *s,
1511            TypeRef::QueueResult(s) => *s,
1512            TypeRef::List(_, s) => *s,
1513            TypeRef::Map(_, _, s) => *s,
1514            TypeRef::Query(_, s) => *s,
1515            TypeRef::Stream(_, s) => *s,
1516            TypeRef::Connection(_, s) => *s,
1517            TypeRef::ValidationError(s) => *s,
1518            TypeRef::JsonError(s) => *s,
1519            TypeRef::Unit(s) => *s,
1520            TypeRef::Fn(_, _, s) => *s,
1521        }
1522    }
1523}
1524
1525#[derive(Debug, Clone)]
1526pub struct Expr {
1527    pub kind: ExprKind,
1528    pub span: Span,
1529}
1530
1531#[derive(Debug, Clone)]
1532pub enum ExprKind {
1533    IntLit(i64),
1534    /// A float literal (v0.21). The lexeme is kept alongside the parsed
1535    /// value so emission and formatting are byte-stable (`1e10` must not
1536    /// normalise to `10000000000`).
1537    FloatLit {
1538        value: f64,
1539        lexeme: String,
1540    },
1541    /// A duration literal `<int>.<unit>` (v0.86, ADR 0112): `5.minutes`,
1542    /// `30.days`. The parser recognises the `IntLit . <unit>` shape and records
1543    /// the magnitude, the unit, and the resolved milliseconds (the value the
1544    /// emitter lowers to). Typed `Duration`.
1545    DurationLit {
1546        /// The integer magnitude as written (`5` in `5.minutes`).
1547        value: i64,
1548        /// The unit name (`minutes`), one of the closed set.
1549        unit: DurationUnit,
1550        /// The value in milliseconds — `value * unit factor`.
1551        millis: i64,
1552    },
1553    StrLit(String),
1554    /// An interpolated string `"… \(expr) …"` (v0.43, ADR 0075). Chunks and
1555    /// holes alternate. A plain `"…"` with no holes stays [`ExprKind::StrLit`],
1556    /// so existing code and the emitter/formatter fast-path are untouched.
1557    InterpStr(Vec<InterpPart>),
1558    BoolLit(bool),
1559    Ident(Ident),
1560    Call {
1561        name: Ident,
1562        /// v0.20a: explicit type arguments (`name[T](…)`); empty when absent.
1563        type_args: Vec<TypeRef>,
1564        args: Vec<Expr>,
1565    },
1566    /// A lambda (v0.20a). See [`LambdaExpr`].
1567    Lambda(LambdaExpr),
1568    BinOp(BinOp, Box<Expr>, Box<Expr>),
1569    UnaryOp(UnaryOp, Box<Expr>),
1570    Paren(Box<Expr>),
1571    /// `{ stmts; expr }` — block expression (v0.1).
1572    Block(Block),
1573    /// `if cond { then } else { else }` (v0.1).
1574    If {
1575        cond: Box<Expr>,
1576        then_block: Box<Block>,
1577        else_block: Box<Block>,
1578    },
1579    /// `Ok(value)` — Result success constructor (v0.1).
1580    Ok(Box<Expr>),
1581    /// `Err(error)` — Result failure constructor (v0.1).
1582    Err(Box<Expr>),
1583    /// `expr?` — propagation operator (v0.1).
1584    Question(Box<Expr>),
1585    /// `TypeName.method(args)` — qualified static call on a type
1586    /// (v0.1: only refined-type `of`; v0.2: any static method or variant
1587    /// constructor for sum types). The resolver decides which.
1588    ConstructorCall {
1589        type_name: Ident,
1590        method: Ident,
1591        args: Vec<Expr>,
1592    },
1593    /// `TypeName { field: value, ... }` — record construction (v0.2).
1594    RecordConstruction {
1595        type_name: Ident,
1596        fields: Vec<FieldInit>,
1597    },
1598    /// `receiver.field` — field access on a record value (v0.2). v0.3 adds
1599    /// `.raw` on opaque types within the defining commons.
1600    FieldAccess {
1601        receiver: Box<Expr>,
1602        field: Ident,
1603    },
1604    /// `receiver.method(args)` — instance method call (v0.2). The
1605    /// resolver determines the receiver's type and looks up the method.
1606    MethodCall {
1607        receiver: Box<Expr>,
1608        method: Ident,
1609        /// v0.22b: explicit type arguments on a qualified static
1610        /// (`Json.decode[T](…)`); empty when absent. The same-line-`[`
1611        /// rule applies as for `Call` type application (0039).
1612        type_args: Vec<TypeRef>,
1613        args: Vec<Expr>,
1614    },
1615    /// `match disc { arm+ }` — pattern matching (v0.2).
1616    Match {
1617        discriminant: Box<Expr>,
1618        arms: Vec<MatchArm>,
1619    },
1620    /// `expr is pattern` — pattern test, returns Bool (v0.2).
1621    Is {
1622        value: Box<Expr>,
1623        pattern: Pattern,
1624    },
1625    /// `Some(value)` — Option Some constructor (v0.2).
1626    Some(Box<Expr>),
1627    /// `None` — Option None constructor (v0.2).
1628    None,
1629    /// `()` — unit literal (v0.5).
1630    UnitLit,
1631    /// `TypeName { ...base, field: value, ... }` or `{ ...base, ... }` —
1632    /// record spread expression (v0.5).
1633    RecordSpread {
1634        /// Optional type prefix (`TypeName { ...base }`). Absent for the
1635        /// bare form used inside `commit`.
1636        type_name: Option<Ident>,
1637        /// The base record being spread.
1638        base: Box<Expr>,
1639        /// Field overrides (always full `name: value` form — never shorthand).
1640        overrides: Vec<FieldInit>,
1641    },
1642    /// `Effect.pure(value)` — wrap a synchronous value into `Effect[T]`
1643    /// (v0.5). Recognised in the parser as a special-form.
1644    EffectPure(Box<Expr>),
1645    /// `assert expr` — assertion as an expression of type `()` (v0.9.1).
1646    /// Valid only inside test bodies. Evaluates `expr` (must be Bool); if
1647    /// false, the surrounding test case fails.
1648    Assert(Box<Expr>),
1649    /// `Mock[T]`, `Mock[T](args)` — test-context value construction (v0.9.4).
1650    /// `args` is empty for the bare form and holds the pin arguments for
1651    /// `Mock[T](...)`. The record-override form `Mock[T] { ... }` is not yet
1652    /// parsed. Valid only inside test bodies; has type `T`.
1653    Mock {
1654        type_ref: TypeRef,
1655        args: Vec<Expr>,
1656    },
1657    /// `[a, b, c]` — list literal (v0.20b). An empty `[]` requires an
1658    /// expected type (`bynk.types.uninferable_element_type`).
1659    ListLit(Vec<Expr>),
1660}
1661
1662/// One part of an interpolated string (v0.43, ADR 0075). An
1663/// [`ExprKind::InterpStr`] holds an alternating run of these.
1664#[derive(Debug, Clone)]
1665pub enum InterpPart {
1666    /// Literal text between holes, with escapes already resolved.
1667    Chunk(String),
1668    /// An interpolated expression `\(expr)`. Type-checked by the hole rule
1669    /// (base scalars only; see the checker) and lowered into a template-
1670    /// literal `${…}` slot.
1671    Hole(Box<Expr>),
1672}
1673
1674/// One field-initialiser inside a record construction expression:
1675/// either `name: expr` or the shorthand `name` (which requires a binding
1676/// of the same name in scope and uses its value).
1677#[derive(Debug, Clone)]
1678pub struct FieldInit {
1679    pub name: Ident,
1680    /// `None` means shorthand — the field's value is the same-named binding.
1681    pub value: Option<Expr>,
1682    pub span: Span,
1683}
1684
1685/// One arm of a `match` expression: `pattern => body`.
1686#[derive(Debug, Clone)]
1687pub struct MatchArm {
1688    pub pattern: Pattern,
1689    pub body: MatchBody,
1690    pub span: Span,
1691}
1692
1693/// The right-hand side of a match arm — either a single expression or
1694/// a block.
1695#[derive(Debug, Clone)]
1696pub enum MatchBody {
1697    Expr(Expr),
1698    Block(Block),
1699}
1700
1701impl MatchBody {
1702    pub fn span(&self) -> Span {
1703        match self {
1704            MatchBody::Expr(e) => e.span,
1705            MatchBody::Block(b) => b.span,
1706        }
1707    }
1708}
1709
1710/// A pattern (v0.2 §3.8). Patterns appear in `match` arms and as the
1711/// right-hand side of the `is` operator.
1712#[derive(Debug, Clone)]
1713pub enum Pattern {
1714    /// `_` — matches any value, no bindings.
1715    Wildcard(Span),
1716    /// `Variant` or `Variant(bindings)` or `TypeName.Variant(bindings)`.
1717    Variant {
1718        /// Optional qualifier: `TypeName.Variant`.
1719        type_name: Option<Ident>,
1720        /// The variant name.
1721        variant: Ident,
1722        /// Payload bindings (empty for nullary variants).
1723        bindings: Vec<PatternBinding>,
1724        span: Span,
1725    },
1726}
1727
1728impl Pattern {
1729    pub fn span(&self) -> Span {
1730        match self {
1731            Pattern::Wildcard(s) => *s,
1732            Pattern::Variant { span, .. } => *span,
1733        }
1734    }
1735}
1736
1737/// A single binding inside a variant pattern. Two surface forms:
1738/// `name` (positional — bind the i-th payload field) and
1739/// `fieldName: bindName` (named — bind the named payload field).
1740/// Both forms also accept `_` as the bind name to discard.
1741#[derive(Debug, Clone)]
1742pub struct PatternBinding {
1743    /// Source form: positional or named.
1744    pub kind: PatternBindingKind,
1745    pub span: Span,
1746}
1747
1748#[derive(Debug, Clone)]
1749pub enum PatternBindingKind {
1750    /// `name` (or `_`): bind the payload field at this position to `name`.
1751    Positional { name: Ident },
1752    /// `field: name` (or `field: _`): bind the named payload field to `name`.
1753    Named { field: Ident, name: Ident },
1754}
1755
1756impl PatternBinding {
1757    /// The local name introduced by this binding (used for scope).
1758    /// `_` is a sentinel for "no binding"; callers should compare against it.
1759    pub fn local_name(&self) -> &Ident {
1760        match &self.kind {
1761            PatternBindingKind::Positional { name } => name,
1762            PatternBindingKind::Named { name, .. } => name,
1763        }
1764    }
1765
1766    pub fn is_wildcard(&self) -> bool {
1767        self.local_name().name == "_"
1768    }
1769}
1770
1771#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1772pub enum BinOp {
1773    /// `P implies Q` — logical implication (v0.80). Desugars to `!P || Q`; sits
1774    /// at the lowest precedence (below `||`). Reads directionally (P → Q).
1775    Implies,
1776    Or,
1777    And,
1778    Eq,
1779    NotEq,
1780    Lt,
1781    LtEq,
1782    Gt,
1783    GtEq,
1784    Add,
1785    Sub,
1786    Mul,
1787    Div,
1788}
1789
1790impl BinOp {
1791    pub fn name(self) -> &'static str {
1792        match self {
1793            BinOp::Implies => "implies",
1794            BinOp::Or => "||",
1795            BinOp::And => "&&",
1796            BinOp::Eq => "==",
1797            BinOp::NotEq => "!=",
1798            BinOp::Lt => "<",
1799            BinOp::LtEq => "<=",
1800            BinOp::Gt => ">",
1801            BinOp::GtEq => ">=",
1802            BinOp::Add => "+",
1803            BinOp::Sub => "-",
1804            BinOp::Mul => "*",
1805            BinOp::Div => "/",
1806        }
1807    }
1808}
1809
1810#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1811pub enum UnaryOp {
1812    Neg,
1813    Not,
1814}
1815
1816impl UnaryOp {
1817    pub fn name(self) -> &'static str {
1818        match self {
1819            UnaryOp::Neg => "-",
1820            UnaryOp::Not => "!",
1821        }
1822    }
1823}