Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5f042fc
Initial plan
Copilot May 26, 2026
0d43c80
JIT: saturate float/double -> small int casts
Copilot May 26, 2026
30c6aec
Add regression test and update interpreter test for saturating R->sma…
Copilot May 26, 2026
3081861
JIT: clamp via Min/MaxNative in float domain for R->smallType saturat…
Copilot May 26, 2026
99629b2
Fix interpreter and ILC preinit saturation for R->small int
Copilot May 27, 2026
f36debb
Merge branch 'main' into copilot/fix-invalid-result-double-32768
tannergooding Jun 2, 2026
84a48ab
Apply suggestions from code review
tannergooding Jun 2, 2026
af1068c
Apply suggestions from code review
jkotas Jun 2, 2026
9e2ae48
Re-enable #116823 generic math conversion tests
Copilot Jun 2, 2026
1486caa
Address code review: remove static from nullEntry, move assert before…
Copilot Jun 2, 2026
f57b586
Optimize TryConvert methods: use direct float/double casts on CoreCLR…
Copilot Jun 2, 2026
d6b33d3
JIT/ARM32: Add SSAT/USAT support for saturating float->small integral…
Copilot Jun 2, 2026
154bb7c
TypePreinit: add TODO comments with commented-out original code for c…
Copilot Jun 2, 2026
c56595a
JIT/WASM: Add TARGET_WASM to float->small-int saturating cast guard i…
Copilot Jun 2, 2026
db398bf
Apply formatting patch
tannergooding Jun 3, 2026
867f217
Fix NaN handling for RISC-V64/LoongArch64 float->small-int casts
Copilot Jun 3, 2026
a5c5f12
Apply suggestion from @jkotas
jkotas Jun 3, 2026
cbcbd69
wip: investigate primitive ConvertToInteger small-type saturation path
Copilot Jun 4, 2026
5515e6b
Handle SaturateTo* intrinsics in remaining JIT paths
Copilot Jun 5, 2026
beedfdd
Merge branch 'main' into copilot/fix-invalid-result-double-32768
tannergooding Jun 5, 2026
71abf12
Address review feedback on saturation intrinsic codegen and ARM32 ssa…
Copilot Jun 5, 2026
c804a6a
Handle MaxNative/MinNative in EvalMathFuncBinary for WASM
Copilot Jun 8, 2026
a8dee91
Skip Runtime_116823 saturation regression test on Mono
Copilot Jun 8, 2026
7615438
Fix CoreCLR interpreter ConvertToIntegerNative for small target types
Copilot Jun 9, 2026
44c2407
Revert "Fix CoreCLR interpreter ConvertToIntegerNative for small targ…
Copilot Jun 10, 2026
2f97daf
Revert interpreter change; trim ConvertToIntegerNative tests to in-ra…
Copilot Jun 10, 2026
2b1509a
Fix jit formatting
tannergooding Jun 15, 2026
02c3bf1
Merge branch 'main' into copilot/fix-invalid-result-double-32768
tannergooding Jun 19, 2026
ca75a0b
Use NaN-propagating max/min for MaxNative/MinNative VN on WASM
Copilot Jun 24, 2026
d0a797f
Merge branch 'main' into copilot/fix-invalid-result-double-32768
tannergooding Jul 7, 2026
48dbe17
Apply suggestion from @jkotas
jkotas Jul 13, 2026
7d50b6d
Model higher cost for branch-based SaturateTo* lowering on RISC-V64/L…
tannergooding Jul 15, 2026
cc4f6a6
Keep WASM on the GT_INTRINSIC min/max path now that it defines FEATUR…
tannergooding Jul 15, 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
23 changes: 19 additions & 4 deletions src/coreclr/interpreter/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3604,6 +3604,13 @@ bool InterpCompiler::EmitNamedIntrinsicCall(NamedIntrinsic ni, bool nonVirtualCa
}

InterpOpcode convOp;
// For small (sub-int32) target types, ConvertToIntegerNative truncates the low
Comment thread
tannergooding marked this conversation as resolved.
Outdated
// bits of the saturating R -> int conversion rather than clamping at the small
// type's range. The "consistent" R -> small conversion opcodes saturate at the
// small type's range (matching the .NET 9 saturating R -> int rules applied at
// the destination width), so we explicitly emit a saturating R -> int4 followed
// by a truncating int4 -> small narrow.
InterpOpcode narrowOp = INTOP_NOP;

// Interpreter-TODO: In theory this should use the "native" variants of the conversion opcodes which are likely faster
// than the ones with cross-platform consistent behavior; however, that is quite a lot of new opcodes for probably little gain, so
Expand All @@ -3619,14 +3626,18 @@ bool InterpCompiler::EmitNamedIntrinsicCall(NamedIntrinsic ni, bool nonVirtualCa
case CORINFO_TYPE_ULONG:
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_U8_R4 : INTOP_CONV_U8_R8; break;
case CORINFO_TYPE_SHORT:
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_I2_R4 : INTOP_CONV_I2_R8; break;
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_I4_R4 : INTOP_CONV_I4_R8;
narrowOp = INTOP_CONV_I2_I4; break;
case CORINFO_TYPE_CHAR:
case CORINFO_TYPE_USHORT:
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_U2_R4 : INTOP_CONV_U2_R8; break;
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_I4_R4 : INTOP_CONV_I4_R8;
narrowOp = INTOP_CONV_U2_I4; break;
case CORINFO_TYPE_BYTE:
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_I1_R4 : INTOP_CONV_I1_R8; break;
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_I4_R4 : INTOP_CONV_I4_R8;
narrowOp = INTOP_CONV_I1_I4; break;
case CORINFO_TYPE_UBYTE:
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_U1_R4 : INTOP_CONV_U1_R8; break;
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_I4_R4 : INTOP_CONV_I4_R8;
narrowOp = INTOP_CONV_U1_I4; break;
#ifdef TARGET_64BIT
case CORINFO_TYPE_NATIVEINT:
convOp = (sourceType == InterpTypeR4) ? INTOP_CONV_I8_R4 : INTOP_CONV_I8_R8; break;
Expand All @@ -3643,6 +3654,10 @@ bool InterpCompiler::EmitNamedIntrinsicCall(NamedIntrinsic ni, bool nonVirtualCa
}

EmitConv(m_pStackPointer - 1, g_stackTypeFromInterpType[targetType], convOp);
if (narrowOp != INTOP_NOP)
{
EmitConv(m_pStackPointer - 1, StackTypeI4, narrowOp);
}
return true;
}

Expand Down
20 changes: 20 additions & 0 deletions src/coreclr/jit/assertionprop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,26 @@ bool IntegralRange::Contains(int64_t value) const
return {SymbolicIntegerValue::Zero, SymbolicIntegerValue::ByteMax};
}

case NI_PRIMITIVE_SaturateToInt8:
{
return {SymbolicIntegerValue::ByteMin, SymbolicIntegerValue::ByteMax};
}

case NI_PRIMITIVE_SaturateToInt16:
{
return {SymbolicIntegerValue::ShortMin, SymbolicIntegerValue::ShortMax};
}

case NI_PRIMITIVE_SaturateToUInt8:
{
return {SymbolicIntegerValue::Zero, SymbolicIntegerValue::UByteMax};
}

case NI_PRIMITIVE_SaturateToUInt16:
{
return {SymbolicIntegerValue::Zero, SymbolicIntegerValue::UShortMax};
}

case NI_System_Runtime_CompilerServices_RuntimeHelpers_IsKnownConstant:
{
return {SymbolicIntegerValue::Zero, SymbolicIntegerValue::One};
Expand Down
26 changes: 26 additions & 0 deletions src/coreclr/jit/codegenarmarch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,32 @@ void CodeGen::genIntrinsic(GenTreeIntrinsic* treeNode)
GetEmitter()->emitInsBinary(INS_SQRT, emitActualTypeSize(treeNode), treeNode, srcNode);
break;

#ifdef TARGET_ARM
case NI_PRIMITIVE_SaturateToInt8:
// SSAT Rd, #8, Rn - saturate int32 to signed 8-bit range [-128, 127]
genConsumeOperands(treeNode->AsOp());
GetEmitter()->emitIns_R_R_I_I(INS_ssat, EA_4BYTE, treeNode->GetRegNum(), srcNode->GetRegNum(), 0, 8);
break;

case NI_PRIMITIVE_SaturateToInt16:
// SSAT Rd, #16, Rn - saturate int32 to signed 16-bit range [-32768, 32767]
genConsumeOperands(treeNode->AsOp());
GetEmitter()->emitIns_R_R_I_I(INS_ssat, EA_4BYTE, treeNode->GetRegNum(), srcNode->GetRegNum(), 0, 16);
break;

case NI_PRIMITIVE_SaturateToUInt8:
// USAT Rd, #8, Rn - saturate int32 to unsigned 8-bit range [0, 255]
genConsumeOperands(treeNode->AsOp());
GetEmitter()->emitIns_R_R_I_I(INS_usat, EA_4BYTE, treeNode->GetRegNum(), srcNode->GetRegNum(), 0, 8);
break;

case NI_PRIMITIVE_SaturateToUInt16:
// USAT Rd, #16, Rn - saturate int32 to unsigned 16-bit range [0, 65535]
genConsumeOperands(treeNode->AsOp());
GetEmitter()->emitIns_R_R_I_I(INS_usat, EA_4BYTE, treeNode->GetRegNum(), srcNode->GetRegNum(), 0, 16);
break;
#endif // TARGET_ARM

#if defined(FEATURE_SIMD)
// The handling is a bit more complex so genSimdUpperSave/Restore
// handles genConsumeOperands and genProduceReg
Expand Down
89 changes: 88 additions & 1 deletion src/coreclr/jit/codegenloongarch64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4450,7 +4450,94 @@ void CodeGen::genEmitGSCookieCheck(bool tailCall)
//
void CodeGen::genIntrinsic(GenTreeIntrinsic* treeNode)
{
NYI("unimplemented on LOONGARCH64 yet");
GenTree* op1 = treeNode->gtGetOp1();
GenTree* op2 = treeNode->gtGetOp2IfPresent();

// Handle integer-domain saturation intrinsics separately; they use branches
// and a temporary register rather than the single-instruction FP pattern below.
switch (treeNode->gtIntrinsicName)
{
case NI_PRIMITIVE_SaturateToInt8:
case NI_PRIMITIVE_SaturateToInt16:
case NI_PRIMITIVE_SaturateToUInt8:
case NI_PRIMITIVE_SaturateToUInt16:
{
ssize_t minVal, maxVal;
switch (treeNode->gtIntrinsicName)
{
case NI_PRIMITIVE_SaturateToInt8:
minVal = INT8_MIN;
maxVal = INT8_MAX;
break;
case NI_PRIMITIVE_SaturateToInt16:
minVal = INT16_MIN;
maxVal = INT16_MAX;
break;
case NI_PRIMITIVE_SaturateToUInt8:
minVal = 0;
maxVal = UINT8_MAX;
break;
case NI_PRIMITIVE_SaturateToUInt16:
minVal = 0;
maxVal = UINT16_MAX;
break;
default:
unreached();
}

genConsumeOperands(treeNode->AsOp());
regNumber dst = treeNode->GetRegNum();
regNumber src = op1->GetRegNum();
regNumber tmpReg = internalRegisters.GetSingle(treeNode);
emitter* emit = GetEmitter();

// Copy src to dst, normalizing to a sign-extended 32-bit value so the
// subsequent full-register bge compares against the (signed) clamp bounds
// are well-defined. `slli.w rd, rs, 0` sign-extends bits[31:0] into rd[63:0].
emit->emitIns_R_R_I(INS_slli_w, EA_4BYTE, dst, src, 0);

// Clamp lower bound: if dst < minVal, dst = minVal.
BasicBlock* skipLo = genCreateTempLabel();
instGen_Set_Reg_To_Imm(EA_PTRSIZE, tmpReg, minVal);
emit->emitIns_J_cond_la(INS_bge, skipLo, dst, tmpReg); // skip if dst >= minVal
emit->emitIns_R_R(INS_mov, EA_PTRSIZE, dst, tmpReg); // dst = minVal
genDefineTempLabel(skipLo);

// Clamp upper bound: if dst > maxVal, dst = maxVal.
BasicBlock* skipHi = genCreateTempLabel();
instGen_Set_Reg_To_Imm(EA_PTRSIZE, tmpReg, maxVal);
emit->emitIns_J_cond_la(INS_bge, skipHi, tmpReg, dst); // skip if maxVal >= dst
emit->emitIns_R_R(INS_mov, EA_PTRSIZE, dst, tmpReg); // dst = maxVal
genDefineTempLabel(skipHi);

genProduceReg(treeNode);
return;
}

default:
break;
}

emitAttr attr = emitActualTypeSize(treeNode);
instruction instr;

// All remaining intrinsics are binary floating-point operations.
assert(op2 != nullptr);

switch (treeNode->gtIntrinsicName)
{
case NI_System_Math_MaxNative:
instr = (attr == EA_4BYTE) ? INS_fmax_s : INS_fmax_d;
break;
case NI_System_Math_MinNative:
instr = (attr == EA_4BYTE) ? INS_fmin_s : INS_fmin_d;
break;
default:
NO_WAY("Unknown intrinsic");
}
genConsumeOperands(treeNode->AsOp());
GetEmitter()->emitIns_R_R_R(instr, attr, treeNode->GetRegNum(), op1->GetRegNum(), op2->GetRegNum());
genProduceReg(treeNode);
Comment thread
tannergooding marked this conversation as resolved.
}

//---------------------------------------------------------------------
Expand Down
65 changes: 65 additions & 0 deletions src/coreclr/jit/codegenriscv64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4224,6 +4224,71 @@ void CodeGen::genIntrinsic(GenTreeIntrinsic* treeNode)
GenTree* op1 = treeNode->gtGetOp1();
GenTree* op2 = treeNode->gtGetOp2IfPresent();

// Handle integer-domain saturation intrinsics separately; they use branches
// and a temporary register rather than the single-instruction pattern below.
switch (treeNode->gtIntrinsicName)
{
case NI_PRIMITIVE_SaturateToInt8:
case NI_PRIMITIVE_SaturateToInt16:
case NI_PRIMITIVE_SaturateToUInt8:
case NI_PRIMITIVE_SaturateToUInt16:
{
ssize_t minVal, maxVal;
switch (treeNode->gtIntrinsicName)
{
case NI_PRIMITIVE_SaturateToInt8:
minVal = INT8_MIN;
maxVal = INT8_MAX;
break;
case NI_PRIMITIVE_SaturateToInt16:
minVal = INT16_MIN;
maxVal = INT16_MAX;
break;
case NI_PRIMITIVE_SaturateToUInt8:
minVal = 0;
maxVal = UINT8_MAX;
break;
case NI_PRIMITIVE_SaturateToUInt16:
minVal = 0;
maxVal = UINT16_MAX;
break;
default:
unreached();
}

genConsumeOperands(treeNode->AsOp());
regNumber dst = treeNode->GetRegNum();
regNumber src = op1->GetRegNum();
regNumber tmpReg = internalRegisters.GetSingle(treeNode);
emitter* emit = GetEmitter();

// Copy src to dst, normalizing to a sign-extended 32-bit value so the
// subsequent full-register bge compares against the (signed) clamp bounds
// are well-defined. `sext.w rd, rs` sign-extends bits[31:0] into rd[63:0].
emit->emitIns_R_R(INS_sext_w, EA_4BYTE, dst, src);

// Clamp lower bound: if dst < minVal, dst = minVal.
BasicBlock* skipLo = genCreateTempLabel();
instGen_Set_Reg_To_Imm(EA_PTRSIZE, tmpReg, minVal);
emit->emitIns_J_cond_la(INS_bge, skipLo, dst, tmpReg); // skip if dst >= minVal
emit->emitIns_R_R(INS_mov, EA_PTRSIZE, dst, tmpReg); // dst = minVal
genDefineTempLabel(skipLo);

// Clamp upper bound: if dst > maxVal, dst = maxVal.
BasicBlock* skipHi = genCreateTempLabel();
instGen_Set_Reg_To_Imm(EA_PTRSIZE, tmpReg, maxVal);
emit->emitIns_J_cond_la(INS_bge, skipHi, tmpReg, dst); // skip if maxVal >= dst
emit->emitIns_R_R(INS_mov, EA_PTRSIZE, dst, tmpReg); // dst = maxVal
genDefineTempLabel(skipHi);

genProduceReg(treeNode);
return;
}

default:
break;
}

emitAttr size = emitActualTypeSize(op1);
bool is4 = (size == EA_4BYTE);

Expand Down
53 changes: 48 additions & 5 deletions src/coreclr/jit/emitarm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3344,11 +3344,6 @@ void emitter::emitIns_R_R_I_I(instruction ins,
int msb = lsb + width - 1;
int imm = 0; /* combined immediate */

assert((lsb >= 0) && (lsb <= 31)); // required for encodings
assert((width > 0) && (width <= 32)); // required for encodings
assert((msb >= 0) && (msb <= 31)); // required for encodings
assert(msb >= lsb); // required for encodings

/* Figure out the encoding format of the instruction */
switch (ins)
{
Expand All @@ -3357,6 +3352,10 @@ void emitter::emitIns_R_R_I_I(instruction ins,
assert(reg2 != REG_PC);

assert(insDoesNotSetFlags(flags));
assert((lsb >= 0) && (lsb <= 31)); // required for encoding
assert((width > 0) && (width <= 32)); // required for encoding
assert((msb >= 0) && (msb <= 31)); // required for encoding
assert(msb >= lsb); // required for encoding
imm = (lsb << 5) | msb;

fmt = IF_T2_D0;
Expand All @@ -3369,12 +3368,44 @@ void emitter::emitIns_R_R_I_I(instruction ins,
assert(reg2 != REG_PC);

assert(insDoesNotSetFlags(flags));
assert((lsb >= 0) && (lsb <= 31)); // required for encoding
assert((width > 0) && (width <= 32)); // required for encoding
assert((msb >= 0) && (msb <= 31)); // required for encoding
assert(msb >= lsb); // required for encoding
imm = (lsb << 5) | (width - 1);

fmt = IF_T2_D0;
sf = INS_FLAGS_NOT_SET;
break;

case INS_ssat:
Comment thread
tannergooding marked this conversation as resolved.
// imm1 = shift amount (must be 0 for no shift), imm2 = saturation bits N (1-32)
// Encoding: sat_imm field = N-1 stored in bits[4:0]; no shift (sh=0, imm5=0).
assert(reg1 != REG_PC); // VM debugging single stepper doesn't support PC register with this instruction.
assert(reg2 != REG_PC);

assert(insDoesNotSetFlags(flags));
assert((imm1 == 0) && (imm2 >= 1) && (imm2 <= 32)); // required for encoding
imm = (lsb << 5) | (width - 1); // lsb=shift=0, width=N -> sat_imm = N-1
Comment thread
tannergooding marked this conversation as resolved.

fmt = IF_T2_D0;
sf = INS_FLAGS_NOT_SET;
break;

case INS_usat:
// imm1 = shift amount (must be 0 for no shift), imm2 = saturation bits N (0-31)
// Encoding: sat_imm field = N stored directly in bits[4:0]; no shift (sh=0, imm5=0).
assert(reg1 != REG_PC); // VM debugging single stepper doesn't support PC register with this instruction.
assert(reg2 != REG_PC);

assert(insDoesNotSetFlags(flags));
assert((imm1 == 0) && (imm2 >= 0) && (imm2 <= 31)); // required for encoding
imm = (lsb << 5) | width; // lsb=shift=0, width=N -> sat_imm = N

Comment thread
tannergooding marked this conversation as resolved.
fmt = IF_T2_D0;
sf = INS_FLAGS_NOT_SET;
break;
Comment thread
tannergooding marked this conversation as resolved.

default:
unreached();
}
Expand Down Expand Up @@ -7575,6 +7606,18 @@ void emitter::emitDispInsHelp(
emitDispImm(imm1, true);
emitDispImm(imm2, false);
}
else if (ins == INS_ssat)
{
// SSAT: stored as sat_imm = N-1; display as #N (saturation bits)
int satBits = (imm & 0x1f) + 1;
emitDispImm(satBits, false);
}
else if (ins == INS_usat)
{
// USAT: stored as sat_imm = N; display as #N (saturation bits)
int satBits = imm & 0x1f;
emitDispImm(satBits, false);
}
Comment thread
tannergooding marked this conversation as resolved.
else
{
int lsb = (imm >> 5) & 0x1f;
Expand Down
Loading
Loading