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
};
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.