Skip to main content

bynk_syntax/
keywords.rs

1//! Registry of reserved keywords.
2//!
3//! Single source of truth for the keyword list in
4//! `site/src/content/docs/book/reference/keywords.md`, generated by
5//! [`render_markdown`]. The test
6//! `tests/keywords_reference.rs` asserts this table matches exactly the
7//! alphabetic `#[token("…")]` keywords declared in `lexer.rs`, so the two
8//! cannot drift.
9
10/// One reserved keyword and a one-line description of its role.
11pub struct KeywordInfo {
12    pub word: &'static str,
13    pub meaning: &'static str,
14}
15
16/// Every reserved keyword, sorted.
17pub const KEYWORDS: &[KeywordInfo] = &[
18    k("Bool", "The boolean base type."),
19    k(
20        "Bytes",
21        "The binary base type — an immutable octet sequence, erased to `Uint8Array` (`Bytes.fromUtf8(s)`).",
22    ),
23    k(
24        "Decimal",
25        "The exact decimal base type — an arbitrary-precision coefficient with a scale (`1.20d`).",
26    ),
27    k(
28        "Duration",
29        "The time-span base type, in milliseconds (`5.minutes`).",
30    ),
31    k("Effect", "The effectful-computation type, `Effect[T]`."),
32    k("Err", "The error variant of `Result`."),
33    k("Float", "The floating-point base type."),
34    k(
35        "Instant",
36        "The absolute-time base type, in epoch milliseconds (`Clock.now()`).",
37    ),
38    k("Int", "The integer base type."),
39    k(
40        "JsonError",
41        "The JSON-decode error type, `Result[T, JsonError]` from `Json.decode`. Its fields are `kind` (`Malformed`, `StructuralMismatch` or `RefinementViolation`), `path` (e.g. `$.qty`) and `message`, all `String`: write `\"\\(e.kind) at \\(e.path): \\(e.message)\"`.",
42    ),
43    k("None", "The empty variant of `Option`."),
44    k("Ok", "The success variant of `Result`."),
45    k("Option", "The optional-value type, `Option[T]`."),
46    k("Result", "The success-or-error type, `Result[T, E]`."),
47    k("Some", "The present variant of `Option`."),
48    k("String", "The string base type."),
49    k(
50        "ValidationError",
51        "The error type returned by a refined type's `.of`. Its fields are `field` and `message`, both `String`: write `\"\\(e.field): \\(e.message)\"`.",
52    ),
53    k(
54        "actor",
55        "Declare an actor — a boundary contract a handler consumes via `by`.",
56    ),
57    k(
58        "adapter",
59        "Declare an adapter — the host boundary (capability contract + binding).",
60    ),
61    k("agent", "Declare a stateful, keyed agent inside a context."),
62    k(
63        "as",
64        "Alias a consumed context (`consumes X as Y`) or a consumed type (`consumes U { T as Y }`).",
65    ),
66    k(
67        "asset",
68        "Declare a static file the platform serves, public by construction (`asset name = \"path\"`).",
69    ),
70    k(
71        "binding",
72        "Name an adapter's TypeScript binding module (`binding \"<module>\"`).",
73    ),
74    k(
75        "by",
76        "Name the actor a handler consumes, after the return type — or a service-level default on the header (`… -> T by <name>: <Actor>`).",
77    ),
78    k(
79        "capability",
80        "Declare a capability (a dependency interface) in a context, or a distinct instance of a platform resource (`capability Sessions = Kv`).",
81    ),
82    k(
83        "case",
84        "Declare a test case inside a `suite` (`case \"…\" { … }`).",
85    ),
86    k(
87        "commons",
88        "Declare a pure, stateless module of types and functions.",
89    ),
90    k(
91        "const",
92        "Declare a named constant — a static value of a declared type, in a context or commons (`const MaxRetries: Int = 3`).",
93    ),
94    k(
95        "consumes",
96        "Declare a dependency on another context's services.",
97    ),
98    k(
99        "context",
100        "Declare a deployable context (services, agents, capabilities).",
101    ),
102    k(
103        "cron",
104        "The cron protocol on a service header (`from cron`).",
105    ),
106    k(
107        "do",
108        "Perform a unit effect as a statement (`do e` — the binder-free `let _ <- e`).",
109    ),
110    k(
111        "else",
112        "The alternative branch of an `if`, or what a block produces when a `let` pattern does not match (`let Ok(x) = r else { … }`).",
113    ),
114    k(
115        "embed",
116        "Declare a project file whose text is a `String` constant, compiled into the code and never served (`embed name = \"path\"`).",
117    ),
118    k(
119        "ensures",
120        "Declare a function postcondition — a pure `Bool` clause over the parameters and `result` (`ensures <name>: <pred>`).",
121    ),
122    k("enum", "Declare a payloadless sum type (`enum { A, B }`)."),
123    k(
124        "event",
125        "Declare a typed fact a context may emit (`event Name = { fields }`), inside a context.",
126    ),
127    k(
128        "expect",
129        "Assert a predicate inside a test case (`expect <bool-predicate>`).",
130    ),
131    k("exports", "Declare which types a context exposes, and how."),
132    k("false", "The boolean literal `false`."),
133    k("fn", "Declare a function."),
134    k(
135        "from",
136        "Name the protocol a service conforms to (`service X from http`).",
137    ),
138    k("given", "Declare the capabilities a handler requires."),
139    k(
140        "http",
141        "The HTTP protocol on a service header (`from http`).",
142    ),
143    k("if", "A conditional expression."),
144    k(
145        "implies",
146        "Logical implication (`P implies Q` ≡ `!P || Q`), used in invariant predicates.",
147    ),
148    k(
149        "invariant",
150        "Declare an agent invariant — a predicate that must hold of every committed state.",
151    ),
152    k(
153        "is",
154        "Test a value against a variant pattern, yielding a `Bool`.",
155    ),
156    k(
157        "let",
158        "Bind a local value (`let x = …`, or `let x <- …` for an effect), or match a pattern with a fallback (`let Ok(x) = r else { … }`).",
159    ),
160    k(
161        "match",
162        "Pattern-match over a sum type, `Result`, or `Option`.",
163    ),
164    k(
165        "messages",
166        "Declare a message bundle for one locale (`messages \"<tag>\" { \"code\" => \"template\" }`), inside a commons.",
167    ),
168    k(
169        "on",
170        "Begin a handler declaration (`on call`, `on GET(…)`, `on message`, `on open`/`on close`).",
171    ),
172    k(
173        "opaque",
174        "Declare an opaque type, or export a type opaquely.",
175    ),
176    k(
177        "property",
178        "Declare a generative test inside a `suite` (`property \"…\" { for all … }`).",
179    ),
180    k(
181        "protocol",
182        "Reserved keyword (protocols are a closed, compiler-known set).",
183    ),
184    k("provides", "Provide an implementation of a capability."),
185    k(
186        "queue",
187        "The queue protocol on a service header (`from queue(\"name\")`).",
188    ),
189    k(
190        "record",
191        "Reserved keyword (records are written `type X = { … }`).",
192    ),
193    k(
194        "requires",
195        "Declare a function precondition — a pure `Bool` clause over the parameters (`requires <name>: <pred>`).",
196    ),
197    k("self", "The current agent instance, inside a handler."),
198    k(
199        "service",
200        "Declare a service (a group of handlers) in a context.",
201    ),
202    k(
203        "stub",
204        "Stub a consumed capability operation at a test seam (`stub Cap.op(…) returns <v>` / `returns <v> after <d>` / `fails` / `never`).",
205    ),
206    k(
207        "suite",
208        "Declare a test suite targeting a unit (`suite <target> { case … }`).",
209    ),
210    k(
211        "transition",
212        "Declare an agent step invariant over the `old`/`new` state pair (`transition <name>: …`).",
213    ),
214    k(
215        "transparent",
216        "Export a type with its structure visible (`exports transparent { … }`).",
217    ),
218    k("true", "The boolean literal `true`."),
219    k(
220        "type",
221        "Declare a type: alias, record, sum, opaque, or refined.",
222    ),
223    k("uses", "Bring a commons into scope."),
224    k("where", "Attach refinement predicates to a base type."),
225];
226
227/// Contextual keywords — words that read as keywords in one position but stay
228/// usable as ordinary identifiers elsewhere, so they are lexed as `Ident` and
229/// are deliberately *absent* from [`KEYWORDS`] (which is drift-guarded to equal
230/// the lexer's reserved `#[token]`s). Editor surfaces still owe them a hover and
231/// a doc — the mechanical floor over this table lives in
232/// `bynk-lsp/tests/editor_coverage.rs`, mirroring the reserved-keyword tooth
233/// (ADR 0156 / ADR 0161).
234pub const CONTEXTUAL_KEYWORDS: &[KeywordInfo] = &[
235    k(
236        "advance",
237        "Move the test harness's clock forward in a `case` (`advance Clock by 15.minutes`).",
238    ),
239    k(
240        "derives",
241        "After a type declaration's body, opts the type into a constraint (`type Priority = enum { Low, High } derives Ord`): the type is then ordered by declaration order.",
242    ),
243    k(
244        "drop",
245        "Inside a `migrate` step, discard a stored field's value on purpose (`drop owner`).",
246    ),
247    k(
248        "fixture",
249        "In a suite, declares a project file's text as a `String` constant for its cases (`fixture issue = \"fixtures/issue.json\"`): test-only, read from the `fixtures/` folder and never deployed.",
250    ),
251    k(
252        "key",
253        "The agent's identity field — one per agent; keys the store.",
254    ),
255    k(
256        "migrate",
257        "An agent's migration step: carries a stored record from an older schema version to the current shape (`migrate from 1 (old: CartV1) { … }`).",
258    ),
259    k(
260        "par",
261        "Opens a concurrent binding block (`par { let a <- e1  let b <- e2 }`): every effect is issued before any is awaited, and the block completes when all have settled.",
262    ),
263    k(
264        "private",
265        "Before a `fn`, `type`, `messages`, `asset`, `embed` or `const` declaration in a commons or context: visible only within its own unit (and that unit's suites), never through `uses` (`private fn helper(…)`).",
266    ),
267    k(
268        "removed",
269        "Declares an agent class gone (`removed agent Old`): its stored data is deleted at the next deploy, after a named confirmation.",
270    ),
271    k(
272        "replaces",
273        "Renames an agent class (`agent New replaces Old`): the old class's stored data and schema history move to the new one.",
274    ),
275    k("store", "A persisted agent-state field."),
276];
277
278/// The reserved lexer tokens the parser deliberately re-admits as identifiers
279/// outside their one keyword position (`expect_ident`, ADR-tracked at
280/// `parser.rs`). Unlike [`CONTEXTUAL_KEYWORDS`] these words *are* real
281/// `#[token]`s and *are* members of [`KEYWORDS`] (so the lexer↔registry drift
282/// guard sees them) — they simply are not rejected in identifier position. The
283/// keyword reference renders them as a distinct tier so the page no longer
284/// claims, falsely, that every listed word is unusable as an identifier.
285///
286/// Single source of truth: the `expect_ident` exemption arm (via
287/// [`is_reserved_contextual`]) and the
288/// `is_reserved_keyword_covers_every_lexer_keyword` drift guard both defer to
289/// this list, so adding a word here is enough to make the parser admit it.
290pub const RESERVED_CONTEXTUAL: &[&str] = &[
291    "asset", "case", "const", "embed", "event", "messages", "on", "suite",
292];
293
294/// True when `word` is a [reserved contextual keyword](RESERVED_CONTEXTUAL) —
295/// a reserved token `expect_ident` re-admits as an identifier. Because each of
296/// these words lexes only to its own dedicated token, matching on the source
297/// text is equivalent to matching the token kind, but keeps the exemption
298/// single-sourced against [`RESERVED_CONTEXTUAL`].
299pub fn is_reserved_contextual(word: &str) -> bool {
300    RESERVED_CONTEXTUAL.contains(&word)
301}
302
303/// Built-in type names — compiler-known type constructors the parser dispatches
304/// on by identifier text in `parser/types.rs`, *outside* the keyword/token
305/// system entirely (they are lexed as ordinary `Ident`s, so they are absent
306/// from both the lexer's `#[token]`s and [`KEYWORDS`]). They are nonetheless
307/// reserved in type position: a `type` declaration may not reuse one of these
308/// names (`bynk.resolve.reserved_builtin_type`), because the parser would
309/// otherwise intercept every later reference and the user's alias would be
310/// silently shadowed or fail incoherently.
311///
312/// Single source of truth: [`is_builtin_type_name`] gates the resolver's
313/// redeclaration diagnostic, and a drift guard
314/// (`bynkc/tests/keywords_reference.rs`) asserts this list equals the set of
315/// names the type parser dispatches on. Keep it sorted.
316pub const BUILTIN_TYPE_NAMES: &[KeywordInfo] = &[
317    k(
318        "Asset",
319        "A static file declared by `asset name = \"path\"`; its `.url` is the content-hashed URL the platform serves it at.",
320    ),
321    k(
322        "BoundaryRejection",
323        "Why the router refused a request before any handler ran; the parameter of `on rejected`.",
324    ),
325    k(
326        "Connection",
327        "A held WebSocket connection, `Connection[F]`.",
328    ),
329    k(
330        "History",
331        "A generated call-history generator, `History[Agent]` (test-only).",
332    ),
333    k(
334        "HttpResult",
335        "The HTTP handler result type, `HttpResult[T]`.",
336    ),
337    k("List", "The immutable list type, `List[T]`."),
338    k("Map", "The immutable map type, `Map[K, V]`."),
339    k("Query", "The lazy storage-read type, `Query[T]`."),
340    k(
341        "QueueResult",
342        "The queue handler result type (non-generic).",
343    ),
344    k(
345        "RequestHeaders",
346        "An HTTP handler's read-only view of the request's headers (`headers: RequestHeaders`).",
347    ),
348    k(
349        "RequestQuery",
350        "An HTTP handler's read-only view of the request's query string (`query: RequestQuery`).",
351    ),
352    k("Set", "The immutable set type, `Set[T]`."),
353    k("Stream", "The value-over-time primitive, `Stream[T]`."),
354    k(
355        "TokenClaims",
356        "An `Oidc` actor's verified token claims, read through its binder (`who.claims`).",
357    ),
358];
359
360/// #2620: ambient built-in functions — bare calls the compiler knows without a
361/// declaration. Not keywords: a `fn` (or any binding) of the same name shadows
362/// them, as `Json` is shadowed. Both have the bottom type, so a call fits any
363/// position, and both fault when evaluated. Keep it sorted.
364pub const BUILTIN_FUNCTIONS: &[KeywordInfo] = &[
365    k(
366        "todo",
367        "Placeholder for unfinished code, `todo(message: String)`: type-checks anywhere, faults when run, and raises the `bynk.fault.todo` warning so `check --deny-warnings` can refuse it.",
368    ),
369    k(
370        "unreachable",
371        "Mark a case that cannot happen, `unreachable(message: String)`: type-checks anywhere and faults when evaluated, so a broken assumption is loud instead of masked by a made-up default.",
372    ),
373];
374
375/// True when `name` is a compiler-known [built-in type name](BUILTIN_TYPE_NAMES).
376/// Used by the resolver to reject `type <name> = …` redeclarations.
377pub fn is_builtin_type_name(name: &str) -> bool {
378    BUILTIN_TYPE_NAMES.iter().any(|b| b.word == name)
379}
380
381const fn k(word: &'static str, meaning: &'static str) -> KeywordInfo {
382    KeywordInfo { word, meaning }
383}
384
385/// Render the keyword reference page — four tiers, each with prose that is
386/// true of *that* tier (the page used to make one blanket claim that was false
387/// of two of them).
388pub fn render_markdown() -> String {
389    let mut out = String::new();
390    out.push_str("# Keywords\n\n");
391    out.push_str(
392        "<!-- GENERATED FILE — do not edit by hand.\n     \
393         Source: bynk-syntax/src/keywords.rs (`render_markdown`).\n     \
394         Regenerate with: BYNK_BLESS=1 cargo test -p bynkc --test it keywords_reference:: -->\n\n",
395    );
396    out.push_str(
397        "Bynk reserves names in four tiers. The first two are lexer keywords; the \
398         third are words the lexer treats as plain identifiers; the fourth are \
399         compiler-known type names. Only the **hard keywords** can never be used \
400         as an identifier. A last section lists the ambient built-in functions, \
401         which reserve nothing.\n\n",
402    );
403    out.push_str(
404        "A hard keyword is still allowed where a name is a *label* rather than a \
405         binding, so a field can carry the key a third-party payload uses \
406         (`type`, `from`, `self`):\n\n\
407         - a record field, a construction initialiser, a record or named pattern \
408         field and a variant payload field, when the keyword is followed by `:` \
409         (`{ type: String }`, `Account { type: \"Org\" }`, `Account { type: t, .. }`, \
410         `Push(from: String)`). `kind` stays reserved as a variant payload field.\n\
411         - a member after `.`, a field access or a method call, and a method \
412         declaration, when the keyword is on the same line as the `.` \
413         (`a.type`, `fn Sorted.from(…)`, `Sorted.from(xs)`).\n\n\
414         A keyword is never a parameter, a `let` or lambda binding or a bare \
415         reference, so field shorthand is refused: `Account { type, .. }` and \
416         `Account { type }` are `bynk.parse.keyword_field_shorthand`. Write \
417         `type: type_` in a pattern and `type: <value>` in a construction.\n\n",
418    );
419
420    // Tier 1 — hard keywords: every reserved token except the contextual ones.
421    let hard: Vec<&KeywordInfo> = KEYWORDS
422        .iter()
423        .filter(|k| !RESERVED_CONTEXTUAL.contains(&k.word))
424        .collect();
425    out.push_str("## Hard keywords\n\n");
426    out.push_str(&format!(
427        "Reserved everywhere — these **{}** words can never be used as an \
428         identifier.\n\n",
429        hard.len()
430    ));
431    out.push_str("| Keyword | Meaning |\n|---|---|\n");
432    for info in hard {
433        out.push_str(&format!("| `{}` | {} |\n", info.word, info.meaning));
434    }
435
436    // Tier 2 — contextual keywords: reserved tokens the parser re-admits as
437    // identifiers outside their one keyword position.
438    let contextual: Vec<&KeywordInfo> = RESERVED_CONTEXTUAL
439        .iter()
440        .filter_map(|w| KEYWORDS.iter().find(|k| &k.word == w))
441        .collect();
442    out.push_str("\n## Contextual keywords\n\n");
443    out.push_str(
444        "Reserved only in the one position named below; elsewhere (a field, \
445         parameter, or other identifier) they are ordinary names.\n\n",
446    );
447    out.push_str("| Keyword | Meaning |\n|---|---|\n");
448    for info in contextual {
449        out.push_str(&format!("| `{}` | {} |\n", info.word, info.meaning));
450    }
451
452    // Tier 3 — contextual words: not lexer keywords at all; ordinary identifiers
453    // that carry meaning in one position.
454    out.push_str("\n## Contextual words\n\n");
455    out.push_str(
456        "Not lexer keywords: the lexer treats these as ordinary identifiers, and \
457         they carry meaning only in the position each entry describes (inside an \
458         agent body or a `migrate` step, for instance). You can still use them as a \
459         field, parameter, or other name elsewhere.\n\n",
460    );
461    out.push_str("| Word | Meaning |\n|---|---|\n");
462    for info in CONTEXTUAL_KEYWORDS {
463        out.push_str(&format!("| `{}` | {} |\n", info.word, info.meaning));
464    }
465
466    // Tier 4 — built-in type names: not lexer keywords at all, but reserved in
467    // type position (a `type` declaration may not reuse one).
468    out.push_str("\n## Built-in type names\n\n");
469    out.push_str(
470        "Compiler-known type constructors. They are not lexer keywords — you may \
471         use them as an identifier in value position — but they are reserved in \
472         type position: a `type` declaration may not reuse one of these names \
473         (`bynk.resolve.reserved_builtin_type`).\n\n",
474    );
475    out.push_str("| Name | Meaning |\n|---|---|\n");
476    for info in BUILTIN_TYPE_NAMES {
477        out.push_str(&format!("| `{}` | {} |\n", info.word, info.meaning));
478    }
479
480    // Ambient built-in functions: not reserved at all — a declaration of the
481    // same name shadows them.
482    out.push_str("\n## Built-in functions\n\n");
483    out.push_str(
484        "Functions the compiler provides without a declaration. They reserve \
485         nothing: a `fn`, parameter or local of the same name shadows the built-in.\n\n",
486    );
487    out.push_str("| Name | Meaning |\n|---|---|\n");
488    for info in BUILTIN_FUNCTIONS {
489        out.push_str(&format!("| `{}` | {} |\n", info.word, info.meaning));
490    }
491    out
492}