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