From 362428f8b4b6f5c9d43f4daf55bcf7873f536c3f Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 17 Mar 2022 17:46:04 -0700 Subject: [PATCH 1/9] Sync to upstream/release/519 (#422) --- Analysis/include/Luau/Error.h | 1 + Analysis/include/Luau/Unifier.h | 2 + Analysis/src/Autocomplete.cpp | 198 +- Analysis/src/EmbeddedBuiltinDefinitions.cpp | 144 +- Analysis/src/Error.cpp | 11 +- Analysis/src/Module.cpp | 7 + Analysis/src/TypeInfer.cpp | 127 +- Analysis/src/TypeVar.cpp | 10 +- Analysis/src/Unifier.cpp | 78 +- Ast/src/Parser.cpp | 117 +- Compiler/src/Builtins.cpp | 4 +- Compiler/src/Compiler.cpp | 4 - Sources.cmake | 7 + VM/include/lua.h | 15 +- VM/src/lapi.cpp | 20 +- VM/src/lbaselib.cpp | 16 +- VM/src/lgc.h | 4 +- VM/src/lmem.cpp | 2 +- VM/src/lstring.cpp | 2 +- VM/src/ltable.cpp | 71 +- VM/src/ltm.cpp | 3 +- VM/src/ludata.cpp | 8 +- VM/src/ludata.h | 3 + VM/src/lvmexecute.cpp | 2 +- tests/Autocomplete.test.cpp | 162 + tests/Compiler.test.cpp | 2 - tests/Conformance.test.cpp | 142 +- tests/Linter.test.cpp | 2 - tests/Module.test.cpp | 10 +- tests/NonstrictMode.test.cpp | 22 +- tests/TypeInfer.aliases.test.cpp | 21 + tests/TypeInfer.anyerror.test.cpp | 335 ++ tests/TypeInfer.builtins.test.cpp | 60 + tests/TypeInfer.definitions.test.cpp | 18 + tests/TypeInfer.functions.test.cpp | 1338 +++++ tests/TypeInfer.generics.test.cpp | 301 ++ tests/TypeInfer.intersectionTypes.test.cpp | 28 + tests/TypeInfer.loops.test.cpp | 473 ++ tests/TypeInfer.modules.test.cpp | 310 ++ tests/TypeInfer.oop.test.cpp | 275 + tests/TypeInfer.operators.test.cpp | 759 +++ tests/TypeInfer.primitives.test.cpp | 100 + tests/TypeInfer.refinements.test.cpp | 18 + tests/TypeInfer.singletons.test.cpp | 116 +- tests/TypeInfer.tables.test.cpp | 500 ++ tests/TypeInfer.test.cpp | 4415 +---------------- tests/TypeInfer.typePacks.cpp | 83 + tests/TypeInfer.unionTypes.test.cpp | 16 + tests/conformance/basic.lua | 19 + tests/conformance/debugger.lua | 4 +- tests/conformance/errors.lua | 2 + tests/conformance/interrupt.lua | 11 + tools/{gdb-printers.py => gdb_printers.py} | 0 tools/lldb-formatters.lldb | 2 - tools/lldb_formatters.lldb | 2 + .../{LuauVisualize.py => lldb_formatters.py} | 0 56 files changed, 5640 insertions(+), 4762 deletions(-) create mode 100644 tests/TypeInfer.anyerror.test.cpp create mode 100644 tests/TypeInfer.functions.test.cpp create mode 100644 tests/TypeInfer.loops.test.cpp create mode 100644 tests/TypeInfer.modules.test.cpp create mode 100644 tests/TypeInfer.oop.test.cpp create mode 100644 tests/TypeInfer.operators.test.cpp create mode 100644 tests/TypeInfer.primitives.test.cpp create mode 100644 tests/conformance/interrupt.lua rename tools/{gdb-printers.py => gdb_printers.py} (100%) delete mode 100644 tools/lldb-formatters.lldb create mode 100644 tools/lldb_formatters.lldb rename tools/{LuauVisualize.py => lldb_formatters.py} (100%) diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index a71e022..7235025 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -107,6 +107,7 @@ struct FunctionDoesNotTakeSelf struct FunctionRequiresSelf { + // TODO: Delete with LuauAnyInIsOptionalIsOptional int requiredExtraNils = 0; bool operator==(const FunctionRequiresSelf& rhs) const; diff --git a/Analysis/include/Luau/Unifier.h b/Analysis/include/Luau/Unifier.h index 71958f4..f1ffbcc 100644 --- a/Analysis/include/Luau/Unifier.h +++ b/Analysis/include/Luau/Unifier.h @@ -86,6 +86,8 @@ private: void tryUnifyIndexer(const TableIndexer& subIndexer, const TableIndexer& superIndexer); TypeId widen(TypeId ty); + TypePackId widen(TypePackId tp); + TypeId deeplyOptional(TypeId ty, std::unordered_map seen = {}); void cacheResult(TypeId subTy, TypeId superTy); diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index e94c432..492edf2 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -14,6 +14,7 @@ #include LUAU_FASTFLAGVARIABLE(LuauIfElseExprFixCompletionIssue, false); +LUAU_FASTFLAG(LuauSelfCallAutocompleteFix) static const std::unordered_set kStatementStartingKeywords = { "while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; @@ -228,11 +229,22 @@ static std::optional findExpectedTypeAt(const Module& module, AstNode* n return *it; } +static bool checkTypeMatch(TypeArena* typeArena, TypeId subTy, TypeId superTy) +{ + InternalErrorReporter iceReporter; + UnifierSharedState unifierState(&iceReporter); + Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState); + + return unifier.canUnify(subTy, superTy).empty(); +} + static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typeArena, AstNode* node, Position position, TypeId ty) { ty = follow(ty); auto canUnify = [&typeArena](TypeId subTy, TypeId superTy) { + LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix); + InternalErrorReporter iceReporter; UnifierSharedState unifierState(&iceReporter); Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState); @@ -249,20 +261,30 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ TypeId expectedType = follow(*typeAtPosition); - auto checkFunctionType = [&canUnify, &expectedType](const FunctionTypeVar* ftv) { - auto [retHead, retTail] = flatten(ftv->retType); - - if (!retHead.empty() && canUnify(retHead.front(), expectedType)) - return true; - - // We might only have a variadic tail pack, check if the element is compatible - if (retTail) + auto checkFunctionType = [typeArena, &canUnify, &expectedType](const FunctionTypeVar* ftv) { + if (FFlag::LuauSelfCallAutocompleteFix) { - if (const VariadicTypePack* vtp = get(follow(*retTail)); vtp && canUnify(vtp->ty, expectedType)) - return true; - } + if (std::optional firstRetTy = first(ftv->retType)) + return checkTypeMatch(typeArena, *firstRetTy, expectedType); - return false; + return false; + } + else + { + auto [retHead, retTail] = flatten(ftv->retType); + + if (!retHead.empty() && canUnify(retHead.front(), expectedType)) + return true; + + // We might only have a variadic tail pack, check if the element is compatible + if (retTail) + { + if (const VariadicTypePack* vtp = get(follow(*retTail)); vtp && canUnify(vtp->ty, expectedType)) + return true; + } + + return false; + } }; // We also want to suggest functions that return compatible result @@ -281,7 +303,10 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ } } - return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None; + if (FFlag::LuauSelfCallAutocompleteFix) + return checkTypeMatch(typeArena, ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None; + else + return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None; } enum class PropIndexType @@ -291,16 +316,22 @@ enum class PropIndexType Key, }; -static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId ty, PropIndexType indexType, const std::vector& nodes, - AutocompleteEntryMap& result, std::unordered_set& seen, std::optional containingClass = std::nullopt) +static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId rootTy, TypeId ty, PropIndexType indexType, + const std::vector& nodes, AutocompleteEntryMap& result, std::unordered_set& seen, + std::optional containingClass = std::nullopt) { + if (FFlag::LuauSelfCallAutocompleteFix) + rootTy = follow(rootTy); + ty = follow(ty); if (seen.count(ty)) return; seen.insert(ty); - auto isWrongIndexer = [indexType, useStrictFunctionIndexers = !!get(ty)](Luau::TypeId type) { + auto isWrongIndexer_DEPRECATED = [indexType, useStrictFunctionIndexers = !!get(ty)](Luau::TypeId type) { + LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix); + if (indexType == PropIndexType::Key) return false; @@ -331,6 +362,48 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId return colonIndex; } }; + auto isWrongIndexer = [typeArena, rootTy, indexType](Luau::TypeId type) { + LUAU_ASSERT(FFlag::LuauSelfCallAutocompleteFix); + + if (indexType == PropIndexType::Key) + return false; + + bool calledWithSelf = indexType == PropIndexType::Colon; + + auto isCompatibleCall = [typeArena, rootTy, calledWithSelf](const FunctionTypeVar* ftv) { + if (get(rootTy)) + { + // Calls on classes require strict match between how function is declared and how it's called + return calledWithSelf == ftv->hasSelf; + } + + if (std::optional firstArgTy = first(ftv->argTypes)) + { + if (checkTypeMatch(typeArena, rootTy, *firstArgTy)) + return calledWithSelf; + } + + return !calledWithSelf; + }; + + if (const FunctionTypeVar* ftv = get(type)) + return !isCompatibleCall(ftv); + + // For intersections, any part that is successful makes the whole call successful + if (const IntersectionTypeVar* itv = get(type)) + { + for (auto subType : itv->parts) + { + if (const FunctionTypeVar* ftv = get(Luau::follow(subType))) + { + if (isCompatibleCall(ftv)) + return false; + } + } + } + + return calledWithSelf; + }; auto fillProps = [&](const ClassTypeVar::Props& props) { for (const auto& [name, prop] : props) @@ -349,7 +422,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId AutocompleteEntryKind::Property, type, prop.deprecated, - isWrongIndexer(type), + FFlag::LuauSelfCallAutocompleteFix ? isWrongIndexer(type) : isWrongIndexer_DEPRECATED(type), typeCorrect, containingClass, &prop, @@ -361,34 +434,60 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId } }; - if (auto cls = get(ty)) - { - containingClass = containingClass.value_or(cls); - fillProps(cls->props); - if (cls->parent) - autocompleteProps(module, typeArena, *cls->parent, indexType, nodes, result, seen, cls); - } - else if (auto tbl = get(ty)) - fillProps(tbl->props); - else if (auto mt = get(ty)) - { - autocompleteProps(module, typeArena, mt->table, indexType, nodes, result, seen); - - auto mtable = get(mt->metatable); - if (!mtable) - return; - + auto fillMetatableProps = [&](const TableTypeVar* mtable) { auto indexIt = mtable->props.find("__index"); if (indexIt != mtable->props.end()) { TypeId followed = follow(indexIt->second.type); if (get(followed) || get(followed)) - autocompleteProps(module, typeArena, followed, indexType, nodes, result, seen); + { + autocompleteProps(module, typeArena, rootTy, followed, indexType, nodes, result, seen); + } else if (auto indexFunction = get(followed)) { std::optional indexFunctionResult = first(indexFunction->retType); if (indexFunctionResult) - autocompleteProps(module, typeArena, *indexFunctionResult, indexType, nodes, result, seen); + autocompleteProps(module, typeArena, rootTy, *indexFunctionResult, indexType, nodes, result, seen); + } + } + }; + + if (auto cls = get(ty)) + { + containingClass = containingClass.value_or(cls); + fillProps(cls->props); + if (cls->parent) + autocompleteProps(module, typeArena, rootTy, *cls->parent, indexType, nodes, result, seen, cls); + } + else if (auto tbl = get(ty)) + fillProps(tbl->props); + else if (auto mt = get(ty)) + { + autocompleteProps(module, typeArena, rootTy, mt->table, indexType, nodes, result, seen); + + if (FFlag::LuauSelfCallAutocompleteFix) + { + if (auto mtable = get(mt->metatable)) + fillMetatableProps(mtable); + } + else + { + auto mtable = get(mt->metatable); + if (!mtable) + return; + + auto indexIt = mtable->props.find("__index"); + if (indexIt != mtable->props.end()) + { + TypeId followed = follow(indexIt->second.type); + if (get(followed) || get(followed)) + autocompleteProps(module, typeArena, rootTy, followed, indexType, nodes, result, seen); + else if (auto indexFunction = get(followed)) + { + std::optional indexFunctionResult = first(indexFunction->retType); + if (indexFunctionResult) + autocompleteProps(module, typeArena, rootTy, *indexFunctionResult, indexType, nodes, result, seen); + } } } } @@ -400,7 +499,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId AutocompleteEntryMap inner; std::unordered_set innerSeen = seen; - autocompleteProps(module, typeArena, ty, indexType, nodes, inner, innerSeen); + autocompleteProps(module, typeArena, rootTy, ty, indexType, nodes, inner, innerSeen); for (auto& pair : inner) result.insert(pair); @@ -423,14 +522,17 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId if (iter == endIter) return; - autocompleteProps(module, typeArena, *iter, indexType, nodes, result, seen); + autocompleteProps(module, typeArena, rootTy, *iter, indexType, nodes, result, seen); ++iter; while (iter != endIter) { AutocompleteEntryMap inner; - std::unordered_set innerSeen = seen; + std::unordered_set innerSeen; + + if (!FFlag::LuauSelfCallAutocompleteFix) + innerSeen = seen; if (isNil(*iter)) { @@ -438,7 +540,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId continue; } - autocompleteProps(module, typeArena, *iter, indexType, nodes, inner, innerSeen); + autocompleteProps(module, typeArena, rootTy, *iter, indexType, nodes, inner, innerSeen); std::unordered_set toRemove; @@ -455,6 +557,18 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId ++iter; } } + else if (auto pt = get(ty); pt && FFlag::LuauSelfCallAutocompleteFix) + { + if (pt->metatable) + { + if (auto mtable = get(*pt->metatable)) + fillMetatableProps(mtable); + } + } + else if (FFlag::LuauSelfCallAutocompleteFix && get(get(ty))) + { + autocompleteProps(module, typeArena, rootTy, getSingletonTypes().stringType, indexType, nodes, result, seen); + } } static void autocompleteKeywords( @@ -482,7 +596,7 @@ static void autocompleteProps( const Module& module, TypeArena* typeArena, TypeId ty, PropIndexType indexType, const std::vector& nodes, AutocompleteEntryMap& result) { std::unordered_set seen; - autocompleteProps(module, typeArena, ty, indexType, nodes, result, seen); + autocompleteProps(module, typeArena, ty, ty, indexType, nodes, result, seen); } AutocompleteEntryMap autocompleteProps( @@ -1352,7 +1466,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M TypeId ty = follow(*it); PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point; - if (isString(ty)) + if (!FFlag::LuauSelfCallAutocompleteFix && isString(ty)) return {autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, finder.ancestry), finder.ancestry}; else diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index 471b61a..be3fcd7 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -95,104 +95,104 @@ declare os: { declare function require(target: any): any -declare function getfenv(target: any?): { [string]: any } +declare function getfenv(target: any): { [string]: any } declare _G: any declare _VERSION: string declare function gcinfo(): number - declare function print(...: T...) +declare function print(...: T...) - declare function type(value: T): string - declare function typeof(value: T): string - - -- `assert` has a magic function attached that will give more detailed type information - declare function assert(value: T, errorMessage: string?): T +declare function type(value: T): string +declare function typeof(value: T): string - declare function error(message: T, level: number?) +-- `assert` has a magic function attached that will give more detailed type information +declare function assert(value: T, errorMessage: string?): T - declare function tostring(value: T): string - declare function tonumber(value: T, radix: number?): number? +declare function error(message: T, level: number?) - declare function rawequal(a: T1, b: T2): boolean - declare function rawget(tab: {[K]: V}, k: K): V - declare function rawset(tab: {[K]: V}, k: K, v: V): {[K]: V} +declare function tostring(value: T): string +declare function tonumber(value: T, radix: number?): number? - declare function setfenv(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)? +declare function rawequal(a: T1, b: T2): boolean +declare function rawget(tab: {[K]: V}, k: K): V +declare function rawset(tab: {[K]: V}, k: K, v: V): {[K]: V} - declare function ipairs(tab: {V}): (({V}, number) -> (number, V), {V}, number) +declare function setfenv(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)? - declare function pcall(f: (A...) -> R..., ...: A...): (boolean, R...) +declare function ipairs(tab: {V}): (({V}, number) -> (number, V), {V}, number) - -- FIXME: The actual type of `xpcall` is: - -- (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(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...) +declare function pcall(f: (A...) -> R..., ...: A...): (boolean, R...) - -- `select` has a magic function attached to provide more detailed type information - declare function select(i: string | number, ...: A...): ...any +-- FIXME: The actual type of `xpcall` is: +-- (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(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...) - -- FIXME: This type is not entirely correct - `loadstring` returns a function or - -- (nil, string). - declare function loadstring(src: string, chunkname: string?): (((A...) -> any)?, string?) +-- `select` has a magic function attached to provide more detailed type information +declare function select(i: string | number, ...: A...): ...any - declare function newproxy(mt: boolean?): any +-- FIXME: This type is not entirely correct - `loadstring` returns a function or +-- (nil, string). +declare function loadstring(src: string, chunkname: string?): (((A...) -> any)?, string?) - declare coroutine: { - create: ((A...) -> R...) -> thread, - resume: (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...) -> any, - yield: (A...) -> R..., - isyieldable: () -> boolean, - close: (thread) -> (boolean, any?) - } +declare function newproxy(mt: boolean?): any - declare table: { - concat: ({V}, string?, number?, number?) -> string, - insert: (({V}, V) -> ()) & (({V}, number, V) -> ()), - maxn: ({V}) -> number, - remove: ({V}, number?) -> V?, - sort: ({V}, ((V, V) -> boolean)?) -> (), - create: (number, V?) -> {V}, - find: ({V}, V, number?) -> number?, +declare coroutine: { + create: ((A...) -> R...) -> thread, + resume: (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...) -> any, + yield: (A...) -> R..., + isyieldable: () -> boolean, + close: (thread) -> (boolean, any) +} - unpack: ({V}, number?, number?) -> ...V, - pack: (...V) -> { n: number, [number]: V }, +declare table: { + concat: ({V}, string?, number?, number?) -> string, + insert: (({V}, V) -> ()) & (({V}, number, V) -> ()), + maxn: ({V}) -> number, + remove: ({V}, number?) -> V?, + sort: ({V}, ((V, V) -> boolean)?) -> (), + create: (number, V?) -> {V}, + find: ({V}, V, number?) -> number?, - getn: ({V}) -> number, - foreach: ({[K]: V}, (K, V) -> ()) -> (), - foreachi: ({V}, (number, V) -> ()) -> (), + unpack: ({V}, number?, number?) -> ...V, + pack: (...V) -> { n: number, [number]: V }, - move: ({V}, number, number, number, {V}?) -> {V}, - clear: ({[K]: V}) -> (), + getn: ({V}) -> number, + foreach: ({[K]: V}, (K, V) -> ()) -> (), + foreachi: ({V}, (number, V) -> ()) -> (), - isfrozen: ({[K]: V}) -> boolean, - } + move: ({V}, number, number, number, {V}?) -> {V}, + clear: ({[K]: V}) -> (), - declare debug: { - info: ((thread, number, string) -> R...) & ((number, string) -> R...) & (((A...) -> R1..., string) -> R2...), - traceback: ((string?, number?) -> string) & ((thread, string?, number?) -> string), - } + isfrozen: ({[K]: V}) -> boolean, +} - 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 debug: { + info: ((thread, number, string) -> R...) & ((number, string) -> R...) & (((A...) -> R1..., string) -> R2...), + traceback: ((string?, number?) -> string) & ((thread, string?, number?) -> string), +} - -- Cannot use `typeof` here because it will produce a polytype when we expect a monotype. - declare function unpack(tab: {V}, i: number?, j: number?): ...V +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)), +} + +-- Cannot use `typeof` here because it will produce a polytype when we expect a monotype. +declare function unpack(tab: {V}, i: number?, j: number?): ...V )BUILTIN_SRC"; diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 88069f1..26d3b76 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -7,6 +7,8 @@ #include +LUAU_FASTFLAGVARIABLE(BetterDiagnosticCodesInStudio, false); + static std::string wrongNumberOfArgsString(size_t expectedCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false) { std::string s = "expects "; @@ -223,7 +225,14 @@ struct ErrorConverter std::string operator()(const Luau::SyntaxError& e) const { - return "Syntax error: " + e.message; + if (FFlag::BetterDiagnosticCodesInStudio) + { + return e.message; + } + else + { + return "Syntax error: " + e.message; + } } std::string operator()(const Luau::CodeTooComplex&) const diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 76dc72d..a330a98 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -14,6 +14,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false) LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false) // Remove with FFlagLuauImmutableTypes LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) +LUAU_FASTFLAGVARIABLE(LuauCloneDeclaredGlobals, false) LUAU_FASTFLAG(LuauImmutableTypes) namespace Luau @@ -536,6 +537,12 @@ bool Module::clonePublicInterface() if (get(follow(ty))) *asMutable(ty) = AnyTypeVar{}; + if (FFlag::LuauCloneDeclaredGlobals) + { + for (auto& [name, ty] : declaredGlobals) + ty = clone(ty, interfaceTypes, seenTypes, seenTypePacks, cloneState); + } + freeze(internalTypes); freeze(interfaceTypes); diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 3fe4c90..41e8ce5 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -29,22 +29,24 @@ LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false) LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false) LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false) LUAU_FASTFLAGVARIABLE(LuauSealExports, false) +LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix, false) LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions2, false) LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false) LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false) LUAU_FASTFLAGVARIABLE(LuauOnlyMutateInstantiatedTables, false) LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false) -LUAU_FASTFLAGVARIABLE(LuauStatFunctionSimplify, false) +LUAU_FASTFLAGVARIABLE(LuauStatFunctionSimplify2, false) LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false) LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false) LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false) LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. -LUAU_FASTFLAG(LuauWidenIfSupertypeIsFree) +LUAU_FASTFLAG(LuauWidenIfSupertypeIsFree2) LUAU_FASTFLAGVARIABLE(LuauDoNotTryToReduce, false) LUAU_FASTFLAGVARIABLE(LuauDoNotAccidentallyDependOnPointerOrdering, false) LUAU_FASTFLAGVARIABLE(LuauFixArgumentCountMismatchAmountWithGenericTypes, false) LUAU_FASTFLAGVARIABLE(LuauFixIncorrectLineNumberDuplicateType, false) +LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional) namespace Luau { @@ -1099,7 +1101,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco scope->bindings[name->local] = {anyIfNonstrict(quantify(funScope, ty, name->local->location)), name->local->location}; return; } - else if (auto name = function.name->as(); name && FFlag::LuauStatFunctionSimplify) + else if (auto name = function.name->as(); name && FFlag::LuauStatFunctionSimplify2) { TypeId exprTy = checkExpr(scope, *name->expr).type; TableTypeVar* ttv = getMutableTableType(exprTy); @@ -1111,7 +1113,10 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco reportError(TypeError{function.location, OnlyTablesCanHaveMethods{exprTy}}); } else if (ttv->state == TableState::Sealed) - reportError(TypeError{function.location, CannotExtendTable{exprTy, CannotExtendTable::Property, name->index.value}}); + { + if (!ttv->indexer || !isPrim(ttv->indexer->indexType, PrimitiveTypeVar::String)) + reportError(TypeError{function.location, CannotExtendTable{exprTy, CannotExtendTable::Property, name->index.value}}); + } ty = follow(ty); @@ -1134,7 +1139,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco if (ttv && ttv->state != TableState::Sealed) ttv->props[name->index.value] = {follow(quantify(funScope, ty, name->indexLocation)), /* deprecated */ false, {}, name->indexLocation}; } - else if (FFlag::LuauStatFunctionSimplify) + else if (FFlag::LuauStatFunctionSimplify2) { LUAU_ASSERT(function.name->is()); @@ -1144,7 +1149,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco } else if (function.func->self) { - LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify); + LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify2); AstExprIndexName* indexName = function.name->as(); if (!indexName) @@ -1183,7 +1188,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco } else { - LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify); + LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify2); TypeId leftType = checkLValueBinding(scope, *function.name); @@ -1410,6 +1415,9 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& declar { ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}}); ftv->argTypes = addTypePack(TypePack{{classTy}, ftv->argTypes}); + + if (FFlag::LuauSelfCallAutocompleteFix) + ftv->hasSelf = true; } } @@ -1883,19 +1891,27 @@ std::optional TypeChecker::tryStripUnionFromNil(TypeId ty) { if (const UnionTypeVar* utv = get(ty)) { - bool hasNil = false; - - for (TypeId option : utv) + if (FFlag::LuauAnyInIsOptionalIsOptional) { - if (isNil(option)) - { - hasNil = true; - break; - } + if (!std::any_of(begin(utv), end(utv), isNil)) + return ty; } + else + { + bool hasNil = false; - if (!hasNil) - return ty; + for (TypeId option : utv) + { + if (isNil(option)) + { + hasNil = true; + break; + } + } + + if (!hasNil) + return ty; + } std::vector result; @@ -1916,14 +1932,34 @@ std::optional TypeChecker::tryStripUnionFromNil(TypeId ty) TypeId TypeChecker::stripFromNilAndReport(TypeId ty, const Location& location) { - if (isOptional(ty)) + if (FFlag::LuauAnyInIsOptionalIsOptional) { - if (std::optional strippedUnion = tryStripUnionFromNil(follow(ty))) + ty = follow(ty); + + if (auto utv = get(ty)) + { + if (!std::any_of(begin(utv), end(utv), isNil)) + return ty; + + } + + if (std::optional strippedUnion = tryStripUnionFromNil(ty)) { reportError(location, OptionalValueAccess{ty}); return follow(*strippedUnion); } } + else + { + if (isOptional(ty)) + { + if (std::optional strippedUnion = tryStripUnionFromNil(follow(ty))) + { + reportError(location, OptionalValueAccess{ty}); + return follow(*strippedUnion); + } + } + } return ty; } @@ -2935,9 +2971,25 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName, T return errorRecoveryType(scope); } - // Cannot extend sealed table, but we dont report an error here because it will be reported during AstStatFunction check - if (lhsType->persistent || ttv->state == TableState::Sealed) - return errorRecoveryType(scope); + if (FFlag::LuauStatFunctionSimplify2) + { + if (lhsType->persistent) + return errorRecoveryType(scope); + + // Cannot extend sealed table, but we dont report an error here because it will be reported during AstStatFunction check + if (ttv->state == TableState::Sealed) + { + if (ttv->indexer && isPrim(ttv->indexer->indexType, PrimitiveTypeVar::String)) + return ttv->indexer->indexResultType; + else + return errorRecoveryType(scope); + } + } + else + { + if (lhsType->persistent || ttv->state == TableState::Sealed) + return errorRecoveryType(scope); + } Name name = indexName->index.value; @@ -3393,7 +3445,7 @@ void TypeChecker::checkArgumentList( else if (state.log.getMutable(t)) { } // ok - else if (isNonstrictMode() && state.log.getMutable(t)) + else if (!FFlag::LuauAnyInIsOptionalIsOptional && isNonstrictMode() && state.log.getMutable(t)) { } // ok else @@ -3467,7 +3519,11 @@ void TypeChecker::checkArgumentList( } TypePackId varPack = addTypePack(TypePackVar{TypePack{rest, argIter.tail()}}); - state.tryUnify(varPack, tail); + if (FFlag::LuauWidenIfSupertypeIsFree2) + state.tryUnify(varPack, tail); + else + state.tryUnify(tail, varPack); + return; } else if (state.log.getMutable(tail)) @@ -3542,6 +3598,23 @@ ExprResult TypeChecker::checkExprPack(const ScopePtr& scope, const A actualFunctionType = follow(actualFunctionType); + TypePackId retPack; + if (!FFlag::LuauWidenIfSupertypeIsFree2) + { + retPack = freshTypePack(scope->level); + } + else + { + if (auto free = get(actualFunctionType)) + { + retPack = freshTypePack(free->level); + TypePackId freshArgPack = freshTypePack(free->level); + *asMutable(actualFunctionType) = FunctionTypeVar(free->level, freshArgPack, retPack); + } + else + retPack = freshTypePack(scope->level); + } + // checkExpr will log the pre-instantiated type of the function. // That's not nearly as interesting as the instantiated type, which will include details about how // generic functions are being instantiated for this particular callsite. @@ -3550,8 +3623,6 @@ ExprResult TypeChecker::checkExprPack(const ScopePtr& scope, const A std::vector overloads = flattenIntersection(actualFunctionType); - TypePackId retPack = freshTypePack(scope->level); - std::vector> expectedTypes = getExpectedTypesForCall(overloads, expr.args.size, expr.self); ExprResult argListResult = checkExprList(scope, expr.location, expr.args, false, {}, expectedTypes); @@ -3682,7 +3753,7 @@ std::optional> TypeChecker::checkCallOverload(const Scope // has been instantiated, so is a monotype. We can therefore // unify it with a monomorphic function. TypeId r = addType(FunctionTypeVar(scope->level, argPack, retPack)); - if (FFlag::LuauWidenIfSupertypeIsFree) + if (FFlag::LuauWidenIfSupertypeIsFree2) { UnifierOptions options; options.isFunctionCall = true; @@ -3772,7 +3843,7 @@ std::optional> TypeChecker::checkCallOverload(const Scope { state.log.commit(); - if (isNonstrictMode() && !expr.self && expr.func->is() && ftv->hasSelf) + if (!FFlag::LuauAnyInIsOptionalIsOptional && isNonstrictMode() && !expr.self && expr.func->is() && ftv->hasSelf) { // If we are running in nonstrict mode, passing fewer arguments than the function is declared to take AND // the function is declared with colon notation AND we use dot notation, warn. diff --git a/Analysis/src/TypeVar.cpp b/Analysis/src/TypeVar.cpp index 5af2c8a..8954953 100644 --- a/Analysis/src/TypeVar.cpp +++ b/Analysis/src/TypeVar.cpp @@ -26,6 +26,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAG(LuauSubtypingAddOptPropsToUnsealedTables) LUAU_FASTFLAG(LuauDiscriminableUnions2) +LUAU_FASTFLAGVARIABLE(LuauAnyInIsOptionalIsOptional, false) namespace Luau { @@ -201,11 +202,16 @@ bool isOptional(TypeId ty) if (isNil(ty)) return true; - auto utv = get(follow(ty)); + ty = follow(ty); + + if (FFlag::LuauAnyInIsOptionalIsOptional && get(ty)) + return true; + + auto utv = get(ty); if (!utv) return false; - return std::any_of(begin(utv), end(utv), isNil); + return std::any_of(begin(utv), end(utv), FFlag::LuauAnyInIsOptionalIsOptional ? isOptional : isNil); } bool isTableIntersection(TypeId ty) diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index 7b781f2..60a9c9a 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -20,11 +20,13 @@ LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false); LUAU_FASTFLAG(LuauSingletonTypes) LUAU_FASTFLAG(LuauErrorRecoveryType); LUAU_FASTFLAGVARIABLE(LuauSubtypingAddOptPropsToUnsealedTables, false) -LUAU_FASTFLAGVARIABLE(LuauWidenIfSupertypeIsFree, false) +LUAU_FASTFLAGVARIABLE(LuauWidenIfSupertypeIsFree2, false) LUAU_FASTFLAGVARIABLE(LuauDifferentOrderOfUnificationDoesntMatter, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogSeesTypePacks2, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogCheckForInvalidation, false) +LUAU_FASTFLAGVARIABLE(LuauTxnLogRefreshFunctionPointers, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogDontRetryForIndexers, false) +LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional) namespace Luau { @@ -272,12 +274,6 @@ TypePackId Widen::clean(TypePackId) bool Widen::ignoreChildren(TypeId ty) { - // Sometimes we unify ("hi") -> free1 with (free2) -> free3, so don't ignore functions. - // TODO: should we be doing this? we would need to rework how checkCallOverload does the unification. - if (log->is(ty)) - return false; - - // We only care about unions. return !log->is(ty); } @@ -990,7 +986,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal if (!log.getMutable(superTp)) { - log.replace(superTp, Unifiable::Bound(subTp)); + log.replace(superTp, Unifiable::Bound(widen(subTp))); } } else if (log.getMutable(subTp)) @@ -1107,7 +1103,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal } // In nonstrict mode, any also marks an optional argument. - else if (superIter.good() && isNonstrictMode() && log.getMutable(log.follow(*superIter))) + else if (!FFlag::LuauAnyInIsOptionalIsOptional && superIter.good() && isNonstrictMode() && log.getMutable(log.follow(*superIter))) { superIter.advance(); continue; @@ -1280,6 +1276,13 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal tryUnify_(subFunction->retType, superFunction->retType); } + if (FFlag::LuauTxnLogRefreshFunctionPointers) + { + // Updating the log may have invalidated the function pointers + superFunction = log.getMutable(superTy); + subFunction = log.getMutable(subTy); + } + if (!FFlag::LuauImmutableTypes) { if (superFunction->definition && !subFunction->definition && !subTy->persistent) @@ -1357,10 +1360,18 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) { auto subIter = subTable->props.find(propName); - bool isAny = log.getMutable(log.follow(superProp.type)); + if (FFlag::LuauAnyInIsOptionalIsOptional) + { + if (subIter == subTable->props.end() && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && !isOptional(superProp.type)) + missingProperties.push_back(propName); + } + else + { + bool isAny = log.getMutable(log.follow(superProp.type)); - if (subIter == subTable->props.end() && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && !isOptional(superProp.type) && !isAny) - missingProperties.push_back(propName); + if (subIter == subTable->props.end() && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && !isOptional(superProp.type) && !isAny) + missingProperties.push_back(propName); + } } if (!missingProperties.empty()) @@ -1378,9 +1389,17 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) { auto superIter = superTable->props.find(propName); - bool isAny = log.is(log.follow(subProp.type)); - if (superIter == superTable->props.end() && (FFlag::LuauSubtypingAddOptPropsToUnsealedTables || (!isOptional(subProp.type) && !isAny))) - extraProperties.push_back(propName); + if (FFlag::LuauAnyInIsOptionalIsOptional) + { + if (superIter == superTable->props.end() && (FFlag::LuauSubtypingAddOptPropsToUnsealedTables || !isOptional(subProp.type))) + extraProperties.push_back(propName); + } + else + { + bool isAny = log.is(log.follow(subProp.type)); + if (superIter == superTable->props.end() && (FFlag::LuauSubtypingAddOptPropsToUnsealedTables || (!isOptional(subProp.type) && !isAny))) + extraProperties.push_back(propName); + } } if (!extraProperties.empty()) @@ -1424,6 +1443,12 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) if (innerState.errors.empty()) log.concat(std::move(innerState.log)); } + else if (FFlag::LuauAnyInIsOptionalIsOptional && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && isOptional(prop.type)) + // This is sound because unsealed table types are precise, so `{ p : T } <: { p : T, q : U? }` + // since if `t : { p : T }` then we are guaranteed that `t.q` is `nil`. + // TODO: if the supertype is written to, the subtype may no longer be precise (alias analysis?) + { + } else if ((!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && (isOptional(prop.type) || get(follow(prop.type)))) // This is sound because unsealed table types are precise, so `{ p : T } <: { p : T, q : U? }` // since if `t : { p : T }` then we are guaranteed that `t.q` is `nil`. @@ -1497,6 +1522,9 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) else if (variance == Covariant) { } + else if (FFlag::LuauAnyInIsOptionalIsOptional && !FFlag::LuauSubtypingAddOptPropsToUnsealedTables && isOptional(prop.type)) + { + } else if (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables && (isOptional(prop.type) || get(follow(prop.type)))) { } @@ -1618,7 +1646,7 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) TypeId Unifier::widen(TypeId ty) { - if (!FFlag::LuauWidenIfSupertypeIsFree) + if (!FFlag::LuauWidenIfSupertypeIsFree2) return ty; Widen widen{types}; @@ -1627,10 +1655,21 @@ TypeId Unifier::widen(TypeId ty) return result.value_or(ty); } +TypePackId Unifier::widen(TypePackId tp) +{ + if (!FFlag::LuauWidenIfSupertypeIsFree2) + return tp; + + Widen widen{types}; + std::optional result = widen.substitute(tp); + // TODO: what does it mean for substitution to fail to widen? + return result.value_or(tp); +} + TypeId Unifier::deeplyOptional(TypeId ty, std::unordered_map seen) { ty = follow(ty); - if (get(ty)) + if (!FFlag::LuauAnyInIsOptionalIsOptional && get(ty)) return ty; else if (isOptional(ty)) return ty; @@ -1744,7 +1783,10 @@ void Unifier::tryUnifyFreeTable(TypeId subTy, TypeId superTy) { if (auto subProp = findTablePropertyRespectingMeta(subTy, freeName)) { - tryUnify_(freeProp.type, *subProp); + if (FFlag::LuauWidenIfSupertypeIsFree2) + tryUnify_(*subProp, freeProp.type); + else + tryUnify_(freeProp.type, *subProp); /* * TypeVars are commonly cyclic, so it is entirely possible diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 1cb8f13..941a3ea 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -11,18 +11,11 @@ LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTFLAGVARIABLE(LuauParseSingletonTypes, false) -LUAU_FASTFLAGVARIABLE(LuauParseAllHotComments, false) LUAU_FASTFLAGVARIABLE(LuauTableFieldFunctionDebugname, false) namespace Luau { -static bool isComment(const Lexeme& lexeme) -{ - LUAU_ASSERT(!FFlag::LuauParseAllHotComments); - return lexeme.type == Lexeme::Comment || lexeme.type == Lexeme::BlockComment; -} - ParseError::ParseError(const Location& location, const std::string& message) : location(location) , message(message) @@ -146,54 +139,13 @@ ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& n { LUAU_TIMETRACE_SCOPE("Parser::parse", "Parser"); - Parser p(buffer, bufferSize, names, allocator, FFlag::LuauParseAllHotComments ? options : ParseOptions()); + Parser p(buffer, bufferSize, names, allocator, options); try { - if (FFlag::LuauParseAllHotComments) - { - AstStatBlock* root = p.parseChunk(); + AstStatBlock* root = p.parseChunk(); - return ParseResult{root, std::move(p.hotcomments), std::move(p.parseErrors), std::move(p.commentLocations)}; - } - else - { - std::vector hotcomments; - - while (isComment(p.lexer.current()) || p.lexer.current().type == Lexeme::BrokenComment) - { - const char* text = p.lexer.current().data; - unsigned int length = p.lexer.current().length; - - if (length && text[0] == '!') - { - unsigned int end = length; - while (end > 0 && isSpace(text[end - 1])) - --end; - - hotcomments.push_back({true, p.lexer.current().location, std::string(text + 1, text + end)}); - } - - const Lexeme::Type type = p.lexer.current().type; - const Location loc = p.lexer.current().location; - - if (options.captureComments) - p.commentLocations.push_back(Comment{type, loc}); - - if (type == Lexeme::BrokenComment) - break; - - p.lexer.next(); - } - - p.lexer.setSkipComments(true); - - p.options = options; - - AstStatBlock* root = p.parseChunk(); - - return ParseResult{root, hotcomments, p.parseErrors, std::move(p.commentLocations)}; - } + return ParseResult{root, std::move(p.hotcomments), std::move(p.parseErrors), std::move(p.commentLocations)}; } catch (ParseError& err) { @@ -225,10 +177,11 @@ Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Alloc matchRecoveryStopOnToken.assign(Lexeme::Type::Reserved_END, 0); matchRecoveryStopOnToken[Lexeme::Type::Eof] = 1; - if (FFlag::LuauParseAllHotComments) - lexer.setSkipComments(true); + // required for lookahead() to work across a comment boundary and for nextLexeme() to work when captureComments is false + lexer.setSkipComments(true); - // read first lexeme + // read first lexeme (any hot comments get .header = true) + LUAU_ASSERT(hotcommentHeader); nextLexeme(); // all hot comments parsed after the first non-comment lexeme are special in that they don't affect type checking / linting mode @@ -2831,49 +2784,31 @@ void Parser::nextLexeme() { if (options.captureComments) { - if (FFlag::LuauParseAllHotComments) + Lexeme::Type type = lexer.next(/* skipComments= */ false).type; + + while (type == Lexeme::BrokenComment || type == Lexeme::Comment || type == Lexeme::BlockComment) { - Lexeme::Type type = lexer.next(/* skipComments= */ false).type; + const Lexeme& lexeme = lexer.current(); + commentLocations.push_back(Comment{lexeme.type, lexeme.location}); - while (type == Lexeme::BrokenComment || type == Lexeme::Comment || type == Lexeme::BlockComment) + // Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme. + // The parser will turn this into a proper syntax error. + if (lexeme.type == Lexeme::BrokenComment) + return; + + // Comments starting with ! are called "hot comments" and contain directives for type checking / linting + if (lexeme.type == Lexeme::Comment && lexeme.length && lexeme.data[0] == '!') { - const Lexeme& lexeme = lexer.current(); - commentLocations.push_back(Comment{lexeme.type, lexeme.location}); + const char* text = lexeme.data; - // Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme. - // The parser will turn this into a proper syntax error. - if (lexeme.type == Lexeme::BrokenComment) - return; + unsigned int end = lexeme.length; + while (end > 0 && isSpace(text[end - 1])) + --end; - // Comments starting with ! are called "hot comments" and contain directives for type checking / linting - if (lexeme.type == Lexeme::Comment && lexeme.length && lexeme.data[0] == '!') - { - const char* text = lexeme.data; - - unsigned int end = lexeme.length; - while (end > 0 && isSpace(text[end - 1])) - --end; - - hotcomments.push_back({hotcommentHeader, lexeme.location, std::string(text + 1, text + end)}); - } - - type = lexer.next(/* skipComments= */ false).type; - } - } - else - { - while (true) - { - const Lexeme& lexeme = lexer.next(/*skipComments*/ false); - // Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme. - // The parser will turn this into a proper syntax error. - if (lexeme.type == Lexeme::BrokenComment) - commentLocations.push_back(Comment{lexeme.type, lexeme.location}); - if (isComment(lexeme)) - commentLocations.push_back(Comment{lexeme.type, lexeme.location}); - else - return; + hotcomments.push_back({hotcommentHeader, lexeme.location, std::string(text + 1, text + end)}); } + + type = lexer.next(/* skipComments= */ false).type; } } else diff --git a/Compiler/src/Builtins.cpp b/Compiler/src/Builtins.cpp index 26360c4..ff75311 100644 --- a/Compiler/src/Builtins.cpp +++ b/Compiler/src/Builtins.cpp @@ -4,8 +4,6 @@ #include "Luau/Bytecode.h" #include "Luau/Compiler.h" -LUAU_FASTFLAGVARIABLE(LuauCompileSelectBuiltin2, false) - namespace Luau { namespace Compile @@ -64,7 +62,7 @@ int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& options) if (builtin.isGlobal("unpack")) return LBF_TABLE_UNPACK; - if (FFlag::LuauCompileSelectBuiltin2 && builtin.isGlobal("select")) + if (builtin.isGlobal("select")) return LBF_SELECT_VARARG; if (builtin.object == "math") diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 656a992..6330bf1 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -15,8 +15,6 @@ #include #include -LUAU_FASTFLAG(LuauCompileSelectBuiltin2) - namespace Luau { @@ -265,7 +263,6 @@ struct Compiler void compileExprSelectVararg(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop, bool multRet, uint8_t regs) { - LUAU_ASSERT(FFlag::LuauCompileSelectBuiltin2); LUAU_ASSERT(targetCount == 1); LUAU_ASSERT(!expr->self); LUAU_ASSERT(expr->args.size == 2 && expr->args.data[1]->is()); @@ -407,7 +404,6 @@ struct Compiler if (bfid == LBF_SELECT_VARARG) { - LUAU_ASSERT(FFlag::LuauCompileSelectBuiltin2); // Optimization: compile select(_, ...) as FASTCALL1; the builtin will read variadic arguments directly // note: for now we restrict this to single-return expressions since our runtime code doesn't deal with general cases if (multRet == false && targetCount == 1 && expr->args.size == 2 && expr->args.data[1]->is()) diff --git a/Sources.cmake b/Sources.cmake index 615641e..59b3849 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -232,11 +232,18 @@ if(TARGET Luau.UnitTest) tests/Transpiler.test.cpp tests/TypeInfer.aliases.test.cpp tests/TypeInfer.annotations.test.cpp + tests/TypeInfer.anyerror.test.cpp tests/TypeInfer.builtins.test.cpp tests/TypeInfer.classes.test.cpp tests/TypeInfer.definitions.test.cpp + tests/TypeInfer.functions.test.cpp tests/TypeInfer.generics.test.cpp tests/TypeInfer.intersectionTypes.test.cpp + tests/TypeInfer.loops.test.cpp + tests/TypeInfer.modules.test.cpp + tests/TypeInfer.oop.test.cpp + tests/TypeInfer.operators.test.cpp + tests/TypeInfer.primitives.test.cpp tests/TypeInfer.provisional.test.cpp tests/TypeInfer.refinements.test.cpp tests/TypeInfer.singletons.test.cpp diff --git a/VM/include/lua.h b/VM/include/lua.h index 274c4ed..d08b73e 100644 --- a/VM/include/lua.h +++ b/VM/include/lua.h @@ -172,9 +172,12 @@ LUA_API const char* lua_pushvfstring(lua_State* L, const char* fmt, va_list argp LUA_API LUA_PRINTF_ATTR(2, 3) const char* lua_pushfstringL(lua_State* L, const char* fmt, ...); LUA_API void lua_pushcclosurek(lua_State* L, lua_CFunction fn, const char* debugname, int nup, lua_Continuation cont); LUA_API void lua_pushboolean(lua_State* L, int b); -LUA_API void lua_pushlightuserdata(lua_State* L, void* p); LUA_API int lua_pushthread(lua_State* L); +LUA_API void lua_pushlightuserdata(lua_State* L, void* p); +LUA_API void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag); +LUA_API void* lua_newuserdatadtor(lua_State* L, size_t sz, void (*dtor)(void*)); + /* ** get functions (Lua -> stack) */ @@ -189,8 +192,6 @@ LUA_API void lua_setreadonly(lua_State* L, int idx, int enabled); LUA_API int lua_getreadonly(lua_State* L, int idx); LUA_API void lua_setsafeenv(lua_State* L, int idx, int enabled); -LUA_API void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag); -LUA_API void* lua_newuserdatadtor(lua_State* L, size_t sz, void (*dtor)(void*)); LUA_API int lua_getmetatable(lua_State* L, int objindex); LUA_API void lua_getfenv(lua_State* L, int idx); @@ -276,6 +277,14 @@ enum lua_GCOp LUA_API int lua_gc(lua_State* L, int what, int data); +/* +** memory statistics +** all allocated bytes are attributed to the memory category of the running thread (0..LUA_MEMORY_CATEGORIES-1) +*/ + +LUA_API void lua_setmemcat(lua_State* L, int category); +LUA_API size_t lua_totalbytes(lua_State* L, int category); + /* ** miscellaneous functions */ diff --git a/VM/src/lapi.cpp b/VM/src/lapi.cpp index f7f1544..3c08731 100644 --- a/VM/src/lapi.cpp +++ b/VM/src/lapi.cpp @@ -35,8 +35,8 @@ const char* luau_ident = "$Luau: Copyright (C) 2019-2022 Roblox Corporation $\n" static Table* getcurrenv(lua_State* L) { - if (L->ci == L->base_ci) /* no enclosing function? */ - return L->gt; /* use global table as environment */ + if (L->ci == L->base_ci) /* no enclosing function? */ + return L->gt; /* use global table as environment */ else return curr_func(L)->env; } @@ -1188,7 +1188,7 @@ void lua_concat(lua_State* L, int n) void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag) { - api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT || tag == UTAG_PROXY); luaC_checkGC(L); luaC_checkthreadsleep(L); Udata* u = luaU_newudata(L, sz, tag); @@ -1317,7 +1317,7 @@ void lua_setuserdatadtor(lua_State* L, int tag, void (*dtor)(void*)) L->global->udatagc[tag] = dtor; } -LUA_API void lua_clonefunction(lua_State* L, int idx) +void lua_clonefunction(lua_State* L, int idx) { StkId p = index2addr(L, idx); api_check(L, isLfunction(p)); @@ -1333,3 +1333,15 @@ lua_Callbacks* lua_callbacks(lua_State* L) { return &L->global->cb; } + +void lua_setmemcat(lua_State* L, int category) +{ + api_check(L, unsigned(category) < LUA_MEMORY_CATEGORIES); + L->activememcat = uint8_t(category); +} + +size_t lua_totalbytes(lua_State* L, int category) +{ + api_check(L, category < LUA_MEMORY_CATEGORIES); + return category < 0 ? L->global->totalbytes : L->global->memcatbytes[category]; +} diff --git a/VM/src/lbaselib.cpp b/VM/src/lbaselib.cpp index 2307598..96ad493 100644 --- a/VM/src/lbaselib.cpp +++ b/VM/src/lbaselib.cpp @@ -5,6 +5,7 @@ #include "lstate.h" #include "lapi.h" #include "ldo.h" +#include "ludata.h" #include #include @@ -190,6 +191,7 @@ static int luaB_type(lua_State* L) luaL_checkany(L, 1); if (DFFlag::LuauMorePreciseLuaLTypeName) { + /* resulting name doesn't differentiate between userdata types */ lua_pushstring(L, lua_typename(L, lua_type(L, 1))); } else @@ -202,8 +204,16 @@ static int luaB_type(lua_State* L) static int luaB_typeof(lua_State* L) { luaL_checkany(L, 1); - const TValue* obj = luaA_toobject(L, 1); - lua_pushstring(L, luaT_objtypename(L, obj)); + if (DFFlag::LuauMorePreciseLuaLTypeName) + { + /* resulting name returns __type if specified unless the input is a newproxy-created userdata */ + lua_pushstring(L, luaL_typename(L, 1)); + } + else + { + const TValue* obj = luaA_toobject(L, 1); + lua_pushstring(L, luaT_objtypename(L, obj)); + } return 1; } @@ -412,7 +422,7 @@ static int luaB_newproxy(lua_State* L) bool needsmt = lua_toboolean(L, 1); - lua_newuserdata(L, 0); + lua_newuserdatatagged(L, 0, UTAG_PROXY); if (needsmt) { diff --git a/VM/src/lgc.h b/VM/src/lgc.h index ad8ee78..ebf999b 100644 --- a/VM/src/lgc.h +++ b/VM/src/lgc.h @@ -9,9 +9,9 @@ /* ** Default settings for GC tunables (settable via lua_gc) */ -#define LUAI_GCGOAL 200 /* 200% (allow heap to double compared to live heap size) */ +#define LUAI_GCGOAL 200 /* 200% (allow heap to double compared to live heap size) */ #define LUAI_GCSTEPMUL 200 /* GC runs 'twice the speed' of memory allocation */ -#define LUAI_GCSTEPSIZE 1 /* GC runs every KB of memory allocation */ +#define LUAI_GCSTEPSIZE 1 /* GC runs every KB of memory allocation */ /* ** Possible states of the Garbage Collector diff --git a/VM/src/lmem.cpp b/VM/src/lmem.cpp index 899cb0c..3cbdaff 100644 --- a/VM/src/lmem.cpp +++ b/VM/src/lmem.cpp @@ -98,7 +98,7 @@ */ #if defined(__APPLE__) #define ABISWITCH(x64, ms32, gcc32) (sizeof(void*) == 8 ? x64 : gcc32) -#elif defined(__i386__) +#elif defined(__i386__) && !defined(_MSC_VER) #define ABISWITCH(x64, ms32, gcc32) (gcc32) #else // Android somehow uses a similar ABI to MSVC, *not* to iOS... diff --git a/VM/src/lstring.cpp b/VM/src/lstring.cpp index 8725014..c0cd3e2 100644 --- a/VM/src/lstring.cpp +++ b/VM/src/lstring.cpp @@ -53,7 +53,7 @@ void luaS_resize(lua_State* L, int newsize) { TString* p = tb->hash[i]; while (p) - { /* for each node in the list */ + { /* for each node in the list */ TString* next = p->next; /* save next */ unsigned int h = p->hash; int h1 = lmod(h, newsize); /* new position */ diff --git a/VM/src/ltable.cpp b/VM/src/ltable.cpp index ef0b4b9..2deec2b 100644 --- a/VM/src/ltable.cpp +++ b/VM/src/ltable.cpp @@ -24,6 +24,8 @@ #include +LUAU_FASTFLAGVARIABLE(LuauTableRehashRework, false) + // max size of both array and hash part is 2^MAXBITS #define MAXBITS 26 #define MAXSIZE (1 << MAXBITS) @@ -351,6 +353,22 @@ static void setnodevector(lua_State* L, Table* t, int size) t->lastfree = size; /* all positions are free */ } +static TValue* newkey(lua_State* L, Table* t, const TValue* key); + +static TValue* arrayornewkey(lua_State* L, Table* t, const TValue* key) +{ + if (ttisnumber(key)) + { + int k; + double n = nvalue(key); + luai_num2int(k, n); + if (luai_numeq(cast_num(k), n) && cast_to(unsigned int, k - 1) < cast_to(unsigned int, t->sizearray)) + return &t->array[k - 1]; + } + + return newkey(L, t, key); +} + static void resize(lua_State* L, Table* t, int nasize, int nhsize) { if (nasize > MAXSIZE || nhsize > MAXSIZE) @@ -369,22 +387,50 @@ static void resize(lua_State* L, Table* t, int nasize, int nhsize) for (int i = nasize; i < oldasize; i++) { if (!ttisnil(&t->array[i])) - setobjt2t(L, luaH_setnum(L, t, i + 1), &t->array[i]); + { + if (FFlag::LuauTableRehashRework) + { + TValue ok; + setnvalue(&ok, cast_num(i + 1)); + setobjt2t(L, newkey(L, t, &ok), &t->array[i]); + } + else + { + setobjt2t(L, luaH_setnum(L, t, i + 1), &t->array[i]); + } + } } /* shrink array */ luaM_reallocarray(L, t->array, oldasize, nasize, TValue, t->memcat); } /* re-insert elements from hash part */ - for (int i = twoto(oldhsize) - 1; i >= 0; i--) + if (FFlag::LuauTableRehashRework) { - LuaNode* old = nold + i; - if (!ttisnil(gval(old))) + for (int i = twoto(oldhsize) - 1; i >= 0; i--) { - TValue ok; - getnodekey(L, &ok, old); - setobjt2t(L, luaH_set(L, t, &ok), gval(old)); + LuaNode* old = nold + i; + if (!ttisnil(gval(old))) + { + TValue ok; + getnodekey(L, &ok, old); + setobjt2t(L, arrayornewkey(L, t, &ok), gval(old)); + } } } + else + { + for (int i = twoto(oldhsize) - 1; i >= 0; i--) + { + LuaNode* old = nold + i; + if (!ttisnil(gval(old))) + { + TValue ok; + getnodekey(L, &ok, old); + setobjt2t(L, luaH_set(L, t, &ok), gval(old)); + } + } + } + if (nold != dummynode) luaM_freearray(L, nold, twoto(oldhsize), LuaNode, t->memcat); /* free old array */ } @@ -482,7 +528,16 @@ static TValue* newkey(lua_State* L, Table* t, const TValue* key) if (n == NULL) { /* cannot find a free place? */ rehash(L, t, key); /* grow table */ - return luaH_set(L, t, key); /* re-insert key into grown table */ + + if (!FFlag::LuauTableRehashRework) + { + return luaH_set(L, t, key); /* re-insert key into grown table */ + } + else + { + // after rehash, numeric keys might be located in the new array part, but won't be found in the node part + return arrayornewkey(L, t, key); + } } LUAU_ASSERT(n != dummynode); TValue mk; diff --git a/VM/src/ltm.cpp b/VM/src/ltm.cpp index a77a7c7..106efb2 100644 --- a/VM/src/ltm.cpp +++ b/VM/src/ltm.cpp @@ -4,6 +4,7 @@ #include "lstate.h" #include "lstring.h" +#include "ludata.h" #include "ltable.h" #include "lgc.h" @@ -116,7 +117,7 @@ const TValue* luaT_gettmbyobj(lua_State* L, const TValue* o, TMS event) const TString* luaT_objtypenamestr(lua_State* L, const TValue* o) { - if (ttisuserdata(o) && uvalue(o)->tag && uvalue(o)->metatable) + if (ttisuserdata(o) && uvalue(o)->tag != UTAG_PROXY && uvalue(o)->metatable) { const TValue* type = luaH_getstr(uvalue(o)->metatable, L->global->tmname[TM_TYPE]); diff --git a/VM/src/ludata.cpp b/VM/src/ludata.cpp index 0dfac50..819d186 100644 --- a/VM/src/ludata.cpp +++ b/VM/src/ludata.cpp @@ -22,13 +22,11 @@ Udata* luaU_newudata(lua_State* L, size_t s, int tag) void luaU_freeudata(lua_State* L, Udata* u, lua_Page* page) { - LUAU_ASSERT(u->tag < LUA_UTAG_LIMIT || u->tag == UTAG_IDTOR); - void (*dtor)(void*) = nullptr; - if (u->tag == UTAG_IDTOR) - memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor)); - else if (u->tag) + if (u->tag < LUA_UTAG_LIMIT) dtor = L->global->udatagc[u->tag]; + else if (u->tag == UTAG_IDTOR) + memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor)); if (dtor) dtor(u->data); diff --git a/VM/src/ludata.h b/VM/src/ludata.h index ec374c2..f24e4a3 100644 --- a/VM/src/ludata.h +++ b/VM/src/ludata.h @@ -7,6 +7,9 @@ /* special tag value is used for user data with inline dtors */ #define UTAG_IDTOR LUA_UTAG_LIMIT +/* special tag value is used for newproxy-created user data (all other user data objects are host-exposed) */ +#define UTAG_PROXY (LUA_UTAG_LIMIT + 1) + #define sizeudata(len) (offsetof(Udata, data) + len) LUAI_FUNC Udata* luaU_newudata(lua_State* L, size_t s, int tag); diff --git a/VM/src/lvmexecute.cpp b/VM/src/lvmexecute.cpp index 5e2ef68..96a87b7 100644 --- a/VM/src/lvmexecute.cpp +++ b/VM/src/lvmexecute.cpp @@ -77,7 +77,7 @@ if (LUAU_UNLIKELY(!!interrupt)) \ { /* the interrupt hook is called right before we advance pc */ \ VM_PROTECT(L->ci->savedpc++; interrupt(L, -1)); \ - if (L->status != 0) \ + if (L->status != 0) \ { \ L->ci->savedpc--; \ goto exit; \ diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 55b0618..17fd6b1 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -2755,4 +2755,166 @@ local abc = b@1 CHECK(ac.entryMap["bar"].parens == ParenthesesRecommendation::CursorInside); } +TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_on_class") +{ + ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true}; + + loadDefinition(R"( +declare class Foo + function one(self): number + two: () -> number +end + )"); + + { + check(R"( +local t: Foo +t:@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("one")); + REQUIRE(ac.entryMap.count("two")); + CHECK(!ac.entryMap["one"].wrongIndexType); + CHECK(ac.entryMap["two"].wrongIndexType); + } + + { + check(R"( +local t: Foo +t.@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("one")); + REQUIRE(ac.entryMap.count("two")); + CHECK(ac.entryMap["one"].wrongIndexType); + CHECK(!ac.entryMap["two"].wrongIndexType); + } +} + +TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls") +{ + ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true}; + + check(R"( +local t = {} +function t.m() end +t:@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("m")); + CHECK(ac.entryMap["m"].wrongIndexType); +} + +TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_2") +{ + ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true}; + + check(R"( +local f: (() -> number) & ((number) -> number) = function(x: number?) return 2 end +local t = {} +t.f = f +t:@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("f")); + CHECK(ac.entryMap["f"].wrongIndexType); +} + +TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_provisional") +{ + check(R"( +local t = {} +function t.m(x: typeof(t)) end +t:@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("m")); + // We can make changes to mark this as a wrong way to call even though it's compatible + CHECK(!ac.entryMap["m"].wrongIndexType); +} + +TEST_CASE_FIXTURE(ACFixture, "string_prim_self_calls_are_fine") +{ + ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true}; + + check(R"( +local s = "hello" +s:@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("byte")); + CHECK(ac.entryMap["byte"].wrongIndexType == false); + REQUIRE(ac.entryMap.count("char")); + CHECK(ac.entryMap["char"].wrongIndexType == true); + REQUIRE(ac.entryMap.count("sub")); + CHECK(ac.entryMap["sub"].wrongIndexType == false); +} + +TEST_CASE_FIXTURE(ACFixture, "string_prim_non_self_calls_are_avoided") +{ + ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true}; + + check(R"( +local s = "hello" +s.@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("byte")); + CHECK(ac.entryMap["byte"].wrongIndexType == true); + REQUIRE(ac.entryMap.count("char")); + CHECK(ac.entryMap["char"].wrongIndexType == false); + REQUIRE(ac.entryMap.count("sub")); + CHECK(ac.entryMap["sub"].wrongIndexType == true); +} + +TEST_CASE_FIXTURE(ACFixture, "string_library_non_self_calls_are_fine") +{ + ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true}; + + check(R"( +string.@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("byte")); + CHECK(ac.entryMap["byte"].wrongIndexType == false); + REQUIRE(ac.entryMap.count("char")); + CHECK(ac.entryMap["char"].wrongIndexType == false); + REQUIRE(ac.entryMap.count("sub")); + CHECK(ac.entryMap["sub"].wrongIndexType == false); +} + +TEST_CASE_FIXTURE(ACFixture, "string_library_self_calls_are_invalid") +{ + ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true}; + + check(R"( +string:@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("byte")); + CHECK(ac.entryMap["byte"].wrongIndexType == true); + REQUIRE(ac.entryMap.count("char")); + CHECK(ac.entryMap["char"].wrongIndexType == true); + REQUIRE(ac.entryMap.count("sub")); + CHECK(ac.entryMap["sub"].wrongIndexType == true); +} + TEST_SUITE_END(); diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index f982c86..3dc57da 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -2819,8 +2819,6 @@ RETURN R1 -1 TEST_CASE("FastcallSelect") { - ScopedFastFlag sff("LuauCompileSelectBuiltin2", true); - // select(_, ...) compiles to a builtin call CHECK_EQ("\n" + compileFunction0("return (select('#', ...))"), R"( LOADK R1 K0 diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 63fbb36..9e4cb4a 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -569,6 +569,11 @@ TEST_CASE("Debugger") CHECK(lua_tointeger(L, -1) == 50); lua_pop(L, 1); + int v = lua_getargument(L, 0, 2); + REQUIRE(v); + CHECK(lua_tointeger(L, -1) == 42); + lua_pop(L, 1); + // test lua_getlocal const char* l = lua_getlocal(L, 0, 1); REQUIRE(l); @@ -652,31 +657,6 @@ TEST_CASE("SameHash") CHECK(luaS_hash(buf + 1, 120) == luaS_hash(buf + 2, 120)); } -TEST_CASE("InlineDtor") -{ - static int dtorhits = 0; - - dtorhits = 0; - - { - StateRef globalState(luaL_newstate(), lua_close); - lua_State* L = globalState.get(); - - void* u1 = lua_newuserdatadtor(L, 4, [](void* data) { - dtorhits += *(int*)data; - }); - - void* u2 = lua_newuserdatadtor(L, 1, [](void* data) { - dtorhits += *(char*)data; - }); - - *(int*)u1 = 39; - *(char*)u2 = 3; - } - - CHECK(dtorhits == 42); -} - TEST_CASE("Reference") { static int dtorhits = 0; @@ -969,7 +949,7 @@ TEST_CASE("StringConversion") TEST_CASE("GCDump") { // internal function, declared in lgc.h - not exposed via lua.h - extern void luaC_dump(lua_State* L, void* file, const char* (*categoryName)(lua_State* L, uint8_t memcat)); + extern void luaC_dump(lua_State * L, void* file, const char* (*categoryName)(lua_State * L, uint8_t memcat)); StateRef globalState(luaL_newstate(), lua_close); lua_State* L = globalState.get(); @@ -1015,4 +995,114 @@ TEST_CASE("GCDump") fclose(f); } +TEST_CASE("Interrupt") +{ + static const int expectedhits[] = { + 2, + 9, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 6, + 11, + }; + static int index; + + index = 0; + + runConformance( + "interrupt.lua", + [](lua_State* L) { + auto* cb = lua_callbacks(L); + + // note: for simplicity here we setup the interrupt callback once + // however, this carries a noticeable performance cost. in a real application, + // it's advised to set interrupt callback on a timer from a different thread, + // and set it back to nullptr once the interrupt triggered. + cb->interrupt = [](lua_State* L, int gc) { + if (gc >= 0) + return; + + CHECK(index < int(std::size(expectedhits))); + + lua_Debug ar = {}; + lua_getinfo(L, 0, "l", &ar); + + CHECK(ar.currentline == expectedhits[index]); + + index++; + + // check that we can yield inside an interrupt + if (index == 5) + lua_yield(L, 0); + }; + }, + [](lua_State* L) { + CHECK(index == 5); // a single yield point + }); + + CHECK(index == int(std::size(expectedhits))); +} + +TEST_CASE("UserdataApi") +{ + static int dtorhits = 0; + + dtorhits = 0; + + StateRef globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + // setup dtor for tag 42 (created later) + lua_setuserdatadtor(L, 42, [](void* data) { + dtorhits += *(int*)data; + }); + + // light user data + int lud; + lua_pushlightuserdata(L, &lud); + + CHECK(lua_touserdata(L, -1) == &lud); + CHECK(lua_topointer(L, -1) == &lud); + + // regular user data + int* ud1 = (int*)lua_newuserdata(L, 4); + *ud1 = 42; + + CHECK(lua_touserdata(L, -1) == ud1); + CHECK(lua_topointer(L, -1) == ud1); + + // tagged user data + int* ud2 = (int*)lua_newuserdatatagged(L, 4, 42); + *ud2 = -4; + + CHECK(lua_touserdatatagged(L, -1, 42) == ud2); + CHECK(lua_touserdatatagged(L, -1, 41) == nullptr); + CHECK(lua_userdatatag(L, -1) == 42); + + // user data with inline dtor + void* ud3 = lua_newuserdatadtor(L, 4, [](void* data) { + dtorhits += *(int*)data; + }); + + void* ud4 = lua_newuserdatadtor(L, 1, [](void* data) { + dtorhits += *(char*)data; + }); + + *(int*)ud3 = 43; + *(char*)ud4 = 3; + + globalState.reset(); + + CHECK(dtorhits == 42); +} + TEST_SUITE_END(); diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index 4d6c207..91b2319 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -1667,8 +1667,6 @@ _ = (math.random() < 0.5 and false) or 42 -- currently ignored TEST_CASE_FIXTURE(Fixture, "WrongComment") { - ScopedFastFlag sff("LuauParseAllHotComments", true); - LintResult result = lint(R"( --!strict --!struct diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index e3993cc..82b7a35 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -248,10 +248,12 @@ TEST_CASE_FIXTURE(Fixture, "clone_seal_free_tables") TEST_CASE_FIXTURE(Fixture, "clone_self_property") { + ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true}; + fileResolver.source["Module/A"] = R"( --!nonstrict local a = {} - function a:foo(x) + function a:foo(x: number) return -x; end return a; @@ -267,10 +269,10 @@ TEST_CASE_FIXTURE(Fixture, "clone_self_property") )"; result = frontend.check("Module/B"); - LUAU_REQUIRE_ERRORS(result); - CHECK_EQ(toString(result.errors[0]), "This function was declared to accept self, but you did not pass enough arguments. Use a colon instead of a " - "dot or pass 1 extra nil to suppress this warning"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("This function must be called with self. Did you mean to use a colon instead of a dot?", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit") diff --git a/tests/NonstrictMode.test.cpp b/tests/NonstrictMode.test.cpp index 5bad990..d3faea2 100644 --- a/tests/NonstrictMode.test.cpp +++ b/tests/NonstrictMode.test.cpp @@ -126,6 +126,8 @@ TEST_CASE_FIXTURE(Fixture, "parameters_having_type_any_are_optional") TEST_CASE_FIXTURE(Fixture, "local_tables_are_not_any") { + ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true}; + CheckResult result = check(R"( --!nonstrict local T = {} @@ -136,31 +138,25 @@ TEST_CASE_FIXTURE(Fixture, "local_tables_are_not_any") T:staticmethod() )"); - LUAU_REQUIRE_ERROR_COUNT(2, result); + LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK(std::any_of(result.errors.begin(), result.errors.end(), [](const TypeError& e) { - return get(e); - })); - CHECK(std::any_of(result.errors.begin(), result.errors.end(), [](const TypeError& e) { - return get(e); - })); + CHECK_EQ("This function does not take self. Did you mean to use a dot instead of a colon?", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "offer_a_hint_if_you_use_a_dot_instead_of_a_colon") { + ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true}; + CheckResult result = check(R"( --!nonstrict local T = {} - function T:method() end - T.method() + function T:method(x: number) end + T.method(5) )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - auto e = get(result.errors[0]); - REQUIRE(e != nullptr); - - REQUIRE_EQ(1, e->requiredExtraNils); + CHECK_EQ("This function must be called with self. Did you mean to use a colon instead of a dot?", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "table_props_are_any") diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 711c0aa..b2e7605 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -676,4 +676,25 @@ TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_uni LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "recursive_types_restriction_ok") +{ + CheckResult result = check(R"( + type Tree = { data: T, children: {Tree} } + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "recursive_types_restriction_not_ok") +{ + ScopedFastFlag sff{"LuauRecursiveTypeParameterRestriction", true}; + + CheckResult result = check(R"( + -- this would be an infinite type if we allowed it + type Tree = { data: T, children: {Tree<{T}>} } + )"); + + LUAU_REQUIRE_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.anyerror.test.cpp b/tests/TypeInfer.anyerror.test.cpp new file mode 100644 index 0000000..5224b5d --- /dev/null +++ b/tests/TypeInfer.anyerror.test.cpp @@ -0,0 +1,335 @@ +// 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/BuiltinDefinitions.h" +#include "Luau/Scope.h" +#include "Luau/TypeInfer.h" +#include "Luau/TypeVar.h" +#include "Luau/VisitTypeVar.h" + +#include "Fixture.h" + +#include "doctest.h" + +using namespace Luau; + +TEST_SUITE_BEGIN("TypeInferAnyError"); + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any") +{ + CheckResult result = check(R"( + function bar(): any + return true + end + + local a + for b in bar do + a = b + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(typeChecker.anyType, requireType("a")); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2") +{ + CheckResult result = check(R"( + function bar(): any + return true + end + + local a + for b in bar() do + a = b + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("any", toString(requireType("a"))); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any") +{ + CheckResult result = check(R"( + local bar: any + + local a + for b in bar do + a = b + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("any", toString(requireType("a"))); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2") +{ + CheckResult result = check(R"( + local bar: any + + local a + for b in bar() do + a = b + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("any", toString(requireType("a"))); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error") +{ + CheckResult result = check(R"( + local a + for b in bar do + a = b + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("*unknown*", toString(requireType("a"))); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error2") +{ + CheckResult result = check(R"( + function bar(c) return c end + + local a + for b in bar() do + a = b + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("*unknown*", toString(requireType("a"))); +} + +TEST_CASE_FIXTURE(Fixture, "length_of_error_type_does_not_produce_an_error") +{ + CheckResult result = check(R"( + local l = #this_is_not_defined + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "indexing_error_type_does_not_produce_an_error") +{ + CheckResult result = check(R"( + local originalReward = unknown.Parent.Reward:GetChildren()[1] + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "dot_on_error_type_does_not_produce_an_error") +{ + CheckResult result = check(R"( + local foo = (true).x + foo.x = foo.y + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "any_type_propagates") +{ + CheckResult result = check(R"( + local foo: any + local bar = foo:method("argument") + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("any", toString(requireType("bar"))); +} + +TEST_CASE_FIXTURE(Fixture, "can_subscript_any") +{ + CheckResult result = check(R"( + local foo: any + local bar = foo[5] + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("any", toString(requireType("bar"))); +} + +// Not strictly correct: metatables permit overriding this +TEST_CASE_FIXTURE(Fixture, "can_get_length_of_any") +{ + CheckResult result = check(R"( + local foo: any = {} + local bar = #foo + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(PrimitiveTypeVar::Number, getPrimitiveType(requireType("bar"))); +} + +TEST_CASE_FIXTURE(Fixture, "assign_prop_to_table_by_calling_any_yields_any") +{ + CheckResult result = check(R"( + local f: any + local T = {} + + T.prop = f() + + return T + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + TableTypeVar* ttv = getMutable(requireType("T")); + REQUIRE(ttv); + REQUIRE(ttv->props.count("prop")); + + REQUIRE_EQ("any", toString(ttv->props["prop"].type)); +} + +TEST_CASE_FIXTURE(Fixture, "quantify_any_does_not_bind_to_itself") +{ + CheckResult result = check(R"( + local A : any + function A.B() end + A:C() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + TypeId aType = requireType("A"); + CHECK_EQ(aType, typeChecker.anyType); +} + +TEST_CASE_FIXTURE(Fixture, "calling_error_type_yields_error") +{ + CheckResult result = check(R"( + local a = unknown.Parent.Reward.GetChildren() + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + UnknownSymbol* err = get(result.errors[0]); + REQUIRE(err != nullptr); + + CHECK_EQ("unknown", err->name); + + CHECK_EQ("*unknown*", toString(requireType("a"))); +} + +TEST_CASE_FIXTURE(Fixture, "chain_calling_error_type_yields_error") +{ + CheckResult result = check(R"( + local a = Utility.Create "Foo" {} + )"); + + CHECK_EQ("*unknown*", toString(requireType("a"))); +} + +TEST_CASE_FIXTURE(Fixture, "replace_every_free_type_when_unifying_a_complex_function_with_any") +{ + CheckResult result = check(R"( + local a: any + local b + for _, i in pairs(a) do + b = i + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("any", toString(requireType("b"))); +} + +TEST_CASE_FIXTURE(Fixture, "call_to_any_yields_any") +{ + CheckResult result = check(R"( + local a: any + local b = a() + )"); + + REQUIRE_EQ("any", toString(requireType("b"))); +} + +TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfAny") +{ + CheckResult result = check(R"( +local x: any = {} +function x:y(z: number) + local s: string = z +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfError") +{ + CheckResult result = check(R"( +local x = (true).foo +function x:y(z: number) + local s: string = z +end +)"); + + LUAU_REQUIRE_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "metatable_of_any_can_be_a_table") +{ + CheckResult result = check(R"( +--!strict +local T: any +T = {} +T.__index = T +function T.new(...) + local self = {} + setmetatable(self, T) + self:construct(...) + return self +end +function T:construct(index) +end +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "type_error_addition") +{ + CheckResult result = check(R"( +--!strict +local foo = makesandwich() +local bar = foo.nutrition + 100 + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + // We should definitely get this error + CHECK_EQ("Unknown global 'makesandwich'", toString(result.errors[0])); + // We get this error if makesandwich() returns a free type + // CHECK_EQ("Unknown type used in + operation; consider adding a type annotation to 'foo'", toString(result.errors[1])); +} + +TEST_CASE_FIXTURE(Fixture, "prop_access_on_any_with_other_options") +{ + CheckResult result = check(R"( + local function f(thing: any | string) + local foo = thing.SomeRandomKey + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_SUITE_END(); diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 8da655b..ec20a2c 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -871,6 +871,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types") ScopedFastFlag sff[]{ {"LuauAssertStripsFalsyTypes", true}, {"LuauDiscriminableUnions2", true}, + {"LuauWidenIfSupertypeIsFree2", true}, }; CheckResult result = check(R"( @@ -879,6 +880,26 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types") end )"); + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("((boolean | number)?) -> boolean | number", toString(requireType("f"))); +} + +TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types2") +{ + ScopedFastFlag sff[]{ + {"LuauParseSingletonTypes", true}, + {"LuauSingletonTypes", true}, + {"LuauAssertStripsFalsyTypes", true}, + {"LuauDiscriminableUnions2", true}, + {"LuauWidenIfSupertypeIsFree2", true}, + }; + + CheckResult result = check(R"( + local function f(x: (number | boolean)?): number | true + return assert(x) + end + )"); + LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f"))); } @@ -958,4 +979,43 @@ a:b({}) CHECK_EQ(result.errors[1], (TypeError{Location{{3, 0}, {3, 5}}, CountMismatch{2, 1}})); } +TEST_CASE_FIXTURE(Fixture, "typeof_unresolved_function") +{ + CheckResult result = check(R"( +local function f(a: typeof(f)) end +)"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Unknown global 'f'", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "no_persistent_typelevel_change") +{ + TypeId mathTy = requireType(typeChecker.globalScope, "math"); + REQUIRE(mathTy); + TableTypeVar* ttv = getMutable(mathTy); + REQUIRE(ttv); + const FunctionTypeVar* ftv = get(ttv->props["frexp"].type); + REQUIRE(ftv); + auto original = ftv->level; + + CheckResult result = check("local a = math.frexp"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK(ftv->level.level == original.level); + CHECK(ftv->level.subLevel == original.subLevel); +} + +TEST_CASE_FIXTURE(Fixture, "global_singleton_types_are_sealed") +{ + CheckResult result = check(R"( +local function f(x: string) + local p = x:split('a') + p = table.pack(table.unpack(p, 1, #p - 1)) + return p +end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index c6d5579..898d890 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -293,4 +293,22 @@ TEST_CASE_FIXTURE(Fixture, "documentation_symbols_dont_attach_to_persistent_type CHECK_EQ(ty->type->documentationSymbol, std::nullopt); } +TEST_CASE_FIXTURE(Fixture, "single_class_type_identity_in_global_types") +{ + ScopedFastFlag luauCloneDeclaredGlobals{"LuauCloneDeclaredGlobals", true}; + + loadDefinition(R"( +declare class Cls +end + +declare GetCls: () -> (Cls) + )"); + + CheckResult result = check(R"( +local s : Cls = GetCls() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp new file mode 100644 index 0000000..4288098 --- /dev/null +++ b/tests/TypeInfer.functions.test.cpp @@ -0,0 +1,1338 @@ +// 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/BuiltinDefinitions.h" +#include "Luau/Scope.h" +#include "Luau/TypeInfer.h" +#include "Luau/TypeVar.h" +#include "Luau/VisitTypeVar.h" + +#include "Fixture.h" + +#include "doctest.h" + +using namespace Luau; + +TEST_SUITE_BEGIN("TypeInferFunctions"); + +TEST_CASE_FIXTURE(Fixture, "tc_function") +{ + CheckResult result = check("function five() return 5 end"); + LUAU_REQUIRE_NO_ERRORS(result); + + const FunctionTypeVar* fiveType = get(requireType("five")); + REQUIRE(fiveType != nullptr); +} + +TEST_CASE_FIXTURE(Fixture, "check_function_bodies") +{ + CheckResult result = check("function myFunction() local a = 0 a = true end"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 44}, Position{0, 48}}, TypeMismatch{ + typeChecker.numberType, + typeChecker.booleanType, + }})); +} + +TEST_CASE_FIXTURE(Fixture, "infer_return_type") +{ + CheckResult result = check("function take_five() return 5 end"); + LUAU_REQUIRE_NO_ERRORS(result); + + const FunctionTypeVar* takeFiveType = get(requireType("take_five")); + REQUIRE(takeFiveType != nullptr); + + std::vector retVec = flatten(takeFiveType->retType).first; + REQUIRE(!retVec.empty()); + + REQUIRE_EQ(*follow(retVec[0]), *typeChecker.numberType); +} + +TEST_CASE_FIXTURE(Fixture, "infer_from_function_return_type") +{ + CheckResult result = check("function take_five() return 5 end local five = take_five()"); + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(*typeChecker.numberType, *follow(requireType("five"))); +} + +TEST_CASE_FIXTURE(Fixture, "infer_that_function_does_not_return_a_table") +{ + CheckResult result = check(R"( + function take_five() + return 5 + end + + take_five().prop = 888 + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(result.errors[0], (TypeError{Location{Position{5, 8}, Position{5, 24}}, NotATable{typeChecker.numberType}})); +} + +TEST_CASE_FIXTURE(Fixture, "vararg_functions_should_allow_calls_of_any_types_and_size") +{ + CheckResult result = check(R"( + function f(...) end + + f(1) + f("foo", 2) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "vararg_function_is_quantified") +{ + CheckResult result = check(R"( + local T = {} + function T.f(...) + local result = {} + + for i = 1, select("#", ...) do + local dictionary = select(i, ...) + for key, value in pairs(dictionary) do + result[key] = value + end + end + + return result + end + + return T + )"); + + auto r = first(getMainModule()->getModuleScope()->returnType); + REQUIRE(r); + + TableTypeVar* ttv = getMutable(*r); + REQUIRE(ttv); + + TypeId k = ttv->props["f"].type; + REQUIRE(k); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "list_only_alternative_overloads_that_match_argument_count") +{ + CheckResult result = check(R"( + local multiply: ((number)->number) & ((number)->string) & ((number, number)->number) + multiply("") + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK_EQ(typeChecker.numberType, tm->wantedType); + CHECK_EQ(typeChecker.stringType, tm->givenType); + + ExtraInformation* ei = get(result.errors[1]); + REQUIRE(ei); + CHECK_EQ("Other overloads are also not viable: (number) -> string", ei->message); +} + +TEST_CASE_FIXTURE(Fixture, "list_all_overloads_if_no_overload_takes_given_argument_count") +{ + CheckResult result = check(R"( + local multiply: ((number)->number) & ((number)->string) & ((number, number)->number) + multiply() + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + + GenericError* ge = get(result.errors[0]); + REQUIRE(ge); + CHECK_EQ("No overload for function accepts 0 arguments.", ge->message); + + ExtraInformation* ei = get(result.errors[1]); + REQUIRE(ei); + CHECK_EQ("Available overloads: (number) -> number; (number) -> string; and (number, number) -> number", ei->message); +} + +TEST_CASE_FIXTURE(Fixture, "dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists") +{ + CheckResult result = check(R"( + local multiply: ((number)->number) & ((number)->string) & ((number, number)->number) + multiply(1, "") + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK_EQ(typeChecker.numberType, tm->wantedType); + CHECK_EQ(typeChecker.stringType, tm->givenType); +} + +TEST_CASE_FIXTURE(Fixture, "infer_return_type_from_selected_overload") +{ + CheckResult result = check(R"( + type T = {method: ((T, number) -> number) & ((number) -> string)} + local T: T + + local a = T.method(T, 4) + local b = T.method(5) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("number", toString(requireType("a"))); + CHECK_EQ("string", toString(requireType("b"))); +} + +TEST_CASE_FIXTURE(Fixture, "too_many_arguments") +{ + CheckResult result = check(R"( + --!nonstrict + + function g(a: number) end + + g() + + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + auto err = result.errors[0]; + auto acm = get(err); + REQUIRE(acm); + + CHECK_EQ(1, acm->expected); + CHECK_EQ(0, acm->actual); +} + +TEST_CASE_FIXTURE(Fixture, "recursive_function") +{ + CheckResult result = check(R"( + function count(n: number) + if n == 0 then + return 0 + else + return count(n - 1) + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "lambda_form_of_local_function_cannot_be_recursive") +{ + CheckResult result = check(R"( + local f = function() return f() end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "recursive_local_function") +{ + CheckResult result = check(R"( + local function count(n: number) + if n == 0 then + return 0 + else + return count(n - 1) + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +// FIXME: This and the above case get handled very differently. It's pretty dumb. +// We really should unify the two code paths, probably by deleting AstStatFunction. +TEST_CASE_FIXTURE(Fixture, "another_recursive_local_function") +{ + CheckResult result = check(R"( + local count + function count(n: number) + if n == 0 then + return 0 + else + return count(n - 1) + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "cyclic_function_type_in_rets") +{ + CheckResult result = check(R"( + function f() + return f + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("t1 where t1 = () -> t1", toString(requireType("f"))); +} + +TEST_CASE_FIXTURE(Fixture, "cyclic_function_type_in_args") +{ + CheckResult result = check(R"( + function f(g) + return f(f) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("t1 where t1 = (t1) -> ()", toString(requireType("f"))); +} + +TEST_CASE_FIXTURE(Fixture, "another_higher_order_function") +{ + CheckResult result = check(R"( + local Get_des + function Get_des(func) + Get_des(func) + end + + local function f(d) + d:IsA("BasePart") + d.Parent:FindFirstChild("Humanoid") + d:IsA("Decal") + end + Get_des(f) + + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "another_other_higher_order_function") +{ + CheckResult result = check(R"( + local d + d:foo() + d:foo() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "local_function") +{ + CheckResult result = check(R"( + function f() + return 8 + end + + function g() + local function f() + return 'hello' + end + return f + end + + local h = g() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + TypeId h = follow(requireType("h")); + + const FunctionTypeVar* ftv = get(h); + REQUIRE(ftv != nullptr); + + std::optional rt = first(ftv->retType); + REQUIRE(bool(rt)); + + TypeId retType = follow(*rt); + CHECK_EQ(PrimitiveTypeVar::String, getPrimitiveType(retType)); +} + +TEST_CASE_FIXTURE(Fixture, "func_expr_doesnt_leak_free") +{ + CheckResult result = check(R"( + local p = function(x) return x end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + const Luau::FunctionTypeVar* fn = get(requireType("p")); + REQUIRE(fn); + auto ret = first(fn->retType); + REQUIRE(ret); + REQUIRE(get(follow(*ret))); +} + +TEST_CASE_FIXTURE(Fixture, "first_argument_can_be_optional") +{ + CheckResult result = check(R"( + local T = {} + function T.new(a: number?, b: number?, c: number?) return 5 end + local m = T.new() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); +} + +TEST_CASE_FIXTURE(Fixture, "it_is_ok_not_to_supply_enough_retvals") +{ + CheckResult result = check(R"( + function get_two() return 5, 6 end + + local a = get_two() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); +} + +TEST_CASE_FIXTURE(Fixture, "duplicate_functions2") +{ + CheckResult result = check(R"( + function foo() end + + function bar() + local function foo() end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(0, result); +} + +TEST_CASE_FIXTURE(Fixture, "duplicate_functions_allowed_in_nonstrict") +{ + CheckResult result = check(R"( + --!nonstrict + function foo() end + + function foo() end + + function bar() + local function foo() end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "duplicate_functions_with_different_signatures_not_allowed_in_nonstrict") +{ + CheckResult result = check(R"( + --!nonstrict + function foo(): number + return 1 + end + foo() + + function foo(n: number): number + return 2 + end + foo() + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK_EQ("() -> number", toString(tm->wantedType)); + CHECK_EQ("(number) -> number", toString(tm->givenType)); +} + +TEST_CASE_FIXTURE(Fixture, "complicated_return_types_require_an_explicit_annotation") +{ + CheckResult result = check(R"( + local i = 0 + function most_of_the_natural_numbers(): number? + if i < 10 then + i = i + 1 + return i + else + return nil + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + const FunctionTypeVar* functionType = get(requireType("most_of_the_natural_numbers")); + + std::optional retType = first(functionType->retType); + REQUIRE(retType); + CHECK(get(*retType)); +} + +TEST_CASE_FIXTURE(Fixture, "infer_higher_order_function") +{ + CheckResult result = check(R"( + function apply(f, x) + return f(x) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + const FunctionTypeVar* ftv = get(requireType("apply")); + REQUIRE(ftv != nullptr); + + std::vector argVec = flatten(ftv->argTypes).first; + + REQUIRE_EQ(2, argVec.size()); + + const FunctionTypeVar* fType = get(follow(argVec[0])); + REQUIRE(fType != nullptr); + + std::vector fArgs = flatten(fType->argTypes).first; + + TypeId xType = argVec[1]; + + CHECK_EQ(1, fArgs.size()); + CHECK_EQ(xType, fArgs[0]); +} + +TEST_CASE_FIXTURE(Fixture, "higher_order_function_2") +{ + CheckResult result = check(R"( + function bottomupmerge(comp, a, b, left, mid, right) + local i, j = left, mid + for k = left, right do + if i < mid and (j > right or not comp(a[j], a[i])) then + b[k] = a[i] + i = i + 1 + else + b[k] = a[j] + j = j + 1 + end + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + const FunctionTypeVar* ftv = get(requireType("bottomupmerge")); + REQUIRE(ftv != nullptr); + + std::vector argVec = flatten(ftv->argTypes).first; + + REQUIRE_EQ(6, argVec.size()); + + const FunctionTypeVar* fType = get(follow(argVec[0])); + REQUIRE(fType != nullptr); +} + +TEST_CASE_FIXTURE(Fixture, "higher_order_function_3") +{ + CheckResult result = check(R"( + function swap(p) + local t = p[0] + p[0] = p[1] + p[1] = t + return nil + end + + function swapTwice(p) + swap(p) + swap(p) + return p + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + const FunctionTypeVar* ftv = get(requireType("swapTwice")); + REQUIRE(ftv != nullptr); + + std::vector argVec = flatten(ftv->argTypes).first; + + REQUIRE_EQ(1, argVec.size()); + + const TableTypeVar* argType = get(follow(argVec[0])); + REQUIRE(argType != nullptr); + + CHECK(bool(argType->indexer)); +} + +TEST_CASE_FIXTURE(Fixture, "higher_order_function_4") +{ + CheckResult result = check(R"( + function bottomupmerge(comp, a, b, left, mid, right) + local i, j = left, mid + for k = left, right do + if i < mid and (j > right or not comp(a[j], a[i])) then + b[k] = a[i] + i = i + 1 + else + b[k] = a[j] + j = j + 1 + end + end + end + + function mergesort(arr, comp) + local work = {} + for i = 1, #arr do + work[i] = arr[i] + end + local width = 1 + while width < #arr do + for i = 1, #arr, 2*width do + bottomupmerge(comp, arr, work, i, math.min(i+width, #arr), math.min(i+2*width-1, #arr)) + end + local temp = work + work = arr + arr = temp + width = width * 2 + end + return arr + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); + + /* + * mergesort takes two arguments: an array of some type T and a function that takes two Ts. + * We must assert that these two types are in fact the same type. + * In other words, comp(arr[x], arr[y]) is well-typed. + */ + + const FunctionTypeVar* ftv = get(requireType("mergesort")); + REQUIRE(ftv != nullptr); + + std::vector argVec = flatten(ftv->argTypes).first; + + REQUIRE_EQ(2, argVec.size()); + + const TableTypeVar* arg0 = get(follow(argVec[0])); + REQUIRE(arg0 != nullptr); + REQUIRE(bool(arg0->indexer)); + + const FunctionTypeVar* arg1 = get(follow(argVec[1])); + REQUIRE(arg1 != nullptr); + REQUIRE_EQ(2, size(arg1->argTypes)); + + std::vector arg1Args = flatten(arg1->argTypes).first; + + CHECK_EQ(*arg0->indexer->indexResultType, *arg1Args[0]); + CHECK_EQ(*arg0->indexer->indexResultType, *arg1Args[1]); +} + +TEST_CASE_FIXTURE(Fixture, "mutual_recursion") +{ + CheckResult result = check(R"( + --!strict + + function newPlayerCharacter() + startGui() -- Unknown symbol 'startGui' + end + + local characterAddedConnection: any + function startGui() + characterAddedConnection = game:GetService("Players").LocalPlayer.CharacterAdded:connect(newPlayerCharacter) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); +} + +TEST_CASE_FIXTURE(Fixture, "toposort_doesnt_break_mutual_recursion") +{ + CheckResult result = check(R"( + --!strict + local x = nil + function f() g() end + -- make sure print(x) doesn't get toposorted here, breaking the mutual block + function g() x = f end + print(x) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); +} + +TEST_CASE_FIXTURE(Fixture, "check_function_before_lambda_that_uses_it") +{ + CheckResult result = check(R"( + --!nonstrict + + function f() + return 114 + end + + return function() + return f():andThen() + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "it_is_ok_to_oversaturate_a_higher_order_function_argument") +{ + CheckResult result = check(R"( + function onerror() end + function foo() end + xpcall(foo, onerror) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "another_indirect_function_case_where_it_is_ok_to_provide_too_many_arguments") +{ + CheckResult result = check(R"( + local mycb: (number, number) -> () + + function f() end + + mycb = f + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_nonstrict") +{ + CheckResult result = check(R"( + --!nonstrict + + local function f1(v): number? + if v then + return 1 + end + end + + local function f2(v) + if v then + return 1 + end + end + + local function f3(v): () + if v then + return + end + end + + local function f4(v) + if v then + return + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + FunctionExitsWithoutReturning* err = get(result.errors[0]); + CHECK(err); +} + +TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_strict") +{ + CheckResult result = check(R"( + --!strict + + local function f1(v): number? + if v then + return 1 + end + end + + local function f2(v) + if v then + return 1 + end + end + + local function f3(v): () + if v then + return + end + end + + local function f4(v) + if v then + return + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + FunctionExitsWithoutReturning* annotatedErr = get(result.errors[0]); + CHECK(annotatedErr); + + FunctionExitsWithoutReturning* inferredErr = get(result.errors[1]); + CHECK(inferredErr); +} + +TEST_CASE_FIXTURE(Fixture, "calling_function_with_incorrect_argument_type_yields_errors_spanning_argument") +{ + CheckResult result = check(R"( + function foo(a: number, b: string) end + + foo("Test", 123) + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + + CHECK_EQ(result.errors[0], (TypeError{Location{Position{3, 12}, Position{3, 18}}, TypeMismatch{ + typeChecker.numberType, + typeChecker.stringType, + }})); + + CHECK_EQ(result.errors[1], (TypeError{Location{Position{3, 20}, Position{3, 23}}, TypeMismatch{ + typeChecker.stringType, + typeChecker.numberType, + }})); +} + +TEST_CASE_FIXTURE(Fixture, "calling_function_with_anytypepack_doesnt_leak_free_types") +{ + CheckResult result = check(R"( + --!nonstrict + + function Test(a) + return 1, "" + end + + + local tab = {} + table.insert(tab, Test(1)); + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + ToStringOptions opts; + opts.exhaustive = true; + opts.maxTableLength = 0; + + CHECK_EQ("{any}", toString(requireType("tab"), opts)); +} + +TEST_CASE_FIXTURE(Fixture, "too_many_return_values") +{ + CheckResult result = check(R"( + --!strict + + function f() + return 55 + end + + local a, b = f() + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CountMismatch* acm = get(result.errors[0]); + REQUIRE(acm); + CHECK_EQ(acm->context, CountMismatch::Result); + CHECK_EQ(acm->expected, 1); + CHECK_EQ(acm->actual, 2); +} + +TEST_CASE_FIXTURE(Fixture, "ignored_return_values") +{ + CheckResult result = check(R"( + --!strict + + function f() + return 55, "" + end + + local a = f() + )"); + + LUAU_REQUIRE_ERROR_COUNT(0, result); +} + +TEST_CASE_FIXTURE(Fixture, "function_does_not_return_enough_values") +{ + CheckResult result = check(R"( + --!strict + + function f(): (number, string) + return 55 + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CountMismatch* acm = get(result.errors[0]); + REQUIRE(acm); + CHECK_EQ(acm->context, CountMismatch::Return); + CHECK_EQ(acm->expected, 2); + CHECK_EQ(acm->actual, 1); +} + +TEST_CASE_FIXTURE(Fixture, "function_cast_error_uses_correct_language") +{ + CheckResult result = check(R"( + function foo(a, b): number + return 0 + end + + local a: (string)->number = foo + local b: (number, number)->(number, number) = foo + + local c: (string, number)->number = foo -- no error + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + + auto tm1 = get(result.errors[0]); + REQUIRE(tm1); + + CHECK_EQ("(string) -> number", toString(tm1->wantedType)); + CHECK_EQ("(string, *unknown*) -> number", toString(tm1->givenType)); + + auto tm2 = get(result.errors[1]); + REQUIRE(tm2); + + CHECK_EQ("(number, number) -> (number, number)", toString(tm2->wantedType)); + CHECK_EQ("(string, *unknown*) -> number", toString(tm2->givenType)); +} + +TEST_CASE_FIXTURE(Fixture, "no_lossy_function_type") +{ + CheckResult result = check(R"( + --!strict + local tbl = {} + function tbl:abc(a: number, b: number) + return a + end + tbl:abc(1, 2) -- Line 6 + -- | Column 14 + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + TypeId type = requireTypeAtPosition(Position(6, 14)); + CHECK_EQ("(tbl, number, number) -> number", toString(type)); + auto ftv = get(type); + REQUIRE(ftv); + CHECK(ftv->hasSelf); +} + +TEST_CASE_FIXTURE(Fixture, "record_matching_overload") +{ + CheckResult result = check(R"( + type Overload = ((string) -> string) & ((number) -> number) + local abc: Overload + abc(1) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // AstExprCall is the node that has the overload stored on it. + // findTypeAtPosition will look at the AstExprLocal, but this is not what + // we want to look at. + std::vector ancestry = findAstAncestryOfPosition(*getMainSourceModule(), Position(3, 10)); + REQUIRE_GE(ancestry.size(), 2); + AstExpr* parentExpr = ancestry[ancestry.size() - 2]->asExpr(); + REQUIRE(bool(parentExpr)); + REQUIRE(parentExpr->is()); + + ModulePtr module = getMainModule(); + auto it = module->astOverloadResolvedTypes.find(parentExpr); + REQUIRE(it); + CHECK_EQ(toString(*it), "(number) -> number"); +} + +TEST_CASE_FIXTURE(Fixture, "return_type_by_overload") +{ + ScopedFastFlag sff{"LuauErrorRecoveryType", true}; + + CheckResult result = check(R"( + type Overload = ((string) -> string) & ((number, number) -> number) + local abc: Overload + local x = abc(true) + local y = abc(true,true) + local z = abc(true,true,true) + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK_EQ("string", toString(requireType("x"))); + CHECK_EQ("number", toString(requireType("y"))); + // Should this be string|number? + CHECK_EQ("string", toString(requireType("z"))); +} + +TEST_CASE_FIXTURE(Fixture, "infer_anonymous_function_arguments") +{ + // Simple direct arg to arg propagation + CheckResult result = check(R"( +type Table = { x: number, y: number } +local function f(a: (Table) -> number) return a({x = 1, y = 2}) end +f(function(a) return a.x + a.y end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // An optional function is accepted, but since we already provide a function, nil can be ignored + result = check(R"( +type Table = { x: number, y: number } +local function f(a: ((Table) -> number)?) if a then return a({x = 1, y = 2}) else return 0 end end +f(function(a) return a.x + a.y end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // Make sure self calls match correct index + result = check(R"( +type Table = { x: number, y: number } +local x = {} +x.b = {x = 1, y = 2} +function x:f(a: (Table) -> number) return a(self.b) end +x:f(function(a) return a.x + a.y end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // Mix inferred and explicit argument types + result = check(R"( +function f(a: (a: number, b: number, c: boolean) -> number) return a(1, 2, true) end +f(function(a: number, b, c) return c and a + b or b - a end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // Anonymous function has a variadic pack + result = check(R"( +type Table = { x: number, y: number } +local function f(a: (Table) -> number) return a({x = 1, y = 2}) end +f(function(...) return select(1, ...).z end) + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0])); + + // Can't accept more arguments than provided + result = check(R"( +function f(a: (a: number, b: number) -> number) return a(1, 2) end +f(function(a, b, c, ...) return a + b end) + )"); + + LUAU_REQUIRE_ERRORS(result); + + CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number' +caused by: + Argument count mismatch. Function expects 3 arguments, but only 2 are specified)", + toString(result.errors[0])); + + // Infer from variadic packs into elements + result = check(R"( +function f(a: (...number) -> number) return a(1, 2) end +f(function(a, b) return a + b end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // Infer from variadic packs into variadic packs + result = check(R"( +type Table = { x: number, y: number } +function f(a: (...Table) -> number) return a({x = 1, y = 2}, {x = 3, y = 4}) end +f(function(a, ...) local b = ... return b.z end) + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0])); + + // Return type inference + result = check(R"( +type Table = { x: number, y: number } +function f(a: (number) -> Table) return a(4) end +f(function(x) return x * 2 end) + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Type 'number' could not be converted into 'Table'", toString(result.errors[0])); + + // Return type doesn't inference 'nil' + result = check(R"( +function f(a: (number) -> nil) return a(4) end +f(function(x) print(x) end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "infer_anonymous_function_arguments") +{ + // Simple direct arg to arg propagation + CheckResult result = check(R"( +type Table = { x: number, y: number } +local function f(a: (Table) -> number) return a({x = 1, y = 2}) end +f(function(a) return a.x + a.y end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // An optional function is accepted, but since we already provide a function, nil can be ignored + result = check(R"( +type Table = { x: number, y: number } +local function f(a: ((Table) -> number)?) if a then return a({x = 1, y = 2}) else return 0 end end +f(function(a) return a.x + a.y end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // Make sure self calls match correct index + result = check(R"( +type Table = { x: number, y: number } +local x = {} +x.b = {x = 1, y = 2} +function x:f(a: (Table) -> number) return a(self.b) end +x:f(function(a) return a.x + a.y end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // Mix inferred and explicit argument types + result = check(R"( +function f(a: (a: number, b: number, c: boolean) -> number) return a(1, 2, true) end +f(function(a: number, b, c) return c and a + b or b - a end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // Anonymous function has a variadic pack + result = check(R"( +type Table = { x: number, y: number } +local function f(a: (Table) -> number) return a({x = 1, y = 2}) end +f(function(...) return select(1, ...).z end) + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0])); + + // Can't accept more arguments than provided + result = check(R"( +function f(a: (a: number, b: number) -> number) return a(1, 2) end +f(function(a, b, c, ...) return a + b end) + )"); + + LUAU_REQUIRE_ERRORS(result); + + CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number' +caused by: + Argument count mismatch. Function expects 3 arguments, but only 2 are specified)", + toString(result.errors[0])); + + // Infer from variadic packs into elements + result = check(R"( +function f(a: (...number) -> number) return a(1, 2) end +f(function(a, b) return a + b end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // Infer from variadic packs into variadic packs + result = check(R"( +type Table = { x: number, y: number } +function f(a: (...Table) -> number) return a({x = 1, y = 2}, {x = 3, y = 4}) end +f(function(a, ...) local b = ... return b.z end) + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0])); + + // Return type inference + result = check(R"( +type Table = { x: number, y: number } +function f(a: (number) -> Table) return a(4) end +f(function(x) return x * 2 end) + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Type 'number' could not be converted into 'Table'", toString(result.errors[0])); + + // Return type doesn't inference 'nil' + result = check(R"( +function f(a: (number) -> nil) return a(4) end +f(function(x) print(x) end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "infer_anonymous_function_arguments_outside_call") +{ + CheckResult result = check(R"( +type Table = { x: number, y: number } +local f: (Table) -> number = function(t) return t.x + t.y end + +type TableWithFunc = { x: number, y: number, f: (number, number) -> number } +local a: TableWithFunc = { x = 3, y = 4, f = function(a, b) return a + b end } + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "infer_return_value_type") +{ + CheckResult result = check(R"( +local function f(): {string|number} + return {1, "b", 3} +end + +local function g(): (number, {string|number}) + return 4, {1, "b", 3} +end + +local function h(): ...{string|number} + return {4}, {1, "b", 3}, {"s"} +end + +local function i(): ...{string|number} + return {1, "b", 3}, h() +end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg_count") +{ + CheckResult result = check(R"( +type A = (number, number) -> string +type B = (number) -> string + +local a: A +local b: B = a + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number) -> string' +caused by: + Argument count mismatch. Function expects 2 arguments, but only 1 is specified)"); +} + +TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg") +{ + CheckResult result = check(R"( +type A = (number, number) -> string +type B = (number, string) -> string + +local a: A +local b: B = a + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number, string) -> string' +caused by: + Argument #2 type is not compatible. Type 'string' could not be converted into 'number')"); +} + +TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_count") +{ + CheckResult result = check(R"( +type A = (number, number) -> (number) +type B = (number, number) -> (number, boolean) + +local a: A +local b: B = a + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> number' could not be converted into '(number, number) -> (number, boolean)' +caused by: + Function only returns 1 value. 2 are required here)"); +} + +TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret") +{ + CheckResult result = check(R"( +type A = (number, number) -> string +type B = (number, number) -> number + +local a: A +local b: B = a + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number, number) -> number' +caused by: + Return type is not compatible. Type 'string' could not be converted into 'number')"); +} + +TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_mult") +{ + CheckResult result = check(R"( +type A = (number, number) -> (number, string) +type B = (number, number) -> (number, boolean) + +local a: A +local b: B = a + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(toString(result.errors[0]), + R"(Type '(number, number) -> (number, string)' could not be converted into '(number, number) -> (number, boolean)' +caused by: + Return #2 type is not compatible. Type 'string' could not be converted into 'boolean')"); +} + +TEST_CASE_FIXTURE(Fixture, "function_decl_quantify_right_type") +{ + ScopedFastFlag statFunctionSimplify{"LuauStatFunctionSimplify2", true}; + + fileResolver.source["game/isAMagicMock"] = R"( +--!nonstrict +return function(value) + return false +end + )"; + + CheckResult result = check(R"( +--!nonstrict +local MagicMock = {} +MagicMock.is = require(game.isAMagicMock) + +function MagicMock.is(value) + return false +end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "function_decl_non_self_sealed_overwrite") +{ + ScopedFastFlag statFunctionSimplify{"LuauStatFunctionSimplify2", true}; + + CheckResult result = check(R"( +function string.len(): number + return 1 +end + )"); + + LUAU_REQUIRE_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "strict_mode_ok_with_missing_arguments") +{ + ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true}; + + CheckResult result = check(R"( + local function f(x: any) end + f() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "function_statement_sealed_table_assignment_through_indexer") +{ + ScopedFastFlag statFunctionSimplify{"LuauStatFunctionSimplify2", true}; + + CheckResult result = check(R"( +local t: {[string]: () -> number} = {} + +function t.a() return 1 end -- OK +function t:b() return 2 end -- not OK + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(R"(Type '(*unknown*) -> number' could not be converted into '() -> number' +caused by: + Argument count mismatch. Function expects 1 argument, but none are specified)", + toString(result.errors[0])); +} + +TEST_SUITE_END(); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 547fbab..f360a77 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -1,6 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypeInfer.h" #include "Luau/TypeVar.h" +#include "Luau/Scope.h" + +#include #include "Fixture.h" @@ -830,5 +833,303 @@ wrapper(test2, 1, "", 3) CHECK_EQ(toString(result.errors[0]), R"(Argument count mismatch. Function expects 1 argument, but 4 are specified)"); } +TEST_CASE_FIXTURE(Fixture, "generic_function") +{ + CheckResult result = check(R"( + function id(x) return x end + local a = id(55) + local b = id(nil) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(*typeChecker.numberType, *requireType("a")); + CHECK_EQ(*typeChecker.nilType, *requireType("b")); +} + +TEST_CASE_FIXTURE(Fixture, "generic_table_method") +{ + CheckResult result = check(R"( + local T = {} + + function T:bar(i) + return i + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + TypeId tType = requireType("T"); + TableTypeVar* tTable = getMutable(tType); + REQUIRE(tTable != nullptr); + + TypeId barType = tTable->props["bar"].type; + REQUIRE(barType != nullptr); + + const FunctionTypeVar* ftv = get(follow(barType)); + REQUIRE_MESSAGE(ftv != nullptr, "Should be a function: " << *barType); + + std::vector args = flatten(ftv->argTypes).first; + TypeId argType = args.at(1); + + CHECK_MESSAGE(get(argType), "Should be generic: " << *barType); +} + +TEST_CASE_FIXTURE(Fixture, "correctly_instantiate_polymorphic_member_functions") +{ + CheckResult result = check(R"( + local T = {} + + function T:foo() + return T:bar(5) + end + + function T:bar(i) + return i + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); + + const TableTypeVar* t = get(requireType("T")); + REQUIRE(t != nullptr); + + std::optional fooProp = get(t->props, "foo"); + REQUIRE(bool(fooProp)); + + const FunctionTypeVar* foo = get(follow(fooProp->type)); + REQUIRE(bool(foo)); + + std::optional ret_ = first(foo->retType); + REQUIRE(bool(ret_)); + TypeId ret = follow(*ret_); + + REQUIRE_EQ(getPrimitiveType(ret), PrimitiveTypeVar::Number); +} + +/* + * We had a bug in instantiation where the argument types of 'f' and 'g' would be inferred as + * f {+ method: function(): (t2, T3...) +} + * g {+ method: function({+ method: function(): (t2, T3...) +}): (t5, T6...) +} + * + * The type of 'g' is totally wrong as t2 and t5 should be unified, as should T3 with T6. + * + * The correct unification of the argument to 'g' is + * + * {+ method: function(): (t5, T6...) +} + */ +TEST_CASE_FIXTURE(Fixture, "instantiate_cyclic_generic_function") +{ + auto result = check(R"( + function f(o) + o:method() + end + + function g(o) + f(o) + end + )"); + + TypeId g = requireType("g"); + const FunctionTypeVar* gFun = get(g); + REQUIRE(gFun != nullptr); + + auto optionArg = first(gFun->argTypes); + REQUIRE(bool(optionArg)); + + TypeId arg = follow(*optionArg); + const TableTypeVar* argTable = get(arg); + REQUIRE(argTable != nullptr); + + std::optional methodProp = get(argTable->props, "method"); + REQUIRE(bool(methodProp)); + + const FunctionTypeVar* methodFunction = get(methodProp->type); + REQUIRE(methodFunction != nullptr); + + std::optional methodArg = first(methodFunction->argTypes); + REQUIRE(bool(methodArg)); + + REQUIRE_EQ(follow(*methodArg), follow(arg)); +} + +TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments") +{ + CheckResult result = check(R"( + function foo(a, b) + return a(b) + end + + function bar() + local c: ((number)->number, number)->number = foo -- no error + c = foo -- no error + local d: ((number)->number, string)->number = foo -- error from arg 2 (string) not being convertable to number from the call a(b) + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType)); + CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType)); +} + +TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2") +{ + CheckResult result = check(R"( + function foo(a, b) + return a(b) + end + + function bar() + local _: (string, string)->number = foo -- string cannot be converted to (string)->number + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK_EQ("(string, string) -> number", toString(tm->wantedType)); + CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType)); +} + +TEST_CASE_FIXTURE(Fixture, "self_recursive_instantiated_param") +{ + ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true}; + + // Mutability in type function application right now can create strange recursive types + CheckResult result = check(R"( +type Table = { a: number } +type Self = T +local a: Self + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ(toString(requireType("a")), "Table"); +} + +TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_quantifying") +{ + CheckResult result = check(R"( + function _(l0:t0): (any, ()->()) + end + + type t0 = t0 | {} + )"); + + CHECK_LE(0, result.errors.size()); + + std::optional t0 = getMainModule()->getModuleScope()->lookupType("t0"); + REQUIRE(t0); + CHECK_EQ("*unknown*", toString(t0->type)); + + auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) { + return get(err); + }); + CHECK(it != result.errors.end()); +} + +TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument") +{ + ScopedFastFlag sff{"LuauUnsealedTableLiteral", true}; + + CheckResult result = check(R"( +local function sum(x: a, y: a, f: (a, a) -> a) return f(x, y) end +return sum(2, 3, function(a, b) return a + b end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + result = check(R"( +local function map(arr: {a}, f: (a) -> b) local r = {} for i,v in ipairs(arr) do table.insert(r, f(v)) end return r end +local a = {1, 2, 3} +local r = map(a, function(a) return a + a > 100 end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + REQUIRE_EQ("{boolean}", toString(requireType("r"))); + + check(R"( +local function foldl(arr: {a}, init: b, f: (b, a) -> b) local r = init for i,v in ipairs(arr) do r = f(r, v) end return r end +local a = {1, 2, 3} +local r = foldl(a, {s=0,c=0}, function(a, b) return {s = a.s + b, c = a.c + 1} end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + REQUIRE_EQ("{ c: number, s: number }", toString(requireType("r"))); +} + +TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument_overloaded") +{ + CheckResult result = check(R"( +local function g1(a: T, f: (T) -> T) return f(a) end +local function g2(a: T, b: T, f: (T, T) -> T) return f(a, b) end + +local g12: typeof(g1) & typeof(g2) + +g12(1, function(x) return x + x end) +g12(1, 2, function(x, y) return x + y end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + result = check(R"( +local function g1(a: T, f: (T) -> T) return f(a) end +local function g2(a: T, b: T, f: (T, T) -> T) return f(a, b) end + +local g12: typeof(g1) & typeof(g2) + +g12({x=1}, function(x) return {x=-x.x} end) +g12({x=1}, {x=2}, function(x, y) return {x=x.x + y.x} end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "infer_generic_lib_function_function_argument") +{ + CheckResult result = check(R"( +local a = {{x=4}, {x=7}, {x=1}} +table.sort(a, function(x, y) return x.x < y.x end) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "do_not_infer_generic_functions") +{ + CheckResult result = check(R"( +local function sum(x: a, y: a, f: (a, a) -> a) return f(x, y) end + +local function sumrec(f: typeof(sum)) + return sum(2, 3, function(a, b) return a + b end) +end + +local b = sumrec(sum) -- ok +local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not inferred + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK_EQ("Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '(a, a, (a, a) -> a) -> a'; different number of generic type " + "parameters", + toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "substitution_with_bound_table") +{ + CheckResult result = check(R"( +type A = { x: number } +local a: A = { x = 1 } +local b = a +type B = typeof(b) +type X = T +local c: X + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} TEST_SUITE_END(); diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 26881b5..d146f4e 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -377,4 +377,32 @@ local b: number = a CHECK_EQ(toString(result.errors[0]), R"(Type 'X & Y & Z' could not be converted into 'number'; none of the intersection parts are compatible)"); } +TEST_CASE_FIXTURE(Fixture, "overload_is_not_a_function") +{ + check(R"( +--!nonstrict +function _(...):((typeof(not _))&(typeof(not _)))&((typeof(not _))&(typeof(not _))) +_(...)(setfenv,_,not _,"")[_] = nil +end +do end +_(...)(...,setfenv,_):_G() +)"); +} + +TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_flattenintersection") +{ + CheckResult result = check(R"( + local l0,l0 + repeat + type t0 = ((any)|((any)&((any)|((any)&((any)|(any))))))&(t0) + function _(l0):(t0)&(t0) + while nil do + end + end + until _(_)(_)._ + )"); + + CHECK_LE(0, result.errors.size()); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.loops.test.cpp b/tests/TypeInfer.loops.test.cpp new file mode 100644 index 0000000..30df717 --- /dev/null +++ b/tests/TypeInfer.loops.test.cpp @@ -0,0 +1,473 @@ +// 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/BuiltinDefinitions.h" +#include "Luau/Scope.h" +#include "Luau/TypeInfer.h" +#include "Luau/TypeVar.h" +#include "Luau/VisitTypeVar.h" + +#include "Fixture.h" + +#include "doctest.h" + +using namespace Luau; + +TEST_SUITE_BEGIN("TypeInferLoops"); + +TEST_CASE_FIXTURE(Fixture, "for_loop") +{ + CheckResult result = check(R"( + local q + for i=0, 50, 2 do + q = i + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(*typeChecker.numberType, *requireType("q")); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop") +{ + CheckResult result = check(R"( + local n + local s + for i, v in pairs({ "foo" }) do + n = i + s = v + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(*typeChecker.numberType, *requireType("n")); + CHECK_EQ(*typeChecker.stringType, *requireType("s")); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_with_next") +{ + CheckResult result = check(R"( + local n + local s + for i, v in next, { "foo", "bar" } do + n = i + s = v + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(*typeChecker.numberType, *requireType("n")); + CHECK_EQ(*typeChecker.stringType, *requireType("s")); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_with_an_iterator_of_type_any") +{ + CheckResult result = check(R"( + local it: any + local a, b + for i, v in it do + a, b = i, v + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_should_fail_with_non_function_iterator") +{ + CheckResult result = check(R"( + local foo = "bar" + for i, v in foo do + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_with_just_one_iterator_is_ok") +{ + CheckResult result = check(R"( + local function keys(dictionary) + local new = {} + local index = 1 + + for key in pairs(dictionary) do + new[index] = key + index = index + 1 + end + + return new + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_with_a_custom_iterator_should_type_check") +{ + CheckResult result = check(R"( + local function range(l, h): () -> number + return function() + return l + end + end + + for n: string in range(1, 10) do + print(n) + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_error") +{ + CheckResult result = check(R"( + function f(x) + gobble.prop = x.otherprop + end + + local p + for _, part in i_am_not_defined do + p = part + f(part) + part.thirdprop = false + end + )"); + + CHECK_EQ(2, result.errors.size()); + + TypeId p = requireType("p"); + CHECK_EQ("*unknown*", toString(p)); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_non_function") +{ + CheckResult result = check(R"( + local bad_iter = 5 + + for a in bad_iter() do + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + REQUIRE(get(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_error_on_factory_not_returning_the_right_amount_of_values") +{ + CheckResult result = check(R"( + local function hasDivisors(value: number, table) + return false + end + + function prime_iter(state, index) + while hasDivisors(index, state) do + index += 1 + end + + state[index] = true + return index + end + + function primes1() + return prime_iter, {} + end + + function primes2() + return prime_iter, {}, "" + end + + function primes3() + return prime_iter, {}, 2 + end + + for p in primes1() do print(p) end -- mismatch in argument count + + for p in primes2() do print(p) end -- mismatch in argument types, prime_iter takes {}, number, we are given {}, string + + for p in primes3() do print(p) end -- no error + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + + CountMismatch* acm = get(result.errors[0]); + REQUIRE(acm); + CHECK_EQ(acm->context, CountMismatch::Arg); + CHECK_EQ(2, acm->expected); + CHECK_EQ(1, acm->actual); + + TypeMismatch* tm = get(result.errors[1]); + REQUIRE(tm); + CHECK_EQ(typeChecker.numberType, tm->wantedType); + CHECK_EQ(typeChecker.stringType, tm->givenType); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_error_on_iterator_requiring_args_but_none_given") +{ + CheckResult result = check(R"( + function prime_iter(state, index) + return 1 + end + + for p in prime_iter do print(p) end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CountMismatch* acm = get(result.errors[0]); + REQUIRE(acm); + CHECK_EQ(acm->context, CountMismatch::Arg); + CHECK_EQ(2, acm->expected); + CHECK_EQ(0, acm->actual); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_with_custom_iterator") +{ + CheckResult result = check(R"( + function primes() + return function (state: number) end, 2 + end + + for p, q in primes do + q = "" + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK_EQ(typeChecker.numberType, tm->wantedType); + CHECK_EQ(typeChecker.stringType, tm->givenType); +} + +TEST_CASE_FIXTURE(Fixture, "while_loop") +{ + CheckResult result = check(R"( + local i + while true do + i = 8 + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(*typeChecker.numberType, *requireType("i")); +} + +TEST_CASE_FIXTURE(Fixture, "repeat_loop") +{ + CheckResult result = check(R"( + local i + repeat + i = 'hi' + until true + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(*typeChecker.stringType, *requireType("i")); +} + +TEST_CASE_FIXTURE(Fixture, "repeat_loop_condition_binds_to_its_block") +{ + CheckResult result = check(R"( + repeat + local x = true + until x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "symbols_in_repeat_block_should_not_be_visible_beyond_until_condition") +{ + CheckResult result = check(R"( + repeat + local x = true + until x + + print(x) + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "varlist_declared_by_for_in_loop_should_be_free") +{ + CheckResult result = check(R"( + local T = {} + + function T.f(p) + for i, v in pairs(p) do + T.f(v) + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "properly_infer_iteratee_is_a_free_table") +{ + // In this case, we cannot know the element type of the table {}. It could be anything. + // We therefore must initially ascribe a free typevar to iter. + CheckResult result = check(R"( + for iter in pairs({}) do + iter:g().p = true + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "correctly_scope_locals_while") +{ + CheckResult result = check(R"( + while true do + local a = 1 + end + + print(a) -- oops! + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + UnknownSymbol* us = get(result.errors[0]); + REQUIRE(us); + CHECK_EQ(us->name, "a"); +} + +TEST_CASE_FIXTURE(Fixture, "ipairs_produces_integral_indices") +{ + CheckResult result = check(R"( + local key + for i, e in ipairs({}) do key = i end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + REQUIRE_EQ("number", toString(requireType("key"))); +} + +TEST_CASE_FIXTURE(Fixture, "for_in_loop_where_iteratee_is_free") +{ + // This code doesn't pass typechecking. We just care that it doesn't crash. + (void)check(R"( + --!nonstrict + function _:_(...) + end + + repeat + if _ then + else + _ = ... + end + until _ + + for _ in _() do + end + )"); +} + +TEST_CASE_FIXTURE(Fixture, "unreachable_code_after_infinite_loop") +{ + { + CheckResult result = check(R"( + function unreachablecodepath(a): number + while true do + if a then return 10 end + end + -- unreachable + end + unreachablecodepath(4) + )"); + + LUAU_REQUIRE_ERROR_COUNT(0, result); + } + + { + CheckResult result = check(R"( + function reachablecodepath(a): number + while true do + if a then break end + return 10 + end + + print("x") -- correct error + end + reachablecodepath(4) + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK(get(result.errors[0])); + } + + { + CheckResult result = check(R"( + function unreachablecodepath(a): number + repeat + if a then return 10 end + until false + + -- unreachable + end + unreachablecodepath(4) + )"); + + LUAU_REQUIRE_ERROR_COUNT(0, result); + } + + { + CheckResult result = check(R"( + function reachablecodepath(a, b): number + repeat + if a then break end + + if b then return 10 end + until false + + print("x") -- correct error + end + reachablecodepath(4) + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK(get(result.errors[0])); + } + + { + CheckResult result = check(R"( + function unreachablecodepath(a: number?): number + repeat + return 10 + until a ~= nil + + -- unreachable + end + unreachablecodepath(4) + )"); + + LUAU_REQUIRE_ERROR_COUNT(0, result); + } +} + +TEST_CASE_FIXTURE(Fixture, "loop_typecheck_crash_on_empty_optional") +{ + CheckResult result = check(R"( + local t = {} + for _ in t do + for _ in assert(missing()) do + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); +} + +TEST_SUITE_END(); diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp new file mode 100644 index 0000000..6364361 --- /dev/null +++ b/tests/TypeInfer.modules.test.cpp @@ -0,0 +1,310 @@ +// 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/BuiltinDefinitions.h" +#include "Luau/Scope.h" +#include "Luau/TypeInfer.h" +#include "Luau/TypeVar.h" +#include "Luau/VisitTypeVar.h" + +#include "Fixture.h" + +#include "doctest.h" + +using namespace Luau; + +TEST_SUITE_BEGIN("TypeInferModules"); + +TEST_CASE_FIXTURE(Fixture, "require") +{ + fileResolver.source["game/A"] = R"( + local function hooty(x: number): string + return "Hi there!" + end + + return {hooty=hooty} + )"; + + fileResolver.source["game/B"] = R"( + local Hooty = require(game.A) + + local h -- free! + local i = Hooty.hooty(h) + )"; + + CheckResult aResult = frontend.check("game/A"); + dumpErrors(aResult); + LUAU_REQUIRE_NO_ERRORS(aResult); + + CheckResult bResult = frontend.check("game/B"); + dumpErrors(bResult); + LUAU_REQUIRE_NO_ERRORS(bResult); + + ModulePtr b = frontend.moduleResolver.modules["game/B"]; + + REQUIRE(b != nullptr); + + dumpErrors(bResult); + + std::optional iType = requireType(b, "i"); + REQUIRE_EQ("string", toString(*iType)); + + std::optional hType = requireType(b, "h"); + REQUIRE_EQ("number", toString(*hType)); +} + +TEST_CASE_FIXTURE(Fixture, "require_types") +{ + fileResolver.source["workspace/A"] = R"( + export type Point = {x: number, y: number} + + return {} + )"; + + fileResolver.source["workspace/B"] = R"( + local Hooty = require(workspace.A) + + local h: Hooty.Point + )"; + + CheckResult bResult = frontend.check("workspace/B"); + dumpErrors(bResult); + + ModulePtr b = frontend.moduleResolver.modules["workspace/B"]; + REQUIRE(b != nullptr); + + TypeId hType = requireType(b, "h"); + REQUIRE_MESSAGE(bool(get(hType)), "Expected table but got " << toString(hType)); +} + +TEST_CASE_FIXTURE(Fixture, "require_a_variadic_function") +{ + fileResolver.source["game/A"] = R"( + local T = {} + function T.f(...) end + return T + )"; + + fileResolver.source["game/B"] = R"( + local A = require(game.A) + local f = A.f + )"; + + CheckResult result = frontend.check("game/B"); + + ModulePtr bModule = frontend.moduleResolver.getModule("game/B"); + REQUIRE(bModule != nullptr); + + TypeId f = follow(requireType(bModule, "f")); + + const FunctionTypeVar* ftv = get(f); + REQUIRE(ftv); + + auto iter = begin(ftv->argTypes); + auto endIter = end(ftv->argTypes); + + REQUIRE(iter == endIter); + REQUIRE(iter.tail()); + + CHECK(get(*iter.tail())); +} + +TEST_CASE_FIXTURE(Fixture, "type_error_of_unknown_qualified_type") +{ + CheckResult result = check(R"( + local p: SomeModule.DoesNotExist + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + REQUIRE_EQ(result.errors[0], (TypeError{Location{{1, 17}, {1, 40}}, UnknownSymbol{"SomeModule.DoesNotExist"}})); +} + +TEST_CASE_FIXTURE(Fixture, "require_module_that_does_not_export") +{ + const std::string sourceA = R"( + )"; + + const std::string sourceB = R"( + local Hooty = require(script.Parent.A) + )"; + + fileResolver.source["game/Workspace/A"] = sourceA; + fileResolver.source["game/Workspace/B"] = sourceB; + + frontend.check("game/Workspace/A"); + frontend.check("game/Workspace/B"); + + ModulePtr aModule = frontend.moduleResolver.modules["game/Workspace/A"]; + ModulePtr bModule = frontend.moduleResolver.modules["game/Workspace/B"]; + + CHECK(aModule->errors.empty()); + REQUIRE_EQ(1, bModule->errors.size()); + CHECK_MESSAGE(get(bModule->errors[0]), "Should be IllegalRequire: " << toString(bModule->errors[0])); + + auto hootyType = requireType(bModule, "Hooty"); + + CHECK_EQ("*unknown*", toString(hootyType)); +} + +TEST_CASE_FIXTURE(Fixture, "warn_if_you_try_to_require_a_non_modulescript") +{ + fileResolver.source["Modules/A"] = ""; + fileResolver.sourceTypes["Modules/A"] = SourceCode::Local; + + fileResolver.source["Modules/B"] = R"( + local M = require(script.Parent.A) + )"; + + CheckResult result = frontend.check("Modules/B"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK(get(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "general_require_call_expression") +{ + fileResolver.source["game/A"] = R"( +--!strict +return { def = 4 } + )"; + + fileResolver.source["game/B"] = R"( +--!strict +local tbl = { abc = require(game.A) } +local a : string = "" +a = tbl.abc.def + )"; + + CheckResult result = frontend.check("game/B"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "general_require_type_mismatch") +{ + fileResolver.source["game/A"] = R"( +return { def = 4 } + )"; + + fileResolver.source["game/B"] = R"( +local tbl: string = require(game.A) + )"; + + CheckResult result = frontend.check("game/B"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Type '{| def: number |}' could not be converted into 'string'", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "bound_free_table_export_is_ok") +{ + CheckResult result = check(R"( +local n = {} +function n:Clone() end + +local m = {} + +function m.a(x) + x:Clone() +end + +function m.b() + m.a(n) +end + +return m +)"); + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "custom_require_global") +{ + CheckResult result = check(R"( +--!nonstrict +require = function(a) end + +local crash = require(game.A) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "require_failed_module") +{ + fileResolver.source["game/A"] = R"( +return unfortunately() + )"; + + CheckResult aResult = frontend.check("game/A"); + LUAU_REQUIRE_ERRORS(aResult); + + CheckResult result = check(R"( +local ModuleA = require(game.A) + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + std::optional oty = requireType("ModuleA"); + CHECK_EQ("*unknown*", toString(*oty)); +} + +TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types") +{ + fileResolver.source["game/A"] = R"( +export type Type = { unrelated: boolean } +return {} + )"; + + fileResolver.source["game/B"] = R"( +local types = require(game.A) +type Type = types.Type +local x: Type = {} +function x:Destroy(): () end + )"; + + CheckResult result = frontend.check("game/B"); + LUAU_REQUIRE_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_2") +{ + ScopedFastFlag immutableTypes{"LuauImmutableTypes", true}; + + fileResolver.source["game/A"] = R"( +export type Type = { x: { a: number } } +return {} + )"; + + fileResolver.source["game/B"] = R"( +local types = require(game.A) +type Type = types.Type +local x: Type = { x = { a = 2 } } +type Rename = typeof(x.x) + )"; + + CheckResult result = frontend.check("game/B"); + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_3") +{ + ScopedFastFlag immutableTypes{"LuauImmutableTypes", true}; + + fileResolver.source["game/A"] = R"( +local y = setmetatable({}, {}) +export type Type = { x: typeof(y) } +return { x = y } + )"; + + fileResolver.source["game/B"] = R"( +local types = require(game.A) +type Type = types.Type +local x: Type = types +type Rename = typeof(x.x) + )"; + + CheckResult result = frontend.check("game/B"); + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_SUITE_END(); diff --git a/tests/TypeInfer.oop.test.cpp b/tests/TypeInfer.oop.test.cpp new file mode 100644 index 0000000..40831bf --- /dev/null +++ b/tests/TypeInfer.oop.test.cpp @@ -0,0 +1,275 @@ +// 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/BuiltinDefinitions.h" +#include "Luau/Scope.h" +#include "Luau/TypeInfer.h" +#include "Luau/TypeVar.h" +#include "Luau/VisitTypeVar.h" + +#include "Fixture.h" + +#include "doctest.h" + +using namespace Luau; + +TEST_SUITE_BEGIN("TypeInferOOP"); + +TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon") +{ + CheckResult result = check(R"( + local someTable = {} + + someTable.Function1 = function(Arg1) + end + + someTable.Function1() -- Argument count mismatch + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + REQUIRE(get(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2") +{ + CheckResult result = check(R"( + local someTable = {} + + someTable.Function2 = function(Arg1, Arg2) + end + + someTable.Function2() -- Argument count mismatch + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + REQUIRE(get(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_another_overload_works") +{ + CheckResult result = check(R"( + type T = {method: ((T, number) -> number) & ((number) -> number)} + local T: T + + T.method(4) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "method_depends_on_table") +{ + CheckResult result = check(R"( + -- This catches a bug where x:m didn't count as a use of x + -- so toposort would happily reorder a definition of + -- function x:m before the definition of x. + function g() f() end + local x = {} + function x:m() end + function f() x:m() end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "methods_are_topologically_sorted") +{ + CheckResult result = check(R"( + local T = {} + + function T:foo() + return T:bar(999), T:bar("hi") + end + + function T:bar(i) + return i + end + + local a, b = T:foo() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); + + CHECK_EQ(PrimitiveTypeVar::Number, getPrimitiveType(requireType("a"))); + CHECK_EQ(PrimitiveTypeVar::String, getPrimitiveType(requireType("b"))); +} + +TEST_CASE_FIXTURE(Fixture, "quantify_methods_defined_using_dot_syntax_and_explicit_self_parameter") +{ + check(R"( + local T = {} + + function T.method(self) + self:method() + end + + function T.method2(self) + self:method() + end + + T:method2() + )"); +} + +TEST_CASE_FIXTURE(Fixture, "inferring_hundreds_of_self_calls_should_not_suffocate_memory") +{ + CheckResult result = check(R"( + ("foo") + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + :lower() + )"); + + ModulePtr module = getMainModule(); + CHECK_GE(50, module->internalTypes.typeVars.size()); +} + +TEST_CASE_FIXTURE(Fixture, "object_constructor_can_refer_to_method_of_self") +{ + // CLI-30902 + CheckResult result = check(R"( + --!strict + + type Foo = { + fooConn: () -> () | nil + } + + local Foo = {} + Foo.__index = Foo + + function Foo.new() + local self: Foo = { + fooConn = nil, + } + setmetatable(self, Foo) + + self.fooConn = function() + self:method() -- Key 'method' not found in table self + end + + return self + end + + function Foo:method() + print("foo") + end + + local foo = Foo.new() + + -- TODO This is the best our current refinement support can offer :( + local bar = foo.fooConn + if bar then bar() end + + -- foo.fooConn() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfSealed") +{ + CheckResult result = check(R"( +local x: {prop: number} = {prop=9999} +function x:y(z: number) + local s: string = z +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); +} + +TEST_CASE_FIXTURE(Fixture, "nonstrict_self_mismatch_tail") +{ + CheckResult result = check(R"( +--!nonstrict +local f = {} +function f:foo(a: number, b: number) end + +function bar(...) + f.foo(f, 1, ...) +end + +bar(2) +)"); + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table") +{ + check(R"( + function Base64FileReader(data) + local reader = {} + local index: number + + function reader:PeekByte() + return data:byte(index) + end + + function reader:Byte() + return data:byte(index - 1) + end + + return reader + end + + Base64FileReader() + + function ReadMidiEvents(data) + + local reader = Base64FileReader(data) + + while reader:HasMore() do + (reader:Byte() % 128) + end + end + )"); +} + +TEST_CASE_FIXTURE(Fixture, "table_oop") +{ + CheckResult result = check(R"( + --!strict +local Class = {} +Class.__index = Class + +type Class = typeof(setmetatable({} :: { x: number }, Class)) + +function Class.new(x: number): Class + return setmetatable({x = x}, Class) +end + +function Class.getx(self: Class) + return self.x +end + +function test() + local c = Class.new(42) + local n = c:getx() + local nn = c.x + + print(string.format("%d %d", n, nn)) +end +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_SUITE_END(); diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp new file mode 100644 index 0000000..baa2597 --- /dev/null +++ b/tests/TypeInfer.operators.test.cpp @@ -0,0 +1,759 @@ +// 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/BuiltinDefinitions.h" +#include "Luau/Scope.h" +#include "Luau/TypeInfer.h" +#include "Luau/TypeVar.h" +#include "Luau/VisitTypeVar.h" + +#include "Fixture.h" + +#include "doctest.h" + +using namespace Luau; + +TEST_SUITE_BEGIN("TypeInferOperators"); + +TEST_CASE_FIXTURE(Fixture, "or_joins_types") +{ + CheckResult result = check(R"( + local s = "a" or 10 + local x:string|number = s + )"); + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ(toString(*requireType("s")), "number | string"); + CHECK_EQ(toString(*requireType("x")), "number | string"); +} + +TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_extras") +{ + CheckResult result = check(R"( + local s = "a" or 10 + local x:number|string = s + local y = x or "s" + )"); + CHECK_EQ(0, result.errors.size()); + CHECK_EQ(toString(*requireType("s")), "number | string"); + CHECK_EQ(toString(*requireType("y")), "number | string"); +} + +TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_superfluous_union") +{ + CheckResult result = check(R"( + local s = "a" or "b" + local x:string = s + )"); + CHECK_EQ(0, result.errors.size()); + CHECK_EQ(*requireType("s"), *typeChecker.stringType); +} + +TEST_CASE_FIXTURE(Fixture, "and_adds_boolean") +{ + CheckResult result = check(R"( + local s = "a" and 10 + local x:boolean|number = s + )"); + CHECK_EQ(0, result.errors.size()); + CHECK_EQ(toString(*requireType("s")), "boolean | number"); +} + +TEST_CASE_FIXTURE(Fixture, "and_adds_boolean_no_superfluous_union") +{ + CheckResult result = check(R"( + local s = "a" and true + local x:boolean = s + )"); + CHECK_EQ(0, result.errors.size()); + CHECK_EQ(*requireType("x"), *typeChecker.booleanType); +} + +TEST_CASE_FIXTURE(Fixture, "and_or_ternary") +{ + CheckResult result = check(R"( + local s = (1/2) > 0.5 and "a" or 10 + )"); + CHECK_EQ(0, result.errors.size()); + CHECK_EQ(toString(*requireType("s")), "number | string"); +} + +TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable") +{ + CheckResult result = check(R"( + function add(a: number, b: string) + return a + (tonumber(b) :: number), a .. b + end + local n, s = add(2,"3") + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + const FunctionTypeVar* functionType = get(requireType("add")); + + std::optional retType = first(functionType->retType); + CHECK_EQ(std::optional(typeChecker.numberType), retType); + CHECK_EQ(requireType("n"), typeChecker.numberType); + CHECK_EQ(requireType("s"), typeChecker.stringType); +} + +TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable_with_follows") +{ + CheckResult result = check(R"( + local PI=3.1415926535897931 + local SOLAR_MASS=4*PI * PI + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ(requireType("SOLAR_MASS"), typeChecker.numberType); +} + +TEST_CASE_FIXTURE(Fixture, "primitive_arith_possible_metatable") +{ + CheckResult result = check(R"( + function add(a: number, b: any) + return a + b + end + local t = add(1,2) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("any", toString(requireType("t"))); +} + +TEST_CASE_FIXTURE(Fixture, "some_primitive_binary_ops") +{ + CheckResult result = check(R"( + local a = 4 + 8 + local b = a + 9 + local s = 'hotdogs' + local t = s .. s + local c = b - a + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("number", toString(requireType("a"))); + CHECK_EQ("number", toString(requireType("b"))); + CHECK_EQ("string", toString(requireType("s"))); + CHECK_EQ("string", toString(requireType("t"))); + CHECK_EQ("number", toString(requireType("c"))); +} + +TEST_CASE_FIXTURE(Fixture, "typecheck_overloaded_multiply_that_is_an_intersection") +{ + ScopedFastFlag sff{"LuauErrorRecoveryType", true}; + + CheckResult result = check(R"( + --!strict + local Vec3 = {} + Vec3.__index = Vec3 + function Vec3.new() + return setmetatable({x=0, y=0, z=0}, Vec3) + end + + export type Vec3 = typeof(Vec3.new()) + + local thefun: any = function(self, o) return self end + + local multiply: ((Vec3, Vec3) -> Vec3) & ((Vec3, number) -> Vec3) = thefun + + Vec3.__mul = multiply + + local a = Vec3.new() + local b = Vec3.new() + local c = a * b + local d = a * 2 + local e = a * 'cabbage' + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("Vec3", toString(requireType("a"))); + CHECK_EQ("Vec3", toString(requireType("b"))); + CHECK_EQ("Vec3", toString(requireType("c"))); + CHECK_EQ("Vec3", toString(requireType("d"))); + CHECK_EQ("Vec3", toString(requireType("e"))); +} + +TEST_CASE_FIXTURE(Fixture, "typecheck_overloaded_multiply_that_is_an_intersection_on_rhs") +{ + ScopedFastFlag sff{"LuauErrorRecoveryType", true}; + + CheckResult result = check(R"( + --!strict + local Vec3 = {} + Vec3.__index = Vec3 + function Vec3.new() + return setmetatable({x=0, y=0, z=0}, Vec3) + end + + export type Vec3 = typeof(Vec3.new()) + + local thefun: any = function(self, o) return self end + + local multiply: ((Vec3, Vec3) -> Vec3) & ((Vec3, number) -> Vec3) = thefun + + Vec3.__mul = multiply + + local a = Vec3.new() + local b = Vec3.new() + local c = b * a + local d = 2 * a + local e = 'cabbage' * a + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("Vec3", toString(requireType("a"))); + CHECK_EQ("Vec3", toString(requireType("b"))); + CHECK_EQ("Vec3", toString(requireType("c"))); + CHECK_EQ("Vec3", toString(requireType("d"))); + CHECK_EQ("Vec3", toString(requireType("e"))); +} + +TEST_CASE_FIXTURE(Fixture, "compare_numbers") +{ + CheckResult result = check(R"( + local a = 441 + local b = 0 + local c = a < b + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "compare_strings") +{ + CheckResult result = check(R"( + local a = '441' + local b = '0' + local c = a < b + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_metatable") +{ + CheckResult result = check(R"( + local a = {} + local b = {} + local c = a < b + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + GenericError* gen = get(result.errors[0]); + + REQUIRE_EQ(gen->message, "Type a cannot be compared with < because it has no metatable"); +} + +TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators") +{ + CheckResult result = check(R"( + local M = {} + function M.new() + return setmetatable({}, M) + end + type M = typeof(M.new()) + + local a = M.new() + local b = M.new() + local c = a < b + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + GenericError* gen = get(result.errors[0]); + REQUIRE(gen != nullptr); + REQUIRE_EQ(gen->message, "Table M does not offer metamethod __lt"); +} + +TEST_CASE_FIXTURE(Fixture, "cannot_compare_tables_that_do_not_have_the_same_metatable") +{ + CheckResult result = check(R"( + --!strict + local M = {} + function M.new() + return setmetatable({}, M) + end + function M.__lt(left, right) return true end + + local a = M.new() + local b = {} + local c = a < b -- line 10 + local d = b < a -- line 11 + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + + REQUIRE_EQ((Location{{10, 18}, {10, 23}}), result.errors[0].location); + + REQUIRE_EQ((Location{{11, 18}, {11, 23}}), result.errors[1].location); +} + +TEST_CASE_FIXTURE(Fixture, "produce_the_correct_error_message_when_comparing_a_table_with_a_metatable_with_one_that_does_not") +{ + CheckResult result = check(R"( + --!strict + local M = {} + function M.new() + return setmetatable({}, M) + end + function M.__lt(left, right) return true end + type M = typeof(M.new()) + + local a = M.new() + local b = {} + local c = a < b -- line 10 + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + auto err = get(result.errors[0]); + REQUIRE(err != nullptr); + + // Frail. :| + REQUIRE_EQ("Types M and b cannot be compared with < because they do not have the same metatable", err->message); +} + +TEST_CASE_FIXTURE(Fixture, "in_nonstrict_mode_strip_nil_from_intersections_when_considering_relational_operators") +{ + CheckResult result = check(R"( + --!nonstrict + + function maybe_a_number(): number? + return 50 + end + + local a = maybe_a_number() < maybe_a_number() + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "compound_assign_basic") +{ + CheckResult result = check(R"( + local s = 10 + s += 20 + )"); + CHECK_EQ(0, result.errors.size()); + CHECK_EQ(toString(*requireType("s")), "number"); +} + +TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_op") +{ + CheckResult result = check(R"( + local s = 10 + s += true + )"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(result.errors[0], (TypeError{Location{{2, 13}, {2, 17}}, TypeMismatch{typeChecker.numberType, typeChecker.booleanType}})); +} + +TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_result") +{ + CheckResult result = check(R"( + local s = 'hello' + s += 10 + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK_EQ(result.errors[0], (TypeError{Location{{2, 8}, {2, 9}}, TypeMismatch{typeChecker.numberType, typeChecker.stringType}})); + CHECK_EQ(result.errors[1], (TypeError{Location{{2, 8}, {2, 15}}, TypeMismatch{typeChecker.stringType, typeChecker.numberType}})); +} + +TEST_CASE_FIXTURE(Fixture, "compound_assign_metatable") +{ + CheckResult result = check(R"( + --!strict + type V2B = { x: number, y: number } + local v2b: V2B = { x = 0, y = 0 } + local VMT = {} + type V2 = typeof(setmetatable(v2b, VMT)) + + function VMT.__add(a: V2, b: V2): V2 + return setmetatable({ x = a.x + b.x, y = a.y + b.y }, VMT) + end + + local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT) + local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT) + v1 += v2 + )"); + CHECK_EQ(0, result.errors.size()); +} + +TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_metatable") +{ + CheckResult result = check(R"( + --!strict + type V2B = { x: number, y: number } + local v2b: V2B = { x = 0, y = 0 } + local VMT = {} + type V2 = typeof(setmetatable(v2b, VMT)) + + function VMT.__mod(a: V2, b: V2): number + return a.x * b.x + a.y * b.y + end + + local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT) + local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT) + v1 %= v2 + )"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + + TypeMismatch* tm = get(result.errors[0]); + CHECK_EQ(*tm->wantedType, *requireType("v2")); + CHECK_EQ(*tm->givenType, *typeChecker.numberType); +} + +TEST_CASE_FIXTURE(Fixture, "CallOrOfFunctions") +{ + CheckResult result = check(R"( +function f() return 1; end +function g() return 2; end +(f or g)() +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "CallAndOrOfFunctions") +{ + CheckResult result = check(R"( +function f() return 1; end +function g() return 2; end +local x = false +(x and f or g)() +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "typecheck_unary_minus") +{ + CheckResult result = check(R"( + --!strict + local foo = { + value = 10 + } + local mt = {} + setmetatable(foo, mt) + + mt.__unm = function(val: typeof(foo)): string + return val.value .. "test" + end + + local a = -foo + + local b = 1+-1 + + local bar = { + value = 10 + } + local c = -bar -- disallowed + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("string", toString(requireType("a"))); + CHECK_EQ("number", toString(requireType("b"))); + + GenericError* gen = get(result.errors[0]); + REQUIRE_EQ(gen->message, "Unary operator '-' not supported by type 'bar'"); +} + +TEST_CASE_FIXTURE(Fixture, "unary_not_is_boolean") +{ + CheckResult result = check(R"( + local b = not "string" + local c = not (math.random() > 0.5 and "string" or 7) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + REQUIRE_EQ("boolean", toString(requireType("b"))); + REQUIRE_EQ("boolean", toString(requireType("c"))); +} + +TEST_CASE_FIXTURE(Fixture, "disallow_string_and_types_without_metatables_from_arithmetic_binary_ops") +{ + CheckResult result = check(R"( + --!strict + local a = "1.24" + 123 -- not allowed + + local foo = { + value = 10 + } + + local b = foo + 1 -- not allowed + + local bar = { + value = 1 + } + + local mt = {} + + setmetatable(bar, mt) + + mt.__add = function(a: typeof(bar), b: number): number + return a.value + b + end + + local c = bar + 1 -- allowed + + local d = bar + foo -- not allowed + )"); + + LUAU_REQUIRE_ERROR_COUNT(3, result); + + TypeMismatch* tm = get(result.errors[0]); + REQUIRE_EQ(*tm->wantedType, *typeChecker.numberType); + REQUIRE_EQ(*tm->givenType, *typeChecker.stringType); + + TypeMismatch* tm2 = get(result.errors[2]); + CHECK_EQ(*tm2->wantedType, *typeChecker.numberType); + CHECK_EQ(*tm2->givenType, *requireType("foo")); + + GenericError* gen2 = get(result.errors[1]); + REQUIRE_EQ(gen2->message, "Binary operator '+' not supported by types 'foo' and 'number'"); +} + +// CLI-29033 +TEST_CASE_FIXTURE(Fixture, "unknown_type_in_comparison") +{ + CheckResult result = check(R"( + function merge(lower, greater) + if lower.y == greater.y then + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "concat_op_on_free_lhs_and_string_rhs") +{ + CheckResult result = check(R"( + local function f(x) + return x .. "y" + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + REQUIRE(get(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "concat_op_on_string_lhs_and_free_rhs") +{ + CheckResult result = check(R"( + local function f(x) + return "foo" .. x + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("(string) -> string", toString(requireType("f"))); +} + +TEST_CASE_FIXTURE(Fixture, "strict_binary_op_where_lhs_unknown") +{ + std::vector ops = {"+", "-", "*", "/", "%", "^", ".."}; + + std::string src = R"( + function foo(a, b) + )"; + + for (const auto& op : ops) + src += "local _ = a " + op + "b\n"; + + src += "end"; + + CheckResult result = check(src); + LUAU_REQUIRE_ERROR_COUNT(ops.size(), result); + + CHECK_EQ("Unknown type used in + operation; consider adding a type annotation to 'a'", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "and_binexps_dont_unify") +{ + CheckResult result = check(R"( + --!strict + local t = {} + while true and t[1] do + print(t[1].test) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators") +{ + CheckResult result = check(R"( + local a: boolean = true + local b: boolean = false + local foo = a < b + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + GenericError* ge = get(result.errors[0]); + REQUIRE(ge); + CHECK_EQ("Type 'boolean' cannot be compared with relational operator <", ge->message); +} + +TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2") +{ + CheckResult result = check(R"( + local a: number | string = "" + local b: number | string = 1 + local foo = a < b + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + GenericError* ge = get(result.errors[0]); + REQUIRE(ge); + CHECK_EQ("Type 'number | string' cannot be compared with relational operator <", ge->message); +} + +TEST_CASE_FIXTURE(Fixture, "cli_38355_recursive_union") +{ + CheckResult result = check(R"( + --!strict + local _ + _ += _ and _ or _ and _ or _ and _ + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Type contains a self-recursive construct that cannot be resolved", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "UnknownGlobalCompoundAssign") +{ + // In non-strict mode, global definition is still allowed + { + CheckResult result = check(R"( + --!nonstrict + a = a + 1 + print(a) + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'"); + } + + // In strict mode we no longer generate two errors from lhs + { + CheckResult result = check(R"( + --!strict + a += 1 + print(a) + )"); + + LUAU_REQUIRE_ERRORS(result); + CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'"); + } + + // In non-strict mode, compound assignment is not a definition, it's a modification + { + CheckResult result = check(R"( + --!nonstrict + a += 1 + print(a) + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'"); + } +} + +TEST_CASE_FIXTURE(Fixture, "strip_nil_from_lhs_or_operator") +{ + CheckResult result = check(R"( +--!strict +local a: number? = nil +local b: number = a or 1 + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "strip_nil_from_lhs_or_operator2") +{ + CheckResult result = check(R"( +--!nonstrict +local a: number? = nil +local b: number = a or 1 + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "dont_strip_nil_from_rhs_or_operator") +{ + CheckResult result = check(R"( +--!strict +local a: number? = nil +local b: number = 1 or a + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK_EQ(typeChecker.numberType, tm->wantedType); + CHECK_EQ("number?", toString(tm->givenType)); +} + +TEST_CASE_FIXTURE(Fixture, "operator_eq_verifies_types_do_intersect") +{ + CheckResult result = check(R"( + type Array = { [number]: T } + type Fiber = { id: number } + type null = {} + + local fiberStack: Array = {} + local index = 0 + + local function f(fiber: Fiber) + local a = fiber ~= fiberStack[index] + local b = fiberStack[index] ~= fiber + end + + return f + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "operator_eq_operands_are_not_subtypes_of_each_other_but_has_overlap") +{ + ScopedFastFlag sff1{"LuauEqConstraint", true}; + + CheckResult result = check(R"( + local function f(a: string | number, b: boolean | number) + return a == b + end + )"); + + // This doesn't produce any errors but for the wrong reasons. + // This unit test serves as a reminder to not try and unify the operands on `==`/`~=`. + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "refine_and_or") +{ + CheckResult result = check(R"( + local t: {x: number?}? = {x = nil} + local u = t and t.x or 5 + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("number", toString(requireType("u"))); +} + +TEST_SUITE_END(); diff --git a/tests/TypeInfer.primitives.test.cpp b/tests/TypeInfer.primitives.test.cpp new file mode 100644 index 0000000..44b7b0d --- /dev/null +++ b/tests/TypeInfer.primitives.test.cpp @@ -0,0 +1,100 @@ +// 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/BuiltinDefinitions.h" +#include "Luau/Scope.h" +#include "Luau/TypeInfer.h" +#include "Luau/TypeVar.h" +#include "Luau/VisitTypeVar.h" + +#include "Fixture.h" + +#include "doctest.h" + +using namespace Luau; + +TEST_SUITE_BEGIN("TypeInferPrimitives"); + +TEST_CASE_FIXTURE(Fixture, "cannot_call_primitives") +{ + CheckResult result = check("local foo = 5 foo()"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + + REQUIRE(get(result.errors[0]) != nullptr); +} + +TEST_CASE_FIXTURE(Fixture, "string_length") +{ + CheckResult result = check(R"( + local s = "Hello, World!" + local t = #s + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("number", toString(requireType("t"))); +} + +TEST_CASE_FIXTURE(Fixture, "string_index") +{ + CheckResult result = check(R"( + local s = "Hello, World!" + local t = s[4] + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + NotATable* nat = get(result.errors[0]); + REQUIRE(nat); + CHECK_EQ("string", toString(nat->ty)); + + CHECK_EQ("*unknown*", toString(requireType("t"))); +} + +TEST_CASE_FIXTURE(Fixture, "string_method") +{ + CheckResult result = check(R"( + local p = ("tacos"):len() + )"); + CHECK_EQ(0, result.errors.size()); + + CHECK_EQ(*requireType("p"), *typeChecker.numberType); +} + +TEST_CASE_FIXTURE(Fixture, "string_function_indirect") +{ + CheckResult result = check(R"( + local s:string + local l = s.lower + local p = l(s) + )"); + CHECK_EQ(0, result.errors.size()); + + CHECK_EQ(*requireType("p"), *typeChecker.stringType); +} + +TEST_CASE_FIXTURE(Fixture, "string_function_other") +{ + CheckResult result = check(R"( + local s:string + local p = s:match("foo") + )"); + CHECK_EQ(0, result.errors.size()); + + CHECK_EQ(toString(requireType("p")), "string?"); +} + +TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfNumber") +{ + ScopedFastFlag sff{"LuauErrorRecoveryType", true}; + + CheckResult result = check(R"( +local x: number = 9999 +function x:y(z: number) + local s: string = z +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); +} + +TEST_SUITE_END(); diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index a5147d5..9b34792 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -1298,4 +1298,22 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "x_is_not_instance_or_else_not_part") CHECK_EQ("Part", toString(requireTypeAtPosition({5, 28}))); } +TEST_CASE_FIXTURE(Fixture, "typeguard_doesnt_leak_to_elseif") +{ + const std::string code = R"( + function f(a) + if type(a) == "boolean" then + local a1 = a + elseif a.fn() then + local a2 = a + else + local a3 = a + end + end + )"; + CheckResult result = check(code); + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 3ed536e..7f8d8fe 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -5,6 +5,8 @@ #include "doctest.h" #include "Luau/BuiltinDefinitions.h" +LUAU_FASTFLAG(BetterDiagnosticCodesInStudio) + using namespace Luau; TEST_SUITE_BEGIN("TypeSingletons"); @@ -353,7 +355,14 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_alias_or_parens_is_indexer") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Syntax error: Cannot have more than one table indexer", toString(result.errors[0])); + if (FFlag::BetterDiagnosticCodesInStudio) + { + CHECK_EQ("Cannot have more than one table indexer", toString(result.errors[0])); + } + else + { + CHECK_EQ("Syntax error: Cannot have more than one table indexer", toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes") @@ -445,7 +454,7 @@ TEST_CASE_FIXTURE(Fixture, "widen_the_supertype_if_it_is_free_and_subtype_has_si {"LuauSingletonTypes", true}, {"LuauEqConstraint", true}, {"LuauDiscriminableUnions2", true}, - {"LuauWidenIfSupertypeIsFree", true}, + {"LuauWidenIfSupertypeIsFree2", true}, {"LuauWeakEqConstraint", false}, }; @@ -472,9 +481,9 @@ TEST_CASE_FIXTURE(Fixture, "return_type_of_f_is_not_widened") {"LuauSingletonTypes", true}, {"LuauDiscriminableUnions2", true}, {"LuauEqConstraint", true}, - {"LuauWidenIfSupertypeIsFree", true}, + {"LuauWidenIfSupertypeIsFree2", true}, {"LuauWeakEqConstraint", false}, - {"LuauDoNotAccidentallyDependOnPointerOrdering", true} + {"LuauDoNotAccidentallyDependOnPointerOrdering", true}, }; CheckResult result = check(R"( @@ -497,7 +506,7 @@ TEST_CASE_FIXTURE(Fixture, "widening_happens_almost_everywhere") ScopedFastFlag sff[]{ {"LuauParseSingletonTypes", true}, {"LuauSingletonTypes", true}, - {"LuauWidenIfSupertypeIsFree", true}, + {"LuauWidenIfSupertypeIsFree2", true}, }; CheckResult result = check(R"( @@ -515,7 +524,7 @@ TEST_CASE_FIXTURE(Fixture, "widening_happens_almost_everywhere_except_for_tables {"LuauParseSingletonTypes", true}, {"LuauSingletonTypes", true}, {"LuauDiscriminableUnions2", true}, - {"LuauWidenIfSupertypeIsFree", true}, + {"LuauWidenIfSupertypeIsFree2", true}, }; CheckResult result = check(R"( @@ -544,7 +553,7 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_with_a_singleton_argument") ScopedFastFlag sff[]{ {"LuauParseSingletonTypes", true}, {"LuauSingletonTypes", true}, - {"LuauWidenIfSupertypeIsFree", true}, + {"LuauWidenIfSupertypeIsFree2", true}, }; CheckResult result = check(R"( @@ -565,4 +574,97 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_with_a_singleton_argument") CHECK_EQ("{string}", toString(requireType("t"))); } +TEST_CASE_FIXTURE(Fixture, "functions_are_not_to_be_widened") +{ + ScopedFastFlag sff[]{ + {"LuauParseSingletonTypes", true}, + {"LuauSingletonTypes", true}, + {"LuauWidenIfSupertypeIsFree2", true}, + }; + + CheckResult result = check(R"( + local function foo(my_enum: "A" | "B") end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(R"(("A" | "B") -> ())", toString(requireType("foo"))); +} + +TEST_CASE_FIXTURE(Fixture, "indexing_on_string_singletons") +{ + ScopedFastFlag sff[]{ + {"LuauDiscriminableUnions2", true}, + {"LuauSingletonTypes", true}, + }; + + CheckResult result = check(R"( + local a: string = "hi" + if a == "hi" then + local x = a:byte() + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 22}))); +} + +TEST_CASE_FIXTURE(Fixture, "indexing_on_union_of_string_singletons") +{ + ScopedFastFlag sff[]{ + {"LuauDiscriminableUnions2", true}, + {"LuauSingletonTypes", true}, + }; + + CheckResult result = check(R"( + local a: string = "hi" + if a == "hi" or a == "bye" then + local x = a:byte() + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 22}))); +} + +TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_string_singleton") +{ + ScopedFastFlag sff[]{ + {"LuauDiscriminableUnions2", true}, + {"LuauSingletonTypes", true}, + }; + + CheckResult result = check(R"( + local a: string = "hi" + if a == "hi" then + local x = #a + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23}))); +} + +TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_union_of_string_singleton") +{ + ScopedFastFlag sff[]{ + {"LuauDiscriminableUnions2", true}, + {"LuauSingletonTypes", true}, + }; + + CheckResult result = check(R"( + local a: string = "hi" + if a == "hi" or a == "bye" then + local x = #a + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 23}))); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index a5eba5d..91140aa 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -2384,4 +2384,504 @@ _ = (_.cos) LUAU_REQUIRE_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "cannot_call_tables") +{ + CheckResult result = check("local foo = {} foo()"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK(get(result.errors[0]) != nullptr); +} + +TEST_CASE_FIXTURE(Fixture, "table_length") +{ + CheckResult result = check(R"( + local t = {} + local s = #t + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK(nullptr != get(requireType("t"))); + CHECK_EQ(*typeChecker.numberType, *requireType("s")); +} + +TEST_CASE_FIXTURE(Fixture, "nil_assign_doesnt_hit_indexer") +{ + CheckResult result = check("local a = {} a[0] = 7 a[0] = nil"); + LUAU_REQUIRE_ERROR_COUNT(0, result); +} + +TEST_CASE_FIXTURE(Fixture, "wrong_assign_does_hit_indexer") +{ + CheckResult result = check("local a = {} a[0] = 7 a[0] = 't'"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 30}, Position{0, 33}}, TypeMismatch{ + typeChecker.numberType, + typeChecker.stringType, + }})); +} + +TEST_CASE_FIXTURE(Fixture, "nil_assign_doesnt_hit_no_indexer") +{ + CheckResult result = check("local a = {a=1, b=2} a['a'] = nil"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 30}, Position{0, 33}}, TypeMismatch{ + typeChecker.numberType, + typeChecker.nilType, + }})); +} + +TEST_CASE_FIXTURE(Fixture, "free_rhs_table_can_also_be_bound") +{ + check(R"( + local o + local v = o:i() + + function g(u) + v = u + end + + o:f(g) + o:h() + o:h() + )"); +} + +TEST_CASE_FIXTURE(Fixture, "table_unifies_into_map") +{ + CheckResult result = check(R"( + local Instance: any + local UDim2: any + + function Create(instanceType) + return function(data) + local obj = Instance.new(instanceType) + for k, v in pairs(data) do + if type(k) == 'number' then + --v.Parent = obj + else + obj[k] = v + end + end + return obj + end + end + + local topbarShadow = Create'ImageLabel'{ + Name = "TopBarShadow"; + Size = UDim2.new(1, 0, 0, 3); + Position = UDim2.new(0, 0, 1, 0); + Image = "rbxasset://textures/ui/TopBar/dropshadow.png"; + BackgroundTransparency = 1; + Active = false; + Visible = false; + }; + + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "tables_get_names_from_their_locals") +{ + CheckResult result = check(R"( + local T = {} + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("T", toString(requireType("T"))); +} + +TEST_CASE_FIXTURE(Fixture, "generalize_table_argument") +{ + CheckResult result = check(R"( + function foo(arr) + local work = {} + for i = 1, #arr do + work[i] = arr[i] + end + + return arr + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + dumpErrors(result); + + const FunctionTypeVar* fooType = get(requireType("foo")); + REQUIRE(fooType); + + std::optional fooArg1 = first(fooType->argTypes); + REQUIRE(fooArg1); + + const TableTypeVar* fooArg1Table = get(*fooArg1); + REQUIRE(fooArg1Table); + + CHECK_EQ(fooArg1Table->state, TableState::Generic); +} + +/* + * This test case exposed an oversight in the treatment of free tables. + * Free tables, like free TypeVars, need to record the scope depth where they were created so that + * we do not erroneously let-generalize them when they are used in a nested lambda. + * + * For more information about let-generalization, see + * + * The important idea here is that the return type of Counter.new is a table with some metatable. + * That metatable *must* be the same TypeVar as the type of Counter. If it is a copy (produced by + * the generalization process), then it loses the knowledge that its metatable will have an :incr() + * method. + */ +TEST_CASE_FIXTURE(Fixture, "dont_quantify_table_that_belongs_to_outer_scope") +{ + CheckResult result = check(R"( + local Counter = {} + Counter.__index = Counter + + function Counter.new() + local self = setmetatable({count=0}, Counter) + return self + end + + function Counter:incr() + self.count = 1 + return self.count + end + + local self = Counter.new() + print(self:incr()) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + TableTypeVar* counterType = getMutable(requireType("Counter")); + REQUIRE(counterType); + + const FunctionTypeVar* newType = get(follow(counterType->props["new"].type)); + REQUIRE(newType); + + std::optional newRetType = *first(newType->retType); + REQUIRE(newRetType); + + const MetatableTypeVar* newRet = get(follow(*newRetType)); + REQUIRE(newRet); + + const TableTypeVar* newRetMeta = get(newRet->metatable); + REQUIRE(newRetMeta); + + CHECK(newRetMeta->props.count("incr")); + CHECK_EQ(follow(newRet->metatable), follow(requireType("Counter"))); +} + +// TODO: CLI-39624 +TEST_CASE_FIXTURE(Fixture, "instantiate_tables_at_scope_level") +{ + CheckResult result = check(R"( + --!strict + local Option = {} + Option.__index = Option + function Option.Is(obj) + return (type(obj) == "table" and getmetatable(obj) == Option) + end + return Option + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "inferring_crazy_table_should_also_be_quick") +{ + CheckResult result = check(R"( + --!strict + function f(U) + U(w:s(an):c()():c():U(s):c():c():U(s):c():U(s):cU()):c():U(s):c():U(s):c():c():U(s):c():U(s):cU() + end + )"); + + ModulePtr module = getMainModule(); + CHECK_GE(100, module->internalTypes.typeVars.size()); +} + +TEST_CASE_FIXTURE(Fixture, "MixedPropertiesAndIndexers") +{ + CheckResult result = check(R"( +local x = {} +x.a = "a" +x[0] = true +x.b = 37 +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "setmetatable_cant_be_used_to_mutate_global_types") +{ + { + Fixture fix; + + // inherit env from parent fixture checker + fix.typeChecker.globalScope = typeChecker.globalScope; + + fix.check(R"( +--!nonstrict +type MT = typeof(setmetatable) +function wtf(arg: {MT}): typeof(table) + arg = wtf(arg) +end +)"); + } + + // validate sharedEnv post-typecheck; valuable for debugging some typeck crashes but slows fuzzing down + // note: it's important for typeck to be destroyed at this point! + { + for (auto& p : typeChecker.globalScope->bindings) + { + toString(p.second.typeId); // toString walks the entire type, making sure ASAN catches access to destroyed type arenas + } + } +} + +TEST_CASE_FIXTURE(Fixture, "evil_table_unification") +{ + // this code re-infers the type of _ while processing fields of _, which can cause use-after-free + check(R"( +--!nonstrict +_ = ... +_:table(_,string)[_:gsub(_,...,n0)],_,_:gsub(_,string)[""],_:split(_,...,table)._,n0 = nil +do end +)"); +} + +TEST_CASE_FIXTURE(Fixture, "dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar") +{ + CheckResult result = check("local x = setmetatable({})"); + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + +TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning") +{ + CheckResult result = check(R"( +--!nonstrict +local l0:any,l61:t0 = _,math +while _ do +_() +end +function _():t0 +end +type t0 = any +)"); + + std::optional ty = requireType("math"); + REQUIRE(ty); + + const TableTypeVar* ttv = get(*ty); + REQUIRE(ttv); + CHECK(ttv->instantiatedTypeParams.empty()); +} + +TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning_2") +{ + ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true}; + + CheckResult result = check(R"( +type X = T +type K = X +)"); + + LUAU_REQUIRE_NO_ERRORS(result); + + std::optional ty = requireType("math"); + REQUIRE(ty); + + const TableTypeVar* ttv = get(*ty); + REQUIRE(ttv); + CHECK(ttv->instantiatedTypeParams.empty()); +} + +TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning_3") +{ + ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true}; + + CheckResult result = check(R"( +type X = T +local a = {} +a.x = 4 +local b: X +a.y = 5 +local c: X +c = b +)"); + + LUAU_REQUIRE_NO_ERRORS(result); + + std::optional ty = requireType("a"); + REQUIRE(ty); + + const TableTypeVar* ttv = get(*ty); + REQUIRE(ttv); + CHECK(ttv->instantiatedTypeParams.empty()); +} + +TEST_CASE_FIXTURE(Fixture, "table_indexing_error_location") +{ + CheckResult result = check(R"( +local foo = {42} +local bar: number? +local baz = foo[bar] + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ(result.errors[0].location, Location{Position{3, 16}, Position{3, 19}}); +} + +TEST_CASE_FIXTURE(Fixture, "table_simple_call") +{ + CheckResult result = check(R"( +local a = setmetatable({ x = 2 }, { + __call = function(self) + return (self.x :: number) * 2 -- should work without annotation in the future + end +}) +local b = a() +local c = a(2) -- too many arguments + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Argument count mismatch. Function expects 1 argument, but 2 are specified", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "access_index_metamethod_that_returns_variadic") +{ + CheckResult result = check(R"( + type Foo = {x: string} + local t = {} + setmetatable(t, { + __index = function(x: string): ...Foo + return {x = x} + end + }) + + local foo = t.bar + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + ToStringOptions o; + o.exhaustive = true; + CHECK_EQ("{| x: string |}", toString(requireType("foo"), o)); +} + +TEST_CASE_FIXTURE(Fixture, "dont_invalidate_the_properties_iterator_of_free_table_when_rolled_back") +{ + fileResolver.source["Module/Backend/Types"] = R"( + export type Fiber = { + return_: Fiber? + } + return {} + )"; + + fileResolver.source["Module/Backend"] = R"( + local Types = require(script.Types) + type Fiber = Types.Fiber + type ReactRenderer = { findFiberByHostInstance: () -> Fiber? } + + local function attach(renderer): () + local function getPrimaryFiber(fiber) + local alternate = fiber.alternate + return fiber + end + + local function getFiberIDForNative() + local fiber = renderer.findFiberByHostInstance() + fiber = fiber.return_ + return getPrimaryFiber(fiber) + end + end + + function culprit(renderer: ReactRenderer): () + attach(renderer) + end + + return culprit + )"; + + CheckResult result = frontend.check("Module/Backend"); +} + +TEST_CASE_FIXTURE(Fixture, "checked_prop_too_early") +{ + CheckResult result = check(R"( + local t: {x: number?}? = {x = nil} + local u = t.x and t or 5 + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0])); + CHECK_EQ("number | {| x: number? |}", toString(requireType("u"))); +} + +TEST_CASE_FIXTURE(Fixture, "accidentally_checked_prop_in_opposite_branch") +{ + CheckResult result = check(R"( + local t: {x: number?}? = {x = nil} + local u = t and t.x == 5 or t.x == 31337 + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0])); + CHECK_EQ("boolean", toString(requireType("u"))); +} + +/* + * We had an issue where part of the type of pairs() was an unsealed table. + * This test depends on FFlagDebugLuauFreezeArena to trigger it. + */ +TEST_CASE_FIXTURE(Fixture, "pairs_parameters_are_not_unsealed_tables") +{ + check(R"( + function _(l0:{n0:any}) + _ = pairs + end + )"); +} + +TEST_CASE_FIXTURE(Fixture, "table_function_check_use_after_free") +{ + CheckResult result = check(R"( +local t = {} + +function t.x(value) + for k,v in pairs(t) do end +end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +/* + * When we add new properties to an unsealed table, we should do a level check and promote the property type to be at + * the level of the table. + */ +TEST_CASE_FIXTURE(Fixture, "inferred_properties_of_a_table_should_start_with_the_same_TypeLevel_of_that_table") +{ + CheckResult result = check(R"( + --!strict + local T = {} + + local function f(prop) + T[1] = { + prop = prop, + } + end + + local function g() + local l = T[1].prop + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index d7bbad2..660ddcf 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -60,15 +60,6 @@ TEST_CASE_FIXTURE(Fixture, "tc_error_2") }})); } -TEST_CASE_FIXTURE(Fixture, "tc_function") -{ - CheckResult result = check("function five() return 5 end"); - LUAU_REQUIRE_NO_ERRORS(result); - - const FunctionTypeVar* fiveType = get(requireType("five")); - REQUIRE(fiveType != nullptr); -} - TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value") { CheckResult result = check("local f = nil; f = 'hello world'"); @@ -108,462 +99,12 @@ TEST_CASE_FIXTURE(Fixture, "infer_in_nocheck_mode") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(Fixture, "check_function_bodies") -{ - CheckResult result = check("function myFunction() local a = 0 a = true end"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 44}, Position{0, 48}}, TypeMismatch{ - typeChecker.numberType, - typeChecker.booleanType, - }})); -} - -TEST_CASE_FIXTURE(Fixture, "infer_return_type") -{ - CheckResult result = check("function take_five() return 5 end"); - LUAU_REQUIRE_NO_ERRORS(result); - - const FunctionTypeVar* takeFiveType = get(requireType("take_five")); - REQUIRE(takeFiveType != nullptr); - - std::vector retVec = flatten(takeFiveType->retType).first; - REQUIRE(!retVec.empty()); - - REQUIRE_EQ(*follow(retVec[0]), *typeChecker.numberType); -} - -TEST_CASE_FIXTURE(Fixture, "infer_from_function_return_type") -{ - CheckResult result = check("function take_five() return 5 end local five = take_five()"); - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(*typeChecker.numberType, *follow(requireType("five"))); -} - -TEST_CASE_FIXTURE(Fixture, "cannot_call_primitives") -{ - CheckResult result = check("local foo = 5 foo()"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - - REQUIRE(get(result.errors[0]) != nullptr); -} - -TEST_CASE_FIXTURE(Fixture, "cannot_call_tables") -{ - CheckResult result = check("local foo = {} foo()"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK(get(result.errors[0]) != nullptr); -} - -TEST_CASE_FIXTURE(Fixture, "infer_that_function_does_not_return_a_table") -{ - CheckResult result = check(R"( - function take_five() - return 5 - end - - take_five().prop = 888 - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(result.errors[0], (TypeError{Location{Position{5, 8}, Position{5, 24}}, NotATable{typeChecker.numberType}})); -} - TEST_CASE_FIXTURE(Fixture, "expr_statement") { CheckResult result = check("local foo = 5 foo()"); LUAU_REQUIRE_ERROR_COUNT(1, result); } -TEST_CASE_FIXTURE(Fixture, "generic_function") -{ - CheckResult result = check(R"( - function id(x) return x end - local a = id(55) - local b = id(nil) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(*typeChecker.numberType, *requireType("a")); - CHECK_EQ(*typeChecker.nilType, *requireType("b")); -} - -TEST_CASE_FIXTURE(Fixture, "vararg_functions_should_allow_calls_of_any_types_and_size") -{ - CheckResult result = check(R"( - function f(...) end - - f(1) - f("foo", 2) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "vararg_function_is_quantified") -{ - CheckResult result = check(R"( - local T = {} - function T.f(...) - local result = {} - - for i = 1, select("#", ...) do - local dictionary = select(i, ...) - for key, value in pairs(dictionary) do - result[key] = value - end - end - - return result - end - - return T - )"); - - auto r = first(getMainModule()->getModuleScope()->returnType); - REQUIRE(r); - - TableTypeVar* ttv = getMutable(*r); - REQUIRE(ttv); - - TypeId k = ttv->props["f"].type; - REQUIRE(k); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "for_loop") -{ - CheckResult result = check(R"( - local q - for i=0, 50, 2 do - q = i - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(*typeChecker.numberType, *requireType("q")); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop") -{ - CheckResult result = check(R"( - local n - local s - for i, v in pairs({ "foo" }) do - n = i - s = v - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(*typeChecker.numberType, *requireType("n")); - CHECK_EQ(*typeChecker.stringType, *requireType("s")); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_with_next") -{ - CheckResult result = check(R"( - local n - local s - for i, v in next, { "foo", "bar" } do - n = i - s = v - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(*typeChecker.numberType, *requireType("n")); - CHECK_EQ(*typeChecker.stringType, *requireType("s")); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_with_an_iterator_of_type_any") -{ - CheckResult result = check(R"( - local it: any - local a, b - for i, v in it do - a, b = i, v - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_should_fail_with_non_function_iterator") -{ - CheckResult result = check(R"( - local foo = "bar" - for i, v in foo do - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_with_just_one_iterator_is_ok") -{ - CheckResult result = check(R"( - local function keys(dictionary) - local new = {} - local index = 1 - - for key in pairs(dictionary) do - new[index] = key - index = index + 1 - end - - return new - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_with_a_custom_iterator_should_type_check") -{ - CheckResult result = check(R"( - local function range(l, h): () -> number - return function() - return l - end - end - - for n: string in range(1, 10) do - print(n) - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_error") -{ - CheckResult result = check(R"( - function f(x) - gobble.prop = x.otherprop - end - - local p - for _, part in i_am_not_defined do - p = part - f(part) - part.thirdprop = false - end - )"); - - CHECK_EQ(2, result.errors.size()); - - TypeId p = requireType("p"); - CHECK_EQ("*unknown*", toString(p)); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_non_function") -{ - CheckResult result = check(R"( - local bad_iter = 5 - - for a in bad_iter() do - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - REQUIRE(get(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_error_on_factory_not_returning_the_right_amount_of_values") -{ - CheckResult result = check(R"( - local function hasDivisors(value: number, table) - return false - end - - function prime_iter(state, index) - while hasDivisors(index, state) do - index += 1 - end - - state[index] = true - return index - end - - function primes1() - return prime_iter, {} - end - - function primes2() - return prime_iter, {}, "" - end - - function primes3() - return prime_iter, {}, 2 - end - - for p in primes1() do print(p) end -- mismatch in argument count - - for p in primes2() do print(p) end -- mismatch in argument types, prime_iter takes {}, number, we are given {}, string - - for p in primes3() do print(p) end -- no error - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - - CountMismatch* acm = get(result.errors[0]); - REQUIRE(acm); - CHECK_EQ(acm->context, CountMismatch::Arg); - CHECK_EQ(2, acm->expected); - CHECK_EQ(1, acm->actual); - - TypeMismatch* tm = get(result.errors[1]); - REQUIRE(tm); - CHECK_EQ(typeChecker.numberType, tm->wantedType); - CHECK_EQ(typeChecker.stringType, tm->givenType); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_error_on_iterator_requiring_args_but_none_given") -{ - CheckResult result = check(R"( - function prime_iter(state, index) - return 1 - end - - for p in prime_iter do print(p) end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CountMismatch* acm = get(result.errors[0]); - REQUIRE(acm); - CHECK_EQ(acm->context, CountMismatch::Arg); - CHECK_EQ(2, acm->expected); - CHECK_EQ(0, acm->actual); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any") -{ - CheckResult result = check(R"( - function bar(): any - return true - end - - local a - for b in bar do - a = b - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(typeChecker.anyType, requireType("a")); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2") -{ - CheckResult result = check(R"( - function bar(): any - return true - end - - local a - for b in bar() do - a = b - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("any", toString(requireType("a"))); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any") -{ - CheckResult result = check(R"( - local bar: any - - local a - for b in bar do - a = b - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("any", toString(requireType("a"))); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2") -{ - CheckResult result = check(R"( - local bar: any - - local a - for b in bar() do - a = b - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("any", toString(requireType("a"))); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error") -{ - CheckResult result = check(R"( - local a - for b in bar do - a = b - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK_EQ("*unknown*", toString(requireType("a"))); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error2") -{ - CheckResult result = check(R"( - function bar(c) return c end - - local a - for b in bar() do - a = b - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK_EQ("*unknown*", toString(requireType("a"))); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_with_custom_iterator") -{ - CheckResult result = check(R"( - function primes() - return function (state: number) end, 2 - end - - for p, q in primes do - q = "" - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - TypeMismatch* tm = get(result.errors[0]); - REQUIRE(tm); - CHECK_EQ(typeChecker.numberType, tm->wantedType); - CHECK_EQ(typeChecker.stringType, tm->givenType); -} - TEST_CASE_FIXTURE(Fixture, "if_statement") { CheckResult result = check(R"( @@ -583,474 +124,6 @@ TEST_CASE_FIXTURE(Fixture, "if_statement") CHECK_EQ(*typeChecker.numberType, *requireType("b")); } -TEST_CASE_FIXTURE(Fixture, "while_loop") -{ - CheckResult result = check(R"( - local i - while true do - i = 8 - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(*typeChecker.numberType, *requireType("i")); -} - -TEST_CASE_FIXTURE(Fixture, "repeat_loop") -{ - CheckResult result = check(R"( - local i - repeat - i = 'hi' - until true - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(*typeChecker.stringType, *requireType("i")); -} - -TEST_CASE_FIXTURE(Fixture, "repeat_loop_condition_binds_to_its_block") -{ - CheckResult result = check(R"( - repeat - local x = true - until x - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "symbols_in_repeat_block_should_not_be_visible_beyond_until_condition") -{ - CheckResult result = check(R"( - repeat - local x = true - until x - - print(x) - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "table_length") -{ - CheckResult result = check(R"( - local t = {} - local s = #t - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK(nullptr != get(requireType("t"))); - CHECK_EQ(*typeChecker.numberType, *requireType("s")); -} - -TEST_CASE_FIXTURE(Fixture, "string_length") -{ - CheckResult result = check(R"( - local s = "Hello, World!" - local t = #s - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("number", toString(requireType("t"))); -} - -TEST_CASE_FIXTURE(Fixture, "string_index") -{ - CheckResult result = check(R"( - local s = "Hello, World!" - local t = s[4] - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - NotATable* nat = get(result.errors[0]); - REQUIRE(nat); - CHECK_EQ("string", toString(nat->ty)); - - CHECK_EQ("*unknown*", toString(requireType("t"))); -} - -TEST_CASE_FIXTURE(Fixture, "length_of_error_type_does_not_produce_an_error") -{ - CheckResult result = check(R"( - local l = #this_is_not_defined - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "indexing_error_type_does_not_produce_an_error") -{ - CheckResult result = check(R"( - local originalReward = unknown.Parent.Reward:GetChildren()[1] - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "nil_assign_doesnt_hit_indexer") -{ - CheckResult result = check("local a = {} a[0] = 7 a[0] = nil"); - LUAU_REQUIRE_ERROR_COUNT(0, result); -} - -TEST_CASE_FIXTURE(Fixture, "wrong_assign_does_hit_indexer") -{ - CheckResult result = check("local a = {} a[0] = 7 a[0] = 't'"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 30}, Position{0, 33}}, TypeMismatch{ - typeChecker.numberType, - typeChecker.stringType, - }})); -} - -TEST_CASE_FIXTURE(Fixture, "nil_assign_doesnt_hit_no_indexer") -{ - CheckResult result = check("local a = {a=1, b=2} a['a'] = nil"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 30}, Position{0, 33}}, TypeMismatch{ - typeChecker.numberType, - typeChecker.nilType, - }})); -} - -TEST_CASE_FIXTURE(Fixture, "dot_on_error_type_does_not_produce_an_error") -{ - CheckResult result = check(R"( - local foo = (true).x - foo.x = foo.y - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon") -{ - CheckResult result = check(R"( - local someTable = {} - - someTable.Function1 = function(Arg1) - end - - someTable.Function1() -- Argument count mismatch - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - REQUIRE(get(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2") -{ - CheckResult result = check(R"( - local someTable = {} - - someTable.Function2 = function(Arg1, Arg2) - end - - someTable.Function2() -- Argument count mismatch - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - REQUIRE(get(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_another_overload_works") -{ - CheckResult result = check(R"( - type T = {method: ((T, number) -> number) & ((number) -> number)} - local T: T - - T.method(4) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "list_only_alternative_overloads_that_match_argument_count") -{ - CheckResult result = check(R"( - local multiply: ((number)->number) & ((number)->string) & ((number, number)->number) - multiply("") - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - - TypeMismatch* tm = get(result.errors[0]); - REQUIRE(tm); - CHECK_EQ(typeChecker.numberType, tm->wantedType); - CHECK_EQ(typeChecker.stringType, tm->givenType); - - ExtraInformation* ei = get(result.errors[1]); - REQUIRE(ei); - CHECK_EQ("Other overloads are also not viable: (number) -> string", ei->message); -} - -TEST_CASE_FIXTURE(Fixture, "list_all_overloads_if_no_overload_takes_given_argument_count") -{ - CheckResult result = check(R"( - local multiply: ((number)->number) & ((number)->string) & ((number, number)->number) - multiply() - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - - GenericError* ge = get(result.errors[0]); - REQUIRE(ge); - CHECK_EQ("No overload for function accepts 0 arguments.", ge->message); - - ExtraInformation* ei = get(result.errors[1]); - REQUIRE(ei); - CHECK_EQ("Available overloads: (number) -> number; (number) -> string; and (number, number) -> number", ei->message); -} - -TEST_CASE_FIXTURE(Fixture, "dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists") -{ - CheckResult result = check(R"( - local multiply: ((number)->number) & ((number)->string) & ((number, number)->number) - multiply(1, "") - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - TypeMismatch* tm = get(result.errors[0]); - REQUIRE(tm); - CHECK_EQ(typeChecker.numberType, tm->wantedType); - CHECK_EQ(typeChecker.stringType, tm->givenType); -} - -TEST_CASE_FIXTURE(Fixture, "infer_return_type_from_selected_overload") -{ - CheckResult result = check(R"( - type T = {method: ((T, number) -> number) & ((number) -> string)} - local T: T - - local a = T.method(T, 4) - local b = T.method(5) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("number", toString(requireType("a"))); - CHECK_EQ("string", toString(requireType("b"))); -} - -TEST_CASE_FIXTURE(Fixture, "too_many_arguments") -{ - CheckResult result = check(R"( - --!nonstrict - - function g(a: number) end - - g() - - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - auto err = result.errors[0]; - auto acm = get(err); - REQUIRE(acm); - - CHECK_EQ(1, acm->expected); - CHECK_EQ(0, acm->actual); -} - -TEST_CASE_FIXTURE(Fixture, "any_type_propagates") -{ - CheckResult result = check(R"( - local foo: any - local bar = foo:method("argument") - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("any", toString(requireType("bar"))); -} - -TEST_CASE_FIXTURE(Fixture, "can_subscript_any") -{ - CheckResult result = check(R"( - local foo: any - local bar = foo[5] - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("any", toString(requireType("bar"))); -} - -// Not strictly correct: metatables permit overriding this -TEST_CASE_FIXTURE(Fixture, "can_get_length_of_any") -{ - CheckResult result = check(R"( - local foo: any = {} - local bar = #foo - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(PrimitiveTypeVar::Number, getPrimitiveType(requireType("bar"))); -} - -TEST_CASE_FIXTURE(Fixture, "recursive_function") -{ - CheckResult result = check(R"( - function count(n: number) - if n == 0 then - return 0 - else - return count(n - 1) - end - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "lambda_form_of_local_function_cannot_be_recursive") -{ - CheckResult result = check(R"( - local f = function() return f() end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "recursive_local_function") -{ - CheckResult result = check(R"( - local function count(n: number) - if n == 0 then - return 0 - else - return count(n - 1) - end - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -// FIXME: This and the above case get handled very differently. It's pretty dumb. -// We really should unify the two code paths, probably by deleting AstStatFunction. -TEST_CASE_FIXTURE(Fixture, "another_recursive_local_function") -{ - CheckResult result = check(R"( - local count - function count(n: number) - if n == 0 then - return 0 - else - return count(n - 1) - end - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "cyclic_function_type_in_rets") -{ - CheckResult result = check(R"( - function f() - return f - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("t1 where t1 = () -> t1", toString(requireType("f"))); -} - -TEST_CASE_FIXTURE(Fixture, "cyclic_function_type_in_args") -{ - CheckResult result = check(R"( - function f(g) - return f(f) - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("t1 where t1 = (t1) -> ()", toString(requireType("f"))); -} - -// TODO: File a Jira about this -/* -TEST_CASE_FIXTURE(Fixture, "unifying_vararg_pack_with_fixed_length_pack_produces_fixed_length_pack") -{ - CheckResult result = check(R"( - function a(x) return 1 end - a(...) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - REQUIRE(bool(getMainModule()->getModuleScope()->varargPack)); - - TypePackId varargPack = *getMainModule()->getModuleScope()->varargPack; - - auto iter = begin(varargPack); - auto endIter = end(varargPack); - - CHECK(iter != endIter); - ++iter; - CHECK(iter == endIter); - - CHECK(!iter.tail()); -} -*/ - -TEST_CASE_FIXTURE(Fixture, "method_depends_on_table") -{ - CheckResult result = check(R"( - -- This catches a bug where x:m didn't count as a use of x - -- so toposort would happily reorder a definition of - -- function x:m before the definition of x. - function g() f() end - local x = {} - function x:m() end - function f() x:m() end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "another_higher_order_function") -{ - CheckResult result = check(R"( - local Get_des - function Get_des(func) - Get_des(func) - end - - local function f(d) - d:IsA("BasePart") - d.Parent:FindFirstChild("Humanoid") - d:IsA("Decal") - end - Get_des(f) - - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "another_other_higher_order_function") -{ - CheckResult result = check(R"( - local d - d:foo() - d:foo() - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - TEST_CASE_FIXTURE(Fixture, "statements_are_topologically_sorted") { CheckResult result = check(R"( @@ -1067,121 +140,6 @@ TEST_CASE_FIXTURE(Fixture, "statements_are_topologically_sorted") dumpErrors(result); } -TEST_CASE_FIXTURE(Fixture, "generic_table_method") -{ - CheckResult result = check(R"( - local T = {} - - function T:bar(i) - return i - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - TypeId tType = requireType("T"); - TableTypeVar* tTable = getMutable(tType); - REQUIRE(tTable != nullptr); - - TypeId barType = tTable->props["bar"].type; - REQUIRE(barType != nullptr); - - const FunctionTypeVar* ftv = get(follow(barType)); - REQUIRE_MESSAGE(ftv != nullptr, "Should be a function: " << *barType); - - std::vector args = flatten(ftv->argTypes).first; - TypeId argType = args.at(1); - - CHECK_MESSAGE(get(argType), "Should be generic: " << *barType); -} - -TEST_CASE_FIXTURE(Fixture, "correctly_instantiate_polymorphic_member_functions") -{ - CheckResult result = check(R"( - local T = {} - - function T:foo() - return T:bar(5) - end - - function T:bar(i) - return i - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); - - const TableTypeVar* t = get(requireType("T")); - REQUIRE(t != nullptr); - - std::optional fooProp = get(t->props, "foo"); - REQUIRE(bool(fooProp)); - - const FunctionTypeVar* foo = get(follow(fooProp->type)); - REQUIRE(bool(foo)); - - std::optional ret_ = first(foo->retType); - REQUIRE(bool(ret_)); - TypeId ret = follow(*ret_); - - REQUIRE_EQ(getPrimitiveType(ret), PrimitiveTypeVar::Number); -} - -TEST_CASE_FIXTURE(Fixture, "methods_are_topologically_sorted") -{ - CheckResult result = check(R"( - local T = {} - - function T:foo() - return T:bar(999), T:bar("hi") - end - - function T:bar(i) - return i - end - - local a, b = T:foo() - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); - - CHECK_EQ(PrimitiveTypeVar::Number, getPrimitiveType(requireType("a"))); - CHECK_EQ(PrimitiveTypeVar::String, getPrimitiveType(requireType("b"))); -} - -TEST_CASE_FIXTURE(Fixture, "local_function") -{ - CheckResult result = check(R"( - function f() - return 8 - end - - function g() - local function f() - return 'hello' - end - return f - end - - local h = g() - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - TypeId h = follow(requireType("h")); - - const FunctionTypeVar* ftv = get(h); - REQUIRE(ftv != nullptr); - - std::optional rt = first(ftv->retType); - REQUIRE(bool(rt)); - - TypeId retType = follow(*rt); - CHECK_EQ(PrimitiveTypeVar::String, getPrimitiveType(retType)); -} - TEST_CASE_FIXTURE(Fixture, "unify_nearly_identical_recursive_types") { CheckResult result = check(R"( @@ -1193,267 +151,10 @@ TEST_CASE_FIXTURE(Fixture, "unify_nearly_identical_recursive_types") o = p )"); -} - -/* - * We had a bug in instantiation where the argument types of 'f' and 'g' would be inferred as - * f {+ method: function(): (t2, T3...) +} - * g {+ method: function({+ method: function(): (t2, T3...) +}): (t5, T6...) +} - * - * The type of 'g' is totally wrong as t2 and t5 should be unified, as should T3 with T6. - * - * The correct unification of the argument to 'g' is - * - * {+ method: function(): (t5, T6...) +} - */ -TEST_CASE_FIXTURE(Fixture, "instantiate_cyclic_generic_function") -{ - auto result = check(R"( - function f(o) - o:method() - end - - function g(o) - f(o) - end - )"); - - TypeId g = requireType("g"); - const FunctionTypeVar* gFun = get(g); - REQUIRE(gFun != nullptr); - - auto optionArg = first(gFun->argTypes); - REQUIRE(bool(optionArg)); - - TypeId arg = follow(*optionArg); - const TableTypeVar* argTable = get(arg); - REQUIRE(argTable != nullptr); - - std::optional methodProp = get(argTable->props, "method"); - REQUIRE(bool(methodProp)); - - const FunctionTypeVar* methodFunction = get(methodProp->type); - REQUIRE(methodFunction != nullptr); - - std::optional methodArg = first(methodFunction->argTypes); - REQUIRE(bool(methodArg)); - - REQUIRE_EQ(follow(*methodArg), follow(arg)); -} - -TEST_CASE_FIXTURE(Fixture, "varlist_declared_by_for_in_loop_should_be_free") -{ - CheckResult result = check(R"( - local T = {} - - function T.f(p) - for i, v in pairs(p) do - T.f(v) - end - end - )"); LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(Fixture, "properly_infer_iteratee_is_a_free_table") -{ - // In this case, we cannot know the element type of the table {}. It could be anything. - // We therefore must initially ascribe a free typevar to iter. - CheckResult result = check(R"( - for iter in pairs({}) do - iter:g().p = true - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "quantify_methods_defined_using_dot_syntax_and_explicit_self_parameter") -{ - check(R"( - local T = {} - - function T.method(self) - self:method() - end - - function T.method2(self) - self:method() - end - - T:method2() - )"); -} - -TEST_CASE_FIXTURE(Fixture, "free_rhs_table_can_also_be_bound") -{ - check(R"( - local o - local v = o:i() - - function g(u) - v = u - end - - o:f(g) - o:h() - o:h() - )"); -} - -TEST_CASE_FIXTURE(Fixture, "require") -{ - fileResolver.source["game/A"] = R"( - local function hooty(x: number): string - return "Hi there!" - end - - return {hooty=hooty} - )"; - - fileResolver.source["game/B"] = R"( - local Hooty = require(game.A) - - local h -- free! - local i = Hooty.hooty(h) - )"; - - CheckResult aResult = frontend.check("game/A"); - dumpErrors(aResult); - LUAU_REQUIRE_NO_ERRORS(aResult); - - CheckResult bResult = frontend.check("game/B"); - dumpErrors(bResult); - LUAU_REQUIRE_NO_ERRORS(bResult); - - ModulePtr b = frontend.moduleResolver.modules["game/B"]; - - REQUIRE(b != nullptr); - - dumpErrors(bResult); - - std::optional iType = requireType(b, "i"); - REQUIRE_EQ("string", toString(*iType)); - - std::optional hType = requireType(b, "h"); - REQUIRE_EQ("number", toString(*hType)); -} - -TEST_CASE_FIXTURE(Fixture, "require_types") -{ - fileResolver.source["workspace/A"] = R"( - export type Point = {x: number, y: number} - - return {} - )"; - - fileResolver.source["workspace/B"] = R"( - local Hooty = require(workspace.A) - - local h: Hooty.Point - )"; - - CheckResult bResult = frontend.check("workspace/B"); - dumpErrors(bResult); - - ModulePtr b = frontend.moduleResolver.modules["workspace/B"]; - REQUIRE(b != nullptr); - - TypeId hType = requireType(b, "h"); - REQUIRE_MESSAGE(bool(get(hType)), "Expected table but got " << toString(hType)); -} - -TEST_CASE_FIXTURE(Fixture, "require_a_variadic_function") -{ - fileResolver.source["game/A"] = R"( - local T = {} - function T.f(...) end - return T - )"; - - fileResolver.source["game/B"] = R"( - local A = require(game.A) - local f = A.f - )"; - - CheckResult result = frontend.check("game/B"); - - ModulePtr bModule = frontend.moduleResolver.getModule("game/B"); - REQUIRE(bModule != nullptr); - - TypeId f = follow(requireType(bModule, "f")); - - const FunctionTypeVar* ftv = get(f); - REQUIRE(ftv); - - auto iter = begin(ftv->argTypes); - auto endIter = end(ftv->argTypes); - - REQUIRE(iter == endIter); - REQUIRE(iter.tail()); - - CHECK(get(*iter.tail())); -} - -TEST_CASE_FIXTURE(Fixture, "assign_prop_to_table_by_calling_any_yields_any") -{ - CheckResult result = check(R"( - local f: any - local T = {} - - T.prop = f() - - return T - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - TableTypeVar* ttv = getMutable(requireType("T")); - REQUIRE(ttv); - REQUIRE(ttv->props.count("prop")); - - REQUIRE_EQ("any", toString(ttv->props["prop"].type)); -} - -TEST_CASE_FIXTURE(Fixture, "type_error_of_unknown_qualified_type") -{ - CheckResult result = check(R"( - local p: SomeModule.DoesNotExist - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - REQUIRE_EQ(result.errors[0], (TypeError{Location{{1, 17}, {1, 40}}, UnknownSymbol{"SomeModule.DoesNotExist"}})); -} - -TEST_CASE_FIXTURE(Fixture, "require_module_that_does_not_export") -{ - const std::string sourceA = R"( - )"; - - const std::string sourceB = R"( - local Hooty = require(script.Parent.A) - )"; - - fileResolver.source["game/Workspace/A"] = sourceA; - fileResolver.source["game/Workspace/B"] = sourceB; - - frontend.check("game/Workspace/A"); - frontend.check("game/Workspace/B"); - - ModulePtr aModule = frontend.moduleResolver.modules["game/Workspace/A"]; - ModulePtr bModule = frontend.moduleResolver.modules["game/Workspace/B"]; - - CHECK(aModule->errors.empty()); - REQUIRE_EQ(1, bModule->errors.size()); - CHECK_MESSAGE(get(bModule->errors[0]), "Should be IllegalRequire: " << toString(bModule->errors[0])); - - auto hootyType = requireType(bModule, "Hooty"); - - CHECK_EQ("*unknown*", toString(hootyType)); -} - TEST_CASE_FIXTURE(Fixture, "warn_on_lowercase_parent_property") { CheckResult result = check(R"( @@ -1468,144 +169,6 @@ TEST_CASE_FIXTURE(Fixture, "warn_on_lowercase_parent_property") REQUIRE_EQ("parent", ed->symbol); } -TEST_CASE_FIXTURE(Fixture, "quantify_any_does_not_bind_to_itself") -{ - CheckResult result = check(R"( - local A : any - function A.B() end - A:C() - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - TypeId aType = requireType("A"); - CHECK_EQ(aType, typeChecker.anyType); -} - -TEST_CASE_FIXTURE(Fixture, "table_unifies_into_map") -{ - CheckResult result = check(R"( - local Instance: any - local UDim2: any - - function Create(instanceType) - return function(data) - local obj = Instance.new(instanceType) - for k, v in pairs(data) do - if type(k) == 'number' then - --v.Parent = obj - else - obj[k] = v - end - end - return obj - end - end - - local topbarShadow = Create'ImageLabel'{ - Name = "TopBarShadow"; - Size = UDim2.new(1, 0, 0, 3); - Position = UDim2.new(0, 0, 1, 0); - Image = "rbxasset://textures/ui/TopBar/dropshadow.png"; - BackgroundTransparency = 1; - Active = false; - Visible = false; - }; - - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "func_expr_doesnt_leak_free") -{ - CheckResult result = check(R"( - local p = function(x) return x end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - const Luau::FunctionTypeVar* fn = get(requireType("p")); - REQUIRE(fn); - auto ret = first(fn->retType); - REQUIRE(ret); - REQUIRE(get(follow(*ret))); -} - -TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments") -{ - CheckResult result = check(R"( - function foo(a, b) - return a(b) - end - - function bar() - local c: ((number)->number, number)->number = foo -- no error - c = foo -- no error - local d: ((number)->number, string)->number = foo -- error from arg 2 (string) not being convertable to number from the call a(b) - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - TypeMismatch* tm = get(result.errors[0]); - REQUIRE(tm); - CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType)); - CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType)); -} - -TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2") -{ - CheckResult result = check(R"( - function foo(a, b) - return a(b) - end - - function bar() - local _: (string, string)->number = foo -- string cannot be converted to (string)->number - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - TypeMismatch* tm = get(result.errors[0]); - REQUIRE(tm); - CHECK_EQ("(string, string) -> number", toString(tm->wantedType)); - CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType)); -} - -TEST_CASE_FIXTURE(Fixture, "string_method") -{ - CheckResult result = check(R"( - local p = ("tacos"):len() - )"); - CHECK_EQ(0, result.errors.size()); - - CHECK_EQ(*requireType("p"), *typeChecker.numberType); -} - -TEST_CASE_FIXTURE(Fixture, "string_function_indirect") -{ - CheckResult result = check(R"( - local s:string - local l = s.lower - local p = l(s) - )"); - CHECK_EQ(0, result.errors.size()); - - CHECK_EQ(*requireType("p"), *typeChecker.stringType); -} - -TEST_CASE_FIXTURE(Fixture, "string_function_other") -{ - CheckResult result = check(R"( - local s:string - local p = s:match("foo") - )"); - CHECK_EQ(0, result.errors.size()); - - CHECK_EQ(toString(requireType("p")), "string?"); -} - TEST_CASE_FIXTURE(Fixture, "weird_case") { CheckResult result = check(R"( @@ -1617,80 +180,6 @@ TEST_CASE_FIXTURE(Fixture, "weird_case") dumpErrors(result); } -TEST_CASE_FIXTURE(Fixture, "or_joins_types") -{ - CheckResult result = check(R"( - local s = "a" or 10 - local x:string|number = s - )"); - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ(toString(*requireType("s")), "number | string"); - CHECK_EQ(toString(*requireType("x")), "number | string"); -} - -TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_extras") -{ - CheckResult result = check(R"( - local s = "a" or 10 - local x:number|string = s - local y = x or "s" - )"); - CHECK_EQ(0, result.errors.size()); - CHECK_EQ(toString(*requireType("s")), "number | string"); - CHECK_EQ(toString(*requireType("y")), "number | string"); -} - -TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_superfluous_union") -{ - CheckResult result = check(R"( - local s = "a" or "b" - local x:string = s - )"); - CHECK_EQ(0, result.errors.size()); - CHECK_EQ(*requireType("s"), *typeChecker.stringType); -} - -TEST_CASE_FIXTURE(Fixture, "and_adds_boolean") -{ - CheckResult result = check(R"( - local s = "a" and 10 - local x:boolean|number = s - )"); - CHECK_EQ(0, result.errors.size()); - CHECK_EQ(toString(*requireType("s")), "boolean | number"); -} - -TEST_CASE_FIXTURE(Fixture, "and_adds_boolean_no_superfluous_union") -{ - CheckResult result = check(R"( - local s = "a" and true - local x:boolean = s - )"); - CHECK_EQ(0, result.errors.size()); - CHECK_EQ(*requireType("x"), *typeChecker.booleanType); -} - -TEST_CASE_FIXTURE(Fixture, "and_or_ternary") -{ - CheckResult result = check(R"( - local s = (1/2) > 0.5 and "a" or 10 - )"); - CHECK_EQ(0, result.errors.size()); - CHECK_EQ(toString(*requireType("s")), "number | string"); -} - -TEST_CASE_FIXTURE(Fixture, "first_argument_can_be_optional") -{ - CheckResult result = check(R"( - local T = {} - function T.new(a: number?, b: number?, c: number?) return 5 end - local m = T.new() - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); -} - TEST_CASE_FIXTURE(Fixture, "dont_ice_when_failing_the_occurs_check") { CheckResult result = check(R"( @@ -1726,303 +215,6 @@ TEST_CASE_FIXTURE(Fixture, "crazy_complexity") } #endif -// We had a bug where a cyclic union caused a stack overflow. -// ex type U = number | U -TEST_CASE_FIXTURE(Fixture, "dont_allow_cyclic_unions_to_be_inferred") -{ - CheckResult result = check(R"( - --!strict - - function f(a, b) - a:g(b or {}) - a:g(b) - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "it_is_ok_not_to_supply_enough_retvals") -{ - CheckResult result = check(R"( - function get_two() return 5, 6 end - - local a = get_two() - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); -} - -TEST_CASE_FIXTURE(Fixture, "duplicate_functions2") -{ - CheckResult result = check(R"( - function foo() end - - function bar() - local function foo() end - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(0, result); -} - -TEST_CASE_FIXTURE(Fixture, "duplicate_functions_allowed_in_nonstrict") -{ - CheckResult result = check(R"( - --!nonstrict - function foo() end - - function foo() end - - function bar() - local function foo() end - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "duplicate_functions_with_different_signatures_not_allowed_in_nonstrict") -{ - CheckResult result = check(R"( - --!nonstrict - function foo(): number - return 1 - end - foo() - - function foo(n: number): number - return 2 - end - foo() - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - TypeMismatch* tm = get(result.errors[0]); - REQUIRE(tm); - CHECK_EQ("() -> number", toString(tm->wantedType)); - CHECK_EQ("(number) -> number", toString(tm->givenType)); -} - -TEST_CASE_FIXTURE(Fixture, "tables_get_names_from_their_locals") -{ - CheckResult result = check(R"( - local T = {} - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("T", toString(requireType("T"))); -} - -TEST_CASE_FIXTURE(Fixture, "generalize_table_argument") -{ - CheckResult result = check(R"( - function foo(arr) - local work = {} - for i = 1, #arr do - work[i] = arr[i] - end - - return arr - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); - - const FunctionTypeVar* fooType = get(requireType("foo")); - REQUIRE(fooType); - - std::optional fooArg1 = first(fooType->argTypes); - REQUIRE(fooArg1); - - const TableTypeVar* fooArg1Table = get(*fooArg1); - REQUIRE(fooArg1Table); - - CHECK_EQ(fooArg1Table->state, TableState::Generic); -} - -TEST_CASE_FIXTURE(Fixture, "complicated_return_types_require_an_explicit_annotation") -{ - CheckResult result = check(R"( - local i = 0 - function most_of_the_natural_numbers(): number? - if i < 10 then - i = i + 1 - return i - else - return nil - end - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - const FunctionTypeVar* functionType = get(requireType("most_of_the_natural_numbers")); - - std::optional retType = first(functionType->retType); - REQUIRE(retType); - CHECK(get(*retType)); -} - -TEST_CASE_FIXTURE(Fixture, "infer_higher_order_function") -{ - CheckResult result = check(R"( - function apply(f, x) - return f(x) - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - const FunctionTypeVar* ftv = get(requireType("apply")); - REQUIRE(ftv != nullptr); - - std::vector argVec = flatten(ftv->argTypes).first; - - REQUIRE_EQ(2, argVec.size()); - - const FunctionTypeVar* fType = get(follow(argVec[0])); - REQUIRE(fType != nullptr); - - std::vector fArgs = flatten(fType->argTypes).first; - - TypeId xType = argVec[1]; - - CHECK_EQ(1, fArgs.size()); - CHECK_EQ(xType, fArgs[0]); -} - -TEST_CASE_FIXTURE(Fixture, "higher_order_function_2") -{ - CheckResult result = check(R"( - function bottomupmerge(comp, a, b, left, mid, right) - local i, j = left, mid - for k = left, right do - if i < mid and (j > right or not comp(a[j], a[i])) then - b[k] = a[i] - i = i + 1 - else - b[k] = a[j] - j = j + 1 - end - end - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - const FunctionTypeVar* ftv = get(requireType("bottomupmerge")); - REQUIRE(ftv != nullptr); - - std::vector argVec = flatten(ftv->argTypes).first; - - REQUIRE_EQ(6, argVec.size()); - - const FunctionTypeVar* fType = get(follow(argVec[0])); - REQUIRE(fType != nullptr); -} - -TEST_CASE_FIXTURE(Fixture, "higher_order_function_3") -{ - CheckResult result = check(R"( - function swap(p) - local t = p[0] - p[0] = p[1] - p[1] = t - return nil - end - - function swapTwice(p) - swap(p) - swap(p) - return p - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - const FunctionTypeVar* ftv = get(requireType("swapTwice")); - REQUIRE(ftv != nullptr); - - std::vector argVec = flatten(ftv->argTypes).first; - - REQUIRE_EQ(1, argVec.size()); - - const TableTypeVar* argType = get(follow(argVec[0])); - REQUIRE(argType != nullptr); - - CHECK(bool(argType->indexer)); -} - -TEST_CASE_FIXTURE(Fixture, "higher_order_function_4") -{ - CheckResult result = check(R"( - function bottomupmerge(comp, a, b, left, mid, right) - local i, j = left, mid - for k = left, right do - if i < mid and (j > right or not comp(a[j], a[i])) then - b[k] = a[i] - i = i + 1 - else - b[k] = a[j] - j = j + 1 - end - end - end - - function mergesort(arr, comp) - local work = {} - for i = 1, #arr do - work[i] = arr[i] - end - local width = 1 - while width < #arr do - for i = 1, #arr, 2*width do - bottomupmerge(comp, arr, work, i, math.min(i+width, #arr), math.min(i+2*width-1, #arr)) - end - local temp = work - work = arr - arr = temp - width = width * 2 - end - return arr - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); - - /* - * mergesort takes two arguments: an array of some type T and a function that takes two Ts. - * We must assert that these two types are in fact the same type. - * In other words, comp(arr[x], arr[y]) is well-typed. - */ - - const FunctionTypeVar* ftv = get(requireType("mergesort")); - REQUIRE(ftv != nullptr); - - std::vector argVec = flatten(ftv->argTypes).first; - - REQUIRE_EQ(2, argVec.size()); - - const TableTypeVar* arg0 = get(follow(argVec[0])); - REQUIRE(arg0 != nullptr); - REQUIRE(bool(arg0->indexer)); - - const FunctionTypeVar* arg1 = get(follow(argVec[1])); - REQUIRE(arg1 != nullptr); - REQUIRE_EQ(2, size(arg1->argTypes)); - - std::vector arg1Args = flatten(arg1->argTypes).first; - - CHECK_EQ(*arg0->indexer->indexResultType, *arg1Args[0]); - CHECK_EQ(*arg0->indexer->indexResultType, *arg1Args[1]); -} - TEST_CASE_FIXTURE(Fixture, "type_errors_infer_types") { CheckResult result = check(R"( @@ -2046,373 +238,6 @@ TEST_CASE_FIXTURE(Fixture, "type_errors_infer_types") CHECK_EQ("*unknown*", toString(requireType("f"))); } -TEST_CASE_FIXTURE(Fixture, "calling_error_type_yields_error") -{ - CheckResult result = check(R"( - local a = unknown.Parent.Reward.GetChildren() - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - UnknownSymbol* err = get(result.errors[0]); - REQUIRE(err != nullptr); - - CHECK_EQ("unknown", err->name); - - CHECK_EQ("*unknown*", toString(requireType("a"))); -} - -TEST_CASE_FIXTURE(Fixture, "chain_calling_error_type_yields_error") -{ - CheckResult result = check(R"( - local a = Utility.Create "Foo" {} - )"); - - CHECK_EQ("*unknown*", toString(requireType("a"))); -} - -TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable") -{ - CheckResult result = check(R"( - function add(a: number, b: string) - return a + (tonumber(b) :: number), a .. b - end - local n, s = add(2,"3") - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - const FunctionTypeVar* functionType = get(requireType("add")); - - std::optional retType = first(functionType->retType); - CHECK_EQ(std::optional(typeChecker.numberType), retType); - CHECK_EQ(requireType("n"), typeChecker.numberType); - CHECK_EQ(requireType("s"), typeChecker.stringType); -} - -TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable_with_follows") -{ - CheckResult result = check(R"( - local PI=3.1415926535897931 - local SOLAR_MASS=4*PI * PI - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ(requireType("SOLAR_MASS"), typeChecker.numberType); -} - -TEST_CASE_FIXTURE(Fixture, "primitive_arith_possible_metatable") -{ - CheckResult result = check(R"( - function add(a: number, b: any) - return a + b - end - local t = add(1,2) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("any", toString(requireType("t"))); -} - -TEST_CASE_FIXTURE(Fixture, "some_primitive_binary_ops") -{ - CheckResult result = check(R"( - local a = 4 + 8 - local b = a + 9 - local s = 'hotdogs' - local t = s .. s - local c = b - a - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("number", toString(requireType("a"))); - CHECK_EQ("number", toString(requireType("b"))); - CHECK_EQ("string", toString(requireType("s"))); - CHECK_EQ("string", toString(requireType("t"))); - CHECK_EQ("number", toString(requireType("c"))); -} - -TEST_CASE_FIXTURE(Fixture, "typecheck_overloaded_multiply_that_is_an_intersection") -{ - ScopedFastFlag sff{"LuauErrorRecoveryType", true}; - - CheckResult result = check(R"( - --!strict - local Vec3 = {} - Vec3.__index = Vec3 - function Vec3.new() - return setmetatable({x=0, y=0, z=0}, Vec3) - end - - export type Vec3 = typeof(Vec3.new()) - - local thefun: any = function(self, o) return self end - - local multiply: ((Vec3, Vec3) -> Vec3) & ((Vec3, number) -> Vec3) = thefun - - Vec3.__mul = multiply - - local a = Vec3.new() - local b = Vec3.new() - local c = a * b - local d = a * 2 - local e = a * 'cabbage' - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK_EQ("Vec3", toString(requireType("a"))); - CHECK_EQ("Vec3", toString(requireType("b"))); - CHECK_EQ("Vec3", toString(requireType("c"))); - CHECK_EQ("Vec3", toString(requireType("d"))); - CHECK_EQ("Vec3", toString(requireType("e"))); -} - -TEST_CASE_FIXTURE(Fixture, "typecheck_overloaded_multiply_that_is_an_intersection_on_rhs") -{ - ScopedFastFlag sff{"LuauErrorRecoveryType", true}; - - CheckResult result = check(R"( - --!strict - local Vec3 = {} - Vec3.__index = Vec3 - function Vec3.new() - return setmetatable({x=0, y=0, z=0}, Vec3) - end - - export type Vec3 = typeof(Vec3.new()) - - local thefun: any = function(self, o) return self end - - local multiply: ((Vec3, Vec3) -> Vec3) & ((Vec3, number) -> Vec3) = thefun - - Vec3.__mul = multiply - - local a = Vec3.new() - local b = Vec3.new() - local c = b * a - local d = 2 * a - local e = 'cabbage' * a - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK_EQ("Vec3", toString(requireType("a"))); - CHECK_EQ("Vec3", toString(requireType("b"))); - CHECK_EQ("Vec3", toString(requireType("c"))); - CHECK_EQ("Vec3", toString(requireType("d"))); - CHECK_EQ("Vec3", toString(requireType("e"))); -} - -TEST_CASE_FIXTURE(Fixture, "compare_numbers") -{ - CheckResult result = check(R"( - local a = 441 - local b = 0 - local c = a < b - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "compare_strings") -{ - CheckResult result = check(R"( - local a = '441' - local b = '0' - local c = a < b - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_metatable") -{ - CheckResult result = check(R"( - local a = {} - local b = {} - local c = a < b - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - GenericError* gen = get(result.errors[0]); - - REQUIRE_EQ(gen->message, "Type a cannot be compared with < because it has no metatable"); -} - -TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators") -{ - CheckResult result = check(R"( - local M = {} - function M.new() - return setmetatable({}, M) - end - type M = typeof(M.new()) - - local a = M.new() - local b = M.new() - local c = a < b - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - GenericError* gen = get(result.errors[0]); - REQUIRE(gen != nullptr); - REQUIRE_EQ(gen->message, "Table M does not offer metamethod __lt"); -} - -TEST_CASE_FIXTURE(Fixture, "cannot_compare_tables_that_do_not_have_the_same_metatable") -{ - CheckResult result = check(R"( - --!strict - local M = {} - function M.new() - return setmetatable({}, M) - end - function M.__lt(left, right) return true end - - local a = M.new() - local b = {} - local c = a < b -- line 10 - local d = b < a -- line 11 - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - - REQUIRE_EQ((Location{{10, 18}, {10, 23}}), result.errors[0].location); - - REQUIRE_EQ((Location{{11, 18}, {11, 23}}), result.errors[1].location); -} - -TEST_CASE_FIXTURE(Fixture, "produce_the_correct_error_message_when_comparing_a_table_with_a_metatable_with_one_that_does_not") -{ - CheckResult result = check(R"( - --!strict - local M = {} - function M.new() - return setmetatable({}, M) - end - function M.__lt(left, right) return true end - type M = typeof(M.new()) - - local a = M.new() - local b = {} - local c = a < b -- line 10 - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - auto err = get(result.errors[0]); - REQUIRE(err != nullptr); - - // Frail. :| - REQUIRE_EQ("Types M and b cannot be compared with < because they do not have the same metatable", err->message); -} - -TEST_CASE_FIXTURE(Fixture, "in_nonstrict_mode_strip_nil_from_intersections_when_considering_relational_operators") -{ - CheckResult result = check(R"( - --!nonstrict - - function maybe_a_number(): number? - return 50 - end - - local a = maybe_a_number() < maybe_a_number() - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -/* - * This test case exposed an oversight in the treatment of free tables. - * Free tables, like free TypeVars, need to record the scope depth where they were created so that - * we do not erroneously let-generalize them when they are used in a nested lambda. - * - * For more information about let-generalization, see - * - * The important idea here is that the return type of Counter.new is a table with some metatable. - * That metatable *must* be the same TypeVar as the type of Counter. If it is a copy (produced by - * the generalization process), then it loses the knowledge that its metatable will have an :incr() - * method. - */ -TEST_CASE_FIXTURE(Fixture, "dont_quantify_table_that_belongs_to_outer_scope") -{ - CheckResult result = check(R"( - local Counter = {} - Counter.__index = Counter - - function Counter.new() - local self = setmetatable({count=0}, Counter) - return self - end - - function Counter:incr() - self.count = 1 - return self.count - end - - local self = Counter.new() - print(self:incr()) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - TableTypeVar* counterType = getMutable(requireType("Counter")); - REQUIRE(counterType); - - const FunctionTypeVar* newType = get(follow(counterType->props["new"].type)); - REQUIRE(newType); - - std::optional newRetType = *first(newType->retType); - REQUIRE(newRetType); - - const MetatableTypeVar* newRet = get(follow(*newRetType)); - REQUIRE(newRet); - - const TableTypeVar* newRetMeta = get(newRet->metatable); - REQUIRE(newRetMeta); - - CHECK(newRetMeta->props.count("incr")); - CHECK_EQ(follow(newRet->metatable), follow(requireType("Counter"))); -} - -// TODO: CLI-39624 -TEST_CASE_FIXTURE(Fixture, "instantiate_tables_at_scope_level") -{ - CheckResult result = check(R"( - --!strict - local Option = {} - Option.__index = Option - function Option.Is(obj) - return (type(obj) == "table" and getmetatable(obj) == Option) - end - return Option - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "typeguard_doesnt_leak_to_elseif") -{ - const std::string code = R"( - function f(a) - if type(a) == "boolean" then - local a1 = a - elseif a.fn() then - local a2 = a - else - local a3 = a - end - end - )"; - CheckResult result = check(code); - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); -} - TEST_CASE_FIXTURE(Fixture, "should_be_able_to_infer_this_without_stack_overflowing") { CheckResult result = check(R"( @@ -2428,47 +253,6 @@ TEST_CASE_FIXTURE(Fixture, "should_be_able_to_infer_this_without_stack_overflowi LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(Fixture, "inferring_hundreds_of_self_calls_should_not_suffocate_memory") -{ - CheckResult result = check(R"( - ("foo") - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - :lower() - )"); - - ModulePtr module = getMainModule(); - CHECK_GE(50, module->internalTypes.typeVars.size()); -} - -TEST_CASE_FIXTURE(Fixture, "inferring_crazy_table_should_also_be_quick") -{ - CheckResult result = check(R"( - --!strict - function f(U) - U(w:s(an):c()():c():U(s):c():c():U(s):c():U(s):cU()):c():U(s):c():U(s):c():c():U(s):c():U(s):cU() - end - )"); - - ModulePtr module = getMainModule(); - CHECK_GE(100, module->internalTypes.typeVars.size()); -} - TEST_CASE_FIXTURE(Fixture, "exponential_blowup_from_copying_types") { CheckResult result = check(R"( @@ -2507,96 +291,6 @@ TEST_CASE_FIXTURE(Fixture, "exponential_blowup_from_copying_types") CHECK_GE(5, module->interfaceTypes.typeVars.size()); } -TEST_CASE_FIXTURE(Fixture, "mutual_recursion") -{ - CheckResult result = check(R"( - --!strict - - function newPlayerCharacter() - startGui() -- Unknown symbol 'startGui' - end - - local characterAddedConnection: any - function startGui() - characterAddedConnection = game:GetService("Players").LocalPlayer.CharacterAdded:connect(newPlayerCharacter) - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); -} - -TEST_CASE_FIXTURE(Fixture, "toposort_doesnt_break_mutual_recursion") -{ - CheckResult result = check(R"( - --!strict - local x = nil - function f() g() end - -- make sure print(x) doesn't get toposorted here, breaking the mutual block - function g() x = f end - print(x) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - dumpErrors(result); -} - -TEST_CASE_FIXTURE(Fixture, "object_constructor_can_refer_to_method_of_self") -{ - // CLI-30902 - CheckResult result = check(R"( - --!strict - - type Foo = { - fooConn: () -> () | nil - } - - local Foo = {} - Foo.__index = Foo - - function Foo.new() - local self: Foo = { - fooConn = nil, - } - setmetatable(self, Foo) - - self.fooConn = function() - self:method() -- Key 'method' not found in table self - end - - return self - end - - function Foo:method() - print("foo") - end - - local foo = Foo.new() - - -- TODO This is the best our current refinement support can offer :( - local bar = foo.fooConn - if bar then bar() end - - -- foo.fooConn() - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "replace_every_free_type_when_unifying_a_complex_function_with_any") -{ - CheckResult result = check(R"( - local a: any - local b - for _, i in pairs(a) do - b = i - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("any", toString(requireType("b"))); -} - // In these tests, a successful parse is required, so we need the parser to return the AST and then we can test the recursion depth limit in type // checker. We also want it to somewhat match up with production values, so we push up the parser recursion limit a little bit instead. TEST_CASE_FIXTURE(Fixture, "check_type_infer_recursion_limit") @@ -2640,7 +334,7 @@ TEST_CASE_FIXTURE(Fixture, "check_expr_recursion_limit") #if defined(LUAU_ENABLE_ASAN) int limit = 250; #elif defined(_DEBUG) || defined(_NOOPT) - int limit = 350; + int limit = 300; #else int limit = 600; #endif @@ -2653,146 +347,6 @@ TEST_CASE_FIXTURE(Fixture, "check_expr_recursion_limit") CHECK(nullptr != get(result.errors[0])); } -TEST_CASE_FIXTURE(Fixture, "compound_assign_basic") -{ - CheckResult result = check(R"( - local s = 10 - s += 20 - )"); - CHECK_EQ(0, result.errors.size()); - CHECK_EQ(toString(*requireType("s")), "number"); -} - -TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_op") -{ - CheckResult result = check(R"( - local s = 10 - s += true - )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(result.errors[0], (TypeError{Location{{2, 13}, {2, 17}}, TypeMismatch{typeChecker.numberType, typeChecker.booleanType}})); -} - -TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_result") -{ - CheckResult result = check(R"( - local s = 'hello' - s += 10 - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ(result.errors[0], (TypeError{Location{{2, 8}, {2, 9}}, TypeMismatch{typeChecker.numberType, typeChecker.stringType}})); - CHECK_EQ(result.errors[1], (TypeError{Location{{2, 8}, {2, 15}}, TypeMismatch{typeChecker.stringType, typeChecker.numberType}})); -} - -TEST_CASE_FIXTURE(Fixture, "compound_assign_metatable") -{ - CheckResult result = check(R"( - --!strict - type V2B = { x: number, y: number } - local v2b: V2B = { x = 0, y = 0 } - local VMT = {} - type V2 = typeof(setmetatable(v2b, VMT)) - - function VMT.__add(a: V2, b: V2): V2 - return setmetatable({ x = a.x + b.x, y = a.y + b.y }, VMT) - end - - local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT) - local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT) - v1 += v2 - )"); - CHECK_EQ(0, result.errors.size()); -} - -TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_metatable") -{ - CheckResult result = check(R"( - --!strict - type V2B = { x: number, y: number } - local v2b: V2B = { x = 0, y = 0 } - local VMT = {} - type V2 = typeof(setmetatable(v2b, VMT)) - - function VMT.__mod(a: V2, b: V2): number - return a.x * b.x + a.y * b.y - end - - local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT) - local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT) - v1 %= v2 - )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - - TypeMismatch* tm = get(result.errors[0]); - CHECK_EQ(*tm->wantedType, *requireType("v2")); - CHECK_EQ(*tm->givenType, *typeChecker.numberType); -} - -TEST_CASE_FIXTURE(Fixture, "dont_ice_if_a_TypePack_is_an_error") -{ - CheckResult result = check(R"( - --!strict - function f(s) - print(s) - return f - end - - f("foo")("bar") - )"); -} - -TEST_CASE_FIXTURE(Fixture, "check_function_before_lambda_that_uses_it") -{ - CheckResult result = check(R"( - --!nonstrict - - function f() - return 114 - end - - return function() - return f():andThen() - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "it_is_ok_to_oversaturate_a_higher_order_function_argument") -{ - CheckResult result = check(R"( - function onerror() end - function foo() end - xpcall(foo, onerror) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "another_indirect_function_case_where_it_is_ok_to_provide_too_many_arguments") -{ - CheckResult result = check(R"( - local mycb: (number, number) -> () - - function f() end - - mycb = f - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "call_to_any_yields_any") -{ - CheckResult result = check(R"( - local a: any - local b = a() - )"); - - REQUIRE_EQ("any", toString(requireType("b"))); -} - TEST_CASE_FIXTURE(Fixture, "globals") { CheckResult result = check(R"( @@ -2853,91 +407,6 @@ TEST_CASE_FIXTURE(Fixture, "globals_everywhere") CHECK_EQ("any", toString(requireType("bar"))); } -TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfAny") -{ - CheckResult result = check(R"( -local x: any = {} -function x:y(z: number) - local s: string = z -end -)"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfSealed") -{ - CheckResult result = check(R"( -local x: {prop: number} = {prop=9999} -function x:y(z: number) - local s: string = z -end -)"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); -} - -TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfNumber") -{ - ScopedFastFlag sff{"LuauErrorRecoveryType", true}; - - CheckResult result = check(R"( -local x: number = 9999 -function x:y(z: number) - local s: string = z -end -)"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); -} - -TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfError") -{ - CheckResult result = check(R"( -local x = (true).foo -function x:y(z: number) - local s: string = z -end -)"); - - LUAU_REQUIRE_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "CallOrOfFunctions") -{ - CheckResult result = check(R"( -function f() return 1; end -function g() return 2; end -(f or g)() -)"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "CallAndOrOfFunctions") -{ - CheckResult result = check(R"( -function f() return 1; end -function g() return 2; end -local x = false -(x and f or g)() -)"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "MixedPropertiesAndIndexers") -{ - CheckResult result = check(R"( -local x = {} -x.a = "a" -x[0] = true -x.b = 37 -)"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - TEST_CASE_FIXTURE(Fixture, "correctly_scope_locals_do") { CheckResult result = check(R"( @@ -2955,35 +424,6 @@ TEST_CASE_FIXTURE(Fixture, "correctly_scope_locals_do") CHECK_EQ(us->name, "a"); } -TEST_CASE_FIXTURE(Fixture, "correctly_scope_locals_while") -{ - CheckResult result = check(R"( - while true do - local a = 1 - end - - print(a) -- oops! - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - UnknownSymbol* us = get(result.errors[0]); - REQUIRE(us); - CHECK_EQ(us->name, "a"); -} - -TEST_CASE_FIXTURE(Fixture, "ipairs_produces_integral_indices") -{ - CheckResult result = check(R"( - local key - for i, e in ipairs({}) do key = i end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - REQUIRE_EQ("number", toString(requireType("key"))); -} - TEST_CASE_FIXTURE(Fixture, "checking_should_not_ice") { CHECK_NOTHROW(check(R"( @@ -2997,79 +437,6 @@ TEST_CASE_FIXTURE(Fixture, "checking_should_not_ice") CHECK_EQ("any", toString(requireType("value"))); } -TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_nonstrict") -{ - CheckResult result = check(R"( - --!nonstrict - - local function f1(v): number? - if v then - return 1 - end - end - - local function f2(v) - if v then - return 1 - end - end - - local function f3(v): () - if v then - return - end - end - - local function f4(v) - if v then - return - end - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - FunctionExitsWithoutReturning* err = get(result.errors[0]); - CHECK(err); -} - -TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_strict") -{ - CheckResult result = check(R"( - --!strict - - local function f1(v): number? - if v then - return 1 - end - end - - local function f2(v) - if v then - return 1 - end - end - - local function f3(v): () - if v then - return - end - end - - local function f4(v) - if v then - return - end - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - FunctionExitsWithoutReturning* annotatedErr = get(result.errors[0]); - CHECK(annotatedErr); - - FunctionExitsWithoutReturning* inferredErr = get(result.errors[1]); - CHECK(inferredErr); -} - // TEST_CASE_FIXTURE(Fixture, "infer_method_signature_of_argument") // { // CheckResult result = check(R"( @@ -3085,363 +452,6 @@ TEST_CASE_FIXTURE(Fixture, "report_exiting_without_return_strict") // CHECK_EQ("A", toString(requireType("f"))); // } -TEST_CASE_FIXTURE(Fixture, "warn_if_you_try_to_require_a_non_modulescript") -{ - fileResolver.source["Modules/A"] = ""; - fileResolver.sourceTypes["Modules/A"] = SourceCode::Local; - - fileResolver.source["Modules/B"] = R"( - local M = require(script.Parent.A) - )"; - - CheckResult result = frontend.check("Modules/B"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK(get(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "calling_function_with_incorrect_argument_type_yields_errors_spanning_argument") -{ - CheckResult result = check(R"( - function foo(a: number, b: string) end - - foo("Test", 123) - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - - CHECK_EQ(result.errors[0], (TypeError{Location{Position{3, 12}, Position{3, 18}}, TypeMismatch{ - typeChecker.numberType, - typeChecker.stringType, - }})); - - CHECK_EQ(result.errors[1], (TypeError{Location{Position{3, 20}, Position{3, 23}}, TypeMismatch{ - typeChecker.stringType, - typeChecker.numberType, - }})); -} - -TEST_CASE_FIXTURE(Fixture, "calling_function_with_anytypepack_doesnt_leak_free_types") -{ - CheckResult result = check(R"( - --!nonstrict - - function Test(a) - return 1, "" - end - - - local tab = {} - table.insert(tab, Test(1)); - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - ToStringOptions opts; - opts.exhaustive = true; - opts.maxTableLength = 0; - - CHECK_EQ("{any}", toString(requireType("tab"), opts)); -} - -TEST_CASE_FIXTURE(Fixture, "too_many_return_values") -{ - CheckResult result = check(R"( - --!strict - - function f() - return 55 - end - - local a, b = f() - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CountMismatch* acm = get(result.errors[0]); - REQUIRE(acm); - CHECK_EQ(acm->context, CountMismatch::Result); - CHECK_EQ(acm->expected, 1); - CHECK_EQ(acm->actual, 2); -} - -TEST_CASE_FIXTURE(Fixture, "ignored_return_values") -{ - CheckResult result = check(R"( - --!strict - - function f() - return 55, "" - end - - local a = f() - )"); - - LUAU_REQUIRE_ERROR_COUNT(0, result); -} - -TEST_CASE_FIXTURE(Fixture, "function_does_not_return_enough_values") -{ - CheckResult result = check(R"( - --!strict - - function f(): (number, string) - return 55 - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CountMismatch* acm = get(result.errors[0]); - REQUIRE(acm); - CHECK_EQ(acm->context, CountMismatch::Return); - CHECK_EQ(acm->expected, 2); - CHECK_EQ(acm->actual, 1); -} - -TEST_CASE_FIXTURE(Fixture, "typecheck_unary_minus") -{ - CheckResult result = check(R"( - --!strict - local foo = { - value = 10 - } - local mt = {} - setmetatable(foo, mt) - - mt.__unm = function(val: typeof(foo)): string - return val.value .. "test" - end - - local a = -foo - - local b = 1+-1 - - local bar = { - value = 10 - } - local c = -bar -- disallowed - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK_EQ("string", toString(requireType("a"))); - CHECK_EQ("number", toString(requireType("b"))); - - GenericError* gen = get(result.errors[0]); - REQUIRE_EQ(gen->message, "Unary operator '-' not supported by type 'bar'"); -} - -TEST_CASE_FIXTURE(Fixture, "unary_not_is_boolean") -{ - CheckResult result = check(R"( - local b = not "string" - local c = not (math.random() > 0.5 and "string" or 7) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - REQUIRE_EQ("boolean", toString(requireType("b"))); - REQUIRE_EQ("boolean", toString(requireType("c"))); -} - -TEST_CASE_FIXTURE(Fixture, "disallow_string_and_types_without_metatables_from_arithmetic_binary_ops") -{ - CheckResult result = check(R"( - --!strict - local a = "1.24" + 123 -- not allowed - - local foo = { - value = 10 - } - - local b = foo + 1 -- not allowed - - local bar = { - value = 1 - } - - local mt = {} - - setmetatable(bar, mt) - - mt.__add = function(a: typeof(bar), b: number): number - return a.value + b - end - - local c = bar + 1 -- allowed - - local d = bar + foo -- not allowed - )"); - - LUAU_REQUIRE_ERROR_COUNT(3, result); - - TypeMismatch* tm = get(result.errors[0]); - REQUIRE_EQ(*tm->wantedType, *typeChecker.numberType); - REQUIRE_EQ(*tm->givenType, *typeChecker.stringType); - - TypeMismatch* tm2 = get(result.errors[2]); - CHECK_EQ(*tm2->wantedType, *typeChecker.numberType); - CHECK_EQ(*tm2->givenType, *requireType("foo")); - - GenericError* gen2 = get(result.errors[1]); - REQUIRE_EQ(gen2->message, "Binary operator '+' not supported by types 'foo' and 'number'"); -} - -// CLI-29033 -TEST_CASE_FIXTURE(Fixture, "unknown_type_in_comparison") -{ - CheckResult result = check(R"( - function merge(lower, greater) - if lower.y == greater.y then - end - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "concat_op_on_free_lhs_and_string_rhs") -{ - CheckResult result = check(R"( - local function f(x) - return x .. "y" - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - REQUIRE(get(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "concat_op_on_string_lhs_and_free_rhs") -{ - CheckResult result = check(R"( - local function f(x) - return "foo" .. x - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("(string) -> string", toString(requireType("f"))); -} - -TEST_CASE_FIXTURE(Fixture, "strict_binary_op_where_lhs_unknown") -{ - std::vector ops = {"+", "-", "*", "/", "%", "^", ".."}; - - std::string src = R"( - function foo(a, b) - )"; - - for (const auto& op : ops) - src += "local _ = a " + op + "b\n"; - - src += "end"; - - CheckResult result = check(src); - LUAU_REQUIRE_ERROR_COUNT(ops.size(), result); - - CHECK_EQ("Unknown type used in + operation; consider adding a type annotation to 'a'", toString(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "function_cast_error_uses_correct_language") -{ - CheckResult result = check(R"( - function foo(a, b): number - return 0 - end - - local a: (string)->number = foo - local b: (number, number)->(number, number) = foo - - local c: (string, number)->number = foo -- no error - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - - auto tm1 = get(result.errors[0]); - REQUIRE(tm1); - - CHECK_EQ("(string) -> number", toString(tm1->wantedType)); - CHECK_EQ("(string, *unknown*) -> number", toString(tm1->givenType)); - - auto tm2 = get(result.errors[1]); - REQUIRE(tm2); - - CHECK_EQ("(number, number) -> (number, number)", toString(tm2->wantedType)); - CHECK_EQ("(string, *unknown*) -> number", toString(tm2->givenType)); -} - -TEST_CASE_FIXTURE(Fixture, "setmetatable_cant_be_used_to_mutate_global_types") -{ - { - Fixture fix; - - // inherit env from parent fixture checker - fix.typeChecker.globalScope = typeChecker.globalScope; - - fix.check(R"( ---!nonstrict -type MT = typeof(setmetatable) -function wtf(arg: {MT}): typeof(table) - arg = wtf(arg) -end -)"); - } - - // validate sharedEnv post-typecheck; valuable for debugging some typeck crashes but slows fuzzing down - // note: it's important for typeck to be destroyed at this point! - { - for (auto& p : typeChecker.globalScope->bindings) - { - toString(p.second.typeId); // toString walks the entire type, making sure ASAN catches access to destroyed type arenas - } - } -} - -TEST_CASE_FIXTURE(Fixture, "evil_table_unification") -{ - // this code re-infers the type of _ while processing fields of _, which can cause use-after-free - check(R"( ---!nonstrict -_ = ... -_:table(_,string)[_:gsub(_,...,n0)],_,_:gsub(_,string)[""],_:split(_,...,table)._,n0 = nil -do end -)"); -} - -TEST_CASE_FIXTURE(Fixture, "overload_is_not_a_function") -{ - check(R"( ---!nonstrict -function _(...):((typeof(not _))&(typeof(not _)))&((typeof(not _))&(typeof(not _))) -_(...)(setfenv,_,not _,"")[_] = nil -end -do end -_(...)(...,setfenv,_):_G() -)"); -} - -TEST_CASE_FIXTURE(Fixture, "cyclic_type_packs") -{ - // this has a risk of creating cyclic type packs, causing infinite loops / OOMs - check(R"( ---!nonstrict -_ += _(_,...) -repeat -_ += _(...) -until ... + _ -)"); - - check(R"( ---!nonstrict -_ += _(_(...,...),_(...)) -repeat -until _ -)"); -} - TEST_CASE_FIXTURE(Fixture, "cyclic_follow") { check(R"( @@ -3470,19 +480,6 @@ end )"); } -TEST_CASE_FIXTURE(Fixture, "and_binexps_dont_unify") -{ - CheckResult result = check(R"( - --!strict - local t = {} - while true and t[1] do - print(t[1].test) - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - struct FindFreeTypeVars { bool foundOne = false; @@ -3506,32 +503,6 @@ struct FindFreeTypeVars } }; -TEST_CASE_FIXTURE(Fixture, "dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar") -{ - CheckResult result = check("local x = setmetatable({})"); - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_CASE_FIXTURE(Fixture, "for_in_loop_where_iteratee_is_free") -{ - // This code doesn't pass typechecking. We just care that it doesn't crash. - (void)check(R"( - --!nonstrict - function _:_(...) - end - - repeat - if _ then - else - _ = ... - end - until _ - - for _ in _() do - end - )"); -} - TEST_CASE_FIXTURE(Fixture, "tc_after_error_recovery") { CheckResult result = check(R"( @@ -3604,36 +575,6 @@ TEST_CASE_FIXTURE(Fixture, "tc_after_error_recovery_no_replacement_name_in_error } } -TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators") -{ - CheckResult result = check(R"( - local a: boolean = true - local b: boolean = false - local foo = a < b - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - GenericError* ge = get(result.errors[0]); - REQUIRE(ge); - CHECK_EQ("Type 'boolean' cannot be compared with relational operator <", ge->message); -} - -TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2") -{ - CheckResult result = check(R"( - local a: number | string = "" - local b: number | string = 1 - local foo = a < b - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - GenericError* ge = get(result.errors[0]); - REQUIRE(ge); - CHECK_EQ("Type 'number | string' cannot be compared with relational operator <", ge->message); -} - TEST_CASE_FIXTURE(Fixture, "index_expr_should_be_checked") { CheckResult result = check(R"( @@ -3650,100 +591,6 @@ TEST_CASE_FIXTURE(Fixture, "index_expr_should_be_checked") CHECK_EQ("x", up->key); } -TEST_CASE_FIXTURE(Fixture, "unreachable_code_after_infinite_loop") -{ - { - CheckResult result = check(R"( - function unreachablecodepath(a): number - while true do - if a then return 10 end - end - -- unreachable - end - unreachablecodepath(4) - )"); - - LUAU_REQUIRE_ERROR_COUNT(0, result); - } - - { - CheckResult result = check(R"( - function reachablecodepath(a): number - while true do - if a then break end - return 10 - end - - print("x") -- correct error - end - reachablecodepath(4) - )"); - - LUAU_REQUIRE_ERRORS(result); - CHECK(get(result.errors[0])); - } - - { - CheckResult result = check(R"( - function unreachablecodepath(a): number - repeat - if a then return 10 end - until false - - -- unreachable - end - unreachablecodepath(4) - )"); - - LUAU_REQUIRE_ERROR_COUNT(0, result); - } - - { - CheckResult result = check(R"( - function reachablecodepath(a, b): number - repeat - if a then break end - - if b then return 10 end - until false - - print("x") -- correct error - end - reachablecodepath(4) - )"); - - LUAU_REQUIRE_ERRORS(result); - CHECK(get(result.errors[0])); - } - - { - CheckResult result = check(R"( - function unreachablecodepath(a: number?): number - repeat - return 10 - until a ~= nil - - -- unreachable - end - unreachablecodepath(4) - )"); - - LUAU_REQUIRE_ERROR_COUNT(0, result); - } -} - -TEST_CASE_FIXTURE(Fixture, "cli_38355_recursive_union") -{ - CheckResult result = check(R"( - --!strict - local _ - _ += _ and _ or _ and _ or _ and _ - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type contains a self-recursive construct that cannot be resolved", toString(result.errors[0])); -} - TEST_CASE_FIXTURE(Fixture, "stringify_nested_unions_with_optionals") { CheckResult result = check(R"( @@ -3759,58 +606,6 @@ TEST_CASE_FIXTURE(Fixture, "stringify_nested_unions_with_optionals") CHECK_EQ("(boolean | number | string)?", toString(tm->givenType)); } -TEST_CASE_FIXTURE(Fixture, "UnknownGlobalCompoundAssign") -{ - // In non-strict mode, global definition is still allowed - { - CheckResult result = check(R"( - --!nonstrict - a = a + 1 - print(a) - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'"); - } - - // In strict mode we no longer generate two errors from lhs - { - CheckResult result = check(R"( - --!strict - a += 1 - print(a) - )"); - - LUAU_REQUIRE_ERRORS(result); - CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'"); - } - - // In non-strict mode, compound assignment is not a definition, it's a modification - { - CheckResult result = check(R"( - --!nonstrict - a += 1 - print(a) - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'"); - } -} - -TEST_CASE_FIXTURE(Fixture, "loop_typecheck_crash_on_empty_optional") -{ - CheckResult result = check(R"( - local t = {} - for _ in t do - for _ in assert(missing()) do - end - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(2, result); -} - TEST_CASE_FIXTURE(Fixture, "cli_39932_use_unifier_in_ensure_methods") { CheckResult result = check(R"( @@ -3821,26 +616,6 @@ TEST_CASE_FIXTURE(Fixture, "cli_39932_use_unifier_in_ensure_methods") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(Fixture, "metatable_of_any_can_be_a_table") -{ - CheckResult result = check(R"( ---!strict -local T: any -T = {} -T.__index = T -function T.new(...) - local self = {} - setmetatable(self, T) - self:construct(...) - return self -end -function T:construct(index) -end -)"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - TEST_CASE_FIXTURE(Fixture, "dont_report_type_errors_within_an_AstStatError") { CheckResult result = check(R"( @@ -3868,64 +643,6 @@ TEST_CASE_FIXTURE(Fixture, "dont_ice_on_astexprerror") LUAU_REQUIRE_ERROR_COUNT(1, result); } -TEST_CASE_FIXTURE(Fixture, "strip_nil_from_lhs_or_operator") -{ - CheckResult result = check(R"( ---!strict -local a: number? = nil -local b: number = a or 1 - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "strip_nil_from_lhs_or_operator2") -{ - CheckResult result = check(R"( ---!nonstrict -local a: number? = nil -local b: number = a or 1 - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "dont_strip_nil_from_rhs_or_operator") -{ - CheckResult result = check(R"( ---!strict -local a: number? = nil -local b: number = 1 or a - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - TypeMismatch* tm = get(result.errors[0]); - REQUIRE(tm); - CHECK_EQ(typeChecker.numberType, tm->wantedType); - CHECK_EQ("number?", toString(tm->givenType)); -} - -TEST_CASE_FIXTURE(Fixture, "no_lossy_function_type") -{ - CheckResult result = check(R"( - --!strict - local tbl = {} - function tbl:abc(a: number, b: number) - return a - end - tbl:abc(1, 2) -- Line 6 - -- | Column 14 - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - TypeId type = requireTypeAtPosition(Position(6, 14)); - CHECK_EQ("(tbl, number, number) -> number", toString(type)); - auto ftv = get(type); - REQUIRE(ftv); - CHECK(ftv->hasSelf); -} - TEST_CASE_FIXTURE(Fixture, "luau_resolves_symbols_the_same_way_lua_does") { CheckResult result = check(R"( @@ -3943,326 +660,6 @@ TEST_CASE_FIXTURE(Fixture, "luau_resolves_symbols_the_same_way_lua_does") REQUIRE_MESSAGE(get(e) != nullptr, "Expected UnknownSymbol, but got " << e); } -TEST_CASE_FIXTURE(Fixture, "operator_eq_verifies_types_do_intersect") -{ - CheckResult result = check(R"( - type Array = { [number]: T } - type Fiber = { id: number } - type null = {} - - local fiberStack: Array = {} - local index = 0 - - local function f(fiber: Fiber) - local a = fiber ~= fiberStack[index] - local b = fiberStack[index] ~= fiber - end - - return f - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "general_require_call_expression") -{ - fileResolver.source["game/A"] = R"( ---!strict -return { def = 4 } - )"; - - fileResolver.source["game/B"] = R"( ---!strict -local tbl = { abc = require(game.A) } -local a : string = "" -a = tbl.abc.def - )"; - - CheckResult result = frontend.check("game/B"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "general_require_type_mismatch") -{ - fileResolver.source["game/A"] = R"( -return { def = 4 } - )"; - - fileResolver.source["game/B"] = R"( -local tbl: string = require(game.A) - )"; - - CheckResult result = frontend.check("game/B"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type '{| def: number |}' could not be converted into 'string'", toString(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "nonstrict_self_mismatch_tail") -{ - CheckResult result = check(R"( ---!nonstrict -local f = {} -function f:foo(a: number, b: number) end - -function bar(...) - f.foo(f, 1, ...) -end - -bar(2) -)"); - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "typeof_unresolved_function") -{ - CheckResult result = check(R"( -local function f(a: typeof(f)) end -)"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Unknown global 'f'", toString(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning") -{ - CheckResult result = check(R"( ---!nonstrict -local l0:any,l61:t0 = _,math -while _ do -_() -end -function _():t0 -end -type t0 = any -)"); - - std::optional ty = requireType("math"); - REQUIRE(ty); - - const TableTypeVar* ttv = get(*ty); - REQUIRE(ttv); - CHECK(ttv->instantiatedTypeParams.empty()); -} - -TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning_2") -{ - ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true}; - - CheckResult result = check(R"( -type X = T -type K = X -)"); - - LUAU_REQUIRE_NO_ERRORS(result); - - std::optional ty = requireType("math"); - REQUIRE(ty); - - const TableTypeVar* ttv = get(*ty); - REQUIRE(ttv); - CHECK(ttv->instantiatedTypeParams.empty()); -} - -TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning_3") -{ - ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true}; - - CheckResult result = check(R"( -type X = T -local a = {} -a.x = 4 -local b: X -a.y = 5 -local c: X -c = b -)"); - - LUAU_REQUIRE_NO_ERRORS(result); - - std::optional ty = requireType("a"); - REQUIRE(ty); - - const TableTypeVar* ttv = get(*ty); - REQUIRE(ttv); - CHECK(ttv->instantiatedTypeParams.empty()); -} - -TEST_CASE_FIXTURE(Fixture, "bound_free_table_export_is_ok") -{ - CheckResult result = check(R"( -local n = {} -function n:Clone() end - -local m = {} - -function m.a(x) - x:Clone() -end - -function m.b() - m.a(n) -end - -return m -)"); - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "self_recursive_instantiated_param") -{ - ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true}; - - // Mutability in type function application right now can create strange recursive types - CheckResult result = check(R"( -type Table = { a: number } -type Self = T -local a: Self
- )"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ(toString(requireType("a")), "Table"); -} - -TEST_CASE_FIXTURE(Fixture, "no_persistent_typelevel_change") -{ - TypeId mathTy = requireType(typeChecker.globalScope, "math"); - REQUIRE(mathTy); - TableTypeVar* ttv = getMutable(mathTy); - REQUIRE(ttv); - const FunctionTypeVar* ftv = get(ttv->props["frexp"].type); - REQUIRE(ftv); - auto original = ftv->level; - - CheckResult result = check("local a = math.frexp"); - - LUAU_REQUIRE_NO_ERRORS(result); - CHECK(ftv->level.level == original.level); - CHECK(ftv->level.subLevel == original.subLevel); -} - -TEST_CASE_FIXTURE(Fixture, "table_indexing_error_location") -{ - CheckResult result = check(R"( -local foo = {42} -local bar: number? -local baz = foo[bar] - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - CHECK_EQ(result.errors[0].location, Location{Position{3, 16}, Position{3, 19}}); -} - -TEST_CASE_FIXTURE(Fixture, "table_simple_call") -{ - CheckResult result = check(R"( -local a = setmetatable({ x = 2 }, { - __call = function(self) - return (self.x :: number) * 2 -- should work without annotation in the future - end -}) -local b = a() -local c = a(2) -- too many arguments - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Argument count mismatch. Function expects 1 argument, but 2 are specified", toString(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "custom_require_global") -{ - CheckResult result = check(R"( ---!nonstrict -require = function(a) end - -local crash = require(game.A) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "operator_eq_operands_are_not_subtypes_of_each_other_but_has_overlap") -{ - ScopedFastFlag sff1{"LuauEqConstraint", true}; - - CheckResult result = check(R"( - local function f(a: string | number, b: boolean | number) - return a == b - end - )"); - - // This doesn't produce any errors but for the wrong reasons. - // This unit test serves as a reminder to not try and unify the operands on `==`/`~=`. - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "access_index_metamethod_that_returns_variadic") -{ - CheckResult result = check(R"( - type Foo = {x: string} - local t = {} - setmetatable(t, { - __index = function(x: string): ...Foo - return {x = x} - end - }) - - local foo = t.bar - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - ToStringOptions o; - o.exhaustive = true; - CHECK_EQ("{| x: string |}", toString(requireType("foo"), o)); -} - -TEST_CASE_FIXTURE(Fixture, "detect_cyclic_typepacks") -{ - CheckResult result = check(R"( - type ( ... ) ( ) ; - ( ... ) ( - - ... ) ( - ... ) - type = ( ... ) ; - ( ... ) ( ) ( ... ) ; - ( ... ) "" - )"); - - CHECK_LE(0, result.errors.size()); -} - -TEST_CASE_FIXTURE(Fixture, "detect_cyclic_typepacks2") -{ - CheckResult result = check(R"( - function _(l0:((typeof((pcall)))|((((t0)->())|(typeof(-67108864)))|(any)))|(any),...):(((typeof(0))|(any))|(any),typeof(-67108864),any) - xpcall(_,_,_) - _(_,_,_) - end - )"); - - CHECK_LE(0, result.errors.size()); -} - -TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_quantifying") -{ - CheckResult result = check(R"( - function _(l0:t0): (any, ()->()) - end - - type t0 = t0 | {} - )"); - - CHECK_LE(0, result.errors.size()); - - std::optional t0 = getMainModule()->getModuleScope()->lookupType("t0"); - REQUIRE(t0); - CHECK_EQ("*unknown*", toString(t0->type)); - - auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) { - return get(err); - }); - CHECK(it != result.errors.end()); -} - TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_isoptional") { CheckResult result = check(R"( @@ -4316,22 +713,6 @@ TEST_CASE_FIXTURE(Fixture, "no_infinite_loop_when_trying_to_unify_uh_this") CHECK_LE(0, result.errors.size()); } -TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_flattenintersection") -{ - CheckResult result = check(R"( - local l0,l0 - repeat - type t0 = ((any)|((any)&((any)|((any)&((any)|(any))))))&(t0) - function _(l0):(t0)&(t0) - while nil do - end - end - until _(_)(_)._ - )"); - - CHECK_LE(0, result.errors.size()); -} - TEST_CASE_FIXTURE(Fixture, "no_heap_use_after_free_error") { CheckResult result = check(R"( @@ -4349,329 +730,6 @@ TEST_CASE_FIXTURE(Fixture, "no_heap_use_after_free_error") CHECK_LE(0, result.errors.size()); } -TEST_CASE_FIXTURE(Fixture, "dont_invalidate_the_properties_iterator_of_free_table_when_rolled_back") -{ - fileResolver.source["Module/Backend/Types"] = R"( - export type Fiber = { - return_: Fiber? - } - return {} - )"; - - fileResolver.source["Module/Backend"] = R"( - local Types = require(script.Types) - type Fiber = Types.Fiber - type ReactRenderer = { findFiberByHostInstance: () -> Fiber? } - - local function attach(renderer): () - local function getPrimaryFiber(fiber) - local alternate = fiber.alternate - return fiber - end - - local function getFiberIDForNative() - local fiber = renderer.findFiberByHostInstance() - fiber = fiber.return_ - return getPrimaryFiber(fiber) - end - end - - function culprit(renderer: ReactRenderer): () - attach(renderer) - end - - return culprit - )"; - - CheckResult result = frontend.check("Module/Backend"); -} - -TEST_CASE_FIXTURE(Fixture, "recursive_types_restriction_ok") -{ - CheckResult result = check(R"( - type Tree = { data: T, children: {Tree} } - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "recursive_types_restriction_not_ok") -{ - ScopedFastFlag sff{"LuauRecursiveTypeParameterRestriction", true}; - - CheckResult result = check(R"( - -- this would be an infinite type if we allowed it - type Tree = { data: T, children: {Tree<{T}>} } - )"); - - LUAU_REQUIRE_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "record_matching_overload") -{ - CheckResult result = check(R"( - type Overload = ((string) -> string) & ((number) -> number) - local abc: Overload - abc(1) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - // AstExprCall is the node that has the overload stored on it. - // findTypeAtPosition will look at the AstExprLocal, but this is not what - // we want to look at. - std::vector ancestry = findAstAncestryOfPosition(*getMainSourceModule(), Position(3, 10)); - REQUIRE_GE(ancestry.size(), 2); - AstExpr* parentExpr = ancestry[ancestry.size() - 2]->asExpr(); - REQUIRE(bool(parentExpr)); - REQUIRE(parentExpr->is()); - - ModulePtr module = getMainModule(); - auto it = module->astOverloadResolvedTypes.find(parentExpr); - REQUIRE(it); - CHECK_EQ(toString(*it), "(number) -> number"); -} - -TEST_CASE_FIXTURE(Fixture, "return_type_by_overload") -{ - ScopedFastFlag sff{"LuauErrorRecoveryType", true}; - - CheckResult result = check(R"( - type Overload = ((string) -> string) & ((number, number) -> number) - local abc: Overload - local x = abc(true) - local y = abc(true,true) - local z = abc(true,true,true) - )"); - - LUAU_REQUIRE_ERRORS(result); - CHECK_EQ("string", toString(requireType("x"))); - CHECK_EQ("number", toString(requireType("y"))); - // Should this be string|number? - CHECK_EQ("string", toString(requireType("z"))); -} - -TEST_CASE_FIXTURE(Fixture, "infer_anonymous_function_arguments") -{ - // Simple direct arg to arg propagation - CheckResult result = check(R"( -type Table = { x: number, y: number } -local function f(a: (Table) -> number) return a({x = 1, y = 2}) end -f(function(a) return a.x + a.y end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - // An optional function is accepted, but since we already provide a function, nil can be ignored - result = check(R"( -type Table = { x: number, y: number } -local function f(a: ((Table) -> number)?) if a then return a({x = 1, y = 2}) else return 0 end end -f(function(a) return a.x + a.y end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - // Make sure self calls match correct index - result = check(R"( -type Table = { x: number, y: number } -local x = {} -x.b = {x = 1, y = 2} -function x:f(a: (Table) -> number) return a(self.b) end -x:f(function(a) return a.x + a.y end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - // Mix inferred and explicit argument types - result = check(R"( -function f(a: (a: number, b: number, c: boolean) -> number) return a(1, 2, true) end -f(function(a: number, b, c) return c and a + b or b - a end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - // Anonymous function has a variadic pack - result = check(R"( -type Table = { x: number, y: number } -local function f(a: (Table) -> number) return a({x = 1, y = 2}) end -f(function(...) return select(1, ...).z end) - )"); - - LUAU_REQUIRE_ERRORS(result); - CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0])); - - // Can't accept more arguments than provided - result = check(R"( -function f(a: (a: number, b: number) -> number) return a(1, 2) end -f(function(a, b, c, ...) return a + b end) - )"); - - LUAU_REQUIRE_ERRORS(result); - - CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number' -caused by: - Argument count mismatch. Function expects 3 arguments, but only 2 are specified)", - toString(result.errors[0])); - - // Infer from variadic packs into elements - result = check(R"( -function f(a: (...number) -> number) return a(1, 2) end -f(function(a, b) return a + b end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - // Infer from variadic packs into variadic packs - result = check(R"( -type Table = { x: number, y: number } -function f(a: (...Table) -> number) return a({x = 1, y = 2}, {x = 3, y = 4}) end -f(function(a, ...) local b = ... return b.z end) - )"); - - LUAU_REQUIRE_ERRORS(result); - CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0])); - - // Return type inference - result = check(R"( -type Table = { x: number, y: number } -function f(a: (number) -> Table) return a(4) end -f(function(x) return x * 2 end) - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'number' could not be converted into 'Table'", toString(result.errors[0])); - - // Return type doesn't inference 'nil' - result = check(R"( -function f(a: (number) -> nil) return a(4) end -f(function(x) print(x) end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument") -{ - ScopedFastFlag sff{"LuauUnsealedTableLiteral", true}; - - CheckResult result = check(R"( -local function sum(x: a, y: a, f: (a, a) -> a) return f(x, y) end -return sum(2, 3, function(a, b) return a + b end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - result = check(R"( -local function map(arr: {a}, f: (a) -> b) local r = {} for i,v in ipairs(arr) do table.insert(r, f(v)) end return r end -local a = {1, 2, 3} -local r = map(a, function(a) return a + a > 100 end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - REQUIRE_EQ("{boolean}", toString(requireType("r"))); - - check(R"( -local function foldl(arr: {a}, init: b, f: (b, a) -> b) local r = init for i,v in ipairs(arr) do r = f(r, v) end return r end -local a = {1, 2, 3} -local r = foldl(a, {s=0,c=0}, function(a, b) return {s = a.s + b, c = a.c + 1} end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - REQUIRE_EQ("{ c: number, s: number }", toString(requireType("r"))); -} - -TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument_overloaded") -{ - CheckResult result = check(R"( -local function g1(a: T, f: (T) -> T) return f(a) end -local function g2(a: T, b: T, f: (T, T) -> T) return f(a, b) end - -local g12: typeof(g1) & typeof(g2) - -g12(1, function(x) return x + x end) -g12(1, 2, function(x, y) return x + y end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - result = check(R"( -local function g1(a: T, f: (T) -> T) return f(a) end -local function g2(a: T, b: T, f: (T, T) -> T) return f(a, b) end - -local g12: typeof(g1) & typeof(g2) - -g12({x=1}, function(x) return {x=-x.x} end) -g12({x=1}, {x=2}, function(x, y) return {x=x.x + y.x} end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "infer_generic_lib_function_function_argument") -{ - CheckResult result = check(R"( -local a = {{x=4}, {x=7}, {x=1}} -table.sort(a, function(x, y) return x.x < y.x end) - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "infer_anonymous_function_arguments_outside_call") -{ - CheckResult result = check(R"( -type Table = { x: number, y: number } -local f: (Table) -> number = function(t) return t.x + t.y end - -type TableWithFunc = { x: number, y: number, f: (number, number) -> number } -local a: TableWithFunc = { x = 3, y = 4, f = function(a, b) return a + b end } - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "do_not_infer_generic_functions") -{ - CheckResult result = check(R"( -local function sum(x: a, y: a, f: (a, a) -> a) return f(x, y) end - -local function sumrec(f: typeof(sum)) - return sum(2, 3, function(a, b) return a + b end) -end - -local b = sumrec(sum) -- ok -local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not inferred - )"); - - LUAU_REQUIRE_ERRORS(result); - CHECK_EQ("Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '(a, a, (a, a) -> a) -> a'; different number of generic type " - "parameters", - toString(result.errors[0])); -} - -TEST_CASE_FIXTURE(Fixture, "infer_return_value_type") -{ - CheckResult result = check(R"( -local function f(): {string|number} - return {1, "b", 3} -end - -local function g(): (number, {string|number}) - return 4, {1, "b", 3} -end - -local function h(): ...{string|number} - return {4}, {1, "b", 3}, {"s"} -end - -local function i(): ...{string|number} - return {1, "b", 3}, h() -end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - TEST_CASE_FIXTURE(Fixture, "infer_type_assertion_value_type") { CheckResult result = check(R"( @@ -4719,56 +777,6 @@ f(((function(a, b) return a + b end))) LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(Fixture, "refine_and_or") -{ - CheckResult result = check(R"( - local t: {x: number?}? = {x = nil} - local u = t and t.x or 5 - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ("number", toString(requireType("u"))); -} - -TEST_CASE_FIXTURE(Fixture, "checked_prop_too_early") -{ - CheckResult result = check(R"( - local t: {x: number?}? = {x = nil} - local u = t.x and t or 5 - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0])); - CHECK_EQ("number | {| x: number? |}", toString(requireType("u"))); -} - -TEST_CASE_FIXTURE(Fixture, "accidentally_checked_prop_in_opposite_branch") -{ - CheckResult result = check(R"( - local t: {x: number?}? = {x = nil} - local u = t and t.x == 5 or t.x == 31337 - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0])); - CHECK_EQ("boolean", toString(requireType("u"))); -} - -TEST_CASE_FIXTURE(Fixture, "substitution_with_bound_table") -{ - CheckResult result = check(R"( -type A = { x: number } -local a: A = { x = 1 } -local b = a -type B = typeof(b) -type X = T -local c: X - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - TEST_CASE_FIXTURE(Fixture, "tc_if_else_expressions1") { CheckResult result = check(R"(local a = if true then "true" else "false")"); @@ -4830,40 +838,6 @@ end LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(Fixture, "type_error_addition") -{ - CheckResult result = check(R"( ---!strict -local foo = makesandwich() -local bar = foo.nutrition + 100 - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - - // We should definitely get this error - CHECK_EQ("Unknown global 'makesandwich'", toString(result.errors[0])); - // We get this error if makesandwich() returns a free type - // CHECK_EQ("Unknown type used in + operation; consider adding a type annotation to 'foo'", toString(result.errors[1])); -} - -TEST_CASE_FIXTURE(Fixture, "require_failed_module") -{ - fileResolver.source["game/A"] = R"( -return unfortunately() - )"; - - CheckResult aResult = frontend.check("game/A"); - LUAU_REQUIRE_ERRORS(aResult); - - CheckResult result = check(R"( -local ModuleA = require(game.A) - )"); - LUAU_REQUIRE_NO_ERRORS(result); - - std::optional oty = requireType("ModuleA"); - CHECK_EQ("*unknown*", toString(*oty)); -} - /* * If it wasn't instantly obvious, we have the fuzzer to thank for this gem of a test. * @@ -4921,184 +895,6 @@ TEST_CASE_FIXTURE(Fixture, "fuzzer_found_this") )"); } -/* - * We had an issue where part of the type of pairs() was an unsealed table. - * This test depends on FFlagDebugLuauFreezeArena to trigger it. - */ -TEST_CASE_FIXTURE(Fixture, "pairs_parameters_are_not_unsealed_tables") -{ - check(R"( - function _(l0:{n0:any}) - _ = pairs - end - )"); -} - -TEST_CASE_FIXTURE(Fixture, "inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table") -{ - check(R"( - function Base64FileReader(data) - local reader = {} - local index: number - - function reader:PeekByte() - return data:byte(index) - end - - function reader:Byte() - return data:byte(index - 1) - end - - return reader - end - - Base64FileReader() - - function ReadMidiEvents(data) - - local reader = Base64FileReader(data) - - while reader:HasMore() do - (reader:Byte() % 128) - end - end - )"); -} - -TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg_count") -{ - CheckResult result = check(R"( -type A = (number, number) -> string -type B = (number) -> string - -local a: A -local b: B = a - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number) -> string' -caused by: - Argument count mismatch. Function expects 2 arguments, but only 1 is specified)"); -} - -TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg") -{ - CheckResult result = check(R"( -type A = (number, number) -> string -type B = (number, string) -> string - -local a: A -local b: B = a - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number, string) -> string' -caused by: - Argument #2 type is not compatible. Type 'string' could not be converted into 'number')"); -} - -TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_count") -{ - CheckResult result = check(R"( -type A = (number, number) -> (number) -type B = (number, number) -> (number, boolean) - -local a: A -local b: B = a - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> number' could not be converted into '(number, number) -> (number, boolean)' -caused by: - Function only returns 1 value. 2 are required here)"); -} - -TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret") -{ - CheckResult result = check(R"( -type A = (number, number) -> string -type B = (number, number) -> number - -local a: A -local b: B = a - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number, number) -> number' -caused by: - Return type is not compatible. Type 'string' could not be converted into 'number')"); -} - -TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_mult") -{ - CheckResult result = check(R"( -type A = (number, number) -> (number, string) -type B = (number, number) -> (number, boolean) - -local a: A -local b: B = a - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(toString(result.errors[0]), - R"(Type '(number, number) -> (number, string)' could not be converted into '(number, number) -> (number, boolean)' -caused by: - Return #2 type is not compatible. Type 'string' could not be converted into 'boolean')"); -} - -TEST_CASE_FIXTURE(Fixture, "prop_access_on_any_with_other_options") -{ - CheckResult result = check(R"( - local function f(thing: any | string) - local foo = thing.SomeRandomKey - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "table_function_check_use_after_free") -{ - CheckResult result = check(R"( -local t = {} - -function t.x(value) - for k,v in pairs(t) do end -end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "table_oop") -{ - CheckResult result = check(R"( - --!strict -local Class = {} -Class.__index = Class - -type Class = typeof(setmetatable({} :: { x: number }, Class)) - -function Class.new(x: number): Class - return setmetatable({x = x}, Class) -end - -function Class.getx(self: Class) - return self.x -end - -function test() - local c = Class.new(42) - local n = c:getx() - local nn = c.x - - print(string.format("%d %d", n, nn)) -end -)"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - TEST_CASE_FIXTURE(Fixture, "recursive_metatable_crash") { CheckResult result = check(R"( @@ -5182,213 +978,4 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types") -{ - fileResolver.source["game/A"] = R"( -export type Type = { unrelated: boolean } -return {} - )"; - - fileResolver.source["game/B"] = R"( -local types = require(game.A) -type Type = types.Type -local x: Type = {} -function x:Destroy(): () end - )"; - - CheckResult result = frontend.check("game/B"); - LUAU_REQUIRE_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_2") -{ - ScopedFastFlag immutableTypes{"LuauImmutableTypes", true}; - - fileResolver.source["game/A"] = R"( -export type Type = { x: { a: number } } -return {} - )"; - - fileResolver.source["game/B"] = R"( -local types = require(game.A) -type Type = types.Type -local x: Type = { x = { a = 2 } } -type Rename = typeof(x.x) - )"; - - CheckResult result = frontend.check("game/B"); - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_3") -{ - ScopedFastFlag immutableTypes{"LuauImmutableTypes", true}; - - fileResolver.source["game/A"] = R"( -local y = setmetatable({}, {}) -export type Type = { x: typeof(y) } -return { x = y } - )"; - - fileResolver.source["game/B"] = R"( -local types = require(game.A) -type Type = types.Type -local x: Type = types -type Rename = typeof(x.x) - )"; - - CheckResult result = frontend.check("game/B"); - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "indexing_on_string_singletons") -{ - ScopedFastFlag sff[]{ - {"LuauDiscriminableUnions2", true}, - {"LuauSingletonTypes", true}, - }; - - CheckResult result = check(R"( - local a: string = "hi" - if a == "hi" then - local x = a:byte() - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 22}))); -} - -TEST_CASE_FIXTURE(Fixture, "indexing_on_union_of_string_singletons") -{ - ScopedFastFlag sff[]{ - {"LuauDiscriminableUnions2", true}, - {"LuauSingletonTypes", true}, - }; - - CheckResult result = check(R"( - local a: string = "hi" - if a == "hi" or a == "bye" then - local x = a:byte() - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 22}))); -} - -TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_string_singleton") -{ - ScopedFastFlag sff[]{ - {"LuauDiscriminableUnions2", true}, - {"LuauSingletonTypes", true}, - }; - - CheckResult result = check(R"( - local a: string = "hi" - if a == "hi" then - local x = #a - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23}))); -} - -TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_union_of_string_singleton") -{ - ScopedFastFlag sff[]{ - {"LuauDiscriminableUnions2", true}, - {"LuauSingletonTypes", true}, - }; - - CheckResult result = check(R"( - local a: string = "hi" - if a == "hi" or a == "bye" then - local x = #a - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 23}))); -} - -/* - * When we add new properties to an unsealed table, we should do a level check and promote the property type to be at - * the level of the table. - */ -TEST_CASE_FIXTURE(Fixture, "inferred_properties_of_a_table_should_start_with_the_same_TypeLevel_of_that_table") -{ - CheckResult result = check(R"( - --!strict - local T = {} - - local function f(prop) - T[1] = { - prop = prop, - } - end - - local function g() - local l = T[1].prop - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "global_singleton_types_are_sealed") -{ - CheckResult result = check(R"( -local function f(x: string) - local p = x:split('a') - p = table.pack(table.unpack(p, 1, #p - 1)) - return p -end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "function_decl_quantify_right_type") -{ - ScopedFastFlag statFunctionSimplify{"LuauStatFunctionSimplify", true}; - - fileResolver.source["game/isAMagicMock"] = R"( ---!nonstrict -return function(value) - return false -end - )"; - - CheckResult result = check(R"( ---!nonstrict -local MagicMock = {} -MagicMock.is = require(game.isAMagicMock) - -function MagicMock.is(value) - return false -end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(Fixture, "function_decl_non_self_sealed_overwrite") -{ - ScopedFastFlag statFunctionSimplify{"LuauStatFunctionSimplify", true}; - - CheckResult result = check(R"( -function string.len(): number - return 1 -end - )"); - - LUAU_REQUIRE_ERRORS(result); -} - TEST_SUITE_END(); diff --git a/tests/TypeInfer.typePacks.cpp b/tests/TypeInfer.typePacks.cpp index fcc21c1..130f33d 100644 --- a/tests/TypeInfer.typePacks.cpp +++ b/tests/TypeInfer.typePacks.cpp @@ -895,4 +895,87 @@ caused by: Type 'boolean' could not be converted into 'string')"); } +// TODO: File a Jira about this +/* +TEST_CASE_FIXTURE(Fixture, "unifying_vararg_pack_with_fixed_length_pack_produces_fixed_length_pack") +{ + CheckResult result = check(R"( + function a(x) return 1 end + a(...) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + REQUIRE(bool(getMainModule()->getModuleScope()->varargPack)); + + TypePackId varargPack = *getMainModule()->getModuleScope()->varargPack; + + auto iter = begin(varargPack); + auto endIter = end(varargPack); + + CHECK(iter != endIter); + ++iter; + CHECK(iter == endIter); + + CHECK(!iter.tail()); +} +*/ + +TEST_CASE_FIXTURE(Fixture, "dont_ice_if_a_TypePack_is_an_error") +{ + CheckResult result = check(R"( + --!strict + function f(s) + print(s) + return f + end + + f("foo")("bar") + )"); +} + +TEST_CASE_FIXTURE(Fixture, "cyclic_type_packs") +{ + // this has a risk of creating cyclic type packs, causing infinite loops / OOMs + check(R"( +--!nonstrict +_ += _(_,...) +repeat +_ += _(...) +until ... + _ +)"); + + check(R"( +--!nonstrict +_ += _(_(...,...),_(...)) +repeat +until _ +)"); +} + +TEST_CASE_FIXTURE(Fixture, "detect_cyclic_typepacks") +{ + CheckResult result = check(R"( + type ( ... ) ( ) ; + ( ... ) ( - - ... ) ( - ... ) + type = ( ... ) ; + ( ... ) ( ) ( ... ) ; + ( ... ) "" + )"); + + CHECK_LE(0, result.errors.size()); +} + +TEST_CASE_FIXTURE(Fixture, "detect_cyclic_typepacks2") +{ + CheckResult result = check(R"( + function _(l0:((typeof((pcall)))|((((t0)->())|(typeof(-67108864)))|(any)))|(any),...):(((typeof(0))|(any))|(any),typeof(-67108864),any) + xpcall(_,_,_) + _(_,_,_) + end + )"); + + CHECK_LE(0, result.errors.size()); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index ad4cecd..ad1e31e 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -496,4 +496,20 @@ caused by: None of the union options are compatible. For example: Table type 'a' not compatible with type 'X' because the former is missing field 'x')"); } +// We had a bug where a cyclic union caused a stack overflow. +// ex type U = number | U +TEST_CASE_FIXTURE(Fixture, "dont_allow_cyclic_unions_to_be_inferred") +{ + CheckResult result = check(R"( + --!strict + + function f(a, b) + a:g(b or {}) + a:g(b) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/conformance/basic.lua b/tests/conformance/basic.lua index 78d9007..f803c31 100644 --- a/tests/conformance/basic.lua +++ b/tests/conformance/basic.lua @@ -833,6 +833,17 @@ assert((function() return sum end)() == 105) +-- shrinking array part +assert((function() + local t = table.create(100, 42) + for i=1,90 do t[i] = nil end + t[101] = 42 + local sum = 0 + for _,v in ipairs(t) do sum += v end + for _,v in pairs(t) do sum += v end + return sum +end)() == 462) + -- upvalues: recursive capture assert((function() local function fact(n) return n < 1 and 1 or n * fact(n-1) end return fact(5) end)() == 120) @@ -881,6 +892,14 @@ end)() == "6,8,10") -- typeof == type in absence of custom userdata assert(concat(typeof(5), typeof(nil), typeof({}), typeof(newproxy())) == "number,nil,table,userdata") +-- type/typeof/newproxy interaction with metatables: __type doesn't work intentionally to avoid spoofing +assert((function() + local ud = newproxy(true) + getmetatable(ud).__type = "number" + + return concat(type(ud),typeof(ud)) +end)() == "userdata,userdata") + testgetfenv() -- DONT MOVE THIS LINE return 'OK' diff --git a/tests/conformance/debugger.lua b/tests/conformance/debugger.lua index 6ba99fb..ec0b412 100644 --- a/tests/conformance/debugger.lua +++ b/tests/conformance/debugger.lua @@ -3,14 +3,14 @@ print "testing debugger" -- note, this file can't run in isolation from C tests local a = 5 -function foo(b) +function foo(b, ...) print("in foo", b) a = 6 end breakpoint(8) -foo(50) +foo(50, 42) breakpoint(16) -- next line print("here") diff --git a/tests/conformance/errors.lua b/tests/conformance/errors.lua index 751188b..297cf01 100644 --- a/tests/conformance/errors.lua +++ b/tests/conformance/errors.lua @@ -305,4 +305,6 @@ assert(ecall(function() return "a" + "b" end) == "attempt to perform arithmetic assert(ecall(function() return 1 > nil end) == "attempt to compare nil < number") -- note reversed order (by design) assert(ecall(function() return "a" <= 5 end) == "attempt to compare string <= number") +assert(ecall(function() local t = {} setmetatable(t, { __newindex = function(t,i,v) end }) t[nil] = 2 end) == "table index is nil") + return('OK') diff --git a/tests/conformance/interrupt.lua b/tests/conformance/interrupt.lua new file mode 100644 index 0000000..2b12709 --- /dev/null +++ b/tests/conformance/interrupt.lua @@ -0,0 +1,11 @@ +-- This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +print("testing interrupts") + +function foo() + for i=1,10 do end + return +end + +foo() + +return "OK" diff --git a/tools/gdb-printers.py b/tools/gdb_printers.py similarity index 100% rename from tools/gdb-printers.py rename to tools/gdb_printers.py diff --git a/tools/lldb-formatters.lldb b/tools/lldb-formatters.lldb deleted file mode 100644 index 3868ac2..0000000 --- a/tools/lldb-formatters.lldb +++ /dev/null @@ -1,2 +0,0 @@ -type synthetic add -x "^Luau::Variant<.+>$" -l LuauVisualize.LuauVariantSyntheticChildrenProvider -type summary add -x "^Luau::Variant<.+>$" -l LuauVisualize.luau_variant_summary diff --git a/tools/lldb_formatters.lldb b/tools/lldb_formatters.lldb new file mode 100644 index 0000000..f6fa6cf --- /dev/null +++ b/tools/lldb_formatters.lldb @@ -0,0 +1,2 @@ +type synthetic add -x "^Luau::Variant<.+>$" -l lldb_formatters.LuauVariantSyntheticChildrenProvider +type summary add -x "^Luau::Variant<.+>$" -l lldb_formatters.luau_variant_summary diff --git a/tools/LuauVisualize.py b/tools/lldb_formatters.py similarity index 100% rename from tools/LuauVisualize.py rename to tools/lldb_formatters.py From 7721955ba54295696064a05a969b2a4fb29f43af Mon Sep 17 00:00:00 2001 From: Alan Jeffrey <403333+asajeffrey@users.noreply.github.com> Date: Wed, 23 Mar 2022 15:02:57 -0500 Subject: [PATCH 2/9] Prototyping: add semantic subtyping (#424) Adds subtyping to strict mode. --- prototyping/FFI/Data/Aeson.agda | 4 +- prototyping/FFI/Data/Either.agda | 24 +- prototyping/Luau/RuntimeError.agda | 2 +- prototyping/Luau/StrictMode.agda | 34 +- prototyping/Luau/StrictMode/ToString.agda | 39 +- prototyping/Luau/Subtyping.agda | 62 ++ prototyping/Luau/Type.agda | 7 + prototyping/Luau/TypeCheck.agda | 23 +- prototyping/Properties.agda | 2 + prototyping/Properties/Functions.agda | 6 + prototyping/Properties/StrictMode.agda | 594 +++++++----------- prototyping/Properties/Subtyping.agda | 221 +++++++ prototyping/Properties/TypeCheck.agda | 18 +- .../concat_number_and_string/out.txt | 1 + 14 files changed, 623 insertions(+), 414 deletions(-) create mode 100644 prototyping/Luau/Subtyping.agda create mode 100644 prototyping/Properties/Functions.agda create mode 100644 prototyping/Properties/Subtyping.agda diff --git a/prototyping/FFI/Data/Aeson.agda b/prototyping/FFI/Data/Aeson.agda index 0cd2723..4301471 100644 --- a/prototyping/FFI/Data/Aeson.agda +++ b/prototyping/FFI/Data/Aeson.agda @@ -10,7 +10,7 @@ open import Agda.Builtin.String using (String) open import FFI.Data.ByteString using (ByteString) open import FFI.Data.HaskellString using (HaskellString; pack) open import FFI.Data.Maybe using (Maybe; just; nothing) -open import FFI.Data.Either using (Either; mapLeft) +open import FFI.Data.Either using (Either; mapL) open import FFI.Data.Scientific using (Scientific) open import FFI.Data.Vector using (Vector) @@ -73,5 +73,5 @@ postulate {-# COMPILE GHC eitherHDecode = Data.Aeson.eitherDecodeStrict #-} eitherDecode : ByteString → Either String Value -eitherDecode bytes = mapLeft pack (eitherHDecode bytes) +eitherDecode bytes = mapL pack (eitherHDecode bytes) diff --git a/prototyping/FFI/Data/Either.agda b/prototyping/FFI/Data/Either.agda index d024c69..8a5d3e6 100644 --- a/prototyping/FFI/Data/Either.agda +++ b/prototyping/FFI/Data/Either.agda @@ -7,10 +7,22 @@ data Either (A B : Set) : Set where Right : B → Either A B {-# COMPILE GHC Either = data Data.Either.Either (Data.Either.Left|Data.Either.Right) #-} -mapLeft : ∀ {A B C} → (A → B) → (Either A C) → (Either B C) -mapLeft f (Left x) = Left (f x) -mapLeft f (Right x) = Right x +swapLR : ∀ {A B} → Either A B → Either B A +swapLR (Left x) = Right x +swapLR (Right x) = Left x -mapRight : ∀ {A B C} → (B → C) → (Either A B) → (Either A C) -mapRight f (Left x) = Left x -mapRight f (Right x) = Right (f x) +mapL : ∀ {A B C} → (A → B) → Either A C → Either B C +mapL f (Left x) = Left (f x) +mapL f (Right x) = Right x + +mapR : ∀ {A B C} → (B → C) → Either A B → Either A C +mapR f (Left x) = Left x +mapR f (Right x) = Right (f x) + +mapLR : ∀ {A B C D} → (A → B) → (C → D) → Either A C → Either B D +mapLR f g (Left x) = Left (f x) +mapLR f g (Right x) = Right (g x) + +cond : ∀ {A B C : Set} → (A → C) → (B → C) → (Either A B) → C +cond f g (Left x) = f x +cond f g (Right x) = g x diff --git a/prototyping/Luau/RuntimeError.agda b/prototyping/Luau/RuntimeError.agda index 0c8a594..b9b305c 100644 --- a/prototyping/Luau/RuntimeError.agda +++ b/prototyping/Luau/RuntimeError.agda @@ -25,7 +25,7 @@ data RuntimeErrorᴮ {a} (H : Heap a) : Block a → Set data RuntimeErrorᴱ {a} (H : Heap a) : Expr a → Set data RuntimeErrorᴱ H where - FunctionMismatch : ∀ v w → (function ≢ valueType v) → RuntimeErrorᴱ H (val v $ val w) + FunctionMismatch : ∀ v w → (valueType v ≢ function) → RuntimeErrorᴱ H (val v $ val w) BinOpMismatch₁ : ∀ v w {op} → (BinOpError op (valueType v)) → RuntimeErrorᴱ H (binexp (val v) op (val w)) BinOpMismatch₂ : ∀ v w {op} → (BinOpError op (valueType w)) → RuntimeErrorᴱ H (binexp (val v) op (val w)) UnboundVariable : ∀ {x} → RuntimeErrorᴱ H (var x) diff --git a/prototyping/Luau/StrictMode.agda b/prototyping/Luau/StrictMode.agda index 08e0f50..0b5fe0d 100644 --- a/prototyping/Luau/StrictMode.agda +++ b/prototyping/Luau/StrictMode.agda @@ -5,28 +5,18 @@ module Luau.StrictMode where open import Agda.Builtin.Equality using (_≡_) open import FFI.Data.Maybe using (just; nothing) open import Luau.Syntax using (Expr; Stat; Block; BinaryOperator; yes; nil; addr; var; binexp; var_∈_; _⟨_⟩∈_; function_is_end; _$_; block_is_end; local_←_; _∙_; done; return; name; +; -; *; /; <; >; <=; >=; ··) -open import Luau.Type using (Type; strict; nil; number; string; _⇒_; tgt) +open import Luau.Type using (Type; strict; nil; number; string; boolean; none; any; _⇒_; _∪_; _∩_; tgt) +open import Luau.Subtyping using (_≮:_) open import Luau.Heap using (Heap; function_is_end) renaming (_[_] to _[_]ᴴ) open import Luau.VarCtxt using (VarCtxt; ∅; _⋒_; _↦_; _⊕_↦_; _⊝_) renaming (_[_] to _[_]ⱽ) -open import Luau.TypeCheck(strict) using (_⊢ᴮ_∈_; _⊢ᴱ_∈_; ⊢ᴴ_; ⊢ᴼ_; _⊢ᴴᴱ_▷_∈_; _⊢ᴴᴮ_▷_∈_; var; addr; app; binexp; block; return; local; function) -open import Properties.Equality using (_≢_) +open import Luau.TypeCheck(strict) using (_⊢ᴮ_∈_; _⊢ᴱ_∈_; ⊢ᴴ_; ⊢ᴼ_; _⊢ᴴᴱ_▷_∈_; _⊢ᴴᴮ_▷_∈_; var; addr; app; binexp; block; return; local; function; srcBinOp) +open import Properties.Contradiction using (¬) open import Properties.TypeCheck(strict) using (typeCheckᴮ) open import Properties.Product using (_,_) src : Type → Type src = Luau.Type.src strict -data BinOpWarning : BinaryOperator → Type → Set where - + : ∀ {T} → (T ≢ number) → BinOpWarning + T - - : ∀ {T} → (T ≢ number) → BinOpWarning - T - * : ∀ {T} → (T ≢ number) → BinOpWarning * T - / : ∀ {T} → (T ≢ number) → BinOpWarning / T - < : ∀ {T} → (T ≢ number) → BinOpWarning < T - > : ∀ {T} → (T ≢ number) → BinOpWarning > T - <= : ∀ {T} → (T ≢ number) → BinOpWarning <= T - >= : ∀ {T} → (T ≢ number) → BinOpWarning >= T - ·· : ∀ {T} → (T ≢ string) → BinOpWarning ·· T - data Warningᴱ (H : Heap yes) {Γ} : ∀ {M T} → (Γ ⊢ᴱ M ∈ T) → Set data Warningᴮ (H : Heap yes) {Γ} : ∀ {B T} → (Γ ⊢ᴮ B ∈ T) → Set @@ -46,7 +36,7 @@ data Warningᴱ H {Γ} where FunctionCallMismatch : ∀ {M N T U} {D₁ : Γ ⊢ᴱ M ∈ T} {D₂ : Γ ⊢ᴱ N ∈ U} → - (src T ≢ U) → + (U ≮: src T) → ----------------- Warningᴱ H (app D₁ D₂) @@ -64,13 +54,13 @@ data Warningᴱ H {Γ} where BinOpMismatch₁ : ∀ {op M N T U} {D₁ : Γ ⊢ᴱ M ∈ T} {D₂ : Γ ⊢ᴱ N ∈ U} → - BinOpWarning op T → + (T ≮: srcBinOp op) → ------------------------------ Warningᴱ H (binexp {op} D₁ D₂) BinOpMismatch₂ : ∀ {op M N T U} {D₁ : Γ ⊢ᴱ M ∈ T} {D₂ : Γ ⊢ᴱ N ∈ U} → - BinOpWarning op U → + (U ≮: srcBinOp op) → ------------------------------ Warningᴱ H (binexp {op} D₁ D₂) @@ -88,7 +78,7 @@ data Warningᴱ H {Γ} where FunctionDefnMismatch : ∀ {f x B T U V} {D : (Γ ⊕ x ↦ T) ⊢ᴮ B ∈ V} → - (U ≢ V) → + (V ≮: U) → ------------------------- Warningᴱ H (function {f} {U = U} D) @@ -100,7 +90,7 @@ data Warningᴱ H {Γ} where BlockMismatch : ∀ {b B T U} {D : Γ ⊢ᴮ B ∈ U} → - (T ≢ U) → + (U ≮: T) → ------------------------------ Warningᴱ H (block {b} {T = T} D) @@ -120,7 +110,7 @@ data Warningᴮ H {Γ} where LocalVarMismatch : ∀ {x M B T U V} {D₁ : Γ ⊢ᴱ M ∈ U} {D₂ : (Γ ⊕ x ↦ T) ⊢ᴮ B ∈ V} → - (T ≢ U) → + (U ≮: T) → -------------------- Warningᴮ H (local D₁ D₂) @@ -138,7 +128,7 @@ data Warningᴮ H {Γ} where FunctionDefnMismatch : ∀ {f x B C T U V W} {D₁ : (Γ ⊕ x ↦ T) ⊢ᴮ C ∈ V} {D₂ : (Γ ⊕ f ↦ (T ⇒ U)) ⊢ᴮ B ∈ W} → - (U ≢ V) → + (V ≮: U) → ------------------------------------- Warningᴮ H (function D₁ D₂) @@ -158,7 +148,7 @@ data Warningᴼ (H : Heap yes) : ∀ {V} → (⊢ᴼ V) → Set where FunctionDefnMismatch : ∀ {f x B T U V} {D : (x ↦ T) ⊢ᴮ B ∈ V} → - (U ≢ V) → + (V ≮: U) → --------------------------------- Warningᴼ H (function {f} {U = U} D) diff --git a/prototyping/Luau/StrictMode/ToString.agda b/prototyping/Luau/StrictMode/ToString.agda index ca55888..08ee13b 100644 --- a/prototyping/Luau/StrictMode/ToString.agda +++ b/prototyping/Luau/StrictMode/ToString.agda @@ -2,37 +2,58 @@ module Luau.StrictMode.ToString where +open import Agda.Builtin.Nat using (Nat; suc) open import FFI.Data.String using (String; _++_) +open import Luau.Subtyping using (_≮:_; Tree; witness; scalar; function; function-ok; function-err) open import Luau.StrictMode using (Warningᴱ; Warningᴮ; UnallocatedAddress; UnboundVariable; FunctionCallMismatch; FunctionDefnMismatch; BlockMismatch; app₁; app₂; BinOpMismatch₁; BinOpMismatch₂; bin₁; bin₂; block₁; return; LocalVarMismatch; local₁; local₂; function₁; function₂; heap; expr; block; addr) open import Luau.Syntax using (Expr; val; yes; var; var_∈_; _⟨_⟩∈_; _$_; addr; number; binexp; nil; function_is_end; block_is_end; done; return; local_←_; _∙_; fun; arg; name) -open import Luau.Type using (strict) +open import Luau.Type using (strict; number; boolean; string; nil) open import Luau.TypeCheck(strict) using (_⊢ᴮ_∈_; _⊢ᴱ_∈_) open import Luau.Addr.ToString using (addrToString) open import Luau.Var.ToString using (varToString) open import Luau.Type.ToString using (typeToString) open import Luau.Syntax.ToString using (binOpToString) +tmp : Nat → String +tmp 0 = "w" +tmp 1 = "x" +tmp 2 = "y" +tmp 3 = "z" +tmp (suc (suc (suc n))) = tmp n ++ "'" + +treeToString : Tree → Nat → String → String +treeToString (scalar number) n v = v ++ " is a number" +treeToString (scalar boolean) n v = v ++ " is a boolean" +treeToString (scalar string) n v = v ++ " is a string" +treeToString (scalar nil) n v = v ++ " is nil" +treeToString function n v = v ++ " is a function" +treeToString (function-ok t) n v = treeToString t n (v ++ "()") +treeToString (function-err t) n v = v ++ "(" ++ w ++ ") can error when\n " ++ treeToString t (suc n) w where w = tmp n + +subtypeWarningToString : ∀ {T U} → (T ≮: U) → String +subtypeWarningToString (witness t p q) = "\n because provided type contains v, where " ++ treeToString t 0 "v" + warningToStringᴱ : ∀ {H Γ T} M → {D : Γ ⊢ᴱ M ∈ T} → Warningᴱ H D → String warningToStringᴮ : ∀ {H Γ T} B → {D : Γ ⊢ᴮ B ∈ T} → Warningᴮ H D → String warningToStringᴱ (var x) (UnboundVariable p) = "Unbound variable " ++ varToString x -warningToStringᴱ (val (addr a)) (UnallocatedAddress p) = "Unallocated adress " ++ addrToString a -warningToStringᴱ (M $ N) (FunctionCallMismatch {T = T} {U = U} p) = "Function has type " ++ typeToString T ++ " but argument has type " ++ typeToString U +warningToStringᴱ (val (addr a)) (UnallocatedAddress p) = "Unallocated address " ++ addrToString a +warningToStringᴱ (M $ N) (FunctionCallMismatch {T = T} {U = U} p) = "Function has type " ++ typeToString T ++ " but argument has type " ++ typeToString U ++ subtypeWarningToString p warningToStringᴱ (M $ N) (app₁ W) = warningToStringᴱ M W warningToStringᴱ (M $ N) (app₂ W) = warningToStringᴱ N W -warningToStringᴱ (function f ⟨ var x ∈ T ⟩∈ U is B end) (FunctionDefnMismatch {V = V} p) = "Function expresion " ++ varToString f ++ " has return type " ++ typeToString U ++ " but body returns " ++ typeToString V +warningToStringᴱ (function f ⟨ var x ∈ T ⟩∈ U is B end) (FunctionDefnMismatch {V = V} p) = "Function expresion " ++ varToString f ++ " has return type " ++ typeToString U ++ " but body returns " ++ typeToString V ++ subtypeWarningToString p warningToStringᴱ (function f ⟨ var x ∈ T ⟩∈ U is B end) (function₁ W) = warningToStringᴮ B W ++ "\n in function expression " ++ varToString f -warningToStringᴱ block var b ∈ T is B end (BlockMismatch {U = U} p) = "Block " ++ varToString b ++ " has type " ++ typeToString T ++ " but body returns " ++ typeToString U +warningToStringᴱ block var b ∈ T is B end (BlockMismatch {U = U} p) = "Block " ++ varToString b ++ " has type " ++ typeToString T ++ " but body returns " ++ typeToString U ++ subtypeWarningToString p warningToStringᴱ block var b ∈ T is B end (block₁ W) = warningToStringᴮ B W ++ "\n in block " ++ varToString b -warningToStringᴱ (binexp M op N) (BinOpMismatch₁ {T = T} p) = "Binary operator " ++ binOpToString op ++ " lhs has type " ++ typeToString T -warningToStringᴱ (binexp M op N) (BinOpMismatch₂ {U = U} p) = "Binary operator " ++ binOpToString op ++ " rhs has type " ++ typeToString U +warningToStringᴱ (binexp M op N) (BinOpMismatch₁ {T = T} p) = "Binary operator " ++ binOpToString op ++ " lhs has type " ++ typeToString T ++ subtypeWarningToString p +warningToStringᴱ (binexp M op N) (BinOpMismatch₂ {U = U} p) = "Binary operator " ++ binOpToString op ++ " rhs has type " ++ typeToString U ++ subtypeWarningToString p warningToStringᴱ (binexp M op N) (bin₁ W) = warningToStringᴱ M W warningToStringᴱ (binexp M op N) (bin₂ W) = warningToStringᴱ N W -warningToStringᴮ (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (FunctionDefnMismatch {V = V} p) = "Function declaration " ++ varToString f ++ " has return type " ++ typeToString U ++ " but body returns " ++ typeToString V +warningToStringᴮ (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (FunctionDefnMismatch {V = V} p) = "Function declaration " ++ varToString f ++ " has return type " ++ typeToString U ++ " but body returns " ++ typeToString V ++ subtypeWarningToString p warningToStringᴮ (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function₁ W) = warningToStringᴮ C W ++ "\n in function declaration " ++ varToString f warningToStringᴮ (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function₂ W) = warningToStringᴮ B W -warningToStringᴮ (local var x ∈ T ← M ∙ B) (LocalVarMismatch {U = U} p) = "Local variable " ++ varToString x ++ " has type " ++ typeToString T ++ " but expression has type " ++ typeToString U +warningToStringᴮ (local var x ∈ T ← M ∙ B) (LocalVarMismatch {U = U} p) = "Local variable " ++ varToString x ++ " has type " ++ typeToString T ++ " but expression has type " ++ typeToString U ++ subtypeWarningToString p warningToStringᴮ (local var x ∈ T ← M ∙ B) (local₁ W) = warningToStringᴱ M W ++ "\n in local variable declaration " ++ varToString x warningToStringᴮ (local var x ∈ T ← M ∙ B) (local₂ W) = warningToStringᴮ B W warningToStringᴮ (return M ∙ B) (return W) = warningToStringᴱ M W ++ "\n in return statement" diff --git a/prototyping/Luau/Subtyping.agda b/prototyping/Luau/Subtyping.agda new file mode 100644 index 0000000..7d67eb4 --- /dev/null +++ b/prototyping/Luau/Subtyping.agda @@ -0,0 +1,62 @@ +{-# OPTIONS --rewriting #-} + +open import Luau.Type using (Type; Scalar; nil; number; string; boolean; none; any; _⇒_; _∪_; _∩_) +open import Properties.Equality using (_≢_) + +module Luau.Subtyping where + +-- An implementation of semantic subtyping + +-- We think of types as languages of trees + +data Tree : Set where + + scalar : ∀ {T} → Scalar T → Tree + function : Tree + function-ok : Tree → Tree + function-err : Tree → Tree + +data Language : Type → Tree → Set +data ¬Language : Type → Tree → Set + +data Language where + + scalar : ∀ {T} → (s : Scalar T) → Language T (scalar s) + function : ∀ {T U} → Language (T ⇒ U) function + function-ok : ∀ {T U u} → (Language U u) → Language (T ⇒ U) (function-ok u) + function-err : ∀ {T U t} → (¬Language T t) → Language (T ⇒ U) (function-err t) + scalar-function-err : ∀ {S t} → (Scalar S) → Language S (function-err t) + left : ∀ {T U t} → Language T t → Language (T ∪ U) t + right : ∀ {T U u} → Language U u → Language (T ∪ U) u + _,_ : ∀ {T U t} → Language T t → Language U t → Language (T ∩ U) t + any : ∀ {t} → Language any t + +data ¬Language where + + scalar-scalar : ∀ {S T} → (s : Scalar S) → (Scalar T) → (S ≢ T) → ¬Language T (scalar s) + scalar-function : ∀ {S} → (Scalar S) → ¬Language S function + scalar-function-ok : ∀ {S u} → (Scalar S) → ¬Language S (function-ok u) + function-scalar : ∀ {S T U} (s : Scalar S) → ¬Language (T ⇒ U) (scalar s) + function-ok : ∀ {T U u} → (¬Language U u) → ¬Language (T ⇒ U) (function-ok u) + function-err : ∀ {T U t} → (Language T t) → ¬Language (T ⇒ U) (function-err t) + _,_ : ∀ {T U t} → ¬Language T t → ¬Language U t → ¬Language (T ∪ U) t + left : ∀ {T U t} → ¬Language T t → ¬Language (T ∩ U) t + right : ∀ {T U u} → ¬Language U u → ¬Language (T ∩ U) u + none : ∀ {t} → ¬Language none t + +-- Subtyping as language inclusion + +_<:_ : Type → Type → Set +(T <: U) = ∀ t → (Language T t) → (Language U t) + +-- For warnings, we are interested in failures of subtyping, +-- which is whrn there is a tree in T's language that isn't in U's. + +data _≮:_ (T U : Type) : Set where + + witness : ∀ t → + + Language T t → + ¬Language U t → + ----------------- + T ≮: U diff --git a/prototyping/Luau/Type.agda b/prototyping/Luau/Type.agda index c5b7614..87815dd 100644 --- a/prototyping/Luau/Type.agda +++ b/prototyping/Luau/Type.agda @@ -17,6 +17,13 @@ data Type : Set where _∪_ : Type → Type → Type _∩_ : Type → Type → Type +data Scalar : Type → Set where + + number : Scalar number + boolean : Scalar boolean + string : Scalar string + nil : Scalar nil + lhs : Type → Type lhs (T ⇒ _) = T lhs (T ∪ _) = T diff --git a/prototyping/Luau/TypeCheck.agda b/prototyping/Luau/TypeCheck.agda index 65ed183..c22618b 100644 --- a/prototyping/Luau/TypeCheck.agda +++ b/prototyping/Luau/TypeCheck.agda @@ -10,7 +10,7 @@ open import Luau.Syntax using (Expr; Stat; Block; BinaryOperator; yes; nil; addr open import Luau.Var using (Var) open import Luau.Addr using (Addr) open import Luau.Heap using (Heap; Object; function_is_end) renaming (_[_] to _[_]ᴴ) -open import Luau.Type using (Type; Mode; nil; none; number; boolean; string; _⇒_; tgt) +open import Luau.Type using (Type; Mode; nil; any; number; boolean; string; _⇒_; tgt) open import Luau.VarCtxt using (VarCtxt; ∅; _⋒_; _↦_; _⊕_↦_; _⊝_) renaming (_[_] to _[_]ⱽ) open import FFI.Data.Vector using (Vector) open import FFI.Data.Maybe using (Maybe; just; nothing) @@ -19,9 +19,22 @@ open import Properties.Product using (_×_; _,_) src : Type → Type src = Luau.Type.src m -orNone : Maybe Type → Type -orNone nothing = none -orNone (just T) = T +orAny : Maybe Type → Type +orAny nothing = any +orAny (just T) = T + +srcBinOp : BinaryOperator → Type +srcBinOp + = number +srcBinOp - = number +srcBinOp * = number +srcBinOp / = number +srcBinOp < = number +srcBinOp > = number +srcBinOp == = any +srcBinOp ~= = any +srcBinOp <= = number +srcBinOp >= = number +srcBinOp ·· = string tgtBinOp : BinaryOperator → Type tgtBinOp + = number @@ -76,7 +89,7 @@ data _⊢ᴱ_∈_ where var : ∀ {x T Γ} → - T ≡ orNone(Γ [ x ]ⱽ) → + T ≡ orAny(Γ [ x ]ⱽ) → ---------------- Γ ⊢ᴱ (var x) ∈ T diff --git a/prototyping/Properties.agda b/prototyping/Properties.agda index 8b30ce1..5594812 100644 --- a/prototyping/Properties.agda +++ b/prototyping/Properties.agda @@ -5,7 +5,9 @@ module Properties where import Properties.Contradiction import Properties.Dec import Properties.Equality +import Properties.Functions import Properties.Remember import Properties.Step import Properties.StrictMode +import Properties.Subtyping import Properties.TypeCheck diff --git a/prototyping/Properties/Functions.agda b/prototyping/Properties/Functions.agda new file mode 100644 index 0000000..313b0ff --- /dev/null +++ b/prototyping/Properties/Functions.agda @@ -0,0 +1,6 @@ +module Properties.Functions where + +infixr 5 _∘_ + +_∘_ : ∀ {A B C : Set} → (B → C) → (A → B) → (A → C) +(f ∘ g) x = f (g x) diff --git a/prototyping/Properties/StrictMode.agda b/prototyping/Properties/StrictMode.agda index 77852f1..2ff2b15 100644 --- a/prototyping/Properties/StrictMode.agda +++ b/prototyping/Properties/StrictMode.agda @@ -4,13 +4,15 @@ module Properties.StrictMode where import Agda.Builtin.Equality.Rewrite open import Agda.Builtin.Equality using (_≡_; refl) +open import FFI.Data.Either using (Either; Left; Right; mapL; mapR; mapLR; swapLR; cond) open import FFI.Data.Maybe using (Maybe; just; nothing) open import Luau.Heap using (Heap; Object; function_is_end; defn; alloc; ok; next; lookup-not-allocated) renaming (_≡_⊕_↦_ to _≡ᴴ_⊕_↦_; _[_] to _[_]ᴴ; ∅ to ∅ᴴ) -open import Luau.StrictMode using (Warningᴱ; Warningᴮ; Warningᴼ; Warningᴴᴱ; Warningᴴᴮ; UnallocatedAddress; UnboundVariable; FunctionCallMismatch; app₁; app₂; BinOpWarning; BinOpMismatch₁; BinOpMismatch₂; bin₁; bin₂; BlockMismatch; block₁; return; LocalVarMismatch; local₁; local₂; FunctionDefnMismatch; function₁; function₂; heap; expr; block; addr; +; -; *; /; <; >; <=; >=; ··) +open import Luau.StrictMode using (Warningᴱ; Warningᴮ; Warningᴼ; Warningᴴ; UnallocatedAddress; UnboundVariable; FunctionCallMismatch; app₁; app₂; BinOpMismatch₁; BinOpMismatch₂; bin₁; bin₂; BlockMismatch; block₁; return; LocalVarMismatch; local₁; local₂; FunctionDefnMismatch; function₁; function₂; heap; expr; block; addr) open import Luau.Substitution using (_[_/_]ᴮ; _[_/_]ᴱ; _[_/_]ᴮunless_; var_[_/_]ᴱwhenever_) +open import Luau.Subtyping using (_≮:_; witness; any; none; scalar; function; scalar-function; scalar-function-ok; scalar-function-err; scalar-scalar; function-scalar; function-ok; function-err; left; right; _,_; Tree; Language; ¬Language) open import Luau.Syntax using (Expr; yes; var; val; var_∈_; _⟨_⟩∈_; _$_; addr; number; bool; string; binexp; nil; function_is_end; block_is_end; done; return; local_←_; _∙_; fun; arg; name; ==; ~=) -open import Luau.Type using (Type; strict; nil; _⇒_; none; tgt; _≡ᵀ_; _≡ᴹᵀ_) -open import Luau.TypeCheck(strict) using (_⊢ᴮ_∈_; _⊢ᴱ_∈_; _⊢ᴴᴮ_▷_∈_; _⊢ᴴᴱ_▷_∈_; nil; var; addr; app; function; block; done; return; local; orNone; tgtBinOp) +open import Luau.Type using (Type; strict; nil; number; boolean; string; _⇒_; none; any; _∩_; _∪_; tgt; _≡ᵀ_; _≡ᴹᵀ_) +open import Luau.TypeCheck(strict) using (_⊢ᴮ_∈_; _⊢ᴱ_∈_; _⊢ᴴᴮ_▷_∈_; _⊢ᴴᴱ_▷_∈_; nil; var; addr; app; function; block; done; return; local; orAny; srcBinOp; tgtBinOp) open import Luau.Var using (_≡ⱽ_) open import Luau.Addr using (_≡ᴬ_) open import Luau.VarCtxt using (VarCtxt; ∅; _⋒_; _↦_; _⊕_↦_; _⊝_; ⊕-lookup-miss; ⊕-swap; ⊕-over) renaming (_[_] to _[_]ⱽ) @@ -18,17 +20,19 @@ open import Luau.VarCtxt using (VarCtxt; ∅) open import Properties.Remember using (remember; _,_) open import Properties.Equality using (_≢_; sym; cong; trans; subst₁) open import Properties.Dec using (Dec; yes; no) -open import Properties.Contradiction using (CONTRADICTION) -open import Properties.TypeCheck(strict) using (typeOfᴼ; typeOfᴹᴼ; typeOfⱽ; typeOfᴱ; typeOfᴮ; typeCheckᴱ; typeCheckᴮ; typeCheckᴼ; typeCheckᴴᴱ; typeCheckᴴᴮ; mustBeFunction; mustBeNumber; mustBeString) +open import Properties.Contradiction using (CONTRADICTION; ¬) +open import Properties.Functions using (_∘_) +open import Properties.Subtyping using (any-≮:; ≡-trans-≮:; ≮:-trans-≡; none-tgt-≮:; tgt-none-≮:; src-any-≮:; any-src-≮:; ≮:-trans; ≮:-refl; scalar-≢-impl-≮:; function-≮:-scalar; scalar-≮:-function; function-≮:-none; any-≮:-scalar; scalar-≮:-none; any-≮:-none) +open import Properties.TypeCheck(strict) using (typeOfᴼ; typeOfᴹᴼ; typeOfⱽ; typeOfᴱ; typeOfᴮ; typeCheckᴱ; typeCheckᴮ; typeCheckᴼ; typeCheckᴴ) open import Luau.OpSem using (_⟦_⟧_⟶_; _⊢_⟶*_⊣_; _⊢_⟶ᴮ_⊣_; _⊢_⟶ᴱ_⊣_; app₁; app₂; function; beta; return; block; done; local; subst; binOp₀; binOp₁; binOp₂; refl; step; +; -; *; /; <; >; ==; ~=; <=; >=; ··) open import Luau.RuntimeError using (BinOpError; RuntimeErrorᴱ; RuntimeErrorᴮ; FunctionMismatch; BinOpMismatch₁; BinOpMismatch₂; UnboundVariable; SEGV; app₁; app₂; bin₁; bin₂; block; local; return; +; -; *; /; <; >; <=; >=; ··) -open import Luau.RuntimeType using (valueType) +open import Luau.RuntimeType using (RuntimeType; valueType; number; string; boolean; nil; function) src = Luau.Type.src strict data _⊑_ (H : Heap yes) : Heap yes → Set where refl : (H ⊑ H) - snoc : ∀ {H′ a V} → (H′ ≡ᴴ H ⊕ a ↦ V) → (H ⊑ H′) + snoc : ∀ {H′ a O} → (H′ ≡ᴴ H ⊕ a ↦ O) → (H ⊑ H′) rednᴱ⊑ : ∀ {H H′ M M′} → (H ⊢ M ⟶ᴱ M′ ⊣ H′) → (H ⊑ H′) rednᴮ⊑ : ∀ {H H′ B B′} → (H ⊢ B ⟶ᴮ B′ ⊣ H′) → (H ⊑ H′) @@ -59,90 +63,51 @@ lookup-⊑-nothing {H} a (snoc defn) p with a ≡ᴬ next H lookup-⊑-nothing {H} a (snoc defn) p | yes refl = refl lookup-⊑-nothing {H} a (snoc o) p | no q = trans (lookup-not-allocated o q) p -data OrWarningᴱ {Γ M T} (H : Heap yes) (D : Γ ⊢ᴱ M ∈ T) A : Set where - ok : A → OrWarningᴱ H D A - warning : Warningᴱ H D → OrWarningᴱ H D A +heap-weakeningᴱ : ∀ Γ H M {H′ U} → (H ⊑ H′) → (typeOfᴱ H′ Γ M ≮: U) → (typeOfᴱ H Γ M ≮: U) +heap-weakeningᴱ Γ H (var x) h p = p +heap-weakeningᴱ Γ H (val nil) h p = p +heap-weakeningᴱ Γ H (val (addr a)) refl p = p +heap-weakeningᴱ Γ H (val (addr a)) (snoc {a = b} q) p with a ≡ᴬ b +heap-weakeningᴱ Γ H (val (addr a)) (snoc {a = a} defn) p | yes refl = any-≮: p +heap-weakeningᴱ Γ H (val (addr a)) (snoc {a = b} q) p | no r = ≡-trans-≮: (cong orAny (cong typeOfᴹᴼ (lookup-not-allocated q r))) p +heap-weakeningᴱ Γ H (val (number x)) h p = p +heap-weakeningᴱ Γ H (val (bool x)) h p = p +heap-weakeningᴱ Γ H (val (string x)) h p = p +heap-weakeningᴱ Γ H (M $ N) h p = none-tgt-≮: (heap-weakeningᴱ Γ H M h (tgt-none-≮: p)) +heap-weakeningᴱ Γ H (function f ⟨ var x ∈ T ⟩∈ U is B end) h p = p +heap-weakeningᴱ Γ H (block var b ∈ T is B end) h p = p +heap-weakeningᴱ Γ H (binexp M op N) h p = p -data OrWarningᴮ {Γ B T} (H : Heap yes) (D : Γ ⊢ᴮ B ∈ T) A : Set where - ok : A → OrWarningᴮ H D A - warning : Warningᴮ H D → OrWarningᴮ H D A +heap-weakeningᴮ : ∀ Γ H B {H′ U} → (H ⊑ H′) → (typeOfᴮ H′ Γ B ≮: U) → (typeOfᴮ H Γ B ≮: U) +heap-weakeningᴮ Γ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h p = heap-weakeningᴮ (Γ ⊕ f ↦ (T ⇒ U)) H B h p +heap-weakeningᴮ Γ H (local var x ∈ T ← M ∙ B) h p = heap-weakeningᴮ (Γ ⊕ x ↦ T) H B h p +heap-weakeningᴮ Γ H (return M ∙ B) h p = heap-weakeningᴱ Γ H M h p +heap-weakeningᴮ Γ H done h p = p -data OrWarningᴴᴱ {Γ M T} H (D : Γ ⊢ᴴᴱ H ▷ M ∈ T) A : Set where - ok : A → OrWarningᴴᴱ H D A - warning : Warningᴴᴱ H D → OrWarningᴴᴱ H D A +substitutivityᴱ : ∀ {Γ T U} H M v x → (typeOfᴱ H Γ (M [ v / x ]ᴱ) ≮: U) → Either (typeOfᴱ H (Γ ⊕ x ↦ T) M ≮: U) (typeOfᴱ H ∅ (val v) ≮: T) +substitutivityᴱ-whenever : ∀ {Γ T U} H v x y (r : Dec(x ≡ y)) → (typeOfᴱ H Γ (var y [ v / x ]ᴱwhenever r) ≮: U) → Either (typeOfᴱ H (Γ ⊕ x ↦ T) (var y) ≮: U) (typeOfᴱ H ∅ (val v) ≮: T) +substitutivityᴮ : ∀ {Γ T U} H B v x → (typeOfᴮ H Γ (B [ v / x ]ᴮ) ≮: U) → Either (typeOfᴮ H (Γ ⊕ x ↦ T) B ≮: U) (typeOfᴱ H ∅ (val v) ≮: T) +substitutivityᴮ-unless : ∀ {Γ T U V} H B v x y (r : Dec(x ≡ y)) → (typeOfᴮ H (Γ ⊕ y ↦ U) (B [ v / x ]ᴮunless r) ≮: V) → Either (typeOfᴮ H ((Γ ⊕ x ↦ T) ⊕ y ↦ U) B ≮: V) (typeOfᴱ H ∅ (val v) ≮: T) +substitutivityᴮ-unless-yes : ∀ {Γ Γ′ T V} H B v x y (r : x ≡ y) → (Γ′ ≡ Γ) → (typeOfᴮ H Γ (B [ v / x ]ᴮunless yes r) ≮: V) → Either (typeOfᴮ H Γ′ B ≮: V) (typeOfᴱ H ∅ (val v) ≮: T) +substitutivityᴮ-unless-no : ∀ {Γ Γ′ T V} H B v x y (r : x ≢ y) → (Γ′ ≡ Γ ⊕ x ↦ T) → (typeOfᴮ H Γ (B [ v / x ]ᴮunless no r) ≮: V) → Either (typeOfᴮ H Γ′ B ≮: V) (typeOfᴱ H ∅ (val v) ≮: T) -data OrWarningᴴᴮ {Γ B T} H (D : Γ ⊢ᴴᴮ H ▷ B ∈ T) A : Set where - ok : A → OrWarningᴴᴮ H D A - warning : Warningᴴᴮ H D → OrWarningᴴᴮ H D A +substitutivityᴱ H (var y) v x p = substitutivityᴱ-whenever H v x y (x ≡ⱽ y) p +substitutivityᴱ H (val w) v x p = Left p +substitutivityᴱ H (binexp M op N) v x p = Left p +substitutivityᴱ H (M $ N) v x p = mapL none-tgt-≮: (substitutivityᴱ H M v x (tgt-none-≮: p)) +substitutivityᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x p = Left p +substitutivityᴱ H (block var b ∈ T is B end) v x p = Left p +substitutivityᴱ-whenever H v x x (yes refl) q = swapLR (≮:-trans q) +substitutivityᴱ-whenever H v x y (no p) q = Left (≡-trans-≮: (cong orAny (sym (⊕-lookup-miss x y _ _ p))) q) -heap-weakeningᴱ : ∀ H M {H′ Γ} → (H ⊑ H′) → OrWarningᴱ H (typeCheckᴱ H Γ M) (typeOfᴱ H Γ M ≡ typeOfᴱ H′ Γ M) -heap-weakeningᴮ : ∀ H B {H′ Γ} → (H ⊑ H′) → OrWarningᴮ H (typeCheckᴮ H Γ B) (typeOfᴮ H Γ B ≡ typeOfᴮ H′ Γ B) - -heap-weakeningᴱ H (var x) h = ok refl -heap-weakeningᴱ H (val nil) h = ok refl -heap-weakeningᴱ H (val (addr a)) refl = ok refl -heap-weakeningᴱ H (val (addr a)) (snoc {a = b} defn) with a ≡ᴬ b -heap-weakeningᴱ H (val (addr a)) (snoc {a = a} defn) | yes refl = warning (UnallocatedAddress refl) -heap-weakeningᴱ H (val (addr a)) (snoc {a = b} p) | no q = ok (cong orNone (cong typeOfᴹᴼ (lookup-not-allocated p q))) -heap-weakeningᴱ H (val (number n)) h = ok refl -heap-weakeningᴱ H (val (bool b)) h = ok refl -heap-weakeningᴱ H (val (string x)) h = ok refl -heap-weakeningᴱ H (binexp M op N) h = ok refl -heap-weakeningᴱ H (M $ N) h with heap-weakeningᴱ H M h -heap-weakeningᴱ H (M $ N) h | ok p = ok (cong tgt p) -heap-weakeningᴱ H (M $ N) h | warning W = warning (app₁ W) -heap-weakeningᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) h = ok refl -heap-weakeningᴱ H (block var b ∈ T is B end) h = ok refl -heap-weakeningᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h with heap-weakeningᴮ H B h -heap-weakeningᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h | ok p = ok p -heap-weakeningᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h | warning W = warning (function₂ W) -heap-weakeningᴮ H (local var x ∈ T ← M ∙ B) h with heap-weakeningᴮ H B h -heap-weakeningᴮ H (local var x ∈ T ← M ∙ B) h | ok p = ok p -heap-weakeningᴮ H (local var x ∈ T ← M ∙ B) h | warning W = warning (local₂ W) -heap-weakeningᴮ H (return M ∙ B) h with heap-weakeningᴱ H M h -heap-weakeningᴮ H (return M ∙ B) h | ok p = ok p -heap-weakeningᴮ H (return M ∙ B) h | warning W = warning (return W) -heap-weakeningᴮ H (done) h = ok refl - -none-not-obj : ∀ O → none ≢ typeOfᴼ O -none-not-obj (function f ⟨ var x ∈ T ⟩∈ U is B end) () - -typeOf-val-not-none : ∀ {H Γ} v → OrWarningᴱ H (typeCheckᴱ H Γ (val v)) (none ≢ typeOfᴱ H Γ (val v)) -typeOf-val-not-none nil = ok (λ ()) -typeOf-val-not-none (number n) = ok (λ ()) -typeOf-val-not-none (bool b) = ok (λ ()) -typeOf-val-not-none (string x) = ok (λ ()) -typeOf-val-not-none {H = H} (addr a) with remember (H [ a ]ᴴ) -typeOf-val-not-none {H = H} (addr a) | (just O , p) = ok (λ q → none-not-obj O (trans q (cong orNone (cong typeOfᴹᴼ p)))) -typeOf-val-not-none {H = H} (addr a) | (nothing , p) = warning (UnallocatedAddress p) - -substitutivityᴱ : ∀ {Γ T} H M v x → (just T ≡ typeOfⱽ H v) → (typeOfᴱ H (Γ ⊕ x ↦ T) M ≡ typeOfᴱ H Γ (M [ v / x ]ᴱ)) -substitutivityᴱ-whenever-yes : ∀ {Γ T} H v x y (p : x ≡ y) → (just T ≡ typeOfⱽ H v) → (typeOfᴱ H (Γ ⊕ x ↦ T) (var y) ≡ typeOfᴱ H Γ (var y [ v / x ]ᴱwhenever (yes p))) -substitutivityᴱ-whenever-no : ∀ {Γ T} H v x y (p : x ≢ y) → (just T ≡ typeOfⱽ H v) → (typeOfᴱ H (Γ ⊕ x ↦ T) (var y) ≡ typeOfᴱ H Γ (var y [ v / x ]ᴱwhenever (no p))) -substitutivityᴮ : ∀ {Γ T} H B v x → (just T ≡ typeOfⱽ H v) → (typeOfᴮ H (Γ ⊕ x ↦ T) B ≡ typeOfᴮ H Γ (B [ v / x ]ᴮ)) -substitutivityᴮ-unless-yes : ∀ {Γ Γ′ T} H B v x y (p : x ≡ y) → (just T ≡ typeOfⱽ H v) → (Γ′ ≡ Γ) → (typeOfᴮ H Γ′ B ≡ typeOfᴮ H Γ (B [ v / x ]ᴮunless (yes p))) -substitutivityᴮ-unless-no : ∀ {Γ Γ′ T} H B v x y (p : x ≢ y) → (just T ≡ typeOfⱽ H v) → (Γ′ ≡ Γ ⊕ x ↦ T) → (typeOfᴮ H Γ′ B ≡ typeOfᴮ H Γ (B [ v / x ]ᴮunless (no p))) - -substitutivityᴱ H (var y) v x p with x ≡ⱽ y -substitutivityᴱ H (var y) v x p | yes q = substitutivityᴱ-whenever-yes H v x y q p -substitutivityᴱ H (var y) v x p | no q = substitutivityᴱ-whenever-no H v x y q p -substitutivityᴱ H (val w) v x p = refl -substitutivityᴱ H (binexp M op N) v x p = refl -substitutivityᴱ H (M $ N) v x p = cong tgt (substitutivityᴱ H M v x p) -substitutivityᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x p = refl -substitutivityᴱ H (block var b ∈ T is B end) v x p = refl -substitutivityᴱ-whenever-yes H v x x refl q = cong orNone q -substitutivityᴱ-whenever-no H v x y p q = cong orNone ( sym (⊕-lookup-miss x y _ _ p)) -substitutivityᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p with x ≡ⱽ f -substitutivityᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p | yes q = substitutivityᴮ-unless-yes H B v x f q p (⊕-over q) -substitutivityᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p | no q = substitutivityᴮ-unless-no H B v x f q p (⊕-swap q) -substitutivityᴮ H (local var y ∈ T ← M ∙ B) v x p with x ≡ⱽ y -substitutivityᴮ H (local var y ∈ T ← M ∙ B) v x p | yes q = substitutivityᴮ-unless-yes H B v x y q p (⊕-over q) -substitutivityᴮ H (local var y ∈ T ← M ∙ B) v x p | no q = substitutivityᴮ-unless-no H B v x y q p (⊕-swap q) +substitutivityᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p = substitutivityᴮ-unless H B v x f (x ≡ⱽ f) p +substitutivityᴮ H (local var y ∈ T ← M ∙ B) v x p = substitutivityᴮ-unless H B v x y (x ≡ⱽ y) p substitutivityᴮ H (return M ∙ B) v x p = substitutivityᴱ H M v x p -substitutivityᴮ H done v x p = refl -substitutivityᴮ-unless-yes H B v x x refl q refl = refl -substitutivityᴮ-unless-no H B v x y p q refl = substitutivityᴮ H B v x q +substitutivityᴮ H done v x p = Left p +substitutivityᴮ-unless H B v x y (yes p) q = substitutivityᴮ-unless-yes H B v x y p (⊕-over p) q +substitutivityᴮ-unless H B v x y (no p) q = substitutivityᴮ-unless-no H B v x y p (⊕-swap p) q +substitutivityᴮ-unless-yes H B v x y refl refl p = Left p +substitutivityᴮ-unless-no H B v x y p refl q = substitutivityᴮ H B v x q binOpPreservation : ∀ H {op v w x} → (v ⟦ op ⟧ w ⟶ x) → (tgtBinOp op ≡ typeOfᴱ H ∅ (val x)) binOpPreservation H (+ m n) = refl @@ -157,306 +122,215 @@ binOpPreservation H (== v w) = refl binOpPreservation H (~= v w) = refl binOpPreservation H (·· v w) = refl -preservationᴱ : ∀ H M {H′ M′} → (H ⊢ M ⟶ᴱ M′ ⊣ H′) → OrWarningᴴᴱ H (typeCheckᴴᴱ H ∅ M) (typeOfᴱ H ∅ M ≡ typeOfᴱ H′ ∅ M′) -preservationᴮ : ∀ H B {H′ B′} → (H ⊢ B ⟶ᴮ B′ ⊣ H′) → OrWarningᴴᴮ H (typeCheckᴴᴮ H ∅ B) (typeOfᴮ H ∅ B ≡ typeOfᴮ H′ ∅ B′) +reflect-subtypingᴱ : ∀ H M {H′ M′ T} → (H ⊢ M ⟶ᴱ M′ ⊣ H′) → (typeOfᴱ H′ ∅ M′ ≮: T) → Either (typeOfᴱ H ∅ M ≮: T) (Warningᴱ H (typeCheckᴱ H ∅ M)) +reflect-subtypingᴮ : ∀ H B {H′ B′ T} → (H ⊢ B ⟶ᴮ B′ ⊣ H′) → (typeOfᴮ H′ ∅ B′ ≮: T) → Either (typeOfᴮ H ∅ B ≮: T) (Warningᴮ H (typeCheckᴮ H ∅ B)) -preservationᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) = ok refl -preservationᴱ H (M $ N) (app₁ s) with preservationᴱ H M s -preservationᴱ H (M $ N) (app₁ s) | ok p = ok (cong tgt p) -preservationᴱ H (M $ N) (app₁ s) | warning (expr W) = warning (expr (app₁ W)) -preservationᴱ H (M $ N) (app₁ s) | warning (heap W) = warning (heap W) -preservationᴱ H (M $ N) (app₂ p s) with heap-weakeningᴱ H M (rednᴱ⊑ s) -preservationᴱ H (M $ N) (app₂ p s) | ok q = ok (cong tgt q) -preservationᴱ H (M $ N) (app₂ p s) | warning W = warning (expr (app₁ W)) -preservationᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ S ⟩∈ T is B end) v refl p) with remember (typeOfⱽ H v) -preservationᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ S ⟩∈ T is B end) v refl p) | (just U , q) with S ≡ᵀ U | T ≡ᵀ typeOfᴮ H (x ↦ S) B -preservationᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ S ⟩∈ T is B end) v refl p) | (just U , q) | yes refl | yes refl = ok (cong tgt (cong orNone (cong typeOfᴹᴼ p))) -preservationᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ S ⟩∈ T is B end) v refl p) | (just U , q) | yes refl | no r = warning (heap (addr a p (FunctionDefnMismatch r))) -preservationᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ S ⟩∈ T is B end) v refl p) | (just U , q) | no r | _ = warning (expr (FunctionCallMismatch (λ s → r (trans (trans (sym (cong src (cong orNone (cong typeOfᴹᴼ p)))) s) (cong orNone q))))) -preservationᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ S ⟩∈ T is B end) v refl p) | (nothing , q) with typeOf-val-not-none v -preservationᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ S ⟩∈ T is B end) v refl p) | (nothing , q) | ok r = CONTRADICTION (r (sym (cong orNone q))) -preservationᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ S ⟩∈ T is B end) v refl p) | (nothing , q) | warning W = warning (expr (app₂ W)) -preservationᴱ H (block var b ∈ T is B end) (block s) = ok refl -preservationᴱ H (block var b ∈ T is return M ∙ B end) (return v) with T ≡ᵀ typeOfᴱ H ∅ (val v) -preservationᴱ H (block var b ∈ T is return M ∙ B end) (return v) | yes p = ok p -preservationᴱ H (block var b ∈ T is return M ∙ B end) (return v) | no p = warning (expr (BlockMismatch p)) -preservationᴱ H (block var b ∈ T is done end) (done) with T ≡ᵀ nil -preservationᴱ H (block var b ∈ T is done end) (done) | yes p = ok p -preservationᴱ H (block var b ∈ T is done end) (done) | no p = warning (expr (BlockMismatch p)) -preservationᴱ H (binexp M op N) (binOp₀ s) = ok (binOpPreservation H s) -preservationᴱ H (binexp M op N) (binOp₁ s) = ok refl -preservationᴱ H (binexp M op N) (binOp₂ s) = ok refl +reflect-subtypingᴱ H (M $ N) (app₁ s) p = mapLR none-tgt-≮: app₁ (reflect-subtypingᴱ H M s (tgt-none-≮: p)) +reflect-subtypingᴱ H (M $ N) (app₂ v s) p = Left (none-tgt-≮: (heap-weakeningᴱ ∅ H M (rednᴱ⊑ s) (tgt-none-≮: p))) +reflect-subtypingᴱ H (M $ N) (beta (function f ⟨ var y ∈ T ⟩∈ U is B end) v refl q) p = Left (≡-trans-≮: (cong tgt (cong orAny (cong typeOfᴹᴼ q))) p) +reflect-subtypingᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) p = Left p +reflect-subtypingᴱ H (block var b ∈ T is B end) (block s) p = Left p +reflect-subtypingᴱ H (block var b ∈ T is return (val v) ∙ B end) (return v) p = mapR BlockMismatch (swapLR (≮:-trans p)) +reflect-subtypingᴱ H (block var b ∈ T is done end) done p = mapR BlockMismatch (swapLR (≮:-trans p)) +reflect-subtypingᴱ H (binexp M op N) (binOp₀ s) p = Left (≡-trans-≮: (binOpPreservation H s) p) +reflect-subtypingᴱ H (binexp M op N) (binOp₁ s) p = Left p +reflect-subtypingᴱ H (binexp M op N) (binOp₂ s) p = Left p -preservationᴮ H (local var x ∈ T ← M ∙ B) (local s) with heap-weakeningᴮ H B (rednᴱ⊑ s) -preservationᴮ H (local var x ∈ T ← M ∙ B) (local s) | ok p = ok p -preservationᴮ H (local var x ∈ T ← M ∙ B) (local s) | warning W = warning (block (local₂ W)) -preservationᴮ H (local var x ∈ T ← M ∙ B) (subst v) with remember (typeOfⱽ H v) -preservationᴮ H (local var x ∈ T ← M ∙ B) (subst v) | (just U , p) with T ≡ᵀ U -preservationᴮ H (local var x ∈ T ← M ∙ B) (subst v) | (just T , p) | yes refl = ok (substitutivityᴮ H B v x (sym p)) -preservationᴮ H (local var x ∈ T ← M ∙ B) (subst v) | (just U , p) | no q = warning (block (LocalVarMismatch (λ r → q (trans r (cong orNone p))))) -preservationᴮ H (local var x ∈ T ← M ∙ B) (subst v) | (nothing , p) with typeOf-val-not-none v -preservationᴮ H (local var x ∈ T ← M ∙ B) (subst v) | (nothing , p) | ok q = CONTRADICTION (q (sym (cong orNone p))) -preservationᴮ H (local var x ∈ T ← M ∙ B) (subst v) | (nothing , p) | warning W = warning (block (local₁ W)) -preservationᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function a defn) with heap-weakeningᴮ H B (snoc defn) -preservationᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function a defn) | ok r = ok (trans r (substitutivityᴮ _ B (addr a) f refl)) -preservationᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function a defn) | warning W = warning (block (function₂ W)) -preservationᴮ H (return M ∙ B) (return s) with preservationᴱ H M s -preservationᴮ H (return M ∙ B) (return s) | ok p = ok p -preservationᴮ H (return M ∙ B) (return s) | warning (expr W) = warning (block (return W)) -preservationᴮ H (return M ∙ B) (return s) | warning (heap W) = warning (heap W) +reflect-subtypingᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function a defn) p = mapLR (heap-weakeningᴮ _ _ B (snoc defn)) (CONTRADICTION ∘ ≮:-refl) (substitutivityᴮ _ B (addr a) f p) +reflect-subtypingᴮ H (local var x ∈ T ← M ∙ B) (local s) p = Left (heap-weakeningᴮ (x ↦ T) H B (rednᴱ⊑ s) p) +reflect-subtypingᴮ H (local var x ∈ T ← M ∙ B) (subst v) p = mapR LocalVarMismatch (substitutivityᴮ H B v x p) +reflect-subtypingᴮ H (return M ∙ B) (return s) p = mapR return (reflect-subtypingᴱ H M s p) -reflect-substitutionᴱ : ∀ {Γ T} H M v x → (just T ≡ typeOfⱽ H v) → Warningᴱ H (typeCheckᴱ H Γ (M [ v / x ]ᴱ)) → Warningᴱ H (typeCheckᴱ H (Γ ⊕ x ↦ T) M) -reflect-substitutionᴱ-whenever-yes : ∀ {Γ T} H v x y (p : x ≡ y) → (just T ≡ typeOfⱽ H v) → Warningᴱ H (typeCheckᴱ H Γ (var y [ v / x ]ᴱwhenever yes p)) → Warningᴱ H (typeCheckᴱ H (Γ ⊕ x ↦ T) (var y)) -reflect-substitutionᴱ-whenever-no : ∀ {Γ T} H v x y (p : x ≢ y) → (just T ≡ typeOfⱽ H v) → Warningᴱ H (typeCheckᴱ H Γ (var y [ v / x ]ᴱwhenever no p)) → Warningᴱ H (typeCheckᴱ H (Γ ⊕ x ↦ T) (var y)) -reflect-substitutionᴮ : ∀ {Γ T} H B v x → (just T ≡ typeOfⱽ H v) → Warningᴮ H (typeCheckᴮ H Γ (B [ v / x ]ᴮ)) → Warningᴮ H (typeCheckᴮ H (Γ ⊕ x ↦ T) B) -reflect-substitutionᴮ-unless-yes : ∀ {Γ Γ′ T} H B v x y (r : x ≡ y) → (just T ≡ typeOfⱽ H v) → (Γ′ ≡ Γ) → Warningᴮ H (typeCheckᴮ H Γ (B [ v / x ]ᴮunless yes r)) → Warningᴮ H (typeCheckᴮ H Γ′ B) -reflect-substitutionᴮ-unless-no : ∀ {Γ Γ′ T} H B v x y (r : x ≢ y) → (just T ≡ typeOfⱽ H v) → (Γ′ ≡ Γ ⊕ x ↦ T) → Warningᴮ H (typeCheckᴮ H Γ (B [ v / x ]ᴮunless no r)) → Warningᴮ H (typeCheckᴮ H Γ′ B) +reflect-substitutionᴱ : ∀ {Γ T} H M v x → Warningᴱ H (typeCheckᴱ H Γ (M [ v / x ]ᴱ)) → Either (Warningᴱ H (typeCheckᴱ H (Γ ⊕ x ↦ T) M)) (Either (Warningᴱ H (typeCheckᴱ H ∅ (val v))) (typeOfᴱ H ∅ (val v) ≮: T)) +reflect-substitutionᴱ-whenever : ∀ {Γ T} H v x y (p : Dec(x ≡ y)) → Warningᴱ H (typeCheckᴱ H Γ (var y [ v / x ]ᴱwhenever p)) → Either (Warningᴱ H (typeCheckᴱ H (Γ ⊕ x ↦ T) (var y))) (Either (Warningᴱ H (typeCheckᴱ H ∅ (val v))) (typeOfᴱ H ∅ (val v) ≮: T)) +reflect-substitutionᴮ : ∀ {Γ T} H B v x → Warningᴮ H (typeCheckᴮ H Γ (B [ v / x ]ᴮ)) → Either (Warningᴮ H (typeCheckᴮ H (Γ ⊕ x ↦ T) B)) (Either (Warningᴱ H (typeCheckᴱ H ∅ (val v))) (typeOfᴱ H ∅ (val v) ≮: T)) +reflect-substitutionᴮ-unless : ∀ {Γ T U} H B v x y (r : Dec(x ≡ y)) → Warningᴮ H (typeCheckᴮ H (Γ ⊕ y ↦ U) (B [ v / x ]ᴮunless r)) → Either (Warningᴮ H (typeCheckᴮ H ((Γ ⊕ x ↦ T) ⊕ y ↦ U) B)) (Either (Warningᴱ H (typeCheckᴱ H ∅ (val v))) (typeOfᴱ H ∅ (val v) ≮: T)) +reflect-substitutionᴮ-unless-yes : ∀ {Γ Γ′ T} H B v x y (r : x ≡ y) → (Γ′ ≡ Γ) → Warningᴮ H (typeCheckᴮ H Γ (B [ v / x ]ᴮunless yes r)) → Either (Warningᴮ H (typeCheckᴮ H Γ′ B)) (Either (Warningᴱ H (typeCheckᴱ H ∅ (val v))) (typeOfᴱ H ∅ (val v) ≮: T)) +reflect-substitutionᴮ-unless-no : ∀ {Γ Γ′ T} H B v x y (r : x ≢ y) → (Γ′ ≡ Γ ⊕ x ↦ T) → Warningᴮ H (typeCheckᴮ H Γ (B [ v / x ]ᴮunless no r)) → Either (Warningᴮ H (typeCheckᴮ H Γ′ B)) (Either (Warningᴱ H (typeCheckᴱ H ∅ (val v))) (typeOfᴱ H ∅ (val v) ≮: T)) -reflect-substitutionᴱ H (var y) v x p W with x ≡ⱽ y -reflect-substitutionᴱ H (var y) v x p W | yes r = reflect-substitutionᴱ-whenever-yes H v x y r p W -reflect-substitutionᴱ H (var y) v x p W | no r = reflect-substitutionᴱ-whenever-no H v x y r p W -reflect-substitutionᴱ H (val (addr a)) v x p (UnallocatedAddress r) = UnallocatedAddress r -reflect-substitutionᴱ H (M $ N) v x p (FunctionCallMismatch q) = FunctionCallMismatch (λ s → q (trans (cong src (sym (substitutivityᴱ H M v x p))) (trans s (substitutivityᴱ H N v x p)))) -reflect-substitutionᴱ H (M $ N) v x p (app₁ W) = app₁ (reflect-substitutionᴱ H M v x p W) -reflect-substitutionᴱ H (M $ N) v x p (app₂ W) = app₂ (reflect-substitutionᴱ H N v x p W) -reflect-substitutionᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x p (FunctionDefnMismatch q) with (x ≡ⱽ y) -reflect-substitutionᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x p (FunctionDefnMismatch q) | yes r = FunctionDefnMismatch (λ s → q (trans s (substitutivityᴮ-unless-yes H B v x y r p (⊕-over r)))) -reflect-substitutionᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x p (FunctionDefnMismatch q) | no r = FunctionDefnMismatch (λ s → q (trans s (substitutivityᴮ-unless-no H B v x y r p (⊕-swap r)))) -reflect-substitutionᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x p (function₁ W) with (x ≡ⱽ y) -reflect-substitutionᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x p (function₁ W) | yes r = function₁ (reflect-substitutionᴮ-unless-yes H B v x y r p (⊕-over r) W) -reflect-substitutionᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x p (function₁ W) | no r = function₁ (reflect-substitutionᴮ-unless-no H B v x y r p (⊕-swap r) W) -reflect-substitutionᴱ H (block var b ∈ T is B end) v x p (BlockMismatch q) = BlockMismatch (λ r → q (trans r (substitutivityᴮ H B v x p))) -reflect-substitutionᴱ H (block var b ∈ T is B end) v x p (block₁ W) = block₁ (reflect-substitutionᴮ H B v x p W) -reflect-substitutionᴱ H (binexp M op N) x v p (BinOpMismatch₁ q) = BinOpMismatch₁ (subst₁ (BinOpWarning op) (sym (substitutivityᴱ H M x v p)) q) -reflect-substitutionᴱ H (binexp M op N) x v p (BinOpMismatch₂ q) = BinOpMismatch₂ (subst₁ (BinOpWarning op) (sym (substitutivityᴱ H N x v p)) q) -reflect-substitutionᴱ H (binexp M op N) x v p (bin₁ W) = bin₁ (reflect-substitutionᴱ H M x v p W) -reflect-substitutionᴱ H (binexp M op N) x v p (bin₂ W) = bin₂ (reflect-substitutionᴱ H N x v p W) +reflect-substitutionᴱ H (var y) v x W = reflect-substitutionᴱ-whenever H v x y (x ≡ⱽ y) W +reflect-substitutionᴱ H (val (addr a)) v x (UnallocatedAddress r) = Left (UnallocatedAddress r) +reflect-substitutionᴱ H (M $ N) v x (FunctionCallMismatch p) with substitutivityᴱ H N v x p +reflect-substitutionᴱ H (M $ N) v x (FunctionCallMismatch p) | Right W = Right (Right W) +reflect-substitutionᴱ H (M $ N) v x (FunctionCallMismatch p) | Left q with substitutivityᴱ H M v x (src-any-≮: q) +reflect-substitutionᴱ H (M $ N) v x (FunctionCallMismatch p) | Left q | Left r = Left ((FunctionCallMismatch ∘ any-src-≮: q) r) +reflect-substitutionᴱ H (M $ N) v x (FunctionCallMismatch p) | Left q | Right W = Right (Right W) +reflect-substitutionᴱ H (M $ N) v x (app₁ W) = mapL app₁ (reflect-substitutionᴱ H M v x W) +reflect-substitutionᴱ H (M $ N) v x (app₂ W) = mapL app₂ (reflect-substitutionᴱ H N v x W) +reflect-substitutionᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x (FunctionDefnMismatch q) = mapLR FunctionDefnMismatch Right (substitutivityᴮ-unless H B v x y (x ≡ⱽ y) q) +reflect-substitutionᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) v x (function₁ W) = mapL function₁ (reflect-substitutionᴮ-unless H B v x y (x ≡ⱽ y) W) +reflect-substitutionᴱ H (block var b ∈ T is B end) v x (BlockMismatch q) = mapLR BlockMismatch Right (substitutivityᴮ H B v x q) +reflect-substitutionᴱ H (block var b ∈ T is B end) v x (block₁ W′) = mapL block₁ (reflect-substitutionᴮ H B v x W′) +reflect-substitutionᴱ H (binexp M op N) v x (BinOpMismatch₁ q) = mapLR BinOpMismatch₁ Right (substitutivityᴱ H M v x q) +reflect-substitutionᴱ H (binexp M op N) v x (BinOpMismatch₂ q) = mapLR BinOpMismatch₂ Right (substitutivityᴱ H N v x q) +reflect-substitutionᴱ H (binexp M op N) v x (bin₁ W) = mapL bin₁ (reflect-substitutionᴱ H M v x W) +reflect-substitutionᴱ H (binexp M op N) v x (bin₂ W) = mapL bin₂ (reflect-substitutionᴱ H N v x W) -reflect-substitutionᴱ-whenever-no H v x y p q (UnboundVariable r) = UnboundVariable (trans (sym (⊕-lookup-miss x y _ _ p)) r) -reflect-substitutionᴱ-whenever-yes H (addr a) x x refl p (UnallocatedAddress q) with trans p (cong typeOfᴹᴼ q) -reflect-substitutionᴱ-whenever-yes H (addr a) x x refl p (UnallocatedAddress q) | () +reflect-substitutionᴱ-whenever H a x x (yes refl) (UnallocatedAddress p) = Right (Left (UnallocatedAddress p)) +reflect-substitutionᴱ-whenever H v x y (no p) (UnboundVariable q) = Left (UnboundVariable (trans (sym (⊕-lookup-miss x y _ _ p)) q)) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (FunctionDefnMismatch q) with (x ≡ⱽ y) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (FunctionDefnMismatch q) | yes r = FunctionDefnMismatch (λ s → q (trans s (substitutivityᴮ-unless-yes H C v x y r p (⊕-over r)))) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (FunctionDefnMismatch q) | no r = FunctionDefnMismatch (λ s → q (trans s (substitutivityᴮ-unless-no H C v x y r p (⊕-swap r)))) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (function₁ W) with (x ≡ⱽ y) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (function₁ W) | yes r = function₁ (reflect-substitutionᴮ-unless-yes H C v x y r p (⊕-over r) W) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (function₁ W) | no r = function₁ (reflect-substitutionᴮ-unless-no H C v x y r p (⊕-swap r) W) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (function₂ W) with (x ≡ⱽ f) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (function₂ W)| yes r = function₂ (reflect-substitutionᴮ-unless-yes H B v x f r p (⊕-over r) W) -reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x p (function₂ W)| no r = function₂ (reflect-substitutionᴮ-unless-no H B v x f r p (⊕-swap r) W) -reflect-substitutionᴮ H (local var y ∈ T ← M ∙ B) v x p (LocalVarMismatch q) = LocalVarMismatch (λ r → q (trans r (substitutivityᴱ H M v x p))) -reflect-substitutionᴮ H (local var y ∈ T ← M ∙ B) v x p (local₁ W) = local₁ (reflect-substitutionᴱ H M v x p W) -reflect-substitutionᴮ H (local var y ∈ T ← M ∙ B) v x p (local₂ W) with (x ≡ⱽ y) -reflect-substitutionᴮ H (local var y ∈ T ← M ∙ B) v x p (local₂ W) | yes r = local₂ (reflect-substitutionᴮ-unless-yes H B v x y r p (⊕-over r) W) -reflect-substitutionᴮ H (local var y ∈ T ← M ∙ B) v x p (local₂ W) | no r = local₂ (reflect-substitutionᴮ-unless-no H B v x y r p (⊕-swap r) W) -reflect-substitutionᴮ H (return M ∙ B) v x p (return W) = return (reflect-substitutionᴱ H M v x p W) +reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x (FunctionDefnMismatch q) = mapLR FunctionDefnMismatch Right (substitutivityᴮ-unless H C v x y (x ≡ⱽ y) q) +reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x (function₁ W) = mapL function₁ (reflect-substitutionᴮ-unless H C v x y (x ≡ⱽ y) W) +reflect-substitutionᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) v x (function₂ W) = mapL function₂ (reflect-substitutionᴮ-unless H B v x f (x ≡ⱽ f) W) +reflect-substitutionᴮ H (local var y ∈ T ← M ∙ B) v x (LocalVarMismatch q) = mapLR LocalVarMismatch Right (substitutivityᴱ H M v x q) +reflect-substitutionᴮ H (local var y ∈ T ← M ∙ B) v x (local₁ W) = mapL local₁ (reflect-substitutionᴱ H M v x W) +reflect-substitutionᴮ H (local var y ∈ T ← M ∙ B) v x (local₂ W) = mapL local₂ (reflect-substitutionᴮ-unless H B v x y (x ≡ⱽ y) W) +reflect-substitutionᴮ H (return M ∙ B) v x (return W) = mapL return (reflect-substitutionᴱ H M v x W) -reflect-substitutionᴮ-unless-yes H B v x y r p refl W = W -reflect-substitutionᴮ-unless-no H B v x y r p refl W = reflect-substitutionᴮ H B v x p W +reflect-substitutionᴮ-unless H B v x y (yes p) W = reflect-substitutionᴮ-unless-yes H B v x y p (⊕-over p) W +reflect-substitutionᴮ-unless H B v x y (no p) W = reflect-substitutionᴮ-unless-no H B v x y p (⊕-swap p) W +reflect-substitutionᴮ-unless-yes H B v x x refl refl W = Left W +reflect-substitutionᴮ-unless-no H B v x y p refl W = reflect-substitutionᴮ H B v x W -reflect-weakeningᴱ : ∀ H M {H′ Γ} → (H ⊑ H′) → Warningᴱ H′ (typeCheckᴱ H′ Γ M) → Warningᴱ H (typeCheckᴱ H Γ M) -reflect-weakeningᴮ : ∀ H B {H′ Γ} → (H ⊑ H′) → Warningᴮ H′ (typeCheckᴮ H′ Γ B) → Warningᴮ H (typeCheckᴮ H Γ B) +reflect-weakeningᴱ : ∀ Γ H M {H′} → (H ⊑ H′) → Warningᴱ H′ (typeCheckᴱ H′ Γ M) → Warningᴱ H (typeCheckᴱ H Γ M) +reflect-weakeningᴮ : ∀ Γ H B {H′} → (H ⊑ H′) → Warningᴮ H′ (typeCheckᴮ H′ Γ B) → Warningᴮ H (typeCheckᴮ H Γ B) -reflect-weakeningᴱ H (var x) h (UnboundVariable p) = (UnboundVariable p) -reflect-weakeningᴱ H (val (addr a)) h (UnallocatedAddress p) = UnallocatedAddress (lookup-⊑-nothing a h p) -reflect-weakeningᴱ H (M $ N) h (FunctionCallMismatch p) with heap-weakeningᴱ H M h | heap-weakeningᴱ H N h -reflect-weakeningᴱ H (M $ N) h (FunctionCallMismatch p) | ok q₁ | ok q₂ = FunctionCallMismatch (λ r → p (trans (cong src (sym q₁)) (trans r q₂))) -reflect-weakeningᴱ H (M $ N) h (FunctionCallMismatch p) | warning W | _ = app₁ W -reflect-weakeningᴱ H (M $ N) h (FunctionCallMismatch p) | _ | warning W = app₂ W -reflect-weakeningᴱ H (M $ N) h (app₁ W) = app₁ (reflect-weakeningᴱ H M h W) -reflect-weakeningᴱ H (M $ N) h (app₂ W) = app₂ (reflect-weakeningᴱ H N h W) -reflect-weakeningᴱ H (binexp M op N) h (BinOpMismatch₁ p) with heap-weakeningᴱ H M h -reflect-weakeningᴱ H (binexp M op N) h (BinOpMismatch₁ p) | ok q = BinOpMismatch₁ (subst₁ (BinOpWarning op) (sym q) p) -reflect-weakeningᴱ H (binexp M op N) h (BinOpMismatch₁ p) | warning W = bin₁ W -reflect-weakeningᴱ H (binexp M op N) h (BinOpMismatch₂ p) with heap-weakeningᴱ H N h -reflect-weakeningᴱ H (binexp M op N) h (BinOpMismatch₂ p) | ok q = BinOpMismatch₂ (subst₁ (BinOpWarning op) (sym q) p) -reflect-weakeningᴱ H (binexp M op N) h (BinOpMismatch₂ p) | warning W = bin₂ W -reflect-weakeningᴱ H (binexp M op N) h (bin₁ W′) = bin₁ (reflect-weakeningᴱ H M h W′) -reflect-weakeningᴱ H (binexp M op N) h (bin₂ W′) = bin₂ (reflect-weakeningᴱ H N h W′) -reflect-weakeningᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) h (FunctionDefnMismatch p) with heap-weakeningᴮ H B h -reflect-weakeningᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) h (FunctionDefnMismatch p) | ok q = FunctionDefnMismatch (λ r → p (trans r q)) -reflect-weakeningᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) h (FunctionDefnMismatch p) | warning W = function₁ W -reflect-weakeningᴱ H (function f ⟨ var y ∈ T ⟩∈ U is B end) h (function₁ W) = function₁ (reflect-weakeningᴮ H B h W) -reflect-weakeningᴱ H (block var b ∈ T is B end) h (BlockMismatch p) with heap-weakeningᴮ H B h -reflect-weakeningᴱ H (block var b ∈ T is B end) h (BlockMismatch p) | ok q = BlockMismatch (λ r → p (trans r q)) -reflect-weakeningᴱ H (block var b ∈ T is B end) h (BlockMismatch p) | warning W = block₁ W -reflect-weakeningᴱ H (block var b ∈ T is B end) h (block₁ W) = block₁ (reflect-weakeningᴮ H B h W) +reflect-weakeningᴱ Γ H (var x) h (UnboundVariable p) = (UnboundVariable p) +reflect-weakeningᴱ Γ H (val (addr a)) h (UnallocatedAddress p) = UnallocatedAddress (lookup-⊑-nothing a h p) +reflect-weakeningᴱ Γ H (M $ N) h (FunctionCallMismatch p) = FunctionCallMismatch (heap-weakeningᴱ Γ H N h (any-src-≮: p (heap-weakeningᴱ Γ H M h (src-any-≮: p)))) +reflect-weakeningᴱ Γ H (M $ N) h (app₁ W) = app₁ (reflect-weakeningᴱ Γ H M h W) +reflect-weakeningᴱ Γ H (M $ N) h (app₂ W) = app₂ (reflect-weakeningᴱ Γ H N h W) +reflect-weakeningᴱ Γ H (binexp M op N) h (BinOpMismatch₁ p) = BinOpMismatch₁ (heap-weakeningᴱ Γ H M h p) +reflect-weakeningᴱ Γ H (binexp M op N) h (BinOpMismatch₂ p) = BinOpMismatch₂ (heap-weakeningᴱ Γ H N h p) +reflect-weakeningᴱ Γ H (binexp M op N) h (bin₁ W′) = bin₁ (reflect-weakeningᴱ Γ H M h W′) +reflect-weakeningᴱ Γ H (binexp M op N) h (bin₂ W′) = bin₂ (reflect-weakeningᴱ Γ H N h W′) +reflect-weakeningᴱ Γ H (function f ⟨ var y ∈ T ⟩∈ U is B end) h (FunctionDefnMismatch p) = FunctionDefnMismatch (heap-weakeningᴮ (Γ ⊕ y ↦ T) H B h p) +reflect-weakeningᴱ Γ H (function f ⟨ var y ∈ T ⟩∈ U is B end) h (function₁ W) = function₁ (reflect-weakeningᴮ (Γ ⊕ y ↦ T) H B h W) +reflect-weakeningᴱ Γ H (block var b ∈ T is B end) h (BlockMismatch p) = BlockMismatch (heap-weakeningᴮ Γ H B h p) +reflect-weakeningᴱ Γ H (block var b ∈ T is B end) h (block₁ W) = block₁ (reflect-weakeningᴮ Γ H B h W) -reflect-weakeningᴮ H (return M ∙ B) h (return W) = return (reflect-weakeningᴱ H M h W) -reflect-weakeningᴮ H (local var y ∈ T ← M ∙ B) h (LocalVarMismatch p) with heap-weakeningᴱ H M h -reflect-weakeningᴮ H (local var y ∈ T ← M ∙ B) h (LocalVarMismatch p) | ok q = LocalVarMismatch (λ r → p (trans r q)) -reflect-weakeningᴮ H (local var y ∈ T ← M ∙ B) h (LocalVarMismatch p) | warning W = local₁ W -reflect-weakeningᴮ H (local var y ∈ T ← M ∙ B) h (local₁ W) = local₁ (reflect-weakeningᴱ H M h W) -reflect-weakeningᴮ H (local var y ∈ T ← M ∙ B) h (local₂ W) = local₂ (reflect-weakeningᴮ H B h W) -reflect-weakeningᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h (FunctionDefnMismatch p) with heap-weakeningᴮ H C h -reflect-weakeningᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h (FunctionDefnMismatch p) | ok q = FunctionDefnMismatch (λ r → p (trans r q)) -reflect-weakeningᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h (FunctionDefnMismatch p) | warning W = function₁ W -reflect-weakeningᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h (function₁ W) = function₁ (reflect-weakeningᴮ H C h W) -reflect-weakeningᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h (function₂ W) = function₂ (reflect-weakeningᴮ H B h W) +reflect-weakeningᴮ Γ H (return M ∙ B) h (return W) = return (reflect-weakeningᴱ Γ H M h W) +reflect-weakeningᴮ Γ H (local var y ∈ T ← M ∙ B) h (LocalVarMismatch p) = LocalVarMismatch (heap-weakeningᴱ Γ H M h p) +reflect-weakeningᴮ Γ H (local var y ∈ T ← M ∙ B) h (local₁ W) = local₁ (reflect-weakeningᴱ Γ H M h W) +reflect-weakeningᴮ Γ H (local var y ∈ T ← M ∙ B) h (local₂ W) = local₂ (reflect-weakeningᴮ (Γ ⊕ y ↦ T) H B h W) +reflect-weakeningᴮ Γ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h (FunctionDefnMismatch p) = FunctionDefnMismatch (heap-weakeningᴮ (Γ ⊕ x ↦ T) H C h p) +reflect-weakeningᴮ Γ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h (function₁ W) = function₁ (reflect-weakeningᴮ (Γ ⊕ x ↦ T) H C h W) +reflect-weakeningᴮ Γ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) h (function₂ W) = function₂ (reflect-weakeningᴮ (Γ ⊕ f ↦ (T ⇒ U)) H B h W) reflect-weakeningᴼ : ∀ H O {H′} → (H ⊑ H′) → Warningᴼ H′ (typeCheckᴼ H′ O) → Warningᴼ H (typeCheckᴼ H O) -reflect-weakeningᴼ H (just (function f ⟨ var x ∈ T ⟩∈ U is B end)) h (FunctionDefnMismatch p) with heap-weakeningᴮ H B h -reflect-weakeningᴼ H (just (function f ⟨ var x ∈ T ⟩∈ U is B end)) h (FunctionDefnMismatch p) | ok q = FunctionDefnMismatch (λ r → p (trans r q)) -reflect-weakeningᴼ H (just (function f ⟨ var x ∈ T ⟩∈ U is B end)) h (FunctionDefnMismatch p) | warning W = function₁ W -reflect-weakeningᴼ H (just (function f ⟨ var x ∈ T ⟩∈ U is B end)) h (function₁ W′) = function₁ (reflect-weakeningᴮ H B h W′) +reflect-weakeningᴼ H (just function f ⟨ var x ∈ T ⟩∈ U is B end) h (FunctionDefnMismatch p) = FunctionDefnMismatch (heap-weakeningᴮ (x ↦ T) H B h p) +reflect-weakeningᴼ H (just function f ⟨ var x ∈ T ⟩∈ U is B end) h (function₁ W) = function₁ (reflect-weakeningᴮ (x ↦ T) H B h W) -reflectᴱ : ∀ H M {H′ M′} → (H ⊢ M ⟶ᴱ M′ ⊣ H′) → Warningᴱ H′ (typeCheckᴱ H′ ∅ M′) → Warningᴴᴱ H (typeCheckᴴᴱ H ∅ M) -reflectᴮ : ∀ H B {H′ B′} → (H ⊢ B ⟶ᴮ B′ ⊣ H′) → Warningᴮ H′ (typeCheckᴮ H′ ∅ B′) → Warningᴴᴮ H (typeCheckᴴᴮ H ∅ B) +reflectᴱ : ∀ H M {H′ M′} → (H ⊢ M ⟶ᴱ M′ ⊣ H′) → Warningᴱ H′ (typeCheckᴱ H′ ∅ M′) → Either (Warningᴱ H (typeCheckᴱ H ∅ M)) (Warningᴴ H (typeCheckᴴ H)) +reflectᴮ : ∀ H B {H′ B′} → (H ⊢ B ⟶ᴮ B′ ⊣ H′) → Warningᴮ H′ (typeCheckᴮ H′ ∅ B′) → Either (Warningᴮ H (typeCheckᴮ H ∅ B)) (Warningᴴ H (typeCheckᴴ H)) -reflectᴱ H (M $ N) (app₁ s) (FunctionCallMismatch p) with preservationᴱ H M s | heap-weakeningᴱ H N (rednᴱ⊑ s) -reflectᴱ H (M $ N) (app₁ s) (FunctionCallMismatch p) | ok q | ok q′ = expr (FunctionCallMismatch (λ r → p (trans (trans (cong src (sym q)) r) q′))) -reflectᴱ H (M $ N) (app₁ s) (FunctionCallMismatch p) | warning (expr W) | _ = expr (app₁ W) -reflectᴱ H (M $ N) (app₁ s) (FunctionCallMismatch p) | warning (heap W) | _ = heap W -reflectᴱ H (M $ N) (app₁ s) (FunctionCallMismatch p) | _ | warning W = expr (app₂ W) -reflectᴱ H (M $ N) (app₁ s) (app₁ W′) with reflectᴱ H M s W′ -reflectᴱ H (M $ N) (app₁ s) (app₁ W′) | heap W = heap W -reflectᴱ H (M $ N) (app₁ s) (app₁ W′) | expr W = expr (app₁ W) -reflectᴱ H (M $ N) (app₁ s) (app₂ W′) = expr (app₂ (reflect-weakeningᴱ H N (rednᴱ⊑ s) W′)) -reflectᴱ H (M $ N) (app₂ p s) (FunctionCallMismatch p′) with heap-weakeningᴱ H (val p) (rednᴱ⊑ s) | preservationᴱ H N s -reflectᴱ H (M $ N) (app₂ p s) (FunctionCallMismatch p′) | ok q | ok q′ = expr (FunctionCallMismatch (λ r → p′ (trans (trans (cong src (sym q)) r) q′))) -reflectᴱ H (M $ N) (app₂ p s) (FunctionCallMismatch p′) | warning W | _ = expr (app₁ W) -reflectᴱ H (M $ N) (app₂ p s) (FunctionCallMismatch p′) | _ | warning (expr W) = expr (app₂ W) -reflectᴱ H (M $ N) (app₂ p s) (FunctionCallMismatch p′) | _ | warning (heap W) = heap W -reflectᴱ H (M $ N) (app₂ p s) (app₁ W′) = expr (app₁ (reflect-weakeningᴱ H M (rednᴱ⊑ s) W′)) -reflectᴱ H (M $ N) (app₂ p s) (app₂ W′) with reflectᴱ H N s W′ -reflectᴱ H (M $ N) (app₂ p s) (app₂ W′) | heap W = heap W -reflectᴱ H (M $ N) (app₂ p s) (app₂ W′) | expr W = expr (app₂ W) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) with remember (typeOfⱽ H v) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) | (just S , r) with S ≡ᵀ T -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) | (just T , r) | yes refl = heap (addr a p (FunctionDefnMismatch (λ s → q (trans s (substitutivityᴮ H B v x (sym r)))))) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) | (just S , r) | no s = expr (FunctionCallMismatch (λ t → s (trans (cong orNone (sym r)) (trans (sym t) (cong src (cong orNone (cong typeOfᴹᴼ p))))))) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) | (nothing , r) with typeOf-val-not-none v -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) | (nothing , r) | ok s = CONTRADICTION (s (cong orNone (sym r))) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) | (nothing , r) | warning W = expr (app₂ W) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) with remember (typeOfⱽ H v) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | (just S , q) with S ≡ᵀ T -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | (just T , q) | yes refl = heap (addr a p (function₁ (reflect-substitutionᴮ H B v x (sym q) W′))) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | (just S , q) | no r = expr (FunctionCallMismatch (λ s → r (trans (cong orNone (sym q)) (trans (sym s) (cong src (cong orNone (cong typeOfᴹᴼ p))))))) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | (nothing , q) with typeOf-val-not-none v -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | (nothing , q) | ok r = CONTRADICTION (r (cong orNone (sym q))) -reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | (nothing , q) | warning W = expr (app₂ W) -reflectᴱ H (block var b ∈ T is B end) (block s) (BlockMismatch p) with preservationᴮ H B s -reflectᴱ H (block var b ∈ T is B end) (block s) (BlockMismatch p) | ok q = expr (BlockMismatch (λ r → p (trans r q))) -reflectᴱ H (block var b ∈ T is B end) (block s) (BlockMismatch p) | warning (heap W) = heap W -reflectᴱ H (block var b ∈ T is B end) (block s) (BlockMismatch p) | warning (block W) = expr (block₁ W) -reflectᴱ H (block var b ∈ T is B end) (block s) (block₁ W′) with reflectᴮ H B s W′ -reflectᴱ H (block var b ∈ T is B end) (block s) (block₁ W′) | heap W = heap W -reflectᴱ H (block var b ∈ T is B end) (block s) (block₁ W′) | block W = expr (block₁ W) -reflectᴱ H (block var b ∈ T is B end) (return v) W′ = expr (block₁ (return W′)) +reflectᴱ H (M $ N) (app₁ s) (FunctionCallMismatch p) = cond (Left ∘ FunctionCallMismatch ∘ heap-weakeningᴱ ∅ H N (rednᴱ⊑ s) ∘ any-src-≮: p) (Left ∘ app₁) (reflect-subtypingᴱ H M s (src-any-≮: p)) +reflectᴱ H (M $ N) (app₁ s) (app₁ W′) = mapL app₁ (reflectᴱ H M s W′) +reflectᴱ H (M $ N) (app₁ s) (app₂ W′) = Left (app₂ (reflect-weakeningᴱ ∅ H N (rednᴱ⊑ s) W′)) +reflectᴱ H (M $ N) (app₂ p s) (FunctionCallMismatch q) = cond (λ r → Left (FunctionCallMismatch (any-src-≮: r (heap-weakeningᴱ ∅ H M (rednᴱ⊑ s) (src-any-≮: r))))) (Left ∘ app₂) (reflect-subtypingᴱ H N s q) +reflectᴱ H (M $ N) (app₂ p s) (app₁ W′) = Left (app₁ (reflect-weakeningᴱ ∅ H M (rednᴱ⊑ s) W′)) +reflectᴱ H (M $ N) (app₂ p s) (app₂ W′) = mapL app₂ (reflectᴱ H N s W′) +reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) with substitutivityᴮ H B v x q +reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) | Left r = Right (addr a p (FunctionDefnMismatch r)) +reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (BlockMismatch q) | Right r = Left (FunctionCallMismatch (≮:-trans-≡ r ((cong src (cong orAny (cong typeOfᴹᴼ (sym p))))))) +reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) with reflect-substitutionᴮ _ B v x W′ +reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | Left W = Right (addr a p (function₁ W)) +reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | Right (Left W) = Left (app₂ W) +reflectᴱ H (val (addr a) $ N) (beta (function f ⟨ var x ∈ T ⟩∈ U is B end) v refl p) (block₁ W′) | Right (Right q) = Left (FunctionCallMismatch (≮:-trans-≡ q (cong src (cong orAny (cong typeOfᴹᴼ (sym p)))))) +reflectᴱ H (block var b ∈ T is B end) (block s) (BlockMismatch p) = Left (cond BlockMismatch block₁ (reflect-subtypingᴮ H B s p)) +reflectᴱ H (block var b ∈ T is B end) (block s) (block₁ W′) = mapL block₁ (reflectᴮ H B s W′) +reflectᴱ H (block var b ∈ T is B end) (return v) W′ = Left (block₁ (return W′)) reflectᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) (UnallocatedAddress ()) reflectᴱ H (binexp M op N) (binOp₀ ()) (UnallocatedAddress p) -reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₁ p) with preservationᴱ H M s -reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₁ p) | ok q = expr (BinOpMismatch₁ (subst₁ (BinOpWarning op) (sym q) p)) -reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₁ p) | warning (heap W) = heap W -reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₁ p) | warning (expr W) = expr (bin₁ W) -reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₂ p) with heap-weakeningᴱ H N (rednᴱ⊑ s) -reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₂ p) | ok q = expr (BinOpMismatch₂ ((subst₁ (BinOpWarning op) (sym q) p))) -reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₂ p) | warning W = expr (bin₂ W) -reflectᴱ H (binexp M op N) (binOp₁ s) (bin₁ W′) with reflectᴱ H M s W′ -reflectᴱ H (binexp M op N) (binOp₁ s) (bin₁ W′) | heap W = heap W -reflectᴱ H (binexp M op N) (binOp₁ s) (bin₁ W′) | expr W = expr (bin₁ W) -reflectᴱ H (binexp M op N) (binOp₁ s) (bin₂ W′) = expr (bin₂ (reflect-weakeningᴱ H N (rednᴱ⊑ s) W′)) -reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₁ p) with heap-weakeningᴱ H M (rednᴱ⊑ s) -reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₁ p) | ok q = expr (BinOpMismatch₁ (subst₁ (BinOpWarning op) (sym q) p)) -reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₁ p) | warning W = expr (bin₁ W) -reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₂ p) with preservationᴱ H N s -reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₂ p) | ok q = expr (BinOpMismatch₂ (subst₁ (BinOpWarning op) (sym q) p)) -reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₂ p) | warning (heap W) = heap W -reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₂ p) | warning (expr W) = expr (bin₂ W) -reflectᴱ H (binexp M op N) (binOp₂ s) (bin₁ W′) = expr (bin₁ (reflect-weakeningᴱ H M (rednᴱ⊑ s) W′)) -reflectᴱ H (binexp M op N) (binOp₂ s) (bin₂ W′) with reflectᴱ H N s W′ -reflectᴱ H (binexp M op N) (binOp₂ s) (bin₂ W′) | heap W = heap W -reflectᴱ H (binexp M op N) (binOp₂ s) (bin₂ W′) | expr W = expr (bin₂ W) +reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₁ p) = Left (cond BinOpMismatch₁ bin₁ (reflect-subtypingᴱ H M s p)) +reflectᴱ H (binexp M op N) (binOp₁ s) (BinOpMismatch₂ p) = Left (BinOpMismatch₂ (heap-weakeningᴱ ∅ H N (rednᴱ⊑ s) p)) +reflectᴱ H (binexp M op N) (binOp₁ s) (bin₁ W′) = mapL bin₁ (reflectᴱ H M s W′) +reflectᴱ H (binexp M op N) (binOp₁ s) (bin₂ W′) = Left (bin₂ (reflect-weakeningᴱ ∅ H N (rednᴱ⊑ s) W′)) +reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₁ p) = Left (BinOpMismatch₁ (heap-weakeningᴱ ∅ H M (rednᴱ⊑ s) p)) +reflectᴱ H (binexp M op N) (binOp₂ s) (BinOpMismatch₂ p) = Left (cond BinOpMismatch₂ bin₂ (reflect-subtypingᴱ H N s p)) +reflectᴱ H (binexp M op N) (binOp₂ s) (bin₁ W′) = Left (bin₁ (reflect-weakeningᴱ ∅ H M (rednᴱ⊑ s) W′)) +reflectᴱ H (binexp M op N) (binOp₂ s) (bin₂ W′) = mapL bin₂ (reflectᴱ H N s W′) -reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (LocalVarMismatch p) with preservationᴱ H M s -reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (LocalVarMismatch p) | ok q = block (LocalVarMismatch (λ r → p (trans r q))) -reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (LocalVarMismatch p) | warning (expr W) = block (local₁ W) -reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (LocalVarMismatch p) | warning (heap W) = heap W -reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (local₁ W′) with reflectᴱ H M s W′ -reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (local₁ W′) | heap W = heap W -reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (local₁ W′) | expr W = block (local₁ W) -reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (local₂ W′) = block (local₂ (reflect-weakeningᴮ H B (rednᴱ⊑ s) W′)) -reflectᴮ H (local var x ∈ T ← M ∙ B) (subst v) W′ with remember (typeOfⱽ H v) -reflectᴮ H (local var x ∈ T ← M ∙ B) (subst v) W′ | (just S , p) with S ≡ᵀ T -reflectᴮ H (local var x ∈ T ← M ∙ B) (subst v) W′ | (just T , p) | yes refl = block (local₂ (reflect-substitutionᴮ H B v x (sym p) W′)) -reflectᴮ H (local var x ∈ T ← M ∙ B) (subst v) W′ | (just S , p) | no q = block (LocalVarMismatch (λ r → q (trans (cong orNone (sym p)) (sym r)))) -reflectᴮ H (local var x ∈ T ← M ∙ B) (subst v) W′ | (nothing , p) with typeOf-val-not-none v -reflectᴮ H (local var x ∈ T ← M ∙ B) (subst v) W′ | (nothing , p) | ok r = CONTRADICTION (r (cong orNone (sym p))) -reflectᴮ H (local var x ∈ T ← M ∙ B) (subst v) W′ | (nothing , p) | warning W = block (local₁ W) -reflectᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) W′ = block (function₂ (reflect-weakeningᴮ H B (snoc defn) (reflect-substitutionᴮ _ B (addr a) f refl W′))) -reflectᴮ H (return M ∙ B) (return s) (return W′) with reflectᴱ H M s W′ -reflectᴮ H (return M ∙ B) (return s) (return W′) | heap W = heap W -reflectᴮ H (return M ∙ B) (return s) (return W′) | expr W = block (return W) +reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (LocalVarMismatch p) = Left (cond LocalVarMismatch local₁ (reflect-subtypingᴱ H M s p)) +reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (local₁ W′) = mapL local₁ (reflectᴱ H M s W′) +reflectᴮ H (local var x ∈ T ← M ∙ B) (local s) (local₂ W′) = Left (local₂ (reflect-weakeningᴮ (x ↦ T) H B (rednᴱ⊑ s) W′)) +reflectᴮ H (local var x ∈ T ← M ∙ B) (subst v) W′ = Left (cond local₂ (cond local₁ LocalVarMismatch) (reflect-substitutionᴮ H B v x W′)) +reflectᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) W′ with reflect-substitutionᴮ _ B (addr a) f W′ +reflectᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) W′ | Left W = Left (function₂ (reflect-weakeningᴮ (f ↦ (T ⇒ U)) H B (snoc defn) W)) +reflectᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) W′ | Right (Left (UnallocatedAddress ())) +reflectᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) W′ | Right (Right p) = CONTRADICTION (≮:-refl p) +reflectᴮ H (return M ∙ B) (return s) (return W′) = mapL return (reflectᴱ H M s W′) -reflectᴴᴱ : ∀ H M {H′ M′} → (H ⊢ M ⟶ᴱ M′ ⊣ H′) → Warningᴴᴱ H′ (typeCheckᴴᴱ H′ ∅ M′) → Warningᴴᴱ H (typeCheckᴴᴱ H ∅ M) -reflectᴴᴮ : ∀ H B {H′ B′} → (H ⊢ B ⟶ᴮ B′ ⊣ H′) → Warningᴴᴮ H′ (typeCheckᴴᴮ H′ ∅ B′) → Warningᴴᴮ H (typeCheckᴴᴮ H ∅ B) +reflectᴴᴱ : ∀ H M {H′ M′} → (H ⊢ M ⟶ᴱ M′ ⊣ H′) → Warningᴴ H′ (typeCheckᴴ H′) → Either (Warningᴱ H (typeCheckᴱ H ∅ M)) (Warningᴴ H (typeCheckᴴ H)) +reflectᴴᴮ : ∀ H B {H′ B′} → (H ⊢ B ⟶ᴮ B′ ⊣ H′) → Warningᴴ H′ (typeCheckᴴ H′) → Either (Warningᴮ H (typeCheckᴮ H ∅ B)) (Warningᴴ H (typeCheckᴴ H)) -reflectᴴᴱ H M s (expr W′) = reflectᴱ H M s W′ -reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a p) (heap (addr b refl W′)) with b ≡ᴬ a -reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) (heap (addr a refl (FunctionDefnMismatch p))) | yes refl with heap-weakeningᴮ H B (snoc defn) -reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) (heap (addr a refl (FunctionDefnMismatch p))) | yes refl | ok r = expr (FunctionDefnMismatch λ q → p (trans q r)) -reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) (heap (addr a refl (FunctionDefnMismatch p))) | yes refl | warning W = expr (function₁ W) -reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) (heap (addr a refl (function₁ W′))) | yes refl = expr (function₁ (reflect-weakeningᴮ H B (snoc defn) W′)) -reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a p) (heap (addr b refl W′)) | no r = heap (addr b (lookup-not-allocated p r) (reflect-weakeningᴼ H _ (snoc p) W′)) -reflectᴴᴱ H (M $ N) (app₁ s) (heap W′) with reflectᴴᴱ H M s (heap W′) -reflectᴴᴱ H (M $ N) (app₁ s) (heap W′) | heap W = heap W -reflectᴴᴱ H (M $ N) (app₁ s) (heap W′) | expr W = expr (app₁ W) -reflectᴴᴱ H (M $ N) (app₂ p s) (heap W′) with reflectᴴᴱ H N s (heap W′) -reflectᴴᴱ H (M $ N) (app₂ p s) (heap W′) | heap W = heap W -reflectᴴᴱ H (M $ N) (app₂ p s) (heap W′) | expr W = expr (app₂ W) -reflectᴴᴱ H (M $ N) (beta O v p q) (heap W′) = heap W′ -reflectᴴᴱ H (block var b ∈ T is B end) (block s) (heap W′) with reflectᴴᴮ H B s (heap W′) -reflectᴴᴱ H (block var b ∈ T is B end) (block s) (heap W′) | heap W = heap W -reflectᴴᴱ H (block var b ∈ T is B end) (block s) (heap W′) | block W = expr (block₁ W) -reflectᴴᴱ H (block var b ∈ T is return N ∙ B end) (return v) (heap W′) = heap W′ -reflectᴴᴱ H (block var b ∈ T is done end) done (heap W′) = heap W′ -reflectᴴᴱ H (binexp M op N) (binOp₀ s) (heap W′) = heap W′ -reflectᴴᴱ H (binexp M op N) (binOp₁ s) (heap W′) with reflectᴴᴱ H M s (heap W′) -reflectᴴᴱ H (binexp M op N) (binOp₁ s) (heap W′) | heap W = heap W -reflectᴴᴱ H (binexp M op N) (binOp₁ s) (heap W′) | expr W = expr (bin₁ W) -reflectᴴᴱ H (binexp M op N) (binOp₂ s) (heap W′) with reflectᴴᴱ H N s (heap W′) -reflectᴴᴱ H (binexp M op N) (binOp₂ s) (heap W′) | heap W = heap W -reflectᴴᴱ H (binexp M op N) (binOp₂ s) (heap W′) | expr W = expr (bin₂ W) +reflectᴴᴱ H (M $ N) (app₁ s) W = mapL app₁ (reflectᴴᴱ H M s W) +reflectᴴᴱ H (M $ N) (app₂ v s) W = mapL app₂ (reflectᴴᴱ H N s W) +reflectᴴᴱ H (M $ N) (beta O v refl p) W = Right W +reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a p) (addr b refl W) with b ≡ᴬ a +reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) (addr b refl (FunctionDefnMismatch p)) | yes refl = Left (FunctionDefnMismatch (heap-weakeningᴮ (x ↦ T) H B (snoc defn) p)) +reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a defn) (addr b refl (function₁ W)) | yes refl = Left (function₁ (reflect-weakeningᴮ (x ↦ T) H B (snoc defn) W)) +reflectᴴᴱ H (function f ⟨ var x ∈ T ⟩∈ U is B end) (function a p) (addr b refl W) | no q = Right (addr b (lookup-not-allocated p q) (reflect-weakeningᴼ H _ (snoc p) W)) +reflectᴴᴱ H (block var b ∈ T is B end) (block s) W = mapL block₁ (reflectᴴᴮ H B s W) +reflectᴴᴱ H (block var b ∈ T is return (val v) ∙ B end) (return v) W = Right W +reflectᴴᴱ H (block var b ∈ T is done end) done W = Right W +reflectᴴᴱ H (binexp M op N) (binOp₀ s) W = Right W +reflectᴴᴱ H (binexp M op N) (binOp₁ s) W = mapL bin₁ (reflectᴴᴱ H M s W) +reflectᴴᴱ H (binexp M op N) (binOp₂ s) W = mapL bin₂ (reflectᴴᴱ H N s W) -reflectᴴᴮ H B s (block W′) = reflectᴮ H B s W′ -reflectᴴᴮ H (local var x ∈ T ← M ∙ B) (local s) (heap W′) with reflectᴴᴱ H M s (heap W′) -reflectᴴᴮ H (local var x ∈ T ← M ∙ B) (local s) (heap W′) | heap W = heap W -reflectᴴᴮ H (local var x ∈ T ← M ∙ B) (local s) (heap W′) | expr W = block (local₁ W) -reflectᴴᴮ H (local var x ∈ T ← M ∙ B) (subst v) (heap W′) = heap W′ -reflectᴴᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a p) (heap (addr b refl W′)) with b ≡ᴬ a -reflectᴴᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) (heap (addr a refl (FunctionDefnMismatch p))) | yes refl with heap-weakeningᴮ H C (snoc defn) -reflectᴴᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) (heap (addr a refl (FunctionDefnMismatch p))) | yes refl | ok r = block (FunctionDefnMismatch (λ q → p (trans q r))) -reflectᴴᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) (heap (addr a refl (FunctionDefnMismatch p))) | yes refl | warning W = block (function₁ W) -reflectᴴᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a defn) (heap (addr a refl (function₁ W′))) | yes refl = block (function₁ (reflect-weakeningᴮ H C (snoc defn) W′)) -reflectᴴᴮ H (function f ⟨ var y ∈ T ⟩∈ U is C end ∙ B) (function a p) (heap (addr b refl W′)) | no r = heap (addr b (lookup-not-allocated p r) (reflect-weakeningᴼ H _ (snoc p) W′)) -reflectᴴᴮ H (return M ∙ B) (return s) (heap W′) with reflectᴴᴱ H M s (heap W′) -reflectᴴᴮ H (return M ∙ B) (return s) (heap W′) | heap W = heap W -reflectᴴᴮ H (return M ∙ B) (return s) (heap W′) | expr W = block (return W) +reflectᴴᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function a p) (addr b refl W) with b ≡ᴬ a +reflectᴴᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function a defn) (addr b refl (FunctionDefnMismatch p)) | yes refl = Left (FunctionDefnMismatch (heap-weakeningᴮ (x ↦ T) H C (snoc defn) p)) +reflectᴴᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function a defn) (addr b refl (function₁ W)) | yes refl = Left (function₁ (reflect-weakeningᴮ (x ↦ T) H C (snoc defn) W)) +reflectᴴᴮ H (function f ⟨ var x ∈ T ⟩∈ U is C end ∙ B) (function a p) (addr b refl W) | no q = Right (addr b (lookup-not-allocated p q) (reflect-weakeningᴼ H _ (snoc p) W)) +reflectᴴᴮ H (local var x ∈ T ← M ∙ B) (local s) W = mapL local₁ (reflectᴴᴱ H M s W) +reflectᴴᴮ H (local var x ∈ T ← M ∙ B) (subst v) W = Right W +reflectᴴᴮ H (return M ∙ B) (return s) W = mapL return (reflectᴴᴱ H M s W) -reflect* : ∀ H B {H′ B′} → (H ⊢ B ⟶* B′ ⊣ H′) → Warningᴴᴮ H′ (typeCheckᴴᴮ H′ ∅ B′) → Warningᴴᴮ H (typeCheckᴴᴮ H ∅ B) +reflect* : ∀ H B {H′ B′} → (H ⊢ B ⟶* B′ ⊣ H′) → Either (Warningᴮ H′ (typeCheckᴮ H′ ∅ B′)) (Warningᴴ H′ (typeCheckᴴ H′)) → Either (Warningᴮ H (typeCheckᴮ H ∅ B)) (Warningᴴ H (typeCheckᴴ H)) reflect* H B refl W = W -reflect* H B (step s t) W = reflectᴴᴮ H B s (reflect* _ _ t W) +reflect* H B (step s t) W = cond (reflectᴮ H B s) (reflectᴴᴮ H B s) (reflect* _ _ t W) -runtimeBinOpWarning : ∀ H {op} v → BinOpError op (valueType v) → BinOpWarning op (orNone (typeOfⱽ H v)) -runtimeBinOpWarning H v (+ p) = + (λ q → p (mustBeNumber H ∅ v q)) -runtimeBinOpWarning H v (- p) = - (λ q → p (mustBeNumber H ∅ v q)) -runtimeBinOpWarning H v (* p) = * (λ q → p (mustBeNumber H ∅ v q)) -runtimeBinOpWarning H v (/ p) = / (λ q → p (mustBeNumber H ∅ v q)) -runtimeBinOpWarning H v (< p) = < (λ q → p (mustBeNumber H ∅ v q)) -runtimeBinOpWarning H v (> p) = > (λ q → p (mustBeNumber H ∅ v q)) -runtimeBinOpWarning H v (<= p) = <= (λ q → p (mustBeNumber H ∅ v q)) -runtimeBinOpWarning H v (>= p) = >= (λ q → p (mustBeNumber H ∅ v q)) -runtimeBinOpWarning H v (·· p) = ·· (λ q → p (mustBeString H ∅ v q)) +isntNumber : ∀ H v → (valueType v ≢ number) → (typeOfᴱ H ∅ (val v) ≮: number) +isntNumber H nil p = scalar-≢-impl-≮: nil number (λ ()) +isntNumber H (addr a) p with remember (H [ a ]ᴴ) +isntNumber H (addr a) p | (just (function f ⟨ var x ∈ T ⟩∈ U is B end) , q) = ≡-trans-≮: (cong orAny (cong typeOfᴹᴼ q)) (function-≮:-scalar number) +isntNumber H (addr a) p | (nothing , q) = ≡-trans-≮: (cong orAny (cong typeOfᴹᴼ q)) (any-≮:-scalar number) +isntNumber H (number x) p = CONTRADICTION (p refl) +isntNumber H (bool x) p = scalar-≢-impl-≮: boolean number (λ ()) +isntNumber H (string x) p = scalar-≢-impl-≮: string number (λ ()) + +isntString : ∀ H v → (valueType v ≢ string) → (typeOfᴱ H ∅ (val v) ≮: string) +isntString H nil p = scalar-≢-impl-≮: nil string (λ ()) +isntString H (addr a) p with remember (H [ a ]ᴴ) +isntString H (addr a) p | (just (function f ⟨ var x ∈ T ⟩∈ U is B end) , q) = ≡-trans-≮: (cong orAny (cong typeOfᴹᴼ q)) (function-≮:-scalar string) +isntString H (addr a) p | (nothing , q) = ≡-trans-≮: (cong orAny (cong typeOfᴹᴼ q)) (any-≮:-scalar string) +isntString H (number x) p = scalar-≢-impl-≮: number string (λ ()) +isntString H (bool x) p = scalar-≢-impl-≮: boolean string (λ ()) +isntString H (string x) p = CONTRADICTION (p refl) + +isntFunction : ∀ H v {T U} → (valueType v ≢ function) → (typeOfᴱ H ∅ (val v) ≮: (T ⇒ U)) +isntFunction H nil p = scalar-≮:-function nil +isntFunction H (addr a) p = CONTRADICTION (p refl) +isntFunction H (number x) p = scalar-≮:-function number +isntFunction H (bool x) p = scalar-≮:-function boolean +isntFunction H (string x) p = scalar-≮:-function string + +isntEmpty : ∀ H v → (typeOfᴱ H ∅ (val v) ≮: none) +isntEmpty H nil = scalar-≮:-none nil +isntEmpty H (addr a) with remember (H [ a ]ᴴ) +isntEmpty H (addr a) | (just (function f ⟨ var x ∈ T ⟩∈ U is B end) , p) = ≡-trans-≮: (cong orAny (cong typeOfᴹᴼ p)) function-≮:-none +isntEmpty H (addr a) | (nothing , p) = ≡-trans-≮: (cong orAny (cong typeOfᴹᴼ p)) any-≮:-none +isntEmpty H (number x) = scalar-≮:-none number +isntEmpty H (bool x) = scalar-≮:-none boolean +isntEmpty H (string x) = scalar-≮:-none string + +runtimeBinOpWarning : ∀ H {op} v → BinOpError op (valueType v) → (typeOfᴱ H ∅ (val v) ≮: srcBinOp op) +runtimeBinOpWarning H v (+ p) = isntNumber H v p +runtimeBinOpWarning H v (- p) = isntNumber H v p +runtimeBinOpWarning H v (* p) = isntNumber H v p +runtimeBinOpWarning H v (/ p) = isntNumber H v p +runtimeBinOpWarning H v (< p) = isntNumber H v p +runtimeBinOpWarning H v (> p) = isntNumber H v p +runtimeBinOpWarning H v (<= p) = isntNumber H v p +runtimeBinOpWarning H v (>= p) = isntNumber H v p +runtimeBinOpWarning H v (·· p) = isntString H v p runtimeWarningᴱ : ∀ H M → RuntimeErrorᴱ H M → Warningᴱ H (typeCheckᴱ H ∅ M) runtimeWarningᴮ : ∀ H B → RuntimeErrorᴮ H B → Warningᴮ H (typeCheckᴮ H ∅ B) runtimeWarningᴱ H (var x) UnboundVariable = UnboundVariable refl runtimeWarningᴱ H (val (addr a)) (SEGV p) = UnallocatedAddress p -runtimeWarningᴱ H (M $ N) (FunctionMismatch v w p) with typeOf-val-not-none w -runtimeWarningᴱ H (M $ N) (FunctionMismatch v w p) | ok q = FunctionCallMismatch (λ r → p (mustBeFunction H ∅ v (λ r′ → q (trans r′ r)))) -runtimeWarningᴱ H (M $ N) (FunctionMismatch v w p) | warning W = app₂ W +runtimeWarningᴱ H (M $ N) (FunctionMismatch v w p) = FunctionCallMismatch (any-src-≮: (isntEmpty H w) (isntFunction H v p)) runtimeWarningᴱ H (M $ N) (app₁ err) = app₁ (runtimeWarningᴱ H M err) runtimeWarningᴱ H (M $ N) (app₂ err) = app₂ (runtimeWarningᴱ H N err) runtimeWarningᴱ H (block var b ∈ T is B end) (block err) = block₁ (runtimeWarningᴮ H B err) @@ -469,6 +343,6 @@ runtimeWarningᴮ H (local var x ∈ T ← M ∙ B) (local err) = local₁ (runt runtimeWarningᴮ H (return M ∙ B) (return err) = return (runtimeWarningᴱ H M err) wellTypedProgramsDontGoWrong : ∀ H′ B B′ → (∅ᴴ ⊢ B ⟶* B′ ⊣ H′) → (RuntimeErrorᴮ H′ B′) → Warningᴮ ∅ᴴ (typeCheckᴮ ∅ᴴ ∅ B) -wellTypedProgramsDontGoWrong H′ B B′ t err with reflect* ∅ᴴ B t (block (runtimeWarningᴮ H′ B′ err)) -wellTypedProgramsDontGoWrong H′ B B′ t err | heap (addr a refl ()) -wellTypedProgramsDontGoWrong H′ B B′ t err | block W = W +wellTypedProgramsDontGoWrong H′ B B′ t err with reflect* ∅ᴴ B t (Left (runtimeWarningᴮ H′ B′ err)) +wellTypedProgramsDontGoWrong H′ B B′ t err | Right (addr a refl ()) +wellTypedProgramsDontGoWrong H′ B B′ t err | Left W = W diff --git a/prototyping/Properties/Subtyping.agda b/prototyping/Properties/Subtyping.agda new file mode 100644 index 0000000..6a0b420 --- /dev/null +++ b/prototyping/Properties/Subtyping.agda @@ -0,0 +1,221 @@ +{-# OPTIONS --rewriting #-} + +module Properties.Subtyping where + +open import Agda.Builtin.Equality using (_≡_; refl) +open import FFI.Data.Either using (Either; Left; Right; mapLR; swapLR; cond) +open import Luau.Subtyping using (_<:_; _≮:_; Tree; Language; ¬Language; witness; any; none; scalar; function; scalar-function; scalar-function-ok; scalar-function-err; scalar-scalar; function-scalar; function-ok; function-err; left; right; _,_) +open import Luau.Type using (Type; Scalar; strict; nil; number; string; boolean; none; any; _⇒_; _∪_; _∩_; tgt) +open import Properties.Contradiction using (CONTRADICTION; ¬) +open import Properties.Equality using (_≢_) +open import Properties.Functions using (_∘_) + +src = Luau.Type.src strict + +-- Language membership is decidable +dec-language : ∀ T t → Either (¬Language T t) (Language T t) +dec-language nil (scalar number) = Left (scalar-scalar number nil (λ ())) +dec-language nil (scalar boolean) = Left (scalar-scalar boolean nil (λ ())) +dec-language nil (scalar string) = Left (scalar-scalar string nil (λ ())) +dec-language nil (scalar nil) = Right (scalar nil) +dec-language nil function = Left (scalar-function nil) +dec-language nil (function-ok t) = Left (scalar-function-ok nil) +dec-language nil (function-err t) = Right (scalar-function-err nil) +dec-language boolean (scalar number) = Left (scalar-scalar number boolean (λ ())) +dec-language boolean (scalar boolean) = Right (scalar boolean) +dec-language boolean (scalar string) = Left (scalar-scalar string boolean (λ ())) +dec-language boolean (scalar nil) = Left (scalar-scalar nil boolean (λ ())) +dec-language boolean function = Left (scalar-function boolean) +dec-language boolean (function-ok t) = Left (scalar-function-ok boolean) +dec-language boolean (function-err t) = Right (scalar-function-err boolean) +dec-language number (scalar number) = Right (scalar number) +dec-language number (scalar boolean) = Left (scalar-scalar boolean number (λ ())) +dec-language number (scalar string) = Left (scalar-scalar string number (λ ())) +dec-language number (scalar nil) = Left (scalar-scalar nil number (λ ())) +dec-language number function = Left (scalar-function number) +dec-language number (function-ok t) = Left (scalar-function-ok number) +dec-language number (function-err t) = Right (scalar-function-err number) +dec-language string (scalar number) = Left (scalar-scalar number string (λ ())) +dec-language string (scalar boolean) = Left (scalar-scalar boolean string (λ ())) +dec-language string (scalar string) = Right (scalar string) +dec-language string (scalar nil) = Left (scalar-scalar nil string (λ ())) +dec-language string function = Left (scalar-function string) +dec-language string (function-ok t) = Left (scalar-function-ok string) +dec-language string (function-err t) = Right (scalar-function-err string) +dec-language (T₁ ⇒ T₂) (scalar s) = Left (function-scalar s) +dec-language (T₁ ⇒ T₂) function = Right function +dec-language (T₁ ⇒ T₂) (function-ok t) = mapLR function-ok function-ok (dec-language T₂ t) +dec-language (T₁ ⇒ T₂) (function-err t) = mapLR function-err function-err (swapLR (dec-language T₁ t)) +dec-language none t = Left none +dec-language any t = Right any +dec-language (T₁ ∪ T₂) t = cond (λ p → cond (Left ∘ _,_ p) (Right ∘ right) (dec-language T₂ t)) (Right ∘ left) (dec-language T₁ t) +dec-language (T₁ ∩ T₂) t = cond (Left ∘ left) (λ p → cond (Left ∘ right) (Right ∘ _,_ p) (dec-language T₂ t)) (dec-language T₁ t) + +-- ¬Language T is the complement of Language T +language-comp : ∀ {T} t → ¬Language T t → ¬(Language T t) +language-comp t (p₁ , p₂) (left q) = language-comp t p₁ q +language-comp t (p₁ , p₂) (right q) = language-comp t p₂ q +language-comp t (left p) (q₁ , q₂) = language-comp t p q₁ +language-comp t (right p) (q₁ , q₂) = language-comp t p q₂ +language-comp (scalar s) (scalar-scalar s p₁ p₂) (scalar s) = p₂ refl +language-comp (scalar s) (function-scalar s) (scalar s) = language-comp function (scalar-function s) function +language-comp (scalar s) none (scalar ()) +language-comp function (scalar-function ()) function +language-comp (function-ok t) (scalar-function-ok ()) (function-ok q) +language-comp (function-ok t) (function-ok p) (function-ok q) = language-comp t p q +language-comp (function-err t) (function-err p) (function-err q) = language-comp t q p + +-- ≮: is the complement of <: +¬≮:-impl-<: : ∀ {T U} → ¬(T ≮: U) → (T <: U) +¬≮:-impl-<: {T} {U} p t q with dec-language U t +¬≮:-impl-<: {T} {U} p t q | Left r = CONTRADICTION (p (witness t q r)) +¬≮:-impl-<: {T} {U} p t q | Right r = r + +<:-impl-¬≮: : ∀ {T U} → (T <: U) → ¬(T ≮: U) +<:-impl-¬≮: p (witness t q r) = language-comp t r (p t q) + +-- reflexivity +≮:-refl : ∀ {T} → ¬(T ≮: T) +≮:-refl (witness t p q) = language-comp t q p + +<:-refl : ∀ {T} → (T <: T) +<:-refl = ¬≮:-impl-<: ≮:-refl + +-- transititivity +≮:-trans-≡ : ∀ {S T U} → (S ≮: T) → (T ≡ U) → (S ≮: U) +≮:-trans-≡ p refl = p + +≡-trans-≮: : ∀ {S T U} → (S ≡ T) → (T ≮: U) → (S ≮: U) +≡-trans-≮: refl p = p + +≮:-trans : ∀ {S T U} → (S ≮: U) → Either (S ≮: T) (T ≮: U) +≮:-trans {T = T} (witness t p q) = mapLR (witness t p) (λ z → witness t z q) (dec-language T t) + +<:-trans : ∀ {S T U} → (S <: T) → (T <: U) → (S <: U) +<:-trans p q = ¬≮:-impl-<: (cond (<:-impl-¬≮: p) (<:-impl-¬≮: q) ∘ ≮:-trans) + +-- Properties of scalars +skalar = number ∪ (string ∪ (nil ∪ boolean)) + +function-≮:-scalar : ∀ {S T U} → (Scalar U) → ((S ⇒ T) ≮: U) +function-≮:-scalar s = witness function function (scalar-function s) + +scalar-≮:-function : ∀ {S T U} → (Scalar U) → (U ≮: (S ⇒ T)) +scalar-≮:-function s = witness (scalar s) (scalar s) (function-scalar s) + +any-≮:-scalar : ∀ {U} → (Scalar U) → (any ≮: U) +any-≮:-scalar s = witness (function-ok (scalar s)) any (scalar-function-ok s) + +scalar-≮:-none : ∀ {U} → (Scalar U) → (U ≮: none) +scalar-≮:-none s = witness (scalar s) (scalar s) none + +scalar-≢-impl-≮: : ∀ {T U} → (Scalar T) → (Scalar U) → (T ≢ U) → (T ≮: U) +scalar-≢-impl-≮: s₁ s₂ p = witness (scalar s₁) (scalar s₁) (scalar-scalar s₁ s₂ p) + +-- Properties of tgt +tgt-function-ok : ∀ {T t} → (Language (tgt T) t) → Language T (function-ok t) +tgt-function-ok {T = nil} (scalar ()) +tgt-function-ok {T = T₁ ⇒ T₂} p = function-ok p +tgt-function-ok {T = none} (scalar ()) +tgt-function-ok {T = any} p = any +tgt-function-ok {T = boolean} (scalar ()) +tgt-function-ok {T = number} (scalar ()) +tgt-function-ok {T = string} (scalar ()) +tgt-function-ok {T = T₁ ∪ T₂} (left p) = left (tgt-function-ok p) +tgt-function-ok {T = T₁ ∪ T₂} (right p) = right (tgt-function-ok p) +tgt-function-ok {T = T₁ ∩ T₂} (p₁ , p₂) = (tgt-function-ok p₁ , tgt-function-ok p₂) + +function-ok-tgt : ∀ {T t} → Language T (function-ok t) → (Language (tgt T) t) +function-ok-tgt (function-ok p) = p +function-ok-tgt (left p) = left (function-ok-tgt p) +function-ok-tgt (right p) = right (function-ok-tgt p) +function-ok-tgt (p₁ , p₂) = (function-ok-tgt p₁ , function-ok-tgt p₂) +function-ok-tgt any = any + +skalar-function-ok : ∀ {t} → (¬Language skalar (function-ok t)) +skalar-function-ok = (scalar-function-ok number , (scalar-function-ok string , (scalar-function-ok nil , scalar-function-ok boolean))) + +skalar-scalar : ∀ {T} (s : Scalar T) → (Language skalar (scalar s)) +skalar-scalar number = left (scalar number) +skalar-scalar boolean = right (right (right (scalar boolean))) +skalar-scalar string = right (left (scalar string)) +skalar-scalar nil = right (right (left (scalar nil))) + +tgt-none-≮: : ∀ {T U} → (tgt T ≮: U) → (T ≮: (skalar ∪ (none ⇒ U))) +tgt-none-≮: (witness t p q) = witness (function-ok t) (tgt-function-ok p) (skalar-function-ok , function-ok q) + +none-tgt-≮: : ∀ {T U} → (T ≮: (skalar ∪ (none ⇒ U))) → (tgt T ≮: U) +none-tgt-≮: (witness (scalar s) p (q₁ , q₂)) = CONTRADICTION (≮:-refl (witness (scalar s) (skalar-scalar s) q₁)) +none-tgt-≮: (witness function p (q₁ , scalar-function ())) +none-tgt-≮: (witness (function-ok t) p (q₁ , function-ok q₂)) = witness t (function-ok-tgt p) q₂ +none-tgt-≮: (witness (function-err (scalar s)) p (q₁ , function-err (scalar ()))) + +-- Properties of src +function-err-src : ∀ {T t} → (¬Language (src T) t) → Language T (function-err t) +function-err-src {T = nil} none = scalar-function-err nil +function-err-src {T = T₁ ⇒ T₂} p = function-err p +function-err-src {T = none} (scalar-scalar number () p) +function-err-src {T = none} (scalar-function-ok ()) +function-err-src {T = any} none = any +function-err-src {T = boolean} p = scalar-function-err boolean +function-err-src {T = number} p = scalar-function-err number +function-err-src {T = string} p = scalar-function-err string +function-err-src {T = T₁ ∪ T₂} (left p) = left (function-err-src p) +function-err-src {T = T₁ ∪ T₂} (right p) = right (function-err-src p) +function-err-src {T = T₁ ∩ T₂} (p₁ , p₂) = function-err-src p₁ , function-err-src p₂ + +¬function-err-src : ∀ {T t} → (Language (src T) t) → ¬Language T (function-err t) +¬function-err-src {T = nil} (scalar ()) +¬function-err-src {T = T₁ ⇒ T₂} p = function-err p +¬function-err-src {T = none} any = none +¬function-err-src {T = any} (scalar ()) +¬function-err-src {T = boolean} (scalar ()) +¬function-err-src {T = number} (scalar ()) +¬function-err-src {T = string} (scalar ()) +¬function-err-src {T = T₁ ∪ T₂} (p₁ , p₂) = (¬function-err-src p₁ , ¬function-err-src p₂) +¬function-err-src {T = T₁ ∩ T₂} (left p) = left (¬function-err-src p) +¬function-err-src {T = T₁ ∩ T₂} (right p) = right (¬function-err-src p) + +src-¬function-err : ∀ {T t} → Language T (function-err t) → (¬Language (src T) t) +src-¬function-err {T = nil} p = none +src-¬function-err {T = T₁ ⇒ T₂} (function-err p) = p +src-¬function-err {T = none} (scalar-function-err ()) +src-¬function-err {T = any} p = none +src-¬function-err {T = boolean} p = none +src-¬function-err {T = number} p = none +src-¬function-err {T = string} p = none +src-¬function-err {T = T₁ ∪ T₂} (left p) = left (src-¬function-err p) +src-¬function-err {T = T₁ ∪ T₂} (right p) = right (src-¬function-err p) +src-¬function-err {T = T₁ ∩ T₂} (p₁ , p₂) = (src-¬function-err p₁ , src-¬function-err p₂) + +src-¬scalar : ∀ {S T t} (s : Scalar S) → Language T (scalar s) → (¬Language (src T) t) +src-¬scalar number (scalar number) = none +src-¬scalar boolean (scalar boolean) = none +src-¬scalar string (scalar string) = none +src-¬scalar nil (scalar nil) = none +src-¬scalar s (left p) = left (src-¬scalar s p) +src-¬scalar s (right p) = right (src-¬scalar s p) +src-¬scalar s (p₁ , p₂) = (src-¬scalar s p₁ , src-¬scalar s p₂) +src-¬scalar s any = none + +src-any-≮: : ∀ {T U} → (T ≮: src U) → (U ≮: (T ⇒ any)) +src-any-≮: (witness t p q) = witness (function-err t) (function-err-src q) (¬function-err-src p) + +any-src-≮: : ∀ {S T U} → (U ≮: S) → (T ≮: (U ⇒ any)) → (U ≮: src T) +any-src-≮: (witness t x x₁) (witness (scalar s) p (function-scalar s)) = witness t x (src-¬scalar s p) +any-src-≮: r (witness (function-ok (scalar s)) p (function-ok (scalar-scalar s () q))) +any-src-≮: r (witness (function-ok (function-ok _)) p (function-ok (scalar-function-ok ()))) +any-src-≮: r (witness (function-err t) p (function-err q)) = witness t q (src-¬function-err p) + +-- Properties of any and none +any-≮: : ∀ {T U} → (T ≮: U) → (any ≮: U) +any-≮: (witness t p q) = witness t any q + +none-≮: : ∀ {T U} → (T ≮: U) → (T ≮: none) +none-≮: (witness t p q) = witness t p none + +any-≮:-none : (any ≮: none) +any-≮:-none = witness (scalar nil) any none + +function-≮:-none : ∀ {T U} → ((T ⇒ U) ≮: none) +function-≮:-none = witness function function none diff --git a/prototyping/Properties/TypeCheck.agda b/prototyping/Properties/TypeCheck.agda index bec6884..ead0c09 100644 --- a/prototyping/Properties/TypeCheck.agda +++ b/prototyping/Properties/TypeCheck.agda @@ -8,7 +8,7 @@ open import Agda.Builtin.Equality using (_≡_; refl) open import Agda.Builtin.Bool using (Bool; true; false) open import FFI.Data.Maybe using (Maybe; just; nothing) open import FFI.Data.Either using (Either) -open import Luau.TypeCheck(m) using (_⊢ᴱ_∈_; _⊢ᴮ_∈_; ⊢ᴼ_; ⊢ᴴ_; _⊢ᴴᴱ_▷_∈_; _⊢ᴴᴮ_▷_∈_; nil; var; addr; number; bool; string; app; function; block; binexp; done; return; local; nothing; orNone; tgtBinOp) +open import Luau.TypeCheck(m) using (_⊢ᴱ_∈_; _⊢ᴮ_∈_; ⊢ᴼ_; ⊢ᴴ_; _⊢ᴴᴱ_▷_∈_; _⊢ᴴᴮ_▷_∈_; nil; var; addr; number; bool; string; app; function; block; binexp; done; return; local; nothing; orAny; tgtBinOp) open import Luau.Syntax using (Block; Expr; Value; BinaryOperator; yes; nil; addr; number; bool; string; val; var; binexp; _$_; function_is_end; block_is_end; _∙_; return; done; local_←_; _⟨_⟩; _⟨_⟩∈_; var_∈_; name; fun; arg; +; -; *; /; <; >; ==; ~=; <=; >=) open import Luau.Type using (Type; nil; any; none; number; boolean; string; _⇒_; tgt) open import Luau.RuntimeType using (RuntimeType; nil; number; function; string; valueType) @@ -42,8 +42,8 @@ typeOfⱽ H (string x) = just string typeOfᴱ : Heap yes → VarCtxt → (Expr yes) → Type typeOfᴮ : Heap yes → VarCtxt → (Block yes) → Type -typeOfᴱ H Γ (var x) = orNone(Γ [ x ]ⱽ) -typeOfᴱ H Γ (val v) = orNone(typeOfⱽ H v) +typeOfᴱ H Γ (var x) = orAny(Γ [ x ]ⱽ) +typeOfᴱ H Γ (val v) = orAny(typeOfⱽ H v) typeOfᴱ H Γ (M $ N) = tgt(typeOfᴱ H Γ M) typeOfᴱ H Γ (function f ⟨ var x ∈ S ⟩∈ T is B end) = S ⇒ T typeOfᴱ H Γ (block var b ∈ T is B end) = T @@ -64,17 +64,17 @@ mustBeFunction H Γ (string x) p = CONTRADICTION (p refl) mustBeNumber : ∀ H Γ v → (typeOfᴱ H Γ (val v) ≡ number) → (valueType(v) ≡ number) mustBeNumber H Γ (addr a) p with remember (H [ a ]ᴴ) -mustBeNumber H Γ (addr a) p | (just O , q) with trans (cong orNone (cong typeOfᴹᴼ (sym q))) p +mustBeNumber H Γ (addr a) p | (just O , q) with trans (cong orAny (cong typeOfᴹᴼ (sym q))) p mustBeNumber H Γ (addr a) p | (just function f ⟨ var x ∈ T ⟩∈ U is B end , q) | () -mustBeNumber H Γ (addr a) p | (nothing , q) with trans (cong orNone (cong typeOfᴹᴼ (sym q))) p +mustBeNumber H Γ (addr a) p | (nothing , q) with trans (cong orAny (cong typeOfᴹᴼ (sym q))) p mustBeNumber H Γ (addr a) p | nothing , q | () mustBeNumber H Γ (number n) p = refl mustBeString : ∀ H Γ v → (typeOfᴱ H Γ (val v) ≡ string) → (valueType(v) ≡ string) mustBeString H Γ (addr a) p with remember (H [ a ]ᴴ) -mustBeString H Γ (addr a) p | (just O , q) with trans (cong orNone (cong typeOfᴹᴼ (sym q))) p +mustBeString H Γ (addr a) p | (just O , q) with trans (cong orAny (cong typeOfᴹᴼ (sym q))) p mustBeString H Γ (addr a) p | (just function f ⟨ var x ∈ T ⟩∈ U is B end , q) | () -mustBeString H Γ (addr a) p | (nothing , q) with trans (cong orNone (cong typeOfᴹᴼ (sym q))) p +mustBeString H Γ (addr a) p | (nothing , q) with trans (cong orAny (cong typeOfᴹᴼ (sym q))) p mustBeString H Γ (addr a) p | (nothing , q) | () mustBeString H Γ (string x) p = refl @@ -83,7 +83,7 @@ typeCheckᴮ : ∀ H Γ B → (Γ ⊢ᴮ B ∈ (typeOfᴮ H Γ B)) typeCheckᴱ H Γ (var x) = var refl typeCheckᴱ H Γ (val nil) = nil -typeCheckᴱ H Γ (val (addr a)) = addr (orNone (typeOfᴹᴼ (H [ a ]ᴴ))) +typeCheckᴱ H Γ (val (addr a)) = addr (orAny (typeOfᴹᴼ (H [ a ]ᴴ))) typeCheckᴱ H Γ (val (number n)) = number typeCheckᴱ H Γ (val (bool b)) = bool typeCheckᴱ H Γ (val (string x)) = string @@ -109,4 +109,4 @@ typeCheckᴴᴱ H Γ M = (typeCheckᴴ H , typeCheckᴱ H Γ M) typeCheckᴴᴮ : ∀ H Γ M → (Γ ⊢ᴴᴮ H ▷ M ∈ typeOfᴮ H Γ M) typeCheckᴴᴮ H Γ M = (typeCheckᴴ H , typeCheckᴮ H Γ M) - \ No newline at end of file + diff --git a/prototyping/Tests/Interpreter/concat_number_and_string/out.txt b/prototyping/Tests/Interpreter/concat_number_and_string/out.txt index a437438..80c9d6e 100644 --- a/prototyping/Tests/Interpreter/concat_number_and_string/out.txt +++ b/prototyping/Tests/Interpreter/concat_number_and_string/out.txt @@ -9,3 +9,4 @@ value 37.0 is not a string TYPE ERROR: Local variable y has type string but expression has type number + because provided type contains v, where v is a number From 2f150796422ba49ed4cd0a12e0e9be230e8a13d8 Mon Sep 17 00:00:00 2001 From: Alexander McCord <11488393+alexmccord@users.noreply.github.com> Date: Wed, 23 Mar 2022 16:53:52 -0700 Subject: [PATCH 3/9] Fold in rationale for making `else` branch mandatory in if-then-else expressions. (#426) This has been discussed but just didn't get incorporated into the RFC. --- rfcs/syntax-if-expression.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/rfcs/syntax-if-expression.md b/rfcs/syntax-if-expression.md index a435fd5..76f76cf 100644 --- a/rfcs/syntax-if-expression.md +++ b/rfcs/syntax-if-expression.md @@ -20,7 +20,7 @@ Instead of `and/or`, `if/else` statement can be used but since that requires a s To solve these problems, we propose introducing a first-class ternary conditional. Instead of `? :` common in C-like languages, we propose an `if-then-else` expression form that is syntactically similar to `if-then-else` statement, but lacks terminating `end`. -Concretely, the `if-then-else` expression must match `if then else `; it can also contain an arbitrary number of `elseif` clauses, like `if then elseif then else `. Note that in either case, `else` is mandatory. +Concretely, the `if-then-else` expression must match `if then else `; it can also contain an arbitrary number of `elseif` clauses, like `if then elseif then else `. Unlike if statements, `else` is mandatory. The result of the expression is the then-expression when condition is truthy (not `nil` or `false`) and else-expression otherwise. Only one of the two possible resulting expressions is evaluated. @@ -30,11 +30,27 @@ Example: local x = if FFlagFoo then A else B MyComponent.validateProps = t.strictInterface({ - layoutOrder = t.optional(t.number), - newThing = if FFlagUseNewThing then t.whatever() else nil, + layoutOrder = t.optional(t.number), + newThing = if FFlagUseNewThing then t.whatever() else nil, }) ``` +Note that `else` is mandatory because it's always better to be explicit. If it weren't mandatory, it opens the possiblity that someone might be writing a chain of if-then-else and forgot to add in the final `else` that _doesn't_ return a `nil` value! Enforcing this syntactically ensures the program does not run. Also, with it being mandatory, it solves many cases where parsing the expression is ambiguous due to the infamous [dangling else](https://en.wikipedia.org/wiki/Dangling_else). + +This example will not do what it looks like it's supposed to do! The if expression will _successfully_ parse and be interpreted as to return `h()` if `g()` evaluates to some falsy value, when in actual fact the clear intention is to evaluate `h()` only if `f()` is falsy. + +```lua +if f() then + ... + local foo = if g() then x +else + h() + ... +end +``` + +The only way to solve this had we chose optional `else` branch would be to wrap the if expression in parentheses or to place a semi-colon. + ## Drawbacks Studio's script editor autocomplete currently adds an indented block followed by `end` whenever a line ends that includes a `then` token. This can make use of the if expression unpleasant as developers have to keep fixing the code by removing auto-inserted `end`. We can work around this on the editor side by (short-term) differentiating between whether `if` token is the first on its line, and (long-term) by refactoring completion engine to use infallible parser for the block completer. From 7ab76e582cea0e8977e5b4b16af1d8a418ab5dba Mon Sep 17 00:00:00 2001 From: Alexander McCord <11488393+alexmccord@users.noreply.github.com> Date: Thu, 24 Mar 2022 08:38:16 -0700 Subject: [PATCH 4/9] RFC: Do not implement non-nil postfix `!` operator. (#420) This is a meta-RFC. I'd like to propose to remove the postfix `!` operator from the language. I'd like to argue that this operator will eventually become useless and can only be used incorrectly at some point in the future, and that there are likely ways for them to be sound without having to reach for the `!` hammer. With that, I present these counterarguments: Shortcoming #1: Refinements does not apply to the block after `return`/`break`/`continue`. ```lua if not x or not y then return end -- both x and y are truthy ``` This will be solved by implementing control flow analysis where it'd apply the inverse of the condition leading up to the control transfer statement to the rest of the scope. Shortcoming #2: Type checker is not aware of the actual state of various locations. ```lua type Foo = { x: { y: number }? }? local foo: Foo = { x = { y = 5 } } print(foo.x.y) -- prints 5 at runtime, type checker warns on this ``` This will be solved by implementing type states where it would inspect the initialization sites as well as assignments to know their actual states. That is, rather than trusting the type annotation `Foo` as the state which gets us far enough, we'd start seeing these type annotations as a subtype constraint for the location `foo`. --- If there are other use cases not covered in this message, we should talk about that and see if there exists an alternative direction that can solve these use cases soundly. --- rfcs/syntax-nil-forgiving-operator.md | 90 --------------------------- 1 file changed, 90 deletions(-) delete mode 100644 rfcs/syntax-nil-forgiving-operator.md diff --git a/rfcs/syntax-nil-forgiving-operator.md b/rfcs/syntax-nil-forgiving-operator.md deleted file mode 100644 index 63d5cab..0000000 --- a/rfcs/syntax-nil-forgiving-operator.md +++ /dev/null @@ -1,90 +0,0 @@ -# nil-forgiving postfix operator ! - -## Summary - -Introduce syntax to suppress typechecking errors for nilable types by ascribing them into a non-nil type. - -## Motivation - -Typechecking might not be able to figure out that a certain expression is a non-nil type, but the user might know additional context of the expression. - -Using `::` ascriptions is the current work-around for this issue, but it's much more verbose and requires providing the full type name when you only want to ascribe T? to T. - -The nil-forgiving operator will also allow chaining to be written in a very terse manner: -```lua -local p = a!.b!.c -``` -instead of -```lua -local ((p :: Part).b :: Folder).c -``` - -Note that nil-forgiving operator is **not** a part of member access operator, it can be used in standalone expressions, indexing and other places: -```lua -local p = f(a!)! -local q = b!['X'] -``` - -Nil-forgiving operator can be found in some programming languages such as C# (null-forgiving or null-suppression operator) and TypeScript (non-null assertion operator). - -## Design - -To implement this, we will change the syntax of the *primaryexp*. - -Before: -``` -primaryexp ::= prefixexp { `.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs } -``` -After: -``` -postfixeexp ::= (`.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs) [`!'] -primaryexp ::= prefixexp [`!'] { postfixeexp } -``` - -When we get the `!` token, we will wrap the expression that we have into a new AstExprNonNilAssertion node. - -An error is generated when the type of expression node this is one of the following: -* AstExprConstantBool -* AstExprConstantNumber -* AstExprConstantString -* AstExprFunction -* AstExprTable - -This operator doesn't have any impact on the run-time behavior of the program, it will only affect the type of the expression in the typechecker. - ---- -While parsing an assignment expression starts with a *primaryexp*, it performs a check that it has an l-value based on a fixed set of AstNode types. - -Since using `!` on an l-value has no effect, we don't extend this list with the new node and will generate a specialized parse error for code like: -```lua -p.a! = b -``` - ---- -When operator is used on expression of a union type with a `nil` option, it removes that option from the set. -If only one option remains, the union type is replaced with the type of a single option. - -If the type is `nil`, typechecker will generate a warning that the operator cannot be applied to the expression. - -For any other type, it has no effect and doesn't generate additional warnings. - -The reason for the last rule is to simplify movement of existing code where context in each location is slightly different. - -As an example from Roblox, instance path could dynamically change from being know to exist to be missing when script is changed in edit mode. - -## Drawbacks - -### Unnecessary operator use - -It might be useful to warn about unnecessary uses of this operator when the value cannot be `nil`, but we have no way of enabling this behavior. - -### Bad practice - -The operator might be placed by users to ignore/silence correct warnings and lower the strength of type checking that Luau provides. - -## Alternatives - -Aside from type assertion operator :: it should be possible to place `assert` function calls before the operation. -Type refinement/constraints should handle that statement and avoid warning in the following expressions. - -But `assert` call will introduce runtime overhead without adding extra safety to the case when the type is nil at run time, in both cases an error will be thrown. From 5a21c413634f628c89323ce06b77720f4b53cb82 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 24 Mar 2022 09:26:42 -0700 Subject: [PATCH 5/9] Update STATUS.md --- rfcs/STATUS.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/rfcs/STATUS.md b/rfcs/STATUS.md index 5d2d835..2b37262 100644 --- a/rfcs/STATUS.md +++ b/rfcs/STATUS.md @@ -29,14 +29,6 @@ This document tracks unimplemented RFCs. **Status**: Implemented but not fully rolled out yet. -## Nil-forgiving operator - -[RFC: nil-forgiving postfix operator !](https://github.com/Roblox/luau/blob/master/rfcs/syntax-nil-forgiving-operator.md) - -**Status**: Needs implementation. - -**Notes**: Do we need to reevaluate the necessity given `assert` and improved refinements? - ## Safe navigation operator [RFC: Safe navigation postfix operator (?)](https://github.com/Roblox/luau/blob/master/rfcs/syntax-safe-navigation-operator.md) @@ -56,9 +48,3 @@ This document tracks unimplemented RFCs. [RFC: Generalized iteration](https://github.com/Roblox/luau/blob/master/rfcs/generalized-iteration.md) **Status**: Needs implementation - -## table.clone - -[RFC: table.clone](https://github.com/Roblox/luau/blob/master/rfcs/function-table-clone.md) - -**Status**: Needs implementation From b8e025311b5ec8599d7ea77ab69225189c9f820b Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 24 Mar 2022 09:29:20 -0700 Subject: [PATCH 6/9] Mark table.clone as implemented --- rfcs/function-table-clone.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rfcs/function-table-clone.md b/rfcs/function-table-clone.md index 78d126e..8cb9798 100644 --- a/rfcs/function-table-clone.md +++ b/rfcs/function-table-clone.md @@ -1,5 +1,7 @@ # table.clone +**Status**: Implemented + ## Summary Add `table.clone` function that, given a table, produces a copy of that table with the same keys/values/metatable. From 2335b26ffc09fda428bc80f126e2f884815f7514 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 24 Mar 2022 09:31:18 -0700 Subject: [PATCH 7/9] Update library.md Add documentation for table.clone --- docs/_pages/library.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/_pages/library.md b/docs/_pages/library.md index 28e7035..eeada33 100644 --- a/docs/_pages/library.md +++ b/docs/_pages/library.md @@ -466,6 +466,13 @@ function table.isfrozen(t: table): boolean Returns `true` iff the input table is frozen. +``` +function table.clone(t: table): table +``` + +Returns a copy of the input table that has the same metatable, same keys and values, and is not frozen even if `t` was. +The copy is shallow: implementing a deep recursive copy automatically is challenging, and often only certain keys need to be cloned recursively which can be done after the initial clone by modifying the resulting table. + ## string library ``` From ab64a097cd72282d4e223cf914b02766f188c12c Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 24 Mar 2022 13:10:30 -0700 Subject: [PATCH 8/9] Mark sealed table subtyping RFC as implemented --- rfcs/sealed-table-subtyping.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rfcs/sealed-table-subtyping.md b/rfcs/sealed-table-subtyping.md index 7310795..7371490 100644 --- a/rfcs/sealed-table-subtyping.md +++ b/rfcs/sealed-table-subtyping.md @@ -1,5 +1,9 @@ # Sealed table subtyping +**Status**: Implemented + +## Summary + In Luau, tables have a state, which can, among others, be "sealed". A sealed table is one that we know the full shape of and cannot have new properties added to it. We would like to introduce subtyping for sealed tables, to allow users to express some subtyping relationships that they currently cannot. ## Motivation From 5e7e462104061f604b9dabcd6cb78573aaf37e8d Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 24 Mar 2022 13:10:56 -0700 Subject: [PATCH 9/9] Update STATUS.md --- rfcs/STATUS.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rfcs/STATUS.md b/rfcs/STATUS.md index 2b37262..93a09ec 100644 --- a/rfcs/STATUS.md +++ b/rfcs/STATUS.md @@ -17,11 +17,10 @@ This document tracks unimplemented RFCs. ## Sealed/unsealed typing changes -[RFC: Sealed table subtyping](https://github.com/Roblox/luau/blob/master/rfcs/sealed-table-subtyping.md) | [RFC: Unsealed table literals](https://github.com/Roblox/luau/blob/master/rfcs/unsealed-table-literals.md) | [RFC: Only strip optional properties from unsealed tables during subtyping](https://github.com/Roblox/luau/blob/master/rfcs/unsealed-table-subtyping-strips-optional-properties.md) -**Status**: Implemented but depends on new transaction log implementation that is not fully live yet. +**Status**: Implemented but not fully rolled out yet. ## Singleton types