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