Skip to content

Commit 18709fb

Browse files
author
Tim Foley
authored
A bunch of work to resolve shader-slang#569 (shader-slang#576)
* render-test should not fail on HLSL compiler *warnings* The logic in `render-test` that invokes `D3DCompile` was causing a test to fail if it produced any warnings (not just if compilation fails). Warning output can be dealt with by the test runner, since it will compare output between runs anyway, and it is useful to be able to run something through `render-test` that compiles with warnings. * Be more careful about deleting IR instructions There was an `IRInst::deallocate()` method that had a precondition that the instruction should already be removed from its parent and clear out all its operands before calling, but it wasn't checking this and the few call sites weren't doing things right either. I consolidated things on `IRInst::removeAndDeallocate()` which does all the things: removes from the parent, clear out operands, and then deallocates. I also made sure to clear out the type operand. This clears up some crashing issues where passes were removing instructions but those instructions would still show up as users of other instructions. * Don't emit bitwise not for non-Boolean types It seems like the logic in `emit.cpp` messed things up and decided that `Not` (the IR instruction that is equivalent to `!` in the AST) should emit as `!` for Boolean types and `~` for other types, but this makes no sense (e.g., `~(a & 1)` is very different from `!(a & 1)`, even when interpreted as a condition). It seems like this logic was intended for the `BitNot` case, where `~a` and `!a` are actually equivalent for Boolean values (but a target language might not like `~a` on `bool` values). Maybe the original plan was that the `Not` instruction should only apply to Boolean values in the first place, and that other values should be converted to `bool` (or a vector of `bool`) before applying `Not`, but even in that case the emit logic makes no sense. This caused an actual problem for one of my test cases, so it was important to fix it now. * Fix issue with cached resolution for overoaded operators The basic problem was that the lookup logic was forming a key based on the *first* definition it found for the overloaded operator, but that means that when processing a prefix `++a` call we might look up the *postfix* definition of `operator++` and decide to use its opcode as the key. This "fixes" the logic by looking for the first definition with a "compatible" definition (e.g., a `__prefix` function if we are checking a `PrefixExpr`), and then uses its opcode. A better fix in the long run would be to make the cache just be keyed on the operator name and the "fixity" of the expression (prefix, postfix, or infix). * Introduce an intermediate structured control-flow representation The code previously used a single function called `emitIRStmtsForBlocks` in `emit.cpp` that would take a logical sub-graph of the CFG and emit it as high-level statements. It would do this by recognizing operations like coniditional branches that it could turn into high-level `if` statements, etc. The main problem with this function was that it mixed together the logic for how we restructure the program with the logic for how we emit high-level code from that structure. This change splits those two parts of the algorithm by introducing an intermediate data structure: a tree of `Region`s, which represent single-entry regions of the CFG. There are subclasses of `Region` corresponding to various structured control-flow constructs, and then a leaf case that wraps a single `IRBlock`. The new function `generateRegionsForIRBlocks()` (in `ir-restructure.cpp`) now handles the restructuring work, by building one or more `Region`s to represent a sub-graph, while `emitRegion()` handles emitting HLSL/GLSL source code from a region. Splitting things in this way opens up some opportunities for future changes: * We can expand the set of IR control-flow constructs allowed, so long as we can still generate structure `Region`s from them, without having to mess with the emit logic (e.g., we could start to support multi-level `break` by introducing temporaries as needed). In the limit we can generate our `Region`s using something like the "Relooper" algorithm. * We can emit to other representations while retaining the same control-flow restructuring support. E.g., if we drop the structured information from the IR, then emitting to SPIR-V for Vulkan would require us to use the strucured control-flow information from these `Region`s. * We can do analysis that needs to understand `Region` structure. This is relevant to issue shader-slang#569, which was what prompted me to start on this work. Now that we have a representation of the nesting of `Region`s, we can use it to reason about visibility of values between blocks. During development of this change I ran into a gotcha, in that I had been assuming each IR block would map to a single `Region`, forgetting that our current lowering of "continue clauses" in `for` loops leads to them being duplicated. The `Region` representation handles this by having a linked-list struct mapping IR blocks to the `SimpleRegion`s that represent them. I added a test case that includes a `for` loop with a continue clause that is reached along multiple paths just to make sure that we continue to support that case. The compiler output should not change as a result of this work; this is supposed to be a pure refactoring change. * Add a pass to resolve scoping issues in generated code Fixes shader-slang#569 The basic problem arises because the structured control flow that we output in high-level HLSL/GLSL doesn't match the "scoping" rules of an SSA IR. In particular, SSA says that a value can be used in any block that is dominated by the definition, but in the presence of `break` and `continue` statements it is easy to construct cases where a block dominates something that is not in its scope for structured control flow. Consider: ```hlsl for(;;) { int a = xyz; if(a) { int b = a; break; } int c = a; } int d = b; ``` This program is invalid as HLSL, because the variable `b` is referenced outside of its scope, but if we look at the CFG for this function, it is clear that the block that computes `b` dominated the block that computes `d`. IR optimizations can easily create code like this, so we need to be ready for it. The previous change added an explicit `Region` structure to represent the structured control flow that we re-form out of the IR, and this change adds a pass that exploits the structuring information to detect cases like the above and introduce temporaries to fix the scoping issue. For example, the pass would change the earlier code block into something like: ```hlsl int tmp; for(;;) { int a = xyz; if(a) { int b = a; tmp = b; break; } int c = a; } int d = tmp; ``` That is, we introduce a new `tmp` variable at a scope "above" both the definition and use of `b`, and then we copy `b` into that temporary right where it is computed, and then use the temporary instead of the original `b` at the use site. A few details that came up during the implementation: * Downstream compilers may get confused by code like the above, and complain that `tmp` may be used before it is initialized, even though the very definition of dominators in a CFG means we don't have to worry about it. Still, I introduced some one-off code to initialize the temporaries just to silence spurious warnings coming from fxc. * We need to be careful not to apply this logic to "phi nodes" (the parameters of basic blocks) since they will already be turned into temporaries by the emit logic, and trying to introduce temporaries with this pass led to broken code (I still need to investigate why). It may be that a future version of this pass should also take the code out of SSA form, so that we can introduce both kinds of temporaries in a single pass (and maybe eliminate some unnecessary variables by doing basic register allocation). There is another transformation that could fix some issues of this kind, by moving code out of a structured control-flow construct and to the "join point" after it. For example, we could turn our loop from the start of this commit message into: ```hlsl for(;;) { int a = xyz; if(a) { break; } int c = a; } int b = a; int d = b; ``` Moving the definition of `b` to after the loop is possible because there is no way to get out of the loop without executing that code anyway. Now the scoping issue for `d`'s use of `b` has gone away, but of course we've introduced a *new* scoping issue for `a`, when it gets used by `b`. Adding a pass to re-arrange control flow like this could reduce the cases where we have to apply the current pass, but it wouldn't eliminate them entirely. That means such a pass can be deferred to future work. This change includes a test case the reproduces the original issue, so that we can confirm the fix works.
1 parent d7515c3 commit 18709fb

17 files changed

+1749
-445
lines changed

source/slang/check.cpp

+41-13
Original file line numberDiff line numberDiff line change
@@ -131,30 +131,58 @@ namespace Slang
131131
}
132132
bool fromOperatorExpr(OperatorExpr* opExpr)
133133
{
134+
// First, lets see if the argument types are ones
135+
// that we can encode in our space of keys.
134136
args[0].aggVal = 0;
135137
args[1].aggVal = 0;
136138
if (opExpr->Arguments.Count() > 2)
137139
return false;
140+
141+
for (UInt i = 0; i < opExpr->Arguments.Count(); i++)
142+
{
143+
if (!args[i].fromType(opExpr->Arguments[i]->type.Ptr()))
144+
return false;
145+
}
146+
147+
// Next, lets see if we can find an intrinsic opcode
148+
// attached to an overloaded definition (filtered for
149+
// definitions that could conceivably apply to us).
150+
//
151+
// TODO: This should really be pased on the operator name
152+
// plus fixity, rather than the intrinsic opcode...
153+
//
154+
// We will need to reject postfix definitions for prefix
155+
// operators, and vice versa, to ensure things work.
156+
//
157+
auto prefixExpr = opExpr->As<PrefixExpr>();
158+
auto postfixExpr = opExpr->As<PostfixExpr>();
159+
138160
if (auto overloadedBase = opExpr->FunctionExpr->As<OverloadedExpr>())
139161
{
140-
Decl* funcDecl = overloadedBase->lookupResult2.item.declRef.decl;
141-
if (auto genDecl = funcDecl->As<GenericDecl>())
142-
funcDecl = genDecl->inner.Ptr();
143-
if (auto intrinsicOp = funcDecl->FindModifier<IntrinsicOpModifier>())
162+
for(auto item : overloadedBase->lookupResult2 )
144163
{
145-
operatorName = intrinsicOp->op;
146-
for (UInt i = 0; i < opExpr->Arguments.Count(); i++)
164+
// Look at a candidate definition to be called and
165+
// see if it gives us a key to work with.
166+
//
167+
Decl* funcDecl = overloadedBase->lookupResult2.item.declRef.decl;
168+
if (auto genDecl = funcDecl->As<GenericDecl>())
169+
funcDecl = genDecl->inner.Ptr();
170+
171+
// Reject definitions that have the wrong fixity.
172+
//
173+
if(prefixExpr && !funcDecl->FindModifier<PrefixModifier>())
174+
continue;
175+
if(postfixExpr && !funcDecl->FindModifier<PostfixModifier>())
176+
continue;
177+
178+
if (auto intrinsicOp = funcDecl->FindModifier<IntrinsicOpModifier>())
147179
{
148-
if (!args[i].fromType(opExpr->Arguments[i]->type.Ptr()))
149-
return false;
180+
operatorName = intrinsicOp->op;
181+
return true;
150182
}
151183
}
152-
else
153-
{
154-
return false;
155-
}
156184
}
157-
return true;
185+
return false;
158186
}
159187
};
160188

source/slang/diagnostic-defs.h

+2
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,8 @@ DIAGNOSTIC(51090, Error, cannotGenerateCodeForExternComponentType, "cannot gener
352352
DIAGNOSTIC(51091, Error, typeCannotBePlacedInATexture, "type '$0' cannot be placed in a texture.")
353353
DIAGNOSTIC(51092, Error, stageDoesntHaveInputWorld, "'$0' doesn't appear to have any input world");
354354

355+
DIAGNOSTIC(52000, Error, multiLevelBreakUnsupported, "control flow appears to require multi-level `break`, which Slang does not yet support");
356+
355357

356358
// 99999 - Internal compiler errors, and not-yet-classified diagnostics.
357359

0 commit comments

Comments
 (0)