Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/tests/OverloadResolver.test.cpp
2723 views
1
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
2
3
#include "doctest.h"
4
#include "Fixture.h"
5
6
#include "Luau/OverloadResolution.h"
7
#include "Luau/Normalize.h"
8
#include "Luau/UnifierSharedState.h"
9
10
LUAU_FASTFLAG(DebugLuauForceOldSolver)
11
12
using namespace Luau;
13
14
struct OverloadResolverFixture : Fixture
15
{
16
TypeArena arena_;
17
NotNull<TypeArena> arena{&arena_};
18
UnifierSharedState sharedState{&ice};
19
Normalizer normalizer{arena, getBuiltins(), NotNull{&sharedState}, !FFlag::DebugLuauForceOldSolver ? SolverMode::New : SolverMode::Old};
20
InternalErrorReporter iceReporter;
21
TypeCheckLimits limits;
22
TypeFunctionRuntime typeFunctionRuntime{NotNull{&iceReporter}, NotNull{&limits}};
23
Scope rootScope{getBuiltins()->emptyTypePack};
24
Location callLocation;
25
26
OverloadResolver resolver = mkResolver();
27
28
OverloadResolver mkResolver()
29
{
30
return OverloadResolver{
31
getBuiltins(),
32
arena,
33
NotNull{&normalizer},
34
NotNull{&typeFunctionRuntime},
35
NotNull{&rootScope},
36
NotNull{&iceReporter},
37
NotNull{&limits},
38
callLocation
39
};
40
}
41
42
DenseHashSet<TypeId> kEmptySet{nullptr};
43
NotNull<DenseHashSet<TypeId>> emptySet{&kEmptySet};
44
Location kDummyLocation;
45
AstExprConstantNil kDummyExpr{kDummyLocation};
46
std::vector<AstExpr*> kEmptyExprs;
47
48
TypePackId pack(std::vector<TypeId> tys) const
49
{
50
return arena->addTypePack(std::move(tys));
51
}
52
53
TypePackId pack(std::initializer_list<TypeId> tys) const
54
{
55
return arena->addTypePack(tys);
56
}
57
58
TypePackId pack(std::initializer_list<TypeId> tys, TypePackVariant tail) const
59
{
60
return arena->addTypePack(tys, arena->addTypePack(std::move(tail)));
61
}
62
63
TypeId fn(std::initializer_list<TypeId> args, std::initializer_list<TypeId> rets) const
64
{
65
return arena->addType(FunctionType{pack(args), pack(rets)});
66
}
67
68
// `&`
69
TypeId meet(TypeId a, TypeId b) const
70
{
71
return arena->addType(IntersectionType{{a, b}});
72
}
73
TypeId meet(std::initializer_list<TypeId> parts) const
74
{
75
return arena->addType(IntersectionType{parts});
76
}
77
TypeId join(TypeId a, TypeId b) const
78
{
79
return arena->addType(UnionType{{a, b}});
80
}
81
82
TypeId tableWithCall(TypeId callMm) const
83
{
84
TypeId table = arena->addType(TableType{TableState::Sealed, TypeLevel{}, /*scope*/ nullptr});
85
TypeId metatable = arena->addType(TableType{TableType::Props{{"__call", callMm}}, std::nullopt, TypeLevel{}, TableState::Sealed});
86
87
return arena->addType(MetatableType{table, metatable});
88
}
89
90
// (number) -> number
91
const TypeId numberToNumber = fn({getBuiltins()->numberType}, {getBuiltins()->numberType});
92
// (number, number) -> number
93
const TypeId numberNumberToNumber = fn({getBuiltins()->numberType, getBuiltins()->numberType}, {getBuiltins()->numberType});
94
// (number) -> string
95
const TypeId numberToString = fn({getBuiltins()->numberType}, {getBuiltins()->stringType});
96
// (string) -> string
97
const TypeId stringToString = fn({getBuiltins()->stringType}, {getBuiltins()->stringType});
98
99
// (number) -> number & (string) -> string
100
const TypeId numberToNumberAndStringToString = meet(numberToNumber, stringToString);
101
// (number) -> number & (number, number) -> number
102
const TypeId numberToNumberAndNumberNumberToNumber = meet(numberToNumber, numberNumberToNumber);
103
};
104
105
TEST_SUITE_BEGIN("OverloadResolverTest");
106
107
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_basic_overload_selection")
108
{
109
// ty: (number) -> number & (string) -> string
110
// args: (number)
111
OverloadResolution result =
112
resolver.resolveOverload(numberToNumberAndStringToString, pack({getBuiltins()->numberType}), Location{}, emptySet, false);
113
114
CHECK(1 == result.ok.size());
115
CHECK(result.ok.at(0) == numberToNumber);
116
}
117
118
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_basic_overload_selection1")
119
{
120
// ty: (number) -> number & (string) -> string
121
// args: (string)
122
OverloadResolution result =
123
resolver.resolveOverload(numberToNumberAndStringToString, pack({getBuiltins()->stringType}), Location{}, emptySet, false);
124
125
CHECK(1 == result.ok.size());
126
CHECK(stringToString == result.ok.at(0));
127
128
CHECK(1 == result.incompatibleOverloads.size());
129
CHECK(numberToNumber == result.incompatibleOverloads.at(0).first);
130
}
131
132
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_match_call_metamethod")
133
{
134
// (unknown, number) -> number
135
TypeId callMm = fn({builtinTypes->unknownType, builtinTypes->numberType}, {builtinTypes->numberType});
136
TypeId tbl = tableWithCall(callMm);
137
138
OverloadResolution result = resolver.resolveOverload(tbl, pack({builtinTypes->numberType}), Location{}, emptySet, false);
139
140
// Possible design issue here: We're indicating that an overload matches,
141
// but it clearly has a different arity than the type pack that was
142
// passed!
143
//
144
// Complicated: The metatable-having table could be part of an overload group. (eg {....} & (number) -> number)
145
// OR the metamethod itself could be overloaded ({__call: (number) -> number & (string) -> string})
146
CHECK(1 == result.ok.size());
147
CHECK(callMm == result.ok.at(0));
148
}
149
150
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_metamethod_could_be_overloaded")
151
{
152
// (unknown, number) -> number
153
TypeId overload1 = fn({builtinTypes->unknownType, builtinTypes->numberType}, {builtinTypes->numberType});
154
// (unknown, string) -> string
155
TypeId overload2 = fn({builtinTypes->unknownType, builtinTypes->stringType}, {builtinTypes->stringType});
156
TypeId tbl = tableWithCall(meet(overload1, overload2));
157
158
OverloadResolution result = resolver.resolveOverload(tbl, pack({builtinTypes->numberType}), Location{}, emptySet, false);
159
160
// Possible design issue here: We're indicating that an overload matches,
161
// but it clearly has a different arity than the type pack that was
162
// passed!
163
//
164
// Complicated: The metatable-having table could be part of an overload group. (eg {....} & (number) -> number)
165
// OR the metamethod itself could be overloaded ({__call: (number) -> number & (string) -> string})
166
CHECK(1 == result.ok.size());
167
CHECK(overload1 == result.ok.at(0));
168
169
CHECK(1 == result.incompatibleOverloads.size());
170
CHECK(overload2 == result.incompatibleOverloads.at(0).first);
171
}
172
173
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_overload_group_could_include_metamethod")
174
{
175
// (unknown, number) -> number
176
TypeId overload1 = fn({builtinTypes->unknownType, builtinTypes->numberType}, {builtinTypes->numberType});
177
// (unknown, string) -> string
178
TypeId overload2 = fn({builtinTypes->unknownType, builtinTypes->stringType}, {builtinTypes->stringType});
179
TypeId tbl = tableWithCall(meet(overload1, overload2));
180
181
TypeId monstrosity = meet(tbl, fn({builtinTypes->booleanType}, {builtinTypes->booleanType}));
182
183
OverloadResolution result = resolver.resolveOverload(monstrosity, pack({builtinTypes->numberType}), Location{}, emptySet, false);
184
185
CHECK(1 == result.ok.size());
186
CHECK(overload1 == result.ok.at(0));
187
}
188
189
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_overloads_with_different_arities")
190
{
191
// ty: (number) -> number & (number, number) -> number
192
// args: (number)
193
OverloadResolution result =
194
resolver.resolveOverload(numberToNumberAndNumberNumberToNumber, pack({getBuiltins()->numberType}), Location{}, emptySet, false);
195
196
CHECK(1 == result.ok.size());
197
CHECK(numberToNumber == result.ok.at(0));
198
199
CHECK(1 == result.arityMismatches.size());
200
CHECK(numberNumberToNumber == result.arityMismatches.at(0));
201
}
202
203
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_overloads_with_different_arities1")
204
{
205
// ty: (number) -> number & (number, number) -> number
206
// args: (number, number)
207
OverloadResolution result = resolver.resolveOverload(
208
numberToNumberAndNumberNumberToNumber, pack({getBuiltins()->numberType, getBuiltins()->numberType}), Location{}, emptySet, false
209
);
210
211
CHECK(1 == result.ok.size());
212
CHECK(numberNumberToNumber == result.ok.at(0));
213
214
CHECK(1 == result.arityMismatches.size());
215
CHECK(numberToNumber == result.arityMismatches.at(0));
216
}
217
218
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_separate_non_viable_overloads_by_arity_mismatch")
219
{
220
// ty: ((number)->number) & ((number)->string) & ((number, number)->number)
221
// args: (string)
222
const TypePack args = TypePack{{builtinTypes->stringType}, std::nullopt};
223
224
OverloadResolution resolution = resolver.resolveOverload(
225
meet({numberToNumber, numberToString, numberNumberToNumber}), pack({builtinTypes->stringType}), Location{}, emptySet, false
226
);
227
228
CHECK(resolution.ok.empty());
229
CHECK(resolution.nonFunctions.empty());
230
CHECK_EQ(1, resolution.arityMismatches.size());
231
CHECK_EQ(numberNumberToNumber, resolution.arityMismatches[0]);
232
233
CHECK_EQ(2, resolution.incompatibleOverloads.size());
234
bool numberToNumberFound = false;
235
bool numberToStringFound = false;
236
for (const auto& [ty, _] : resolution.incompatibleOverloads)
237
{
238
if (ty == numberToNumber)
239
numberToNumberFound = true;
240
else if (ty == numberToString)
241
numberToStringFound = true;
242
}
243
CHECK(numberToNumberFound);
244
CHECK(numberToStringFound);
245
}
246
247
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_select")
248
{
249
TypeId numberOrString = join(builtinTypes->numberType, builtinTypes->stringType);
250
TypePackId genericAs = arena->addTypePack(GenericTypePack{"A"});
251
252
TypeId selectTy = arena->addType(FunctionType{{}, {genericAs}, arena->addTypePack({numberOrString}, genericAs), builtinTypes->anyTypePack});
253
254
OverloadResolver r = mkResolver();
255
OverloadResolution resolution =
256
r.resolveOverload(selectTy, arena->addTypePack({numberOrString}, builtinTypes->anyTypePack), Location{}, emptySet, false);
257
258
CHECK(1 == resolution.ok.size());
259
}
260
261
TEST_CASE_FIXTURE(OverloadResolverFixture, "new_pass_table_with_indexer")
262
{
263
// {[any]: number}
264
TypeId anyNumberTable = arena->addType(
265
TableType{TableType::Props{}, TableIndexer{builtinTypes->anyType, builtinTypes->numberType}, TypeLevel{}, &rootScope, TableState::Sealed}
266
);
267
268
TypeId tableToTable = fn({anyNumberTable}, {anyNumberTable});
269
270
OverloadResolver r = mkResolver();
271
OverloadResolution resolution = r.resolveOverload(tableToTable, pack({anyNumberTable}), Location{}, emptySet, false);
272
273
CHECK(1 == resolution.ok.size());
274
CHECK(0 == resolution.potentialOverloads.size());
275
CHECK(0 == resolution.incompatibleOverloads.size());
276
CHECK(0 == resolution.nonFunctions.size());
277
CHECK(0 == resolution.arityMismatches.size());
278
}
279
280
TEST_CASE_FIXTURE(OverloadResolverFixture, "generic_higher_order_function_called_improperly")
281
{
282
// apply: <A, B..., C...>((A, B...) -> C..., A) -> C...
283
const TypeId genericA = arena->addType(GenericType{"A", Polarity::Mixed});
284
const TypePackId genericBs = arena->addTypePack(GenericTypePack{"B"});
285
const TypePackId genericCs = arena->addTypePack(GenericTypePack{"C"});
286
287
TypeId functionArgument = arena->addType(FunctionType{arena->addTypePack({genericA}, genericBs), genericCs});
288
289
TypePackId applyArgs = pack({functionArgument, genericA});
290
291
TypeId applyTy = arena->addType(FunctionType{{genericA}, {genericBs, genericCs}, applyArgs, genericCs});
292
293
TypePackId callArgsPack = pack({numberNumberToNumber, builtinTypes->numberType});
294
295
OverloadResolver r = mkResolver();
296
OverloadResolution resolution = r.resolveOverload(applyTy, callArgsPack, Location{}, emptySet, false);
297
298
CHECK(1 == resolution.ok.size());
299
}
300
301
TEST_CASE_FIXTURE(OverloadResolverFixture, "debug_traceback")
302
{
303
// ((message: string?, level: number?) -> string) & ((thread: thread, message: string?, level: number?) -> string)
304
TypeId overload1 = fn({builtinTypes->optionalStringType, builtinTypes->optionalNumberType}, {builtinTypes->stringType});
305
TypeId overload2 = fn({builtinTypes->threadType, builtinTypes->optionalStringType, builtinTypes->optionalNumberType}, {builtinTypes->stringType});
306
307
TypeId debugTraceback = meet({overload1, overload2});
308
309
OverloadResolver r = mkResolver();
310
OverloadResolution resolution;
311
312
SUBCASE("no_arguments")
313
{
314
resolution = r.resolveOverload(debugTraceback, builtinTypes->emptyTypePack, Location{}, emptySet, false);
315
CHECK(1 == resolution.ok.size());
316
}
317
318
SUBCASE("message_only")
319
{
320
resolution = r.resolveOverload(debugTraceback, pack({builtinTypes->stringType}), Location{}, emptySet, false);
321
CHECK(1 == resolution.ok.size());
322
}
323
324
SUBCASE("message_and_level")
325
{
326
resolution = r.resolveOverload(debugTraceback, pack({builtinTypes->stringType, builtinTypes->numberType}), Location{}, emptySet, false);
327
CHECK(1 == resolution.ok.size());
328
}
329
330
SUBCASE("thread")
331
{
332
resolution = r.resolveOverload(debugTraceback, pack({builtinTypes->threadType}), Location{}, emptySet, false);
333
CHECK(1 == resolution.ok.size());
334
}
335
336
SUBCASE("thread_and_message")
337
{
338
resolution = r.resolveOverload(debugTraceback, pack({builtinTypes->threadType, builtinTypes->stringType}), Location{}, emptySet, false);
339
CHECK(1 == resolution.ok.size());
340
}
341
342
SUBCASE("thread_message_and_level")
343
{
344
resolution = r.resolveOverload(
345
debugTraceback, pack({builtinTypes->threadType, builtinTypes->stringType, builtinTypes->numberType}), Location{}, emptySet, false
346
);
347
CHECK(1 == resolution.ok.size());
348
}
349
}
350
351
TEST_SUITE_END(); // OverloadResolverTest
352
353