dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.97k stars 4.65k forks source link

Existing behavior of AndAlso and OrElse in DLR is wrong #83350

Open ds1709 opened 1 year ago

ds1709 commented 1 year ago

Description

As described on MSDN, in DLR operations AndAlso and OrElse represent pairs of IsFalse/And and IsTrue/Or. This is becoming a problem when I'm trying to implement some zero/non-zero boolean logic.

For example, I have some dynamic object over integer. Zero value is false, non-zero is true. In c++ it works like this:

int a = 10;
int b = 20;
cout << (a && b); // out '1', coz 10 and 20 both are non-zero values
cout << (a & b); // out '0', coz it's a result of bitwise and

With current implementation of DLR I can't implement such behavior coz when I binding binary operation, I got And/Or operation type instead of expected AndAlso/OrElse. So I dont know, was it logical or bitwise operation.

Reproduction Steps

Define this simple class:

public sealed class MetaUInt32 : DynamicObject
{
    private readonly uint value;

    public MetaUInt32(uint value)
    {
        this.value = value;
    }

    public override bool TryUnaryOperation(UnaryOperationBinder binder, out object? result)
    {
        if (binder.Operation == ExpressionType.IsFalse)
        {
            result = (value == 0u);
            return true;
        }
        else
        {
            return base.TryUnaryOperation(binder, out result);
        }
    }

    public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object? result)
    {
        if (arg is MetaUInt32 m && binder.Operation is ExpressionType.And)
        {
            result = new MetaUInt32(value & m.value);
            return true;
        }
        else
        {
            return base.TryBinaryOperation(binder, arg, out result);
        }
    }
}

And then use it like this:

dynamic a = new MetaUInt32(10);
dynamic b = new MetaUInt32(20);
var c = a && b;

Expected behavior

Variable c must be instance of MetaUInt32 over 1, because a and b are non-zero values (10 and 20).

Actual behavior

Variable c is instance of MetaUInt32 over 0, because it's a result of bitwise and over 10 and 20.

Regression?

No response

Known Workarounds

As workaround, I can implement casting of MetaUInt32 to boolean and then make boolean binary operation over cast results, like this:

dynamic c = new MetaUInt32((bool)a && (bool)c ? 1 : 0);

It's not too hard to do, but if your dynamic class also implements bitwise logic, you must keep in mind, that boolean binary logic works correctly only if you cast your object to boolean first.

Also it's becoming a problem when you are working with dynamic expressions (System.Linq.Expressions.Expression.Dynamic). If you'are implementing binary operation, you must have special case for AndAlso/OrElse operations, because them are not supported by BinaryOperationBinder.

Configuration

No response

Other information

No response

dotnet-issue-labeler[bot] commented 1 year ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

ghost commented 1 year ago

Tagging subscribers to this area: @cston See info in area-owners.md if you want to be subscribed.

Issue Details
### Description As described on [MSDN](https://learn.microsoft.com/ru-ru/dotnet/api/system.dynamic.dynamicobject.trybinaryoperation?view=net-7.0), in DLR operations `AndAlso` and `OrElse` represent pairs of `IsFalse/And` and `IsTrue/Or`. This is becoming a problem when I'm trying to implement some zero/non-zero boolean logic. For example, I have some dynamic object over integer. Zero value is false, non-zero is true. In c++ it works like this: ```cpp int a = 10; int b = 20; cout << (a && b); // out '1', coz 10 and 20 both are non-zero values cout << (a & b); // out '0', coz it's a result of bitwise and ``` With current implementation of DLR I can't implement such behavior coz when I binding binary operation, I got `And/Or` operation type instead of expected `AndAlso/OrElse`. So I dont know, was it logical or bitwise operation. ### Reproduction Steps Define this simple class: ```csharp public sealed class MetaUInt32 : DynamicObject { private readonly uint value; public MetaUInt32(uint value) { this.value = value; } public override bool TryUnaryOperation(UnaryOperationBinder binder, out object? result) { if (binder.Operation == ExpressionType.IsFalse) { result = (value == 0u); return true; } else { return base.TryUnaryOperation(binder, out result); } } public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object? result) { if (arg is MetaUInt32 m && binder.Operation is ExpressionType.And) { result = new MetaUInt32(value & m.value); return true; } else { return base.TryBinaryOperation(binder, arg, out result); } } } ``` And then use it like this: ```csharp dynamic a = new MetaUInt32(10); dynamic b = new MetaUInt32(20); var c = a && b; ``` ### Expected behavior Variable `c` must be instance of `MetaUInt32` over `1`, because `a` and `b` are non-zero values (`10` and `20`). ### Actual behavior Variable `c` is instance of `MetaUInt32` over `0`, because it's a result of `bitwise and` over `10` and `20`. ### Regression? _No response_ ### Known Workarounds As workaround, I can implement casting of `MetaUInt32` to boolean and then make boolean binary operation over cast results, like this: ```csharp dynamic c = new MetaUInt32((bool)a && (bool)c ? 1 : 0); ``` It's not too hard to do, but if your dynamic class also implements bitwise logic, you must keep in mind, that boolean binary logic works correctly only if you cast your object to boolean first. Also it's becoming a problem when you are working with dynamic expressions (`System.Linq.Expressions.Expression.Dynamic`). If you'are implementing binary operation, you must have special case for `AndAlso/OrElse` operations, because them are not supported by `BinaryOperationBinder`. ### Configuration _No response_ ### Other information _No response_
Author: ds1709
Assignees: -
Labels: `area-System.Dynamic.Runtime`, `untriaged`
Milestone: -
svick commented 1 year ago

The DLR is basically fossilized now, so I wouldn't expect it to improve here. But the information you're looking for is actually present on the CSharpBinaryOperationBinder, just hidden in an internal property. If accessing that is acceptable for you, you could get the information you need:

private static Type cSharpBinaryOperationBinder = typeof(Microsoft.CSharp.RuntimeBinder.Binder).Assembly
    .GetType("Microsoft.CSharp.RuntimeBinder.CSharpBinaryOperationBinder")!;

...

if (binder.GetType() == cSharpBinaryOperationBinder)
{
    bool isLogicalOperation = (bool)cSharpBinaryOperationBinder
        .GetProperty("IsLogicalOperation", BindingFlags.Instance | BindingFlags.NonPublic)!
        .GetValue(binder)!;

    ...
}
ds1709 commented 1 year ago

But the information you're looking for is actually present on the CSharpBinaryOperationBinder, just hidden in an internal property.

Thnx for information. Seems like it can work, will try. However, it's better to be any public way to do this.