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