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