diff --git a/src/Clast.DatabaseDecimal/Arithmetic/AddKernel.cs b/src/Clast.DatabaseDecimal/Arithmetic/AddKernel.cs
index dd0d47a..c5be895 100644
--- a/src/Clast.DatabaseDecimal/Arithmetic/AddKernel.cs
+++ b/src/Clast.DatabaseDecimal/Arithmetic/AddKernel.cs
@@ -95,6 +95,31 @@ public static Decimal128 Subtract(Decimal128 left, DecimalType leftType, Decimal
return new Decimal128(DecimalRange.Enforce(checked(l - r), resultType, overflow));
}
+ ///
+ /// Subtract two 32-bit values, widening to 64-bit result.
+ /// Used when the result precision exceeds 9 digits.
+ ///
+ public static Decimal64 SubtractWiden(Decimal32 left, DecimalType leftType, Decimal32 right, DecimalType rightType, DecimalType resultType,
+ DecimalRounding rounding = DecimalRounding.HalfEven,
+ DecimalOverflow overflow = DecimalOverflow.Throw)
+ {
+ long l = ScaleHelper.Widen32To64(left.Mantissa, leftType.Scale, resultType.Scale, rounding);
+ long r = ScaleHelper.Widen32To64(right.Mantissa, rightType.Scale, resultType.Scale, rounding);
+ return new Decimal64(DecimalRange.Enforce(checked(l - r), resultType, overflow));
+ }
+
+ ///
+ /// Subtract two 64-bit values, widening to 128-bit result.
+ ///
+ public static Decimal128 SubtractWiden(Decimal64 left, DecimalType leftType, Decimal64 right, DecimalType rightType, DecimalType resultType,
+ DecimalRounding rounding = DecimalRounding.HalfEven,
+ DecimalOverflow overflow = DecimalOverflow.Throw)
+ {
+ Int128 l = ScaleHelper.Widen64To128(left.Mantissa, leftType.Scale, resultType.Scale, rounding);
+ Int128 r = ScaleHelper.Widen64To128(right.Mantissa, rightType.Scale, resultType.Scale, rounding);
+ return new Decimal128(DecimalRange.Enforce(checked(l - r), resultType, overflow));
+ }
+
// --- 256-bit ---
public static Decimal256 Add(Decimal256 left, DecimalType leftType, Decimal256 right, DecimalType rightType, DecimalType resultType,
@@ -126,4 +151,16 @@ public static Decimal256 Subtract(Decimal256 left, DecimalType leftType, Decimal
Int256 r = ScaleHelper.Rescale256(right.Mantissa, rightType.Scale, resultType.Scale, rounding);
return new Decimal256(DecimalRange.Enforce(checked(l - r), resultType, overflow));
}
+
+ ///
+ /// Subtract two 128-bit values, widening to 256-bit result.
+ ///
+ public static Decimal256 SubtractWiden(Decimal128 left, DecimalType leftType, Decimal128 right, DecimalType rightType, DecimalType resultType,
+ DecimalRounding rounding = DecimalRounding.HalfEven,
+ DecimalOverflow overflow = DecimalOverflow.Throw)
+ {
+ Int256 l = ScaleHelper.Widen128To256(left.Mantissa, leftType.Scale, resultType.Scale, rounding);
+ Int256 r = ScaleHelper.Widen128To256(right.Mantissa, rightType.Scale, resultType.Scale, rounding);
+ return new Decimal256(DecimalRange.Enforce(checked(l - r), resultType, overflow));
+ }
}
diff --git a/src/Clast.DatabaseDecimal/Arithmetic/SpanAddKernel.cs b/src/Clast.DatabaseDecimal/Arithmetic/SpanAddKernel.cs
index 925117e..5d5fd2d 100644
--- a/src/Clast.DatabaseDecimal/Arithmetic/SpanAddKernel.cs
+++ b/src/Clast.DatabaseDecimal/Arithmetic/SpanAddKernel.cs
@@ -14,6 +14,15 @@ namespace Clast.DatabaseDecimal.Arithmetic;
/// are pre-computed once before the loop.
/// The result span may safely overlap with either input span.
///
+///
+/// The overlap guarantee covers the same-width overloads, where an element is
+/// written only after both operands at that index have been read. It does not
+/// extend to the widening overloads: their result element is twice the width of
+/// their inputs, so an in-place widening operation has nowhere to put the
+/// second half. Reaching that case at all takes a deliberate
+/// reinterpretation
+/// of one buffer as both element types, since the spans are differently typed.
+///
public static class SpanAddKernel
{
// ================================================================
@@ -446,6 +455,80 @@ public static void Subtract(
DecimalRange.Validate(result.Slice(0, left.Length), resultType);
}
+ // ================================================================
+ // Subtract — column - column, widening
+ // ================================================================
+
+ public static void SubtractWiden(
+ ReadOnlySpan left, DecimalType leftType,
+ ReadOnlySpan right, DecimalType rightType,
+ Span result, DecimalType resultType,
+ DecimalRounding rounding = DecimalRounding.HalfEven,
+ DecimalOverflow overflow = DecimalOverflow.Throw)
+ {
+ ValidateLengths(left.Length, right.Length, result.Length);
+
+ int ld = resultType.Scale - leftType.Scale;
+ int rd = resultType.Scale - rightType.Scale;
+
+ if (ld == 0 && rd == 0)
+ {
+ DecimalRange.GetBounds(resultType, out long lower, out long upper);
+ if (SubtractWidenSameScale32To64(left, right, result, lower, upper) && overflow == DecimalOverflow.Throw)
+ DecimalRange.ThrowOutOfRange(resultType);
+ return;
+ }
+ else
+ {
+ for (int i = 0; i < left.Length; i++)
+ result[i] = checked(ScaleHelper.WidenByDelta32To64(left[i], ld, rounding)
+ - ScaleHelper.WidenByDelta32To64(right[i], rd, rounding));
+ }
+
+ if (overflow == DecimalOverflow.Throw)
+ DecimalRange.Validate(result.Slice(0, left.Length), resultType);
+ }
+
+ public static void SubtractWiden(
+ ReadOnlySpan left, DecimalType leftType,
+ ReadOnlySpan right, DecimalType rightType,
+ Span result, DecimalType resultType,
+ DecimalRounding rounding = DecimalRounding.HalfEven,
+ DecimalOverflow overflow = DecimalOverflow.Throw)
+ {
+ ValidateLengths(left.Length, right.Length, result.Length);
+
+ int ld = resultType.Scale - leftType.Scale;
+ int rd = resultType.Scale - rightType.Scale;
+
+ for (int i = 0; i < left.Length; i++)
+ result[i] = checked(ScaleHelper.WidenByDelta64To128(left[i], ld, rounding)
+ - ScaleHelper.WidenByDelta64To128(right[i], rd, rounding));
+
+ if (overflow == DecimalOverflow.Throw)
+ DecimalRange.Validate(result.Slice(0, left.Length), resultType);
+ }
+
+ public static void SubtractWiden(
+ ReadOnlySpan left, DecimalType leftType,
+ ReadOnlySpan right, DecimalType rightType,
+ Span result, DecimalType resultType,
+ DecimalRounding rounding = DecimalRounding.HalfEven,
+ DecimalOverflow overflow = DecimalOverflow.Throw)
+ {
+ ValidateLengths(left.Length, right.Length, result.Length);
+
+ int ld = resultType.Scale - leftType.Scale;
+ int rd = resultType.Scale - rightType.Scale;
+
+ for (int i = 0; i < left.Length; i++)
+ result[i] = checked(ScaleHelper.WidenByDelta128To256(left[i], ld, rounding)
+ - ScaleHelper.WidenByDelta128To256(right[i], rd, rounding));
+
+ if (overflow == DecimalOverflow.Throw)
+ DecimalRange.Validate(result.Slice(0, left.Length), resultType);
+ }
+
// ================================================================
// Subtract — column - scalar (broadcast)
// ================================================================
@@ -915,6 +998,51 @@ private static bool AddWidenSameScale32To64(ReadOnlySpan left, ReadOnlySpan
return outOfRangeSeen;
}
+ private static bool SubtractWidenSameScale32To64(ReadOnlySpan left, ReadOnlySpan right, Span result, long lower, long upper)
+ {
+ int i = 0;
+ bool outOfRangeSeen = false;
+#if NET5_0_OR_GREATER
+ if (Vector.IsHardwareAccelerated && left.Length >= Vector.Count)
+ {
+ ReadOnlySpan> lv = MemoryMarshal.Cast>(left);
+ ReadOnlySpan> rv = MemoryMarshal.Cast>(right);
+ Span> ov = MemoryMarshal.Cast>(result);
+ int chunks = lv.Length;
+ // As with the widening add, the difference of two widened 32-bit
+ // values cannot overflow 64 bits, so there is no overflow
+ // accumulator here — only the declared precision has to be
+ // enforced, on both halves of each widened pair.
+ Vector outOfRange = Vector.Zero;
+ Vector loVec = new Vector(lower);
+ Vector hiVec = new Vector(upper);
+ for (int k = 0; k < chunks; k++)
+ {
+ Vector a = lv[k];
+ Vector b = rv[k];
+ Vector.Widen(a, out Vector aLo, out Vector aHi);
+ Vector.Widen(b, out Vector bLo, out Vector bHi);
+ Vector low = aLo - bLo;
+ Vector high = aHi - bHi;
+ outOfRange |= Vector.LessThan(low, loVec) | Vector.GreaterThan(low, hiVec);
+ outOfRange |= Vector.LessThan(high, loVec) | Vector.GreaterThan(high, hiVec);
+ ov[k * 2] = low;
+ ov[k * 2 + 1] = high;
+ }
+ outOfRangeSeen |= outOfRange != Vector.Zero;
+ i = chunks * Vector.Count;
+ }
+#endif
+ for (; i < left.Length; i++)
+ {
+ var value = (long)left[i] - right[i];
+ result[i] = value;
+ outOfRangeSeen |= value < lower || value > upper;
+ }
+
+ return outOfRangeSeen;
+ }
+
private static bool AddBroadcastSameScale32(ReadOnlySpan left, int right, Span result, int lower, int upper)
{
int i = 0;
diff --git a/tests/Clast.DatabaseDecimal.Tests/ArithmeticTests.cs b/tests/Clast.DatabaseDecimal.Tests/ArithmeticTests.cs
index 27fe26b..534cbc2 100644
--- a/tests/Clast.DatabaseDecimal.Tests/ArithmeticTests.cs
+++ b/tests/Clast.DatabaseDecimal.Tests/ArithmeticTests.cs
@@ -88,6 +88,86 @@ public void Subtract_NegativeResult()
Assert.Equal("-1.50", result.ToString(resultType.Scale));
}
+ [Fact]
+ public void Subtract_Widening_32To64()
+ {
+ // Two NUMERIC(9,2) values whose difference exceeds 32-bit
+ var type = DecimalType.Numeric(9, 2);
+ var resultType = DecimalTypeRules.Subtract(type, type); // NUMERIC(10,2) => 64-bit
+
+ Assert.Equal(DecimalWidth.W64, resultType.Width);
+
+ var left = new Decimal32(999_999_999); // 9,999,999.99
+ var right = new Decimal32(-999_999_999); // -9,999,999.99
+
+ var result = AddKernel.SubtractWiden(left, type, right, type, resultType);
+ Assert.Equal(1_999_999_998L, result.Mantissa); // 19,999,999.98
+ }
+
+ [Fact]
+ public void Subtract_Widening_64To128()
+ {
+ var type = DecimalType.Numeric(18, 0);
+ var resultType = DecimalTypeRules.Subtract(type, type); // NUMERIC(19,0) => 128-bit
+
+ Assert.Equal(DecimalWidth.W128, resultType.Width);
+
+ var left = new Decimal64(long.MaxValue / 2);
+ var right = new Decimal64(-(long.MaxValue / 2));
+
+ var result = AddKernel.SubtractWiden(left, type, right, type, resultType);
+ Assert.Equal((Int128)(long.MaxValue / 2) - -(Int128)(long.MaxValue / 2), result.Mantissa);
+ }
+
+ [Fact]
+ public void SubtractWiden_MatchesPromotingBothOperandsFirst()
+ {
+ // The workaround SubtractWiden replaces: promote to the wider tier by
+ // hand and subtract there. Rescaling is monotone in the mantissa, so the
+ // two must agree — including when the operands carry different scales.
+ var leftType = DecimalType.Numeric(9, 2);
+ var rightType = DecimalType.Numeric(9, 4);
+ var resultType = DecimalTypeRules.Subtract(leftType, rightType); // NUMERIC(12,4)
+
+ Assert.Equal(DecimalWidth.W64, resultType.Width);
+
+ int[] mantissas = [0, 1, -1, 12_345, -12_345, 999_999_999, -999_999_999];
+ foreach (int l in mantissas)
+ {
+ foreach (int r in mantissas)
+ {
+ var widened = AddKernel.SubtractWiden(
+ new Decimal32(l), leftType, new Decimal32(r), rightType, resultType);
+ var byHand = AddKernel.Subtract(
+ new Decimal64(l), leftType, new Decimal64(r), rightType, resultType);
+
+ Assert.Equal(byHand.Mantissa, widened.Mantissa);
+ }
+ }
+ }
+
+ [Fact]
+ public void SubtractWiden_PastResultPrecision_Throws()
+ {
+ var type = DecimalType.Numeric(9, 0);
+
+ // The difference needs 10 digits, but the result type allows 9.
+ Assert.Throws(() => AddKernel.SubtractWiden(
+ new Decimal32(999_999_999), type, new Decimal32(-1), type, type));
+ }
+
+ [Fact]
+ public void SubtractWiden_PastResultPrecision_Ignore_DoesNotThrow()
+ {
+ var type = DecimalType.Numeric(9, 0);
+
+ var result = AddKernel.SubtractWiden(
+ new Decimal32(999_999_999), type, new Decimal32(-1), type, type,
+ DecimalRounding.HalfEven, DecimalOverflow.Ignore);
+
+ Assert.Equal(1_000_000_000L, result.Mantissa);
+ }
+
// --- Multiplication ---
[Fact]
diff --git a/tests/Clast.DatabaseDecimal.Tests/Decimal256Tests.cs b/tests/Clast.DatabaseDecimal.Tests/Decimal256Tests.cs
index 9984408..17cd111 100644
--- a/tests/Clast.DatabaseDecimal.Tests/Decimal256Tests.cs
+++ b/tests/Clast.DatabaseDecimal.Tests/Decimal256Tests.cs
@@ -126,6 +126,28 @@ public void Subtract_256Bit()
Assert.Equal((Int256)32500, result.Mantissa); // 325.00
}
+ [Fact]
+ public void Subtract_Widening_128To256()
+ {
+ var leftType = DecimalType.Numeric(38, 0);
+ var rightType = DecimalType.Numeric(38, 0);
+ var wideResultType = DecimalType.Numeric(39, 0); // 256-bit result
+
+ Assert.Equal(DecimalWidth.W256, wideResultType.Width);
+
+ var left = new Decimal128(Int128.MaxValue / 2);
+ var right = new Decimal128(-(Int128.MaxValue / 2));
+
+ var result = AddKernel.SubtractWiden(left, leftType, right, rightType, wideResultType);
+ var expected = (Int256)(Int128.MaxValue / 2) - (Int256)(-(Int128.MaxValue / 2));
+ Assert.Equal(expected, result.Mantissa);
+
+ // The by-hand workaround: promote both operands and subtract at 256-bit.
+ var byHand = AddKernel.Subtract(
+ (Decimal256)left, leftType, (Decimal256)right, rightType, wideResultType);
+ Assert.Equal(byHand.Mantissa, result.Mantissa);
+ }
+
[Fact]
public void Multiply_Widening_128To256()
{
diff --git a/tests/Clast.DatabaseDecimal.Tests/FusedRangeCheckTests.cs b/tests/Clast.DatabaseDecimal.Tests/FusedRangeCheckTests.cs
index f35a82c..5b0658a 100644
--- a/tests/Clast.DatabaseDecimal.Tests/FusedRangeCheckTests.cs
+++ b/tests/Clast.DatabaseDecimal.Tests/FusedRangeCheckTests.cs
@@ -137,6 +137,39 @@ public void AddWiden32To64_PastPrecision_Throws(int index)
SpanAddKernel.AddWiden(left, T9, right, T9, new long[Length], T9));
}
+ [Theory]
+ [InlineData(InVectorBody)]
+ [InlineData(InScalarTail)]
+ public void SubtractWiden32To64_PastPrecision_Throws(int index)
+ {
+ // Widening cannot overflow the width, so only the declared precision
+ // can reject this: NUMERIC(9,0) in a 64-bit result.
+ int[] left = new int[Length];
+ int[] right = new int[Length];
+ left[index] = Max9;
+ right[index] = -1;
+
+ Assert.Throws(() =>
+ SpanAddKernel.SubtractWiden(left, T9, right, T9, new long[Length], T9));
+ }
+
+ [Theory]
+ [InlineData(InVectorBody)]
+ [InlineData(InScalarTail)]
+ public void SubtractWiden32To64_Ignore_WritesEverythingAndDoesNotThrow(int index)
+ {
+ int[] left = new int[Length];
+ int[] right = new int[Length];
+ left[index] = Max9;
+ right[index] = -1;
+ long[] result = new long[Length];
+
+ SpanAddKernel.SubtractWiden(left, T9, right, T9, result, T9,
+ DecimalRounding.HalfEven, DecimalOverflow.Ignore);
+
+ Assert.Equal(Max9 + 1L, result[index]);
+ }
+
[Theory]
[InlineData(InVectorBody)]
[InlineData(InScalarTail)]
diff --git a/tests/Clast.DatabaseDecimal.Tests/SpanAddKernelTests.cs b/tests/Clast.DatabaseDecimal.Tests/SpanAddKernelTests.cs
index 79c5464..c6f7e74 100644
--- a/tests/Clast.DatabaseDecimal.Tests/SpanAddKernelTests.cs
+++ b/tests/Clast.DatabaseDecimal.Tests/SpanAddKernelTests.cs
@@ -100,6 +100,73 @@ public void AddWiden_64To128()
Assert.Equal((Int128)3_000_000_000_000_000_000, result[1]);
}
+ // ----------------------------------------------------------------
+ // Subtract — column - column, widening
+ // ----------------------------------------------------------------
+
+ [Fact]
+ public void SubtractWiden_32To64()
+ {
+ var type = DecimalType.Numeric(9, 2);
+ var resultType = DecimalType.Numeric(10, 2);
+
+ int[] left = [999_999_999, 500_000_000];
+ int[] right = [-999_999_999, -500_000_000];
+ long[] result = new long[2];
+
+ SpanAddKernel.SubtractWiden(left, type, right, type, result, resultType);
+
+ Assert.Equal([1_999_999_998L, 1_000_000_000L], result);
+ }
+
+ [Fact]
+ public void SubtractWiden_32To64_DifferentScales()
+ {
+ var leftType = DecimalType.Numeric(9, 0);
+ var rightType = DecimalType.Numeric(9, 2);
+ var resultType = DecimalType.Numeric(12, 2);
+
+ int[] left = [1, -1];
+ int[] right = [50, -50];
+ long[] result = new long[2];
+
+ SpanAddKernel.SubtractWiden(left, leftType, right, rightType, result, resultType);
+
+ Assert.Equal([50L, -50L], result); // 1.00 - 0.50, -1.00 - -0.50
+ }
+
+ [Fact]
+ public void SubtractWiden_64To128()
+ {
+ var type = DecimalType.Numeric(18, 0);
+ var resultType = DecimalType.Numeric(19, 0);
+
+ long[] left = [long.MaxValue / 2, 1_000_000_000_000_000_000];
+ long[] right = [-(long.MaxValue / 2), -2_000_000_000_000_000_000];
+ Int128[] result = new Int128[2];
+
+ SpanAddKernel.SubtractWiden(left, type, right, type, result, resultType);
+
+ Assert.Equal((Int128)(long.MaxValue / 2) + (long.MaxValue / 2), result[0]);
+ Assert.Equal((Int128)3_000_000_000_000_000_000, result[1]);
+ }
+
+ [Fact]
+ public void SubtractWiden_128To256()
+ {
+ var type = DecimalType.Numeric(38, 0);
+ var resultType = DecimalType.Numeric(39, 0);
+
+ Int128[] left = [Int128.MaxValue / 2];
+ Int128[] right = [-(Int128.MaxValue / 2)];
+ Int256[] result = new Int256[1];
+
+ SpanAddKernel.SubtractWiden(left, type, right, type, result, resultType);
+
+ Int256 expected = (Int256)(Int128.MaxValue / 2) + (Int256)(Int128.MaxValue / 2);
+ Assert.Equal(expected, result[0]);
+ }
+
// ----------------------------------------------------------------
// Add — column + scalar (broadcast)
// ----------------------------------------------------------------
@@ -470,6 +537,29 @@ public void AddWiden_32To64_SimdChunkedAndTail_PreservesElementOrder()
Assert.Equal(expected, result);
}
+ [Fact]
+ public void SubtractWiden_32To64_SimdChunkedAndTail_PreservesElementOrder()
+ {
+ // As with the widening add, Vector.Widen splits each Vector into
+ // lower/upper Vector halves; this checks every index, so a swap
+ // of the low/high writes cannot pass.
+ var type = DecimalType.Numeric(9, 2);
+ var resultType = DecimalType.Numeric(10, 2);
+ int n = 23;
+ int[] left = new int[n];
+ int[] right = new int[n];
+ long[] expected = new long[n];
+ for (int i = 0; i < n; i++)
+ {
+ left[i] = int.MaxValue - i;
+ right[i] = int.MinValue + 2 * i;
+ expected[i] = (long)left[i] - right[i];
+ }
+ long[] result = new long[n];
+ SpanAddKernel.SubtractWiden(left, type, right, type, result, resultType);
+ Assert.Equal(expected, result);
+ }
+
// ----------------------------------------------------------------
// Broadcast (column + scalar / column - scalar / scalar - column)
// SIMD chunked-path coverage. The broadcast helpers load the scalar