Browse Source

Validate tensor primitive span contracts

pull/3161/head
James Jackson-South 3 weeks ago
parent
commit
14cf138cbb
  1. 9
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs
  2. 4
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs
  3. 4
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs
  4. 76
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs
  5. 4
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs
  6. 4
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs
  7. 11
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
  8. 82
      tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs

9
src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs

@ -16,6 +16,11 @@ internal static partial class TensorPrimitives_
/// <param name="x">The first addends.</param>
/// <param name="y">The second addends.</param>
/// <param name="destination">The destination for the sums.</param>
/// <exception cref="ArgumentException"><paramref name="x"/> and <paramref name="y"/> do not have the same length.</exception>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than the input spans.</exception>
/// <exception cref="ArgumentException">
/// An input and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<T>(ReadOnlySpan<T> x, ReadOnlySpan<T> y, Span<T> destination)
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
@ -28,6 +33,10 @@ internal static partial class TensorPrimitives_
/// <param name="x">The first addends.</param>
/// <param name="y">The scalar second addend.</param>
/// <param name="destination">The destination for the sums.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add<T>(ReadOnlySpan<T> x, T y, Span<T> destination)
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>

4
src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs

@ -18,6 +18,10 @@ internal static partial class TensorPrimitives_
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <param name="destination">The destination for the clamped values.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp<T>(ReadOnlySpan<T> x, T min, T max, Span<T> destination)
where T : INumber<T>

4
src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs

@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_
/// <param name="x">The dividend values.</param>
/// <param name="y">The divisor.</param>
/// <param name="destination">The destination for the quotient values.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Divide<T>(ReadOnlySpan<T> x, T y, Span<T> destination)
where T : IDivisionOperators<T, T, T>

76
src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -109,6 +110,47 @@ internal static partial class TensorPrimitives_
public static abstract Vector512<T> Invoke(Vector512<T> x, Vector512<T> y, Vector512<T> z);
}
/// <summary>
/// Validates that an input and destination are either disjoint or begin at the same memory location.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="input">The input values.</param>
/// <param name="destination">The destination values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidateInputOutputSpanNonOverlapping<T>(ReadOnlySpan<T> input, Span<T> destination)
{
// Runtime TensorPrimitives permits exact same-start overlap for in-place operation. A shifted overlap is
// rejected because forward SIMD stores could overwrite input elements before a later load consumes them.
if (!Unsafe.AreSame(ref MemoryMarshal.GetReference(input), ref MemoryMarshal.GetReference(destination))
&& input.Overlaps(destination))
{
ThrowInputAndDestinationSpanMustNotOverlap();
}
}
/// <summary>
/// Throws when input spans do not have the same length.
/// </summary>
[DoesNotReturn]
private static void ThrowSpansMustHaveSameLength()
=> throw new ArgumentException("Input span arguments must all have the same length.");
/// <summary>
/// Throws when the destination cannot hold every result.
/// </summary>
[DoesNotReturn]
private static void ThrowDestinationTooShort()
=> throw new ArgumentException("Destination is too short.", "destination");
/// <summary>
/// Throws when an input and destination overlap without beginning at the same memory location.
/// </summary>
[DoesNotReturn]
private static void ThrowInputAndDestinationSpanMustNotOverlap()
=> throw new ArgumentException(
"The destination span may only overlap with an input span if the two spans start at the same memory location.",
"destination");
/// <summary>
/// Performs an element-wise binary operation between two spans.
/// </summary>
@ -124,6 +166,19 @@ internal static partial class TensorPrimitives_
Span<T> destination)
where TOperator : struct, IBinaryOperator<T>
{
if (x.Length != y.Length)
{
ThrowSpansMustHaveSameLength();
}
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ValidateInputOutputSpanNonOverlapping(y, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T yRef = ref MemoryMarshal.GetReference(y);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
@ -173,6 +228,13 @@ internal static partial class TensorPrimitives_
Span<T> destination)
where TOperator : struct, IBinaryOperator<T>
{
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
@ -220,6 +282,13 @@ internal static partial class TensorPrimitives_
Span<T> destination)
where TOperator : struct, IBinaryOperator<T>
{
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
@ -282,6 +351,13 @@ internal static partial class TensorPrimitives_
Span<T> destination)
where TOperator : struct, ITernaryOperator<T>
{
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;

4
src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs

@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_
/// <param name="x">The values to compare.</param>
/// <param name="y">The value to compare with each element.</param>
/// <param name="destination">The destination for the maximum values.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Max<T>(ReadOnlySpan<T> x, T y, Span<T> destination)
where T : INumber<T>

4
src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs

@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_
/// <param name="x">The multiplicands.</param>
/// <param name="y">The multiplier.</param>
/// <param name="destination">The destination for the products.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Multiply<T>(ReadOnlySpan<T> x, T y, Span<T> destination)
where T : IMultiplyOperators<T, T, T>, IMultiplicativeIdentity<T, T>

11
src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs

@ -56,6 +56,10 @@ internal static partial class TensorPrimitives_
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The values to negate.</param>
/// <param name="destination">The destination for the negated values.</param>
/// <exception cref="ArgumentException"><paramref name="destination"/> is shorter than <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="x"/> and <paramref name="destination"/> overlap without beginning at the same memory location.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Negate<T>(ReadOnlySpan<T> x, Span<T> destination)
where T : IUnaryNegationOperators<T, T>
@ -72,6 +76,13 @@ internal static partial class TensorPrimitives_
private static void InvokeSpanIntoSpan<T, TOperator>(ReadOnlySpan<T> x, Span<T> destination)
where TOperator : struct, IUnaryOperator<T>
{
if (x.Length > destination.Length)
{
ThrowDestinationTooShort();
}
ValidateInputOutputSpanNonOverlapping(x, destination);
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;

82
tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs

@ -51,6 +51,88 @@ public class TensorPrimitivesTests
| HwIntrinsics.DisableArm64Sve
| HwIntrinsics.DisableHWIntrinsic);
/// <summary>
/// Verifies that span-to-span operations require equal input lengths before accessing either input.
/// </summary>
[Fact]
public void AddSpanSpanRejectsMismatchedInputLengths()
{
ArgumentException exception = Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Add<int>(new int[4], new int[3], new int[4]));
Assert.Null(exception.ParamName);
}
/// <summary>
/// Verifies that every operation rejects a destination that cannot hold all input elements.
/// </summary>
[Fact]
public void OperationsRejectShortDestinations()
{
int[] integers = new int[4];
float[] singles = new float[4];
Assert.Equal("destination", Assert.Throws<ArgumentException>(() => TensorPrimitives_.Add<int>(integers, integers, new int[3])).ParamName);
Assert.Equal("destination", Assert.Throws<ArgumentException>(() => TensorPrimitives_.Add<int>(integers, 1, new int[3])).ParamName);
Assert.Equal("destination", Assert.Throws<ArgumentException>(() => TensorPrimitives_.Multiply<float>(singles, 2F, new float[3])).ParamName);
Assert.Equal("destination", Assert.Throws<ArgumentException>(() => TensorPrimitives_.Divide<float>(singles, 2F, new float[3])).ParamName);
Assert.Equal("destination", Assert.Throws<ArgumentException>(() => TensorPrimitives_.Max<float>(singles, 2F, new float[3])).ParamName);
Assert.Equal("destination", Assert.Throws<ArgumentException>(() => TensorPrimitives_.Clamp<float>(singles, 0F, 1F, new float[3])).ParamName);
Assert.Equal("destination", Assert.Throws<ArgumentException>(() => TensorPrimitives_.Negate<float>(singles, new float[3])).ParamName);
}
/// <summary>
/// Verifies that every operation rejects shifted input and destination overlap.
/// </summary>
[Fact]
public void OperationsRejectShiftedOverlap()
{
int[] integers = new int[5];
int[] separateIntegers = new int[4];
float[] singles = new float[5];
// Both inputs need independent validation because either one may alias a shifted destination.
Assert.Equal(
"destination",
Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Add<int>(integers.AsSpan(0, 4), separateIntegers, integers.AsSpan(1, 4))).ParamName);
Assert.Equal(
"destination",
Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Add<int>(separateIntegers, integers.AsSpan(0, 4), integers.AsSpan(1, 4))).ParamName);
Assert.Equal(
"destination",
Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Add<int>(integers.AsSpan(0, 4), 1, integers.AsSpan(1, 4))).ParamName);
Assert.Equal(
"destination",
Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Multiply<float>(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName);
Assert.Equal(
"destination",
Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Divide<float>(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName);
Assert.Equal(
"destination",
Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Max<float>(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName);
Assert.Equal(
"destination",
Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Clamp<float>(singles.AsSpan(0, 4), 0F, 1F, singles.AsSpan(1, 4))).ParamName);
Assert.Equal(
"destination",
Assert.Throws<ArgumentException>(
() => TensorPrimitives_.Negate<float>(singles.AsSpan(0, 4), singles.AsSpan(1, 4))).ParamName);
}
/// <summary>
/// Runs the TensorPrimitives compatibility assertions inside a process configured for one hardware-intrinsic tier.
/// </summary>

Loading…
Cancel
Save