The optimized importer expansion for Span<T>.get_Item and ReadOnlySpan<T>.get_Item can spill/evaluate the index before the receiver, violating IL evaluation order and producing wrong results when the receiver mutates state read by the index.
Minimal Repro
using System;
using System.Runtime.CompilerServices;
public class Program
{
static int s_idx;
[MethodImpl(MethodImplOptions.NoInlining)]
static ref Span<int> GetSpan(ref Span<int> s) { s_idx = 1; return ref s; }
[MethodImpl(MethodImplOptions.NoInlining)]
static ref ReadOnlySpan<int> GetROSpan(ref ReadOnlySpan<int> s) { s_idx = 1; return ref s; }
[MethodImpl(MethodImplOptions.NoInlining)]
static int[] GetArr(int[] a) { s_idx = 1; return a; }
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static int TestSpan(ref Span<int> s) => GetSpan(ref s)[s_idx];
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static int TestROSpan(ref ReadOnlySpan<int> s) => GetROSpan(ref s)[s_idx];
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static int TestArr(int[] a) => GetArr(a)[s_idx];
public static void Main()
{
int[] data = { 10, 20 };
Span<int> span = data;
ReadOnlySpan<int> rospan = data;
s_idx = 0;
Console.WriteLine("span: " + TestSpan(ref span));
s_idx = 0;
Console.WriteLine("rosp: " + TestROSpan(ref rospan));
s_idx = 0;
Console.WriteLine("arr : " + TestArr(data));
}
}
Expected
span: 20
rosp: 20
arr : 20
Actual
span: 10
rosp: 10
arr : 20
Notes
The intrinsic path pops receiver and index, then clones/spills the index first; since the receiver has already been popped, that spill is appended before the side-effecting receiver evaluation.
The array indexer path preserves the required receiver-before-index order, so it returns 20.
The optimized importer expansion for
Span<T>.get_ItemandReadOnlySpan<T>.get_Itemcan spill/evaluate the index before the receiver, violating IL evaluation order and producing wrong results when the receiver mutates state read by the index.Minimal Repro
Expected
Actual
Notes
The intrinsic path pops receiver and index, then clones/spills the index first; since the receiver has already been popped, that spill is appended before the side-effecting receiver evaluation.
The array indexer path preserves the required receiver-before-index order, so it returns
20.