Merge branch 'main' into prototyping-numbers

This commit is contained in:
Lily Brown 2022-02-18 11:03:37 -08:00
commit f33d0c1313
98 changed files with 1835 additions and 603 deletions

View file

@ -9,12 +9,14 @@ on:
- 'papers/**' - 'papers/**'
- 'rfcs/**' - 'rfcs/**'
- '*.md' - '*.md'
- 'prototyping/**'
pull_request: pull_request:
paths-ignore: paths-ignore:
- 'docs/**' - 'docs/**'
- 'papers/**' - 'papers/**'
- 'rfcs/**' - 'rfcs/**'
- '*.md' - '*.md'
- 'prototyping/**'
jobs: jobs:
unix: unix:

View file

@ -8,12 +8,18 @@ on:
paths: paths:
- '.github/workflows/**' - '.github/workflows/**'
- 'prototyping/**' - 'prototyping/**'
- 'Analysis/src/JsonEncoder.cpp' - 'Analysis/**'
- 'Ast/**'
- 'CLI/Ast.cpp'
- 'CLI/FileUtils.*'
pull_request: pull_request:
paths: paths:
- '.github/workflows/**' - '.github/workflows/**'
- 'prototyping/**' - 'prototyping/**'
- 'Analysis/src/JsonEncoder.cpp' - 'Analysis/**'
- 'Ast/**'
- 'CLI/Ast.cpp'
- 'CLI/FileUtils.*'
jobs: jobs:
linux: linux:
@ -28,6 +34,11 @@ jobs:
with: with:
path: ~/.cabal/store path: ~/.cabal/store
key: prototyping-${{ runner.os }}-${{ matrix.agda }} key: prototyping-${{ runner.os }}-${{ matrix.agda }}
- uses: actions/cache@v2
id: luau-ast-cache
with:
path: ./build
key: prototyping-${{ runner.os }}-${{ hashFiles('Ast/**', 'Analysis/**', 'CLI/Ast.cpp', 'CLI/FileUtils.*')}}
- name: install cabal - name: install cabal
run: sudo apt-get install -y cabal-install run: sudo apt-get install -y cabal-install
- name: cabal update - name: cabal update
@ -37,26 +48,35 @@ jobs:
working-directory: prototyping working-directory: prototyping
run: | run: |
cabal install Agda-${{ matrix.agda }} cabal install Agda-${{ matrix.agda }}
cabal install --lib scientific --package-env . cabal install --lib scientific vector aeson --package-env .
cabal install --lib vector --package-env . - name: check targets
cabal install --lib aeson --package-env .
- name: check examples
working-directory: prototyping working-directory: prototyping
run: ~/.cabal/bin/agda Examples.agda run: |
~/.cabal/bin/agda Examples.agda
~/.cabal/bin/agda Properties.agda
- name: build executables - name: build executables
working-directory: prototyping working-directory: prototyping
run: | run: |
~/.cabal/bin/agda --compile PrettyPrinter.agda ~/.cabal/bin/agda --compile PrettyPrinter.agda
~/.cabal/bin/agda --compile Interpreter.agda ~/.cabal/bin/agda --compile Interpreter.agda
- name: cmake configure - name: cmake configure
run: cmake . if: steps.luau-ast-cache.outputs.cache-hit != 'true'
run: |
mkdir -p build
cd build
cmake build ../
- name: cmake build luau-ast - name: cmake build luau-ast
run: cmake --build . --target Luau.Ast.CLI if: steps.luau-ast-cache.outputs.cache-hit != 'true'
- name: run smoketest run: |
cmake --build ./build --target Luau.Ast.CLI
- name: run tests
working-directory: prototyping working-directory: prototyping
run: | run: |
../luau-ast Examples/SmokeTest.lua | ./PrettyPrinter > Examples/SmokeTestOutput.lua mkdir test-failures
../luau-ast Examples/SmokeTest.lua | ./Interpreter python tests.py -l ../build/luau-ast --write-diff-failures --diff-failure-location test-failures/
- name: diff smoketest - uses: actions/upload-artifact@v2
working-directory: prototyping if: failure()
run: diff Examples/SmokeTest.lua Examples/SmokeTestOutput.lua with:
name: test failures
path: prototyping/test-failures
retention-days: 5

View file

@ -9,6 +9,7 @@ on:
- 'papers/**' - 'papers/**'
- 'rfcs/**' - 'rfcs/**'
- '*.md' - '*.md'
- 'prototyping/**'
jobs: jobs:
build: build:

View file

@ -21,6 +21,7 @@ struct BasicDocumentation
{ {
std::string documentation; std::string documentation;
std::string learnMoreLink; std::string learnMoreLink;
std::string codeSample;
}; };
struct FunctionParameterDocumentation struct FunctionParameterDocumentation
@ -37,6 +38,7 @@ struct FunctionDocumentation
std::vector<FunctionParameterDocumentation> parameters; std::vector<FunctionParameterDocumentation> parameters;
std::vector<DocumentationSymbol> returns; std::vector<DocumentationSymbol> returns;
std::string learnMoreLink; std::string learnMoreLink;
std::string codeSample;
}; };
struct OverloadedFunctionDocumentation struct OverloadedFunctionDocumentation
@ -52,6 +54,7 @@ struct TableDocumentation
std::string documentation; std::string documentation;
Luau::DenseHashMap<std::string, DocumentationSymbol> keys; Luau::DenseHashMap<std::string, DocumentationSymbol> keys;
std::string learnMoreLink; std::string learnMoreLink;
std::string codeSample;
}; };
using DocumentationDatabase = Luau::DenseHashMap<DocumentationSymbol, Documentation>; using DocumentationDatabase = Luau::DenseHashMap<DocumentationSymbol, Documentation>;

View file

@ -24,6 +24,7 @@ struct TypeChecker;
struct FileResolver; struct FileResolver;
struct ModuleResolver; struct ModuleResolver;
struct ParseResult; struct ParseResult;
struct HotComment;
struct LoadDefinitionFileResult struct LoadDefinitionFileResult
{ {
@ -35,7 +36,7 @@ struct LoadDefinitionFileResult
LoadDefinitionFileResult loadDefinitionFile( LoadDefinitionFileResult loadDefinitionFile(
TypeChecker& typeChecker, ScopePtr targetScope, std::string_view definition, const std::string& packageName); TypeChecker& typeChecker, ScopePtr targetScope, std::string_view definition, const std::string& packageName);
std::optional<Mode> parseMode(const std::vector<std::string>& hotcomments); std::optional<Mode> parseMode(const std::vector<HotComment>& hotcomments);
std::vector<std::string_view> parsePathExpr(const AstExpr& pathExpr); std::vector<std::string_view> parsePathExpr(const AstExpr& pathExpr);

View file

@ -14,6 +14,7 @@ class AstStat;
class AstNameTable; class AstNameTable;
struct TypeChecker; struct TypeChecker;
struct Module; struct Module;
struct HotComment;
using ScopePtr = std::shared_ptr<struct Scope>; using ScopePtr = std::shared_ptr<struct Scope>;
@ -50,6 +51,7 @@ struct LintWarning
Code_TableOperations = 23, Code_TableOperations = 23,
Code_DuplicateCondition = 24, Code_DuplicateCondition = 24,
Code_MisleadingAndOr = 25, Code_MisleadingAndOr = 25,
Code_CommentDirective = 26,
Code__Count Code__Count
}; };
@ -60,7 +62,7 @@ struct LintWarning
static const char* getName(Code code); static const char* getName(Code code);
static Code parseName(const char* name); static Code parseName(const char* name);
static uint64_t parseMask(const std::vector<std::string>& hotcomments); static uint64_t parseMask(const std::vector<HotComment>& hotcomments);
}; };
struct LintResult struct LintResult
@ -90,7 +92,8 @@ struct LintOptions
void setDefaults(); void setDefaults();
}; };
std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module, const LintOptions& options); std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module,
const std::vector<HotComment>& hotcomments, const LintOptions& options);
std::vector<AstName> getDeprecatedGlobals(const AstNameTable& names); std::vector<AstName> getDeprecatedGlobals(const AstNameTable& names);

View file

@ -6,7 +6,7 @@
#include "Luau/TypedAllocator.h" #include "Luau/TypedAllocator.h"
#include "Luau/ParseOptions.h" #include "Luau/ParseOptions.h"
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/Parser.h" #include "Luau/ParseResult.h"
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -37,8 +37,8 @@ struct SourceModule
AstStatBlock* root = nullptr; AstStatBlock* root = nullptr;
std::optional<Mode> mode; std::optional<Mode> mode;
uint64_t ignoreLints = 0;
std::vector<HotComment> hotcomments;
std::vector<Comment> commentLocations; std::vector<Comment> commentLocations;
SourceModule() SourceModule()

View file

@ -6,9 +6,6 @@
namespace Luau namespace Luau
{ {
struct Module; void quantify(TypeId ty, TypeLevel level);
using ModulePtr = std::shared_ptr<Module>;
void quantify(ModulePtr module, TypeId ty, TypeLevel level);
} // namespace Luau } // namespace Luau

View file

@ -101,9 +101,6 @@ struct Tarjan
// This is hot code, so we optimize recursion to a stack. // This is hot code, so we optimize recursion to a stack.
TarjanResult loop(); TarjanResult loop();
// Clear the state
void clear();
// Find or create the index for a vertex. // Find or create the index for a vertex.
// Return a boolean which is `true` if it's a freshly created index. // Return a boolean which is `true` if it's a freshly created index.
std::pair<int, bool> indexify(TypeId ty); std::pair<int, bool> indexify(TypeId ty);
@ -166,7 +163,17 @@ struct FindDirty : Tarjan
// and replaces them with clean ones. // and replaces them with clean ones.
struct Substitution : FindDirty struct Substitution : FindDirty
{ {
ModulePtr currentModule; protected:
Substitution(const TxnLog* log_, TypeArena* arena)
: arena(arena)
{
log = log_;
LUAU_ASSERT(log);
LUAU_ASSERT(arena);
}
public:
TypeArena* arena;
DenseHashMap<TypeId, TypeId> newTypes{nullptr}; DenseHashMap<TypeId, TypeId> newTypes{nullptr};
DenseHashMap<TypePackId, TypePackId> newPacks{nullptr}; DenseHashMap<TypePackId, TypePackId> newPacks{nullptr};
@ -192,12 +199,13 @@ struct Substitution : FindDirty
template<typename T> template<typename T>
TypeId addType(const T& tv) TypeId addType(const T& tv)
{ {
return currentModule->internalTypes.addType(tv); return arena->addType(tv);
} }
template<typename T> template<typename T>
TypePackId addTypePack(const T& tp) TypePackId addTypePack(const T& tp)
{ {
return currentModule->internalTypes.addTypePack(TypePackVar{tp}); return arena->addTypePack(TypePackVar{tp});
} }
}; };

View file

@ -5,7 +5,6 @@
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/Module.h" #include "Luau/Module.h"
#include "Luau/Symbol.h" #include "Luau/Symbol.h"
#include "Luau/Parser.h"
#include "Luau/Substitution.h" #include "Luau/Substitution.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
@ -37,6 +36,15 @@ struct Unifier;
// A substitution which replaces generic types in a given set by free types. // A substitution which replaces generic types in a given set by free types.
struct ReplaceGenerics : Substitution struct ReplaceGenerics : Substitution
{ {
ReplaceGenerics(
const TxnLog* log, TypeArena* arena, TypeLevel level, const std::vector<TypeId>& generics, const std::vector<TypePackId>& genericPacks)
: Substitution(log, arena)
, level(level)
, generics(generics)
, genericPacks(genericPacks)
{
}
TypeLevel level; TypeLevel level;
std::vector<TypeId> generics; std::vector<TypeId> generics;
std::vector<TypePackId> genericPacks; std::vector<TypePackId> genericPacks;
@ -50,8 +58,13 @@ struct ReplaceGenerics : Substitution
// A substitution which replaces generic functions by monomorphic functions // A substitution which replaces generic functions by monomorphic functions
struct Instantiation : Substitution struct Instantiation : Substitution
{ {
Instantiation(const TxnLog* log, TypeArena* arena, TypeLevel level)
: Substitution(log, arena)
, level(level)
{
}
TypeLevel level; TypeLevel level;
ReplaceGenerics replaceGenerics;
bool ignoreChildren(TypeId ty) override; bool ignoreChildren(TypeId ty) override;
bool isDirty(TypeId ty) override; bool isDirty(TypeId ty) override;
bool isDirty(TypePackId tp) override; bool isDirty(TypePackId tp) override;
@ -62,6 +75,12 @@ struct Instantiation : Substitution
// A substitution which replaces free types by generic types. // A substitution which replaces free types by generic types.
struct Quantification : Substitution struct Quantification : Substitution
{ {
Quantification(TypeArena* arena, TypeLevel level)
: Substitution(TxnLog::empty(), arena)
, level(level)
{
}
TypeLevel level; TypeLevel level;
std::vector<TypeId> generics; std::vector<TypeId> generics;
std::vector<TypePackId> genericPacks; std::vector<TypePackId> genericPacks;
@ -74,6 +93,13 @@ struct Quantification : Substitution
// A substitution which replaces free types by any // A substitution which replaces free types by any
struct Anyification : Substitution struct Anyification : Substitution
{ {
Anyification(TypeArena* arena, TypeId anyType, TypePackId anyTypePack)
: Substitution(TxnLog::empty(), arena)
, anyType(anyType)
, anyTypePack(anyTypePack)
{
}
TypeId anyType; TypeId anyType;
TypePackId anyTypePack; TypePackId anyTypePack;
bool isDirty(TypeId ty) override; bool isDirty(TypeId ty) override;
@ -85,6 +111,13 @@ struct Anyification : Substitution
// A substitution which replaces the type parameters of a type function by arguments // A substitution which replaces the type parameters of a type function by arguments
struct ApplyTypeFunction : Substitution struct ApplyTypeFunction : Substitution
{ {
ApplyTypeFunction(TypeArena* arena, TypeLevel level)
: Substitution(TxnLog::empty(), arena)
, level(level)
, encounteredForwardedType(false)
{
}
TypeLevel level; TypeLevel level;
bool encounteredForwardedType; bool encounteredForwardedType;
std::unordered_map<TypeId, TypeId> typeArguments; std::unordered_map<TypeId, TypeId> typeArguments;
@ -351,8 +384,7 @@ private:
// Note: `scope` must be a fresh scope. // Note: `scope` must be a fresh scope.
GenericTypeDefinitions createGenericTypes(const ScopePtr& scope, std::optional<TypeLevel> levelOpt, const AstNode& node, GenericTypeDefinitions createGenericTypes(const ScopePtr& scope, std::optional<TypeLevel> levelOpt, const AstNode& node,
const AstArray<AstGenericType>& genericNames, const AstArray<AstGenericTypePack>& genericPackNames, const AstArray<AstGenericType>& genericNames, const AstArray<AstGenericTypePack>& genericPackNames, bool useCache = false);
bool useCache = false);
public: public:
ErrorVec resolve(const PredicateVec& predicates, const ScopePtr& scope, bool sense); ErrorVec resolve(const PredicateVec& predicates, const ScopePtr& scope, bool sense);
@ -392,11 +424,6 @@ public:
ModulePtr currentModule; ModulePtr currentModule;
ModuleName currentModuleName; ModuleName currentModuleName;
Instantiation instantiation;
Quantification quantification;
Anyification anyification;
ApplyTypeFunction applyTypeFunction;
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope; std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope;
InternalErrorReporter* iceHandler; InternalErrorReporter* iceHandler;

View file

@ -7,6 +7,7 @@
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
#include "Luau/Parser.h" // TODO: only needed for autocompleteSource which is deprecated
#include <algorithm> #include <algorithm>
#include <unordered_set> #include <unordered_set>
@ -14,9 +15,9 @@
LUAU_FASTFLAG(LuauUseCommittingTxnLog) LUAU_FASTFLAG(LuauUseCommittingTxnLog)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteAvoidMutation, false); LUAU_FASTFLAGVARIABLE(LuauAutocompleteAvoidMutation, false);
LUAU_FASTFLAGVARIABLE(LuauCompleteBrokenStringParams, false);
LUAU_FASTFLAGVARIABLE(LuauMissingFollowACMetatables, false); LUAU_FASTFLAGVARIABLE(LuauMissingFollowACMetatables, false);
LUAU_FASTFLAGVARIABLE(PreferToCallFunctionsForIntersects, false); LUAU_FASTFLAGVARIABLE(PreferToCallFunctionsForIntersects, false);
LUAU_FASTFLAGVARIABLE(LuauIfElseExprFixCompletionIssue, false);
static const std::unordered_set<std::string> kStatementStartingKeywords = { static const std::unordered_set<std::string> kStatementStartingKeywords = {
"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; "while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
@ -380,7 +381,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
{ {
// We are walking up the class hierarchy, so if we encounter a property that we have // We are walking up the class hierarchy, so if we encounter a property that we have
// already populated, it takes precedence over the property we found just now. // already populated, it takes precedence over the property we found just now.
if (result.count(name) == 0 && name != Parser::errorName) if (result.count(name) == 0 && name != kParseNameError)
{ {
Luau::TypeId type = Luau::follow(prop.type); Luau::TypeId type = Luau::follow(prop.type);
TypeCorrectKind typeCorrect = indexType == PropIndexType::Key ? TypeCorrectKind::Correct TypeCorrectKind typeCorrect = indexType == PropIndexType::Key ? TypeCorrectKind::Correct
@ -948,9 +949,12 @@ AutocompleteEntryMap autocompleteTypeNames(const Module& module, Position positi
} }
} }
for (size_t i = 0; i < node->returnAnnotation.types.size; i++) if (!node->returnAnnotation)
return result;
for (size_t i = 0; i < node->returnAnnotation->types.size; i++)
{ {
AstType* ret = node->returnAnnotation.types.data[i]; AstType* ret = node->returnAnnotation->types.data[i];
if (ret->location.containsClosed(position)) if (ret->location.containsClosed(position))
{ {
@ -965,7 +969,7 @@ AutocompleteEntryMap autocompleteTypeNames(const Module& module, Position positi
} }
} }
if (AstTypePack* retTp = node->returnAnnotation.tailType) if (AstTypePack* retTp = node->returnAnnotation->tailType)
{ {
if (auto variadic = retTp->as<AstTypePackVariadic>()) if (auto variadic = retTp->as<AstTypePackVariadic>())
{ {
@ -1136,7 +1140,7 @@ static AutocompleteEntryMap autocompleteStatement(
AstNode* parent = ancestry.rbegin()[1]; AstNode* parent = ancestry.rbegin()[1];
if (AstStatIf* statIf = parent->as<AstStatIf>()) if (AstStatIf* statIf = parent->as<AstStatIf>())
{ {
if (!statIf->elsebody || (statIf->hasElse && statIf->elseLocation.containsClosed(position))) if (!statIf->elsebody || (statIf->elseLocation && statIf->elseLocation->containsClosed(position)))
{ {
result.emplace("else", AutocompleteEntry{AutocompleteEntryKind::Keyword}); result.emplace("else", AutocompleteEntry{AutocompleteEntryKind::Keyword});
result.emplace("elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword}); result.emplace("elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword});
@ -1164,8 +1168,7 @@ static AutocompleteEntryMap autocompleteStatement(
return result; return result;
} }
// Returns true if completions were generated (completions will be inserted into 'outResult') // Returns true iff `node` was handled by this function (completions, if any, are returned in `outResult`)
// Returns false if no completions were generated
static bool autocompleteIfElseExpression( static bool autocompleteIfElseExpression(
const AstNode* node, const std::vector<AstNode*>& ancestry, const Position& position, AutocompleteEntryMap& outResult) const AstNode* node, const std::vector<AstNode*>& ancestry, const Position& position, AutocompleteEntryMap& outResult)
{ {
@ -1173,6 +1176,13 @@ static bool autocompleteIfElseExpression(
if (!parent) if (!parent)
return false; return false;
if (FFlag::LuauIfElseExprFixCompletionIssue && node->is<AstExprIfElse>())
{
// Don't try to complete when the current node is an if-else expression (i.e. only try to complete when the node is a child of an if-else
// expression.
return true;
}
AstExprIfElse* ifElseExpr = parent->as<AstExprIfElse>(); AstExprIfElse* ifElseExpr = parent->as<AstExprIfElse>();
if (!ifElseExpr || ifElseExpr->condition->location.containsClosed(position)) if (!ifElseExpr || ifElseExpr->condition->location.containsClosed(position))
{ {
@ -1310,7 +1320,7 @@ static std::optional<AutocompleteEntryMap> autocompleteStringParams(const Source
return std::nullopt; return std::nullopt;
} }
if (!nodes.back()->is<AstExprConstantString>() && (!FFlag::LuauCompleteBrokenStringParams || !nodes.back()->is<AstExprError>())) if (!nodes.back()->is<AstExprConstantString>() && !nodes.back()->is<AstExprError>())
{ {
return std::nullopt; return std::nullopt;
} }
@ -1408,8 +1418,8 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
} }
else if (auto typeReference = node->as<AstTypeReference>()) else if (auto typeReference = node->as<AstTypeReference>())
{ {
if (typeReference->hasPrefix) if (typeReference->prefix)
return {autocompleteModuleTypes(*module, position, typeReference->prefix.value), finder.ancestry}; return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), finder.ancestry};
else else
return {autocompleteTypeNames(*module, position, finder.ancestry), finder.ancestry}; return {autocompleteTypeNames(*module, position, finder.ancestry), finder.ancestry};
} }
@ -1419,9 +1429,9 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
} }
else if (AstStatLocal* statLocal = node->as<AstStatLocal>()) else if (AstStatLocal* statLocal = node->as<AstStatLocal>())
{ {
if (statLocal->vars.size == 1 && (!statLocal->hasEqualsSign || position < statLocal->equalsSignLocation.begin)) if (statLocal->vars.size == 1 && (!statLocal->equalsSignLocation || position < statLocal->equalsSignLocation->begin))
return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry}; return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
else if (statLocal->hasEqualsSign && position >= statLocal->equalsSignLocation.end) else if (statLocal->equalsSignLocation && position >= statLocal->equalsSignLocation->end)
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry}; return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
else else
return {}; return {};
@ -1449,7 +1459,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (!statForIn->hasIn || position <= statForIn->inLocation.begin) if (!statForIn->hasIn || position <= statForIn->inLocation.begin)
{ {
AstLocal* lastName = statForIn->vars.data[statForIn->vars.size - 1]; AstLocal* lastName = statForIn->vars.data[statForIn->vars.size - 1];
if (lastName->name == Parser::errorName || lastName->location.containsClosed(position)) if (lastName->name == kParseNameError || lastName->location.containsClosed(position))
{ {
// Here we are either working with a missing binding (as would be the case in a bare "for" keyword) or // Here we are either working with a missing binding (as would be the case in a bare "for" keyword) or
// the cursor is still touching a binding name. The user is still typing a new name, so we should not offer // the cursor is still touching a binding name. The user is still typing a new name, so we should not offer
@ -1499,7 +1509,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
else if (AstStatWhile* statWhile = extractStat<AstStatWhile>(finder.ancestry); statWhile && !statWhile->hasDo) else if (AstStatWhile* statWhile = extractStat<AstStatWhile>(finder.ancestry); statWhile && !statWhile->hasDo)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry}; return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
else if (AstStatIf* statIf = node->as<AstStatIf>(); statIf && !statIf->hasElse) else if (AstStatIf* statIf = node->as<AstStatIf>(); statIf && !statIf->elseLocation.has_value())
{ {
return {{{"else", AutocompleteEntry{AutocompleteEntryKind::Keyword}}, {"elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, return {{{"else", AutocompleteEntry{AutocompleteEntryKind::Keyword}}, {"elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword}}},
finder.ancestry}; finder.ancestry};
@ -1508,11 +1518,11 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
{ {
if (statIf->condition->is<AstExprError>()) if (statIf->condition->is<AstExprError>())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry}; return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
else if (!statIf->hasThen || statIf->thenLocation.containsClosed(position)) else if (!statIf->thenLocation || statIf->thenLocation->containsClosed(position))
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry}; return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
} }
else if (AstStatIf* statIf = extractStat<AstStatIf>(finder.ancestry); else if (AstStatIf* statIf = extractStat<AstStatIf>(finder.ancestry);
statIf && (!statIf->hasThen || statIf->thenLocation.containsClosed(position))) statIf && (!statIf->thenLocation || statIf->thenLocation->containsClosed(position)))
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry}; return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
else if (AstStatRepeat* statRepeat = node->as<AstStatRepeat>(); statRepeat && statRepeat->condition->is<AstExprError>()) else if (AstStatRepeat* statRepeat = node->as<AstStatRepeat>(); statRepeat && statRepeat->condition->is<AstExprError>())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry}; return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
@ -1612,6 +1622,7 @@ AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName
OwningAutocompleteResult autocompleteSource(Frontend& frontend, std::string_view source, Position position, StringCompletionCallback callback) OwningAutocompleteResult autocompleteSource(Frontend& frontend, std::string_view source, Position position, StringCompletionCallback callback)
{ {
// TODO: Remove #include "Luau/Parser.h" with this function
auto sourceModule = std::make_unique<SourceModule>(); auto sourceModule = std::make_unique<SourceModule>();
ParseOptions parseOptions; ParseOptions parseOptions;
parseOptions.captureComments = true; parseOptions.captureComments = true;

View file

@ -1,7 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Config.h" #include "Luau/Config.h"
#include "Luau/Parser.h" #include "Luau/Lexer.h"
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
namespace namespace

View file

@ -167,7 +167,7 @@ declare function gcinfo(): number
foreach: <K, V>({[K]: V}, (K, V) -> ()) -> (), foreach: <K, V>({[K]: V}, (K, V) -> ()) -> (),
foreachi: <V>({V}, (number, V) -> ()) -> (), foreachi: <V>({V}, (number, V) -> ()) -> (),
move: <V>({V}, number, number, number, {V}?) -> (), move: <V>({V}, number, number, number, {V}?) -> {V},
clear: <K, V>({[K]: V}) -> (), clear: <K, V>({[K]: V}) -> (),
freeze: <K, V>({[K]: V}) -> {[K]: V}, freeze: <K, V>({[K]: V}) -> {[K]: V},

View file

@ -4,6 +4,7 @@
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/Config.h" #include "Luau/Config.h"
#include "Luau/FileResolver.h" #include "Luau/FileResolver.h"
#include "Luau/Parser.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
#include "Luau/TimeTrace.h" #include "Luau/TimeTrace.h"
@ -16,23 +17,25 @@
#include <stdexcept> #include <stdexcept>
LUAU_FASTFLAG(LuauInferInNoCheckMode) LUAU_FASTFLAG(LuauInferInNoCheckMode)
LUAU_FASTFLAGVARIABLE(LuauTypeCheckTwice, false)
LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false) LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
namespace Luau namespace Luau
{ {
std::optional<Mode> parseMode(const std::vector<std::string>& hotcomments) std::optional<Mode> parseMode(const std::vector<HotComment>& hotcomments)
{ {
for (const std::string& hc : hotcomments) for (const HotComment& hc : hotcomments)
{ {
if (hc == "nocheck") if (!hc.header)
continue;
if (hc.content == "nocheck")
return Mode::NoCheck; return Mode::NoCheck;
if (hc == "nonstrict") if (hc.content == "nonstrict")
return Mode::Nonstrict; return Mode::Nonstrict;
if (hc == "strict") if (hc.content == "strict")
return Mode::Strict; return Mode::Strict;
} }
@ -607,13 +610,15 @@ std::pair<SourceModule, LintResult> Frontend::lintFragment(std::string_view sour
SourceModule sourceModule = parse(ModuleName{}, source, config.parseOptions); SourceModule sourceModule = parse(ModuleName{}, source, config.parseOptions);
uint64_t ignoreLints = LintWarning::parseMask(sourceModule.hotcomments);
Luau::LintOptions lintOptions = enabledLintWarnings.value_or(config.enabledLint); Luau::LintOptions lintOptions = enabledLintWarnings.value_or(config.enabledLint);
lintOptions.warningMask &= sourceModule.ignoreLints; lintOptions.warningMask &= ~ignoreLints;
double timestamp = getTimestamp(); double timestamp = getTimestamp();
std::vector<LintWarning> warnings = std::vector<LintWarning> warnings = Luau::lint(sourceModule.root, *sourceModule.names.get(), typeChecker.globalScope, nullptr,
Luau::lint(sourceModule.root, *sourceModule.names.get(), typeChecker.globalScope, nullptr, enabledLintWarnings.value_or(config.enabledLint)); sourceModule.hotcomments, enabledLintWarnings.value_or(config.enabledLint));
stats.timeLint += getTimestamp() - timestamp; stats.timeLint += getTimestamp() - timestamp;
@ -651,8 +656,10 @@ LintResult Frontend::lint(const SourceModule& module, std::optional<Luau::LintOp
const Config& config = configResolver->getConfig(module.name); const Config& config = configResolver->getConfig(module.name);
uint64_t ignoreLints = LintWarning::parseMask(module.hotcomments);
LintOptions options = enabledLintWarnings.value_or(config.enabledLint); LintOptions options = enabledLintWarnings.value_or(config.enabledLint);
options.warningMask &= ~module.ignoreLints; options.warningMask &= ~ignoreLints;
Mode mode = module.mode.value_or(config.mode); Mode mode = module.mode.value_or(config.mode);
if (mode != Mode::NoCheck) if (mode != Mode::NoCheck)
@ -671,7 +678,7 @@ LintResult Frontend::lint(const SourceModule& module, std::optional<Luau::LintOp
double timestamp = getTimestamp(); double timestamp = getTimestamp();
std::vector<LintWarning> warnings = Luau::lint(module.root, *module.names, environmentScope, modulePtr.get(), options); std::vector<LintWarning> warnings = Luau::lint(module.root, *module.names, environmentScope, modulePtr.get(), module.hotcomments, options);
stats.timeLint += getTimestamp() - timestamp; stats.timeLint += getTimestamp() - timestamp;
@ -839,7 +846,6 @@ SourceModule Frontend::parse(const ModuleName& name, std::string_view src, const
{ {
sourceModule.root = parseResult.root; sourceModule.root = parseResult.root;
sourceModule.mode = parseMode(parseResult.hotcomments); sourceModule.mode = parseMode(parseResult.hotcomments);
sourceModule.ignoreLints = LintWarning::parseMask(parseResult.hotcomments);
} }
else else
{ {
@ -848,8 +854,13 @@ SourceModule Frontend::parse(const ModuleName& name, std::string_view src, const
} }
sourceModule.name = name; sourceModule.name = name;
if (parseOptions.captureComments) if (parseOptions.captureComments)
{
sourceModule.commentLocations = std::move(parseResult.commentLocations); sourceModule.commentLocations = std::move(parseResult.commentLocations);
sourceModule.hotcomments = std::move(parseResult.hotcomments);
}
return sourceModule; return sourceModule;
} }

View file

@ -158,6 +158,13 @@ struct AstJsonEncoder : public AstVisitor
{ {
writeString(str); writeString(str);
} }
void write(std::optional<AstName> name)
{
if (name)
write(*name);
else
writeRaw("null");
}
void write(AstName name) void write(AstName name)
{ {
writeString(name.value ? name.value : ""); writeString(name.value ? name.value : "");
@ -327,7 +334,7 @@ struct AstJsonEncoder : public AstVisitor
if (node->self) if (node->self)
PROP(self); PROP(self);
PROP(args); PROP(args);
if (node->hasReturnAnnotation) if (node->returnAnnotation)
PROP(returnAnnotation); PROP(returnAnnotation);
PROP(vararg); PROP(vararg);
PROP(varargLocation); PROP(varargLocation);
@ -341,6 +348,14 @@ struct AstJsonEncoder : public AstVisitor
}); });
} }
void write(const std::optional<AstTypeList>& typeList)
{
if (typeList)
write(*typeList);
else
writeRaw("null");
}
void write(const AstTypeList& typeList) void write(const AstTypeList& typeList)
{ {
writeRaw("{"); writeRaw("{");
@ -544,7 +559,7 @@ struct AstJsonEncoder : public AstVisitor
PROP(thenbody); PROP(thenbody);
if (node->elsebody) if (node->elsebody)
PROP(elsebody); PROP(elsebody);
PROP(hasThen); write("hasThen", node->thenLocation.has_value());
PROP(hasEnd); PROP(hasEnd);
}); });
} }
@ -728,7 +743,7 @@ struct AstJsonEncoder : public AstVisitor
void write(class AstTypeReference* node) void write(class AstTypeReference* node)
{ {
writeNode(node, "AstTypeReference", [&]() { writeNode(node, "AstTypeReference", [&]() {
if (node->hasPrefix) if (node->prefix)
PROP(prefix); PROP(prefix);
PROP(name); PROP(name);
PROP(parameters); PROP(parameters);

View file

@ -12,6 +12,8 @@
#include <math.h> #include <math.h>
#include <limits.h> #include <limits.h>
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
namespace Luau namespace Luau
{ {
@ -44,6 +46,7 @@ static const char* kWarningNames[] = {
"TableOperations", "TableOperations",
"DuplicateCondition", "DuplicateCondition",
"MisleadingAndOr", "MisleadingAndOr",
"CommentDirective",
}; };
// clang-format on // clang-format on
@ -732,13 +735,13 @@ private:
bool visit(AstTypeReference* node) override bool visit(AstTypeReference* node) override
{ {
if (!node->hasPrefix) if (!node->prefix)
return true; return true;
if (!imports.contains(node->prefix)) if (!imports.contains(*node->prefix))
return true; return true;
AstLocal* astLocal = imports[node->prefix]; AstLocal* astLocal = imports[*node->prefix];
Local& local = locals[astLocal]; Local& local = locals[astLocal];
LUAU_ASSERT(local.import); LUAU_ASSERT(local.import);
local.used = true; local.used = true;
@ -2527,13 +2530,108 @@ static void fillBuiltinGlobals(LintContext& context, const AstNameTable& names,
} }
} }
static const char* fuzzyMatch(std::string_view str, const char** array, size_t size)
{
if (FInt::LuauSuggestionDistance == 0)
return nullptr;
size_t bestDistance = FInt::LuauSuggestionDistance;
size_t bestMatch = size;
for (size_t i = 0; i < size; ++i)
{
size_t ed = editDistance(str, array[i]);
if (ed <= bestDistance)
{
bestDistance = ed;
bestMatch = i;
}
}
return bestMatch < size ? array[bestMatch] : nullptr;
}
static void lintComments(LintContext& context, const std::vector<HotComment>& hotcomments)
{
bool seenMode = false;
for (const HotComment& hc : hotcomments)
{
// We reserve --!<space> for various informational (non-directive) comments
if (hc.content.empty() || hc.content[0] == ' ' || hc.content[0] == '\t')
continue;
if (!hc.header)
{
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"Comment directive is ignored because it is placed after the first non-comment token");
}
else
{
std::string::size_type space = hc.content.find_first_of(" \t");
std::string_view first = std::string_view(hc.content).substr(0, space);
if (first == "nolint")
{
std::string::size_type notspace = hc.content.find_first_not_of(" \t", space);
if (space == std::string::npos || notspace == std::string::npos)
{
// disables all lints
}
else if (LintWarning::parseName(hc.content.c_str() + notspace) == LintWarning::Code_Unknown)
{
const char* rule = hc.content.c_str() + notspace;
// skip Unknown
if (const char* suggestion = fuzzyMatch(rule, kWarningNames + 1, LintWarning::Code__Count - 1))
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"nolint directive refers to unknown lint rule '%s'; did you mean '%s'?", rule, suggestion);
else
emitWarning(
context, LintWarning::Code_CommentDirective, hc.location, "nolint directive refers to unknown lint rule '%s'", rule);
}
}
else if (first == "nocheck" || first == "nonstrict" || first == "strict")
{
if (space != std::string::npos)
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"Comment directive with the type checking mode has extra symbols at the end of the line");
else if (seenMode)
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"Comment directive with the type checking mode has already been used");
else
seenMode = true;
}
else
{
static const char* kHotComments[] = {
"nolint",
"nocheck",
"nonstrict",
"strict",
};
if (const char* suggestion = fuzzyMatch(first, kHotComments, std::size(kHotComments)))
emitWarning(context, LintWarning::Code_CommentDirective, hc.location, "Unknown comment directive '%.*s'; did you mean '%s'?",
int(first.size()), first.data(), suggestion);
else
emitWarning(context, LintWarning::Code_CommentDirective, hc.location, "Unknown comment directive '%.*s'", int(first.size()),
first.data());
}
}
}
}
void LintOptions::setDefaults() void LintOptions::setDefaults()
{ {
// By default, we enable all warnings // By default, we enable all warnings
warningMask = ~0ull; warningMask = ~0ull;
} }
std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module, const LintOptions& options) std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module,
const std::vector<HotComment>& hotcomments, const LintOptions& options)
{ {
LintContext context; LintContext context;
@ -2609,6 +2707,9 @@ std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const Sc
if (context.warningEnabled(LintWarning::Code_MisleadingAndOr)) if (context.warningEnabled(LintWarning::Code_MisleadingAndOr))
LintMisleadingAndOr::process(context); LintMisleadingAndOr::process(context);
if (context.warningEnabled(LintWarning::Code_CommentDirective))
lintComments(context, hotcomments);
std::sort(context.result.begin(), context.result.end(), WarningComparator()); std::sort(context.result.begin(), context.result.end(), WarningComparator());
return context.result; return context.result;
@ -2630,23 +2731,30 @@ LintWarning::Code LintWarning::parseName(const char* name)
return Code_Unknown; return Code_Unknown;
} }
uint64_t LintWarning::parseMask(const std::vector<std::string>& hotcomments) uint64_t LintWarning::parseMask(const std::vector<HotComment>& hotcomments)
{ {
uint64_t result = 0; uint64_t result = 0;
for (const std::string& hc : hotcomments) for (const HotComment& hc : hotcomments)
{ {
if (hc.compare(0, 6, "nolint") != 0) if (!hc.header)
continue; continue;
std::string::size_type name = hc.find_first_not_of(" \t", 6); if (hc.content.compare(0, 6, "nolint") != 0)
continue;
std::string::size_type name = hc.content.find_first_not_of(" \t", 6);
// --!nolint disables everything // --!nolint disables everything
if (name == std::string::npos) if (name == std::string::npos)
return ~0ull; return ~0ull;
// --!nolint needs to be followed by a whitespace character
if (name == 6)
continue;
// --!nolint name disables the specific lint // --!nolint name disables the specific lint
LintWarning::Code code = LintWarning::parseName(hc.c_str() + name); LintWarning::Code code = LintWarning::parseName(hc.content.c_str() + name);
if (code != LintWarning::Code_Unknown) if (code != LintWarning::Code_Unknown)
result |= 1ull << int(code); result |= 1ull << int(code);

View file

@ -9,14 +9,12 @@ namespace Luau
struct Quantifier struct Quantifier
{ {
ModulePtr module;
TypeLevel level; TypeLevel level;
std::vector<TypeId> generics; std::vector<TypeId> generics;
std::vector<TypePackId> genericPacks; std::vector<TypePackId> genericPacks;
Quantifier(ModulePtr module, TypeLevel level) Quantifier(TypeLevel level)
: module(module) : level(level)
, level(level)
{ {
} }
@ -76,9 +74,9 @@ struct Quantifier
} }
}; };
void quantify(ModulePtr module, TypeId ty, TypeLevel level) void quantify(TypeId ty, TypeLevel level)
{ {
Quantifier q{std::move(module), level}; Quantifier q{level};
DenseHashSet<void*> seen{nullptr}; DenseHashSet<void*> seen{nullptr};
visitTypeVarOnce(ty, q, seen); visitTypeVarOnce(ty, q, seen);

View file

@ -226,27 +226,11 @@ TarjanResult Tarjan::loop()
return TarjanResult::Ok; return TarjanResult::Ok;
} }
void Tarjan::clear()
{
typeToIndex.clear();
indexToType.clear();
packToIndex.clear();
indexToPack.clear();
lowlink.clear();
stack.clear();
onStack.clear();
edgesTy.clear();
edgesTp.clear();
worklist.clear();
}
TarjanResult Tarjan::visitRoot(TypeId ty) TarjanResult Tarjan::visitRoot(TypeId ty)
{ {
childCount = 0; childCount = 0;
ty = log->follow(ty); ty = log->follow(ty);
clear();
auto [index, fresh] = indexify(ty); auto [index, fresh] = indexify(ty);
worklist.push_back({index, -1, -1}); worklist.push_back({index, -1, -1});
return loop(); return loop();
@ -257,7 +241,6 @@ TarjanResult Tarjan::visitRoot(TypePackId tp)
childCount = 0; childCount = 0;
tp = log->follow(tp); tp = log->follow(tp);
clear();
auto [index, fresh] = indexify(tp); auto [index, fresh] = indexify(tp);
worklist.push_back({index, -1, -1}); worklist.push_back({index, -1, -1});
return loop(); return loop();
@ -314,21 +297,17 @@ void FindDirty::visitSCC(int index)
TarjanResult FindDirty::findDirty(TypeId ty) TarjanResult FindDirty::findDirty(TypeId ty)
{ {
dirty.clear();
return visitRoot(ty); return visitRoot(ty);
} }
TarjanResult FindDirty::findDirty(TypePackId tp) TarjanResult FindDirty::findDirty(TypePackId tp)
{ {
dirty.clear();
return visitRoot(tp); return visitRoot(tp);
} }
std::optional<TypeId> Substitution::substitute(TypeId ty) std::optional<TypeId> Substitution::substitute(TypeId ty)
{ {
ty = log->follow(ty); ty = log->follow(ty);
newTypes.clear();
newPacks.clear();
auto result = findDirty(ty); auto result = findDirty(ty);
if (result != TarjanResult::Ok) if (result != TarjanResult::Ok)
@ -347,8 +326,6 @@ std::optional<TypeId> Substitution::substitute(TypeId ty)
std::optional<TypePackId> Substitution::substitute(TypePackId tp) std::optional<TypePackId> Substitution::substitute(TypePackId tp)
{ {
tp = log->follow(tp); tp = log->follow(tp);
newTypes.clear();
newPacks.clear();
auto result = findDirty(tp); auto result = findDirty(tp);
if (result != TarjanResult::Ok) if (result != TarjanResult::Ok)

View file

@ -26,9 +26,10 @@
* 3. Cyclic dependencies can be resolved by picking an arbitrary statement to check first. * 3. Cyclic dependencies can be resolved by picking an arbitrary statement to check first.
*/ */
#include "Luau/Parser.h" #include "Luau/Ast.h"
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/StringUtils.h"
#include <algorithm> #include <algorithm>
#include <deque> #include <deque>

View file

@ -933,12 +933,12 @@ struct Printer
writer.symbol(")"); writer.symbol(")");
if (writeTypes && func.hasReturnAnnotation) if (writeTypes && func.returnAnnotation)
{ {
writer.symbol(":"); writer.symbol(":");
writer.space(); writer.space();
visualizeTypeList(func.returnAnnotation, false); visualizeTypeList(*func.returnAnnotation, false);
} }
visualizeBlock(*func.body); visualizeBlock(*func.body);
@ -989,9 +989,9 @@ struct Printer
advance(typeAnnotation.location.begin); advance(typeAnnotation.location.begin);
if (const auto& a = typeAnnotation.as<AstTypeReference>()) if (const auto& a = typeAnnotation.as<AstTypeReference>())
{ {
if (a->hasPrefix) if (a->prefix)
{ {
writer.write(a->prefix.value); writer.write(a->prefix->value);
writer.symbol("."); writer.symbol(".");
} }

View file

@ -3,7 +3,6 @@
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/Module.h" #include "Luau/Module.h"
#include "Luau/Parser.h"
#include "Luau/RecursionCounter.h" #include "Luau/RecursionCounter.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
@ -476,12 +475,11 @@ public:
visitLocal(arg); visitLocal(arg);
} }
if (!fn->hasReturnAnnotation) if (!fn->returnAnnotation)
{ {
if (auto result = getScope(fn->body->location)) if (auto result = getScope(fn->body->location))
{ {
TypePackId ret = result->returnType; TypePackId ret = result->returnType;
fn->hasReturnAnnotation = true;
AstTypePack* variadicAnnotation = nullptr; AstTypePack* variadicAnnotation = nullptr;
const auto& [v, tail] = flatten(ret); const auto& [v, tail] = flatten(ret);

View file

@ -3,7 +3,6 @@
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/ModuleResolver.h" #include "Luau/ModuleResolver.h"
#include "Luau/Parser.h"
#include "Luau/Quantify.h" #include "Luau/Quantify.h"
#include "Luau/RecursionCounter.h" #include "Luau/RecursionCounter.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
@ -24,16 +23,12 @@ LUAU_FASTINTVARIABLE(LuauTypeInferTypePackLoopLimit, 5000)
LUAU_FASTINTVARIABLE(LuauCheckRecursionLimit, 500) LUAU_FASTINTVARIABLE(LuauCheckRecursionLimit, 500)
LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAG(LuauKnowsTheDataModel3)
LUAU_FASTFLAGVARIABLE(LuauEqConstraint, false) LUAU_FASTFLAGVARIABLE(LuauEqConstraint, false)
LUAU_FASTFLAGVARIABLE(LuauGroupExpectedType, false)
LUAU_FASTFLAGVARIABLE(LuauWeakEqConstraint, false) // Eventually removed as false. LUAU_FASTFLAGVARIABLE(LuauWeakEqConstraint, false) // Eventually removed as false.
LUAU_FASTFLAG(LuauUseCommittingTxnLog) LUAU_FASTFLAG(LuauUseCommittingTxnLog)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false) LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false)
LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false) LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseBranchTypeUnion, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseExpectedType2, false)
LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false) LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false)
LUAU_FASTFLAGVARIABLE(LuauLengthOnCompositeType, false)
LUAU_FASTFLAGVARIABLE(LuauNoSealedTypeMod, false) LUAU_FASTFLAGVARIABLE(LuauNoSealedTypeMod, false)
LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false) LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false)
LUAU_FASTFLAGVARIABLE(LuauSealExports, false) LUAU_FASTFLAGVARIABLE(LuauSealExports, false)
@ -43,7 +38,6 @@ LUAU_FASTFLAGVARIABLE(LuauTypeAliasDefaults, false)
LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false) LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false)
LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false) LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false)
LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false) LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false)
LUAU_FASTFLAGVARIABLE(LuauPerModuleUnificationCache, false)
LUAU_FASTFLAGVARIABLE(LuauProperTypeLevels, false) LUAU_FASTFLAGVARIABLE(LuauProperTypeLevels, false)
LUAU_FASTFLAGVARIABLE(LuauAscribeCorrectLevelToInferredProperitesOfFreeTables, false) LUAU_FASTFLAGVARIABLE(LuauAscribeCorrectLevelToInferredProperitesOfFreeTables, false)
LUAU_FASTFLAG(LuauUnionTagMatchFix) LUAU_FASTFLAG(LuauUnionTagMatchFix)
@ -293,13 +287,10 @@ ModulePtr TypeChecker::check(const SourceModule& module, Mode mode, std::optiona
GenericError{"Free types leaked into this module's public interface. This is an internal Luau error; please report it."}}); GenericError{"Free types leaked into this module's public interface. This is an internal Luau error; please report it."}});
} }
if (FFlag::LuauPerModuleUnificationCache) // Clear unifier cache since it's keyed off internal types that get deallocated
{ // This avoids fake cross-module cache hits and keeps cache size at bay when typechecking large module graphs.
// Clear unifier cache since it's keyed off internal types that get deallocated unifierState.cachedUnify.clear();
// This avoids fake cross-module cache hits and keeps cache size at bay when typechecking large module graphs. unifierState.skipCacheForType.clear();
unifierState.cachedUnify.clear();
unifierState.skipCacheForType.clear();
}
if (FFlag::LuauTwoPassAliasDefinitionFix) if (FFlag::LuauTwoPassAliasDefinitionFix)
duplicateTypeAliases.clear(); duplicateTypeAliases.clear();
@ -509,7 +500,7 @@ LUAU_NOINLINE void TypeChecker::checkBlockTypeAliases(const ScopePtr& scope, std
{ {
if (const auto& typealias = stat->as<AstStatTypeAlias>()) if (const auto& typealias = stat->as<AstStatTypeAlias>())
{ {
if (FFlag::LuauTwoPassAliasDefinitionFix && typealias->name == Parser::errorName) if (FFlag::LuauTwoPassAliasDefinitionFix && typealias->name == kParseNameError)
continue; continue;
auto& bindings = typealias->exported ? scope->exportedTypeBindings : scope->privateTypeBindings; auto& bindings = typealias->exported ? scope->exportedTypeBindings : scope->privateTypeBindings;
@ -1193,7 +1184,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
Name name = typealias.name.value; Name name = typealias.name.value;
// If the alias is missing a name, we can't do anything with it. Ignore it. // If the alias is missing a name, we can't do anything with it. Ignore it.
if (FFlag::LuauTwoPassAliasDefinitionFix && name == Parser::errorName) if (FFlag::LuauTwoPassAliasDefinitionFix && name == kParseNameError)
return; return;
std::optional<TypeFun> binding; std::optional<TypeFun> binding;
@ -1222,7 +1213,8 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
if (FFlag::LuauProperTypeLevels) if (FFlag::LuauProperTypeLevels)
aliasScope->level.subLevel = subLevel; aliasScope->level.subLevel = subLevel;
auto [generics, genericPacks] = createGenericTypes(aliasScope, scope->level, typealias, typealias.generics, typealias.genericPacks, /* useCache = */ true); auto [generics, genericPacks] =
createGenericTypes(aliasScope, scope->level, typealias, typealias.generics, typealias.genericPacks, /* useCache = */ true);
TypeId ty = freshType(aliasScope); TypeId ty = freshType(aliasScope);
FreeTypeVar* ftv = getMutable<FreeTypeVar>(ty); FreeTypeVar* ftv = getMutable<FreeTypeVar>(ty);
@ -1464,7 +1456,7 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExpr&
ExprResult<TypeId> result; ExprResult<TypeId> result;
if (auto a = expr.as<AstExprGroup>()) if (auto a = expr.as<AstExprGroup>())
result = checkExpr(scope, *a->expr, FFlag::LuauGroupExpectedType ? expectedType : std::nullopt); result = checkExpr(scope, *a->expr, expectedType);
else if (expr.is<AstExprConstantNil>()) else if (expr.is<AstExprConstantNil>())
result = {nilType}; result = {nilType};
else if (const AstExprConstantBool* bexpr = expr.as<AstExprConstantBool>()) else if (const AstExprConstantBool* bexpr = expr.as<AstExprConstantBool>())
@ -1508,7 +1500,7 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExpr&
else if (auto a = expr.as<AstExprError>()) else if (auto a = expr.as<AstExprError>())
result = checkExpr(scope, *a); result = checkExpr(scope, *a);
else if (auto a = expr.as<AstExprIfElse>()) else if (auto a = expr.as<AstExprIfElse>())
result = checkExpr(scope, *a, FFlag::LuauIfElseExpectedType2 ? expectedType : std::nullopt); result = checkExpr(scope, *a, expectedType);
else else
ice("Unhandled AstExpr?"); ice("Unhandled AstExpr?");
@ -2093,6 +2085,7 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprUn
return {numberType}; return {numberType};
} }
case AstExprUnary::Len: case AstExprUnary::Len:
{
tablify(operandType); tablify(operandType);
operandType = stripFromNilAndReport(operandType, expr.location); operandType = stripFromNilAndReport(operandType, expr.location);
@ -2100,30 +2093,13 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprUn
if (get<ErrorTypeVar>(operandType)) if (get<ErrorTypeVar>(operandType))
return {errorRecoveryType(scope)}; return {errorRecoveryType(scope)};
if (FFlag::LuauLengthOnCompositeType) DenseHashSet<TypeId> seen{nullptr};
{
DenseHashSet<TypeId> seen{nullptr};
if (!hasLength(operandType, seen, &recursionCount)) if (!hasLength(operandType, seen, &recursionCount))
reportError(TypeError{expr.location, NotATable{operandType}}); reportError(TypeError{expr.location, NotATable{operandType}});
}
else
{
if (get<AnyTypeVar>(operandType))
return {numberType}; // Not strictly correct: metatables permit overriding this
if (auto p = get<PrimitiveTypeVar>(operandType))
{
if (p->type == PrimitiveTypeVar::String)
return {numberType};
}
if (!getTableType(operandType))
reportError(TypeError{expr.location, NotATable{operandType}});
}
return {numberType}; return {numberType};
}
default: default:
ice("Unknown AstExprUnary " + std::to_string(int(expr.op))); ice("Unknown AstExprUnary " + std::to_string(int(expr.op)));
} }
@ -2618,22 +2594,11 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprIf
resolve(result.predicates, falseScope, false); resolve(result.predicates, falseScope, false);
ExprResult<TypeId> falseType = checkExpr(falseScope, *expr.falseExpr, expectedType); ExprResult<TypeId> falseType = checkExpr(falseScope, *expr.falseExpr, expectedType);
if (FFlag::LuauIfElseBranchTypeUnion) if (falseType.type == trueType.type)
{
if (falseType.type == trueType.type)
return {trueType.type};
std::vector<TypeId> types = reduceUnion({trueType.type, falseType.type});
return {types.size() == 1 ? types[0] : addType(UnionTypeVar{std::move(types)})};
}
else
{
unify(falseType.type, trueType.type, expr.location);
// TODO: normalize(UnionTypeVar{{trueType, falseType}})
// For now both trueType and falseType must be the same type.
return {trueType.type}; return {trueType.type};
}
std::vector<TypeId> types = reduceUnion({trueType.type, falseType.type});
return {types.size() == 1 ? types[0] : addType(UnionTypeVar{std::move(types)})};
} }
TypeId TypeChecker::checkLValue(const ScopePtr& scope, const AstExpr& expr) TypeId TypeChecker::checkLValue(const ScopePtr& scope, const AstExpr& expr)
@ -2986,8 +2951,8 @@ std::pair<TypeId, ScopePtr> TypeChecker::checkFunctionSignature(
auto [generics, genericPacks] = createGenericTypes(funScope, std::nullopt, expr, expr.generics, expr.genericPacks); auto [generics, genericPacks] = createGenericTypes(funScope, std::nullopt, expr, expr.generics, expr.genericPacks);
TypePackId retPack; TypePackId retPack;
if (expr.hasReturnAnnotation) if (expr.returnAnnotation)
retPack = resolveTypePack(funScope, expr.returnAnnotation); retPack = resolveTypePack(funScope, *expr.returnAnnotation);
else if (isNonstrictMode()) else if (isNonstrictMode())
retPack = anyTypePack; retPack = anyTypePack;
else if (expectedFunctionType) else if (expectedFunctionType)
@ -3181,7 +3146,7 @@ void TypeChecker::checkFunctionBody(const ScopePtr& scope, TypeId ty, const AstE
// If we're in nonstrict mode we want to only report this missing return // If we're in nonstrict mode we want to only report this missing return
// statement if there are type annotations on the function. In strict mode // statement if there are type annotations on the function. In strict mode
// we report it regardless. // we report it regardless.
if (!isNonstrictMode() || function.hasReturnAnnotation) if (!isNonstrictMode() || function.returnAnnotation)
{ {
reportError(getEndLocation(function), FunctionExitsWithoutReturning{funTy->retType}); reportError(getEndLocation(function), FunctionExitsWithoutReturning{funTy->retType});
} }
@ -4403,11 +4368,7 @@ TypeId Instantiation::clean(TypeId ty)
// Annoyingly, we have to do this even if there are no generics, // Annoyingly, we have to do this even if there are no generics,
// to replace any generic tables. // to replace any generic tables.
replaceGenerics.log = log; ReplaceGenerics replaceGenerics{log, arena, level, ftv->generics, ftv->genericPacks};
replaceGenerics.level = level;
replaceGenerics.currentModule = currentModule;
replaceGenerics.generics.assign(ftv->generics.begin(), ftv->generics.end());
replaceGenerics.genericPacks.assign(ftv->genericPacks.begin(), ftv->genericPacks.end());
// TODO: What to do if this returns nullopt? // TODO: What to do if this returns nullopt?
// We don't have access to the error-reporting machinery // We don't have access to the error-reporting machinery
@ -4573,16 +4534,11 @@ TypeId TypeChecker::quantify(const ScopePtr& scope, TypeId ty, Location location
if (FFlag::LuauQuantifyInPlace2) if (FFlag::LuauQuantifyInPlace2)
{ {
Luau::quantify(currentModule, ty, scope->level); Luau::quantify(ty, scope->level);
return ty; return ty;
} }
quantification.log = TxnLog::empty(); Quantification quantification{&currentModule->internalTypes, scope->level};
quantification.level = scope->level;
quantification.generics.clear();
quantification.genericPacks.clear();
quantification.currentModule = currentModule;
std::optional<TypeId> qty = quantification.substitute(ty); std::optional<TypeId> qty = quantification.substitute(ty);
if (!qty.has_value()) if (!qty.has_value())
@ -4596,18 +4552,14 @@ TypeId TypeChecker::quantify(const ScopePtr& scope, TypeId ty, Location location
FunctionTypeVar* qftv = getMutable<FunctionTypeVar>(*qty); FunctionTypeVar* qftv = getMutable<FunctionTypeVar>(*qty);
LUAU_ASSERT(qftv); LUAU_ASSERT(qftv);
qftv->generics = quantification.generics; qftv->generics = std::move(quantification.generics);
qftv->genericPacks = quantification.genericPacks; qftv->genericPacks = std::move(quantification.genericPacks);
return *qty; return *qty;
} }
TypeId TypeChecker::instantiate(const ScopePtr& scope, TypeId ty, Location location, const TxnLog* log) TypeId TypeChecker::instantiate(const ScopePtr& scope, TypeId ty, Location location, const TxnLog* log)
{ {
LUAU_ASSERT(log != nullptr); Instantiation instantiation{FFlag::LuauUseCommittingTxnLog ? log : TxnLog::empty(), &currentModule->internalTypes, scope->level};
instantiation.log = FFlag::LuauUseCommittingTxnLog ? log : TxnLog::empty();
instantiation.level = scope->level;
instantiation.currentModule = currentModule;
std::optional<TypeId> instantiated = instantiation.substitute(ty); std::optional<TypeId> instantiated = instantiation.substitute(ty);
if (instantiated.has_value()) if (instantiated.has_value())
return *instantiated; return *instantiated;
@ -4620,10 +4572,7 @@ TypeId TypeChecker::instantiate(const ScopePtr& scope, TypeId ty, Location locat
TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location) TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location)
{ {
anyification.log = TxnLog::empty(); Anyification anyification{&currentModule->internalTypes, anyType, anyTypePack};
anyification.anyType = anyType;
anyification.anyTypePack = anyTypePack;
anyification.currentModule = currentModule;
std::optional<TypeId> any = anyification.substitute(ty); std::optional<TypeId> any = anyification.substitute(ty);
if (any.has_value()) if (any.has_value())
return *any; return *any;
@ -4636,10 +4585,7 @@ TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location)
TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location location) TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location location)
{ {
anyification.log = TxnLog::empty(); Anyification anyification{&currentModule->internalTypes, anyType, anyTypePack};
anyification.anyType = anyType;
anyification.anyTypePack = anyTypePack;
anyification.currentModule = currentModule;
std::optional<TypePackId> any = anyification.substitute(ty); std::optional<TypePackId> any = anyification.substitute(ty);
if (any.has_value()) if (any.has_value())
return *any; return *any;
@ -4823,7 +4769,8 @@ TypePackId TypeChecker::errorRecoveryTypePack(TypePackId guess)
return getSingletonTypes().errorRecoveryTypePack(guess); return getSingletonTypes().errorRecoveryTypePack(guess);
} }
TypeIdPredicate TypeChecker::mkTruthyPredicate(bool sense) { TypeIdPredicate TypeChecker::mkTruthyPredicate(bool sense)
{
return [this, sense](TypeId ty) -> std::optional<TypeId> { return [this, sense](TypeId ty) -> std::optional<TypeId> {
// any/error/free gets a special pass unconditionally because they can't be decided. // any/error/free gets a special pass unconditionally because they can't be decided.
if (get<AnyTypeVar>(ty) || get<ErrorTypeVar>(ty) || get<FreeTypeVar>(ty)) if (get<AnyTypeVar>(ty) || get<ErrorTypeVar>(ty) || get<FreeTypeVar>(ty))
@ -4904,8 +4851,8 @@ TypeId TypeChecker::resolveType(const ScopePtr& scope, const AstType& annotation
if (const auto& lit = annotation.as<AstTypeReference>()) if (const auto& lit = annotation.as<AstTypeReference>())
{ {
std::optional<TypeFun> tf; std::optional<TypeFun> tf;
if (lit->hasPrefix) if (lit->prefix)
tf = scope->lookupImportedType(lit->prefix.value, lit->name.value); tf = scope->lookupImportedType(lit->prefix->value, lit->name.value);
else if (FFlag::DebugLuauMagicTypes && lit->name == "_luau_ice") else if (FFlag::DebugLuauMagicTypes && lit->name == "_luau_ice")
ice("_luau_ice encountered", lit->location); ice("_luau_ice encountered", lit->location);
@ -4932,12 +4879,12 @@ TypeId TypeChecker::resolveType(const ScopePtr& scope, const AstType& annotation
if (!tf) if (!tf)
{ {
if (lit->name == Parser::errorName) if (lit->name == kParseNameError)
return errorRecoveryType(scope); return errorRecoveryType(scope);
std::string typeName; std::string typeName;
if (lit->hasPrefix) if (lit->prefix)
typeName = std::string(lit->prefix.value) + "."; typeName = std::string(lit->prefix->value) + ".";
typeName += lit->name.value; typeName += lit->name.value;
if (scope->lookupPack(typeName)) if (scope->lookupPack(typeName))
@ -5038,12 +4985,7 @@ TypeId TypeChecker::resolveType(const ScopePtr& scope, const AstType& annotation
if (notEnoughParameters && hasDefaultParameters) if (notEnoughParameters && hasDefaultParameters)
{ {
// 'applyTypeFunction' is used to substitute default types that reference previous generic types // 'applyTypeFunction' is used to substitute default types that reference previous generic types
applyTypeFunction.log = TxnLog::empty(); ApplyTypeFunction applyTypeFunction{&currentModule->internalTypes, scope->level};
applyTypeFunction.typeArguments.clear();
applyTypeFunction.typePackArguments.clear();
applyTypeFunction.currentModule = currentModule;
applyTypeFunction.level = scope->level;
applyTypeFunction.encounteredForwardedType = false;
for (size_t i = 0; i < typesProvided; ++i) for (size_t i = 0; i < typesProvided; ++i)
applyTypeFunction.typeArguments[tf->typeParams[i].ty] = typeParams[i]; applyTypeFunction.typeArguments[tf->typeParams[i].ty] = typeParams[i];
@ -5362,18 +5304,14 @@ TypeId TypeChecker::instantiateTypeFun(const ScopePtr& scope, const TypeFun& tf,
if (tf.typeParams.empty() && tf.typePackParams.empty()) if (tf.typeParams.empty() && tf.typePackParams.empty())
return tf.type; return tf.type;
applyTypeFunction.typeArguments.clear(); ApplyTypeFunction applyTypeFunction{&currentModule->internalTypes, scope->level};
for (size_t i = 0; i < tf.typeParams.size(); ++i) for (size_t i = 0; i < tf.typeParams.size(); ++i)
applyTypeFunction.typeArguments[tf.typeParams[i].ty] = typeParams[i]; applyTypeFunction.typeArguments[tf.typeParams[i].ty] = typeParams[i];
applyTypeFunction.typePackArguments.clear();
for (size_t i = 0; i < tf.typePackParams.size(); ++i) for (size_t i = 0; i < tf.typePackParams.size(); ++i)
applyTypeFunction.typePackArguments[tf.typePackParams[i].tp] = typePackParams[i]; applyTypeFunction.typePackArguments[tf.typePackParams[i].tp] = typePackParams[i];
applyTypeFunction.log = TxnLog::empty();
applyTypeFunction.currentModule = currentModule;
applyTypeFunction.level = scope->level;
applyTypeFunction.encounteredForwardedType = false;
std::optional<TypeId> maybeInstantiated = applyTypeFunction.substitute(tf.type); std::optional<TypeId> maybeInstantiated = applyTypeFunction.substitute(tf.type);
if (!maybeInstantiated.has_value()) if (!maybeInstantiated.has_value())
{ {

View file

@ -5,6 +5,8 @@
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
LUAU_FASTFLAGVARIABLE(LuauTerminateCyclicMetatableIndexLookup, false)
namespace Luau namespace Luau
{ {
@ -48,9 +50,19 @@ std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, const Sc
} }
std::optional<TypeId> mtIndex = findMetatableEntry(errors, globalScope, ty, "__index", location); std::optional<TypeId> mtIndex = findMetatableEntry(errors, globalScope, ty, "__index", location);
int count = 0;
while (mtIndex) while (mtIndex)
{ {
TypeId index = follow(*mtIndex); TypeId index = follow(*mtIndex);
if (FFlag::LuauTerminateCyclicMetatableIndexLookup)
{
if (count >= 100)
return std::nullopt;
++count;
}
if (const auto& itt = getTableType(index)) if (const auto& itt = getTableType(index))
{ {
const auto& fit = itt->props.find(name); const auto& fit = itt->props.find(name);

View file

@ -23,8 +23,6 @@ LUAU_FASTFLAG(DebugLuauFreezeArena)
LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500) LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500)
LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0) LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0)
LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauLengthOnCompositeType)
LUAU_FASTFLAGVARIABLE(LuauMetatableAreEqualRecursion, false)
LUAU_FASTFLAGVARIABLE(LuauRefactorTypeVarQuestions, false) LUAU_FASTFLAGVARIABLE(LuauRefactorTypeVarQuestions, false)
LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAG(LuauUnionTagMatchFix) LUAU_FASTFLAG(LuauUnionTagMatchFix)
@ -385,8 +383,6 @@ bool maybeSingleton(TypeId ty)
bool hasLength(TypeId ty, DenseHashSet<TypeId>& seen, int* recursionCount) bool hasLength(TypeId ty, DenseHashSet<TypeId>& seen, int* recursionCount)
{ {
LUAU_ASSERT(FFlag::LuauLengthOnCompositeType);
RecursionLimiter _rl(recursionCount, FInt::LuauTypeInferRecursionLimit); RecursionLimiter _rl(recursionCount, FInt::LuauTypeInferRecursionLimit);
ty = follow(ty); ty = follow(ty);
@ -555,7 +551,7 @@ bool areEqual(SeenSet& seen, const TableTypeVar& lhs, const TableTypeVar& rhs)
static bool areEqual(SeenSet& seen, const MetatableTypeVar& lhs, const MetatableTypeVar& rhs) static bool areEqual(SeenSet& seen, const MetatableTypeVar& lhs, const MetatableTypeVar& rhs)
{ {
if (FFlag::LuauMetatableAreEqualRecursion && areSeen(seen, &lhs, &rhs)) if (areSeen(seen, &lhs, &rhs))
return true; return true;
return areEqual(seen, *lhs.table, *rhs.table) && areEqual(seen, *lhs.metatable, *rhs.metatable); return areEqual(seen, *lhs.table, *rhs.table) && areEqual(seen, *lhs.metatable, *rhs.metatable);

View file

@ -7,6 +7,9 @@
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#endif #endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h> #include <Windows.h>
const size_t kPageSize = 4096; const size_t kPageSize = 4096;

View file

@ -14,7 +14,6 @@
LUAU_FASTINT(LuauTypeInferRecursionLimit); LUAU_FASTINT(LuauTypeInferRecursionLimit);
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit); LUAU_FASTINT(LuauTypeInferTypePackLoopLimit);
LUAU_FASTFLAGVARIABLE(LuauCommittingTxnLogFreeTpPromote, false)
LUAU_FASTFLAG(LuauImmutableTypes) LUAU_FASTFLAG(LuauImmutableTypes)
LUAU_FASTFLAG(LuauUseCommittingTxnLog) LUAU_FASTFLAG(LuauUseCommittingTxnLog)
LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000); LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000);
@ -23,7 +22,6 @@ LUAU_FASTFLAGVARIABLE(LuauTableUnificationEarlyTest, false)
LUAU_FASTFLAG(LuauSingletonTypes) LUAU_FASTFLAG(LuauSingletonTypes)
LUAU_FASTFLAG(LuauErrorRecoveryType); LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauProperTypeLevels); LUAU_FASTFLAG(LuauProperTypeLevels);
LUAU_FASTFLAGVARIABLE(LuauUnifyPackTails, false)
LUAU_FASTFLAGVARIABLE(LuauUnionTagMatchFix, false) LUAU_FASTFLAGVARIABLE(LuauUnionTagMatchFix, false)
LUAU_FASTFLAGVARIABLE(LuauFollowWithCommittingTxnLogInAnyUnification, false) LUAU_FASTFLAGVARIABLE(LuauFollowWithCommittingTxnLogInAnyUnification, false)
@ -116,7 +114,7 @@ struct PromoteTypeLevels
{ {
// Surprise, it's actually a BoundTypePack that hasn't been committed yet. // Surprise, it's actually a BoundTypePack that hasn't been committed yet.
// Calling getMutable on this will trigger an assertion. // Calling getMutable on this will trigger an assertion.
if (FFlag::LuauCommittingTxnLogFreeTpPromote && FFlag::LuauUseCommittingTxnLog && !log.is<FreeTypePack>(tp)) if (FFlag::LuauUseCommittingTxnLog && !log.is<FreeTypePack>(tp))
return true; return true;
promote(tp, FFlag::LuauUseCommittingTxnLog ? log.getMutable<FreeTypePack>(tp) : getMutable<FreeTypePack>(tp)); promote(tp, FFlag::LuauUseCommittingTxnLog ? log.getMutable<FreeTypePack>(tp) : getMutable<FreeTypePack>(tp));
@ -1242,7 +1240,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
// If both are at the end, we're done // If both are at the end, we're done
if (!superIter.good() && !subIter.good()) if (!superIter.good() && !subIter.good())
{ {
if (FFlag::LuauUnifyPackTails && subTpv->tail && superTpv->tail) if (subTpv->tail && superTpv->tail)
{ {
tryUnify_(*subTpv->tail, *superTpv->tail); tryUnify_(*subTpv->tail, *superTpv->tail);
break; break;
@ -1250,9 +1248,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
const bool lFreeTail = superTpv->tail && log.getMutable<FreeTypePack>(log.follow(*superTpv->tail)) != nullptr; const bool lFreeTail = superTpv->tail && log.getMutable<FreeTypePack>(log.follow(*superTpv->tail)) != nullptr;
const bool rFreeTail = subTpv->tail && log.getMutable<FreeTypePack>(log.follow(*subTpv->tail)) != nullptr; const bool rFreeTail = subTpv->tail && log.getMutable<FreeTypePack>(log.follow(*subTpv->tail)) != nullptr;
if (!FFlag::LuauUnifyPackTails && lFreeTail && rFreeTail) if (lFreeTail)
tryUnify_(*subTpv->tail, *superTpv->tail);
else if (lFreeTail)
tryUnify_(emptyTp, *superTpv->tail); tryUnify_(emptyTp, *superTpv->tail);
else if (rFreeTail) else if (rFreeTail)
tryUnify_(emptyTp, *subTpv->tail); tryUnify_(emptyTp, *subTpv->tail);
@ -1448,7 +1444,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
// If both are at the end, we're done // If both are at the end, we're done
if (!superIter.good() && !subIter.good()) if (!superIter.good() && !subIter.good())
{ {
if (FFlag::LuauUnifyPackTails && subTpv->tail && superTpv->tail) if (subTpv->tail && superTpv->tail)
{ {
tryUnify_(*subTpv->tail, *superTpv->tail); tryUnify_(*subTpv->tail, *superTpv->tail);
break; break;
@ -1456,9 +1452,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
const bool lFreeTail = superTpv->tail && get<FreeTypePack>(follow(*superTpv->tail)) != nullptr; const bool lFreeTail = superTpv->tail && get<FreeTypePack>(follow(*superTpv->tail)) != nullptr;
const bool rFreeTail = subTpv->tail && get<FreeTypePack>(follow(*subTpv->tail)) != nullptr; const bool rFreeTail = subTpv->tail && get<FreeTypePack>(follow(*subTpv->tail)) != nullptr;
if (!FFlag::LuauUnifyPackTails && lFreeTail && rFreeTail) if (lFreeTail)
tryUnify_(*subTpv->tail, *superTpv->tail);
else if (lFreeTail)
tryUnify_(emptyTp, *superTpv->tail); tryUnify_(emptyTp, *superTpv->tail);
else if (rFreeTail) else if (rFreeTail)
tryUnify_(emptyTp, *subTpv->tail); tryUnify_(emptyTp, *subTpv->tail);

View file

@ -594,8 +594,7 @@ public:
AstArray<AstGenericTypePack> genericPacks; AstArray<AstGenericTypePack> genericPacks;
AstLocal* self; AstLocal* self;
AstArray<AstLocal*> args; AstArray<AstLocal*> args;
bool hasReturnAnnotation; std::optional<AstTypeList> returnAnnotation;
AstTypeList returnAnnotation;
bool vararg = false; bool vararg = false;
Location varargLocation; Location varargLocation;
AstTypePack* varargAnnotation; AstTypePack* varargAnnotation;
@ -740,7 +739,7 @@ class AstStatIf : public AstStat
public: public:
LUAU_RTTI(AstStatIf) LUAU_RTTI(AstStatIf)
AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody, bool hasThen, const Location& thenLocation, AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody, const std::optional<Location>& thenLocation,
const std::optional<Location>& elseLocation, bool hasEnd); const std::optional<Location>& elseLocation, bool hasEnd);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
@ -749,12 +748,10 @@ public:
AstStatBlock* thenbody; AstStatBlock* thenbody;
AstStat* elsebody; AstStat* elsebody;
bool hasThen = false; std::optional<Location> thenLocation;
Location thenLocation;
// Active for 'elseif' as well // Active for 'elseif' as well
bool hasElse = false; std::optional<Location> elseLocation;
Location elseLocation;
bool hasEnd = false; bool hasEnd = false;
}; };
@ -849,8 +846,7 @@ public:
AstArray<AstLocal*> vars; AstArray<AstLocal*> vars;
AstArray<AstExpr*> values; AstArray<AstExpr*> values;
bool hasEqualsSign = false; std::optional<Location> equalsSignLocation;
Location equalsSignLocation;
}; };
class AstStatFor : public AstStat class AstStatFor : public AstStat
@ -1053,9 +1049,8 @@ public:
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
bool hasPrefix;
bool hasParameterList; bool hasParameterList;
AstName prefix; std::optional<AstName> prefix;
AstName name; AstName name;
AstArray<AstTypeOrPack> parameters; AstArray<AstTypeOrPack> parameters;
}; };

View file

@ -233,4 +233,9 @@ private:
bool readNames; bool readNames;
}; };
inline bool isSpace(char ch)
{
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\v' || ch == '\f';
}
} // namespace Luau } // namespace Luau

View file

@ -0,0 +1,69 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Common.h"
#include "Luau/Location.h"
#include "Luau/Lexer.h"
#include "Luau/StringUtils.h"
namespace Luau
{
class AstStatBlock;
class ParseError : public std::exception
{
public:
ParseError(const Location& location, const std::string& message);
virtual const char* what() const throw();
const Location& getLocation() const;
const std::string& getMessage() const;
static LUAU_NORETURN void raise(const Location& location, const char* format, ...) LUAU_PRINTF_ATTR(2, 3);
private:
Location location;
std::string message;
};
class ParseErrors : public std::exception
{
public:
ParseErrors(std::vector<ParseError> errors);
virtual const char* what() const throw();
const std::vector<ParseError>& getErrors() const;
private:
std::vector<ParseError> errors;
std::string message;
};
struct HotComment
{
bool header;
Location location;
std::string content;
};
struct Comment
{
Lexeme::Type type; // Comment, BlockComment, or BrokenComment
Location location;
};
struct ParseResult
{
AstStatBlock* root;
std::vector<HotComment> hotcomments;
std::vector<ParseError> errors;
std::vector<Comment> commentLocations;
};
static constexpr const char* kParseNameError = "%error-id%";
} // namespace Luau

View file

@ -4,6 +4,7 @@
#include "Luau/Ast.h" #include "Luau/Ast.h"
#include "Luau/Lexer.h" #include "Luau/Lexer.h"
#include "Luau/ParseOptions.h" #include "Luau/ParseOptions.h"
#include "Luau/ParseResult.h"
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/Common.h" #include "Luau/Common.h"
@ -14,37 +15,6 @@
namespace Luau namespace Luau
{ {
class ParseError : public std::exception
{
public:
ParseError(const Location& location, const std::string& message);
virtual const char* what() const throw();
const Location& getLocation() const;
const std::string& getMessage() const;
static LUAU_NORETURN void raise(const Location& location, const char* format, ...) LUAU_PRINTF_ATTR(2, 3);
private:
Location location;
std::string message;
};
class ParseErrors : public std::exception
{
public:
ParseErrors(std::vector<ParseError> errors);
virtual const char* what() const throw();
const std::vector<ParseError>& getErrors() const;
private:
std::vector<ParseError> errors;
std::string message;
};
template<typename T> template<typename T>
class TempVector class TempVector
{ {
@ -80,34 +50,17 @@ private:
size_t size_; size_t size_;
}; };
struct Comment
{
Lexeme::Type type; // Comment, BlockComment, or BrokenComment
Location location;
};
struct ParseResult
{
AstStatBlock* root;
std::vector<std::string> hotcomments;
std::vector<ParseError> errors;
std::vector<Comment> commentLocations;
};
class Parser class Parser
{ {
public: public:
static ParseResult parse( static ParseResult parse(
const char* buffer, std::size_t bufferSize, AstNameTable& names, Allocator& allocator, ParseOptions options = ParseOptions()); const char* buffer, std::size_t bufferSize, AstNameTable& names, Allocator& allocator, ParseOptions options = ParseOptions());
static constexpr const char* errorName = "%error-id%";
private: private:
struct Name; struct Name;
struct Binding; struct Binding;
Parser(const char* buffer, std::size_t bufferSize, AstNameTable& names, Allocator& allocator); Parser(const char* buffer, std::size_t bufferSize, AstNameTable& names, Allocator& allocator, const ParseOptions& options);
bool blockFollow(const Lexeme& l); bool blockFollow(const Lexeme& l);
@ -330,7 +283,7 @@ private:
AstTypeError* reportTypeAnnotationError(const Location& location, const AstArray<AstType*>& types, bool isMissing, const char* format, ...) AstTypeError* reportTypeAnnotationError(const Location& location, const AstArray<AstType*>& types, bool isMissing, const char* format, ...)
LUAU_PRINTF_ATTR(5, 6); LUAU_PRINTF_ATTR(5, 6);
const Lexeme& nextLexeme(); void nextLexeme();
struct Function struct Function
{ {
@ -386,6 +339,9 @@ private:
Allocator& allocator; Allocator& allocator;
std::vector<Comment> commentLocations; std::vector<Comment> commentLocations;
std::vector<HotComment> hotcomments;
bool hotcommentHeader = true;
unsigned int recursionCounter; unsigned int recursionCounter;

View file

@ -167,8 +167,7 @@ AstExprFunction::AstExprFunction(const Location& location, const AstArray<AstGen
, genericPacks(genericPacks) , genericPacks(genericPacks)
, self(self) , self(self)
, args(args) , args(args)
, hasReturnAnnotation(returnAnnotation.has_value()) , returnAnnotation(returnAnnotation)
, returnAnnotation()
, vararg(vararg.has_value()) , vararg(vararg.has_value())
, varargLocation(vararg.value_or(Location())) , varargLocation(vararg.value_or(Location()))
, varargAnnotation(varargAnnotation) , varargAnnotation(varargAnnotation)
@ -178,8 +177,6 @@ AstExprFunction::AstExprFunction(const Location& location, const AstArray<AstGen
, hasEnd(hasEnd) , hasEnd(hasEnd)
, argLocation(argLocation) , argLocation(argLocation)
{ {
if (returnAnnotation.has_value())
this->returnAnnotation = *returnAnnotation;
} }
void AstExprFunction::visit(AstVisitor* visitor) void AstExprFunction::visit(AstVisitor* visitor)
@ -195,8 +192,8 @@ void AstExprFunction::visit(AstVisitor* visitor)
if (varargAnnotation) if (varargAnnotation)
varargAnnotation->visit(visitor); varargAnnotation->visit(visitor);
if (hasReturnAnnotation) if (returnAnnotation)
visitTypeList(visitor, returnAnnotation); visitTypeList(visitor, *returnAnnotation);
body->visit(visitor); body->visit(visitor);
} }
@ -375,21 +372,16 @@ void AstStatBlock::visit(AstVisitor* visitor)
} }
} }
AstStatIf::AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody, bool hasThen, AstStatIf::AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody,
const Location& thenLocation, const std::optional<Location>& elseLocation, bool hasEnd) const std::optional<Location>& thenLocation, const std::optional<Location>& elseLocation, bool hasEnd)
: AstStat(ClassIndex(), location) : AstStat(ClassIndex(), location)
, condition(condition) , condition(condition)
, thenbody(thenbody) , thenbody(thenbody)
, elsebody(elsebody) , elsebody(elsebody)
, hasThen(hasThen)
, thenLocation(thenLocation) , thenLocation(thenLocation)
, elseLocation(elseLocation)
, hasEnd(hasEnd) , hasEnd(hasEnd)
{ {
if (bool(elseLocation))
{
hasElse = true;
this->elseLocation = *elseLocation;
}
} }
void AstStatIf::visit(AstVisitor* visitor) void AstStatIf::visit(AstVisitor* visitor)
@ -492,12 +484,8 @@ AstStatLocal::AstStatLocal(
: AstStat(ClassIndex(), location) : AstStat(ClassIndex(), location)
, vars(vars) , vars(vars)
, values(values) , values(values)
, equalsSignLocation(equalsSignLocation)
{ {
if (bool(equalsSignLocation))
{
hasEqualsSign = true;
this->equalsSignLocation = *equalsSignLocation;
}
} }
void AstStatLocal::visit(AstVisitor* visitor) void AstStatLocal::visit(AstVisitor* visitor)
@ -750,9 +738,8 @@ void AstStatError::visit(AstVisitor* visitor)
AstTypeReference::AstTypeReference( AstTypeReference::AstTypeReference(
const Location& location, std::optional<AstName> prefix, AstName name, bool hasParameterList, const AstArray<AstTypeOrPack>& parameters) const Location& location, std::optional<AstName> prefix, AstName name, bool hasParameterList, const AstArray<AstTypeOrPack>& parameters)
: AstType(ClassIndex(), location) : AstType(ClassIndex(), location)
, hasPrefix(bool(prefix))
, hasParameterList(hasParameterList) , hasParameterList(hasParameterList)
, prefix(prefix ? *prefix : AstName()) , prefix(prefix)
, name(name) , name(name)
, parameters(parameters) , parameters(parameters)
{ {

View file

@ -101,11 +101,6 @@ Lexeme::Lexeme(const Location& location, Type type, const char* name)
LUAU_ASSERT(type == Name || (type >= Reserved_BEGIN && type < Lexeme::Reserved_END)); LUAU_ASSERT(type == Name || (type >= Reserved_BEGIN && type < Lexeme::Reserved_END));
} }
static bool isComment(const Lexeme& lexeme)
{
return lexeme.type == Lexeme::Comment || lexeme.type == Lexeme::BlockComment;
}
static const char* kReserved[] = {"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", static const char* kReserved[] = {"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or",
"repeat", "return", "then", "true", "until", "while"}; "repeat", "return", "then", "true", "until", "while"};
@ -282,11 +277,6 @@ AstName AstNameTable::get(const char* name) const
return getWithType(name, strlen(name)).first; return getWithType(name, strlen(name)).first;
} }
inline bool isSpace(char ch)
{
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\v' || ch == '\f';
}
inline bool isAlpha(char ch) inline bool isAlpha(char ch)
{ {
// use or trick to convert to lower case and unsigned comparison to do range check // use or trick to convert to lower case and unsigned comparison to do range check
@ -372,7 +362,7 @@ const Lexeme& Lexer::next(bool skipComments)
prevLocation = lexeme.location; prevLocation = lexeme.location;
lexeme = readNext(); lexeme = readNext();
} while (skipComments && isComment(lexeme)); } while (skipComments && (lexeme.type == Lexeme::Comment || lexeme.type == Lexeme::BlockComment));
return lexeme; return lexeme;
} }

View file

@ -13,18 +13,15 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParseSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauParseSingletonTypes, false)
LUAU_FASTFLAGVARIABLE(LuauParseTypeAliasDefaults, false) LUAU_FASTFLAGVARIABLE(LuauParseTypeAliasDefaults, false)
LUAU_FASTFLAGVARIABLE(LuauParseRecoverTypePackEllipsis, false) LUAU_FASTFLAGVARIABLE(LuauParseRecoverTypePackEllipsis, false)
LUAU_FASTFLAGVARIABLE(LuauStartingBrokenComment, false) LUAU_FASTFLAGVARIABLE(LuauParseAllHotComments, false)
LUAU_FASTFLAGVARIABLE(LuauTableFieldFunctionDebugname, false)
namespace Luau namespace Luau
{ {
inline bool isSpace(char ch)
{
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\v' || ch == '\f';
}
static bool isComment(const Lexeme& lexeme) static bool isComment(const Lexeme& lexeme)
{ {
LUAU_ASSERT(!FFlag::LuauParseAllHotComments);
return lexeme.type == Lexeme::Comment || lexeme.type == Lexeme::BlockComment; return lexeme.type == Lexeme::Comment || lexeme.type == Lexeme::BlockComment;
} }
@ -151,31 +148,37 @@ ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& n
{ {
LUAU_TIMETRACE_SCOPE("Parser::parse", "Parser"); LUAU_TIMETRACE_SCOPE("Parser::parse", "Parser");
Parser p(buffer, bufferSize, names, allocator); Parser p(buffer, bufferSize, names, allocator, FFlag::LuauParseAllHotComments ? options : ParseOptions());
try try
{ {
std::vector<std::string> hotcomments; if (FFlag::LuauParseAllHotComments)
while (isComment(p.lexer.current()) || p.lexer.current().type == Lexeme::BrokenComment)
{ {
const char* text = p.lexer.current().data; AstStatBlock* root = p.parseChunk();
unsigned int length = p.lexer.current().length;
if (length && text[0] == '!') return ParseResult{root, std::move(p.hotcomments), std::move(p.parseErrors), std::move(p.commentLocations)};
}
else
{
std::vector<HotComment> hotcomments;
while (isComment(p.lexer.current()) || p.lexer.current().type == Lexeme::BrokenComment)
{ {
unsigned int end = length; const char* text = p.lexer.current().data;
while (end > 0 && isSpace(text[end - 1])) unsigned int length = p.lexer.current().length;
--end;
hotcomments.push_back(std::string(text + 1, text + end)); if (length && text[0] == '!')
} {
unsigned int end = length;
while (end > 0 && isSpace(text[end - 1]))
--end;
const Lexeme::Type type = p.lexer.current().type; hotcomments.push_back({true, p.lexer.current().location, std::string(text + 1, text + end)});
const Location loc = p.lexer.current().location; }
const Lexeme::Type type = p.lexer.current().type;
const Location loc = p.lexer.current().location;
if (FFlag::LuauStartingBrokenComment)
{
if (options.captureComments) if (options.captureComments)
p.commentLocations.push_back(Comment{type, loc}); p.commentLocations.push_back(Comment{type, loc});
@ -184,22 +187,15 @@ ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& n
p.lexer.next(); p.lexer.next();
} }
else
{
p.lexer.next();
if (options.captureComments) p.lexer.setSkipComments(true);
p.commentLocations.push_back(Comment{type, loc});
} p.options = options;
AstStatBlock* root = p.parseChunk();
return ParseResult{root, hotcomments, p.parseErrors, std::move(p.commentLocations)};
} }
p.lexer.setSkipComments(true);
p.options = options;
AstStatBlock* root = p.parseChunk();
return ParseResult{root, hotcomments, p.parseErrors, std::move(p.commentLocations)};
} }
catch (ParseError& err) catch (ParseError& err)
{ {
@ -210,8 +206,9 @@ ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& n
} }
} }
Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Allocator& allocator) Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Allocator& allocator, const ParseOptions& options)
: lexer(buffer, bufferSize, names) : options(options)
, lexer(buffer, bufferSize, names)
, allocator(allocator) , allocator(allocator)
, recursionCounter(0) , recursionCounter(0)
, endMismatchSuspect(Location(), Lexeme::Eof) , endMismatchSuspect(Location(), Lexeme::Eof)
@ -224,14 +221,20 @@ Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Alloc
nameSelf = names.addStatic("self"); nameSelf = names.addStatic("self");
nameNumber = names.addStatic("number"); nameNumber = names.addStatic("number");
nameError = names.addStatic(errorName); nameError = names.addStatic(kParseNameError);
nameNil = names.getOrAdd("nil"); // nil is a reserved keyword nameNil = names.getOrAdd("nil"); // nil is a reserved keyword
matchRecoveryStopOnToken.assign(Lexeme::Type::Reserved_END, 0); matchRecoveryStopOnToken.assign(Lexeme::Type::Reserved_END, 0);
matchRecoveryStopOnToken[Lexeme::Type::Eof] = 1; matchRecoveryStopOnToken[Lexeme::Type::Eof] = 1;
if (FFlag::LuauParseAllHotComments)
lexer.setSkipComments(true);
// read first lexeme // read first lexeme
nextLexeme(); nextLexeme();
// all hot comments parsed after the first non-comment lexeme are special in that they don't affect type checking / linting mode
hotcommentHeader = false;
} }
bool Parser::blockFollow(const Lexeme& l) bool Parser::blockFollow(const Lexeme& l)
@ -396,7 +399,9 @@ AstStat* Parser::parseIf()
AstExpr* cond = parseExpr(); AstExpr* cond = parseExpr();
Lexeme matchThen = lexer.current(); Lexeme matchThen = lexer.current();
bool hasThen = expectAndConsume(Lexeme::ReservedThen, "if statement"); std::optional<Location> thenLocation;
if (expectAndConsume(Lexeme::ReservedThen, "if statement"))
thenLocation = matchThen.location;
AstStatBlock* thenbody = parseBlock(); AstStatBlock* thenbody = parseBlock();
@ -434,7 +439,7 @@ AstStat* Parser::parseIf()
hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchThenElse); hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchThenElse);
} }
return allocator.alloc<AstStatIf>(Location(start, end), cond, thenbody, elsebody, hasThen, matchThen.location, elseLocation, hasEnd); return allocator.alloc<AstStatIf>(Location(start, end), cond, thenbody, elsebody, thenLocation, elseLocation, hasEnd);
} }
// while exp do block end // while exp do block end
@ -769,7 +774,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported)
{ {
// note: `type` token is already parsed for us, so we just need to parse the rest // note: `type` token is already parsed for us, so we just need to parse the rest
auto name = parseNameOpt("type name"); std::optional<Name> name = parseNameOpt("type name");
// Use error name if the name is missing // Use error name if the name is missing
if (!name) if (!name)
@ -925,7 +930,7 @@ AstStat* Parser::parseDeclaration(const Location& start)
return allocator.alloc<AstStatDeclareClass>(Location(classStart, classEnd), className.name, superName, copy(props)); return allocator.alloc<AstStatDeclareClass>(Location(classStart, classEnd), className.name, superName, copy(props));
} }
else if (auto globalName = parseNameOpt("global variable name")) else if (std::optional<Name> globalName = parseNameOpt("global variable name"))
{ {
expectAndConsume(':', "global variable declaration"); expectAndConsume(':', "global variable declaration");
@ -1066,7 +1071,7 @@ void Parser::parseExprList(TempVector<AstExpr*>& result)
Parser::Binding Parser::parseBinding() Parser::Binding Parser::parseBinding()
{ {
auto name = parseNameOpt("variable name"); std::optional<Name> name = parseNameOpt("variable name");
// Use placeholder if the name is missing // Use placeholder if the name is missing
if (!name) if (!name)
@ -1325,7 +1330,7 @@ AstType* Parser::parseTableTypeAnnotation()
} }
else else
{ {
auto name = parseNameOpt("table field"); std::optional<Name> name = parseNameOpt("table field");
if (!name) if (!name)
break; break;
@ -1422,7 +1427,7 @@ AstType* Parser::parseFunctionTypeAnnotationTail(const Lexeme& begin, AstArray<A
expectAndConsume(Lexeme::SkinnyArrow, "function type"); expectAndConsume(Lexeme::SkinnyArrow, "function type");
} }
const auto [endLocation, returnTypeList] = parseReturnTypeAnnotation(); auto [endLocation, returnTypeList] = parseReturnTypeAnnotation();
AstTypeList paramTypes = AstTypeList{params, varargAnnotation}; AstTypeList paramTypes = AstTypeList{params, varargAnnotation};
return allocator.alloc<AstTypeFunction>(Location(begin.location, endLocation), generics, genericPacks, paramTypes, paramNames, returnTypeList); return allocator.alloc<AstTypeFunction>(Location(begin.location, endLocation), generics, genericPacks, paramTypes, paramNames, returnTypeList);
@ -1869,7 +1874,7 @@ AstExpr* Parser::parseExpr(unsigned int limit)
// NAME // NAME
AstExpr* Parser::parseNameExpr(const char* context) AstExpr* Parser::parseNameExpr(const char* context)
{ {
auto name = parseNameOpt(context); std::optional<Name> name = parseNameOpt(context);
if (!name) if (!name)
return allocator.alloc<AstExprError>(lexer.current().location, copy<AstExpr*>({}), unsigned(parseErrors.size() - 1)); return allocator.alloc<AstExprError>(lexer.current().location, copy<AstExpr*>({}), unsigned(parseErrors.size() - 1));
@ -2233,6 +2238,12 @@ AstExpr* Parser::parseTableConstructor()
AstExpr* key = allocator.alloc<AstExprConstantString>(name.location, nameString); AstExpr* key = allocator.alloc<AstExprConstantString>(name.location, nameString);
AstExpr* value = parseExpr(); AstExpr* value = parseExpr();
if (FFlag::LuauTableFieldFunctionDebugname)
{
if (AstExprFunction* func = value->as<AstExprFunction>())
func->debugname = name.name;
}
items.push_back({AstExprTable::Item::Record, key, value}); items.push_back({AstExprTable::Item::Record, key, value});
} }
else else
@ -2313,7 +2324,7 @@ std::optional<Parser::Name> Parser::parseNameOpt(const char* context)
Parser::Name Parser::parseName(const char* context) Parser::Name Parser::parseName(const char* context)
{ {
if (auto name = parseNameOpt(context)) if (std::optional<Name> name = parseNameOpt(context))
return *name; return *name;
Location location = lexer.current().location; Location location = lexer.current().location;
@ -2324,7 +2335,7 @@ Parser::Name Parser::parseName(const char* context)
Parser::Name Parser::parseIndexName(const char* context, const Position& previous) Parser::Name Parser::parseIndexName(const char* context, const Position& previous)
{ {
if (auto name = parseNameOpt(context)) if (std::optional<Name> name = parseNameOpt(context))
return *name; return *name;
// If we have a reserved keyword next at the same line, assume it's an incomplete name // If we have a reserved keyword next at the same line, assume it's an incomplete name
@ -2379,7 +2390,7 @@ std::pair<AstArray<AstGenericType>, AstArray<AstGenericTypePack>> Parser::parseG
if (shouldParseTypePackAnnotation(lexer)) if (shouldParseTypePackAnnotation(lexer))
{ {
auto typePack = parseTypePackAnnotation(); AstTypePack* typePack = parseTypePackAnnotation();
namePacks.push_back({name, nameLocation, typePack}); namePacks.push_back({name, nameLocation, typePack});
} }
@ -2451,7 +2462,7 @@ AstArray<AstTypeOrPack> Parser::parseTypeParams()
{ {
if (shouldParseTypePackAnnotation(lexer)) if (shouldParseTypePackAnnotation(lexer))
{ {
auto typePack = parseTypePackAnnotation(); AstTypePack* typePack = parseTypePackAnnotation();
parameters.push_back({{}, typePack}); parameters.push_back({{}, typePack});
} }
@ -2821,25 +2832,57 @@ AstTypeError* Parser::reportTypeAnnotationError(const Location& location, const
return allocator.alloc<AstTypeError>(location, types, isMissing, unsigned(parseErrors.size() - 1)); return allocator.alloc<AstTypeError>(location, types, isMissing, unsigned(parseErrors.size() - 1));
} }
const Lexeme& Parser::nextLexeme() void Parser::nextLexeme()
{ {
if (options.captureComments) if (options.captureComments)
{ {
while (true) if (FFlag::LuauParseAllHotComments)
{ {
const Lexeme& lexeme = lexer.next(/*skipComments*/ false); Lexeme::Type type = lexer.next(/* skipComments= */ false).type;
// Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme.
// The parser will turn this into a proper syntax error. while (type == Lexeme::BrokenComment || type == Lexeme::Comment || type == Lexeme::BlockComment)
if (lexeme.type == Lexeme::BrokenComment) {
const Lexeme& lexeme = lexer.current();
commentLocations.push_back(Comment{lexeme.type, lexeme.location}); commentLocations.push_back(Comment{lexeme.type, lexeme.location});
if (isComment(lexeme))
commentLocations.push_back(Comment{lexeme.type, lexeme.location}); // Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme.
else // The parser will turn this into a proper syntax error.
return lexeme; if (lexeme.type == Lexeme::BrokenComment)
return;
// Comments starting with ! are called "hot comments" and contain directives for type checking / linting
if (lexeme.type == Lexeme::Comment && lexeme.length && lexeme.data[0] == '!')
{
const char* text = lexeme.data;
unsigned int end = lexeme.length;
while (end > 0 && isSpace(text[end - 1]))
--end;
hotcomments.push_back({hotcommentHeader, lexeme.location, std::string(text + 1, text + end)});
}
type = lexer.next(/* skipComments= */ false).type;
}
}
else
{
while (true)
{
const Lexeme& lexeme = lexer.next(/*skipComments*/ false);
// Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme.
// The parser will turn this into a proper syntax error.
if (lexeme.type == Lexeme::BrokenComment)
commentLocations.push_back(Comment{lexeme.type, lexeme.location});
if (isComment(lexeme))
commentLocations.push_back(Comment{lexeme.type, lexeme.location});
else
return;
}
} }
} }
else else
return lexer.next(); lexer.next();
} }
} // namespace Luau } // namespace Luau

View file

@ -9,6 +9,12 @@
#include <stdlib.h> #include <stdlib.h>
#ifdef _WIN32 #ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h> #include <Windows.h>
#endif #endif

View file

@ -4,8 +4,13 @@
#include "Luau/Common.h" #include "Luau/Common.h"
#ifdef _WIN32 #ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <windows.h> #endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#else #else
#include <dirent.h> #include <dirent.h>
#include <fcntl.h> #include <fcntl.h>

View file

@ -37,6 +37,8 @@ enum class CompileFormat
Binary Binary
}; };
constexpr int MaxTraversalLimit = 50;
struct GlobalOptions struct GlobalOptions
{ {
int optimizationLevel = 1; int optimizationLevel = 1;
@ -243,10 +245,115 @@ std::string runCode(lua_State* L, const std::string& source)
return std::string(); return std::string();
} }
// Replaces the top of the lua stack with the metatable __index for the value
// if it exists. Returns true iff __index exists.
static bool tryReplaceTopWithIndex(lua_State* L)
{
if (luaL_getmetafield(L, -1, "__index"))
{
// Remove the table leaving __index on the top of stack
lua_remove(L, -2);
return true;
}
return false;
}
// This function is similar to lua_gettable, but it avoids calling any
// lua callback functions (e.g. __index) which might modify the Lua VM state.
static void safeGetTable(lua_State* L, int tableIndex)
{
lua_pushvalue(L, tableIndex); // Duplicate the table
// The loop invariant is that the table to search is at -1
// and the key is at -2.
for (int loopCount = 0;; loopCount++)
{
lua_pushvalue(L, -2); // Duplicate the key
lua_rawget(L, -2); // Try to find the key
if (!lua_isnil(L, -1) || loopCount >= MaxTraversalLimit)
{
// Either the key has been found, and/or we have reached the max traversal limit
break;
}
else
{
lua_pop(L, 1); // Pop the nil result
if (!luaL_getmetafield(L, -1, "__index"))
{
lua_pushnil(L);
break;
}
else if (lua_istable(L, -1))
{
// Replace the current table being searched with __index table
lua_replace(L, -2);
}
else
{
lua_pop(L, 1); // Pop the value
lua_pushnil(L);
break;
}
}
}
lua_remove(L, -2); // Remove the table
lua_remove(L, -2); // Remove the original key
}
// completePartialMatches finds keys that match the specified 'prefix'
// Note: the table/object to be searched must be on the top of the Lua stack
static void completePartialMatches(lua_State* L, bool completeOnlyFunctions, const std::string& editBuffer, std::string_view prefix,
const AddCompletionCallback& addCompletionCallback)
{
for (int i = 0; i < MaxTraversalLimit && lua_istable(L, -1); i++)
{
// table, key
lua_pushnil(L);
// Loop over all the keys in the current table
while (lua_next(L, -2) != 0)
{
if (lua_type(L, -2) == LUA_TSTRING)
{
// table, key, value
std::string_view key = lua_tostring(L, -2);
int valueType = lua_type(L, -1);
// If the last separator was a ':' (i.e. a method call) then only functions should be completed.
bool requiredValueType = (!completeOnlyFunctions || valueType == LUA_TFUNCTION);
if (!key.empty() && requiredValueType && Luau::startsWith(key, prefix))
{
std::string completedComponent(key.substr(prefix.size()));
std::string completion(editBuffer + completedComponent);
if (valueType == LUA_TFUNCTION)
{
// Add an opening paren for function calls by default.
completion += "(";
}
addCompletionCallback(completion, std::string(key));
}
}
lua_pop(L, 1);
}
// Replace the current table being searched with an __index table if one exists
if (!tryReplaceTopWithIndex(L))
{
break;
}
}
}
static void completeIndexer(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback) static void completeIndexer(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback)
{ {
std::string_view lookup = editBuffer; std::string_view lookup = editBuffer;
char lastSep = 0; bool completeOnlyFunctions = false;
// Push the global variable table to begin the search
lua_pushvalue(L, LUA_GLOBALSINDEX);
for (;;) for (;;)
{ {
@ -255,60 +362,26 @@ static void completeIndexer(lua_State* L, const std::string& editBuffer, const A
if (sep == std::string_view::npos) if (sep == std::string_view::npos)
{ {
// table, key completePartialMatches(L, completeOnlyFunctions, editBuffer, prefix, addCompletionCallback);
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
if (lua_type(L, -2) == LUA_TSTRING)
{
// table, key, value
std::string_view key = lua_tostring(L, -2);
int valueType = lua_type(L, -1);
// If the last separator was a ':' (i.e. a method call) then only functions should be completed.
bool requiredValueType = (lastSep != ':' || valueType == LUA_TFUNCTION);
if (!key.empty() && requiredValueType && Luau::startsWith(key, prefix))
{
std::string completedComponent(key.substr(prefix.size()));
std::string completion(editBuffer + completedComponent);
if (valueType == LUA_TFUNCTION)
{
// Add an opening paren for function calls by default.
completion += "(";
}
addCompletionCallback(completion, std::string(key));
}
}
lua_pop(L, 1);
}
break; break;
} }
else else
{ {
// find the key in the table // find the key in the table
lua_pushlstring(L, prefix.data(), prefix.size()); lua_pushlstring(L, prefix.data(), prefix.size());
lua_rawget(L, -2); safeGetTable(L, -2);
lua_remove(L, -2); lua_remove(L, -2);
if (lua_type(L, -1) == LUA_TSTRING) if (lua_istable(L, -1) || tryReplaceTopWithIndex(L))
{ {
// Replace the string object with the string class to perform further lookups of string functions completeOnlyFunctions = lookup[sep] == ':';
// Note: We retrieve the string class from _G to prevent issues if the user assigns to `string`. lookup.remove_prefix(sep + 1);
lua_pop(L, 1); // Pop the string instance
lua_getglobal(L, "_G");
lua_pushlstring(L, "string", 6);
lua_rawget(L, -2);
lua_remove(L, -2); // Remove the global table
LUAU_ASSERT(lua_istable(L, -1));
} }
else if (!lua_istable(L, -1)) else
{
// Unable to search for keys, so stop searching
break; break;
}
lastSep = lookup[sep];
lookup.remove_prefix(sep + 1);
} }
} }
@ -317,12 +390,6 @@ static void completeIndexer(lua_State* L, const std::string& editBuffer, const A
void getCompletions(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback) void getCompletions(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback)
{ {
// look the value up in current global table first
lua_pushvalue(L, LUA_GLOBALSINDEX);
completeIndexer(L, editBuffer, addCompletionCallback);
// and in actual global table after that
lua_getglobal(L, "_G");
completeIndexer(L, editBuffer, addCompletionCallback); completeIndexer(L, editBuffer, addCompletionCallback);
} }
@ -365,9 +432,7 @@ struct LinenoiseScopedHistory
ic_set_history(historyFilepath.c_str(), -1 /* default entries (= 200) */); ic_set_history(historyFilepath.c_str(), -1 /* default entries (= 200) */);
} }
~LinenoiseScopedHistory() ~LinenoiseScopedHistory() {}
{
}
std::string historyFilepath; std::string historyFilepath;
}; };

View file

@ -104,6 +104,20 @@ if (MSVC AND MSVC_VERSION GREATER_EQUAL 1924)
set_source_files_properties(VM/src/lvmexecute.cpp PROPERTIES COMPILE_FLAGS /d2ssa-pre-) set_source_files_properties(VM/src/lvmexecute.cpp PROPERTIES COMPILE_FLAGS /d2ssa-pre-)
endif() endif()
# embed .natvis inside the library debug information
if(MSVC)
target_link_options(Luau.Ast INTERFACE /NATVIS:${CMAKE_CURRENT_SOURCE_DIR}/tools/natvis/Ast.natvis)
target_link_options(Luau.Analysis INTERFACE /NATVIS:${CMAKE_CURRENT_SOURCE_DIR}/tools/natvis/Analysis.natvis)
target_link_options(Luau.VM INTERFACE /NATVIS:${CMAKE_CURRENT_SOURCE_DIR}/tools/natvis/VM.natvis)
endif()
# make .natvis visible inside the solution
if(MSVC_IDE)
target_sources(Luau.Ast PRIVATE tools/natvis/Ast.natvis)
target_sources(Luau.Analysis PRIVATE tools/natvis/Analysis.natvis)
target_sources(Luau.VM PRIVATE tools/natvis/VM.natvis)
endif()
if(LUAU_BUILD_CLI) if(LUAU_BUILD_CLI)
target_compile_options(Luau.Repl.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Repl.CLI PRIVATE ${LUAU_OPTIONS})
target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS})

View file

@ -8,6 +8,7 @@ target_sources(Luau.Ast PRIVATE
Ast/include/Luau/Location.h Ast/include/Luau/Location.h
Ast/include/Luau/ParseOptions.h Ast/include/Luau/ParseOptions.h
Ast/include/Luau/Parser.h Ast/include/Luau/Parser.h
Ast/include/Luau/ParseResult.h
Ast/include/Luau/StringUtils.h Ast/include/Luau/StringUtils.h
Ast/include/Luau/TimeTrace.h Ast/include/Luau/TimeTrace.h

View file

@ -36,12 +36,9 @@ const char* luau_ident = "$Luau: Copyright (C) 2019-2022 Roblox Corporation $\n"
static Table* getcurrenv(lua_State* L) static Table* getcurrenv(lua_State* L)
{ {
if (L->ci == L->base_ci) /* no enclosing function? */ if (L->ci == L->base_ci) /* no enclosing function? */
return hvalue(gt(L)); /* use global table as environment */ return L->gt; /* use global table as environment */
else else
{ return curr_func(L)->env;
Closure* func = curr_func(L);
return func->env;
}
} }
static LUAU_NOINLINE TValue* pseudo2addr(lua_State* L, int idx) static LUAU_NOINLINE TValue* pseudo2addr(lua_State* L, int idx)
@ -53,11 +50,14 @@ static LUAU_NOINLINE TValue* pseudo2addr(lua_State* L, int idx)
return registry(L); return registry(L);
case LUA_ENVIRONINDEX: case LUA_ENVIRONINDEX:
{ {
sethvalue(L, &L->env, getcurrenv(L)); sethvalue(L, &L->global->pseudotemp, getcurrenv(L));
return &L->env; return &L->global->pseudotemp;
} }
case LUA_GLOBALSINDEX: case LUA_GLOBALSINDEX:
return gt(L); {
sethvalue(L, &L->global->pseudotemp, L->gt);
return &L->global->pseudotemp;
}
default: default:
{ {
Closure* func = curr_func(L); Closure* func = curr_func(L);
@ -237,6 +237,11 @@ void lua_replace(lua_State* L, int idx)
func->env = hvalue(L->top - 1); func->env = hvalue(L->top - 1);
luaC_barrier(L, func, L->top - 1); luaC_barrier(L, func, L->top - 1);
} }
else if (idx == LUA_GLOBALSINDEX)
{
api_check(L, ttistable(L->top - 1));
L->gt = hvalue(L->top - 1);
}
else else
{ {
setobj(L, o, L->top - 1); setobj(L, o, L->top - 1);
@ -783,7 +788,7 @@ void lua_getfenv(lua_State* L, int idx)
sethvalue(L, L->top, clvalue(o)->env); sethvalue(L, L->top, clvalue(o)->env);
break; break;
case LUA_TTHREAD: case LUA_TTHREAD:
setobj2s(L, L->top, gt(thvalue(o))); sethvalue(L, L->top, thvalue(o)->gt);
break; break;
default: default:
setnilvalue(L->top); setnilvalue(L->top);
@ -914,7 +919,7 @@ int lua_setfenv(lua_State* L, int idx)
clvalue(o)->env = hvalue(L->top - 1); clvalue(o)->env = hvalue(L->top - 1);
break; break;
case LUA_TTHREAD: case LUA_TTHREAD:
sethvalue(L, gt(thvalue(o)), hvalue(L->top - 1)); thvalue(o)->gt = hvalue(L->top - 1);
break; break;
default: default:
res = 0; res = 0;

View file

@ -419,7 +419,7 @@ static void getcoverage(Proto* p, int depth, int* buffer, size_t size, void* con
} }
const char* debugname = p->debugname ? getstr(p->debugname) : NULL; const char* debugname = p->debugname ? getstr(p->debugname) : NULL;
int linedefined = luaG_getline(p, 0); int linedefined = getlinedefined(p);
callback(context, debugname, linedefined, depth, buffer, size); callback(context, debugname, linedefined, depth, buffer, size);

View file

@ -17,6 +17,8 @@
#include <string.h> #include <string.h>
LUAU_FASTFLAG(LuauReduceStackReallocs)
/* /*
** {====================================================== ** {======================================================
** Error-recovery functions ** Error-recovery functions
@ -164,13 +166,14 @@ static void correctstack(lua_State* L, TValue* oldstack)
void luaD_reallocstack(lua_State* L, int newsize) void luaD_reallocstack(lua_State* L, int newsize)
{ {
TValue* oldstack = L->stack; TValue* oldstack = L->stack;
int realsize = newsize + 1 + EXTRA_STACK; int realsize = newsize + (FFlag::LuauReduceStackReallocs ? EXTRA_STACK : 1 + EXTRA_STACK);
LUAU_ASSERT(L->stack_last - L->stack == L->stacksize - EXTRA_STACK - 1); LUAU_ASSERT(L->stack_last - L->stack == L->stacksize - (FFlag::LuauReduceStackReallocs ? EXTRA_STACK : 1 + EXTRA_STACK));
luaM_reallocarray(L, L->stack, L->stacksize, realsize, TValue, L->memcat); luaM_reallocarray(L, L->stack, L->stacksize, realsize, TValue, L->memcat);
TValue* newstack = L->stack;
for (int i = L->stacksize; i < realsize; i++) for (int i = L->stacksize; i < realsize; i++)
setnilvalue(L->stack + i); /* erase new segment */ setnilvalue(newstack + i); /* erase new segment */
L->stacksize = realsize; L->stacksize = realsize;
L->stack_last = L->stack + newsize; L->stack_last = newstack + newsize;
correctstack(L, oldstack); correctstack(L, oldstack);
} }
@ -512,7 +515,7 @@ static void callerrfunc(lua_State* L, void* ud)
static void restore_stack_limit(lua_State* L) static void restore_stack_limit(lua_State* L)
{ {
LUAU_ASSERT(L->stack_last - L->stack == L->stacksize - EXTRA_STACK - 1); LUAU_ASSERT(L->stack_last - L->stack == L->stacksize - (FFlag::LuauReduceStackReallocs ? EXTRA_STACK : 1 + EXTRA_STACK));
if (L->size_ci > LUAI_MAXCALLS) if (L->size_ci > LUAI_MAXCALLS)
{ /* there was an overflow? */ { /* there was an overflow? */
int inuse = cast_int(L->ci - L->base_ci); int inuse = cast_int(L->ci - L->base_ci);

View file

@ -11,7 +11,7 @@
if ((char*)L->stack_last - (char*)L->top <= (n) * (int)sizeof(TValue)) \ if ((char*)L->stack_last - (char*)L->top <= (n) * (int)sizeof(TValue)) \
luaD_growstack(L, n); \ luaD_growstack(L, n); \
else \ else \
condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK - 1)); condhardstacktests(luaD_reallocstack(L, L->stacksize - (FFlag::LuauReduceStackReallocs ? EXTRA_STACK : 1 + EXTRA_STACK)));
#define incr_top(L) \ #define incr_top(L) \
{ \ { \

View file

@ -268,7 +268,7 @@ static void traverseclosure(global_State* g, Closure* cl)
static void traversestack(global_State* g, lua_State* l, bool clearstack) static void traversestack(global_State* g, lua_State* l, bool clearstack)
{ {
markvalue(g, gt(l)); markobject(g, l->gt);
if (l->namecall) if (l->namecall)
stringmark(l->namecall); stringmark(l->namecall);
for (StkId o = l->stack; o < l->top; o++) for (StkId o = l->stack; o < l->top; o++)
@ -643,7 +643,7 @@ static void markroot(lua_State* L)
g->weak = NULL; g->weak = NULL;
markobject(g, g->mainthread); markobject(g, g->mainthread);
/* make global table be traversed before main stack */ /* make global table be traversed before main stack */
markvalue(g, gt(g->mainthread)); markobject(g, g->mainthread->gt);
markvalue(g, registry(L)); markvalue(g, registry(L));
markmt(g); markmt(g);
g->gcstate = GCSpropagate; g->gcstate = GCSpropagate;

View file

@ -77,7 +77,7 @@
#define luaC_checkGC(L) \ #define luaC_checkGC(L) \
{ \ { \
condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK - 1)); \ condhardstacktests(luaD_reallocstack(L, L->stacksize - (FFlag::LuauReduceStackReallocs ? EXTRA_STACK : 1 + EXTRA_STACK))); \
if (L->global->totalbytes >= L->global->GCthreshold) \ if (L->global->totalbytes >= L->global->GCthreshold) \
{ \ { \
condhardmemtests(luaC_validate(L), 1); \ condhardmemtests(luaC_validate(L), 1); \

View file

@ -88,7 +88,7 @@ static void validateclosure(global_State* g, Closure* cl)
static void validatestack(global_State* g, lua_State* l) static void validatestack(global_State* g, lua_State* l)
{ {
validateref(g, obj2gco(l), gt(l)); validateobjref(g, obj2gco(l), obj2gco(l->gt));
for (CallInfo* ci = l->base_ci; ci <= l->ci; ++ci) for (CallInfo* ci = l->base_ci; ci <= l->ci; ++ci)
{ {
@ -370,6 +370,7 @@ static void dumpclosure(FILE* f, Closure* cl)
fprintf(f, ",\"env\":"); fprintf(f, ",\"env\":");
dumpref(f, obj2gco(cl->env)); dumpref(f, obj2gco(cl->env));
if (cl->isC) if (cl->isC)
{ {
if (cl->nupvalues) if (cl->nupvalues)
@ -411,11 +412,8 @@ static void dumpthread(FILE* f, lua_State* th)
fprintf(f, "{\"type\":\"thread\",\"cat\":%d,\"size\":%d", th->memcat, int(size)); fprintf(f, "{\"type\":\"thread\",\"cat\":%d,\"size\":%d", th->memcat, int(size));
if (iscollectable(&th->l_gt)) fprintf(f, ",\"env\":");
{ dumpref(f, obj2gco(th->gt));
fprintf(f, ",\"env\":");
dumpref(f, gcvalue(&th->l_gt));
}
Closure* tcl = 0; Closure* tcl = 0;
for (CallInfo* ci = th->base_ci; ci <= th->ci; ++ci) for (CallInfo* ci = th->base_ci; ci <= th->ci; ++ci)

View file

@ -3,6 +3,12 @@
#include "lua.h" #include "lua.h"
#ifdef _WIN32 #ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h> #include <Windows.h>
#endif #endif

View file

@ -11,6 +11,7 @@
#include "ldebug.h" #include "ldebug.h"
LUAU_FASTFLAG(LuauGcPagedSweep) LUAU_FASTFLAG(LuauGcPagedSweep)
LUAU_FASTFLAGVARIABLE(LuauReduceStackReallocs, false)
/* /*
** Main thread combines a thread state and the global state ** Main thread combines a thread state and the global state
@ -31,10 +32,11 @@ static void stack_init(lua_State* L1, lua_State* L)
/* initialize stack array */ /* initialize stack array */
L1->stack = luaM_newarray(L, BASIC_STACK_SIZE + EXTRA_STACK, TValue, L1->memcat); L1->stack = luaM_newarray(L, BASIC_STACK_SIZE + EXTRA_STACK, TValue, L1->memcat);
L1->stacksize = BASIC_STACK_SIZE + EXTRA_STACK; L1->stacksize = BASIC_STACK_SIZE + EXTRA_STACK;
TValue* stack = L1->stack;
for (int i = 0; i < BASIC_STACK_SIZE + EXTRA_STACK; i++) for (int i = 0; i < BASIC_STACK_SIZE + EXTRA_STACK; i++)
setnilvalue(L1->stack + i); /* erase new stack */ setnilvalue(stack + i); /* erase new stack */
L1->top = L1->stack; L1->top = stack;
L1->stack_last = L1->stack + (L1->stacksize - EXTRA_STACK) - 1; L1->stack_last = stack + (L1->stacksize - (FFlag::LuauReduceStackReallocs ? EXTRA_STACK : 1 + EXTRA_STACK));
/* initialize first ci */ /* initialize first ci */
L1->ci->func = L1->top; L1->ci->func = L1->top;
setnilvalue(L1->top++); /* `function' entry for this `ci' */ setnilvalue(L1->top++); /* `function' entry for this `ci' */
@ -55,7 +57,7 @@ static void f_luaopen(lua_State* L, void* ud)
{ {
global_State* g = L->global; global_State* g = L->global;
stack_init(L, L); /* init stack */ stack_init(L, L); /* init stack */
sethvalue(L, gt(L), luaH_new(L, 0, 2)); /* table of globals */ L->gt = luaH_new(L, 0, 2); /* table of globals */
sethvalue(L, registry(L), luaH_new(L, 0, 2)); /* registry */ sethvalue(L, registry(L), luaH_new(L, 0, 2)); /* registry */
luaS_resize(L, LUA_MINSTRTABSIZE); /* initial size of string table */ luaS_resize(L, LUA_MINSTRTABSIZE); /* initial size of string table */
luaT_init(L); luaT_init(L);
@ -69,6 +71,7 @@ static void preinit_state(lua_State* L, global_State* g)
L->global = g; L->global = g;
L->stack = NULL; L->stack = NULL;
L->stacksize = 0; L->stacksize = 0;
L->gt = NULL;
L->openupval = NULL; L->openupval = NULL;
L->size_ci = 0; L->size_ci = 0;
L->nCcalls = L->baseCcalls = 0; L->nCcalls = L->baseCcalls = 0;
@ -80,7 +83,6 @@ static void preinit_state(lua_State* L, global_State* g)
L->stackstate = 0; L->stackstate = 0;
L->activememcat = 0; L->activememcat = 0;
L->userdata = NULL; L->userdata = NULL;
setnilvalue(gt(L));
} }
static void close_state(lua_State* L) static void close_state(lua_State* L)
@ -116,7 +118,7 @@ lua_State* luaE_newthread(lua_State* L)
preinit_state(L1, L->global); preinit_state(L1, L->global);
L1->activememcat = L->activememcat; // inherit the active memory category L1->activememcat = L->activememcat; // inherit the active memory category
stack_init(L1, L); /* init stack */ stack_init(L1, L); /* init stack */
setobj2n(L, gt(L1), gt(L)); /* share table of globals */ L1->gt = L->gt; /* share table of globals */
L1->singlestep = L->singlestep; L1->singlestep = L->singlestep;
LUAU_ASSERT(iswhite(obj2gco(L1))); LUAU_ASSERT(iswhite(obj2gco(L1)));
return L1; return L1;
@ -144,14 +146,30 @@ void lua_resetthread(lua_State* L)
ci->top = ci->base + LUA_MINSTACK; ci->top = ci->base + LUA_MINSTACK;
setnilvalue(ci->func); setnilvalue(ci->func);
L->ci = ci; L->ci = ci;
luaD_reallocCI(L, BASIC_CI_SIZE); if (FFlag::LuauReduceStackReallocs)
{
if (L->size_ci != BASIC_CI_SIZE)
luaD_reallocCI(L, BASIC_CI_SIZE);
}
else
{
luaD_reallocCI(L, BASIC_CI_SIZE);
}
/* clear thread state */ /* clear thread state */
L->status = LUA_OK; L->status = LUA_OK;
L->base = L->ci->base; L->base = L->ci->base;
L->top = L->ci->base; L->top = L->ci->base;
L->nCcalls = L->baseCcalls = 0; L->nCcalls = L->baseCcalls = 0;
/* clear thread stack */ /* clear thread stack */
luaD_reallocstack(L, BASIC_STACK_SIZE); if (FFlag::LuauReduceStackReallocs)
{
if (L->stacksize != BASIC_STACK_SIZE + EXTRA_STACK)
luaD_reallocstack(L, BASIC_STACK_SIZE);
}
else
{
luaD_reallocstack(L, BASIC_STACK_SIZE);
}
for (int i = 0; i < L->stacksize; i++) for (int i = 0; i < L->stacksize; i++)
setnilvalue(L->stack + i); setnilvalue(L->stack + i);
} }
@ -193,6 +211,7 @@ lua_State* lua_newstate(lua_Alloc f, void* ud)
g->strt.size = 0; g->strt.size = 0;
g->strt.nuse = 0; g->strt.nuse = 0;
g->strt.hash = NULL; g->strt.hash = NULL;
setnilvalue(&g->pseudotemp);
setnilvalue(registry(L)); setnilvalue(registry(L));
g->gcstate = GCSpause; g->gcstate = GCSpause;
if (!FFlag::LuauGcPagedSweep) if (!FFlag::LuauGcPagedSweep)

View file

@ -5,9 +5,6 @@
#include "lobject.h" #include "lobject.h"
#include "ltm.h" #include "ltm.h"
/* table of globals */
#define gt(L) (&L->l_gt)
/* registry */ /* registry */
#define registry(L) (&L->global->registry) #define registry(L) (&L->global->registry)
@ -177,6 +174,8 @@ typedef struct global_State
TString* ttname[LUA_T_COUNT]; /* names for basic types */ TString* ttname[LUA_T_COUNT]; /* names for basic types */
TString* tmname[TM_N]; /* array with tag-method names */ TString* tmname[TM_N]; /* array with tag-method names */
TValue pseudotemp; /* storage for temporary values used in pseudo2addr */
TValue registry; /* registry table, used by lua_ref and LUA_REGISTRYINDEX */ TValue registry; /* registry table, used by lua_ref and LUA_REGISTRYINDEX */
int registryfree; /* next free slot in registry */ int registryfree; /* next free slot in registry */
@ -231,8 +230,7 @@ struct lua_State
int cachedslot; /* when table operations or INDEX/NEWINDEX is invoked from Luau, what is the expected slot for lookup? */ int cachedslot; /* when table operations or INDEX/NEWINDEX is invoked from Luau, what is the expected slot for lookup? */
TValue l_gt; /* table of globals */ Table* gt; /* table of globals */
TValue env; /* temporary place for environments */
UpVal* openupval; /* list of open upvalues in this stack */ UpVal* openupval; /* list of open upvalues in this stack */
GCObject* gclist; GCObject* gclist;

View file

@ -39,7 +39,7 @@
// When calling luau_callTM, we usually push the arguments to the top of the stack. // When calling luau_callTM, we usually push the arguments to the top of the stack.
// This is safe to do for complicated reasons: // This is safe to do for complicated reasons:
// - stack guarantees 1 + EXTRA_STACK room beyond stack_last (see luaD_reallocstack) // - stack guarantees EXTRA_STACK room beyond stack_last (see luaD_reallocstack)
// - stack reallocation copies values past stack_last // - stack reallocation copies values past stack_last
// All external function calls that can cause stack realloc or Lua calls have to be wrapped in VM_PROTECT // All external function calls that can cause stack realloc or Lua calls have to be wrapped in VM_PROTECT

View file

@ -116,12 +116,12 @@ static void resolveImportSafe(lua_State* L, Table* env, TValue* k, uint32_t id)
// note: we call getimport with nil propagation which means that accesses to table chains like A.B.C will resolve in nil // note: we call getimport with nil propagation which means that accesses to table chains like A.B.C will resolve in nil
// this is technically not necessary but it reduces the number of exceptions when loading scripts that rely on getfenv/setfenv for global // this is technically not necessary but it reduces the number of exceptions when loading scripts that rely on getfenv/setfenv for global
// injection // injection
luaV_getimport(L, hvalue(gt(L)), self->k, self->id, /* propagatenil= */ true); luaV_getimport(L, L->gt, self->k, self->id, /* propagatenil= */ true);
} }
}; };
ResolveImport ri = {k, id}; ResolveImport ri = {k, id};
if (hvalue(gt(L))->safeenv) if (L->gt->safeenv)
{ {
// luaD_pcall will make sure that if any C/Lua calls during import resolution fail, the thread state is restored back // luaD_pcall will make sure that if any C/Lua calls during import resolution fail, the thread state is restored back
int oldTop = lua_gettop(L); int oldTop = lua_gettop(L);
@ -171,7 +171,7 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
L->global->GCthreshold = SIZE_MAX; L->global->GCthreshold = SIZE_MAX;
// env is 0 for current environment and a stack index otherwise // env is 0 for current environment and a stack index otherwise
Table* envt = (env == 0) ? hvalue(gt(L)) : hvalue(luaA_toobject(L, env)); Table* envt = (env == 0) ? L->gt : hvalue(luaA_toobject(L, env));
TString* source = luaS_new(L, chunkname); TString* source = luaS_new(L, chunkname);

View file

@ -55,7 +55,7 @@ static void callTMres(lua_State* L, StkId res, const TValue* f, const TValue* p1
{ {
ptrdiff_t result = savestack(L, res); ptrdiff_t result = savestack(L, res);
// using stack room beyond top is technically safe here, but for very complicated reasons: // using stack room beyond top is technically safe here, but for very complicated reasons:
// * The stack guarantees 1 + EXTRA_STACK room beyond stack_last (see luaD_reallocstack) will be allocated // * The stack guarantees EXTRA_STACK room beyond stack_last (see luaD_reallocstack) will be allocated
// * we cannot move luaD_checkstack above because the arguments are *sometimes* pointers to the lua // * we cannot move luaD_checkstack above because the arguments are *sometimes* pointers to the lua
// stack and checkstack may invalidate those pointers // stack and checkstack may invalidate those pointers
// * we cannot use savestack/restorestack because the arguments are sometimes on the C++ stack // * we cannot use savestack/restorestack because the arguments are sometimes on the C++ stack
@ -76,7 +76,7 @@ static void callTMres(lua_State* L, StkId res, const TValue* f, const TValue* p1
static void callTM(lua_State* L, const TValue* f, const TValue* p1, const TValue* p2, const TValue* p3) static void callTM(lua_State* L, const TValue* f, const TValue* p1, const TValue* p2, const TValue* p3)
{ {
// using stack room beyond top is technically safe here, but for very complicated reasons: // using stack room beyond top is technically safe here, but for very complicated reasons:
// * The stack guarantees 1 + EXTRA_STACK room beyond stack_last (see luaD_reallocstack) will be allocated // * The stack guarantees EXTRA_STACK room beyond stack_last (see luaD_reallocstack) will be allocated
// * we cannot move luaD_checkstack above because the arguments are *sometimes* pointers to the lua // * we cannot move luaD_checkstack above because the arguments are *sometimes* pointers to the lua
// stack and checkstack may invalidate those pointers // stack and checkstack may invalidate those pointers
// * we cannot use savestack/restorestack because the arguments are sometimes on the C++ stack // * we cannot use savestack/restorestack because the arguments are sometimes on the C++ stack

View file

@ -317,3 +317,13 @@ The code above can be rewritten as follows to avoid the warning and the associat
```lua ```lua
local x = if flag then false else true local x = if flag then false else true
``` ```
## CommentDirective (26)
Luau uses comments that start from `!` to control certain aspects of analysis, for example setting type checking mode via `--!strict` or disabling individual lints with `--!nolint`. Unknown directives are ignored, for example `--!nostrict` doesn't have any effect on the type checking process as the correct spelling is `--!nonstrict`. This warning flags comment directives that are ignored during processing:
```lua
--!nostrict
-- Unknown comment directive 'nostrict'; did you mean 'nonstrict'?"
```
```

View file

@ -32,7 +32,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* Data, size_t Size)
Luau::LintOptions lintOptions; Luau::LintOptions lintOptions;
lintOptions.warningMask = ~0ull; lintOptions.warningMask = ~0ull;
Luau::lint(parseResult.root, names, typeck.globalScope, nullptr, lintOptions); Luau::lint(parseResult.root, names, typeck.globalScope, nullptr, {}, lintOptions);
} }
return 0; return 0;

View file

@ -227,7 +227,7 @@ DEFINE_PROTO_FUZZER(const luau::StatBlock& message)
if (kFuzzLinter) if (kFuzzLinter)
{ {
Luau::LintOptions lintOptions = {~0u}; Luau::LintOptions lintOptions = {~0u};
Luau::lint(parseResult.root, names, sharedEnv.globalScope, module.get(), lintOptions); Luau::lint(parseResult.root, names, sharedEnv.globalScope, module.get(), {}, lintOptions);
} }
} }

View file

@ -2,7 +2,11 @@
*.agdai *.agdai
Main Main
MAlonzo MAlonzo
PrettyPrinter
.ghc.*
Interpreter
Examples Examples
PrettyPrinter
Interpreter
Properties
!Tests/Interpreter
!Tests/PrettyPrinter
.ghc.*
test-failures/

View file

@ -25,3 +25,21 @@ and run!
``` ```
luau-ast Examples/SmokeTest.lua | ./PrettyPrinter luau-ast Examples/SmokeTest.lua | ./PrettyPrinter
``` ```
## Testing
We have a series of snapshot tests in the `Tests/` directory. You interact with the tests using the `tests` Python script in the `prototyping` directory. To simply run the tests, run:
```sh
tests --luau-cli ../build/luau-ast --build
```
This will build the test targets and run them. Run `tests --help` for information about all the command-line options.
### Adding a new test
To add a new test, add it to `Tests/{SUITE_NAME}/{CASE_NAME}`. You'll need an `in.lua` file and an `out.txt` file. The `in.lua` file is the input Luau source code, while the `out.txt` file is the expected output after running `luau-ast in.lua | test_executable`.
### Updating a test
If you make a change to the prototype that results in an expected change in behavior, you might want to update the test cases automatically. To do this, run `tests` with the `--accept-new-output` (`-a` for short) flag. Rather than diffing the output, this will overwrite the `out.txt` files for each test case with the actual result. Commit the resulting changes with your PR.

View file

@ -0,0 +1,5 @@
local function foo(x)
return nil
end
return foo(nil)

View file

@ -0,0 +1 @@
nil

View file

@ -1,12 +1,12 @@
local function id(x) local function id(x)
return x return x
end end
local function comp(f) local function comp(f)
return function(g) return function(g)
return function(x) return function(x)
return f(g(x)) return f(g(x))
end end
end end
end end
local id2 = comp(id)(id) local id2 = comp(id)(id)
local nil2 = id2(nil) local nil2 = id2(nil)

197
prototyping/tests.py Executable file
View file

@ -0,0 +1,197 @@
#!/usr/bin/python
import argparse
import difflib
import enum
import os
import os.path
import subprocess
import sys
SUITES = ["interpreter", "prettyprinter"]
IN_FILE_NAME = "in.lua"
OUT_FILE_NAME = "out.txt"
SUITE_EXE_NAMES = {
"interpreter": "Interpreter",
"prettyprinter": "PrettyPrinter",
}
SUITE_ENTRY_POINTS = {
"interpreter": "Interpreter.agda",
"prettyprinter": "PrettyPrinter.agda",
}
SUITE_ROOTS = {
"interpreter": "Tests/Interpreter",
"prettyprinter": "Tests/PrettyPrinter",
}
class TestResultStatus(enum.Enum):
CLI_ERROR = 0
EXE_ERROR = 1
DIFF_ERROR = 2
SUCCESS = 3
WROTE_NEW = 4
class DiffFailure:
def __init__(self, expected, actual):
self.expected = expected
self.actual = actual
def diff_text(self):
diff_generator = difflib.context_diff(self.expected.splitlines(), self.actual.splitlines(), fromfile="expected", tofile="actual", n=3)
return "".join(diff_generator)
def diff_html(self):
differ = difflib.HtmlDiff(tabsize=4)
return differ.make_file(self.expected.splitlines(), self.actual.splitlines(), fromdesc="Expected", todesc="Actual", context=True, numlines=5)
class TestCaseResult:
def __init__(self, suite, case, status, details):
self.suite = suite
self.case = case
self.status = status
self.details = details
def did_pass(self):
return self.status == TestResultStatus.SUCCESS or self.status == TestResultStatus.WROTE_NEW
def to_string(self):
prefix = f"[{self.suite}/{self.case}]: "
if self.status == TestResultStatus.CLI_ERROR:
return f"{prefix}CLI ERROR: {self.details}"
elif self.status == TestResultStatus.EXE_ERROR:
return f"{prefix}EXE ERROR: {self.details}"
elif self.status == TestResultStatus.DIFF_ERROR:
text_diff = self.details.diff_text()
return f"{prefix}FAILED:\n{text_diff}"
elif self.status == TestResultStatus.SUCCESS:
return f"{prefix}SUCCEEDED"
elif self.status == TestResultStatus.WROTE_NEW:
return f"{prefix}WROTE NEW RESULT"
def write_artifact(self, artifact_root):
if self.status != TestResultStatus.DIFF_ERROR:
return
filename = f"{self.suite}-{self.case}.out.html"
path = os.path.join(artifact_root, filename)
html = self.details.diff_html()
with open(path, "w") as file:
file.write(html)
parser = argparse.ArgumentParser(description="Runs prototype test cases")
parser.add_argument("--luau-cli", "-l", dest="cli_location", required=True, help="The location of luau-cli")
parser.add_argument("--root", "-r", dest="prototype_root", required=False, default=os.getcwd(), help="The root of the prototype")
parser.add_argument("--build", "-b", dest="build", action="store_true", default=True, help="Whether to automatically build required test binaries")
parser.add_argument("--suite", "-s", dest="suites", action="append", default=[], choices=SUITES, help="Which test suites to run")
parser.add_argument("--case", "-c", dest="cases", action="append", default=[], help="Which test cases to run")
parser.add_argument("--accept-new-output", "-a", dest="snapshot", action="store_true", default=False, help="Whether to write the new output to files, instead of diffing against it")
parser.add_argument("--write-diff-failures", dest="write_diffs", action="store_true", default=False, help="Whether to write test failure diffs to files")
parser.add_argument("--diff-failure-location", dest="diff_location", default=None, help="Where to write diff failure files to")
def build_suite(root, suite):
entry_point = SUITE_ENTRY_POINTS.get(suite)
if entry_point is None:
return (False, "Invalid suite")
result = subprocess.run(["~/.cabal/bin/agda", "--compile", entry_point], shell=True, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if result.returncode == 0:
return (True, None)
else:
return (False, result.stdout)
def run_test(in_path, out_path, cli_path, exe_path, snapshot):
cli_result = subprocess.run([cli_path, in_path], capture_output=True)
if cli_result.returncode != 0:
return (TestResultStatus.CLI_ERROR, f"CLI error: {cli_result.stderr}")
exe_result = subprocess.run(exe_path, input=cli_result.stdout, capture_output=True)
if exe_result.returncode != 0:
return (TestResultStatus.EXE_ERROR, f"Executable error; stdout:{exe_result.stdout}\n\nstderr: {exe_result.stderr}")
actual_result = exe_result.stdout.decode("utf-8")
if snapshot:
with open(out_path, "w") as out_file:
out_file.write(actual_result)
return (TestResultStatus.WROTE_NEW, None)
else:
with open(out_path, "r") as out_file:
expected_result = out_file.read()
if expected_result != actual_result:
return (TestResultStatus.DIFF_ERROR, DiffFailure(expected_result, actual_result))
return (TestResultStatus.SUCCESS, None)
def should_run_case(case_name, filters):
if len(filters) == 0:
return True
return any([f in case_name for f in filters])
def run_test_suite(args, suite, suite_root, suite_exe):
results = []
for entry in os.listdir(suite_root):
if not should_run_case(entry, args.cases):
continue
case_path = os.path.join(suite_root, entry)
if os.path.isdir(case_path):
in_path = os.path.join(case_path, IN_FILE_NAME)
out_path = os.path.join(case_path, OUT_FILE_NAME)
if not os.path.exists(in_path) or not os.path.exists(out_path):
continue
status, details = run_test(in_path, out_path, args.cli_location, suite_exe, args.snapshot)
result = TestCaseResult(suite, entry, status, details)
results.append(result)
return results
def main():
args = parser.parse_args()
suites = args.suites if len(args.suites) > 0 else SUITES
root = os.path.abspath(args.prototype_root)
if args.build:
for suite in suites:
success, reason = build_suite(root, suite)
if not success:
print(f"Error building executable for test suite {suite}:\n{reason}")
sys.exit(1)
else:
print(f"Built executable for test suite {suite} successfully.")
failed = False
for suite in suites:
suite_root = os.path.join(root, SUITE_ROOTS.get(suite))
suite_exe = os.path.join(root, SUITE_EXE_NAMES.get(suite))
print(f"Running test suite {suite}...")
results = run_test_suite(args, suite, suite_root, suite_exe)
passed = 0
total = len(results)
for result in results:
if result.did_pass():
passed += 1
else:
failed = True
print(f"Suite {suite} [{passed} / {total} passed]:")
for result in results:
print(result.to_string())
if args.write_diffs:
result.write_artifact(args.diff_location)
if failed:
sys.exit(1)
if __name__ == "__main__":
main()

52
rfcs/STATUS.md Normal file
View file

@ -0,0 +1,52 @@
This document tracks unimplemented RFCs.
## Deprecate getfenv/setfenv
[RFC: Deprecate getfenv/setfenv](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-getfenv-setfenv.md)
**Status**: Needs implementation.
**Notes**: Implementing this RFC triggers warnings across the board in the apps ecosystem, in particular in testing libraries. Pending code changes / decisions.
## Read-only and write-only properties
[RFC: Read-only properties](https://github.com/Roblox/luau/blob/master/rfcs/property-readonly.md) |
[RFC: Write-only properties](https://github.com/Roblox/luau/blob/master/rfcs/property-writeonly.md)
**Status**: Needs implementation
## Sealed/unsealed typing changes
[RFC: Sealed table subtyping](https://github.com/Roblox/luau/blob/master/rfcs/sealed-table-subtyping.md) |
[RFC: Unsealed table literals](https://github.com/Roblox/luau/blob/master/rfcs/unsealed-table-literals.md) |
[RFC: Only strip optional properties from unsealed tables during subtyping](https://github.com/Roblox/luau/blob/master/rfcs/unsealed-table-subtyping-strips-optional-properties.md)
**Status**: Implemented but depends on new transaction log implementation that is not fully live yet.
## Nil-forgiving operator
[RFC: nil-forgiving postfix operator !](https://github.com/Roblox/luau/blob/master/rfcs/syntax-nil-forgiving-operator.md)
**Status**: Needs implementation.
**Notes**: Do we need to reevaluate the necessity given `assert` and improved refinements?
## Safe navigation operator
[RFC: Safe navigation postfix operator (?)](https://github.com/Roblox/luau/blob/master/rfcs/syntax-safe-navigation-operator.md)
**Status**: Needs implementation.
**Notes**: We have unresolved issues with interaction between this feature and Roblox instance hierarchy. This may affect the viability of this proposal.
## String interpolation
[RFC: String interpolation](https://github.com/Roblox/luau/blob/master/rfcs/syntax-string-interpolation.md)
**Status**: Needs implementation
## Generalized iteration
[RFC: Generalized iteration](https://github.com/Roblox/luau/blob/master/rfcs/generalized-iteration.md)
**Status**: Needs implementation

View file

@ -1,5 +1,7 @@
# Default type alias type parameters # Default type alias type parameters
**Status**: Implemented
## Summary ## Summary
Introduce syntax to provide default type values inside the type alias type parameter list. Introduce syntax to provide default type values inside the type alias type parameter list.

View file

@ -2,6 +2,8 @@
> Note: this RFC was adapted from an internal proposal that predates RFC process > Note: this RFC was adapted from an internal proposal that predates RFC process
**Status**: Implemented
## Summary ## Summary
Introduce a new kind of type variable, called singleton types. They are just like normal types but has the capability to represent a constant runtime value as a type. Introduce a new kind of type variable, called singleton types. They are just like normal types but has the capability to represent a constant runtime value as a type.

View file

@ -1,7 +1,6 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Autocomplete.h" #include "Luau/Autocomplete.h"
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h" #include "Luau/VisitTypeVar.h"
@ -2610,6 +2609,27 @@ a = if temp then even elseif true then temp else e@9
CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elseif") == 0);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_else_regression")
{
ScopedFastFlag FFlagLuauIfElseExprFixCompletionIssue("LuauIfElseExprFixCompletionIssue", true);
check(R"(
local abcdef = 0;
local temp = false
local even = true;
local a
a = if temp then even else@1
a = if temp then even else @2
a = if temp then even else abc@3
)");
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("else") == 0);
ac = autocomplete('2');
CHECK(ac.entryMap.count("else") == 0);
ac = autocomplete('3');
CHECK(ac.entryMap.count("abcdef"));
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_explicit_type_pack") TEST_CASE_FIXTURE(ACFixture, "autocomplete_explicit_type_pack")
{ {
check(R"( check(R"(

View file

@ -10,6 +10,11 @@
#include <sstream> #include <sstream>
#include <string_view> #include <string_view>
namespace Luau
{
std::string rep(const std::string& s, size_t n);
}
using namespace Luau; using namespace Luau;
static std::string compileFunction(const char* source, uint32_t id) static std::string compileFunction(const char* source, uint32_t id)
@ -1960,15 +1965,6 @@ RETURN R8 -1
)"); )");
} }
static std::string rep(const std::string& s, size_t n)
{
std::string r;
r.reserve(s.length() * n);
for (size_t i = 0; i < n; ++i)
r += s;
return r;
}
TEST_CASE("RecursionParse") TEST_CASE("RecursionParse")
{ {
// The test forcibly pushes the stack limit during compilation; in NoOpt, the stack consumption is much larger so we need to reduce the limit to // The test forcibly pushes the stack limit during compilation; in NoOpt, the stack consumption is much larger so we need to reduce the limit to

View file

@ -492,6 +492,8 @@ TEST_CASE("DateTime")
TEST_CASE("Debug") TEST_CASE("Debug")
{ {
ScopedFastFlag luauTableFieldFunctionDebugname{"LuauTableFieldFunctionDebugname", true};
runConformance("debug.lua"); runConformance("debug.lua");
} }
@ -890,6 +892,12 @@ TEST_CASE("Coverage")
lua_pushstring(L, function); lua_pushstring(L, function);
lua_setfield(L, -2, "name"); lua_setfield(L, -2, "name");
lua_pushinteger(L, linedefined);
lua_setfield(L, -2, "linedefined");
lua_pushinteger(L, depth);
lua_setfield(L, -2, "depth");
for (size_t i = 0; i < size; ++i) for (size_t i = 0; i < size; ++i)
if (hits[i] != -1) if (hits[i] != -1)
{ {

View file

@ -2,6 +2,7 @@
#include "Fixture.h" #include "Fixture.h"
#include "Luau/AstQuery.h" #include "Luau/AstQuery.h"
#include "Luau/Parser.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
#include "Luau/TypeAttach.h" #include "Luau/TypeAttach.h"
#include "Luau/Transpiler.h" #include "Luau/Transpiler.h"
@ -112,7 +113,7 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars
sourceModule->name = fromString(mainModuleName); sourceModule->name = fromString(mainModuleName);
sourceModule->root = result.root; sourceModule->root = result.root;
sourceModule->mode = parseMode(result.hotcomments); sourceModule->mode = parseMode(result.hotcomments);
sourceModule->ignoreLints = LintWarning::parseMask(result.hotcomments); sourceModule->hotcomments = std::move(result.hotcomments);
if (!result.errors.empty()) if (!result.errors.empty())
{ {
@ -157,6 +158,7 @@ CheckResult Fixture::check(const std::string& source)
LintResult Fixture::lint(const std::string& source, const std::optional<LintOptions>& lintOptions) LintResult Fixture::lint(const std::string& source, const std::optional<LintOptions>& lintOptions)
{ {
ParseOptions parseOptions; ParseOptions parseOptions;
parseOptions.captureComments = true;
configResolver.defaultConfig.mode = Mode::Nonstrict; configResolver.defaultConfig.mode = Mode::Nonstrict;
parse(source, parseOptions); parse(source, parseOptions);

View file

@ -8,7 +8,6 @@
#include "Luau/Linter.h" #include "Luau/Linter.h"
#include "Luau/Location.h" #include "Luau/Location.h"
#include "Luau/ModuleResolver.h" #include "Luau/ModuleResolver.h"
#include "Luau/Parser.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -2,7 +2,6 @@
#include "Luau/AstQuery.h" #include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Frontend.h" #include "Luau/Frontend.h"
#include "Luau/Parser.h"
#include "Luau/RequireTracer.h" #include "Luau/RequireTracer.h"
#include "Fixture.h" #include "Fixture.h"
@ -897,8 +896,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "clearStats")
TEST_CASE_FIXTURE(FrontendFixture, "typecheck_twice_for_ast_types") TEST_CASE_FIXTURE(FrontendFixture, "typecheck_twice_for_ast_types")
{ {
ScopedFastFlag sffs("LuauTypeCheckTwice", true);
fileResolver.source["Module/A"] = R"( fileResolver.source["Module/A"] = R"(
local a = 1 local a = 1
)"; )";

View file

@ -1395,12 +1395,10 @@ TEST_CASE_FIXTURE(Fixture, "DeprecatedApi")
TypeId colorType = typeChecker.globalTypes.addType(TableTypeVar{{}, std::nullopt, typeChecker.globalScope->level, Luau::TableState::Sealed}); TypeId colorType = typeChecker.globalTypes.addType(TableTypeVar{{}, std::nullopt, typeChecker.globalScope->level, Luau::TableState::Sealed});
getMutable<TableTypeVar>(colorType)->props = { getMutable<TableTypeVar>(colorType)->props = {{"toHSV", {typeChecker.anyType, /* deprecated= */ true, "Color3:ToHSV"}}};
{"toHSV", {typeChecker.anyType, /* deprecated= */ true, "Color3:ToHSV"} }
};
addGlobalBinding(typeChecker, "Color3", Binding{colorType, {}}); addGlobalBinding(typeChecker, "Color3", Binding{colorType, {}});
freeze(typeChecker.globalTypes); freeze(typeChecker.globalTypes);
LintResult result = lintTyped(R"( LintResult result = lintTyped(R"(
@ -1554,8 +1552,46 @@ _ = (math.random() < 0.5 and false) or 42 -- currently ignored
)"); )");
REQUIRE_EQ(result.warnings.size(), 2); REQUIRE_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "The and-or expression always evaluates to the second alternative because the first alternative is false; consider using if-then-else expression instead"); CHECK_EQ(result.warnings[0].text, "The and-or expression always evaluates to the second alternative because the first alternative is false; "
CHECK_EQ(result.warnings[1].text, "The and-or expression always evaluates to the second alternative because the first alternative is nil; consider using if-then-else expression instead"); "consider using if-then-else expression instead");
CHECK_EQ(result.warnings[1].text, "The and-or expression always evaluates to the second alternative because the first alternative is nil; "
"consider using if-then-else expression instead");
}
TEST_CASE_FIXTURE(Fixture, "WrongComment")
{
ScopedFastFlag sff("LuauParseAllHotComments", true);
LintResult result = lint(R"(
--!strict
--!struct
--!nolintGlobal
--!nolint Global
--!nolint KnownGlobal
--!nolint UnknownGlobal
--! no more lint
--!strict here
do end
--!nolint
)");
REQUIRE_EQ(result.warnings.size(), 6);
CHECK_EQ(result.warnings[0].text, "Unknown comment directive 'struct'; did you mean 'strict'?");
CHECK_EQ(result.warnings[1].text, "Unknown comment directive 'nolintGlobal'");
CHECK_EQ(result.warnings[2].text, "nolint directive refers to unknown lint rule 'Global'");
CHECK_EQ(result.warnings[3].text, "nolint directive refers to unknown lint rule 'KnownGlobal'; did you mean 'UnknownGlobal'?");
CHECK_EQ(result.warnings[4].text, "Comment directive with the type checking mode has extra symbols at the end of the line");
CHECK_EQ(result.warnings[5].text, "Comment directive is ignored because it is placed after the first non-comment token");
}
TEST_CASE_FIXTURE(Fixture, "WrongCommentMuteSelf")
{
LintResult result = lint(R"(
--!nolint
--!struct
)");
REQUIRE_EQ(result.warnings.size(), 0); // --!nolint disables WrongComment lint :)
} }
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,6 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h" #include "Luau/Parser.h"
#include "Luau/TypeInfer.h"
#include "Fixture.h" #include "Fixture.h"
#include "ScopedFlags.h" #include "ScopedFlags.h"
@ -300,8 +299,9 @@ TEST_CASE_FIXTURE(Fixture, "functions_can_have_return_annotations")
AstStatFunction* statFunction = block->body.data[0]->as<AstStatFunction>(); AstStatFunction* statFunction = block->body.data[0]->as<AstStatFunction>();
REQUIRE(statFunction != nullptr); REQUIRE(statFunction != nullptr);
CHECK_EQ(statFunction->func->returnAnnotation.types.size, 1); REQUIRE(statFunction->func->returnAnnotation.has_value());
CHECK(statFunction->func->returnAnnotation.tailType == nullptr); CHECK_EQ(statFunction->func->returnAnnotation->types.size, 1);
CHECK(statFunction->func->returnAnnotation->tailType == nullptr);
} }
TEST_CASE_FIXTURE(Fixture, "functions_can_have_a_function_type_annotation") TEST_CASE_FIXTURE(Fixture, "functions_can_have_a_function_type_annotation")
@ -316,9 +316,9 @@ TEST_CASE_FIXTURE(Fixture, "functions_can_have_a_function_type_annotation")
AstStatFunction* statFunc = block->body.data[0]->as<AstStatFunction>(); AstStatFunction* statFunc = block->body.data[0]->as<AstStatFunction>();
REQUIRE(statFunc != nullptr); REQUIRE(statFunc != nullptr);
AstArray<AstType*>& retTypes = statFunc->func->returnAnnotation.types; REQUIRE(statFunc->func->returnAnnotation.has_value());
REQUIRE(statFunc->func->hasReturnAnnotation); CHECK(statFunc->func->returnAnnotation->tailType == nullptr);
CHECK(statFunc->func->returnAnnotation.tailType == nullptr); AstArray<AstType*>& retTypes = statFunc->func->returnAnnotation->types;
REQUIRE(retTypes.size == 1); REQUIRE(retTypes.size == 1);
AstTypeFunction* funTy = retTypes.data[0]->as<AstTypeFunction>(); AstTypeFunction* funTy = retTypes.data[0]->as<AstTypeFunction>();
@ -337,9 +337,9 @@ TEST_CASE_FIXTURE(Fixture, "function_return_type_should_disambiguate_from_functi
AstStatFunction* statFunc = block->body.data[0]->as<AstStatFunction>(); AstStatFunction* statFunc = block->body.data[0]->as<AstStatFunction>();
REQUIRE(statFunc != nullptr); REQUIRE(statFunc != nullptr);
AstArray<AstType*>& retTypes = statFunc->func->returnAnnotation.types; REQUIRE(statFunc->func->returnAnnotation.has_value());
REQUIRE(statFunc->func->hasReturnAnnotation); CHECK(statFunc->func->returnAnnotation->tailType == nullptr);
CHECK(statFunc->func->returnAnnotation.tailType == nullptr); AstArray<AstType*>& retTypes = statFunc->func->returnAnnotation->types;
REQUIRE(retTypes.size == 2); REQUIRE(retTypes.size == 2);
AstTypeReference* ty0 = retTypes.data[0]->as<AstTypeReference>(); AstTypeReference* ty0 = retTypes.data[0]->as<AstTypeReference>();
@ -363,9 +363,9 @@ TEST_CASE_FIXTURE(Fixture, "function_return_type_should_parse_as_function_type_a
AstStatFunction* statFunc = block->body.data[0]->as<AstStatFunction>(); AstStatFunction* statFunc = block->body.data[0]->as<AstStatFunction>();
REQUIRE(statFunc != nullptr); REQUIRE(statFunc != nullptr);
AstArray<AstType*>& retTypes = statFunc->func->returnAnnotation.types; REQUIRE(statFunc->func->returnAnnotation.has_value());
REQUIRE(statFunc->func->hasReturnAnnotation); CHECK(statFunc->func->returnAnnotation->tailType == nullptr);
CHECK(statFunc->func->returnAnnotation.tailType == nullptr); AstArray<AstType*>& retTypes = statFunc->func->returnAnnotation->types;
REQUIRE(retTypes.size == 1); REQUIRE(retTypes.size == 1);
AstTypeFunction* funTy = retTypes.data[0]->as<AstTypeFunction>(); AstTypeFunction* funTy = retTypes.data[0]->as<AstTypeFunction>();
@ -707,12 +707,25 @@ TEST_CASE_FIXTURE(Fixture, "mode_is_unset_if_no_hot_comment")
TEST_CASE_FIXTURE(Fixture, "sense_hot_comment_on_first_line") TEST_CASE_FIXTURE(Fixture, "sense_hot_comment_on_first_line")
{ {
ParseResult result = parseEx(" --!strict "); ParseOptions options;
options.captureComments = true;
ParseResult result = parseEx(" --!strict ", options);
std::optional<Mode> mode = parseMode(result.hotcomments); std::optional<Mode> mode = parseMode(result.hotcomments);
REQUIRE(bool(mode)); REQUIRE(bool(mode));
CHECK_EQ(int(*mode), int(Mode::Strict)); CHECK_EQ(int(*mode), int(Mode::Strict));
} }
TEST_CASE_FIXTURE(Fixture, "non_header_hot_comments")
{
ParseOptions options;
options.captureComments = true;
ParseResult result = parseEx("do end --!strict", options);
std::optional<Mode> mode = parseMode(result.hotcomments);
REQUIRE(!mode);
}
TEST_CASE_FIXTURE(Fixture, "stop_if_line_ends_with_hyphen") TEST_CASE_FIXTURE(Fixture, "stop_if_line_ends_with_hyphen")
{ {
CHECK_THROWS_AS(parse(" -"), std::exception); CHECK_THROWS_AS(parse(" -"), std::exception);
@ -720,7 +733,10 @@ TEST_CASE_FIXTURE(Fixture, "stop_if_line_ends_with_hyphen")
TEST_CASE_FIXTURE(Fixture, "nonstrict_mode") TEST_CASE_FIXTURE(Fixture, "nonstrict_mode")
{ {
ParseResult result = parseEx("--!nonstrict"); ParseOptions options;
options.captureComments = true;
ParseResult result = parseEx("--!nonstrict", options);
CHECK(result.errors.empty()); CHECK(result.errors.empty());
std::optional<Mode> mode = parseMode(result.hotcomments); std::optional<Mode> mode = parseMode(result.hotcomments);
REQUIRE(bool(mode)); REQUIRE(bool(mode));
@ -729,7 +745,10 @@ TEST_CASE_FIXTURE(Fixture, "nonstrict_mode")
TEST_CASE_FIXTURE(Fixture, "nocheck_mode") TEST_CASE_FIXTURE(Fixture, "nocheck_mode")
{ {
ParseResult result = parseEx("--!nocheck"); ParseOptions options;
options.captureComments = true;
ParseResult result = parseEx("--!nocheck", options);
CHECK(result.errors.empty()); CHECK(result.errors.empty());
std::optional<Mode> mode = parseMode(result.hotcomments); std::optional<Mode> mode = parseMode(result.hotcomments);
REQUIRE(bool(mode)); REQUIRE(bool(mode));
@ -1498,8 +1517,6 @@ return
TEST_CASE_FIXTURE(Fixture, "parse_error_broken_comment") TEST_CASE_FIXTURE(Fixture, "parse_error_broken_comment")
{ {
ScopedFastFlag luauStartingBrokenComment{"LuauStartingBrokenComment", true};
const char* expected = "Expected identifier when parsing expression, got unfinished comment"; const char* expected = "Expected identifier when parsing expression, got unfinished comment";
matchParseError("--[[unfinished work", expected); matchParseError("--[[unfinished work", expected);

View file

@ -73,7 +73,7 @@ public:
private: private:
std::unique_ptr<lua_State, void (*)(lua_State*)> luaState; std::unique_ptr<lua_State, void (*)(lua_State*)> luaState;
// This is a simplicitic and incomplete pretty printer. // This is a simplistic and incomplete pretty printer.
// It is included here to test that the pretty printer hook is being called. // It is included here to test that the pretty printer hook is being called.
// More elaborate tests to ensure correct output can be added if we introduce // More elaborate tests to ensure correct output can be added if we introduce
// a more feature rich pretty printer. // a more feature rich pretty printer.
@ -158,12 +158,25 @@ TEST_CASE_FIXTURE(ReplFixture, "CompleteGlobalVariables")
myvariable1 = 5 myvariable1 = 5
myvariable2 = 5 myvariable2 = 5
)"); )");
CompletionSet completions = getCompletionSet("myvar"); {
// Try to complete globals that are added by the user's script
CompletionSet completions = getCompletionSet("myvar");
std::string prefix = ""; std::string prefix = "";
CHECK(completions.size() == 2); CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "myvariable1")); CHECK(checkCompletion(completions, prefix, "myvariable1"));
CHECK(checkCompletion(completions, prefix, "myvariable2")); CHECK(checkCompletion(completions, prefix, "myvariable2"));
}
{
// Try completing some builtin functions
CompletionSet completions = getCompletionSet("math.m");
std::string prefix = "math.";
CHECK(completions.size() == 3);
CHECK(checkCompletion(completions, prefix, "max("));
CHECK(checkCompletion(completions, prefix, "min("));
CHECK(checkCompletion(completions, prefix, "modf("));
}
} }
TEST_CASE_FIXTURE(ReplFixture, "CompleteTableKeys") TEST_CASE_FIXTURE(ReplFixture, "CompleteTableKeys")
@ -206,4 +219,188 @@ TEST_CASE_FIXTURE(ReplFixture, "StringMethods")
} }
} }
TEST_CASE_FIXTURE(ReplFixture, "TableWithMetatableIndexTable")
{
runCode(L, R"(
-- Create 't' which is a table with a metatable with an __index table
mt = {}
mt.__index = mt
t = {}
setmetatable(t, mt)
mt.mtkey1 = {x="x value", y="y value", 1, 2}
mt.mtkey2 = 2
t.tkey1 = {data1 = 2, data2 = "str", 3, 4}
t.tkey2 = 4
)");
{
CompletionSet completions = getCompletionSet("t.t");
std::string prefix = "t.";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "tkey1"));
CHECK(checkCompletion(completions, prefix, "tkey2"));
}
{
CompletionSet completions = getCompletionSet("t.tkey1.data2:re");
std::string prefix = "t.tkey1.data2:";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "rep("));
CHECK(checkCompletion(completions, prefix, "reverse("));
}
{
CompletionSet completions = getCompletionSet("t.mtk");
std::string prefix = "t.";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "mtkey1"));
CHECK(checkCompletion(completions, prefix, "mtkey2"));
}
{
CompletionSet completions = getCompletionSet("t.mtkey1.");
std::string prefix = "t.mtkey1.";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "x"));
CHECK(checkCompletion(completions, prefix, "y"));
}
}
TEST_CASE_FIXTURE(ReplFixture, "TableWithMetatableIndexFunction")
{
runCode(L, R"(
-- Create 't' which is a table with a metatable with an __index function
mt = {}
mt.__index = function(table, key)
print("mt.__index called")
if key == "foo" then
return "FOO"
elseif key == "bar" then
return "BAR"
else
return nil
end
end
t = {}
setmetatable(t, mt)
t.tkey = 0
)");
{
CompletionSet completions = getCompletionSet("t.t");
std::string prefix = "t.";
CHECK(completions.size() == 1);
CHECK(checkCompletion(completions, prefix, "tkey"));
}
{
// t.foo is a valid key, but should not be completed because it requires calling an __index function
CompletionSet completions = getCompletionSet("t.foo");
CHECK(completions.size() == 0);
}
{
// t.foo is a valid key, but should not be found because it requires calling an __index function
CompletionSet completions = getCompletionSet("t.foo:");
CHECK(completions.size() == 0);
}
}
TEST_CASE_FIXTURE(ReplFixture, "TableWithMultipleMetatableIndexTables")
{
runCode(L, R"(
-- Create a table with a chain of metatables
mt2 = {}
mt2.__index = mt2
mt = {}
mt.__index = mt
setmetatable(mt, mt2)
t = {}
setmetatable(t, mt)
mt2.mt2key = {x=1, y=2}
mt.mtkey = 2
t.tkey = 3
)");
{
CompletionSet completions = getCompletionSet("t.");
std::string prefix = "t.";
CHECK(completions.size() == 4);
CHECK(checkCompletion(completions, prefix, "__index"));
CHECK(checkCompletion(completions, prefix, "tkey"));
CHECK(checkCompletion(completions, prefix, "mtkey"));
CHECK(checkCompletion(completions, prefix, "mt2key"));
}
{
CompletionSet completions = getCompletionSet("t.__index.");
std::string prefix = "t.__index.";
CHECK(completions.size() == 3);
CHECK(checkCompletion(completions, prefix, "__index"));
CHECK(checkCompletion(completions, prefix, "mtkey"));
CHECK(checkCompletion(completions, prefix, "mt2key"));
}
{
CompletionSet completions = getCompletionSet("t.mt2key.");
std::string prefix = "t.mt2key.";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "x"));
CHECK(checkCompletion(completions, prefix, "y"));
}
}
TEST_CASE_FIXTURE(ReplFixture, "TableWithDeepMetatableIndexTables")
{
runCode(L, R"(
-- Creates a table with a chain of metatables of length `count`
function makeChainedTable(count)
local result = {}
result.__index = result
result[string.format("entry%d", count)] = { count = count }
if count == 0 then
return result
else
return setmetatable(result, makeChainedTable(count - 1))
end
end
t30 = makeChainedTable(30)
t60 = makeChainedTable(60)
)");
{
// Check if entry0 exists
CompletionSet completions = getCompletionSet("t30.entry0");
std::string prefix = "t30.";
CHECK(checkCompletion(completions, prefix, "entry0"));
}
{
// Check if entry0.count exists
CompletionSet completions = getCompletionSet("t30.entry0.co");
std::string prefix = "t30.entry0.";
CHECK(checkCompletion(completions, prefix, "count"));
}
{
// Check if entry0 exists. With the max traversal limit of 50 in the repl, this should fail.
CompletionSet completions = getCompletionSet("t60.entry0");
CHECK(completions.size() == 0);
}
{
// Check if entry0.count exists. With the max traversal limit of 50 in the repl, this should fail.
CompletionSet completions = getCompletionSet("t60.entry0.co");
CHECK(completions.size() == 0);
}
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -1,6 +1,6 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/RequireTracer.h" #include "Luau/RequireTracer.h"
#include "Luau/Parser.h"
#include "Fixture.h" #include "Fixture.h"

View file

@ -598,15 +598,13 @@ TEST_CASE_FIXTURE(Fixture, "generic_typevars_are_not_considered_to_escape_their_
/* /*
* The two-pass alias definition system starts by ascribing a free TypeVar to each alias. It then * The two-pass alias definition system starts by ascribing a free TypeVar to each alias. It then
* circles back to fill in the actual type later on. * circles back to fill in the actual type later on.
* *
* If this free type is unified with something degenerate like `any`, we need to take extra care * If this free type is unified with something degenerate like `any`, we need to take extra care
* to ensure that the alias actually binds to the type that the user expected. * to ensure that the alias actually binds to the type that the user expected.
*/ */
TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_unification_with_any") TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_unification_with_any")
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {{"LuauTwoPassAliasDefinitionFix", true}};
{"LuauTwoPassAliasDefinitionFix", true}
};
CheckResult result = check(R"( CheckResult result = check(R"(
local function x() local function x()

View file

@ -1,6 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"

View file

@ -1,6 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,6 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Fixture.h" #include "Fixture.h"

View file

@ -426,8 +426,6 @@ TEST_CASE_FIXTURE(Fixture, "if_then_else_expression_singleton_options")
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauExpectedTypesOfProperties", true}, {"LuauExpectedTypesOfProperties", true},
{"LuauIfElseExpectedType2", true},
{"LuauIfElseBranchTypeUnion", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(

View file

@ -1,6 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
@ -2168,8 +2167,6 @@ b()
TEST_CASE_FIXTURE(Fixture, "length_operator_union") TEST_CASE_FIXTURE(Fixture, "length_operator_union")
{ {
ScopedFastFlag luauLengthOnCompositeType{"LuauLengthOnCompositeType", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local x: {number} | {string} local x: {number} | {string}
local y = #x local y = #x
@ -2180,8 +2177,6 @@ local y = #x
TEST_CASE_FIXTURE(Fixture, "length_operator_intersection") TEST_CASE_FIXTURE(Fixture, "length_operator_intersection")
{ {
ScopedFastFlag luauLengthOnCompositeType{"LuauLengthOnCompositeType", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local x: {number} & {z:string} -- mixed tables are evil local x: {number} & {z:string} -- mixed tables are evil
local y = #x local y = #x
@ -2192,8 +2187,6 @@ local y = #x
TEST_CASE_FIXTURE(Fixture, "length_operator_non_table_union") TEST_CASE_FIXTURE(Fixture, "length_operator_non_table_union")
{ {
ScopedFastFlag luauLengthOnCompositeType{"LuauLengthOnCompositeType", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local x: {number} | any | string local x: {number} | any | string
local y = #x local y = #x
@ -2204,8 +2197,6 @@ local y = #x
TEST_CASE_FIXTURE(Fixture, "length_operator_union_errors") TEST_CASE_FIXTURE(Fixture, "length_operator_union_errors")
{ {
ScopedFastFlag luauLengthOnCompositeType{"LuauLengthOnCompositeType", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local x: {number} | number | string local x: {number} | number | string
local y = #x local y = #x
@ -2214,4 +2205,38 @@ local y = #x
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
} }
TEST_CASE_FIXTURE(Fixture, "dont_hang_when_trying_to_look_up_in_cyclic_metatable_index")
{
ScopedFastFlag sff{"LuauTerminateCyclicMetatableIndexLookup", true};
// t :: t1 where t1 = {metatable {__index: t1, __tostring: (t1) -> string}}
CheckResult result = check(R"(
local mt = {}
local t = setmetatable({}, mt)
mt.__index = t
function mt:__tostring()
return t.p
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 't' does not have key 'p'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "give_up_after_one_metatable_index_look_up")
{
CheckResult result = check(R"(
local data = { x = 5 }
local t1 = setmetatable({}, { __index = data })
local t2 = setmetatable({}, t1) -- note: must be t1, not a new table
local x1 = t1.x -- ok
local x2 = t2.x -- nope
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 't2' does not have key 'x'", toString(result.errors[0]));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -2,7 +2,6 @@
#include "Luau/AstQuery.h" #include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Parser.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
@ -4654,8 +4653,6 @@ a = setmetatable(a, { __call = function(x) end })
TEST_CASE_FIXTURE(Fixture, "infer_through_group_expr") TEST_CASE_FIXTURE(Fixture, "infer_through_group_expr")
{ {
ScopedFastFlag luauGroupExpectedType{"LuauGroupExpectedType", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local function f(a: (number, number) -> number) return a(1, 3) end local function f(a: (number, number) -> number) return a(1, 3) end
f(((function(a, b) return a + b end))) f(((function(a, b) return a + b end)))
@ -4735,21 +4732,14 @@ local a = if false then "a" elseif false then "b" else "c"
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_type_union") TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_type_union")
{ {
ScopedFastFlag sff3{"LuauIfElseBranchTypeUnion", true}; CheckResult result = check(R"(local a: number? = if true then 42 else nil)");
{ LUAU_REQUIRE_NO_ERRORS(result);
CheckResult result = check(R"(local a: number? = if true then 42 else nil)"); CHECK_EQ(toString(requireType("a"), {true}), "number?");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a"), {true}), "number?");
}
} }
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_1") TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_1")
{ {
ScopedFastFlag luauIfElseExpectedType2{"LuauIfElseExpectedType2", true};
ScopedFastFlag luauIfElseBranchTypeUnion{"LuauIfElseBranchTypeUnion", true};
CheckResult result = check(R"( CheckResult result = check(R"(
type X = {number | string} type X = {number | string}
local a: X = if true then {"1", 2, 3} else {4, 5, 6} local a: X = if true then {"1", 2, 3} else {4, 5, 6}
@ -4761,9 +4751,6 @@ local a: X = if true then {"1", 2, 3} else {4, 5, 6}
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_2") TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_2")
{ {
ScopedFastFlag luauIfElseExpectedType2{"LuauIfElseExpectedType2", true};
ScopedFastFlag luauIfElseBranchTypeUnion{"LuauIfElseBranchTypeUnion", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local a: number? = if true then 1 else nil local a: number? = if true then 1 else nil
)"); )");
@ -4773,8 +4760,6 @@ local a: number? = if true then 1 else nil
TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_3") TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions_expected_type_3")
{ {
ScopedFastFlag luauIfElseExpectedType2{"LuauIfElseExpectedType2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local function times<T>(n: any, f: () -> T) local function times<T>(n: any, f: () -> T)
local result: {T} = {} local result: {T} = {}
@ -5058,8 +5043,6 @@ end
TEST_CASE_FIXTURE(Fixture, "recursive_metatable_crash") TEST_CASE_FIXTURE(Fixture, "recursive_metatable_crash")
{ {
ScopedFastFlag luauMetatableAreEqualRecursion{"LuauMetatableAreEqualRecursion", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local function getIt() local function getIt()
local y local y
@ -5076,8 +5059,6 @@ local c = a or b
TEST_CASE_FIXTURE(Fixture, "bound_typepack_promote") TEST_CASE_FIXTURE(Fixture, "bound_typepack_promote")
{ {
ScopedFastFlag luauCommittingTxnLogFreeTpPromote{"LuauCommittingTxnLogFreeTpPromote", true};
// No assertions should trigger // No assertions should trigger
check(R"( check(R"(
local function p() local function p()
@ -5251,7 +5232,6 @@ TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_string_singleton")
{"LuauDiscriminableUnions2", true}, {"LuauDiscriminableUnions2", true},
{"LuauRefactorTypeVarQuestions", true}, {"LuauRefactorTypeVarQuestions", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauLengthOnCompositeType", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -5272,7 +5252,6 @@ TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_union_of_string_singleton")
{"LuauDiscriminableUnions2", true}, {"LuauDiscriminableUnions2", true},
{"LuauRefactorTypeVarQuestions", true}, {"LuauRefactorTypeVarQuestions", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauLengthOnCompositeType", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,6 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
@ -930,8 +929,6 @@ type R = { m: F<R> }
TEST_CASE_FIXTURE(Fixture, "pack_tail_unification_check") TEST_CASE_FIXTURE(Fixture, "pack_tail_unification_check")
{ {
ScopedFastFlag luauUnifyPackTails{"LuauUnifyPackTails", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local a: () -> (number, ...string) local a: () -> (number, ...string)
local b: () -> (number, ...boolean) local b: () -> (number, ...boolean)

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -1,5 +1,4 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"

View file

@ -371,6 +371,15 @@ do
st, msg = coroutine.close(co) st, msg = coroutine.close(co)
assert(st and msg == nil) assert(st and msg == nil)
assert(f() == 42) assert(f() == 42)
-- closing a coroutine with a large stack
co = coroutine.create(function()
local function f(depth) return if depth > 0 then f(depth - 1) + depth else 0 end
coroutine.yield(f(100))
end)
assert(coroutine.resume(co))
st, msg = coroutine.close(co)
assert(st and msg == nil)
end end
return 'OK' return 'OK'

View file

@ -49,16 +49,24 @@ foo()
c = getcoverage(foo) c = getcoverage(foo)
assert(#c == 1) assert(#c == 1)
assert(c[1].name == "foo") assert(c[1].name == "foo")
assert(c[1].linedefined == 4)
assert(c[1].depth == 0)
assert(validate(c[1], {5, 6, 7}, {})) assert(validate(c[1], {5, 6, 7}, {}))
bar() bar()
c = getcoverage(bar) c = getcoverage(bar)
assert(#c == 3) assert(#c == 3)
assert(c[1].name == "bar") assert(c[1].name == "bar")
assert(c[1].linedefined == 10)
assert(c[1].depth == 0)
assert(validate(c[1], {11, 15, 19}, {})) assert(validate(c[1], {11, 15, 19}, {}))
assert(c[2].name == "one") assert(c[2].name == "one")
assert(c[2].linedefined == 11)
assert(c[2].depth == 1)
assert(validate(c[2], {12}, {})) assert(validate(c[2], {12}, {}))
assert(c[3].name == nil) assert(c[3].name == nil)
assert(c[3].linedefined == 15)
assert(c[3].depth == 1)
assert(validate(c[3], {}, {16})) assert(validate(c[3], {}, {16}))
return 'OK' return 'OK'

View file

@ -76,6 +76,9 @@ assert(baz(co, 2, "n") == nil)
assert(baz(math.sqrt, "n") == "sqrt") assert(baz(math.sqrt, "n") == "sqrt")
assert(baz(math.sqrt, "f") == math.sqrt) -- yes this is pointless assert(baz(math.sqrt, "f") == math.sqrt) -- yes this is pointless
local t = { foo = function() return 1 end }
assert(baz(t.foo, "n") == "foo")
-- info multi-arg returns -- info multi-arg returns
function quux(...) function quux(...)
return {debug.info(...)} return {debug.info(...)}

View file

@ -10,6 +10,9 @@
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#endif #endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h> // IsDebuggerPresent #include <Windows.h> // IsDebuggerPresent
#endif #endif
@ -52,7 +55,7 @@ static bool debuggerPresent()
#endif #endif
} }
static int assertionHandler(const char* expr, const char* file, int line, const char* function) static int testAssertionHandler(const char* expr, const char* file, int line, const char* function)
{ {
if (debuggerPresent()) if (debuggerPresent())
LUAU_DEBUGBREAK(); LUAU_DEBUGBREAK();
@ -218,7 +221,7 @@ static void setFastFlags(const std::vector<doctest::String>& flags)
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
Luau::assertHandler() = assertionHandler; Luau::assertHandler() = testAssertionHandler;
doctest::registerReporter<BoostLikeReporter>("boost", 0, true); doctest::registerReporter<BoostLikeReporter>("boost", 0, true);

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="Luau::AnyTypeVar">
<DisplayString>AnyTypeVar</DisplayString>
</Type>
<Type Name="Luau::Variant&lt;*&gt;">
<DisplayString Condition="typeId == 0" Optional="true">{{ index=0, value={*($T1*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 1" Optional="true">{{ index=1, value={*($T2*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 2" Optional="true">{{ index=2, value={*($T3*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 3" Optional="true">{{ index=3, value={*($T4*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 4" Optional="true">{{ index=4, value={*($T5*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 5" Optional="true">{{ index=5, value={*($T6*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 6" Optional="true">{{ index=6, value={*($T7*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 7" Optional="true">{{ index=7, value={*($T8*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 8" Optional="true">{{ index=8, value={*($T9*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 9" Optional="true">{{ index=9, value={*($T10*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 10" Optional="true">{{ index=10, value={*($T11*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 11" Optional="true">{{ index=11, value={*($T12*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 12" Optional="true">{{ index=12, value={*($T13*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 13" Optional="true">{{ index=13, value={*($T14*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 14" Optional="true">{{ index=14, value={*($T15*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 15" Optional="true">{{ index=15, value={*($T16*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 16" Optional="true">{{ index=16, value={*($T17*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 17" Optional="true">{{ index=17, value={*($T18*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 18" Optional="true">{{ index=18, value={*($T19*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 19" Optional="true">{{ index=19, value={*($T20*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 20" Optional="true">{{ index=20, value={*($T21*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 21" Optional="true">{{ index=21, value={*($T22*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 22" Optional="true">{{ index=22, value={*($T23*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 23" Optional="true">{{ index=23, value={*($T24*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 24" Optional="true">{{ index=24, value={*($T25*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 25" Optional="true">{{ index=25, value={*($T26*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 26" Optional="true">{{ index=26, value={*($T27*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 27" Optional="true">{{ index=27, value={*($T28*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 28" Optional="true">{{ index=28, value={*($T29*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 29" Optional="true">{{ index=29, value={*($T30*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 30" Optional="true">{{ index=30, value={*($T31*)storage} }}</DisplayString>
<DisplayString Condition="typeId == 31" Optional="true">{{ index=31, value={*($T32*)storage} }}</DisplayString>
<Expand>
<Item Name="index">typeId</Item>
<Item Name="[value]" Condition="typeId == 0" Optional="true">*($T1*)storage</Item>
<Item Name="[value]" Condition="typeId == 1" Optional="true">*($T2*)storage</Item>
<Item Name="[value]" Condition="typeId == 2" Optional="true">*($T3*)storage</Item>
<Item Name="[value]" Condition="typeId == 3" Optional="true">*($T4*)storage</Item>
<Item Name="[value]" Condition="typeId == 4" Optional="true">*($T5*)storage</Item>
<Item Name="[value]" Condition="typeId == 5" Optional="true">*($T6*)storage</Item>
<Item Name="[value]" Condition="typeId == 6" Optional="true">*($T7*)storage</Item>
<Item Name="[value]" Condition="typeId == 7" Optional="true">*($T8*)storage</Item>
<Item Name="[value]" Condition="typeId == 8" Optional="true">*($T9*)storage</Item>
<Item Name="[value]" Condition="typeId == 9" Optional="true">*($T10*)storage</Item>
<Item Name="[value]" Condition="typeId == 10" Optional="true">*($T11*)storage</Item>
<Item Name="[value]" Condition="typeId == 11" Optional="true">*($T12*)storage</Item>
<Item Name="[value]" Condition="typeId == 12" Optional="true">*($T13*)storage</Item>
<Item Name="[value]" Condition="typeId == 13" Optional="true">*($T14*)storage</Item>
<Item Name="[value]" Condition="typeId == 14" Optional="true">*($T15*)storage</Item>
<Item Name="[value]" Condition="typeId == 15" Optional="true">*($T16*)storage</Item>
<Item Name="[value]" Condition="typeId == 16" Optional="true">*($T17*)storage</Item>
<Item Name="[value]" Condition="typeId == 17" Optional="true">*($T18*)storage</Item>
<Item Name="[value]" Condition="typeId == 18" Optional="true">*($T19*)storage</Item>
<Item Name="[value]" Condition="typeId == 19" Optional="true">*($T20*)storage</Item>
<Item Name="[value]" Condition="typeId == 20" Optional="true">*($T21*)storage</Item>
<Item Name="[value]" Condition="typeId == 21" Optional="true">*($T22*)storage</Item>
<Item Name="[value]" Condition="typeId == 22" Optional="true">*($T23*)storage</Item>
<Item Name="[value]" Condition="typeId == 23" Optional="true">*($T24*)storage</Item>
<Item Name="[value]" Condition="typeId == 24" Optional="true">*($T25*)storage</Item>
<Item Name="[value]" Condition="typeId == 25" Optional="true">*($T26*)storage</Item>
<Item Name="[value]" Condition="typeId == 26" Optional="true">*($T27*)storage</Item>
<Item Name="[value]" Condition="typeId == 27" Optional="true">*($T28*)storage</Item>
<Item Name="[value]" Condition="typeId == 28" Optional="true">*($T29*)storage</Item>
<Item Name="[value]" Condition="typeId == 29" Optional="true">*($T30*)storage</Item>
<Item Name="[value]" Condition="typeId == 30" Optional="true">*($T31*)storage</Item>
<Item Name="[value]" Condition="typeId == 31" Optional="true">*($T32*)storage</Item>
</Expand>
</Type>
</AutoVisualizer>

25
tools/natvis/Ast.natvis Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="Luau::AstArray&lt;*&gt;">
<DisplayString>AstArray size={size}</DisplayString>
<Expand>
<Item Name="[size]">size</Item>
<ArrayItems>
<Size>size</Size>
<ValuePointer>data</ValuePointer>
</ArrayItems>
</Expand>
</Type>
<Type Name="Luau::TempVector&lt;*&gt;">
<Expand>
<Item Name="[size]">size_</Item>
<ArrayItems>
<Size>size_</Size>
<ValuePointer>storage._Mypair._Myval2._Myfirst + offset</ValuePointer>
</ArrayItems>
</Expand>
</Type>
</AutoVisualizer>

269
tools/natvis/VM.natvis Normal file
View file

@ -0,0 +1,269 @@
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="::lua_TValue">
<DisplayString Condition="tt == lua_Type::LUA_TNIL">nil</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TBOOLEAN">{(bool)value.b}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">lightuserdata {value.p}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TNUMBER">number = {value.n}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TVECTOR">vector = {value.v[0]}, {value.v[1]}, {*(float*)&amp;extra}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TSTRING">{value.gc->ts}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TTABLE">{value.gc->h}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TFUNCTION">function {value.gc->cl,view(short)}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TUSERDATA">userdata {value.gc->u}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TTHREAD">thread {value.gc->th}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TPROTO">proto {value.gc->p}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TUPVAL">upvalue {value.gc->uv}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TDEADKEY">deadkey</DisplayString>
<DisplayString>empty</DisplayString>
<Expand>
<Item Name="[lightuserdata]" Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">value.p</Item>
<Item Name="[string]" Condition="tt == lua_Type::LUA_TSTRING">value.gc->ts</Item>
<Item Name="[table]" Condition="tt == lua_Type::LUA_TTABLE">value.gc->h</Item>
<Item Name="[C function]" Condition="tt == lua_Type::LUA_TFUNCTION &amp;&amp; value.gc-&gt;cl.isC">value.gc->cl</Item>
<Item Name="[Lua function]" Condition="tt == lua_Type::LUA_TFUNCTION &amp;&amp; !value.gc-&gt;cl.isC">value.gc->cl</Item>
<Item Name="[userdata]" Condition="tt == lua_Type::LUA_TUSERDATA">value.gc->u</Item>
<Item Name="[thread]" Condition="tt == lua_Type::LUA_TTHREAD">value.gc->th</Item>
<Item Name="[proto]" Condition="tt == lua_Type::LUA_TPROTO">value.gc->p</Item>
<Item Name="[upvalue]" Condition="tt == lua_Type::LUA_TUPVAL">value.gc->uv</Item>
<Synthetic Name="[gc]" Condition="tt >= lua_Type::LUA_TSTRING">
<DisplayString Condition="value.gc-&gt;gch.marked &amp; 8">fixed ({(int)value.gc-&gt;gch.marked})</DisplayString>
<DisplayString Condition="value.gc-&gt;gch.marked &amp; 4">black ({(int)value.gc-&gt;gch.marked})</DisplayString>
<DisplayString Condition="value.gc-&gt;gch.marked &amp; 1">white ({(int)value.gc-&gt;gch.marked})</DisplayString>
<DisplayString Condition="value.gc-&gt;gch.marked &amp; 2">white ({(int)value.gc-&gt;gch.marked})</DisplayString>
<DisplayString Condition="(value.gc-&gt;gch.marked &amp; 3) == 0">gray ({(int)value.gc-&gt;gch.marked})</DisplayString>
</Synthetic>
</Expand>
</Type>
<Type Name="::TKey">
<DisplayString Condition="tt == lua_Type::LUA_TNIL">nil</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TBOOLEAN">{(bool)value.b}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">lightuserdata {value.p}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TNUMBER">number = {value.n}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TVECTOR">vector = {value.v[0]}, {value.v[1]}, {*(float*)&amp;extra}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TSTRING">{value.gc->ts}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TTABLE">{value.gc->h}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TFUNCTION">function {value.gc->cl,view(short)}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TUSERDATA">userdata {value.gc->u}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TTHREAD">thread {value.gc->th}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TPROTO">proto {value.gc->p}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TUPVAL">upvalue {value.gc->uv}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TDEADKEY">deadkey</DisplayString>
<DisplayString>empty</DisplayString>
<Expand>
<Item Name="[lightuserdata]" Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">(void**)value.p</Item>
<Item Name="[string]" Condition="tt == lua_Type::LUA_TSTRING">value.gc->ts</Item>
<Item Name="[table]" Condition="tt == lua_Type::LUA_TTABLE">value.gc->h</Item>
<Item Name="[C function]" Condition="tt == lua_Type::LUA_TFUNCTION &amp;&amp; value.gc-&gt;cl.isC">value.gc->cl</Item>
<Item Name="[Lua function]" Condition="tt == lua_Type::LUA_TFUNCTION &amp;&amp; !value.gc-&gt;cl.isC">value.gc->cl</Item>
<Item Name="[userdata]" Condition="tt == lua_Type::LUA_TUSERDATA">value.gc->u</Item>
<Item Name="[thread]" Condition="tt == lua_Type::LUA_TTHREAD">value.gc->th</Item>
<Item Name="[proto]" Condition="tt == lua_Type::LUA_TPROTO">value.gc->p</Item>
<Item Name="[upvalue]" Condition="tt == lua_Type::LUA_TUPVAL">value.gc->uv</Item>
<Item Name="[next]">next</Item>
</Expand>
</Type>
<Type Name="::LuaNode">
<DisplayString Condition="key.tt != lua_Type::LUA_TNIL || val.tt != lua_Type::LUA_TNIL">{key,na} = {val}</DisplayString>
<DisplayString Condition="key.tt == lua_Type::LUA_TNIL &amp;&amp; val.tt == 0">---</DisplayString>
</Type>
<Type Name="::Table">
<DisplayString>table</DisplayString>
<Expand>
<Item Name="metatable" Condition="metatable">metatable</Item>
<Synthetic Name="hash">
<DisplayString>[size] {1&lt;&lt;lsizenode}</DisplayString>
<Expand>
<IndexListItems>
<Size>1&lt;&lt;lsizenode</Size>
<ValueNode>node[$i]</ValueNode>
</IndexListItems>
</Expand>
</Synthetic>
<Synthetic Name="array">
<DisplayString>[size] {sizearray}</DisplayString>
<Expand>
<IndexListItems>
<Size>sizearray</Size>
<ValueNode>array[$i]</ValueNode>
</IndexListItems>
</Expand>
</Synthetic>
</Expand>
</Type>
<Type Name="::Udata">
<Expand>
<CustomListItems>
<Variable Name="count" InitialValue="1&lt;&lt;metatable->lsizenode" />
<Variable Name="i" InitialValue="0" />
<Size>1</Size>
<Loop>
<Break Condition="i &gt;= count" />
<If Condition="metatable->node[i].key.tt == lua_Type::LUA_TSTRING">
<If Condition="strcmp(metatable-&gt;node[i].key.value.gc->ts.data, &quot;__type&quot;) == 0">
<Item Name="[type]">metatable-&gt;node[i].val</Item>
<Break/>
</If>
</If>
<Exec>i = i + 1</Exec>
</Loop>
<If Condition="i == count">
<Item Name="[type]">"unknown",sb</Item>
</If>
</CustomListItems>
<Item Name="tag">tag</Item>
<Item Name="len">len</Item>
<Item Name="metatable">metatable</Item>
<Item Name="data">data</Item>
</Expand>
</Type>
<Type Name="::Closure">
<DisplayString Condition="isC == 1" IncludeView="short">{c.f,na}</DisplayString>
<DisplayString Condition="isC == 0" IncludeView="short">{l.p,na}</DisplayString>
<DisplayString Condition="isC == 1" ExcludeView="short">{c}</DisplayString>
<DisplayString Condition="isC == 0" ExcludeView="short">{l}</DisplayString>
<DisplayString>invalid</DisplayString>
</Type>
<Type Name="::TString">
<DisplayString>{data,s}</DisplayString>
</Type>
<Type Name ="::lua_State">
<DisplayString Condition="ci-&gt;func-&gt;value.gc-&gt;cl.isC">
{ci-&gt;func-&gt;value.gc-&gt;cl.c.f,na}
</DisplayString>
<DisplayString Condition="!ci-&gt;func-&gt;value.gc-&gt;cl.isC &amp;&amp; ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;debugname" Optional="true">
{ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;source-&gt;data,sb}:{ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;linedefined,d} {ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;debugname->data,sb}
</DisplayString>
<DisplayString Condition="!ci-&gt;func-&gt;value.gc-&gt;cl.isC" Optional="true">
{ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;source-&gt;data,sb}:{ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;linedefined,d}
</DisplayString>
<DisplayString>thread</DisplayString>
<Expand>
<Synthetic Name="[call stack]">
<DisplayString>{ci-base_ci} frames</DisplayString>
<Expand>
<IndexListItems>
<Size>ci-base_ci</Size>
<!-- the +1 is omitted here to avoid some issues with a blank call -->
<ValueNode>
base_ci[ci-base_ci - $i].func-&gt;value.gc-&gt;cl,view(short)
</ValueNode>
</IndexListItems>
</Expand>
</Synthetic>
<Synthetic Name="[top frame stack]">
<DisplayString>{top-base} values</DisplayString>
<Expand>
<ArrayItems>
<Size>top-base</Size>
<ValuePointer>base</ValuePointer>
</ArrayItems>
</Expand>
</Synthetic>
<Synthetic Name="[stack]">
<DisplayString>{top-stack} values</DisplayString>
<Expand>
<ArrayItems>
<Size>top-stack</Size>
<ValuePointer>stack</ValuePointer>
</ArrayItems>
</Expand>
</Synthetic>
<Synthetic Name="[open upvals]" Condition="openupval != 0">
<Expand>
<LinkedListItems>
<HeadPointer>openupval</HeadPointer>
<NextPointer>u.l.next</NextPointer>
<ValueNode>this</ValueNode>
</LinkedListItems>
</Expand>
</Synthetic>
<Item Name="l_gt">l_gt</Item>
<Item Name="env">env</Item>
<Item Name="userdata">userdata</Item>
</Expand>
</Type>
<Type Name="::Proto">
<DisplayString Condition="debugname">{source->data,sb}:{linedefined} function {debugname->data,sb} [{(int)numparams} arg, {(int)nups} upval]</DisplayString>
<DisplayString Condition="!debugname">{source->data,sb}:{linedefined} [{(int)numparams} arg, {(int)nups} upval]</DisplayString>
<Expand>
<Item Name="debugname">debugname</Item>
<Synthetic Name="[constants]">
<DisplayString>constants</DisplayString>
<Expand>
<IndexListItems>
<Size>sizek</Size>
<ValueNode>k[$i]</ValueNode>
</IndexListItems>
</Expand>
</Synthetic>
<Synthetic Name="[locals]">
<DisplayString>locals</DisplayString>
<Expand>
<IndexListItems>
<Size>sizelocvars</Size>
<ValueNode>locvars[$i]</ValueNode>
</IndexListItems>
</Expand>
</Synthetic>
<Synthetic Name="[bytecode]">
<DisplayString>bytecode</DisplayString>
<Expand>
<IndexListItems>
<Size>sizecode</Size>
<ValueNode>code[$i]</ValueNode>
</IndexListItems>
</Expand>
</Synthetic>
<Synthetic Name="[functions]">
<DisplayString>functions</DisplayString>
<Expand>
<IndexListItems>
<Size>sizep</Size>
<ValueNode>p[$i]</ValueNode>
</IndexListItems>
</Expand>
</Synthetic>
<Synthetic Name="[upvals]">
<DisplayString>upvals</DisplayString>
<Expand>
<IndexListItems>
<Size>sizeupvalues</Size>
<ValueNode>upvalues[$i]</ValueNode>
</IndexListItems>
</Expand>
</Synthetic>
<Item Name="source">source</Item>
</Expand>
</Type>
<Type Name="::GCheader">
<Expand>
<Synthetic Name="[type]">
<DisplayString>{(lua_Type)tt}</DisplayString>
</Synthetic>
<Synthetic Name="[gc]">
<DisplayString Condition="marked &amp; 8">fixed ({(int)marked})</DisplayString>
<DisplayString Condition="marked &amp; 4">black ({(int)marked})</DisplayString>
<DisplayString Condition="marked &amp; 1">white ({(int)marked})</DisplayString>
<DisplayString Condition="marked &amp; 2">white ({(int)marked})</DisplayString>
<DisplayString Condition="(marked &amp; 3) == 0">gray ({(int)marked})</DisplayString>
<DisplayString>unknown</DisplayString>
</Synthetic>
<Item Name="memcat">memcat</Item>
</Expand>
</Type>
</AutoVisualizer>