Skip to main content

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}