Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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
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
87 changes: 86 additions & 1 deletion src/coreclr/jit/codegenloongarch64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4450,7 +4450,92 @@ 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 (no-op when dst == src).
emit->emitIns_R_R(INS_mov, EA_PTRSIZE, 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 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);
}

//---------------------------------------------------------------------
Expand Down
63 changes: 63 additions & 0 deletions src/coreclr/jit/codegenriscv64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4224,6 +4224,69 @@ 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 (no-op when dst == src).
emit->emitIns_R_R(INS_mov, EA_PTRSIZE, 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
47 changes: 42 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,38 @@ 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:
// 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(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

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

fmt = IF_T2_D0;
sf = INS_FLAGS_NOT_SET;
break;

default:
unreached();
}
Expand Down Expand Up @@ -7575,6 +7600,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);
}
else
{
int lsb = (imm >> 5) & 0x1f;
Expand Down
16 changes: 13 additions & 3 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5973,9 +5973,17 @@ GenTree* Compiler::impPrimitiveNamedIntrinsic(NamedIntrinsic intrinsic,

if (varTypeIsSmall(tgtType))
{
res = gtNewCastNodeL(retType, op1, /* uns */ false, retType);
res = gtFoldExpr(res);
res = gtNewCastNode(TYP_INT, res, /* uns */ false, tgtType);
if (intrinsic == NI_PRIMITIVE_ConvertToInteger)
{
// Preserve saturating semantics for floating-point -> small integral conversions.
res = gtNewCastNodeL(retType, op1, /* uns */ false, tgtType);
}
else
{
res = gtNewCastNodeL(retType, op1, /* uns */ false, retType);
res = gtFoldExpr(res);
res = gtNewCastNode(TYP_INT, res, /* uns */ false, tgtType);
}
}
else
{
Expand Down Expand Up @@ -8681,6 +8689,8 @@ bool Compiler::IsTargetIntrinsic(NamedIntrinsic intrinsicName)
return false;
}

case NI_System_Math_MaxNative:
case NI_System_Math_MinNative:
case NI_System_Math_MultiplyAddEstimate:
case NI_System_Math_ReciprocalEstimate:
return true;
Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/jit/instrsarm.h
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ INST1(sbfx, "sbfx", 0, 0, IF_T2_D0, 0xF3400000)
// Rd,Rn,#b,#w T2_D0 111100110100nnnn 0iiiddddii0wwwww F340 0000 imm(0-31),imm(0-31)
INST1(sdiv, "sdiv", 0, 0, IF_T2_C5, 0xFB90F0F0)
// Rd,Rn,Rm T2_C5 111110111001nnnn 1111dddd1111mmmm FB90 F0F0
INST1(ssat, "ssat", 0, 0, IF_T2_D0, 0xF3000000)
// Rd,Rn,#sat T2_D0 111100110000nnnn 0iiiddddii0sssss F300 0000 imm(0-31)
INST1(smlal, "smlal", 0, 0, IF_T2_F1, 0xFBC00000)
// Rl,Rh,Rn,Rm T2_F1 111110111100nnnn llllhhhh0000mmmm FBC0 0000
INST1(smull, "smull", 0, 0, IF_T2_F1, 0xFB800000)
Expand Down Expand Up @@ -459,6 +461,8 @@ INST1(umlal, "umlal", 0, 0, IF_T2_F1, 0xFBE00000)
// Rl,Rh,Rn,Rm T2_F1 111110111110nnnn llllhhhh0000mmmm FBE0 0000
INST1(umull, "umull", 0, 0, IF_T2_F1, 0xFBA00000)
// Rl,Rh,Rn,Rm T2_F1 111110111010nnnn llllhhhh0000mmmm FBA0 0000
INST1(usat, "usat", 0, 0, IF_T2_D0, 0xF3800000)
// Rd,Rn,#sat T2_D0 111100111000nnnn 0iiiddddii0sssss F380 0000 imm(0-31)

#ifdef FEATURE_ITINSTRUCTION
INST1(it, "it", 0, 0, IF_T1_B, 0xBF08)
Expand Down
17 changes: 13 additions & 4 deletions src/coreclr/jit/lsraarm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,18 +270,27 @@ int LinearScan::BuildNode(GenTree* tree)

case GT_INTRINSIC:
{
// TODO-ARM: Implement other type of intrinsics (round, sqrt and etc.)
// Both operand and its result must be of the same floating point type.
GenTree* op1 = tree->gtGetOp1();
assert(varTypeIsFloating(op1));
assert(op1->TypeGet() == tree->TypeGet());
BuildUse(op1);
srcCount = 1;

switch (tree->AsIntrinsic()->gtIntrinsicName)
{
case NI_System_Math_Abs:
case NI_System_Math_Sqrt:
// Both operand and result must be of the same floating-point type.
assert(varTypeIsFloating(op1));
assert(op1->TypeGet() == tree->TypeGet());
assert(dstCount == 1);
BuildDef(tree);
break;
case NI_PRIMITIVE_SaturateToInt8:
case NI_PRIMITIVE_SaturateToInt16:
case NI_PRIMITIVE_SaturateToUInt8:
case NI_PRIMITIVE_SaturateToUInt16:
// Integer-domain saturation via SSAT/USAT: operand and result are TYP_INT.
assert(op1->TypeGet() == TYP_INT);
assert(tree->TypeGet() == TYP_INT);
assert(dstCount == 1);
BuildDef(tree);
break;
Expand Down
Loading