Sync to upstream/release/501 (#20)

Co-authored-by: Rodactor <rodactor@roblox.com>
This commit is contained in:
Arseny Kapoulkine 2021-10-29 13:25:12 -07:00
parent 12b2838de0
commit d01addc625
336 changed files with 118239 additions and 0 deletions

25
.clang-format Normal file
View file

@ -0,0 +1,25 @@
BasedOnStyle: LLVM
AccessModifierOffset: -4
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortLambdasOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
BreakBeforeBraces: Allman
BreakConstructorInitializers: BeforeComma
BreakInheritanceList: BeforeComma
ColumnLimit: 150
IndentCaseLabels: false
SortIncludes: false
IndentWidth: 4
TabWidth: 4
ObjCBlockIndentWidth: 4
AlignAfterOpenBracket: DontAlign
UseTab: Never
PointerAlignment: Left
SpaceAfterTemplateKeyword: false
AlignEscapedNewlines: DontAlign
AlwaysBreakTemplateDeclarations: Yes
MaxEmptyLinesToKeep: 10

View file

@ -0,0 +1,63 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Ast.h"
#include "Luau/Documentation.h"
#include <memory>
namespace Luau
{
struct Binding;
struct SourceModule;
struct Module;
struct TypeVar;
using TypeId = const TypeVar*;
using ScopePtr = std::shared_ptr<struct Scope>;
struct ExprOrLocal
{
AstExpr* getExpr()
{
return expr;
}
AstLocal* getLocal()
{
return local;
}
void setExpr(AstExpr* newExpr)
{
expr = newExpr;
local = nullptr;
}
void setLocal(AstLocal* newLocal)
{
local = newLocal;
expr = nullptr;
}
std::optional<Location> getLocation()
{
return expr ? expr->location : (local ? local->location : std::optional<Location>{});
}
private:
AstExpr* expr = nullptr;
AstLocal* local = nullptr;
};
std::vector<AstNode*> findAstAncestryOfPosition(const SourceModule& source, Position pos);
AstNode* findNodeAtPosition(const SourceModule& source, Position pos);
AstExpr* findExprAtPosition(const SourceModule& source, Position pos);
ScopePtr findScopeAtPosition(const Module& module, Position pos);
std::optional<Binding> findBindingAtPosition(const Module& module, const SourceModule& source, Position pos);
ExprOrLocal findExprOrLocalAtPosition(const SourceModule& source, Position pos);
std::optional<TypeId> findTypeAtPosition(const Module& module, const SourceModule& sourceModule, Position pos);
std::optional<TypeId> findExpectedTypeAtPosition(const Module& module, const SourceModule& sourceModule, Position pos);
std::optional<DocumentationSymbol> getDocumentationSymbolAtPosition(const SourceModule& source, const Module& module, Position position);
} // namespace Luau

View file

@ -0,0 +1,91 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Location.h"
#include "Luau/TypeVar.h"
#include <unordered_map>
#include <string>
#include <memory>
#include <optional>
namespace Luau
{
struct Frontend;
struct SourceModule;
struct Module;
struct TypeChecker;
using ModulePtr = std::shared_ptr<Module>;
enum class AutocompleteEntryKind
{
Property,
Binding,
Keyword,
String,
Type,
Module,
};
enum class ParenthesesRecommendation
{
None,
CursorAfter,
CursorInside,
};
enum class TypeCorrectKind
{
None,
Correct,
CorrectFunctionResult,
};
struct AutocompleteEntry
{
AutocompleteEntryKind kind = AutocompleteEntryKind::Property;
// Nullopt if kind is Keyword
std::optional<TypeId> type = std::nullopt;
bool deprecated = false;
// Only meaningful if kind is Property.
bool wrongIndexType = false;
// Set if this suggestion matches the type expected in the context
TypeCorrectKind typeCorrect = TypeCorrectKind::None;
std::optional<const ClassTypeVar*> containingClass = std::nullopt;
std::optional<const Property*> prop = std::nullopt;
std::optional<std::string> documentationSymbol = std::nullopt;
Tags tags;
ParenthesesRecommendation parens = ParenthesesRecommendation::None;
};
using AutocompleteEntryMap = std::unordered_map<std::string, AutocompleteEntry>;
struct AutocompleteResult
{
AutocompleteEntryMap entryMap;
std::vector<AstNode*> ancestry;
AutocompleteResult() = default;
AutocompleteResult(AutocompleteEntryMap entryMap, std::vector<AstNode*> ancestry)
: entryMap(std::move(entryMap))
, ancestry(std::move(ancestry))
{
}
};
using ModuleName = std::string;
using StringCompletionCallback = std::function<std::optional<AutocompleteEntryMap>(std::string tag, std::optional<const ClassTypeVar*> ctx)>;
struct OwningAutocompleteResult
{
AutocompleteResult result;
ModulePtr module;
std::unique_ptr<SourceModule> sourceModule;
};
AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback);
OwningAutocompleteResult autocompleteSource(Frontend& frontend, std::string_view source, Position position, StringCompletionCallback callback);
} // namespace Luau

View file

@ -0,0 +1,51 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "TypeInfer.h"
namespace Luau
{
void registerBuiltinTypes(TypeChecker& typeChecker);
TypeId makeUnion(TypeArena& arena, std::vector<TypeId>&& types);
TypeId makeIntersection(TypeArena& arena, std::vector<TypeId>&& types);
/** Build an optional 't'
*/
TypeId makeOption(TypeChecker& typeChecker, TypeArena& arena, TypeId t);
/** Small utility function for building up type definitions from C++.
*/
TypeId makeFunction( // Monomorphic
TypeArena& arena, std::optional<TypeId> selfType, std::initializer_list<TypeId> paramTypes, std::initializer_list<TypeId> retTypes);
TypeId makeFunction( // Polymorphic
TypeArena& arena, std::optional<TypeId> selfType, std::initializer_list<TypeId> generics, std::initializer_list<TypePackId> genericPacks,
std::initializer_list<TypeId> paramTypes, std::initializer_list<TypeId> retTypes);
TypeId makeFunction( // Monomorphic
TypeArena& arena, std::optional<TypeId> selfType, std::initializer_list<TypeId> paramTypes, std::initializer_list<std::string> paramNames,
std::initializer_list<TypeId> retTypes);
TypeId makeFunction( // Polymorphic
TypeArena& arena, std::optional<TypeId> selfType, std::initializer_list<TypeId> generics, std::initializer_list<TypePackId> genericPacks,
std::initializer_list<TypeId> paramTypes, std::initializer_list<std::string> paramNames, std::initializer_list<TypeId> retTypes);
void attachMagicFunction(TypeId ty, MagicFunction fn);
void attachFunctionTag(TypeId ty, std::string constraint);
Property makeProperty(TypeId ty, std::optional<std::string> documentationSymbol = std::nullopt);
void assignPropDocumentationSymbols(TableTypeVar::Props& props, const std::string& baseName);
std::string getBuiltinDefinitionSource();
void addGlobalBinding(TypeChecker& typeChecker, const std::string& name, TypeId ty, const std::string& packageName);
void addGlobalBinding(TypeChecker& typeChecker, const std::string& name, Binding binding);
void addGlobalBinding(TypeChecker& typeChecker, const ScopePtr& scope, const std::string& name, TypeId ty, const std::string& packageName);
void addGlobalBinding(TypeChecker& typeChecker, const ScopePtr& scope, const std::string& name, Binding binding);
std::optional<Binding> tryGetGlobalBinding(TypeChecker& typeChecker, const std::string& name);
Binding* tryGetGlobalBindingRef(TypeChecker& typeChecker, const std::string& name);
TypeId getGlobalBinding(TypeChecker& typeChecker, const std::string& name);
} // namespace Luau

View file

@ -0,0 +1,58 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Linter.h"
#include "Luau/ParseOptions.h"
#include <string>
#include <optional>
#include <vector>
namespace Luau
{
using ModuleName = std::string;
constexpr const char* kConfigName = ".luaurc";
struct Config
{
Config()
{
enabledLint.setDefaults();
}
Mode mode = Mode::NoCheck;
ParseOptions parseOptions;
LintOptions enabledLint;
LintOptions fatalLint;
bool lintErrors = false;
bool typeErrors = true;
std::vector<std::string> globals;
};
struct ConfigResolver
{
virtual ~ConfigResolver() {}
virtual const Config& getConfig(const ModuleName& name) const = 0;
};
struct NullConfigResolver : ConfigResolver
{
Config defaultConfig;
virtual const Config& getConfig(const ModuleName& name) const override;
};
std::optional<std::string> parseModeString(Mode& mode, const std::string& modeString, bool compat = false);
std::optional<std::string> parseLintRuleString(
LintOptions& enabledLints, LintOptions& fatalLints, const std::string& warningName, const std::string& value, bool compat = false);
std::optional<std::string> parseConfig(const std::string& contents, Config& config, bool compat = false);
} // namespace Luau

View file

@ -0,0 +1,50 @@
#pragma once
#include "Luau/DenseHash.h"
#include "Luau/Variant.h"
#include <string>
#include <vector>
namespace Luau
{
struct FunctionDocumentation;
struct TableDocumentation;
struct OverloadedFunctionDocumentation;
using Documentation = Luau::Variant<std::string, FunctionDocumentation, TableDocumentation, OverloadedFunctionDocumentation>;
using DocumentationSymbol = std::string;
struct FunctionParameterDocumentation
{
std::string name;
DocumentationSymbol documentation;
};
// Represents documentation for anything callable. This could be a method or a
// callback or a free function.
struct FunctionDocumentation
{
std::string documentation;
std::vector<FunctionParameterDocumentation> parameters;
std::vector<DocumentationSymbol> returns;
};
struct OverloadedFunctionDocumentation
{
// This is a map of function signature to overload symbol name.
Luau::DenseHashMap<std::string, DocumentationSymbol> overloads;
};
// Represents documentation for a table-like item, meaning "anything with keys".
// This could be a table or a class.
struct TableDocumentation
{
std::string documentation;
Luau::DenseHashMap<std::string, DocumentationSymbol> keys;
};
using DocumentationDatabase = Luau::DenseHashMap<DocumentationSymbol, Documentation>;
} // namespace Luau

View file

@ -0,0 +1,332 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/FileResolver.h"
#include "Luau/Location.h"
#include "Luau/TypeVar.h"
#include "Luau/Variant.h"
namespace Luau
{
struct TypeMismatch
{
TypeId wantedType;
TypeId givenType;
bool operator==(const TypeMismatch& rhs) const;
};
struct UnknownSymbol
{
enum Context
{
Binding,
Type,
Generic
};
Name name;
Context context;
bool operator==(const UnknownSymbol& rhs) const;
};
struct UnknownProperty
{
TypeId table;
Name key;
bool operator==(const UnknownProperty& rhs) const;
};
struct NotATable
{
TypeId ty;
bool operator==(const NotATable& rhs) const;
};
struct CannotExtendTable
{
enum Context
{
Property,
Indexer,
Metatable
};
TypeId tableType;
Context context;
Name prop;
bool operator==(const CannotExtendTable& rhs) const;
};
struct OnlyTablesCanHaveMethods
{
TypeId tableType;
bool operator==(const OnlyTablesCanHaveMethods& rhs) const;
};
struct DuplicateTypeDefinition
{
Name name;
Location previousLocation;
bool operator==(const DuplicateTypeDefinition& rhs) const;
};
struct CountMismatch
{
enum Context
{
Arg,
Result,
Return,
};
size_t expected;
size_t actual;
Context context = Arg;
bool operator==(const CountMismatch& rhs) const;
};
struct FunctionDoesNotTakeSelf
{
bool operator==(const FunctionDoesNotTakeSelf& rhs) const;
};
struct FunctionRequiresSelf
{
int requiredExtraNils = 0;
bool operator==(const FunctionRequiresSelf& rhs) const;
};
struct OccursCheckFailed
{
bool operator==(const OccursCheckFailed& rhs) const;
};
struct UnknownRequire
{
std::string modulePath;
bool operator==(const UnknownRequire& rhs) const;
};
struct IncorrectGenericParameterCount
{
Name name;
TypeFun typeFun;
size_t actualParameters;
bool operator==(const IncorrectGenericParameterCount& rhs) const;
};
struct SyntaxError
{
std::string message;
bool operator==(const SyntaxError& rhs) const;
};
struct CodeTooComplex
{
bool operator==(const CodeTooComplex&) const;
};
struct UnificationTooComplex
{
bool operator==(const UnificationTooComplex&) const;
};
// Could easily be folded into UnknownProperty with an extra field, std::set<Name> candidates.
// But for telemetry purposes, we want to have this be a distinct variant.
struct UnknownPropButFoundLikeProp
{
TypeId table;
Name key;
std::set<Name> candidates;
bool operator==(const UnknownPropButFoundLikeProp& rhs) const;
};
struct GenericError
{
std::string message;
bool operator==(const GenericError& rhs) const;
};
struct CannotCallNonFunction
{
TypeId ty;
bool operator==(const CannotCallNonFunction& rhs) const;
};
struct ExtraInformation
{
std::string message;
bool operator==(const ExtraInformation& rhs) const;
};
struct DeprecatedApiUsed
{
std::string symbol;
std::string useInstead;
bool operator==(const DeprecatedApiUsed& rhs) const;
};
struct ModuleHasCyclicDependency
{
std::vector<ModuleName> cycle;
bool operator==(const ModuleHasCyclicDependency& rhs) const;
};
struct FunctionExitsWithoutReturning
{
TypePackId expectedReturnType;
bool operator==(const FunctionExitsWithoutReturning& rhs) const;
};
struct IllegalRequire
{
std::string moduleName;
std::string reason;
bool operator==(const IllegalRequire& rhs) const;
};
struct MissingProperties
{
enum Context
{
Missing,
Extra
};
TypeId superType;
TypeId subType;
std::vector<Name> properties;
Context context = Missing;
bool operator==(const MissingProperties& rhs) const;
};
struct DuplicateGenericParameter
{
std::string parameterName;
bool operator==(const DuplicateGenericParameter& rhs) const;
};
struct CannotInferBinaryOperation
{
enum OpKind
{
Operation,
Comparison,
};
AstExprBinary::Op op;
std::optional<std::string> suggestedToAnnotate;
OpKind kind;
bool operator==(const CannotInferBinaryOperation& rhs) const;
};
struct SwappedGenericTypeParameter
{
enum Kind
{
Type,
Pack,
};
std::string name;
// What was `name` being used as?
Kind kind;
bool operator==(const SwappedGenericTypeParameter& rhs) const;
};
struct OptionalValueAccess
{
TypeId optional;
bool operator==(const OptionalValueAccess& rhs) const;
};
struct MissingUnionProperty
{
TypeId type;
std::vector<TypeId> missing;
Name key;
bool operator==(const MissingUnionProperty& rhs) const;
};
using TypeErrorData = Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods,
DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire,
IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError,
CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning,
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty>;
struct TypeError
{
Location location;
ModuleName moduleName;
TypeErrorData data;
int code() const;
TypeError() = default;
TypeError(const Location& location, const ModuleName& moduleName, const TypeErrorData& data)
: location(location)
, moduleName(moduleName)
, data(data)
{
}
TypeError(const Location& location, const TypeErrorData& data)
: TypeError(location, {}, data)
{
}
bool operator==(const TypeError& rhs) const;
};
template<typename T>
const T* get(const TypeError& e)
{
return get_if<T>(&e.data);
}
template<typename T>
T* get(TypeError& e)
{
return get_if<T>(&e.data);
}
using ErrorVec = std::vector<TypeError>;
std::string toString(const TypeError& error);
bool containsParseErrorName(const TypeError& error);
// Copy any types named in the error into destArena.
void copyErrors(ErrorVec& errors, struct TypeArena& destArena);
// Internal Compiler Error
struct InternalErrorReporter
{
std::function<void(const char*)> onInternalError;
std::string moduleName;
[[noreturn]] void ice(const std::string& message, const Location& location);
[[noreturn]] void ice(const std::string& message);
};
} // namespace Luau

View file

@ -0,0 +1,103 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include <string>
#include <optional>
namespace Luau
{
class AstExpr;
using ModuleName = std::string;
struct SourceCode
{
enum Type
{
None,
Module,
Script,
Local
};
std::string source;
Type type;
};
struct FileResolver
{
virtual ~FileResolver() {}
/** Fetch the source code associated with the provided ModuleName.
*
* FIXME: This requires a string copy!
*
* @returns The actual Lua code on success.
* @returns std::nullopt if no such file exists. When this occurs, type inference will report an UnknownRequire error.
*/
virtual std::optional<SourceCode> readSource(const ModuleName& name) = 0;
/** Does the module exist?
*
* Saves a string copy over reading the source and throwing it away.
*/
virtual bool moduleExists(const ModuleName& name) const = 0;
virtual std::optional<ModuleName> fromAstFragment(AstExpr* expr) const = 0;
/** Given a valid module name and a string of arbitrary data, figure out the concatenation.
*/
virtual ModuleName concat(const ModuleName& lhs, std::string_view rhs) const = 0;
/** Goes "up" a level in the hierarchy that the ModuleName represents.
*
* For instances, this is analogous to someInstance.Parent; for paths, this is equivalent to removing the last
* element of the path. Other ModuleName representations may have other ways of doing this.
*
* @returns The parent ModuleName, if one exists.
* @returns std::nullopt if there is no parent for this module name.
*/
virtual std::optional<ModuleName> getParentModuleName(const ModuleName& name) const = 0;
virtual std::optional<std::string> getHumanReadableModuleName_(const ModuleName& name) const
{
return name;
}
virtual std::optional<std::string> getEnvironmentForModule(const ModuleName& name) const = 0;
/** LanguageService only:
* std::optional<ModuleName> fromInstance(Instance* inst)
*/
};
struct NullFileResolver : FileResolver
{
std::optional<SourceCode> readSource(const ModuleName& name) override
{
return std::nullopt;
}
bool moduleExists(const ModuleName& name) const override
{
return false;
}
std::optional<ModuleName> fromAstFragment(AstExpr* expr) const override
{
return std::nullopt;
}
ModuleName concat(const ModuleName& lhs, std::string_view rhs) const override
{
return lhs;
}
std::optional<ModuleName> getParentModuleName(const ModuleName& name) const override
{
return std::nullopt;
}
std::optional<std::string> getEnvironmentForModule(const ModuleName& name) const override
{
return std::nullopt;
}
};
} // namespace Luau

View file

@ -0,0 +1,179 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Config.h"
#include "Luau/Module.h"
#include "Luau/ModuleResolver.h"
#include "Luau/RequireTracer.h"
#include "Luau/TypeInfer.h"
#include "Luau/Variant.h"
#include <string>
#include <vector>
#include <optional>
namespace Luau
{
class AstStat;
class ParseError;
struct Frontend;
struct TypeError;
struct LintWarning;
struct TypeChecker;
struct FileResolver;
struct ModuleResolver;
struct ParseResult;
struct LoadDefinitionFileResult
{
bool success;
ParseResult parseResult;
ModulePtr module;
};
LoadDefinitionFileResult loadDefinitionFile(
TypeChecker& typeChecker, ScopePtr targetScope, std::string_view definition, const std::string& packageName);
std::optional<Mode> parseMode(const std::vector<std::string>& hotcomments);
std::vector<std::string_view> parsePathExpr(const AstExpr& pathExpr);
// Exported only for convenient testing.
std::optional<ModuleName> pathExprToModuleName(const ModuleName& currentModuleName, const std::vector<std::string_view>& expr);
/** Try to convert an AST fragment into a ModuleName.
* Returns std::nullopt if the expression cannot be resolved. This will most likely happen in cases where
* the import path involves some dynamic computation that we cannot see into at typechecking time.
*
* Unintuitively, weirdly-formulated modules (like game.Parent.Parent.Parent.Foo) will successfully produce a ModuleName
* as long as it falls within the permitted syntax. This is ok because we will fail to find the module and produce an
* error when we try during typechecking.
*/
std::optional<ModuleName> pathExprToModuleName(const ModuleName& currentModuleName, const AstExpr& expr);
struct SourceNode
{
ModuleName name;
std::unordered_set<ModuleName> requires;
std::vector<std::pair<ModuleName, Location>> requireLocations;
bool dirty = true;
};
struct FrontendOptions
{
// When true, we retain full type information about every term in the AST.
// Setting this to false cuts back on RAM and is a good idea for batch
// jobs where the type graph is not deeply inspected after typechecking
// is complete.
bool retainFullTypeGraphs = false;
// When true, we run typechecking twice, one in the regular mode, ond once in strict mode
// in order to get more precise type information (e.g. for autocomplete).
bool typecheckTwice = false;
};
struct CheckResult
{
std::vector<TypeError> errors;
};
struct FrontendModuleResolver : ModuleResolver
{
FrontendModuleResolver(Frontend* frontend);
const ModulePtr getModule(const ModuleName& moduleName) const override;
bool moduleExists(const ModuleName& moduleName) const override;
std::optional<ModuleInfo> resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr) override;
std::string getHumanReadableModuleName(const ModuleName& moduleName) const override;
Frontend* frontend;
std::unordered_map<ModuleName, ModulePtr> modules;
};
struct Frontend
{
struct Stats
{
size_t files = 0;
size_t lines = 0;
size_t filesStrict = 0;
size_t filesNonstrict = 0;
double timeRead = 0;
double timeParse = 0;
double timeCheck = 0;
double timeLint = 0;
};
Frontend(FileResolver* fileResolver, ConfigResolver* configResolver, const FrontendOptions& options = {});
CheckResult check(const ModuleName& name); // new shininess
LintResult lint(const ModuleName& name, std::optional<Luau::LintOptions> enabledLintWarnings = {});
/** Lint some code that has no associated DataModel object
*
* Since this source fragment has no name, we cannot cache its AST. Instead,
* we return it to the caller to use as they wish.
*/
std::pair<SourceModule, LintResult> lintFragment(std::string_view source, std::optional<Luau::LintOptions> enabledLintWarnings = {});
CheckResult check(const SourceModule& module); // OLD. TODO KILL
LintResult lint(const SourceModule& module, std::optional<Luau::LintOptions> enabledLintWarnings = {});
bool isDirty(const ModuleName& name) const;
void markDirty(const ModuleName& name, std::vector<ModuleName>* markedDirty = nullptr);
/** Borrow a pointer into the SourceModule cache.
*
* Returns nullptr if we don't have it. This could mean that the script
* doesn't exist, or simply that its contents have changed since the previous
* check, in which case we do not have its AST.
*
* IMPORTANT: this pointer is only valid until the next call to markDirty. Do not retain it.
*/
SourceModule* getSourceModule(const ModuleName& name);
const SourceModule* getSourceModule(const ModuleName& name) const;
void clearStats();
void clear();
ScopePtr addEnvironment(const std::string& environmentName);
ScopePtr getEnvironmentScope(const std::string& environmentName);
void registerBuiltinDefinition(const std::string& name, std::function<void(TypeChecker&, ScopePtr)>);
void applyBuiltinDefinitionToEnvironment(const std::string& environmentName, const std::string& definitionName);
private:
std::pair<SourceNode*, SourceModule*> getSourceNode(CheckResult& checkResult, const ModuleName& name);
SourceModule parse(const ModuleName& name, std::string_view src, const ParseOptions& parseOptions);
bool parseGraph(std::vector<ModuleName>& buildQueue, CheckResult& checkResult, const ModuleName& root);
static LintResult classifyLints(const std::vector<LintWarning>& warnings, const Config& config);
ScopePtr getModuleEnvironment(const SourceModule& module, const Config& config);
std::unordered_map<std::string, ScopePtr> environments;
std::unordered_map<std::string, std::function<void(TypeChecker&, ScopePtr)>> builtinDefinitions;
public:
FileResolver* fileResolver;
FrontendModuleResolver moduleResolver;
FrontendModuleResolver moduleResolverForAutocomplete;
TypeChecker typeChecker;
TypeChecker typeCheckerForAutocomplete;
ConfigResolver* configResolver;
FrontendOptions options;
InternalErrorReporter iceHandler;
TypeArena arenaForAutocomplete;
std::unordered_map<ModuleName, SourceNode> sourceNodes;
std::unordered_map<ModuleName, SourceModule> sourceModules;
std::unordered_map<ModuleName, RequireTraceResult> requires;
Stats stats = {};
};
} // namespace Luau

View file

@ -0,0 +1,46 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Error.h"
#include "Luau/Location.h"
#include "Luau/TypeVar.h"
#include "Luau/Ast.h"
#include <ostream>
namespace Luau
{
std::ostream& operator<<(std::ostream& lhs, const Position& position);
std::ostream& operator<<(std::ostream& lhs, const Location& location);
std::ostream& operator<<(std::ostream& lhs, const AstName& name);
std::ostream& operator<<(std::ostream& lhs, const TypeError& error);
std::ostream& operator<<(std::ostream& lhs, const TypeMismatch& error);
std::ostream& operator<<(std::ostream& lhs, const UnknownSymbol& error);
std::ostream& operator<<(std::ostream& lhs, const UnknownProperty& error);
std::ostream& operator<<(std::ostream& lhs, const NotATable& error);
std::ostream& operator<<(std::ostream& lhs, const CannotExtendTable& error);
std::ostream& operator<<(std::ostream& lhs, const OnlyTablesCanHaveMethods& error);
std::ostream& operator<<(std::ostream& lhs, const DuplicateTypeDefinition& error);
std::ostream& operator<<(std::ostream& lhs, const CountMismatch& error);
std::ostream& operator<<(std::ostream& lhs, const FunctionDoesNotTakeSelf& error);
std::ostream& operator<<(std::ostream& lhs, const FunctionRequiresSelf& error);
std::ostream& operator<<(std::ostream& lhs, const OccursCheckFailed& error);
std::ostream& operator<<(std::ostream& lhs, const UnknownRequire& error);
std::ostream& operator<<(std::ostream& lhs, const UnknownPropButFoundLikeProp& e);
std::ostream& operator<<(std::ostream& lhs, const GenericError& error);
std::ostream& operator<<(std::ostream& lhs, const FunctionExitsWithoutReturning& error);
std::ostream& operator<<(std::ostream& lhs, const MissingProperties& error);
std::ostream& operator<<(std::ostream& lhs, const IllegalRequire& error);
std::ostream& operator<<(std::ostream& lhs, const ModuleHasCyclicDependency& error);
std::ostream& operator<<(std::ostream& lhs, const DuplicateGenericParameter& error);
std::ostream& operator<<(std::ostream& lhs, const CannotInferBinaryOperation& error);
std::ostream& operator<<(std::ostream& lhs, const TableState& tv);
std::ostream& operator<<(std::ostream& lhs, const TypeVar& tv);
std::ostream& operator<<(std::ostream& lhs, const TypePackVar& tv);
std::ostream& operator<<(std::ostream& lhs, const TypeErrorData& ted);
} // namespace Luau

View file

@ -0,0 +1,13 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include <string>
namespace Luau
{
class AstNode;
std::string toJson(AstNode* node);
} // namespace Luau

View file

@ -0,0 +1,96 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Location.h"
#include <memory>
#include <vector>
namespace Luau
{
struct AstName;
class AstStat;
class AstNameTable;
struct TypeChecker;
struct Module;
using ScopePtr = std::shared_ptr<struct Scope>;
struct LintWarning
{
// Make sure any new lint codes are documented here: https://luau-lang.org/lint
// Note that in Studio, the active set of lint warnings is determined by FStringStudioLuauLints
enum Code
{
Code_Unknown = 0,
Code_UnknownGlobal = 1, // superseded by type checker
Code_DeprecatedGlobal = 2,
Code_GlobalUsedAsLocal = 3,
Code_LocalShadow = 4, // disabled in Studio
Code_SameLineStatement = 5, // disabled in Studio
Code_MultiLineStatement = 6,
Code_LocalUnused = 7, // disabled in Studio
Code_FunctionUnused = 8, // disabled in Studio
Code_ImportUnused = 9, // disabled in Studio
Code_BuiltinGlobalWrite = 10,
Code_PlaceholderRead = 11,
Code_UnreachableCode = 12,
Code_UnknownType = 13,
Code_ForRange = 14,
Code_UnbalancedAssignment = 15,
Code_ImplicitReturn = 16, // disabled in Studio, superseded by type checker in strict mode
Code_DuplicateLocal = 17,
Code_FormatString = 18,
Code_TableLiteral = 19,
Code_UninitializedLocal = 20,
Code_DuplicateFunction = 21,
Code_DeprecatedApi = 22,
Code_TableOperations = 23,
Code_DuplicateCondition = 24,
Code__Count
};
Code code;
Location location;
std::string text;
static const char* getName(Code code);
static Code parseName(const char* name);
static uint64_t parseMask(const std::vector<std::string>& hotcomments);
};
struct LintResult
{
std::vector<LintWarning> errors;
std::vector<LintWarning> warnings;
};
struct LintOptions
{
uint64_t warningMask = 0;
void enableWarning(LintWarning::Code code)
{
warningMask |= 1ull << code;
}
void disableWarning(LintWarning::Code code)
{
warningMask &= ~(1ull << code);
}
bool isEnabled(LintWarning::Code code) const
{
return 0 != (warningMask & (1ull << code));
}
void setDefaults();
};
std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module, const LintOptions& options);
std::vector<AstName> getDeprecatedGlobals(const AstNameTable& names);
} // namespace Luau

View file

@ -0,0 +1,111 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/FileResolver.h"
#include "Luau/TypePack.h"
#include "Luau/TypedAllocator.h"
#include "Luau/ParseOptions.h"
#include "Luau/Error.h"
#include "Luau/Parser.h"
#include <memory>
#include <vector>
#include <unordered_map>
#include <optional>
namespace Luau
{
struct Module;
using ScopePtr = std::shared_ptr<struct Scope>;
using ModulePtr = std::shared_ptr<Module>;
/// Root of the AST of a parsed source file
struct SourceModule
{
ModuleName name; // DataModel path if possible. Filename if not.
SourceCode::Type type = SourceCode::None;
std::optional<std::string> environmentName;
bool cyclic = false;
std::unique_ptr<Allocator> allocator;
std::unique_ptr<AstNameTable> names;
std::vector<ParseError> parseErrors;
AstStatBlock* root = nullptr;
std::optional<Mode> mode;
uint64_t ignoreLints = 0;
std::vector<Comment> commentLocations;
SourceModule()
: allocator(new Allocator)
, names(new AstNameTable(*allocator))
{
}
};
bool isWithinComment(const SourceModule& sourceModule, Position pos);
struct TypeArena
{
TypedAllocator<TypeVar> typeVars;
TypedAllocator<TypePackVar> typePacks;
void clear();
template<typename T>
TypeId addType(T tv)
{
return addTV(TypeVar(std::move(tv)));
}
TypeId addTV(TypeVar&& tv);
TypeId freshType(TypeLevel level);
TypePackId addTypePack(std::initializer_list<TypeId> types);
TypePackId addTypePack(std::vector<TypeId> types);
TypePackId addTypePack(TypePack pack);
TypePackId addTypePack(TypePackVar pack);
};
void freeze(TypeArena& arena);
void unfreeze(TypeArena& arena);
// Only exposed so they can be unit tested.
using SeenTypes = std::unordered_map<TypeId, TypeId>;
using SeenTypePacks = std::unordered_map<TypePackId, TypePackId>;
TypePackId clone(TypePackId tp, TypeArena& dest, SeenTypes& seenTypes, SeenTypePacks& seenTypePacks, bool* encounteredFreeType = nullptr);
TypeId clone(TypeId tp, TypeArena& dest, SeenTypes& seenTypes, SeenTypePacks& seenTypePacks, bool* encounteredFreeType = nullptr);
TypeFun clone(const TypeFun& typeFun, TypeArena& dest, SeenTypes& seenTypes, SeenTypePacks& seenTypePacks, bool* encounteredFreeType = nullptr);
struct Module
{
~Module();
TypeArena interfaceTypes;
TypeArena internalTypes;
std::vector<std::pair<Location, ScopePtr>> scopes; // never empty
std::unordered_map<const AstExpr*, TypeId> astTypes;
std::unordered_map<const AstExpr*, TypeId> astExpectedTypes;
std::unordered_map<const AstExpr*, TypeId> astOriginalCallTypes;
std::unordered_map<const AstExpr*, TypeId> astOverloadResolvedTypes;
std::unordered_map<Name, TypeId> declaredGlobals;
ErrorVec errors;
Mode mode;
SourceCode::Type type;
ScopePtr getModuleScope() const;
// Once a module has been typechecked, we clone its public interface into a separate arena.
// This helps us to force TypeVar ownership into a DAG rather than a DCG.
// Returns true if there were any free types encountered in the public interface. This
// indicates a bug in the type checker that we want to surface.
bool clonePublicInterface();
};
} // namespace Luau

View file

@ -0,0 +1,79 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/FileResolver.h"
#include <memory>
#include <optional>
#include <string>
namespace Luau
{
class AstExpr;
struct Module;
using ModulePtr = std::shared_ptr<Module>;
struct ModuleInfo
{
ModuleName name;
bool optional = false;
};
struct ModuleResolver
{
virtual ~ModuleResolver() {}
/** Compute a ModuleName from an AST fragment. This AST fragment is generally the argument to the require() function.
*
* You probably want to implement this with some variation of pathExprToModuleName.
*
* @returns The ModuleInfo if the expression is a syntactically legal path.
* @returns std::nullopt if we are unable to determine whether or not the expression is a valid path. Type inference will
* silently assume that it could succeed in this case.
*
* FIXME: This is clearly not the right behaviour longterm. We'll want to adust this interface to be able to signal
* a) success,
* b) Definitive failure (this expression will absolutely cause require() to fail at runtime), and
* c) uncertainty
*/
virtual std::optional<ModuleInfo> resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr) = 0;
/** Get a typechecked module from its name.
*
* This can return null under two circumstances: the module is unknown at compile time,
* or there's a cycle, and we are still in the middle of typechecking the module.
*/
virtual const ModulePtr getModule(const ModuleName& moduleName) const = 0;
/** Is a module known at compile time?
*
* This function can be used to distinguish the above two cases.
*/
virtual bool moduleExists(const ModuleName& moduleName) const = 0;
virtual std::string getHumanReadableModuleName(const ModuleName& moduleName) const = 0;
};
struct NullModuleResolver : ModuleResolver
{
std::optional<ModuleInfo> resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr) override
{
return std::nullopt;
}
const ModulePtr getModule(const ModuleName& moduleName) const override
{
return nullptr;
}
bool moduleExists(const ModuleName& moduleName) const override
{
return false;
}
std::string getHumanReadableModuleName(const ModuleName& moduleName) const override
{
return moduleName;
}
};
} // namespace Luau

View file

@ -0,0 +1,120 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Variant.h"
#include "Luau/Location.h"
#include "Luau/Symbol.h"
#include <map>
#include <memory>
#include <vector>
namespace Luau
{
struct TypeVar;
using TypeId = const TypeVar*;
struct Field;
using LValue = Variant<Symbol, Field>;
struct Field
{
std::shared_ptr<LValue> parent; // TODO: Eventually use unique_ptr to enforce non-copyable trait.
std::string key;
};
std::optional<LValue> tryGetLValue(const class AstExpr& expr);
// Utility function: breaks down an LValue to get at the Symbol, and reverses the vector of keys.
std::pair<Symbol, std::vector<std::string>> getFullName(const LValue& lvalue);
std::string toString(const LValue& lvalue);
template<typename T>
const T* get(const LValue& lvalue)
{
return get_if<T>(&lvalue);
}
// Key is a stringified encoding of an LValue.
using RefinementMap = std::map<std::string, TypeId>;
void merge(RefinementMap& l, const RefinementMap& r, std::function<TypeId(TypeId, TypeId)> f);
void addRefinement(RefinementMap& refis, const LValue& lvalue, TypeId ty);
struct TruthyPredicate;
struct IsAPredicate;
struct TypeGuardPredicate;
struct EqPredicate;
struct AndPredicate;
struct OrPredicate;
struct NotPredicate;
using Predicate = Variant<TruthyPredicate, IsAPredicate, TypeGuardPredicate, EqPredicate, AndPredicate, OrPredicate, NotPredicate>;
using PredicateVec = std::vector<Predicate>;
struct TruthyPredicate
{
LValue lvalue;
Location location;
};
struct IsAPredicate
{
LValue lvalue;
Location location;
TypeId ty;
};
struct TypeGuardPredicate
{
LValue lvalue;
Location location;
std::string kind; // TODO: When singleton types arrive, replace this with `TypeId ty;`
bool isTypeof;
};
struct EqPredicate
{
LValue lvalue;
TypeId type;
Location location;
};
struct AndPredicate
{
PredicateVec lhs;
PredicateVec rhs;
AndPredicate(PredicateVec&& lhs, PredicateVec&& rhs)
: lhs(std::move(lhs))
, rhs(std::move(rhs))
{
}
};
struct OrPredicate
{
PredicateVec lhs;
PredicateVec rhs;
OrPredicate(PredicateVec&& lhs, PredicateVec&& rhs)
: lhs(std::move(lhs))
, rhs(std::move(rhs))
{
}
};
struct NotPredicate
{
PredicateVec predicates;
};
template<typename T>
const T* get(const Predicate& predicate)
{
return get_if<T>(&predicate);
}
} // namespace Luau

View file

@ -0,0 +1,39 @@
// 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 <stdexcept>
namespace Luau
{
struct RecursionCounter
{
RecursionCounter(int* count)
: count(count)
{
++(*count);
}
~RecursionCounter()
{
LUAU_ASSERT(*count > 0);
--(*count);
}
private:
int* count;
};
struct RecursionLimiter : RecursionCounter
{
RecursionLimiter(int* count, int limit)
: RecursionCounter(count)
{
if (limit > 0 && *count > limit)
throw std::runtime_error("Internal recursion counter limit exceeded");
}
};
} // namespace Luau

View file

@ -0,0 +1,28 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/DenseHash.h"
#include "Luau/FileResolver.h"
#include "Luau/Location.h"
#include <string>
namespace Luau
{
class AstStat;
class AstExpr;
class AstStatBlock;
struct AstLocal;
struct RequireTraceResult
{
DenseHashMap<const AstExpr*, ModuleName> exprs{0};
DenseHashMap<const AstExpr*, bool> optional{0};
std::vector<std::pair<ModuleName, Location>> requires;
};
RequireTraceResult traceRequires(FileResolver* fileResolver, AstStatBlock* root, ModuleName currentModuleName);
} // namespace Luau

View file

@ -0,0 +1,208 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Module.h"
#include "Luau/ModuleResolver.h"
#include "Luau/TypePack.h"
#include "Luau/TypeVar.h"
#include "Luau/DenseHash.h"
// We provide an implementation of substitution on types,
// which recursively replaces types by other types.
// Examples include quantification (replacing free types by generics)
// and instantiation (replacing generic types by free ones).
//
// To implement a substitution, implement a subclass of `Substitution`
// and provide implementations of `isDirty` (which should be true for types that
// should be replaced) and `clean` which replaces any dirty types.
//
// struct MySubst : Substitution
// {
// bool isDirty(TypeId ty) override { ... }
// bool isDirty(TypePackId tp) override { ... }
// TypeId clean(TypeId ty) override { ... }
// TypePackId clean(TypePackId tp) override { ... }
// bool ignoreChildren(TypeId ty) override { ... }
// bool ignoreChildren(TypePackId tp) override { ... }
// };
//
// For example, `Instantiation` in `TypeInfer.cpp` uses this.
// The implementation of substitution tries not to copy types
// unnecessarily. It first finds all the types which can reach
// a dirty type, and either cleans them (if they are dirty)
// or clones them (if they are not). It then updates the children
// of the newly created types. When considering reachability,
// we do not consider the children of any type where ignoreChildren(ty) is true.
// There is a gotcha for cyclic types, which means we can't just use
// a straightforward DFS. For example:
//
// type T = { f : () -> T, g: () -> number, h: X }
//
// If X is dirty, and is being replaced by X' then the result should be:
//
// type T' = { f : () -> T', g: () -> number, h: X' }
//
// that is the type of `f` is replaced, but the type of `g` is not.
//
// For this reason, we first use Tarjan's algorithm to find strongly
// connected components. If any type in an SCC can reach a dirty type,
// them the whole SCC can. For instance, in the above example,
// `T`, and the type of `f` are in the same SCC, which is why `f` gets
// replaced.
LUAU_FASTFLAG(DebugLuauTrackOwningArena)
namespace Luau
{
enum class TarjanResult
{
TooManyChildren,
Ok
};
struct TarjanWorklistVertex
{
int index;
int currEdge;
int lastEdge;
};
// Tarjan's algorithm for finding the SCCs in a cyclic structure.
// https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
struct Tarjan
{
// Vertices (types and type packs) are indexed, using pre-order traversal.
DenseHashMap<TypeId, int> typeToIndex{nullptr};
DenseHashMap<TypePackId, int> packToIndex{nullptr};
std::vector<TypeId> indexToType;
std::vector<TypePackId> indexToPack;
// Tarjan keeps a stack of vertices where we're still in the process
// of finding their SCC.
std::vector<int> stack;
std::vector<bool> onStack;
// Tarjan calculates the lowlink for each vertex,
// which is the lowest ancestor index reachable from the vertex.
std::vector<int> lowlink;
int childCount = 0;
std::vector<TypeId> edgesTy;
std::vector<TypePackId> edgesTp;
std::vector<TarjanWorklistVertex> worklist;
// This is hot code, so we optimize recursion to a stack.
TarjanResult loop();
// Clear the state
void clear();
// Find or create the index for a vertex.
// 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(TypePackId tp);
// Recursively visit all the children of a vertex
void visitChildren(TypeId ty, int index);
void visitChildren(TypePackId tp, int index);
void visitChild(TypeId ty);
void visitChild(TypePackId ty);
// Visit the root vertex.
TarjanResult visitRoot(TypeId ty);
TarjanResult visitRoot(TypePackId ty);
// Each subclass gets called back once for each edge,
// and once for each SCC.
virtual void visitEdge(int index, int parentIndex) {}
virtual void visitSCC(int index) {}
// Each subclass can decide to ignore some nodes.
virtual bool ignoreChildren(TypeId ty)
{
return false;
}
virtual bool ignoreChildren(TypePackId ty)
{
return false;
}
};
// We use Tarjan to calculate dirty bits. We set `dirty[i]` true
// if the vertex with index `i` can reach a dirty vertex.
struct FindDirty : Tarjan
{
std::vector<bool> dirty;
// Get/set the dirty bit for an index (grows the vector if needed)
bool getDirty(int index);
void setDirty(int index, bool d);
// Find all the dirty vertices reachable from `t`.
TarjanResult findDirty(TypeId t);
TarjanResult findDirty(TypePackId t);
// We find dirty vertices using Tarjan
void visitEdge(int index, int parentIndex) override;
void visitSCC(int index) override;
// Subclasses should say which vertices are dirty,
// and what to do with dirty vertices.
virtual bool isDirty(TypeId ty) = 0;
virtual bool isDirty(TypePackId tp) = 0;
virtual void foundDirty(TypeId ty) = 0;
virtual void foundDirty(TypePackId tp) = 0;
};
// And finally substitution, which finds all the reachable dirty vertices
// and replaces them with clean ones.
struct Substitution : FindDirty
{
ModulePtr currentModule;
DenseHashMap<TypeId, TypeId> newTypes{nullptr};
DenseHashMap<TypePackId, TypePackId> newPacks{nullptr};
std::optional<TypeId> substitute(TypeId ty);
std::optional<TypePackId> substitute(TypePackId tp);
TypeId replace(TypeId ty);
TypePackId replace(TypePackId tp);
void replaceChildren(TypeId ty);
void replaceChildren(TypePackId tp);
TypeId clone(TypeId ty);
TypePackId clone(TypePackId tp);
// Substitutions use Tarjan to find dirty nodes and replace them
void foundDirty(TypeId ty) override;
void foundDirty(TypePackId tp) override;
// Implementing subclasses define how to clean a dirty type.
virtual TypeId clean(TypeId ty) = 0;
virtual TypePackId clean(TypePackId tp) = 0;
// Helper functions to create new types (used by subclasses)
template<typename T>
TypeId addType(const T& tv)
{
TypeId allocated = currentModule->internalTypes.typeVars.allocate(tv);
if (FFlag::DebugLuauTrackOwningArena)
asMutable(allocated)->owningArena = &currentModule->internalTypes;
return allocated;
}
template<typename T>
TypePackId addTypePack(const T& tp)
{
TypePackId allocated = currentModule->internalTypes.typePacks.allocate(tp);
if (FFlag::DebugLuauTrackOwningArena)
asMutable(allocated)->owningArena = &currentModule->internalTypes;
return allocated;
}
};
} // namespace Luau

View file

@ -0,0 +1,95 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Ast.h"
#include "Luau/Common.h"
#include <string>
namespace Luau
{
// TODO Rename this to Name once the old type alias is gone.
struct Symbol
{
Symbol()
: local(nullptr)
, global()
{
}
Symbol(AstLocal* local)
: local(local)
, global()
{
}
Symbol(const AstName& global)
: local(nullptr)
, global(global)
{
}
AstLocal* local;
AstName global;
bool operator==(const Symbol& rhs) const
{
if (local)
return local == rhs.local;
if (global.value)
return rhs.global.value && global == rhs.global.value; // Subtlety: AstName::operator==(const char*) uses strcmp, not pointer identity.
return false;
}
bool operator!=(const Symbol& rhs) const
{
return !(*this == rhs);
}
bool operator<(const Symbol& rhs) const
{
if (local && rhs.local)
return local < rhs.local;
else if (global.value && rhs.global.value)
return global < rhs.global;
else if (local)
return true;
else
return false;
}
AstName astName() const
{
if (local)
return local->name;
LUAU_ASSERT(global.value);
return global;
}
const char* c_str() const
{
if (local)
return local->name.value;
LUAU_ASSERT(global.value);
return global.value;
}
};
std::string toString(const Symbol& name);
} // namespace Luau
namespace std
{
template<>
struct hash<Luau::Symbol>
{
std::size_t operator()(const Luau::Symbol& s) const noexcept
{
return std::hash<const Luau::AstLocal*>()(s.local) ^ (s.global.value ? std::hash<std::string_view>()(s.global.value) : 0);
}
};
} // namespace std

View file

@ -0,0 +1,72 @@
// 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/TypeVar.h"
#include <unordered_map>
#include <optional>
#include <memory>
#include <string>
LUAU_FASTINT(LuauTableTypeMaximumStringifierLength)
LUAU_FASTINT(LuauTypeMaximumStringifierLength)
namespace Luau
{
struct ToStringNameMap
{
std::unordered_map<TypeId, std::string> typeVars;
std::unordered_map<TypePackId, std::string> typePacks;
};
struct ToStringOptions
{
bool exhaustive = false; // If true, we produce complete output rather than comprehensible output
bool useLineBreaks = false; // If true, we insert new lines to separate long results such as table entries/metatable.
bool functionTypeArguments = false; // If true, output function type argument names when they are available
bool hideTableKind = false; // If true, all tables will be surrounded with plain '{}'
size_t maxTableLength = size_t(FInt::LuauTableTypeMaximumStringifierLength); // Only applied to TableTypeVars
size_t maxTypeLength = size_t(FInt::LuauTypeMaximumStringifierLength);
std::optional<ToStringNameMap> nameMap;
std::shared_ptr<Scope> scope; // If present, module names will be added and types that are not available in scope will be marked as 'invalid'
};
struct ToStringResult
{
std::string name;
ToStringNameMap nameMap;
bool invalid = false;
bool error = false;
bool cycle = false;
bool truncated = false;
};
ToStringResult toStringDetailed(TypeId ty, const ToStringOptions& opts = {});
ToStringResult toStringDetailed(TypePackId ty, const ToStringOptions& opts = {});
std::string toString(TypeId ty, const ToStringOptions& opts);
std::string toString(TypePackId ty, const ToStringOptions& opts);
// These are offered as overloads rather than a default parameter so that they can be easily invoked from within the MSVC debugger.
// You can use them in watch expressions!
inline std::string toString(TypeId ty)
{
return toString(ty, ToStringOptions{});
}
inline std::string toString(TypePackId ty)
{
return toString(ty, ToStringOptions{});
}
std::string toString(const TypeVar& tv, const ToStringOptions& opts = {});
std::string toString(const TypePackVar& tp, const ToStringOptions& opts = {});
// It could be useful to see the text representation of a type during a debugging session instead of exploring the content of the class
// These functions will dump the type to stdout and can be evaluated in Watch/Immediate windows or as gdb/lldb expression
void dump(TypeId ty);
void dump(TypePackId ty);
} // namespace Luau

View file

@ -0,0 +1,18 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include <vector>
namespace Luau
{
template<typename T>
struct AstArray;
class AstStat;
bool containsFunctionCall(const AstStat& stat);
bool isFunction(const AstStat& stat);
void toposort(std::vector<AstStat*>& stats);
} // namespace Luau

View file

@ -0,0 +1,30 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Location.h"
#include "Luau/ParseOptions.h"
#include <string>
namespace Luau
{
class AstNode;
class AstStatBlock;
struct TranspileResult
{
std::string code;
Location errorLocation;
std::string parseError; // Nonempty if the transpile failed
};
void dump(AstNode* node);
// Never fails on a well-formed AST
std::string transpile(AstStatBlock& ast);
std::string transpileWithTypes(AstStatBlock& block);
// Only fails when parsing fails
TranspileResult transpile(std::string_view source, ParseOptions options = ParseOptions{});
} // namespace Luau

View file

@ -0,0 +1,46 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/TypeVar.h"
namespace Luau
{
// Log of where what TypeIds we are rebinding and what they used to be
struct TxnLog
{
TxnLog() = default;
explicit TxnLog(const std::vector<std::pair<TypeId, TypeId>>& seen)
: seen(seen)
{
}
TxnLog(const TxnLog&) = delete;
TxnLog& operator=(const TxnLog&) = delete;
TxnLog(TxnLog&&) = default;
TxnLog& operator=(TxnLog&&) = default;
void operator()(TypeId a);
void operator()(TypePackId a);
void operator()(TableTypeVar* a);
void rollback();
void concat(TxnLog rhs);
bool haveSeen(TypeId lhs, TypeId rhs);
void pushSeen(TypeId lhs, TypeId rhs);
void popSeen(TypeId lhs, TypeId rhs);
private:
std::vector<std::pair<TypeId, TypeVar>> typeVarChanges;
std::vector<std::pair<TypePackId, TypePackVar>> typePackChanges;
std::vector<std::pair<TableTypeVar*, std::optional<TypeId>>> tableChanges;
public:
std::vector<std::pair<TypeId, TypeId>> seen; // used to avoid infinite recursion when types are cyclic
};
} // namespace Luau

View file

@ -0,0 +1,21 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Module.h"
#include <unordered_set>
namespace Luau
{
struct TypeRehydrationOptions
{
std::unordered_set<std::string> bannedNames;
bool expandClassProps = false;
};
void attachTypeData(SourceModule& source, Module& result);
AstType* rehydrateAnnotation(TypeId type, Allocator* allocator, const TypeRehydrationOptions& options = {});
} // namespace Luau

View file

@ -0,0 +1,453 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Predicate.h"
#include "Luau/Error.h"
#include "Luau/Module.h"
#include "Luau/Symbol.h"
#include "Luau/Parser.h"
#include "Luau/Substitution.h"
#include "Luau/TxnLog.h"
#include "Luau/TypePack.h"
#include "Luau/TypeVar.h"
#include "Luau/Unifier.h"
#include <memory>
#include <unordered_map>
#include <unordered_set>
namespace Luau
{
struct Scope;
struct TypeChecker;
struct ModuleResolver;
using Name = std::string;
using ScopePtr = std::shared_ptr<Scope>;
using OverloadErrorEntry = std::tuple<std::vector<TypeError>, std::vector<TypeId>, const FunctionTypeVar*>;
bool doesCallError(const AstExprCall* call);
bool hasBreak(AstStat* node);
const AstStat* getFallthrough(const AstStat* node);
struct Unifier;
// A substitution which replaces generic types in a given set by free types.
struct ReplaceGenerics : Substitution
{
TypeLevel level;
std::vector<TypeId> generics;
std::vector<TypePackId> genericPacks;
bool ignoreChildren(TypeId ty) override;
bool isDirty(TypeId ty) override;
bool isDirty(TypePackId tp) override;
TypeId clean(TypeId ty) override;
TypePackId clean(TypePackId tp) override;
};
// A substitution which replaces generic functions by monomorphic functions
struct Instantiation : Substitution
{
TypeLevel level;
ReplaceGenerics replaceGenerics;
bool ignoreChildren(TypeId ty) override;
bool isDirty(TypeId ty) override;
bool isDirty(TypePackId tp) override;
TypeId clean(TypeId ty) override;
TypePackId clean(TypePackId tp) override;
};
// A substitution which replaces free types by generic types.
struct Quantification : Substitution
{
TypeLevel level;
std::vector<TypeId> generics;
std::vector<TypePackId> genericPacks;
bool isDirty(TypeId ty) override;
bool isDirty(TypePackId tp) override;
TypeId clean(TypeId ty) override;
TypePackId clean(TypePackId tp) override;
};
// A substitution which replaces free types by any
struct Anyification : Substitution
{
TypeId anyType;
TypePackId anyTypePack;
bool isDirty(TypeId ty) override;
bool isDirty(TypePackId tp) override;
TypeId clean(TypeId ty) override;
TypePackId clean(TypePackId tp) override;
};
// A substitution which replaces the type parameters of a type function by arguments
struct ApplyTypeFunction : Substitution
{
TypeLevel level;
bool encounteredForwardedType;
std::unordered_map<TypeId, TypeId> arguments;
bool isDirty(TypeId ty) override;
bool isDirty(TypePackId tp) override;
TypeId clean(TypeId ty) override;
TypePackId clean(TypePackId tp) override;
};
// All TypeVars are retained via Environment::typeVars. All TypeIds
// within a program are borrowed pointers into this set.
struct TypeChecker
{
explicit TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHandler);
TypeChecker(const TypeChecker&) = delete;
TypeChecker& operator=(const TypeChecker&) = delete;
ModulePtr check(const SourceModule& module, Mode mode, std::optional<ScopePtr> environmentScope = std::nullopt);
std::vector<std::pair<Location, ScopePtr>> getScopes() const;
void check(const ScopePtr& scope, const AstStat& statement);
void check(const ScopePtr& scope, const AstStatBlock& statement);
void check(const ScopePtr& scope, const AstStatIf& statement);
void check(const ScopePtr& scope, const AstStatWhile& statement);
void check(const ScopePtr& scope, const AstStatRepeat& statement);
void check(const ScopePtr& scope, const AstStatReturn& return_);
void check(const ScopePtr& scope, const AstStatAssign& assign);
void check(const ScopePtr& scope, const AstStatCompoundAssign& assign);
void check(const ScopePtr& scope, const AstStatLocal& local);
void check(const ScopePtr& scope, const AstStatFor& local);
void check(const ScopePtr& scope, const AstStatForIn& forin);
void check(const ScopePtr& scope, TypeId ty, const ScopePtr& funScope, const AstStatFunction& function);
void check(const ScopePtr& scope, TypeId ty, const ScopePtr& funScope, const AstStatLocalFunction& function);
void check(const ScopePtr& scope, const AstStatTypeAlias& typealias, bool forwardDeclare = false);
void check(const ScopePtr& scope, const AstStatDeclareClass& declaredClass);
void check(const ScopePtr& scope, const AstStatDeclareFunction& declaredFunction);
void checkBlock(const ScopePtr& scope, const AstStatBlock& statement);
void checkBlockTypeAliases(const ScopePtr& scope, std::vector<AstStat*>& sorted);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExpr& expr, std::optional<TypeId> expectedType = std::nullopt);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprLocal& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprGlobal& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprVarargs& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprCall& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprIndexName& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprIndexExpr& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprFunction& expr, std::optional<TypeId> expectedType = std::nullopt);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprTable& expr, std::optional<TypeId> expectedType = std::nullopt);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprUnary& expr);
TypeId checkRelationalOperation(
const ScopePtr& scope, const AstExprBinary& expr, TypeId lhsType, TypeId rhsType, const PredicateVec& predicates = {});
TypeId checkBinaryOperation(
const ScopePtr& scope, const AstExprBinary& expr, TypeId lhsType, TypeId rhsType, const PredicateVec& predicates = {});
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprBinary& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprTypeAssertion& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprError& expr);
ExprResult<TypeId> checkExpr(const ScopePtr& scope, const AstExprIfElse& expr);
TypeId checkExprTable(const ScopePtr& scope, const AstExprTable& expr, const std::vector<std::pair<TypeId, TypeId>>& fieldTypes,
std::optional<TypeId> expectedType);
// Returns the type of the lvalue.
TypeId checkLValue(const ScopePtr& scope, const AstExpr& expr);
// Returns both the type of the lvalue and its binding (if the caller wants to mutate the binding).
// Note: the binding may be null.
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExpr& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprLocal& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprGlobal& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprIndexName& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprIndexExpr& expr);
TypeId checkFunctionName(const ScopePtr& scope, AstExpr& funName);
std::pair<TypeId, ScopePtr> checkFunctionSignature(const ScopePtr& scope, int subLevel, const AstExprFunction& expr,
std::optional<Location> originalNameLoc, std::optional<TypeId> expectedType);
void checkFunctionBody(const ScopePtr& scope, TypeId type, const AstExprFunction& function);
void checkArgumentList(
const ScopePtr& scope, Unifier& state, TypePackId paramPack, TypePackId argPack, const std::vector<Location>& argLocations);
ExprResult<TypePackId> checkExprPack(const ScopePtr& scope, const AstExpr& expr);
ExprResult<TypePackId> checkExprPack(const ScopePtr& scope, const AstExprCall& expr);
std::vector<std::optional<TypeId>> getExpectedTypesForCall(const std::vector<TypeId>& overloads, size_t argumentCount, bool selfCall);
std::optional<ExprResult<TypePackId>> checkCallOverload(const ScopePtr& scope, const AstExprCall& expr, TypeId fn, TypePackId retPack,
TypePackId argPack, TypePack* args, const std::vector<Location>& argLocations, const ExprResult<TypePackId>& argListResult,
std::vector<TypeId>& overloadsThatMatchArgCount, std::vector<OverloadErrorEntry>& errors);
bool handleSelfCallMismatch(const ScopePtr& scope, const AstExprCall& expr, TypePack* args, const std::vector<Location>& argLocations,
const std::vector<OverloadErrorEntry>& errors);
ExprResult<TypePackId> reportOverloadResolutionError(const ScopePtr& scope, const AstExprCall& expr, TypePackId retPack, TypePackId argPack,
const std::vector<Location>& argLocations, const std::vector<TypeId>& overloads, const std::vector<TypeId>& overloadsThatMatchArgCount,
const std::vector<OverloadErrorEntry>& errors);
ExprResult<TypePackId> checkExprList(const ScopePtr& scope, const Location& location, const AstArray<AstExpr*>& exprs,
bool substituteFreeForNil = false, const std::vector<bool>& lhsAnnotations = {},
const std::vector<std::optional<TypeId>>& expectedTypes = {});
static std::optional<AstExpr*> matchRequire(const AstExprCall& call);
TypeId checkRequire(const ScopePtr& scope, const ModuleInfo& moduleInfo, const Location& location);
// Try to infer that the provided type is a table of some sort.
// Reports an error if the type is already some kind of non-table.
void tablify(TypeId type);
/** In nonstrict mode, many typevars need to be replaced by any.
*/
TypeId anyIfNonstrict(TypeId ty) const;
/** Attempt to unify the types left and right. Treat any failures as type errors
* in the final typecheck report.
*/
bool unify(TypeId left, TypeId right, const Location& location);
bool unify(TypePackId left, TypePackId right, const Location& location, CountMismatch::Context ctx = CountMismatch::Context::Arg);
/** Attempt to unify the types left and right.
* If this fails, and the right type can be instantiated, do so and try unification again.
*/
bool unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId left, TypeId right, const Location& location);
void unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId left, TypeId right, Unifier& state);
/** Attempt to unify left with right.
* If there are errors, undo everything and return the errors.
* If there are no errors, commit and return an empty error vector.
*/
ErrorVec tryUnify(TypeId left, TypeId right, const Location& location);
ErrorVec tryUnify(TypePackId left, TypePackId right, const Location& location);
// Test whether the two type vars unify. Never commits the result.
ErrorVec canUnify(TypeId superTy, TypeId subTy, const Location& location);
ErrorVec canUnify(TypePackId superTy, TypePackId subTy, const Location& location);
// Variant that takes a preexisting 'seen' set. We need this in certain cases to avoid infinitely recursing
// into cyclic types.
ErrorVec canUnify(const std::vector<std::pair<TypeId, TypeId>>& seen, TypeId left, TypeId right, const Location& location);
std::optional<TypeId> findMetatableEntry(TypeId type, std::string entry, const Location& location);
std::optional<TypeId> findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location);
std::optional<TypeId> getIndexTypeFromType(const ScopePtr& scope, TypeId type, const Name& name, const Location& location, bool addErrors);
// Reduces the union to its simplest possible shape.
// (A | B) | B | C yields A | B | C
std::vector<TypeId> reduceUnion(const std::vector<TypeId>& types);
std::optional<TypeId> tryStripUnionFromNil(TypeId ty);
TypeId stripFromNilAndReport(TypeId ty, const Location& location);
template<typename Id>
ErrorVec tryUnify_(Id left, Id right, const Location& location);
template<typename Id>
ErrorVec canUnify_(Id left, Id right, const Location& location);
public:
/*
* Convert monotype into a a polytype, by replacing any metavariables in descendant scopes
* by bound generic type variables. This is used to infer that a function is generic.
*/
TypeId quantify(const ScopePtr& scope, TypeId ty, Location location);
/*
* Convert a polytype into a monotype, by replacing any bound generic types by type metavariables.
* This is used to typecheck particular calls to generic functions, and when generic functions
* are passed as arguments.
*
* The "changed" boolean is used to permit us to return the same TypeId in the case that the instantiated type is unchanged.
* This is important in certain cases, such as methods on objects, where a table contains a function whose first argument is the table.
* Without this property, we can wind up in a situation where a new TypeId is allocated for the outer table. This can cause us to produce
* unfortunate types like
*
* {method: ({method: (<CYCLE>) -> a}) -> a}
*
*/
TypeId instantiate(const ScopePtr& scope, TypeId ty, Location location);
// Removed by FFlag::LuauRankNTypes
TypePackId DEPRECATED_instantiate(const ScopePtr& scope, TypePackId ty, Location location);
// Replace any free types or type packs by `any`.
// This is used when exporting types from modules, to make sure free types don't leak.
TypeId anyify(const ScopePtr& scope, TypeId ty, Location location);
TypePackId anyify(const ScopePtr& scope, TypePackId ty, Location location);
void reportError(const TypeError& error);
void reportError(const Location& location, TypeErrorData error);
void reportErrors(const ErrorVec& errors);
[[noreturn]] void ice(const std::string& message, const Location& location);
[[noreturn]] void ice(const std::string& message);
ScopePtr childFunctionScope(const ScopePtr& parent, const Location& location, int subLevel = 0);
ScopePtr childScope(const ScopePtr& parent, const Location& location, int subLevel = 0);
// Wrapper for merge(l, r, toUnion) but without the lambda junk.
void merge(RefinementMap& l, const RefinementMap& r);
private:
void prepareErrorsForDisplay(ErrorVec& errVec);
void diagnoseMissingTableKey(UnknownProperty* utk, TypeErrorData& data);
void reportErrorCodeTooComplex(const Location& location);
private:
Unifier mkUnifier(const Location& location);
Unifier mkUnifier(const std::vector<std::pair<TypeId, TypeId>>& seen, const Location& location);
// These functions are only safe to call when we are in the process of typechecking a module.
// Produce a new free type var.
TypeId freshType(const ScopePtr& scope);
TypeId freshType(TypeLevel level);
TypeId DEPRECATED_freshType(const ScopePtr& scope, bool canBeGeneric = false);
TypeId DEPRECATED_freshType(TypeLevel level, bool canBeGeneric = false);
// Returns nullopt if the predicate filters down the TypeId to 0 options.
std::optional<TypeId> filterMap(TypeId type, TypeIdPredicate predicate);
TypeId unionOfTypes(TypeId a, TypeId b, const Location& location, bool unifyFreeTypes = true);
// ex
// TypeId id = addType(FreeTypeVar());
template<typename T>
TypeId addType(const T& tv)
{
return addTV(TypeVar(tv));
}
TypeId addType(const UnionTypeVar& utv);
TypeId addTV(TypeVar&& tv);
TypePackId addTypePack(TypePackVar&& tp);
TypePackId addTypePack(TypePack&& tp);
TypePackId addTypePack(const std::vector<TypeId>& ty);
TypePackId addTypePack(const std::vector<TypeId>& ty, std::optional<TypePackId> tail);
TypePackId addTypePack(std::initializer_list<TypeId>&& ty);
TypePackId freshTypePack(const ScopePtr& scope);
TypePackId freshTypePack(TypeLevel level);
TypePackId DEPRECATED_freshTypePack(const ScopePtr& scope, bool canBeGeneric = false);
TypePackId DEPRECATED_freshTypePack(TypeLevel level, bool canBeGeneric = false);
TypeId resolveType(const ScopePtr& scope, const AstType& annotation, bool canBeGeneric = false);
TypePackId resolveTypePack(const ScopePtr& scope, const AstTypeList& types);
TypePackId resolveTypePack(const ScopePtr& scope, const AstTypePack& annotation);
TypeId instantiateTypeFun(const ScopePtr& scope, const TypeFun& tf, const std::vector<TypeId>& typeParams, const Location& location);
// Note: `scope` must be a fresh scope.
std::pair<std::vector<TypeId>, std::vector<TypePackId>> createGenericTypes(
const ScopePtr& scope, const AstNode& node, const AstArray<AstName>& genericNames, const AstArray<AstName>& genericPackNames);
public:
ErrorVec resolve(const PredicateVec& predicates, const ScopePtr& scope, bool sense);
private:
std::optional<TypeId> resolveLValue(const ScopePtr& scope, const LValue& lvalue);
std::optional<TypeId> resolveLValue(const RefinementMap& refis, const ScopePtr& scope, const LValue& lvalue);
void resolve(const PredicateVec& predicates, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr = false);
void resolve(const Predicate& predicate, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr);
void resolve(const TruthyPredicate& truthyP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr);
void resolve(const AndPredicate& andP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense);
void resolve(const OrPredicate& orP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense);
void resolve(const IsAPredicate& isaP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense);
void resolve(const TypeGuardPredicate& typeguardP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense);
void DEPRECATED_resolve(const TypeGuardPredicate& typeguardP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense);
void resolve(const EqPredicate& eqP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense);
bool isNonstrictMode() const;
public:
/** Extract the types in a type pack, given the assumption that the pack must have some exact length.
* TypePacks can have free tails, which means that inference has not yet determined the length of the pack.
* Calling this function means submitting evidence that the pack must have the length provided.
* If the pack is known not to have the correct length, an error will be reported.
* The return vector is always of the exact requested length. In the event that the pack's length does
* not match up, excess TypeIds will be ErrorTypeVars.
*/
std::vector<TypeId> unTypePack(const ScopePtr& scope, TypePackId pack, size_t expectedLength, const Location& location);
TypeArena globalTypes;
ModuleResolver* resolver;
SourceModule globalNames; // names for symbols entered into globalScope
ScopePtr globalScope; // shared by all modules
ModulePtr currentModule;
ModuleName currentModuleName;
Instantiation instantiation;
Quantification quantification;
Anyification anyification;
ApplyTypeFunction applyTypeFunction;
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope;
InternalErrorReporter* iceHandler;
public:
const TypeId nilType;
const TypeId numberType;
const TypeId stringType;
const TypeId booleanType;
const TypeId threadType;
const TypeId anyType;
const TypeId errorType;
const TypeId optionalNumberType;
const TypePackId anyTypePack;
const TypePackId errorTypePack;
private:
int checkRecursionCount = 0;
int recursionCount = 0;
};
struct Binding
{
TypeId typeId;
Location location;
bool deprecated = false;
std::string deprecatedSuggestion;
std::optional<std::string> documentationSymbol;
};
struct Scope
{
explicit Scope(TypePackId returnType); // root scope
explicit Scope(const ScopePtr& parent, int subLevel = 0); // child scope. Parent must not be nullptr.
const ScopePtr parent; // null for the root
std::unordered_map<Symbol, Binding> bindings;
TypePackId returnType;
bool breakOk = false;
std::optional<TypePackId> varargPack;
TypeLevel level;
std::unordered_map<Name, TypeFun> exportedTypeBindings;
std::unordered_map<Name, TypeFun> privateTypeBindings;
std::unordered_map<Name, Location> typeAliasLocations;
std::unordered_map<Name, std::unordered_map<Name, TypeFun>> importedTypeBindings;
std::optional<TypeId> lookup(const Symbol& name);
std::optional<TypeFun> lookupType(const Name& name);
std::optional<TypeFun> lookupImportedType(const Name& moduleAlias, const Name& name);
std::unordered_map<Name, TypePackId> privateTypePackBindings;
std::optional<TypePackId> lookupPack(const Name& name);
// WARNING: This function linearly scans for a string key of equal value! It is thus O(n**2)
std::optional<Binding> linearSearchForBinding(const std::string& name, bool traverseScopeChain = true);
RefinementMap refinements;
// For mutually recursive type aliases, it's important that
// they use the same types for the same names.
// For instance, in `type Tree<T> { data: T, children: Forest<T> } type Forest<T> = {Tree<T>}`
// we need that the generic type `T` in both cases is the same, so we use a cache.
std::unordered_map<Name, TypeId> typeAliasParameters;
};
// Unit test hook
void setPrintLine(void (*pl)(const std::string& s));
void resetPrintLine();
} // namespace Luau

View file

@ -0,0 +1,161 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/TypeVar.h"
#include "Luau/Unifiable.h"
#include "Luau/Variant.h"
#include <optional>
#include <set>
LUAU_FASTFLAG(LuauAddMissingFollow)
namespace Luau
{
struct TypeArena;
struct TypePack;
struct VariadicTypePack;
struct TypePackVar;
using TypePackId = const TypePackVar*;
using FreeTypePack = Unifiable::Free;
using BoundTypePack = Unifiable::Bound<TypePackId>;
using GenericTypePack = Unifiable::Generic;
using TypePackVariant = Unifiable::Variant<TypePackId, TypePack, VariadicTypePack>;
/* A TypePack is a rope-like string of TypeIds. We use this structure to encode
* notions like packs of unknown length and packs of any length, as well as more
* nuanced compositions like "a pack which is a number prepended to this other pack,"
* or "a pack that is 2 numbers followed by any number of any other types."
*/
struct TypePack
{
std::vector<TypeId> head;
std::optional<TypePackId> tail;
};
struct VariadicTypePack
{
TypeId ty;
};
struct TypePackVar
{
explicit TypePackVar(const TypePackVariant& ty);
explicit TypePackVar(TypePackVariant&& ty);
TypePackVar(TypePackVariant&& ty, bool persistent);
bool operator==(const TypePackVar& rhs) const;
TypePackVar& operator=(TypePackVariant&& tp);
TypePackVariant ty;
bool persistent = false;
// Pointer to the type arena that allocated this type.
// Do not depend on the value of this under any circumstances. This is for
// debugging purposes only. This is only set in debug builds; it is nullptr
// in all other environments.
TypeArena* owningArena = nullptr;
};
/* Walk the set of TypeIds in a TypePack.
*
* Like TypeVars, individual TypePacks can be free, generic, or any.
*
* We afford the ability to work with these kinds of packs by giving the
* iterator a .tail() property that yields the tail-most TypePack in the
* rope.
*
* It is very commonplace to want to walk each type in a pack, then handle
* the tail specially. eg when checking parameters, it might be the case
* that the parameter pack ends with a VariadicTypePack. In this case, we
* want to allow any number of extra arguments.
*
* The iterator obtained by calling end(tp) does not have a .tail(), but is
* equivalent with end(tp2) for any two type packs.
*/
struct TypePackIterator
{
using value_type = Luau::TypeId;
using pointer = value_type*;
using reference = value_type&;
using difference_type = size_t;
using iterator_category = std::input_iterator_tag;
TypePackIterator() = default;
explicit TypePackIterator(TypePackId tp);
TypePackIterator& operator++();
TypePackIterator operator++(int);
bool operator!=(const TypePackIterator& rhs);
bool operator==(const TypePackIterator& rhs);
const TypeId& operator*();
/** Return the tail of a TypePack.
* This may *only* be called on an iterator that has been incremented to the end.
* Returns nullopt if the pack has fixed length.
*/
std::optional<TypePackId> tail();
friend TypePackIterator end(TypePackId tp);
private:
TypePackId currentTypePack = nullptr;
const TypePack* tp = nullptr;
size_t currentIndex = 0;
};
TypePackIterator begin(TypePackId tp);
TypePackIterator end(TypePackId tp);
using SeenSet = std::set<std::pair<void*, void*>>;
bool areEqual(SeenSet& seen, const TypePackVar& lhs, const TypePackVar& rhs);
TypePackId follow(TypePackId tp);
size_t size(const TypePackId tp);
size_t size(const TypePack& tp);
std::optional<TypeId> first(TypePackId tp);
TypePackVar* asMutable(TypePackId tp);
TypePack* asMutable(const TypePack* tp);
template<typename T>
const T* get(TypePackId tp)
{
if (FFlag::LuauAddMissingFollow)
{
LUAU_ASSERT(tp);
if constexpr (!std::is_same_v<T, BoundTypePack>)
LUAU_ASSERT(get_if<BoundTypePack>(&tp->ty) == nullptr);
}
return get_if<T>(&(tp->ty));
}
template<typename T>
T* getMutable(TypePackId tp)
{
if (FFlag::LuauAddMissingFollow)
{
LUAU_ASSERT(tp);
if constexpr (!std::is_same_v<T, BoundTypePack>)
LUAU_ASSERT(get_if<BoundTypePack>(&tp->ty) == nullptr);
}
return get_if<T>(&(asMutable(tp)->ty));
}
/// Returns true if the type pack is known to be empty (no types in the head and no/an empty tail).
bool isEmpty(TypePackId tp);
/// Flattens out a type pack. Also returns a valid TypePackId tail if the type pack's full size is not known
std::pair<std::vector<TypeId>, std::optional<TypePackId>> flatten(TypePackId tp);
} // namespace Luau

View file

@ -0,0 +1,19 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Error.h"
#include "Luau/Location.h"
#include "Luau/TypeVar.h"
#include <memory>
#include <optional>
namespace Luau
{
using ScopePtr = std::shared_ptr<struct Scope>;
std::optional<TypeId> findMetatableEntry(ErrorVec& errors, const ScopePtr& globalScope, TypeId type, std::string entry, Location location);
std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, const ScopePtr& globalScope, TypeId ty, Name name, Location location);
} // namespace Luau

View file

@ -0,0 +1,531 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Predicate.h"
#include "Luau/Unifiable.h"
#include "Luau/Variant.h"
#include "Luau/Common.h"
#include <set>
#include <string>
#include <map>
#include <unordered_set>
#include <unordered_map>
#include <vector>
#include <deque>
#include <memory>
#include <optional>
LUAU_FASTINT(LuauTableTypeMaximumStringifierLength)
LUAU_FASTINT(LuauTypeMaximumStringifierLength)
LUAU_FASTFLAG(LuauAddMissingFollow)
namespace Luau
{
struct TypeArena;
/**
* There are three kinds of type variables:
* - `Free` variables are metavariables, which stand for unconstrained types.
* - `Bound` variables are metavariables that have an equality constraint.
* - `Generic` variables are type variables that are bound by generic functions.
*
* For example, consider the program:
* ```
* function(x, y) x.f = y end
* ```
* To typecheck this, we first introduce free metavariables for the types of `x` and `y`:
* ```
* function(x: X, y: Y) x.f = y end
* ```
* Type inference for the function body then produces the constraint:
* ```
* X = { f: Y }
* ```
* so `X` is now a bound metavariable. We can then quantify the metavariables,
* which replaces any bound metavariables by their binding, and free metavariables
* by bound generic variables:
* ```
* function<a>(x: { f: a }, y: a) x.f = y end
* ```
*/
// So... why `const T*` here rather than `T*`?
// It's because we've had problems caused by the type graph being mutated
// in ways it shouldn't be, for example mutating types from other modules.
// To try to control this, we make the use of types immutable by default,
// then provide explicit mutable access via getMutable and asMutable.
// This means we can grep for all the places we're mutating the type graph,
// and it makes it possible to provide other APIs (e.g. the txn log)
// which control mutable access to the type graph.
struct TypePackVar;
using TypePackId = const TypePackVar*;
// TODO: rename to Type? CLI-39100
struct TypeVar;
// Should never be null
using TypeId = const TypeVar*;
using Name = std::string;
// A free type var is one whose exact shape has yet to be fully determined.
using FreeTypeVar = Unifiable::Free;
// When a free type var is unified with any other, it is then "bound"
// to that type var, indicating that the two types are actually the same type.
using BoundTypeVar = Unifiable::Bound<TypeId>;
using GenericTypeVar = Unifiable::Generic;
using Tags = std::vector<std::string>;
using ModuleName = std::string;
struct PrimitiveTypeVar
{
enum Type
{
NilType, // ObjC #defines Nil :(
Boolean,
Number,
String,
Thread,
};
Type type;
std::optional<TypeId> metatable; // string has a metatable
explicit PrimitiveTypeVar(Type type)
: type(type)
{
}
explicit PrimitiveTypeVar(Type type, TypeId metatable)
: type(type)
, metatable(metatable)
{
}
};
struct FunctionArgument
{
Name name;
Location location;
};
struct FunctionDefinition
{
std::optional<ModuleName> definitionModuleName;
Location definitionLocation;
std::optional<Location> varargLocation;
Location originalNameLocation;
};
// TODO: Come up with a better name.
// TODO: Do we actually need this? We'll find out later if we can delete this.
// Does not exactly belong in TypeVar.h, but this is the only way to appease the compiler.
template<typename T>
struct ExprResult
{
T type;
PredicateVec predicates;
};
using MagicFunction = std::function<std::optional<ExprResult<TypePackId>>(
struct TypeChecker&, const std::shared_ptr<struct Scope>&, const class AstExprCall&, ExprResult<TypePackId>)>;
struct FunctionTypeVar
{
// Global monomorphic function
FunctionTypeVar(TypePackId argTypes, TypePackId retType, std::optional<FunctionDefinition> defn = {}, bool hasSelf = false);
// Global polymorphic function
FunctionTypeVar(std::vector<TypeId> generics, std::vector<TypePackId> genericPacks, TypePackId argTypes, TypePackId retType,
std::optional<FunctionDefinition> defn = {}, bool hasSelf = false);
// Local monomorphic function
FunctionTypeVar(TypeLevel level, TypePackId argTypes, TypePackId retType, std::optional<FunctionDefinition> defn = {}, bool hasSelf = false);
// Local polymorphic function
FunctionTypeVar(TypeLevel level, std::vector<TypeId> generics, std::vector<TypePackId> genericPacks, TypePackId argTypes, TypePackId retType,
std::optional<FunctionDefinition> defn = {}, bool hasSelf = false);
TypeLevel level;
/// These should all be generic
std::vector<TypeId> generics;
std::vector<TypePackId> genericPacks;
TypePackId argTypes;
std::vector<std::optional<FunctionArgument>> argNames;
TypePackId retType;
std::optional<FunctionDefinition> definition;
MagicFunction magicFunction = nullptr; // Function pointer, can be nullptr.
bool hasSelf;
Tags tags;
};
enum class TableState
{
// Sealed tables have an exact, known shape
Sealed,
// An unsealed table can have extra properties added to it
Unsealed,
// Tables which are not yet fully understood. We are still in the process of learning its shape.
Free,
// A table which is a generic parameter to a function. We know that certain properties are required,
// but we don't care about the full shape.
Generic,
};
struct TableIndexer
{
TableIndexer(TypeId indexType, TypeId indexResultType)
: indexType(indexType)
, indexResultType(indexResultType)
{
}
TypeId indexType;
TypeId indexResultType;
};
struct Property
{
TypeId type;
bool deprecated = false;
std::string deprecatedSuggestion;
std::optional<Location> location = std::nullopt;
Tags tags;
std::optional<std::string> documentationSymbol;
};
struct TableTypeVar
{
// We choose std::map over unordered_map here just because we have unit tests that compare
// textual outputs. I don't want to spend the effort making them resilient in the case where
// random events cause the iteration order of the map elements to change.
// If this shows up in a profile, we can revisit it.
using Props = std::map<Name, Property>;
TableTypeVar() = default;
explicit TableTypeVar(TableState state, TypeLevel level);
TableTypeVar(const Props& props, const std::optional<TableIndexer>& indexer, TypeLevel level, TableState state = TableState::Unsealed);
Props props;
std::optional<TableIndexer> indexer;
TableState state = TableState::Unsealed;
TypeLevel level;
std::optional<std::string> name;
// Sometimes we throw a type on a name to make for nicer error messages, but without creating any entry in the type namespace
// We need to know which is which when we stringify types.
std::optional<std::string> syntheticName;
std::map<Name, Location> methodDefinitionLocations;
std::vector<TypeId> instantiatedTypeParams;
ModuleName definitionModuleName;
std::optional<TypeId> boundTo;
Tags tags;
};
// Represents a metatable attached to a table typevar. Somewhat analogous to a bound typevar.
struct MetatableTypeVar
{
// Always points to a TableTypeVar.
TypeId table;
// Always points to either a TableTypeVar or a MetatableTypeVar.
TypeId metatable;
std::optional<std::string> syntheticName;
};
// Custom userdata of a class type
struct ClassUserData
{
virtual ~ClassUserData() {}
};
/** The type of a class.
*
* Classes behave like tables in many ways, but there are some important differences:
*
* The properties of a class are always exactly known.
* Classes optionally have a parent class.
* Two different classes that share the same properties are nevertheless distinct and mutually incompatible.
*/
struct ClassTypeVar
{
using Props = TableTypeVar::Props;
Name name;
Props props;
std::optional<TypeId> parent;
std::optional<TypeId> metatable; // metaclass?
Tags tags;
std::shared_ptr<ClassUserData> userData;
ClassTypeVar(
Name name, Props props, std::optional<TypeId> parent, std::optional<TypeId> metatable, Tags tags, std::shared_ptr<ClassUserData> userData)
: name(name)
, props(props)
, parent(parent)
, metatable(metatable)
, tags(tags)
, userData(userData)
{
}
};
struct TypeFun
{
/// These should all be generic
std::vector<TypeId> typeParams;
/** The underlying type.
*
* WARNING! This is not safe to use as a type if typeParams is not empty!!
* You must first use TypeChecker::instantiateTypeFun to turn it into a real type.
*/
TypeId type;
};
// Anything! All static checking is off.
struct AnyTypeVar
{
};
struct UnionTypeVar
{
std::vector<TypeId> options;
};
struct IntersectionTypeVar
{
std::vector<TypeId> parts;
};
struct LazyTypeVar
{
std::function<TypeId()> thunk;
};
using ErrorTypeVar = Unifiable::Error;
using TypeVariant = Unifiable::Variant<TypeId, PrimitiveTypeVar, FunctionTypeVar, TableTypeVar, MetatableTypeVar, ClassTypeVar, AnyTypeVar,
UnionTypeVar, IntersectionTypeVar, LazyTypeVar>;
struct TypeVar final
{
explicit TypeVar(const TypeVariant& ty)
: ty(ty)
{
}
explicit TypeVar(TypeVariant&& ty)
: ty(std::move(ty))
{
}
TypeVar(const TypeVariant& ty, bool persistent)
: ty(ty)
, persistent(persistent)
{
}
TypeVariant ty;
// Kludge: A persistent TypeVar is one that belongs to the global scope.
// Global type bindings are immutable but are reused many times.
// Persistent TypeVars do not get cloned.
bool persistent = false;
std::optional<std::string> documentationSymbol;
// Pointer to the type arena that allocated this type.
// Do not depend on the value of this under any circumstances. This is for
// debugging purposes only. This is only set in debug builds; it is nullptr
// in all other environments.
TypeArena* owningArena = nullptr;
bool operator==(const TypeVar& rhs) const;
bool operator!=(const TypeVar& rhs) const;
TypeVar& operator=(const TypeVariant& rhs);
TypeVar& operator=(TypeVariant&& rhs);
};
using SeenSet = std::set<std::pair<void*, void*>>;
bool areEqual(SeenSet& seen, const TypeVar& lhs, const TypeVar& rhs);
// Follow BoundTypeVars until we get to something real
TypeId follow(TypeId t);
std::vector<TypeId> flattenIntersection(TypeId ty);
bool isPrim(TypeId ty, PrimitiveTypeVar::Type primType);
bool isNil(TypeId ty);
bool isBoolean(TypeId ty);
bool isNumber(TypeId ty);
bool isString(TypeId ty);
bool isThread(TypeId ty);
bool isOptional(TypeId ty);
bool isTableIntersection(TypeId ty);
bool isOverloadedFunction(TypeId ty);
std::optional<TypeId> getMetatable(TypeId type);
TableTypeVar* getMutableTableType(TypeId type);
const TableTypeVar* getTableType(TypeId type);
// If the type has a name, return that. Else if it has a synthetic name, return that.
// Returns nullptr if the type has no name.
const std::string* getName(TypeId type);
// Checks whether a union contains all types of another union.
bool isSubset(const UnionTypeVar& super, const UnionTypeVar& sub);
// Checks if a type conains generic type binders
bool isGeneric(const TypeId ty);
// Checks if a type may be instantiated to one containing generic type binders
bool maybeGeneric(const TypeId ty);
struct SingletonTypes
{
const TypeId nilType = &nilType_;
const TypeId numberType = &numberType_;
const TypeId stringType = &stringType_;
const TypeId booleanType = &booleanType_;
const TypeId threadType = &threadType_;
const TypeId anyType = &anyType_;
const TypeId errorType = &errorType_;
SingletonTypes();
SingletonTypes(const SingletonTypes&) = delete;
void operator=(const SingletonTypes&) = delete;
private:
std::unique_ptr<struct TypeArena> arena;
TypeVar nilType_;
TypeVar numberType_;
TypeVar stringType_;
TypeVar booleanType_;
TypeVar threadType_;
TypeVar anyType_;
TypeVar errorType_;
TypeId makeStringMetatable();
};
extern SingletonTypes singletonTypes;
void persist(TypeId ty);
void persist(TypePackId tp);
struct ToDotOptions
{
bool showPointers = true; // Show pointer value in the node label
bool duplicatePrimitives = true; // Display primitive types and 'any' as separate nodes
};
std::string toDot(TypeId ty, const ToDotOptions& opts);
std::string toDot(TypePackId tp, const ToDotOptions& opts);
std::string toDot(TypeId ty);
std::string toDot(TypePackId tp);
void dumpDot(TypeId ty);
void dumpDot(TypePackId tp);
const TypeLevel* getLevel(TypeId ty);
TypeLevel* getMutableLevel(TypeId ty);
const Property* lookupClassProp(const ClassTypeVar* cls, const Name& name);
bool isSubclass(const ClassTypeVar* cls, const ClassTypeVar* parent);
bool hasGeneric(TypeId ty);
bool hasGeneric(TypePackId tp);
TypeVar* asMutable(TypeId ty);
template<typename T>
const T* get(TypeId tv)
{
if (FFlag::LuauAddMissingFollow)
{
LUAU_ASSERT(tv);
if constexpr (!std::is_same_v<T, BoundTypeVar>)
LUAU_ASSERT(get_if<BoundTypeVar>(&tv->ty) == nullptr);
}
return get_if<T>(&tv->ty);
}
template<typename T>
T* getMutable(TypeId tv)
{
if (FFlag::LuauAddMissingFollow)
{
LUAU_ASSERT(tv);
if constexpr (!std::is_same_v<T, BoundTypeVar>)
LUAU_ASSERT(get_if<BoundTypeVar>(&tv->ty) == nullptr);
}
return get_if<T>(&asMutable(tv)->ty);
}
/* Traverses the UnionTypeVar yielding each TypeId.
* If the iterator encounters a nested UnionTypeVar, it will instead yield each TypeId within.
*
* Beware: the iterator does not currently filter for unique TypeIds. This may change in the future.
*/
struct UnionTypeVarIterator
{
using value_type = Luau::TypeId;
using pointer = value_type*;
using reference = value_type&;
using difference_type = size_t;
using iterator_category = std::input_iterator_tag;
explicit UnionTypeVarIterator(const UnionTypeVar* utv);
UnionTypeVarIterator& operator++();
UnionTypeVarIterator operator++(int);
bool operator!=(const UnionTypeVarIterator& rhs);
bool operator==(const UnionTypeVarIterator& rhs);
const TypeId& operator*();
friend UnionTypeVarIterator end(const UnionTypeVar* utv);
private:
UnionTypeVarIterator() = default;
// (UnionTypeVar* utv, size_t currentIndex)
using SavedIterInfo = std::pair<const UnionTypeVar*, size_t>;
std::deque<SavedIterInfo> stack;
std::unordered_set<const UnionTypeVar*> seen; // Only needed to protect the iterator from hanging the thread.
void advance();
void descend();
};
UnionTypeVarIterator begin(const UnionTypeVar* utv);
UnionTypeVarIterator end(const UnionTypeVar* utv);
using TypeIdPredicate = std::function<std::optional<TypeId>(TypeId)>;
std::vector<TypeId> filterMap(TypeId type, TypeIdPredicate predicate);
// TEMP: Clip this prototype with FFlag::LuauStringMetatable
std::optional<ExprResult<TypePackId>> magicFunctionFormat(
struct TypeChecker& typechecker, const std::shared_ptr<struct Scope>& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult);
} // namespace Luau

View file

@ -0,0 +1,134 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Common.h"
#include <vector>
#include <memory>
namespace Luau
{
void* pagedAllocate(size_t size);
void pagedDeallocate(void* ptr);
void pagedFreeze(void* ptr, size_t size);
void pagedUnfreeze(void* ptr, size_t size);
template<typename T>
class TypedAllocator
{
public:
TypedAllocator()
{
appendBlock();
}
~TypedAllocator()
{
if (frozen)
unfreeze();
free();
}
template<typename... Args>
T* allocate(Args&&... args)
{
LUAU_ASSERT(!frozen);
if (currentBlockSize >= kBlockSize)
{
LUAU_ASSERT(currentBlockSize == kBlockSize);
appendBlock();
}
T* block = stuff.back();
T* res = block + currentBlockSize;
new (res) T(std::forward<Args&&...>(args...));
++currentBlockSize;
return res;
}
bool contains(const T* ptr) const
{
for (T* block : stuff)
if (ptr >= block && ptr < block + kBlockSize)
return true;
return false;
}
bool empty() const
{
return stuff.size() == 1 && currentBlockSize == 0;
}
size_t size() const
{
return kBlockSize * (stuff.size() - 1) + currentBlockSize;
}
void clear()
{
if (frozen)
unfreeze();
free();
appendBlock();
}
void freeze()
{
for (T* block : stuff)
pagedFreeze(block, kBlockSizeBytes);
frozen = true;
}
void unfreeze()
{
for (T* block : stuff)
pagedUnfreeze(block, kBlockSizeBytes);
frozen = false;
}
bool isFrozen()
{
return frozen;
}
private:
void free()
{
LUAU_ASSERT(!frozen);
for (T* block : stuff)
{
size_t blockSize = (block == stuff.back()) ? currentBlockSize : kBlockSize;
for (size_t i = 0; i < blockSize; ++i)
block[i].~T();
pagedDeallocate(block);
}
stuff.clear();
currentBlockSize = 0;
}
void appendBlock()
{
void* block = pagedAllocate(kBlockSizeBytes);
if (!block)
throw std::bad_alloc();
stuff.emplace_back(static_cast<T*>(block));
currentBlockSize = 0;
}
bool frozen = false;
std::vector<T*> stuff;
size_t currentBlockSize = 0;
static constexpr size_t kBlockSizeBytes = 32768;
static constexpr size_t kBlockSize = kBlockSizeBytes / sizeof(T);
};
} // namespace Luau

View file

@ -0,0 +1,123 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Variant.h"
#include <string>
namespace Luau
{
/**
* The 'level' of a TypeVar is an indirect way to talk about the scope that it 'belongs' too.
* To start, read http://okmij.org/ftp/ML/generalization.html
*
* We extend the idea by adding a "sub-level" which helps us to differentiate sibling scopes
* within a single larger scope.
*
* We need this because we try to prototype functions and add them to the type environment before
* we check the function bodies. This allows us to properly typecheck many scenarios where there
* is no single good order in which to typecheck a program.
*/
struct TypeLevel
{
int level = 0;
int subLevel = 0;
// Returns true if the typelevel "this" is "bigger" than rhs
bool subsumes(const TypeLevel& rhs) const
{
if (level < rhs.level)
return true;
if (level > rhs.level)
return false;
if (subLevel == rhs.subLevel)
return true; // if level == rhs.level and subLevel == rhs.subLevel, then they are the exact same TypeLevel
// Sibling TypeLevels (that is, TypeLevels that share a level but have a different subLevel) are not considered to subsume one another
return false;
}
TypeLevel incr() const
{
TypeLevel result;
result.level = level + 1;
result.subLevel = 0;
return result;
}
};
inline TypeLevel min(const TypeLevel& a, const TypeLevel& b)
{
if (a.subsumes(b))
return a;
else
return b;
}
namespace Unifiable
{
using Name = std::string;
struct Free
{
explicit Free(TypeLevel level);
Free(TypeLevel level, bool DEPRECATED_canBeGeneric);
int index;
TypeLevel level;
// Removed by FFlag::LuauRankNTypes
bool DEPRECATED_canBeGeneric = false;
// True if this free type variable is part of a mutually
// recursive type alias whose definitions haven't been
// resolved yet.
bool forwardedTypeAlias = false;
private:
static int nextIndex;
};
template<typename Id>
struct Bound
{
explicit Bound(Id boundTo)
: boundTo(boundTo)
{
}
Id boundTo;
};
struct Generic
{
// By default, generics are global, with a synthetic name
Generic();
explicit Generic(TypeLevel level);
explicit Generic(const Name& name);
Generic(TypeLevel level, const Name& name);
int index;
TypeLevel level;
Name name;
bool explicitName;
private:
static int nextIndex;
};
struct Error
{
Error();
int index;
private:
static int nextIndex;
};
template<typename Id, typename... Value>
using Variant = Variant<Free, Bound<Id>, Generic, Error, Value...>;
} // namespace Unifiable
} // namespace Luau

View file

@ -0,0 +1,98 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Error.h"
#include "Luau/Location.h"
#include "Luau/TxnLog.h"
#include "Luau/TypeInfer.h"
#include "Luau/Module.h" // FIXME: For TypeArena. It merits breaking out into its own header.
#include <unordered_set>
namespace Luau
{
enum Variance
{
Covariant,
Invariant
};
struct UnifierCounters
{
int recursionCount = 0;
int iterationCount = 0;
};
struct Unifier
{
TypeArena* const types;
Mode mode;
ScopePtr globalScope; // sigh. Needed solely to get at string's metatable.
TxnLog log;
ErrorVec errors;
Location location;
Variance variance = Covariant;
CountMismatch::Context ctx = CountMismatch::Arg;
std::shared_ptr<UnifierCounters> counters;
InternalErrorReporter* iceHandler;
Unifier(TypeArena* types, Mode mode, ScopePtr globalScope, const Location& location, Variance variance, InternalErrorReporter* iceHandler);
Unifier(TypeArena* types, Mode mode, ScopePtr globalScope, const std::vector<std::pair<TypeId, TypeId>>& seen, const Location& location,
Variance variance, InternalErrorReporter* iceHandler, const std::shared_ptr<UnifierCounters>& counters = nullptr);
// Test whether the two type vars unify. Never commits the result.
ErrorVec canUnify(TypeId superTy, TypeId subTy);
ErrorVec canUnify(TypePackId superTy, TypePackId subTy, bool isFunctionCall = false);
/** Attempt to unify left with right.
* Populate the vector errors with any type errors that may arise.
* Populate the transaction log with the set of TypeIds that need to be reset to undo the unification attempt.
*/
void tryUnify(TypeId superTy, TypeId subTy, bool isFunctionCall = false, bool isIntersection = false);
private:
void tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall = false, bool isIntersection = false);
void tryUnifyPrimitives(TypeId superTy, TypeId subTy);
void tryUnifyFunctions(TypeId superTy, TypeId subTy, bool isFunctionCall = false);
void tryUnifyTables(TypeId left, TypeId right, bool isIntersection = false);
void tryUnifyFreeTable(TypeId free, TypeId other);
void tryUnifySealedTables(TypeId left, TypeId right, bool isIntersection);
void tryUnifyWithMetatable(TypeId metatable, TypeId other, bool reversed);
void tryUnifyWithClass(TypeId superTy, TypeId subTy, bool reversed);
void tryUnify(const TableIndexer& superIndexer, const TableIndexer& subIndexer);
public:
void tryUnify(TypePackId superTy, TypePackId subTy, bool isFunctionCall = false);
private:
void tryUnify_(TypePackId superTy, TypePackId subTy, bool isFunctionCall = false);
void tryUnifyVariadics(TypePackId superTy, TypePackId subTy, bool reversed, int subOffset = 0);
void tryUnifyWithAny(TypeId any, TypeId ty);
void tryUnifyWithAny(TypePackId any, TypePackId ty);
std::optional<TypeId> findTablePropertyRespectingMeta(TypeId lhsType, Name name);
std::optional<TypeId> findMetatableEntry(TypeId type, std::string entry);
public:
// Report an "infinite type error" if the type "needle" already occurs within "haystack"
void occursCheck(TypeId needle, TypeId haystack);
void occursCheck(std::unordered_set<TypeId>& seen, TypeId needle, TypeId haystack);
void occursCheck(TypePackId needle, TypePackId haystack);
void occursCheck(std::unordered_set<TypePackId>& seen, TypePackId needle, TypePackId haystack);
Unifier makeChildUnifier();
private:
bool isNonstrictMode() const;
void checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, TypeId wantedType, TypeId givenType);
[[noreturn]] void ice(const std::string& message, const Location& location);
[[noreturn]] void ice(const std::string& message);
};
} // namespace Luau

View file

@ -0,0 +1,302 @@
// 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"
#ifndef LUAU_USE_STD_VARIANT
#define LUAU_USE_STD_VARIANT 0
#endif
#if LUAU_USE_STD_VARIANT
#include <variant>
#else
#include <new>
#include <type_traits>
#include <initializer_list>
#include <stddef.h>
#endif
namespace Luau
{
#if LUAU_USE_STD_VARIANT
template<typename... Ts>
using Variant = std::variant<Ts...>;
template<class Visitor, class Variant>
auto visit(Visitor&& vis, Variant&& var)
{
// This change resolves the ABI issues with std::variant on libc++; std::visit normally throws bad_variant_access
// but it requires an update to libc++.dylib which ships with macOS 10.14. To work around this, we assert on valueless
// variants since we will never generate them and call into a libc++ function that doesn't throw.
LUAU_ASSERT(!var.valueless_by_exception());
#ifdef __APPLE__
// See https://stackoverflow.com/a/53868971/503215
return std::__variant_detail::__visitation::__variant::__visit_value(vis, var);
#else
return std::visit(vis, var);
#endif
}
using std::get_if;
#else
template<typename... Ts>
class Variant
{
static_assert(sizeof...(Ts) > 0, "variant must have at least 1 type (empty variants are ill-formed)");
static_assert(std::disjunction_v<std::is_void<Ts>...> == false, "variant does not allow void as an alternative type");
static_assert(std::disjunction_v<std::is_reference<Ts>...> == false, "variant does not allow references as an alternative type");
static_assert(std::disjunction_v<std::is_array<Ts>...> == false, "variant does not allow arrays as an alternative type");
private:
template<typename T>
static constexpr int getTypeId()
{
using TT = std::decay_t<T>;
constexpr int N = sizeof...(Ts);
constexpr bool is[N] = {std::is_same_v<TT, Ts>...};
for (int i = 0; i < N; ++i)
if (is[i])
return i;
return -1;
}
template<typename T, typename... Tail>
struct First
{
using type = T;
};
public:
using first_alternative = typename First<Ts...>::type;
Variant()
{
static_assert(std::is_default_constructible_v<first_alternative>, "first alternative type must be default constructible");
typeId = 0;
new (&storage) first_alternative();
}
template<typename T>
Variant(T&& value, std::enable_if_t<getTypeId<T>() >= 0>* = 0)
{
using TT = std::decay_t<T>;
constexpr int tid = getTypeId<T>();
typeId = tid;
new (&storage) TT(value);
}
Variant(const Variant& other)
{
typeId = other.typeId;
tableCopy[typeId](&storage, &other.storage);
}
Variant(Variant&& other)
{
typeId = other.typeId;
tableMove[typeId](&storage, &other.storage);
}
~Variant()
{
tableDtor[typeId](&storage);
}
Variant& operator=(const Variant& other)
{
Variant copy(other);
// static_cast<T&&> is equivalent to std::move() but faster in Debug
return *this = static_cast<Variant&&>(copy);
}
Variant& operator=(Variant&& other)
{
if (this != &other)
{
tableDtor[typeId](&storage);
typeId = other.typeId;
tableMove[typeId](&storage, &other.storage); // nothrow
}
return *this;
}
template<typename T>
const T* get_if() const
{
constexpr int tid = getTypeId<T>();
static_assert(tid >= 0, "unsupported T");
return tid == typeId ? reinterpret_cast<const T*>(&storage) : nullptr;
}
template<typename T>
T* get_if()
{
constexpr int tid = getTypeId<T>();
static_assert(tid >= 0, "unsupported T");
return tid == typeId ? reinterpret_cast<T*>(&storage) : nullptr;
}
bool valueless_by_exception() const
{
return false;
}
int index() const
{
return typeId;
}
bool operator==(const Variant& other) const
{
static constexpr FnPred table[sizeof...(Ts)] = {&fnPredEq<Ts>...};
return typeId == other.typeId && table[typeId](&storage, &other.storage);
}
bool operator!=(const Variant& other) const
{
return !(*this == other);
}
private:
static constexpr size_t cmax(std::initializer_list<size_t> l)
{
size_t res = 0;
for (size_t i : l)
res = (res < i) ? i : res;
return res;
}
static constexpr size_t storageSize = cmax({sizeof(Ts)...});
static constexpr size_t storageAlign = cmax({alignof(Ts)...});
using FnCopy = void (*)(void*, const void*);
using FnMove = void (*)(void*, void*);
using FnDtor = void (*)(void*);
using FnPred = bool (*)(const void*, const void*);
template<typename T>
static void fnCopy(void* dst, const void* src)
{
new (dst) T(*static_cast<const T*>(src));
}
template<typename T>
static void fnMove(void* dst, void* src)
{
// static_cast<T&&> is equivalent to std::move() but faster in Debug
new (dst) T(static_cast<T&&>(*static_cast<T*>(src)));
}
template<typename T>
static void fnDtor(void* dst)
{
static_cast<T*>(dst)->~T();
}
template<typename T>
static bool fnPredEq(const void* lhs, const void* rhs)
{
return *static_cast<const T*>(lhs) == *static_cast<const T*>(rhs);
}
static constexpr FnCopy tableCopy[sizeof...(Ts)] = {&fnCopy<Ts>...};
static constexpr FnMove tableMove[sizeof...(Ts)] = {&fnMove<Ts>...};
static constexpr FnDtor tableDtor[sizeof...(Ts)] = {&fnDtor<Ts>...};
int typeId;
alignas(storageAlign) char storage[storageSize];
template<class Visitor, typename... _Ts>
friend auto visit(Visitor&& vis, const Variant<_Ts...>& var);
template<class Visitor, typename... _Ts>
friend auto visit(Visitor&& vis, Variant<_Ts...>& var);
};
template<typename T, typename... Ts>
const T* get_if(const Variant<Ts...>* var)
{
return var ? var->template get_if<T>() : nullptr;
}
template<typename T, typename... Ts>
T* get_if(Variant<Ts...>* var)
{
return var ? var->template get_if<T>() : nullptr;
}
template<typename Visitor, typename Result, typename T>
static void fnVisitR(Visitor& vis, Result& dst, std::conditional_t<std::is_const_v<T>, const void, void>* src)
{
dst = vis(*static_cast<T*>(src));
}
template<typename Visitor, typename T>
static void fnVisitV(Visitor& vis, std::conditional_t<std::is_const_v<T>, const void, void>* src)
{
vis(*static_cast<T*>(src));
}
template<class Visitor, typename... Ts>
auto visit(Visitor&& vis, const Variant<Ts...>& var)
{
using Result = std::invoke_result_t<Visitor, typename Variant<Ts...>::first_alternative>;
static_assert(std::conjunction_v<std::is_same<Result, std::invoke_result_t<Visitor, Ts>>...>,
"visitor result type must be consistent between alternatives");
if constexpr (std::is_same_v<Result, void>)
{
using FnVisitV = void (*)(Visitor&, const void*);
static const FnVisitV tableVisit[sizeof...(Ts)] = {&fnVisitV<Visitor, const Ts>...};
tableVisit[var.typeId](vis, &var.storage);
}
else
{
using FnVisitR = void (*)(Visitor&, Result&, const void*);
static const FnVisitR tableVisit[sizeof...(Ts)] = {&fnVisitR<Visitor, Result, const Ts>...};
Result res;
tableVisit[var.typeId](vis, res, &var.storage);
return res;
}
}
template<class Visitor, typename... Ts>
auto visit(Visitor&& vis, Variant<Ts...>& var)
{
using Result = std::invoke_result_t<Visitor, typename Variant<Ts...>::first_alternative&>;
static_assert(std::conjunction_v<std::is_same<Result, std::invoke_result_t<Visitor, Ts&>>...>,
"visitor result type must be consistent between alternatives");
if constexpr (std::is_same_v<Result, void>)
{
using FnVisitV = void (*)(Visitor&, void*);
static const FnVisitV tableVisit[sizeof...(Ts)] = {&fnVisitV<Visitor, Ts>...};
tableVisit[var.typeId](vis, &var.storage);
}
else
{
using FnVisitR = void (*)(Visitor&, Result&, void*);
static const FnVisitR tableVisit[sizeof...(Ts)] = {&fnVisitR<Visitor, Result, Ts>...};
Result res;
tableVisit[var.typeId](vis, res, &var.storage);
return res;
}
}
#endif
template<class>
inline constexpr bool always_false_v = false;
} // namespace Luau

View file

@ -0,0 +1,200 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/TypeVar.h"
#include "Luau/TypePack.h"
namespace Luau
{
namespace visit_detail
{
/**
* Apply f(tid, t, seen) if doing so would pass type checking, else apply f(tid, t)
*
* We do this to permit (but not require) TypeVar visitors to accept the seen set as an argument.
*/
template<typename F, typename A, typename B, typename C>
auto apply(A tid, const B& t, C& c, F& f) -> decltype(f(tid, t, c))
{
return f(tid, t, c);
}
template<typename A, typename B, typename C, typename F>
auto apply(A tid, const B& t, C&, F& f) -> decltype(f(tid, t))
{
return f(tid, t);
}
inline bool hasSeen(std::unordered_set<void*>& seen, const void* tv)
{
void* ttv = const_cast<void*>(tv);
return !seen.insert(ttv).second;
}
inline void unsee(std::unordered_set<void*>& seen, const void* tv)
{
void* ttv = const_cast<void*>(tv);
seen.erase(ttv);
}
template<typename F>
void visit(TypePackId tp, F& f, std::unordered_set<void*>& seen);
template<typename F>
void visit(TypeId ty, F& f, std::unordered_set<void*>& seen)
{
if (visit_detail::hasSeen(seen, ty))
{
f.cycle(ty);
return;
}
if (auto btv = get<BoundTypeVar>(ty))
{
if (apply(ty, *btv, seen, f))
visit(btv->boundTo, f, seen);
}
else if (auto ftv = get<FreeTypeVar>(ty))
apply(ty, *ftv, seen, f);
else if (auto gtv = get<GenericTypeVar>(ty))
apply(ty, *gtv, seen, f);
else if (auto etv = get<ErrorTypeVar>(ty))
apply(ty, *etv, seen, f);
else if (auto ptv = get<PrimitiveTypeVar>(ty))
apply(ty, *ptv, seen, f);
else if (auto ftv = get<FunctionTypeVar>(ty))
{
if (apply(ty, *ftv, seen, f))
{
visit(ftv->argTypes, f, seen);
visit(ftv->retType, f, seen);
}
}
else if (auto ttv = get<TableTypeVar>(ty))
{
if (apply(ty, *ttv, seen, f))
{
for (auto& [_name, prop] : ttv->props)
visit(prop.type, f, seen);
if (ttv->indexer)
{
visit(ttv->indexer->indexType, f, seen);
visit(ttv->indexer->indexResultType, f, seen);
}
}
}
else if (auto mtv = get<MetatableTypeVar>(ty))
{
if (apply(ty, *mtv, seen, f))
{
visit(mtv->table, f, seen);
visit(mtv->metatable, f, seen);
}
}
else if (auto ctv = get<ClassTypeVar>(ty))
{
if (apply(ty, *ctv, seen, f))
{
for (const auto& [name, prop] : ctv->props)
visit(prop.type, f, seen);
if (ctv->parent)
visit(*ctv->parent, f, seen);
if (ctv->metatable)
visit(*ctv->metatable, f, seen);
}
}
else if (auto atv = get<AnyTypeVar>(ty))
apply(ty, *atv, seen, f);
else if (auto utv = get<UnionTypeVar>(ty))
{
if (apply(ty, *utv, seen, f))
{
for (TypeId optTy : utv->options)
visit(optTy, f, seen);
}
}
else if (auto itv = get<IntersectionTypeVar>(ty))
{
if (apply(ty, *itv, seen, f))
{
for (TypeId partTy : itv->parts)
visit(partTy, f, seen);
}
}
visit_detail::unsee(seen, ty);
}
template<typename F>
void visit(TypePackId tp, F& f, std::unordered_set<void*>& seen)
{
if (visit_detail::hasSeen(seen, tp))
{
f.cycle(tp);
return;
}
if (auto btv = get<BoundTypePack>(tp))
{
if (apply(tp, *btv, seen, f))
visit(btv->boundTo, f, seen);
}
else if (auto ftv = get<Unifiable::Free>(tp))
apply(tp, *ftv, seen, f);
else if (auto gtv = get<Unifiable::Generic>(tp))
apply(tp, *gtv, seen, f);
else if (auto etv = get<Unifiable::Error>(tp))
apply(tp, *etv, seen, f);
else if (auto pack = get<TypePack>(tp))
{
apply(tp, *pack, seen, f);
for (TypeId ty : pack->head)
visit(ty, f, seen);
if (pack->tail)
visit(*pack->tail, f, seen);
}
else if (auto pack = get<VariadicTypePack>(tp))
{
apply(tp, *pack, seen, f);
visit(pack->ty, f, seen);
}
visit_detail::unsee(seen, tp);
}
} // namespace visit_detail
template<typename TID, typename F>
void visitTypeVar(TID ty, F& f, std::unordered_set<void*>& seen)
{
visit_detail::visit(ty, f, seen);
}
template<typename TID, typename F>
void visitTypeVar(TID ty, F& f)
{
std::unordered_set<void*> seen;
visit_detail::visit(ty, f, seen);
}
} // namespace Luau

411
Analysis/src/AstQuery.cpp Normal file
View file

@ -0,0 +1,411 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/Module.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Luau/ToString.h"
#include "Luau/Common.h"
#include <algorithm>
namespace Luau
{
namespace
{
struct FindNode : public AstVisitor
{
const Position pos;
const Position documentEnd;
AstNode* best = nullptr;
explicit FindNode(Position pos, Position documentEnd)
: pos(pos)
, documentEnd(documentEnd)
{
}
bool visit(AstNode* node) override
{
if (node->location.contains(pos))
{
best = node;
return true;
}
// Edge case: If we ask for the node at the position that is the very end of the document
// return the innermost AST element that ends at that position.
if (node->location.end == documentEnd && pos >= documentEnd)
{
best = node;
return true;
}
return false;
}
bool visit(AstStatBlock* block) override
{
visit(static_cast<AstNode*>(block));
for (AstStat* stat : block->body)
{
if (stat->location.end < pos)
continue;
if (stat->location.begin > pos)
break;
stat->visit(this);
}
return false;
}
};
struct FindFullAncestry final : public AstVisitor
{
std::vector<AstNode*> nodes;
Position pos;
explicit FindFullAncestry(Position pos)
: pos(pos)
{
}
bool visit(AstNode* node)
{
if (node->location.contains(pos))
{
nodes.push_back(node);
return true;
}
return false;
}
};
} // namespace
std::vector<AstNode*> findAstAncestryOfPosition(const SourceModule& source, Position pos)
{
FindFullAncestry finder(pos);
source.root->visit(&finder);
return std::move(finder.nodes);
}
AstNode* findNodeAtPosition(const SourceModule& source, Position pos)
{
const Position end = source.root->location.end;
if (pos < source.root->location.begin)
return source.root;
if (pos > end)
pos = end;
FindNode findNode{pos, end};
findNode.visit(source.root);
return findNode.best;
}
AstExpr* findExprAtPosition(const SourceModule& source, Position pos)
{
AstNode* node = findNodeAtPosition(source, pos);
if (node)
return node->asExpr();
else
return nullptr;
}
ScopePtr findScopeAtPosition(const Module& module, Position pos)
{
LUAU_ASSERT(!module.scopes.empty());
Location scopeLocation = module.scopes.front().first;
ScopePtr scope = module.scopes.front().second;
for (const auto& s : module.scopes)
{
if (s.first.contains(pos))
{
if (!scope || scopeLocation.encloses(s.first))
{
scopeLocation = s.first;
scope = s.second;
}
}
}
return scope;
}
std::optional<TypeId> findTypeAtPosition(const Module& module, const SourceModule& sourceModule, Position pos)
{
if (auto expr = findExprAtPosition(sourceModule, pos))
{
if (auto it = module.astTypes.find(expr); it != module.astTypes.end())
return it->second;
}
return std::nullopt;
}
std::optional<TypeId> findExpectedTypeAtPosition(const Module& module, const SourceModule& sourceModule, Position pos)
{
if (auto expr = findExprAtPosition(sourceModule, pos))
{
if (auto it = module.astExpectedTypes.find(expr); it != module.astExpectedTypes.end())
return it->second;
}
return std::nullopt;
}
static std::optional<AstStatLocal*> findBindingLocalStatement(const SourceModule& source, const Binding& binding)
{
std::vector<AstNode*> nodes = findAstAncestryOfPosition(source, binding.location.begin);
auto iter = std::find_if(nodes.rbegin(), nodes.rend(), [](AstNode* node) {
return node->is<AstStatLocal>();
});
return iter != nodes.rend() ? std::make_optional((*iter)->as<AstStatLocal>()) : std::nullopt;
}
std::optional<Binding> findBindingAtPosition(const Module& module, const SourceModule& source, Position pos)
{
AstExpr* expr = findExprAtPosition(source, pos);
if (!expr)
return std::nullopt;
Symbol name;
if (auto g = expr->as<AstExprGlobal>())
name = g->name;
else if (auto l = expr->as<AstExprLocal>())
name = l->local;
else
return std::nullopt;
ScopePtr currentScope = findScopeAtPosition(module, pos);
LUAU_ASSERT(currentScope);
while (currentScope)
{
auto iter = currentScope->bindings.find(name);
if (iter != currentScope->bindings.end() && iter->second.location.begin <= pos)
{
/* Ignore this binding if we're inside its definition. e.g. local abc = abc -- Will take the definition of abc from outer scope */
std::optional<AstStatLocal*> bindingStatement = findBindingLocalStatement(source, iter->second);
if (!bindingStatement || !(*bindingStatement)->location.contains(pos))
return iter->second;
}
currentScope = currentScope->parent;
}
return std::nullopt;
}
namespace
{
struct FindExprOrLocal : public AstVisitor
{
const Position pos;
ExprOrLocal result;
explicit FindExprOrLocal(Position pos)
: pos(pos)
{
}
// We want to find the result with the smallest location range.
bool isCloserMatch(Location newLocation)
{
auto current = result.getLocation();
return newLocation.contains(pos) && (!current || current->encloses(newLocation));
}
bool visit(AstStatBlock* block) override
{
for (AstStat* stat : block->body)
{
if (stat->location.end <= pos)
continue;
if (stat->location.begin > pos)
break;
stat->visit(this);
}
return false;
}
bool visit(AstExpr* expr) override
{
if (isCloserMatch(expr->location))
{
result.setExpr(expr);
return true;
}
return false;
}
bool visitLocal(AstLocal* local)
{
if (isCloserMatch(local->location))
{
result.setLocal(local);
return true;
}
return false;
}
bool visit(AstStatLocalFunction* function) override
{
visitLocal(function->name);
return true;
}
bool visit(AstStatLocal* al) override
{
for (size_t i = 0; i < al->vars.size; ++i)
{
visitLocal(al->vars.data[i]);
}
return true;
}
virtual bool visit(AstExprFunction* fn) override
{
for (size_t i = 0; i < fn->args.size; ++i)
{
visitLocal(fn->args.data[i]);
}
return visit((class AstExpr*)fn);
}
virtual bool visit(AstStatFor* forStat) override
{
visitLocal(forStat->var);
return true;
}
virtual bool visit(AstStatForIn* forIn) override
{
for (AstLocal* var : forIn->vars)
{
visitLocal(var);
}
return true;
}
};
}; // namespace
ExprOrLocal findExprOrLocalAtPosition(const SourceModule& source, Position pos)
{
FindExprOrLocal findVisitor{pos};
findVisitor.visit(source.root);
return findVisitor.result;
}
std::optional<DocumentationSymbol> getDocumentationSymbolAtPosition(const SourceModule& source, const Module& module, Position position)
{
std::vector<AstNode*> ancestry = findAstAncestryOfPosition(source, position);
AstExpr* targetExpr = ancestry.size() >= 1 ? ancestry[ancestry.size() - 1]->asExpr() : nullptr;
AstExpr* parentExpr = ancestry.size() >= 2 ? ancestry[ancestry.size() - 2]->asExpr() : nullptr;
if (std::optional<Binding> binding = findBindingAtPosition(module, source, position))
{
if (binding->documentationSymbol)
{
// This might be an overloaded function binding.
if (get<IntersectionTypeVar>(follow(binding->typeId)))
{
TypeId matchingOverload = nullptr;
if (parentExpr && parentExpr->is<AstExprCall>())
{
if (auto it = module.astOverloadResolvedTypes.find(parentExpr); it != module.astOverloadResolvedTypes.end())
{
matchingOverload = it->second;
}
}
if (matchingOverload)
{
std::string overloadSymbol = *binding->documentationSymbol + "/overload/";
// Default toString options are fine for this purpose.
overloadSymbol += toString(matchingOverload);
return overloadSymbol;
}
}
}
return binding->documentationSymbol;
}
if (targetExpr)
{
if (AstExprIndexName* indexName = targetExpr->as<AstExprIndexName>())
{
if (auto it = module.astTypes.find(indexName->expr); it != module.astTypes.end())
{
TypeId parentTy = follow(it->second);
if (const TableTypeVar* ttv = get<TableTypeVar>(parentTy))
{
if (auto propIt = ttv->props.find(indexName->index.value); propIt != ttv->props.end())
{
return propIt->second.documentationSymbol;
}
}
else if (const ClassTypeVar* ctv = get<ClassTypeVar>(parentTy))
{
if (auto propIt = ctv->props.find(indexName->index.value); propIt != ctv->props.end())
{
return propIt->second.documentationSymbol;
}
}
}
}
else if (AstExprFunction* fn = targetExpr->as<AstExprFunction>())
{
// Handle event connection-like structures where we have
// something:Connect(function(a, b, c) end)
// In this case, we want to ascribe a documentation symbol to 'a'
// based on the documentation symbol of Connect.
if (parentExpr && parentExpr->is<AstExprCall>())
{
AstExprCall* call = parentExpr->as<AstExprCall>();
if (std::optional<DocumentationSymbol> parentSymbol = getDocumentationSymbolAtPosition(source, module, call->func->location.begin))
{
for (size_t i = 0; i < call->args.size; ++i)
{
AstExpr* callArg = call->args.data[i];
if (callArg == targetExpr)
{
std::string fnSymbol = *parentSymbol + "/param/" + std::to_string(i);
for (size_t j = 0; j < fn->args.size; ++j)
{
AstLocal* fnArg = fn->args.data[j];
if (fnArg->location.contains(position))
{
return fnSymbol + "/param/" + std::to_string(j);
}
}
}
}
}
}
}
}
if (std::optional<TypeId> ty = findTypeAtPosition(module, source, position))
{
if ((*ty)->documentationSymbol)
{
return (*ty)->documentationSymbol;
}
}
return std::nullopt;
}
} // namespace Luau

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,805 @@
// 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/Frontend.h"
#include "Luau/Symbol.h"
#include "Luau/Common.h"
#include "Luau/ToString.h"
#include <algorithm>
LUAU_FASTFLAG(LuauParseGenericFunctions)
LUAU_FASTFLAG(LuauGenericFunctions)
LUAU_FASTFLAG(LuauRankNTypes)
LUAU_FASTFLAG(LuauStringMetatable)
/** FIXME: Many of these type definitions are not quite completely accurate.
*
* Some of them require richer generics than we have. For instance, we do not yet have a way to talk
* about a function that takes any number of values, but where each value must have some specific type.
*/
namespace Luau
{
static std::optional<ExprResult<TypePackId>> magicFunctionSelect(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult);
static std::optional<ExprResult<TypePackId>> magicFunctionSetMetaTable(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult);
static std::optional<ExprResult<TypePackId>> magicFunctionAssert(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult);
static std::optional<ExprResult<TypePackId>> magicFunctionPack(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult);
static std::optional<ExprResult<TypePackId>> magicFunctionRequire(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult);
TypeId makeUnion(TypeArena& arena, std::vector<TypeId>&& types)
{
return arena.addType(UnionTypeVar{std::move(types)});
}
TypeId makeIntersection(TypeArena& arena, std::vector<TypeId>&& types)
{
return arena.addType(IntersectionTypeVar{std::move(types)});
}
TypeId makeOption(TypeChecker& typeChecker, TypeArena& arena, TypeId t)
{
return makeUnion(arena, {typeChecker.nilType, t});
}
TypeId makeFunction(
TypeArena& arena, std::optional<TypeId> selfType, std::initializer_list<TypeId> paramTypes, std::initializer_list<TypeId> retTypes)
{
return makeFunction(arena, selfType, {}, {}, paramTypes, {}, retTypes);
}
TypeId makeFunction(TypeArena& arena, std::optional<TypeId> selfType, std::initializer_list<TypeId> generics,
std::initializer_list<TypePackId> genericPacks, std::initializer_list<TypeId> paramTypes, std::initializer_list<TypeId> retTypes)
{
return makeFunction(arena, selfType, generics, genericPacks, paramTypes, {}, retTypes);
}
TypeId makeFunction(TypeArena& arena, std::optional<TypeId> selfType, std::initializer_list<TypeId> paramTypes,
std::initializer_list<std::string> paramNames, std::initializer_list<TypeId> retTypes)
{
return makeFunction(arena, selfType, {}, {}, paramTypes, paramNames, retTypes);
}
TypeId makeFunction(TypeArena& arena, std::optional<TypeId> selfType, std::initializer_list<TypeId> generics,
std::initializer_list<TypePackId> genericPacks, std::initializer_list<TypeId> paramTypes, std::initializer_list<std::string> paramNames,
std::initializer_list<TypeId> retTypes)
{
std::vector<TypeId> params;
if (selfType)
params.push_back(*selfType);
for (auto&& p : paramTypes)
params.push_back(p);
TypePackId paramPack = arena.addTypePack(std::move(params));
TypePackId retPack = arena.addTypePack(std::vector<TypeId>(retTypes));
FunctionTypeVar ftv{generics, genericPacks, paramPack, retPack, {}, selfType.has_value()};
if (selfType)
ftv.argNames.push_back(Luau::FunctionArgument{"self", {}});
if (paramNames.size() != 0)
{
for (auto&& p : paramNames)
ftv.argNames.push_back(Luau::FunctionArgument{std::move(p), {}});
}
else if (selfType)
{
// If argument names were not provided, but we have already added a name for 'self' argument, we have to fill remaining slots as well
for (size_t i = 0; i < paramTypes.size(); i++)
ftv.argNames.push_back(std::nullopt);
}
return arena.addType(std::move(ftv));
}
void attachMagicFunction(TypeId ty, MagicFunction fn)
{
if (auto ftv = getMutable<FunctionTypeVar>(ty))
ftv->magicFunction = fn;
else
LUAU_ASSERT(!"Got a non functional type");
}
void attachFunctionTag(TypeId ty, std::string tag)
{
if (auto ftv = getMutable<FunctionTypeVar>(ty))
{
ftv->tags.emplace_back(std::move(tag));
}
else
{
LUAU_ASSERT(!"Got a non functional type");
}
}
Property makeProperty(TypeId ty, std::optional<std::string> documentationSymbol)
{
return {
/* type */ ty,
/* deprecated */ false,
/* deprecatedSuggestion */ {},
/* location */ std::nullopt,
/* tags */ {},
documentationSymbol,
};
}
void addGlobalBinding(TypeChecker& typeChecker, const std::string& name, TypeId ty, const std::string& packageName)
{
addGlobalBinding(typeChecker, typeChecker.globalScope, name, ty, packageName);
}
void addGlobalBinding(TypeChecker& typeChecker, const std::string& name, Binding binding)
{
addGlobalBinding(typeChecker, typeChecker.globalScope, name, binding);
}
void addGlobalBinding(TypeChecker& typeChecker, const ScopePtr& scope, const std::string& name, TypeId ty, const std::string& packageName)
{
std::string documentationSymbol = packageName + "/global/" + name;
addGlobalBinding(typeChecker, scope, name, Binding{ty, Location{}, {}, {}, documentationSymbol});
}
void addGlobalBinding(TypeChecker& typeChecker, const ScopePtr& scope, const std::string& name, Binding binding)
{
scope->bindings[typeChecker.globalNames.names->getOrAdd(name.c_str())] = binding;
}
TypeId getGlobalBinding(TypeChecker& typeChecker, const std::string& name)
{
auto t = tryGetGlobalBinding(typeChecker, name);
LUAU_ASSERT(t.has_value());
return t->typeId;
}
std::optional<Binding> tryGetGlobalBinding(TypeChecker& typeChecker, const std::string& name)
{
AstName astName = typeChecker.globalNames.names->getOrAdd(name.c_str());
auto it = typeChecker.globalScope->bindings.find(astName);
if (it != typeChecker.globalScope->bindings.end())
return it->second;
return std::nullopt;
}
Binding* tryGetGlobalBindingRef(TypeChecker& typeChecker, const std::string& name)
{
AstName astName = typeChecker.globalNames.names->get(name.c_str());
if (astName == AstName())
return nullptr;
auto it = typeChecker.globalScope->bindings.find(astName);
if (it != typeChecker.globalScope->bindings.end())
return &it->second;
return nullptr;
}
void assignPropDocumentationSymbols(TableTypeVar::Props& props, const std::string& baseName)
{
for (auto& [name, prop] : props)
{
prop.documentationSymbol = baseName + "." + name;
}
}
void registerBuiltinTypes(TypeChecker& typeChecker)
{
LUAU_ASSERT(!typeChecker.globalTypes.typeVars.isFrozen());
LUAU_ASSERT(!typeChecker.globalTypes.typePacks.isFrozen());
TypeId numberType = typeChecker.numberType;
TypeId booleanType = typeChecker.booleanType;
TypeId nilType = typeChecker.nilType;
TypeId stringType = typeChecker.stringType;
TypeId threadType = typeChecker.threadType;
TypeId anyType = typeChecker.anyType;
TypeArena& arena = typeChecker.globalTypes;
TypeId optionalNumber = makeOption(typeChecker, arena, numberType);
TypeId optionalString = makeOption(typeChecker, arena, stringType);
TypeId optionalBoolean = makeOption(typeChecker, arena, booleanType);
TypeId stringOrNumber = makeUnion(arena, {stringType, numberType});
TypePackId emptyPack = arena.addTypePack({});
TypePackId oneNumberPack = arena.addTypePack({numberType});
TypePackId oneStringPack = arena.addTypePack({stringType});
TypePackId oneBooleanPack = arena.addTypePack({booleanType});
TypePackId oneAnyPack = arena.addTypePack({anyType});
TypePackId anyTypePack = typeChecker.anyTypePack;
TypePackId numberVariadicList = arena.addTypePack(TypePackVar{VariadicTypePack{numberType}});
TypePackId stringVariadicList = arena.addTypePack(TypePackVar{VariadicTypePack{stringType}});
TypePackId listOfAtLeastOneNumber = arena.addTypePack(TypePack{{numberType}, numberVariadicList});
TypeId listOfAtLeastOneNumberToNumberType = arena.addType(FunctionTypeVar{
listOfAtLeastOneNumber,
oneNumberPack,
});
TypeId listOfAtLeastZeroNumbersToNumberType = arena.addType(FunctionTypeVar{numberVariadicList, oneNumberPack});
TypeId stringToAnyMap = arena.addType(TableTypeVar{{}, TableIndexer(stringType, anyType), typeChecker.globalScope->level});
LoadDefinitionFileResult loadResult = Luau::loadDefinitionFile(typeChecker, typeChecker.globalScope, getBuiltinDefinitionSource(), "@luau");
LUAU_ASSERT(loadResult.success);
TypeId mathLibType = getGlobalBinding(typeChecker, "math");
if (TableTypeVar* ttv = getMutable<TableTypeVar>(mathLibType))
{
ttv->props["min"] = makeProperty(listOfAtLeastOneNumberToNumberType, "@luau/global/math.min");
ttv->props["max"] = makeProperty(listOfAtLeastOneNumberToNumberType, "@luau/global/math.max");
}
TypeId bit32LibType = getGlobalBinding(typeChecker, "bit32");
if (TableTypeVar* ttv = getMutable<TableTypeVar>(bit32LibType))
{
ttv->props["band"] = makeProperty(listOfAtLeastZeroNumbersToNumberType, "@luau/global/bit32.band");
ttv->props["bor"] = makeProperty(listOfAtLeastZeroNumbersToNumberType, "@luau/global/bit32.bor");
ttv->props["bxor"] = makeProperty(listOfAtLeastZeroNumbersToNumberType, "@luau/global/bit32.bxor");
ttv->props["btest"] = makeProperty(arena.addType(FunctionTypeVar{listOfAtLeastOneNumber, oneBooleanPack}), "@luau/global/bit32.btest");
}
TypeId anyFunction = arena.addType(FunctionTypeVar{anyTypePack, anyTypePack});
TypeId genericK = arena.addType(GenericTypeVar{"K"});
TypeId genericV = arena.addType(GenericTypeVar{"V"});
TypeId mapOfKtoV = arena.addType(TableTypeVar{{}, TableIndexer(genericK, genericV), typeChecker.globalScope->level});
if (FFlag::LuauStringMetatable)
{
std::optional<TypeId> stringMetatableTy = getMetatable(singletonTypes.stringType);
LUAU_ASSERT(stringMetatableTy);
const TableTypeVar* stringMetatableTable = get<TableTypeVar>(follow(*stringMetatableTy));
LUAU_ASSERT(stringMetatableTable);
auto it = stringMetatableTable->props.find("__index");
LUAU_ASSERT(it != stringMetatableTable->props.end());
TypeId stringLib = it->second.type;
addGlobalBinding(typeChecker, "string", stringLib, "@luau");
}
if (FFlag::LuauParseGenericFunctions && FFlag::LuauGenericFunctions)
{
if (!FFlag::LuauStringMetatable)
{
TypeId stringLibTy = getGlobalBinding(typeChecker, "string");
TableTypeVar* stringLib = getMutable<TableTypeVar>(stringLibTy);
TypeId replArgType = makeUnion(
arena, {stringType,
arena.addType(TableTypeVar({}, TableIndexer(stringType, stringType), typeChecker.globalScope->level, TableState::Generic)),
makeFunction(arena, std::nullopt, {stringType}, {stringType})});
TypeId gsubFunc = makeFunction(arena, stringType, {stringType, replArgType, optionalNumber}, {stringType, numberType});
stringLib->props["gsub"] = makeProperty(gsubFunc, "@luau/global/string.gsub");
}
}
else
{
if (!FFlag::LuauStringMetatable)
{
TypeId stringToStringType = makeFunction(arena, std::nullopt, {stringType}, {stringType});
TypeId gmatchFunc = makeFunction(arena, stringType, {stringType}, {arena.addType(FunctionTypeVar{emptyPack, stringVariadicList})});
TypeId replArgType = makeUnion(
arena, {stringType,
arena.addType(TableTypeVar({}, TableIndexer(stringType, stringType), typeChecker.globalScope->level, TableState::Generic)),
makeFunction(arena, std::nullopt, {stringType}, {stringType})});
TypeId gsubFunc = makeFunction(arena, stringType, {stringType, replArgType, optionalNumber}, {stringType, numberType});
TypeId formatFn = arena.addType(FunctionTypeVar{arena.addTypePack(TypePack{{stringType}, anyTypePack}), oneStringPack});
TableTypeVar::Props stringLib = {
// FIXME string.byte "can" return a pack of numbers, but only if 2nd or 3rd arguments were supplied
{"byte", {makeFunction(arena, stringType, {optionalNumber, optionalNumber}, {optionalNumber})}},
// FIXME char takes a variadic pack of numbers
{"char", {makeFunction(arena, std::nullopt, {numberType, optionalNumber, optionalNumber, optionalNumber}, {stringType})}},
{"find", {makeFunction(arena, stringType, {stringType, optionalNumber, optionalBoolean}, {optionalNumber, optionalNumber})}},
{"format", {formatFn}}, // FIXME
{"gmatch", {gmatchFunc}},
{"gsub", {gsubFunc}},
{"len", {makeFunction(arena, stringType, {}, {numberType})}},
{"lower", {stringToStringType}},
{"match", {makeFunction(arena, stringType, {stringType, optionalNumber}, {optionalString})}},
{"rep", {makeFunction(arena, stringType, {numberType}, {stringType})}},
{"reverse", {stringToStringType}},
{"sub", {makeFunction(arena, stringType, {numberType, optionalNumber}, {stringType})}},
{"upper", {stringToStringType}},
{"split", {makeFunction(arena, stringType, {stringType, optionalString},
{arena.addType(TableTypeVar{{}, TableIndexer{numberType, stringType}, typeChecker.globalScope->level})})}},
{"pack", {arena.addType(FunctionTypeVar{
arena.addTypePack(TypePack{{stringType}, anyTypePack}),
oneStringPack,
})}},
{"packsize", {makeFunction(arena, stringType, {}, {numberType})}},
{"unpack", {arena.addType(FunctionTypeVar{
arena.addTypePack(TypePack{{stringType, stringType, optionalNumber}}),
anyTypePack,
})}},
};
assignPropDocumentationSymbols(stringLib, "@luau/global/string");
addGlobalBinding(typeChecker, "string",
arena.addType(TableTypeVar{stringLib, std::nullopt, typeChecker.globalScope->level, TableState::Sealed}), "@luau");
}
TableTypeVar::Props debugLib{
{"info", {makeIntersection(arena,
{
arena.addType(FunctionTypeVar{arena.addTypePack({typeChecker.threadType, numberType, stringType}), anyTypePack}),
arena.addType(FunctionTypeVar{arena.addTypePack({numberType, stringType}), anyTypePack}),
arena.addType(FunctionTypeVar{arena.addTypePack({anyFunction, stringType}), anyTypePack}),
})}},
{"traceback", {makeIntersection(arena,
{
makeFunction(arena, std::nullopt, {optionalString, optionalNumber}, {stringType}),
makeFunction(arena, std::nullopt, {typeChecker.threadType, optionalString, optionalNumber}, {stringType}),
})}},
};
assignPropDocumentationSymbols(debugLib, "@luau/global/debug");
addGlobalBinding(typeChecker, "debug",
arena.addType(TableTypeVar{debugLib, std::nullopt, typeChecker.globalScope->level, Luau::TableState::Sealed}), "@luau");
TableTypeVar::Props utf8Lib = {
{"char", {arena.addType(FunctionTypeVar{listOfAtLeastOneNumber, oneStringPack})}}, // FIXME
{"charpattern", {stringType}},
{"codes", {makeFunction(arena, std::nullopt, {stringType},
{makeFunction(arena, std::nullopt, {stringType, numberType}, {numberType, numberType}), stringType, numberType})}},
{"codepoint",
{arena.addType(FunctionTypeVar{arena.addTypePack({stringType, optionalNumber, optionalNumber}), listOfAtLeastOneNumber})}}, // FIXME
{"len", {makeFunction(arena, std::nullopt, {stringType, optionalNumber, optionalNumber}, {optionalNumber, numberType})}},
{"offset", {makeFunction(arena, std::nullopt, {stringType, optionalNumber, optionalNumber}, {numberType})}},
{"nfdnormalize", {makeFunction(arena, std::nullopt, {stringType}, {stringType})}},
{"graphemes", {makeFunction(arena, std::nullopt, {stringType, optionalNumber, optionalNumber},
{makeFunction(arena, std::nullopt, {}, {numberType, numberType})})}},
{"nfcnormalize", {makeFunction(arena, std::nullopt, {stringType}, {stringType})}},
};
assignPropDocumentationSymbols(utf8Lib, "@luau/global/utf8");
addGlobalBinding(
typeChecker, "utf8", arena.addType(TableTypeVar{utf8Lib, std::nullopt, typeChecker.globalScope->level, TableState::Sealed}), "@luau");
TypeId optionalV = makeOption(typeChecker, arena, genericV);
TypeId arrayOfV = arena.addType(TableTypeVar{{}, TableIndexer(numberType, genericV), typeChecker.globalScope->level});
TypePackId unpackArgsPack = arena.addTypePack(TypePack{{arrayOfV, optionalNumber, optionalNumber}});
TypePackId unpackReturnPack = arena.addTypePack(TypePack{{}, anyTypePack});
TypeId unpackFunc = arena.addType(FunctionTypeVar{{genericV}, {}, unpackArgsPack, unpackReturnPack});
TypeId packResult = arena.addType(TableTypeVar{
TableTypeVar::Props{{"n", {numberType}}}, TableIndexer{numberType, numberType}, typeChecker.globalScope->level, TableState::Sealed});
TypePackId packArgsPack = arena.addTypePack(TypePack{{}, anyTypePack});
TypePackId packReturnPack = arena.addTypePack(TypePack{{packResult}});
TypeId comparator = makeFunction(arena, std::nullopt, {genericV, genericV}, {booleanType});
TypeId optionalComparator = makeOption(typeChecker, arena, comparator);
TypeId packFn = arena.addType(FunctionTypeVar(packArgsPack, packReturnPack));
TableTypeVar::Props tableLib = {
{"concat", {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV, optionalString, optionalNumber, optionalNumber}, {stringType})}},
{"insert", {makeIntersection(arena, {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV, genericV}, {}),
makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV, numberType, genericV}, {})})}},
{"maxn", {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV}, {numberType})}},
{"remove", {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV, optionalNumber}, {optionalV})}},
{"sort", {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV, optionalComparator}, {})}},
{"create", {makeFunction(arena, std::nullopt, {genericV}, {}, {numberType, optionalV}, {arrayOfV})}},
{"find", {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV, genericV, optionalNumber}, {optionalNumber})}},
{"unpack", {unpackFunc}}, // FIXME
{"pack", {packFn}},
// Lua 5.0 compat
{"getn", {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV}, {numberType})}},
{"foreach", {makeFunction(arena, std::nullopt, {genericK, genericV}, {},
{mapOfKtoV, makeFunction(arena, std::nullopt, {genericK, genericV}, {})}, {})}},
{"foreachi", {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV, makeFunction(arena, std::nullopt, {genericV}, {})}, {})}},
// backported from Lua 5.3
{"move", {makeFunction(arena, std::nullopt, {genericV}, {}, {arrayOfV, numberType, numberType, numberType, arrayOfV}, {})}},
// added in Luau (borrowed from LuaJIT)
{"clear", {makeFunction(arena, std::nullopt, {genericK, genericV}, {}, {mapOfKtoV}, {})}},
{"freeze", {makeFunction(arena, std::nullopt, {genericK, genericV}, {}, {mapOfKtoV}, {mapOfKtoV})}},
{"isfrozen", {makeFunction(arena, std::nullopt, {genericK, genericV}, {}, {mapOfKtoV}, {booleanType})}},
};
assignPropDocumentationSymbols(tableLib, "@luau/global/table");
addGlobalBinding(
typeChecker, "table", arena.addType(TableTypeVar{tableLib, std::nullopt, typeChecker.globalScope->level, TableState::Sealed}), "@luau");
TableTypeVar::Props coroutineLib = {
{"create", {makeFunction(arena, std::nullopt, {anyFunction}, {threadType})}},
{"resume", {arena.addType(FunctionTypeVar{arena.addTypePack(TypePack{{threadType}, anyTypePack}), anyTypePack})}},
{"running", {makeFunction(arena, std::nullopt, {}, {threadType})}},
{"status", {makeFunction(arena, std::nullopt, {threadType}, {stringType})}},
{"wrap", {makeFunction(
arena, std::nullopt, {anyFunction}, {anyType})}}, // FIXME this technically returns a function, but we can't represent this
// atm since it can be called with different arg types at different times
{"yield", {arena.addType(FunctionTypeVar{anyTypePack, anyTypePack})}},
{"isyieldable", {makeFunction(arena, std::nullopt, {}, {booleanType})}},
};
assignPropDocumentationSymbols(coroutineLib, "@luau/global/coroutine");
addGlobalBinding(typeChecker, "coroutine",
arena.addType(TableTypeVar{coroutineLib, std::nullopt, typeChecker.globalScope->level, TableState::Sealed}), "@luau");
TypeId genericT = arena.addType(GenericTypeVar{"T"});
TypeId genericR = arena.addType(GenericTypeVar{"R"});
// assert returns all arguments
TypePackId assertArgs = arena.addTypePack({genericT, optionalString});
TypePackId assertRets = arena.addTypePack({genericT});
addGlobalBinding(typeChecker, "assert", arena.addType(FunctionTypeVar{assertArgs, assertRets}), "@luau");
addGlobalBinding(typeChecker, "print", arena.addType(FunctionTypeVar{anyTypePack, emptyPack}), "@luau");
addGlobalBinding(typeChecker, "type", makeFunction(arena, std::nullopt, {genericT}, {}, {genericT}, {stringType}), "@luau");
addGlobalBinding(typeChecker, "typeof", makeFunction(arena, std::nullopt, {genericT}, {}, {genericT}, {stringType}), "@luau");
addGlobalBinding(typeChecker, "error", makeFunction(arena, std::nullopt, {genericT}, {}, {genericT, optionalNumber}, {}), "@luau");
addGlobalBinding(typeChecker, "tostring", makeFunction(arena, std::nullopt, {genericT}, {}, {genericT}, {stringType}), "@luau");
addGlobalBinding(
typeChecker, "tonumber", makeFunction(arena, std::nullopt, {genericT}, {}, {genericT, optionalNumber}, {numberType}), "@luau");
addGlobalBinding(
typeChecker, "rawequal", makeFunction(arena, std::nullopt, {genericT, genericR}, {}, {genericT, genericR}, {booleanType}), "@luau");
addGlobalBinding(
typeChecker, "rawget", makeFunction(arena, std::nullopt, {genericK, genericV}, {}, {mapOfKtoV, genericK}, {genericV}), "@luau");
addGlobalBinding(typeChecker, "rawset",
makeFunction(arena, std::nullopt, {genericK, genericV}, {}, {mapOfKtoV, genericK, genericV}, {mapOfKtoV}), "@luau");
TypePackId genericTPack = arena.addTypePack({genericT});
TypePackId genericRPack = arena.addTypePack({genericR});
TypeId genericArgsToReturnFunction = arena.addType(
FunctionTypeVar{{genericT, genericR}, {}, arena.addTypePack(TypePack{{}, genericTPack}), arena.addTypePack(TypePack{{}, genericRPack})});
TypeId setfenvArgType = makeUnion(arena, {numberType, genericArgsToReturnFunction});
TypeId setfenvReturnType = makeOption(typeChecker, arena, genericArgsToReturnFunction);
addGlobalBinding(typeChecker, "setfenv", makeFunction(arena, std::nullopt, {setfenvArgType, stringToAnyMap}, {setfenvReturnType}), "@luau");
TypePackId ipairsArgsTypePack = arena.addTypePack({arrayOfV});
TypeId ipairsNextFunctionType = arena.addType(
FunctionTypeVar{{genericK, genericV}, {}, arena.addTypePack({arrayOfV, numberType}), arena.addTypePack({numberType, genericV})});
// ipairs returns 'next, Array<V>, 0' so we would need type-level primitives and change to
// again, we have a direct reference to 'next' because ipairs returns it
// ipairs<V>(t: Array<V>) -> ((Array<V>) -> (number, V), Array<V>, 0)
TypePackId ipairsReturnTypePack = arena.addTypePack(TypePack{{ipairsNextFunctionType, arrayOfV, numberType}});
// ipairs<V>(t: Array<V>) -> ((Array<V>) -> (number, V), Array<V>, number)
addGlobalBinding(typeChecker, "ipairs", arena.addType(FunctionTypeVar{{genericV}, {}, ipairsArgsTypePack, ipairsReturnTypePack}), "@luau");
TypePackId pcallArg0FnArgs = arena.addTypePack(TypePackVar{GenericTypeVar{"A"}});
TypePackId pcallArg0FnRet = arena.addTypePack(TypePackVar{GenericTypeVar{"R"}});
TypeId pcallArg0 = arena.addType(FunctionTypeVar{pcallArg0FnArgs, pcallArg0FnRet});
TypePackId pcallArgsTypePack = arena.addTypePack(TypePack{{pcallArg0}, pcallArg0FnArgs});
TypePackId pcallReturnTypePack = arena.addTypePack(TypePack{{booleanType}, pcallArg0FnRet});
// pcall<A..., R...>(f: (A...) -> R..., args: A...) -> boolean, R...
addGlobalBinding(typeChecker, "pcall",
arena.addType(FunctionTypeVar{{}, {pcallArg0FnArgs, pcallArg0FnRet}, pcallArgsTypePack, pcallReturnTypePack}), "@luau");
// errors thrown by the function 'f' are propagated onto the function 'err' that accepts it.
// and either 'f' or 'err' are valid results of this xpcall
// if 'err' did throw an error, then it returns: false, "error in error handling"
// TODO: the above is not represented (nor representable) in the type annotation below.
//
// The real type of xpcall is as such: <E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., A...) -> (true, R1...) | (false,
// R2...)
TypePackId genericAPack = arena.addTypePack(TypePackVar{GenericTypeVar{"A"}});
TypePackId genericR1Pack = arena.addTypePack(TypePackVar{GenericTypeVar{"R1"}});
TypePackId genericR2Pack = arena.addTypePack(TypePackVar{GenericTypeVar{"R2"}});
TypeId genericE = arena.addType(GenericTypeVar{"E"});
TypeId xpcallFArg = arena.addType(FunctionTypeVar{genericAPack, genericR1Pack});
TypeId xpcallErrArg = arena.addType(FunctionTypeVar{arena.addTypePack({genericE}), genericR2Pack});
TypePackId xpcallArgsPack = arena.addTypePack({{xpcallFArg, xpcallErrArg}, genericAPack});
TypePackId xpcallRetPack = arena.addTypePack({{booleanType}, genericR1Pack}); // FIXME
addGlobalBinding(typeChecker, "xpcall",
arena.addType(FunctionTypeVar{{genericE}, {genericAPack, genericR1Pack, genericR2Pack}, xpcallArgsPack, xpcallRetPack}), "@luau");
addGlobalBinding(typeChecker, "unpack", unpackFunc, "@luau");
TypePackId selectArgsTypePack = arena.addTypePack(TypePack{
{stringOrNumber},
anyTypePack // FIXME? select() is tricky.
});
addGlobalBinding(typeChecker, "select", arena.addType(FunctionTypeVar{selectArgsTypePack, anyTypePack}), "@luau");
// TODO: not completely correct. loadstring's return type should be a function or (nil, string)
TypeId loadstringFunc = arena.addType(FunctionTypeVar{anyTypePack, oneAnyPack});
addGlobalBinding(typeChecker, "loadstring",
makeFunction(arena, std::nullopt, {stringType, optionalString},
{
makeOption(typeChecker, arena, loadstringFunc),
makeOption(typeChecker, arena, stringType),
}),
"@luau");
// a userdata object is "roughly" the same as a sealed empty table
// except `type(newproxy(false))` evaluates to "userdata" so we may need another special type here too.
// another important thing to note: the value passed in conditionally creates an empty metatable, and you have to use getmetatable, NOT
// setmetatable.
// TODO: change this to something Luau can understand how to reject `setmetatable(newproxy(false or true), {})`.
TypeId sealedTable = arena.addType(TableTypeVar(TableState::Sealed, typeChecker.globalScope->level));
addGlobalBinding(typeChecker, "newproxy", makeFunction(arena, std::nullopt, {optionalBoolean}, {sealedTable}), "@luau");
}
// next<K, V>(t: Table<K, V>, i: K | nil) -> (K, V)
TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(typeChecker, arena, genericK)}});
addGlobalBinding(typeChecker, "next",
arena.addType(FunctionTypeVar{{genericK, genericV}, {}, nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})}), "@luau");
TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV});
TypeId pairsNext = (FFlag::LuauRankNTypes ? arena.addType(FunctionTypeVar{nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})})
: getGlobalBinding(typeChecker, "next"));
TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, nilType}});
// NOTE we are missing 'i: K | nil' argument in the first return types' argument.
// pairs<K, V>(t: Table<K, V>) -> ((Table<K, V>) -> (K, V), Table<K, V>, nil)
addGlobalBinding(typeChecker, "pairs", arena.addType(FunctionTypeVar{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau");
TypeId genericMT = arena.addType(GenericTypeVar{"MT"});
TableTypeVar tab{TableState::Generic, typeChecker.globalScope->level};
TypeId tabTy = arena.addType(tab);
TypeId tableMetaMT = arena.addType(MetatableTypeVar{tabTy, genericMT});
addGlobalBinding(typeChecker, "getmetatable", makeFunction(arena, std::nullopt, {genericMT}, {}, {tableMetaMT}, {genericMT}), "@luau");
// setmetatable<MT>({ @metatable MT }, MT) -> { @metatable MT }
// clang-format off
addGlobalBinding(typeChecker, "setmetatable",
arena.addType(
FunctionTypeVar{
{genericMT},
{},
arena.addTypePack(TypePack{{tableMetaMT, genericMT}}),
arena.addTypePack(TypePack{{tableMetaMT}})
}
), "@luau"
);
// clang-format on
for (const auto& pair : typeChecker.globalScope->bindings)
{
persist(pair.second.typeId);
if (TableTypeVar* ttv = getMutable<TableTypeVar>(pair.second.typeId))
ttv->name = toString(pair.first);
}
attachMagicFunction(getGlobalBinding(typeChecker, "assert"), magicFunctionAssert);
attachMagicFunction(getGlobalBinding(typeChecker, "setmetatable"), magicFunctionSetMetaTable);
attachMagicFunction(getGlobalBinding(typeChecker, "select"), magicFunctionSelect);
auto tableLib = getMutable<TableTypeVar>(getGlobalBinding(typeChecker, "table"));
attachMagicFunction(tableLib->props["pack"].type, magicFunctionPack);
auto stringLib = getMutable<TableTypeVar>(getGlobalBinding(typeChecker, "string"));
attachMagicFunction(stringLib->props["format"].type, magicFunctionFormat);
attachMagicFunction(getGlobalBinding(typeChecker, "require"), magicFunctionRequire);
}
static std::optional<ExprResult<TypePackId>> magicFunctionSelect(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult)
{
auto [paramPack, _predicates] = exprResult;
(void)scope;
if (expr.args.size <= 0)
{
typechecker.reportError(TypeError{expr.location, GenericError{"select should take 1 or more arguments"}});
return std::nullopt;
}
AstExpr* arg1 = expr.args.data[0];
if (AstExprConstantNumber* num = arg1->as<AstExprConstantNumber>())
{
const auto& [v, tail] = flatten(paramPack);
int offset = int(num->value);
if (offset > 0)
{
if (size_t(offset) < v.size())
{
std::vector<TypeId> result(v.begin() + offset, v.end());
return ExprResult<TypePackId>{typechecker.currentModule->internalTypes.addTypePack(TypePack{std::move(result), tail})};
}
else if (tail)
return ExprResult<TypePackId>{*tail};
}
typechecker.reportError(TypeError{arg1->location, GenericError{"bad argument #1 to select (index out of range)"}});
}
else if (AstExprConstantString* str = arg1->as<AstExprConstantString>())
{
if (str->value.size == 1 && str->value.data[0] == '#')
return ExprResult<TypePackId>{typechecker.currentModule->internalTypes.addTypePack({typechecker.numberType})};
}
return std::nullopt;
}
static std::optional<ExprResult<TypePackId>> magicFunctionSetMetaTable(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult)
{
auto [paramPack, _predicates] = exprResult;
TypeArena& arena = typechecker.currentModule->internalTypes;
std::vector<TypeId> expectedArgs = typechecker.unTypePack(scope, paramPack, 2, expr.location);
TypeId target = follow(expectedArgs[0]);
TypeId mt = follow(expectedArgs[1]);
if (const auto& tab = get<TableTypeVar>(target))
{
if (target->persistent)
{
typechecker.reportError(TypeError{expr.location, CannotExtendTable{target, CannotExtendTable::Metatable}});
}
else
{
typechecker.tablify(mt);
const TableTypeVar* mtTtv = get<TableTypeVar>(mt);
MetatableTypeVar mtv{target, mt};
if ((tab->name || tab->syntheticName) && (mtTtv && (mtTtv->name || mtTtv->syntheticName)))
{
std::string tableName = tab->name ? *tab->name : *tab->syntheticName;
std::string metatableName = mtTtv->name ? *mtTtv->name : *mtTtv->syntheticName;
if (tableName == metatableName)
mtv.syntheticName = tableName;
else
mtv.syntheticName = "{ @metatable: " + metatableName + ", " + tableName + " }";
}
TypeId mtTy = arena.addType(mtv);
AstExpr* targetExpr = expr.args.data[0];
if (AstExprLocal* targetLocal = targetExpr->as<AstExprLocal>())
{
const Name targetName(targetLocal->local->name.value);
scope->bindings[targetLocal->local] = Binding{mtTy, expr.location};
}
return ExprResult<TypePackId>{arena.addTypePack({mtTy})};
}
}
else if (get<AnyTypeVar>(target) || get<ErrorTypeVar>(target) || isTableIntersection(target))
{
}
else
{
typechecker.reportError(TypeError{expr.location, GenericError{"setmetatable should take a table"}});
}
return ExprResult<TypePackId>{arena.addTypePack({target})};
}
static std::optional<ExprResult<TypePackId>> magicFunctionAssert(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult)
{
auto [paramPack, predicates] = exprResult;
if (expr.args.size < 1)
return ExprResult<TypePackId>{paramPack};
typechecker.reportErrors(typechecker.resolve(predicates, scope, true));
return ExprResult<TypePackId>{paramPack};
}
static std::optional<ExprResult<TypePackId>> magicFunctionPack(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult)
{
auto [paramPack, _predicates] = exprResult;
TypeArena& arena = typechecker.currentModule->internalTypes;
const auto& [paramTypes, paramTail] = flatten(paramPack);
std::vector<TypeId> options;
options.reserve(paramTypes.size());
for (auto type : paramTypes)
options.push_back(type);
if (paramTail)
{
if (const VariadicTypePack* vtp = get<VariadicTypePack>(*paramTail))
options.push_back(vtp->ty);
}
options = typechecker.reduceUnion(options);
// table.pack() -> {| n: number, [number]: nil |}
// table.pack(1) -> {| n: number, [number]: number |}
// table.pack(1, "foo") -> {| n: number, [number]: number | string |}
TypeId result = nullptr;
if (options.empty())
result = typechecker.nilType;
else if (options.size() == 1)
result = options[0];
else
result = arena.addType(UnionTypeVar{std::move(options)});
TypeId packedTable = arena.addType(
TableTypeVar{{{"n", {typechecker.numberType}}}, TableIndexer(typechecker.numberType, result), scope->level, TableState::Sealed});
return ExprResult<TypePackId>{arena.addTypePack({packedTable})};
}
static bool checkRequirePath(TypeChecker& typechecker, AstExpr* expr)
{
// require(foo.parent.bar) will technically work, but it depends on legacy goop that
// Luau does not and could not support without a bunch of work. It's deprecated anyway, so
// we'll warn here if we see it.
bool good = true;
AstExprIndexName* indexExpr = expr->as<AstExprIndexName>();
while (indexExpr)
{
if (indexExpr->index == "parent")
{
typechecker.reportError(indexExpr->indexLocation, DeprecatedApiUsed{"parent", "Parent"});
good = false;
}
indexExpr = indexExpr->expr->as<AstExprIndexName>();
}
return good;
}
static std::optional<ExprResult<TypePackId>> magicFunctionRequire(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult<TypePackId> exprResult)
{
TypeArena& arena = typechecker.currentModule->internalTypes;
if (expr.args.size != 1)
{
typechecker.reportError(TypeError{expr.location, GenericError{"require takes 1 argument"}});
return std::nullopt;
}
AstExpr* require = expr.args.data[0];
if (!checkRequirePath(typechecker, require))
return std::nullopt;
if (auto moduleInfo = typechecker.resolver->resolveModuleInfo(typechecker.currentModuleName, *require))
return ExprResult<TypePackId>{arena.addTypePack({typechecker.checkRequire(scope, *moduleInfo, expr.location)})};
return std::nullopt;
}
} // namespace Luau

278
Analysis/src/Config.cpp Normal file
View file

@ -0,0 +1,278 @@
// 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/Parser.h"
#include "Luau/StringUtils.h"
namespace
{
using Error = std::optional<std::string>;
}
namespace Luau
{
static Error parseBoolean(bool& result, const std::string& value)
{
if (value == "true")
result = true;
else if (value == "false")
result = false;
else
return Error{"Bad setting '" + value + "'. Valid options are true and false"};
return std::nullopt;
}
Error parseModeString(Mode& mode, const std::string& modeString, bool compat)
{
if (modeString == "nocheck")
mode = Mode::NoCheck;
else if (modeString == "strict")
mode = Mode::Strict;
else if (modeString == "nonstrict")
mode = Mode::Nonstrict;
else if (modeString == "noinfer" && compat)
mode = Mode::NoCheck;
else
return Error{"Bad mode \"" + modeString + "\". Valid options are nocheck, nonstrict, and strict"};
return std::nullopt;
}
static Error parseLintRuleStringForCode(
LintOptions& enabledLints, LintOptions& fatalLints, LintWarning::Code code, const std::string& value, bool compat)
{
if (value == "true")
{
enabledLints.enableWarning(code);
}
else if (value == "false")
{
enabledLints.disableWarning(code);
}
else if (compat)
{
if (value == "enabled")
{
enabledLints.enableWarning(code);
fatalLints.disableWarning(code);
}
else if (value == "disabled")
{
enabledLints.disableWarning(code);
fatalLints.disableWarning(code);
}
else if (value == "fatal")
{
enabledLints.enableWarning(code);
fatalLints.enableWarning(code);
}
else
{
return Error{"Bad setting '" + value + "'. Valid options are enabled, disabled, and fatal"};
}
}
else
{
return Error{"Bad setting '" + value + "'. Valid options are true and false"};
}
return std::nullopt;
}
Error parseLintRuleString(LintOptions& enabledLints, LintOptions& fatalLints, const std::string& warningName, const std::string& value, bool compat)
{
if (warningName == "*")
{
for (int code = LintWarning::Code_Unknown; code < LintWarning::Code__Count; ++code)
{
if (auto err = parseLintRuleStringForCode(enabledLints, fatalLints, LintWarning::Code(code), value, compat))
return Error{"In key " + warningName + ": " + *err};
}
}
else
{
LintWarning::Code code = LintWarning::parseName(warningName.c_str());
if (code == LintWarning::Code_Unknown)
return Error{"Unknown lint " + warningName};
if (auto err = parseLintRuleStringForCode(enabledLints, fatalLints, code, value, compat))
return Error{"In key " + warningName + ": " + *err};
}
return std::nullopt;
}
static void next(Lexer& lexer)
{
lexer.next();
// skip C-style comments as Lexer only understands Lua-style comments atm
while (lexer.current().type == '/')
{
Lexeme peek = lexer.lookahead();
if (peek.type != '/' || peek.location.begin != lexer.current().location.end)
break;
lexer.nextline();
}
}
static Error fail(Lexer& lexer, const char* message)
{
Lexeme cur = lexer.current();
return format("Expected %s at line %d, got %s instead", message, cur.location.begin.line + 1, cur.toString().c_str());
}
template<typename Action>
static Error parseJson(const std::string& contents, Action action)
{
Allocator allocator;
AstNameTable names(allocator);
Lexer lexer(contents.data(), contents.size(), names);
next(lexer);
std::vector<std::string> keys;
bool arrayTop = false; // we don't support nested arrays
if (lexer.current().type != '{')
return fail(lexer, "'{'");
next(lexer);
for (;;)
{
if (arrayTop)
{
if (lexer.current().type == ']')
{
next(lexer);
arrayTop = false;
LUAU_ASSERT(!keys.empty());
keys.pop_back();
if (lexer.current().type == ',')
next(lexer);
else if (lexer.current().type != '}')
return fail(lexer, "',' or '}'");
}
else if (lexer.current().type == Lexeme::QuotedString)
{
std::string value(lexer.current().data, lexer.current().length);
next(lexer);
if (Error err = action(keys, value))
return err;
if (lexer.current().type == ',')
next(lexer);
else if (lexer.current().type != ']')
return fail(lexer, "',' or ']'");
}
else
return fail(lexer, "array element or ']'");
}
else
{
if (lexer.current().type == '}')
{
next(lexer);
if (keys.empty())
{
if (lexer.current().type != Lexeme::Eof)
return fail(lexer, "end of file");
return {};
}
else
keys.pop_back();
if (lexer.current().type == ',')
next(lexer);
else if (lexer.current().type != '}')
return fail(lexer, "',' or '}'");
}
else if (lexer.current().type == Lexeme::QuotedString)
{
std::string key(lexer.current().data, lexer.current().length);
next(lexer);
keys.push_back(key);
if (lexer.current().type != ':')
return fail(lexer, "':'");
next(lexer);
if (lexer.current().type == '{' || lexer.current().type == '[')
{
arrayTop = (lexer.current().type == '[');
next(lexer);
}
else if (lexer.current().type == Lexeme::QuotedString || lexer.current().type == Lexeme::ReservedTrue ||
lexer.current().type == Lexeme::ReservedFalse)
{
std::string value = lexer.current().type == Lexeme::QuotedString
? std::string(lexer.current().data, lexer.current().length)
: (lexer.current().type == Lexeme::ReservedTrue ? "true" : "false");
next(lexer);
if (Error err = action(keys, value))
return err;
keys.pop_back();
if (lexer.current().type == ',')
next(lexer);
else if (lexer.current().type != '}')
return fail(lexer, "',' or '}'");
}
else
return fail(lexer, "field value");
}
else
return fail(lexer, "field key");
}
}
return {};
}
Error parseConfig(const std::string& contents, Config& config, bool compat)
{
return parseJson(contents, [&](const std::vector<std::string>& keys, const std::string& value) -> Error {
if (keys.size() == 1 && keys[0] == "languageMode")
return parseModeString(config.mode, value, compat);
else if (keys.size() == 2 && keys[0] == "lint")
return parseLintRuleString(config.enabledLint, config.fatalLint, keys[1], value, compat);
else if (keys.size() == 1 && keys[0] == "lintErrors")
return parseBoolean(config.lintErrors, value);
else if (keys.size() == 1 && keys[0] == "typeErrors")
return parseBoolean(config.typeErrors, value);
else if (keys.size() == 1 && keys[0] == "globals")
{
config.globals.push_back(value);
return std::nullopt;
}
else if (compat && keys.size() == 2 && keys[0] == "language" && keys[1] == "mode")
return parseModeString(config.mode, value, compat);
else
{
std::vector<std::string_view> keysv(keys.begin(), keys.end());
return "Unknown key " + join(keysv, "/");
}
});
}
const Config& NullConfigResolver::getConfig(const ModuleName& name) const
{
return defaultConfig;
}
} // namespace Luau

View file

@ -0,0 +1,238 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAG(LuauParseGenericFunctions)
LUAU_FASTFLAG(LuauGenericFunctions)
namespace Luau
{
static const std::string kBuiltinDefinitionLuaSrc = R"BUILTIN_SRC(
declare bit32: {
-- band, bor, bxor, and btest are declared in C++
rrotate: (number, number) -> number,
lrotate: (number, number) -> number,
lshift: (number, number) -> number,
arshift: (number, number) -> number,
rshift: (number, number) -> number,
bnot: (number) -> number,
extract: (number, number, number?) -> number,
replace: (number, number, number, number?) -> number,
}
declare math: {
frexp: (number) -> (number, number),
ldexp: (number, number) -> number,
fmod: (number, number) -> number,
modf: (number) -> (number, number),
pow: (number, number) -> number,
exp: (number) -> number,
ceil: (number) -> number,
floor: (number) -> number,
abs: (number) -> number,
sqrt: (number) -> number,
log: (number, number?) -> number,
log10: (number) -> number,
rad: (number) -> number,
deg: (number) -> number,
sin: (number) -> number,
cos: (number) -> number,
tan: (number) -> number,
sinh: (number) -> number,
cosh: (number) -> number,
tanh: (number) -> number,
atan: (number) -> number,
acos: (number) -> number,
asin: (number) -> number,
atan2: (number, number) -> number,
-- min and max are declared in C++.
pi: number,
huge: number,
randomseed: (number) -> (),
random: (number?, number?) -> number,
sign: (number) -> number,
clamp: (number, number, number) -> number,
noise: (number, number?, number?) -> number,
round: (number) -> number,
}
type DateTypeArg = {
year: number,
month: number,
day: number,
hour: number?,
min: number?,
sec: number?,
isdst: boolean?,
}
type DateTypeResult = {
year: number,
month: number,
wday: number,
yday: number,
day: number,
hour: number,
min: number,
sec: number,
isdst: boolean,
}
declare os: {
time: (DateTypeArg?) -> number,
date: (string?, number?) -> DateTypeResult | string,
difftime: (DateTypeResult | number, DateTypeResult | number) -> number,
clock: () -> number,
}
declare function require(target: any): any
declare function getfenv(target: any?): { [string]: any }
declare _G: any
declare _VERSION: string
declare function gcinfo(): number
)BUILTIN_SRC";
std::string getBuiltinDefinitionSource()
{
std::string src = kBuiltinDefinitionLuaSrc;
if (FFlag::LuauParseGenericFunctions && FFlag::LuauGenericFunctions)
{
src += R"(
declare function print<T...>(...: T...)
declare function type<T>(value: T): string
declare function typeof<T>(value: T): string
-- `assert` has a magic function attached that will give more detailed type information
declare function assert<T>(value: T, errorMessage: string?): T
declare function error<T>(message: T, level: number?)
declare function tostring<T>(value: T): string
declare function tonumber<T>(value: T, radix: number?): number
declare function rawequal<T1, T2>(a: T1, b: T2): boolean
declare function rawget<K, V>(tab: {[K]: V}, k: K): V
declare function rawset<K, V>(tab: {[K]: V}, k: K, v: V): {[K]: V}
declare function setfenv<T..., R...>(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)?
declare function ipairs<V>(tab: {V}): (({V}, number) -> (number, V), {V}, number)
declare function pcall<A..., R...>(f: (A...) -> R..., ...: A...): (boolean, R...)
-- FIXME: The actual type of `xpcall` is:
-- <E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., A...) -> (true, R1...) | (false, R2...)
-- Since we can't represent the return value, we use (boolean, R1...).
declare function xpcall<E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...)
-- `select` has a magic function attached to provide more detailed type information
declare function select<A...>(i: string | number, ...: A...): ...any
-- FIXME: This type is not entirely correct - `loadstring` returns a function or
-- (nil, string).
declare function loadstring<A...>(src: string, chunkname: string?): (((A...) -> any)?, string?)
-- a userdata object is "roughly" the same as a sealed empty table
-- except `type(newproxy(false))` evaluates to "userdata" so we may need another special type here too.
-- another important thing to note: the value passed in conditionally creates an empty metatable, and you have to use getmetatable, NOT
-- setmetatable.
-- FIXME: change this to something Luau can understand how to reject `setmetatable(newproxy(false or true), {})`.
declare function newproxy(mt: boolean?): {}
declare coroutine: {
create: <A..., R...>((A...) -> R...) -> thread,
resume: <A..., R...>(thread, A...) -> (boolean, R...),
running: () -> thread,
status: (thread) -> string,
-- FIXME: This technically returns a function, but we can't represent this yet.
wrap: <A..., R...>((A...) -> R...) -> any,
yield: <A..., R...>(A...) -> R...,
isyieldable: () -> boolean,
}
declare table: {
concat: <V>({V}, string?, number?, number?) -> string,
insert: (<V>({V}, V) -> ()) & (<V>({V}, number, V) -> ()),
maxn: <V>({V}) -> number,
remove: <V>({V}, number?) -> V?,
sort: <V>({V}, ((V, V) -> boolean)?) -> (),
create: <V>(number, V?) -> {V},
find: <V>({V}, V, number?) -> number?,
unpack: <V>({V}, number?, number?) -> ...V,
pack: <V>(...V) -> { n: number, [number]: V },
getn: <V>({V}) -> number,
foreach: <K, V>({[K]: V}, (K, V) -> ()) -> (),
foreachi: <V>({V}, (number, V) -> ()) -> (),
move: <V>({V}, number, number, number, {V}?) -> (),
clear: <K, V>({[K]: V}) -> (),
freeze: <K, V>({[K]: V}) -> {[K]: V},
isfrozen: <K, V>({[K]: V}) -> boolean,
}
declare debug: {
info: (<R...>(thread, number, string) -> R...) & (<R...>(number, string) -> R...) & (<A..., R1..., R2...>((A...) -> R1..., string) -> R2...),
traceback: ((string?, number?) -> string) & ((thread, string?, number?) -> string),
}
declare utf8: {
char: (number, ...number) -> string,
charpattern: string,
codes: (string) -> ((string, number) -> (number, number), string, number),
-- FIXME
codepoint: (string, number?, number?) -> (number, ...number),
len: (string, number?, number?) -> (number?, number?),
offset: (string, number?, number?) -> number,
nfdnormalize: (string) -> string,
nfcnormalize: (string) -> string,
graphemes: (string, number?, number?) -> (() -> (number, number)),
}
declare string: {
byte: (string, number?, number?) -> ...number,
char: (number, ...number) -> string,
find: (string, string, number?, boolean?) -> (number?, number?),
-- `string.format` has a magic function attached that will provide more type information for literal format strings.
format: <A...>(string, A...) -> string,
gmatch: (string, string) -> () -> (...string),
-- gsub is defined in C++ because we don't have syntax for describing a generic table.
len: (string) -> number,
lower: (string) -> string,
match: (string, string, number?) -> string?,
rep: (string, number) -> string,
reverse: (string) -> string,
sub: (string, number, number?) -> string,
upper: (string) -> string,
split: (string, string, string?) -> {string},
pack: <A...>(string, A...) -> string,
packsize: (string) -> number,
unpack: <R...>(string, string, number?) -> R...,
}
-- Cannot use `typeof` here because it will produce a polytype when we expect a monotype.
declare function unpack<V>(tab: {V}, i: number?, j: number?): ...V
)";
}
return src;
}
} // namespace Luau

751
Analysis/src/Error.cpp Normal file
View file

@ -0,0 +1,751 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Error.h"
#include "Luau/Module.h"
#include "Luau/StringUtils.h"
#include "Luau/ToString.h"
#include <stdexcept>
LUAU_FASTFLAG(LuauFasterStringifier)
static std::string wrongNumberOfArgsString(size_t expectedCount, size_t actualCount, bool isTypeArgs = false)
{
std::string s = "expects " + std::to_string(expectedCount) + " ";
if (isTypeArgs)
s += "type ";
s += "argument";
if (expectedCount != 1)
s += "s";
s += ", but ";
if (actualCount == 0)
{
s += "none";
}
else
{
if (actualCount < expectedCount)
s += "only ";
s += std::to_string(actualCount);
}
s += (actualCount == 1) ? " is" : " are";
s += " specified";
return s;
}
namespace Luau
{
struct ErrorConverter
{
std::string operator()(const Luau::TypeMismatch& tm) const
{
ToStringOptions opts;
return "Type '" + Luau::toString(tm.givenType, opts) + "' could not be converted into '" + Luau::toString(tm.wantedType, opts) + "'";
}
std::string operator()(const Luau::UnknownSymbol& e) const
{
switch (e.context)
{
case UnknownSymbol::Binding:
return "Unknown global '" + e.name + "'";
case UnknownSymbol::Type:
return "Unknown type '" + e.name + "'";
case UnknownSymbol::Generic:
return "Unknown generic '" + e.name + "'";
}
LUAU_ASSERT(!"Unexpected context for UnknownSymbol");
return "";
}
std::string operator()(const Luau::UnknownProperty& e) const
{
TypeId t = follow(e.table);
if (get<TableTypeVar>(t))
return "Key '" + e.key + "' not found in table '" + Luau::toString(t) + "'";
else if (get<ClassTypeVar>(t))
return "Key '" + e.key + "' not found in class '" + Luau::toString(t) + "'";
else
return "Type '" + Luau::toString(e.table) + "' does not have key '" + e.key + "'";
}
std::string operator()(const Luau::NotATable& e) const
{
return "Expected type table, got '" + Luau::toString(e.ty) + "' instead";
}
std::string operator()(const Luau::CannotExtendTable& e) const
{
switch (e.context)
{
case Luau::CannotExtendTable::Property:
return "Cannot add property '" + e.prop + "' to table '" + Luau::toString(e.tableType) + "'";
case Luau::CannotExtendTable::Metatable:
return "Cannot add metatable to table '" + Luau::toString(e.tableType) + "'";
case Luau::CannotExtendTable::Indexer:
return "Cannot add indexer to table '" + Luau::toString(e.tableType) + "'";
}
LUAU_ASSERT(!"Unknown context");
return "";
}
std::string operator()(const Luau::OnlyTablesCanHaveMethods& e) const
{
return "Cannot add method to non-table type '" + Luau::toString(e.tableType) + "'";
}
std::string operator()(const Luau::DuplicateTypeDefinition& e) const
{
return "Redefinition of type '" + e.name + "', previously defined at line " + std::to_string(e.previousLocation.begin.line + 1);
}
std::string operator()(const Luau::CountMismatch& e) const
{
switch (e.context)
{
case CountMismatch::Return:
{
const std::string expectedS = e.expected == 1 ? "" : "s";
const std::string actualS = e.actual == 1 ? "is" : "are";
return "Expected to return " + std::to_string(e.expected) + " value" + expectedS + ", but " + std::to_string(e.actual) + " " + actualS +
" returned here";
}
case CountMismatch::Result:
if (e.expected > e.actual)
return "Function returns " + std::to_string(e.expected) + " values but there are only " + std::to_string(e.expected) +
" values to unpack them into.";
else
return "Function only returns " + std::to_string(e.expected) + " values. " + std::to_string(e.actual) + " are required here";
case CountMismatch::Arg:
return "Argument count mismatch. Function " + wrongNumberOfArgsString(e.expected, e.actual);
}
LUAU_ASSERT(!"Unknown context");
return "";
}
std::string operator()(const Luau::FunctionDoesNotTakeSelf&) const
{
return std::string("This function does not take self. Did you mean to use a dot instead of a colon?");
}
std::string operator()(const Luau::FunctionRequiresSelf& e) const
{
if (e.requiredExtraNils)
{
const char* plural = e.requiredExtraNils == 1 ? "" : "s";
return format("This function was declared to accept self, but you did not pass enough arguments. Use a colon instead of a dot or "
"pass %i extra nil%s to suppress this warning",
e.requiredExtraNils, plural);
}
else
return "This function must be called with self. Did you mean to use a colon instead of a dot?";
}
std::string operator()(const Luau::OccursCheckFailed&) const
{
return "Type contains a self-recursive construct that cannot be resolved";
}
std::string operator()(const Luau::UnknownRequire& e) const
{
return "Unknown require: " + e.modulePath;
}
std::string operator()(const Luau::IncorrectGenericParameterCount& e) const
{
std::string name = e.name;
if (!e.typeFun.typeParams.empty())
{
name += "<";
bool first = true;
for (TypeId t : e.typeFun.typeParams)
{
if (first)
first = false;
else
name += ", ";
name += toString(t);
}
name += ">";
}
return "Generic type '" + name + "' " + wrongNumberOfArgsString(e.typeFun.typeParams.size(), e.actualParameters, /*isTypeArgs*/ true);
}
std::string operator()(const Luau::SyntaxError& e) const
{
return "Syntax error: " + e.message;
}
std::string operator()(const Luau::CodeTooComplex&) const
{
return "Code is too complex to typecheck! Consider simplifying the code around this area";
}
std::string operator()(const Luau::UnificationTooComplex&) const
{
return "Internal error: Code is too complex to typecheck! Consider adding type annotations around this area";
}
std::string operator()(const Luau::UnknownPropButFoundLikeProp& e) const
{
std::string candidatesSuggestion = "Did you mean ";
if (e.candidates.size() != 1)
candidatesSuggestion += "one of ";
bool first = true;
for (Name name : e.candidates)
{
if (first)
first = false;
else
candidatesSuggestion += ", ";
candidatesSuggestion += "'" + name + "'";
}
std::string s = "Key '" + e.key + "' not found in ";
TypeId t = follow(e.table);
if (get<ClassTypeVar>(t))
s += "class";
else
s += "table";
s += " '" + toString(e.table) + "'. " + candidatesSuggestion + "?";
return s;
}
std::string operator()(const Luau::GenericError& e) const
{
return e.message;
}
std::string operator()(const Luau::CannotCallNonFunction& e) const
{
return "Cannot call non-function " + toString(e.ty);
}
std::string operator()(const Luau::ExtraInformation& e) const
{
return e.message;
}
std::string operator()(const Luau::DeprecatedApiUsed& e) const
{
return "The property ." + e.symbol + " is deprecated. Use ." + e.useInstead + " instead.";
}
std::string operator()(const Luau::ModuleHasCyclicDependency& e) const
{
if (e.cycle.empty())
return "Cyclic module dependency detected";
std::string s = "Cyclic module dependency: ";
bool first = true;
for (const ModuleName& name : e.cycle)
{
if (first)
first = false;
else
s += " -> ";
s += name;
}
return s;
}
std::string operator()(const Luau::FunctionExitsWithoutReturning& e) const
{
return "Not all codepaths in this function return '" + toString(e.expectedReturnType) + "'.";
}
std::string operator()(const Luau::IllegalRequire& e) const
{
return "Cannot require module " + e.moduleName + ": " + e.reason;
}
std::string operator()(const Luau::MissingProperties& e) const
{
std::string s = "Table type '" + toString(e.subType) + "' not compatible with type '" + toString(e.superType) + "' because the former";
switch (e.context)
{
case MissingProperties::Missing:
s += " is missing field";
break;
case MissingProperties::Extra:
s += " has extra field";
break;
}
if (e.properties.size() > 1)
s += "s";
s += " ";
for (size_t i = 0; i < e.properties.size(); ++i)
{
if (i > 0)
s += ", ";
if (i > 0 && i == e.properties.size() - 1)
s += "and ";
s += "'" + e.properties[i] + "'";
}
return s;
}
std::string operator()(const Luau::DuplicateGenericParameter& e) const
{
return "Duplicate type parameter '" + e.parameterName + "'";
}
std::string operator()(const Luau::CannotInferBinaryOperation& e) const
{
std::string ss = "Unknown type used in " + toString(e.op);
switch (e.kind)
{
case Luau::CannotInferBinaryOperation::Comparison:
ss += " comparison";
break;
case Luau::CannotInferBinaryOperation::Operation:
ss += " operation";
}
if (e.suggestedToAnnotate)
ss += "; consider adding a type annotation to '" + *e.suggestedToAnnotate + "'";
return ss;
}
std::string operator()(const Luau::SwappedGenericTypeParameter& e) const
{
switch (e.kind)
{
case Luau::SwappedGenericTypeParameter::Type:
return "Variadic type parameter '" + e.name + "...' is used as a regular generic type; consider changing '" + e.name + "...' to '" +
e.name + "' in the generic argument list";
case Luau::SwappedGenericTypeParameter::Pack:
return "Generic type '" + e.name + "' is used as a variadic type parameter; consider changing '" + e.name + "' to '" + e.name +
"...' in the generic argument list";
default:
LUAU_ASSERT(!"Unknown kind");
return "";
}
}
std::string operator()(const Luau::OptionalValueAccess& e) const
{
return "Value of type '" + toString(e.optional) + "' could be nil";
}
std::string operator()(const Luau::MissingUnionProperty& e) const
{
std::string ss = "Key '" + e.key + "' is missing from ";
bool first = true;
for (auto ty : e.missing)
{
if (first)
first = false;
else
ss += ", ";
ss += "'" + toString(ty) + "'";
}
return ss + " in the type '" + toString(e.type) + "'";
}
};
struct InvalidNameChecker
{
std::string invalidName = "%error-id%";
bool operator()(const Luau::UnknownProperty& e) const
{
return e.key == invalidName;
}
bool operator()(const Luau::CannotExtendTable& e) const
{
return e.prop == invalidName;
}
bool operator()(const Luau::DuplicateTypeDefinition& e) const
{
return e.name == invalidName;
}
template<typename T>
bool operator()(const T& other) const
{
return false;
}
};
bool TypeMismatch::operator==(const TypeMismatch& rhs) const
{
return *wantedType == *rhs.wantedType && *givenType == *rhs.givenType;
}
bool UnknownSymbol::operator==(const UnknownSymbol& rhs) const
{
return name == rhs.name;
}
bool UnknownProperty::operator==(const UnknownProperty& rhs) const
{
return *table == *rhs.table && key == rhs.key;
}
bool NotATable::operator==(const NotATable& rhs) const
{
return ty == rhs.ty;
}
bool CannotExtendTable::operator==(const CannotExtendTable& rhs) const
{
return *tableType == *rhs.tableType && prop == rhs.prop && context == rhs.context;
}
bool OnlyTablesCanHaveMethods::operator==(const OnlyTablesCanHaveMethods& rhs) const
{
return *tableType == *rhs.tableType;
}
bool DuplicateTypeDefinition::operator==(const DuplicateTypeDefinition& rhs) const
{
return name == rhs.name && previousLocation == rhs.previousLocation;
}
bool CountMismatch::operator==(const CountMismatch& rhs) const
{
return expected == rhs.expected && actual == rhs.actual && context == rhs.context;
}
bool FunctionDoesNotTakeSelf::operator==(const FunctionDoesNotTakeSelf&) const
{
return true;
}
bool FunctionRequiresSelf::operator==(const FunctionRequiresSelf& e) const
{
return requiredExtraNils == e.requiredExtraNils;
}
bool OccursCheckFailed::operator==(const OccursCheckFailed&) const
{
return true;
}
bool UnknownRequire::operator==(const UnknownRequire& rhs) const
{
return modulePath == rhs.modulePath;
}
bool IncorrectGenericParameterCount::operator==(const IncorrectGenericParameterCount& rhs) const
{
if (name != rhs.name)
return false;
if (typeFun.type != rhs.typeFun.type)
return false;
if (typeFun.typeParams.size() != rhs.typeFun.typeParams.size())
return false;
for (size_t i = 0; i < typeFun.typeParams.size(); ++i)
if (typeFun.typeParams[i] != rhs.typeFun.typeParams[i])
return false;
return true;
}
bool SyntaxError::operator==(const SyntaxError& rhs) const
{
return message == rhs.message;
}
bool CodeTooComplex::operator==(const CodeTooComplex&) const
{
return true;
}
bool UnificationTooComplex::operator==(const UnificationTooComplex&) const
{
return true;
}
bool UnknownPropButFoundLikeProp::operator==(const UnknownPropButFoundLikeProp& rhs) const
{
return *table == *rhs.table && key == rhs.key && candidates.size() == rhs.candidates.size() &&
std::equal(candidates.begin(), candidates.end(), rhs.candidates.begin());
}
bool GenericError::operator==(const GenericError& rhs) const
{
return message == rhs.message;
}
bool CannotCallNonFunction::operator==(const CannotCallNonFunction& rhs) const
{
return ty == rhs.ty;
}
bool ExtraInformation::operator==(const ExtraInformation& rhs) const
{
return message == rhs.message;
}
bool DeprecatedApiUsed::operator==(const DeprecatedApiUsed& rhs) const
{
return symbol == rhs.symbol && useInstead == rhs.useInstead;
}
bool FunctionExitsWithoutReturning::operator==(const FunctionExitsWithoutReturning& rhs) const
{
return expectedReturnType == rhs.expectedReturnType;
}
int TypeError::code() const
{
return 1000 + int(data.index());
}
bool TypeError::operator==(const TypeError& rhs) const
{
return location == rhs.location && data == rhs.data;
}
bool ModuleHasCyclicDependency::operator==(const ModuleHasCyclicDependency& rhs) const
{
return cycle.size() == rhs.cycle.size() && std::equal(cycle.begin(), cycle.end(), rhs.cycle.begin());
}
bool IllegalRequire::operator==(const IllegalRequire& rhs) const
{
return moduleName == rhs.moduleName && reason == rhs.reason;
}
bool MissingProperties::operator==(const MissingProperties& rhs) const
{
return *superType == *rhs.superType && *subType == *rhs.subType && properties.size() == rhs.properties.size() &&
std::equal(properties.begin(), properties.end(), rhs.properties.begin()) && context == rhs.context;
}
bool DuplicateGenericParameter::operator==(const DuplicateGenericParameter& rhs) const
{
return parameterName == rhs.parameterName;
}
bool CannotInferBinaryOperation::operator==(const CannotInferBinaryOperation& rhs) const
{
return op == rhs.op && suggestedToAnnotate == rhs.suggestedToAnnotate && kind == rhs.kind;
}
bool SwappedGenericTypeParameter::operator==(const SwappedGenericTypeParameter& rhs) const
{
return name == rhs.name && kind == rhs.kind;
}
bool OptionalValueAccess::operator==(const OptionalValueAccess& rhs) const
{
return *optional == *rhs.optional;
}
bool MissingUnionProperty::operator==(const MissingUnionProperty& rhs) const
{
if (missing.size() != rhs.missing.size())
return false;
for (size_t i = 0; i < missing.size(); ++i)
{
if (*missing[i] != *rhs.missing[i])
return false;
}
return *type == *rhs.type && key == rhs.key;
}
std::string toString(const TypeError& error)
{
ErrorConverter converter;
return Luau::visit(converter, error.data);
}
bool containsParseErrorName(const TypeError& error)
{
return Luau::visit(InvalidNameChecker{}, error.data);
}
void copyErrors(ErrorVec& errors, struct TypeArena& destArena)
{
SeenTypes seenTypes;
SeenTypePacks seenTypePacks;
auto clone = [&](auto&& ty) {
return ::Luau::clone(ty, destArena, seenTypes, seenTypePacks);
};
auto visitErrorData = [&](auto&& e) {
using T = std::decay_t<decltype(e)>;
if constexpr (false)
{
}
else if constexpr (std::is_same_v<T, TypeMismatch>)
{
e.wantedType = clone(e.wantedType);
e.givenType = clone(e.givenType);
}
else if constexpr (std::is_same_v<T, UnknownSymbol>)
{
}
else if constexpr (std::is_same_v<T, UnknownProperty>)
{
e.table = clone(e.table);
}
else if constexpr (std::is_same_v<T, NotATable>)
{
e.ty = clone(e.ty);
}
else if constexpr (std::is_same_v<T, CannotExtendTable>)
{
e.tableType = clone(e.tableType);
}
else if constexpr (std::is_same_v<T, OnlyTablesCanHaveMethods>)
{
e.tableType = clone(e.tableType);
}
else if constexpr (std::is_same_v<T, DuplicateTypeDefinition>)
{
}
else if constexpr (std::is_same_v<T, CountMismatch>)
{
}
else if constexpr (std::is_same_v<T, FunctionDoesNotTakeSelf>)
{
}
else if constexpr (std::is_same_v<T, FunctionRequiresSelf>)
{
}
else if constexpr (std::is_same_v<T, OccursCheckFailed>)
{
}
else if constexpr (std::is_same_v<T, UnknownRequire>)
{
}
else if constexpr (std::is_same_v<T, IncorrectGenericParameterCount>)
{
e.typeFun = clone(e.typeFun);
}
else if constexpr (std::is_same_v<T, SyntaxError>)
{
}
else if constexpr (std::is_same_v<T, CodeTooComplex>)
{
}
else if constexpr (std::is_same_v<T, UnificationTooComplex>)
{
}
else if constexpr (std::is_same_v<T, UnknownPropButFoundLikeProp>)
{
e.table = clone(e.table);
}
else if constexpr (std::is_same_v<T, GenericError>)
{
}
else if constexpr (std::is_same_v<T, CannotCallNonFunction>)
{
e.ty = clone(e.ty);
}
else if constexpr (std::is_same_v<T, ExtraInformation>)
{
}
else if constexpr (std::is_same_v<T, DeprecatedApiUsed>)
{
}
else if constexpr (std::is_same_v<T, ModuleHasCyclicDependency>)
{
}
else if constexpr (std::is_same_v<T, IllegalRequire>)
{
}
else if constexpr (std::is_same_v<T, FunctionExitsWithoutReturning>)
{
e.expectedReturnType = clone(e.expectedReturnType);
}
else if constexpr (std::is_same_v<T, DuplicateGenericParameter>)
{
}
else if constexpr (std::is_same_v<T, CannotInferBinaryOperation>)
{
}
else if constexpr (std::is_same_v<T, MissingProperties>)
{
e.superType = clone(e.superType);
e.subType = clone(e.subType);
}
else if constexpr (std::is_same_v<T, SwappedGenericTypeParameter>)
{
}
else if constexpr (std::is_same_v<T, OptionalValueAccess>)
{
e.optional = clone(e.optional);
}
else if constexpr (std::is_same_v<T, MissingUnionProperty>)
{
e.type = clone(e.type);
for (auto& ty : e.missing)
ty = clone(ty);
}
else
static_assert(always_false_v<T>, "Non-exhaustive type switch");
};
LUAU_ASSERT(!destArena.typeVars.isFrozen());
LUAU_ASSERT(!destArena.typePacks.isFrozen());
for (TypeError& error : errors)
visit(visitErrorData, error.data);
}
void InternalErrorReporter::ice(const std::string& message, const Location& location)
{
std::runtime_error error("Internal error in " + moduleName + " at " + toString(location) + ": " + message);
if (onInternalError)
onInternalError(error.what());
throw error;
}
void InternalErrorReporter::ice(const std::string& message)
{
std::runtime_error error("Internal error in " + moduleName + ": " + message);
if (onInternalError)
onInternalError(error.what());
throw error;
}
} // namespace Luau

967
Analysis/src/Frontend.cpp Normal file
View file

@ -0,0 +1,967 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Frontend.h"
#include "Luau/Config.h"
#include "Luau/FileResolver.h"
#include "Luau/StringUtils.h"
#include "Luau/TypeInfer.h"
#include "Luau/Variant.h"
#include "Luau/Common.h"
#include <algorithm>
#include <chrono>
#include <stdexcept>
LUAU_FASTFLAG(LuauInferInNoCheckMode)
LUAU_FASTFLAGVARIABLE(LuauTypeCheckTwice, false)
LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
LUAU_FASTFLAGVARIABLE(LuauSecondTypecheckKnowsTheDataModel, false)
LUAU_FASTFLAGVARIABLE(LuauResolveModuleNameWithoutACurrentModule, false)
LUAU_FASTFLAG(LuauTraceRequireLookupChild)
LUAU_FASTFLAGVARIABLE(LuauPersistDefinitionFileTypes, false)
namespace Luau
{
std::optional<Mode> parseMode(const std::vector<std::string>& hotcomments)
{
for (const std::string& hc : hotcomments)
{
if (hc == "nocheck")
return Mode::NoCheck;
if (hc == "nonstrict")
return Mode::Nonstrict;
if (hc == "strict")
return Mode::Strict;
}
return std::nullopt;
}
static void generateDocumentationSymbols(TypeId ty, const std::string& rootName)
{
// TODO: What do we do in this situation? This means that the definition
// file is exporting a type that is also a persistent type.
if (ty->persistent)
{
return;
}
asMutable(ty)->documentationSymbol = rootName;
if (TableTypeVar* ttv = getMutable<TableTypeVar>(ty))
{
for (auto& [name, prop] : ttv->props)
{
prop.documentationSymbol = rootName + "." + name;
}
}
else if (ClassTypeVar* ctv = getMutable<ClassTypeVar>(ty))
{
for (auto& [name, prop] : ctv->props)
{
prop.documentationSymbol = rootName + "." + name;
}
}
}
LoadDefinitionFileResult loadDefinitionFile(TypeChecker& typeChecker, ScopePtr targetScope, std::string_view source, const std::string& packageName)
{
Luau::Allocator allocator;
Luau::AstNameTable names(allocator);
ParseOptions options;
options.allowDeclarationSyntax = true;
Luau::ParseResult parseResult = Luau::Parser::parse(source.data(), source.size(), names, allocator, options);
if (parseResult.errors.size() > 0)
return LoadDefinitionFileResult{false, parseResult, nullptr};
Luau::SourceModule module;
module.root = parseResult.root;
module.mode = Mode::Definition;
ModulePtr checkedModule = typeChecker.check(module, Mode::Definition);
if (checkedModule->errors.size() > 0)
return LoadDefinitionFileResult{false, parseResult, checkedModule};
SeenTypes seenTypes;
SeenTypePacks seenTypePacks;
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
TypeId globalTy = clone(ty, typeChecker.globalTypes, seenTypes, seenTypePacks);
std::string documentationSymbol = packageName + "/global/" + name;
generateDocumentationSymbols(globalTy, documentationSymbol);
targetScope->bindings[typeChecker.globalNames.names->getOrAdd(name.c_str())] = {globalTy, Location(), false, {}, documentationSymbol};
if (FFlag::LuauPersistDefinitionFileTypes)
persist(globalTy);
}
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
{
TypeFun globalTy = clone(ty, typeChecker.globalTypes, seenTypes, seenTypePacks);
std::string documentationSymbol = packageName + "/globaltype/" + name;
generateDocumentationSymbols(globalTy.type, documentationSymbol);
targetScope->exportedTypeBindings[name] = globalTy;
if (FFlag::LuauPersistDefinitionFileTypes)
persist(globalTy.type);
}
return LoadDefinitionFileResult{true, parseResult, checkedModule};
}
std::vector<std::string_view> parsePathExpr(const AstExpr& pathExpr)
{
const AstExprIndexName* indexName = pathExpr.as<AstExprIndexName>();
if (!indexName)
return {};
std::vector<std::string_view> segments{indexName->index.value};
while (true)
{
if (AstExprIndexName* in = indexName->expr->as<AstExprIndexName>())
{
segments.push_back(in->index.value);
indexName = in;
continue;
}
else if (AstExprGlobal* indexNameAsGlobal = indexName->expr->as<AstExprGlobal>())
{
segments.push_back(indexNameAsGlobal->name.value);
break;
}
else if (AstExprLocal* indexNameAsLocal = indexName->expr->as<AstExprLocal>())
{
segments.push_back(indexNameAsLocal->local->name.value);
break;
}
else
return {};
}
std::reverse(segments.begin(), segments.end());
return segments;
}
std::optional<std::string> pathExprToModuleName(const ModuleName& currentModuleName, const std::vector<std::string_view>& segments)
{
if (segments.empty())
return std::nullopt;
std::vector<std::string_view> result;
auto it = segments.begin();
if (*it == "script" && !currentModuleName.empty())
{
result = split(currentModuleName, '/');
++it;
}
for (; it != segments.end(); ++it)
{
if (result.size() > 1 && *it == "Parent")
result.pop_back();
else
result.push_back(*it);
}
return join(result, "/");
}
std::optional<std::string> pathExprToModuleName(const ModuleName& currentModuleName, const AstExpr& pathExpr)
{
std::vector<std::string_view> segments = parsePathExpr(pathExpr);
return pathExprToModuleName(currentModuleName, segments);
}
namespace
{
ErrorVec accumulateErrors(
const std::unordered_map<ModuleName, SourceNode>& sourceNodes, const std::unordered_map<ModuleName, ModulePtr>& modules, const ModuleName& name)
{
std::unordered_set<ModuleName> seen;
std::vector<ModuleName> queue{name};
ErrorVec result;
while (!queue.empty())
{
ModuleName next = std::move(queue.back());
queue.pop_back();
if (seen.count(next))
continue;
seen.insert(next);
auto it = sourceNodes.find(next);
if (it == sourceNodes.end())
continue;
const SourceNode& sourceNode = it->second;
queue.insert(queue.end(), sourceNode.requires.begin(), sourceNode.requires.end());
// FIXME: If a module has a syntax error, we won't be able to re-report it here.
// The solution is probably to move errors from Module to SourceNode
auto it2 = modules.find(next);
if (it2 == modules.end())
continue;
Module& module = *it2->second;
std::sort(module.errors.begin(), module.errors.end(), [](const TypeError& e1, const TypeError& e2) -> bool {
return e1.location.begin > e2.location.begin;
});
result.insert(result.end(), module.errors.begin(), module.errors.end());
}
std::reverse(result.begin(), result.end());
return result;
}
struct RequireCycle
{
Location location;
std::vector<ModuleName> path; // one of the paths for a require() to go all the way back to the originating module
};
// Given a source node (start), find all requires that start a transitive dependency path that ends back at start
// For each such path, record the full path and the location of the require in the starting module.
// Note that this is O(V^2) for a fully connected graph and produces O(V) paths of length O(V)
// However, when the graph is acyclic, this is O(V), as well as when only the first cycle is needed (stopAtFirst=true)
std::vector<RequireCycle> getRequireCycles(
const std::unordered_map<ModuleName, SourceNode>& sourceNodes, const SourceNode* start, bool stopAtFirst = false)
{
std::vector<RequireCycle> result;
DenseHashSet<const SourceNode*> seen(nullptr);
std::vector<const SourceNode*> stack;
std::vector<const SourceNode*> path;
for (const auto& [depName, depLocation] : start->requireLocations)
{
std::vector<ModuleName> cycle;
auto dit = sourceNodes.find(depName);
if (dit == sourceNodes.end())
continue;
stack.push_back(&dit->second);
while (!stack.empty())
{
const SourceNode* top = stack.back();
stack.pop_back();
if (top == nullptr)
{
// special marker for post-order processing
LUAU_ASSERT(!path.empty());
top = path.back();
path.pop_back();
// we reached the node! path must form a cycle now
if (top == start)
{
for (const SourceNode* node : path)
cycle.push_back(node->name);
cycle.push_back(top->name);
break;
}
}
else if (!seen.contains(top))
{
seen.insert(top);
// push marker for post-order processing
path.push_back(top);
stack.push_back(nullptr);
// note: we push require edges in the opposite order
// because it's a stack, the last edge to be pushed gets processed first
// this ensures that the cyclic path we report is the first one in DFS order
for (size_t i = top->requireLocations.size(); i > 0; --i)
{
const ModuleName& reqName = top->requireLocations[i - 1].first;
auto rit = sourceNodes.find(reqName);
if (rit != sourceNodes.end())
stack.push_back(&rit->second);
}
}
}
path.clear();
stack.clear();
if (!cycle.empty())
{
result.push_back({depLocation, std::move(cycle)});
if (stopAtFirst)
return result;
// note: if we didn't find a cycle, all nodes that we've seen don't depend [transitively] on start
// so it's safe to *only* clear seen vector when we find a cycle
// if we don't do it, we will not have correct reporting for some cycles
seen.clear();
}
}
return result;
}
double getTimestamp()
{
using namespace std::chrono;
return double(duration_cast<nanoseconds>(high_resolution_clock::now().time_since_epoch()).count()) / 1e9;
}
} // namespace
Frontend::Frontend(FileResolver* fileResolver, ConfigResolver* configResolver, const FrontendOptions& options)
: fileResolver(fileResolver)
, moduleResolver(this)
, moduleResolverForAutocomplete(this)
, typeChecker(&moduleResolver, &iceHandler)
, typeCheckerForAutocomplete(&moduleResolverForAutocomplete, &iceHandler)
, configResolver(configResolver)
, options(options)
{
}
FrontendModuleResolver::FrontendModuleResolver(Frontend* frontend)
: frontend(frontend)
{
}
CheckResult Frontend::check(const ModuleName& name)
{
CheckResult checkResult;
auto it = sourceNodes.find(name);
if (it != sourceNodes.end() && !it->second.dirty)
{
// No recheck required.
auto it2 = moduleResolver.modules.find(name);
if (it2 == moduleResolver.modules.end() || it2->second == nullptr)
throw std::runtime_error("Frontend::modules does not have data for " + name);
return CheckResult{accumulateErrors(sourceNodes, moduleResolver.modules, name)};
}
std::vector<ModuleName> buildQueue;
bool cycleDetected = parseGraph(buildQueue, checkResult, name);
// Keep track of which AST nodes we've reported cycles in
std::unordered_set<AstNode*> reportedCycles;
for (const ModuleName& moduleName : buildQueue)
{
LUAU_ASSERT(sourceNodes.count(moduleName));
SourceNode& sourceNode = sourceNodes[moduleName];
if (!sourceNode.dirty)
continue;
LUAU_ASSERT(sourceModules.count(moduleName));
SourceModule& sourceModule = sourceModules[moduleName];
const Config& config = configResolver->getConfig(moduleName);
Mode mode = sourceModule.mode.value_or(config.mode);
ScopePtr environmentScope = getModuleEnvironment(sourceModule, config);
double timestamp = getTimestamp();
std::vector<RequireCycle> requireCycles;
// in NoCheck mode we only need to compute the value of .cyclic for typeck
// in the future we could replace toposort with an algorithm that can flag cyclic nodes by itself
// however, for now getRequireCycles isn't expensive in practice on the cases we care about, and long term
// all correct programs must be acyclic so this code triggers rarely
if (cycleDetected)
requireCycles = getRequireCycles(sourceNodes, &sourceNode, mode == Mode::NoCheck);
// This is used by the type checker to replace the resulting type of cyclic modules with any
sourceModule.cyclic = !requireCycles.empty();
ModulePtr module = typeChecker.check(sourceModule, mode, environmentScope);
// If we're typechecking twice, we do so.
// The second typecheck is always in strict mode with DM awareness
// to provide better typen information for IDE features.
if (options.typecheckTwice && FFlag::LuauSecondTypecheckKnowsTheDataModel)
{
ModulePtr moduleForAutocomplete = typeCheckerForAutocomplete.check(sourceModule, Mode::Strict);
moduleResolverForAutocomplete.modules[moduleName] = moduleForAutocomplete;
}
else if (options.retainFullTypeGraphs && options.typecheckTwice && mode != Mode::Strict)
{
ModulePtr strictModule = typeChecker.check(sourceModule, Mode::Strict, environmentScope);
module->astTypes.clear();
module->astOriginalCallTypes.clear();
module->astExpectedTypes.clear();
SeenTypes seenTypes;
SeenTypePacks seenTypePacks;
for (const auto& [expr, strictTy] : strictModule->astTypes)
module->astTypes[expr] = clone(strictTy, module->interfaceTypes, seenTypes, seenTypePacks);
for (const auto& [expr, strictTy] : strictModule->astOriginalCallTypes)
module->astOriginalCallTypes[expr] = clone(strictTy, module->interfaceTypes, seenTypes, seenTypePacks);
for (const auto& [expr, strictTy] : strictModule->astExpectedTypes)
module->astExpectedTypes[expr] = clone(strictTy, module->interfaceTypes, seenTypes, seenTypePacks);
}
stats.timeCheck += getTimestamp() - timestamp;
stats.filesStrict += mode == Mode::Strict;
stats.filesNonstrict += mode == Mode::Nonstrict;
if (module == nullptr)
throw std::runtime_error("Frontend::check produced a nullptr module for " + moduleName);
if (!options.retainFullTypeGraphs)
{
// copyErrors needs to allocate into interfaceTypes as it copies
// types out of internalTypes, so we unfreeze it here.
unfreeze(module->interfaceTypes);
copyErrors(module->errors, module->interfaceTypes);
freeze(module->interfaceTypes);
module->internalTypes.clear();
module->astTypes.clear();
module->astExpectedTypes.clear();
module->astOriginalCallTypes.clear();
}
if (mode != Mode::NoCheck)
{
for (const RequireCycle& cyc : requireCycles)
{
TypeError te{cyc.location, moduleName, ModuleHasCyclicDependency{cyc.path}};
module->errors.push_back(te);
}
}
ErrorVec parseErrors;
for (const ParseError& pe : sourceModule.parseErrors)
parseErrors.push_back(TypeError{pe.getLocation(), moduleName, SyntaxError{pe.what()}});
module->errors.insert(module->errors.begin(), parseErrors.begin(), parseErrors.end());
checkResult.errors.insert(checkResult.errors.end(), module->errors.begin(), module->errors.end());
moduleResolver.modules[moduleName] = std::move(module);
sourceNode.dirty = false;
}
return checkResult;
}
bool Frontend::parseGraph(std::vector<ModuleName>& buildQueue, CheckResult& checkResult, const ModuleName& root)
{
// https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
enum Mark
{
None,
Temporary,
Permanent
};
DenseHashMap<SourceNode*, Mark> seen(nullptr);
std::vector<SourceNode*> stack;
std::vector<SourceNode*> path;
bool cyclic = false;
{
auto [sourceNode, _] = getSourceNode(checkResult, root);
if (sourceNode)
stack.push_back(sourceNode);
}
while (!stack.empty())
{
SourceNode* top = stack.back();
stack.pop_back();
if (top == nullptr)
{
// special marker for post-order processing
LUAU_ASSERT(!path.empty());
top = path.back();
path.pop_back();
// note: topseen ref gets invalidated in any seen[] access, beware - only one seen[] access per iteration!
Mark& topseen = seen[top];
LUAU_ASSERT(topseen == Temporary);
topseen = Permanent;
buildQueue.push_back(top->name);
}
else
{
// note: topseen ref gets invalidated in any seen[] access, beware - only one seen[] access per iteration!
Mark& topseen = seen[top];
if (topseen != None)
{
cyclic |= topseen == Temporary;
continue;
}
topseen = Temporary;
// push marker for post-order processing
stack.push_back(nullptr);
path.push_back(top);
// push children
for (const ModuleName& dep : top->requires)
{
auto it = sourceNodes.find(dep);
if (it != sourceNodes.end())
{
// this is a critical optimization: we do *not* traverse non-dirty subtrees.
// this relies on the fact that markDirty marks reverse-dependencies dirty as well
// thus if a node is not dirty, all its transitive deps aren't dirty, which means that they won't ever need
// to be built, *and* can't form a cycle with any nodes we did process.
if (!it->second.dirty)
continue;
// note: this check is technically redundant *except* that getSourceNode has somewhat broken memoization
// calling getSourceNode twice in succession will reparse the file, since getSourceNode leaves dirty flag set
if (seen.contains(&it->second))
{
stack.push_back(&it->second);
continue;
}
}
auto [sourceNode, _] = getSourceNode(checkResult, dep);
if (sourceNode)
{
stack.push_back(sourceNode);
// note: this assignment is paired with .contains() check above and effectively deduplicates getSourceNode()
seen[sourceNode] = None;
}
}
}
}
return cyclic;
}
ScopePtr Frontend::getModuleEnvironment(const SourceModule& module, const Config& config)
{
ScopePtr result = typeChecker.globalScope;
if (module.environmentName)
result = getEnvironmentScope(*module.environmentName);
if (!config.globals.empty())
{
result = std::make_shared<Scope>(result);
for (const std::string& global : config.globals)
{
AstName name = module.names->get(global.c_str());
if (name.value)
result->bindings[name].typeId = typeChecker.anyType;
}
}
return result;
}
LintResult Frontend::lint(const ModuleName& name, std::optional<Luau::LintOptions> enabledLintWarnings)
{
CheckResult checkResult;
auto [_sourceNode, sourceModule] = getSourceNode(checkResult, name);
if (!sourceModule)
return LintResult{}; // FIXME: We really should do something a bit more obvious when a file is too broken to lint.
return lint(*sourceModule, enabledLintWarnings);
}
std::pair<SourceModule, LintResult> Frontend::lintFragment(std::string_view source, std::optional<Luau::LintOptions> enabledLintWarnings)
{
const Config& config = configResolver->getConfig("");
SourceModule sourceModule = parse(ModuleName{}, source, config.parseOptions);
Luau::LintOptions lintOptions = enabledLintWarnings.value_or(config.enabledLint);
lintOptions.warningMask &= sourceModule.ignoreLints;
double timestamp = getTimestamp();
std::vector<LintWarning> warnings =
Luau::lint(sourceModule.root, *sourceModule.names.get(), typeChecker.globalScope, nullptr, enabledLintWarnings.value_or(config.enabledLint));
stats.timeLint += getTimestamp() - timestamp;
return {std::move(sourceModule), classifyLints(warnings, config)};
}
CheckResult Frontend::check(const SourceModule& module)
{
const Config& config = configResolver->getConfig(module.name);
Mode mode = module.mode.value_or(config.mode);
double timestamp = getTimestamp();
ModulePtr checkedModule = typeChecker.check(module, mode);
stats.timeCheck += getTimestamp() - timestamp;
stats.filesStrict += mode == Mode::Strict;
stats.filesNonstrict += mode == Mode::Nonstrict;
if (checkedModule == nullptr)
throw std::runtime_error("Frontend::check produced a nullptr module for module " + module.name);
moduleResolver.modules[module.name] = checkedModule;
return CheckResult{checkedModule->errors};
}
LintResult Frontend::lint(const SourceModule& module, std::optional<Luau::LintOptions> enabledLintWarnings)
{
const Config& config = configResolver->getConfig(module.name);
LintOptions options = enabledLintWarnings.value_or(config.enabledLint);
options.warningMask &= ~module.ignoreLints;
Mode mode = module.mode.value_or(config.mode);
if (mode != Mode::NoCheck)
{
options.disableWarning(Luau::LintWarning::Code_UnknownGlobal);
}
if (mode == Mode::Strict)
{
options.disableWarning(Luau::LintWarning::Code_ImplicitReturn);
}
ScopePtr environmentScope = getModuleEnvironment(module, config);
ModulePtr modulePtr = moduleResolver.getModule(module.name);
double timestamp = getTimestamp();
std::vector<LintWarning> warnings = Luau::lint(module.root, *module.names, environmentScope, modulePtr.get(), options);
stats.timeLint += getTimestamp() - timestamp;
return classifyLints(warnings, config);
}
bool Frontend::isDirty(const ModuleName& name) const
{
auto it = sourceNodes.find(name);
return it == sourceNodes.end() || it->second.dirty;
}
/*
* Mark a file as requiring rechecking before its type information can be safely used again.
*
* I am not particularly pleased with the way each dirty() operation involves a BFS on reverse dependencies.
* It would be nice for this function to be O(1)
*/
void Frontend::markDirty(const ModuleName& name, std::vector<ModuleName>* markedDirty)
{
if (!moduleResolver.modules.count(name))
return;
std::unordered_map<ModuleName, std::vector<ModuleName>> reverseDeps;
for (const auto& module : sourceNodes)
{
for (const auto& dep : module.second.requires)
reverseDeps[dep].push_back(module.first);
}
std::vector<ModuleName> queue{name};
while (!queue.empty())
{
ModuleName next = std::move(queue.back());
queue.pop_back();
LUAU_ASSERT(sourceNodes.count(next) > 0);
SourceNode& sourceNode = sourceNodes[next];
if (markedDirty)
markedDirty->push_back(next);
if (sourceNode.dirty)
continue;
sourceNode.dirty = true;
if (0 == reverseDeps.count(name))
continue;
sourceModules.erase(name);
const std::vector<ModuleName>& dependents = reverseDeps[name];
queue.insert(queue.end(), dependents.begin(), dependents.end());
}
}
SourceModule* Frontend::getSourceModule(const ModuleName& moduleName)
{
auto it = sourceModules.find(moduleName);
if (it != sourceModules.end())
return &it->second;
else
return nullptr;
}
const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) const
{
return const_cast<Frontend*>(this)->getSourceModule(moduleName);
}
// Read AST into sourceModules if necessary. Trace require()s. Report parse errors.
std::pair<SourceNode*, SourceModule*> Frontend::getSourceNode(CheckResult& checkResult, const ModuleName& name)
{
auto it = sourceNodes.find(name);
if (it != sourceNodes.end() && !it->second.dirty)
{
auto moduleIt = sourceModules.find(name);
if (moduleIt != sourceModules.end())
return {&it->second, &moduleIt->second};
else
{
LUAU_ASSERT(!"Everything in sourceNodes should also be in sourceModules");
return {&it->second, nullptr};
}
}
double timestamp = getTimestamp();
std::optional<SourceCode> source = fileResolver->readSource(name);
std::optional<std::string> environmentName = fileResolver->getEnvironmentForModule(name);
stats.timeRead += getTimestamp() - timestamp;
if (!source)
{
sourceModules.erase(name);
return {nullptr, nullptr};
}
const Config& config = configResolver->getConfig(name);
ParseOptions opts = config.parseOptions;
opts.captureComments = true;
SourceModule result = parse(name, source->source, opts);
result.type = source->type;
RequireTraceResult& requireTrace = requires[name];
requireTrace = traceRequires(fileResolver, result.root, name);
SourceNode& sourceNode = sourceNodes[name];
SourceModule& sourceModule = sourceModules[name];
sourceModule = std::move(result);
sourceModule.environmentName = environmentName;
sourceNode.name = name;
sourceNode.requires.clear();
sourceNode.requireLocations.clear();
sourceNode.dirty = true;
for (const auto& [moduleName, location] : requireTrace.requires)
sourceNode.requires.insert(moduleName);
sourceNode.requireLocations = requireTrace.requires;
return {&sourceNode, &sourceModule};
}
/** Try to parse a source file into a SourceModule.
*
* The logic here is a little bit more complicated than we'd like it to be.
*
* If a file does not exist, we return none to prevent the Frontend from creating knowledge that this module exists.
* If the Frontend thinks that the file exists, it will not produce an "Unknown require" error.
*
* If the file has syntax errors, we report them and synthesize an empty AST if it's not available.
* This suppresses the Unknown require error and allows us to make a best effort to typecheck code that require()s
* something that has broken syntax.
* We also translate Luau::ParseError into a Luau::TypeError so that we can use a vector<TypeError> to describe the
* result of the check()
*/
SourceModule Frontend::parse(const ModuleName& name, std::string_view src, const ParseOptions& parseOptions)
{
SourceModule sourceModule;
double timestamp = getTimestamp();
auto parseResult = Luau::Parser::parse(src.data(), src.size(), *sourceModule.names, *sourceModule.allocator, parseOptions);
stats.timeParse += getTimestamp() - timestamp;