Skip to content

Commit

Permalink
Support Control Flow type Refinements for "break" and "continue" stat…
Browse files Browse the repository at this point in the history
…ements (#1004)

Fixes: #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 <[email protected]>
  • Loading branch information
AmberGraceSoftware and alexmccord authored Sep 21, 2023
1 parent 3090010 commit d00e93c
Show file tree
Hide file tree
Showing 4 changed files with 786 additions and 18 deletions.
4 changes: 2 additions & 2 deletions Analysis/include/Luau/ControlFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 10 additions & 8 deletions Analysis/src/ConstraintGraphBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit);
LUAU_FASTFLAG(DebugLuauLogSolverToJson);
LUAU_FASTFLAG(DebugLuauMagicTypes);
LUAU_FASTFLAG(LuauParseDeclareClassIndexer);
LUAU_FASTFLAG(LuauLoopControlFlowAnalysis);
LUAU_FASTFLAG(LuauFloorDivision);

namespace Luau
Expand Down Expand Up @@ -537,11 +538,10 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat)
return visit(scope, s);
else if (auto s = stat->as<AstStatRepeat>())
return visit(scope, s);
else if (stat->is<AstStatBreak>() || stat->is<AstStatContinue>())
{
// Nothing
return ControlFlow::None; // TODO: ControlFlow::Break/Continue
}
else if (stat->is<AstStatBreak>())
return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Breaks : ControlFlow::None;
else if (stat->is<AstStatContinue>())
return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Continues : ControlFlow::None;
else if (auto r = stat->as<AstStatReturn>())
return visit(scope, r);
else if (auto e = stat->as<AstStatExpr>())
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 10 additions & 8 deletions Analysis/src/TypeInfer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -350,11 +351,10 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStat& program)
return check(scope, *while_);
else if (auto repeat = program.as<AstStatRepeat>())
return check(scope, *repeat);
else if (program.is<AstStatBreak>() || program.is<AstStatContinue>())
{
// Nothing to do
return ControlFlow::None;
}
else if (program.is<AstStatBreak>())
return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Breaks : ControlFlow::None;
else if (program.is<AstStatContinue>())
return FFlag::LuauLoopControlFlowAnalysis ? ControlFlow::Continues : ControlFlow::None;
else if (auto return_ = program.as<AstStatReturn>())
return check(scope, *return_);
else if (auto expr = program.as<AstStatExpr>())
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit d00e93c

Please sign in to comment.