Skip to main content

luaur_analysis/functions/
index_function_impl.rs

1//! C++ `TypeFunctionReductionResult<TypeId> indexFunctionImpl(
2//! const std::vector<TypeId>& typeParams, const std::vector<TypePackId>&
3//! packParams, NotNull<TypeFunctionContext> ctx, bool isRaw)`
4//! (BuiltinTypeFunctions.cpp:2077-2209). Shared implementation behind `index`
5//! and `rawget`.
6//!
7//! Vocabulary note: indexee refers to the type that contains the properties,
8//! indexer refers to the type that is used to access indexee.
9//! `index<Person, "name">` => `Person` is the indexee and `"name"` is the indexer.
10use crate::enums::reduction::Reduction;
11use crate::functions::follow_type::follow_type_id;
12use crate::functions::get_singleton_type::get_singleton_type;
13use crate::functions::get_type_alt_j::get_type_id;
14use crate::functions::is_pending::is_pending;
15use crate::functions::search_props_and_indexer::search_props_and_indexer;
16use crate::functions::tbl_index_into_builtin_type_functions::tbl_index_into;
17use crate::records::boolean_singleton::BooleanSingleton;
18use crate::records::extern_type::ExternType;
19use crate::records::singleton_type::SingletonType;
20use crate::records::table_type::TableType;
21use crate::records::type_function_context::TypeFunctionContext;
22use crate::records::type_function_reduction_result::TypeFunctionReductionResult;
23use crate::records::union_type::UnionType;
24use crate::type_aliases::error_vec::ErrorVec;
25use crate::type_aliases::type_id::TypeId;
26use crate::type_aliases::type_pack_id::TypePackId;
27use alloc::vec;
28use alloc::vec::Vec;
29use luaur_ast::records::location::Location;
30use luaur_ast::records::position::Position;
31use luaur_common::macros::luau_assert::LUAU_ASSERT;
32use luaur_common::records::dense_hash_set::DenseHashSet;
33
34fn empty_location() -> Location {
35    Location::new(
36        Position { line: 0, column: 0 },
37        Position { line: 0, column: 0 },
38    )
39}
40
41/// C++ overload `bool tblIndexInto(TypeId indexer, TypeId indexee,
42/// DenseHashSet<TypeId>& result, NotNull<TypeFunctionContext> ctx, bool isRaw)`
43/// (BuiltinTypeFunctions.cpp:2068-2072): seeds an empty seen-set and delegates to
44/// the recursive form.
45fn tbl_index_into_2(
46    indexer: TypeId,
47    indexee: TypeId,
48    result: &mut DenseHashSet<TypeId>,
49    ctx: *mut TypeFunctionContext,
50    is_raw: bool,
51) -> bool {
52    let mut seen_set: DenseHashSet<TypeId> = DenseHashSet::new(core::ptr::null());
53    tbl_index_into(indexer, indexee, result, &mut seen_set, ctx, is_raw)
54}
55
56fn erroneous() -> TypeFunctionReductionResult {
57    TypeFunctionReductionResult {
58        result: None,
59        reduction_status: Reduction::Erroneous,
60        blocked_types: vec![],
61        blocked_packs: vec![],
62        error: None,
63        messages: vec![],
64    }
65}
66
67fn maybe_ok_blocked(blocked: Vec<TypeId>) -> TypeFunctionReductionResult {
68    TypeFunctionReductionResult {
69        result: None,
70        reduction_status: Reduction::MaybeOk,
71        blocked_types: blocked,
72        blocked_packs: vec![],
73        error: None,
74        messages: vec![],
75    }
76}
77
78fn is_boolean_singleton(ty: TypeId) -> bool {
79    let singleton = unsafe { get_type_id::<SingletonType>(follow_type_id(ty)) };
80    !singleton.is_null() && !get_singleton_type::<BooleanSingleton>(singleton).is_null()
81}
82
83fn is_nonempty_table(ty: TypeId) -> bool {
84    let table = unsafe { get_type_id::<TableType>(follow_type_id(ty)) };
85    unsafe { table.as_ref() }
86        .map(|table| !table.props.is_empty())
87        .unwrap_or(false)
88}
89
90pub fn index_function_impl(
91    type_params: Vec<TypeId>,
92    _pack_params: Vec<TypePackId>,
93    ctx: *mut TypeFunctionContext,
94    is_raw: bool,
95) -> TypeFunctionReductionResult {
96    let ctx_ref = unsafe { &*ctx };
97
98    let indexee_ty = unsafe { follow_type_id(type_params[0]) };
99
100    if is_pending(indexee_ty, ctx_ref.solver) {
101        return maybe_ok_blocked(vec![indexee_ty]);
102    }
103
104    let Some(indexee_norm_ty) =
105        (unsafe { (*ctx_ref.normalizer.as_ptr()).try_normalize(indexee_ty) })
106    else {
107        return maybe_ok_blocked(vec![]);
108    };
109
110    // if the indexee is `any`, then indexing also gives us `any`.
111    if indexee_norm_ty.should_suppress_errors() {
112        return TypeFunctionReductionResult {
113            result: Some(unsafe { ctx_ref.builtins.as_ref().anyType }),
114            reduction_status: Reduction::MaybeOk,
115            blocked_types: vec![],
116            blocked_packs: vec![],
117            error: None,
118            messages: vec![],
119        };
120    }
121
122    // if we don't have either just tables or just extern types, we've got nothing to index into
123    if indexee_norm_ty.has_tables() == indexee_norm_ty.has_extern_types() {
124        return erroneous();
125    }
126
127    // we're trying to reject any type that has not normalized to a table or
128    // extern type or a union of tables or extern types.
129    if indexee_norm_ty.has_tops()
130        || indexee_norm_ty.has_booleans()
131        || indexee_norm_ty.has_errors()
132        || indexee_norm_ty.has_nils()
133        || indexee_norm_ty.has_numbers()
134        || indexee_norm_ty.has_strings()
135        || indexee_norm_ty.has_threads()
136        || indexee_norm_ty.has_buffers()
137        || indexee_norm_ty.has_functions()
138        || indexee_norm_ty.has_tyvars()
139    {
140        return erroneous();
141    }
142
143    let indexer_ty = unsafe { follow_type_id(type_params[1]) };
144
145    if is_pending(indexer_ty, ctx_ref.solver) {
146        return maybe_ok_blocked(vec![indexer_ty]);
147    }
148
149    let Some(indexer_norm_ty) =
150        (unsafe { (*ctx_ref.normalizer.as_ptr()).try_normalize(indexer_ty) })
151    else {
152        return maybe_ok_blocked(vec![]);
153    };
154
155    // we're trying to reject any type that is not a string singleton or primitive
156    if indexer_norm_ty.has_tops() || indexer_norm_ty.has_errors() {
157        return erroneous();
158    }
159
160    // indexer can be a union —> break them down into a vector
161    let single_type: Vec<TypeId> = vec![indexer_ty];
162    let types_to_find: &Vec<TypeId> =
163        if let Some(union_ty) = unsafe { get_type_id::<UnionType>(indexer_ty).as_ref() } {
164            &union_ty.options
165        } else {
166            &single_type
167        };
168
169    let mut properties: DenseHashSet<TypeId> = DenseHashSet::new(core::ptr::null()); // types that will be returned
170
171    if indexee_norm_ty.has_extern_types() {
172        LUAU_ASSERT!(!indexee_norm_ty.has_tables());
173
174        if is_raw {
175            // rawget should never reduce for extern types (to match the behavior
176            // of the rawget global function)
177            return erroneous();
178        }
179
180        // at least one class is guaranteed to be in the iterator by .hasExternTypes()
181        for &extern_type_iter in &indexee_norm_ty.extern_types.ordering {
182            let extern_ty = unsafe { get_type_id::<ExternType>(extern_type_iter) };
183            if extern_ty.is_null() {
184                LUAU_ASSERT!(false); // not possible according to normalization's spec
185                return erroneous();
186            }
187
188            for &ty in types_to_find {
189                // Search for all instances of indexer in class->props and class->indexer
190                let extern_ref = unsafe { &*extern_ty };
191                if search_props_and_indexer(
192                    ty,
193                    extern_ref.props.clone(),
194                    extern_ref.indexer.clone(),
195                    &mut properties,
196                    ctx,
197                ) {
198                    continue; // found in this class, move to the next type
199                }
200
201                let mut parent = extern_ref.parent;
202                let mut found_in_parent = false;
203                while let Some(parent_ty) = parent {
204                    if found_in_parent {
205                        break;
206                    }
207                    let parent_extern_type =
208                        unsafe { get_type_id::<ExternType>(follow_type_id(parent_ty)) };
209                    let parent_ref = unsafe { &*parent_extern_type };
210                    found_in_parent = search_props_and_indexer(
211                        ty,
212                        parent_ref.props.clone(),
213                        parent_ref.indexer.clone(),
214                        &mut properties,
215                        ctx,
216                    );
217                    parent = parent_ref.parent;
218                }
219
220                // we move on to the next type if any of the parents had the property.
221                if found_in_parent {
222                    continue;
223                }
224
225                // property not found -> check in the metatable's __index.
226                // findMetatableEntry demands the ability to emit errors, so we
227                // must give it the state to do that, even if we eat the errors.
228                let mut dummy: ErrorVec = vec![];
229                let mm_type = unsafe {
230                    crate::functions::find_metatable_entry::find_metatable_entry(
231                        ctx_ref.builtins.as_ptr(),
232                        &mut dummy,
233                        extern_type_iter,
234                        "__index",
235                        empty_location(),
236                    )
237                };
238                let mm_type = match mm_type {
239                    Some(mm) => mm,
240                    None => return erroneous(), // no metatable -> nowhere else to look
241                };
242
243                if !tbl_index_into_2(ty, mm_type, &mut properties, ctx, is_raw) {
244                    // if indexer is not in the metatable, we fail to reduce
245                    return erroneous();
246                }
247            }
248        }
249    }
250
251    if indexee_norm_ty.has_tables() {
252        LUAU_ASSERT!(!indexee_norm_ty.has_extern_types());
253
254        // at least one table is guaranteed to be in the iterator by .hasTables()
255        for &tables_iter in &indexee_norm_ty.tables.order {
256            for &ty in types_to_find {
257                if !tbl_index_into_2(ty, tables_iter, &mut properties, ctx, is_raw) {
258                    if is_raw {
259                        properties.insert(unsafe { ctx_ref.builtins.as_ref().nilType });
260                    } else if is_boolean_singleton(ty) && is_nonempty_table(tables_iter) {
261                        properties.insert(unsafe { ctx_ref.builtins.as_ref().unknownType });
262                    } else {
263                        return erroneous();
264                    }
265                }
266            }
267        }
268    }
269
270    // If the type being reduced to is a single type, no need to union
271    if properties.size() == 1 {
272        let only = *properties
273            .iter()
274            .next()
275            .expect("properties has exactly one element");
276        return TypeFunctionReductionResult {
277            result: Some(only),
278            reduction_status: Reduction::MaybeOk,
279            blocked_types: vec![],
280            blocked_packs: vec![],
281            error: None,
282            messages: vec![],
283        };
284    }
285
286    let options: Vec<TypeId> = properties.iter().copied().collect();
287    let union_ty = unsafe { (*ctx_ref.arena.as_ptr()).add_type(UnionType { options }) };
288    TypeFunctionReductionResult {
289        result: Some(union_ty),
290        reduction_status: Reduction::MaybeOk,
291        blocked_types: vec![],
292        blocked_packs: vec![],
293        error: None,
294        messages: vec![],
295    }
296}