Skip to content

Commit be8b891

Browse files
author
Tim Foley
authored
Generate SSA form for IR functions (shader-slang#400)
* Generate SSA form for IR functions The basic idea here is simple: in the front-end after we have lowered the AST to initial IR we will apply a set of "mandatory" optimization passes. The first of these is to attempt to translate the all functions into SSA form so that they are amenable to subsequent dataflow optimizations. Eventually, the mandatory optimization passes would include diagnostic passes that make sure variables aren't used when undefined, etc. Just doing basic SSA generation already cleans up a lot of the messiness in our IR today, because constructs that used to involve many local variables can now be handled via SSA temporaries. The implementation of SSA generation is in `ir-ssa.cpp`, and it follows the approach of Braun et al.'s "Simple and Efficient Construction of Static Single Assignment Form." I used this instead of the more well-known Cytron et al. algorithm because Braun's algorith mis very simple to code, and does not require auxiliary analyses to generate the dominance frontier. The main wrinkle in our SSA representation right now is that instead of using ordinary phi nodes, we instead allow basic blocks to have parameters, where predecessor blocks pass in different parameter values. This encodes information equivalent to traditional phi nodes, but has two (small) benefits: 1. There is no fixed relationship between the order of phi operands and predecessor blocks, so we don't have to worry about breaking the phis when we alter the order in which predecessors are stored. This is important for us because predecessors are being stored implicitly. 2. It is easy to operationalize a "branch with arguments" either when lowering to other languages, or when interpreting the IR. A branch with arguments is implemented as a sequence of stores from the arguments to the parameters of the target block (very similar to a call), followed by a jump to the block. Relevant to the above, this change also adds an interface for enumerating the predecessors or successors of a block in our CFG. Rather than use an auxliary structure, we directly use the information already encoded in the IR: * The sucessors of a block are the target label operands of its terminator instruction. In our IR this is a contiguous range of `IRUse`s, possible with a stride (to account for the way `switch` interleaves values and blocks). * The predecessors of a block are a subset of the uses of the block's value. Specifically, they are any uses that are on a terminator instruction, and within the range of values that represent the successor list of that instruction. One important limitation of the "blocks with arguments" model for handling phis is that it is really only convenient to stash extra arguments on an unconditional terminator instruction. This change works around this prob lem by breaking any "critical edges" - edges between a block with multiple successors and one with multiple predecessors. We assume that "phi" nodes will only ever be needed on a block with multiple predecessors, and because critical edges are broken, each of these predecessors will then have only a single successor, so its branch instruction can handle the extra arguments. This change introduces a notion of an "undefined" instruction in the IR. This is handled as an instruction rather than a value because I anticipate that we will want to distinguish different undefined values when it comes time to start issuing error messages (those messages will need to point to the variable that was used when undefined). * Fix expected test output. Another change was merged that enabled the `glsl-parameter-blocks` test, and its output is affected by our IR optimization work.
1 parent 1fbc73d commit be8b891

14 files changed

+1420
-81
lines changed

slang.h

+1
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,7 @@ namespace slang
12321232
#include "source/slang/emit.cpp"
12331233
#include "source/slang/ir.cpp"
12341234
#include "source/slang/ir-legalize-types.cpp"
1235+
#include "source/slang/ir-ssa.cpp"
12351236
#include "source/slang/legalize-types.cpp"
12361237
#include "source/slang/lexer.cpp"
12371238
#include "source/slang/mangle.cpp"

source/slang/emit.cpp

+112-2
Original file line numberDiff line numberDiff line change
@@ -6074,6 +6074,14 @@ emitDeclImpl(decl, nullptr);
60746074
emit(";\n");
60756075
break;
60766076

6077+
case kIROp_undefined:
6078+
{
6079+
auto type = inst->getType();
6080+
emitIRType(ctx, type, getIRName(inst));
6081+
emit(";\n");
6082+
}
6083+
break;
6084+
60776085
case kIROp_Var:
60786086
{
60796087
auto ptrType = inst->getType();
@@ -6202,6 +6210,37 @@ emitDeclImpl(decl, nullptr);
62026210
}
62036211
}
62046212

6213+
// When we are about to traverse an edge from one block to another,
6214+
// we need to emit the assignments that conceptually occur "along"
6215+
// the edge. In traditional SSA these are the phi nodes in the
6216+
// target block, while in our representation these use the arguments
6217+
// to the branch instruction to fill in the parameters of the target.
6218+
void emitPhiVarAssignments(
6219+
EmitContext* ctx,
6220+
UInt argCount,
6221+
IRUse* args,
6222+
IRBlock* targetBlock)
6223+
{
6224+
UInt argCounter = 0;
6225+
for (auto pp = targetBlock->getFirstParam(); pp; pp = pp->getNextParam())
6226+
{
6227+
UInt argIndex = argCounter++;
6228+
6229+
if (argIndex >= argCount)
6230+
{
6231+
assert(!"not enough arguments for branch");
6232+
break;
6233+
}
6234+
6235+
IRValue* arg = args[argIndex].usedValue;
6236+
6237+
emitIROperand(ctx, pp);
6238+
emit(" = ");
6239+
emitIROperand(ctx, arg);
6240+
emit(";\n");
6241+
}
6242+
}
6243+
62056244
// We want to emit a range of code in the IR, represented
62066245
// by the blocks that are logically in the interval [begin, end)
62076246
// which we consider as a single-entry multiple-exit region.
@@ -6305,6 +6344,14 @@ emitDeclImpl(decl, nullptr);
63056344
auto breakBlock = t->getBreakBlock();
63066345
auto continueBlock = t->getContinueBlock();
63076346

6347+
UInt argCount = t->getArgCount();
6348+
static const UInt kFixedArgCount = 3;
6349+
emitPhiVarAssignments(
6350+
ctx,
6351+
argCount - kFixedArgCount,
6352+
t->getArgs() + kFixedArgCount,
6353+
targetBlock);
6354+
63086355
if (auto loopControlDecoration = t->findDecoration<IRLoopControlDecoration>())
63096356
{
63106357
switch (loopControlDecoration->mode)
@@ -6388,7 +6435,20 @@ emitDeclImpl(decl, nullptr);
63886435
break;
63896436

63906437
case kIROp_break:
6391-
emit("break;\n");
6438+
{
6439+
auto t = (IRBreak*)terminator;
6440+
auto targetBlock = t->getTargetBlock();
6441+
6442+
UInt argCount = t->getArgCount();
6443+
static const UInt kFixedArgCount = 1;
6444+
emitPhiVarAssignments(
6445+
ctx,
6446+
argCount - kFixedArgCount,
6447+
t->getArgs() + kFixedArgCount,
6448+
targetBlock);
6449+
6450+
emit("break;\n");
6451+
}
63926452
return;
63936453

63946454
case kIROp_continue:
@@ -6405,6 +6465,15 @@ emitDeclImpl(decl, nullptr);
64056465
{
64066466
auto continueInst = (IRContinue*) terminator;
64076467
auto targetBlock = continueInst->getTargetBlock();
6468+
6469+
UInt argCount = continueInst->getArgCount();
6470+
static const UInt kFixedArgCount = 1;
6471+
emitPhiVarAssignments(
6472+
ctx,
6473+
argCount - kFixedArgCount,
6474+
continueInst->getArgs() + kFixedArgCount,
6475+
targetBlock);
6476+
64086477
IRBlock* loopHead = nullptr;
64096478
ctx->shared->irMapContinueTargetToLoopHead.TryGetValue(targetBlock, loopHead);
64106479
SLANG_ASSERT(loopHead);
@@ -6422,10 +6491,16 @@ emitDeclImpl(decl, nullptr);
64226491
auto t = (IRLoopTest*)terminator;
64236492

64246493
auto afterBlock = t->getTrueBlock();
6494+
auto exitBlock = t->getFalseBlock();
64256495

64266496
emit("if(");
64276497
emitIROperand(ctx, t->getCondition());
6428-
emit(")\n{} else break;\n");
6498+
emit(")\n{} else {\n");
6499+
emitIRStmtsForBlocks(
6500+
ctx,
6501+
exitBlock,
6502+
nullptr);
6503+
emit("}\n");
64296504

64306505
// Continue with the block after the test
64316506
block = afterBlock;
@@ -6440,6 +6515,16 @@ emitDeclImpl(decl, nullptr);
64406515
// block, or a backward edge to the top
64416516
// of a loop.
64426517
auto t = (IRUnconditionalBranch*)terminator;
6518+
auto targetBlock = t->getTargetBlock();
6519+
6520+
UInt argCount = t->getArgCount();
6521+
static const UInt kFixedArgCount = 1;
6522+
emitPhiVarAssignments(
6523+
ctx,
6524+
argCount - kFixedArgCount,
6525+
t->getArgs() + kFixedArgCount,
6526+
targetBlock);
6527+
64436528
block = t->getTargetBlock();
64446529
}
64456530
break;
@@ -6758,6 +6843,27 @@ emitDeclImpl(decl, nullptr);
67586843
}
67596844
}
67606845

6846+
void emitPhiVarDecls(
6847+
EmitContext* ctx,
6848+
IRFunc* func)
6849+
{
6850+
// We will skip the first block, since its parameters are
6851+
// the parameters of the whole function.
6852+
auto bb = func->getFirstBlock();
6853+
if (!bb)
6854+
return;
6855+
bb = bb->getNextBlock();
6856+
6857+
for (; bb; bb = bb->getNextBlock())
6858+
{
6859+
for (auto pp = bb->getFirstParam(); pp; pp = pp->getNextParam())
6860+
{
6861+
emitIRType(ctx, pp->getType(), getIRName(pp));
6862+
emit(";\n");
6863+
}
6864+
}
6865+
}
6866+
67616867
void emitIRSimpleFunc(
67626868
EmitContext* ctx,
67636869
IRFunc* func)
@@ -6816,6 +6922,10 @@ emitDeclImpl(decl, nullptr);
68166922
{
68176923
emit("\n{\n");
68186924

6925+
// HACK: forward-declare all the local variables needed for the
6926+
// prameters of non-entry blocks.
6927+
emitPhiVarDecls(ctx, func);
6928+
68196929
// Need to emit the operations in the blocks of the function
68206930

68216931
emitIRStmtsForBlocks(ctx, func->getFirstBlock(), nullptr);

source/slang/ir-inst-defs.h

+2
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ INST(IntLit, integer_constant, 0, 0)
9696
INST(FloatLit, float_constant, 0, 0)
9797
INST(decl_ref, decl_ref, 0, 0)
9898

99+
INST(undefined, undefined, 0, 0)
100+
99101
INST(specialize, specialize, 2, 0)
100102
INST(lookup_interface_method, lookup_interface_method, 2, 0)
101103
INST(lookup_witness_table, lookup_witness_table, 2, 0)

source/slang/ir-insts.h

+16
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,15 @@ struct IRWitnessTable : IRGlobalValue
326326
IRValueList<IRWitnessTableEntry> entries;
327327
};
328328

329+
// An instruction that yields an undefined value.
330+
//
331+
// Note that we make this an instruction rather than a value,
332+
// so that we will be able to identify a variable that is
333+
// used when undefined.
334+
struct IRUndefined : IRInst
335+
{
336+
};
337+
329338
// Description of an instruction to be used for global value numbering
330339
struct IRInstKey
331340
{
@@ -394,6 +403,7 @@ struct IRBuilder
394403
IRValue* getBoolValue(bool value);
395404
IRValue* getIntValue(IRType* type, IRIntegerValue value);
396405
IRValue* getFloatValue(IRType* type, IRFloatingPointValue value);
406+
397407
IRValue* getDeclRefVal(
398408
DeclRefBase const& declRef);
399409
IRValue* getTypeVal(IRType* type); // create an IR value that represents a type
@@ -443,6 +453,10 @@ struct IRBuilder
443453
UInt argCount,
444454
IRValue* const* args);
445455

456+
IRUndefined* emitUndefined(IRType* type);
457+
458+
459+
446460
IRModule* createModule();
447461

448462
IRFunc* createFunc();
@@ -458,6 +472,8 @@ struct IRBuilder
458472
IRBlock* createBlock();
459473
IRBlock* emitBlock();
460474

475+
IRParam* createParam(
476+
IRType* type);
461477
IRParam* emitParam(
462478
IRType* type);
463479

0 commit comments

Comments
 (0)