[Wasm RyuJIT] Address block store issues from #125756#125770
[Wasm RyuJIT] Address block store issues from #125756#125770kg wants to merge 4 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR targets WebAssembly RyuJIT block-store lowering/codegen issues that are triggering asserts during crossgen/debug builds (per #125756), by adjusting how GT_STORE_BLK operands are represented/contained for memory.copy/memory.fill.
Changes:
- Update WASM
LowerBlockStoreto treatGT_INDsources as contained, and attempt to convert local sources to addresses for thememory.copyopcode path. - Add debug assertions in WASM codegen to validate containment expectations for native bulk-memory op emission (
memory.copy/memory.fill).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/coreclr/jit/lowerwasm.cpp | Changes block-store source handling/containment and attempts to convert local sources into address form for memory.copy. |
| src/coreclr/jit/codegenwasm.cpp | Adds debug assertions around contained operands for the native bulk-memory opcode path. |
You can also share your feedback on Copilot code review. Take the survey.
| } | ||
| else | ||
| { | ||
| assert(operand->OperIs(GT_LCL_FLD, GT_LCL_VAR, GT_LCL_ADDR)); |
There was a problem hiding this comment.
GT_LCL_ADDR is here for dests?
There was a problem hiding this comment.
Yeah when I expanded the cases this got called in, this assert tripped up on a LCL_ADDR at least once. Conceptually it seems reasonable to get an address here.
AndyAyersMS
left a comment
There was a problem hiding this comment.
This looks good to me overall.
There was a problem hiding this comment.
Pull request overview
This PR targets WASM RyuJIT block-store correctness by adjusting lowering/RA hints and consolidating codegen paths for different GT_STORE_BLK lowering kinds, aiming to address debug/assert failures reported in #125756.
Changes:
- Move
SetMultiplyUsedmarking inLowerBlockStoreso it applies consistently across block-store kinds. - Route
BlkOpKindNativeOpcodeblock stores throughgenCodeForCpObjto share logic with theCpObjUnrollpath. - Refactor
genCodeForCpObjoperand handling to support both cpobj and nativememory.copy/memory.fill.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/coreclr/jit/lowerwasm.cpp | Moves multiply-used marking to apply across cpobj and native-op block stores. |
| src/coreclr/jit/codegenwasm.cpp | Unifies NativeOpcode handling with CpObjUnroll and refactors operand decoding/emission. |
| else if (isSource && operand->OperIs(GT_CNS_INT)) | ||
| { | ||
| addrType = TYP_INT; | ||
| offset = 0; | ||
| reg = REG_NA; | ||
| } | ||
| else | ||
| { | ||
| assert(operand->OperIs(GT_LCL_FLD, GT_LCL_VAR, GT_LCL_ADDR)); | ||
| GenTreeLclVarCommon* lclVar = operand->AsLclVarCommon(); |
There was a problem hiding this comment.
makeOperandRec doesn't handle initblk sources wrapped in GT_INIT_VAL (used to represent byte-pattern fills). For BlkOpKindNativeOpcode+initblk, cpObjNode->Data() can be GT_INIT_VAL, which will currently fall into the LCL_* branch and hit the assert / invalid AsLclVarCommon cast. Consider unwrapping GT_INIT_VAL to its operand before the GT_IND/GT_CNS_INT/LCL_* classification (similar to how other targets/codepaths drop GT_INIT_VAL).
| bool fpBased; | ||
| reg = GetFramePointerReg(); | ||
| offset = m_compiler->lvaFrameAddress(lclVar->GetLclNum(), &fpBased) + lclVar->GetLclOffs(); | ||
| assert(fpBased); |
There was a problem hiding this comment.
The LCL_* case in makeOperandRec computes an address using frame pointer + lvaFrameAddress, which is only valid when the local itself is the source buffer living on the stack. For destination address operands (and for byref locals that are enregistered), this computes the address of the local’s home/slot rather than the address value, and can also assert if the local isn’t FP-based. Consider using GetMultiUseOperandReg/operand->GetRegNum() for address-valued operands, and reserving the FP+offset computation only for stack-resident struct sources.
| bool fpBased; | |
| reg = GetFramePointerReg(); | |
| offset = m_compiler->lvaFrameAddress(lclVar->GetLclNum(), &fpBased) + lclVar->GetLclOffs(); | |
| assert(fpBased); | |
| // For address-valued locals (e.g. GT_LCL_ADDR) or destination operands, the | |
| // address is already materialized in a register; use that directly. | |
| if (operand->OperIs(GT_LCL_ADDR) || !isSource) | |
| { | |
| addrType = operand->TypeGet(); | |
| reg = GetMultiUseOperandReg(operand); | |
| offset = 0; | |
| } | |
| else | |
| { | |
| // For source locals that represent stack-resident buffers, compute | |
| // the address as frame-pointer + frame offset. | |
| bool fpBased; | |
| reg = GetFramePointerReg(); | |
| offset = m_compiler->lvaFrameAddress(lclVar->GetLclNum(), &fpBased) + lclVar->GetLclOffs(); | |
| assert(fpBased); | |
| } |
There was a problem hiding this comment.
Is this correct? I don't really understand it
There was a problem hiding this comment.
There are quite a few different cases and it's hard to be sure without tracing through what happens upstream.
You might want to work through a set of examples just to see the variety of things that need to be handled. Something like:
struct S { ... }
S getS();
void useS(ref S s);
void foo(ref S src, ref S dst) { dst = src }
void foo(S src, ref S dst) { dst = src }
void foo(ref S src) { S dst = src; }
void foo(ref S src) { useS(src); }
void foo (ref S dst) { S src = new S(...); dst = src }
void foo (ref S dst) { S src = new S; dst = src }
void foo (ref S dst) { dst = getS(); }
and try cases where S is a single-field wrapper and also has multiple fields.
| blkNode->gtBlkOpKind = GenTreeBlk::BlkOpKindNativeOpcode; | ||
| } | ||
|
|
||
| SetMultiplyUsed(dstAddr); |
There was a problem hiding this comment.
Checks missing for whether this actually needs the null check (GTF_IND_NONFAULTING).
Also, since we're fixing this code, may as well fix it completely by adding the null check for init blocks too.
|
|
||
| //------------------------------------------------------------------------ | ||
| // genCodeForCpObj: Produce code for a GT_STORE_BLK node that represents a cpobj operation. | ||
| // genCodeForCpObj: Produce code for a GT_STORE_BLK node that represents a cpobj operation |
There was a problem hiding this comment.
There is no specific need to overload genCodeForCpObj... we can just house all this logic in genCodeForStoreBlk if it makes things easier to share. It actually looks like it would flush out the logic to look more straight as well.
| bool isContained; | ||
| }; | ||
|
|
||
| auto makeOperandRec = [&](GenTree* operand, bool isSource) { |
There was a problem hiding this comment.
Using this for the destination looks unnecessary, since we don't need to materialize the destination address manually. The pattern we had here before was simpler. More generally it feels like open-coding the various possibilities would lead to simpler code.
| srcOffset = 0; | ||
|
|
||
| if (doNullCheck) | ||
| struct operandRec |
There was a problem hiding this comment.
Naming nit: operandRec -> OperandRec, addrType -> AddrType.
(What does "rec" mean?)
Fixes some of the issues from #125756 , at least based on my isolated repros.
cc @AndyAyersMS still figuring this out, don't completely understand it.