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