diff --git a/Analysis/include/Luau/ControlFlow.h b/Analysis/include/Luau/ControlFlow.h index 566d77bd..c51da707 100644 --- a/Analysis/include/Luau/ControlFlow.h +++ b/Analysis/include/Luau/ControlFlow.h @@ -14,9 +14,24 @@ enum class ControlFlow None = 0b00001, Returns = 0b00010, Throws = 0b00100, - Break = 0b01000, // Currently unused. - Continue = 0b10000, // Currently unused. + Breaks = 0b01000, + Continues = 0b10000, + + // Returns OR Throws (e.g. if predicate then return else error() end) + MixedFunctionExit = 0b100000, + + // Breaks OR Continues (e.g. if predicate then break else continue end) + MixedLoopExit = 0b1000000, + + // Exits a loop OR function (e.g. if prediate then continue else return end) + MixedExit = 0b10000000, }; +// Bitmask of all control flows which exit the nearest function scope +#define FunctionExitControlFlows ControlFlow::Returns | ControlFlow::Throws | ControlFlow::MixedFunctionExit +// Bitmask of all control flows which exit the nearest loop scope +#define LoopExitControlFlows ControlFlow::Breaks | ControlFlow::Continues | ControlFlow::MixedLoopExit +// Bitmask of all control flows which exit the nearest function or loop scopes +#define ExitingControlFlows ControlFlow::Returns | ControlFlow::Throws | ControlFlow::Breaks | ControlFlow::Continues | ControlFlow::MixedFunctionExit | ControlFlow::MixedLoopExit | ControlFlow::MixedExit inline ControlFlow operator&(ControlFlow a, ControlFlow b) { diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index 7d35ebc6..a605a4a9 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); namespace Luau { @@ -536,11 +537,14 @@ 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()) @@ -1066,20 +1070,60 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifSt ScopePtr elseScope = childScope(ifStatement->elsebody ? ifStatement->elsebody : ifStatement, scope); applyRefinements(elseScope, ifStatement->elseLocation.value_or(ifStatement->condition->location), refinementArena.negation(refinement)); + const ControlFlow guardClauseFlows = FFlag::LuauLoopControlFlowAnalysis + ? ExitingControlFlows + : ControlFlow::Returns | ControlFlow :: Throws; + ControlFlow thencf = visit(thenScope, ifStatement->thenbody); ControlFlow elsecf = ControlFlow::None; if (ifStatement->elsebody) elsecf = visit(elseScope, ifStatement->elsebody); - if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && elsecf == ControlFlow::None) + if (matches(thencf, guardClauseFlows) && elsecf == ControlFlow::None) scope->inheritRefinements(elseScope); - else if (thencf == ControlFlow::None && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + else if (thencf == ControlFlow::None && matches(elsecf, guardClauseFlows)) scope->inheritRefinements(thenScope); - if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) - return ControlFlow::Returns; - else + if (FFlag::LuauLoopControlFlowAnalysis) + { + if (thencf == elsecf) + { + return thencf; + } + if ( + matches(thencf, FunctionExitControlFlows) + && matches(elsecf, FunctionExitControlFlows) + ) + { + return ControlFlow::MixedFunctionExit; + } + if ( + matches(thencf, LoopExitControlFlows) + && matches(elsecf, LoopExitControlFlows) + ) + { + return ControlFlow::MixedLoopExit; + } + if ( + matches(thencf, ExitingControlFlows) + && matches(elsecf, ExitingControlFlows) + ) + { + return ControlFlow::MixedExit; + } return ControlFlow::None; + } + else + { + if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + { + return ControlFlow::Returns; + } + else + { + return ControlFlow::None; + } + } } static bool occursCheck(TypeId needle, TypeId haystack) diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index d4a56f11..04b97e7f 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -366,11 +366,14 @@ 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()) @@ -763,20 +766,60 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatIf& statement ScopePtr elseScope = childScope(scope, statement.elsebody ? statement.elsebody->location : statement.location); resolve(result.predicates, elseScope, false); + const ControlFlow guardClauseFlows = FFlag::LuauLoopControlFlowAnalysis + ? ExitingControlFlows + : ControlFlow::Returns | ControlFlow :: Throws; + ControlFlow thencf = check(thenScope, *statement.thenbody); ControlFlow elsecf = ControlFlow::None; if (statement.elsebody) elsecf = check(elseScope, *statement.elsebody); - if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && elsecf == ControlFlow::None) + if (matches(thencf, guardClauseFlows) && elsecf == ControlFlow::None) scope->inheritRefinements(elseScope); - else if (thencf == ControlFlow::None && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + else if (thencf == ControlFlow::None && matches(elsecf, guardClauseFlows)) scope->inheritRefinements(thenScope); - if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) - return ControlFlow::Returns; - else + if (FFlag::LuauLoopControlFlowAnalysis) + { + if (thencf == elsecf) + { + return thencf; + } + if ( + matches(thencf, FunctionExitControlFlows) + && matches(elsecf, FunctionExitControlFlows) + ) + { + return ControlFlow::MixedFunctionExit; + } + if ( + matches(thencf, LoopExitControlFlows) + && matches(elsecf, LoopExitControlFlows) + ) + { + return ControlFlow::MixedLoopExit; + } + if ( + matches(thencf, ExitingControlFlows) + && matches(elsecf, ExitingControlFlows) + ) + { + return ControlFlow::MixedExit; + } return ControlFlow::None; + } + else + { + if (matches(thencf, ControlFlow::Returns | ControlFlow::Throws) && matches(elsecf, ControlFlow::Returns | ControlFlow::Throws)) + { + return ControlFlow::Returns; + } + else + { + return ControlFlow::None; + } + } } else {