From a7a54153fd800f019f956c54c0c212289d3f5bd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?=
 <MichalStrehovsky@users.noreply.github.com>
Date: Tue, 7 Feb 2023 16:34:18 +0900
Subject: [PATCH 1/3] Intrinsify `CreateSpan` in static constructor interpreter

---
 .../Compiler/TypePreinit.cs                   | 158 +++++++++++++++++-
 .../Preinitialization/Preinitialization.cs    |  55 ++++++
 2 files changed, 210 insertions(+), 3 deletions(-)

diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
index aee735bf2d8637..ace788daa04c96 100644
--- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
+++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
@@ -895,11 +895,11 @@ private Status TryScanMethod(MethodIL methodIL, Value[] parameters, Stack<Method
                             }
 
                             TypeDesc token = (TypeDesc)methodIL.GetObject(reader.ReadILToken());
-                            if (token.IsGCPointer)
+                            if (token.IsGCPointer || popped.Value is not ByRefValue byrefVal)
                             {
                                 return Status.Fail(methodIL.OwningMethod, opcode);
                             }
-                            ((ByRefValue)popped.Value).Initialize(token.GetElementSize().AsInt);
+                            byrefVal.Initialize(token.GetElementSize().AsInt);
                         }
                         break;
 
@@ -1415,6 +1415,27 @@ private Status TryScanMethod(MethodIL methodIL, Value[] parameters, Stack<Method
                         }
                         break;
 
+                    case ILOpcode.ldind_i4:
+                    case ILOpcode.ldind_u4:
+                        {
+                            StackEntry entry = stack.Pop();
+                            if (entry.Value is ByRefValue byRefVal)
+                            {
+                                switch (opcode)
+                                {
+                                    case ILOpcode.ldind_i4:
+                                    case ILOpcode.ldind_u4:
+                                        stack.Push(StackValueKind.Int32, ValueTypeValue.FromInt32(byRefVal.DereferenceAsInt32()));
+                                        break;
+                                }
+                            }
+                            else
+                            {
+                                ThrowHelper.ThrowInvalidProgramException();
+                            }
+                        }
+                        break;
+
                     case ILOpcode.constrained:
                         // Fallthrough. If this is ever implemented, make sure delegates to static virtual methods
                         // are also handled. We currently assume the frozen delegate will not be to a static
@@ -1428,12 +1449,20 @@ private Status TryScanMethod(MethodIL methodIL, Value[] parameters, Stack<Method
             return Status.Fail(methodIL.OwningMethod, "Control fell through");
         }
 
-        private static ValueTypeValue NewUninitializedLocationValue(TypeDesc locationType)
+        private static BaseValueTypeValue NewUninitializedLocationValue(TypeDesc locationType)
         {
             if (locationType.IsGCPointer || locationType.IsByRef)
             {
                 return null;
             }
+            else if (locationType.IsByRefLike && locationType is MetadataType maybeReadOnlySpan
+                && maybeReadOnlySpan.Module == locationType.Context.SystemModule
+                && maybeReadOnlySpan.Name == "ReadOnlySpan`1"
+                && maybeReadOnlySpan.Namespace == "System"
+                && maybeReadOnlySpan.Instantiation[0] is MetadataType readOnlySpanElementType)
+            {
+                return new ReadOnlySpanValue(readOnlySpanElementType, Array.Empty<byte>());
+            }
             else
             {
                 Debug.Assert(locationType.IsValueType || locationType.IsPointer || locationType.IsFunctionPointer);
@@ -1460,6 +1489,33 @@ private static bool TryHandleIntrinsicCall(MethodDesc method, Value[] parameters
                         return array.TryInitialize(rvaData);
                     }
                     return false;
+                case "CreateSpan":
+                    if (method.OwningType is MetadataType createSpanType
+                        && createSpanType.Name == "RuntimeHelpers" && createSpanType.Namespace == "System.Runtime.CompilerServices"
+                        && createSpanType.Module == createSpanType.Context.SystemModule
+                        && parameters[0] is RuntimeFieldHandleValue createSpanFieldHandle
+                        && createSpanFieldHandle.Field.IsStatic && createSpanFieldHandle.Field.HasRva
+                        && createSpanFieldHandle.Field is Internal.TypeSystem.Ecma.EcmaField createSpanEcmaField
+                        && method.Instantiation[0].IsValueType)
+                    {
+                        var elementType = (MetadataType)method.Instantiation[0];
+                        int elementSize = elementType.InstanceFieldSize.AsInt;
+                        byte[] rvaData = Internal.TypeSystem.Ecma.EcmaFieldExtensions.GetFieldRvaData(createSpanEcmaField);
+                        if (rvaData.Length % elementSize != 0)
+                            return false;
+                        retVal = new ReadOnlySpanValue(elementType, rvaData);
+                        return true;
+                    }
+                    return false;
+                case "get_Item":
+                    if (method.OwningType is MetadataType readonlySpanType
+                        && readonlySpanType.Name == "ReadOnlySpan`1" && readonlySpanType.Namespace == "System"
+                        && parameters[0] is ReadOnlySpanReferenceValue spanRef
+                        && parameters[1] is ValueTypeValue spanIndex)
+                    {
+                        return spanRef.TryAccessElement(spanIndex.AsInt32(), out retVal);
+                    }
+                    return false;
             }
 
             return false;
@@ -1938,6 +1994,93 @@ public override bool GetRawData(NodeFactory factory, out object data)
             }
         }
 
+        private sealed class ReadOnlySpanValue : BaseValueTypeValue, IInternalModelingOnlyValue
+        {
+            private readonly MetadataType _elementType;
+            private readonly byte[] _bytes;
+
+            public ReadOnlySpanValue(MetadataType elementType, byte[] bytes)
+            {
+                _elementType = elementType;
+                _bytes = bytes;
+            }
+
+            public override int Size => 2 * _elementType.Context.Target.PointerSize;
+
+            public override bool Equals(Value value)
+            {
+                // ceq instruction on ReadOnlySpans is hard to support.
+                // We should not see it in the first place.
+                ThrowHelper.ThrowInvalidProgramException();
+                return false;
+            }
+
+            public override void WriteFieldData(ref ObjectDataBuilder builder, NodeFactory factory)
+            {
+                throw new NotSupportedException();
+            }
+
+            public override bool GetRawData(NodeFactory factory, out object data)
+            {
+                data = null;
+                return false;
+            }
+
+            public override Value Clone()
+            {
+                // ReadOnlySpan is immutable and there's no way for the data to escape
+                return this;
+            }
+
+            public override bool TryCreateByRef(out Value value)
+            {
+                value = new ReadOnlySpanReferenceValue(_elementType, _bytes);
+                return true;
+            }
+        }
+
+        private sealed class ReadOnlySpanReferenceValue : Value
+        {
+            private readonly MetadataType _elementType;
+            private readonly byte[] _bytes;
+
+            public ReadOnlySpanReferenceValue(MetadataType elementType, byte[] bytes)
+            {
+                _elementType = elementType;
+                _bytes = bytes;
+            }
+
+            public override bool Equals(Value value)
+            {
+                // ceq instruction on refs to ReadOnlySpans is hard to support.
+                // We should not see it in the first place.
+                ThrowHelper.ThrowInvalidProgramException();
+                return false;
+            }
+
+            public override void WriteFieldData(ref ObjectDataBuilder builder, NodeFactory factory)
+            {
+                throw new NotSupportedException();
+            }
+
+            public override bool GetRawData(NodeFactory factory, out object data)
+            {
+                data = null;
+                return false;
+            }
+
+            public bool TryAccessElement(int index, out Value value)
+            {
+                value = default;
+                int limit = _bytes.Length / _elementType.InstanceFieldSize.AsInt;
+                if (index >= limit)
+                    return false;
+
+                value = new ByRefValue(_bytes, index * _elementType.InstanceFieldSize.AsInt);
+                return true;
+            }
+        }
+
         private sealed class MethodPointerValue : BaseValueTypeValue, IInternalModelingOnlyValue
         {
             public MethodDesc PointedToMethod { get; }
@@ -2021,6 +2164,15 @@ public override bool GetRawData(NodeFactory factory, out object data)
                 data = null;
                 return false;
             }
+
+            private ReadOnlySpan<byte> AsExactByteCount(int count)
+            {
+                if (PointedToOffset + count > PointedToBytes.Length)
+                    ThrowHelper.ThrowInvalidProgramException();
+                return new ReadOnlySpan<byte>(PointedToBytes, PointedToOffset, count);
+            }
+
+            public int DereferenceAsInt32() => BitConverter.ToInt32(AsExactByteCount(4));
         }
 
         private abstract class ReferenceTypeValue : Value
diff --git a/src/tests/nativeaot/SmokeTests/Preinitialization/Preinitialization.cs b/src/tests/nativeaot/SmokeTests/Preinitialization/Preinitialization.cs
index 803e6d179467ee..e87d6f44f46735 100644
--- a/src/tests/nativeaot/SmokeTests/Preinitialization/Preinitialization.cs
+++ b/src/tests/nativeaot/SmokeTests/Preinitialization/Preinitialization.cs
@@ -48,6 +48,7 @@ private static int Main()
         TestInstanceDelegate.Run();
         TestStringFields.Run();
         TestSharedCode.Run();
+        TestReadOnlySpan.Run();
 #else
         Console.WriteLine("Preinitialization is disabled in multimodule builds for now. Skipping test.");
 #endif
@@ -1000,6 +1001,60 @@ public static void Run()
     }
 }
 
+class TestReadOnlySpan
+{
+    class SimpleReadOnlySpanAccess
+    {
+        private static ReadOnlySpan<int> Ints => new int[] { 5, 6, 7, 8 };
+
+        public /* not readonly on purpose */ static int Sum;
+
+        static SimpleReadOnlySpanAccess()
+        {
+            ReadOnlySpan<int> val = Ints;
+            Sum = val[0] + val[1] + val[2] + val[3];
+        }
+    }
+
+    class OutOfRangeAccess
+    {
+        private static ReadOnlySpan<int> Ints => new int[] { 5, 6, 7, 8 };
+
+        public readonly static int Sum;
+
+        static OutOfRangeAccess()
+        {
+            ReadOnlySpan<int> val = Ints;
+            Sum = val[4];
+        }
+    }
+
+    class DefaultInstanceAccess
+    {
+        public readonly static int Sum;
+
+        static DefaultInstanceAccess()
+        {
+            ReadOnlySpan<int> val = default;
+            Sum = val[0];
+        }
+    }
+
+    public static void Run()
+    {
+        Assert.IsPreinitialized(typeof(SimpleReadOnlySpanAccess));
+        Assert.AreEqual(26, SimpleReadOnlySpanAccess.Sum);
+
+        Assert.IsLazyInitialized(typeof(OutOfRangeAccess));
+        if (SimpleReadOnlySpanAccess.Sum == 1000) // never true
+            OutOfRangeAccess.Sum.ToString(); // make sure cctor is looked at
+
+        Assert.IsLazyInitialized(typeof(DefaultInstanceAccess));
+        if (SimpleReadOnlySpanAccess.Sum == 1000) // never true
+            DefaultInstanceAccess.Sum.ToString(); // make sure cctor is looked at
+    }
+}
+
 static class Assert
 {
     [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",

From b7e4fb814b1de15d26983750d30c1f3dec93e2c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?=
 <MichalStrehovsky@users.noreply.github.com>
Date: Mon, 13 Feb 2023 09:57:44 +0900
Subject: [PATCH 2/3] Apply suggestions from code review

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
---
 .../Compiler/TypePreinit.cs                   | 26 +++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
index ace788daa04c96..68c5b25276837e 100644
--- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
+++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
@@ -1415,18 +1415,44 @@ private Status TryScanMethod(MethodIL methodIL, Value[] parameters, Stack<Method
                         }
                         break;
 
+                    case ILOpcode.ldind_i1:
+                    case ILOpcode.ldind_u1:
+                    case ILOpcode.ldind_i2:
+                    case ILOpcode.ldind_u2:
                     case ILOpcode.ldind_i4:
                     case ILOpcode.ldind_u4:
+                    case ILOpcode.ldind_i8:
                         {
                             StackEntry entry = stack.Pop();
                             if (entry.Value is ByRefValue byRefVal)
                             {
                                 switch (opcode)
                                 {
+                                    case ILOpcode.ldind_i1:
+                                        stack.Push(StackValueKind.Int32, ValueTypeValue.FromInt32(byRefVal.DereferenceAsSByte()));
+                                        break;
+                                    case ILOpcode.ldind_u1:
+                                        stack.Push(StackValueKind.Int32, ValueTypeValue.FromInt32((byte)byRefVal.DereferenceAsSByte()));
+                                        break;
+                                    case ILOpcode.ldind_i2:
+                                        stack.Push(StackValueKind.Int32, ValueTypeValue.FromInt32(byRefVal.DereferenceAsInt16()));
+                                        break;
+                                    case ILOpcode.ldind_u2:
+                                        stack.Push(StackValueKind.Int32, ValueTypeValue.FromInt32((ushort)byRefVal.DereferenceAsInt16()));
+                                        break;
                                     case ILOpcode.ldind_i4:
                                     case ILOpcode.ldind_u4:
                                         stack.Push(StackValueKind.Int32, ValueTypeValue.FromInt32(byRefVal.DereferenceAsInt32()));
                                         break;
+                                    case ILOpcode.ldind_i8:
+                                        stack.Push(StackValueKind.Int64, ValueTypeValue.FromInt64(byRefVal.DereferenceAsInt64()));
+                                        break;
+                                    case ILOpcode.ldind_r4:
+                                        stack.Push(StackValueKind.Float, ValueTypeValue.FromDouble(byRefVal.DereferenceAsSingle()));
+                                        break;
+                                    case ILOpcode.ldind_r8:
+                                        stack.Push(StackValueKind.Float, ValueTypeValue.FromDouble(byRefVal.DereferenceAsDouble()));
+                                        break;
                                 }
                             }
                             else

From 2c25c070cc92a4f4e7084232ab2577475f6f1b0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?=
 <MichalStrehovsky@users.noreply.github.com>
Date: Mon, 13 Feb 2023 10:58:04 +0900
Subject: [PATCH 3/3] Update
 src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
---
 .../tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs    | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
index 68c5b25276837e..61adf5a2d5559c 100644
--- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
+++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs
@@ -2198,7 +2198,12 @@ private ReadOnlySpan<byte> AsExactByteCount(int count)
                 return new ReadOnlySpan<byte>(PointedToBytes, PointedToOffset, count);
             }
 
+            public sbyte DereferenceAsSByte() => (sbyte)AsExactByteCount(1)[0];
+            public short DereferenceAsInt16() => BitConverter.ToInt16(AsExactByteCount(2));
             public int DereferenceAsInt32() => BitConverter.ToInt32(AsExactByteCount(4));
+            public long DereferenceAsInt64() => BitConverter.ToInt64(AsExactByteCount(8));
+            public float DereferenceAsSingle() => BitConverter.ToSingle(AsExactByteCount(4));
+            public double DereferenceAsDouble() => BitConverter.ToDouble(AsExactByteCount(8));
         }
 
         private abstract class ReferenceTypeValue : Value
