Skip to content

Commit c9368fe

Browse files
author
Tim Foley
authored
IR: Add support for break and continue statements (shader-slang#272)
* IR: Add support for break and continue statements The front-end is already doing the work of connecting this statements to their "parent" statement, so we just needed to build a map from the `Stmt*` to the corresponding `IRBlock*`s to use for break/continue when outputting any loop statement, and then look up in the map for the branch target when outputting a break/continue. When we get around to adding `switch` statements, the same pattern should work just fine. I also added support for `do/while` statements in IR codegen, and made sure to exercise those in one of the test cases I added. There is also an unrelated IR codegen fix for when there is a "bound subscript" on the RHS of an assignment. * IR: fix handling of do/while and continue Thanks to @csyonghe for pointing out my mistake in the earlier commit. I implemented `continue` for `do/while` loops incorrectly, branching to the head of the loop instead of the loop test. I'll try to blame this mistake on the fact that I never use `do/while` loops because I think they are awful. :) The fix for that issue wasn't too bad (see `lower-to-ir.cpp`) but it surfaces a much more serious issue: I wasn't actually implementing `continue` correctly *at all* when it comes to generating HLSL/GLSL from the IR (I can't easily make an excuse for that one). The basic issue at the heart of this is that given an input statement like: ``` for(int ii = 0; ii < N; ii = doSomething(ii)) { ... } ``` The continue clause (`ii = doSomething(ii)`) could expand into many instructions (across multiple blocks, if we inline), and there is in general no guarantee when we are done that we can package up that code as an expression and spit out a new `for` loop (the same basic argument applies to a `do { ... } while(someComplexExpression())`. So, if we assume that in general we have to generate a full *statement* for the `continue` clause, what can we emit? - We could try to "outline" the continue code into its own function, so that we can call it from an expression. That could work, but has high implementation complexity. - We could introduce additional `bool` variables for control flow, outputting something like: ``` bool useContinueBlock = false; for(;;) { if(useContinueBlock) { <CONTINUE CODE>; } useContinueBlock = true; <LOOP TEST> <LOOP BODY> } ``` This works but user might balk at the extra variable we introduce. - We could duplicate the code at each continue site. That is, we emit the loop as: ``` for(;;) { <LOOP TEST> <LOOP BODY> <CONTINUE CODE> } ``` but then whenever we'd like to emit `continue;` we instead emit `{ <CONTINUE CODE>; continue; }`. This doesn't introduce any extra variables, but it causes code duplication (limited, if we don't have too many `continue` sites, and the continue clause is small - which are the common cases). When I was initially working on the IR codegen I picked that last option just because it is what `fxc` seems to do, but I neglected to actually *implement* the special-case codegen for a `continue` instruction. This change addresses that (see `emit.cpp`). Finally, once things were fixed the `continue` test case produced the results Yong told me to expect, but it also produced a warning from the downstream HLSL compiler ("hey, your loop doesn't ever actually *loop*!"), so I reworked the test back to one that actually loops (but still tests `continue`). As a final aside in this essay of a commit message: the current IR representation of control flow uses special-case instructions for various cases of unconditional branch (and two variations on `if`), but these are not strictly necessary, and a future change will hopefully clean it up. The biggest catch in doing that is that it will require the IR->source codegen to carefully track which blocks represent which kinds of branch targets in context (e.g., you can't assume that a `continue` that nees the special handling above will appear as a distinct kind of instruction).
1 parent 9a26ba8 commit c9368fe

6 files changed

+203
-9
lines changed

source/slang/emit.cpp

+28-5
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ struct SharedEmitContext
9797
Dictionary<IRValue*, UInt> mapIRValueToID;
9898

9999
HashSet<Decl*> irDeclsVisited;
100+
101+
Dictionary<IRBlock*, IRBlock*> irMapContinueTargetToLoopHead;
100102
};
101103

102104
struct EmitContext
@@ -5555,10 +5557,10 @@ emitDeclImpl(decl, nullptr);
55555557

55565558
emit("for(;;)\n{\n");
55575559

5558-
// TODO: Okay, we *said* we'd do this special
5559-
// handling of the `continue` sites, but
5560-
// we aren't actually setting anything up here...
5561-
//
5560+
// Register information so that `continue` sites
5561+
// can do the right thing:
5562+
ctx->shared->irMapContinueTargetToLoopHead.Add(continueBlock, targetBlock);
5563+
55625564

55635565
emitIRStmtsForBlocks(
55645566
ctx,
@@ -5579,7 +5581,28 @@ emitDeclImpl(decl, nullptr);
55795581
return;
55805582

55815583
case kIROp_continue:
5582-
emit("continue;\n");
5584+
// With out current strategy for outputting loops,
5585+
// just outputting an AST-level `continue` here won't
5586+
// actually execute the statements in the continue block.
5587+
//
5588+
// Instead, we have to manually output those statements
5589+
// directly here, and *then* do an AST-level `continue`.
5590+
//
5591+
// This leads to code duplication when we have multiple
5592+
// `continue` sites in the original program, but it avoids
5593+
// introducing additional temporaries for control flow.
5594+
{
5595+
auto continueInst = (IRContinue*) terminator;
5596+
auto targetBlock = continueInst->getTargetBlock();
5597+
IRBlock* loopHead = nullptr;
5598+
ctx->shared->irMapContinueTargetToLoopHead.TryGetValue(targetBlock, loopHead);
5599+
SLANG_ASSERT(loopHead);
5600+
emitIRStmtsForBlocks(
5601+
ctx,
5602+
targetBlock,
5603+
loopHead);
5604+
emit("continue;\n");
5605+
}
55835606
return;
55845607

55855608
case kIROp_loopTest:

source/slang/lower-to-ir.cpp

+108-4
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,12 @@ struct SharedIRGenContext
279279
// to reference-count these along the way because
280280
// they need to get stored into a `union` inside `LoweredValInfo`
281281
List<RefPtr<ExtendedValueInfo>> extValues;
282+
283+
// Map from an AST-level statement that can be
284+
// used as the target of a `break` or `continue`
285+
// to the appropriate basic block to jump to.
286+
Dictionary<Stmt*, IRBlock*> breakLabels;
287+
Dictionary<Stmt*, IRBlock*> continueLabels;
282288
};
283289

284290

@@ -1741,8 +1747,10 @@ struct StmtLoweringVisitor : StmtVisitor<StmtLoweringVisitor>
17411747
auto breakLabel = createBlock();
17421748
auto continueLabel = createBlock();
17431749

1744-
// TODO: register `loopHead` as the target for a
1745-
// `continue` statement.
1750+
// Register the `break` and `continue` labels so
1751+
// that we can find them for nested statements.
1752+
context->shared->breakLabels.Add(stmt, breakLabel);
1753+
context->shared->continueLabels.Add(stmt, continueLabel);
17461754

17471755
// Emit the branch that will start out loop,
17481756
// and then insert the block for the head.
@@ -1807,8 +1815,10 @@ struct StmtLoweringVisitor : StmtVisitor<StmtLoweringVisitor>
18071815
// jumps to the head of hte loop.
18081816
auto continueLabel = loopHead;
18091817

1810-
// TODO: register appropriate targets for
1811-
// break/continue statements.
1818+
// Register the `break` and `continue` labels so
1819+
// that we can find them for nested statements.
1820+
context->shared->breakLabels.Add(stmt, breakLabel);
1821+
context->shared->continueLabels.Add(stmt, continueLabel);
18121822

18131823
// Emit the branch that will start out loop,
18141824
// and then insert the block for the head.
@@ -1847,6 +1857,66 @@ struct StmtLoweringVisitor : StmtVisitor<StmtLoweringVisitor>
18471857
insertBlock(breakLabel);
18481858
}
18491859

1860+
void visitDoWhileStmt(DoWhileStmt* stmt)
1861+
{
1862+
// Generating IR for `do {...} while` statement is similar to a
1863+
// `while` statement, just with the test in a different place
1864+
1865+
auto builder = getBuilder();
1866+
1867+
// We will create blocks for the various places
1868+
// we need to jump to inside the control flow,
1869+
// including the blocks that will be referenced
1870+
// by `continue` or `break` statements.
1871+
auto loopHead = createBlock();
1872+
auto testLabel = createBlock();
1873+
auto breakLabel = createBlock();
1874+
1875+
// A `continue` inside a `do { ... } while ( ... )` loop always
1876+
// jumps to the loop test.
1877+
auto continueLabel = testLabel;
1878+
1879+
// Register the `break` and `continue` labels so
1880+
// that we can find them for nested statements.
1881+
context->shared->breakLabels.Add(stmt, breakLabel);
1882+
context->shared->continueLabels.Add(stmt, continueLabel);
1883+
1884+
// Emit the branch that will start out loop,
1885+
// and then insert the block for the head.
1886+
1887+
auto loopInst = builder->emitLoop(
1888+
loopHead,
1889+
breakLabel,
1890+
continueLabel);
1891+
1892+
addLoopDecorations(loopInst, stmt);
1893+
1894+
insertBlock(loopHead);
1895+
1896+
// Emit the body of the loop
1897+
lowerStmt(context, stmt->Statement);
1898+
1899+
insertBlock(testLabel);
1900+
1901+
// Now that we are within the header block, we
1902+
// want to emit the expression for the loop condition:
1903+
if (auto condExpr = stmt->Predicate)
1904+
{
1905+
auto irCondition = getSimpleVal(context,
1906+
lowerRValueExpr(context, condExpr));
1907+
1908+
// Now we want to `break` if the loop condition is false,
1909+
// otherwise we will jump back to the head of the loop.
1910+
builder->emitLoopTest(
1911+
irCondition,
1912+
loopHead,
1913+
breakLabel);
1914+
}
1915+
1916+
// Finally we insert the label that a `break` will jump to
1917+
insertBlock(breakLabel);
1918+
}
1919+
18501920
void visitExpressionStmt(ExpressionStmt* stmt)
18511921
{
18521922
// The statement evaluates an expression
@@ -1917,6 +1987,39 @@ struct StmtLoweringVisitor : StmtVisitor<StmtLoweringVisitor>
19171987
{
19181988
getBuilder()->emitDiscard();
19191989
}
1990+
1991+
void visitBreakStmt(BreakStmt* stmt)
1992+
{
1993+
// Semantic checking is responsible for finding
1994+
// the statement taht this `break` breaks out of
1995+
auto parentStmt = stmt->parentStmt;
1996+
SLANG_ASSERT(parentStmt);
1997+
1998+
// We just need to look up the basic block that
1999+
// corresponds to the break label for that statement,
2000+
// and then emit an instruction to jump to it.
2001+
IRBlock* targetBlock;
2002+
context->shared->breakLabels.TryGetValue(parentStmt, targetBlock);
2003+
SLANG_ASSERT(targetBlock);
2004+
getBuilder()->emitBreak(targetBlock);
2005+
}
2006+
2007+
void visitContinueStmt(ContinueStmt* stmt)
2008+
{
2009+
// Semantic checking is responsible for finding
2010+
// the loop that this `continue` statement continues
2011+
auto parentStmt = stmt->parentStmt;
2012+
SLANG_ASSERT(parentStmt);
2013+
2014+
2015+
// We just need to look up the basic block that
2016+
// corresponds to the continue label for that statement,
2017+
// and then emit an instruction to jump to it.
2018+
IRBlock* targetBlock;
2019+
context->shared->continueLabels.TryGetValue(parentStmt, targetBlock);
2020+
SLANG_ASSERT(targetBlock);
2021+
getBuilder()->emitContinue(targetBlock);
2022+
}
19202023
};
19212024

19222025
void lowerStmt(
@@ -1947,6 +2050,7 @@ void assign(
19472050
case LoweredValInfo::Flavor::Simple:
19482051
case LoweredValInfo::Flavor::Ptr:
19492052
case LoweredValInfo::Flavor::SwizzledLValue:
2053+
case LoweredValInfo::Flavor::BoundSubscript:
19502054
{
19512055
builder->emitStore(
19522056
left.val,

tests/compute/break-stmt.slang

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//TEST(compute):COMPARE_COMPUTE:
2+
//TEST_INPUT:ubuffer(data=[0 1 2 3], stride=4):dxbinding(0),glbinding(0),out
3+
4+
// Test that `break` from a loop works.
5+
6+
int test(int inVal)
7+
{
8+
int ii = 0;
9+
for(;;)
10+
{
11+
if(ii >= inVal)
12+
break;
13+
ii++;
14+
}
15+
return -ii;
16+
}
17+
18+
RWStructuredBuffer<int> outputBuffer : register(u0);
19+
20+
[numthreads(4, 1, 1)]
21+
void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
22+
{
23+
uint tid = dispatchThreadID.x;
24+
int inVal = outputBuffer[tid];
25+
int outVal = test(inVal);
26+
outputBuffer[tid] = outVal;
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0
2+
FFFFFFFF
3+
FFFFFFFE
4+
FFFFFFFD

tests/compute/continue-stmt.slang

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//TEST(compute):COMPARE_COMPUTE:
2+
//TEST_INPUT:ubuffer(data=[0 1 2 3], stride=4):dxbinding(0),glbinding(0),out
3+
4+
// Test that `break` from a loop works.
5+
6+
int test(int inVal)
7+
{
8+
int ii = 0;
9+
do
10+
{
11+
if(ii < inVal)
12+
{
13+
ii++;
14+
continue;
15+
}
16+
break;
17+
}
18+
while(true);
19+
20+
return -ii;
21+
}
22+
23+
RWStructuredBuffer<int> outputBuffer : register(u0);
24+
25+
[numthreads(4, 1, 1)]
26+
void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
27+
{
28+
uint tid = dispatchThreadID.x;
29+
int inVal = outputBuffer[tid];
30+
int outVal = test(inVal);
31+
outputBuffer[tid] = outVal;
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0
2+
FFFFFFFF
3+
FFFFFFFE
4+
FFFFFFFD

0 commit comments

Comments
 (0)