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