diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 4c738385..c7531608 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -41,7 +41,7 @@ jobs: - name: Build Luau (clang) run: | make config=profile clean - CXX=clang++ make config=profile luau luau-analyze + CXX=clang++ make config=profile luau luau-analyze luau-compile - name: Run benchmark (bench-gcc) run: | @@ -70,16 +70,16 @@ jobs: - name: Run benchmark (compile) run: | filter() { - awk '/.*I\s+refs:\s+[0-9,]+/ {gsub(",", "", $4); X=$4} END {print "SUCCESS: '$1' : " X/1e7 "ms +/- 0% on luau --compile"}' + awk '/.*I\s+refs:\s+[0-9,]+/ {gsub(",", "", $4); X=$4} END {print "SUCCESS: '$1' : " X/1e7 "ms +/- 0% on luau-compile"}' } - valgrind --tool=callgrind ./luau --compile=null -O0 bench/other/LuauPolyfillMap.lua 2>&1 | filter map-O0 | tee -a compile-output.txt - valgrind --tool=callgrind ./luau --compile=null -O1 bench/other/LuauPolyfillMap.lua 2>&1 | filter map-O1 | tee -a compile-output.txt - valgrind --tool=callgrind ./luau --compile=null -O2 bench/other/LuauPolyfillMap.lua 2>&1 | filter map-O2 | tee -a compile-output.txt - valgrind --tool=callgrind ./luau --compile=codegennull -O2 bench/other/LuauPolyfillMap.lua 2>&1 | filter map-O2-codegen | tee -a compile-output.txt - valgrind --tool=callgrind ./luau --compile=null -O0 bench/other/regex.lua 2>&1 | filter regex-O0 | tee -a compile-output.txt - valgrind --tool=callgrind ./luau --compile=null -O1 bench/other/regex.lua 2>&1 | filter regex-O1 | tee -a compile-output.txt - valgrind --tool=callgrind ./luau --compile=null -O2 bench/other/regex.lua 2>&1 | filter regex-O2 | tee -a compile-output.txt - valgrind --tool=callgrind ./luau --compile=codegennull -O2 bench/other/regex.lua 2>&1 | filter regex-O2-codegen | tee -a compile-output.txt + valgrind --tool=callgrind ./luau-compile --null -O0 bench/other/LuauPolyfillMap.lua 2>&1 | filter map-O0 | tee -a compile-output.txt + valgrind --tool=callgrind ./luau-compile --null -O1 bench/other/LuauPolyfillMap.lua 2>&1 | filter map-O1 | tee -a compile-output.txt + valgrind --tool=callgrind ./luau-compile --null -O2 bench/other/LuauPolyfillMap.lua 2>&1 | filter map-O2 | tee -a compile-output.txt + valgrind --tool=callgrind ./luau-compile --codegennull -O2 bench/other/LuauPolyfillMap.lua 2>&1 | filter map-O2-codegen | tee -a compile-output.txt + valgrind --tool=callgrind ./luau-compile --null -O0 bench/other/regex.lua 2>&1 | filter regex-O0 | tee -a compile-output.txt + valgrind --tool=callgrind ./luau-compile --null -O1 bench/other/regex.lua 2>&1 | filter regex-O1 | tee -a compile-output.txt + valgrind --tool=callgrind ./luau-compile --null -O2 bench/other/regex.lua 2>&1 | filter regex-O2 | tee -a compile-output.txt + valgrind --tool=callgrind ./luau-compile --codegennull -O2 bench/other/regex.lua 2>&1 | filter regex-O2-codegen | tee -a compile-output.txt - name: Checkout benchmark results uses: actions/checkout@v3 @@ -124,7 +124,7 @@ jobs: - name: Store results (compile) uses: Roblox/rhysd-github-action-benchmark@v-luau with: - name: luau --compile + name: luau-compile tool: "benchmarkluau" output-file-path: ./compile-output.txt external-data-json-path: ./gh-pages/compile.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b084a4f5..b278c437 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,9 +42,10 @@ jobs: ./luau-tests -ts=Conformance --codegen -O2 --fflags=true - name: make cli run: | - make -j2 config=sanitize werror=1 luau luau-analyze # match config with tests to improve build time + make -j2 config=sanitize werror=1 luau luau-analyze luau-compile # match config with tests to improve build time ./luau tests/conformance/assert.lua ./luau-analyze tests/conformance/assert.lua + ./luau-compile tests/conformance/assert.lua windows: runs-on: windows-latest @@ -76,9 +77,10 @@ jobs: - name: cmake cli shell: bash # necessary for fail-fast run: | - cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI --config Debug # match config with tests to improve build time + cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI Luau.Compile.CLI --config Debug # match config with tests to improve build time Debug/luau tests/conformance/assert.lua Debug/luau-analyze tests/conformance/assert.lua + Debug/luau-compile tests/conformance/assert.lua coverage: runs-on: ubuntu-20.04 diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 5fe7f792..52b2eb0c 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -38,7 +38,7 @@ jobs: - name: configure run: cmake . -DCMAKE_BUILD_TYPE=Release - name: build - run: cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI --config Release -j 2 + run: cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI Luau.Compile.CLI --config Release -j 2 - name: pack if: matrix.os.name != 'windows' run: zip luau-${{matrix.os.name}}.zip luau* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 497483a7..f796a7c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - name: configure run: cmake . -DCMAKE_BUILD_TYPE=Release - name: build - run: cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI --config Release -j 2 + run: cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI Luau.Compile.CLI --config Release -j 2 - uses: actions/upload-artifact@v2 if: matrix.os.name != 'windows' with: diff --git a/Analysis/src/AstJsonEncoder.cpp b/Analysis/src/AstJsonEncoder.cpp index a964c785..f2943c4d 100644 --- a/Analysis/src/AstJsonEncoder.cpp +++ b/Analysis/src/AstJsonEncoder.cpp @@ -752,6 +752,7 @@ struct AstJsonEncoder : public AstVisitor if (node->superName) write("superName", *node->superName); PROP(props); + PROP(indexer); }); } diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index 821f6c26..07dba921 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -22,6 +22,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit); LUAU_FASTFLAG(DebugLuauMagicTypes); +LUAU_FASTFLAG(LuauParseDeclareClassIndexer); namespace Luau { @@ -1157,6 +1158,23 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareC scope->exportedTypeBindings[className] = TypeFun{{}, classTy}; + if (FFlag::LuauParseDeclareClassIndexer && declaredClass->indexer) + { + RecursionCounter counter{&recursionCount}; + + if (recursionCount >= FInt::LuauCheckRecursionLimit) + { + reportCodeTooComplex(declaredClass->indexer->location); + } + else + { + ctv->indexer = TableIndexer{ + resolveType(scope, declaredClass->indexer->indexType, /* inTypeArguments */ false), + resolveType(scope, declaredClass->indexer->resultType, /* inTypeArguments */ false), + }; + } + } + for (const AstDeclaredClassProp& prop : declaredClass->props) { Name propName(prop.name.value); diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index dba95479..3a1217bf 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -13,6 +13,8 @@ #include +LUAU_FASTFLAG(LuauParseDeclareClassIndexer); + static char* allocateString(Luau::Allocator& allocator, std::string_view contents) { char* result = (char*)allocator.allocate(contents.size() + 1); @@ -227,7 +229,17 @@ public: idx++; } - return allocator->alloc(Location(), props); + AstTableIndexer* indexer = nullptr; + if (FFlag::LuauParseDeclareClassIndexer && ctv.indexer) + { + RecursionCounter counter(&count); + + indexer = allocator->alloc(); + indexer->indexType = Luau::visit(*this, ctv.indexer->indexType->ty); + indexer->resultType = Luau::visit(*this, ctv.indexer->indexResultType->ty); + } + + return allocator->alloc(Location(), props, indexer); } AstType* operator()(const FunctionType& ftv) diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 5127febe..a3d91704 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -41,6 +41,7 @@ LUAU_FASTFLAGVARIABLE(LuauTypecheckTypeguards, false) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauTypecheckClassTypeIndexers, false) LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false) +LUAU_FASTFLAG(LuauParseDeclareClassIndexer) namespace Luau { @@ -1757,6 +1758,9 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& if (!ctv->metatable) ice("No metatable for declared class"); + if (const auto& indexer = declaredClass.indexer; FFlag::LuauParseDeclareClassIndexer && indexer) + ctv->indexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); + TableType* metatable = getMutable(*ctv->metatable); for (const AstDeclaredClassProp& prop : declaredClass.props) { diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index a486ad0f..f9f9ab41 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -801,12 +801,20 @@ struct AstDeclaredClassProp bool isMethod = false; }; +struct AstTableIndexer +{ + AstType* indexType; + AstType* resultType; + Location location; +}; + class AstStatDeclareClass : public AstStat { public: LUAU_RTTI(AstStatDeclareClass) - AstStatDeclareClass(const Location& location, const AstName& name, std::optional superName, const AstArray& props); + AstStatDeclareClass(const Location& location, const AstName& name, std::optional superName, const AstArray& props, + AstTableIndexer* indexer = nullptr); void visit(AstVisitor* visitor) override; @@ -814,6 +822,7 @@ public: std::optional superName; AstArray props; + AstTableIndexer* indexer; }; class AstType : public AstNode @@ -862,13 +871,6 @@ struct AstTableProp AstType* type; }; -struct AstTableIndexer -{ - AstType* indexType; - AstType* resultType; - Location location; -}; - class AstTypeTable : public AstType { public: diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index d2c552a3..3c87e36c 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -714,12 +714,13 @@ void AstStatDeclareFunction::visit(AstVisitor* visitor) } } -AstStatDeclareClass::AstStatDeclareClass( - const Location& location, const AstName& name, std::optional superName, const AstArray& props) +AstStatDeclareClass::AstStatDeclareClass(const Location& location, const AstName& name, std::optional superName, + const AstArray& props, AstTableIndexer* indexer) : AstStat(ClassIndex(), location) , name(name) , superName(superName) , props(props) + , indexer(indexer) { } diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 7cae609d..cc5d7b38 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -13,6 +13,7 @@ // See docs/SyntaxChanges.md for an explanation. LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) +LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false) #define ERROR_INVALID_INTERP_DOUBLE_BRACE "Double braces are not permitted within interpolated strings. Did you mean '\\{'?" @@ -877,6 +878,7 @@ AstStat* Parser::parseDeclaration(const Location& start) } TempVector props(scratchDeclaredClassProps); + AstTableIndexer* indexer = nullptr; while (lexer.current().type != Lexeme::ReservedEnd) { @@ -885,7 +887,8 @@ AstStat* Parser::parseDeclaration(const Location& start) { props.push_back(parseDeclaredClassMethod()); } - else if (lexer.current().type == '[') + else if (lexer.current().type == '[' && (!FFlag::LuauParseDeclareClassIndexer || lexer.lookahead().type == Lexeme::RawString || + lexer.lookahead().type == Lexeme::QuotedString)) { const Lexeme begin = lexer.current(); nextLexeme(); // [ @@ -904,6 +907,22 @@ AstStat* Parser::parseDeclaration(const Location& start) else report(begin.location, "String literal contains malformed escape sequence"); } + else if (lexer.current().type == '[' && FFlag::LuauParseDeclareClassIndexer) + { + if (indexer) + { + // maybe we don't need to parse the entire badIndexer... + // however, we either have { or [ to lint, not the entire table type or the bad indexer. + AstTableIndexer* badIndexer = parseTableIndexer(); + + // we lose all additional indexer expressions from the AST after error recovery here + report(badIndexer->location, "Cannot have more than one class indexer"); + } + else + { + indexer = parseTableIndexer(); + } + } else { Name propName = parseName("property name"); @@ -916,7 +935,7 @@ AstStat* Parser::parseDeclaration(const Location& start) Location classEnd = lexer.current().location; nextLexeme(); // skip past `end` - return allocator.alloc(Location(classStart, classEnd), className.name, superName, copy(props)); + return allocator.alloc(Location(classStart, classEnd), className.name, superName, copy(props), indexer); } else if (std::optional globalName = parseNameOpt("global variable name")) { diff --git a/README.md b/README.md index a0ce6f76..06b23253 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ You can install and run Luau by downloading the compiled binaries from [a recent Alternatively, you can use one of the packaged distributions (note that these are not maintained by Luau development team): - macOS: [Install Homebrew](https://docs.brew.sh/Installation) and run `brew install luau` -- Arch Linux: Run `pacman -S luau` +- Arch Linux: From the AUR (Arch Linux User Repository), install one of these packages via a AUR helper or manually (by cloning their repo and using ``makepkg``): [luau](https://aur.archlinux.org/packages/luau) (manual build), [luau-git](https://aur.archlinux.org/packages/luau-git) (manual build by cloning this repo), or [luau-bin](https://aur.archlinux.org/packages/luau-bin) (pre-built binaries from releases) - Alpine Linux: [Enable community repositories](https://wiki.alpinelinux.org/w/index.php?title=Enable_Community_Repository) and run `apk add luau` After installing, you will want to validate the installation was successful by running the test case [here](https://luau-lang.org/getting-started). diff --git a/tests/AstJsonEncoder.test.cpp b/tests/AstJsonEncoder.test.cpp index 82577bed..a264d0e7 100644 --- a/tests/AstJsonEncoder.test.cpp +++ b/tests/AstJsonEncoder.test.cpp @@ -432,11 +432,11 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatDeclareClass") REQUIRE(2 == root->body.size); std::string_view expected1 = - R"({"type":"AstStatDeclareClass","location":"1,22 - 4,11","name":"Foo","props":[{"name":"prop","type":"AstDeclaredClassProp","luauType":{"type":"AstTypeReference","location":"2,18 - 2,24","name":"number","nameLocation":"2,18 - 2,24","parameters":[]}},{"name":"method","type":"AstDeclaredClassProp","luauType":{"type":"AstTypeFunction","location":"3,21 - 4,11","generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"3,39 - 3,45","name":"number","nameLocation":"3,39 - 3,45","parameters":[]}]},"returnTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"3,48 - 3,54","name":"string","nameLocation":"3,48 - 3,54","parameters":[]}]}}}]})"; + R"({"type":"AstStatDeclareClass","location":"1,22 - 4,11","name":"Foo","props":[{"name":"prop","type":"AstDeclaredClassProp","luauType":{"type":"AstTypeReference","location":"2,18 - 2,24","name":"number","nameLocation":"2,18 - 2,24","parameters":[]}},{"name":"method","type":"AstDeclaredClassProp","luauType":{"type":"AstTypeFunction","location":"3,21 - 4,11","generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"3,39 - 3,45","name":"number","nameLocation":"3,39 - 3,45","parameters":[]}]},"returnTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"3,48 - 3,54","name":"string","nameLocation":"3,48 - 3,54","parameters":[]}]}}}],"indexer":null})"; CHECK(toJson(root->body.data[0]) == expected1); std::string_view expected2 = - R"({"type":"AstStatDeclareClass","location":"6,22 - 8,11","name":"Bar","superName":"Foo","props":[{"name":"prop2","type":"AstDeclaredClassProp","luauType":{"type":"AstTypeReference","location":"7,19 - 7,25","name":"string","nameLocation":"7,19 - 7,25","parameters":[]}}]})"; + R"({"type":"AstStatDeclareClass","location":"6,22 - 8,11","name":"Bar","superName":"Foo","props":[{"name":"prop2","type":"AstDeclaredClassProp","luauType":{"type":"AstTypeReference","location":"7,19 - 7,25","name":"string","nameLocation":"7,19 - 7,25","parameters":[]}}],"indexer":null})"; CHECK(toJson(root->body.data[1]) == expected2); } diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index 1335b6f4..a8738ac8 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -54,7 +54,8 @@ TEST_SUITE_BEGIN("AllocatorTests"); TEST_CASE("allocator_can_be_moved") { Counter* c = nullptr; - auto inner = [&]() { + auto inner = [&]() + { Luau::Allocator allocator; c = allocator.alloc(); Luau::Allocator moved{std::move(allocator)}; @@ -921,7 +922,8 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_double_brace_mid") TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_end_brace") { - auto columnOfEndBraceError = [this](const char* code) { + auto columnOfEndBraceError = [this](const char* code) + { try { parse(code); @@ -1882,6 +1884,44 @@ TEST_CASE_FIXTURE(Fixture, "class_method_properties") CHECK_EQ(2, klass2->props.size); } +TEST_CASE_FIXTURE(Fixture, "class_indexer") +{ + ScopedFastFlag LuauParseDeclareClassIndexer("LuauParseDeclareClassIndexer", true); + + AstStatBlock* stat = parseEx(R"( + declare class Foo + prop: boolean + [string]: number + end + )") + .root; + + REQUIRE_EQ(stat->body.size, 1); + + AstStatDeclareClass* declaredClass = stat->body.data[0]->as(); + REQUIRE(declaredClass); + REQUIRE(declaredClass->indexer); + REQUIRE(declaredClass->indexer->indexType->is()); + CHECK(declaredClass->indexer->indexType->as()->name == "string"); + REQUIRE(declaredClass->indexer->resultType->is()); + CHECK(declaredClass->indexer->resultType->as()->name == "number"); + + const ParseResult p1 = matchParseError(R"( + declare class Foo + [string]: number + -- can only have one indexer + [number]: number + end + )", + "Cannot have more than one class indexer"); + + REQUIRE_EQ(1, p1.root->body.size); + + AstStatDeclareClass* klass = p1.root->body.data[0]->as(); + REQUIRE(klass != nullptr); + CHECK(klass->indexer); +} + TEST_CASE_FIXTURE(Fixture, "parse_variadics") { //clang-format off @@ -2347,7 +2387,8 @@ public: TEST_CASE_FIXTURE(Fixture, "recovery_of_parenthesized_expressions") { - auto checkAstEquivalence = [this](const char* codeWithErrors, const char* code) { + auto checkAstEquivalence = [this](const char* codeWithErrors, const char* code) + { try { parse(codeWithErrors); @@ -2367,7 +2408,8 @@ TEST_CASE_FIXTURE(Fixture, "recovery_of_parenthesized_expressions") CHECK_EQ(counterWithErrors.count, counter.count); }; - auto checkRecovery = [this, checkAstEquivalence](const char* codeWithErrors, const char* code, unsigned expectedErrorCount) { + auto checkRecovery = [this, checkAstEquivalence](const char* codeWithErrors, const char* code, unsigned expectedErrorCount) + { try { parse(codeWithErrors); diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index d6799757..0ca9bd73 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -394,6 +394,36 @@ TEST_CASE_FIXTURE(Fixture, "class_definition_string_props") CHECK_EQ(toString(requireType("y")), "string"); } + +TEST_CASE_FIXTURE(Fixture, "class_definition_indexer") +{ + ScopedFastFlag LuauParseDeclareClassIndexer("LuauParseDeclareClassIndexer", true); + ScopedFastFlag LuauTypecheckClassTypeIndexers("LuauTypecheckClassTypeIndexers", true); + + loadDefinition(R"( + declare class Foo + [number]: string + end + )"); + + CheckResult result = check(R"( + local x: Foo + local y = x[1] + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + const ClassType* ctv = get(requireType("x")); + REQUIRE(ctv != nullptr); + + REQUIRE(bool(ctv->indexer)); + + CHECK_EQ(*ctv->indexer->indexType, *builtinTypes->numberType); + CHECK_EQ(*ctv->indexer->indexResultType, *builtinTypes->stringType); + + CHECK_EQ(toString(requireType("y")), "string"); +} + TEST_CASE_FIXTURE(Fixture, "class_definitions_reference_other_classes") { unfreeze(frontend.globals.globalTypes);