From 59a4c0caed15b607990fdc1990992fb1b944ae96 Mon Sep 17 00:00:00 2001 From: Tim Foley Date: Tue, 14 Nov 2017 11:33:36 -0800 Subject: IR: add support for `switch` statements (#278) * IR: add support for `switch` statements Fixes #273 This is just something we hadn't gotten to yet on the IR. The actual design of the instruction is unsurprising (once you take into consideration the requirement for structured control flow). A `switch` instruction takes the form: switch [ ]* Where `condition` is the value to switch on, `breakLabel` is the "join point" after the original `switch` statement, `defaultLabel` is where to go if the value doesn't match any case, and each pair of `caseVal` and `caseLabel` is what to do on a particular value. It is required that `caseVal` be a literal, but this isn't currently being enforced in the IR (the front-end should be making a check and constant-folding the case labels). For structured control flow, we also make the assumption that the cases are in order: cases with the same label must be grouped together, and any case that falls through to another must come right before it. Given this representation, the emit logic can reconstruct a `switch` statement with relative ease, given the machinery we already have. It makes sure to group together case values with the same label (again, assuming they are contiguous), and will insert the `default:` label in with whatever group it belongs to. Actually emitting code for a `switch` statement seems superficially simple, until you realize that a complete implementation needs to handle stuff like "Duff's Device." The current implementation makes the assumption that all `case` and `default` statements are directly nested under a `switch`, and that there is no way for control flow to enter a case except by the `switch` itself, or fall-through. In order to facilitate the grouping of cases in the IR-to-HLSL emit logic, the AST-to-IR lowering logic tries to detect cases where there are multiple `case`s in a row, and emit only a single label for them. One big/annoying gotcha is that we don't properly handle the case where a `default:` case has a non-trivial fall-throguh to another case. That seems fine for now since HLSL doesn't support fall-through anyway, but it probably needs to get detected somewhere in the Slang compiler (e.g., maybe we should add a diagnostic pass over the IR that detects target-specific problems like that and emits errors). * IR: Add support for empty statements. - Add empty statement in `lower-to-ir.cpp` - Go ahead and eliminate the statement catch-all and explicitly enumerate the cases we don't support - Fix up parser for block statements so that it doesn't leave a null statement as the body of a `{}` - Add an empty statement to one of the cases for the `switch` test, to ensure we are testing empty statements --- source/slang/emit.cpp | 136 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) (limited to 'source/slang/emit.cpp') diff --git a/source/slang/emit.cpp b/source/slang/emit.cpp index a8cd11af4..84d8f113e 100644 --- a/source/slang/emit.cpp +++ b/source/slang/emit.cpp @@ -2896,7 +2896,7 @@ struct EmitVisitor } else if (auto defaultStmt = stmt.As()) { - Emit("default:{}\n"); + Emit("default:\n"); return; } else if (auto breakStmt = stmt.As()) @@ -5643,8 +5643,142 @@ emitDeclImpl(decl, nullptr); break; case kIROp_conditionalBranch: + // Note: We currently do not generate any plain + // `conditionalBranch` instructions when lowering + // to IR, because these would not have the annotations + // needed to be able to emit high-level control + // flow from them. SLANG_UNEXPECTED("terminator inst"); return; + + + case kIROp_switch: + { + // A `switch` instruction will always translate + // to a `switch` statement, but we need to + // take some care to emit the `case`s in ways + // that avoid code duplication. + + // TODO: Eventually, the "right" way to handle `switch` + // statements while being more robust about Duff's Device, etc. + // would be to register each of the case labels in a lookup + // table, and then walk the blocks in the region between + // the `switch` and the `break` and then whenever we see a block + // that matches one of the registered labels, emit the appropriate + // `case ...:` or `default:` label. + + auto t = (IRSwitch*) terminator; + + // Extract the fixed arguments. + auto conditionVal = t->getCondition(); + auto breakLabel = t->getBreakLabel(); + auto defaultLabel = t->getDefaultLabel(); + + // We need to track whether we've dealt with + // the `default` case already. + bool defaultLabelHandled = false; + + // If the `default` case just branches to + // the join point, then we don't need to + // do anything with it. + if(defaultLabel == breakLabel) + defaultLabelHandled = true; + + // Emit the start of our statement. + emit("switch("); + emitIROperand(ctx, conditionVal); + emit(")\n{\n"); + + // Now iterate over the `case`s of the branch + UInt caseIndex = 0; + UInt caseCount = t->getCaseCount(); + while(caseIndex < caseCount) + { + // We are going to extract one case here, + // but we might need to fold additional + // cases into it, if they share the + // same label. + // + // Note: this makes assumptions that the + // IR code generator orders cases such + // that: (1) cases with the same label + // are consecutive, and (2) any case + // that "falls through" to another must + // come right before it in the list. + auto caseVal = t->getCaseValue(caseIndex); + auto caseLabel = t->getCaseLabel(caseIndex); + caseIndex++; + + // Emit the `case ...:` for this case, and any + // others that share the same label + for(;;) + { + emit("case "); + emitIROperand(ctx, caseVal); + emit(":\n"); + + if(caseIndex >= caseCount) + break; + + auto nextCaseLabel = t->getCaseLabel(caseIndex); + if(nextCaseLabel != caseLabel) + break; + + caseVal = t->getCaseValue(caseIndex); + caseIndex++; + } + + // The label for the current `case` might also + // be the label used by the `default` case, so + // check for that here. + if(caseLabel == defaultLabel) + { + emit("default:\n"); + defaultLabelHandled = true; + } + + // Now we need to emit the statements that make + // up this case. The 99% case will be that it + // will terminate with a `break` (or a `return`, + // `continue`, etc.) and so we can pass in + // `nullptr` for the ending block. + IRBlock* caseEndLabel = nullptr; + + // However, there is also the possibility that + // this case will fall through to the next, and + // so we need to prepare for that possibility here. + // + // If there is a next case, then we will set its + // label up as the "end" label when emitting + // the statements inside the block. + if(caseIndex < caseCount) + { + caseEndLabel = t->getCaseLabel(caseIndex); + } + + // Now emit the statements for this case. + emit("{\n"); + emitIRStmtsForBlocks(ctx, caseLabel, caseEndLabel); + emit("}\n"); + } + + // If we've gone through all the cases and haven't + // managed to encounter the `default:` label, + // then assume it is a distinct case and handle it here. + if(!defaultLabelHandled) + { + emit("default:\n"); + emit("{\n"); + emitIRStmtsForBlocks(ctx, defaultLabel, breakLabel); + emit("break;\n"); + emit("}\n"); + } + + emit("}\n"); + block = breakLabel; + + } + break; } // If we reach this point, then we've emitted -- cgit v1.2.3