Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fb61856
Properly handle Wasm PackedSimd unsigned ulong compares in lowering
adamperlin Jun 26, 2026
733ac6c
Proper codegen and containment for ExtractScalar/ReplaceScalar
adamperlin Jul 1, 2026
5e39af6
Enable Splat and Left/Right shifts as SIMD operations
adamperlin Jul 2, 2026
d6ef2cd
Properly lower non-const immediate operands (preparation for building…
adamperlin Jul 2, 2026
9fb2ff9
WIP jump tables
adamperlin Jul 3, 2026
1d3a915
Properly handle multiply used operands for jump table intrinsics on W…
adamperlin Jul 7, 2026
200327e
Apply suggestions from code review
adamperlin Jul 7, 2026
03074c8
Address some review feedback and fix ABI classification for all SIMD …
adamperlin Jul 7, 2026
24d8d06
More cleanup
adamperlin Jul 7, 2026
de21e1c
Merge branch 'main' of github.com:dotnet/runtime into adamperlin/wasm…
adamperlin Jul 7, 2026
0eba689
Properly add range check for ReplaceScalar
adamperlin Jul 7, 2026
26eb5a9
Fix hwintrinsiclist table changes that were reverted during merge con…
adamperlin Jul 7, 2026
7d842b0
Fix order of MultiplyUsed operand consumption for hardware intrinsics…
adamperlin Jul 9, 2026
d63e3d3
Merge branch 'main' of github.com:dotnet/runtime into adamperlin/wasm…
adamperlin Jul 9, 2026
227b78c
Clean up some comments
adamperlin Jul 9, 2026
3dc83dd
Fix copilot feedback
adamperlin Jul 9, 2026
4e98a74
Add efault case to switch
adamperlin Jul 9, 2026
1cd93db
Merge branch 'main' of github.com:dotnet/runtime into adamperlin/wasm…
adamperlin Jul 9, 2026
fc10e1a
Fix compilation errors on linux
adamperlin Jul 9, 2026
7823f44
More review feedback
adamperlin Jul 9, 2026
f94a3f6
Merge branch 'adamperlin/wasm-simd-unsigned-compare-lane-ops' of gith…
adamperlin Jul 9, 2026
af827a3
More review feedback
adamperlin Jul 9, 2026
a70c71b
Add GetImmOp GenTree method for wasm only to avoid a hw intrinsic fla…
adamperlin Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/coreclr/jit/codegen.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ class CodeGen final : public CodeGenInterface
void ensureCurrentFuncIsUnwindable();
void genEmitIf(WasmValueType blockType = WasmValueType::Invalid);
void genEmitEndIf();
void genEmitBeginBlock(WasmValueType blockType = WasmValueType::Invalid);
void genEmitEndBlock();
void genEmitFunctionEnd(bool emitTerminalUnreachable = true);
#endif

Expand Down Expand Up @@ -1085,6 +1087,10 @@ class CodeGen final : public CodeGenInterface

#endif // TARGET_ARM64

#if defined(TARGET_WASM)
void genHWIntrinsicJumpTableFallback(GenTreeHWIntrinsic* node, HWIntrinsic info);
Comment thread
tannergooding marked this conversation as resolved.
#endif

#endif // FEATURE_HW_INTRINSICS

#if !defined(TARGET_64BIT)
Expand Down
85 changes: 59 additions & 26 deletions src/coreclr/jit/codegenwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2274,9 +2274,10 @@ void CodeGen::genEmitNullCheck(regNumber reg)
void CodeGen::genRangeCheck(GenTree* tree)
{
assert(tree->OperIs(GT_BOUNDS_CHECK));
genConsumeOperands(tree->AsOp());
GenTreeBoundsChk* boundsCheck = tree->AsBoundsChk();
genConsumeOperands(boundsCheck->AsOp());
Comment thread
adamperlin marked this conversation as resolved.
Outdated
GetEmitter()->emitIns(INS_I_ge_u);
genJumpToThrowHlpBlk(SCK_RNGCHK_FAIL);
genJumpToThrowHlpBlk(boundsCheck->gtThrowKind);
Comment thread
adamperlin marked this conversation as resolved.
Comment thread
adamperlin marked this conversation as resolved.
}

//------------------------------------------------------------------------
Expand Down Expand Up @@ -3797,6 +3798,38 @@ void CodeGen::genEmitEndIf()
GetEmitter()->emitIns(INS_end);
}

// ------------------------------------------------------------------------
// genEmitBeginBlock: Emit a 'block' instruction.
//
// Notes:
// The block is not modeled as part of wasmControlFlowStack.
// This is used for jump tables which don't have corresponding BasicBlock's
// and which will be emitted within a single block with known offsets for each case.
void CodeGen::genEmitBeginBlock(WasmValueType blockType)
{
wasmExtraControlFlowDepth++;
if (blockType != WasmValueType::Invalid)
{
GetEmitter()->emitIns_BlockTy(INS_block, blockType);
}
else
{
GetEmitter()->emitIns(INS_block);
}
}

// -----------------------------------------------------------------------
// genEmitEndBlock: Emit an 'end' instruction closing a 'block'.
//
// Notes:
// Removes the added stack depth from genEmitBeginBlock.
void CodeGen::genEmitEndBlock()
{
assert(wasmExtraControlFlowDepth > 0);
wasmExtraControlFlowDepth--;
GetEmitter()->emitIns(INS_end);
}

//------------------------------------------------------------------------
// inst_JMP: Emit a jump instruction.
//
Expand Down Expand Up @@ -3849,34 +3882,34 @@ void CodeGen::genWasmEmitterUnitTestsSimd()
DROP

// Extract lane: v128 -> scalar (i32/i64/f32/f64), then drop
#define TEST_EXTRACT_LANE(bytes, ins, attr, lane) \
#define TEST_EXTRACT_LANE(bytes, ins, lane) \
PUSH_V128(bytes); \
emit->emitIns_Lane(ins, attr, lane); \
emit->emitIns_Lane(ins, lane); \
DROP

// Replace lane: [v128, scalar] -> v128, then drop
#define TEST_REPLACE_LANE_I32(bytes, ins, attr, lane) \
#define TEST_REPLACE_LANE_I32(bytes, ins, lane) \
PUSH_V128(bytes); \
PUSH_I32(42); \
emit->emitIns_Lane(ins, attr, lane); \
emit->emitIns_Lane(ins, lane); \
DROP

#define TEST_REPLACE_LANE_I64(bytes, ins, attr, lane) \
#define TEST_REPLACE_LANE_I64(bytes, ins, lane) \
PUSH_V128(bytes); \
PUSH_I64(42); \
emit->emitIns_Lane(ins, attr, lane); \
emit->emitIns_Lane(ins, lane); \
DROP

#define TEST_REPLACE_LANE_F32(bytes, ins, attr, lane) \
#define TEST_REPLACE_LANE_F32(bytes, ins, lane) \
PUSH_V128(bytes); \
PUSH_F32(0); \
emit->emitIns_Lane(ins, attr, lane); \
emit->emitIns_Lane(ins, lane); \
DROP

#define TEST_REPLACE_LANE_F64(bytes, ins, attr, lane) \
#define TEST_REPLACE_LANE_F64(bytes, ins, lane) \
PUSH_V128(bytes); \
PUSH_F64(0); \
emit->emitIns_Lane(ins, attr, lane); \
emit->emitIns_Lane(ins, lane); \
DROP

// Load lane: [i32_addr, v128] -> v128, then drop
Expand Down Expand Up @@ -3918,30 +3951,30 @@ void CodeGen::genWasmEmitterUnitTestsSimd()

// --- IF_LANE: extract/replace lane instructions ---
// i8x16 lanes (0..15)
TEST_EXTRACT_LANE(v128Ones, INS_i8x16_extract_lane_s, EA_1BYTE, 0);
TEST_EXTRACT_LANE(v128Ones, INS_i8x16_extract_lane_u, EA_1BYTE, 15);
TEST_REPLACE_LANE_I32(v128Ones, INS_i8x16_replace_lane, EA_1BYTE, 7);
TEST_EXTRACT_LANE(v128Ones, INS_i8x16_extract_lane_s, 0);
TEST_EXTRACT_LANE(v128Ones, INS_i8x16_extract_lane_u, 15);
TEST_REPLACE_LANE_I32(v128Ones, INS_i8x16_replace_lane, 7);

// i16x8 lanes (0..7)
TEST_EXTRACT_LANE(v128Ones, INS_i16x8_extract_lane_s, EA_2BYTE, 0);
TEST_EXTRACT_LANE(v128Ones, INS_i16x8_extract_lane_u, EA_2BYTE, 7);
TEST_REPLACE_LANE_I32(v128Ones, INS_i16x8_replace_lane, EA_2BYTE, 3);
TEST_EXTRACT_LANE(v128Ones, INS_i16x8_extract_lane_s, 0);
TEST_EXTRACT_LANE(v128Ones, INS_i16x8_extract_lane_u, 7);
TEST_REPLACE_LANE_I32(v128Ones, INS_i16x8_replace_lane, 3);

// i32x4 lanes (0..3)
TEST_EXTRACT_LANE(v128Ones, INS_i32x4_extract_lane, EA_4BYTE, 0);
TEST_REPLACE_LANE_I32(v128Ones, INS_i32x4_replace_lane, EA_4BYTE, 3);
TEST_EXTRACT_LANE(v128Ones, INS_i32x4_extract_lane, 0);
TEST_REPLACE_LANE_I32(v128Ones, INS_i32x4_replace_lane, 3);

// i64x2 lanes (0..1)
TEST_EXTRACT_LANE(v128Ones, INS_i64x2_extract_lane, EA_8BYTE, 0);
TEST_REPLACE_LANE_I64(v128Ones, INS_i64x2_replace_lane, EA_8BYTE, 1);
TEST_EXTRACT_LANE(v128Ones, INS_i64x2_extract_lane, 0);
TEST_REPLACE_LANE_I64(v128Ones, INS_i64x2_replace_lane, 1);

// f32x4 lanes (0..3)
TEST_EXTRACT_LANE(v128Ones, INS_f32x4_extract_lane, EA_4BYTE, 3);
TEST_REPLACE_LANE_F32(v128Ones, INS_f32x4_replace_lane, EA_4BYTE, 0);
TEST_EXTRACT_LANE(v128Ones, INS_f32x4_extract_lane, 3);
TEST_REPLACE_LANE_F32(v128Ones, INS_f32x4_replace_lane, 0);

// f64x2 lanes (0..1)
TEST_EXTRACT_LANE(v128Ones, INS_f64x2_extract_lane, EA_8BYTE, 0);
TEST_REPLACE_LANE_F64(v128Ones, INS_f64x2_replace_lane, EA_8BYTE, 1);
TEST_EXTRACT_LANE(v128Ones, INS_f64x2_extract_lane, 0);
TEST_REPLACE_LANE_F64(v128Ones, INS_f64x2_replace_lane, 1);

// --- IF_MEMARG_LANE: load/store lane with memarg ---
TEST_LOAD_LANE(v128Ones, INS_v128_load8_lane, EA_1BYTE, 0, 5);
Expand Down
6 changes: 3 additions & 3 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -10164,16 +10164,16 @@ class Compiler
return sizeBytes;
}

// Get the number of elements of baseType of SIMD vector given by its size and baseType
static int getSIMDVectorLength(unsigned simdSize, var_types baseType);

// Get the number of elements of baseType of SIMD vector given by its type handle
int getSIMDVectorLength(CORINFO_CLASS_HANDLE typeHnd);

// Get preferred alignment of SIMD type.
int getSIMDTypeAlignment(var_types simdType);

public:
// Get the number of elements of baseType of SIMD vector given by its size and baseType
static int getSIMDVectorLength(unsigned simdSize, var_types baseType);

// Get the number of bytes in a System.Numeric.Vector<T> for the current compilation.
// Note - cannot be used for System.Runtime.Intrinsic
uint32_t getVectorTByteLength()
Expand Down
34 changes: 27 additions & 7 deletions src/coreclr/jit/emitwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ void emitter::emitIns_I(instruction ins, emitAttr attr, cnsval_ssize_t imm)
// ins - instruction to emit
// attr - emit attributes
// imm - immediate value (depth in control flow stack)
// targetBlock - block at that depth
// targetBlock - block at that depth (may be null, in the case of jump table which doesn't target a real block)
//
void emitter::emitIns_J(instruction ins, emitAttr attr, cnsval_ssize_t imm, BasicBlock* targetBlock)
{
Expand All @@ -116,7 +116,7 @@ void emitter::emitIns_J(instruction ins, emitAttr attr, cnsval_ssize_t imm, Basi
id->idIns(ins);
id->idInsFmt(fmt);

if (m_debugInfoSize > 0)
if (m_debugInfoSize > 0 && targetBlock != nullptr)
{
id->idDebugOnlyInfo()->idTargetBlock = targetBlock;
}
Expand Down Expand Up @@ -463,14 +463,34 @@ void emitter::emitIns_V128Imm(instruction ins, const uint8_t bytes[16])
//
// Arguments:
// ins - instruction (e.g., INS_i8x16_extract_lane_s)
// attr - emit attribute indicating the lane element size
// laneIdx - lane index byte
//
void emitter::emitIns_Lane(instruction ins, emitAttr attr, uint8_t laneIdx)
void emitter::emitIns_Lane(instruction ins, uint8_t laneIdx)
{
instrDesc* id = emitNewInstrSC(attr, laneIdx);
insFormat fmt = emitInsFormat(ins);
uint8_t elemSize = CodeGenInterface::instSimdElemSize(ins);
uint8_t elemSize = CodeGenInterface::instSimdElemSize(ins);

// Add element width as an emit attribute
emitAttr attr = EA_UNKNOWN;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this not just be EA_ATTR(size) or similar?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I knew there had to be a helper to do this, thank you

switch (elemSize)
Comment thread
adamperlin marked this conversation as resolved.
{
case 1:
attr = EA_1BYTE;
break;
case 2:
attr = EA_2BYTE;
break;
case 4:
attr = EA_4BYTE;
break;
case 8:
attr = EA_8BYTE;
break;
default:
unreached();
}

instrDesc* id = emitNewInstrSC(attr, laneIdx);
insFormat fmt = emitInsFormat(ins);
assert(fmt == IF_LANE);
assert(isValidVectorIndex(elemSize, laneIdx));

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/emitwasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ void emitIns_S_R(instruction ins, emitAttr attr, regNumber ireg, int varx, int o

// Packed SIMD instruction emit functions
void emitIns_V128Imm(instruction ins, const uint8_t bytes[16]);
void emitIns_Lane(instruction ins, emitAttr attr, uint8_t laneIdx);
void emitIns_Lane(instruction ins, uint8_t laneIdx);
void emitIns_MemargLane(instruction ins, emitAttr attr, cnsval_ssize_t offset, uint8_t laneIdx);

void emitAddressConstant(void* address);
Expand Down
17 changes: 17 additions & 0 deletions src/coreclr/jit/gentree.h
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,10 @@ enum GenTreeFlags : unsigned
#ifdef FEATURE_HW_INTRINSICS
GTF_HW_EM_OP = 0x10000000, // GT_HWINTRINSIC -- node is used as an operand to an embedded mask
GTF_HW_USER_CALL = 0x20000000, // GT_HWINTRINSIC -- node is implemented via a user call
#ifdef TARGET_WASM
GTF_HW_NEEDS_JUMP_TABLE = 0x40000000, // GT_HWINTRINSIC -- (WASM) intrinsic has a non-constant immediate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really a small optimization to avoid having to re-check the operands for a GT_HWINTRINSIC over and over again to see if a jump table is needed (this mostly comes up in regalloc, where we only need to mark the operands for an intrinsic as MultiplyUsed if it needs a jump table)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expensive to do for wasm?

On most platforms this is just checking if (lastOp->IsCnsIntOrI()) and we already have the last op, so it avoids needing to burn any flag.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The immediate value isn't always lastOp on Wasm, so I think we'd have to use HWIntrinsicInfo::GetImmOpsPositions or cache that information, I think.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to pull all the operands anyways, so is that necessarily a bad thing?

The main concern I have is that the available GenTree flags are scarce. The need for a jump table is then expected to be rare and such nodes are rare enough that it's unlikely to even show up on the SPMI throughput tracking, so just doing this the "same way" as all other architectures (i.e. checking immOp->IsCnsIntOrI()) feels "best"

// operand and requires a jump-table fallback at codegen
#endif // TARGET_WASM
#endif // FEATURE_HW_INTRINSICS
};

Expand Down Expand Up @@ -2456,6 +2460,19 @@ struct GenTree
gtFlags |= GTF_HW_EM_OP;
}

#ifdef TARGET_WASM
bool NeedsJumpTableFallback() const
{
return OperIsHWIntrinsic() && ((gtFlags & GTF_HW_NEEDS_JUMP_TABLE) != 0);
}

void SetNeedsJumpTableFallback()
{
assert(OperIsHWIntrinsic());
gtFlags |= GTF_HW_NEEDS_JUMP_TABLE;
}
#endif // TARGET_WASM

#endif // FEATURE_HW_INTRINSICS

static bool HandleKindDataIsInvariant(GenTreeFlags flags);
Expand Down
12 changes: 11 additions & 1 deletion src/coreclr/jit/hwintrinsic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2322,7 +2322,7 @@ GenTree* Compiler::impHWIntrinsic(NamedIntrinsic intrinsic,
immUpperBound = HWIntrinsicInfo::lookupImmUpperBound(intrinsic);
hasFullRangeImm = HWIntrinsicInfo::HasFullRangeImm(intrinsic);
#elif defined(TARGET_WASM)
immUpperBound = HWIntrinsicInfo::lookupImmUpperBound(intrinsic, simdBaseType);
immUpperBound = HWIntrinsicInfo::lookupImmUpperBound(intrinsic, simdSize, simdBaseType);
#endif

if (!CheckHWIntrinsicImmRange(intrinsic, simdBaseType, immOp1, mustExpand, immLowerBound, immUpperBound,
Expand Down Expand Up @@ -2642,6 +2642,16 @@ GenTree* Compiler::impHWIntrinsic(NamedIntrinsic intrinsic,
op2 = addRangeCheckIfNeeded(intrinsic, op2, immLowerBound, immUpperBound);
}
else
#elif defined(TARGET_WASM)
// On WASM, PackedSimd.ReplaceScalar takes the lane immediate as the middle
// (op2) operand: ReplaceScalar(Vector128<T> vector, byte imm, T value). Other
// 3-arg PackedSimd immediate intrinsics (LoadScalarAndInsert, StoreSelectedScalar)
// put the immediate at op3 and fall through to the default handling below.
if (intrinsic == NI_PackedSimd_ReplaceScalar)
{
op2 = addRangeCheckIfNeeded(intrinsic, op2, immLowerBound, immUpperBound);
}
else
#endif
{
op3 = addRangeCheckIfNeeded(intrinsic, op3, immLowerBound, immUpperBound);
Expand Down
Loading
Loading