Skip to main content

luaur_ast/
rtti.rs

1//! AST RTTI mechanism — the faithful Rust analog of Luau's
2//! `AstRtti<T>::value` / `LUAU_RTTI(Class)` / `AstNode::is<T>()` / `as<T>()`.
3//! Reference: `luau/Ast/include/Luau/Ast.h` (the `AstNode` base + the
4//! `LUAU_RTTI` macro).
5//!
6//! In C++ every node carries a `const int classIndex` set at construction to a
7//! per-type id, and `node->as<T>()` is `classIndex == T::ClassIndex() ?
8//! static_cast<T*>(this) : nullptr`. The cast is sound because Luau nodes are
9//! standard-layout single-inheritance, so the base subobject sits at offset 0.
10//!
11//! We reproduce that exactly: every node is `#[repr(C)]` with its parent as the
12//! first field (`pub base: Parent`), so a `*mut AstNode` that actually points at
13//! an `AstExprGroup` can be reinterpreted as `*mut AstExprGroup` once the class
14//! index matches. The class index is a compile-time hash of the type name, so
15//! each node file is self-contained (no shared mutable counter / central
16//! registry to serialize against, unlike C++'s `++gAstRttiIndex`). The exact
17//! integer is irrelevant — only that it is unique per type and stable — which
18//! the `rtti_indices_unique` test enforces over the full node set.
19
20use crate::records::ast_node::AstNode;
21use crate::records::cst_node::CstNode;
22
23/// Stable per-type class index, the analog of `AstRtti<Class>::value`. FNV-1a
24/// over the class name, folded to a positive `i32` so it can never collide with
25/// a future "no class" sentinel (e.g. `-1`).
26pub const fn ast_rtti_index(name: &str) -> i32 {
27    let bytes = name.as_bytes();
28    let mut hash: u32 = 0x811c_9dc5;
29    let mut i = 0;
30    while i < bytes.len() {
31        hash ^= bytes[i] as u32;
32        hash = hash.wrapping_mul(0x0100_0193);
33        i += 1;
34    }
35    (hash & 0x7fff_ffff) as i32
36}
37
38/// Implemented by every concrete AST node type — the analog of the
39/// `LUAU_RTTI(Class)` macro, which expands to `static int ClassIndex()`.
40///
41/// A node `class X : Y` becomes `#[repr(C)] struct X { pub base: Y, ... }` plus
42/// `impl AstNodeClass for X { const CLASS_INDEX: i32 = ast_rtti_index("X"); }`.
43pub trait AstNodeClass {
44    /// The node's RTTI id; mirrors `T::ClassIndex()`.
45    const CLASS_INDEX: i32;
46}
47
48/// `node->is<T>()` — does this node have `T`'s dynamic type?
49pub trait AstNodeRef {
50    fn class_index(self) -> Option<i32>;
51}
52
53impl AstNodeRef for &AstNode {
54    #[inline]
55    fn class_index(self) -> Option<i32> {
56        Some(self.class_index)
57    }
58}
59
60impl AstNodeRef for *mut AstNode {
61    #[inline]
62    fn class_index(self) -> Option<i32> {
63        if self.is_null() {
64            None
65        } else {
66            unsafe { Some((*self).class_index) }
67        }
68    }
69}
70
71impl AstNodeRef for *const AstNode {
72    #[inline]
73    fn class_index(self) -> Option<i32> {
74        if self.is_null() {
75            None
76        } else {
77            unsafe { Some((*self).class_index) }
78        }
79    }
80}
81
82impl AstNodeRef for &crate::records::ast_type::AstType {
83    #[inline]
84    fn class_index(self) -> Option<i32> {
85        Some(self.base.class_index)
86    }
87}
88
89impl AstNodeRef for &crate::records::ast_expr::AstExpr {
90    #[inline]
91    fn class_index(self) -> Option<i32> {
92        Some(self.base.class_index)
93    }
94}
95
96impl AstNodeRef for &crate::records::ast_stat::AstStat {
97    #[inline]
98    fn class_index(self) -> Option<i32> {
99        Some(self.base.class_index)
100    }
101}
102
103impl AstNodeRef for &crate::records::ast_type_pack::AstTypePack {
104    #[inline]
105    fn class_index(self) -> Option<i32> {
106        Some(self.base.class_index)
107    }
108}
109
110#[inline]
111pub fn ast_node_is<T: AstNodeClass>(node: impl AstNodeRef) -> bool {
112    node.class_index() == Some(T::CLASS_INDEX)
113}
114
115/// `node->as<T>()` — downcast a base-node pointer to `*mut T`, or null when the
116/// dynamic type does not match.
117///
118/// # Safety
119/// `node` must be null or point to a live node whose first field is (transitively)
120/// an `AstNode` — i.e. any of the generated `#[repr(C)]` node structs. This is the
121/// same precondition as the C++ `static_cast<T*>(this)` it replaces.
122#[inline]
123pub unsafe fn ast_node_as<T: AstNodeClass>(node: *mut AstNode) -> *mut T {
124    if !node.is_null() && (*node).class_index == T::CLASS_INDEX {
125        node.cast::<T>()
126    } else {
127        core::ptr::null_mut()
128    }
129}
130
131/// `const` variant of [`ast_node_as`] for `*const AstNode`.
132///
133/// # Safety
134/// Same precondition as [`ast_node_as`].
135#[inline]
136pub unsafe fn ast_node_as_const<T: AstNodeClass>(node: *const AstNode) -> *const T {
137    if !node.is_null() && (*node).class_index == T::CLASS_INDEX {
138        node.cast::<T>()
139    } else {
140        core::ptr::null()
141    }
142}
143
144/// CST spelling of [`ast_rtti_index`] (CST and AST share the index function but
145/// separate index spaces). Provided so `LUAU_CST_RTTI(Class)` translations can
146/// read naturally as `cst_rtti_index("CstX")`.
147#[inline]
148pub const fn cst_rtti_index(name: &str) -> i32 {
149    ast_rtti_index(name)
150}
151
152/// CST analog of [`AstNodeClass`] — the `LUAU_CST_RTTI(Class)` macro, which
153/// expands to `static int CstClassIndex()`. CST nodes form a separate RTTI
154/// space (`gCstRttiIndex`) and a `CstNode*` is never cross-cast to an `AstNode*`,
155/// so reusing [`ast_rtti_index`] for the index value is sound — uniqueness only
156/// has to hold among CST names ([`tests::cst_rtti_indices_unique`]).
157pub trait CstNodeClass {
158    /// The node's CST RTTI id; mirrors `T::CstClassIndex()`.
159    const CLASS_INDEX: i32;
160}
161
162/// `cstNode->is<T>()`.
163#[inline]
164pub fn cst_node_is<T: CstNodeClass>(node: &CstNode) -> bool {
165    node.class_index == T::CLASS_INDEX
166}
167
168/// `cstNode->as<T>()` — downcast a base `*mut CstNode` to `*mut T`, or null on
169/// mismatch.
170///
171/// # Safety
172/// `node` must be null or point to a live CST node whose first field is
173/// (transitively) a `CstNode` — i.e. any generated `#[repr(C)]` CST node struct.
174#[inline]
175pub unsafe fn cst_node_as<T: CstNodeClass>(node: *mut CstNode) -> *mut T {
176    if !node.is_null() && (*node).class_index == T::CLASS_INDEX {
177        node.cast::<T>()
178    } else {
179        core::ptr::null_mut()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::ast_rtti_index;
186    use alloc::collections::BTreeMap;
187    use alloc::vec::Vec;
188
189    /// The full set of `LUAU_RTTI(Class)` names in `Ast.h` / `Cst.h`. The C++
190    /// guarantees uniqueness by construction (`++gAstRttiIndex`); our hash-based
191    /// scheme must be checked. If a future node collides, add a salt to its name
192    /// here and in its `impl AstNodeClass`.
193    const RTTI_NAMES: &[&str] = &[
194        "AstAttr",
195        "AstGenericType",
196        "AstGenericTypePack",
197        "AstExprGroup",
198        "AstExprConstantNil",
199        "AstExprConstantBool",
200        "AstExprConstantNumber",
201        "AstExprConstantString",
202        "AstExprLocal",
203        "AstExprGlobal",
204        "AstExprVarargs",
205        "AstExprCall",
206        "AstExprIndexName",
207        "AstExprIndexExpr",
208        "AstExprFunction",
209        "AstExprTable",
210        "AstExprUnary",
211        "AstExprBinary",
212        "AstExprTypeAssertion",
213        "AstExprIfElse",
214        "AstExprInterpString",
215        "AstExprError",
216        "AstStatBlock",
217        "AstStatIf",
218        "AstStatWhile",
219        "AstStatRepeat",
220        "AstStatBreak",
221        "AstStatContinue",
222        "AstStatReturn",
223        "AstStatExpr",
224        "AstStatLocal",
225        "AstStatFor",
226        "AstStatForIn",
227        "AstStatAssign",
228        "AstStatCompoundAssign",
229        "AstStatFunction",
230        "AstStatLocalFunction",
231        "AstStatTypeAlias",
232        "AstStatTypeFunction",
233        "AstStatDeclareGlobal",
234        "AstStatDeclareFunction",
235        "AstStatDeclareExternType",
236        "AstStatError",
237        "AstTypeReference",
238        "AstTypeTable",
239        "AstTypeFunction",
240        "AstTypeTypeof",
241        "AstTypeOptional",
242        "AstTypeUnion",
243        "AstTypeIntersection",
244        "AstTypeSingletonBool",
245        "AstTypeSingletonString",
246        "AstTypeGroup",
247        "AstTypeError",
248        "AstTypePackExplicit",
249        "AstTypePackVariadic",
250        "AstTypePackGeneric",
251    ];
252
253    #[test]
254    fn rtti_indices_unique() {
255        let mut seen: BTreeMap<i32, &str> = BTreeMap::new();
256        let mut collisions: Vec<(&str, &str, i32)> = Vec::new();
257        for &name in RTTI_NAMES {
258            let idx = ast_rtti_index(name);
259            if let Some(&prev) = seen.get(&idx) {
260                collisions.push((prev, name, idx));
261            } else {
262                seen.insert(idx, name);
263            }
264        }
265        assert!(
266            collisions.is_empty(),
267            "AST RTTI index collisions: {collisions:?}"
268        );
269    }
270
271    #[test]
272    fn rtti_index_is_stable_and_positive() {
273        // Stability: the value is a pure function of the name.
274        assert_eq!(
275            ast_rtti_index("AstExprGroup"),
276            ast_rtti_index("AstExprGroup")
277        );
278        // Positivity: leaves room for negative sentinels.
279        assert!(ast_rtti_index("AstExprGroup") >= 0);
280    }
281
282    /// The full set of `LUAU_CST_RTTI(Class)` names in `Cst.h`. CST nodes share
283    /// the index function with AST nodes but a separate index *space*, so only
284    /// CST-vs-CST uniqueness matters.
285    const CST_RTTI_NAMES: &[&str] = &[
286        "CstExprGroup",
287        "CstExprConstantNumber",
288        "CstExprConstantInteger",
289        "CstExprConstantString",
290        "CstExprCall",
291        "CstExprIndexExpr",
292        "CstExprFunction",
293        "CstExprTable",
294        "CstExprOp",
295        "CstExprTypeAssertion",
296        "CstExprIfElse",
297        "CstExprInterpString",
298        "CstExprExplicitTypeInstantiation",
299        "CstStatDo",
300        "CstStatRepeat",
301        "CstStatReturn",
302        "CstStatLocal",
303        "CstStatFor",
304        "CstStatForIn",
305        "CstStatAssign",
306        "CstStatCompoundAssign",
307        "CstStatFunction",
308        "CstStatLocalFunction",
309        "CstGenericType",
310        "CstGenericTypePack",
311        "CstStatTypeAlias",
312        "CstStatTypeFunction",
313        "CstTypeReference",
314        "CstTypeTable",
315        "CstTypeFunction",
316        "CstTypeTypeof",
317        "CstTypeUnion",
318        "CstTypeIntersection",
319        "CstTypeSingletonString",
320        "CstTypeGroup",
321        "CstTypePackExplicit",
322        "CstTypePackGeneric",
323    ];
324
325    #[test]
326    fn cst_rtti_indices_unique() {
327        let mut seen: BTreeMap<i32, &str> = BTreeMap::new();
328        let mut collisions: Vec<(&str, &str, i32)> = Vec::new();
329        for &name in CST_RTTI_NAMES {
330            let idx = ast_rtti_index(name);
331            if let Some(&prev) = seen.get(&idx) {
332                collisions.push((prev, name, idx));
333            } else {
334                seen.insert(idx, name);
335            }
336        }
337        assert!(
338            collisions.is_empty(),
339            "CST RTTI index collisions: {collisions:?}"
340        );
341    }
342}