diff options
| author | Tim Foley <tfoleyNV@users.noreply.github.com> | 2018-02-07 14:37:37 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-02-07 14:37:37 -0800 |
| commit | be8b891c4e0b7541a1c5f1aafa6f562113d5cdcb (patch) | |
| tree | 471279604e75ba6c28bf999da09fa57955b09757 /source/slang/emit.cpp | |
| parent | 1fbc73d96fbc0a199d823dfb38fc8f02bf7ada0a (diff) | |
Generate SSA form for IR functions (#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.
Diffstat (limited to 'source/slang/emit.cpp')
| -rw-r--r-- | source/slang/emit.cpp | 114 |
1 files changed, 112 insertions, 2 deletions
diff --git a/source/slang/emit.cpp b/source/slang/emit.cpp index f9f24fcf6..eaabd520c 100644 --- a/source/slang/emit.cpp +++ b/source/slang/emit.cpp @@ -6074,6 +6074,14 @@ emitDeclImpl(decl, nullptr); emit(";\n"); break; + case kIROp_undefined: + { + auto type = inst->getType(); + emitIRType(ctx, type, getIRName(inst)); + emit(";\n"); + } + break; + case kIROp_Var: { auto ptrType = inst->getType(); @@ -6202,6 +6210,37 @@ emitDeclImpl(decl, nullptr); } } + // When we are about to traverse an edge from one block to another, + // we need to emit the assignments that conceptually occur "along" + // the edge. In traditional SSA these are the phi nodes in the + // target block, while in our representation these use the arguments + // to the branch instruction to fill in the parameters of the target. + void emitPhiVarAssignments( + EmitContext* ctx, + UInt argCount, + IRUse* args, + IRBlock* targetBlock) + { + UInt argCounter = 0; + for (auto pp = targetBlock->getFirstParam(); pp; pp = pp->getNextParam()) + { + UInt argIndex = argCounter++; + + if (argIndex >= argCount) + { + assert(!"not enough arguments for branch"); + break; + } + + IRValue* arg = args[argIndex].usedValue; + + emitIROperand(ctx, pp); + emit(" = "); + emitIROperand(ctx, arg); + emit(";\n"); + } + } + // We want to emit a range of code in the IR, represented // by the blocks that are logically in the interval [begin, end) // which we consider as a single-entry multiple-exit region. @@ -6305,6 +6344,14 @@ emitDeclImpl(decl, nullptr); auto breakBlock = t->getBreakBlock(); auto continueBlock = t->getContinueBlock(); + UInt argCount = t->getArgCount(); + static const UInt kFixedArgCount = 3; + emitPhiVarAssignments( + ctx, + argCount - kFixedArgCount, + t->getArgs() + kFixedArgCount, + targetBlock); + if (auto loopControlDecoration = t->findDecoration<IRLoopControlDecoration>()) { switch (loopControlDecoration->mode) @@ -6388,7 +6435,20 @@ emitDeclImpl(decl, nullptr); break; case kIROp_break: - emit("break;\n"); + { + auto t = (IRBreak*)terminator; + auto targetBlock = t->getTargetBlock(); + + UInt argCount = t->getArgCount(); + static const UInt kFixedArgCount = 1; + emitPhiVarAssignments( + ctx, + argCount - kFixedArgCount, + t->getArgs() + kFixedArgCount, + targetBlock); + + emit("break;\n"); + } return; case kIROp_continue: @@ -6405,6 +6465,15 @@ emitDeclImpl(decl, nullptr); { auto continueInst = (IRContinue*) terminator; auto targetBlock = continueInst->getTargetBlock(); + + UInt argCount = continueInst->getArgCount(); + static const UInt kFixedArgCount = 1; + emitPhiVarAssignments( + ctx, + argCount - kFixedArgCount, + continueInst->getArgs() + kFixedArgCount, + targetBlock); + IRBlock* loopHead = nullptr; ctx->shared->irMapContinueTargetToLoopHead.TryGetValue(targetBlock, loopHead); SLANG_ASSERT(loopHead); @@ -6422,10 +6491,16 @@ emitDeclImpl(decl, nullptr); auto t = (IRLoopTest*)terminator; auto afterBlock = t->getTrueBlock(); + auto exitBlock = t->getFalseBlock(); emit("if("); emitIROperand(ctx, t->getCondition()); - emit(")\n{} else break;\n"); + emit(")\n{} else {\n"); + emitIRStmtsForBlocks( + ctx, + exitBlock, + nullptr); + emit("}\n"); // Continue with the block after the test block = afterBlock; @@ -6440,6 +6515,16 @@ emitDeclImpl(decl, nullptr); // block, or a backward edge to the top // of a loop. auto t = (IRUnconditionalBranch*)terminator; + auto targetBlock = t->getTargetBlock(); + + UInt argCount = t->getArgCount(); + static const UInt kFixedArgCount = 1; + emitPhiVarAssignments( + ctx, + argCount - kFixedArgCount, + t->getArgs() + kFixedArgCount, + targetBlock); + block = t->getTargetBlock(); } break; @@ -6758,6 +6843,27 @@ emitDeclImpl(decl, nullptr); } } + void emitPhiVarDecls( + EmitContext* ctx, + IRFunc* func) + { + // We will skip the first block, since its parameters are + // the parameters of the whole function. + auto bb = func->getFirstBlock(); + if (!bb) + return; + bb = bb->getNextBlock(); + + for (; bb; bb = bb->getNextBlock()) + { + for (auto pp = bb->getFirstParam(); pp; pp = pp->getNextParam()) + { + emitIRType(ctx, pp->getType(), getIRName(pp)); + emit(";\n"); + } + } + } + void emitIRSimpleFunc( EmitContext* ctx, IRFunc* func) @@ -6816,6 +6922,10 @@ emitDeclImpl(decl, nullptr); { emit("\n{\n"); + // HACK: forward-declare all the local variables needed for the + // prameters of non-entry blocks. + emitPhiVarDecls(ctx, func); + // Need to emit the operations in the blocks of the function emitIRStmtsForBlocks(ctx, func->getFirstBlock(), nullptr); |
