From 7e0c9cc323542c7c38bb196d6563877ccefec1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Sun, 18 Oct 2026 23:51:01 -0400 Subject: [PATCH 1/2] Support non-void index assignment in LightCompiler --- .../Interpreter/LightCompiler.cs | 14 +++++++--- .../Microsoft.Dynamic.Test/InterpreterTest.cs | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs b/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs index eaef8782..0a60e9a2 100644 --- a/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs +++ b/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs @@ -408,10 +408,6 @@ private void CompileIndexExpression(Expression expr) { private void CompileIndexAssignment(BinaryExpression node, bool asVoid) { var index = (IndexExpression)node.Left; - if (!asVoid) { - throw new NotImplementedException(); - } - // instance: if (index.Object is not null) { Compile(index.Object); @@ -424,6 +420,11 @@ private void CompileIndexAssignment(BinaryExpression node, bool asVoid) { // value: Compile(node.Right); + LocalDefinition local = default; + if (!asVoid) { + local = _locals.DefineLocal(Expression.Parameter(node.Right.Type), Instructions.Count); + Instructions.EmitAssignLocal(local.Index); + } if (index.Indexer is not null) { EmitCall(index.Indexer.GetSetMethod(true)); @@ -432,6 +433,11 @@ private void CompileIndexAssignment(BinaryExpression node, bool asVoid) { } else { Instructions.EmitSetArrayItem(index.Object.Type); } + + if (!asVoid) { + Instructions.EmitLoadLocal(local.Index); + _locals.UndefineLocal(local, Instructions.Count); + } } private void CompileMemberAssignment(BinaryExpression node, bool asVoid) { diff --git a/tests/Microsoft.Dynamic.Test/InterpreterTest.cs b/tests/Microsoft.Dynamic.Test/InterpreterTest.cs index eebf4db9..4ca5c8c2 100644 --- a/tests/Microsoft.Dynamic.Test/InterpreterTest.cs +++ b/tests/Microsoft.Dynamic.Test/InterpreterTest.cs @@ -39,6 +39,32 @@ public void PopTest() { Scripting.Generation.CompilerHelpers.LightCompile(lambda).DynamicInvoke(); } + [Test] + public void IndexAssignmentValueTest() { + // the value of an index assignment is the assigned value + var array = Expression.Parameter(typeof(int[]), "array"); + var arrayLambda = Expression.Lambda>( + Expression.Assign(Expression.ArrayAccess(array, Expression.Constant(1)), Expression.Constant(42)), + array + ); + + var interpretedArray = new int[3]; + var compiledArray = new int[3]; + Assert.That(Scripting.Generation.CompilerHelpers.LightCompile(arrayLambda)(interpretedArray), Is.EqualTo(arrayLambda.Compile()(compiledArray))); + Assert.That(interpretedArray, Is.EqualTo(compiledArray)); + + var list = Expression.Parameter(typeof(List), "list"); + var listLambda = Expression.Lambda, int>>( + Expression.Assign(Expression.Property(list, "Item", Expression.Constant(1)), Expression.Constant(42)), + list + ); + + var interpretedList = new List { 0, 0, 0 }; + var compiledList = new List { 0, 0, 0 }; + Assert.That(Scripting.Generation.CompilerHelpers.LightCompile(listLambda)(interpretedList), Is.EqualTo(listLambda.Compile()(compiledList))); + Assert.That(interpretedList, Is.EqualTo(compiledList)); + } + private static readonly Type[] PrimitiveTypes = { typeof(bool), typeof(char), typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), From b61dd2c705c9ddba3979ca7ce32a06ff562aca4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Sun, 18 Oct 2026 23:51:12 -0400 Subject: [PATCH 2/2] Fix value-type field assignment in LightCompiler --- .../Interpreter/LightCompiler.cs | 4 ++ .../Microsoft.Dynamic.Test/InterpreterTest.cs | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs b/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs index 0a60e9a2..911d77ad 100644 --- a/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs +++ b/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs @@ -464,6 +464,10 @@ private void CompileMemberAssignment(BinaryExpression node, bool asVoid) { if (member.Member is FieldInfo fi) { if (member.Expression is not null) { Compile(member.Expression); + if (fi.DeclaringType.IsValueType && member.Expression.NodeType is not (ExpressionType.Parameter or ExpressionType.Unbox)) { + // the instance is a temporary value so the store must not update the original boxed value + EmitCall(typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.GetObjectValue))); + } } Compile(node.Right); diff --git a/tests/Microsoft.Dynamic.Test/InterpreterTest.cs b/tests/Microsoft.Dynamic.Test/InterpreterTest.cs index 4ca5c8c2..39e82c19 100644 --- a/tests/Microsoft.Dynamic.Test/InterpreterTest.cs +++ b/tests/Microsoft.Dynamic.Test/InterpreterTest.cs @@ -39,6 +39,44 @@ public void PopTest() { Scripting.Generation.CompilerHelpers.LightCompile(lambda).DynamicInvoke(); } + public struct MutableStruct { + public int Field; + } + + [Test] + public void ValueTypeFieldAssignmentTest() { + // assigning a field of a converted (unboxed) value type updates a copy, not the boxed value + var obj = Expression.Parameter(typeof(object), "obj"); + var lambda = Expression.Lambda>( + Expression.Block( + Expression.Assign(Expression.Field(Expression.Convert(obj, typeof(MutableStruct)), nameof(MutableStruct.Field)), Expression.Constant(42)), + Expression.Field(Expression.Convert(obj, typeof(MutableStruct)), nameof(MutableStruct.Field)) + ), + obj + ); + + object interpretedBox = new MutableStruct { Field = 1 }; + object compiledBox = new MutableStruct { Field = 1 }; + Assert.That(Scripting.Generation.CompilerHelpers.LightCompile(lambda)(interpretedBox), Is.EqualTo(lambda.Compile()(compiledBox))); + Assert.That(((MutableStruct)interpretedBox).Field, Is.EqualTo(((MutableStruct)compiledBox).Field)); + } + + [Test] + public void UnboxedValueTypeFieldAssignmentTest() { + // assigning a field through Unbox updates the boxed value + var obj = Expression.Parameter(typeof(object), "obj"); + var lambda = Expression.Lambda>( + Expression.Assign(Expression.Field(Expression.Unbox(obj, typeof(MutableStruct)), nameof(MutableStruct.Field)), Expression.Constant(42)), + obj + ); + + object interpretedBox = new MutableStruct { Field = 1 }; + object compiledBox = new MutableStruct { Field = 1 }; + Scripting.Generation.CompilerHelpers.LightCompile(lambda)(interpretedBox); + lambda.Compile()(compiledBox); + Assert.That(((MutableStruct)interpretedBox).Field, Is.EqualTo(((MutableStruct)compiledBox).Field)); + } + [Test] public void IndexAssignmentValueTest() { // the value of an index assignment is the assigned value