From e905e305702388543dac9040667968c0b856506d Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:29:30 -0800 Subject: [PATCH] Sync to upstream/release/652 (#1525) ## What's new? * Add support for mixed-mode type checking, which allows modules checked in the old type solver to be checked and autocompleted by the new one. * Generalize `RequireResolver` to support require-by-string semantics in `luau-analyze`. * Fix a bug in incremental autocomplete where `DefId`s associated with index expressions were not correctly picked up. * Fix a bug that prevented "complex" types in generic parameters (for example, `local x: X<(() -> ())?>`). ### Issues fixed * #1507 * #1518 --- Internal Contributors: Co-authored-by: Aaron Weiss Co-authored-by: Andy Friesen Co-authored-by: Hunter Goldstein Co-authored-by: Varun Saini Co-authored-by: Vighnesh Vijay --- .gitignore | 1 + Analysis/include/Luau/DataFlowGraph.h | 19 - Analysis/include/Luau/FragmentAutocomplete.h | 13 +- Analysis/include/Luau/Module.h | 3 + Analysis/src/AutocompleteCore.cpp | 1 - Analysis/src/BuiltinDefinitions.cpp | 1 - Analysis/src/ConstraintGenerator.cpp | 209 +--- Analysis/src/ConstraintSolver.cpp | 71 +- Analysis/src/DataFlowGraph.cpp | 55 -- Analysis/src/EqSatSimplification.cpp | 11 +- Analysis/src/FragmentAutocomplete.cpp | 151 ++- Analysis/src/Frontend.cpp | 3 + Analysis/src/Generalization.cpp | 127 +-- Analysis/src/Module.cpp | 46 +- Analysis/src/NonStrictTypeChecker.cpp | 126 ++- Analysis/src/Simplify.cpp | 6 - Analysis/src/Subtyping.cpp | 15 +- Analysis/src/TableLiteralInference.cpp | 16 +- Analysis/src/TypeChecker2.cpp | 20 +- Analysis/src/TypeFunction.cpp | 5 +- Analysis/src/TypeFunctionRuntime.cpp | 10 +- Analysis/src/TypeInfer.cpp | 53 - Ast/src/Ast.cpp | 7 +- Ast/src/Parser.cpp | 74 +- CLI/Analyze.cpp | 60 +- CLI/FileUtils.cpp | 33 +- CLI/Flags.cpp | 10 - CLI/Repl.cpp | 109 ++- CLI/Require.cpp | 248 ++--- CLI/Require.h | 66 +- Makefile | 2 +- Sources.cmake | 4 +- VM/src/lobject.cpp | 7 + tests/Fixture.cpp | 9 +- tests/Fixture.h | 4 +- tests/FragmentAutocomplete.test.cpp | 910 +++++++++++++++--- tests/NonStrictTypeChecker.test.cpp | 24 + tests/Parser.test.cpp | 65 ++ tests/RequireByString.test.cpp | 11 +- tests/TypeInfer.builtins.test.cpp | 1 - tests/TypeInfer.tables.test.cpp | 30 +- tests/TypeInfer.unionTypes.test.cpp | 6 +- tests/main.cpp | 9 - .../without_config/validate_cache.luau | 4 + 44 files changed, 1750 insertions(+), 905 deletions(-) create mode 100644 tests/require/without_config/validate_cache.luau diff --git a/.gitignore b/.gitignore index 764b97cf9..5852b330c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ /luau /luau-tests /luau-analyze +/luau-bytecode /luau-compile __pycache__ .cache diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index 662e50aa1..b3fc8f05f 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -126,25 +126,6 @@ struct DataFlowGraphBuilder NotNull handle ); - /** - * Takes a stale graph along with a list of scopes, a small fragment of the ast, and a cursor position - * and constructs the DataFlowGraph for just that fragment. This method will fabricate defs in the final - * DFG for things that have been referenced and exist in the stale dfg. - * For example, the fragment local z = x + y will populate defs for x and y from the stale graph. - * @param staleGraph - the old DFG - * @param scopes - the old DfgScopes in the graph - * @param fragment - the Ast Fragment to re-build the root for - * @param cursorPos - the current location of the cursor - used to determine which scope we are currently in - * @param handle - for internal compiler errors - */ - static DataFlowGraph updateGraph( - const DataFlowGraph& staleGraph, - const std::vector>& scopes, - AstStatBlock* fragment, - const Position& cursorPos, - NotNull handle - ); - private: DataFlowGraphBuilder() = default; diff --git a/Analysis/include/Luau/FragmentAutocomplete.h b/Analysis/include/Luau/FragmentAutocomplete.h index 50c456f11..2bbba6e67 100644 --- a/Analysis/include/Luau/FragmentAutocomplete.h +++ b/Analysis/include/Luau/FragmentAutocomplete.h @@ -49,14 +49,20 @@ struct FragmentAutocompleteResult FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos); -FragmentParseResult parseFragment(const SourceModule& srcModule, std::string_view src, const Position& cursorPos); +FragmentParseResult parseFragment( + const SourceModule& srcModule, + std::string_view src, + const Position& cursorPos, + std::optional fragmentEndPosition +); FragmentTypeCheckResult typecheckFragment( Frontend& frontend, const ModuleName& moduleName, const Position& cursorPos, std::optional opts, - std::string_view src + std::string_view src, + std::optional fragmentEndPosition ); FragmentAutocompleteResult fragmentAutocomplete( @@ -65,7 +71,8 @@ FragmentAutocompleteResult fragmentAutocomplete( const ModuleName& moduleName, Position cursorPosition, std::optional opts, - StringCompletionCallback callback + StringCompletionCallback callback, + std::optional fragmentEndPosition = std::nullopt ); diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index 82c189aa1..61877d51b 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -68,6 +68,9 @@ struct Module { ~Module(); + // TODO: Clip this when we clip FFlagLuauSolverV2 + bool checkedInNewSolver = false; + ModuleName name; std::string humanReadableName; diff --git a/Analysis/src/AutocompleteCore.cpp b/Analysis/src/AutocompleteCore.cpp index ee0457718..06080b8c4 100644 --- a/Analysis/src/AutocompleteCore.cpp +++ b/Analysis/src/AutocompleteCore.cpp @@ -21,7 +21,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(AutocompleteRequirePathSuggestions2) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 3dacae04f..c0a50ec33 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -29,7 +29,6 @@ */ LUAU_FASTFLAG(LuauSolverV2) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTFLAGVARIABLE(LuauTypestateBuiltins2) LUAU_FASTFLAGVARIABLE(LuauStringFormatArityFix) diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index ee602999a..07458279e 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -32,7 +32,6 @@ LUAU_FASTINT(LuauCheckRecursionLimit) LUAU_FASTFLAG(DebugLuauLogSolverToJson) LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTFLAG(LuauTypestateBuiltins2) LUAU_FASTFLAGVARIABLE(LuauNewSolverVisitErrorExprLvalues) @@ -225,8 +224,7 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) Checkpoint start = checkpoint(this); - ControlFlow cf = - DFInt::LuauTypeSolverRelease >= 646 ? visitBlockWithoutChildScope(scope, block) : visitBlockWithoutChildScope_DEPRECATED(scope, block); + ControlFlow cf = visitBlockWithoutChildScope(scope, block); if (cf == ControlFlow::None) addConstraint(scope, block->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, rootScope->returnType}); @@ -876,123 +874,6 @@ ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& sco return firstControlFlow.value_or(ControlFlow::None); } -ControlFlow ConstraintGenerator::visitBlockWithoutChildScope_DEPRECATED(const ScopePtr& scope, AstStatBlock* block) -{ - RecursionCounter counter{&recursionCount}; - - if (recursionCount >= FInt::LuauCheckRecursionLimit) - { - reportCodeTooComplex(block->location); - return ControlFlow::None; - } - - std::unordered_map aliasDefinitionLocations; - - // In order to enable mutually-recursive type aliases, we need to - // populate the type bindings before we actually check any of the - // alias statements. - for (AstStat* stat : block->body) - { - if (auto alias = stat->as()) - { - if (scope->exportedTypeBindings.count(alias->name.value) || scope->privateTypeBindings.count(alias->name.value)) - { - auto it = aliasDefinitionLocations.find(alias->name.value); - LUAU_ASSERT(it != aliasDefinitionLocations.end()); - reportError(alias->location, DuplicateTypeDefinition{alias->name.value, it->second}); - continue; - } - - // A type alias might have no name if the code is syntactically - // illegal. We mustn't prepopulate anything in this case. - if (alias->name == kParseNameError || alias->name == "typeof") - continue; - - ScopePtr defnScope = childScope(alias, scope); - - TypeId initialType = arena->addType(BlockedType{}); - TypeFun initialFun{initialType}; - - for (const auto& [name, gen] : createGenerics(defnScope, alias->generics, /* useCache */ true)) - { - initialFun.typeParams.push_back(gen); - } - - for (const auto& [name, genPack] : createGenericPacks(defnScope, alias->genericPacks, /* useCache */ true)) - { - initialFun.typePackParams.push_back(genPack); - } - - if (alias->exported) - scope->exportedTypeBindings[alias->name.value] = std::move(initialFun); - else - scope->privateTypeBindings[alias->name.value] = std::move(initialFun); - - astTypeAliasDefiningScopes[alias] = defnScope; - aliasDefinitionLocations[alias->name.value] = alias->location; - } - else if (auto function = stat->as()) - { - // If a type function w/ same name has already been defined, error for having duplicates - if (scope->exportedTypeBindings.count(function->name.value) || scope->privateTypeBindings.count(function->name.value)) - { - auto it = aliasDefinitionLocations.find(function->name.value); - LUAU_ASSERT(it != aliasDefinitionLocations.end()); - reportError(function->location, DuplicateTypeDefinition{function->name.value, it->second}); - continue; - } - - if (scope->parent != globalScope) - { - reportError(function->location, GenericError{"Local user-defined functions are not supported yet"}); - continue; - } - - ScopePtr defnScope = childScope(function, scope); - - // Create TypeFunctionInstanceType - - std::vector typeParams; - typeParams.reserve(function->body->args.size); - - std::vector quantifiedTypeParams; - quantifiedTypeParams.reserve(function->body->args.size); - - for (size_t i = 0; i < function->body->args.size; i++) - { - std::string name = format("T%zu", i); - TypeId ty = arena->addType(GenericType{name}); - typeParams.push_back(ty); - - GenericTypeDefinition genericTy{ty}; - quantifiedTypeParams.push_back(genericTy); - } - - if (std::optional error = typeFunctionRuntime->registerFunction(function)) - reportError(function->location, GenericError{*error}); - - TypeId typeFunctionTy = - arena->addType(TypeFunctionInstanceType{NotNull{&builtinTypeFunctions().userFunc}, std::move(typeParams), {}, function->name, {}}); - - TypeFun typeFunction{std::move(quantifiedTypeParams), typeFunctionTy}; - - // Set type bindings and definition locations for this user-defined type function - scope->privateTypeBindings[function->name.value] = std::move(typeFunction); - aliasDefinitionLocations[function->name.value] = function->location; - } - } - - std::optional firstControlFlow; - for (AstStat* stat : block->body) - { - ControlFlow cf = visit(scope, stat); - if (cf != ControlFlow::None && !firstControlFlow) - firstControlFlow = cf; - } - - return firstControlFlow.value_or(ControlFlow::None); -} - ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat) { RecursionLimiter limiter{&recursionCount, FInt::LuauCheckRecursionLimit}; @@ -1336,10 +1217,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatRepeat* rep { ScopePtr repeatScope = childScope(repeat, scope); - if (DFInt::LuauTypeSolverRelease >= 646) - visitBlockWithoutChildScope(repeatScope, repeat->body); - else - visitBlockWithoutChildScope_DEPRECATED(repeatScope, repeat->body); + visitBlockWithoutChildScope(repeatScope, repeat->body); check(repeatScope, repeat->condition); @@ -1513,8 +1391,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatBlock* bloc { ScopePtr innerScope = childScope(block, scope); - ControlFlow flow = DFInt::LuauTypeSolverRelease >= 646 ? visitBlockWithoutChildScope(innerScope, block) - : visitBlockWithoutChildScope_DEPRECATED(innerScope, block); + ControlFlow flow = visitBlockWithoutChildScope(innerScope, block); // An AstStatBlock has linear control flow, i.e. one entry and one exit, so we can inherit // all the changes to the environment occurred by the statements in that block. @@ -1705,7 +1582,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunctio TypeFun typeFunction = bindingIt->second; // Adding typeAliasExpansionConstraint on user-defined type function for the constraint solver - if (auto typeFunctionTy = get(DFInt::LuauTypeSolverRelease >= 646 ? follow(typeFunction.type) : typeFunction.type)) + if (auto typeFunctionTy = get(follow(typeFunction.type))) { TypeId expansionTy = arena->addType(PendingExpansionType{{}, function->name, typeFunctionTy->typeArguments, typeFunctionTy->packArguments}); addConstraint(scope, function->location, TypeAliasExpansionConstraint{/* target */ expansionTy}); @@ -3026,32 +2903,12 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, { Unifier2 unifier{arena, builtinTypes, NotNull{scope.get()}, ice}; std::vector toBlock; - if (DFInt::LuauTypeSolverRelease >= 648) - { - // This logic is incomplete as we want to re-run this - // _after_ blocked types have resolved, but this - // allows us to do some bidirectional inference. - toBlock = findBlockedTypesIn(expr, NotNull{&module->astTypes}); - if (toBlock.empty()) - { - matchLiteralType( - NotNull{&module->astTypes}, - NotNull{&module->astExpectedTypes}, - builtinTypes, - arena, - NotNull{&unifier}, - *expectedType, - ty, - expr, - toBlock - ); - // The visitor we ran prior should ensure that there are no - // blocked types that we would encounter while matching on - // this expression. - LUAU_ASSERT(toBlock.empty()); - } - } - else + // This logic is incomplete as we want to re-run this + // _after_ blocked types have resolved, but this + // allows us to do some bidirectional inference. + toBlock = findBlockedTypesIn(expr, NotNull{&module->astTypes}); + + if (toBlock.empty()) { matchLiteralType( NotNull{&module->astTypes}, @@ -3063,7 +2920,11 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, ty, expr, toBlock - ); + ); + // The visitor we ran prior should ensure that there are no + // blocked types that we would encounter while matching on + // this expression. + LUAU_ASSERT(toBlock.empty()); } } @@ -3265,8 +3126,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn) { // If it is possible for execution to reach the end of the function, the return type must be compatible with () - ControlFlow cf = - DFInt::LuauTypeSolverRelease >= 646 ? visitBlockWithoutChildScope(scope, fn->body) : visitBlockWithoutChildScope_DEPRECATED(scope, fn->body); + ControlFlow cf = visitBlockWithoutChildScope(scope, fn->body); if (cf == ControlFlow::None) addConstraint(scope, fn->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, scope->returnType}); } @@ -3745,11 +3605,18 @@ struct FragmentTypeCheckGlobalPrepopulator : AstVisitor const NotNull globalScope; const NotNull currentScope; const NotNull dfg; + const NotNull arena; - FragmentTypeCheckGlobalPrepopulator(NotNull globalScope, NotNull currentScope, NotNull dfg) + FragmentTypeCheckGlobalPrepopulator( + NotNull globalScope, + NotNull currentScope, + NotNull dfg, + NotNull arena + ) : globalScope(globalScope) , currentScope(currentScope) , dfg(dfg) + , arena(arena) { } @@ -3761,6 +3628,32 @@ struct FragmentTypeCheckGlobalPrepopulator : AstVisitor // We only want to write into the current scope the type of the global currentScope->lvalueTypes[def] = *ty; } + else if (auto ty = currentScope->lookup(global->name)) + { + // We are trying to create a binding for a brand new function, so we actually do have to write it into the scope. + DefId def = dfg->getDef(global); + // We only want to write into the current scope the type of the global + currentScope->lvalueTypes[def] = *ty; + } + + return true; + } + + bool visit(AstStatFunction* function) override + { + if (AstExprGlobal* g = function->name->as()) + { + if (auto ty = globalScope->lookup(g->name)) + { + currentScope->bindings[g->name] = Binding{*ty}; + } + else + { + // Hasn't existed since a previous typecheck + TypeId bt = arena->addType(BlockedType{}); + currentScope->bindings[g->name] = Binding{bt}; + } + } return true; } @@ -3814,7 +3707,7 @@ struct GlobalPrepopulator : AstVisitor void ConstraintGenerator::prepopulateGlobalScopeForFragmentTypecheck(const ScopePtr& globalScope, const ScopePtr& resumeScope, AstStatBlock* program) { - FragmentTypeCheckGlobalPrepopulator gp{NotNull{globalScope.get()}, NotNull{resumeScope.get()}, dfg}; + FragmentTypeCheckGlobalPrepopulator gp{NotNull{globalScope.get()}, NotNull{resumeScope.get()}, dfg, arena}; if (prepareModuleScope) prepareModuleScope(module->name, resumeScope); program->visit(&gp); diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 2b7a72320..65a1a5113 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -31,7 +31,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies) LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings) LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTFLAGVARIABLE(LuauRemoveNotAnyHack) LUAU_FASTFLAGVARIABLE(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauNewSolverPopulateTableLocations) @@ -919,19 +918,10 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul auto bindResult = [this, &c, constraint](TypeId result) { - if (DFInt::LuauTypeSolverRelease >= 646) - { - auto cTarget = follow(c.target); - LUAU_ASSERT(get(cTarget)); - shiftReferences(cTarget, result); - bind(constraint, cTarget, result); - } - else - { - LUAU_ASSERT(get(c.target)); - shiftReferences(c.target, result); - bind(constraint, c.target, result); - } + auto cTarget = follow(c.target); + LUAU_ASSERT(get(cTarget)); + shiftReferences(cTarget, result); + bind(constraint, cTarget, result); }; std::optional tf = (petv->prefix) ? constraint->scope->lookupImportedType(petv->prefix->value, petv->name.value) @@ -959,7 +949,7 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul // Due to how pending expansion types and TypeFun's are created // If this check passes, we have created a cyclic / corecursive type alias // of size 0 - TypeId lhs = DFInt::LuauTypeSolverRelease >= 646 ? follow(c.target) : c.target; + TypeId lhs = follow(c.target); TypeId rhs = tf->type; if (occursCheck(lhs, rhs)) { @@ -1343,21 +1333,18 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull= 648) + // This is expensive as we need to traverse a (potentially large) + // literal up front in order to determine if there are any blocked + // types, otherwise we may run `matchTypeLiteral` multiple times, + // which right now may fail due to being non-idempotent (it + // destructively updates the underlying literal type). + auto blockedTypes = findBlockedArgTypesIn(c.callSite, c.astTypes); + for (const auto ty : blockedTypes) { - // This is expensive as we need to traverse a (potentially large) - // literal up front in order to determine if there are any blocked - // types, otherwise we may run `matchTypeLiteral` multiple times, - // which right now may fail due to being non-idempotent (it - // destructively updates the underlying literal type). - auto blockedTypes = findBlockedArgTypesIn(c.callSite, c.astTypes); - for (const auto ty : blockedTypes) - { - block(ty, constraint); - } - if (!blockedTypes.empty()) - return false; + block(ty, constraint); } + if (!blockedTypes.empty()) + return false; // We know the type of the function and the arguments it expects to receive. // We also know the TypeIds of the actual arguments that will be passed. @@ -1454,17 +1441,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNullscope, NotNull{&iceReporter}}; std::vector toBlock; (void)matchLiteralType(c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, expectedArgTy, actualArgTy, expr, toBlock); - if (DFInt::LuauTypeSolverRelease >= 648) - { - LUAU_ASSERT(toBlock.empty()); - } - else - { - for (auto t : toBlock) - block(t, constraint); - if (!toBlock.empty()) - return false; - } + LUAU_ASSERT(toBlock.empty()); } } @@ -1498,17 +1475,9 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNulllowerBound; - if (DFInt::LuauTypeSolverRelease >= 645) - { - auto ty = follow(c.freeType); - shiftReferences(ty, bindTo); - bind(constraint, ty, bindTo); - } - else - { - shiftReferences(c.freeType, bindTo); - bind(constraint, c.freeType, bindTo); - } + auto ty = follow(c.freeType); + shiftReferences(ty, bindTo); + bind(constraint, ty, bindTo); return true; } @@ -1793,7 +1762,7 @@ bool ConstraintSolver::tryDispatch(const AssignPropConstraint& c, NotNull(lhsType)) { - auto lhsFreeUpperBound = DFInt::LuauTypeSolverRelease >= 648 ? follow(lhsFree->upperBound) : lhsFree->upperBound; + auto lhsFreeUpperBound = follow(lhsFree->upperBound); if (get(lhsFreeUpperBound) || get(lhsFreeUpperBound)) lhsType = lhsFreeUpperBound; else diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index 246edb89b..0becd49d2 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -182,8 +182,6 @@ DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNulllocation); @@ -208,8 +206,6 @@ std::pair, std::vector> LUAU_TIMETRACE_SCOPE("DataFlowGraphBuilder::build", "Typechecking"); - LUAU_ASSERT(FFlag::LuauSolverV2); - DataFlowGraphBuilder builder; builder.handle = handle; DfgScope* moduleScope = builder.makeChildScope(block->location); @@ -226,56 +222,6 @@ std::pair, std::vector> return {std::make_shared(std::move(builder.graph)), std::move(builder.scopes)}; } -DataFlowGraph DataFlowGraphBuilder::updateGraph( - const DataFlowGraph& staleGraph, - const std::vector>& scopes, - AstStatBlock* fragment, - const Position& cursorPos, - NotNull handle -) -{ - LUAU_TIMETRACE_SCOPE("DataFlowGraphBuilder::build", "Typechecking"); - LUAU_ASSERT(FFlag::LuauSolverV2); - - DataFlowGraphBuilder builder; - builder.handle = handle; - // Generate a list of prepopulated locals - ReferencedDefFinder finder; - fragment->visit(&finder); - for (AstLocal* loc : finder.referencedLocalDefs) - { - if (staleGraph.localDefs.contains(loc)) - { - builder.graph.localDefs[loc] = *staleGraph.localDefs.find(loc); - } - } - - // Figure out which scope we should start re-accumulating DFG information from again - DfgScope* nearest = nullptr; - for (auto& sc : scopes) - { - if (nearest == nullptr || (sc->location.begin <= cursorPos && nearest->location.begin < sc->location.begin)) - nearest = sc.get(); - } - - // The scope stack should start with the nearest enclosing scope so we can resume DFG'ing correctly - PushScope ps{builder.scopeStack, nearest}; - // Conspire for the current scope in the scope stack to be a fresh dfg scope, parented to the above nearest enclosing scope, so any insertions are - // isolated there - DfgScope* scope = builder.makeChildScope(fragment->location); - PushScope psAgain{builder.scopeStack, scope}; - - builder.visitBlockWithoutChildScope(fragment); - - if (FFlag::DebugLuauFreezeArena) - { - builder.defArena->allocator.freeze(); - builder.keyArena->allocator.freeze(); - } - - return std::move(builder.graph); -} - void DataFlowGraphBuilder::resolveCaptures() { for (const auto& [_, capture] : captures) @@ -982,7 +928,6 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprCall* c) DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprIndexName* i) { auto [parentDef, parentKey] = visitExpr(i->expr); - std::string index = i->index.value; DefId def = lookup(parentDef, index); diff --git a/Analysis/src/EqSatSimplification.cpp b/Analysis/src/EqSatSimplification.cpp index 41e87de20..9e69baf50 100644 --- a/Analysis/src/EqSatSimplification.cpp +++ b/Analysis/src/EqSatSimplification.cpp @@ -23,6 +23,7 @@ #include LUAU_FASTFLAGVARIABLE(DebugLuauLogSimplification) +LUAU_FASTFLAGVARIABLE(DebugLuauLogSimplificationToDot) LUAU_FASTFLAGVARIABLE(DebugLuauExtraEqSatSanityChecks) namespace Luau::EqSatSimplification @@ -2327,7 +2328,7 @@ std::optional eqSatSimplify(NotNull simpl int count = 0; const int MAX_COUNT = 1000; - if (FFlag::DebugLuauLogSimplification) + if (FFlag::DebugLuauLogSimplificationToDot) std::ofstream("begin.dot") << toDot(simplifier->stringCache, simplifier->egraph); auto& egraph = simplifier->egraph; @@ -2409,11 +2410,11 @@ std::optional eqSatSimplify(NotNull simpl ++count; - if (FFlag::DebugLuauLogSimplification) - { - if (isFresh) - std::cout << "count=" << std::setw(3) << count << "\t" << subst.desc << '\n'; + if (FFlag::DebugLuauLogSimplification && isFresh) + std::cout << "count=" << std::setw(3) << count << "\t" << subst.desc << '\n'; + if (FFlag::DebugLuauLogSimplificationToDot) + { std::string filename = format("step%03d.dot", count); std::ofstream(filename) << toDot(simplifier->stringCache, egraph); } diff --git a/Analysis/src/FragmentAutocomplete.cpp b/Analysis/src/FragmentAutocomplete.cpp index 3395f1253..7515ee986 100644 --- a/Analysis/src/FragmentAutocomplete.cpp +++ b/Analysis/src/FragmentAutocomplete.cpp @@ -3,6 +3,7 @@ #include "Luau/Ast.h" #include "Luau/AstQuery.h" +#include "Luau/Autocomplete.h" #include "Luau/Common.h" #include "Luau/EqSatSimplification.h" #include "Luau/Parser.h" @@ -21,6 +22,7 @@ #include "AutocompleteCore.h" + LUAU_FASTINT(LuauTypeInferRecursionLimit); LUAU_FASTINT(LuauTypeInferIterationLimit); LUAU_FASTINT(LuauTarjanChildLimit) @@ -48,6 +50,14 @@ void copyModuleMap(Luau::DenseHashMap& result, const Luau::DenseHashMap options) +{ + if (FFlag::LuauSolverV2 || !options) + return frontend.moduleResolver; + + return options->forAutocomplete ? frontend.moduleResolverForAutocomplete : frontend.moduleResolver; +} + FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos) { std::vector ancestry = findAncestryAtPositionForAutocomplete(root, cursorPos); @@ -93,13 +103,19 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro /** * Get document offsets is a function that takes a source text document as well as a start position and end position(line, column) in that - * document and attempts to get the concrete text between those points. It returns a pair of: + * document and attempts to get the concrete text between those points. It returns a tuple of: * - start offset that represents an index in the source `char*` corresponding to startPos * - length, that represents how many more bytes to read to get to endPos. - * Example - your document is "foo bar baz" and getDocumentOffsets is passed (1, 4) - (1, 8). This function returns the pair {3, 7}, - * which corresponds to the string " bar " + * - cursorPos, that represents the position of the cursor relative to the start offset. + * Example - your document is "foo bar baz" and getDocumentOffsets is passed (0, 4), (0, 7), (0, 8). This function returns the tuple {3, 5, + * Position{0, 4}}, which corresponds to the string " bar " */ -std::pair getDocumentOffsets(const std::string_view& src, const Position& startPos, const Position& endPos) +std::tuple getDocumentOffsets( + const std::string_view& src, + const Position& startPos, + Position cursorPos, + const Position& endPos +) { size_t lineCount = 0; size_t colCount = 0; @@ -108,7 +124,12 @@ std::pair getDocumentOffsets(const std::string_view& src, const size_t startOffset = 0; size_t endOffset = 0; bool foundStart = false; + bool foundCursor = false; bool foundEnd = false; + + unsigned int colOffsetFromStart = 0; + unsigned int lineOffsetFromStart = 0; + for (char c : src) { if (foundStart && foundEnd) @@ -120,6 +141,12 @@ std::pair getDocumentOffsets(const std::string_view& src, const startOffset = docOffset; } + if (cursorPos.line == lineCount && cursorPos.column == colCount) + { + foundCursor = true; + cursorPos = {lineOffsetFromStart, colOffsetFromStart}; + } + if (endPos.line == lineCount && endPos.column == colCount) { endOffset = docOffset; @@ -135,20 +162,32 @@ std::pair getDocumentOffsets(const std::string_view& src, const if (c == '\n') { + if (foundStart) + { + lineOffsetFromStart++; + colOffsetFromStart = 0; + } lineCount++; colCount = 0; } else + { + if (foundStart) + colOffsetFromStart++; colCount++; + } docOffset++; } if (foundStart && !foundEnd) endOffset = src.length(); + if (foundStart && !foundCursor) + cursorPos = {lineOffsetFromStart, colOffsetFromStart}; + size_t min = std::min(startOffset, endOffset); size_t len = std::max(startOffset, endOffset) - min; - return {min, len}; + return {min, len, cursorPos}; } ScopePtr findClosestScope(const ModulePtr& module, const AstStat* nearestStatement) @@ -167,12 +206,17 @@ ScopePtr findClosestScope(const ModulePtr& module, const AstStat* nearestStateme return closest; } -FragmentParseResult parseFragment(const SourceModule& srcModule, std::string_view src, const Position& cursorPos) +FragmentParseResult parseFragment( + const SourceModule& srcModule, + std::string_view src, + const Position& cursorPos, + std::optional fragmentEndPosition +) { FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse(srcModule.root, cursorPos); ParseOptions opts; opts.allowDeclarationSyntax = false; - opts.captureComments = false; + opts.captureComments = true; opts.parseFragment = FragmentParseResumeSettings{std::move(result.localMap), std::move(result.localStack)}; AstStat* nearestStatement = result.nearestStatement; @@ -182,7 +226,7 @@ FragmentParseResult parseFragment(const SourceModule& srcModule, std::string_vie // statement spans multiple lines bool multiline = nearestStatement->location.begin.line != nearestStatement->location.end.line; - const Position endPos = cursorPos; + const Position endPos = fragmentEndPosition.value_or(cursorPos); // We start by re-parsing everything (we'll refine this as we go) Position startPos = srcModule.root->location.begin; @@ -193,10 +237,13 @@ FragmentParseResult parseFragment(const SourceModule& srcModule, std::string_vie // Statement spans one line && cursorPos is on a different line else if (!multiline && cursorPos.line != nearestStatement->location.end.line) startPos = nearestStatement->location.end; + else if (multiline && nearestStatement->location.end.line < cursorPos.line) + startPos = nearestStatement->location.end; else startPos = nearestStatement->location.begin; - auto [offsetStart, parseLength] = getDocumentOffsets(src, startPos, endPos); + auto [offsetStart, parseLength, cursorInFragment] = getDocumentOffsets(src, startPos, cursorPos, endPos); + const char* srcStart = src.data() + offsetStart; std::string_view dbg = src.substr(offsetStart, parseLength); @@ -207,7 +254,11 @@ FragmentParseResult parseFragment(const SourceModule& srcModule, std::string_vie ParseResult p = Luau::Parser::parse(srcStart, parseLength, *nameTbl, *fragmentResult.alloc.get(), opts); std::vector fabricatedAncestry = std::move(result.ancestry); - std::vector fragmentAncestry = findAncestryAtPositionForAutocomplete(p.root, p.root->location.end); + + // Get the ancestry for the fragment at the offset cursor position. + // Consumers have the option to request with fragment end position, so we cannot just use the end position of our parse result as the + // cursor position. Instead, use the cursor position calculated as an offset from our start position. + std::vector fragmentAncestry = findAncestryAtPositionForAutocomplete(p.root, cursorInFragment); fabricatedAncestry.insert(fabricatedAncestry.end(), fragmentAncestry.begin(), fragmentAncestry.end()); if (nearestStatement == nullptr) nearestStatement = p.root; @@ -242,6 +293,46 @@ ModulePtr copyModule(const ModulePtr& result, std::unique_ptr alloc) return incrementalModule; } +struct MixedModeIncrementalTCDefFinder : public AstVisitor +{ + bool visit(AstExprLocal* local) override + { + referencedLocalDefs.push_back({local->local, local}); + return true; + } + // ast defs is just a mapping from expr -> def in general + // will get built up by the dfg builder + + // localDefs, we need to copy over + std::vector> referencedLocalDefs; +}; + +void mixedModeCompatibility( + const ScopePtr& bottomScopeStale, + const ScopePtr& myFakeScope, + const ModulePtr& stale, + NotNull dfg, + AstStatBlock* program +) +{ + // This code does the following + // traverse program + // look for ast refs for locals + // ask for the corresponding defId from dfg + // given that defId, and that expression, in the incremental module, map lvalue types from defID to + + MixedModeIncrementalTCDefFinder finder; + program->visit(&finder); + std::vector> locals = std::move(finder.referencedLocalDefs); + for (auto [loc, expr] : locals) + { + if (std::optional binding = bottomScopeStale->linearSearchForBinding(loc->name.value, true)) + { + myFakeScope->lvalueTypes[dfg->getDef(expr)] = binding->typeId; + } + } +} + FragmentTypeCheckResult typecheckFragment_( Frontend& frontend, AstStatBlock* root, @@ -255,6 +346,7 @@ FragmentTypeCheckResult typecheckFragment_( freeze(stale->internalTypes); freeze(stale->interfaceTypes); ModulePtr incrementalModule = copyModule(stale, std::move(astAllocator)); + incrementalModule->checkedInNewSolver = true; unfreeze(incrementalModule->internalTypes); unfreeze(incrementalModule->interfaceTypes); @@ -280,30 +372,35 @@ FragmentTypeCheckResult typecheckFragment_( TypeFunctionRuntime typeFunctionRuntime(iceHandler, NotNull{&limits}); /// Create a DataFlowGraph just for the surrounding context - auto updatedDfg = DataFlowGraphBuilder::updateGraph(*stale->dataFlowGraph.get(), stale->dfgScopes, root, cursorPos, iceHandler); - + auto dfg = DataFlowGraphBuilder::build(root, iceHandler); SimplifierPtr simplifier = newSimplifier(NotNull{&incrementalModule->internalTypes}, frontend.builtinTypes); + FrontendModuleResolver& resolver = getModuleResolver(frontend, opts); + /// Contraint Generator ConstraintGenerator cg{ incrementalModule, NotNull{&normalizer}, NotNull{simplifier.get()}, NotNull{&typeFunctionRuntime}, - NotNull{&frontend.moduleResolver}, + NotNull{&resolver}, frontend.builtinTypes, iceHandler, stale->getModuleScope(), nullptr, nullptr, - NotNull{&updatedDfg}, + NotNull{&dfg}, {} }; + cg.rootScope = stale->getModuleScope().get(); // Any additions to the scope must occur in a fresh scope auto freshChildOfNearestScope = std::make_shared(closestScope); incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope); + // Update freshChildOfNearestScope with the appropriate lvalueTypes + mixedModeCompatibility(closestScope, freshChildOfNearestScope, stale, NotNull{&dfg}, root); + // closest Scope -> children = { ...., freshChildOfNearestScope} // We need to trim nearestChild from the scope hierarcy closestScope->children.push_back(NotNull{freshChildOfNearestScope.get()}); @@ -323,10 +420,10 @@ FragmentTypeCheckResult typecheckFragment_( NotNull(cg.rootScope), borrowConstraints(cg.constraints), incrementalModule->name, - NotNull{&frontend.moduleResolver}, + NotNull{&resolver}, {}, nullptr, - NotNull{&updatedDfg}, + NotNull{&dfg}, limits }; @@ -358,7 +455,8 @@ FragmentTypeCheckResult typecheckFragment( const ModuleName& moduleName, const Position& cursorPos, std::optional opts, - std::string_view src + std::string_view src, + std::optional fragmentEndPosition ) { const SourceModule* sourceModule = frontend.getSourceModule(moduleName); @@ -368,8 +466,15 @@ FragmentTypeCheckResult typecheckFragment( return {}; } - ModulePtr module = frontend.moduleResolver.getModule(moduleName); - FragmentParseResult parseResult = parseFragment(*sourceModule, src, cursorPos); + FrontendModuleResolver& resolver = getModuleResolver(frontend, opts); + ModulePtr module = resolver.getModule(moduleName); + if (!module) + { + LUAU_ASSERT(!"Expected Module for fragment typecheck"); + return {}; + } + + FragmentParseResult parseResult = parseFragment(*sourceModule, src, cursorPos, fragmentEndPosition); FrontendOptions frontendOptions = opts.value_or(frontend.options); const ScopePtr& closestScope = findClosestScope(module, parseResult.nearestStatement); FragmentTypeCheckResult result = @@ -385,10 +490,10 @@ FragmentAutocompleteResult fragmentAutocomplete( const ModuleName& moduleName, Position cursorPosition, std::optional opts, - StringCompletionCallback callback + StringCompletionCallback callback, + std::optional fragmentEndPosition ) { - LUAU_ASSERT(FFlag::LuauSolverV2); LUAU_ASSERT(FFlag::LuauAllowFragmentParsing); LUAU_ASSERT(FFlag::LuauStoreDFGOnModule2); LUAU_ASSERT(FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete); @@ -400,7 +505,8 @@ FragmentAutocompleteResult fragmentAutocomplete( return {}; } - auto tcResult = typecheckFragment(frontend, moduleName, cursorPosition, opts, src); + auto tcResult = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition); + TypeArena arenaForFragmentAutocomplete; auto result = Luau::autocomplete_( tcResult.incrementalModule, @@ -413,6 +519,7 @@ FragmentAutocompleteResult fragmentAutocomplete( frontend.fileResolver, callback ); + return {std::move(tcResult.incrementalModule), tcResult.freshScope.get(), std::move(arenaForFragmentAutocomplete), std::move(result)}; } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 261e37810..d219f1820 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -50,6 +50,7 @@ LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauRunCustomModuleChecks, false) LUAU_FASTFLAG(StudioReportLuauAny2) LUAU_FASTFLAGVARIABLE(LuauStoreDFGOnModule2) +LUAU_FASTFLAGVARIABLE(LuauStoreSolverTypeOnModule) namespace Luau { @@ -1285,6 +1286,8 @@ ModulePtr check( LUAU_TIMETRACE_ARGUMENT("name", sourceModule.humanReadableName.c_str()); ModulePtr result = std::make_shared(); + if (FFlag::LuauStoreSolverTypeOnModule) + result->checkedInNewSolver = true; result->name = sourceModule.name; result->humanReadableName = sourceModule.humanReadableName; result->mode = mode; diff --git a/Analysis/src/Generalization.cpp b/Analysis/src/Generalization.cpp index 8dce95f9e..c495fb6bf 100644 --- a/Analysis/src/Generalization.cpp +++ b/Analysis/src/Generalization.cpp @@ -9,8 +9,6 @@ #include "Luau/TypePack.h" #include "Luau/VisitType.h" -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) - namespace Luau { @@ -528,12 +526,7 @@ struct TypeCacher : TypeOnceVisitor DenseHashSet uncacheablePacks{nullptr}; explicit TypeCacher(NotNull> cachedTypes) - // CLI-120975: once we roll out release 646, we _want_ to visit bound - // types to ensure they're marked as uncacheable if the types they are - // bound to are also uncacheable. Hence: if LuauTypeSolverRelease is - // less than 646, skip bound types (the prior behavior). Otherwise, - // do not skip bound types. - : TypeOnceVisitor(/* skipBoundTypes */ DFInt::LuauTypeSolverRelease < 646) + : TypeOnceVisitor(/* skipBoundTypes */ false) , cachedTypes(cachedTypes) { } @@ -570,33 +563,19 @@ struct TypeCacher : TypeOnceVisitor bool visit(TypeId ty) override { - if (DFInt::LuauTypeSolverRelease >= 646) - { - // NOTE: `TypeCacher` should explicitly visit _all_ types and type packs, - // otherwise it's prone to marking types that cannot be cached as - // cacheable. - LUAU_ASSERT(false); - LUAU_UNREACHABLE(); - } - else - { - return true; - } + // NOTE: `TypeCacher` should explicitly visit _all_ types and type packs, + // otherwise it's prone to marking types that cannot be cached as + // cacheable. + LUAU_ASSERT(false); + LUAU_UNREACHABLE(); } bool visit(TypeId ty, const BoundType& btv) override { - if (DFInt::LuauTypeSolverRelease >= 646) - { - traverse(btv.boundTo); - if (isUncacheable(btv.boundTo)) - markUncacheable(ty); - return false; - } - else - { - return true; - } + traverse(btv.boundTo); + if (isUncacheable(btv.boundTo)) + markUncacheable(ty); + return false; } bool visit(TypeId ty, const FreeType& ft) override @@ -623,15 +602,8 @@ struct TypeCacher : TypeOnceVisitor bool visit(TypeId ty, const ErrorType&) override { - if (DFInt::LuauTypeSolverRelease >= 646) - { - cache(ty); - return false; - } - else - { - return true; - } + cache(ty); + return false; } bool visit(TypeId ty, const PrimitiveType&) override @@ -773,20 +745,13 @@ struct TypeCacher : TypeOnceVisitor bool visit(TypeId ty, const MetatableType& mtv) override { - if (DFInt::LuauTypeSolverRelease >= 646) - { - traverse(mtv.table); - traverse(mtv.metatable); - if (isUncacheable(mtv.table) || isUncacheable(mtv.metatable)) - markUncacheable(ty); - else - cache(ty); - return false; - } + traverse(mtv.table); + traverse(mtv.metatable); + if (isUncacheable(mtv.table) || isUncacheable(mtv.metatable)) + markUncacheable(ty); else - { - return true; - } + cache(ty); + return false; } bool visit(TypeId ty, const ClassType&) override @@ -911,18 +876,11 @@ struct TypeCacher : TypeOnceVisitor bool visit(TypePackId tp) override { - if (DFInt::LuauTypeSolverRelease >= 646) - { - // NOTE: `TypeCacher` should explicitly visit _all_ types and type packs, - // otherwise it's prone to marking types that cannot be cached as - // cacheable, which will segfault down the line. - LUAU_ASSERT(false); - LUAU_UNREACHABLE(); - } - else - { - return true; - } + // NOTE: `TypeCacher` should explicitly visit _all_ types and type packs, + // otherwise it's prone to marking types that cannot be cached as + // cacheable, which will segfault down the line. + LUAU_ASSERT(false); + LUAU_UNREACHABLE(); } bool visit(TypePackId tp, const FreeTypePack&) override @@ -967,35 +925,28 @@ struct TypeCacher : TypeOnceVisitor } bool visit(TypePackId tp, const BoundTypePack& btp) override { - if (DFInt::LuauTypeSolverRelease >= 645) { - traverse(btp.boundTo); - if (isUncacheable(btp.boundTo)) - markUncacheable(tp); - return false; - } - return true; + traverse(btp.boundTo); + if (isUncacheable(btp.boundTo)) + markUncacheable(tp); + return false; } bool visit(TypePackId tp, const TypePack& typ) override { - if (DFInt::LuauTypeSolverRelease >= 646) + bool uncacheable = false; + for (TypeId ty : typ.head) { - bool uncacheable = false; - for (TypeId ty : typ.head) - { - traverse(ty); - uncacheable |= isUncacheable(ty); - } - if (typ.tail) - { - traverse(*typ.tail); - uncacheable |= isUncacheable(*typ.tail); - } - if (uncacheable) - markUncacheable(tp); - return false; + traverse(ty); + uncacheable |= isUncacheable(ty); } - return true; + if (typ.tail) + { + traverse(*typ.tail); + uncacheable |= isUncacheable(*typ.tail); + } + if (uncacheable) + markUncacheable(tp); + return false; } }; diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 0c5b361cf..cd133ba00 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -15,7 +15,6 @@ #include LUAU_FASTFLAG(LuauSolverV2); -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) namespace Luau { @@ -132,34 +131,27 @@ struct ClonePublicInterface : Substitution } ftv->level = TypeLevel{0, 0}; - if (FFlag::LuauSolverV2 && DFInt::LuauTypeSolverRelease >= 645) + if (FFlag::LuauSolverV2) ftv->scope = nullptr; } else if (TableType* ttv = getMutable(result)) { ttv->level = TypeLevel{0, 0}; - if (FFlag::LuauSolverV2 && DFInt::LuauTypeSolverRelease >= 645) + if (FFlag::LuauSolverV2) ttv->scope = nullptr; } - if (FFlag::LuauSolverV2 && DFInt::LuauTypeSolverRelease >= 645) + if (FFlag::LuauSolverV2) { if (auto freety = getMutable(result)) { - if (DFInt::LuauTypeSolverRelease >= 646) - { - module->errors.emplace_back( - freety->scope->location, - module->name, - InternalError{"Free type is escaping its module; please report this bug at " - "https://github.com/luau-lang/luau/issues"} + module->errors.emplace_back( + freety->scope->location, + module->name, + InternalError{"Free type is escaping its module; please report this bug at " + "https://github.com/luau-lang/luau/issues"} ); result = builtinTypes->errorRecoveryType(); - } - else - { - freety->scope = nullptr; - } } else if (auto genericty = getMutable(result)) { @@ -172,26 +164,18 @@ struct ClonePublicInterface : Substitution TypePackId clean(TypePackId tp) override { - if (FFlag::LuauSolverV2 && DFInt::LuauTypeSolverRelease >= 645) + if (FFlag::LuauSolverV2) { auto clonedTp = clone(tp); if (auto ftp = getMutable(clonedTp)) { - - if (DFInt::LuauTypeSolverRelease >= 646) - { - module->errors.emplace_back( - ftp->scope->location, - module->name, - InternalError{"Free type pack is escaping its module; please report this bug at " - "https://github.com/luau-lang/luau/issues"} + module->errors.emplace_back( + ftp->scope->location, + module->name, + InternalError{"Free type pack is escaping its module; please report this bug at " + "https://github.com/luau-lang/luau/issues"} ); - clonedTp = builtinTypes->errorRecoveryTypePack(); - } - else - { - ftp->scope = nullptr; - } + clonedTp = builtinTypes->errorRecoveryTypePack(); } else if (auto gtp = getMutable(clonedTp)) gtp->scope = nullptr; diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 4fa0d9959..e5f3c4693 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -20,6 +20,7 @@ #include LUAU_FASTFLAGVARIABLE(LuauUserTypeFunNonstrict) +LUAU_FASTFLAGVARIABLE(LuauCountSelfCallsNonstrict) namespace Luau { @@ -537,9 +538,132 @@ struct NonStrictTypeChecker return {}; } - NonStrictContext visit(AstExprCall* call) { + if (FFlag::LuauCountSelfCallsNonstrict) + return visitCall(call); + else + return visitCall_DEPRECATED(call); + } + + // rename this to `visit` when `FFlag::LuauCountSelfCallsNonstrict` is removed, and clean up above `visit`. + NonStrictContext visitCall(AstExprCall* call) + { + LUAU_ASSERT(FFlag::LuauCountSelfCallsNonstrict); + + NonStrictContext fresh{}; + TypeId* originalCallTy = module->astOriginalCallTypes.find(call->func); + if (!originalCallTy) + return fresh; + + TypeId fnTy = *originalCallTy; + if (auto fn = get(follow(fnTy)); fn && fn->isCheckedFunction) + { + // We know fn is a checked function, which means it looks like: + // (S1, ... SN) -> T & + // (~S1, unknown^N-1) -> error & + // (unknown, ~S2, unknown^N-2) -> error + // ... + // ... + // (unknown^N-1, ~S_N) -> error + + std::vector arguments; + arguments.reserve(call->args.size + (call->self ? 1 : 0)); + if (call->self) + { + if (auto indexExpr = call->func->as()) + arguments.push_back(indexExpr->expr); + else + ice->ice("method call expression has no 'self'"); + } + arguments.insert(arguments.end(), call->args.begin(), call->args.end()); + + std::vector argTypes; + argTypes.reserve(arguments.size()); + + // Move all the types over from the argument typepack for `fn` + TypePackIterator curr = begin(fn->argTypes); + TypePackIterator fin = end(fn->argTypes); + for (; curr != fin; curr++) + argTypes.push_back(*curr); + + // Pad out the rest with the variadic as needed. + if (auto argTail = curr.tail()) + { + if (const VariadicTypePack* vtp = get(follow(*argTail))) + { + while (argTypes.size() < arguments.size()) + { + argTypes.push_back(vtp->ty); + } + } + } + + std::string functionName = getFunctionNameAsString(*call->func).value_or(""); + if (arguments.size() > argTypes.size()) + { + // We are passing more arguments than we expect, so we should error + reportError(CheckedFunctionIncorrectArgs{functionName, argTypes.size(), arguments.size()}, call->location); + return fresh; + } + + for (size_t i = 0; i < arguments.size(); i++) + { + // For example, if the arg is "hi" + // The actual arg type is string + // The expected arg type is number + // The type of the argument in the overload is ~number + // We will compare arg and ~number + AstExpr* arg = arguments[i]; + TypeId expectedArgType = argTypes[i]; + std::shared_ptr norm = normalizer.normalize(expectedArgType); + DefId def = dfg->getDef(arg); + TypeId runTimeErrorTy; + // If we're dealing with any, negating any will cause all subtype tests to fail + // However, when someone calls this function, they're going to want to be able to pass it anything, + // for that reason, we manually inject never into the context so that the runtime test will always pass. + if (!norm) + reportError(NormalizationTooComplex{}, arg->location); + + if (norm && get(norm->tops)) + runTimeErrorTy = builtinTypes->neverType; + else + runTimeErrorTy = getOrCreateNegation(expectedArgType); + fresh.addContext(def, runTimeErrorTy); + } + + // Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types + for (size_t i = 0; i < arguments.size(); i++) + { + AstExpr* arg = arguments[i]; + if (auto runTimeFailureType = willRunTimeError(arg, fresh)) + reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location); + } + + if (arguments.size() < argTypes.size()) + { + // We are passing fewer arguments than we expect + // so we need to ensure that the rest of the args are optional. + bool remainingArgsOptional = true; + for (size_t i = arguments.size(); i < argTypes.size(); i++) + remainingArgsOptional = remainingArgsOptional && isOptional(argTypes[i]); + + if (!remainingArgsOptional) + { + reportError(CheckedFunctionIncorrectArgs{functionName, argTypes.size(), arguments.size()}, call->location); + return fresh; + } + } + } + + return fresh; + } + + // Remove with `FFlag::LuauCountSelfCallsNonstrict` clean up. + NonStrictContext visitCall_DEPRECATED(AstExprCall* call) + { + LUAU_ASSERT(!FFlag::LuauCountSelfCallsNonstrict); + NonStrictContext fresh{}; TypeId* originalCallTy = module->astOriginalCallTypes.find(call->func); if (!originalCallTy) diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index 507abdf23..6cb511ebb 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -1411,8 +1411,6 @@ TypeId TypeSimplifier::simplify(TypeId ty, DenseHashSet& seen) SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, TypeId left, TypeId right) { - LUAU_ASSERT(FFlag::LuauSolverV2); - TypeSimplifier s{builtinTypes, arena}; // fprintf(stderr, "Intersect %s and %s ...\n", toString(left).c_str(), toString(right).c_str()); @@ -1426,8 +1424,6 @@ SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull< SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, std::set parts) { - LUAU_ASSERT(FFlag::LuauSolverV2); - TypeSimplifier s{builtinTypes, arena}; TypeId res = s.intersectFromParts(std::move(parts)); @@ -1437,8 +1433,6 @@ SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull< SimplifyResult simplifyUnion(NotNull builtinTypes, NotNull arena, TypeId left, TypeId right) { - LUAU_ASSERT(FFlag::LuauSolverV2); - TypeSimplifier s{builtinTypes, arena}; TypeId res = s.union_(left, right); diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 914ded05b..a93d910be 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -22,7 +22,6 @@ #include LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTFLAGVARIABLE(LuauRetrySubtypingWithoutHiddenPack) namespace Luau @@ -1395,17 +1394,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt, NotNull scope) { - if (DFInt::LuauTypeSolverRelease >= 646) - { - return isCovariantWith(env, subMt->table, superMt->table, scope) - .withBothComponent(TypePath::TypeField::Table) - .andAlso(isCovariantWith(env, subMt->metatable, superMt->metatable, scope).withBothComponent(TypePath::TypeField::Metatable)); - } - else - { - return isCovariantWith(env, subMt->table, superMt->table, scope) - .andAlso(isCovariantWith(env, subMt->metatable, superMt->metatable, scope).withBothComponent(TypePath::TypeField::Metatable)); - } + return isCovariantWith(env, subMt->table, superMt->table, scope) + .withBothComponent(TypePath::TypeField::Table) + .andAlso(isCovariantWith(env, subMt->metatable, superMt->metatable, scope).withBothComponent(TypePath::TypeField::Metatable)); } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable, NotNull scope) diff --git a/Analysis/src/TableLiteralInference.cpp b/Analysis/src/TableLiteralInference.cpp index bcd5c1d8c..c50cd16e7 100644 --- a/Analysis/src/TableLiteralInference.cpp +++ b/Analysis/src/TableLiteralInference.cpp @@ -9,8 +9,6 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) - namespace Luau { @@ -376,21 +374,11 @@ TypeId matchLiteralType( const TypeId* keyTy = astTypes->find(item.key); LUAU_ASSERT(keyTy); TypeId tKey = follow(*keyTy); - if (DFInt::LuauTypeSolverRelease >= 648) - { - LUAU_ASSERT(!is(tKey)); - } - else if (get(tKey)) - toBlock.push_back(tKey); + LUAU_ASSERT(!is(tKey)); const TypeId* propTy = astTypes->find(item.value); LUAU_ASSERT(propTy); TypeId tProp = follow(*propTy); - if (DFInt::LuauTypeSolverRelease >= 648) - { - LUAU_ASSERT(!is(tKey)); - } - else if (get(tProp)) - toBlock.push_back(tProp); + LUAU_ASSERT(!is(tProp)); // Populate expected types for non-string keys declared with [] (the code below will handle the case where they are strings) if (!item.key->as() && expectedTableTy->indexer) (*astExpectedTypes)[item.key] = expectedTableTy->indexer->indexType; diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 2634b89ed..794200116 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -32,7 +32,8 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauUserDefinedTypeFunctions2) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) + +LUAU_FASTFLAGVARIABLE(LuauTableKeysAreRValues) namespace Luau { @@ -1625,10 +1626,6 @@ void TypeChecker2::indexExprMetatableHelper(AstExprIndexExpr* indexExpr, const M indexExprMetatableHelper(indexExpr, mtmt, exprType, indexType); else { - if (!(DFInt::LuauTypeSolverRelease >= 647)) - { - LUAU_ASSERT(tt || get(follow(metaTable->table))); - } // CLI-122161: We're not handling unions correctly (probably). reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location); } @@ -1836,11 +1833,18 @@ void TypeChecker2::visit(AstExprFunction* fn) void TypeChecker2::visit(AstExprTable* expr) { - // TODO! for (const AstExprTable::Item& item : expr->items) { - if (item.key) - visit(item.key, ValueContext::LValue); + if (FFlag::LuauTableKeysAreRValues) + { + if (item.key) + visit(item.key, ValueContext::RValue); + } + else + { + if (item.key) + visit(item.key, ValueContext::LValue); + } visit(item.value, ValueContext::RValue); } } diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index d0ad82ec7..6b5dd88bf 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -53,8 +53,6 @@ LUAU_FASTFLAG(LuauRemoveNotAnyHack) LUAU_FASTFLAGVARIABLE(LuauUserDefinedTypeFunctionResetState) LUAU_FASTFLAG(LuauUserTypeFunExportedAndLocal) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) - namespace Luau { @@ -848,8 +846,7 @@ TypeFunctionReductionResult lenTypeFunction( return {ctx->builtins->numberType, false, {}, {}}; // we use the normalized operand here in case there was an intersection or union. - TypeId normalizedOperand = - DFInt::LuauTypeSolverRelease >= 646 ? follow(ctx->normalizer->typeFromNormal(*normTy)) : ctx->normalizer->typeFromNormal(*normTy); + TypeId normalizedOperand = follow(ctx->normalizer->typeFromNormal(*normTy)); if (normTy->hasTopTable() || get(normalizedOperand)) return {ctx->builtins->numberType, false, {}, {}}; diff --git a/Analysis/src/TypeFunctionRuntime.cpp b/Analysis/src/TypeFunctionRuntime.cpp index b8518636a..ad38a1f3d 100644 --- a/Analysis/src/TypeFunctionRuntime.cpp +++ b/Analysis/src/TypeFunctionRuntime.cpp @@ -14,7 +14,6 @@ #include LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTFLAGVARIABLE(LuauUserTypeFunFixRegister) LUAU_FASTFLAGVARIABLE(LuauUserTypeFunFixNoReadWrite) @@ -699,13 +698,10 @@ static int setTableIndexer(lua_State* L) TypeFunctionTypeId key = getTypeUserData(L, 2); TypeFunctionTypeId value = getTypeUserData(L, 3); - if (DFInt::LuauTypeSolverRelease >= 646) + if (auto tfnt = get(key)) { - if (auto tfnt = get(key)) - { - tftt->indexer = std::nullopt; - return 0; - } + tftt->indexer = std::nullopt; + return 0; } tftt->indexer = TypeFunctionTableIndexer{key, value}; diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 5eb01b3e6..34b101554 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -32,7 +32,6 @@ LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500) LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAGVARIABLE(LuauAcceptIndexingTableUnionsIntersections) LUAU_FASTFLAGVARIABLE(LuauMetatableFollow) LUAU_FASTFLAGVARIABLE(LuauRequireCyclesDontAlwaysReturnAny) @@ -3490,7 +3489,6 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex } } - if (FFlag::LuauAcceptIndexingTableUnionsIntersections) { // We're going to have a whole vector. std::vector tableTypes{}; @@ -3641,57 +3639,6 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex return addType(IntersectionType{{resultTypes.begin(), resultTypes.end()}}); } - else - { - TableType* exprTable = getMutableTableType(exprType); - if (!exprTable) - { - reportError(TypeError{expr.expr->location, NotATable{exprType}}); - return errorRecoveryType(scope); - } - - if (value) - { - const auto& it = exprTable->props.find(value->value.data); - if (it != exprTable->props.end()) - { - return it->second.type(); - } - else if ((ctx == ValueContext::LValue && exprTable->state == TableState::Unsealed) || exprTable->state == TableState::Free) - { - TypeId resultType = freshType(scope); - Property& property = exprTable->props[value->value.data]; - property.setType(resultType); - property.location = expr.index->location; - return resultType; - } - } - - if (exprTable->indexer) - { - const TableIndexer& indexer = *exprTable->indexer; - unify(indexType, indexer.indexType, scope, expr.index->location); - return indexer.indexResultType; - } - else if ((ctx == ValueContext::LValue && exprTable->state == TableState::Unsealed) || exprTable->state == TableState::Free) - { - TypeId indexerType = freshType(exprTable->level); - unify(indexType, indexerType, scope, expr.location); - TypeId indexResultType = freshType(exprTable->level); - - exprTable->indexer = TableIndexer{anyIfNonstrict(indexerType), anyIfNonstrict(indexResultType)}; - return indexResultType; - } - else - { - /* - * If we use [] indexing to fetch a property from a sealed table that - * has no indexer, we have no idea if it will work so we just return any - * and hope for the best. - */ - return anyType; - } - } } // Answers the question: "Can I define another function with this name?" diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index a06fcb091..d9a948be4 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -3,12 +3,7 @@ #include "Luau/Common.h" -LUAU_FASTFLAG(LuauNativeAttribute); - -// The default value here is 643 because the first release in which this was implemented is 644, -// and actively we want new changes to be off by default until they're enabled consciously. -// The flag is placed in AST project here to be common in all Luau libraries -LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeSolverRelease, 643) +LUAU_FASTFLAG(LuauNativeAttribute) namespace Luau { diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 02d17c1d5..a0231fc8a 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -24,6 +24,7 @@ LUAU_FASTFLAGVARIABLE(LuauUserDefinedTypeFunctionsSyntax2) LUAU_FASTFLAGVARIABLE(LuauUserDefinedTypeFunParseExport) LUAU_FASTFLAGVARIABLE(LuauAllowFragmentParsing) LUAU_FASTFLAGVARIABLE(LuauPortableStringZeroCheck) +LUAU_FASTFLAGVARIABLE(LuauAllowComplexTypesInGenericParams) namespace Luau { @@ -1773,6 +1774,11 @@ AstType* Parser::parseFunctionTypeTail( ); } +static bool isTypeFollow(Lexeme::Type c) +{ + return c == '|' || c == '?' || c == '&'; +} + // Type ::= // nil | // Name[`.' Name] [`<' namelist `>'] | @@ -2953,17 +2959,75 @@ AstArray Parser::parseTypeParams() if (shouldParseTypePack(lexer)) { AstTypePack* typePack = parseTypePack(); - parameters.push_back({{}, typePack}); } else if (lexer.current().type == '(') { - auto [type, typePack] = parseSimpleTypeOrPack(); + if (FFlag::LuauAllowComplexTypesInGenericParams) + { + Location begin = lexer.current().location; + AstType* type = nullptr; + AstTypePack* typePack = nullptr; + Lexeme::Type c = lexer.current().type; - if (typePack) - parameters.push_back({{}, typePack}); + if (c != '|' && c != '&') + { + auto typeOrTypePack = parseSimpleType(/* allowPack */ true, /* inDeclarationContext */ false); + type = typeOrTypePack.type; + typePack = typeOrTypePack.typePack; + } + + // Consider the following type: + // + // X<(T)> + // + // Is this a type pack or a parenthesized type? The + // assumption will be a type pack, as that's what allows one + // to express either a singular type pack or a potential + // complex type. + + if (typePack) + { + auto explicitTypePack = typePack->as(); + if (explicitTypePack && explicitTypePack->typeList.tailType == nullptr && explicitTypePack->typeList.types.size == 1 && + isTypeFollow(lexer.current().type)) + { + // If we parsed an explicit type pack with a single + // type in it (something of the form `(T)`), and + // the next lexeme is one that follows a type + // (&, |, ?), then assume that this was actually a + // parenthesized type. + parameters.push_back({parseTypeSuffix(explicitTypePack->typeList.types.data[0], begin), {}}); + } + else + { + // Otherwise, it's a type pack. + parameters.push_back({{}, typePack}); + } + } + else + { + // There's two cases in which `typePack` will be null: + // - We try to parse a simple type or a type pack, and + // we get a simple type: there's no ambiguity and + // we attempt to parse a complex type. + // - The next lexeme was a `|` or `&` indicating a + // union or intersection type with a leading + // separator. We just fall right into + // `parseTypeSuffix`, which allows its first + // argument to be `nullptr` + parameters.push_back({parseTypeSuffix(type, begin), {}}); + } + } else - parameters.push_back({type, {}}); + { + auto [type, typePack] = parseSimpleTypeOrPack(); + + if (typePack) + parameters.push_back({{}, typePack}); + else + parameters.push_back({type, {}}); + } } else if (lexer.current().type == '>' && parameters.empty()) { diff --git a/CLI/Analyze.cpp b/CLI/Analyze.cpp index 80ede2d02..bc78f7cb3 100644 --- a/CLI/Analyze.cpp +++ b/CLI/Analyze.cpp @@ -9,6 +9,7 @@ #include "FileUtils.h" #include "Flags.h" +#include "Require.h" #include #include @@ -170,14 +171,17 @@ struct CliFileResolver : Luau::FileResolver { if (Luau::AstExprConstantString* expr = node->as()) { - Luau::ModuleName name = std::string(expr->value.data, expr->value.size) + ".luau"; - if (!readFile(name)) - { - // fall back to .lua if a module with .luau doesn't exist - name = std::string(expr->value.data, expr->value.size) + ".lua"; - } + std::string path{expr->value.data, expr->value.size}; + + AnalysisRequireContext requireContext{context->name}; + AnalysisCacheManager cacheManager; + AnalysisErrorHandler errorHandler; + + RequireResolver resolver(path, requireContext, cacheManager, errorHandler); + RequireResolver::ResolvedRequire resolvedRequire = resolver.resolveRequire(); - return {{name}}; + if (resolvedRequire.status == RequireResolver::ModuleStatus::FileRead) + return {{resolvedRequire.identifier}}; } return std::nullopt; @@ -189,6 +193,48 @@ struct CliFileResolver : Luau::FileResolver return "stdin"; return name; } + +private: + struct AnalysisRequireContext : RequireResolver::RequireContext + { + explicit AnalysisRequireContext(std::string path) + : path(std::move(path)) + { + } + + std::string getPath() override + { + return path; + } + + bool isRequireAllowed() override + { + return true; + } + + bool isStdin() override + { + return path == "-"; + } + + std::string createNewIdentifer(const std::string& path) override + { + return path; + } + + private: + std::string path; + }; + + struct AnalysisCacheManager : public RequireResolver::CacheManager + { + AnalysisCacheManager() = default; + }; + + struct AnalysisErrorHandler : RequireResolver::ErrorHandler + { + AnalysisErrorHandler() = default; + }; }; struct CliConfigResolver : Luau::ConfigResolver diff --git a/CLI/FileUtils.cpp b/CLI/FileUtils.cpp index 4906d55a3..6925e99f1 100644 --- a/CLI/FileUtils.cpp +++ b/CLI/FileUtils.cpp @@ -108,6 +108,7 @@ std::string resolvePath(std::string_view path, std::string_view baseFilePath) // - if relative (when path and baseFilePath are both relative), resolvedPathPrefix remains empty // - if absolute (if either path or baseFilePath are absolute), resolvedPathPrefix is "C:\", "/", etc. std::string resolvedPathPrefix; + bool isResolvedPathRelative = false; if (isAbsolutePath(path)) { @@ -118,19 +119,19 @@ std::string resolvePath(std::string_view path, std::string_view baseFilePath) } else { - pathComponents = splitPath(path); + size_t afterPrefix = baseFilePath.find_first_of("\\/") + 1; + baseFilePathComponents = splitPath(baseFilePath.substr(afterPrefix)); if (isAbsolutePath(baseFilePath)) { // path is relative and baseFilePath is absolute, we use baseFilePath's prefix - size_t afterPrefix = baseFilePath.find_first_of("\\/") + 1; resolvedPathPrefix = baseFilePath.substr(0, afterPrefix); - baseFilePathComponents = splitPath(baseFilePath.substr(afterPrefix)); } else { // path and baseFilePath are both relative, we do not set a prefix (resolved path will be relative) - baseFilePathComponents = splitPath(baseFilePath); + isResolvedPathRelative = true; } + pathComponents = splitPath(path); } // Remove filename from components @@ -145,7 +146,7 @@ std::string resolvePath(std::string_view path, std::string_view baseFilePath) { if (baseFilePathComponents.empty()) { - if (resolvedPathPrefix.empty()) // only when final resolved path will be relative + if (isResolvedPathRelative) numPrependedParents++; // "../" will later be added to the beginning of the resolved path } else if (baseFilePathComponents.back() != "..") @@ -159,13 +160,25 @@ std::string resolvePath(std::string_view path, std::string_view baseFilePath) } } - // Join baseFilePathComponents to form the resolved path - std::string resolvedPath = resolvedPathPrefix; - // Only when resolvedPath will be relative - for (int i = 0; i < numPrependedParents; i++) + // Create resolved path prefix for relative paths + if (isResolvedPathRelative) { - resolvedPath += "../"; + if (numPrependedParents > 0) + { + resolvedPathPrefix.reserve(numPrependedParents * 3); + for (int i = 0; i < numPrependedParents; i++) + { + resolvedPathPrefix += "../"; + } + } + else + { + resolvedPathPrefix = "./"; + } } + + // Join baseFilePathComponents to form the resolved path + std::string resolvedPath = resolvedPathPrefix; for (auto iter = baseFilePathComponents.begin(); iter != baseFilePathComponents.end(); ++iter) { if (iter != baseFilePathComponents.begin()) diff --git a/CLI/Flags.cpp b/CLI/Flags.cpp index c0bb485f4..ee5918c91 100644 --- a/CLI/Flags.cpp +++ b/CLI/Flags.cpp @@ -2,14 +2,11 @@ #include "Luau/Common.h" #include "Luau/ExperimentalFlags.h" -#include // TODO: remove with LuauTypeSolverRelease #include #include #include -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) - static void setLuauFlag(std::string_view name, bool state) { for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) @@ -26,13 +23,6 @@ static void setLuauFlag(std::string_view name, bool state) static void setLuauFlags(bool state) { - if (state) - { - // Setting flags to 'true' means enabling all Luau flags including new type solver - // In that case, it is provided with all fixes enabled (as if each fix had its own boolean flag) - DFInt::LuauTypeSolverRelease.value = std::numeric_limits::max(); - } - for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) if (strncmp(flag->name, "Luau", 4) == 0) flag->value = state; diff --git a/CLI/Repl.cpp b/CLI/Repl.cpp index 8e7a84384..43aab4e49 100644 --- a/CLI/Repl.cpp +++ b/CLI/Repl.cpp @@ -19,6 +19,8 @@ #include "isocline.h" #include +#include +#include #ifdef _WIN32 #include @@ -119,18 +121,113 @@ static int finishrequire(lua_State* L) return 1; } +struct RuntimeRequireContext : public RequireResolver::RequireContext +{ + // In the context of the REPL, source is the calling context's chunkname. + // + // These chunknames have certain prefixes that indicate context. These + // are used when displaying debug information (see luaO_chunkid). + // + // Generally, the '@' prefix is used for filepaths, and the '=' prefix is + // used for custom chunknames, such as =stdin. + explicit RuntimeRequireContext(std::string source) + : source(std::move(source)) + { + } + + std::string getPath() override + { + return source.substr(1); + } + + bool isRequireAllowed() override + { + return isStdin() || (!source.empty() && source[0] == '@'); + } + + bool isStdin() override + { + return source == "=stdin"; + } + + std::string createNewIdentifer(const std::string& path) override + { + return "@" + path; + } + +private: + std::string source; +}; + +struct RuntimeCacheManager : public RequireResolver::CacheManager +{ + explicit RuntimeCacheManager(lua_State* L) + : L(L) + { + } + + bool isCached(const std::string& path) override + { + luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1); + lua_getfield(L, -1, path.c_str()); + bool cached = !lua_isnil(L, -1); + lua_pop(L, 2); + + if (cached) + cacheKey = path; + + return cached; + } + + std::string cacheKey; + +private: + lua_State* L; +}; + +struct RuntimeErrorHandler : RequireResolver::ErrorHandler +{ + explicit RuntimeErrorHandler(lua_State* L) + : L(L) + { + } + + void reportError(const std::string message) override + { + luaL_errorL(L, "%s", message.c_str()); + } + +private: + lua_State* L; +}; + static int lua_require(lua_State* L) { std::string name = luaL_checkstring(L, 1); - RequireResolver::ResolvedRequire resolvedRequire = RequireResolver::resolveRequire(L, std::move(name)); + RequireResolver::ResolvedRequire resolvedRequire; + { + lua_Debug ar; + lua_getinfo(L, 1, "s", &ar); + + RuntimeRequireContext requireContext{ar.source}; + RuntimeCacheManager cacheManager{L}; + RuntimeErrorHandler errorHandler{L}; + + RequireResolver resolver(std::move(name), requireContext, cacheManager, errorHandler); + + resolvedRequire = resolver.resolveRequire( + [L, &cacheKey = cacheManager.cacheKey](const RequireResolver::ModuleStatus status) + { + lua_getfield(L, LUA_REGISTRYINDEX, "_MODULES"); + if (status == RequireResolver::ModuleStatus::Cached) + lua_getfield(L, -1, cacheKey.c_str()); + } + ); + } if (resolvedRequire.status == RequireResolver::ModuleStatus::Cached) return finishrequire(L); - else if (resolvedRequire.status == RequireResolver::ModuleStatus::Ambiguous) - luaL_errorL(L, "require path could not be resolved to a unique file"); - else if (resolvedRequire.status == RequireResolver::ModuleStatus::NotFound) - luaL_errorL(L, "error requiring module"); // module needs to run in a new thread, isolated from the rest // note: we create ML on main thread so that it doesn't inherit environment of L @@ -143,7 +240,7 @@ static int lua_require(lua_State* L) // now we can compile & run module on the new thread std::string bytecode = Luau::compile(resolvedRequire.sourceCode, copts()); - if (luau_load(ML, resolvedRequire.chunkName.c_str(), bytecode.data(), bytecode.size(), 0) == 0) + if (luau_load(ML, resolvedRequire.identifier.c_str(), bytecode.data(), bytecode.size(), 0) == 0) { if (codegen) { diff --git a/CLI/Require.cpp b/CLI/Require.cpp index 2c45d0acf..4c1c3ac63 100644 --- a/CLI/Require.cpp +++ b/CLI/Require.cpp @@ -9,165 +9,170 @@ #include #include -RequireResolver::RequireResolver(lua_State* L, std::string path) +static constexpr char kRequireErrorGeneric[] = "error requiring module"; + +RequireResolver::RequireResolver(std::string path, RequireContext& requireContext, CacheManager& cacheManager, ErrorHandler& errorHandler) : pathToResolve(std::move(path)) - , L(L) + , requireContext(requireContext) + , cacheManager(cacheManager) + , errorHandler(errorHandler) { - lua_Debug ar; - lua_getinfo(L, 1, "s", &ar); - sourceChunkname = ar.source; +} - if (!isRequireAllowed(sourceChunkname)) - luaL_errorL(L, "require is not supported in this context"); +RequireResolver::ResolvedRequire RequireResolver::resolveRequire(std::function completionCallback) +{ + if (isRequireResolved) + { + errorHandler.reportError("require statement has already been resolved"); + return ResolvedRequire{ModuleStatus::ErrorReported}; + } - if (isAbsolutePath(pathToResolve)) - luaL_argerrorL(L, 1, "cannot require an absolute path"); + if (!initialize()) + return ResolvedRequire{ModuleStatus::ErrorReported}; - std::replace(pathToResolve.begin(), pathToResolve.end(), '\\', '/'); + resolvedRequire.status = findModule(); - if (!isPrefixValid()) - luaL_argerrorL(L, 1, "require path must start with a valid prefix: ./, ../, or @"); + if (completionCallback) + completionCallback(resolvedRequire.status); - substituteAliasIfPresent(pathToResolve); + isRequireResolved = true; + return resolvedRequire; } -[[nodiscard]] RequireResolver::ResolvedRequire RequireResolver::resolveRequire(lua_State* L, std::string path) +static bool hasValidPrefix(std::string_view path) { - RequireResolver resolver(L, std::move(path)); - ModuleStatus status = resolver.findModule(); - if (status != ModuleStatus::FileRead) - return ResolvedRequire{status}; - else - return ResolvedRequire{status, std::move(resolver.chunkname), std::move(resolver.absolutePath), std::move(resolver.sourceCode)}; + return path.compare(0, 2, "./") == 0 || path.compare(0, 3, "../") == 0 || path.compare(0, 1, "@") == 0; } -RequireResolver::ModuleStatus RequireResolver::findModule() +static bool isPathAmbiguous(const std::string& path) { - resolveAndStoreDefaultPaths(); + bool found = false; + for (const char* suffix : {".luau", ".lua"}) + { + if (isFile(path + suffix)) + { + if (found) + return true; + else + found = true; + } + } + if (isDirectory(path) && found) + return true; - // Put _MODULES table on stack for checking and saving to the cache - luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1); + return false; +} + +bool RequireResolver::initialize() +{ + if (!requireContext.isRequireAllowed()) + { + errorHandler.reportError("require is not supported in this context"); + return false; + } - return findModuleImpl(); + if (isAbsolutePath(pathToResolve)) + { + errorHandler.reportError("cannot require an absolute path"); + return false; + } + + std::replace(pathToResolve.begin(), pathToResolve.end(), '\\', '/'); + + if (!hasValidPrefix(pathToResolve)) + { + errorHandler.reportError("require path must start with a valid prefix: ./, ../, or @"); + return false; + } + + return substituteAliasIfPresent(pathToResolve); } -RequireResolver::ModuleStatus RequireResolver::findModuleImpl() +RequireResolver::ModuleStatus RequireResolver::findModule() { - if (isPathAmbiguous(absolutePath)) - return ModuleStatus::Ambiguous; + if (!resolveAndStoreDefaultPaths()) + return ModuleStatus::ErrorReported; - static const std::array possibleSuffixes = {".luau", ".lua", "/init.luau", "/init.lua"}; + if (isPathAmbiguous(resolvedRequire.absolutePath)) + { + errorHandler.reportError("require path could not be resolved to a unique file"); + return ModuleStatus::ErrorReported; + } - size_t unsuffixedAbsolutePathSize = absolutePath.size(); + static constexpr std::array possibleSuffixes = {".luau", ".lua", "/init.luau", "/init.lua"}; + size_t unsuffixedAbsolutePathSize = resolvedRequire.absolutePath.size(); for (const char* possibleSuffix : possibleSuffixes) { - absolutePath += possibleSuffix; + resolvedRequire.absolutePath += possibleSuffix; - // Check cache for module - lua_getfield(L, -1, absolutePath.c_str()); - if (!lua_isnil(L, -1)) - { + if (cacheManager.isCached(resolvedRequire.absolutePath)) return ModuleStatus::Cached; - } - lua_pop(L, 1); // Try to read the matching file - std::optional source = readFile(absolutePath); - if (source) + if (std::optional source = readFile(resolvedRequire.absolutePath)) { - chunkname = "=" + chunkname + possibleSuffix; - sourceCode = *source; + resolvedRequire.identifier = requireContext.createNewIdentifer(resolvedRequire.identifier + possibleSuffix); + resolvedRequire.sourceCode = *source; return ModuleStatus::FileRead; } - absolutePath.resize(unsuffixedAbsolutePathSize); // truncate to remove suffix + resolvedRequire.absolutePath.resize(unsuffixedAbsolutePathSize); // truncate to remove suffix } - if (hasFileExtension(absolutePath, {".luau", ".lua"}) && isFile(absolutePath)) - luaL_argerrorL(L, 1, "error requiring module: consider removing the file extension"); - - return ModuleStatus::NotFound; -} - -bool RequireResolver::isPathAmbiguous(const std::string& path) -{ - bool found = false; - for (const char* suffix : {".luau", ".lua"}) + if (hasFileExtension(resolvedRequire.absolutePath, {".luau", ".lua"}) && isFile(resolvedRequire.absolutePath)) { - if (isFile(path + suffix)) - { - if (found) - return true; - else - found = true; - } + errorHandler.reportError("error requiring module: consider removing the file extension"); + return ModuleStatus::ErrorReported; } - if (isDirectory(path) && found) - return true; - - return false; -} -bool RequireResolver::isRequireAllowed(std::string_view sourceChunkname) -{ - LUAU_ASSERT(!sourceChunkname.empty()); - return (sourceChunkname[0] == '=' || sourceChunkname[0] == '@'); -} - -bool RequireResolver::isPrefixValid() -{ - return pathToResolve.compare(0, 2, "./") == 0 || pathToResolve.compare(0, 3, "../") == 0 || pathToResolve.compare(0, 1, "@") == 0; + errorHandler.reportError(kRequireErrorGeneric); + return ModuleStatus::ErrorReported; } -void RequireResolver::resolveAndStoreDefaultPaths() +bool RequireResolver::resolveAndStoreDefaultPaths() { if (!isAbsolutePath(pathToResolve)) { - std::string chunknameContext = getRequiringContextRelative(); + std::string identifierContext = getRequiringContextRelative(); std::optional absolutePathContext = getRequiringContextAbsolute(); if (!absolutePathContext) - luaL_errorL(L, "error requiring module"); + return false; // resolvePath automatically sanitizes/normalizes the paths - std::optional chunknameOpt = resolvePath(pathToResolve, chunknameContext); - std::optional absolutePathOpt = resolvePath(pathToResolve, *absolutePathContext); - - if (!chunknameOpt || !absolutePathOpt) - luaL_errorL(L, "error requiring module"); - - chunkname = std::move(*chunknameOpt); - absolutePath = std::move(*absolutePathOpt); + resolvedRequire.identifier = resolvePath(pathToResolve, identifierContext); + resolvedRequire.absolutePath = resolvePath(pathToResolve, *absolutePathContext); } else { // Here we must explicitly sanitize, as the path is taken as is - std::optional sanitizedPath = normalizePath(pathToResolve); - if (!sanitizedPath) - luaL_errorL(L, "error requiring module"); - - chunkname = *sanitizedPath; - absolutePath = std::move(*sanitizedPath); + std::string sanitizedPath = normalizePath(pathToResolve); + resolvedRequire.identifier = sanitizedPath; + resolvedRequire.absolutePath = std::move(sanitizedPath); } + return true; } std::optional RequireResolver::getRequiringContextAbsolute() { std::string requiringFile; - if (isAbsolutePath(sourceChunkname.substr(1))) + if (isAbsolutePath(requireContext.getPath())) { // We already have an absolute path for the requiring file - requiringFile = sourceChunkname.substr(1); + requiringFile = requireContext.getPath(); } else { // Requiring file's stored path is relative to the CWD, must make absolute std::optional cwd = getCurrentWorkingDirectory(); if (!cwd) + { + errorHandler.reportError("could not determine current working directory"); return std::nullopt; + } - if (sourceChunkname.substr(1) == "stdin") + if (requireContext.isStdin()) { // Require statement is being executed from REPL input prompt // The requiring context is the pseudo-file "stdin" in the CWD @@ -176,11 +181,7 @@ std::optional RequireResolver::getRequiringContextAbsolute() else { // Require statement is being executed in a file, must resolve relative to CWD - std::optional requiringFileOpt = resolvePath(sourceChunkname.substr(1), joinPaths(*cwd, "stdin")); - if (!requiringFileOpt) - return std::nullopt; - - requiringFile = *requiringFileOpt; + requiringFile = resolvePath(requireContext.getPath(), joinPaths(*cwd, "stdin")); } } std::replace(requiringFile.begin(), requiringFile.end(), '\\', '/'); @@ -189,17 +190,13 @@ std::optional RequireResolver::getRequiringContextAbsolute() std::string RequireResolver::getRequiringContextRelative() { - std::string baseFilePath; - if (sourceChunkname.substr(1) != "stdin") - baseFilePath = sourceChunkname.substr(1); - - return baseFilePath; + return requireContext.isStdin() ? "" : requireContext.getPath(); } -void RequireResolver::substituteAliasIfPresent(std::string& path) +bool RequireResolver::substituteAliasIfPresent(std::string& path) { if (path.size() < 1 || path[0] != '@') - return; + return true; // To ignore the '@' alias prefix when processing the alias const size_t aliasStartPos = 1; @@ -215,17 +212,19 @@ void RequireResolver::substituteAliasIfPresent(std::string& path) // Not worth searching when potentialAlias cannot be an alias if (!Luau::isValidAlias(potentialAlias)) - luaL_errorL(L, "@%s is not a valid alias", potentialAlias.c_str()); - - std::optional alias = getAlias(potentialAlias); - if (alias) { - path = *alias + path.substr(potentialAlias.size() + 1); + errorHandler.reportError("@" + potentialAlias + " is not a valid alias"); + return false; } - else + + if (std::optional alias = getAlias(potentialAlias)) { - luaL_errorL(L, "@%s is not a valid alias", potentialAlias.c_str()); + path = *alias + path.substr(potentialAlias.size() + 1); + return true; } + + errorHandler.reportError("@" + potentialAlias + " is not a valid alias"); + return false; } std::optional RequireResolver::getAlias(std::string alias) @@ -241,7 +240,8 @@ std::optional RequireResolver::getAlias(std::string alias) ); while (!config.aliases.contains(alias) && !isConfigFullyResolved) { - parseNextConfig(); + if (!parseNextConfig()) + return std::nullopt; // error parsing config } if (!config.aliases.contains(alias) && isConfigFullyResolved) return std::nullopt; // could not find alias @@ -250,17 +250,17 @@ std::optional RequireResolver::getAlias(std::string alias) return resolvePath(aliasInfo.value, aliasInfo.configLocation); } -void RequireResolver::parseNextConfig() +bool RequireResolver::parseNextConfig() { if (isConfigFullyResolved) - return; // no config files left to parse + return true; // no config files left to parse std::optional directory; if (lastSearchedDir.empty()) { std::optional requiringFile = getRequiringContextAbsolute(); if (!requiringFile) - luaL_errorL(L, "error requiring module"); + return false; directory = getParentPath(*requiringFile); } @@ -270,13 +270,16 @@ void RequireResolver::parseNextConfig() if (directory) { lastSearchedDir = *directory; - parseConfigInDirectory(*directory); + if (!parseConfigInDirectory(*directory)) + return false; } else isConfigFullyResolved = true; + + return true; } -void RequireResolver::parseConfigInDirectory(const std::string& directory) +bool RequireResolver::parseConfigInDirectory(const std::string& directory) { std::string configPath = joinPaths(directory, Luau::kConfigName); @@ -291,6 +294,11 @@ void RequireResolver::parseConfigInDirectory(const std::string& directory) { std::optional error = Luau::parseConfig(*contents, config, opts); if (error) - luaL_errorL(L, "error parsing %s (%s)", configPath.c_str(), (*error).c_str()); + { + errorHandler.reportError("error parsing " + configPath + "(" + *error + ")"); + return false; + } } -} + + return true; +} \ No newline at end of file diff --git a/CLI/Require.h b/CLI/Require.h index 9c86c3cca..e4fc019a0 100644 --- a/CLI/Require.h +++ b/CLI/Require.h @@ -1,64 +1,84 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "lua.h" -#include "lualib.h" - #include "Luau/Config.h" +#include #include #include class RequireResolver { public: - std::string chunkname; - std::string absolutePath; - std::string sourceCode; - enum class ModuleStatus { Cached, FileRead, - Ambiguous, - NotFound + ErrorReported }; struct ResolvedRequire { ModuleStatus status; - std::string chunkName; + std::string identifier; std::string absolutePath; std::string sourceCode; }; - [[nodiscard]] ResolvedRequire static resolveRequire(lua_State* L, std::string path); + struct RequireContext + { + virtual ~RequireContext() = default; + virtual std::string getPath() = 0; + virtual bool isRequireAllowed() = 0; + virtual bool isStdin() = 0; + virtual std::string createNewIdentifer(const std::string& path) = 0; + }; + + struct CacheManager + { + virtual ~CacheManager() = default; + virtual bool isCached(const std::string& path) + { + return false; + } + }; + + struct ErrorHandler + { + virtual ~ErrorHandler() = default; + virtual void reportError(const std::string message) {} + }; + + RequireResolver(std::string pathToResolve, RequireContext& requireContext, CacheManager& cacheManager, ErrorHandler& errorHandler); + + [[nodiscard]] ResolvedRequire resolveRequire(std::function completionCallback = nullptr); private: std::string pathToResolve; - std::string_view sourceChunkname; - RequireResolver(lua_State* L, std::string path); + RequireContext& requireContext; + CacheManager& cacheManager; + ErrorHandler& errorHandler; + + ResolvedRequire resolvedRequire; + bool isRequireResolved = false; - ModuleStatus findModule(); - lua_State* L; Luau::Config config; std::string lastSearchedDir; bool isConfigFullyResolved = false; - bool isRequireAllowed(std::string_view sourceChunkname); - bool isPrefixValid(); + [[nodiscard]] bool initialize(); - void resolveAndStoreDefaultPaths(); + ModuleStatus findModule(); ModuleStatus findModuleImpl(); - bool isPathAmbiguous(const std::string& path); + [[nodiscard]] bool resolveAndStoreDefaultPaths(); std::optional getRequiringContextAbsolute(); std::string getRequiringContextRelative(); - void substituteAliasIfPresent(std::string& path); + [[nodiscard]] bool substituteAliasIfPresent(std::string& path); std::optional getAlias(std::string alias); - void parseNextConfig(); - void parseConfigInDirectory(const std::string& path); -}; + [[nodiscard]] bool parseNextConfig(); + [[nodiscard]] bool parseConfigInDirectory(const std::string& directory); +}; \ No newline at end of file diff --git a/Makefile b/Makefile index 1f082f388..6fb0b8f62 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ REPL_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Profiler.cpp CLI/Coverage.c REPL_CLI_OBJECTS=$(REPL_CLI_SOURCES:%=$(BUILD)/%.o) REPL_CLI_TARGET=$(BUILD)/luau -ANALYZE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Analyze.cpp +ANALYZE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Require.cpp CLI/Analyze.cpp ANALYZE_CLI_OBJECTS=$(ANALYZE_CLI_SOURCES:%=$(BUILD)/%.o) ANALYZE_CLI_TARGET=$(BUILD)/luau-analyze diff --git a/Sources.cmake b/Sources.cmake index 1299b1191..1adbe862e 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -410,7 +410,9 @@ endif() if(TARGET Luau.Analyze.CLI) # Luau.Analyze.CLI Sources target_sources(Luau.Analyze.CLI PRIVATE - CLI/Analyze.cpp) + CLI/Analyze.cpp + CLI/Require.cpp + ) endif() if(TARGET Luau.Ast.CLI) diff --git a/VM/src/lobject.cpp b/VM/src/lobject.cpp index f685b2350..e4202d700 100644 --- a/VM/src/lobject.cpp +++ b/VM/src/lobject.cpp @@ -116,6 +116,13 @@ const char* luaO_pushfstring(lua_State* L, const char* fmt, ...) return msg; } +// Possible chunkname prefixes: +// +// '=' prefix: meant to represent custom chunknames. When truncation is needed, +// the beginning of the chunkname is kept. +// +// '@' prefix: meant to represent filepaths. When truncation is needed, the end +// of the filepath is kept, as this is more useful for identifying the file. const char* luaO_chunkid(char* buf, size_t buflen, const char* source, size_t srclen) { if (*source == '=') diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index cf92ff66b..57ff8ebd8 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -26,7 +26,6 @@ static const char* mainModuleName = "MainModule"; LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTFLAGVARIABLE(DebugLuauForceAllNewSolverTests); @@ -243,21 +242,21 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars return result.root; } -CheckResult Fixture::check(Mode mode, const std::string& source) +CheckResult Fixture::check(Mode mode, const std::string& source, std::optional options) { ModuleName mm = fromString(mainModuleName); configResolver.defaultConfig.mode = mode; fileResolver.source[mm] = std::move(source); frontend.markDirty(mm); - CheckResult result = frontend.check(mm); + CheckResult result = frontend.check(mm, options); return result; } -CheckResult Fixture::check(const std::string& source) +CheckResult Fixture::check(const std::string& source, std::optional options) { - return check(Mode::Strict, source); + return check(Mode::Strict, source, options); } LintResult Fixture::lint(const std::string& source, const std::optional& lintOptions) diff --git a/tests/Fixture.h b/tests/Fixture.h index 39222a252..d0a2ca9f9 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -76,8 +76,8 @@ struct Fixture // Throws Luau::ParseErrors if the parse fails. AstStatBlock* parse(const std::string& source, const ParseOptions& parseOptions = {}); - CheckResult check(Mode mode, const std::string& source); - CheckResult check(const std::string& source); + CheckResult check(Mode mode, const std::string& source, std::optional = std::nullopt); + CheckResult check(const std::string& source, std::optional = std::nullopt); LintResult lint(const std::string& source, const std::optional& lintOptions = {}); LintResult lintModule(const ModuleName& moduleName, const std::optional& lintOptions = {}); diff --git a/tests/FragmentAutocomplete.test.cpp b/tests/FragmentAutocomplete.test.cpp index 81e42f870..d90481b20 100644 --- a/tests/FragmentAutocomplete.test.cpp +++ b/tests/FragmentAutocomplete.test.cpp @@ -10,73 +10,138 @@ #include "Luau/Frontend.h" #include "Luau/AutocompleteTypes.h" +#include +#include +#include +#include +#include + using namespace Luau; LUAU_FASTFLAG(LuauAllowFragmentParsing); LUAU_FASTFLAG(LuauStoreDFGOnModule2); LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete) +LUAU_FASTFLAG(LuauSymbolEquality); +LUAU_FASTFLAG(LuauStoreSolverTypeOnModule); static std::optional nullCallback(std::string tag, std::optional ptr, std::optional contents) { return std::nullopt; } -struct FragmentAutocompleteFixture : Fixture + +static FrontendOptions getOptions() +{ + FrontendOptions options; + options.retainFullTypeGraphs = true; + + if (!FFlag::LuauSolverV2) + options.forAutocomplete = true; + + options.runLintChecks = false; + + return options; +} + +template +struct FragmentAutocompleteFixtureImpl : BaseType { - ScopedFastFlag sffs[4] = { + ScopedFastFlag sffs[5] = { {FFlag::LuauAllowFragmentParsing, true}, - {FFlag::LuauSolverV2, true}, {FFlag::LuauStoreDFGOnModule2, true}, - {FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true} + {FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true}, + {FFlag::LuauStoreSolverTypeOnModule, true}, + {FFlag::LuauSymbolEquality, true} }; - FragmentAutocompleteFixture() + FragmentAutocompleteFixtureImpl() + : BaseType(true) { - addGlobalBinding(frontend.globals, "table", Binding{builtinTypes->anyType}); - addGlobalBinding(frontend.globals, "math", Binding{builtinTypes->anyType}); } + FragmentAutocompleteAncestryResult runAutocompleteVisitor(const std::string& source, const Position& cursorPos) { - ParseResult p = tryParse(source); // We don't care about parsing incomplete asts + ParseResult p = this->tryParse(source); // We don't care about parsing incomplete asts REQUIRE(p.root); return findAncestryForFragmentParse(p.root, cursorPos); } - CheckResult checkBase(const std::string& document) - { - FrontendOptions opts; - opts.retainFullTypeGraphs = true; - return this->frontend.check("MainModule", opts); - } - FragmentParseResult parseFragment(const std::string& document, const Position& cursorPos) + FragmentParseResult parseFragment( + const std::string& document, + const Position& cursorPos, + std::optional fragmentEndPosition = std::nullopt + ) { SourceModule* srcModule = this->getMainSourceModule(); std::string_view srcString = document; - return Luau::parseFragment(*srcModule, srcString, cursorPos); + return Luau::parseFragment(*srcModule, srcString, cursorPos, fragmentEndPosition); } - FragmentTypeCheckResult checkFragment(const std::string& document, const Position& cursorPos) + CheckResult checkOldSolver(const std::string& source) { - FrontendOptions options; - options.retainFullTypeGraphs = true; - // Don't strictly need this in the new solver - options.forAutocomplete = true; - options.runLintChecks = false; - return Luau::typecheckFragment(frontend, "MainModule", cursorPos, options, document); + ScopedFastFlag sff{FFlag::LuauSolverV2, false}; + return this->check(Mode::Strict, source, getOptions()); + } + + FragmentTypeCheckResult checkFragment( + const std::string& document, + const Position& cursorPos, + std::optional fragmentEndPosition = std::nullopt + ) + { + return Luau::typecheckFragment(this->frontend, "MainModule", cursorPos, getOptions(), document, fragmentEndPosition); } - FragmentAutocompleteResult autocompleteFragment(const std::string& document, Position cursorPos) + FragmentAutocompleteResult autocompleteFragment( + const std::string& document, + Position cursorPos, + std::optional fragmentEndPosition = std::nullopt + ) { FrontendOptions options; - options.retainFullTypeGraphs = true; - // Don't strictly need this in the new solver - options.forAutocomplete = true; - options.runLintChecks = false; - return Luau::fragmentAutocomplete(frontend, document, "MainModule", cursorPos, options, nullCallback); + return Luau::fragmentAutocomplete(this->frontend, document, "MainModule", cursorPos, getOptions(), nullCallback, fragmentEndPosition); + } + + + void autocompleteFragmentInBothSolvers( + const std::string& document, + const std::string& updated, + Position cursorPos, + std::function assertions, + std::optional fragmentEndPosition = std::nullopt + ) + { + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + this->check(document); + + FragmentAutocompleteResult result = autocompleteFragment(updated, cursorPos, fragmentEndPosition); + assertions(result); + + ScopedFastFlag _{FFlag::LuauSolverV2, false}; + this->check(document, getOptions()); + + result = autocompleteFragment(updated, cursorPos, fragmentEndPosition); + assertions(result); + } +}; + +struct FragmentAutocompleteFixture : FragmentAutocompleteFixtureImpl +{ + FragmentAutocompleteFixture() + : FragmentAutocompleteFixtureImpl() + { + addGlobalBinding(frontend.globals, "table", Binding{builtinTypes->anyType}); + addGlobalBinding(frontend.globals, "math", Binding{builtinTypes->anyType}); + addGlobalBinding(frontend.globalsForAutocomplete, "table", Binding{builtinTypes->anyType}); + addGlobalBinding(frontend.globalsForAutocomplete, "math", Binding{builtinTypes->anyType}); } }; +struct FragmentAutocompleteBuiltinsFixture : FragmentAutocompleteFixtureImpl +{ +}; + TEST_SUITE_BEGIN("FragmentAutocompleteTraversalTests"); TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "just_two_locals") @@ -201,6 +266,7 @@ TEST_SUITE_BEGIN("FragmentAutocompleteParserTests"); TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer") { + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; check("local a ="); auto fragment = parseFragment("local a =", Position(0, 10)); CHECK_EQ("local a =", fragment.fragmentToParse); @@ -208,6 +274,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer") TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "statement_in_empty_fragment_is_non_null") { + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; auto res = check(R"( )"); @@ -230,6 +297,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "statement_in_empty_fragment_is_n TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_complete_fragments") { + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; auto res = check( R"( local x = 4 @@ -272,6 +340,7 @@ local z = x + y TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_fragments_in_line") { + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; auto res = check( R"( local x = 4 @@ -314,7 +383,7 @@ local y = 5 TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_in_correct_scope") { - + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; check(R"( local myLocal = 4 function abc() @@ -334,9 +403,123 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_in_correct_scope") Position{6, 0} ); + CHECK_EQ("\n", fragment.fragmentToParse); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_single_line_fragment_override") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + auto res = check("function abc(foo: string) end"); + + LUAU_REQUIRE_NO_ERRORS(res); + + auto callFragment = parseFragment( + R"(function abc(foo: string) end +abc("foo") +abc("bar") +)", + Position{1, 6}, + Position{1, 10} + ); + + CHECK_EQ("\nabc(\"foo\")", callFragment.fragmentToParse); + CHECK(callFragment.nearestStatement->is()); + CHECK_GE(callFragment.ancestry.size(), 2); - CHECK_EQ("function abc()\n local myInnerLocal = 1\n\n end\n", fragment.fragmentToParse); + AstNode* back = callFragment.ancestry.back(); + CHECK(back->is()); + CHECK_EQ(Position{1, 4}, back->location.begin); + CHECK_EQ(Position{1, 9}, back->location.end); + + AstNode* parent = callFragment.ancestry.rbegin()[1]; + CHECK(parent->is()); + CHECK_EQ(Position{1, 0}, parent->location.begin); + CHECK_EQ(Position{1, 10}, parent->location.end); + + + auto stringFragment = parseFragment( + R"(function abc(foo: string) end +abc("foo") +abc("bar") +)", + Position{1, 6}, + Position{1, 9} + ); + + CHECK_EQ("\nabc(\"foo\"", stringFragment.fragmentToParse); + CHECK(stringFragment.nearestStatement->is()); + + CHECK_GE(stringFragment.ancestry.size(), 1); + + back = stringFragment.ancestry.back(); + + auto asString = back->as(); + CHECK(asString); + + CHECK_EQ(Position{1, 4}, asString->location.begin); + CHECK_EQ(Position{1, 9}, asString->location.end); + CHECK_EQ("foo", std::string{asString->value.data}); + CHECK_EQ(AstExprConstantString::QuotedSimple, asString->quoteStyle); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_multi_line_fragment_override") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + + auto res = check("function abc(foo: string) end"); + + LUAU_REQUIRE_NO_ERRORS(res); + + auto fragment = parseFragment( + R"(function abc(foo: string) end +abc( +"foo" +) +abc("bar") +)", + Position{2, 5}, + Position{3, 1} + ); + + CHECK_EQ("\nabc(\n\"foo\"\n)", fragment.fragmentToParse); + CHECK(fragment.nearestStatement->is()); + + CHECK_GE(fragment.ancestry.size(), 2); + + AstNode* back = fragment.ancestry.back(); + CHECK(back->is()); + CHECK_EQ(Position{2, 0}, back->location.begin); + CHECK_EQ(Position{2, 5}, back->location.end); + + AstNode* parent = fragment.ancestry.rbegin()[1]; + CHECK(parent->is()); + CHECK_EQ(Position{1, 0}, parent->location.begin); + CHECK_EQ(Position{3, 1}, parent->location.end); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "respects_frontend_options") +{ + DOES_NOT_PASS_NEW_SOLVER_GUARD(); + + std::string source = R"( +local tbl = { abc = 1234} +t +)"; + fileResolver.source["game/A"] = source; + + FrontendOptions opts; + opts.forAutocomplete = true; + + frontend.check("game/A", opts); + CHECK_NE(frontend.moduleResolverForAutocomplete.getModule("game/A"), nullptr); + CHECK_EQ(frontend.moduleResolver.getModule("game/A"), nullptr); + + + FragmentAutocompleteResult result = Luau::fragmentAutocomplete(frontend, source, "game/A", Position{2, 1}, opts, nullCallback); + CHECK_EQ("game/A", result.incrementalModule->name); + CHECK_NE(frontend.moduleResolverForAutocomplete.getModule("game/A"), nullptr); + CHECK_EQ(frontend.moduleResolver.getModule("game/A"), nullptr); } TEST_SUITE_END(); @@ -345,6 +528,7 @@ TEST_SUITE_BEGIN("FragmentAutocompleteTypeCheckerTests"); TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_typecheck_simple_fragment") { + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; auto res = check( R"( local x = 4 @@ -370,6 +554,7 @@ local z = x + y TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_typecheck_fragment_inserted_inline") { + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; auto res = check( R"( local x = 4 @@ -394,11 +579,63 @@ local y = 5 TEST_SUITE_END(); -TEST_SUITE_BEGIN("FragmentAutocompleteTests"); -TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_autocomplete_simple_property_access") +TEST_SUITE_BEGIN("MixedModeTests"); + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "mixed_mode_basic_example_append") { - auto res = check( + ScopedFastFlag sff{FFlag::LuauSolverV2, false}; + auto res = checkOldSolver( + R"( +local x = 4 +local y = 5 +)" + ); + + LUAU_REQUIRE_NO_ERRORS(res); + + auto fragment = checkFragment( + R"( +local x = 4 +local y = 5 +local z = x + y +)", + Position{3, 15} + ); + + auto opt = linearSearchForBinding(fragment.freshScope.get(), "z"); + REQUIRE(opt); + CHECK_EQ("number", toString(*opt)); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "mixed_mode_basic_example_inlined") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, false}; + auto res = checkOldSolver( + R"( +local x = 4 +local y = 5 +)" + ); + + auto fragment = checkFragment( + R"( +local x = 4 +local z = x +local y = 5 +)", + Position{2, 11} + ); + + auto correct = linearSearchForBinding(fragment.freshScope.get(), "z"); + REQUIRE(correct); + CHECK_EQ("number", toString(*correct)); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "mixed_mode_can_autocomplete_simple_property_access") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, false}; + auto res = checkOldSolver( R"( local tbl = { abc = 1234} )" @@ -421,188 +658,569 @@ tbl. CHECK_EQ(AutocompleteContext::Property, fragment.acResults.context); } -TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_autocomplete_nested_property_access") +TEST_SUITE_END(); + +TEST_SUITE_BEGIN("FragmentAutocompleteTests"); + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_autocomplete_simple_property_access") { - auto res = check( - R"( -local tbl = { abc = { def = 1234, egh = false } } -)" - ); - LUAU_REQUIRE_NO_ERRORS(res); + const std::string source = R"( +local tbl = { abc = 1234} +)"; + const std::string updated = R"( +local tbl = { abc = 1234} +tbl. +)"; + + autocompleteFragmentInBothSolvers( + source, + updated, + Position{2, 5}, + [](FragmentAutocompleteResult& fragment) + { + LUAU_ASSERT(fragment.freshScope); + + CHECK_EQ(1, fragment.acResults.entryMap.size()); + CHECK(fragment.acResults.entryMap.count("abc")); + CHECK_EQ(AutocompleteContext::Property, fragment.acResults.context); + } + ); +} - auto fragment = autocompleteFragment( - R"( +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_autocomplete_nested_property_access") +{ + const std::string source = R"( +local tbl = { abc = { def = 1234, egh = false } } +)"; + const std::string updated = R"( local tbl = { abc = { def = 1234, egh = false } } tbl.abc. -)", - Position{2, 8} +)"; + autocompleteFragmentInBothSolvers( + source, + updated, + Position{2, 8}, + [](FragmentAutocompleteResult& fragment) + { + LUAU_ASSERT(fragment.freshScope); + + CHECK_EQ(2, fragment.acResults.entryMap.size()); + CHECK(fragment.acResults.entryMap.count("def")); + CHECK(fragment.acResults.entryMap.count("egh")); + CHECK_EQ(fragment.acResults.context, AutocompleteContext::Property); + } ); - - LUAU_ASSERT(fragment.freshScope); - - CHECK_EQ(2, fragment.acResults.entryMap.size()); - CHECK(fragment.acResults.entryMap.count("def")); - CHECK(fragment.acResults.entryMap.count("egh")); - CHECK_EQ(fragment.acResults.context, AutocompleteContext::Property); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "inline_autocomplete_picks_the_right_scope") { - auto res = check( - R"( + const std::string source = R"( type Table = { a: number, b: number } do type Table = { x: string, y: string } end -)" - ); +)"; - LUAU_REQUIRE_NO_ERRORS(res); - - auto fragment = autocompleteFragment( - R"( + const std::string updated = R"( type Table = { a: number, b: number } do type Table = { x: string, y: string } local a : T end -)", - Position{4, 15} +)"; + + autocompleteFragmentInBothSolvers( + source, + updated, + Position{4, 15}, + [](FragmentAutocompleteResult& fragment) + { + LUAU_ASSERT(fragment.freshScope); + + REQUIRE(fragment.acResults.entryMap.count("Table")); + REQUIRE(fragment.acResults.entryMap["Table"].type); + const TableType* tv = get(follow(*fragment.acResults.entryMap["Table"].type)); + REQUIRE(tv); + CHECK(tv->props.count("x")); + } ); - - LUAU_ASSERT(fragment.freshScope); - - REQUIRE(fragment.acResults.entryMap.count("Table")); - REQUIRE(fragment.acResults.entryMap["Table"].type); - const TableType* tv = get(follow(*fragment.acResults.entryMap["Table"].type)); - REQUIRE(tv); - CHECK(tv->props.count("x")); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "nested_recursive_function") { - auto res = check(R"( + const std::string source = R"( function foo() end -)"); - - LUAU_REQUIRE_NO_ERRORS(res); - - auto fragment = autocompleteFragment( - R"( -function foo() -end -)", - Position{2, 0} +)"; + autocompleteFragmentInBothSolvers( + source, + source, + Position{2, 0}, + [](FragmentAutocompleteResult& fragment) + { + CHECK(fragment.acResults.entryMap.count("foo")); + CHECK_EQ(AutocompleteContext::Statement, fragment.acResults.context); + } ); - - CHECK(fragment.acResults.entryMap.count("foo")); - CHECK_EQ(AutocompleteContext::Statement, fragment.acResults.context); } +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "string_literal_with_override") +{ + const std::string source = R"( +function foo(bar: string) end +foo("abc") +)"; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{2, 6}, + [](FragmentAutocompleteResult& fragment) + { + CHECK(fragment.acResults.entryMap.empty()); + CHECK_EQ(AutocompleteContext::String, fragment.acResults.context); + }, + Position{2, 9} + ); +} // Start compatibility tests! TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "empty_program") { - check(""); - - auto frag = autocompleteFragment(" ", Position{0, 1}); - auto ac = frag.acResults; - CHECK(ac.entryMap.count("table")); - CHECK(ac.entryMap.count("math")); - CHECK_EQ(ac.context, AutocompleteContext::Statement); + autocompleteFragmentInBothSolvers( + "", + "", + Position{0, 1}, + [](FragmentAutocompleteResult& frag) + { + auto ac = frag.acResults; + CHECK(ac.entryMap.count("table")); + CHECK(ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); + } + ); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer") { - check("local a ="); - auto frag = autocompleteFragment("local a =", Position{0, 9}); - auto ac = frag.acResults; - - CHECK(ac.entryMap.count("table")); - CHECK(ac.entryMap.count("math")); - CHECK_EQ(ac.context, AutocompleteContext::Expression); + const std::string source = "local a ="; + autocompleteFragmentInBothSolvers( + source, + source, + Position{0, 9}, + [](FragmentAutocompleteResult& frag) + { + auto ac = frag.acResults; + + CHECK(ac.entryMap.count("table")); + CHECK(ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Expression); + } + ); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "leave_numbers_alone") { - check("local a = 3."); - - auto frag = autocompleteFragment("local a = 3.", Position{0, 12}); - auto ac = frag.acResults; - CHECK(ac.entryMap.empty()); - CHECK_EQ(ac.context, AutocompleteContext::Unknown); + const std::string source = "local a = 3."; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{0, 12}, + [](FragmentAutocompleteResult& frag) + { + auto ac = frag.acResults; + CHECK(ac.entryMap.empty()); + CHECK_EQ(ac.context, AutocompleteContext::Unknown); + } + ); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "user_defined_globals") { - check("local myLocal = 4; "); - - auto frag = autocompleteFragment("local myLocal = 4; ", Position{0, 18}); - auto ac = frag.acResults; - - CHECK(ac.entryMap.count("myLocal")); - CHECK(ac.entryMap.count("table")); - CHECK(ac.entryMap.count("math")); - CHECK_EQ(ac.context, AutocompleteContext::Statement); + const std::string source = "local myLocal = 4; "; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{0, 18}, + [](FragmentAutocompleteResult& frag) + { + auto ac = frag.acResults; + + CHECK(ac.entryMap.count("myLocal")); + CHECK(ac.entryMap.count("table")); + CHECK(ac.entryMap.count("math")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); + } + ); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "dont_suggest_local_before_its_definition") { - check(R"( + const std::string source = R"( local myLocal = 4 function abc() local myInnerLocal = 1 end - )"); + )"; // autocomplete after abc but before myInnerLocal - auto fragment = autocompleteFragment( - R"( - local myLocal = 4 - function abc() - local myInnerLocal = 1 + autocompleteFragmentInBothSolvers( + source, + source, + Position{3, 0}, + [](FragmentAutocompleteResult& fragment) + { + auto ac = fragment.acResults; + CHECK(ac.entryMap.count("myLocal")); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "myInnerLocal"); + } + ); + // autocomplete after my inner local + autocompleteFragmentInBothSolvers( + source, + source, + Position{4, 0}, + [](FragmentAutocompleteResult& fragment) + { + auto ac = fragment.acResults; + CHECK(ac.entryMap.count("myLocal")); + CHECK(ac.entryMap.count("myInnerLocal")); + } + ); + // autocomplete after abc, but don't include myInnerLocal(in the hidden scope) + autocompleteFragmentInBothSolvers( + source, + source, + Position{6, 0}, + [](FragmentAutocompleteResult& fragment) + { + auto ac = fragment.acResults; + CHECK(ac.entryMap.count("myLocal")); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "myInnerLocal"); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "nested_recursive_function") +{ + const std::string source = R"( + local function outer() + local function inner() + end end -)", - Position{3, 0} + )"; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{3, 0}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK(ac.entryMap.count("inner")); + CHECK(ac.entryMap.count("outer")); + } ); - auto ac = fragment.acResults; - CHECK(ac.entryMap.count("myLocal")); - LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "myInnerLocal"); +} - // autocomplete after my inner local - fragment = autocompleteFragment( - R"( - local myLocal = 4 - function abc() - local myInnerLocal = 1 +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "user_defined_local_functions_in_own_definition") +{ + const std::string source = R"( + local function abc() end - )", - Position{4, 0} + )"; + // Autocomplete inside of abc + autocompleteFragmentInBothSolvers( + source, + source, + Position{2, 0}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK(ac.entryMap.count("abc")); + CHECK(ac.entryMap.count("table")); + CHECK(ac.entryMap.count("math")); + } ); - ac = fragment.acResults; - CHECK(ac.entryMap.count("myLocal")); - CHECK(ac.entryMap.count("myInnerLocal")); +} - fragment = autocompleteFragment( - R"( - local myLocal = 4 - function abc() - local myInnerLocal = 1 +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "global_functions_are_not_scoped_lexically") +{ + const std::string source = R"( + if true then + function abc() + end end - )", - Position{6, 0} + )"; + autocompleteFragmentInBothSolvers( + source, + source, + Position{6, 0}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK(!ac.entryMap.empty()); + CHECK(ac.entryMap.count("abc")); + CHECK(ac.entryMap.count("table")); + CHECK(ac.entryMap.count("math")); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_functions_fall_out_of_scope") +{ + const std::string source = R"( + if true then + local function abc() + + end + end + )"; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{6, 0}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK_NE(0, ac.entryMap.size()); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "abc"); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "function_parameters") +{ + const std::string source = R"( + function abc(test) + + end + )"; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{3, 0}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK(ac.entryMap.count("test")); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "unsealed_table") +{ + const std::string source = R"( + local tbl = {} + tbl.prop = 5 + tbl. + )"; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{3, 12}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK_EQ(1, ac.entryMap.size()); + CHECK(ac.entryMap.count("prop")); + CHECK_EQ(ac.context, AutocompleteContext::Property); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "unsealed_table_2") +{ + const std::string source = R"( + local tbl = {} + local inner = { prop = 5 } + tbl.inner = inner + tbl.inner. + )"; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{4, 18}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK_EQ(1, ac.entryMap.size()); + CHECK(ac.entryMap.count("prop")); + CHECK_EQ(ac.context, AutocompleteContext::Property); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "cyclic_table") +{ + const std::string source = R"( + local abc = {} + local def = { abc = abc } + abc.def = def + abc.def. + )"; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{4, 16}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK(ac.entryMap.count("abc")); + CHECK_EQ(ac.context, AutocompleteContext::Property); + } ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "table_union") +{ + const std::string source = R"( + type t1 = { a1 : string, b2 : number } + type t2 = { b2 : string, c3 : string } + function func(abc : t1 | t2) + + end + )"; + const std::string updated = R"( + type t1 = { a1 : string, b2 : number } + type t2 = { b2 : string, c3 : string } + function func(abc : t1 | t2) + abc. + end + )"; + + autocompleteFragmentInBothSolvers( + source, + updated, + Position{4, 16}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK_EQ(1, ac.entryMap.size()); + CHECK(ac.entryMap.count("b2")); + CHECK_EQ(ac.context, AutocompleteContext::Property); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "table_intersection") +{ + const std::string source = R"( + type t1 = { a1 : string, b2 : number } + type t2 = { b2 : number, c3 : string } + function func(abc : t1 & t2) - ac = fragment.acResults; - CHECK(ac.entryMap.count("myLocal")); - LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "myInnerLocal"); + end + )"; + const std::string updated = R"( + type t1 = { a1 : string, b2 : number } + type t2 = { b2 : number, c3 : string } + function func(abc : t1 & t2) + abc. + end + )"; + + autocompleteFragmentInBothSolvers( + source, + updated, + Position{4, 16}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK_EQ(3, ac.entryMap.size()); + CHECK(ac.entryMap.count("a1")); + CHECK(ac.entryMap.count("b2")); + CHECK(ac.entryMap.count("c3")); + CHECK_EQ(ac.context, AutocompleteContext::Property); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "get_suggestions_for_the_very_start_of_the_script") +{ + const std::string source = R"( + + function aaa() end + )"; + + autocompleteFragmentInBothSolvers( + source, + source, + Position{0, 0}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK(ac.entryMap.count("table")); + CHECK_EQ(ac.context, AutocompleteContext::Statement); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "studio_ice_1") +{ + const std::string source = R"( +--Woop +@native +local function test() + +end +)"; + + const std::string updated = R"( +--Woop +@native +local function test() + +end +function a +)"; + autocompleteFragmentInBothSolvers(source, updated, Position{6, 10}, [](FragmentAutocompleteResult& result) {}); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "method_call_inside_function_body") +{ + const std::string source = R"( + local game = { GetService=function(s) return 'hello' end } + + function a() + + end + )"; + + const std::string updated = R"( + local game = { GetService=function(s) return 'hello' end } + + function a() + game: + end + )"; + + autocompleteFragmentInBothSolvers( + source, + updated, + Position{4, 17}, + [](FragmentAutocompleteResult& result) + { + auto ac = result.acResults; + CHECK_NE(0, ac.entryMap.size()); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "math"); + CHECK_EQ(ac.context, AutocompleteContext::Property); + } + ); } TEST_SUITE_END(); diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index ffb440492..8d13ebde9 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -4,6 +4,7 @@ #include "Fixture.h" #include "Luau/Ast.h" +#include "Luau/BuiltinDefinitions.h" #include "Luau/Common.h" #include "Luau/IostreamHelpers.h" #include "Luau/ModuleResolver.h" @@ -13,6 +14,8 @@ #include "doctest.h" #include +LUAU_FASTFLAG(LuauCountSelfCallsNonstrict) + using namespace Luau; #define NONSTRICT_REQUIRE_ERR_AT_POS(pos, result, idx) \ @@ -576,4 +579,25 @@ buffer.readi8(b, 0) LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "nonstrict_method_calls") +{ + ScopedFastFlag sff{FFlag::LuauCountSelfCallsNonstrict, true}; + + Luau::unfreeze(frontend.globals.globalTypes); + Luau::unfreeze(frontend.globalsForAutocomplete.globalTypes); + + registerBuiltinGlobals(frontend, frontend.globals); + registerTestTypes(); + + Luau::freeze(frontend.globals.globalTypes); + Luau::freeze(frontend.globalsForAutocomplete.globalTypes); + + CheckResult result = checkNonStrict(R"( + local test = "test" + test:lower() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index 693300578..30b0da1f5 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -19,6 +19,7 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauAttributeSyntaxFunExpr) LUAU_FASTFLAG(LuauUserDefinedTypeFunctionsSyntax2) LUAU_FASTFLAG(LuauUserDefinedTypeFunParseExport) +LUAU_FASTFLAG(LuauAllowComplexTypesInGenericParams) namespace { @@ -3678,5 +3679,69 @@ TEST_CASE_FIXTURE(Fixture, "mixed_leading_intersection_and_union_not_allowed") matchParseError("type A = | number & string & boolean", "Mixing union and intersection types is not allowed; consider wrapping in parentheses."); } +TEST_CASE_FIXTURE(Fixture, "grouped_function_type") +{ + ScopedFastFlag _{FFlag::LuauAllowComplexTypesInGenericParams, true}; + const auto root = parse(R"( + type X = T + local x: X<(() -> ())?> + )"); + LUAU_ASSERT(root); + CHECK_EQ(root->body.size, 2); + auto assignment = root->body.data[1]->as(); + LUAU_ASSERT(assignment); + CHECK_EQ(assignment->vars.size, 1); + CHECK_EQ(assignment->values.size, 0); + auto binding = assignment->vars.data[0]; + CHECK_EQ(binding->name, "x"); + auto genericTy = binding->annotation->as(); + LUAU_ASSERT(genericTy); + CHECK_EQ(genericTy->parameters.size, 1); + auto paramTy = genericTy->parameters.data[0]; + LUAU_ASSERT(paramTy.type); + auto unionTy = paramTy.type->as(); + LUAU_ASSERT(unionTy); + CHECK_EQ(unionTy->types.size, 2); + CHECK(unionTy->types.data[0]->is()); // () -> () + CHECK(unionTy->types.data[1]->is()); // nil +} + +TEST_CASE_FIXTURE(Fixture, "complex_union_in_generic_ty") +{ + ScopedFastFlag _{FFlag::LuauAllowComplexTypesInGenericParams, true}; + const auto root = parse(R"( + type X = T + local x: X< + | number + | boolean + | string + > + )"); + LUAU_ASSERT(root); + CHECK_EQ(root->body.size, 2); + auto assignment = root->body.data[1]->as(); + LUAU_ASSERT(assignment); + CHECK_EQ(assignment->vars.size, 1); + CHECK_EQ(assignment->values.size, 0); + auto binding = assignment->vars.data[0]; + CHECK_EQ(binding->name, "x"); + auto genericTy = binding->annotation->as(); + LUAU_ASSERT(genericTy); + CHECK_EQ(genericTy->parameters.size, 1); + auto paramTy = genericTy->parameters.data[0]; + LUAU_ASSERT(paramTy.type); + auto unionTy = paramTy.type->as(); + LUAU_ASSERT(unionTy); + CHECK_EQ(unionTy->types.size, 3); + // NOTE: These are `const char*` so we can compare them to `AstName`s later. + std::vector expectedTypes{"number", "boolean", "string"}; + for (size_t i = 0; i < expectedTypes.size(); i++) + { + auto ty = unionTy->types.data[i]->as(); + LUAU_ASSERT(ty); + CHECK_EQ(ty->name, expectedTypes[i]); + } +} + TEST_SUITE_END(); diff --git a/tests/RequireByString.test.cpp b/tests/RequireByString.test.cpp index f9bc3afbb..bc1161b04 100644 --- a/tests/RequireByString.test.cpp +++ b/tests/RequireByString.test.cpp @@ -225,8 +225,8 @@ TEST_CASE("PathResolution") CHECK(resolvePath("../module", "") == "../module"); CHECK(resolvePath("../../module", "") == "../../module"); - CHECK(resolvePath("../module/..", "") == ".."); - CHECK(resolvePath("../module/../..", "") == "../.."); + CHECK(resolvePath("../module/..", "") == "../"); + CHECK(resolvePath("../module/../..", "") == "../../"); CHECK(resolvePath("../dependency", prefix + "Users/modules/module.luau") == prefix + "Users/dependency"); CHECK(resolvePath("../dependency/", prefix + "Users/modules/module.luau") == prefix + "Users/dependency"); @@ -400,6 +400,13 @@ TEST_CASE_FIXTURE(ReplWithPathFixture, "CheckCacheAfterRequireInitLua") REQUIRE_FALSE_MESSAGE(lua_isnil(L, -1), "Cache did not contain module result"); } +TEST_CASE_FIXTURE(ReplWithPathFixture, "CheckCachedResult") +{ + std::string relativePath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/validate_cache"; + runProtectedRequire(relativePath); + assertOutputContainsAll({"true"}); +} + TEST_CASE_FIXTURE(ReplWithPathFixture, "LoadStringRelative") { runCode(L, "return pcall(function() return loadstring(\"require('a/relative/path')\")() end)"); diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 44b0dd77e..d9c4c13e4 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -10,7 +10,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) LUAU_FASTFLAG(LuauTypestateBuiltins2) LUAU_FASTFLAG(LuauStringFormatArityFix) diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index ce9f6ccfa..d11a5f302 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -18,10 +18,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering) -LUAU_FASTFLAG(LuauAcceptIndexingTableUnionsIntersections) LUAU_FASTFLAG(LuauRetrySubtypingWithoutHiddenPack) - -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) +LUAU_FASTFLAG(LuauTableKeysAreRValues) TEST_SUITE_BEGIN("TableTests"); @@ -4802,8 +4800,6 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "indexing_branching_table") { - ScopedFastFlag sff{FFlag::LuauAcceptIndexingTableUnionsIntersections, true}; - CheckResult result = check(R"( local test = if true then { "meow", "woof" } else { 4, 81 } local test2 = test[1] @@ -4820,8 +4816,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "indexing_branching_table") TEST_CASE_FIXTURE(BuiltinsFixture, "indexing_branching_table2") { - ScopedFastFlag sff{FFlag::LuauAcceptIndexingTableUnionsIntersections, true}; - CheckResult result = check(R"( local test = if true then {} else {} local test2 = test[1] @@ -4936,4 +4930,26 @@ TEST_CASE_FIXTURE(Fixture, "function_check_constraint_too_eager") )")); } + +TEST_CASE_FIXTURE(BuiltinsFixture, "read_only_property_reads") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauTableKeysAreRValues, true}; + + // none of the `t.id` accesses here should error + auto result = check(R"( + --!strict + type readonlyTable = {read id: number} + local t:readonlyTable = {id = 1} + + local _:{number} = {[t.id] = 1} + local _:{number} = {[t.id::number] = 1} + + local arr:{number} = {} + arr[t.id] = 1 + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index 320124794..49ec61e75 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -9,7 +9,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauAcceptIndexingTableUnionsIntersections) TEST_SUITE_BEGIN("UnionTypes"); @@ -645,10 +644,7 @@ TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash") // this is a cyclic union of number arrays, so it _is_ a table, even if it's a nonsense type. // no need to generate a NotATable error here. The new solver automatically handles this and // correctly reports no errors. - if (FFlag::LuauAcceptIndexingTableUnionsIntersections || FFlag::LuauSolverV2) - LUAU_REQUIRE_NO_ERRORS(result); - else - LUAU_REQUIRE_ERROR_COUNT(1, result); + LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect") diff --git a/tests/main.cpp b/tests/main.cpp index d80589c15..bd5a05176 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -27,13 +27,10 @@ #include #endif -#include // TODO: remove with LuauTypeSolverRelease #include #include -LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) - // Indicates if verbose output is enabled; can be overridden via --verbose // Currently, this enables output from 'print', but other verbose output could be enabled eventually. bool verbose = false; @@ -415,12 +412,6 @@ int main(int argc, char** argv) printf("Using RNG seed %u\n", *randomSeed); } - // New Luau type solver uses a temporary scheme where fixes are made under a single version flag - // When flags are enabled, new solver is enabled with all new features and fixes - // When it's disabled, this value should have no effect (all uses under a new solver) - // Flag setup argument can still be used to override this to a specific value if desired - DFInt::LuauTypeSolverRelease.value = std::numeric_limits::max(); - if (std::vector flags; doctest::parseCommaSepArgs(argc, argv, "--fflags=", flags)) setFastFlags(flags); diff --git a/tests/require/without_config/validate_cache.luau b/tests/require/without_config/validate_cache.luau new file mode 100644 index 000000000..8e729af18 --- /dev/null +++ b/tests/require/without_config/validate_cache.luau @@ -0,0 +1,4 @@ +local result1 = require("./dependency") +local result2 = require("./dependency") +assert(result1 == result2) +return {} \ No newline at end of file