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 /// The bound as written (`-20_000`), digit separators kept (#2993). Only
2747 /// the formatter reads it; `value` is what the checker uses.
2748 pub lexeme: String,
2749 pub span: Span,
2750}
2751
2752/// A float refinement bound (v0.21): the parsed value plus the signed source
2753/// lexeme (for byte-stable emission). v0.40 (ADR 0073): also the source span,
2754/// for the `InRange`-swap quick-fix.
2755#[derive(Debug, Clone)]
2756pub struct FloatBound {
2757 pub value: f64,
2758 pub lexeme: String,
2759 pub span: Span,
2760}
2761
2762/// A decimal refinement bound (#2622): `InRange(0.00d, 100.00d)`. The
2763/// coefficient keeps any leading `-`; the lexeme is the signed source text.
2764#[derive(Debug, Clone)]
2765pub struct DecimalBound {
2766 pub coef: String,
2767 pub scale: u32,
2768 pub lexeme: String,
2769 pub span: Span,
2770}
2771
2772#[derive(Debug, Clone)]
2773pub struct Refinement {
2774 pub predicates: Vec<RefinementPred>,
2775 pub span: Span,
2776}
2777
2778#[derive(Debug, Clone)]
2779pub struct RefinementPred {
2780 pub kind: PredKind,
2781 /// The integer argument of `MinLength`/`MaxLength`/`Length` as written
2782 /// (`1_000`), digit separators kept (#2993); `None` for other predicates
2783 /// and for a predicate built without source. Only the formatter reads it.
2784 pub arg_text: Option<String>,
2785 pub span: Span,
2786}
2787
2788#[derive(Debug, Clone)]
2789pub enum PredKind {
2790 Matches(String),
2791 InRange(IntBound, IntBound),
2792 /// `InRange` with float bounds (v0.21) — a separate variant so every
2793 /// `Int` refinement path stays untouched. Bounds keep their source
2794 /// lexemes (including any sign) so emitted runtime checks are
2795 /// byte-stable.
2796 InRangeF(FloatBound, FloatBound),
2797 /// `InRange` with decimal bounds (#2622): `InRange(0.00d, 100.00d)`.
2798 InRangeD(DecimalBound, DecimalBound),
2799 MinLength(i64),
2800 MaxLength(i64),
2801 Length(i64),
2802 NonNegative,
2803 Positive,
2804 NonEmpty,
2805 /// #2867: a `String` that is a real calendar date, exactly `YYYY-MM-DD`
2806 /// (years 0000-9999). The same date rule `Instant.fromIso` applies.
2807 IsoDate,
2808}
2809
2810/// #2867: is `s` exactly `YYYY-MM-DD` and a date that exists? The one
2811/// compile-time statement of the rule `IsoDate` enforces; the runtime's
2812/// `__bynkIsIsoDate` (`bynk-emit/runtime/src/time.ts`) is its TypeScript twin
2813/// and shares the calendar with `Instant.fromIso`.
2814pub fn is_iso_date(s: &str) -> bool {
2815 let b = s.as_bytes();
2816 if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
2817 return false;
2818 }
2819 let digits = |r: std::ops::Range<usize>| -> Option<u32> {
2820 let part = &b[r];
2821 if part.iter().all(u8::is_ascii_digit) {
2822 Some(part.iter().fold(0, |n, d| n * 10 + u32::from(d - b'0')))
2823 } else {
2824 None
2825 }
2826 };
2827 let (Some(year), Some(month), Some(day)) = (digits(0..4), digits(5..7), digits(8..10)) else {
2828 return false;
2829 };
2830 let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
2831 let days = match month {
2832 2 if leap => 29,
2833 2 => 28,
2834 4 | 6 | 9 | 11 => 30,
2835 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2836 _ => return false,
2837 };
2838 (1..=days).contains(&day)
2839}
2840
2841impl PredKind {
2842 pub fn name(&self) -> &'static str {
2843 match self {
2844 PredKind::Matches(_) => "Matches",
2845 PredKind::InRange(..) | PredKind::InRangeF(..) | PredKind::InRangeD(..) => "InRange",
2846 PredKind::MinLength(_) => "MinLength",
2847 PredKind::MaxLength(_) => "MaxLength",
2848 PredKind::Length(_) => "Length",
2849 PredKind::NonNegative => "NonNegative",
2850 PredKind::Positive => "Positive",
2851 PredKind::NonEmpty => "NonEmpty",
2852 PredKind::IsoDate => "IsoDate",
2853 }
2854 }
2855}
2856
2857/// #1651: the order a refinement's predicates are **checked** in at runtime:
2858/// every `Matches` after every other predicate, and otherwise source order.
2859///
2860/// A `Matches` pattern may take time polynomial in the input's length, and a
2861/// `MaxLength`/`Length` bound is what caps it (`bynk.types.polynomial_regex_capped`),
2862/// so the length checks must run first wherever the author wrote them. The order
2863/// is observable only in which failure a value failing several predicates
2864/// reports. Every runtime check site (`.of`, the boundary codec, `is`) and the
2865/// checker's compile-time literal check use this order, so they agree. The
2866/// canonical form a contract fingerprint hashes sorts its own copy and is
2867/// unaffected.
2868pub fn in_check_order<T>(preds: &[T], kind: impl Fn(&T) -> &PredKind) -> Vec<&T> {
2869 let (regex, rest): (Vec<&T>, Vec<&T>) = preds
2870 .iter()
2871 .partition(|p| matches!(kind(p), PredKind::Matches(_)));
2872 rest.into_iter().chain(regex).collect()
2873}
2874
2875/// A function type parameter (v0.20a, `fn name[A, B](…)`). A struct rather
2876/// than a bare Ident so the ADR-0028 "bound-capable" promise is a later field
2877/// addition, not a representation change.
2878#[derive(Debug, Clone)]
2879pub struct TypeParam {
2880 pub name: Ident,
2881 /// Declared constraints, `T: C1 + C2` (constrained-generics slice 0). Empty
2882 /// for an unbounded parameter. Slice 0 admits only `Eq`; the checker
2883 /// reports any other name as `bynk.generics.unknown_constraint`.
2884 pub bounds: Vec<Ident>,
2885 pub span: Span,
2886}
2887
2888/// One `A: C1 + C2` entry of a method's `where` clause (constrained-generics
2889/// slice 0): bounds a type parameter the method does not declare itself — the
2890/// receiver type's (`fn Box.has(self, x: A) -> Bool where A: Eq`).
2891#[derive(Debug, Clone)]
2892pub struct WhereBound {
2893 pub param: Ident,
2894 pub bounds: Vec<Ident>,
2895 pub span: Span,
2896}
2897
2898/// A lambda expression (v0.20a): `(params) => expr` or `(params) => { … }`.
2899/// `=>` is the value arrow (shared with `match`); param annotations are
2900/// optional where an expected function type supplies them.
2901#[derive(Debug, Clone)]
2902pub struct LambdaExpr {
2903 pub params: Vec<LambdaParam>,
2904 pub body: Box<Expr>,
2905 pub span: Span,
2906 /// #2712: whether the author wrote the body as a `{ … }` block. Only read
2907 /// when a parameter is a pattern, whose desugaring wraps the body in a block
2908 /// either way; the formatter needs the source form back.
2909 pub block_body: bool,
2910}
2911
2912/// A lambda parameter. A separate type from [`Param`] because its annotation
2913/// is optional — `Param.type_ref` stays mandatory at every signature site.
2914#[derive(Debug, Clone)]
2915pub struct LambdaParam {
2916 pub name: Ident,
2917 pub type_ref: Option<TypeRef>,
2918 pub span: Span,
2919 /// #2712: the irrefutable record pattern the author wrote in place of a name
2920 /// — `(Item { qty, .. }) => …`. `name` is then the hidden
2921 /// `__lambdaArg<offset>` (empty span, at the pattern), `type_ref` is `None`,
2922 /// and the lambda's body is already desugared to
2923 /// `{ let <pattern> = __lambdaArg<offset>; <body> }` through the `else`-less
2924 /// `let` (ADR 0484, #2654). The pattern is kept here for the checker (the
2925 /// parameter's type comes from its name) and to print the source form back.
2926 pub pattern: Option<Pattern>,
2927}
2928
2929/// #2712: the prefix of every hidden lambda-parameter binder. See
2930/// [`LambdaParam::pattern`]; like [`LET_ELSE_BINDER_PREFIX`] it cannot collide
2931/// with a user identifier.
2932pub const LAMBDA_ARG_BINDER_PREFIX: &str = "__lambdaArg";
2933
2934/// #2896: the prefix of the hidden name a wildcard lambda parameter (`_`) is
2935/// given — `__wild<offset>`, with an empty span at the `_`. The parameter takes
2936/// its type from the expected function type like a named one; the unique name
2937/// keeps `(_, _) => …` from emitting duplicate TS identifiers, and no user
2938/// identifier can spell it, so nothing in the body can refer to it.
2939pub const LAMBDA_WILDCARD_PREFIX: &str = "__wild";
2940
2941impl LambdaParam {
2942 /// #2896: whether this is a `_` parameter. Keyed on the hidden prefix,
2943 /// which no user identifier can start with (identifiers do not begin with
2944 /// `_`); any new generated name must not reuse `__wild`.
2945 pub fn is_wildcard(&self) -> bool {
2946 self.pattern.is_none() && self.name.name.starts_with(LAMBDA_WILDCARD_PREFIX)
2947 }
2948}
2949
2950#[derive(Debug, Clone)]
2951pub struct FnDecl {
2952 /// v0.20a: `[A, B]` type parameters; empty for non-generic functions.
2953 pub type_params: Vec<TypeParam>,
2954 /// Free function or method (`TypeName.methodName`). See [`FnName`].
2955 pub name: FnName,
2956 pub params: Vec<Param>,
2957 pub return_type: TypeRef,
2958 /// #2868: `given C1, C2` on a context-level function, parsed straight after
2959 /// the return type: the capabilities the body may use, and that every
2960 /// caller must itself have in scope. Empty when absent.
2961 pub given: Vec<CapRef>,
2962 /// `where A: Eq, …`, parsed between the return type and the contract
2963 /// clauses; empty when absent. Bounds receiver type parameters (and may
2964 /// restate the method's own).
2965 pub where_bounds: Vec<WhereBound>,
2966 /// v0.115: preconditions (`requires <name>: <pred>`), parsed between the
2967 /// return type and the body. A contract clause is the invariant predicate
2968 /// attached to a function (ADR 0144 — one predicate surface); `requires`
2969 /// scopes over the parameters only.
2970 pub requires: Vec<Contract>,
2971 /// v0.115: postconditions (`ensures <name>: <pred>`). Scopes over the
2972 /// parameters *and* `result`, the contextual binding for the return value.
2973 pub ensures: Vec<Contract>,
2974 pub body: Block,
2975 /// True when the first parameter is the special `self` parameter. Only
2976 /// valid for method declarations.
2977 pub has_self: bool,
2978 /// Documentation block attached to this declaration (v0.3).
2979 pub documentation: Option<Doc>,
2980 /// #2618: the span of a leading `private` modifier, when the declaration
2981 /// is unit-private: visible across its own unit's files (and to that
2982 /// unit's suites), never through `uses`. `None` is the default, public.
2983 pub private: Option<Span>,
2984 pub span: Span,
2985 pub trivia: Trivia,
2986}
2987
2988/// A function-declaration name: either a free function `f` or a method
2989/// `T.method` (v0.2 §3.6).
2990#[derive(Debug, Clone)]
2991pub enum FnName {
2992 /// `fn name(...)` — a free function.
2993 Free(Ident),
2994 /// `fn TypeName.methodName(...)` — a method attached to a type.
2995 Method {
2996 type_name: Ident,
2997 method_name: Ident,
2998 },
2999}
3000
3001impl FnName {
3002 /// The function's short name for diagnostics. For methods returns the
3003 /// method portion only; the type prefix is recovered via `type_name`.
3004 pub fn ident(&self) -> &Ident {
3005 match self {
3006 FnName::Free(id) => id,
3007 FnName::Method { method_name, .. } => method_name,
3008 }
3009 }
3010
3011 /// For methods, the attached type's identifier; `None` for free fns.
3012 pub fn type_name(&self) -> Option<&Ident> {
3013 match self {
3014 FnName::Free(_) => None,
3015 FnName::Method { type_name, .. } => Some(type_name),
3016 }
3017 }
3018
3019 /// The displayed full name (e.g., `Money.add` or `parseSku`).
3020 pub fn display(&self) -> String {
3021 match self {
3022 FnName::Free(id) => id.name.clone(),
3023 FnName::Method {
3024 type_name,
3025 method_name,
3026 } => format!("{}.{}", type_name.name, method_name.name),
3027 }
3028 }
3029}
3030
3031/// A brace-delimited block of statements ending in a tail expression
3032/// whose value is the block's value (spec v0.1 §3.1).
3033#[derive(Debug, Clone)]
3034pub struct Block {
3035 pub statements: Vec<Statement>,
3036 pub tail: Box<Expr>,
3037 pub span: Span,
3038 /// Line comments that appear between the last statement (or the
3039 /// opening brace) and the tail expression. Preserved here because
3040 /// expressions do not carry trivia in v1.1.
3041 pub tail_leading_comments: Vec<Comment>,
3042 /// `true` when the block was written with no explicit tail expression and
3043 /// the parser synthesised a `()` (unit) tail (v0.146, ADR 0170). The tail
3044 /// is a real `ExprKind::UnitLit` either way; this flag records that it was
3045 /// *implicit* so the formatter can omit it (Bynk has no statement
3046 /// terminator, so a printed `()` would re-attach to the last statement on
3047 /// re-parse — `x` `()` → `x()`). The parser re-derives the implicit unit
3048 /// tail, so omitting it is loss-free.
3049 pub implicit_tail: bool,
3050}
3051
3052impl Block {
3053 /// Whether this block is a synthesised empty unit block — no statements and
3054 /// an *implicit* `()` tail (v0.146, ADR 0170). This is exactly the shape the
3055 /// parser inserts for an `if` with no `else` branch, so both the checker
3056 /// (gating the else-less form to unit) and the formatter (omitting the
3057 /// synthetic `else { () }`) recognise it here.
3058 pub fn is_synth_unit(&self) -> bool {
3059 self.statements.is_empty()
3060 && self.implicit_tail
3061 && matches!(self.tail.kind, ExprKind::UnitLit)
3062 }
3063}
3064
3065/// Block-level statement.
3066#[derive(Debug, Clone)]
3067pub enum Statement {
3068 /// `let name (: T)? = expr` — pure binding (v0.1).
3069 Let(LetStmt),
3070 /// `let name (: T)? <- expr` — effectful binding (v0.5).
3071 EffectLet(LetStmt),
3072 /// `expect expr` — verify a Bool predicate at test runtime (v0.7; renamed
3073 /// from `assert` in v0.112). Only valid inside test case bodies.
3074 Expect(ExpectStmt),
3075 /// `~> expr` — an asynchronous fire-and-forget send (v0.79). The caller does
3076 /// not await the reply; legal only when the reply is `Effect[()]`. No binder.
3077 Send(SendStmt),
3078 /// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
3079 /// Runs an `Effect[()]` and discards its (unit) result — the binder-free
3080 /// sugar for `let _ <- expr` when the awaited value is unit. Legal only in
3081 /// an effectful body; the operand MUST be `Effect[()]` (a valued reply keeps
3082 /// the explicit `let _ <- e`, so throwing away a real value stays visible).
3083 Do(DoStmt),
3084 /// `name := expr` — a `Cell` store write (v0.81, storage track). The
3085 /// unconditional write form; `.update(fn)` (a method call) is the
3086 /// read-modify-write form. ADR 0108.
3087 Assign(AssignStmt),
3088 /// `advance Clock by <Duration>` — move the harness clock forward
3089 /// (#2137, end-to-end testing track §3.4). `advance` is contextual: it
3090 /// opens a statement only before `<Ident> by`. Only valid in a `case` body.
3091 Advance(AdvanceStmt),
3092 /// `par { let a <- e1 let b <- e2 }` — a concurrent binding block (#2640,
3093 /// concurrent-effects slice 0). `par` is contextual: it opens a statement
3094 /// only before `{`. The parser admits any statements inside; the checker
3095 /// accepts only `let x <- e` and `do e`.
3096 Par(ParStmt),
3097}
3098
3099impl Statement {
3100 pub fn span(&self) -> Span {
3101 match self {
3102 Statement::Let(l) | Statement::EffectLet(l) => l.span,
3103 Statement::Expect(a) => a.span,
3104 Statement::Send(s) => s.span,
3105 Statement::Do(d) => d.span,
3106 Statement::Assign(a) => a.span,
3107 Statement::Advance(a) => a.span,
3108 Statement::Par(p) => p.span,
3109 }
3110 }
3111}
3112
3113/// #2640: a `par { … }` block. `statements` is what the author wrote between
3114/// the braces, unvalidated: the checker rejects every form but the effectful
3115/// `let` and `do` (`bynk.par.invalid_statement`), so the editor still sees the
3116/// rejected statement in place.
3117#[derive(Debug, Clone)]
3118pub struct ParStmt {
3119 /// The contextual `par` keyword.
3120 pub keyword: Span,
3121 pub statements: Vec<Statement>,
3122 /// Comments between the last statement and the closing `}`.
3123 pub closing_comments: Vec<Comment>,
3124 pub span: Span,
3125 pub trivia: Trivia,
3126}
3127
3128/// #2137: `advance <Cap> by <amount>` — moves the case's harness clock forward
3129/// by a `Duration`. `cap` must be `Clock` (the checker refuses any other).
3130#[derive(Debug, Clone)]
3131pub struct AdvanceStmt {
3132 /// The contextual `advance` keyword.
3133 pub keyword: Span,
3134 /// The capability whose time moves (`Clock`).
3135 pub cap: Ident,
3136 /// The `Duration` to move it by.
3137 pub amount: Expr,
3138 pub span: Span,
3139 pub trivia: Trivia,
3140}
3141
3142/// #2842: the `matches snapshot "<name>"` tail of an `expect`. `matches` and
3143/// `snapshot` are contextual words, matched by text like `faults`.
3144#[derive(Debug, Clone)]
3145pub struct SnapshotClause {
3146 /// The snapshot's name (the literal's text), unique within its case.
3147 pub name: String,
3148 /// The `matches` keyword through the closing quote of the name.
3149 pub span: Span,
3150 /// The name literal.
3151 pub name_span: Span,
3152}
3153
3154#[derive(Debug, Clone)]
3155pub struct ExpectStmt {
3156 pub value: Expr,
3157 /// #2842: `expect <value> matches snapshot "<name>"` compares `value`'s
3158 /// canonical JSON to a stored snapshot instead of asserting a `Bool`. `None`
3159 /// for every other `expect`.
3160 pub snapshot: Option<SnapshotClause>,
3161 pub span: Span,
3162 pub trivia: Trivia,
3163}
3164
3165/// `name := expr` — a `Cell` store write (v0.81, storage track). `target` is the
3166/// `Cell` field being written (a bare name for now; the checker resolves it to a
3167/// `store` field). `value` is the new value.
3168#[derive(Debug, Clone)]
3169pub struct AssignStmt {
3170 pub target: Ident,
3171 pub value: Expr,
3172 pub span: Span,
3173 pub trivia: Trivia,
3174}
3175
3176#[derive(Debug, Clone)]
3177pub struct LetStmt {
3178 pub name: Ident,
3179 pub type_annot: Option<TypeRef>,
3180 pub value: Expr,
3181 /// v0.182 (#664): the call-site `by <Actor>(<identity>)` clause on an
3182 /// `EffectLet` whose value addresses a test service handler. `None` on a pure
3183 /// `Let` (the `by` is parsed only in the `<-` arm) and on an effect-let with
3184 /// no principal.
3185 pub principal: Option<CallSiteActor>,
3186 /// #2010: set when this binding is the hidden one a `let <pattern> … else …`
3187 /// statement desugars into. `None` for every binding the author wrote.
3188 pub let_else: Option<LetElse>,
3189 pub span: Span,
3190 pub trivia: Trivia,
3191}
3192
3193/// #2010: what remains of a `let <pattern> <- e else …` (or `= e else …`)
3194/// statement once the parser has desugared it.
3195///
3196/// The parser rewrites `let P <- e else E` followed by the rest of the block
3197/// into a hidden binding, `let __letElse<offset> <- e`, and a `match` on it as
3198/// the block's tail: `P => { rest of the block }` first, then either
3199/// `_ => E` (an `else` block) or the `else match` arms. Everything downstream
3200/// (checker, emitter, editor) sees only that ordinary `match`. This marker,
3201/// carried on the hidden binding, is what lets the formatter print the sugar
3202/// back and the checker word its diagnostics in the author's terms.
3203#[derive(Debug, Clone)]
3204pub struct LetElse {
3205 /// Whether the `else` was a block or a `match { arms }`.
3206 pub form: LetElseForm,
3207 /// The `else` keyword (empty, at the end of the statement, for [`LetElseForm::Plain`]).
3208 pub else_span: Span,
3209 /// #2654: for a record pattern, the source ranges whose deletion leaves the
3210 /// `else`-less form — any parentheses around the pattern, and the `else`
3211 /// clause with the space before it. Empty for every other pattern, and for
3212 /// the plain form.
3213 pub remove_else: Vec<Span>,
3214}
3215
3216/// #2010: the spellings of a `let … else` fallback (and, #2654, its absence).
3217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3218pub enum LetElseForm {
3219 /// `else { … }` — desugared to a trailing `_ => { … }` arm.
3220 Block,
3221 /// `else match { arms }` — the arms follow the pattern's arm unchanged.
3222 Match,
3223 /// No `else` at all (#2654): an irrefutable record pattern, `let Item { sku, .. } = e`,
3224 /// desugared to a one-arm `match`. `else_span` is then empty, at the end of
3225 /// the statement.
3226 Plain,
3227}
3228
3229/// #2010: the prefix of every hidden `let … else` binding. User identifiers
3230/// start with a letter (the lexer's `Ident` rule), so no user name can collide
3231/// with it, and the emitter already treats `__`-prefixed names as its own.
3232pub const LET_ELSE_BINDER_PREFIX: &str = "__letElse";
3233
3234impl Expr {
3235 /// #2010: whether this is the `match` a `let <pattern> … else …` was
3236 /// desugared into — one whose scrutinee is the hidden binder. It is not an
3237 /// expression the author wrote, so source-level tools (refactorings,
3238 /// diagnostics) treat it as the statement it came from.
3239 pub fn is_let_else_match(&self) -> bool {
3240 matches!(
3241 &self.kind,
3242 ExprKind::Match { discriminant, .. }
3243 if matches!(&discriminant.kind, ExprKind::Ident(i) if i.name.starts_with(LET_ELSE_BINDER_PREFIX))
3244 )
3245 }
3246}
3247
3248/// #2010: whether `name` is a binding the compiler generated rather than one
3249/// the author wrote. Editor surfaces (inlay hints, locals, symbols) skip these.
3250pub fn is_generated_binding(name: &str) -> bool {
3251 name.starts_with("__")
3252}
3253
3254#[derive(Debug, Clone)]
3255pub struct SendStmt {
3256 /// The send target — a recipient call, e.g. `Logger.info(msg)`.
3257 pub value: Expr,
3258 pub span: Span,
3259 pub trivia: Trivia,
3260}
3261
3262/// `do expr` — an effect-performing expression statement (v0.146, ADR 0170).
3263/// `value` is the awaited effect, which MUST be `Effect[()]`.
3264#[derive(Debug, Clone)]
3265pub struct DoStmt {
3266 pub value: Expr,
3267 pub span: Span,
3268 pub trivia: Trivia,
3269}
3270
3271#[derive(Debug, Clone)]
3272pub struct Param {
3273 pub name: Ident,
3274 pub type_ref: TypeRef,
3275 pub span: Span,
3276}
3277
3278#[derive(Debug, Clone)]
3279pub enum TypeRef {
3280 Base(BaseType, Span),
3281 Named(Ident),
3282 /// `Result[T, E]` — the built-in generic Result type (v0.1).
3283 Result(Box<TypeRef>, Box<TypeRef>, Span),
3284 /// `Option[T]` — the built-in generic Option type (v0.2).
3285 Option(Box<TypeRef>, Span),
3286 /// `Effect[T]` — the built-in generic Effect type (v0.5).
3287 Effect(Box<TypeRef>, Span),
3288 /// `HttpResult[T]` — the built-in HTTP-result sum (v0.9).
3289 HttpResult(Box<TypeRef>, Span),
3290 /// `QueueResult` — the built-in queue verdict sum (`Ack | Retry`),
3291 /// non-generic; the required return of a queue handler (v0.44).
3292 QueueResult(Span),
3293 /// `Asset` — a static file declared by `asset name = "path"` (#1883,
3294 /// slice 1a, [DECISION H]). Non-generic, like `QueueResult`; its one
3295 /// field is `.url : String`.
3296 Asset(Span),
3297 /// `RequestHeaders` (#2341, decision A) — an HTTP handler's read-only view
3298 /// of the request's headers, the type of its `headers` parameter.
3299 /// Non-generic and opaque: `get`, `bearer`, `clientIp`. Never stored or
3300 /// sent (it has no wire form).
3301 RequestHeaders(Span),
3302 /// `RequestQuery` (#2854, decision B) — an HTTP handler's read-only view
3303 /// of the request's query string, the type of its `query` parameter when
3304 /// it wants raw access: `get`, `all`, `params`. Never stored or sent.
3305 RequestQuery(Span),
3306 /// `TokenClaims` (#2380, L5) — the verified claims of an `Oidc` actor's
3307 /// token, read through its binder (`who.claims`). Read-only, never stored
3308 /// or sent.
3309 TokenClaims(Span),
3310 /// `BoundaryRejection` (#2341, decision C) — why the router refused a
3311 /// request before any handler ran; the type of an `on rejected` handler's
3312 /// parameter. Opaque here (its variants are matched, never built). Never
3313 /// stored or sent.
3314 BoundaryRejection(Span),
3315 /// `List[T]` — the built-in generic immutable list type (v0.20b).
3316 List(Box<TypeRef>, Span),
3317 /// `Map[K, V]` — the built-in generic immutable map type (v0.20b).
3318 /// Keys are confined to `Key` types
3319 /// (`bynk.types.not_keyable`).
3320 Map(Box<TypeRef>, Box<TypeRef>, Span),
3321 /// `Set[T]` — the built-in generic immutable set type (standard-library
3322 /// K6a, #2596). Insertion-ordered like `Map`; elements are confined to the
3323 /// `Map`-key rule (`bynk.types.not_keyable`).
3324 Set(Box<TypeRef>, Span),
3325 /// `Query[T]` — the built-in lazy storage-read description (v0.91, ADR 0115).
3326 /// Nameable in a pure helper's return type; non-storable and non-boundary
3327 /// (like `Effect`/`Fn`).
3328 Query(Box<TypeRef>, Span),
3329 /// `Stream[T]` — the value-over-time primitive (v0.100, real-time track
3330 /// slice 0). A lazy, pull-shaped sequence produced over time; non-storable
3331 /// and non-boundary (like `Query`/`Effect`/`Fn`).
3332 Stream(Box<TypeRef>, Span),
3333 /// `Connection[F]` — a held WebSocket connection (v0.102, real-time track
3334 /// slice 2). `F` is the server→client frame type. A `Held` resource:
3335 /// non-serialisable, non-boundary, and governed by the linearity discipline
3336 /// (§2.9); storable only in `Cell[Option[Connection]]` / `Map[K, Connection]`.
3337 Connection(Box<TypeRef>, Span),
3338 /// `History[Agent]` — a generated, driven call-history of an agent (v0.119,
3339 /// testing track slice 7, ADR 0155). A test-only generator, legal only in
3340 /// `for all` binding position inside a `property`; it is not a value type,
3341 /// so it never resolves in a field/param/return position. The bound subject
3342 /// behaves as an ordinary `List[Step]`.
3343 History(Box<TypeRef>, Span),
3344 /// `ValidationError` — the built-in error type used by refined-type
3345 /// constructors (v0.1).
3346 ValidationError(Span),
3347 /// `JsonError` — the built-in JSON-decode error type (v0.22b). A
3348 /// uniform record (`kind`/`path`/`message`, all `String`) the codec
3349 /// maps `BoundaryError` variants and parse failures into.
3350 JsonError(Span),
3351 /// `()` — the unit type (v0.5).
3352 Unit(Span),
3353 /// `A -> B` / `(A, B) -> C` / `() -> B` — a function type (v0.20a).
3354 /// Right-associative; effectful iff the return type is `Effect[_]`
3355 /// (the structural rule). Confined to non-boundary positions
3356 /// (`bynk.types.function_at_boundary`).
3357 Fn(Vec<TypeRef>, Box<TypeRef>, Span),
3358 /// `Name[Arg, …]` — an application of a user-declared generic type
3359 /// (v0.157, ADR 0183). `name` is a user type name (never a built-in
3360 /// generic, which each have a dedicated variant above). Arity and the
3361 /// existence of the referenced type are checked in the resolver.
3362 App {
3363 name: Ident,
3364 args: Vec<TypeRef>,
3365 span: Span,
3366 },
3367}
3368
3369impl TypeRef {
3370 pub fn span(&self) -> Span {
3371 match self {
3372 TypeRef::Base(_, s) => *s,
3373 TypeRef::Named(id) => id.span,
3374 TypeRef::Result(_, _, s) => *s,
3375 TypeRef::Option(_, s) => *s,
3376 TypeRef::Effect(_, s) => *s,
3377 TypeRef::HttpResult(_, s) => *s,
3378 TypeRef::QueueResult(s) => *s,
3379 TypeRef::Asset(s) => *s,
3380 TypeRef::RequestHeaders(s) => *s,
3381 TypeRef::RequestQuery(s) => *s,
3382 TypeRef::TokenClaims(s) => *s,
3383 TypeRef::BoundaryRejection(s) => *s,
3384 TypeRef::List(_, s) => *s,
3385 TypeRef::Map(_, _, s) => *s,
3386 TypeRef::Set(_, s) => *s,
3387 TypeRef::Query(_, s) => *s,
3388 TypeRef::Stream(_, s) => *s,
3389 TypeRef::Connection(_, s) => *s,
3390 TypeRef::History(_, s) => *s,
3391 TypeRef::ValidationError(s) => *s,
3392 TypeRef::JsonError(s) => *s,
3393 TypeRef::Unit(s) => *s,
3394 TypeRef::Fn(_, _, s) => *s,
3395 TypeRef::App { span, .. } => *span,
3396 }
3397 }
3398}
3399
3400/// K-streams (#2623): where `t` reaches a `Stream`, as the chain of fields it
3401/// is reached through (`["Bundle.download", "Download.body"]`), or `None`. A
3402/// bare `Stream` (or one inside a wrapper) is the empty chain. A record or sum
3403/// may carry a stream; such a type is then non-boundary like the stream
3404/// itself, and this is the walk every boundary check uses to find it. It
3405/// follows named and applied types into their declarations (a generic
3406/// record's own fields too, so `Wrap[T] = { s: Stream[T] }` is found through
3407/// `Wrap[Int]`), but not into a function type, which is rejected at a
3408/// boundary on its own. Terminates via the `visited` set.
3409pub fn stream_path(
3410 t: &TypeRef,
3411 types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3412) -> Option<Vec<String>> {
3413 fn walk(
3414 t: &TypeRef,
3415 types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3416 visited: &mut std::collections::HashSet<String>,
3417 ) -> Option<Vec<String>> {
3418 match t {
3419 TypeRef::Stream(..) => Some(Vec::new()),
3420 TypeRef::Named(id) => named(&id.name, types, visited),
3421 TypeRef::App { name, args, .. } => named(&name.name, types, visited)
3422 .or_else(|| args.iter().find_map(|a| walk(a, types, visited))),
3423 TypeRef::Option(a, _)
3424 | TypeRef::List(a, _)
3425 | TypeRef::Set(a, _)
3426 | TypeRef::Effect(a, _)
3427 | TypeRef::HttpResult(a, _)
3428 | TypeRef::Query(a, _)
3429 | TypeRef::Connection(a, _)
3430 | TypeRef::History(a, _) => walk(a, types, visited),
3431 TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
3432 walk(a, types, visited).or_else(|| walk(b, types, visited))
3433 }
3434 TypeRef::Fn(..)
3435 | TypeRef::Base(..)
3436 | TypeRef::QueueResult(_)
3437 | TypeRef::Asset(_)
3438 | TypeRef::RequestHeaders(_)
3439 | TypeRef::RequestQuery(_)
3440 | TypeRef::TokenClaims(_)
3441 | TypeRef::BoundaryRejection(_)
3442 | TypeRef::ValidationError(_)
3443 | TypeRef::JsonError(_)
3444 | TypeRef::Unit(_) => None,
3445 }
3446 }
3447 fn named(
3448 name: &str,
3449 types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3450 visited: &mut std::collections::HashSet<String>,
3451 ) -> Option<Vec<String>> {
3452 if !visited.insert(name.to_string()) {
3453 return None;
3454 }
3455 let decl = types.get(name)?;
3456 let fields: Vec<(String, &TypeRef)> = match &decl.body {
3457 TypeBody::Record(r) => r
3458 .fields
3459 .iter()
3460 .map(|f| (format!("{name}.{}", f.name.name), &f.type_ref))
3461 .collect(),
3462 TypeBody::Sum(s) => s
3463 .variants
3464 .iter()
3465 .flat_map(|v| {
3466 v.payload.iter().map(move |p| {
3467 (
3468 format!("{name}.{}.{}", v.name.name, p.name.name),
3469 &p.type_ref,
3470 )
3471 })
3472 })
3473 .collect(),
3474 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => Vec::new(),
3475 };
3476 fields.into_iter().find_map(|(at, t)| {
3477 walk(t, types, visited).map(|mut rest| {
3478 rest.insert(0, at);
3479 rest
3480 })
3481 })
3482 }
3483 walk(t, types, &mut std::collections::HashSet::new())
3484}
3485
3486/// K-streams (#2623): whether the named type `name` carries a `Stream` in a
3487/// field, at any depth ([`stream_path`]). Such a type has no codec.
3488pub fn type_carries_stream(
3489 name: &str,
3490 types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3491) -> bool {
3492 stream_path(
3493 &TypeRef::Named(Ident {
3494 name: name.to_string(),
3495 span: Span::default(),
3496 }),
3497 types,
3498 )
3499 .is_some()
3500}
3501
3502/// v0.174 (#592): does the generic record type `name` transitively contain a
3503/// reference to itself — through any field-type path, including collection and
3504/// `Option` wrappers, sum-variant payloads, and generic type arguments? Such a
3505/// type has no finite set of monomorphised boundary codecs: uniform recursion
3506/// (`Node[T] = { next: Option[Node[T]] }`) would need a self-referential codec
3507/// chain the per-instantiation model does not yet generate, and polymorphic
3508/// recursion (`Weird[T] = { next: Option[Weird[List[T]]] }`) an unbounded set of
3509/// instantiations. Both are rejected at a boundary
3510/// (`bynk.generics.recursive_generic_at_boundary`).
3511///
3512/// Detection is reachability over the type-containment graph: `name` is
3513/// recursive iff it is reachable from its own body, following every named /
3514/// applied head and descending into every wrapper, map/result pair, function
3515/// position, and generic argument. Terminates via the `visited` set.
3516pub fn generic_record_is_recursive(
3517 name: &str,
3518 types: &std::collections::HashMap<String, std::sync::Arc<TypeDecl>>,
3519) -> bool {
3520 fn heads(t: &TypeRef, out: &mut Vec<String>) {
3521 match t {
3522 TypeRef::Named(id) => out.push(id.name.clone()),
3523 TypeRef::App {
3524 name: app_name,
3525 args,
3526 ..
3527 } => {
3528 out.push(app_name.name.clone());
3529 for a in args {
3530 heads(a, out);
3531 }
3532 }
3533 TypeRef::Option(a, _)
3534 | TypeRef::List(a, _)
3535 | TypeRef::Set(a, _)
3536 | TypeRef::Effect(a, _)
3537 | TypeRef::HttpResult(a, _)
3538 | TypeRef::Query(a, _)
3539 | TypeRef::Stream(a, _)
3540 | TypeRef::Connection(a, _)
3541 | TypeRef::History(a, _) => heads(a, out),
3542 TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
3543 heads(a, out);
3544 heads(b, out);
3545 }
3546 TypeRef::Fn(ps, r, _) => {
3547 for p in ps {
3548 heads(p, out);
3549 }
3550 heads(r, out);
3551 }
3552 TypeRef::Base(..)
3553 | TypeRef::QueueResult(_)
3554 | TypeRef::Asset(_)
3555 | TypeRef::RequestHeaders(_)
3556 | TypeRef::RequestQuery(_)
3557 | TypeRef::TokenClaims(_)
3558 | TypeRef::BoundaryRejection(_)
3559 | TypeRef::ValidationError(_)
3560 | TypeRef::JsonError(_)
3561 | TypeRef::Unit(_) => {}
3562 }
3563 }
3564 fn body_heads(decl: &TypeDecl, out: &mut Vec<String>) {
3565 match &decl.body {
3566 TypeBody::Record(r) => {
3567 for f in &r.fields {
3568 heads(&f.type_ref, out);
3569 }
3570 }
3571 TypeBody::Sum(s) => {
3572 for v in &s.variants {
3573 for p in &v.payload {
3574 heads(&p.type_ref, out);
3575 }
3576 }
3577 }
3578 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
3579 }
3580 }
3581 let Some(root) = types.get(name) else {
3582 return false;
3583 };
3584 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
3585 let mut stack: Vec<String> = Vec::new();
3586 body_heads(root, &mut stack);
3587 while let Some(n) = stack.pop() {
3588 if n == name {
3589 return true;
3590 }
3591 if !visited.insert(n.clone()) {
3592 continue;
3593 }
3594 if let Some(decl) = types.get(&n) {
3595 body_heads(decl, &mut stack);
3596 }
3597 }
3598 false
3599}
3600
3601/// T3.4 (R2.4): a node's identity, independent of position — allocated once,
3602/// monotonically, per expression the parser constructs (`Parser::alloc_expr_id`
3603/// in `bynk-syntax/src/parser.rs`). Never derived from a `Span`, so two
3604/// expressions occupying the same byte range (a synthetic node, a
3605/// zero-width span) never collide the way a span-keyed side table could.
3606#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
3607pub struct ExprId(pub u32);
3608
3609impl ExprId {
3610 /// Reserved for `Expr` nodes built outside the parser — after checking,
3611 /// during emission — that are never looked up in a checker-populated
3612 /// `expr_types`/`expr_ty` table (they are lowered directly, from
3613 /// already-typed sub-expressions they wrap or splice). A lookup against
3614 /// this id is a bug: the node was never checked and has no recorded
3615 /// type of its own.
3616 pub const SYNTHETIC: ExprId = ExprId(u32::MAX);
3617}
3618
3619#[derive(Debug, Clone)]
3620pub struct Expr {
3621 pub id: ExprId,
3622 pub kind: ExprKind,
3623 pub span: Span,
3624}
3625
3626/// Finding #31: `Expr` sets the size of every expression node in the
3627/// program — `ExprKind::Observation`'s payload and `ExprKind::Is`'s pattern
3628/// field are boxed specifically to keep it small (176 bytes unboxed, 128
3629/// boxed, measured on this target). Pinned so the next large variant added
3630/// to `ExprKind` is a compile error here rather than a silent regression.
3631/// T3.4: `id: ExprId` adds 4 bytes (padded); the budget is unchanged, so this
3632/// still fits.
3633/// T3.4 (R2.4): `id: ExprId` is a deliberate 8-byte increase (128 → 136,
3634/// alignment-padded from 4), not a silent regression — the ceiling moves
3635/// with it, once, here, so the next *accidental* growth still trips this
3636/// assertion rather than hiding under slack headroom.
3637/// T3.5 (R2.2): `file: FileId` on `Span` is a deliberate increase (136 → 160
3638/// — `Span` itself grows from 16 to 24 bytes with alignment padding, and
3639/// `ExprKind`'s largest variant carries more than one `Span`), not a silent
3640/// regression — the ceiling moves with it, once, here, exactly as T3.4 did
3641/// for `id: ExprId`.
3642const _: () = assert!(std::mem::size_of::<Expr>() <= 160);
3643
3644impl ExprKind {
3645 /// Construct an `IntLit` for a *synthesized* integer — one the compiler
3646 /// invents rather than reading from source (a default `1`, a computed bound).
3647 /// The lexeme is the canonical decimal form (no separators). Source-parsed
3648 /// literals keep their as-written lexeme instead (v0.142, ADR 0166).
3649 pub fn int_lit(value: i64) -> ExprKind {
3650 ExprKind::IntLit {
3651 value,
3652 lexeme: value.to_string(),
3653 }
3654 }
3655}
3656
3657#[derive(Debug, Clone)]
3658pub enum ExprKind {
3659 /// An integer literal (typed `Int`). The lexeme is kept alongside the parsed
3660 /// value (v0.142, ADR 0166) so formatting is byte-stable: an author's `_`
3661 /// digit separators (`1_048_576`) survive a round-trip, mirroring the
3662 /// `FloatLit` treatment. The value is separator-free; emission lowers the
3663 /// value, so emitted output is unaffected.
3664 IntLit {
3665 value: i64,
3666 lexeme: String,
3667 },
3668 /// A float literal (v0.21). The lexeme is kept alongside the parsed
3669 /// value so emission and formatting are byte-stable (`1e10` must not
3670 /// normalise to `10000000000`).
3671 FloatLit {
3672 value: f64,
3673 lexeme: String,
3674 },
3675 /// A decimal literal (#2622): `1.20d`, `5d`. Typed `Decimal`; the scale is
3676 /// the number of fraction digits as written, so `1.20d` keeps scale 2.
3677 DecimalLit {
3678 /// The coefficient: the digits with the `.` and `_` separators removed
3679 /// (`120` for `1.20d`), as an arbitrary-precision base-10 string.
3680 coef: String,
3681 /// The number of fraction digits as written (`2` for `1.20d`).
3682 scale: u32,
3683 /// The lexeme as written, `d` suffix included (formatting is verbatim).
3684 lexeme: String,
3685 },
3686 /// A duration literal `<int>.<unit>` (v0.86, ADR 0112): `5.minutes`,
3687 /// `30.days`. The parser recognises the `IntLit . <unit>` shape and records
3688 /// the magnitude, the unit, and the resolved milliseconds (the value the
3689 /// emitter lowers to). Typed `Duration`.
3690 DurationLit {
3691 /// The integer magnitude as written (`5` in `5.minutes`).
3692 value: i64,
3693 /// The unit name (`minutes`), one of the closed set.
3694 unit: DurationUnit,
3695 /// The value in milliseconds — `value * unit factor`.
3696 millis: i64,
3697 },
3698 StrLit(String),
3699 /// An interpolated string `"… \(expr) …"` (v0.43, ADR 0075). Chunks and
3700 /// holes alternate. A plain `"…"` with no holes stays [`ExprKind::StrLit`],
3701 /// so existing code and the emitter/formatter fast-path are untouched.
3702 InterpStr(Vec<InterpPart>),
3703 BoolLit(bool),
3704 Ident(Ident),
3705 Call {
3706 name: Ident,
3707 /// v0.20a: explicit type arguments (`name[T](…)`); empty when absent.
3708 type_args: Vec<TypeRef>,
3709 args: Vec<Expr>,
3710 },
3711 /// A lambda (v0.20a). See [`LambdaExpr`].
3712 Lambda(LambdaExpr),
3713 BinOp(BinOp, Box<Expr>, Box<Expr>),
3714 UnaryOp(UnaryOp, Box<Expr>),
3715 Paren(Box<Expr>),
3716 /// `{ stmts; expr }` — block expression (v0.1).
3717 Block(Block),
3718 /// `if cond { then } else { else }` (v0.1).
3719 If {
3720 cond: Box<Expr>,
3721 then_block: Box<Block>,
3722 else_block: Box<Block>,
3723 },
3724 /// `Ok(value)` — Result success constructor (v0.1).
3725 Ok(Box<Expr>),
3726 /// `Err(error)` — Result failure constructor (v0.1).
3727 Err(Box<Expr>),
3728 /// `expr?` — propagation operator (v0.1).
3729 Question(Box<Expr>),
3730 /// `TypeName { field: value, ... }` — record construction (v0.2).
3731 RecordConstruction {
3732 type_name: Ident,
3733 fields: Vec<FieldInit>,
3734 },
3735 /// `receiver.field` — field access on a record value (v0.2). v0.3 adds
3736 /// `.raw` on opaque types within the defining commons.
3737 FieldAccess {
3738 receiver: Box<Expr>,
3739 field: Ident,
3740 },
3741 /// `receiver.method(args)` — instance method call (v0.2). The
3742 /// resolver determines the receiver's type and looks up the method.
3743 MethodCall {
3744 receiver: Box<Expr>,
3745 method: Ident,
3746 /// v0.22b: explicit type arguments on a qualified static
3747 /// (`Json.decode[T](…)`); empty when absent. The same-line-`[`
3748 /// rule applies as for `Call` type application (0039).
3749 type_args: Vec<TypeRef>,
3750 args: Vec<Expr>,
3751 },
3752 /// `match disc { arm+ }` — pattern matching (v0.2).
3753 Match {
3754 discriminant: Box<Expr>,
3755 arms: Vec<MatchArm>,
3756 },
3757 /// `expr is pattern` — pattern test, returns Bool (v0.2).
3758 ///
3759 /// `pattern` is boxed (finding #31): `Pattern`'s `Variant` case carries two
3760 /// `Ident`s plus a `Vec`, inlining it into every `ExprKind` sets the size
3761 /// of every expression node in the program for the one variant that
3762 /// tests a pattern.
3763 Is {
3764 value: Box<Expr>,
3765 pattern: Box<Pattern>,
3766 },
3767 /// `Some(value)` — Option Some constructor (v0.2).
3768 Some(Box<Expr>),
3769 /// `None` — Option None constructor (v0.2).
3770 None,
3771 /// `()` — unit literal (v0.5).
3772 UnitLit,
3773 /// `TypeName { ...base, field: value, ... }` or `{ ...base, ... }` —
3774 /// record spread expression (v0.5).
3775 RecordSpread {
3776 /// Optional type prefix (`TypeName { ...base }`). Absent for the
3777 /// bare form used inside `commit`.
3778 type_name: Option<Ident>,
3779 /// The base record being spread.
3780 base: Box<Expr>,
3781 /// Field overrides (always full `name: value` form — never shorthand).
3782 overrides: Vec<FieldInit>,
3783 },
3784 /// `Effect.pure(value)` — wrap a synchronous value into `Effect[T]`
3785 /// (v0.5). Recognised in the parser as a special-form.
3786 EffectPure(Box<Expr>),
3787 /// `expect expr` — expectation as an expression of type `()` (v0.9.1;
3788 /// renamed from `assert` in v0.112). Valid only inside test bodies. Evaluates
3789 /// `expr` (must be Bool); if false, the surrounding test case fails.
3790 Expect(Box<Expr>),
3791 /// `Val[T]`, `Val[T](args)` — test-context value construction (v0.9.4).
3792 /// `args` is empty for the bare form and holds the pin arguments for
3793 /// `Val[T](...)`. The record-override form `Val[T] { ... }` is not yet
3794 /// parsed. Valid only inside test bodies; has type `T`.
3795 Val {
3796 type_ref: TypeRef,
3797 args: Vec<Expr>,
3798 },
3799 /// `Wire(<String>)` — a raw, pre-validation argument to a `system`-tier
3800 /// service address (testing-the-boundary Slice C). The inner expression is a
3801 /// `String` carrying the wire form the boundary will receive *unvalidated* —
3802 /// a body's JSON text or a path segment — so a case can drive the router with
3803 /// input the type system forbids and observe the rejection. Legal only at
3804 /// `system` (there is no wire at `unit`); the router validates it, so no
3805 /// refined value is ever minted from a `Wire` (ADR 0182 untouched).
3806 Wire(Box<Expr>),
3807 /// `[a, b, c]` — list literal (v0.20b). An empty `[]` requires an
3808 /// expected type (`bynk.types.uninferable_element_type`).
3809 ListLit(Vec<Expr>),
3810 /// An observation over a consumed capability's recorded calls (v0.117,
3811 /// testing track slice 5). The direct subject of an `expect` in a `case`
3812 /// body — `expect Cap.op called once with <pred>`, `expect Cap.op never
3813 /// called`, `expect A.op before B.op`. Types as `Bool` (the claim about the
3814 /// recorded trace), lowered to a boolean over the recorded log.
3815 /// Boxed (finding #31): at ~160 bytes, `ObservationExpr` inlined here set
3816 /// the size of every `ExprKind` for the one variant that records a
3817 /// capability-call observation.
3818 Observation(Box<ObservationExpr>),
3819 /// `<call> faults` — the claim that an effectful call **faults** (#1706).
3820 /// The direct subject of an `expect` in a `case` body —
3821 /// `expect quote.call("GBP") faults`. The claim's `call` is the call
3822 /// itself, an `Effect[_]` the claim awaits; the claim types as `Bool` and
3823 /// holds when awaiting the call throws (a capability fault, an injected
3824 /// `stub … fails`, an invariant violation) rather than returning a value.
3825 /// A fault is untyped and uncatchable by the caller, so this is a test's
3826 /// observation of the fault, not a handler for it: no production code can
3827 /// write it.
3828 ///
3829 /// #1812: the claim may carry a call-site principal (`expect
3830 /// api.POST(…) by User("bob") faults`), so an identity-carrying handler's
3831 /// fault path is testable; see [`FaultsExpr`]. Boxed, as `Observation` is,
3832 /// so the principal does not widen every `ExprKind`.
3833 Faults(Box<FaultsExpr>),
3834 /// `trace(Cap.op)` — the bound-trace escape hatch (v0.117, testing track
3835 /// slice 5). Yields the recorded calls of `Cap.op` as a `List[<CallRecord>]`
3836 /// (a synthetic record of the operation's parameters), asserted over with the
3837 /// ordinary value surface. Test-body-only, like [`ExprKind::Val`].
3838 Trace {
3839 cap: Ident,
3840 op: Ident,
3841 },
3842}
3843
3844/// Every directly-nested sub-expression of `e` — the **total** child
3845/// iterator. The match is exhaustive (no `_` arm), so adding an [`ExprKind`]
3846/// variant is a compile error here rather than a silently incomplete walk —
3847/// the trap the checker's three hand-rolled partial walkers each fell into
3848/// (block statements and match-arm bodies were skipped, so e.g. the `:=`
3849/// self-reference rule was bypassable through a match arm).
3850///
3851/// Exhaustive over [`ExprKind`] *and* over the expressions each variant holds
3852/// (#1760: match-arm guards were once missed, and every walk built on this one
3853/// missed them with it).
3854///
3855/// Descends one level: block *statements* and the tail, match-arm guards and
3856/// bodies, lambda bodies, interpolation holes, record-field values, and
3857/// observation predicates are all children. Callers recurse for a deep walk.
3858pub fn expr_children(e: &Expr) -> Vec<&Expr> {
3859 fn block_children<'a>(b: &'a Block, out: &mut Vec<&'a Expr>) {
3860 for s in &b.statements {
3861 statement_exprs(s, out);
3862 }
3863 out.push(&b.tail);
3864 }
3865 let mut out = Vec::new();
3866 match &e.kind {
3867 ExprKind::IntLit { .. }
3868 | ExprKind::FloatLit { .. }
3869 | ExprKind::DecimalLit { .. }
3870 | ExprKind::DurationLit { .. }
3871 | ExprKind::StrLit(_)
3872 | ExprKind::BoolLit(_)
3873 | ExprKind::Ident(_)
3874 | ExprKind::None
3875 | ExprKind::UnitLit
3876 | ExprKind::Trace { .. } => {}
3877 ExprKind::InterpStr(parts) => {
3878 for p in parts {
3879 if let InterpPart::Hole(h) = p {
3880 out.push(h.as_ref());
3881 }
3882 }
3883 }
3884 ExprKind::Call { args, .. } | ExprKind::Val { args, .. } | ExprKind::ListLit(args) => {
3885 out.extend(args.iter())
3886 }
3887 ExprKind::Wire(inner) => out.push(inner.as_ref()),
3888 ExprKind::Lambda(l) => out.push(l.body.as_ref()),
3889 ExprKind::BinOp(_, l, r) => {
3890 out.push(l.as_ref());
3891 out.push(r.as_ref());
3892 }
3893 ExprKind::UnaryOp(_, inner)
3894 | ExprKind::Paren(inner)
3895 | ExprKind::Ok(inner)
3896 | ExprKind::Err(inner)
3897 | ExprKind::Question(inner)
3898 | ExprKind::Some(inner)
3899 | ExprKind::EffectPure(inner)
3900 | ExprKind::Expect(inner) => out.push(inner.as_ref()),
3901 ExprKind::Faults(f) => {
3902 // #1812: a call-site principal's identity is evaluated first, as
3903 // an argument to the claimed call — the same order as an
3904 // effect-let's in `statement_exprs`.
3905 if let Some(identity) = f.principal.as_ref().and_then(|p| p.identity.as_deref()) {
3906 out.push(identity);
3907 }
3908 if let Some(claims) = f.principal.as_ref().and_then(|p| p.claims.as_deref()) {
3909 out.push(claims);
3910 }
3911 out.push(&f.call);
3912 }
3913 ExprKind::Block(b) => block_children(b, &mut out),
3914 ExprKind::If {
3915 cond,
3916 then_block,
3917 else_block,
3918 } => {
3919 out.push(cond.as_ref());
3920 block_children(then_block, &mut out);
3921 block_children(else_block, &mut out);
3922 }
3923 ExprKind::RecordConstruction { fields, .. } => {
3924 out.extend(fields.iter().filter_map(|f| f.value.as_ref()));
3925 }
3926 ExprKind::FieldAccess { receiver, .. } => out.push(receiver.as_ref()),
3927 ExprKind::MethodCall { receiver, args, .. } => {
3928 out.push(receiver.as_ref());
3929 out.extend(args.iter());
3930 }
3931 ExprKind::Match { discriminant, arms } => {
3932 out.push(discriminant.as_ref());
3933 for arm in arms {
3934 // #1760: the guard, in evaluation order before the body. It is
3935 // an ordinary expression, checked like any other.
3936 if let Some(guard) = &arm.guard {
3937 out.push(guard);
3938 }
3939 match &arm.body {
3940 MatchBody::Expr(e) => out.push(e),
3941 MatchBody::Block(b) => block_children(b, &mut out),
3942 }
3943 }
3944 }
3945 ExprKind::Is { value, .. } => out.push(value.as_ref()),
3946 ExprKind::RecordSpread {
3947 base, overrides, ..
3948 } => {
3949 out.push(base.as_ref());
3950 out.extend(overrides.iter().filter_map(|f| f.value.as_ref()));
3951 }
3952 ExprKind::Observation(obs) => match &obs.matcher {
3953 ObservationMatcher::Called { count, with_pred } => {
3954 if let Some(c) = count {
3955 out.push(c.as_ref());
3956 }
3957 if let Some(p) = with_pred {
3958 out.push(p.as_ref());
3959 }
3960 }
3961 ObservationMatcher::NeverCalled | ObservationMatcher::Before { .. } => {}
3962 },
3963 }
3964 out
3965}
3966
3967/// The expressions directly contained in a statement — the statement half of
3968/// [`expr_children`]'s total walk. Exhaustive over [`Statement`] for the same
3969/// reason, and over each statement's expressions: a `let`'s call-site
3970/// principal identity included.
3971pub fn statement_exprs<'a>(s: &'a Statement, out: &mut Vec<&'a Expr>) {
3972 match s {
3973 Statement::Let(l) | Statement::EffectLet(l) => {
3974 // #1766 review: a call-site principal's identity (`by User(who)`)
3975 // is a full expression. It is evaluated first, as an argument to
3976 // the addressed call.
3977 if let Some(identity) = l.principal.as_ref().and_then(|p| p.identity.as_deref()) {
3978 out.push(identity);
3979 }
3980 if let Some(claims) = l.principal.as_ref().and_then(|p| p.claims.as_deref()) {
3981 out.push(claims);
3982 }
3983 out.push(&l.value)
3984 }
3985 Statement::Expect(a) => out.push(&a.value),
3986 Statement::Send(snd) => out.push(&snd.value),
3987 Statement::Do(d) => out.push(&d.value),
3988 Statement::Assign(a) => out.push(&a.value),
3989 Statement::Advance(a) => out.push(&a.amount),
3990 Statement::Par(p) => p.statements.iter().for_each(|s| statement_exprs(s, out)),
3991 }
3992}
3993
3994/// A fault claim, `<call> [by <Actor>(<identity>)] faults` (#1706, #1812).
3995/// `call` is the effectful call the claim awaits; `principal` is the optional
3996/// call-site `by` clause, read exactly as on an effect-let (`let r <- call by
3997/// User("bob")`): it supplies the identity the addressed handler reads.
3998#[derive(Debug, Clone)]
3999pub struct FaultsExpr {
4000 pub call: Expr,
4001 pub principal: Option<CallSiteActor>,
4002}
4003
4004/// An observation of a capability operation's recorded calls (v0.117, testing
4005/// track slice 5). `cap`/`op` name the seam (`Logger.log`); `matcher` is the
4006/// claim about the recorded calls.
4007#[derive(Debug, Clone)]
4008pub struct ObservationExpr {
4009 pub cap: Ident,
4010 pub op: Ident,
4011 pub matcher: ObservationMatcher,
4012}
4013
4014/// The claim an [`ObservationExpr`] makes about a seam's recorded calls (v0.117).
4015#[derive(Debug, Clone)]
4016pub enum ObservationMatcher {
4017 /// `called` [`once` | `<n> times`]? [`with` `<pred>`]?. `count` is `None`
4018 /// for a bare `called` (at least one); `Some(expr)` is the exact-count claim
4019 /// (a literal; `once` desugars to `1`). `with_pred` matches a call whose
4020 /// arguments (in scope by the operation's parameter names) satisfy it.
4021 Called {
4022 count: Option<Box<Expr>>,
4023 with_pred: Option<Box<Expr>>,
4024 },
4025 /// `never called` — zero calls.
4026 NeverCalled,
4027 /// `before Cap.op` — the first call of the subject precedes the first call
4028 /// of the named operation (both must have occurred).
4029 Before { cap: Ident, op: Ident },
4030}
4031
4032/// One part of an interpolated string (v0.43, ADR 0075). An
4033/// [`ExprKind::InterpStr`] holds an alternating run of these.
4034#[derive(Debug, Clone)]
4035pub enum InterpPart {
4036 /// Literal text between holes, with escapes already resolved.
4037 Chunk(String),
4038 /// An interpolated expression `\(expr)`. Type-checked by the hole rule
4039 /// (base scalars only; see the checker) and lowered into a template-
4040 /// literal `${…}` slot.
4041 Hole(Box<Expr>),
4042}
4043
4044/// One field-initialiser inside a record construction expression:
4045/// either `name: expr` or the shorthand `name` (which requires a binding
4046/// of the same name in scope and uses its value).
4047#[derive(Debug, Clone)]
4048pub struct FieldInit {
4049 pub name: Ident,
4050 /// `None` means shorthand — the field's value is the same-named binding.
4051 pub value: Option<Expr>,
4052 pub span: Span,
4053}
4054
4055/// One arm of a `match` expression: `pattern => body` or, with a guard,
4056/// `pattern if guard => body` (guard added in the nested-patterns increment,
4057/// ADR 0169). A guarded arm matches only when the pattern matches **and** the
4058/// `Bool` guard evaluates true; it never contributes to exhaustiveness.
4059#[derive(Debug, Clone)]
4060pub struct MatchArm {
4061 pub pattern: Pattern,
4062 /// Optional `if <Bool-expr>` guard between the pattern and `=>`.
4063 pub guard: Option<Expr>,
4064 pub body: MatchBody,
4065 pub span: Span,
4066}
4067
4068/// The right-hand side of a match arm — either a single expression or
4069/// a block.
4070#[derive(Debug, Clone)]
4071pub enum MatchBody {
4072 Expr(Expr),
4073 Block(Block),
4074}
4075
4076impl MatchBody {
4077 pub fn span(&self) -> Span {
4078 match self {
4079 MatchBody::Expr(e) => e.span,
4080 MatchBody::Block(b) => b.span,
4081 }
4082 }
4083}
4084
4085/// A pattern (v0.2 §3.8). Patterns appear in `match` arms and as the
4086/// right-hand side of the `is` operator.
4087#[derive(Debug, Clone)]
4088pub enum Pattern {
4089 /// `_` — matches any value, no bindings.
4090 Wildcard(Span),
4091 /// A lowercase identifier — binds the whole value to `name` and matches
4092 /// anything (ADR 0169). At the top of a `match` arm it binds the scrutinee
4093 /// (`n if n > 0 => …`); inside a payload position it binds the field
4094 /// (`Some(user)`). The uppercase-led counterpart is a nullary [`Pattern::Variant`].
4095 Binding(Ident),
4096 /// A literal pattern — `31`, `"english"`, `true` (v0.130 §2.3.4). Matches a
4097 /// primitive scrutinee (`Int`/`String`/`Bool`) by value equality. The
4098 /// admitted set mirrors ADR 0001's closed literal set (integers — including
4099 /// a leading unary minus — strings, and booleans); `Float`/`()` are not
4100 /// admitted as patterns.
4101 Literal {
4102 value: LiteralValue,
4103 /// The literal as written (`-1_000`), digit separators kept; `None`
4104 /// for a pattern built without source. Only the formatter reads it,
4105 /// and it sits outside `value` so the duplicate-arm check ignores it.
4106 text: Option<String>,
4107 span: Span,
4108 },
4109 /// `lo..hi`, `lo..` or `..hi` — an inclusive integer range (patterns track,
4110 /// slice 5, #2699). Matches an `Int` scrutinee by comparison. Binds nothing.
4111 /// The one production `via schema(...)` shares ([`SchemaVersionPattern::Range`]).
4112 Range(IntRange),
4113 /// `Variant` or `Variant(bindings)` or `TypeName.Variant(bindings)`. Each
4114 /// payload binding is itself a [`Pattern`] (ADR 0169), so payloads nest:
4115 /// `Some(Ok(x))`, `Err(PollClosed)`.
4116 Variant {
4117 /// Optional qualifier: `TypeName.Variant`.
4118 type_name: Option<Ident>,
4119 /// The variant name.
4120 variant: Ident,
4121 /// Payload bindings (empty for nullary variants).
4122 bindings: Vec<PatternBinding>,
4123 span: Span,
4124 },
4125 /// `TypeName { f1, f2: pattern, .. }` — a record pattern (patterns track,
4126 /// slice 1, #2641). Always open: unlisted fields are ignored, and the
4127 /// trailing `..` is mandatory so that adding a field to the record never
4128 /// breaks a pattern. A field with no sub-pattern (`f`) binds the field to a
4129 /// local of the same name; `f: p` matches the field against `p`.
4130 Record {
4131 /// The record type's name — required, and never carries type
4132 /// arguments (a generic record's come from the scrutinee).
4133 type_name: Ident,
4134 /// The listed fields, in source order. `None` is the shorthand `f`.
4135 fields: Vec<(Ident, Option<Pattern>)>,
4136 /// The span of the mandatory trailing `..`.
4137 rest_span: Span,
4138 span: Span,
4139 },
4140 /// `[p₁, …, pₙ]` or `[p₁, …, pₙ, ..]` / `[p₁, …, pₙ, ..rest]` — a list
4141 /// pattern (patterns track, slice 4, #2680). Without `rest` it matches a
4142 /// list of exactly `n` elements, element `i` against `pᵢ`; with `rest` it
4143 /// matches `n` or more, and a named rest binds the remaining elements.
4144 List {
4145 elems: Vec<Pattern>,
4146 rest: Option<ListRest>,
4147 span: Span,
4148 },
4149 /// `p 'where' refinement-predicate` — a refinement guard on a pattern
4150 /// (#472). Matches when `inner` matches *and* the scrutinee satisfies
4151 /// `predicate` at runtime. v1 admits only `Wildcard` as `inner` (no
4152 /// binding form yet); refutable — never counts toward exhaustiveness or
4153 /// as a catch-all arm, the same treatment as an `if` guard (§2.3.4).
4154 Refined {
4155 inner: Box<Pattern>,
4156 predicate: Refinement,
4157 span: Span,
4158 },
4159 /// `p₁ | p₂ | … | pₙ` — an or-pattern (#474 §2.3.4): matches if any
4160 /// alternative matches. Left-associative `|`, flattened by the parser's
4161 /// chain fold into one `Vec` — an alternative is always a leaf
4162 /// (`Wildcard`/`Binding`/`Literal`/`Variant`), never itself an `Or`
4163 /// (there is no parenthesized-pattern syntax to nest one inside another).
4164 /// Well-typedness (checked, not parsed): every alternative binds the same
4165 /// set of names, a name shared across alternatives has the same type
4166 /// (including refinement) in each, and every alternative matches the same
4167 /// value type.
4168 Or(Vec<Pattern>, Span),
4169}
4170
4171/// An inclusive integer range, `lo..hi`, `lo..` or `..hi` (patterns track,
4172/// slice 5, #2699; events slice 4b, #990): the one production a match pattern
4173/// and a `via schema(...)` clause share. At least one bound is present — a bare
4174/// `..` is the rest marker, never a range. Each bound is an optionally negative
4175/// `Int` literal, parsed permissively: whether it is in range and whether
4176/// `lo <= hi` are checker questions.
4177#[derive(Debug, Clone, PartialEq, Eq)]
4178pub struct IntRange {
4179 pub lo: Option<i64>,
4180 pub hi: Option<i64>,
4181 /// The low bound as written (`-1_000`), digit separators kept; `None` when
4182 /// `lo` is. Only the formatter reads it; a range built without source
4183 /// (a suggested fix) leaves it `None` and prints from the value.
4184 pub lo_text: Option<String>,
4185 /// The high bound as written; see `lo_text`.
4186 pub hi_text: Option<String>,
4187 pub span: Span,
4188}
4189
4190impl IntRange {
4191 /// As [`IntRange::describe`], but each bound keeps its source spelling
4192 /// (`1_000..2_000`), so formatting does not drop digit separators.
4193 pub fn describe_source(&self) -> String {
4194 let lo = match (&self.lo_text, self.lo) {
4195 (Some(t), _) => t.clone(),
4196 (None, Some(n)) => n.to_string(),
4197 (None, None) => String::new(),
4198 };
4199 let hi = match (&self.hi_text, self.hi) {
4200 (Some(t), _) => t.clone(),
4201 (None, Some(n)) => n.to_string(),
4202 (None, None) => String::new(),
4203 };
4204 format!("{lo}..{hi}")
4205 }
4206
4207 /// The range as written, `0..9`, `18..` or `..-1`; the form the formatter
4208 /// prints and diagnostics quote.
4209 pub fn describe(&self) -> String {
4210 let lo = self.lo.map(|n| n.to_string()).unwrap_or_default();
4211 let hi = self.hi.map(|n| n.to_string()).unwrap_or_default();
4212 format!("{lo}..{hi}")
4213 }
4214}
4215
4216/// The trailing `..` of a list pattern, with the optional name that binds the
4217/// remaining elements (`..rest`).
4218#[derive(Debug, Clone)]
4219pub struct ListRest {
4220 pub binder: Option<Ident>,
4221 /// The span of the `..` token.
4222 pub span: Span,
4223}
4224
4225/// The value carried by a [`Pattern::Literal`]. A closed set (ADR 0001):
4226/// integer, string, and boolean. Kept distinct from [`ExprKind`] so patterns
4227/// carry only what they can actually match, and so it is `Eq`/`Hash` for the
4228/// duplicate-arm check.
4229#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4230pub enum LiteralValue {
4231 Int(i64),
4232 Str(String),
4233 Bool(bool),
4234}
4235
4236impl LiteralValue {
4237 /// A human-readable rendering for diagnostics (`31`, `"english"`, `true`).
4238 pub fn describe(&self) -> String {
4239 match self {
4240 LiteralValue::Int(n) => n.to_string(),
4241 LiteralValue::Str(s) => format!("{s:?}"),
4242 LiteralValue::Bool(b) => b.to_string(),
4243 }
4244 }
4245}
4246
4247impl Pattern {
4248 pub fn span(&self) -> Span {
4249 match self {
4250 Pattern::Wildcard(s) => *s,
4251 Pattern::Binding(id) => id.span,
4252 Pattern::Literal { span, .. } => *span,
4253 Pattern::Range(r) => r.span,
4254 Pattern::Variant { span, .. } => *span,
4255 Pattern::Record { span, .. } => *span,
4256 Pattern::List { span, .. } => *span,
4257 Pattern::Refined { span, .. } => *span,
4258 Pattern::Or(_, span) => *span,
4259 }
4260 }
4261
4262 /// Every identifier this pattern binds into scope, recursively (`_` and
4263 /// nullary variants bind nothing). Used by the resolver and the checker to
4264 /// populate an arm's scope, and by the guard to see the arm's bindings.
4265 ///
4266 /// For [`Pattern::Or`] this returns the *first* alternative's names — the
4267 /// checker separately verifies (#474 Rule 1) that every alternative binds
4268 /// the same set, so this is a defensive default when that rule is
4269 /// violated, not a semantic choice among alternatives.
4270 pub fn bound_names(&self) -> Vec<&Ident> {
4271 match self {
4272 Pattern::Wildcard(_) | Pattern::Literal { .. } | Pattern::Range(_) => Vec::new(),
4273 Pattern::Binding(id) => vec![id],
4274 Pattern::Variant { bindings, .. } => bindings
4275 .iter()
4276 .flat_map(|b| b.pattern().bound_names())
4277 .collect(),
4278 Pattern::Record { fields, .. } => fields
4279 .iter()
4280 .flat_map(|(name, sub)| match sub {
4281 Some(p) => p.bound_names(),
4282 None => vec![name],
4283 })
4284 .collect(),
4285 Pattern::List { elems, rest, .. } => elems
4286 .iter()
4287 .flat_map(Pattern::bound_names)
4288 .chain(rest.iter().filter_map(|r| r.binder.as_ref()))
4289 .collect(),
4290 Pattern::Refined { inner, .. } => inner.bound_names(),
4291 Pattern::Or(alts, _) => alts.first().map(Pattern::bound_names).unwrap_or_default(),
4292 }
4293 }
4294
4295 /// True when this pattern matches every value and binds nothing — a bare
4296 /// `_`. A [`Pattern::Binding`] also matches everything but *does* bind, so it
4297 /// is not a pure wildcard.
4298 pub fn is_wildcard(&self) -> bool {
4299 matches!(self, Pattern::Wildcard(_))
4300 }
4301
4302 /// True when this pattern matches every value (a `_` or a name binding),
4303 /// i.e. it is irrefutable and covers the position for exhaustiveness. An
4304 /// [`Pattern::Or`] is irrefutable when any alternative is — `_` in any
4305 /// position already makes the whole pattern match everything.
4306 pub fn is_irrefutable(&self) -> bool {
4307 match self {
4308 Pattern::Wildcard(_) | Pattern::Binding(_) => true,
4309 Pattern::Or(alts, _) => alts.iter().any(Pattern::is_irrefutable),
4310 Pattern::Record { fields, .. } => fields
4311 .iter()
4312 .all(|(_, sub)| sub.as_ref().is_none_or(Pattern::is_irrefutable)),
4313 // `[..]` and `[..rest]` match every list; any fixed length does not.
4314 Pattern::List { elems, rest, .. } => elems.is_empty() && rest.is_some(),
4315 _ => false,
4316 }
4317 }
4318}
4319
4320/// A single binding inside a variant pattern. Two surface forms:
4321/// `pattern` (positional — match the i-th payload field) and
4322/// `fieldName: pattern` (named — match the named payload field). The matched
4323/// sub-`pattern` is a full [`Pattern`] (ADR 0169), so a plain `name` is a
4324/// [`Pattern::Binding`], `_` a [`Pattern::Wildcard`], and `Ok(x)` a nested
4325/// [`Pattern::Variant`].
4326#[derive(Debug, Clone)]
4327pub struct PatternBinding {
4328 /// Source form: positional or named.
4329 pub kind: PatternBindingKind,
4330 pub span: Span,
4331}
4332
4333#[derive(Debug, Clone)]
4334pub enum PatternBindingKind {
4335 /// `pattern` (e.g. `x`, `_`, `Ok(v)`): match the payload field at this position.
4336 Positional { pattern: Pattern },
4337 /// `field: pattern`: match the named payload field against `pattern`.
4338 Named { field: Ident, pattern: Pattern },
4339}
4340
4341impl PatternBinding {
4342 /// The sub-pattern this binding matches its payload field against.
4343 pub fn pattern(&self) -> &Pattern {
4344 match &self.kind {
4345 PatternBindingKind::Positional { pattern } => pattern,
4346 PatternBindingKind::Named { pattern, .. } => pattern,
4347 }
4348 }
4349
4350 /// True when this binding discards its field (`_` or `field: _`) — a pure
4351 /// wildcard sub-pattern that binds nothing.
4352 pub fn is_wildcard(&self) -> bool {
4353 self.pattern().is_wildcard()
4354 }
4355}
4356
4357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4358pub enum BinOp {
4359 /// `P implies Q` — logical implication (v0.80). Desugars to `!P || Q`; sits
4360 /// at the lowest precedence (below `||`). Reads directionally (P → Q).
4361 Implies,
4362 Or,
4363 And,
4364 Eq,
4365 NotEq,
4366 Lt,
4367 LtEq,
4368 Gt,
4369 GtEq,
4370 Add,
4371 Sub,
4372 Mul,
4373 Div,
4374}
4375
4376impl BinOp {
4377 pub fn name(self) -> &'static str {
4378 match self {
4379 BinOp::Implies => "implies",
4380 BinOp::Or => "||",
4381 BinOp::And => "&&",
4382 BinOp::Eq => "==",
4383 BinOp::NotEq => "!=",
4384 BinOp::Lt => "<",
4385 BinOp::LtEq => "<=",
4386 BinOp::Gt => ">",
4387 BinOp::GtEq => ">=",
4388 BinOp::Add => "+",
4389 BinOp::Sub => "-",
4390 BinOp::Mul => "*",
4391 BinOp::Div => "/",
4392 }
4393 }
4394}
4395
4396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4397pub enum UnaryOp {
4398 Neg,
4399 Not,
4400}
4401
4402impl UnaryOp {
4403 pub fn name(self) -> &'static str {
4404 match self {
4405 UnaryOp::Neg => "-",
4406 UnaryOp::Not => "!",
4407 }
4408 }
4409}
4410
4411#[cfg(test)]
4412mod size_tests {
4413 use super::*;
4414
4415 /// Finding #31: boxing `ExprKind::Observation`'s payload and
4416 /// `ExprKind::Is`'s pattern field took `Expr` from 176 to 128 bytes on
4417 /// this target (the module-level `const _` assertion is the real pin;
4418 /// this test just makes the before/after concrete and fails loudly if a
4419 /// future change silently regresses the win rather than tripping the
4420 /// `<= 128` ceiling by enough to notice).
4421 #[test]
4422 fn expr_is_smaller_than_before_the_boxing() {
4423 assert!(
4424 std::mem::size_of::<Expr>() < 176,
4425 "Expr should be smaller than its pre-#31 size of 176 bytes"
4426 );
4427 }
4428}
4429
4430#[cfg(test)]
4431mod expr_children_tests {
4432 use super::*;
4433
4434 /// #1760: a match arm's guard is a child, between the discriminant and
4435 /// the arm's body, in evaluation order.
4436 #[test]
4437 fn a_match_arms_guard_is_a_child_in_evaluation_order() {
4438 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";
4439 let tokens = crate::lexer::tokenize(source).unwrap();
4440 let units = crate::parser::parse_units(&tokens, source).unwrap();
4441 let SourceUnit::Commons(c) = &units[0] else {
4442 panic!("a commons");
4443 };
4444 let CommonsItem::Fn(f) = &c.items[0] else {
4445 panic!("a fn");
4446 };
4447 let ExprKind::Match { .. } = &f.body.tail.kind else {
4448 panic!("a match tail");
4449 };
4450 let children: Vec<&str> = expr_children(&f.body.tail)
4451 .into_iter()
4452 .map(|e| &source[e.span.start..e.span.end])
4453 .collect();
4454 assert_eq!(children, ["n", "k > lim", "1", "0"]);
4455 }
4456
4457 /// #1766 review: a `let`'s call-site principal identity is a statement
4458 /// expression, before the value it addresses.
4459 #[test]
4460 fn a_principal_identity_is_a_statement_expression() {
4461 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";
4462 let tokens = crate::lexer::tokenize(source).unwrap();
4463 let units = crate::parser::parse_units(&tokens, source).unwrap();
4464 let SourceUnit::Suite(t) = &units[0] else {
4465 panic!("a suite");
4466 };
4467 let mut exprs = Vec::new();
4468 statement_exprs(&t.cases[0].body.statements[1], &mut exprs);
4469 let texts: Vec<&str> = exprs
4470 .into_iter()
4471 .map(|e| &source[e.span.start..e.span.end])
4472 .collect();
4473 assert_eq!(texts, ["who", "api.get()"]);
4474 }
4475}
4476
4477/// #1832: [`expr_children`] and [`statement_exprs`] reach every child slot of
4478/// every expression, so every walk built on them does too.
4479///
4480/// The guard is in two parts:
4481///
4482/// - **An oracle that cannot fall behind the AST.** [`oracle_children`] and
4483/// [`oracle_statement`] destructure every field of every `ExprKind` variant
4484/// and of every expression-bearing struct with no `..`, so adding a variant
4485/// or a field anywhere an expression can hide is a compile error here until
4486/// the oracle names the new slot.
4487/// - **A program that uses every slot.** [`COVERAGE`] is parsed (never
4488/// checked, so it only has to be syntactically valid), and two tests run
4489/// over it: every slot the oracle knows is used at least once, and at every
4490/// node `expr_children`/`statement_exprs` return exactly the oracle's
4491/// children. A new slot therefore fails the first test until the program
4492/// uses it, and the second until `expr_children` walks it.
4493///
4494/// The hand-rolled walks that cannot be built on `expr_children` (they carry
4495/// scopes, ownership or position) are tested where they live (#1832's
4496/// triage: the resolver, structure, linearity, effects and emit walks).
4497#[cfg(test)]
4498mod slot_coverage_tests {
4499 use super::*;
4500 use std::collections::BTreeSet;
4501
4502 /// Every slot, used at least once. `p` is a commons for the production
4503 /// forms; the suite holds the test-only ones (`Val`, `Wire`,
4504 /// observations, `faults`, `trace`, call-site principals).
4505 const COVERAGE: &str = r#"commons p
4506
4507fn f(x: Int) -> Int {
4508 let a = "s \(x) t"
4509 let b = g(x)
4510 let c = (y) => y
4511 let d = x + 1
4512 let e = -x
4513 let h = (x)
4514 let i = (z) => {
4515 let q = z
4516 q
4517 }
4518 let j = if x > 0 {
4519 let q = 1
4520 q
4521 } else {
4522 let w = 2
4523 w
4524 }
4525 let k = Ok(x)
4526 let l = Err(x)
4527 let n = r?
4528 let o = Some(x)
4529 let u = R { a: x }
4530 let v = r.a
4531 let w = r.m(x)
4532 let y = match x {
4533 z if z > 0 => z
4534 0 => expect x
4535 _ => {
4536 let s = 1
4537 s
4538 }
4539 }
4540 let aa = x is Some(v)
4541 let bb = R { ...r, a: x }
4542 let cc = Effect.pure(x)
4543 let dd = [x]
4544 ~> L.log(x)
4545 do s.put(x)
4546 cell := x
4547 x
4548}
4549"#;
4550
4551 const COVERAGE_SUITE: &str = r#"suite p {
4552 case "c" {
4553 let who = "alice"
4554 let a <- api.get(x) by User(who)
4555 let b = Val[T](x)
4556 let c = Wire(x)
4557 expect L.log called 2 times with msg == x
4558 expect box.call() faults
4559 expect api.post(x) by User(who) faults
4560 let t = trace(L.log)
4561 expect x
4562 }
4563}
4564"#;
4565
4566 type Slots<'a> = Vec<(&'static str, &'a Expr)>;
4567
4568 fn oracle_block<'a>(owner: &'static str, b: &'a Block, out: &mut Slots<'a>) {
4569 let Block {
4570 statements,
4571 tail,
4572 span: _,
4573 tail_leading_comments: _,
4574 implicit_tail: _,
4575 } = b;
4576 for s in statements {
4577 for (slot, e) in oracle_statement(s) {
4578 out.push((slot, e));
4579 }
4580 }
4581 out.push((owner, tail));
4582 }
4583
4584 /// A statement's expressions, each tagged with its slot.
4585 fn oracle_statement(s: &Statement) -> Slots<'_> {
4586 fn let_slots<'a>(kind: &'static str, l: &'a LetStmt, out: &mut Slots<'a>) {
4587 let LetStmt {
4588 name: _,
4589 type_annot: _,
4590 value,
4591 principal,
4592 let_else: _,
4593 span: _,
4594 trivia: _,
4595 } = l;
4596 if let Some(CallSiteActor {
4597 actor: _,
4598 identity,
4599 claims: _,
4600 span: _,
4601 }) = principal
4602 && let Some(identity) = identity
4603 {
4604 out.push((
4605 if kind == "let" {
4606 "let.identity"
4607 } else {
4608 "let<-.identity"
4609 },
4610 identity,
4611 ));
4612 }
4613 out.push((
4614 if kind == "let" {
4615 "let.value"
4616 } else {
4617 "let<-.value"
4618 },
4619 value,
4620 ));
4621 }
4622 let mut out = Vec::new();
4623 match s {
4624 Statement::Let(l) => let_slots("let", l, &mut out),
4625 Statement::EffectLet(l) => let_slots("let<-", l, &mut out),
4626 Statement::Expect(ExpectStmt {
4627 value,
4628 snapshot: _,
4629 span: _,
4630 trivia: _,
4631 }) => out.push(("expect.value", value)),
4632 Statement::Send(SendStmt {
4633 value,
4634 span: _,
4635 trivia: _,
4636 }) => out.push(("send.value", value)),
4637 Statement::Do(DoStmt {
4638 value,
4639 span: _,
4640 trivia: _,
4641 }) => out.push(("do.value", value)),
4642 Statement::Assign(AssignStmt {
4643 target: _,
4644 value,
4645 span: _,
4646 trivia: _,
4647 }) => out.push(("assign.value", value)),
4648 Statement::Advance(AdvanceStmt {
4649 keyword: _,
4650 cap: _,
4651 amount,
4652 span: _,
4653 trivia: _,
4654 }) => out.push(("advance.amount", amount)),
4655 Statement::Par(ParStmt {
4656 keyword: _,
4657 statements,
4658 closing_comments: _,
4659 span: _,
4660 trivia: _,
4661 }) => {
4662 for s in statements {
4663 for (slot, e) in oracle_statement(s) {
4664 out.push((slot, e));
4665 }
4666 }
4667 }
4668 }
4669 out
4670 }
4671
4672 /// An expression's children, each tagged with its slot.
4673 fn oracle_children(e: &Expr) -> Slots<'_> {
4674 let Expr {
4675 id: _,
4676 kind,
4677 span: _,
4678 } = e;
4679 let mut out = Vec::new();
4680 match kind {
4681 ExprKind::IntLit {
4682 value: _,
4683 lexeme: _,
4684 }
4685 | ExprKind::FloatLit {
4686 value: _,
4687 lexeme: _,
4688 }
4689 | ExprKind::DecimalLit {
4690 coef: _,
4691 scale: _,
4692 lexeme: _,
4693 }
4694 | ExprKind::DurationLit {
4695 value: _,
4696 unit: _,
4697 millis: _,
4698 }
4699 | ExprKind::StrLit(_)
4700 | ExprKind::BoolLit(_)
4701 | ExprKind::Ident(_)
4702 | ExprKind::None
4703 | ExprKind::UnitLit
4704 | ExprKind::Trace { cap: _, op: _ } => {}
4705 ExprKind::InterpStr(parts) => {
4706 for p in parts {
4707 match p {
4708 InterpPart::Chunk(_) => {}
4709 InterpPart::Hole(h) => out.push(("interp.hole", h.as_ref())),
4710 }
4711 }
4712 }
4713 ExprKind::Call {
4714 name: _,
4715 type_args: _,
4716 args,
4717 } => args.iter().for_each(|a| out.push(("call.arg", a))),
4718 ExprKind::Lambda(LambdaExpr {
4719 params: _,
4720 body,
4721 span: _,
4722 block_body: _,
4723 }) => out.push(("lambda.body", body)),
4724 ExprKind::BinOp(_, l, r) => {
4725 out.push(("binop.left", l));
4726 out.push(("binop.right", r));
4727 }
4728 ExprKind::UnaryOp(_, x) => out.push(("unary.operand", x)),
4729 ExprKind::Paren(x) => out.push(("paren.inner", x)),
4730 ExprKind::Block(b) => oracle_block("block.tail", b, &mut out),
4731 ExprKind::If {
4732 cond,
4733 then_block,
4734 else_block,
4735 } => {
4736 out.push(("if.cond", cond));
4737 oracle_block("if.then.tail", then_block, &mut out);
4738 oracle_block("if.else.tail", else_block, &mut out);
4739 }
4740 ExprKind::Ok(x) => out.push(("ok.inner", x)),
4741 ExprKind::Err(x) => out.push(("err.inner", x)),
4742 ExprKind::Question(x) => out.push(("question.inner", x)),
4743 ExprKind::RecordConstruction {
4744 type_name: _,
4745 fields,
4746 } => {
4747 for FieldInit {
4748 name: _,
4749 value,
4750 span: _,
4751 } in fields
4752 {
4753 if let Some(v) = value {
4754 out.push(("record.field", v));
4755 }
4756 }
4757 }
4758 ExprKind::FieldAccess { receiver, field: _ } => out.push(("field.receiver", receiver)),
4759 ExprKind::MethodCall {
4760 receiver,
4761 method: _,
4762 type_args: _,
4763 args,
4764 } => {
4765 out.push(("method.receiver", receiver));
4766 args.iter().for_each(|a| out.push(("method.arg", a)));
4767 }
4768 ExprKind::Match { discriminant, arms } => {
4769 out.push(("match.discriminant", discriminant));
4770 for MatchArm {
4771 pattern: _,
4772 guard,
4773 body,
4774 span: _,
4775 } in arms
4776 {
4777 if let Some(g) = guard {
4778 out.push(("match.guard", g));
4779 }
4780 match body {
4781 MatchBody::Expr(x) => out.push(("match.body", x)),
4782 MatchBody::Block(b) => oracle_block("match.block.tail", b, &mut out),
4783 }
4784 }
4785 }
4786 ExprKind::Is { value, pattern: _ } => out.push(("is.value", value)),
4787 ExprKind::Some(x) => out.push(("some.inner", x)),
4788 ExprKind::RecordSpread {
4789 type_name: _,
4790 base,
4791 overrides,
4792 } => {
4793 out.push(("spread.base", base));
4794 for FieldInit {
4795 name: _,
4796 value,
4797 span: _,
4798 } in overrides
4799 {
4800 if let Some(v) = value {
4801 out.push(("spread.override", v));
4802 }
4803 }
4804 }
4805 ExprKind::EffectPure(x) => out.push(("effect_pure.inner", x)),
4806 ExprKind::Expect(x) => out.push(("expect_expr.inner", x)),
4807 ExprKind::Val { type_ref: _, args } => {
4808 args.iter().for_each(|a| out.push(("val.arg", a)))
4809 }
4810 ExprKind::Wire(x) => out.push(("wire.inner", x)),
4811 ExprKind::ListLit(items) => items.iter().for_each(|i| out.push(("list.item", i))),
4812 ExprKind::Observation(obs) => {
4813 let ObservationExpr {
4814 cap: _,
4815 op: _,
4816 matcher,
4817 } = obs.as_ref();
4818 match matcher {
4819 ObservationMatcher::Called { count, with_pred } => {
4820 if let Some(c) = count {
4821 out.push(("observation.count", c));
4822 }
4823 if let Some(p) = with_pred {
4824 out.push(("observation.with", p));
4825 }
4826 }
4827 ObservationMatcher::NeverCalled
4828 | ObservationMatcher::Before { cap: _, op: _ } => {}
4829 }
4830 }
4831 ExprKind::Faults(f) => {
4832 let FaultsExpr { call, principal } = f.as_ref();
4833 if let Some(CallSiteActor {
4834 actor: _,
4835 identity,
4836 claims: _,
4837 span: _,
4838 }) = principal
4839 && let Some(identity) = identity
4840 {
4841 out.push(("faults.identity", identity));
4842 }
4843 out.push(("faults.inner", call));
4844 }
4845 }
4846 out
4847 }
4848
4849 /// Every slot [`oracle_children`] and [`oracle_statement`] can name.
4850 const ALL_SLOTS: &[&str] = &[
4851 "interp.hole",
4852 "call.arg",
4853 "lambda.body",
4854 "binop.left",
4855 "binop.right",
4856 "unary.operand",
4857 "paren.inner",
4858 "block.tail",
4859 "if.cond",
4860 "if.then.tail",
4861 "if.else.tail",
4862 "ok.inner",
4863 "err.inner",
4864 "question.inner",
4865 "record.field",
4866 "field.receiver",
4867 "method.receiver",
4868 "method.arg",
4869 "match.discriminant",
4870 "match.guard",
4871 "match.body",
4872 "match.block.tail",
4873 "is.value",
4874 "some.inner",
4875 "spread.base",
4876 "spread.override",
4877 "effect_pure.inner",
4878 "expect_expr.inner",
4879 "val.arg",
4880 "wire.inner",
4881 "list.item",
4882 "observation.count",
4883 "observation.with",
4884 "faults.identity",
4885 "faults.inner",
4886 "let.value",
4887 "let<-.value",
4888 "let<-.identity",
4889 "expect.value",
4890 "send.value",
4891 "do.value",
4892 "assign.value",
4893 ];
4894
4895 /// Every top-level block of the coverage program: fn bodies and case
4896 /// bodies.
4897 fn bodies(units: &[SourceUnit]) -> Vec<&Block> {
4898 let mut out = Vec::new();
4899 for u in units {
4900 match u {
4901 SourceUnit::Commons(c) => {
4902 for item in &c.items {
4903 if let CommonsItem::Fn(f) = item {
4904 out.push(&f.body);
4905 }
4906 }
4907 }
4908 SourceUnit::Suite(t) => out.extend(t.cases.iter().map(|c| &c.body)),
4909 _ => {}
4910 }
4911 }
4912 out
4913 }
4914
4915 fn parse(source: &str) -> Vec<SourceUnit> {
4916 let tokens = crate::lexer::tokenize(source).expect("lex");
4917 crate::parser::parse_units(&tokens, source).expect("the coverage program parses")
4918 }
4919
4920 /// Visit every expression under `b` through the oracle, depth first.
4921 fn visit<'a>(b: &'a Block, f: &mut impl FnMut(&'a Expr)) {
4922 fn go<'a>(e: &'a Expr, f: &mut impl FnMut(&'a Expr)) {
4923 f(e);
4924 for (_, c) in oracle_children(e) {
4925 go(c, f);
4926 }
4927 }
4928 let mut top = Vec::new();
4929 oracle_block("body.tail", b, &mut top);
4930 for (_, e) in top {
4931 go(e, f);
4932 }
4933 }
4934
4935 #[test]
4936 fn the_coverage_program_uses_every_slot() {
4937 let mut used = BTreeSet::new();
4938 for source in [COVERAGE, COVERAGE_SUITE] {
4939 let units = parse(source);
4940 for b in bodies(&units) {
4941 for s in &b.statements {
4942 used.extend(oracle_statement(s).into_iter().map(|(slot, _)| slot));
4943 }
4944 visit(b, &mut |e| {
4945 used.extend(oracle_children(e).into_iter().map(|(slot, _)| slot));
4946 if let ExprKind::Block(b) = &e.kind {
4947 for s in &b.statements {
4948 used.extend(oracle_statement(s).into_iter().map(|(slot, _)| slot));
4949 }
4950 }
4951 });
4952 }
4953 }
4954 let missing: Vec<&str> = ALL_SLOTS
4955 .iter()
4956 .copied()
4957 .filter(|s| !used.contains(s))
4958 .collect();
4959 let unknown: Vec<&str> = used
4960 .iter()
4961 .copied()
4962 .filter(|s| !ALL_SLOTS.contains(s))
4963 .collect();
4964 assert!(
4965 missing.is_empty() && unknown.is_empty(),
4966 "slots the coverage program never uses: {missing:?}; slots missing from ALL_SLOTS: {unknown:?}"
4967 );
4968 }
4969
4970 #[test]
4971 fn expr_children_returns_exactly_the_oracle_s_children_at_every_node() {
4972 let key = |e: &Expr| (e.span.start, e.span.end, e.id);
4973 let mut checked = 0;
4974 for source in [COVERAGE, COVERAGE_SUITE] {
4975 let units = parse(source);
4976 for b in bodies(&units) {
4977 for s in &b.statements {
4978 let mut got = Vec::new();
4979 statement_exprs(s, &mut got);
4980 let got: Vec<_> = got.into_iter().map(key).collect();
4981 let want: Vec<_> = oracle_statement(s)
4982 .into_iter()
4983 .map(|(_, e)| key(e))
4984 .collect();
4985 assert_eq!(got, want, "statement_exprs at {:?}", s.span());
4986 }
4987 visit(b, &mut |e| {
4988 let got: Vec<_> = expr_children(e).into_iter().map(key).collect();
4989 let want: Vec<_> = oracle_children(e)
4990 .into_iter()
4991 .map(|(_, c)| key(c))
4992 .collect();
4993 assert_eq!(
4994 got,
4995 want,
4996 "expr_children of `{}`",
4997 &source[e.span.start..e.span.end]
4998 );
4999 checked += 1;
5000 });
5001 }
5002 }
5003 assert!(checked > 50, "only {checked} nodes checked");
5004 }
5005}
5006
5007#[cfg(test)]
5008mod stream_path_tests {
5009 use super::*;
5010
5011 fn types(source: &str) -> std::collections::HashMap<String, std::sync::Arc<TypeDecl>> {
5012 let tokens = crate::lexer::tokenize(source).unwrap();
5013 let units = crate::parser::parse_units(&tokens, source).unwrap();
5014 let SourceUnit::Commons(c) = &units[0] else {
5015 panic!("a commons");
5016 };
5017 c.items
5018 .iter()
5019 .filter_map(|i| match i {
5020 CommonsItem::Type(t) => Some((t.name.name.clone(), std::sync::Arc::new(t.clone()))),
5021 _ => None,
5022 })
5023 .collect()
5024 }
5025
5026 fn named(name: &str) -> TypeRef {
5027 TypeRef::Named(Ident {
5028 name: name.to_string(),
5029 span: Span::default(),
5030 })
5031 }
5032
5033 /// K-streams (#2623): the chain of fields a stream is reached through,
5034 /// across records, sums and a generic record's own fields, terminating on
5035 /// a recursive type.
5036 #[test]
5037 fn the_stream_path_names_each_field_it_passes_through() {
5038 let t = types(
5039 "commons d\n\
5040 type Download = { contentType: String, body: Stream[Bytes] }\n\
5041 type Shelf = { name: String, latest: Option[Download] }\n\
5042 type Pick = | Got(d: Download) | Nothing\n\
5043 type Wrap[T] = { s: Stream[T] }\n\
5044 type Tree = { kids: List[Tree], n: Int }\n\
5045 type Plain = { n: Int }\n",
5046 );
5047 let path = |n: &str| stream_path(&named(n), &t);
5048 assert_eq!(path("Download"), Some(vec!["Download.body".to_string()]));
5049 assert_eq!(
5050 path("Shelf"),
5051 Some(vec![
5052 "Shelf.latest".to_string(),
5053 "Download.body".to_string()
5054 ])
5055 );
5056 assert_eq!(
5057 path("Pick"),
5058 Some(vec!["Pick.Got.d".to_string(), "Download.body".to_string()])
5059 );
5060 assert_eq!(path("Wrap"), Some(vec!["Wrap.s".to_string()]));
5061 assert_eq!(path("Tree"), None);
5062 assert_eq!(path("Plain"), None);
5063 assert!(type_carries_stream("Shelf", &t));
5064 assert!(!type_carries_stream("Tree", &t));
5065 }
5066
5067 /// #2867: the calendar rule at its edges.
5068 #[test]
5069 fn is_iso_date_decides_the_calendar() {
5070 for ok in [
5071 "2026-11-03",
5072 "2028-02-29",
5073 "2000-02-29",
5074 "0000-01-01",
5075 "9999-12-31",
5076 "2026-04-30",
5077 "2026-12-31",
5078 "2400-02-29",
5079 ] {
5080 assert!(is_iso_date(ok), "{ok}");
5081 }
5082 for bad in [
5083 "2026-02-30",
5084 "2026-02-29",
5085 "1900-02-29",
5086 "2100-02-29",
5087 "2026-04-31",
5088 "2026-13-01",
5089 "2026-00-10",
5090 "2026-01-00",
5091 "2026-1-1",
5092 "yesterday",
5093 "",
5094 "2026-11-03 ",
5095 "20261103",
5096 "2026/11/03",
5097 "+2026-11-03",
5098 "2026-11-03T00:00:00Z",
5099 "2026-11-03",
5100 "2026-+1-03",
5101 ] {
5102 assert!(!is_iso_date(bad), "{bad:?}");
5103 }
5104 }
5105}