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