Skip to main content

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}