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