From 083eecee3f56b90c7011895f53aaafa9db15856e Mon Sep 17 00:00:00 2001 From: Mukund Keshava Date: Sun, 11 May 2025 02:29:37 +0530 Subject: Add debug information for slang inling (#6621) --- source/slang/slang-emit-c-like.cpp | 15 ++ source/slang/slang-emit-spirv-ops-debug-info-ext.h | 86 ++++++- source/slang/slang-emit-spirv.cpp | 212 +++++++++++++---- source/slang/slang-emit.cpp | 4 + source/slang/slang-ir-autodiff-fwd.cpp | 5 + source/slang/slang-ir-autodiff-rev.cpp | 5 + source/slang/slang-ir-autodiff.cpp | 4 +- source/slang/slang-ir-inline.cpp | 258 ++++++++++++++++++++- source/slang/slang-ir-inst-defs.h | 8 + source/slang/slang-ir-inst-pass-base.h | 2 + source/slang/slang-ir-insts.h | 74 ++++++ source/slang/slang-ir-strip-debug-info.cpp | 4 + source/slang/slang-ir-validate.cpp | 1 + source/slang/slang-ir.cpp | 52 ++++- source/slang/slang-lower-to-ir.cpp | 25 ++ .../forceinline-basic-block-inline-order.slang | 39 ++++ .../function-calls/forceinline-basic-block.slang | 37 +++ .../forceinline-multiple-blocks.slang | 79 +++++++ .../forceinline-multiple-cases.slang | 141 +++++++++++ 19 files changed, 1000 insertions(+), 51 deletions(-) create mode 100644 tests/language-feature/function-calls/forceinline-basic-block-inline-order.slang create mode 100644 tests/language-feature/function-calls/forceinline-basic-block.slang create mode 100644 tests/language-feature/function-calls/forceinline-multiple-blocks.slang create mode 100644 tests/language-feature/function-calls/forceinline-multiple-cases.slang diff --git a/source/slang/slang-emit-c-like.cpp b/source/slang/slang-emit-c-like.cpp index 18dbcf1cd..56668d092 100644 --- a/source/slang/slang-emit-c-like.cpp +++ b/source/slang/slang-emit-c-like.cpp @@ -3168,6 +3168,11 @@ void CLikeSourceEmitter::_emitInst(IRInst* inst) case kIROp_DebugLine: case kIROp_DebugVar: case kIROp_DebugValue: + case kIROp_DebugInlinedAt: + case kIROp_DebugScope: + case kIROp_DebugNoScope: + case kIROp_DebugInlinedVariable: + case kIROp_DebugFunction: break; case kIROp_Unmodified: @@ -5151,6 +5156,16 @@ void CLikeSourceEmitter::ensureGlobalInst( return; case kIROp_ThisType: return; + case kIROp_DebugInlinedAt: + case kIROp_DebugScope: + case kIROp_DebugNoScope: + case kIROp_DebugFunction: + case kIROp_DebugVar: + case kIROp_DebugLine: + case kIROp_DebugSource: + case kIROp_DebugValue: + case kIROp_DebugInlinedVariable: + return; default: break; } diff --git a/source/slang/slang-emit-spirv-ops-debug-info-ext.h b/source/slang/slang-emit-spirv-ops-debug-info-ext.h index 41c6e1faa..d8826afdc 100644 --- a/source/slang/slang-emit-spirv-ops-debug-info-ext.h +++ b/source/slang/slang-emit-spirv-ops-debug-info-ext.h @@ -428,12 +428,74 @@ SpvInst* emitOpDebugScope( IRInst* inst, const T& idResultType, SpvInst* set, - SpvInst* scope) + SpvInst* scope, + SpvInst* inlinedAt = nullptr) { static_assert(isSingular); + if (inlinedAt) + { + return emitInst( + parent, + inst, + SpvOpExtInst, + idResultType, + kResultID, + set, + SpvWord(23), + scope, + inlinedAt); + } return emitInst(parent, inst, SpvOpExtInst, idResultType, kResultID, set, SpvWord(23), scope); } +template +SpvInst* emitOpDebugNoScope( + SpvInstParent* parent, + IRInst* inst, + const T& idResultType, + SpvInst* set) +{ + static_assert(isSingular); + return emitInst(parent, inst, SpvOpExtInst, idResultType, kResultID, set, SpvWord(24)); +} + +template +SpvInst* emitOpDebugInlinedAt( + SpvInstParent* parent, + IRInst* inst, + const T& idResultType, + SpvInst* set, + IRInst* line, + SpvInst* scope, + SpvInst* inlinedAt = nullptr) +{ + static_assert(isSingular); + if (inlinedAt) + { + return emitInst( + parent, + inst, + SpvOpExtInst, + idResultType, + kResultID, + set, + SpvWord(25), + line, + scope, + inlinedAt); + } + return emitInst( + parent, + inst, + SpvOpExtInst, + idResultType, + kResultID, + set, + SpvWord(25), + line, + scope); +} + template SpvInst* emitOpDebugLocalVariable( SpvInstParent* parent, @@ -484,6 +546,28 @@ SpvInst* emitOpDebugLocalVariable( flags); } +template +SpvInst* emitOpDebugInlinedVariable( + SpvInstParent* parent, + IRInst* inst, + const T& idResultType, + SpvInst* set, + IRInst* variable, + IRInst* inlined) +{ + static_assert(isSingular); + return emitInst( + parent, + inst, + SpvOpExtInst, + idResultType, + kResultID, + set, + SpvWord(27), + variable, + inlined); +} + template SpvInst* emitOpDebugValue( SpvInstParent* parent, diff --git a/source/slang/slang-emit-spirv.cpp b/source/slang/slang-emit-spirv.cpp index ad1f1bdf8..bd9b23b2d 100644 --- a/source/slang/slang-emit-spirv.cpp +++ b/source/slang/slang-emit-spirv.cpp @@ -1952,6 +1952,17 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex case kIROp_IndicesType: case kIROp_PrimitivesType: return nullptr; + case kIROp_DebugFunction: + return emitDebugFunction( + getSection(SpvLogicalSectionID::ConstantsAndTypes), + nullptr, + nullptr, + as(inst), + nullptr); + case kIROp_DebugInlinedAt: + return emitDebugInlinedAt( + getSection(SpvLogicalSectionID::ConstantsAndTypes), + as(inst)); default: { if (as(inst)) @@ -3026,7 +3037,17 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex } } // DebugInfo. - funcDebugScope = emitDebugFunction(spvBlock, spvFunc, irFunc); + IRDebugFunction* irDebugFunc = nullptr; + if (auto debugFuncDecor = irFunc->findDecoration()) + { + irDebugFunc = as(debugFuncDecor->getDebugFunc()); + } + funcDebugScope = emitDebugFunction( + getSection(SpvLogicalSectionID::ConstantsAndTypes), + spvBlock, + spvFunc, + irDebugFunc, + irFunc->getDataType()); } if (funcDebugScope) { @@ -4083,6 +4104,25 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex operands.getArrayView()); } break; + case kIROp_DebugFunction: + return emitDebugFunction( + getSection(SpvLogicalSectionID::ConstantsAndTypes), + nullptr, + nullptr, + as(inst), + nullptr); + case kIROp_DebugInlinedAt: + return emitDebugInlinedAt( + getSection(SpvLogicalSectionID::ConstantsAndTypes), + as(inst)); + case kIROp_DebugScope: + return emitDebugScope(parent, as(inst)); + case kIROp_DebugNoScope: + return emitDebugNoScope(parent); + case kIROp_DebugInlinedVariable: + return emitDebugInlinedVariable( + getSection(SpvLogicalSectionID::ConstantsAndTypes), + as(inst)); } if (result) emitDecorations(inst, getID(result)); @@ -7395,6 +7435,136 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex return m_nullDwarfExpr; } + SpvInst* emitDebugScope(SpvInstParent* parent, IRDebugScope* debugScope) + { + auto inlinedAt = ensureInst(debugScope->getInlinedAt()); + if (!inlinedAt) + return nullptr; + + SpvInst* scope = ensureInst(debugScope->getScope()); + if (!scope) + return nullptr; + + return emitOpDebugScope( + parent, + nullptr, + m_voidType, + getNonSemanticDebugInfoExtInst(), + scope, + inlinedAt); + } + + + SpvInst* emitDebugFunction( + SpvInstParent* parent, + SpvInst* firstBlock, + SpvInst* spvFunc, + IRDebugFunction* debugFunc, + IRFuncType* debugType) + { + SpvInst* debugFuncInfo = nullptr; + if (debugFunc && m_mapIRInstToSpvInst.tryGetValue(debugFunc, debugFuncInfo)) + { + return debugFuncInfo; + } + + auto scope = findDebugScope(debugFunc); + if (!scope) + return nullptr; + + SpvInst* neededDebugType = nullptr; + if (debugType) + { + neededDebugType = emitDebugType(debugType); + } + else + { + neededDebugType = emitDebugType(as(debugFunc->getDebugType())); + } + + IRBuilder builder(debugFunc); + debugFuncInfo = emitOpDebugFunction( + parent, + debugFunc, + m_voidType, + getNonSemanticDebugInfoExtInst(), + debugFunc->getName(), + neededDebugType, + debugFunc->getFile(), + debugFunc->getLine(), + debugFunc->getCol(), + scope, + debugFunc->getName(), + builder.getIntValue(builder.getUIntType(), 0), + debugFunc->getLine()); + + if (firstBlock && spvFunc) + { + emitOpDebugFunctionDefinition( + firstBlock, + nullptr, + m_voidType, + getNonSemanticDebugInfoExtInst(), + debugFuncInfo, + spvFunc); + } + return debugFuncInfo; + } + + SpvInst* emitDebugNoScope(SpvInstParent* parent) + { + return emitOpDebugNoScope(parent, nullptr, m_voidType, getNonSemanticDebugInfoExtInst()); + } + + SpvInst* emitDebugInlinedAt(SpvInstParent* parent, IRDebugInlinedAt* debugInlinedAt) + { + IRInst* lineInst = debugInlinedAt->getLine(); + + SpvInst* scope = nullptr; + if (as(debugInlinedAt->getDebugFunc())) + { + scope = ensureInst(debugInlinedAt->getDebugFunc()); + } + if (scope == nullptr) + { + scope = findDebugScope(debugInlinedAt); + } + + // If it's not chained to another IRDebugInlinedAt, we don't use this. + SpvInst* inlined = nullptr; + if (as(debugInlinedAt)->isOuterInlinedPresent()) + { + inlined = ensureInst(debugInlinedAt->getOuterInlinedAt()); + } + + return emitOpDebugInlinedAt( + parent, + debugInlinedAt, + m_voidType, + getNonSemanticDebugInfoExtInst(), + lineInst, + scope, + inlined); + } + + SpvInst* emitDebugInlinedVariable( + SpvInstParent* parent, + IRDebugInlinedVariable* debugInlinedVar) + { + // Get the operands from the IRDebugInlinedVariable instruction + IRInst* variable = debugInlinedVar->getVariable(); + IRInst* inlinedAt = debugInlinedVar->getInlinedAt(); + + // Emit the OpDebugInlinedVariable instruction + return emitOpDebugInlinedVariable( + parent, + debugInlinedVar, + m_voidType, + getNonSemanticDebugInfoExtInst(), + variable, + inlinedAt); + } + bool translateIRAccessChain( IRBuilder& builder, IRInst* baseType, @@ -7860,46 +8030,6 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex return result; } - SpvInst* emitDebugFunction(SpvInst* firstBlock, SpvInst* spvFunc, IRFunc* function) - { - auto scope = findDebugScope(function); - if (!scope) - return nullptr; - auto name = getName(function); - if (!name) - return nullptr; - auto debugLoc = function->findDecoration(); - if (!debugLoc) - return nullptr; - auto debugType = emitDebugType(function->getDataType()); - IRBuilder builder(function); - auto debugFunc = emitOpDebugFunction( - getSection(SpvLogicalSectionID::ConstantsAndTypes), - nullptr, - m_voidType, - getNonSemanticDebugInfoExtInst(), - name, - debugType, - debugLoc->getSource(), - debugLoc->getLine(), - debugLoc->getCol(), - scope, - name, - builder.getIntValue(builder.getUIntType(), 0), - debugLoc->getLine()); - registerDebugInst(function, debugFunc); - - emitOpDebugFunctionDefinition( - firstBlock, - nullptr, - m_voidType, - getNonSemanticDebugInfoExtInst(), - debugFunc, - spvFunc); - - return debugFunc; - } - SpvInst* emitSPIRVAsm(SpvInstParent* parent, IRSPIRVAsm* inst) { SpvInst* last = nullptr; diff --git a/source/slang/slang-emit.cpp b/source/slang/slang-emit.cpp index ac6336e9c..9410a8f45 100644 --- a/source/slang/slang-emit.cpp +++ b/source/slang/slang-emit.cpp @@ -350,6 +350,10 @@ void calcRequiredLoweringPassSet( case kIROp_DebugLine: case kIROp_DebugLocationDecoration: case kIROp_DebugSource: + case kIROp_DebugInlinedAt: + case kIROp_DebugScope: + case kIROp_DebugNoScope: + case kIROp_DebugFunction: result.debugInfo = true; break; case kIROp_ResultType: diff --git a/source/slang/slang-ir-autodiff-fwd.cpp b/source/slang/slang-ir-autodiff-fwd.cpp index febd30d7e..df690a4e2 100644 --- a/source/slang/slang-ir-autodiff-fwd.cpp +++ b/source/slang/slang-ir-autodiff-fwd.cpp @@ -2184,6 +2184,11 @@ InstPair ForwardDiffTranscriber::transcribeInstImpl(IRBuilder* builder, IRInst* case kIROp_DebugLine: case kIROp_DebugVar: case kIROp_DebugValue: + case kIROp_DebugInlinedAt: + case kIROp_DebugScope: + case kIROp_DebugNoScope: + case kIROp_DebugInlinedVariable: + case kIROp_DebugFunction: case kIROp_GetArrayLength: case kIROp_SizeOf: case kIROp_AlignOf: diff --git a/source/slang/slang-ir-autodiff-rev.cpp b/source/slang/slang-ir-autodiff-rev.cpp index 519f796b4..773a7741f 100644 --- a/source/slang/slang-ir-autodiff-rev.cpp +++ b/source/slang/slang-ir-autodiff-rev.cpp @@ -275,6 +275,11 @@ InstPair BackwardDiffTranscriberBase::transcribeInstImpl(IRBuilder* builder, IRI case kIROp_WrapExistential: case kIROp_MakeExistential: case kIROp_MakeExistentialWithRTTI: + case kIROp_DebugInlinedAt: + case kIROp_DebugScope: + case kIROp_DebugNoScope: + case kIROp_DebugInlinedVariable: + case kIROp_DebugFunction: return transcribeNonDiffInst(builder, origInst); case kIROp_StructKey: diff --git a/source/slang/slang-ir-autodiff.cpp b/source/slang/slang-ir-autodiff.cpp index 6ad8c5dbb..c8dc3b480 100644 --- a/source/slang/slang-ir-autodiff.cpp +++ b/source/slang/slang-ir-autodiff.cpp @@ -965,7 +965,9 @@ IRInst* DifferentialPairTypeBuilder::lowerDiffPairType(IRBuilder* builder, IRTyp if (as(primalType) || as(primalType)) { List constraintTypes; - constraintTypes.add(this->commonDiffPairInterface); + auto diffPairInterfaceType = + cast(getOrCreateCommonDiffPairInterface(builder)); + constraintTypes.add(diffPairInterfaceType); return builder->getAssociatedType(constraintTypes.getArrayView()); } diff --git a/source/slang/slang-ir-inline.cpp b/source/slang/slang-ir-inline.cpp index 73cf6eb39..8575330f8 100644 --- a/source/slang/slang-ir-inline.cpp +++ b/source/slang/slang-ir-inline.cpp @@ -3,6 +3,8 @@ #include "../core/slang-performance-profiler.h" #include "slang-ir-ssa-simplification.h" +#include "slang-ir-util.h" + // This file provides general facilities for inlining function calls. // @@ -290,6 +292,124 @@ struct InliningPassBase return true; } + // Find an existing debug function for the given function + IRInst* findExistingDebugFunc(IRFunc* func) + { + if (auto debugFuncDecor = func->findDecoration()) + { + return debugFuncDecor->getDebugFunc(); + } + return nullptr; + } + + struct DebugInlineInfo + { + IRInst* newDebugInlinedAt = nullptr; + IRInst* calleeDebugFunc = nullptr; + }; + + // Sets up the initial debug information structures required *before* inlining a call site. + // + // This function performs the following steps: + // 1. Checks if the callee function has associated debug location information. + // 2. Obtain the call inst's existing debug information. + // 3. Finds the last `IRDebugLine` preceding the `call` instruction to determine the source + // location (line, col, file) of the call site. + // 4. Emits an `IRDebugInlinedAt` instruction with outer set to callDebugInlinedAt. + // 5. Inserts the newly created `IRDebugInlinedAt` instruction immediately *before* the `call` + // instruction. + DebugInlineInfo emitCalleeDebugInlinedAt(IRCall* call, IRFunc* callee, IRBuilder& builder) + { + IRDebugLine* lastDebugLine = nullptr; + IRInst* newDebugInlinedAt = nullptr; + IRInst* callerDebugFunc = nullptr; + IRInst* calleeDebugFunc = nullptr; + + if (!callee->findDecoration()) + { + return DebugInlineInfo(); + } + else + { + // Check if the call inst is part of an existing scope. If yes, then we restore + // that scope after the inlining of callee. This case can occur when we have out of + // order inlining. See forceinline-basic-block-inline-order.slang test for that use + // case. If we are travesing back the call inst and if we find a DebugNoScope, it means + // that there's another function that was inlined. We don't want that scope. If the call + // inst truly belongs to another DebugScope, then we should hit a DebugScope inst + // *before* we see a DebugNoScope + IRDebugScope* callDebugScope = nullptr; + builder.setInsertAfter(call); + for (IRInst* inst = call->getPrevInst(); inst; inst = inst->getPrevInst()) + { + if (as(inst)) + { + break; + } + if (as(inst)) + { + callDebugScope = as(inst); + builder.emitDebugScope( + callDebugScope->getScope(), + callDebugScope->getInlinedAt()); + break; + } + } + if (!callDebugScope) + { + builder.emitDebugNoScope(); + } + + IRDebugInlinedAt* callDebugInlinedAt = nullptr; + for (IRInst* inst = call->getPrevInst(); inst; inst = inst->getPrevInst()) + { + if (as(inst)) + { + break; + } + if (as(inst)) + { + callDebugInlinedAt = as(inst); + break; + } + } + + // Find the last IRDebugLine to extract debug info. + for (IRInst* inst = call->getPrevInst(); inst; inst = inst->getPrevInst()) + { + if (auto debugLine = as(inst)) + { + lastDebugLine = debugLine; + break; + } + } + + if (!lastDebugLine) + return DebugInlineInfo(); + + calleeDebugFunc = findExistingDebugFunc(callee); + + // The caller func is the right lexical scope needed for nsight to show where + // the function is getting inlined inside. + auto callerFunc = getParentFunc(call); + callerDebugFunc = findExistingDebugFunc(callerFunc); + + builder.setInsertBefore(call); + newDebugInlinedAt = builder.emitDebugInlinedAt( + lastDebugLine->getLineStart(), + lastDebugLine->getColStart(), + lastDebugLine->getSource(), + callerDebugFunc, + callDebugInlinedAt); + + if (newDebugInlinedAt && calleeDebugFunc) + { + return DebugInlineInfo{newDebugInlinedAt, calleeDebugFunc}; + } + } + return DebugInlineInfo(); + } + /// Inline the given `callSite`, which is assumed to have been validated void inlineCallSite(CallSiteInfo const& callSite) { @@ -481,7 +601,9 @@ struct InliningPassBase void inlineSingleBlockFuncBody( CallSiteInfo const& callSite, IRCloneEnv* env, - IRBuilder* builder) + IRBuilder* builder, + IRInst* newDebugInlinedAt, + IRInst* calleeDebugFunc) { auto call = callSite.call; auto callee = callSite.callee; @@ -495,18 +617,22 @@ struct InliningPassBase // them into the same basic block as the `call`. // builder->setInsertBefore(call); + IRInst* calleeDebugScope = nullptr; + if (calleeDebugFunc && newDebugInlinedAt) + { + calleeDebugScope = builder->emitDebugScope(calleeDebugFunc, newDebugInlinedAt); + } // Along the way, we will detect any `return` instruction, // and remember the (clone of the) returned value. // IRInst* returnVal = nullptr; - + List debugInlinedInsts; for (auto inst : firstBlock->getChildren()) { switch (inst->getOp()) { default: - // In the common case we just clone the instruction as-is _cloneInstWithSourceLoc(callSite, env, builder, inst); break; @@ -524,9 +650,40 @@ struct InliningPassBase SLANG_ASSERT(!returnVal); returnVal = findCloneForOperand(env, inst->getOperand(0)); break; + + case kIROp_DebugNoScope: + { + if (calleeDebugScope) + _cloneInstWithSourceLoc(callSite, env, builder, calleeDebugScope); + break; + } + + case kIROp_DebugInlinedAt: + { + auto clonedInst = _cloneInstWithSourceLoc(callSite, env, builder, inst); + if (!as(clonedInst)->isOuterInlinedPresent()) + debugInlinedInsts.add(as(clonedInst)); + break; + } + } + } + // For any debugInlinedAt without an outerinlinedAt, emit a new debugInlinedAt with the + // outer set, and delete the older debugInlinedAt + for (auto inst : debugInlinedInsts) + { + if (newDebugInlinedAt && !inst->isOuterInlinedPresent()) + { + builder->setInsertAfter(inst); + auto newInlinedAt = builder->emitDebugInlinedAt( + inst->getLine(), + inst->getCol(), + inst->getFile(), + inst->getDebugFunc(), + newDebugInlinedAt); + inst->replaceUsesWith(newInlinedAt); + inst->removeAndDeallocate(); } } - // We are going to remove the original `call` now that the callee // has been inlined, but before we do that we need to replace // all uses of the `call` with whatever value was produced by the @@ -547,10 +704,28 @@ struct InliningPassBase } /// Inline the body of the callee for `callSite`. + // Here is the algorithm for inserting debug information for slang inlined functions: + // 1. Check if the call inst belongs to an existing debug scope and find corresponding + // debugInlinedAt. [callDebugScope, callDebugInlinedAt] + // 1a. If callDebugScope exists, emit this debug Scope* after* the call inst. + // 2. Emit a new DebugInlinedAt inst, with debugFunc of the callee, and outer debugInlinedAt is + // callDebugInlinedAt. [newDebugInlinedAt] + // 2a. If calleDebugScope does not exist, emit debugNoScope after the call inst. + // 3. Clone the callee body. + // 4. For each cloned block, do this: + // 4a.Emit a new DebugScope inst setting the current scope to newDebugInlinedAt. + // [calleeDebugScope] 4b.Emit a DebugNoScope at the end of each block. 4c.If callDebugScope + // exists, do not emit a DebugNoScope for the last block. + // 5. For each cloned debugInlinedAt inst, if its outer inlined at operand is null, set it to + // the new DebugInlinedAt inst inserted at the top of the block. + // 6. For each cloned debugNoScope inst, replace it with calleeDebugScope. (This is because all + // cloned insts are in callee's scope). void inlineFuncBody(CallSiteInfo const& callSite, IRCloneEnv* env, IRBuilder* builder) { - auto call = callSite.call; auto callee = callSite.callee; + auto call = callSite.call; + + auto debugInlineInfo = emitCalleeDebugInlinedAt(call, callee, *builder); // If the callee consists of a single basic block *and* that block // ends with a `return` instruction, then we can apply a simple approach @@ -561,10 +736,35 @@ struct InliningPassBase SLANG_ASSERT(firstBlock); if (!firstBlock->getNextBlock() && as(firstBlock->getTerminator())) { - inlineSingleBlockFuncBody(callSite, env, builder); + inlineSingleBlockFuncBody( + callSite, + env, + builder, + debugInlineInfo.newDebugInlinedAt, + debugInlineInfo.calleeDebugFunc); return; } + // If the callee has multiple blocks, use the more complex inlining approach + inlineMultipleBlockFuncBody( + callSite, + env, + builder, + debugInlineInfo.newDebugInlinedAt, + debugInlineInfo.calleeDebugFunc); + } + + // Inline the body of the callee for `callSite`, for a callee that has multiple basic blocks. + void inlineMultipleBlockFuncBody( + CallSiteInfo const& callSite, + IRCloneEnv* env, + IRBuilder* builder, + IRInst* newDebugInlinedAt, + IRInst* calleeDebugFunc) + { + auto call = callSite.call; + auto callee = callSite.callee; + // If the callee has any non-trivial control flow (multiple basic blocks // and terminators other than `return`), we will need to split the control // flow of the caller at the block that contains `call`. @@ -680,7 +880,53 @@ struct InliningPassBase } isFirstBlock = false; } + // For each existing debugNoScope inst, replace it with new debug scope we emit. + // For any debugInlinedAt without an outerinlinedAt, emit a new debugInlinedAt with the + // outer set, and delete the older debugInlinedAt + if (newDebugInlinedAt && callee->findDecoration()) + { + for (auto calleeBlock : callee->getBlocks()) + { + IRBlock* clonedBlock = as(env->mapOldValToNew.getValue(calleeBlock)); + setInsertBeforeOrdinaryInst(builder, clonedBlock->getFirstOrdinaryInst()); + builder->emitDebugScope(calleeDebugFunc, newDebugInlinedAt); + List debugNoScopeToRemove; + List debugInlinedAtToProcess; + for (auto inst : clonedBlock->getChildren()) + { + if (as(inst)) + { + debugNoScopeToRemove.add(inst); + } + if (auto inlinedAt = as(inst)) + { + if (!inlinedAt->isOuterInlinedPresent()) + { + debugInlinedAtToProcess.add(inlinedAt); + } + } + } + for (auto inst : debugNoScopeToRemove) + { + builder->setInsertAfter(inst); + builder->emitDebugScope(calleeDebugFunc, newDebugInlinedAt); + inst->removeAndDeallocate(); + } + for (auto inlinedAt : debugInlinedAtToProcess) + { + builder->setInsertAfter(inlinedAt); + auto newInlinedAt = builder->emitDebugInlinedAt( + inlinedAt->getLine(), + inlinedAt->getCol(), + inlinedAt->getFile(), + inlinedAt->getDebugFunc(), + newDebugInlinedAt); + inlinedAt->replaceUsesWith(newInlinedAt); + inlinedAt->removeAndDeallocate(); + } + } + } // If there was a `returnVal` instruction that established // the return value of the inlined function, then that value // should be used to replace any uses of the original call. diff --git a/source/slang/slang-ir-inst-defs.h b/source/slang/slang-ir-inst-defs.h index 66505dbd0..5a62c8063 100644 --- a/source/slang/slang-ir-inst-defs.h +++ b/source/slang/slang-ir-inst-defs.h @@ -1165,6 +1165,9 @@ INST_RANGE(BindingQuery, GetRegisterIndex, GetRegisterSpace) /// Decorates an inst with a debug source location (IRDebugSource, IRIntLit(line), IRIntLit(col)). INST(DebugLocationDecoration, DebugLocation, 3, 0) + /// Decorates a function with a link to its debug function representation + INST(DebugFunctionDecoration, DebugFunction, 1, 0) + /// Recognized by SPIRV-emit pass so we can emit a SPIRV `Block` decoration. INST(SPIRVBlockDecoration, spvBlock, 0, 0) @@ -1356,6 +1359,11 @@ INST(DebugSource, DebugSource, 2, HOISTABLE) INST(DebugLine, DebugLine, 5, 0) INST(DebugVar, DebugVar, 4, 0) INST(DebugValue, DebugValue, 2, 0) +INST(DebugInlinedAt, DebugInlinedAt, 5, 0) +INST(DebugFunction, DebugFunction, 5, 0) +INST(DebugInlinedVariable, DebugInlinedVariable, 2, 0) +INST(DebugScope, DebugScope, 2, 0) +INST(DebugNoScope, DebugNoScope, 1, 0) /* Embedded Precompiled Libraries */ INST(EmbeddedDownstreamIR, EmbeddedDownstreamIR, 2, 0) diff --git a/source/slang/slang-ir-inst-pass-base.h b/source/slang/slang-ir-inst-pass-base.h index 5ce73f617..8b7685652 100644 --- a/source/slang/slang-ir-inst-pass-base.h +++ b/source/slang/slang-ir-inst-pass-base.h @@ -137,6 +137,8 @@ public: case kIROp_GenericSpecializationDictionary: case kIROp_ExistentialFuncSpecializationDictionary: case kIROp_ExistentialTypeSpecializationDictionary: + case kIROp_DebugInlinedAt: + case kIROp_DebugFunction: continue; default: break; diff --git a/source/slang/slang-ir-insts.h b/source/slang/slang-ir-insts.h index 5b196cf24..4bef81f47 100644 --- a/source/slang/slang-ir-insts.h +++ b/source/slang/slang-ir-insts.h @@ -3442,6 +3442,60 @@ struct IRDebugValue : IRInst IRInst* getValue() { return getOperand(1); } }; +struct IRDebugInlinedAt : IRInst +{ + IR_LEAF_ISA(DebugInlinedAt) + IRInst* getLine() { return getOperand(0); } + IRInst* getCol() { return getOperand(1); } + IRInst* getFile() { return getOperand(2); } + IRInst* getDebugFunc() { return getOperand(3); } + IRInst* getOuterInlinedAt() + { + if (operandCount == 5) + return getOperand(4); + return nullptr; + } + void setDebugFunc(IRInst* func) { setOperand(3, func); } + bool isOuterInlinedPresent() { return operandCount == 5; } +}; + +struct IRDebugScope : IRInst +{ + IR_LEAF_ISA(DebugScope) + IRInst* getScope() { return getOperand(0); } + IRInst* getInlinedAt() { return getOperand(1); } + void setInlinedAt(IRInst* inlinedAt) { setOperand(1, inlinedAt); } +}; + +struct IRDebugNoScope : IRInst +{ + IR_LEAF_ISA(DebugNoScope) + IRInst* getScope() { return getOperand(0); } +}; + +struct IRDebugInlinedVariable : IRInst +{ + IR_LEAF_ISA(DebugInlinedVariable) + IRInst* getVariable() { return getOperand(0); } + IRInst* getInlinedAt() { return getOperand(1); } +}; + +struct IRDebugFunction : IRInst +{ + IR_LEAF_ISA(DebugFunction) + IRInst* getName() { return getOperand(0); } + IRInst* getLine() { return getOperand(1); } + IRInst* getCol() { return getOperand(2); } + IRInst* getFile() { return getOperand(3); } + IRInst* getDebugType() { return getOperand(4); } +}; + +struct IRDebugFuncDecoration : IRInst +{ + IR_LEAF_ISA(DebugFunctionDecoration) + IRInst* getDebugFunc() { return getOperand(0); } +}; + struct IRDebugLocationDecoration : IRDecoration { IRInst* getSource() { return getOperand(0); } @@ -3943,6 +3997,21 @@ public: IRInst* col, IRInst* argIndex = nullptr); IRInst* emitDebugValue(IRInst* debugVar, IRInst* debugValue); + IRInst* emitDebugInlinedAt( + IRInst* line, + IRInst* col, + IRInst* file, + IRInst* debugFunc, + IRInst* outerInlinedAt); + IRInst* emitDebugInlinedVariable(IRInst* variable, IRInst* inlinedAt); + IRInst* emitDebugScope(IRInst* scope, IRInst* inlinedAt); + IRInst* emitDebugNoScope(); + IRInst* emitDebugFunction( + IRInst* name, + IRInst* line, + IRInst* col, + IRInst* file, + IRInst* debugType); /// Emit an LiveRangeStart instruction indicating the referenced item is live following this /// instruction @@ -5012,6 +5081,11 @@ public: getIntValue(getUIntType(), col)); } + void addDebugFunctionDecoration(IRInst* value, IRInst* debugFunction) + { + addDecoration(value, kIROp_DebugFunctionDecoration, debugFunction); + } + void addUnsafeForceInlineDecoration(IRInst* value) { addDecoration(value, kIROp_UnsafeForceInlineEarlyDecoration); diff --git a/source/slang/slang-ir-strip-debug-info.cpp b/source/slang/slang-ir-strip-debug-info.cpp index 8b2a07663..124c41a5c 100644 --- a/source/slang/slang-ir-strip-debug-info.cpp +++ b/source/slang/slang-ir-strip-debug-info.cpp @@ -13,6 +13,10 @@ static void findDebugInfo(IRInst* inst, List& debugInstructions) case kIROp_DebugLine: case kIROp_DebugLocationDecoration: case kIROp_DebugSource: + case kIROp_DebugInlinedAt: + case kIROp_DebugScope: + case kIROp_DebugNoScope: + case kIROp_DebugFunction: debugInstructions.add(inst); break; default: diff --git a/source/slang/slang-ir-validate.cpp b/source/slang/slang-ir-validate.cpp index 11587d600..19de64618 100644 --- a/source/slang/slang-ir-validate.cpp +++ b/source/slang/slang-ir-validate.cpp @@ -244,6 +244,7 @@ void validateIRInstOperand(IRValidateContext* context, IRInst* inst, IRUse* oper switch (inst->getOp()) { case kIROp_DifferentiableTypeDictionaryItem: + case kIROp_DebugScope: return; } // diff --git a/source/slang/slang-ir.cpp b/source/slang/slang-ir.cpp index 98c0fa471..fb7d752d5 100644 --- a/source/slang/slang-ir.cpp +++ b/source/slang/slang-ir.cpp @@ -3411,6 +3411,53 @@ IRInst* IRBuilder::emitDebugValue(IRInst* debugVar, IRInst* debugValue) args.getBuffer()); } +IRInst* IRBuilder::emitDebugInlinedAt( + IRInst* line, + IRInst* col, + IRInst* file, + IRInst* debugFunc, + IRInst* outerInlinedAt) +{ + if (outerInlinedAt) + { + IRInst* args[] = {line, col, file, debugFunc, outerInlinedAt}; + return emitIntrinsicInst(getVoidType(), kIROp_DebugInlinedAt, 5, args); + } + else + { + IRInst* args[] = {line, col, file, debugFunc}; + return emitIntrinsicInst(getVoidType(), kIROp_DebugInlinedAt, 4, args); + } +} + +IRInst* IRBuilder::emitDebugFunction( + IRInst* name, + IRInst* line, + IRInst* col, + IRInst* file, + IRInst* debugType) +{ + IRInst* args[] = {name, line, col, file, debugType}; + return emitIntrinsicInst(getVoidType(), kIROp_DebugFunction, 5, args); +} + +IRInst* IRBuilder::emitDebugInlinedVariable(IRInst* variable, IRInst* inlinedAt) +{ + IRInst* args[] = {variable, inlinedAt}; + return emitIntrinsicInst(getVoidType(), kIROp_DebugInlinedVariable, 2, args); +} + +IRInst* IRBuilder::emitDebugScope(IRInst* scope, IRInst* inlinedAt) +{ + IRInst* args[] = {scope, inlinedAt}; + return emitIntrinsicInst(getVoidType(), kIROp_DebugScope, 2, args); +} + +IRInst* IRBuilder::emitDebugNoScope() +{ + return emitIntrinsicInst(getVoidType(), kIROp_DebugNoScope, 0, nullptr); +} + IRLiveRangeStart* IRBuilder::emitLiveRangeStart(IRInst* referenced) { // This instruction doesn't produce any result, @@ -7941,8 +7988,8 @@ IRInstList IRInst::getDecorations() IRInst* IRInst::getFirstChild() { // The children come after any decorations, - // so if there are any decorations, then the - // first child is right after the last decoration. + // so if there are any decorations, then + // the first child is right after the last decoration. // if (auto lastDecoration = getLastDecoration()) return lastDecoration->getNextInst(); @@ -8462,6 +8509,7 @@ bool IRInst::mightHaveSideEffects(SideEffectAnalysisOptions options) case kIROp_RTTIObject: case kIROp_RTTIType: case kIROp_Func: + case kIROp_DebugFunction: case kIROp_Generic: case kIROp_Var: case kIROp_Param: diff --git a/source/slang/slang-lower-to-ir.cpp b/source/slang/slang-lower-to-ir.cpp index 2271e750e..c58eed1c1 100644 --- a/source/slang/slang-lower-to-ir.cpp +++ b/source/slang/slang-lower-to-ir.cpp @@ -11011,6 +11011,31 @@ struct DeclLoweringVisitor : DeclVisitor } } + // Add debugfunction decoration and emit debug function. This + // is needed for emitting debug information + auto nameHint = irFunc->findDecoration(); + IRStringLit* nameOperand = nameHint ? as(nameHint->getNameOperand()) : nullptr; + if (nameOperand) + { + getBuilder()->setInsertInto(getBuilder()->getModule()->getModuleInst()); + + auto locationDecor = irFunc->findDecoration(); + IRInst* debugType = irFunc->getDataType(); + + if (locationDecor && debugType) + { + auto debugFuncCallee = getBuilder()->emitDebugFunction( + nameOperand, + locationDecor->getLine(), + locationDecor->getCol(), + locationDecor->getSource(), + debugType); + + // Add a decoration to link the function to its debug function + getBuilder()->addDecoration(irFunc, kIROp_DebugFunctionDecoration, debugFuncCallee); + } + } + // For convenience, ensure that any additional global // values that were emitted while outputting the function // body appear before the function itself in the list diff --git a/tests/language-feature/function-calls/forceinline-basic-block-inline-order.slang b/tests/language-feature/function-calls/forceinline-basic-block-inline-order.slang new file mode 100644 index 000000000..24bfd5dff --- /dev/null +++ b/tests/language-feature/function-calls/forceinline-basic-block-inline-order.slang @@ -0,0 +1,39 @@ +//TEST:SIMPLE(filecheck=CHECK): -stage compute -entry computeMain -target spirv -O0 -g3 +RWStructuredBuffer outputBuffer; + +// Test where outer function is inlined before the inner function. + +[ForceInline] +int inlineSingleBasicBlock1(int value1, int value2) +{ + // Simple operation that should be inlined + return value1 * 2 + value2; +} + +[__unsafeForceInlineEarly] +int inlineSingleBasicBlock2(int value1, int value2) +{ + int result1 = inlineSingleBasicBlock1(10, 20); + // Simple operation that should be inlined + return value1 * 2 + value2 + result1; +} + + +[numthreads(4, 1, 1)] +void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID) +{ + int i = dispatchThreadID.x; + + // Call the forceinline function + int result = inlineSingleBasicBlock2(16, 10); + + outputBuffer[i] = result; +} + + +// CHECK-COUNT-2: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugInlinedAt %uint_{{[0-9]+}} %{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugInlinedAt %uint_{{[0-9]+}} %{{[0-9]+}} +// CHECK-COUNT-3: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugScope %{{[0-9]+}} %{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugScope %{{[0-9]+}} %{{[0-9]+}} +// CHECK-COUNT-1: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugNoScope +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugNoScope \ No newline at end of file diff --git a/tests/language-feature/function-calls/forceinline-basic-block.slang b/tests/language-feature/function-calls/forceinline-basic-block.slang new file mode 100644 index 000000000..32e8faef5 --- /dev/null +++ b/tests/language-feature/function-calls/forceinline-basic-block.slang @@ -0,0 +1,37 @@ +//TEST:SIMPLE(filecheck=CHECK): -stage compute -entry computeMain -target spirv -O0 -g3 +RWStructuredBuffer outputBuffer; + +[ForceInline] +int inlineSingleBasicBlock1(int value1, int value2) +{ + // Simple operation that should be inlined + return value1 * 2 + value2; +} + +[ForceInline] +int inlineSingleBasicBlock2(int value1, int value2) +{ + int result1 = inlineSingleBasicBlock1(10, 20); + // Simple operation that should be inlined + return value1 * 2 + value2 + result1; +} + + +[numthreads(4, 1, 1)] +void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID) +{ + int i = dispatchThreadID.x; + + // Call the forceinline function + int result = inlineSingleBasicBlock2(16, 10); + + outputBuffer[i] = result; +} + + +// CHECK-COUNT-2: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugInlinedAt %uint_{{[0-9]+}} %{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugInlinedAt %uint_{{[0-9]+}} %{{[0-9]+}} +// CHECK-COUNT-3: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugScope %{{[0-9]+}} %{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugScope %{{[0-9]+}} %{{[0-9]+}} +// CHECK-COUNT-1: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugNoScope +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugNoScope \ No newline at end of file diff --git a/tests/language-feature/function-calls/forceinline-multiple-blocks.slang b/tests/language-feature/function-calls/forceinline-multiple-blocks.slang new file mode 100644 index 000000000..dba1d3953 --- /dev/null +++ b/tests/language-feature/function-calls/forceinline-multiple-blocks.slang @@ -0,0 +1,79 @@ +//TEST:SIMPLE(filecheck=CHECK): -stage compute -entry computeMain -target spirv -O0 -g3 +RWStructuredBuffer outputBuffer; + +// This tests the following use cases: +// Inline single block function in multiple places. +// Inline single block function into multi-block function(This also covers multiple blocks with a phi node.) +// Inline single block function in multiple places in the same function. +// Inline multi-block function into another function +// Inline multi-block function multiple times +// Recursive inlining use case. + +[ForceInline] +int calculateAdjustment(int value) +{ + return value * 3 / 2; +} + +[ForceInline] +int inlineMultipleBasicBlocks(int value) +{ + int result = 0; + + result = value * 2; + result = calculateAdjustment(result); + + // Add another branch to create more basic blocks + if (result > 20) + { + result = result + 25; + } + else + { + result = result + 50; + } + + result = value * 4; + result = calculateAdjustment(result); + + // Add another branch to create more basic blocks + if (result < 20) + { + result = result + 10; + } + + result = value * 10; + result = calculateAdjustment(result); + + if (result < 250) + { + result = result + 100; + } + + return result; +} + +[numthreads(4, 1, 1)] +void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID) +{ + int i = dispatchThreadID.x; + + // Call the forceinline function + int result1 = inlineMultipleBasicBlocks(16); + + int result2 = inlineMultipleBasicBlocks(22); + + int result3 = calculateAdjustment(2); + + outputBuffer[i] = result1 + result2 + result3; +} + +// CHECK-COUNT-3: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugFunction %{{[0-9]+}} %{{[0-9]+}} %{{[0-9]+}} %uint_{{[0-9]+}} %uint_{{[0-9]+}} %{{[0-9]+}} %{{[0-9]+}} %uint_{{[0-9]+}} %uint_{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugFunction %{{[0-9]+}} %{{[0-9]+}} %{{[0-9]+}} %uint_{{[0-9]+}} %uint_{{[0-9]+}} %{{[0-9]+}} %{{[0-9]+}} %uint_{{[0-9]+}} %uint_{{[0-9]+}} + +// TODO: Actual count is 6. But the pattern matcher complains to match. +// CHECK-COUNT-5: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugInlinedAt %uint_{{[0-9]+}} %{{[0-9]+}} %{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugInlinedAt %uint_{{[0-9]+}} %{{[0-9]+}} %{{[0-9]+}} + +// CHECK-COUNT-27: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugScope %{{[0-9]+}} %{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugScope %{{[0-9]+}} %{{[0-9]+}} \ No newline at end of file diff --git a/tests/language-feature/function-calls/forceinline-multiple-cases.slang b/tests/language-feature/function-calls/forceinline-multiple-cases.slang new file mode 100644 index 000000000..3f1cfc0f7 --- /dev/null +++ b/tests/language-feature/function-calls/forceinline-multiple-cases.slang @@ -0,0 +1,141 @@ +//TEST:SIMPLE(filecheck=CHECK): -stage compute -entry computeMain -target spirv -O0 -g3 +RWStructuredBuffer outputBuffer; + +// Function with a single basic block +[ForceInline] +int basicBlockFunc(int value1, int value2) +{ + // Simple operation that should be inlined + return value1 * 2 + value2; +} + +// Function with multiple basic blocks +[ForceInline] +int multipleBlockFunc(int value1, int value2) +{ + int result = value1 * 2; + + // Add a condition to create multiple basic blocks + if (value1 > value2) + { + result += value2 * 3; + } + else + { + result -= value2; + } + + return result; +} + +// Test case a: Multiple calls to basic block function +int testMultipleBasicBlockCalls() +{ + int result = basicBlockFunc(10, 5); + result += basicBlockFunc(20, 15); + result += basicBlockFunc(30, 25); + return result; +} + +// Test case b: Multiple calls to multiple block function +int testMultipleBlockCalls() +{ + int result = multipleBlockFunc(10, 5); + result += multipleBlockFunc(20, 25); + result += multipleBlockFunc(30, 25); + return result; +} + +// Test case c: One call to basic block, other instructions, another call to basic block +int testBasicBlockWithInstructions() +{ + int result = basicBlockFunc(10, 5); + + // Some other instructions + result *= 2; + result += 10; + + result += basicBlockFunc(20, 15); + return result; +} + +// Test case d: One call to multiple block func, other instructions, another call to multiple block func +int testMultipleBlockWithInstructions() +{ + int result = multipleBlockFunc(10, 5); + + // Some other instructions + result *= 2; + result += 10; + + result += multipleBlockFunc(20, 25); + return result; +} + +// Test case e: One call to basic block, other instructions, call to multiple block +int testBasicToMultipleBlock() +{ + int result = basicBlockFunc(10, 5); + + // Some other instructions + result *= 2; + result += 10; + + result += multipleBlockFunc(20, 25); + return result; +} + +// Additional test case: Mixed calls with condition +int testMixedCallsWithCondition() +{ + int result = basicBlockFunc(15, 7); + if (result > 30) + { + result += multipleBlockFunc(8, 3); + } + else + { + result += multipleBlockFunc(12, 9); + } + return result; +} + +[numthreads(8, 1, 1)] +void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID) +{ + // Call all test functions directly and store results in different indices + + // Test case a: Multiple calls to basic block function + outputBuffer[0] = testMultipleBasicBlockCalls(); + + // Test case b: Multiple calls to multiple block function + outputBuffer[1] = testMultipleBlockCalls(); + + // Test case c: One call to basic block, other instructions, another call to basic block + outputBuffer[2] = testBasicBlockWithInstructions(); + + // Test case d: One call to multiple block func, other instructions, another call to multiple block func + outputBuffer[3] = testMultipleBlockWithInstructions(); + + // Test case e: One call to basic block, other instructions, call to multiple block + outputBuffer[4] = testBasicToMultipleBlock(); + + // Additional test case: Mixed calls with condition + outputBuffer[5] = testMixedCallsWithCondition(); + + // Set any remaining indices to 0 + if (dispatchThreadID.x >= 6) + { + outputBuffer[dispatchThreadID.x] = 0; + } +} + +// CHECK-COUNT-14: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugInlinedAt %uint_{{[0-9]+}} %{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugInlinedAt %uint_{{[0-9]+}} %{{[0-9]+}} +// CHECK-COUNT-28: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugScope %{{[0-9]+}} %{{[0-9]+}} +// CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugScope %{{[0-9]+}} %{{[0-9]+}} + +// TODO: Verified manually that the count in the .actual file is 28. +// But the pattern matcher complains to match. +// _CHECK-COUNT-28: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugNoScope +// _CHECK-NOT: %{{[0-9]+}} = OpExtInst %void %{{[0-9]+}} DebugNoScope -- cgit v1.2.3