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