From d00e93c82c0ad156afa8cf0c100c2becfe83b808 Mon Sep 17 00:00:00 2001 From: Amber Grace <131925693+AmberGraceSoftware@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:28:42 -0600 Subject: [PATCH] Support Control Flow type Refinements for "break" and "continue" statements (#1004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/Roblox/luau/issues/913 This PR adds support for type refinements around guard clauses that use `break` and `continue` statements inside a loop, similar to how guard clauses with `return` is supported. I had some free time today, so I figure I'd give a shot at a naïve fix for this at the very least. --- ## Resulting Change: Luau now supports type refinements within loops where a `continue` or `break` guard clause was used. For example: ```lua for _, object in objects :: {{value: string?}} do if not object.value then continue end local x: string = object.value -- OK; Used to emit "Type 'string?' could not be converted into 'string'" end ``` --------- Co-authored-by: Alexander McCord --- Analysis/include/Luau/ControlFlow.h | 4 +- Analysis/src/ConstraintGraphBuilder.cpp | 18 +- Analysis/src/TypeInfer.cpp | 18 +- tests/TypeInfer.cfa.test.cpp | 764 ++++++++++++++++++++++++ 4 files changed, 786 insertions(+), 18 deletions(-) diff --git a/Analysis/include/Luau/ControlFlow.h b/Analysis/include/Luau/ControlFlow.h index 566d77bd..82c0403c 100644 --- a/Analysis/include/Luau/ControlFlow.h +++ b/Analysis/include/Luau/ControlFlow.h @@ -14,8 +14,8 @@ enum class ControlFlow None = 0b00001, Returns = 0b00010, Throws = 0b00100, - Break = 0b01000, // Currently unused. - Continue = 0b10000, // Currently unused. + Breaks = 0b01000, + Continues = 0b10000, }; inline ControlFlow operator&(ControlFlow a, ControlFlow b) diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index ae143ca5..f9b0dbf8 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -24,6 +24,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit); LUAU_FASTFLAG(DebugLuauLogSolverToJson); LUAU_FASTFLAG(DebugLuauMagicTypes); LUAU_FASTFLAG(LuauParseDeclareClassIndexer); +LUAU_FASTFLAG(LuauLoopControlFlowAnalysis); LUAU_FASTFLAG(LuauFloorDivision); namespace Luau @@ -537,11 +538,10 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) return visit(scope, s); else if (auto s = stat->as()) return visit(scope, s); - else if (stat->is() || stat->is()) - { - // Nothing - return ControlFlow::None; // TODO: ControlFlow::Break/Continue - } + else if (stat->is()) + return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Breaks : ControlFlow::None; + else if (stat->is()) + return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Continues : ControlFlow::None; else if (auto r = stat->as()) return visit(scope, r); else if (auto e = stat->as()) @@ -1072,12 +1072,14 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifSt if (ifStatement->elsebody) elsecf = visit(elseScope, ifStatement->elsebody); - if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && elsecf == ControlFlow::None) + if (thencf != ControlFlow::None && elsecf == ControlFlow::None) scope->inheritRefinements(elseScope); - else if (thencf == ControlFlow::None && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) scope->inheritRefinements(thenScope); - if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + if (FFlag::LuauLoopControlFlowAnalysis && thencf == elsecf) + return thencf; + else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) return ControlFlow::Returns; else return ControlFlow::None; diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 61c90ba8..a29b1e06 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -38,6 +38,7 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false) LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) +LUAU_FASTFLAGVARIABLE(LuauLoopControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauVariadicOverloadFix, false) LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false) LUAU_FASTFLAG(LuauParseDeclareClassIndexer) @@ -350,11 +351,10 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStat& program) return check(scope, *while_); else if (auto repeat = program.as()) return check(scope, *repeat); - else if (program.is() || program.is()) - { - // Nothing to do - return ControlFlow::None; - } + else if (program.is()) + return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Breaks : ControlFlow::None; + else if (program.is()) + return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Continues : ControlFlow::None; else if (auto return_ = program.as()) return check(scope, *return_); else if (auto expr = program.as()) @@ -752,12 +752,14 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatIf& statement if (statement.elsebody) elsecf = check(elseScope, *statement.elsebody); - if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && elsecf == ControlFlow::None) + if (thencf != ControlFlow::None && elsecf == ControlFlow::None) scope->inheritRefinements(elseScope); - else if (thencf == ControlFlow::None && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + else if (thencf == ControlFlow::None && elsecf != ControlFlow::None) scope->inheritRefinements(thenScope); - if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + if (FFlag::LuauLoopControlFlowAnalysis && thencf == elsecf) + return thencf; + else if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) return ControlFlow::Returns; else return ControlFlow::None; diff --git a/tests/TypeInfer.cfa.test.cpp b/tests/TypeInfer.cfa.test.cpp index 04aeb54b..19700d2c 100644 --- a/tests/TypeInfer.cfa.test.cpp +++ b/tests/TypeInfer.cfa.test.cpp @@ -26,6 +26,52 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return") CHECK_EQ("string", toString(requireTypeAtPosition({6, 24}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}) + for _, record in x do + if not record.value then + break + end + + local foo = record.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({7, 34}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}) + for _, record in x do + if not record.value then + continue + end + + local foo = record.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({7, 38}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_return") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -48,6 +94,118 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_return") CHECK_EQ("string", toString(requireTypeAtPosition({9, 24}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_not_y_break") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + break + elseif not recordY.value then + break + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({10, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({11, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_not_y_continue") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + continue + elseif not recordY.value then + continue + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({10, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({11, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_break") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + return + elseif not recordY.value then + break + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({10, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({11, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_not_y_continue") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + break + elseif not recordY.value then + continue + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({10, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({11, 38}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_rand_return_elif_not_y_return") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -72,6 +230,66 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_rand_return_elif_not_y_ CHECK_EQ("string", toString(requireTypeAtPosition({11, 24}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_rand_break_elif_not_y_break") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + break + elseif math.random() > 0.5 then + break + elseif not recordY.value then + break + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({12, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_rand_continue_elif_not_y_continue") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + continue + elseif math.random() > 0.5 then + continue + elseif not recordY.value then + continue + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({12, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_rand_return_elif_not_y_fallthrough") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -96,6 +314,66 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_rand_return_elif_no CHECK_EQ("string?", toString(requireTypeAtPosition({11, 24}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_rand_break_elif_not_y_fallthrough") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + break + elseif math.random() > 0.5 then + break + elseif not recordY.value then + + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({12, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({13, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_rand_continue_elif_not_y_fallthrough") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + continue + elseif math.random() > 0.5 then + continue + elseif not recordY.value then + + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({12, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({13, 38}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_fallthrough_elif_not_z_return") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -122,6 +400,138 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_fallthrough_elif_ CHECK_EQ("string?", toString(requireTypeAtPosition({12, 24}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_elif_not_y_fallthrough_elif_not_z_break") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}, z: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + local recordZ = y[i] + if not recordX.value then + break + elseif not recordY.value then + + elseif not recordZ.value then + break + end + + local foo = recordX.value + local bar = recordY.value + local baz = recordZ.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({14, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({15, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_not_y_fallthrough_elif_not_z_continue") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}, z: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + local recordZ = y[i] + if not recordX.value then + continue + elseif not recordY.value then + + elseif not recordZ.value then + continue + end + + local foo = recordX.value + local bar = recordY.value + local baz = recordZ.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({14, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({15, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_elif_not_y_throw_elif_not_z_fallthrough") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}, z: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + local recordZ = y[i] + if not recordX.value then + continue + elseif not recordY.value then + error("Y value not defined") + elseif not recordZ.value then + + end + + local foo = recordX.value + local bar = recordY.value + local baz = recordZ.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({14, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({15, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_elif_not_y_fallthrough_elif_not_z_break") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}, z: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + local recordZ = y[i] + if not recordX.value then + return + elseif not recordY.value then + + elseif not recordZ.value then + break + end + + local foo = recordX.value + local bar = recordY.value + local baz = recordZ.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({14, 38}))); + CHECK_EQ("string?", toString(requireTypeAtPosition({15, 38}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "do_if_not_x_return") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -142,6 +552,56 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_if_not_x_return") CHECK_EQ("string", toString(requireTypeAtPosition({8, 24}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "for_record_do_if_not_x_break") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}) + for _, record in x do + do + if not record.value then + break + end + end + + local foo = record.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({9, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "for_record_do_if_not_x_continue") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}) + for _, record in x do + do + if not record.value then + continue + end + end + + local foo = record.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({9, 38}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "early_return_in_a_loop_which_isnt_guaranteed_to_run_first") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -271,6 +731,126 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_return_if_not_y_return") CHECK_EQ("string", toString(requireTypeAtPosition({11, 24}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_if_not_y_break") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + break + end + + if not recordY.value then + break + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({12, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_if_not_y_continue") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + continue + end + + if not recordY.value then + continue + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({12, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_continue_if_not_y_throw") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + continue + end + + if not recordY.value then + error("Y value not defined") + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({12, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "if_not_x_break_if_not_y_continue") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}, y: {{value: string?}}) + for i, recordX in x do + local recordY = y[i] + if not recordX.value then + break + end + + if not recordY.value then + continue + end + + local foo = recordX.value + local bar = recordY.value + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("string", toString(requireTypeAtPosition({12, 38}))); + CHECK_EQ("string", toString(requireTypeAtPosition({13, 38}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_does_not_leak_out") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -294,6 +874,62 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_does_not_leak_out") CHECK_EQ("nil", toString(requireTypeAtPosition({8, 29}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_does_not_leak_out_breaking") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}) + for _, record in x do + if typeof(record.value) == "string" then + break + else + type Foo = number + end + + local foo: Foo = record.value + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("Unknown type 'Foo'", toString(result.errors[0])); + + CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_does_not_leak_out_continuing") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}) + for _, record in x do + if typeof(record.value) == "string" then + continue + else + type Foo = number + end + + local foo: Foo = record.value + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("Unknown type 'Foo'", toString(result.errors[0])); + + CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_scope") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -320,6 +956,62 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_ CHECK_EQ("nil", toString(requireTypeAtPosition({8, 29}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_scope_breaking") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}) + for _, record in x do + type Foo = number + + if typeof(record.value) == "string" then + break + end + + local foo: Foo = record.value + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0])); + + CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_scope_continuing") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + local function f(x: {{value: string?}}) + for _, record in x do + type Foo = number + + if typeof(record.value) == "string" then + continue + end + + local foo: Foo = record.value + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ("Type 'nil' could not be converted into 'number'", toString(result.errors[0])); + + CHECK_EQ("nil", toString(requireTypeAtPosition({9, 43}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; @@ -355,6 +1047,78 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions") CHECK_EQ("Err", toString(requireTypeAtPosition({16, 19}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions_breaking") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + type Ok = { tag: "ok", value: T } + type Err = { tag: "err", error: E } + type Result = Ok | Err + + local function process(results: {Result}) + for _, result in results do + if result.tag == "ok" then + local tag = result.tag + local val = result.value + + break + end + + local tag = result.tag + local err = result.error + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("\"ok\"", toString(requireTypeAtPosition({8, 39}))); + CHECK_EQ("T", toString(requireTypeAtPosition({9, 39}))); + + CHECK_EQ("\"err\"", toString(requireTypeAtPosition({14, 35}))); + CHECK_EQ("E", toString(requireTypeAtPosition({15, 35}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "tagged_unions_continuing") +{ + ScopedFastFlag flags[] = { + {"LuauTinyControlFlowAnalysis", true}, + {"LuauLoopControlFlowAnalysis", true} + }; + + CheckResult result = check(R"( + type Ok = { tag: "ok", value: T } + type Err = { tag: "err", error: E } + type Result = Ok | Err + + local function process(results: {Result}) + for _, result in results do + if result.tag == "ok" then + local tag = result.tag + local val = result.value + + continue + end + + local tag = result.tag + local err = result.error + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("\"ok\"", toString(requireTypeAtPosition({8, 39}))); + CHECK_EQ("T", toString(requireTypeAtPosition({9, 39}))); + + CHECK_EQ("\"err\"", toString(requireTypeAtPosition({14, 35}))); + CHECK_EQ("E", toString(requireTypeAtPosition({15, 35}))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "do_assert_x") { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true};