diff --git a/Analysis/include/Luau/InsertionOrderedMap.h b/Analysis/include/Luau/InsertionOrderedMap.h new file mode 100644 index 00000000..66d6b2ab --- /dev/null +++ b/Analysis/include/Luau/InsertionOrderedMap.h @@ -0,0 +1,134 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" + +#include +#include +#include +#include + +namespace Luau +{ + +template +struct InsertionOrderedMap +{ + static_assert(std::is_trivially_copyable_v, "key must be trivially copyable"); + + private: + using vec = std::vector>; + + public: + using iterator = typename vec::iterator; + using const_iterator = typename vec::const_iterator; + + void insert(K k, V v) + { + if (indices.count(k) != 0) + return; + + pairs.push_back(std::make_pair(k, std::move(v))); + indices[k] = pairs.size() - 1; + } + + void clear() + { + pairs.clear(); + indices.clear(); + } + + size_t size() const + { + LUAU_ASSERT(pairs.size() == indices.size()); + return pairs.size(); + } + + bool contains(const K& k) const + { + return indices.count(k) > 0; + } + + const V* get(const K& k) const + { + auto it = indices.find(k); + if (it == indices.end()) + return nullptr; + else + return &pairs.at(it->second).second; + } + + V* get(const K& k) + { + auto it = indices.find(k); + if (it == indices.end()) + return nullptr; + else + return &pairs.at(it->second).second; + } + + const_iterator begin() const + { + return pairs.begin(); + } + + const_iterator end() const + { + return pairs.end(); + } + + iterator begin() + { + return pairs.begin(); + } + + iterator end() + { + return pairs.end(); + } + + const_iterator find(K k) const + { + auto indicesIt = indices.find(k); + if (indicesIt == indices.end()) + return end(); + else + return begin() + indicesIt->second; + } + + iterator find(K k) + { + auto indicesIt = indices.find(k); + if (indicesIt == indices.end()) + return end(); + else + return begin() + indicesIt->second; + } + + void erase(iterator it) + { + if (it == pairs.end()) + return; + + K k = it->first; + auto indexIt = indices.find(k); + if (indexIt == indices.end()) + return; + + size_t removed = indexIt->second; + indices.erase(indexIt); + pairs.erase(it); + + for (auto& [_, index] : indices) + { + if (index > removed) + --index; + } + } + +private: + vec pairs; + std::unordered_map indices; +}; + +} diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index 7d415e92..72be0832 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -64,6 +64,7 @@ public: bool operator==(const TypeIds& there) const; size_t getHash() const; + bool isNever() const; }; } // namespace Luau @@ -269,12 +270,24 @@ struct NormalizedType NormalizedType& operator=(NormalizedType&&) = default; // IsType functions + /// Returns true if the type is exactly a number. Behaves like Type::isNumber() + bool isExactlyNumber() const; - /// Returns true if the type is a subtype of function. This includes any and unknown. - bool isFunction() const; + /// Returns true if the type is a subtype of string(it could be a singleton). Behaves like Type::isString() + bool isSubtypeOfString() const; - /// Returns true if the type is a subtype of number. This includes any and unknown. - bool isNumber() const; + // Helpers that improve readability of the above (they just say if the component is present) + bool hasTops() const; + bool hasBooleans() const; + bool hasClasses() const; + bool hasErrors() const; + bool hasNils() const; + bool hasNumbers() const; + bool hasStrings() const; + bool hasThreads() const; + bool hasTables() const; + bool hasFunctions() const; + bool hasTyvars() const; }; diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index c14f10e5..821f6c26 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -16,6 +16,7 @@ #include "Luau/TypeFamily.h" #include "Luau/Simplify.h" #include "Luau/VisitType.h" +#include "Luau/InsertionOrderedMap.h" #include @@ -196,7 +197,7 @@ struct RefinementPartition bool shouldAppendNilType = false; }; -using RefinementContext = std::unordered_map; +using RefinementContext = InsertionOrderedMap; static void unionRefinements(NotNull builtinTypes, NotNull arena, const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints) @@ -229,8 +230,9 @@ static void unionRefinements(NotNull builtinTypes, NotNullsecond.discriminantTypes.size() == 1 ? rhsIt->second.discriminantTypes[0] : intersect(rhsIt->second.discriminantTypes); - dest[def].discriminantTypes.push_back(simplifyUnion(builtinTypes, arena, leftDiscriminantTy, rightDiscriminantTy).result); - dest[def].shouldAppendNilType |= partition.shouldAppendNilType || rhsIt->second.shouldAppendNilType; + dest.insert(def, {}); + dest.get(def)->discriminantTypes.push_back(simplifyUnion(builtinTypes, arena, leftDiscriminantTy, rightDiscriminantTy).result); + dest.get(def)->shouldAppendNilType |= partition.shouldAppendNilType || rhsIt->second.shouldAppendNilType; } } @@ -285,11 +287,12 @@ static void computeRefinement(NotNull builtinTypes, NotNullbreadcrumb->def].discriminantTypes.push_back(discriminantTy); + uncommittedRefis.insert(proposition->breadcrumb->def, {}); + uncommittedRefis.get(proposition->breadcrumb->def)->discriminantTypes.push_back(discriminantTy); // When the top-level expression is `t[x]`, we want to refine it into `nil`, not `never`. if ((sense || !eq) && getMetadata(proposition->breadcrumb)) - uncommittedRefis[proposition->breadcrumb->def].shouldAppendNilType = true; + uncommittedRefis.get(proposition->breadcrumb->def)->shouldAppendNilType = true; for (NullableBreadcrumbId current = proposition->breadcrumb; current && current->previous; current = current->previous) { @@ -302,17 +305,20 @@ static void computeRefinement(NotNull builtinTypes, NotNullprop, Property{discriminantTy}}}; discriminantTy = arena->addType(TableType{std::move(props), std::nullopt, TypeLevel{}, scope.get(), TableState::Sealed}); - uncommittedRefis[current->previous->def].discriminantTypes.push_back(discriminantTy); + uncommittedRefis.insert(current->previous->def, {}); + uncommittedRefis.get(current->previous->def)->discriminantTypes.push_back(discriminantTy); } } // And now it's time to commit it. for (auto& [def, partition] : uncommittedRefis) { - for (TypeId discriminantTy : partition.discriminantTypes) - (*refis)[def].discriminantTypes.push_back(discriminantTy); + (*refis).insert(def, {}); - (*refis)[def].shouldAppendNilType |= partition.shouldAppendNilType; + for (TypeId discriminantTy : partition.discriminantTypes) + (*refis).get(def)->discriminantTypes.push_back(discriminantTy); + + (*refis).get(def)->shouldAppendNilType |= partition.shouldAppendNilType; } } } diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index f96f54b6..c9ac8cc9 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -785,7 +785,8 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNullnormalize(leftType); if (hasTypeInIntersection(leftType) && force) asMutable(leftType)->ty.emplace(anyPresent ? builtinTypes->anyType : builtinTypes->numberType); - if (normLeftTy && normLeftTy->isNumber()) + // We want to check if the left type has tops because `any` is a valid type for the lhs + if (normLeftTy && (normLeftTy->isExactlyNumber() || get(normLeftTy->tops))) { unify(leftType, rightType, constraint->scope); asMutable(resultType)->ty.emplace(anyPresent ? builtinTypes->anyType : leftType); @@ -805,9 +806,11 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull(leftType) && force) asMutable(leftType)->ty.emplace(anyPresent ? builtinTypes->anyType : builtinTypes->stringType); - if (isString(leftType)) + const NormalizedType* leftNormTy = normalizer->normalize(leftType); + if (leftNormTy && leftNormTy->isSubtypeOfString()) { unify(leftType, rightType, constraint->scope); asMutable(resultType)->ty.emplace(anyPresent ? builtinTypes->anyType : leftType); @@ -823,14 +826,33 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull(leftType) || - get(rightType)) + { + const NormalizedType* lt = normalizer->normalize(leftType); + const NormalizedType* rt = normalizer->normalize(rightType); + // If the lhs is any, comparisons should be valid. + if (lt && rt && (lt->isExactlyNumber() || get(lt->tops)) && rt->isExactlyNumber()) + { + asMutable(resultType)->ty.emplace(builtinTypes->booleanType); + unblock(resultType); + return true; + } + + if (lt && rt && (lt->isSubtypeOfString() || get(lt->tops)) && rt->isSubtypeOfString()) + { + asMutable(resultType)->ty.emplace(builtinTypes->booleanType); + unblock(resultType); + return true; + } + + + if (get(leftType) || get(rightType)) { asMutable(resultType)->ty.emplace(builtinTypes->booleanType); unblock(resultType); @@ -838,6 +860,8 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull builtinTypes, TypePackId) return builtinTypes->errorRecoveryTypePack(); } -template +template bool ConstraintSolver::tryUnify(NotNull constraint, TID subTy, TID superTy) { Unifier u{normalizer, constraint->scope, constraint->location, Covariant}; diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 062050fa..409e2eb3 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -35,7 +35,6 @@ LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 100) LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false) -LUAU_FASTFLAGVARIABLE(LuauTypeCheckerUseCorrectScope, false) namespace Luau { @@ -1196,8 +1195,7 @@ ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vect } else { - TypeChecker typeChecker(FFlag::LuauTypeCheckerUseCorrectScope ? (forAutocomplete ? globalsForAutocomplete.globalScope : globals.globalScope) - : globals.globalScope, + TypeChecker typeChecker(forAutocomplete ? globalsForAutocomplete.globalScope : globals.globalScope, forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver, builtinTypes, &iceHandler); if (prepareModuleScope) diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 6a78bc66..3af7e857 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -108,6 +108,14 @@ size_t TypeIds::getHash() const return hash; } +bool TypeIds::isNever() const +{ + return std::all_of(begin(), end(), [&](TypeId i) { + // If each typeid is never, then I guess typeid's is also never? + return get(i) != nullptr; + }); +} + bool TypeIds::operator==(const TypeIds& there) const { return hash == there.hash && types == there.types; @@ -228,14 +236,72 @@ NormalizedType::NormalizedType(NotNull builtinTypes) { } -bool NormalizedType::isFunction() const +bool NormalizedType::isExactlyNumber() const { - return !get(tops) || !functions.parts.empty(); + return hasNumbers() && !hasTops() && !hasBooleans() && !hasClasses() && !hasErrors() && !hasNils() && !hasStrings() && !hasThreads() && + !hasTables() && !hasFunctions() && !hasTyvars(); } -bool NormalizedType::isNumber() const +bool NormalizedType::isSubtypeOfString() const { - return !get(tops) || !get(numbers); + return hasStrings() && !hasTops() && !hasBooleans() && !hasClasses() && !hasErrors() && !hasNils() && !hasNumbers() && !hasThreads() && + !hasTables() && !hasFunctions() && !hasTyvars(); +} + +bool NormalizedType::hasTops() const +{ + return !get(tops); +} + + +bool NormalizedType::hasBooleans() const +{ + return !get(booleans); +} + +bool NormalizedType::hasClasses() const +{ + return !classes.isNever(); +} + +bool NormalizedType::hasErrors() const +{ + return !get(errors); +} + +bool NormalizedType::hasNils() const +{ + return !get(nils); +} + +bool NormalizedType::hasNumbers() const +{ + return !get(numbers); +} + +bool NormalizedType::hasStrings() const +{ + return !strings.isNever(); +} + +bool NormalizedType::hasThreads() const +{ + return !get(threads); +} + +bool NormalizedType::hasTables() const +{ + return !tables.isNever(); +} + +bool NormalizedType::hasFunctions() const +{ + return !functions.isNever(); +} + +bool NormalizedType::hasTyvars() const +{ + return !tyvars.empty(); } static bool isShallowInhabited(const NormalizedType& norm) diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index c1146b5c..0a9e9b64 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -1067,9 +1067,7 @@ struct TypeChecker2 std::vector argLocs; argLocs.reserve(call->args.size + 1); - TypeId* maybeOriginalCallTy = module->astOriginalCallTypes.find(call); - TypeId* maybeSelectedOverload = module->astOverloadResolvedTypes.find(call); - + auto maybeOriginalCallTy = module->astOriginalCallTypes.find(call); if (!maybeOriginalCallTy) return; @@ -1093,8 +1091,19 @@ struct TypeChecker2 return; } } - else if (get(originalCallTy) || get(originalCallTy)) + else if (get(originalCallTy)) { + // ok. + } + else if (get(originalCallTy)) + { + auto norm = normalizer.normalize(originalCallTy); + if (!norm) + return reportError(CodeTooComplex{}, call->location); + + // NormalizedType::hasFunction returns true if its' tops component is `unknown`, but for soundness we want the reverse. + if (get(norm->tops) || !norm->hasFunctions()) + return reportError(CannotCallNonFunction{originalCallTy}, call->func->location); } else if (auto utv = get(originalCallTy)) { @@ -1164,7 +1173,7 @@ struct TypeChecker2 TypePackId expectedArgTypes = testArena.addTypePack(args); - if (maybeSelectedOverload) + if (auto maybeSelectedOverload = module->astOverloadResolvedTypes.find(call)) { // This overload might not work still: the constraint solver will // pass the type checker an instantiated function type that matches @@ -1414,7 +1423,7 @@ struct TypeChecker2 { // Nothing } - else if (!normalizedFnTy->isFunction()) + else if (!normalizedFnTy->hasFunctions()) { ice->ice("Internal error: Lambda has non-function type " + toString(inferredFnTy), fn->location); } @@ -1793,12 +1802,14 @@ struct TypeChecker2 case AstExprBinary::Op::CompareGt: case AstExprBinary::Op::CompareLe: case AstExprBinary::Op::CompareLt: - if (isNumber(leftType)) + { + const NormalizedType* leftTyNorm = normalizer.normalize(leftType); + if (leftTyNorm && leftTyNorm->isExactlyNumber()) { reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType)); return builtinTypes->numberType; } - else if (isString(leftType)) + else if (leftTyNorm && leftTyNorm->isSubtypeOfString()) { reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->stringType)); return builtinTypes->stringType; @@ -1810,6 +1821,8 @@ struct TypeChecker2 expr->location); return builtinTypes->errorRecoveryType(); } + } + case AstExprBinary::Op::And: case AstExprBinary::Op::Or: case AstExprBinary::Op::CompareEq: diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index e68187fd..4adf0f8a 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -346,7 +346,6 @@ TypeFamilyReductionResult addFamilyFn(std::vector typeParams, st TypeId rhsTy = log->follow(typeParams.at(1)); const NormalizedType* normLhsTy = normalizer->normalize(lhsTy); const NormalizedType* normRhsTy = normalizer->normalize(rhsTy); - if (!normLhsTy || !normRhsTy) { return {std::nullopt, false, {}, {}}; @@ -355,7 +354,7 @@ TypeFamilyReductionResult addFamilyFn(std::vector typeParams, st { return {builtins->anyType, false, {}, {}}; } - else if (normLhsTy->isNumber() && normRhsTy->isNumber()) + else if ((normLhsTy->hasNumbers() || normLhsTy->hasTops()) && (normRhsTy->hasNumbers() || normRhsTy->hasTops())) { return {builtins->numberType, false, {}, {}}; } diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp new file mode 100644 index 00000000..293809d0 --- /dev/null +++ b/CLI/Compile.cpp @@ -0,0 +1,346 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lua.h" +#include "lualib.h" + +#include "Luau/CodeGen.h" +#include "Luau/Compiler.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/Parser.h" +#include "Luau/TimeTrace.h" + +#include "FileUtils.h" +#include "Flags.h" + +#include + +#ifdef _WIN32 +#include +#include +#endif + +LUAU_FASTFLAG(DebugLuauTimeTracing) + +enum class CompileFormat +{ + Text, + Binary, + Remarks, + Codegen, // Prints annotated native code including IR and assembly + CodegenAsm, // Prints annotated native code assembly + CodegenIr, // Prints annotated native code IR + CodegenVerbose, // Prints annotated native code including IR, assembly and outlined code + CodegenNull, + Null +}; + +struct GlobalOptions +{ + int optimizationLevel = 1; + int debugLevel = 1; +} globalOptions; + +static Luau::CompileOptions copts() +{ + Luau::CompileOptions result = {}; + result.optimizationLevel = globalOptions.optimizationLevel; + result.debugLevel = globalOptions.debugLevel; + + return result; +} + +static std::optional getCompileFormat(const char* name) +{ + if (strcmp(name, "text") == 0) + return CompileFormat::Text; + else if (strcmp(name, "binary") == 0) + return CompileFormat::Binary; + else if (strcmp(name, "text") == 0) + return CompileFormat::Text; + else if (strcmp(name, "remarks") == 0) + return CompileFormat::Remarks; + else if (strcmp(name, "codegen") == 0) + return CompileFormat::Codegen; + else if (strcmp(name, "codegenasm") == 0) + return CompileFormat::CodegenAsm; + else if (strcmp(name, "codegenir") == 0) + return CompileFormat::CodegenIr; + else if (strcmp(name, "codegenverbose") == 0) + return CompileFormat::CodegenVerbose; + else if (strcmp(name, "codegennull") == 0) + return CompileFormat::CodegenNull; + else if (strcmp(name, "null") == 0) + return CompileFormat::Null; + else + return std::nullopt; +} + +static void report(const char* name, const Luau::Location& location, const char* type, const char* message) +{ + fprintf(stderr, "%s(%d,%d): %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, type, message); +} + +static void reportError(const char* name, const Luau::ParseError& error) +{ + report(name, error.getLocation(), "SyntaxError", error.what()); +} + +static void reportError(const char* name, const Luau::CompileError& error) +{ + report(name, error.getLocation(), "CompileError", error.what()); +} + +static std::string getCodegenAssembly(const char* name, const std::string& bytecode, Luau::CodeGen::AssemblyOptions options) +{ + std::unique_ptr globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + if (luau_load(L, name, bytecode.data(), bytecode.size(), 0) == 0) + return Luau::CodeGen::getAssembly(L, -1, options); + + fprintf(stderr, "Error loading bytecode %s\n", name); + return ""; +} + +static void annotateInstruction(void* context, std::string& text, int fid, int instpos) +{ + Luau::BytecodeBuilder& bcb = *(Luau::BytecodeBuilder*)context; + + bcb.annotateInstruction(text, fid, instpos); +} + +struct CompileStats +{ + size_t lines; + size_t bytecode; + size_t codegen; + + double readTime; + double miscTime; + double parseTime; + double compileTime; + double codegenTime; +}; + +static double recordDeltaTime(double& timer) +{ + double now = Luau::TimeTrace::getClock(); + double delta = now - timer; + timer = now; + return delta; +} + +static bool compileFile(const char* name, CompileFormat format, CompileStats& stats) +{ + double currts = Luau::TimeTrace::getClock(); + + std::optional source = readFile(name); + if (!source) + { + fprintf(stderr, "Error opening %s\n", name); + return false; + } + + stats.readTime += recordDeltaTime(currts); + + // NOTE: Normally, you should use Luau::compile or luau_compile (see lua_require as an example) + // This function is much more complicated because it supports many output human-readable formats through internal interfaces + + try + { + Luau::BytecodeBuilder bcb; + + Luau::CodeGen::AssemblyOptions options; + options.outputBinary = format == CompileFormat::CodegenNull; + + if (!options.outputBinary) + { + options.includeAssembly = format != CompileFormat::CodegenIr; + options.includeIr = format != CompileFormat::CodegenAsm; + options.includeOutlinedCode = format == CompileFormat::CodegenVerbose; + } + + options.annotator = annotateInstruction; + options.annotatorContext = &bcb; + + if (format == CompileFormat::Text) + { + bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals | + Luau::BytecodeBuilder::Dump_Remarks); + bcb.setDumpSource(*source); + } + else if (format == CompileFormat::Remarks) + { + bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Remarks); + bcb.setDumpSource(*source); + } + else if (format == CompileFormat::Codegen || format == CompileFormat::CodegenAsm || format == CompileFormat::CodegenIr || + format == CompileFormat::CodegenVerbose) + { + bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals | + Luau::BytecodeBuilder::Dump_Remarks); + bcb.setDumpSource(*source); + } + + stats.miscTime += recordDeltaTime(currts); + + Luau::Allocator allocator; + Luau::AstNameTable names(allocator); + Luau::ParseResult result = Luau::Parser::parse(source->c_str(), source->size(), names, allocator); + + if (!result.errors.empty()) + throw Luau::ParseErrors(result.errors); + + stats.lines += result.lines; + stats.parseTime += recordDeltaTime(currts); + + Luau::compileOrThrow(bcb, result, names, copts()); + stats.bytecode += bcb.getBytecode().size(); + stats.compileTime += recordDeltaTime(currts); + + switch (format) + { + case CompileFormat::Text: + printf("%s", bcb.dumpEverything().c_str()); + break; + case CompileFormat::Remarks: + printf("%s", bcb.dumpSourceRemarks().c_str()); + break; + case CompileFormat::Binary: + fwrite(bcb.getBytecode().data(), 1, bcb.getBytecode().size(), stdout); + break; + case CompileFormat::Codegen: + case CompileFormat::CodegenAsm: + case CompileFormat::CodegenIr: + case CompileFormat::CodegenVerbose: + printf("%s", getCodegenAssembly(name, bcb.getBytecode(), options).c_str()); + break; + case CompileFormat::CodegenNull: + stats.codegen += getCodegenAssembly(name, bcb.getBytecode(), options).size(); + stats.codegenTime += recordDeltaTime(currts); + break; + case CompileFormat::Null: + break; + } + + return true; + } + catch (Luau::ParseErrors& e) + { + for (auto& error : e.getErrors()) + reportError(name, error); + return false; + } + catch (Luau::CompileError& e) + { + reportError(name, e); + return false; + } +} + +static void displayHelp(const char* argv0) +{ + printf("Usage: %s [--mode] [options] [file list]\n", argv0); + printf("\n"); + printf("Available modes:\n"); + printf(" binary, text, remarks, codegen\n"); + printf("\n"); + printf("Available options:\n"); + printf(" -h, --help: Display this usage message.\n"); + printf(" -O: compile with optimization level n (default 1, n should be between 0 and 2).\n"); + printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); + printf(" --timetrace: record compiler time tracing information into trace.json\n"); +} + +static int assertionHandler(const char* expr, const char* file, int line, const char* function) +{ + printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr); + return 1; +} + +int main(int argc, char** argv) +{ + Luau::assertHandler() = assertionHandler; + + setLuauFlagsDefault(); + + CompileFormat compileFormat = CompileFormat::Text; + + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) + { + displayHelp(argv[0]); + return 0; + } + else if (strncmp(argv[i], "-O", 2) == 0) + { + int level = atoi(argv[i] + 2); + if (level < 0 || level > 2) + { + fprintf(stderr, "Error: Optimization level must be between 0 and 2 inclusive.\n"); + return 1; + } + globalOptions.optimizationLevel = level; + } + else if (strncmp(argv[i], "-g", 2) == 0) + { + int level = atoi(argv[i] + 2); + if (level < 0 || level > 2) + { + fprintf(stderr, "Error: Debug level must be between 0 and 2 inclusive.\n"); + return 1; + } + globalOptions.debugLevel = level; + } + else if (strcmp(argv[i], "--timetrace") == 0) + { + FFlag::DebugLuauTimeTracing.value = true; + } + else if (strncmp(argv[i], "--fflags=", 9) == 0) + { + setLuauFlags(argv[i] + 9); + } + else if (argv[i][0] == '-' && argv[i][1] == '-' && getCompileFormat(argv[i] + 2)) + { + compileFormat = *getCompileFormat(argv[i] + 2); + } + else if (argv[i][0] == '-') + { + fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]); + displayHelp(argv[0]); + return 1; + } + } + +#if !defined(LUAU_ENABLE_TIME_TRACE) + if (FFlag::DebugLuauTimeTracing) + { + fprintf(stderr, "To run with --timetrace, Luau has to be built with LUAU_ENABLE_TIME_TRACE enabled\n"); + return 1; + } +#endif + + const std::vector files = getSourceFiles(argc, argv); + +#ifdef _WIN32 + if (compileFormat == CompileFormat::Binary) + _setmode(_fileno(stdout), _O_BINARY); +#endif + + CompileStats stats = {}; + int failed = 0; + + for (const std::string& path : files) + failed += !compileFile(path.c_str(), compileFormat, stats); + + if (compileFormat == CompileFormat::Null) + printf("Compiled %d KLOC into %d KB bytecode (read %.2fs, parse %.2fs, compile %.2fs)\n", int(stats.lines / 1000), int(stats.bytecode / 1024), + stats.readTime, stats.parseTime, stats.compileTime); + else if (compileFormat == CompileFormat::CodegenNull) + printf("Compiled %d KLOC into %d KB bytecode => %d KB native code (%.2fx) (read %.2fs, parse %.2fs, compile %.2fs, codegen %.2fs)\n", + int(stats.lines / 1000), int(stats.bytecode / 1024), int(stats.codegen / 1024), + stats.bytecode == 0 ? 0.0 : double(stats.codegen) / double(stats.bytecode), stats.readTime, stats.parseTime, stats.compileTime, + stats.codegenTime); + + return failed ? 1 : 0; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index b6e8b591..bc66a83d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,12 +36,14 @@ if(LUAU_BUILD_CLI) add_executable(Luau.Analyze.CLI) add_executable(Luau.Ast.CLI) add_executable(Luau.Reduce.CLI) + add_executable(Luau.Compile.CLI) # This also adds target `name` on Linux/macOS and `name.exe` on Windows set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau) set_target_properties(Luau.Analyze.CLI PROPERTIES OUTPUT_NAME luau-analyze) set_target_properties(Luau.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast) set_target_properties(Luau.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce) + set_target_properties(Luau.Compile.CLI PROPERTIES OUTPUT_NAME luau-compile) endif() if(LUAU_BUILD_TESTS) @@ -186,6 +188,7 @@ if(LUAU_BUILD_CLI) target_compile_options(Luau.Reduce.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Ast.CLI PRIVATE ${LUAU_OPTIONS}) + target_compile_options(Luau.Compile.CLI PRIVATE ${LUAU_OPTIONS}) target_include_directories(Luau.Repl.CLI PRIVATE extern extern/isocline/include) @@ -206,6 +209,8 @@ if(LUAU_BUILD_CLI) target_compile_features(Luau.Reduce.CLI PRIVATE cxx_std_17) target_include_directories(Luau.Reduce.CLI PUBLIC Reduce/include) target_link_libraries(Luau.Reduce.CLI PRIVATE Luau.Common Luau.Ast Luau.Analysis) + + target_link_libraries(Luau.Compile.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen) endif() if(LUAU_BUILD_TESTS) diff --git a/CodeGen/include/Luau/AddressA64.h b/CodeGen/include/Luau/AddressA64.h index acb64e39..097cc136 100644 --- a/CodeGen/include/Luau/AddressA64.h +++ b/CodeGen/include/Luau/AddressA64.h @@ -14,13 +14,10 @@ namespace A64 enum class AddressKindA64 : uint8_t { - imm, // reg + imm - reg, // reg + reg - - // TODO: - // reg + reg << shift - // reg + sext(reg) << shift - // reg + uext(reg) << shift + reg, // reg + reg + imm, // reg + imm + pre, // reg + imm, reg += imm + post, // reg, reg += imm }; struct AddressA64 @@ -29,13 +26,14 @@ struct AddressA64 // For example, ldr x0, [reg+imm] is limited to 8 KB offsets assuming imm is divisible by 8, but loading into w0 reduces the range to 4 KB static constexpr size_t kMaxOffset = 1023; - constexpr AddressA64(RegisterA64 base, int off = 0) - : kind(AddressKindA64::imm) + constexpr AddressA64(RegisterA64 base, int off = 0, AddressKindA64 kind = AddressKindA64::imm) + : kind(kind) , base(base) , offset(xzr) , data(off) { LUAU_ASSERT(base.kind == KindA64::x || base == sp); + LUAU_ASSERT(kind != AddressKindA64::reg); } constexpr AddressA64(RegisterA64 base, RegisterA64 offset) diff --git a/CodeGen/include/Luau/IrAnalysis.h b/CodeGen/include/Luau/IrAnalysis.h index 5418009a..ca1eba62 100644 --- a/CodeGen/include/Luau/IrAnalysis.h +++ b/CodeGen/include/Luau/IrAnalysis.h @@ -1,6 +1,8 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/Common.h" + #include #include #include @@ -37,6 +39,16 @@ struct RegisterSet void requireVariadicSequence(RegisterSet& sourceRs, const RegisterSet& defRs, uint8_t varargStart); +struct BlockOrdering +{ + uint32_t depth = 0; + + uint32_t preOrder = ~0u; + uint32_t postOrder = ~0u; + + bool visited = false; +}; + struct CfgInfo { std::vector predecessors; @@ -45,6 +57,15 @@ struct CfgInfo std::vector successors; std::vector successorsOffsets; + // Immediate dominators (unique parent in the dominator tree) + std::vector idoms; + + // Children in the dominator tree + std::vector domChildren; + std::vector domChildrenOffsets; + + std::vector domOrdering; + // VM registers that are live when the block is entered // Additionally, an active variadic sequence can exist at the entry of the block std::vector in; @@ -64,6 +85,18 @@ struct CfgInfo RegisterSet captured; }; +// A quick refresher on dominance and dominator trees: +// * If A is a dominator of B (A dom B), you can never execute B without executing A first +// * A is a strict dominator of B (A sdom B) is similar to previous one but A != B +// * Immediate dominator node N (idom N) is a unique node T so that T sdom N, +// but T does not strictly dominate any other node that dominates N. +// * Dominance frontier is a set of nodes where dominance of a node X ends. +// In practice this is where values established by node X might no longer hold because of join edges from other nodes coming in. +// This is also where PHI instructions in SSA are placed. +void computeCfgImmediateDominators(IrFunction& function); +void computeCfgDominanceTreeChildren(IrFunction& function); + +// Function used to update all CFG data void computeCfgInfo(IrFunction& function); struct BlockIteratorWrapper @@ -90,10 +123,17 @@ struct BlockIteratorWrapper { return itEnd; } + + uint32_t operator[](size_t pos) const + { + LUAU_ASSERT(pos < size_t(itEnd - itBegin)); + return itBegin[pos]; + } }; BlockIteratorWrapper predecessors(const CfgInfo& cfg, uint32_t blockIdx); BlockIteratorWrapper successors(const CfgInfo& cfg, uint32_t blockIdx); +BlockIteratorWrapper domChildren(const CfgInfo& cfg, uint32_t blockIdx); } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index 0e17cba9..4a3fa424 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -823,6 +823,7 @@ struct IrFunction uint32_t validRestoreOpBlockIdx = 0; Proto* proto = nullptr; + bool variadic = false; CfgInfo cfg; diff --git a/CodeGen/src/AssemblyBuilderA64.cpp b/CodeGen/src/AssemblyBuilderA64.cpp index 99a68481..c62d797a 100644 --- a/CodeGen/src/AssemblyBuilderA64.cpp +++ b/CodeGen/src/AssemblyBuilderA64.cpp @@ -876,6 +876,9 @@ void AssemblyBuilderA64::placeA(const char* name, RegisterA64 dst, AddressA64 sr switch (src.kind) { + case AddressKindA64::reg: + place(dst.index | (src.base.index << 5) | (0b011'0'10 << 10) | (src.offset.index << 16) | (1 << 21) | (opsize << 22)); + break; case AddressKindA64::imm: if (unsigned(src.data >> sizelog) < 1024 && (src.data & ((1 << sizelog) - 1)) == 0) place(dst.index | (src.base.index << 5) | ((src.data >> sizelog) << 10) | (opsize << 22) | (1 << 24)); @@ -884,8 +887,13 @@ void AssemblyBuilderA64::placeA(const char* name, RegisterA64 dst, AddressA64 sr else LUAU_ASSERT(!"Unable to encode large immediate offset"); break; - case AddressKindA64::reg: - place(dst.index | (src.base.index << 5) | (0b011'0'10 << 10) | (src.offset.index << 16) | (1 << 21) | (opsize << 22)); + case AddressKindA64::pre: + LUAU_ASSERT(src.data >= -256 && src.data <= 255); + place(dst.index | (src.base.index << 5) | (0b11 << 10) | ((src.data & ((1 << 9) - 1)) << 12) | (opsize << 22)); + break; + case AddressKindA64::post: + LUAU_ASSERT(src.data >= -256 && src.data <= 255); + place(dst.index | (src.base.index << 5) | (0b01 << 10) | ((src.data & ((1 << 9) - 1)) << 12) | (opsize << 22)); break; } @@ -1312,23 +1320,37 @@ void AssemblyBuilderA64::log(RegisterA64 reg) void AssemblyBuilderA64::log(AddressA64 addr) { - text.append("["); switch (addr.kind) { - case AddressKindA64::imm: - log(addr.base); - if (addr.data != 0) - logAppend(",#%d", addr.data); - break; case AddressKindA64::reg: + text.append("["); log(addr.base); text.append(","); log(addr.offset); + text.append("]"); + break; + case AddressKindA64::imm: + text.append("["); + log(addr.base); if (addr.data != 0) - logAppend(" LSL #%d", addr.data); + logAppend(",#%d", addr.data); + text.append("]"); + break; + case AddressKindA64::pre: + text.append("["); + log(addr.base); + if (addr.data != 0) + logAppend(",#%d", addr.data); + text.append("]!"); + break; + case AddressKindA64::post: + text.append("["); + log(addr.base); + text.append("]!"); + if (addr.data != 0) + logAppend(",#%d", addr.data); break; } - text.append("]"); } } // namespace A64 diff --git a/CodeGen/src/AssemblyBuilderX64.cpp b/CodeGen/src/AssemblyBuilderX64.cpp index 426a0259..c7644a86 100644 --- a/CodeGen/src/AssemblyBuilderX64.cpp +++ b/CodeGen/src/AssemblyBuilderX64.cpp @@ -1415,7 +1415,7 @@ void AssemblyBuilderX64::commit() { LUAU_ASSERT(codePos <= codeEnd); - if (unsigned(codeEnd - codePos) < kMaxInstructionLength) + if (codeEnd - codePos < kMaxInstructionLength) extend(); } diff --git a/CodeGen/src/CodeAllocator.cpp b/CodeGen/src/CodeAllocator.cpp index 880a3244..09e1bb71 100644 --- a/CodeGen/src/CodeAllocator.cpp +++ b/CodeGen/src/CodeAllocator.cpp @@ -56,10 +56,8 @@ static void makePagesExecutable(uint8_t* mem, size_t size) static void flushInstructionCache(uint8_t* mem, size_t size) { -#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM) if (FlushInstructionCache(GetCurrentProcess(), mem, size) == 0) LUAU_ASSERT(!"Failed to flush instruction cache"); -#endif } #else static uint8_t* allocatePages(size_t size) diff --git a/CodeGen/src/CodeGen.cpp b/CodeGen/src/CodeGen.cpp index 4ee8e444..89399cbc 100644 --- a/CodeGen/src/CodeGen.cpp +++ b/CodeGen/src/CodeGen.cpp @@ -268,7 +268,7 @@ static bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& [[maybe_unused]] static bool lowerIr( A64::AssemblyBuilderA64& build, IrBuilder& ir, NativeState& data, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options) { - A64::IrLoweringA64 lowering(build, helpers, data, proto, ir.function); + A64::IrLoweringA64 lowering(build, helpers, data, ir.function); return lowerImpl(build, lowering, ir.function, proto->bytecodeid, options); } diff --git a/CodeGen/src/CodeGenA64.cpp b/CodeGen/src/CodeGenA64.cpp index c5042fc3..355e29ca 100644 --- a/CodeGen/src/CodeGenA64.cpp +++ b/CodeGen/src/CodeGenA64.cpp @@ -117,6 +117,81 @@ static void emitReentry(AssemblyBuilderA64& build, ModuleHelpers& helpers) build.br(x4); } +void emitReturn(AssemblyBuilderA64& build, ModuleHelpers& helpers) +{ + // x1 = res + // w2 = number of written values + + // x0 = ci + build.ldr(x0, mem(rState, offsetof(lua_State, ci))); + // w3 = ci->nresults + build.ldr(w3, mem(x0, offsetof(CallInfo, nresults))); + + Label skipResultCopy; + + // Fill the rest of the expected results (nresults - written) with 'nil' + build.cmp(w2, w3); + build.b(ConditionA64::GreaterEqual, skipResultCopy); + + // TODO: cmp above could compute this and flags using subs + build.sub(w2, w3, w2); // counter = nresults - written + build.mov(w4, LUA_TNIL); + + Label repeatNilLoop = build.setLabel(); + build.str(w4, mem(x1, offsetof(TValue, tt))); + build.add(x1, x1, sizeof(TValue)); + build.sub(w2, w2, 1); + build.cbnz(w2, repeatNilLoop); + + build.setLabel(skipResultCopy); + + // x2 = cip = ci - 1 + build.sub(x2, x0, sizeof(CallInfo)); + + // res = cip->top when nresults >= 0 + Label skipFixedRetTop; + build.tbnz(w3, 31, skipFixedRetTop); + build.ldr(x1, mem(x2, offsetof(CallInfo, top))); // res = cip->top + build.setLabel(skipFixedRetTop); + + // Update VM state (ci, base, top) + build.str(x2, mem(rState, offsetof(lua_State, ci))); // L->ci = cip + build.ldr(rBase, mem(x2, offsetof(CallInfo, base))); // sync base = L->base while we have a chance + build.str(rBase, mem(rState, offsetof(lua_State, base))); // L->base = cip->base + + build.str(x1, mem(rState, offsetof(lua_State, top))); // L->top = res + + // Unlikely, but this might be the last return from VM + build.ldr(w4, mem(x0, offsetof(CallInfo, flags))); + build.tbnz(w4, countrz(LUA_CALLINFO_RETURN), helpers.exitNoContinueVm); + + // Continue in interpreter if function has no native data + build.ldr(w4, mem(x2, offsetof(CallInfo, flags))); + build.tbz(w4, countrz(LUA_CALLINFO_NATIVE), helpers.exitContinueVm); + + // Need to update state of the current function before we jump away + build.ldr(rClosure, mem(x2, offsetof(CallInfo, func))); + build.ldr(rClosure, mem(rClosure, offsetof(TValue, value.gc))); + + build.ldr(x1, mem(rClosure, offsetof(Closure, l.p))); // cl->l.p aka proto + + LUAU_ASSERT(offsetof(Proto, code) == offsetof(Proto, k) + 8); + build.ldp(rConstants, rCode, mem(x1, offsetof(Proto, k))); // proto->k, proto->code + + // Get instruction index from instruction pointer + // To get instruction index from instruction pointer, we need to divide byte offset by 4 + // But we will actually need to scale instruction index by 4 back to byte offset later so it cancels out + build.ldr(x2, mem(x2, offsetof(CallInfo, savedpc))); // cip->savedpc + build.sub(x2, x2, rCode); + + // Get new instruction location and jump to it + LUAU_ASSERT(offsetof(Proto, exectarget) == offsetof(Proto, execdata) + 8); + build.ldp(x3, x4, mem(x1, offsetof(Proto, execdata))); + build.ldr(w2, mem(x3, x2)); + build.add(x4, x4, x2); + build.br(x4); +} + static EntryLocations buildEntryFunction(AssemblyBuilderA64& build, UnwindBuilder& unwind) { EntryLocations locations; @@ -230,6 +305,11 @@ void assembleHelpers(AssemblyBuilderA64& build, ModuleHelpers& helpers) build.logAppend("; interrupt\n"); helpers.interrupt = build.setLabel(); emitInterrupt(build); + + if (build.logText) + build.logAppend("; return\n"); + helpers.return_ = build.setLabel(); + emitReturn(build, helpers); } } // namespace A64 diff --git a/CodeGen/src/CodeGenUtils.cpp b/CodeGen/src/CodeGenUtils.cpp index a7131e11..20269cfd 100644 --- a/CodeGen/src/CodeGenUtils.cpp +++ b/CodeGen/src/CodeGenUtils.cpp @@ -17,8 +17,6 @@ #include -LUAU_FASTFLAG(LuauUniformTopHandling) - // All external function calls that can cause stack realloc or Lua calls have to be wrapped in VM_PROTECT // This makes sure that we save the pc (in case the Lua call needs to generate a backtrace) before the call, // and restores the stack pointer after in case stack gets reallocated @@ -306,44 +304,6 @@ Closure* callFallback(lua_State* L, StkId ra, StkId argtop, int nresults) } } -// Extracted as-is from lvmexecute.cpp with the exception of control flow (reentry) and removed interrupts -Closure* returnFallback(lua_State* L, StkId ra, StkId valend) -{ - // ci is our callinfo, cip is our parent - CallInfo* ci = L->ci; - CallInfo* cip = ci - 1; - - StkId res = ci->func; // note: we assume CALL always puts func+args and expects results to start at func - StkId vali = ra; - - int nresults = ci->nresults; - - // copy return values into parent stack (but only up to nresults!), fill the rest with nil - // note: in MULTRET context nresults starts as -1 so i != 0 condition never activates intentionally - int i; - for (i = nresults; i != 0 && vali < valend; i--) - setobj2s(L, res++, vali++); - while (i-- > 0) - setnilvalue(res++); - - // pop the stack frame - L->ci = cip; - L->base = cip->base; - L->top = (nresults == LUA_MULTRET) ? res : cip->top; - - // we're done! - if (LUAU_UNLIKELY(ci->flags & LUA_CALLINFO_RETURN)) - { - if (!FFlag::LuauUniformTopHandling) - L->top = res; - return NULL; - } - - // keep executing new function - LUAU_ASSERT(isLua(cip)); - return clvalue(cip->func); -} - const Instruction* executeGETGLOBAL(lua_State* L, const Instruction* pc, StkId base, TValue* k) { [[maybe_unused]] Closure* cl = clvalue(L->ci->func); diff --git a/CodeGen/src/CodeGenUtils.h b/CodeGen/src/CodeGenUtils.h index 87b6ec44..a30d7e98 100644 --- a/CodeGen/src/CodeGenUtils.h +++ b/CodeGen/src/CodeGenUtils.h @@ -18,7 +18,6 @@ Closure* callProlog(lua_State* L, TValue* ra, StkId argtop, int nresults); void callEpilogC(lua_State* L, int nresults, int n); Closure* callFallback(lua_State* L, StkId ra, StkId argtop, int nresults); -Closure* returnFallback(lua_State* L, StkId ra, StkId valend); const Instruction* executeGETGLOBAL(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeSETGLOBAL(lua_State* L, const Instruction* pc, StkId base, TValue* k); diff --git a/CodeGen/src/CodeGenX64.cpp b/CodeGen/src/CodeGenX64.cpp index ec032c02..4100e667 100644 --- a/CodeGen/src/CodeGenX64.cpp +++ b/CodeGen/src/CodeGenX64.cpp @@ -189,6 +189,11 @@ void assembleHelpers(X64::AssemblyBuilderX64& build, ModuleHelpers& helpers) build.logAppend("; continueCallInVm\n"); helpers.continueCallInVm = build.setLabel(); emitContinueCallInVm(build); + + if (build.logText) + build.logAppend("; return\n"); + helpers.return_ = build.setLabel(); + emitReturn(build, helpers); } } // namespace X64 diff --git a/CodeGen/src/EmitCommon.h b/CodeGen/src/EmitCommon.h index 6b19912b..bfdde169 100644 --- a/CodeGen/src/EmitCommon.h +++ b/CodeGen/src/EmitCommon.h @@ -24,6 +24,7 @@ struct ModuleHelpers // A64/X64 Label exitContinueVm; Label exitNoContinueVm; + Label return_; // X64 Label continueCallInVm; diff --git a/CodeGen/src/EmitCommonX64.cpp b/CodeGen/src/EmitCommonX64.cpp index 0095f288..4ad4efe7 100644 --- a/CodeGen/src/EmitCommonX64.cpp +++ b/CodeGen/src/EmitCommonX64.cpp @@ -352,6 +352,89 @@ void emitContinueCallInVm(AssemblyBuilderX64& build) emitExit(build, /* continueInVm */ true); } +void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers) +{ + // input: ci in r8, res in rdi, number of written values in ecx + RegisterX64 ci = r8; + RegisterX64 res = rdi; + RegisterX64 written = ecx; + + RegisterX64 cip = r9; + RegisterX64 nresults = esi; + + build.lea(cip, addr[ci - sizeof(CallInfo)]); + + // nresults = ci->nresults + build.mov(nresults, dword[ci + offsetof(CallInfo, nresults)]); + + Label skipResultCopy; + + // Fill the rest of the expected results (nresults - written) with 'nil' + RegisterX64 counter = written; + build.sub(counter, nresults); // counter = -(nresults - written) + build.jcc(ConditionX64::GreaterEqual, skipResultCopy); + + Label repeatNilLoop = build.setLabel(); + build.mov(dword[res + offsetof(TValue, tt)], LUA_TNIL); + build.add(res, sizeof(TValue)); + build.inc(counter); + build.jcc(ConditionX64::NotZero, repeatNilLoop); + + build.setLabel(skipResultCopy); + + build.mov(qword[rState + offsetof(lua_State, ci)], cip); // L->ci = cip + build.mov(rBase, qword[cip + offsetof(CallInfo, base)]); // sync base = L->base while we have a chance + build.mov(qword[rState + offsetof(lua_State, base)], rBase); // L->base = cip->base + + Label skipFixedRetTop; + build.test(nresults, nresults); // test here will set SF=1 for a negative number and it always sets OF to 0 + build.jcc(ConditionX64::Less, skipFixedRetTop); // jl jumps if SF != OF + build.mov(res, qword[cip + offsetof(CallInfo, top)]); // res = cip->top + build.setLabel(skipFixedRetTop); + + build.mov(qword[rState + offsetof(lua_State, top)], res); // L->top = res + + // Unlikely, but this might be the last return from VM + build.test(byte[ci + offsetof(CallInfo, flags)], LUA_CALLINFO_RETURN); + build.jcc(ConditionX64::NotZero, helpers.exitNoContinueVm); + + // Returning back to the previous function is a bit tricky + // Registers alive: r9 (cip) + RegisterX64 proto = rcx; + RegisterX64 execdata = rbx; + + // Change closure + build.mov(rax, qword[cip + offsetof(CallInfo, func)]); + build.mov(rax, qword[rax + offsetof(TValue, value.gc)]); + build.mov(sClosure, rax); + + build.mov(proto, qword[rax + offsetof(Closure, l.p)]); + + build.mov(execdata, qword[proto + offsetof(Proto, execdata)]); + + build.test(byte[cip + offsetof(CallInfo, flags)], LUA_CALLINFO_NATIVE); + build.jcc(ConditionX64::Zero, helpers.exitContinueVm); // Continue in interpreter if function has no native data + + // Change constants + build.mov(rConstants, qword[proto + offsetof(Proto, k)]); + + // Change code + build.mov(rdx, qword[proto + offsetof(Proto, code)]); + build.mov(sCode, rdx); + + build.mov(rax, qword[cip + offsetof(CallInfo, savedpc)]); + + // To get instruction index from instruction pointer, we need to divide byte offset by 4 + // But we will actually need to scale instruction index by 4 back to byte offset later so it cancels out + build.sub(rax, rdx); + + // Get new instruction location and jump to it + build.mov(edx, dword[execdata + rax]); + build.add(rdx, qword[proto + offsetof(Proto, exectarget)]); + build.jmp(rdx); +} + + } // namespace X64 } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/EmitCommonX64.h b/CodeGen/src/EmitCommonX64.h index 3f723f45..eb4532a0 100644 --- a/CodeGen/src/EmitCommonX64.h +++ b/CodeGen/src/EmitCommonX64.h @@ -207,6 +207,8 @@ void emitFallback(IrRegAllocX64& regs, AssemblyBuilderX64& build, int offset, in void emitContinueCallInVm(AssemblyBuilderX64& build); +void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers); + } // namespace X64 } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/EmitInstructionX64.cpp b/CodeGen/src/EmitInstructionX64.cpp index f2012ca9..5d1c642f 100644 --- a/CodeGen/src/EmitInstructionX64.cpp +++ b/CodeGen/src/EmitInstructionX64.cpp @@ -166,160 +166,61 @@ void emitInstCall(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int actualResults) { RegisterX64 ci = r8; - RegisterX64 cip = r9; RegisterX64 res = rdi; - RegisterX64 nresults = esi; + RegisterX64 written = ecx; build.mov(ci, qword[rState + offsetof(lua_State, ci)]); - build.lea(cip, addr[ci - sizeof(CallInfo)]); - - // res = ci->func; note: we assume CALL always puts func+args and expects results to start at func build.mov(res, qword[ci + offsetof(CallInfo, func)]); - // nresults = ci->nresults - build.mov(nresults, dword[ci + offsetof(CallInfo, nresults)]); + if (actualResults == 0) { - Label skipResultCopy; - - RegisterX64 counter = ecx; - - if (actualResults == 0) - { - // Our instruction doesn't have any results, so just fill results expected in parent with 'nil' - build.test(nresults, nresults); // test here will set SF=1 for a negative number, ZF=1 for zero and OF=0 - build.jcc(ConditionX64::LessEqual, skipResultCopy); // jle jumps if SF != OF or ZF == 1 - - build.mov(counter, nresults); - - Label repeatNilLoop = build.setLabel(); - build.mov(dword[res + offsetof(TValue, tt)], LUA_TNIL); - build.add(res, sizeof(TValue)); - build.dec(counter); - build.jcc(ConditionX64::NotZero, repeatNilLoop); - } - else if (actualResults == 1) - { - // Try setting our 1 result - build.test(nresults, nresults); - build.jcc(ConditionX64::Zero, skipResultCopy); - - build.lea(counter, addr[nresults - 1]); - - build.vmovups(xmm0, luauReg(ra)); - build.vmovups(xmmword[res], xmm0); - build.add(res, sizeof(TValue)); - - // Fill the rest of the expected results with 'nil' - build.test(counter, counter); // test here will set SF=1 for a negative number, ZF=1 for zero and OF=0 - build.jcc(ConditionX64::LessEqual, skipResultCopy); // jle jumps if SF != OF or ZF == 1 - - Label repeatNilLoop = build.setLabel(); - build.mov(dword[res + offsetof(TValue, tt)], LUA_TNIL); - build.add(res, sizeof(TValue)); - build.dec(counter); - build.jcc(ConditionX64::NotZero, repeatNilLoop); - } - else - { - RegisterX64 vali = rax; - RegisterX64 valend = rdx; - - // Copy return values into parent stack (but only up to nresults!) - build.test(nresults, nresults); - build.jcc(ConditionX64::Zero, skipResultCopy); - - // vali = ra - build.lea(vali, luauRegAddress(ra)); - - // Copy as much as possible for MULTRET calls, and only as much as needed otherwise - if (actualResults == LUA_MULTRET) - build.mov(valend, qword[rState + offsetof(lua_State, top)]); // valend = L->top - else - build.lea(valend, luauRegAddress(ra + actualResults)); // valend = ra + actualResults - - build.mov(counter, nresults); - - Label repeatValueLoop, exitValueLoop; - - build.setLabel(repeatValueLoop); - build.cmp(vali, valend); - build.jcc(ConditionX64::NotBelow, exitValueLoop); - - build.vmovups(xmm0, xmmword[vali]); - build.vmovups(xmmword[res], xmm0); - build.add(vali, sizeof(TValue)); - build.add(res, sizeof(TValue)); - build.dec(counter); - build.jcc(ConditionX64::NotZero, repeatValueLoop); - - build.setLabel(exitValueLoop); - - // Fill the rest of the expected results with 'nil' - build.test(counter, counter); // test here will set SF=1 for a negative number, ZF=1 for zero and OF=0 - build.jcc(ConditionX64::LessEqual, skipResultCopy); // jle jumps if SF != OF or ZF == 1 - - Label repeatNilLoop = build.setLabel(); - build.mov(dword[res + offsetof(TValue, tt)], LUA_TNIL); - build.add(res, sizeof(TValue)); - build.dec(counter); - build.jcc(ConditionX64::NotZero, repeatNilLoop); - } - - build.setLabel(skipResultCopy); + build.xor_(written, written); + build.jmp(helpers.return_); } + else if (actualResults >= 1 && actualResults <= 3) + { + for (int r = 0; r < actualResults; ++r) + { + build.vmovups(xmm0, luauReg(ra + r)); + build.vmovups(xmmword[res + r * sizeof(TValue)], xmm0); + } + build.add(res, actualResults * sizeof(TValue)); + build.mov(written, actualResults); + build.jmp(helpers.return_); + } + else + { + RegisterX64 vali = rax; + RegisterX64 valend = rdx; - build.mov(qword[rState + offsetof(lua_State, ci)], cip); // L->ci = cip - build.mov(rBase, qword[cip + offsetof(CallInfo, base)]); // sync base = L->base while we have a chance - build.mov(qword[rState + offsetof(lua_State, base)], rBase); // L->base = cip->base + // vali = ra + build.lea(vali, luauRegAddress(ra)); - // Start with result for LUA_MULTRET/exit value - build.mov(qword[rState + offsetof(lua_State, top)], res); // L->top = res + // Copy as much as possible for MULTRET calls, and only as much as needed otherwise + if (actualResults == LUA_MULTRET) + build.mov(valend, qword[rState + offsetof(lua_State, top)]); // valend = L->top + else + build.lea(valend, luauRegAddress(ra + actualResults)); // valend = ra + actualResults - // Unlikely, but this might be the last return from VM - build.test(byte[ci + offsetof(CallInfo, flags)], LUA_CALLINFO_RETURN); - build.jcc(ConditionX64::NotZero, helpers.exitNoContinueVm); + build.xor_(written, written); - Label skipFixedRetTop; - build.test(nresults, nresults); // test here will set SF=1 for a negative number and it always sets OF to 0 - build.jcc(ConditionX64::Less, skipFixedRetTop); // jl jumps if SF != OF - build.mov(rax, qword[cip + offsetof(CallInfo, top)]); - build.mov(qword[rState + offsetof(lua_State, top)], rax); // L->top = cip->top - build.setLabel(skipFixedRetTop); + Label repeatValueLoop, exitValueLoop; - // Returning back to the previous function is a bit tricky - // Registers alive: r9 (cip) - RegisterX64 proto = rcx; - RegisterX64 execdata = rbx; + build.cmp(vali, valend); + build.jcc(ConditionX64::NotBelow, exitValueLoop); - // Change closure - build.mov(rax, qword[cip + offsetof(CallInfo, func)]); - build.mov(rax, qword[rax + offsetof(TValue, value.gc)]); - build.mov(sClosure, rax); + build.setLabel(repeatValueLoop); + build.vmovups(xmm0, xmmword[vali]); + build.vmovups(xmmword[res], xmm0); + build.add(vali, sizeof(TValue)); + build.add(res, sizeof(TValue)); + build.inc(written); + build.cmp(vali, valend); + build.jcc(ConditionX64::Below, repeatValueLoop); - build.mov(proto, qword[rax + offsetof(Closure, l.p)]); - - build.mov(execdata, qword[proto + offsetof(Proto, execdata)]); - - build.test(byte[cip + offsetof(CallInfo, flags)], LUA_CALLINFO_NATIVE); - build.jcc(ConditionX64::Zero, helpers.exitContinueVm); // Continue in interpreter if function has no native data - - // Change constants - build.mov(rConstants, qword[proto + offsetof(Proto, k)]); - - // Change code - build.mov(rdx, qword[proto + offsetof(Proto, code)]); - build.mov(sCode, rdx); - - build.mov(rax, qword[cip + offsetof(CallInfo, savedpc)]); - - // To get instruction index from instruction pointer, we need to divide byte offset by 4 - // But we will actually need to scale instruction index by 4 back to byte offset later so it cancels out - build.sub(rax, rdx); - - // Get new instruction location and jump to it - build.mov(edx, dword[execdata + rax]); - build.add(rdx, qword[proto + offsetof(Proto, exectarget)]); - build.jmp(rdx); + build.setLabel(exitValueLoop); + build.jmp(helpers.return_); + } } void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, int count, uint32_t index) diff --git a/CodeGen/src/IrAnalysis.cpp b/CodeGen/src/IrAnalysis.cpp index 85811f05..14fc9b46 100644 --- a/CodeGen/src/IrAnalysis.cpp +++ b/CodeGen/src/IrAnalysis.cpp @@ -661,9 +661,212 @@ static void computeCfgBlockEdges(IrFunction& function) } } +// Assign tree depth and pre- and post- DFS visit order of the tree/graph nodes +// Optionally, collect required node order into a vector +template +void computeBlockOrdering( + IrFunction& function, std::vector& ordering, std::vector* preOrder, std::vector* postOrder) +{ + CfgInfo& info = function.cfg; + + LUAU_ASSERT(info.idoms.size() == function.blocks.size()); + + ordering.clear(); + ordering.resize(function.blocks.size()); + + // Get depth-first post-order using manual stack instead of recursion + struct StackItem + { + uint32_t blockIdx; + uint32_t itPos; + }; + std::vector stack; + + if (preOrder) + preOrder->reserve(function.blocks.size()); + if (postOrder) + postOrder->reserve(function.blocks.size()); + + uint32_t nextPreOrder = 0; + uint32_t nextPostOrder = 0; + + stack.push_back({0, 0}); + ordering[0].visited = true; + ordering[0].preOrder = nextPreOrder++; + + while (!stack.empty()) + { + StackItem& item = stack.back(); + BlockIteratorWrapper children = childIt(info, item.blockIdx); + + if (item.itPos < children.size()) + { + uint32_t childIdx = children[item.itPos++]; + + BlockOrdering& childOrdering = ordering[childIdx]; + + if (!childOrdering.visited) + { + childOrdering.visited = true; + childOrdering.depth = uint32_t(stack.size()); + childOrdering.preOrder = nextPreOrder++; + + if (preOrder) + preOrder->push_back(item.blockIdx); + + stack.push_back({childIdx, 0}); + } + } + else + { + ordering[item.blockIdx].postOrder = nextPostOrder++; + + if (postOrder) + postOrder->push_back(item.blockIdx); + + stack.pop_back(); + } + } +} + +// Dominance tree construction based on 'A Simple, Fast Dominance Algorithm' [Keith D. Cooper, et al] +// This solution has quadratic complexity in the worst case. +// It is possible to switch to SEMI-NCA algorithm (also quadratic) mentioned in 'Linear-Time Algorithms for Dominators and Related Problems' [Loukas +// Georgiadis] + +// Find block that is common between blocks 'a' and 'b' on the path towards the entry +static uint32_t findCommonDominator(const std::vector& idoms, const std::vector& data, uint32_t a, uint32_t b) +{ + while (a != b) + { + while (data[a].postOrder < data[b].postOrder) + { + a = idoms[a]; + LUAU_ASSERT(a != ~0u); + } + + while (data[b].postOrder < data[a].postOrder) + { + b = idoms[b]; + LUAU_ASSERT(b != ~0u); + } + } + + return a; +} + +void computeCfgImmediateDominators(IrFunction& function) +{ + CfgInfo& info = function.cfg; + + // Clear existing data + info.idoms.clear(); + info.idoms.resize(function.blocks.size(), ~0u); + + std::vector ordering; + std::vector blocksInPostOrder; + computeBlockOrdering(function, ordering, /* preOrder */ nullptr, &blocksInPostOrder); + + // Entry node is temporarily marked to be an idom of itself to make algorithm work + info.idoms[0] = 0; + + // Iteratively compute immediate dominators + bool updated = true; + + while (updated) + { + updated = false; + + // Go over blocks in reverse post-order of CFG + // '- 2' skips the root node which is last in post-order traversal + for (int i = int(blocksInPostOrder.size() - 2); i >= 0; i--) + { + uint32_t blockIdx = blocksInPostOrder[i]; + uint32_t newIdom = ~0u; + + for (uint32_t predIdx : predecessors(info, blockIdx)) + { + if (uint32_t predIdom = info.idoms[predIdx]; predIdom != ~0u) + { + if (newIdom == ~0u) + newIdom = predIdx; + else + newIdom = findCommonDominator(info.idoms, ordering, newIdom, predIdx); + } + } + + if (newIdom != info.idoms[blockIdx]) + { + info.idoms[blockIdx] = newIdom; + + // Run until a fixed point is reached + updated = true; + } + } + } + + // Entry node doesn't have an immediate dominator + info.idoms[0] = ~0u; +} + +void computeCfgDominanceTreeChildren(IrFunction& function) +{ + CfgInfo& info = function.cfg; + + // Clear existing data + info.domChildren.clear(); + + info.domChildrenOffsets.clear(); + info.domChildrenOffsets.resize(function.blocks.size()); + + // First we need to know children count of each node in the dominance tree + // We use offset array for to hold this data, counts will be readjusted to offsets later + for (size_t blockIdx = 0; blockIdx < function.blocks.size(); blockIdx++) + { + uint32_t domParent = info.idoms[blockIdx]; + + if (domParent != ~0u) + info.domChildrenOffsets[domParent]++; + } + + // Convert counds to offsets using prefix sum + uint32_t total = 0; + + for (size_t blockIdx = 0; blockIdx < function.blocks.size(); blockIdx++) + { + uint32_t& offset = info.domChildrenOffsets[blockIdx]; + uint32_t count = offset; + offset = total; + total += count; + } + + info.domChildren.resize(total); + + for (size_t blockIdx = 0; blockIdx < function.blocks.size(); blockIdx++) + { + // We use a trick here, where we use the starting offset of the dominance children list as the position where to write next child + // The values will be adjusted back in a separate loop later + uint32_t domParent = info.idoms[blockIdx]; + + if (domParent != ~0u) + info.domChildren[info.domChildrenOffsets[domParent]++] = uint32_t(blockIdx); + } + + // Offsets into the dominance children list were used as iterators in the previous loop + // That process basically moved the values in the array 1 step towards the start + // Here we move them one step towards the end and restore 0 for first offset + for (int blockIdx = int(function.blocks.size() - 1); blockIdx > 0; blockIdx--) + info.domChildrenOffsets[blockIdx] = info.domChildrenOffsets[blockIdx - 1]; + info.domChildrenOffsets[0] = 0; + + computeBlockOrdering(function, info.domOrdering, /* preOrder */ nullptr, /* postOrder */ nullptr); +} + void computeCfgInfo(IrFunction& function) { computeCfgBlockEdges(function); + computeCfgImmediateDominators(function); + computeCfgDominanceTreeChildren(function); computeCfgLiveInOutRegSets(function); } @@ -687,5 +890,15 @@ BlockIteratorWrapper successors(const CfgInfo& cfg, uint32_t blockIdx) return BlockIteratorWrapper{cfg.successors.data() + start, cfg.successors.data() + end}; } +BlockIteratorWrapper domChildren(const CfgInfo& cfg, uint32_t blockIdx) +{ + LUAU_ASSERT(blockIdx < cfg.domChildrenOffsets.size()); + + uint32_t start = cfg.domChildrenOffsets[blockIdx]; + uint32_t end = blockIdx + 1 < cfg.domChildrenOffsets.size() ? cfg.domChildrenOffsets[blockIdx + 1] : uint32_t(cfg.domChildren.size()); + + return BlockIteratorWrapper{cfg.domChildren.data() + start, cfg.domChildren.data() + end}; +} + } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/IrBuilder.cpp b/CodeGen/src/IrBuilder.cpp index a12eca34..6ab5e249 100644 --- a/CodeGen/src/IrBuilder.cpp +++ b/CodeGen/src/IrBuilder.cpp @@ -25,6 +25,7 @@ IrBuilder::IrBuilder() void IrBuilder::buildFunctionIr(Proto* proto) { function.proto = proto; + function.variadic = proto->is_vararg != 0; // Rebuild original control flow blocks rebuildBytecodeBasicBlocks(proto); diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 5f624900..5c29ad41 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -185,11 +185,10 @@ static bool emitBuiltin( } } -IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, NativeState& data, Proto* proto, IrFunction& function) +IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, NativeState& data, IrFunction& function) : build(build) , helpers(helpers) , data(data) - , proto(proto) , function(function) , regs(function, {{x0, x15}, {x16, x17}, {q0, q7}, {q16, q31}}) , valueTracker(function) @@ -1343,19 +1342,71 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) break; case IrCmd::RETURN: regs.spill(build, index); - // valend = (n == LUA_MULTRET) ? L->top : ra + n - if (intOp(inst.b) == LUA_MULTRET) - build.ldr(x2, mem(rState, offsetof(lua_State, top))); - else - build.add(x2, rBase, uint16_t((vmRegOp(inst.a) + intOp(inst.b)) * sizeof(TValue))); - // returnFallback(L, ra, valend) - build.mov(x0, rState); - build.add(x1, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue))); - build.ldr(x3, mem(rNativeContext, offsetof(NativeContext, returnFallback))); - build.blr(x3); - // reentry with x0=closure (NULL will trigger exit) - build.b(helpers.reentry); + if (function.variadic) + { + build.ldr(x1, mem(rState, offsetof(lua_State, ci))); + build.ldr(x1, mem(x1, offsetof(CallInfo, func))); + } + else if (intOp(inst.b) != 1) + build.sub(x1, rBase, sizeof(TValue)); // invariant: ci->func + 1 == ci->base for non-variadic frames + + if (intOp(inst.b) == 0) + { + build.mov(w2, 0); + build.b(helpers.return_); + } + else if (intOp(inst.b) == 1 && !function.variadic) + { + // fast path: minimizes x1 adjustments + // note that we skipped x1 computation for this specific case above + build.ldr(q0, mem(rBase, vmRegOp(inst.a) * sizeof(TValue))); + build.str(q0, mem(rBase, -int(sizeof(TValue)))); + build.mov(x1, rBase); + build.mov(w2, 1); + build.b(helpers.return_); + } + else if (intOp(inst.b) >= 1 && intOp(inst.b) <= 3) + { + for (int r = 0; r < intOp(inst.b); ++r) + { + build.ldr(q0, mem(rBase, (vmRegOp(inst.a) + r) * sizeof(TValue))); + build.str(q0, mem(x1, sizeof(TValue), AddressKindA64::post)); + } + build.mov(w2, intOp(inst.b)); + build.b(helpers.return_); + } + else + { + build.mov(w2, 0); + + // vali = ra + build.add(x3, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue))); + + // valend = (n == LUA_MULTRET) ? L->top : ra + n + if (intOp(inst.b) == LUA_MULTRET) + build.ldr(x4, mem(rState, offsetof(lua_State, top))); + else + build.add(x4, rBase, uint16_t((vmRegOp(inst.a) + intOp(inst.b)) * sizeof(TValue))); + + Label repeatValueLoop, exitValueLoop; + + if (intOp(inst.b) == LUA_MULTRET) + { + build.cmp(x3, x4); + build.b(ConditionA64::CarrySet, exitValueLoop); // CarrySet == UnsignedGreaterEqual + } + + build.setLabel(repeatValueLoop); + build.ldr(q0, mem(x3, sizeof(TValue), AddressKindA64::post)); + build.str(q0, mem(x1, sizeof(TValue), AddressKindA64::post)); + build.add(w2, w2, 1); + build.cmp(x3, x4); + build.b(ConditionA64::CarryClear, repeatValueLoop); // CarryClear == UnsignedLess + + build.setLabel(exitValueLoop); + build.b(helpers.return_); + } break; case IrCmd::FORGLOOP: // register layout: ra + 1 = table, ra + 2 = internal index, ra + 3 .. ra + aux = iteration variables diff --git a/CodeGen/src/IrLoweringA64.h b/CodeGen/src/IrLoweringA64.h index 26478904..1df09bd3 100644 --- a/CodeGen/src/IrLoweringA64.h +++ b/CodeGen/src/IrLoweringA64.h @@ -9,8 +9,6 @@ #include -struct Proto; - namespace Luau { namespace CodeGen @@ -25,7 +23,7 @@ namespace A64 struct IrLoweringA64 { - IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, NativeState& data, Proto* proto, IrFunction& function); + IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, NativeState& data, IrFunction& function); void lowerInst(IrInst& inst, uint32_t index, IrBlock& next); void finishBlock(); @@ -58,7 +56,6 @@ struct IrLoweringA64 AssemblyBuilderA64& build; ModuleHelpers& helpers; NativeState& data; - Proto* proto = nullptr; // Temporarily required to provide 'Instruction* pc' to old emitInst* methods IrFunction& function; diff --git a/CodeGen/src/NativeState.cpp b/CodeGen/src/NativeState.cpp index 17977c3c..14c1acd9 100644 --- a/CodeGen/src/NativeState.cpp +++ b/CodeGen/src/NativeState.cpp @@ -90,7 +90,6 @@ void initFunctions(NativeState& data) data.context.callEpilogC = callEpilogC; data.context.callFallback = callFallback; - data.context.returnFallback = returnFallback; data.context.executeGETGLOBAL = executeGETGLOBAL; data.context.executeSETGLOBAL = executeSETGLOBAL; diff --git a/CodeGen/src/NativeState.h b/CodeGen/src/NativeState.h index 0140448f..a2393bbf 100644 --- a/CodeGen/src/NativeState.h +++ b/CodeGen/src/NativeState.h @@ -86,7 +86,6 @@ struct NativeContext void (*callEpilogC)(lua_State* L, int nresults, int n) = nullptr; Closure* (*callFallback)(lua_State* L, StkId ra, StkId argtop, int nresults) = nullptr; - Closure* (*returnFallback)(lua_State* L, StkId ra, StkId valend) = nullptr; // Opcode fallbacks, implemented in C const Instruction* (*executeGETGLOBAL)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; diff --git a/Makefile b/Makefile index 99eb93e6..d3bf31d2 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,10 @@ ANALYZE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Analyze.cpp ANALYZE_CLI_OBJECTS=$(ANALYZE_CLI_SOURCES:%=$(BUILD)/%.o) ANALYZE_CLI_TARGET=$(BUILD)/luau-analyze +COMPILE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Compile.cpp +COMPILE_CLI_OBJECTS=$(COMPILE_CLI_SOURCES:%=$(BUILD)/%.o) +COMPILE_CLI_TARGET=$(BUILD)/luau-compile + FUZZ_SOURCES=$(wildcard fuzz/*.cpp) fuzz/luau.pb.cpp FUZZ_OBJECTS=$(FUZZ_SOURCES:%=$(BUILD)/%.o) @@ -55,8 +59,8 @@ ifneq ($(opt),) TESTS_ARGS+=-O$(opt) endif -OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(ANALYSIS_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(FUZZ_OBJECTS) -EXECUTABLE_ALIASES = luau luau-analyze luau-tests +OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(ANALYSIS_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(FUZZ_OBJECTS) +EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-tests # common flags CXXFLAGS=-g -Wall @@ -132,6 +136,7 @@ $(ISOCLINE_OBJECTS): CXXFLAGS+=-Wno-unused-function -Iextern/isocline/include $(TESTS_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -ICodeGen/include -IVM/include -ICLI -Iextern -DDOCTEST_CONFIG_DOUBLE_STRINGIFY $(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -Iextern -Iextern/isocline/include $(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -Iextern +$(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include $(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -IVM/include -ICodeGen/include $(TESTS_TARGET): LDFLAGS+=-lpthread @@ -189,6 +194,9 @@ luau: $(REPL_CLI_TARGET) luau-analyze: $(ANALYZE_CLI_TARGET) ln -fs $^ $@ +luau-compile: $(COMPILE_CLI_TARGET) + ln -fs $^ $@ + luau-tests: $(TESTS_TARGET) ln -fs $^ $@ @@ -196,8 +204,9 @@ luau-tests: $(TESTS_TARGET) $(TESTS_TARGET): $(TESTS_OBJECTS) $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(ISOCLINE_TARGET) $(REPL_CLI_TARGET): $(REPL_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(ISOCLINE_TARGET) $(ANALYZE_CLI_TARGET): $(ANALYZE_CLI_OBJECTS) $(ANALYSIS_TARGET) $(AST_TARGET) +$(COMPILE_CLI_TARGET): $(COMPILE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) -$(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET): +$(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET) $(COMPILE_CLI_TARGET): $(CXX) $^ $(LDFLAGS) -o $@ # executable targets for fuzzing diff --git a/Sources.cmake b/Sources.cmake index 853b1b86..b1693c36 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -141,7 +141,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/BuiltinDefinitions.h Analysis/include/Luau/Clone.h Analysis/include/Luau/Config.h - Analysis/include/Luau/Refinement.h Analysis/include/Luau/Constraint.h Analysis/include/Luau/ConstraintGraphBuilder.h Analysis/include/Luau/ConstraintSolver.h @@ -153,6 +152,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Error.h Analysis/include/Luau/FileResolver.h Analysis/include/Luau/Frontend.h + Analysis/include/Luau/InsertionOrderedMap.h Analysis/include/Luau/Instantiation.h Analysis/include/Luau/IostreamHelpers.h Analysis/include/Luau/JsonEmitter.h @@ -165,6 +165,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Predicate.h Analysis/include/Luau/Quantify.h Analysis/include/Luau/RecursionCounter.h + Analysis/include/Luau/Refinement.h Analysis/include/Luau/RequireTracer.h Analysis/include/Luau/Scope.h Analysis/include/Luau/Simplify.h @@ -175,6 +176,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/ToString.h Analysis/include/Luau/Transpiler.h Analysis/include/Luau/TxnLog.h + Analysis/include/Luau/Type.h Analysis/include/Luau/TypeArena.h Analysis/include/Luau/TypeAttach.h Analysis/include/Luau/TypeChecker2.h @@ -183,7 +185,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/TypeInfer.h Analysis/include/Luau/TypePack.h Analysis/include/Luau/TypeUtils.h - Analysis/include/Luau/Type.h Analysis/include/Luau/Unifiable.h Analysis/include/Luau/Unifier.h Analysis/include/Luau/UnifierSharedState.h @@ -198,7 +199,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/BuiltinDefinitions.cpp Analysis/src/Clone.cpp Analysis/src/Config.cpp - Analysis/src/Refinement.cpp Analysis/src/Constraint.cpp Analysis/src/ConstraintGraphBuilder.cpp Analysis/src/ConstraintSolver.cpp @@ -216,6 +216,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/Module.cpp Analysis/src/Normalize.cpp Analysis/src/Quantify.cpp + Analysis/src/Refinement.cpp Analysis/src/RequireTracer.cpp Analysis/src/Scope.cpp Analysis/src/Simplify.cpp @@ -226,6 +227,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/ToString.cpp Analysis/src/Transpiler.cpp Analysis/src/TxnLog.cpp + Analysis/src/Type.cpp Analysis/src/TypeArena.cpp Analysis/src/TypeAttach.cpp Analysis/src/TypeChecker2.cpp @@ -234,7 +236,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/TypeInfer.cpp Analysis/src/TypePack.cpp Analysis/src/TypeUtils.cpp - Analysis/src/Type.cpp Analysis/src/Unifiable.cpp Analysis/src/Unifier.cpp ) @@ -326,6 +327,7 @@ if(TARGET Luau.Analyze.CLI) endif() if(TARGET Luau.Ast.CLI) + # Luau.Ast.CLI Sources target_sources(Luau.Ast.CLI PRIVATE CLI/Ast.cpp CLI/FileUtils.h @@ -415,6 +417,7 @@ if(TARGET Luau.UnitTest) tests/TypeVar.test.cpp tests/Variant.test.cpp tests/VisitType.test.cpp + tests/InsertionOrderedMap.test.cpp tests/main.cpp) endif() @@ -449,9 +452,20 @@ if(TARGET Luau.Web) endif() if(TARGET Luau.Reduce.CLI) + # Luau.Reduce.CLI Sources target_sources(Luau.Reduce.CLI PRIVATE CLI/Reduce.cpp CLI/FileUtils.cpp CLI/FileUtils.h ) endif() + +if(TARGET Luau.Compile.CLI) + # Luau.Compile.CLI Sources + target_sources(Luau.Compile.CLI PRIVATE + CLI/FileUtils.h + CLI/FileUtils.cpp + CLI/Flags.h + CLI/Flags.cpp + CLI/Compile.cpp) +endif() diff --git a/VM/src/ldo.cpp b/VM/src/ldo.cpp index 7f58d963..e5fde4d4 100644 --- a/VM/src/ldo.cpp +++ b/VM/src/ldo.cpp @@ -17,8 +17,6 @@ #include -LUAU_FASTFLAGVARIABLE(LuauUniformTopHandling, false) - /* ** {====================================================== ** Error-recovery functions @@ -252,7 +250,7 @@ void luaD_call(lua_State* L, StkId func, int nresults) L->isactive = false; } - if (FFlag::LuauUniformTopHandling && nresults != LUA_MULTRET) + if (nresults != LUA_MULTRET) L->top = restorestack(L, old_func) + nresults; L->nCcalls--; diff --git a/VM/src/lvmexecute.cpp b/VM/src/lvmexecute.cpp index 280c4792..79bf807b 100644 --- a/VM/src/lvmexecute.cpp +++ b/VM/src/lvmexecute.cpp @@ -16,7 +16,6 @@ #include -LUAU_FASTFLAG(LuauUniformTopHandling) LUAU_FASTFLAG(LuauGetImportDirect) // Disable c99-designator to avoid the warning in CGOTO dispatch table @@ -1043,8 +1042,6 @@ reentry: // we're done! if (LUAU_UNLIKELY(ci->flags & LUA_CALLINFO_RETURN)) { - if (!FFlag::LuauUniformTopHandling) - L->top = res; goto exit; } diff --git a/tests/AssemblyBuilderA64.test.cpp b/tests/AssemblyBuilderA64.test.cpp index c917a7bb..ba8d40c2 100644 --- a/tests/AssemblyBuilderA64.test.cpp +++ b/tests/AssemblyBuilderA64.test.cpp @@ -460,6 +460,25 @@ TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "Undefined") SINGLE_COMPARE(udf(), 0x00000000); } +TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "PrePostIndexing") +{ + SINGLE_COMPARE(ldr(x0, mem(x1, 1)), 0xF8401020); + SINGLE_COMPARE(ldr(x0, mem(x1, 1, AddressKindA64::pre)), 0xF8401C20); + SINGLE_COMPARE(ldr(x0, mem(x1, 1, AddressKindA64::post)), 0xF8401420); + + SINGLE_COMPARE(ldr(q0, mem(x1, 1)), 0x3CC01020); + SINGLE_COMPARE(ldr(q0, mem(x1, 1, AddressKindA64::pre)), 0x3CC01C20); + SINGLE_COMPARE(ldr(q0, mem(x1, 1, AddressKindA64::post)), 0x3CC01420); + + SINGLE_COMPARE(str(x0, mem(x1, 1)), 0xF8001020); + SINGLE_COMPARE(str(x0, mem(x1, 1, AddressKindA64::pre)), 0xF8001C20); + SINGLE_COMPARE(str(x0, mem(x1, 1, AddressKindA64::post)), 0xF8001420); + + SINGLE_COMPARE(str(q0, mem(x1, 1)), 0x3C801020); + SINGLE_COMPARE(str(q0, mem(x1, 1, AddressKindA64::pre)), 0x3C801C20); + SINGLE_COMPARE(str(q0, mem(x1, 1, AddressKindA64::post)), 0x3C801420); +} + TEST_CASE("LogTest") { AssemblyBuilderA64 build(/* logText= */ true); @@ -501,6 +520,10 @@ TEST_CASE("LogTest") build.ubfx(x1, x2, 37, 5); + build.ldr(x0, mem(x1, 1)); + build.ldr(x0, mem(x1, 1, AddressKindA64::pre)); + build.ldr(x0, mem(x1, 1, AddressKindA64::post)); + build.setLabel(l); build.ret(); @@ -534,6 +557,9 @@ TEST_CASE("LogTest") tbz x0,#5,.L1 fcvt s1,d2 ubfx x1,x2,#3705 + ldr x0,[x1,#1] + ldr x0,[x1,#1]! + ldr x0,[x1]!,#1 .L1: ret )"; diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index d66eb18e..b8dee997 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -3471,8 +3471,6 @@ local a: T@1 TEST_CASE_FIXTURE(ACFixture, "frontend_use_correct_global_scope") { - ScopedFastFlag sff("LuauTypeCheckerUseCorrectScope", true); - loadDefinition(R"( declare class Instance Name: string diff --git a/tests/InsertionOrderedMap.test.cpp b/tests/InsertionOrderedMap.test.cpp new file mode 100644 index 00000000..ca6f1499 --- /dev/null +++ b/tests/InsertionOrderedMap.test.cpp @@ -0,0 +1,140 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/InsertionOrderedMap.h" + +#include + +#include "doctest.h" + +using namespace Luau; + +struct MapFixture +{ + std::vector> ptrs; + + int* makePtr() + { + ptrs.push_back(std::make_unique(int{})); + return ptrs.back().get(); + } +}; + +TEST_SUITE_BEGIN("InsertionOrderedMap"); + +TEST_CASE_FIXTURE(MapFixture, "map_insertion") +{ + InsertionOrderedMap map; + + int* a = makePtr(); + int* b = makePtr(); + + map.insert(a, 1); + map.insert(b, 2); +} + +TEST_CASE_FIXTURE(MapFixture, "map_lookup") +{ + InsertionOrderedMap map; + + int* a = makePtr(); + map.insert(a, 1); + + int* r = map.get(a); + REQUIRE(r != nullptr); + CHECK(*r == 1); + + r = map.get(makePtr()); + CHECK(r == nullptr); +} + +TEST_CASE_FIXTURE(MapFixture, "insert_does_not_update") +{ + InsertionOrderedMap map; + + int* k = makePtr(); + map.insert(k, 1); + map.insert(k, 2); + + int* v = map.get(k); + REQUIRE(v != nullptr); + CHECK(*v == 1); +} + +TEST_CASE_FIXTURE(MapFixture, "insertion_order_is_iteration_order") +{ + // This one is a little hard to prove, in that if the ordering guarantees + // fail this test isn't guaranteed to fail, but it is strictly better than + // nothing. + + InsertionOrderedMap map; + int* a = makePtr(); + int* b = makePtr(); + int* c = makePtr(); + map.insert(a, 1); + map.insert(b, 1); + map.insert(c, 1); + + auto it = map.begin(); + REQUIRE(it != map.end()); + CHECK(it->first == a); + CHECK(it->second == 1); + + ++it; + REQUIRE(it != map.end()); + CHECK(it->first == b); + CHECK(it->second == 1); + + ++it; + REQUIRE(it != map.end()); + CHECK(it->first == c); + CHECK(it->second == 1); + + ++it; + CHECK(it == map.end()); +} + +TEST_CASE_FIXTURE(MapFixture, "destructuring_iterator_compiles") +{ + // This test's only purpose is to successfully compile. + InsertionOrderedMap map; + + for (auto [k, v] : map) + { + // Checks here solely to silence unused variable warnings. + CHECK(k); + CHECK(v > 0); + } +} + +TEST_CASE_FIXTURE(MapFixture, "map_erasure") +{ + InsertionOrderedMap map; + + int* a = makePtr(); + int* b = makePtr(); + + map.insert(a, 1); + map.insert(b, 2); + + map.erase(map.find(a)); + CHECK(map.size() == 1); + CHECK(!map.contains(a)); + CHECK(map.get(a) == nullptr); + + int* v = map.get(b); + REQUIRE(v); +} + +TEST_CASE_FIXTURE(MapFixture, "map_clear") +{ + InsertionOrderedMap map; + int* a = makePtr(); + + map.insert(a, 1); + + map.clear(); + CHECK(map.size() == 0); + CHECK(!map.contains(a)); + CHECK(map.get(a) == nullptr); +} + +TEST_SUITE_END(); diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index 32634225..5b0c44d0 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -74,6 +74,35 @@ public: CHECK(target.f == inst.f); } + void defineCfgTree(const std::vector>& successorSets) + { + for (const std::vector& successorSet : successorSets) + { + build.beginBlock(build.block(IrBlockKind::Internal)); + + build.function.cfg.successorsOffsets.push_back(uint32_t(build.function.cfg.successors.size())); + build.function.cfg.successors.insert(build.function.cfg.successors.end(), successorSet.begin(), successorSet.end()); + } + + // Brute-force the predecessor list + for (int i = 0; i < int(build.function.blocks.size()); i++) + { + build.function.cfg.predecessorsOffsets.push_back(uint32_t(build.function.cfg.predecessors.size())); + + for (int k = 0; k < int(build.function.blocks.size()); k++) + { + for (uint32_t succIdx : successors(build.function.cfg, k)) + { + if (succIdx == uint32_t(i)) + build.function.cfg.predecessors.push_back(k); + } + } + } + + computeCfgImmediateDominators(build.function); + computeCfgDominanceTreeChildren(build.function); + } + IrBuilder build; // Luau.VM headers are not accessible @@ -2164,6 +2193,30 @@ bb_0: )"); } +// 'A Simple, Fast Dominance Algorithm' [Keith D. Cooper, et al]. Figure 2. +TEST_CASE_FIXTURE(IrBuilderFixture, "DominanceVerification1") +{ + defineCfgTree({{1, 2}, {3}, {4}, {4}, {3}}); + + CHECK(build.function.cfg.idoms == std::vector{{~0u, 0, 0, 0, 0}}); +} + +// 'A Linear Time Algorithm for Placing Phi-Nodes' [Vugranam C.Sreedhar]. Figure 1. +TEST_CASE_FIXTURE(IrBuilderFixture, "DominanceVerification2") +{ + defineCfgTree({{1, 16}, {2, 3, 4}, {4, 7}, {9}, {5}, {6}, {2, 8}, {8}, {7, 15}, {10, 11}, {12}, {12}, {13}, {3, 14, 15}, {12}, {16}, {}}); + + CHECK(build.function.cfg.idoms == std::vector{~0u, 0, 1, 1, 1, 4, 5, 1, 1, 3, 9, 9, 9, 12, 13, 1, 0}); +} + +// 'A Linear Time Algorithm for Placing Phi-Nodes' [Vugranam C.Sreedhar]. Figure 4. +TEST_CASE_FIXTURE(IrBuilderFixture, "DominanceVerification3") +{ + defineCfgTree({{1, 2}, {3}, {3, 4}, {5}, {5, 6}, {7}, {7}, {}}); + + CHECK(build.function.cfg.idoms == std::vector{~0u, 0, 0, 0, 2, 0, 4, 0}); +} + TEST_SUITE_END(); TEST_SUITE_BEGIN("ValueNumbering"); diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 93ea7510..8fe86655 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -791,14 +791,21 @@ TEST_CASE_FIXTURE(NormalizeFixture, "normalize_blocked_types") CHECK_EQ(normalizer.typeFromNormal(*norm), &blocked); } -TEST_CASE_FIXTURE(NormalizeFixture, "normalize_pending_expansion_types") +TEST_CASE_FIXTURE(NormalizeFixture, "normalize_is_exactly_number") { - AstName name; - Type pending{PendingExpansionType{std::nullopt, name, {}, {}}}; + const NormalizedType* number = normalizer.normalize(builtinTypes->numberType); + // 1. all types for which Types::number say true for, NormalizedType::isExactlyNumber should say true as well + CHECK(Luau::isNumber(builtinTypes->numberType) == number->isExactlyNumber()); + // 2. isExactlyNumber should handle cases like `number & number` + TypeId intersection = arena.addType(IntersectionType{{builtinTypes->numberType, builtinTypes->numberType}}); + const NormalizedType* normIntersection = normalizer.normalize(intersection); + CHECK(normIntersection->isExactlyNumber()); - const NormalizedType* norm = normalizer.normalize(&pending); + // 3. isExactlyNumber should reject things that are definitely not precisely numbers `number | any` - CHECK_EQ(normalizer.typeFromNormal(*norm), &pending); + TypeId yoonion = arena.addType(UnionType{{builtinTypes->anyType, builtinTypes->numberType}}); + const NormalizedType* unionIntersection = normalizer.normalize(yoonion); + CHECK(!unionIntersection->isExactlyNumber()); } TEST_SUITE_END(); diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index e5bcfa30..5aabb240 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -14,7 +14,7 @@ using namespace Luau; -LUAU_FASTFLAG(LuauInstantiateInSubtyping); +LUAU_FASTFLAG(LuauInstantiateInSubtyping) TEST_SUITE_BEGIN("TypeInferFunctions"); @@ -2073,4 +2073,20 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_bu CHECK_EQ(toString(result.errors[1]), "Type 'number' could not be converted into 'boolean'"); } +TEST_CASE_FIXTURE(Fixture, "attempt_to_call_an_intersection_of_tables") +{ + CheckResult result = check(R"( + local function f(t: { x: number } & { y: string }) + t() + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ(toString(result.errors[0]), "Cannot call non-function {| x: number |} & {| y: string |}"); + else + CHECK_EQ(toString(result.errors[0]), "Cannot call non-function {| x: number |}"); +} + TEST_SUITE_END(); diff --git a/tools/faillist.txt b/tools/faillist.txt index e7d1f5f4..f049a0ee 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -88,7 +88,6 @@ TableTests.oop_polymorphic TableTests.quantify_even_that_table_was_never_exported_at_all TableTests.quantify_metatables_of_metatables_of_table TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table -TableTests.result_is_bool_for_equality_operators_if_lhs_is_any TableTests.right_table_missing_key2 TableTests.shared_selfs TableTests.shared_selfs_from_free_param @@ -167,7 +166,6 @@ TypeInferOperators.CallOrOfFunctions TypeInferOperators.cli_38355_recursive_union TypeInferOperators.compound_assign_mismatch_metatable TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops -TypeInferOperators.luau-polyfill.String.slice TypeInferOperators.operator_eq_completely_incompatible TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs