Skip to content

[BUG]: Incorrect handling of value-type to object assignment when using BlockAsync (missing boxing) #126

@victorr99

Description

@victorr99

Current Behavior

The reproduction code shows an expression were I assign a constant to a variable before an Await call is made. This expression raises an exception when being lowered.

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.ArgumentException: Expression of type 'System.Int32' cannot be used for assignment to type 'System.Object'
   at System.Linq.Expressions.Expression.Assign(Expression left, Expression right)
   at Hyperbee.Expressions.CompilerServices.Transitions.Transition.SetResult(List`1 expressions, StateMachineContext context)
   at Hyperbee.Expressions.CompilerServices.Transitions.Transition.AddExpressions(List`1 expressions, StateMachineContext context)
   at Hyperbee.Expressions.CompilerServices.Transitions.AwaitTransition.AddExpressions(List`1 expressions, StateMachineContext context)
   at Hyperbee.Expressions.CompilerServices.StateNode.GetExpression(StateMachineContext context)
   at Hyperbee.Expressions.CompilerServices.StateContext.Scope.GetExpressions(StateMachineContext context)
   at Hyperbee.Expressions.CompilerServices.AsyncStateMachineBuilder`1.CreateBody(FieldInfo[] fields, StateMachineContext context, Expression[] antecedents)
   at Hyperbee.Expressions.CompilerServices.AsyncStateMachineBuilder`1.CreateMoveNextBody(Int32 id, StateMachineContext context, Type stateMachineType, FieldInfo[] fields)
   at Hyperbee.Expressions.CompilerServices.AsyncStateMachineBuilder`1.CreateStateMachine(AsyncLoweringTransformer loweringTransformer, Int32 id)
   at Hyperbee.Expressions.CompilerServices.AsyncStateMachineBuilder.Create[TResult](AsyncLoweringTransformer loweringTransformer)
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
   --- End of inner exception stack trace ---
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
   at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at Hyperbee.Expressions.CompilerServices.AsyncStateMachineBuilder.Create(Type resultType, AsyncLoweringTransformer loweringTransformer)
   at Hyperbee.Expressions.AsyncBlockExpression.Reduce()
   at System.Linq.Expressions.Expression.ReduceAndCheck()
   at System.Linq.Expressions.Expression.ReduceExtensions()
   at System.Linq.Expressions.Compiler.StackSpiller.RewriteExtensionExpression(Expression expr, Stack stack)
   at System.Linq.Expressions.Compiler.StackSpiller.RewriteExpression(Expression node, Stack stack)
   at System.Linq.Expressions.Compiler.StackSpiller.Rewrite[T](Expression`1 lambda)
   at System.Linq.Expressions.Compiler.LambdaCompiler.Compile(LambdaExpression lambda)
   at System.Linq.Expressions.Expression`1.Compile()
   at Program.Main() in C:\something\Program.cs:line 25
   at Program.<Main>()

Expected Behavior

No expression is thrown and the value is either converted or ignored (the example does not need to have the result of the assign).

Steps To Reproduce

var variable = Variable(typeof(int));

var lambda = Lambda<Func<Task<object>>>(
    BlockAsync(
        [variable],
        [
            Assign(variable, Constant(0)),
            Await(Constant(Task.FromResult(new object())))
        ]));

var method = lambda.Compile();
Console.WriteLine(method());

Exception is raied when Compile is called.

Anything else?

It seems that this line is causing the issue:

if ( variable.Type.IsAssignableFrom( lastExpression.Type ) )

This line returns true when variable.Type == typeof(object) and lastExpression.Type == typeof(int). Assigning an int to an object is perfectly valid in C#, but require boxing in the expression trees. My guess would be that this check needs to be stricter. Maybe something like this should be added:

if ( variable.Type.IsAssignableFrom( lastExpression.Type ) && ( variable.Type != typeof( object ) || !lastExpression.Type.IsValueType )) 

This will only assign the result of the expression to the variable if it's truly compatible. If the user wanted to return an int were the return type is object, a Convert call would be required anyways as the value type (int) needs to be boxed.

I'll try to fix it in my fork of this repo. If the other tests remain working I'll submit the PR.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions