ridl_syntax/keywords.rs
1//! The RIDL family reserved-word registry (typl reference §1.4).
2//!
3//! The family maintains one reserved-word registry across every profile: every
4//! word in it is reserved in every profile, including words a profile does not
5//! accept. The ADR-0012 domain spellings are not in it: a spelling is a keyword
6//! only as the first token of an interface member, in a profile that implements
7//! it. This module is the single source of truth the lexer maps identifiers
8//! against, and the source a governance test can read.
9//!
10//! An identifier that matches a keyword the active [`Profile`] uses lexes to
11//! that keyword's [`SyntaxKind`]; one that matches any other registry word lexes
12//! to [`SyntaxKind::ReservedWord`]; anything else stays an
13//! [`Ident`](SyntaxKind::Ident).
14
15use crate::syntax_kind::SyntaxKind;
16
17/// The profile a source file is lexed and parsed under (ridl reference §2,
18/// ADR-0008): which member of the family the file is written in. The profile
19/// selects which registry words are active keywords — every other registry
20/// word stays [`SyntaxKind::ReservedWord`] — and where the parser draws its
21/// profile-boundary diagnostics (TYPL-304 / RIDL-403).
22///
23/// `ridl-core` derives the profile from the file extension
24/// (`profile_of_path`): `.ridl` selects [`Profile::Ridl`], `.rsdl` selects
25/// [`Profile::Rsdl`], everything else parses as typl.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum Profile {
28 /// `.typl` — the vocabulary layer. Behavior is identical to the E1
29 /// toolchain: only the typl keywords are active.
30 Typl,
31 /// `.ridl` — the interface-description layer. The typl keywords plus the
32 /// nine ridl words are active; durations and `@` are ordinary tokens.
33 Ridl,
34 /// `.rsdl` — the system-description layer (rsdl reference §2). The typl
35 /// keywords plus the eight rsdl words are active; durations and `@` are
36 /// ordinary tokens, as under [`Profile::Ridl`].
37 Rsdl,
38}
39
40/// The keywords the typl profile uses (typl reference §1.4), paired with the
41/// token kind each lexes to. Every entry here is also present in
42/// [`FAMILY_RESERVED`]; the `family_registry_is_consistent` test guards that.
43const TYPL_KEYWORDS: &[(&str, SyntaxKind)] = &[
44 ("package", SyntaxKind::PackageKw),
45 ("import", SyntaxKind::ImportKw),
46 ("as", SyntaxKind::AsKw),
47 ("internal", SyntaxKind::InternalKw),
48 ("type", SyntaxKind::TypeKw),
49 ("const", SyntaxKind::ConstKw),
50 ("struct", SyntaxKind::StructKw),
51 ("enum", SyntaxKind::EnumKw),
52 ("enumset", SyntaxKind::EnumsetKw),
53 ("union", SyntaxKind::UnionKw),
54 ("boolean", SyntaxKind::BooleanKw),
55 ("integer", SyntaxKind::IntegerKw),
56 ("float", SyntaxKind::FloatKw),
57 ("string", SyntaxKind::StringKw),
58 ("bytes", SyntaxKind::BytesKw),
59 ("true", SyntaxKind::TrueKw),
60 ("false", SyntaxKind::FalseKw),
61 ("step", SyntaxKind::StepKw),
62 ("match", SyntaxKind::MatchKw),
63 ("reserved", SyntaxKind::ReservedKw),
64 ("error", SyntaxKind::ErrorKw),
65];
66
67/// The contextual keywords of the typl profile: a word that is a keyword only
68/// in one position and an ordinary identifier everywhere else. None of them is
69/// in [`FAMILY_RESERVED`], and the lexer lexes each as [`SyntaxKind::Ident`];
70/// the parser remaps `open` when `enum` or `enumset` follows it.
71pub const TYPL_CONTEXTUAL: &[&str] = &["open"];
72
73/// The nine words the ridl profile activates beyond typl's set (ridl reference
74/// §2.3): the interaction and container keywords plus the two expr-core words
75/// (`require`/`ensure`). Every entry here is also present in
76/// [`FAMILY_RESERVED`]; the `family_registry_is_consistent` test guards that.
77const RIDL_KEYWORDS: &[(&str, SyntaxKind)] = &[
78 ("interface", SyntaxKind::InterfaceKw),
79 ("service", SyntaxKind::ServiceKw),
80 ("signal", SyntaxKind::SignalKw),
81 ("event", SyntaxKind::EventKw),
82 ("command", SyntaxKind::CommandKw),
83 ("query", SyntaxKind::QueryKw),
84 ("fixed", SyntaxKind::FixedKw),
85 ("require", SyntaxKind::RequireKw),
86 ("ensure", SyntaxKind::EnsureKw),
87];
88
89/// The eight words the rsdl profile activates beyond typl's set (rsdl reference
90/// §2): the five declaration keywords, the two component-line keywords, and the
91/// `for` of a deployment's relation clause. Every entry here is also present in
92/// [`FAMILY_RESERVED`]; the `family_registry_is_consistent` test guards that.
93const RSDL_KEYWORDS: &[(&str, SyntaxKind)] = &[
94 ("system", SyntaxKind::SystemKw),
95 ("component", SyntaxKind::ComponentKw),
96 ("distribution", SyntaxKind::DistributionKw),
97 ("deployment", SyntaxKind::DeploymentKw),
98 ("machine", SyntaxKind::MachineKw),
99 ("offers", SyntaxKind::OffersKw),
100 ("requires", SyntaxKind::RequiresKw),
101 ("for", SyntaxKind::ForKw),
102];
103
104/// The full family reserved-word registry (typl reference §1.4): the words the
105/// typl profile uses, followed by the words reserved family-wide for the other
106/// profiles but rejected by `.typl`. This is the governance source of truth —
107/// every word here is reserved in every profile and may not be an identifier.
108///
109/// The registry grows as the other profiles land; the per-profile keyword
110/// sections of each language reference enumerate their own additions, and the
111/// union of those sections is this list until the platform spec extracts it as a
112/// standalone normative list.
113pub const FAMILY_RESERVED: &[&str] = &[
114 // typl (used) — typl reference §1.4.
115 "package",
116 "import",
117 "as",
118 "internal",
119 "type",
120 "const",
121 "struct",
122 "enum",
123 "enumset",
124 "union",
125 "boolean",
126 "integer",
127 "float",
128 "string",
129 "bytes",
130 "true",
131 "false",
132 "step",
133 "match",
134 "reserved",
135 "error",
136 // ridl.
137 "interface",
138 "service",
139 "signal",
140 "event",
141 "command",
142 "query",
143 // ridl. `fixed` is family-neutral (ADR-0012 decision 4).
144 "fixed",
145 // The two ridl clause words the general form names (§4.8). The other
146 // retired uxdl words left the registry with driftsys/ridl#370 and are
147 // identifiers in every profile.
148 "states",
149 "during",
150 // expr core.
151 "require",
152 "ensure",
153 // rmdl.
154 "model",
155 "function",
156 "let",
157 "init",
158 "last",
159 "case",
160 "if",
161 "then",
162 "else",
163 "when",
164 "emit",
165 "merge",
166 "current",
167 "state",
168 "transition",
169 "automaton",
170 // rsdl — rsdl reference §2. The v0.1 words (`provides`, `instance`,
171 // `assurance`, `target`, `place`, `on`, `transport`, `bundle`, `time`,
172 // `base`, `redundant`, `supervise`, `degraded`) left the registry with
173 // rsdl v0.2: no other profile uses one, so each is an identifier again.
174 "system",
175 "component",
176 "distribution",
177 "deployment",
178 "machine",
179 "offers",
180 "requires",
181 "for",
182];
183
184/// The [`SyntaxKind`] a typl keyword lexes to, or `None` if `text` is not a
185/// keyword the typl profile uses (it may still be a reserved word — see
186/// [`is_reserved`]).
187pub fn typl_keyword(text: &str) -> Option<SyntaxKind> {
188 TYPL_KEYWORDS
189 .iter()
190 .find(|(word, _)| *word == text)
191 .map(|(_, kind)| *kind)
192}
193
194/// The [`SyntaxKind`] one of the nine ridl words lexes to under
195/// [`Profile::Ridl`], or `None` if `text` is not one of them. The parser also
196/// uses this table to spot an interaction keyword in a typl parse (TYPL-304).
197pub fn ridl_keyword(text: &str) -> Option<SyntaxKind> {
198 RIDL_KEYWORDS
199 .iter()
200 .find(|(word, _)| *word == text)
201 .map(|(_, kind)| *kind)
202}
203
204/// The [`SyntaxKind`] one of the eight rsdl words lexes to under
205/// [`Profile::Rsdl`], or `None` if `text` is not one of them.
206pub fn rsdl_keyword(text: &str) -> Option<SyntaxKind> {
207 RSDL_KEYWORDS
208 .iter()
209 .find(|(word, _)| *word == text)
210 .map(|(_, kind)| *kind)
211}
212
213/// The [`SyntaxKind`] `text` lexes to as an active keyword of `profile`, or
214/// `None` when the profile does not use it — it may still be a reserved word
215/// of another profile ([`is_reserved`]) or an ordinary identifier.
216pub fn keyword_in(profile: Profile, text: &str) -> Option<SyntaxKind> {
217 match profile {
218 Profile::Typl => typl_keyword(text),
219 Profile::Ridl => typl_keyword(text).or_else(|| ridl_keyword(text)),
220 Profile::Rsdl => typl_keyword(text).or_else(|| rsdl_keyword(text)),
221 }
222}
223
224/// Whether `text` is reserved anywhere in the family — a keyword the typl
225/// profile uses or a word reserved for another profile. A reserved word may not
226/// be used as an identifier in any profile.
227pub fn is_reserved(text: &str) -> bool {
228 FAMILY_RESERVED.contains(&text)
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn family_registry_is_consistent() {
237 // Every keyword the typl, ridl or rsdl profile uses is in the family
238 // registry.
239 for (word, _) in TYPL_KEYWORDS
240 .iter()
241 .chain(RIDL_KEYWORDS)
242 .chain(RSDL_KEYWORDS)
243 {
244 assert!(
245 FAMILY_RESERVED.contains(word),
246 "used keyword `{word}` is missing from FAMILY_RESERVED",
247 );
248 }
249 // No word is claimed by two profiles' own tables.
250 for (word, _) in RIDL_KEYWORDS {
251 assert!(
252 typl_keyword(word).is_none(),
253 "`{word}` is in both TYPL_KEYWORDS and RIDL_KEYWORDS",
254 );
255 }
256 for (word, _) in RSDL_KEYWORDS {
257 assert!(
258 typl_keyword(word).is_none() && ridl_keyword(word).is_none(),
259 "`{word}` is in RSDL_KEYWORDS and in the typl or ridl table",
260 );
261 }
262 // The registry has no duplicate entries.
263 for (i, word) in FAMILY_RESERVED.iter().enumerate() {
264 assert!(
265 !FAMILY_RESERVED[..i].contains(word),
266 "duplicate registry word `{word}`",
267 );
268 }
269 }
270
271 #[test]
272 fn keyword_in_maps_per_profile() {
273 // A typl keyword is active in both profiles.
274 assert_eq!(keyword_in(Profile::Typl, "type"), Some(SyntaxKind::TypeKw));
275 assert_eq!(keyword_in(Profile::Ridl, "type"), Some(SyntaxKind::TypeKw));
276 // A ridl word is active under Ridl only.
277 assert_eq!(keyword_in(Profile::Typl, "signal"), None);
278 assert_eq!(
279 keyword_in(Profile::Ridl, "signal"),
280 Some(SyntaxKind::SignalKw)
281 );
282 assert_eq!(
283 keyword_in(Profile::Ridl, "ensure"),
284 Some(SyntaxKind::EnsureKw)
285 );
286 // A word of another profile is active in neither.
287 assert_eq!(keyword_in(Profile::Typl, "model"), None);
288 assert_eq!(keyword_in(Profile::Ridl, "model"), None);
289 // An ordinary identifier is never a keyword.
290 assert_eq!(keyword_in(Profile::Ridl, "Speed"), None);
291 }
292
293 #[test]
294 fn used_keywords_map_to_their_kind() {
295 assert_eq!(typl_keyword("package"), Some(SyntaxKind::PackageKw));
296 assert_eq!(typl_keyword("enumset"), Some(SyntaxKind::EnumsetKw));
297 assert_eq!(typl_keyword("error"), Some(SyntaxKind::ErrorKw));
298 // A reserved-but-unused word is not a typl keyword.
299 assert_eq!(typl_keyword("signal"), None);
300 // A plain identifier is neither.
301 assert_eq!(typl_keyword("Speed"), None);
302 }
303
304 #[test]
305 fn reserved_covers_used_and_unused() {
306 assert!(is_reserved("type")); // used
307 assert!(is_reserved("signal")); // reserved for ridl
308 assert!(is_reserved("model")); // reserved for rmdl
309 assert!(!is_reserved("Speed")); // ordinary identifier
310 assert!(!is_reserved("node")); // considered and rejected — never reserved
311 }
312
313 #[test]
314 fn fixed_is_an_active_ridl_keyword_and_final_is_not_reserved() {
315 assert_eq!(
316 keyword_in(Profile::Ridl, "fixed"),
317 Some(SyntaxKind::FixedKw),
318 "`fixed` is the provisioned-constant keyword in the ridl profile"
319 );
320 assert!(
321 !FAMILY_RESERVED.contains(&"final"),
322 "`final` was retired from the registry, the way `default` was"
323 );
324 }
325
326 #[test]
327 fn the_retired_uxdl_words_are_identifiers() {
328 // The retired uxdl words leave the registry, except `states` and
329 // `during`, which the general form names as ridl clauses
330 // (ADR-0012 decision 4 as amended on 2026-10-11).
331 for word in [
332 "view", "display", "input", "action", "activate", "toggle", "select", "adjust",
333 "dismiss", "fetch", "navigate", "scroll", "drag", "observe", "surface", "agent",
334 ] {
335 assert!(!is_reserved(word), "`{word}` must be an identifier");
336 for profile in [Profile::Typl, Profile::Ridl, Profile::Rsdl] {
337 assert_eq!(
338 keyword_in(profile, word),
339 None,
340 "`{word}` must not be a keyword under {profile:?}"
341 );
342 }
343 }
344 for word in ["states", "during"] {
345 assert!(
346 is_reserved(word),
347 "`{word}` stays reserved: a ridl clause word"
348 );
349 }
350 }
351
352 #[test]
353 fn the_domain_spellings_are_identifiers_until_a_profile_implements_them() {
354 // A spelling is a keyword only as the first token of an interface
355 // member, in a profile that implements it; no profile does yet
356 // (ADR-0012 decision 4 as amended on 2026-10-11).
357 for word in [
358 "present", "notify", "measure", "detect", "actuate", "trigger", "supply", "read",
359 ] {
360 assert!(!is_reserved(word), "`{word}` must be an identifier");
361 }
362 }
363}