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        "Duration",
25        "The time-span base type, in milliseconds (`5.minutes`).",
26    ),
27    k("Effect", "The effectful-computation type, `Effect[T]`."),
28    k("Err", "The error variant of `Result`."),
29    k("Float", "The floating-point base type."),
30    k(
31        "Instant",
32        "The absolute-time base type, in epoch milliseconds (`Clock.now()`).",
33    ),
34    k("Int", "The integer base type."),
35    k(
36        "JsonError",
37        "The JSON-decode error type, `Result[T, JsonError]` from `Json.decode`.",
38    ),
39    k("None", "The empty variant of `Option`."),
40    k("Ok", "The success variant of `Result`."),
41    k("Option", "The optional-value type, `Option[T]`."),
42    k("Result", "The success-or-error type, `Result[T, E]`."),
43    k("Some", "The present variant of `Option`."),
44    k("String", "The string base type."),
45    k(
46        "ValidationError",
47        "The error type returned by a refined type's `.of`.",
48    ),
49    k(
50        "actor",
51        "Declare an actor — a boundary contract a handler consumes via `by`.",
52    ),
53    k(
54        "adapter",
55        "Declare an adapter — the host boundary (capability contract + binding).",
56    ),
57    k("agent", "Declare a stateful, keyed agent inside a context."),
58    k("as", "Alias a consumed context (`consumes X as Y`)."),
59    k(
60        "asset",
61        "Declare a static file the platform serves, public by construction (`asset name = \"path\"`).",
62    ),
63    k(
64        "binding",
65        "Name an adapter's TypeScript binding module (`binding \"<module>\"`).",
66    ),
67    k(
68        "by",
69        "Name the actor a handler consumes, after the return type — or a service-level default on the header (`… -> T by <name>: <Actor>`).",
70    ),
71    k(
72        "capability",
73        "Declare a capability (a dependency interface) in a context, or a distinct instance of a platform resource (`capability Sessions = Kv`).",
74    ),
75    k(
76        "case",
77        "Declare a test case inside a `suite` (`case \"…\" { … }`).",
78    ),
79    k(
80        "commons",
81        "Declare a pure, stateless module of types and functions.",
82    ),
83    k(
84        "consumes",
85        "Declare a dependency on another context's services.",
86    ),
87    k(
88        "context",
89        "Declare a deployable context (services, agents, capabilities).",
90    ),
91    k(
92        "cron",
93        "The cron protocol on a service header (`from cron`).",
94    ),
95    k(
96        "do",
97        "Perform a unit effect as a statement (`do e` — the binder-free `let _ <- e`).",
98    ),
99    k(
100        "else",
101        "The alternative branch of an `if`, or what a block produces when a `let` pattern does not match (`let Ok(x) = r else { … }`).",
102    ),
103    k(
104        "embed",
105        "Declare a project file whose text is a `String` constant, compiled into the code and never served (`embed name = \"path\"`).",
106    ),
107    k(
108        "ensures",
109        "Declare a function postcondition — a pure `Bool` clause over the parameters and `result` (`ensures <name>: <pred>`).",
110    ),
111    k("enum", "Declare a payloadless sum type (`enum { A, B }`)."),
112    k(
113        "event",
114        "Declare a typed fact a context may emit (`event Name = { fields }`), inside a context.",
115    ),
116    k(
117        "expect",
118        "Assert a predicate inside a test case (`expect <bool-predicate>`).",
119    ),
120    k("exports", "Declare which types a context exposes, and how."),
121    k("false", "The boolean literal `false`."),
122    k("fn", "Declare a function."),
123    k(
124        "from",
125        "Name the protocol a service conforms to (`service X from http`).",
126    ),
127    k("given", "Declare the capabilities a handler requires."),
128    k(
129        "http",
130        "The HTTP protocol on a service header (`from http`).",
131    ),
132    k("if", "A conditional expression."),
133    k(
134        "implies",
135        "Logical implication (`P implies Q` ≡ `!P || Q`), used in invariant predicates.",
136    ),
137    k(
138        "invariant",
139        "Declare an agent invariant — a predicate that must hold of every committed state.",
140    ),
141    k(
142        "is",
143        "Test a value against a variant pattern, yielding a `Bool`.",
144    ),
145    k(
146        "let",
147        "Bind a local value (`let x = …`, or `let x <- …` for an effect), or match a pattern with a fallback (`let Ok(x) = r else { … }`).",
148    ),
149    k(
150        "match",
151        "Pattern-match over a sum type, `Result`, or `Option`.",
152    ),
153    k(
154        "messages",
155        "Declare a message bundle for one locale (`messages \"<tag>\" { \"code\" => \"template\" }`), inside a commons.",
156    ),
157    k(
158        "on",
159        "Begin a handler declaration (`on call`, `on GET(…)`, `on message`, `on open`/`on close`).",
160    ),
161    k(
162        "opaque",
163        "Declare an opaque type, or export a type opaquely.",
164    ),
165    k(
166        "property",
167        "Declare a generative test inside a `suite` (`property \"…\" { for all … }`).",
168    ),
169    k(
170        "protocol",
171        "Reserved keyword (protocols are a closed, compiler-known set).",
172    ),
173    k("provides", "Provide an implementation of a capability."),
174    k(
175        "queue",
176        "The queue protocol on a service header (`from queue(\"name\")`).",
177    ),
178    k(
179        "record",
180        "Reserved keyword (records are written `type X = { … }`).",
181    ),
182    k(
183        "requires",
184        "Declare a function precondition — a pure `Bool` clause over the parameters (`requires <name>: <pred>`).",
185    ),
186    k("self", "The current agent instance, inside a handler."),
187    k(
188        "service",
189        "Declare a service (a group of handlers) in a context.",
190    ),
191    k(
192        "stub",
193        "Stub a consumed capability operation at a test seam (`stub Cap.op(…) returns <v>` / `fails`).",
194    ),
195    k(
196        "suite",
197        "Declare a test suite targeting a unit (`suite <target> { case … }`).",
198    ),
199    k(
200        "transition",
201        "Declare an agent step invariant over the `old`/`new` state pair (`transition <name>: …`).",
202    ),
203    k(
204        "transparent",
205        "Export a type with its structure visible (`exports transparent { … }`).",
206    ),
207    k("true", "The boolean literal `true`."),
208    k(
209        "type",
210        "Declare a type: alias, record, sum, opaque, or refined.",
211    ),
212    k("uses", "Bring a commons into scope."),
213    k("where", "Attach refinement predicates to a base type."),
214];
215
216/// Contextual keywords — words that read as keywords in one position but stay
217/// usable as ordinary identifiers elsewhere, so they are lexed as `Ident` and
218/// are deliberately *absent* from [`KEYWORDS`] (which is drift-guarded to equal
219/// the lexer's reserved `#[token]`s). Editor surfaces still owe them a hover and
220/// a doc — the mechanical floor over this table lives in
221/// `bynk-lsp/tests/editor_coverage.rs`, mirroring the reserved-keyword tooth
222/// (ADR 0156 / ADR 0161).
223pub const CONTEXTUAL_KEYWORDS: &[KeywordInfo] = &[
224    k(
225        "advance",
226        "Move the test harness's clock forward in a `case` (`advance Clock by 15.minutes`).",
227    ),
228    k(
229        "drop",
230        "Inside a `migrate` step, discard a stored field's value on purpose (`drop owner`).",
231    ),
232    k(
233        "key",
234        "The agent's identity field — one per agent; keys the store.",
235    ),
236    k(
237        "migrate",
238        "An agent's migration step: carries a stored record from an older schema version to the current shape (`migrate from 1 (old: CartV1) { … }`).",
239    ),
240    k(
241        "removed",
242        "Declares an agent class gone (`removed agent Old`): its stored data is deleted at the next deploy, after a named confirmation.",
243    ),
244    k(
245        "replaces",
246        "Renames an agent class (`agent New replaces Old`): the old class's stored data and schema history move to the new one.",
247    ),
248    k("store", "A persisted agent-state field."),
249];
250
251/// The reserved lexer tokens the parser deliberately re-admits as identifiers
252/// outside their one keyword position (`expect_ident`, ADR-tracked at
253/// `parser.rs`). Unlike [`CONTEXTUAL_KEYWORDS`] these words *are* real
254/// `#[token]`s and *are* members of [`KEYWORDS`] (so the lexer↔registry drift
255/// guard sees them) — they simply are not rejected in identifier position. The
256/// keyword reference renders them as a distinct tier so the page no longer
257/// claims, falsely, that every listed word is unusable as an identifier.
258///
259/// Single source of truth: the `expect_ident` exemption arm (via
260/// [`is_reserved_contextual`]) and the
261/// `is_reserved_keyword_covers_every_lexer_keyword` drift guard both defer to
262/// this list, so adding a word here is enough to make the parser admit it.
263pub const RESERVED_CONTEXTUAL: &[&str] =
264    &["asset", "case", "embed", "event", "messages", "on", "suite"];
265
266/// True when `word` is a [reserved contextual keyword](RESERVED_CONTEXTUAL) —
267/// a reserved token `expect_ident` re-admits as an identifier. Because each of
268/// these words lexes only to its own dedicated token, matching on the source
269/// text is equivalent to matching the token kind, but keeps the exemption
270/// single-sourced against [`RESERVED_CONTEXTUAL`].
271pub fn is_reserved_contextual(word: &str) -> bool {
272    RESERVED_CONTEXTUAL.contains(&word)
273}
274
275/// Built-in type names — compiler-known type constructors the parser dispatches
276/// on by identifier text in `parser/types.rs`, *outside* the keyword/token
277/// system entirely (they are lexed as ordinary `Ident`s, so they are absent
278/// from both the lexer's `#[token]`s and [`KEYWORDS`]). They are nonetheless
279/// reserved in type position: a `type` declaration may not reuse one of these
280/// names (`bynk.resolve.reserved_builtin_type`), because the parser would
281/// otherwise intercept every later reference and the user's alias would be
282/// silently shadowed or fail incoherently.
283///
284/// Single source of truth: [`is_builtin_type_name`] gates the resolver's
285/// redeclaration diagnostic, and a drift guard
286/// (`bynkc/tests/keywords_reference.rs`) asserts this list equals the set of
287/// names the type parser dispatches on. Keep it sorted.
288pub const BUILTIN_TYPE_NAMES: &[KeywordInfo] = &[
289    k(
290        "Asset",
291        "A static file declared by `asset name = \"path\"`; its `.url` is the content-hashed URL the platform serves it at.",
292    ),
293    k(
294        "BoundaryRejection",
295        "Why the router refused a request before any handler ran; the parameter of `on rejected`.",
296    ),
297    k(
298        "Connection",
299        "A held WebSocket connection, `Connection[F]`.",
300    ),
301    k(
302        "History",
303        "A generated call-history generator, `History[Agent]` (test-only).",
304    ),
305    k(
306        "HttpResult",
307        "The HTTP handler result type, `HttpResult[T]`.",
308    ),
309    k("List", "The immutable list type, `List[T]`."),
310    k("Map", "The immutable map type, `Map[K, V]`."),
311    k("Query", "The lazy storage-read type, `Query[T]`."),
312    k(
313        "QueueResult",
314        "The queue handler result type (non-generic).",
315    ),
316    k(
317        "RequestHeaders",
318        "An HTTP handler's read-only view of the request's headers (`headers: RequestHeaders`).",
319    ),
320    k("Stream", "The value-over-time primitive, `Stream[T]`."),
321    k(
322        "TokenClaims",
323        "An `Oidc` actor's verified token claims, read through its binder (`who.claims`).",
324    ),
325];
326
327/// True when `name` is a compiler-known [built-in type name](BUILTIN_TYPE_NAMES).
328/// Used by the resolver to reject `type <name> = …` redeclarations.
329pub fn is_builtin_type_name(name: &str) -> bool {
330    BUILTIN_TYPE_NAMES.iter().any(|b| b.word == name)
331}
332
333const fn k(word: &'static str, meaning: &'static str) -> KeywordInfo {
334    KeywordInfo { word, meaning }
335}
336
337/// Render the keyword reference page — three tiers, each with prose that is
338/// true of *that* tier (the page used to make one blanket claim that was false
339/// of two of them).
340pub fn render_markdown() -> String {
341    let mut out = String::new();
342    out.push_str("# Keywords\n\n");
343    out.push_str(
344        "<!-- GENERATED FILE — do not edit by hand.\n     \
345         Source: bynk-syntax/src/keywords.rs (`render_markdown`).\n     \
346         Regenerate with: BYNK_BLESS=1 cargo test -p bynkc --test it keywords_reference:: -->\n\n",
347    );
348    out.push_str(
349        "Bynk reserves names in three tiers. The first two are lexer keywords; the \
350         third are compiler-known type names. Only the **hard keywords** can never \
351         be used as an identifier.\n\n",
352    );
353
354    // Tier 1 — hard keywords: every reserved token except the contextual ones.
355    let hard: Vec<&KeywordInfo> = KEYWORDS
356        .iter()
357        .filter(|k| !RESERVED_CONTEXTUAL.contains(&k.word))
358        .collect();
359    out.push_str("## Hard keywords\n\n");
360    out.push_str(&format!(
361        "Reserved everywhere — these **{}** words can never be used as an \
362         identifier.\n\n",
363        hard.len()
364    ));
365    out.push_str("| Keyword | Meaning |\n|---|---|\n");
366    for info in hard {
367        out.push_str(&format!("| `{}` | {} |\n", info.word, info.meaning));
368    }
369
370    // Tier 2 — contextual keywords: reserved tokens the parser re-admits as
371    // identifiers outside their one keyword position.
372    let contextual: Vec<&KeywordInfo> = RESERVED_CONTEXTUAL
373        .iter()
374        .filter_map(|w| KEYWORDS.iter().find(|k| &k.word == w))
375        .collect();
376    out.push_str("\n## Contextual keywords\n\n");
377    out.push_str(
378        "Reserved only in the one position named below; elsewhere (a field, \
379         parameter, or other identifier) they are ordinary names.\n\n",
380    );
381    out.push_str("| Keyword | Meaning |\n|---|---|\n");
382    for info in contextual {
383        out.push_str(&format!("| `{}` | {} |\n", info.word, info.meaning));
384    }
385
386    // Tier 3 — built-in type names: not lexer keywords at all, but reserved in
387    // type position (a `type` declaration may not reuse one).
388    out.push_str("\n## Built-in type names\n\n");
389    out.push_str(
390        "Compiler-known type constructors. They are not lexer keywords — you may \
391         use them as an identifier in value position — but they are reserved in \
392         type position: a `type` declaration may not reuse one of these names \
393         (`bynk.resolve.reserved_builtin_type`).\n\n",
394    );
395    out.push_str("| Name | Meaning |\n|---|---|\n");
396    for info in BUILTIN_TYPE_NAMES {
397        out.push_str(&format!("| `{}` | {} |\n", info.word, info.meaning));
398    }
399    out
400}