Skip to content

Commit 6e2c887

Browse files
committed
feature: out/ref parameter write-back for Invoke node and AOT expression executor
1 parent fdf8a4a commit 6e2c887

11 files changed

Lines changed: 378 additions & 18 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using GameDevWare.Dynamic.Expressions.Execution;
6+
using Xunit;
7+
8+
namespace GameDevWare.Dynamic.Expressions.Tests;
9+
10+
public class ExecutionExtendedTests
11+
{
12+
private class Parent
13+
{
14+
public MyStruct StructField;
15+
public MyStruct StructProperty { get; set; }
16+
public Parent SubParent;
17+
}
18+
19+
private struct MyStruct
20+
{
21+
public int Value;
22+
}
23+
24+
[Fact]
25+
public void MemberInit_NestedStructField_ModifiesOriginal()
26+
{
27+
// () => new Parent { StructField = { Value = 1 } }
28+
var parentParam = Expression.Parameter(typeof(Parent), "p");
29+
var structField = typeof(Parent).GetField(nameof(Parent.StructField));
30+
var valueField = typeof(MyStruct).GetField(nameof(MyStruct.Value));
31+
32+
var binding = Expression.MemberBind(structField, Expression.Bind(valueField, Expression.Constant(1)));
33+
var memberInit = Expression.MemberInit(Expression.New(typeof(Parent)), binding);
34+
var lambda = Expression.Lambda<Func<Parent>>(memberInit);
35+
36+
var compiled = lambda.CompileAot(forceAot: true);
37+
var result = compiled();
38+
39+
Assert.Equal(1, result.StructField.Value);
40+
}
41+
42+
[Fact]
43+
public void MemberInit_NestedStructProperty_ModifiesOriginal()
44+
{
45+
// () => new Parent { StructProperty = { Value = 1 } }
46+
var structProp = typeof(Parent).GetProperty(nameof(Parent.StructProperty));
47+
var valueField = typeof(MyStruct).GetField(nameof(MyStruct.Value));
48+
49+
var binding = Expression.MemberBind(structProp, Expression.Bind(valueField, Expression.Constant(1)));
50+
var memberInit = Expression.MemberInit(Expression.New(typeof(Parent)), binding);
51+
var lambda = Expression.Lambda<Func<Parent>>(memberInit);
52+
53+
var compiled = lambda.CompileAot(forceAot: true);
54+
var result = compiled();
55+
56+
Assert.Equal(1, result.StructProperty.Value);
57+
}
58+
59+
[Fact]
60+
public void MemberInit_DeeplyNested()
61+
{
62+
// () => new Parent { SubParent = new Parent { StructField = { Value = 5 } } }
63+
var subParentField = typeof(Parent).GetField(nameof(Parent.SubParent));
64+
var structField = typeof(Parent).GetField(nameof(Parent.StructField));
65+
var valueField = typeof(MyStruct).GetField(nameof(MyStruct.Value));
66+
67+
var innerBinding = Expression.MemberBind(structField, Expression.Bind(valueField, Expression.Constant(5)));
68+
var innerInit = Expression.MemberInit(Expression.New(typeof(Parent)), innerBinding);
69+
var outerBinding = Expression.Bind(subParentField, innerInit);
70+
var outerInit = Expression.MemberInit(Expression.New(typeof(Parent)), outerBinding);
71+
var lambda = Expression.Lambda<Func<Parent>>(outerInit);
72+
73+
var compiled = lambda.CompileAot(forceAot: true);
74+
var result = compiled();
75+
76+
Assert.NotNull(result.SubParent);
77+
Assert.Equal(5, result.SubParent.StructField.Value);
78+
}
79+
80+
[Fact]
81+
public void ListInit_ComplexInitializers()
82+
{
83+
// () => new List<int> { 1, 2, 3 }
84+
var listInit = (Expression<Func<List<int>>>)(() => new List<int> { 1, 2, 3 });
85+
var compiled = listInit.CompileAot(forceAot: true);
86+
var result = compiled();
87+
88+
Assert.Equal(new[] { 1, 2, 3 }, result);
89+
}
90+
91+
[Fact]
92+
public void ListInit_Dictionary()
93+
{
94+
// () => new Dictionary<int, string> { { 1, "one" }, { 2, "two" } }
95+
var dictInit = (Expression<Func<Dictionary<int, string>>>)(() => new Dictionary<int, string> { { 1, "one" }, { 2, "two" } });
96+
var compiled = dictInit.CompileAot(forceAot: true);
97+
var result = compiled();
98+
99+
Assert.Equal(2, result.Count);
100+
Assert.Equal("one", result[1]);
101+
Assert.Equal("two", result[2]);
102+
}
103+
104+
[Fact]
105+
public void NewArrayBounds_Multidimensional()
106+
{
107+
// () => new int[2, 3]
108+
var expr = (Expression<Func<int[,]>>)(() => new int[2, 3]);
109+
var compiled = expr.CompileAot(forceAot: true);
110+
var result = compiled();
111+
112+
Assert.Equal(2, result.GetLength(0));
113+
Assert.Equal(3, result.GetLength(1));
114+
}
115+
116+
[Fact]
117+
public void ArrayIndex_Multidimensional()
118+
{
119+
// (arr) => arr[1, 2]
120+
var param = Expression.Parameter(typeof(int[,]), "arr");
121+
var arrayIndex = Expression.ArrayIndex(param, Expression.Constant(1), Expression.Constant(2));
122+
var lambda = Expression.Lambda<Func<int[,], int>>(arrayIndex, param);
123+
124+
var compiled = lambda.CompileAot(forceAot: true);
125+
var arr = new int[2, 3];
126+
arr[1, 2] = 42;
127+
128+
var result = compiled(arr);
129+
Assert.Equal(42, result);
130+
}
131+
132+
[Fact]
133+
public void ArrayIndex_SingleDimensional()
134+
{
135+
// (arr) => arr[1]
136+
var param = Expression.Parameter(typeof(int[]), "arr");
137+
var arrayIndex = Expression.ArrayIndex(param, Expression.Constant(1));
138+
var lambda = Expression.Lambda<Func<int[], int>>(arrayIndex, param);
139+
140+
var compiled = lambda.CompileAot(forceAot: true);
141+
var arr = new int[] { 10, 20, 30 };
142+
143+
var result = compiled(arr);
144+
Assert.Equal(20, result);
145+
}
146+
}

src/GameDevWare.Dynamic.Expressions.Tests/ExecutorTests.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,4 +1142,118 @@ public void UnboxNullable()
11421142
Assert.Equal(expected, actual);
11431143
Assert.Equal(expectedAlt, actual);
11441144
}
1145+
1146+
public delegate void OutDelegate(out int x);
1147+
public class Container { public int Value; }
1148+
1149+
[Theory]
1150+
[InlineData(true)]
1151+
[InlineData(false)]
1152+
public void TestInvokeOutParameterToField(bool forceAot)
1153+
{
1154+
OutDelegate d = (out int val) => { val = 10; };
1155+
var container = new Container { Value = 0 };
1156+
1157+
// (del, c) => del(out c.Value)
1158+
var delParam = Expression.Parameter(typeof(OutDelegate), "del");
1159+
var cParam = Expression.Parameter(typeof(Container), "c");
1160+
var valueField = typeof(Container).GetField("Value");
1161+
var fieldAccess = Expression.Field(cParam, valueField);
1162+
var invoke = Expression.Invoke(delParam, fieldAccess);
1163+
var lambda = Expression.Lambda<Action<OutDelegate, Container>>(invoke, delParam, cParam);
1164+
1165+
var compiled = lambda.CompileAot(forceAot);
1166+
1167+
compiled(d, container);
1168+
Assert.Equal(10, container.Value);
1169+
}
1170+
1171+
[Theory]
1172+
[InlineData(true)]
1173+
[InlineData(false)]
1174+
public void TestInvokeOutParameterToProperty(bool forceAot)
1175+
{
1176+
OutDelegate d = (out int val) => { val = 10; };
1177+
var container = new ContainerPropertyOut { Value = 0 };
1178+
1179+
// (del, c) => del(out c.Value)
1180+
var delParam = Expression.Parameter(typeof(OutDelegate), "del");
1181+
var cParam = Expression.Parameter(typeof(ContainerPropertyOut), "c");
1182+
var valueProperty = typeof(ContainerPropertyOut).GetProperty("Value");
1183+
var propertyAccess = Expression.Property(cParam, valueProperty);
1184+
var invoke = Expression.Invoke(delParam, propertyAccess);
1185+
var lambda = Expression.Lambda<Action<OutDelegate, ContainerPropertyOut>>(invoke, delParam, cParam);
1186+
1187+
var compiled = lambda.CompileAot(forceAot);
1188+
1189+
compiled(d, container);
1190+
Assert.Equal(10, container.Value);
1191+
}
1192+
1193+
[Theory]
1194+
[InlineData(true)]
1195+
[InlineData(false)]
1196+
public void TestInvokeOutParameterToArrayElement(bool forceAot)
1197+
{
1198+
OutDelegate d = (out int val) => { val = 10; };
1199+
var array = new int[1];
1200+
1201+
// (del, arr) => del(out arr[0])
1202+
var delParam = Expression.Parameter(typeof(OutDelegate), "del");
1203+
var arrParam = Expression.Parameter(typeof(int[]), "arr");
1204+
var arrayAccess = Expression.ArrayIndex(arrParam, Expression.Constant(0));
1205+
var invoke = Expression.Invoke(delParam, arrayAccess);
1206+
var lambda = Expression.Lambda<Action<OutDelegate, int[]>>(invoke, delParam, arrParam);
1207+
1208+
var compiled = lambda.CompileAot(forceAot);
1209+
1210+
compiled(d, array);
1211+
Assert.Equal(10, array[0]);
1212+
}
1213+
1214+
public class ContainerPropertyOut { public int Value { get; set; } }
1215+
1216+
public class StaticContainerOut
1217+
{
1218+
public static void StaticOutMethod(out int x) { x = 20; }
1219+
public void InstanceOutMethod(out int x) { x = 30; }
1220+
}
1221+
1222+
[Theory]
1223+
[InlineData(true)]
1224+
[InlineData(false)]
1225+
public void TestCallOutParameterStatic(bool forceAot)
1226+
{
1227+
var container = new Container { Value = 0 };
1228+
var method = typeof(StaticContainerOut).GetMethod("StaticOutMethod");
1229+
var cParam = Expression.Parameter(typeof(Container), "c");
1230+
var valueField = typeof(Container).GetField("Value");
1231+
var fieldAccess = Expression.Field(cParam, valueField);
1232+
var call = Expression.Call(method, fieldAccess);
1233+
var lambda = Expression.Lambda<Action<Container>>(call, cParam);
1234+
1235+
var compiled = lambda.CompileAot(forceAot);
1236+
compiled(container);
1237+
Assert.Equal(20, container.Value);
1238+
}
1239+
1240+
[Theory]
1241+
[InlineData(true)]
1242+
[InlineData(false)]
1243+
public void TestCallOutParameterInstance(bool forceAot)
1244+
{
1245+
var staticContainer = new StaticContainerOut();
1246+
var container = new Container { Value = 0 };
1247+
var method = typeof(StaticContainerOut).GetMethod("InstanceOutMethod");
1248+
var scParam = Expression.Parameter(typeof(StaticContainerOut), "sc");
1249+
var cParam = Expression.Parameter(typeof(Container), "c");
1250+
var valueField = typeof(Container).GetField("Value");
1251+
var fieldAccess = Expression.Field(cParam, valueField);
1252+
var call = Expression.Call(scParam, method, fieldAccess);
1253+
var lambda = Expression.Lambda<Action<StaticContainerOut, Container>>(call, scParam, cParam);
1254+
1255+
var compiled = lambda.CompileAot(forceAot);
1256+
compiled(staticContainer, container);
1257+
Assert.Equal(30, container.Value);
1258+
}
11451259
}

src/GameDevWare.Dynamic.Expressions.Tests/PackerExtendedTests.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,12 @@ public static IEnumerable<object[]> ExtendedPackUnpackExpressionData()
109109
};
110110
foreach (var expr in expressions)
111111
{
112-
string format;
113-
try { format = CSharpExpression.Format(expr); }
114-
catch { format = expr.ToString(); }
115-
yield return new object[] { format, expr };
112+
yield return new object[] { expr };
116113
}
117114
}
118115

119116
[Theory, MemberData(nameof(ExtendedPackUnpackExpressionData))]
120-
public void PackUnpackExpression(string expressionStr, LambdaExpression lambdaExpression)
117+
public void PackUnpackExpression(LambdaExpression lambdaExpression)
121118
{
122119
this.output.WriteLine("Original: " + lambdaExpression);
123120

src/GameDevWare.Dynamic.Expressions.Unity.2021/Packages/com.gamedevware.csharpeval/Runtime/Execution/ArrayIndexNode.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ public override object Run(Closure closure)
4747
: target.GetValue(closure.Unbox<int>(index));
4848
}
4949

50+
public override bool WriteBack(Closure closure, object value)
51+
{
52+
if (this.methodCallNode != null) return false;
53+
54+
var target = closure.Unbox<Array>(this.targetNode.Run(closure));
55+
56+
if (target == null)
57+
throw new NullReferenceException(string.Format(Resources.EXCEPTION_EXECUTION_EXPRESSIONGIVESNULLRESULT, this.expression));
58+
59+
var index = this.indexNode.Run(closure);
60+
if (closure.Is<int[]>(index))
61+
target.SetValue(value, closure.Unbox<int[]>(index));
62+
else
63+
target.SetValue(value, closure.Unbox<int>(index));
64+
65+
return true;
66+
}
67+
5068
/// <inheritdoc />
5169
public override string ToString()
5270
{

src/GameDevWare.Dynamic.Expressions.Unity.2021/Packages/com.gamedevware.csharpeval/Runtime/Execution/CallNode.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,16 @@ public override object Run(Closure closure)
4646
arguments[i] = closure.Unbox<object>(this.argumentNodes[i].Run(closure));
4747
}
4848

49-
return this.methodCallExpression.Method.Invoke(target, arguments);
49+
var result = this.methodCallExpression.Method.Invoke(target, arguments);
50+
51+
var parameters = this.methodCallExpression.Method.GetParameters();
52+
for (var i = 0; i < parameters.Length && i < arguments.Length; i++)
53+
{
54+
if (parameters[i].ParameterType.IsByRef)
55+
this.argumentNodes[i].WriteBack(closure, arguments[i]);
56+
}
57+
58+
return result;
5059
}
5160

5261
/// <inheritdoc />

src/GameDevWare.Dynamic.Expressions.Unity.2021/Packages/com.gamedevware.csharpeval/Runtime/Execution/ExecutionNode.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Linq.Expressions;
33

44
namespace GameDevWare.Dynamic.Expressions.Execution
@@ -12,6 +12,17 @@ internal abstract class ExecutionNode
1212

1313
public abstract object Run(Closure closure);
1414

15+
/// <summary>
16+
/// Writes specified <paramref name="value" /> back to the expression's source.
17+
/// Used for propagating <c>out</c> and <c>ref</c> parameters.
18+
/// </summary>
19+
/// <param name="closure">Current execution closure. Not null.</param>
20+
/// <param name="value">Value to write back.</param>
21+
/// <returns>True if value was written back; otherwise, false.</returns>
22+
public virtual bool WriteBack(Closure closure, object value)
23+
{
24+
return false;
25+
}
1526
protected static bool IsNullable(Expression expression)
1627
{
1728
if (expression == null) throw new ArgumentException("expression");

src/GameDevWare.Dynamic.Expressions.Unity.2021/Packages/com.gamedevware.csharpeval/Runtime/Execution/InvocationNode.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
using System;
1+
using System;
22
using System.Linq.Expressions;
3+
using System.Reflection;
34
using GameDevWare.Dynamic.Expressions.Properties;
45

56
namespace GameDevWare.Dynamic.Expressions.Execution
@@ -27,17 +28,31 @@ public InvocationNode(InvocationExpression invocationExpression, ConstantExpress
2728
public override object Run(Closure closure)
2829
{
2930
var targetDelegate = closure.Unbox<Delegate>(this.target.Run(closure));
30-
var invokeArguments = new object[this.argumentNodes.Length];
31-
for (var i = 0; i < invokeArguments.Length; i++)
32-
invokeArguments[i] = closure.Unbox<object>(this.argumentNodes[i].Run(closure));
3331

3432
if (targetDelegate == null)
3533
{
3634
throw new NullReferenceException(string.Format(Resources.EXCEPTION_EXECUTION_EXPRESSIONGIVESNULLRESULT,
3735
this.invocationExpression.Expression));
3836
}
3937

40-
return targetDelegate.DynamicInvoke(invokeArguments);
38+
var invokeArguments = new object[this.argumentNodes.Length];
39+
for (var i = 0; i < invokeArguments.Length; i++)
40+
invokeArguments[i] = closure.Unbox<object>(this.argumentNodes[i].Run(closure));
41+
42+
var result = targetDelegate.DynamicInvoke(invokeArguments);
43+
44+
var invokeMethod = targetDelegate.GetType().GetTypeInfo().GetMethod(Constants.DELEGATE_INVOKE_NAME);
45+
if (invokeMethod != null)
46+
{
47+
var parameters = invokeMethod.GetParameters();
48+
for (var i = 0; i < parameters.Length && i < invokeArguments.Length; i++)
49+
{
50+
if (parameters[i].ParameterType.IsByRef)
51+
this.argumentNodes[i].WriteBack(closure, invokeArguments[i]);
52+
}
53+
}
54+
55+
return result;
4156
}
4257

4358
/// <inheritdoc />

0 commit comments

Comments
 (0)