Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 44 additions & 20 deletions src/FastExpressionCompiler/FastExpressionCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2287,11 +2287,13 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext

case ExpressionType.ArrayIndex:
var arrIndexExpr = (BinaryExpression)expr;
return TryEmit(arrIndexExpr.Left, il, ref context, parent | ParentFlags.IndexAccess)
&& TryEmit(arrIndexExpr.Right, il, ref context, parent | ParentFlags.IndexAccess) // #265
&& (IsNonVectorArray(arrIndexExpr.Left.Type)
? EmitMethodCallOrVirtualCallCheckForNull(il, arrIndexExpr.Left.Type.FindMethod("Get")) // the lower bound is not zero
: TryEmitArrayIndexGet(il, exprType, ref context, parent));
var arrayFlags = ParentFlags.IndexAccess | ParentFlags.AssignmentRightValue;
if (!TryEmit(arrIndexExpr.Left, il, ref context, arrayFlags))
return false;
if (!TryEmit(arrIndexExpr.Right, il, ref context, arrayFlags))
return false;
return TryEmitArrayElementGet(il, arrIndexExpr.Left.Type, exprType, ref context,
byRefIndex >= 0 ? parent | ParentFlags.AssignmentByRef : parent);

case ExpressionType.ArrayLength:
if (!TryEmit(((UnaryExpression)expr).Operand, il, ref context, parent))
Expand Down Expand Up @@ -2717,7 +2719,7 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext
return true;

case ExpressionType.Index:
return TryEmitIndexGet((IndexExpression)expr, il, ref context, parent);
return TryEmitIndexGet((IndexExpression)expr, il, ref context, parent, byRefIndex);

case ExpressionType.Goto:
return TryEmitGoto((GotoExpression)expr, il, ref context, parent);
Expand Down Expand Up @@ -2874,7 +2876,7 @@ private static bool TryEmitLoop(LoopExpression loopExpr, ILGenerator il, ref Com
}

// similar code is used by the TryEmitArithmeticAndOrAssign, so don't forget to modify it as well
private static bool TryEmitIndexGet(IndexExpression indexExpr, ILGenerator il, ref CompilerContext context, ParentFlags parent)
private static bool TryEmitIndexGet(IndexExpression indexExpr, ILGenerator il, ref CompilerContext context, ParentFlags parent, int byRefIndex = -1)
{
if (context.ArgsContainingComplexExpression.Set.ContainsKey(indexExpr))
return false;
Expand All @@ -2900,12 +2902,31 @@ private static bool TryEmitIndexGet(IndexExpression indexExpr, ILGenerator il, r
var indexerProp = indexExpr.Indexer;
if (indexerProp != null)
return EmitMethodCallOrVirtualCallCheckForNull(il, indexerProp.GetMethod);
if (indexArgCount == 1 && !IsNonVectorArray(objExpr.Type))
return TryEmitArrayIndexGet(il, indexExpr.Type, ref context, parent);
// Struct members need the element storage, rather than a copy returned by Get.
var byAddress = indexExpr.Type.IsValueType &&
(parent & (ParentFlags.MemberAccess | ParentFlags.InstanceAccess | ParentFlags.AssignmentByRef)) != 0;
var ok = EmitMethodCallOrVirtualCallCheckForNull(il, objExpr.Type.FindMethod(byAddress ? "Address" : "Get"));
return TryEmitArrayElementGet(il, objExpr.Type, indexExpr.Type, ref context,
byRefIndex >= 0 ? parent | ParentFlags.AssignmentByRef : parent);
}

private static bool IsVectorArray(Type type)
Comment thread
jogibear9988 marked this conversation as resolved.
Resolved
{
if (type.IsByRef)
type = type.GetElementType();
#if NETFRAMEWORK
return type.IsArray && type == type.GetElementType().MakeArrayType();
#else
return type.IsSZArray;
#endif
}

private static bool TryEmitArrayElementGet(ILGenerator il, Type arrayType, Type elementType,
ref CompilerContext context, ParentFlags parent)
{
if (arrayType.IsByRef)
arrayType = arrayType.GetElementType();
if (IsVectorArray(arrayType))
return TryEmitArrayIndexGet(il, elementType, ref context, parent) && il.EmitPopIfIgnoreResult(parent);
var byAddress = (parent & ParentFlags.AssignmentByRef) != 0 ||
elementType.IsValueType && (parent & (ParentFlags.MemberAccess | ParentFlags.InstanceAccess)) != 0;
var ok = EmitMethodCallOrVirtualCallCheckForNull(il, arrayType.FindMethod(byAddress ? "Address" : "Get"));
context.LastEmitIsAddress = byAddress && !parent.IgnoresResult();
return ok && il.EmitPopIfIgnoreResult(parent);
}
Expand Down Expand Up @@ -4945,7 +4966,7 @@ private static bool TryEmitArithmeticAndOrAssign(
Debug.Assert(orLeftIndexExpr != null);
if (objExpr != null)
{
var isIndexerAMethodCall = indexArgCount > 1 | orLeftIndexExpr.Indexer != null || IsNonVectorArray(objExpr.Type);
var isIndexerAMethodCall = indexArgCount > 1 | orLeftIndexExpr.Indexer != null || !IsVectorArray(objExpr.Type);
var objFlags = isIndexerAMethodCall ? ParentFlags.InstanceCall : ParentFlags.InstanceAccess | ParentFlags.IndexAccess;
if (!TryEmit(objExpr, il, ref context, objFlags | ParentFlags.AssignmentLeftValue))
return false;
Expand Down Expand Up @@ -4977,7 +4998,7 @@ private static bool TryEmitArithmeticAndOrAssign(
{
var ok = orLeftIndexExpr.Indexer != null
? EmitMethodCallOrVirtualCallCheckForNull(il, orLeftIndexExpr.Indexer.SetMethod)
: indexArgCount == 1 && !IsNonVectorArray(objExpr.Type)
: IsVectorArray(objExpr.Type)
? TryEmitArrayIndexSet(il, orLeftIndexExpr.Type) // one-dimensional array
: EmitMethodCallOrVirtualCallCheckForNull(il, objExpr?.Type.FindMethod("Set")); // multi-dimensional array
if (!ok)
Expand Down Expand Up @@ -5008,7 +5029,7 @@ private static bool TryEmitArithmeticAndOrAssign(
{
// determine is the index essentially the method call to get/set value
// objExpr is not null when there is no Indexer, because then it is an array access, so `||` short-circuits before the `.Type`
var isIndexerAMethodCall = indexArgCount > 1 | orLeftIndexExpr.Indexer != null || IsNonVectorArray(objExpr.Type);
var isIndexerAMethodCall = indexArgCount > 1 | orLeftIndexExpr.Indexer != null || !IsVectorArray(objExpr.Type);
var objVar = -1;
var objVarByAddress = false;
if (objExpr != null)
Expand Down Expand Up @@ -5190,7 +5211,7 @@ private static bool TryEmitArithmeticAndOrAssign(
{
var ok = orLeftIndexExpr.Indexer != null
? EmitMethodCallOrVirtualCallCheckForNull(il, orLeftIndexExpr.Indexer.SetMethod)
: indexArgCount == 1 && !IsNonVectorArray(objExpr.Type)
: IsVectorArray(objExpr.Type)
? TryEmitArrayIndexSet(il, orLeftIndexExpr.Type) // one-dimensional array
: EmitMethodCallOrVirtualCallCheckForNull(il, objExpr?.Type.FindMethod("Set")); // multi-dimensional array
if (!ok)
Expand Down Expand Up @@ -5575,11 +5596,14 @@ private static void EmitStoreIndirectlyByRef(ILGenerator il, Type type)
}
}

// The one-dimensional array with the non-zero lower bound, e.g. Array.CreateInstance(typeof(int), new[] { 3 }, new[] { -1 }), is not the T[] vector and has no ldelem
private static bool IsNonVectorArray(Type type) => type.IsArray && type.GetArrayRank() == 1 && type != type.GetElementType().MakeArrayType();

private static bool TryEmitArrayIndexGet(ILGenerator il, Type type, ref CompilerContext closure, ParentFlags parent)
{
if ((parent & ParentFlags.AssignmentByRef) != 0)
{
il.Demit(OpCodes.Ldelema, type);
closure.LastEmitIsAddress = true;
return true;
}
if (!type.IsValueType)
{
il.Demit(OpCodes.Ldelem_Ref);
Expand Down
2 changes: 2 additions & 0 deletions test/FastExpressionCompiler.TestsRunner.Net472/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ static void RunLightExpressionTests(object state)
t.Run(new LightExpression.UnitTests.AbstractConstructorRegressionTests());
t.Run(new LightExpression.UnitTests.DirectLogicalFactoryRegressionTests());
t.Run(new LightExpression.UnitTests.TryResultValueRegressionTests());
t.Run(new LightExpression.UnitTests.NonVectorArrayRegressionTests());
t.Run(new LightExpression.UnitTests.UnboxStorageRegressionTests());
t.Run(new LightExpression.UnitTests.VoidSwitchConversionRegressionTests());
t.Run(new LightExpression.UnitTests.ReadonlyRefFieldRegressionTests());
Expand Down Expand Up @@ -89,6 +90,7 @@ static void RunLightExpressionTests(object state)
fecTests.Run(new AbstractConstructorRegressionTests());
fecTests.Run(new DirectLogicalFactoryRegressionTests());
fecTests.Run(new TryResultValueRegressionTests());
fecTests.Run(new NonVectorArrayRegressionTests());
fecTests.Run(new UnboxStorageRegressionTests());
fecTests.Run(new ReadonlyRefFieldRegressionTests());
fecTests.Run(new LiftedUserDefinedLogicalRegressionTests());
Expand Down
2 changes: 2 additions & 0 deletions test/FastExpressionCompiler.TestsRunner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public static void Main(string[] args)
lt.Run(new LightExpression.UnitTests.AbstractConstructorRegressionTests());
lt.Run(new LightExpression.UnitTests.DirectLogicalFactoryRegressionTests());
lt.Run(new LightExpression.UnitTests.TryResultValueRegressionTests());
lt.Run(new LightExpression.UnitTests.NonVectorArrayRegressionTests());
lt.Run(new LightExpression.UnitTests.UnboxStorageRegressionTests());
lt.Run(new LightExpression.UnitTests.VoidSwitchConversionRegressionTests());
lt.Run(new LightExpression.UnitTests.ReadonlyRefFieldRegressionTests());
Expand Down Expand Up @@ -115,6 +116,7 @@ public static void Main(string[] args)
st.Run(new AbstractConstructorRegressionTests());
st.Run(new DirectLogicalFactoryRegressionTests());
st.Run(new TryResultValueRegressionTests());
st.Run(new NonVectorArrayRegressionTests());
st.Run(new UnboxStorageRegressionTests());
st.Run(new ReadonlyRefFieldRegressionTests());
st.Run(new LiftedUserDefinedLogicalRegressionTests());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using S=System.Linq.Expressions.Expression;
#if LIGHT_EXPRESSION
namespace FastExpressionCompiler.LightExpression.UnitTests;
#else
namespace FastExpressionCompiler.UnitTests;
#endif
public struct NonVectorArrayRegressionTests : ITestX
{
public void Run(TestRun t) {
Reads_use_actual_lower_bounds(t); Writes_and_compounds_use_actual_lower_bounds(t);
Struct_elements_remain_addressable(t); Ref_arguments_preserve_array_element_storage(t);
Ignored_reads_still_validate_array_and_bounds(t); Receiver_addresses_survive_exception_arguments(t);
}
public struct Cell { public int Value; public int Increment(int n) => Value+=n; }
public static int Increment(ref int n) => ++n;
public static void Replace(ref string value) => value="changed";
public static void ReplaceObject(ref object value) => value=new object();
static Array Make(Type t, int lower) => Array.CreateInstance(t,new[]{3},new[]{lower});
public void Reads_use_actual_lower_bounds(TestContext t) {
Check(t,42); Check(t,"hello"); Check(t,(int?)7); Check(t,(string)null); Check(t,new Cell{Value=9});
}
static void Check<T>(TestContext t,T value) {
foreach(var lower in new[]{-2,1}) {
var array=Make(typeof(T),lower);array.SetValue(value,lower);
foreach(var binary in new[]{false,true}) {
var a=S.Parameter(typeof(Array));var i=S.Parameter(typeof(int));var converted=S.Convert(a,array.GetType());
var access=binary?(System.Linq.Expressions.Expression)S.ArrayIndex(converted,i):S.ArrayAccess(converted,i);
var e=S.Lambda<Func<Array,int,T>>(access,a,i);var f=e.FromSysExpression().CompileFast(true);
if(!t.IsNotNull(f))return;t.AreEqual(value,f(array,lower));
#if !NETFRAMEWORK
// Framework's System compiler emits unverifiable IL for these non-vector array reads.
t.AreEqual(e.Compile()(array,lower),f(array,lower));
#endif
}
}
}
public void Writes_and_compounds_use_actual_lower_bounds(TestContext t) {
foreach(var lower in new[]{-2,0,1}) {
var array=Make(typeof(int),lower);var a=S.Parameter(typeof(Array));var i=S.Parameter(typeof(int));
var access=S.ArrayAccess(S.Convert(a,array.GetType()),i);
var e=S.Lambda<Func<Array,int,int>>(S.Block(S.Assign(access,S.Constant(5)),S.AddAssign(access,S.Constant(7)),S.PreIncrementAssign(access),S.PostDecrementAssign(access)),a,i);
var f=e.FromSysExpression().CompileFast(true);if(!t.IsNotNull(f))return;
t.AreEqual(13,f(array,lower));t.AreEqual(12,array.GetValue(lower));
}
}
public void Struct_elements_remain_addressable(TestContext t) {
foreach(var lower in new[]{-2,0,1}) {
var array=Make(typeof(Cell),lower);var a=S.Parameter(typeof(Array));var i=S.Parameter(typeof(int));
var access=S.ArrayAccess(S.Convert(a,array.GetType()),i);
var e=S.Lambda<Func<Array,int,int>>(S.Block(S.Assign(S.Field(access,nameof(Cell.Value)),S.Constant(40)),S.Call(access,typeof(Cell).GetMethod(nameof(Cell.Increment)),S.Constant(2))),a,i);
var f=e.FromSysExpression().CompileFast(true);if(!t.IsNotNull(f))return;
t.AreEqual(42,f(array,lower));t.AreEqual(42,((Cell)array.GetValue(lower)).Value);
}
}
public void Ref_arguments_preserve_array_element_storage(TestContext t) {
foreach(var lower in new[]{-2,0,1}) {
var ints=Make(typeof(int),lower);ints.SetValue(40,lower);var a=S.Parameter(typeof(Array));var i=S.Parameter(typeof(int));
var access=S.ArrayAccess(S.Convert(a,ints.GetType()),i);
var f=S.Lambda<Func<Array,int,int>>(S.Call(typeof(NonVectorArrayRegressionTests).GetMethod(nameof(Increment)),access),a,i).FromSysExpression().CompileFast(true);
if(!t.IsNotNull(f))return;t.AreEqual(41,f(ints,lower));t.AreEqual(41,ints.GetValue(lower));
var strings=Make(typeof(string),lower);strings.SetValue("old",lower);
var textAccess=S.ArrayAccess(S.Convert(a,strings.GetType()),i);
var set=S.Lambda<Action<Array,int>>(S.Call(typeof(NonVectorArrayRegressionTests).GetMethod(nameof(Replace)),textAccess),a,i).FromSysExpression().CompileFast(true);
if(!t.IsNotNull(set))return;set(strings,lower);t.AreEqual("changed",strings.GetValue(lower));
var objectArrayType=lower==0?typeof(object[]):typeof(object).MakeArrayType(1);
var covariantAccess=S.ArrayAccess(S.Convert(a,objectArrayType),i);
var covariant=S.Lambda<Action<Array,int>>(S.Call(typeof(NonVectorArrayRegressionTests).GetMethod(nameof(ReplaceObject)),covariantAccess),a,i).FromSysExpression().CompileFast(true);
if(!t.IsNotNull(covariant))return;t.Throws<ArrayTypeMismatchException>(()=>covariant(strings,lower));
}
}
public void Ignored_reads_still_validate_array_and_bounds(TestContext t) {
foreach(var lower in new[]{-2,0,1}) foreach(var binary in new[]{false,true}) {
var array=Make(typeof(int),lower);var a=S.Parameter(typeof(Array));var i=S.Parameter(typeof(int));var converted=S.Convert(a,array.GetType());
var access=binary?(System.Linq.Expressions.Expression)S.ArrayIndex(converted,i):S.ArrayAccess(converted,i);
var f=S.Lambda<Action<Array,int>>(S.Block(access,S.Empty()),a,i).FromSysExpression().CompileFast(true);
if(!t.IsNotNull(f))return;f(array,lower);t.Throws<IndexOutOfRangeException>(()=>f(array,lower-1));t.Throws<NullReferenceException>(()=>f(null,lower));
}
}
public void Receiver_addresses_survive_exception_arguments(TestContext t) {
foreach(var lower in new[]{-2,1}) {
var array=Make(typeof(Cell),lower);var a=S.Parameter(typeof(Array));
var element=S.ArrayAccess(S.Convert(a,array.GetType()),S.Constant(lower));
var rhs=S.TryFinally(S.Constant(3),S.Empty());
var tree=S.Lambda<Func<Array,int>>(S.Call(element,typeof(Cell).GetMethod(nameof(Cell.Increment)),rhs),a);
var fast=tree.FromSysExpression().CompileFast(true);if(!t.IsNotNull(fast))return;
t.AreEqual(3,fast(array));t.AreEqual(6,fast(array));t.AreEqual(6,((Cell)array.GetValue(lower)).Value);
}
}
}
Loading