dadhi / FastExpressionCompiler

Fast Compiler for C# Expression Trees and the lightweight LightExpression alternative. Diagnostic and code generation tools for the expressions.
MIT License
1.16k stars 82 forks source link

The JIT compiler encountered invalid IL code or an internal limitation #410

Open cda963 opened 5 months ago

cda963 commented 5 months ago

I'm using FastExpressionCompiler v2.0.0 along with Mapster (latest version) in a gRPC .Net Core project where I need to convert from a POCO object to a proto object, and everything works just fine.

When I try upgrading FastExpressionCompiler to any version above 2.0.0, the conversion throws an exception with the message "The JIT compiler encountered invalid IL code or an internal limitation".

Example: TenantConfig is my source object (a simple .Net object with a few string/boolean properties), and Protos.TenantConfig is the .proto equivalent of my .Net object:

var failure = new TenantConfig().Adapt<Protos.TenantConfig>();

dadhi commented 5 months ago

@cda963 Please provide the complete code for TenantConfig and Protos.TenantConfig. Every property matters, actually.

cda963 commented 5 months ago

This is my .Net object

public class TenantConfig
{
    public string Name { get; set; }

    public string Value { get; set; }

    public bool IsEncrypted { get; set; }
}

The Protos.TenantConfig is an auto-generated file based on the the following .proto file:

syntax = "proto3";

option csharp_namespace = "Kq.Protos";

package tenants;

service Tenants
{
    rpc TenantConfig (TenantConfigRequest) returns (TenantConfigResponse);
}

message TenantConfigRequest
{
    string tenantId = 1;
}

message TenantConfigResponse
{
    repeated TenantConfig TenantConfig = 1;
}

message TenantConfig
{
    string id = 1;
    string name = 2;
    string value = 3;
}

Below is the auto-generated file:

using System;
using System.CodeDom.Compiler;
using System.Diagnostics;
using Google.Protobuf;
using Google.Protobuf.Reflection;

namespace Kq.Protos;

public sealed class TenantConfig : IMessage<TenantConfig>, IMessage, IEquatable<TenantConfig>, IDeepCloneable<TenantConfig>, IBufferMessage
{
    private static readonly MessageParser<TenantConfig> _parser = new MessageParser<TenantConfig>(() => new TenantConfig());

    private UnknownFieldSet _unknownFields;

    public const int IdFieldNumber = 1;

    private string id_ = "";

    public const int NameFieldNumber = 2;

    private string name_ = "";

    public const int ValueFieldNumber = 3;

    private string value_ = "";

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public static MessageParser<TenantConfig> Parser => _parser;

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public static MessageDescriptor Descriptor => TenantsReflection.Descriptor.MessageTypes[13];

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    MessageDescriptor IMessage.Descriptor => Descriptor;

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public string Id
    {
        get
        {
            return id_;
        }
        set
        {
            id_ = ProtoPreconditions.CheckNotNull(value, "value");
        }
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public string Name
    {
        get
        {
            return name_;
        }
        set
        {
            name_ = ProtoPreconditions.CheckNotNull(value, "value");
        }
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public string Value
    {
        get
        {
            return value_;
        }
        set
        {
            value_ = ProtoPreconditions.CheckNotNull(value, "value");
        }
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public TenantConfig()
    {
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public TenantConfig(TenantConfig other)
        : this()
    {
        id_ = other.id_;
        name_ = other.name_;
        value_ = other.value_;
        _unknownFields = UnknownFieldSet.Clone(other._unknownFields);
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public TenantConfig Clone()
    {
        return new TenantConfig(this);
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public override bool Equals(object other)
    {
        return Equals(other as TenantConfig);
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public bool Equals(TenantConfig other)
    {
        if (other == null)
        {
            return false;
        }

        if (other == this)
        {
            return true;
        }

        if (Id != other.Id)
        {
            return false;
        }

        if (Name != other.Name)
        {
            return false;
        }

        if (Value != other.Value)
        {
            return false;
        }

        return object.Equals(_unknownFields, other._unknownFields);
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public override int GetHashCode()
    {
        int num = 1;
        if (Id.Length != 0)
        {
            num ^= Id.GetHashCode();
        }

        if (Name.Length != 0)
        {
            num ^= Name.GetHashCode();
        }

        if (Value.Length != 0)
        {
            num ^= Value.GetHashCode();
        }

        if (_unknownFields != null)
        {
            num ^= _unknownFields.GetHashCode();
        }

        return num;
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public override string ToString()
    {
        return JsonFormatter.ToDiagnosticString(this);
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public void WriteTo(CodedOutputStream output)
    {
        output.WriteRawMessage(this);
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    void IBufferMessage.InternalWriteTo(ref WriteContext output)
    {
        if (Id.Length != 0)
        {
            output.WriteRawTag(10);
            output.WriteString(Id);
        }

        if (Name.Length != 0)
        {
            output.WriteRawTag(18);
            output.WriteString(Name);
        }

        if (Value.Length != 0)
        {
            output.WriteRawTag(26);
            output.WriteString(Value);
        }

        if (_unknownFields != null)
        {
            _unknownFields.WriteTo(ref output);
        }
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public int CalculateSize()
    {
        int num = 0;
        if (Id.Length != 0)
        {
            num += 1 + CodedOutputStream.ComputeStringSize(Id);
        }

        if (Name.Length != 0)
        {
            num += 1 + CodedOutputStream.ComputeStringSize(Name);
        }

        if (Value.Length != 0)
        {
            num += 1 + CodedOutputStream.ComputeStringSize(Value);
        }

        if (_unknownFields != null)
        {
            num += _unknownFields.CalculateSize();
        }

        return num;
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public void MergeFrom(TenantConfig other)
    {
        if (other != null)
        {
            if (other.Id.Length != 0)
            {
                Id = other.Id;
            }

            if (other.Name.Length != 0)
            {
                Name = other.Name;
            }

            if (other.Value.Length != 0)
            {
                Value = other.Value;
            }

            _unknownFields = UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
        }
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    public void MergeFrom(CodedInputStream input)
    {
        input.ReadRawMessage(this);
    }

    [DebuggerNonUserCode]
    [GeneratedCode("protoc", null)]
    void IBufferMessage.InternalMergeFrom(ref ParseContext input)
    {
        uint num;
        while ((num = input.ReadTag()) != 0)
        {
            switch (num)
            {
                default:
                    _unknownFields = UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
                    break;
                case 10u:
                    Id = input.ReadString();
                    break;
                case 18u:
                    Name = input.ReadString();
                    break;
                case 26u:
                    Value = input.ReadString();
                    break;
            }
        }
    }
}
#if false // Decompilation log
'430' items in cache
------------------
Resolve: 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
Found single assembly: 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
Load from: 'C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.3\ref\net8.0\System.Runtime.dll'
------------------
Resolve: 'Google.Protobuf, Version=3.23.1.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604'
Found single assembly: 'Google.Protobuf, Version=3.25.0.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604'
WARN: Version mismatch. Expected: '3.23.1.0', Got: '3.25.0.0'
Load from: 'C:\Users\Dragos\.nuget\packages\google.protobuf\3.25.0\lib\net5.0\Google.Protobuf.dll'
------------------
Resolve: 'Grpc.Core.Api, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d754f35622e28bad'
Found single assembly: 'Grpc.Core.Api, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d754f35622e28bad'
Load from: 'C:\Users\Dragos\.nuget\packages\grpc.core.api\2.62.0\lib\netstandard2.1\Grpc.Core.Api.dll'
------------------
Resolve: 'System.Collections, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
Found single assembly: 'System.Collections, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
Load from: 'C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.3\ref\net8.0\System.Collections.dll'
------------------
Resolve: 'System.Memory, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'
Found single assembly: 'System.Memory, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'
Load from: 'C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.3\ref\net8.0\System.Memory.dll'
------------------
Resolve: 'System.Runtime.InteropServices, Version=3.1.0.0, Culture=neutral, PublicKeyToken=null'
Found single assembly: 'System.Runtime.InteropServices, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
WARN: Version mismatch. Expected: '3.1.0.0', Got: '8.0.0.0'
Load from: 'C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.3\ref\net8.0\System.Runtime.InteropServices.dll'
------------------
Resolve: 'System.Runtime.CompilerServices.Unsafe, Version=3.1.0.0, Culture=neutral, PublicKeyToken=null'
Found single assembly: 'System.Runtime.CompilerServices.Unsafe, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
WARN: Version mismatch. Expected: '3.1.0.0', Got: '8.0.0.0'
Load from: 'C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.3\ref\net8.0\System.Runtime.CompilerServices.Unsafe.dll'
#endif
dadhi commented 5 months ago

Just to double-check. You still have the error with FEC v4.2.0?

cda963 commented 5 months ago

Yes, I just tried upgrading a couple of hours ago.

dadhi commented 5 months ago

Ok. I will check. The thing is simple so not sure what is going on here.

What are the TargetFrameworks?

cda963 commented 5 months ago

Thanks.

<TargetFramework>net8.0</TargetFramework>

dadhi commented 5 months ago

Ok, I do see the Id field in the Proto and no IsEncrypted. How do they map? What is Mapster Config for the mapping?

cda963 commented 5 months ago

Indeed I have changed last night the POCO properties and haven't updated the proto file.. this is what 14hrs work day does to you :). I'll test again and come back if this is still an issue. I apologize for this oversight.


Converting a single object works with v4.2.0, but converting a list of objects doesn't.

    var single = new TenantConfig
    {
        Name = Guid.NewGuid().ToString(),
        Value = Guid.NewGuid().ToString(),
        IsEncrypted = true
    }.Adapt<Protos.TenantConfig>();

    var list = new List<TenantConfig>()
    {
        new() 
        {
            Name = Guid.NewGuid().ToString(),
            Value = Guid.NewGuid().ToString(),
            IsEncrypted = true
        }
    };

    var exceptionHere = list.Adapt<List<Protos.TenantConfig>>();

I can work around this, by creating a list of converted objects.

dadhi commented 5 months ago

Ok, sorry to hear about your 14h wday. Will check the new example.

dadhi commented 5 months ago

@cda963 I have added the list adapter to test and it is working fine. Check here https://github.com/dadhi/FastExpressionCompiler/commit/0f908c40e98f1e65831b4b6637a5dc0d7b314da1

Maybe I am missing some details?

cda963 commented 5 months ago

In your test I see var failure = new TenantConfig().Adapt<TenantConfigVal>(); which tests for a simple object conversion, not a list. I couldn't find the part where you test against a list of objects.

dadhi commented 5 months ago

Yep, it is here https://github.com/dadhi/FastExpressionCompiler/blob/912db98743209d5f1f2ccf688886b49a9d970d08/test/FastExpressionCompiler.IssueTests/Issue390_405_406_Mapster_tests.cs#L152

cda963 commented 5 months ago

Yes, I see it now. The issue I'm facing is in a large solution with many projects. Let me try this in an empty project, and I'll get back to you. Thanks.