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