luau/tests/Transpiler.test.cpp
aaron 02241b6d24
Some checks failed
benchmark / callgrind (map[branch:main name:luau-lang/benchmark-data], ubuntu-22.04) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:macos version:macos-latest]) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:macos-arm version:macos-14]) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:ubuntu version:ubuntu-latest]) (push) Has been cancelled
build / windows (Win32) (push) Has been cancelled
build / windows (x64) (push) Has been cancelled
build / coverage (push) Has been cancelled
build / web (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:macos version:macos-latest]) (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:ubuntu version:ubuntu-20.04]) (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:windows version:windows-latest]) (push) Has been cancelled
release / web (push) Has been cancelled
Sync to upstream/release/645 (#1440)
In this update, we continue to improve the overall stability of the new
type solver. We're also shipping some early bits of two new features,
one of the language and one of the analysis API: user-defined type
functions and an incremental typechecking API.

If you use the new solver and want to use all new fixes included in this
release, you have to reference an additional Luau flag:
```c++
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
```
And set its value to `645`:
```c++
DFInt::LuauTypeSolverRelease.value = 645; // Or a higher value for future updates
```

## New Solver

* Fix a crash where scopes are incorrectly accessed cross-module after
they've been deallocated by appropriately zeroing out associated scope
pointers for free types, generic types, table types, etc.
* Fix a crash where we were incorrectly caching results for bound types
in generalization.
* Eliminated some unnecessary intermediate allocations in the constraint
solver and type function infrastructure.
* Built some initial groundwork for an incremental typecheck API for use
by language servers.
* Built an initial technical preview for [user-defined type
functions](https://rfcs.luau-lang.org/user-defined-type-functions.html),
more work still to come (including calling type functions from other
type functions), but adventurous folks wanting to experiment with it can
try it out by enabling `FFlag::LuauUserDefinedTypeFunctionsSyntax` and
`FFlag::LuauUserDefinedTypeFunction` in their local environment. Special
thanks to @joonyoo181 who built up all the initial infrastructure for
this during his internship!

## Miscellaneous changes

* Fix a compilation error on Ubuntu (fixes #1437)

---

Internal Contributors:

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Jeremy Yoo <jyoo@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

---------

Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: David Cope <dcope@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Junseo Yoo <jyoo@roblox.com>
2024-09-27 11:58:21 -07:00

709 lines
18 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/TypeAttach.h"
#include "Luau/TypeInfer.h"
#include "Luau/Type.h"
#include "Luau/Transpiler.h"
#include "Fixture.h"
#include "ScopedFlags.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(LuauUserDefinedTypeFunctionsSyntax)
TEST_SUITE_BEGIN("TranspilerTests");
TEST_CASE("test_1")
{
const std::string example = R"(
local function isPortal(element)
if type(element)~='table'then
return false
end
return element.component == Core.Portal
end
)";
CHECK_EQ(example, transpile(example).code);
}
TEST_CASE("string_literals")
{
const std::string code = R"( local S='abcdef\n\f\a\020' )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("string_literals_containing_utf8")
{
const std::string code = R"( local S='lalala こんにちは' )"; // Konichiwa!
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("elseif_chains_indent_sensibly")
{
const std::string code = R"(
if This then
Once()
elseif That then
Another()
elseif SecondLast then
Third()
else
IfAllElseFails()
end
)";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("strips_type_annotations")
{
const std::string code = R"( local s: string= 'hello there' )";
const std::string expected = R"( local s = 'hello there' )";
CHECK_EQ(expected, transpile(code).code);
}
TEST_CASE("strips_type_assertion_expressions")
{
const std::string code = R"( local s= some_function() :: any+ something_else() :: number )";
const std::string expected = R"( local s= some_function() + something_else() )";
CHECK_EQ(expected, transpile(code).code);
}
TEST_CASE("function_taking_ellipsis")
{
const std::string code = R"( function F(...) end )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("omit_decimal_place_for_integers")
{
const std::string code = R"( local a=5, 6, 7, 3.141, 1.1290000000000002e+45 )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("for_loop")
{
const std::string one = R"( for i=1,10 do end )";
CHECK_EQ(one, transpile(one).code);
const std::string code = R"( for i=5,6,7 do end )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("for_in_loop")
{
const std::string code = R"( for k, v in ipairs(x)do end )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("while_loop")
{
const std::string code = R"( while f(x)do print() end )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("repeat_until_loop")
{
const std::string code = R"( repeat print() until f(x) )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("lambda")
{
const std::string one = R"( local p=function(o, m, g) return 77 end )";
CHECK_EQ(one, transpile(one).code);
const std::string two = R"( local p=function(o, m, g,...) return 77 end )";
CHECK_EQ(two, transpile(two).code);
}
TEST_CASE("local_function")
{
const std::string one = R"( local function p(o, m, g) return 77 end )";
CHECK_EQ(one, transpile(one).code);
const std::string two = R"( local function p(o, m, g,...) return 77 end )";
CHECK_EQ(two, transpile(two).code);
}
TEST_CASE("function")
{
const std::string one = R"( function p(o, m, g) return 77 end )";
CHECK_EQ(one, transpile(one).code);
const std::string two = R"( function p(o, m, g,...) return 77 end )";
CHECK_EQ(two, transpile(two).code);
}
TEST_CASE("table_literals")
{
const std::string code = R"( local t={1, 2, 3, foo='bar', baz=99,[5.5]='five point five', 'end'} )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("more_table_literals")
{
const std::string code = R"( local t={['Content-Type']='text/plain'} )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("table_literal_preserves_record_vs_general")
{
const std::string code = R"( local t={['foo']='bar',quux=42} )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("table_literal_with_numeric_key")
{
const std::string code = R"( local t={[5]='five',[6]='six'} )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("table_literal_with_keyword_key")
{
const std::string code = R"( local t={['nil']=nil,['true']=true} )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("table_literal_closing_brace_at_correct_position")
{
const std::string code = R"(
local t={
eggs='Tasty',
avocado='more like awesomecavo amirite'
}
)";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("method_calls")
{
const std::string code = R"( foo.bar.baz:quux() )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("method_definitions")
{
const std::string code = R"( function foo.bar.baz:quux() end )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("spaces_between_keywords_even_if_it_pushes_the_line_estimation_off")
{
// Luau::Parser doesn't exactly preserve the string representation of numbers in Lua, so we can find ourselves
// falling out of sync with the original code. We need to push keywords out so that there's at least one space between them.
const std::string code = R"( if math.abs(raySlope) < .01 then return 0 end )";
const std::string expected = R"( if math.abs(raySlope) < 0.01 then return 0 end)";
CHECK_EQ(expected, transpile(code).code);
}
TEST_CASE("numbers")
{
const std::string code = R"( local a=2510238627 )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("infinity")
{
const std::string code = R"( local a = 1e500 local b = 1e400 )";
const std::string expected = R"( local a = 1e500 local b = 1e500 )";
CHECK_EQ(expected, transpile(code).code);
}
TEST_CASE("escaped_strings")
{
const std::string code = R"( local s='\\b\\t\\n\\\\' )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("escaped_strings_2")
{
const std::string code = R"( local s="\a\b\f\n\r\t\v\'\"\\" )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("need_a_space_between_number_literals_and_dots")
{
const std::string code = R"( return point and math.ceil(point* 100000* 100)/ 100000 .. '%'or '' )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("binary_keywords")
{
const std::string code = "local c = a0 ._ or b0 ._";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("do_blocks")
{
const std::string code = R"(
foo()
do
local bar=baz()
quux()
end
foo2()
)";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("emit_a_do_block_in_cases_of_potentially_ambiguous_syntax")
{
const std::string code = R"(
f();
(g or f)()
)";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("roundtrip_types")
{
const std::string code = R"(
local s:string='str'
local t:{a:string,b:number,[string]:number}
local fn:(string,string)->(number,number)
local s2:typeof(s)='foo'
local os:string?
local sn:string|number
local it:{x:number}&{y:number}
)";
auto allocator = Allocator{};
auto names = AstNameTable{allocator};
ParseOptions options;
ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, options);
REQUIRE(parseResult.errors.empty());
CHECK_EQ(code, transpileWithTypes(*parseResult.root));
}
TEST_CASE("roundtrip_generic_types")
{
const std::string code = R"(
export type A<T> = {v:T, next:A<T>}
)";
auto allocator = Allocator{};
auto names = AstNameTable{allocator};
ParseOptions options;
ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, options);
REQUIRE(parseResult.errors.empty());
CHECK_EQ(code, transpileWithTypes(*parseResult.root));
}
TEST_CASE_FIXTURE(Fixture, "attach_types")
{
const std::string code = R"(
local s='str'
local t={a=1,b=false}
local function fn()
return 10
end
)";
const std::string expected = R"(
local s:string='str'
local t:{a:number,b:boolean}={a=1,b=false}
local function fn(): number
return 10
end
)";
CHECK_EQ(expected, decorateWithTypes(code));
}
TEST_CASE("a_table_key_can_be_the_empty_string")
{
std::string code = "local T = {[''] = true}";
CHECK_EQ(code, transpile(code).code);
}
// There's a bit of login in the transpiler that always adds a space before a dot if the previous symbol ends in a digit.
// This was surfacing an issue where we might not insert a space after the 'local' keyword.
TEST_CASE("always_emit_a_space_after_local_keyword")
{
std::string code = "do local aZZZZ = Workspace.P1.Shape local bZZZZ = Enum.PartType.Cylinder end";
std::string expected = "do local aZZZZ = Workspace.P1 .Shape local bZZZZ= Enum.PartType.Cylinder end";
CHECK_EQ(expected, transpile(code).code);
}
TEST_CASE_FIXTURE(Fixture, "types_should_not_be_considered_cyclic_if_they_are_not_recursive")
{
std::string code = R"(
local common: {foo:string} = {foo = 'foo'}
local t = {}
t.x = common
t.y = common
)";
std::string expected = R"(
local common: {foo:string} = {foo = 'foo'}
local t:{x:{foo:string},y:{foo:string}}={}
t.x = common
t.y = common
)";
CHECK_EQ(expected, decorateWithTypes(code));
}
TEST_CASE_FIXTURE(Fixture, "type_lists_should_be_emitted_correctly")
{
std::string code = R"(
local a = function(a: string, b: number, ...: string): (string, ...number)
end
local b = function(...: string): ...number
end
local c = function()
end
)";
std::string expected = R"(
local a:(string,number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number)
end
local b:(...string)->(...number)=function(...:string): ...number
end
local c:()->()=function(): ()
end
)";
std::string actual = decorateWithTypes(code);
CHECK_EQ(expected, actual);
}
TEST_CASE_FIXTURE(Fixture, "function_type_location")
{
std::string code = R"(
local function foo(x: number): number
return x
end
local g: (number)->number = foo
)";
std::string expected = R"(
local function foo(x: number): number
return x
end
local g: (number)->(number)=foo
)";
std::string actual = decorateWithTypes(code);
CHECK_EQ(expected, actual);
}
TEST_CASE_FIXTURE(Fixture, "transpile_type_assertion")
{
std::string code = "local a = 5 :: number";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else")
{
std::string code = "local a = if 1 then 2 else 3";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_type_reference_import")
{
fileResolver.source["game/A"] = R"(
export type Type = { a: number }
return {}
)";
std::string code = R"(
local Import = require(game.A)
local a: Import.Type
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_type_packs")
{
std::string code = R"(
type Packed<T...> = (T...)->(T...)
local a: Packed<>
local b: Packed<(number, string)>
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested")
{
std::string code = "local a: ((number)->(string))|((string)->(string))";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_2")
{
std::string code = "local a: (number&string)|(string&boolean)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_3")
{
std::string code = "local a: nil | (string & number)";
CHECK_EQ("local a: ( string & number)?", transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_nested")
{
std::string code = "local a: ((number)->(string))&((string)->(string))";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_nested_2")
{
std::string code = "local a: (number|string)&(string|boolean)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_varargs")
{
std::string code = "local function f(...) return ... end";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_index_expr")
{
std::string code = "local a = {1, 2, 3} local b = a[2]";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_unary")
{
std::string code = R"(
local a = 1
local b = -1
local c = true
local d = not c
local e = 'hello'
local d = #e
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_break_continue")
{
std::string code = R"(
local a, b, c
repeat
if a then break end
if b then continue end
until c
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_compound_assignment")
{
std::string code = R"(
local a = 1
a += 2
a -= 3
a *= 4
a /= 5
a //= 5
a %= 6
a ^= 7
a ..= ' - result'
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_assign_multiple")
{
std::string code = "a, b, c = 1, 2, 3";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_generic_function")
{
std::string code = R"(
local function foo<T,S...>(a: T, ...: S...) return 1 end
local f: <T,S...>(T, S...)->(number) = foo
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_union_reverse")
{
std::string code = "local a: nil | number";
CHECK_EQ("local a: number?", transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple")
{
std::string code = "for k,v in next,{}do print(k,v) end";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_error_expr")
{
std::string code = "local a = f:-";
auto allocator = Allocator{};
auto names = AstNameTable{allocator};
ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {});
CHECK_EQ("local a = (error-expr: f:%error-id%)-(error-expr)", transpileWithTypes(*parseResult.root));
}
TEST_CASE_FIXTURE(Fixture, "transpile_error_stat")
{
std::string code = "-";
auto allocator = Allocator{};
auto names = AstNameTable{allocator};
ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {});
CHECK_EQ("(error-stat: (error-expr))", transpileWithTypes(*parseResult.root));
}
TEST_CASE_FIXTURE(Fixture, "transpile_error_type")
{
std::string code = "local a: ";
auto allocator = Allocator{};
auto names = AstNameTable{allocator};
ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {});
CHECK_EQ("local a:%error-type%", transpileWithTypes(*parseResult.root));
}
TEST_CASE_FIXTURE(Fixture, "transpile_parse_error")
{
std::string code = "local a = -";
auto result = transpile(code);
CHECK_EQ("", result.code);
CHECK_EQ("Expected identifier when parsing expression, got <eof>", result.parseError);
}
TEST_CASE_FIXTURE(Fixture, "transpile_to_string")
{
std::string code = "local a: string = 'hello'";
auto allocator = Allocator{};
auto names = AstNameTable{allocator};
ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {});
REQUIRE(parseResult.root);
REQUIRE(parseResult.root->body.size == 1);
AstStatLocal* statLocal = parseResult.root->body.data[0]->as<AstStatLocal>();
REQUIRE(statLocal);
CHECK_EQ("local a: string = 'hello'", toString(statLocal));
REQUIRE(statLocal->vars.size == 1);
AstLocal* local = statLocal->vars.data[0];
REQUIRE(local->annotation);
CHECK_EQ("string", toString(local->annotation));
REQUIRE(statLocal->values.size == 1);
AstExpr* expr = statLocal->values.data[0];
CHECK_EQ("'hello'", toString(expr));
}
TEST_CASE_FIXTURE(Fixture, "transpile_type_alias_default_type_parameters")
{
std::string code = R"(
type Packed<T = string, U = T, V... = ...boolean, W... = (T, U, V...)> = (T, U, V...)->(W...)
local a: Packed<number>
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_singleton_types")
{
std::string code = R"(
type t1 = 'hello'
type t2 = true
type t3 = ''
type t4 = false
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_array_types")
{
std::string code = R"(
type t1 = {number}
type t2 = {[string]: number}
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple_types")
{
std::string code = "for k:string,v:boolean in next,{}do end";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_string_interp")
{
std::string code = R"( local _ = `hello {name}` )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape")
{
std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_type_functions")
{
ScopedFastFlag sff{FFlag::LuauUserDefinedTypeFunctionsSyntax, true};
std::string code = R"( type function foo(arg1, arg2) if arg1 == arg2 then return arg1 end return arg2 end )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_SUITE_END();