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 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}