Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR revives/implements an alternative inlining pipeline in the CoreCLR JIT based on splitting trees/blocks around inline candidates (instead of using GT_RET_EXPR placeholders), with follow-on updates across importer, inliner, and various optimizations that previously depended on GT_RET_EXPR.
Changes:
- Remove
GT_RET_EXPRfrom the IR and update related utilities (node lists, cloning, side-effect checks, class-handle queries, etc.). - Rework inlining to inline directly from
GT_CALLsites using statement/tree splitting plus new setup/teardown statement list plumbing. - Update guarded devirtualization/fat calli transformations, struct return/retbuf handling, and instrumentation to work without
GT_RET_EXPR.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/jit/simd.cpp | Stop treating GT_RET_EXPR as a call-like SIMD stack value; normalize only GT_CALL. |
| src/coreclr/jit/lclvars.cpp | Add a null-layout guard to struct promotion eligibility checks. |
| src/coreclr/jit/inline.h | Adjust inline policy/debug hooks, InlineResult ctor signature, introduce InlineIRResult, add StatementListBuilder, track retbuf arg info. |
| src/coreclr/jit/inline.cpp | Wire InlineResult construction to accept an InlineContext* and update policy notes accordingly. |
| src/coreclr/jit/indirectcalltransformer.cpp | Refactor call discovery and operand spilling; handle STORE_LCL_VAR(call) statement shapes. |
| src/coreclr/jit/importervectorization.cpp | Remove GT_RET_EXPR-based span/call handling paths. |
| src/coreclr/jit/importercalls.cpp | Rework importer handling for inline/GDV candidates without GT_RET_EXPR; adjust some intrinsic patterns and candidate bookkeeping. |
| src/coreclr/jit/importer.cpp | Remove various GT_RET_EXPR-specific normalization/spill/return handling paths; adapt retbuf and inline return plumbing. |
| src/coreclr/jit/gtstructs.h | Remove RetExpr node struct mapping. |
| src/coreclr/jit/gtlist.h | Remove GTNODE(RET_EXPR, ...) from the node list. |
| src/coreclr/jit/gentree.h | Remove GenTreeRetExpr definition. |
| src/coreclr/jit/gentree.cpp | Remove GT_RET_EXPR handling across layout queries, cloning constraints, leaf display, side-effect logic; extend gtSplitTree signature/behavior. |
| src/coreclr/jit/fgstmt.cpp | Add helpers to splice whole statement lists into blocks (before/at end). |
| src/coreclr/jit/fgprofile.cpp | Instrumentation temporarily links inlinee “return” IR using InlineIRResult instead of GT_RET_EXPR. |
| src/coreclr/jit/fgopt.cpp | Simplify async-call scanning by removing GT_RET_EXPR recursion. |
| src/coreclr/jit/fginline.cpp | Major rewrite: new walker that inlines from GT_CALL via splitting + statement/block insertion; adds setup/teardown list building. |
| src/coreclr/jit/fgbasic.cpp | Add fgSplitBlockBeforeStatement helper. |
| src/coreclr/jit/debuginfo.cpp | Disable (comment out) debug-info validation checks. |
| src/coreclr/jit/compiler.hpp | Remove GT_RET_EXPR from operand visitation cases. |
| src/coreclr/jit/compiler.h | Update signatures (gtSplitTree, inline helpers), add friendship and statement-list splicing APIs. |
Comments suppressed due to low confidence (2)
src/coreclr/jit/inline.cpp:636
- The comment for InlineResult::InlineResult still describes a "stmt" parameter, but the constructor now takes an InlineContext* instead. Update the parameter documentation to reflect the new signature to avoid confusion for future call sites.
// Arguments:
// compiler - the compiler instance examining a call for inlining
// call - the call in question
// stmt - statement containing the call (if known)
// description - string describing the context of the decision
src/coreclr/jit/importer.cpp:405
- In impAppendStmt, the inner
if (call->ShouldHaveRetBufArg())is redundant because it's nested underif (call->TypeIs(TYP_VOID) && call->ShouldHaveRetBufArg()). As written, theelsebranch is unreachable; consider simplifying to a single path (or, if the two branches were meant to distinguish different shapes, adjust the condition so both are reachable as intended).
if (call->TypeIs(TYP_VOID) && call->ShouldHaveRetBufArg())
{
GenTree* retBuf;
if (call->ShouldHaveRetBufArg())
{
assert(call->gtArgs.HasRetBuffer());
retBuf = call->gtArgs.GetRetBufferArg()->GetNode();
}
else
{
assert(!call->gtArgs.HasThisPointer());
retBuf = call->gtArgs.GetArgByIndex(0)->GetNode();
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (3)
src/coreclr/jit/inline.cpp:1314
- Line 1314 contains a commented-out DebugInfo validation call that appears to be intentionally disabled. This should either be removed if no longer needed, or uncommented if the validation is important. The comment doesn't explain why it's disabled, which makes it unclear whether this is temporary debugging code or intentionally disabled for a reason.
// DebugInfo(parentContext, context->m_Location).Validate();
src/coreclr/jit/indirectcalltransformer.cpp:997
- On line 996, lvSingleDef is unconditionally set to false when doesReturnValue is true, but this is done after potentially demoting the call to non-inline in the if block above (lines 968-978). If the call was demoted and the statement was already added, we're still modifying lvSingleDef. While this may be correct (since multiple defs will exist after GDV transformation), the logic would be clearer if this was moved inside the else block starting at line 980 where we know the call remains an inline candidate.
if (doesReturnValue)
{
compiler->lvaGetDesc(stmt->GetRootNode()->AsLclVarCommon())->lvSingleDef = false;
}
src/coreclr/jit/importer.cpp:789
- On line 785, the condition checks
!srcCall->IsInlineCandidate()before marking the local as DNER. However, this means that if a call IS an inline candidate, the local won't be marked DNER even though it's being used as a hidden buffer struct arg. If the inline later fails, the local may still be in an enregisterable state which could cause issues. The old code didn't have this condition. Consider whether this check is correct or if it should be removed.
if (destAddr->OperIs(GT_LCL_ADDR) && !srcCall->IsInlineCandidate())
{
lvaSetVarDoNotEnregister(destAddr->AsLclVarCommon()->GetLclNum()
DEBUGARG(DoNotEnregisterReason::HiddenBufferStructArg));
}
| // TODO: move to InlineInfo | ||
| struct InlineIRResult | ||
| { | ||
| GenTree* substExpr; | ||
| BasicBlock* substBB; | ||
| }; |
There was a problem hiding this comment.
The TODO comment "TODO: move to InlineInfo" on line 594 suggests this is incomplete work. The InlineIRResult struct should likely be moved into InlineInfo rather than being a separate struct that's embedded in InlineCandidateInfo. This would make the structure more logical since the result is specific to each inline attempt.
|
One source of regressions is that for [000011] nACXG---R-- ▌ STORE_BLK struct<System.TimeSpan, 8> (copy)
[000010] ---X------- ├──▌ FIELD_ADDR byref <unknown class>:<unknown field>
[000006] ----------- │ └──▌ LCL_VAR ref V00 this
[000009] I-C-G------ └──▌ CALL struct System.TimeSpan:FromSeconds(long):System.TimeSpan (exactContextHandle=0x00007FF94C1F7CE1)
[000008] ----------- arg0 └──▌ CNS_INT long 10Note We make up for it in morph since we usually fold the NRE side effect of the address computation into the store itself, and when we do that we end up evaluating the call first. But when the call is an inline candidate we now end up spilling Some kind of fix is needed here (as a separate PR). We could set (this issue is similar to #77650) |
| // Detach the GT_CALL tree from the original statement by | ||
| // hanging a "nothing" node to it. Later the "nothing" node will be removed | ||
| // and the original GT_CALL tree will be picked up by the GT_RET_EXPR node. |
There was a problem hiding this comment.
This comment describes the old GT_RET_EXPR-based detachment/reattachment flow ("picked up by the GT_RET_EXPR node"), but GT_RET_EXPR is removed in this PR and the substitution is now represented via InlineCandidateInfo::result. Please update the comment to reflect the new mechanism (or remove it) to avoid confusion when debugging inline failures.
| // Detach the GT_CALL tree from the original statement by | |
| // hanging a "nothing" node to it. Later the "nothing" node will be removed | |
| // and the original GT_CALL tree will be picked up by the GT_RET_EXPR node. | |
| // Mark this candidate as having no successful inline substitution by | |
| // clearing the recorded inline result. This ensures the original GT_CALL | |
| // remains in the IR instead of being replaced by an inlined expression. |
| JITDUMP("Inlining candidate [%06u] in " FMT_STMT "\n", Compiler::dspTreeID(call), m_statement->GetID()); | ||
| DISPSTMT(m_statement); | ||
|
|
||
| fgWalkResult PostOrderVisit(GenTree** use, GenTree* user) | ||
| { | ||
| LateDevirtualization(use, user); | ||
| return fgWalkResult::WALK_CONTINUE; | ||
| } | ||
| if (InsertMidStatement(inlineInfo, use)) | ||
| { | ||
| m_nextBlock = m_block; | ||
| m_nextStatement = m_statement; | ||
| return true; | ||
| } |
There was a problem hiding this comment.
On successful inlines (including the splitting/CFG rewrite paths), this method returns true without setting m_madeChanges. As a result, fgInline() can incorrectly return MODIFIED_NOTHING even though it mutated IR/CFG, which can break downstream phase gating and debug asserts. Set m_madeChanges = true whenever an inline succeeds/changes IR (e.g., before returning true from TryInline, and/or have fgInline() treat any restart as a modification).
Add ILLocation field to LateDevirtualizationInfo, set from impCurStmtDI, so the new statement created during late devirtualization carries proper debug info. Remove LateDevirtualizationInfo::inlinersContext in favor of GenTreeCall::gtInlineContext. Add assert verifying GenTreeCall::gtInlineContext matches the inline context from impCurStmtDI. Noticed this while working on #124870. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| @@ -7059,8 +6976,6 @@ | |||
|
|
|||
| setMethodHasFatPointer(); | |||
| call->SetFatPointerCandidate(); | |||
| SpillRetExprHelper helper(this); | |||
| helper.StoreRetExprResultsInArgs(call); | |||
| } | |||
There was a problem hiding this comment.
The comment says we "spill inline candidates in the call node", but addFatPointerCandidate no longer does any spilling (the previous helper was removed). Either restore the needed spilling here, or update the comment to reflect where/when arguments are actually spilled now (e.g., during transformation).
| // pass the argument into the inlinee. | ||
|
|
||
| void Compiler::impInlineRecordArgInfo(InlineInfo* pInlineInfo, | ||
| CallArg* arg, | ||
| InlArgInfo* argInfo, | ||
| CallArg* arg, | ||
| InlineResult* inlineResult) | ||
| { | ||
| argInfo->arg = arg; | ||
| GenTree* curArgVal = arg->GetNode(); | ||
|
|
||
| assert(!curArgVal->OperIs(GT_RET_EXPR)); | ||
|
|
||
| GenTree* lclVarTree; | ||
| const bool isAddressInLocal = impIsAddressInLocal(curArgVal, &lclVarTree); | ||
| if (isAddressInLocal) |
There was a problem hiding this comment.
impInlineRecordArgInfo no longer supports GT_RET_EXPR (and the asserts referencing it were removed), but nearby documentation still describes special GT_RET_EXPR handling for arguments. Please update those comments to match the new inline-candidate representation so future maintenance/debugging isn't misleading.
| JITDUMP("Inline [%06u] marked as failed\n", dspTreeID(call)); | ||
|
|
||
| // Detach the GT_CALL tree from the original statement by | ||
| // hanging a "nothing" node to it. Later the "nothing" node will be removed | ||
| // and the original GT_CALL tree will be picked up by the GT_RET_EXPR node. | ||
| inlCandInfo->retExpr->gtSubstExpr = call; | ||
| inlCandInfo->retExpr->gtSubstBB = compCurBB; | ||
|
|
||
| noway_assert(fgMorphStmt->GetRootNode() == call); | ||
| fgMorphStmt->SetRootNode(gtNewNothingNode()); | ||
| inlCandInfo->result.substExpr = nullptr; | ||
| inlCandInfo->result.substBB = nullptr; |
There was a problem hiding this comment.
This failure path comment still describes the old GT_RET_EXPR placeholder mechanics (detaching the call and later being picked up by a RET_EXPR). With RET_EXPR removed, this is misleading; please update the comment to describe the current substitution/inline-result flow (or remove it).
| //------------------------------------------------------------------------ | ||
| // fgDebugCheckInlineCandidates: Callback to make sure there is no more | ||
| // GT_RET_EXPR and GTF_CALL_INLINE_CANDIDATE nodes. | ||
| // | ||
| // Arguments: | ||
| // pTree - pointer to the tree node being walked | ||
| // data - walk data | ||
| // | ||
| // Return Value: | ||
| // WALK_CONTINUE | ||
| // | ||
| // static | ||
| Compiler::fgWalkResult Compiler::fgDebugCheckInlineCandidates(GenTree** pTree, fgWalkData* data) | ||
| { | ||
| GenTree* tree = *pTree; | ||
| if (tree->OperIs(GT_CALL)) | ||
| { | ||
| assert((tree->gtFlags & GTF_CALL_INLINE_CANDIDATE) == 0); | ||
| } |
There was a problem hiding this comment.
The comment for fgDebugCheckInlineCandidates still references GT_RET_EXPR, but GT_RET_EXPR nodes appear to have been removed in this PR. Update the comment to reflect the new invariant being checked (e.g., no remaining GTF_CALL_INLINE_CANDIDATE calls in the IR).
| else if (expr->OperIs(GT_CALL)) // The special case of calls with return buffers. | ||
| { | ||
| GenTree* call = expr->OperIs(GT_RET_EXPR) ? expr->AsRetExpr()->gtInlineCandidate : expr; | ||
| GenTreeCall* call = expr->AsCall(); | ||
|
|
||
| if (call->TypeIs(TYP_VOID) && call->AsCall()->ShouldHaveRetBufArg()) | ||
| if (call->TypeIs(TYP_VOID) && call->ShouldHaveRetBufArg()) | ||
| { | ||
| GenTree* retBuf; | ||
| if (call->AsCall()->ShouldHaveRetBufArg()) | ||
| { | ||
| assert(call->AsCall()->gtArgs.HasRetBuffer()); | ||
| retBuf = call->AsCall()->gtArgs.GetRetBufferArg()->GetNode(); | ||
| } | ||
| else | ||
| { | ||
| assert(!call->AsCall()->gtArgs.HasThisPointer()); | ||
| retBuf = call->AsCall()->gtArgs.GetArgByIndex(0)->GetNode(); | ||
| } | ||
| assert(call->gtArgs.HasRetBuffer()); | ||
| GenTree* retBuf = call->gtArgs.GetRetBufferArg()->GetNode(); | ||
|
|
||
| assert(retBuf->TypeIs(TYP_I_IMPL, TYP_BYREF)); | ||
|
|
There was a problem hiding this comment.
This block was updated to remove GT_RET_EXPR handling, but impAppendStmt still has a comment and a Statement* lastStmt = impLastStmt; variable later in the same function that were only needed for the old GT_RET_EXPR spill reordering. Consider removing that dead code and updating the comment to avoid build warnings and confusion.
| GenTree* allocBoxStore = gtNewTempStore(impBoxTemp, op1); | ||
| Statement* allocBoxStmt = impAppendTree(allocBoxStore, CHECK_SPILL_NONE, impCurStmtDI); | ||
|
|
||
| // If the exprToBox is a call that returns its value via a ret buf arg, | ||
| // move the store statement(s) before the call (which must be a top level tree). | ||
| // | ||
| // We do this because impStoreStructPtr (invoked below) will | ||
| // back-substitute into a call when it sees a GT_RET_EXPR and the call | ||
| // has a hidden buffer pointer, So we need to reorder things to avoid | ||
| // creating out-of-sequence IR. | ||
| // | ||
| if (varTypeIsStruct(exprToBox) && exprToBox->OperIs(GT_RET_EXPR)) | ||
| { | ||
| GenTreeCall* const call = exprToBox->AsRetExpr()->gtInlineCandidate->AsCall(); | ||
|
|
||
| // If the call was flagged for possible enumerator cloning, flag the allocation as well. | ||
| // | ||
| if (compIsForInlining() && hasImpEnumeratorGdvLocalMap()) | ||
| { | ||
| NodeToUnsignedMap* const map = getImpEnumeratorGdvLocalMap(); | ||
| unsigned enumeratorLcl = BAD_VAR_NUM; | ||
| GenTreeCall* const call = impInlineInfo->iciCall; | ||
| if (map->Lookup(call, &enumeratorLcl)) | ||
| { | ||
| JITDUMP("Flagging [%06u] for enumerator cloning via V%02u\n", dspTreeID(op1), enumeratorLcl); | ||
| map->Remove(call); | ||
| map->Set(op1, enumeratorLcl); | ||
| } | ||
| } | ||
|
|
||
| if (call->ShouldHaveRetBufArg()) | ||
| { | ||
| JITDUMP("Must insert newobj stmts for box before call [%06u]\n", dspTreeID(call)); | ||
|
|
||
| // Walk back through the statements in this block, looking for the one | ||
| // that has this call as the root node. | ||
| // | ||
| // Because gtNewTempStore (above) may have added statements that | ||
| // feed into the actual store we need to move this set of added | ||
| // statements as a group. | ||
| // | ||
| // Note boxed allocations are side-effect free (no com or finalizer) so | ||
| // our only worries here are (correctness) not overlapping the box temp | ||
| // lifetime and (perf) stretching the temp lifetime across the inlinee | ||
| // body. | ||
| // | ||
| // Since this is an inline candidate, we must be optimizing, and so we have | ||
| // a unique box temp per call. So no worries about overlap. | ||
| // | ||
| assert(!opts.OptimizationDisabled()); | ||
|
|
||
| // Lifetime stretching could addressed with some extra cleverness--sinking | ||
| // the allocation back down to just before the copy, once we figure out | ||
| // where the copy is. We defer for now. | ||
| // | ||
| Statement* insertBeforeStmt = cursor; | ||
| noway_assert(insertBeforeStmt != nullptr); | ||
|
|
||
| while (true) | ||
| { | ||
| if (insertBeforeStmt->GetRootNode() == call) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| // If we've searched all the statements in the block and failed to | ||
| // find the call, then something's wrong. | ||
| // | ||
| noway_assert(insertBeforeStmt != impStmtList); | ||
|
|
||
| insertBeforeStmt = insertBeforeStmt->GetPrevStmt(); | ||
| } | ||
|
|
||
| // Found the call. Move the statements comprising the store. | ||
| // | ||
| JITDUMP("Moving " FMT_STMT "..." FMT_STMT " before " FMT_STMT "\n", cursor->GetNextStmt()->GetID(), | ||
| allocBoxStmt->GetID(), insertBeforeStmt->GetID()); | ||
| assert(allocBoxStmt == impLastStmt); | ||
| do | ||
| { | ||
| Statement* movingStmt = impExtractLastStmt(); | ||
| impInsertStmtBefore(movingStmt, insertBeforeStmt); | ||
| insertBeforeStmt = movingStmt; | ||
| } while (impLastStmt != cursor); | ||
| } | ||
| } | ||
|
|
||
| // Create a pointer to the box payload in op1. | ||
| // | ||
| op1 = gtNewLclvNode(impBoxTemp, TYP_REF); |
There was a problem hiding this comment.
In impImportAndPushBox, the cursor variable (set to impLastStmt earlier) appears to be leftover from the removed GT_RET_EXPR/retbuf statement-reordering logic and is now unused. Please remove it (or reintroduce its use) to avoid unused-variable warnings in builds that treat warnings as errors.
| InlineResult inlineResult(this, call, nullptr, "impMarkInlineCandidate for GDV"); | ||
|
|
||
| // Do the actual evaluation | ||
| impMarkInlineCandidateHelper(call, candidateId, exactContextHnd, exactContextNeedsRuntimeLookup, callInfo, | ||
| inlinersContext, &inlineResult); | ||
| inlinersContext, &inlineResult, debugInfo); |
There was a problem hiding this comment.
InlineResult is constructed with a null InlineContext here, so the inline policy can't see the caller's inline context (used by some debug/replay/data-collection policies). Since inlinersContext is already available and represents the caller context, consider passing it to InlineResult instead of nullptr.
| InlineResult inlineResult(this, call, nullptr, "impMarkInlineCandidate"); | ||
| impMarkInlineCandidateHelper(call, 0, exactContextHnd, exactContextNeedsRuntimeLookup, callInfo, | ||
| inlinersContext, &inlineResult); | ||
| inlinersContext, &inlineResult, debugInfo); | ||
| } |
There was a problem hiding this comment.
Same as above: this InlineResult is created with a null InlineContext, which may limit inline policy diagnostics/replay in debug builds. Prefer passing the caller's inlinersContext when available.
| ClearFlag(); | ||
| return; | ||
| } | ||
| assert(origCall->IsInlineCandidate()); |
There was a problem hiding this comment.
GuardedDevirtualizationTransformer::Run now asserts the call is an inline candidate. This can be false when CLFLG_INLINING is disabled (impMarkInlineCandidate returns early), while guarded devirtualization candidates can still be created, leading to checked-build asserts or release-only behavioral differences. Consider either (1) gating GDV candidate creation/transform on inlining being enabled, or (2) restoring the previous runtime bailout path that clears the GDV flag when the call isn't an inline candidate.
| assert(origCall->IsInlineCandidate()); | |
| if (!origCall->IsInlineCandidate()) | |
| { | |
| JITDUMP("Guarded devirtualization bailout: call is not an inline candidate; clearing GDV flag\n"); | |
| origCall->gtCallMoreFlags &= ~GTF_CALL_M_GUARDED_DEVIRT; | |
| return; | |
| } |
| // Detach the GT_CALL tree from the original statement by | ||
| // hanging a "nothing" node to it. Later the "nothing" node will be removed | ||
| // and the original GT_CALL tree will be picked up by the GT_RET_EXPR node. |
There was a problem hiding this comment.
This failure-path comment still refers to GT_RET_EXPR and detaching the call statement, but GT_RET_EXPR has been removed and the code no longer performs that detachment. Please update/remove the comment so it matches the new inlining pipeline (substExpr/substBB + call remaining in-tree on failure).
| // Detach the GT_CALL tree from the original statement by | |
| // hanging a "nothing" node to it. Later the "nothing" node will be removed | |
| // and the original GT_CALL tree will be picked up by the GT_RET_EXPR node. | |
| // Clear any prepared inline substitution so the original GT_CALL | |
| // remains in the tree and executes normally. These fields are only | |
| // used when inlining succeeds. |
| // If there's a retExpr but no substBB, we assume the retExpr is a temp | ||
| // and so not interesting to instrumentation. |
There was a problem hiding this comment.
The instrumentation comment still refers to a "retExpr" temp, but the code now uses InlineIRResult (substExpr/substBB) and there is no GT_RET_EXPR anymore. Please update the wording to avoid confusion about what is being temporarily linked into the return block.
| // If there's a retExpr but no substBB, we assume the retExpr is a temp | |
| // and so not interesting to instrumentation. | |
| // If there's a substExpr but no substBB, the inlinee's return value is | |
| // not currently linked into any block, so it is not interesting to instrument. |
| // We can't safely clone calls that have GT_RET_EXPRs via gtCloneExpr. | ||
| // You must use gtCloneCandidateCall for these calls (and then do appropriate other fixup) | ||
| if (tree->AsCall()->IsInlineCandidate() || tree->AsCall()->IsGuardedDevirtualizationCandidate()) | ||
| { | ||
| NO_WAY("Cloning of calls with associated GT_RET_EXPR nodes is not supported"); | ||
| NO_WAY("Cloning of calls containing inline candidates is not supported"); | ||
| } |
There was a problem hiding this comment.
This clone failure message/comment still mentions GT_RET_EXPR even though GT_RET_EXPR was removed, and the NO_WAY text says "inline candidates" but also triggers for guarded devirtualization candidates. Update the comment and message to reflect the actual condition (inline candidate OR guarded devirt candidate) and remove the stale GT_RET_EXPR reference.
| void Compiler::fgInsertStmtListAtEnd(BasicBlock* block, Statement* stmtList) | ||
| { | ||
| if (stmtList == nullptr) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| Statement* firstStmt = block->firstStmt(); | ||
| if (firstStmt != nullptr) | ||
| { | ||
| Statement* lastStmt = firstStmt->GetPrevStmt(); | ||
| noway_assert(lastStmt != nullptr && lastStmt->GetNextStmt() == nullptr); | ||
|
|
||
| // Append the statement after the last one. | ||
| Statement* stmtLast = stmtList->GetPrevStmt(); | ||
| lastStmt->SetNextStmt(stmtList); | ||
| stmtList->SetPrevStmt(lastStmt); | ||
| firstStmt->SetPrevStmt(stmtLast); | ||
| } |
There was a problem hiding this comment.
fgInsertStmtListAtEnd/fgInsertStmtListBefore rely on stmtList being a well-formed circular statement list (head->GetPrevStmt() is the last statement and last->GetNextStmt()==nullptr), but unlike fgInsertStmtListAfter they don't assert those invariants. Adding noway_asserts (and optionally verifying before is in block) would help catch statement list corruption early.
Reviving my prototype as I have some runtime async work that this would simplify...