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