On arm64, Lowering::LowerHWIntrinsicFusedMultiplyAddScalar strips a GT_NEG under the operand of AdvSimd.FusedMultiplyAddScalar and compensates with fnmsub/fnmadd, without checking the type of the negated value. When AsDouble()/AsSingle() reinterprets the bits, the sign flip is applied to a completely different value and the result is silently wrong.
Minimal Repro
using System;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
public class Program
{
// Integer (two's complement) negation reinterpreted as double.
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static double TestA(long bits)
{
Vector64<double> addend = Vector64.Create(-bits).AsDouble();
return AdvSimd.FusedMultiplyAddScalar(addend, Vector64.Create(1.0), Vector64.Create(1.0)).ToScalar();
}
// Double negation flips bit 63; the float FMA consumes bits 0..31.
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static float TestB(double d)
{
Vector64<float> addend = Vector64.Create(-d).AsSingle();
return AdvSimd.FusedMultiplyAddScalar(addend, Vector64.Create(1.0f), Vector64.Create(1.0f)).ToScalar();
}
public static void Main()
{
long bits = BitConverter.DoubleToInt64Bits(3.0);
double expectedA = BitConverter.Int64BitsToDouble(-bits) * 1.0 + 1.0;
Console.WriteLine($"A: actual={TestA(bits)} expected={expectedA}");
double d = BitConverter.Int64BitsToDouble(0x4008_0000_4000_0000);
float lo = BitConverter.Int32BitsToSingle((int)BitConverter.DoubleToInt64Bits(-d));
float expectedB = lo * 1.0f + 1.0f;
Console.WriteLine($"B: actual={TestB(d)} expected={expectedB}");
}
}
Expected
A: actual=-0.5 expected=-0.5
B: actual=3 expected=3
Actual
A: actual=-2 expected=-0.5
B: actual=-1 expected=3
Codegen for TestA contains no neg at all: mov v0.d[0], x0 / fnmsub d0, d16, d16, d0.
For TestB a float-width sign flip (bit 31) replaces the double negation (bit 63).
Notes
lowerarmarch.cpp, LowerHWIntrinsicFusedMultiplyAddScalar: the valueOp->OperIs(GT_NEG) check
removes the NEG with no type check, but AsDouble()/AsSingle() between Vector64.Create and the
FMA are pure bit reinterprets, so valueOp's type can differ from node->GetSimdBaseType() (case B)
or not be floating-point at all (case A).
Fix direction: also require varTypeIsFloating(valueOp) and valueOp->TypeGet() == node->GetSimdBaseType().
Deterministic; also reproduces on released .NET 10.0.9.
On arm64,
Lowering::LowerHWIntrinsicFusedMultiplyAddScalarstrips aGT_NEGunder the operand ofAdvSimd.FusedMultiplyAddScalarand compensates withfnmsub/fnmadd, without checking the type of the negated value. WhenAsDouble()/AsSingle()reinterprets the bits, the sign flip is applied to a completely different value and the result is silently wrong.Minimal Repro
Expected
Actual
Codegen for
TestAcontains nonegat all:mov v0.d[0], x0/fnmsub d0, d16, d16, d0.For
TestBa float-width sign flip (bit 31) replaces thedoublenegation (bit 63).Notes
lowerarmarch.cpp,LowerHWIntrinsicFusedMultiplyAddScalar: thevalueOp->OperIs(GT_NEG)checkremoves the
NEGwith no type check, butAsDouble()/AsSingle()betweenVector64.Createand theFMA are pure bit reinterprets, so
valueOp's type can differ fromnode->GetSimdBaseType()(case B)or not be floating-point at all (case A).
Fix direction: also require
varTypeIsFloating(valueOp)andvalueOp->TypeGet() == node->GetSimdBaseType().Deterministic; also reproduces on released .NET 10.0.9.