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