0

I'm creating a real-time interpreted scripting service with a roslyn and C# back-end and need to determine if any given primitive is specifically implicitly castable to any other given primitive.

I search through the IL and SO and found a couple posts such as this one covering workarounds.

How does rosyln / C# determine if any given primitive is implicitly castable to any other primitive. The IL would lead me to believe it simply uses IConvertible wrapped into different casting functions, but I feel this would be ridiculously slow given that their implementation of Convert.ChangeType throws exceptions.

I've implemented my own version to check implicit casting(seen below) between primitives, But I feel like I may be over-complicating it and there exists some way to check for only implicit conversion between two primitives.

public static bool IsImplicitlyCastable(object Instance, Type DesiredType)
{
    // for convenience assume null can't be casted to non Nullable<T>
    if (Instance is null)
    {
        return false;
    }

    // cast the typecode of the instance to int
    int instanceTypeCode = (int)Type.GetTypeCode(Instance.GetType());

    // cast the typecode of the desired type to int
    int desiredTypeCode = (int)Type.GetTypeCode(DesiredType);

    // convert system typecode to BinaryTypeCode
    int desiredBinaryCode = 1 << (desiredTypeCode - 1);

    // determine if the instance is implicitly castable to the desired type, this was found to be 20% faster than a switch statement with constant integers
    return (desiredBinaryCode & Conversions[instanceTypeCode]) != 0;

}

public static readonly int[] Conversions = {
    0,
    0,
    0,
    4,
    32640,
    30032,
    32736,
    30016,
    32640,
    29952,
    32256,
    29696,
    30720,
    12288,
    8192,
    16384,
    0,
    0,
    0
};
DekuDesu
  • 2,224
  • 1
  • 5
  • 19
  • Have you considered using reflection to determine if one or other of the types has an implicit conversion operator for the other? https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators – Martin Costello Jun 06 '21 at 15:12
  • 1
    As far as the language goes, this is covered under [implicit conversions](https://learn.microsoft.com/dotnet/csharp/language-reference/language-specification/conversions#implicit-conversions), which include user-specified ones. `IConvertible` is never involved. The rules are dictated by the language and not the runtime (there is obviously heavy overlap between the systems, but they are not identical), which is why there is no easy way to do exactly what the C# compiler does using only IL. – Jeroen Mostert Jun 06 '21 at 15:12
  • @MartinCostello, primitive types that implement `IConvertible` do not possess a `op_Implicit` method, but the source I found for that is from 2015, I will look into however – DekuDesu Jun 06 '21 at 15:15
  • The notion of "primitive type" is also not shared between the runtime and C#, incidentally -- C# considers code like `int i = 1; decimal d = i;` to involve an implicit numeric conversion defined by the language, but on the runtime level, `Decimal` is not a special type and this works only by invoking the implicit conversion operator defined on it. This is very different from (say) `long l = i`, as that uses a `conv.i8` opcode implemented by the runtime (or just a direct load/store with an implicit conversion, depending on optimization). – Jeroen Mostert Jun 06 '21 at 15:20
  • @JeroenMostert, how does the compiler verify that two variables that are pushed to the stack(`ldc.i4.n`) are implictly convertible using `conv.(u)in` before actually taking it to runtime? Does it just compile it to raw IL with an assumed `conv.i`, execute it in the background, and if it throws and over/underflow it's not implicitly castable? – DekuDesu Jun 06 '21 at 15:31
  • No, of course not -- the compiler knows the exact rules C# uses for implicit conversion, it doesn't need to rely on the runtime. It also, by definition, know what's on the stack because the compiler is the one who put the values there. I wouldn't know where/how in the source the rules are encoded, though (nor is this code likely to be easily extractable to other projects). – Jeroen Mostert Jun 06 '21 at 16:08

1 Answers1

0

How does rosyln / C# determine if any given primitive is implicitly castable to any other primitive.

The Roslyn C# compiler uses multi-dimensional array of booleans to parse implicit and explicit unmanaged (built in) conversions.

As of the time of writing the current implementation is defined by the semantic binder – ConversionBase class which specifically handles the semantics around conversions between user defined and unmanaged (built in) types during compile time.

Per Binder/Semantics/Conversions/ConversionsBase.cs

// Licensed to the .NET Foundation

{ ... }

// Notice that there is no implicit numeric conversion from a type to itself. That's an
// identity conversion.
private static readonly bool[,] s_implicitNumericConversions =
{
            // to     sb  b  s  us i ui  l ul  c  f  d  m
            // from
            /* sb */
         { F, F, T, F, T, F, T, F, F, T, T, T },
            /*  b */
         { F, F, T, T, T, T, T, T, F, T, T, T },
            /*  s */
         { F, F, F, F, T, F, T, F, F, T, T, T },
            /* us */
         { F, F, F, F, T, T, T, T, F, T, T, T },
            /*  i */
         { F, F, F, F, F, F, T, F, F, T, T, T },
            /* ui */
         { F, F, F, F, F, F, T, T, F, T, T, T },
            /*  l */
         { F, F, F, F, F, F, F, F, F, T, T, T },
            /* ul */
         { F, F, F, F, F, F, F, F, F, T, T, T },
            /*  c */
         { F, F, F, T, T, T, T, T, F, T, T, T },
            /*  f */
         { F, F, F, F, F, F, F, F, F, F, T, F },
            /*  d */
         { F, F, F, F, F, F, F, F, F, F, F, F },
            /*  m */
         { F, F, F, F, F, F, F, F, F, F, F, F }
        };

private static readonly bool[,] s_explicitNumericConversions =
{
            // to     sb  b  s us  i ui  l ul  c  f  d  m
            // from
            /* sb */
         { F, T, F, T, F, T, F, T, T, F, F, F },
            /*  b */
         { T, F, F, F, F, F, F, F, T, F, F, F },
            /*  s */
         { T, T, F, T, F, T, F, T, T, F, F, F },
            /* us */
         { T, T, T, F, F, F, F, F, T, F, F, F },
            /*  i */
         { T, T, T, T, F, T, F, T, T, F, F, F },
            /* ui */
         { T, T, T, T, T, F, F, F, T, F, F, F },
            /*  l */
         { T, T, T, T, T, T, F, T, T, F, F, F },
            /* ul */
         { T, T, T, T, T, T, T, F, T, F, F, F },
            /*  c */
         { T, T, T, F, F, F, F, F, F, F, F, F },
            /*  f */
         { T, T, T, T, T, T, T, T, T, F, F, T },
            /*  d */
         { T, T, T, T, T, T, T, T, T, T, F, T },
            /*  m */
         { T, T, T, T, T, T, T, T, T, T, T, F }
        };

private static int GetNumericTypeIndex(SpecialType specialType)
{
    switch (specialType)
    {
        case SpecialType.System_SByte: return 0;
        case SpecialType.System_Byte: return 1;
        case SpecialType.System_Int16: return 2;
        case SpecialType.System_UInt16: return 3;
        case SpecialType.System_Int32: return 4;
        case SpecialType.System_UInt32: return 5;
        case SpecialType.System_Int64: return 6;
        case SpecialType.System_UInt64: return 7;
        case SpecialType.System_Char: return 8;
        case SpecialType.System_Single: return 9;
        case SpecialType.System_Double: return 10;
        case SpecialType.System_Decimal: return 11;
        default: return -1;
    }
}

#nullable enable
private static bool HasImplicitNumericConversion(TypeSymbol source, TypeSymbol destination)
{
    Debug.Assert((object)source != null);
    Debug.Assert((object)destination != null);

    int sourceIndex = GetNumericTypeIndex(source.SpecialType);
    if (sourceIndex < 0)
    {
        return false;
    }

    int destinationIndex = GetNumericTypeIndex(destination.SpecialType);
    if (destinationIndex < 0)
    {
        return false;
    }

    return s_implicitNumericConversions[sourceIndex, destinationIndex];
}

private static bool HasExplicitNumericConversion(TypeSymbol source, TypeSymbol destination)
{
    // SPEC: The explicit numeric conversions are the conversions from a numeric-type to another 
    // SPEC: numeric-type for which an implicit numeric conversion does not already exist.
    Debug.Assert((object)source != null);
    Debug.Assert((object)destination != null);

    int sourceIndex = GetNumericTypeIndex(source.SpecialType);
    if (sourceIndex < 0)
    {
        return false;
    }

    int destinationIndex = GetNumericTypeIndex(destination.SpecialType);
    if (destinationIndex < 0)
    {
        return false;
    }

    return s_explicitNumericConversions[sourceIndex, destinationIndex];
}

The important thing to note, from what I was able to derive from the source, was that this uses TypeSymbol which is the abstract representation of a source code Type in a particular member or expression body. These do not represent instances of an object and as such can not be used during run time without significant abuse of the ConversionsBase class.

With this information we can determine my original workaround was a very close approximation to the compilers choice in determining implicit conversion availability at compile time.

Unfortunately this implementation in the compiler has little implications for runtime use, at least in any meaningful way.

It's best to continue to use reflection, table lookups, or binary mathematics to lookup these conversions at runtime, in my opinion from what I was able to research.

DekuDesu
  • 2,224
  • 1
  • 5
  • 19