TypeRules.cs defines conversion and promotion rules for shader data types. It classifies conversions (Identity, Splat, Widen, IntToFloat, Pad, Truncate, Illegal), computes severity and cost, describes swizzles and default fill values for padding, and provides promotion/unification logic for scalar, vector and matrix ShaderType values.
namespace Editor.Prism.Core;
/// <summary>
/// How one <see cref="ShaderType"/> reaches another. Exactly one kind is reported per conversion —
/// the most severe one, using the ordering in <see cref="TypeRules.Severity"/>.
/// </summary>
public enum ConversionKind
{
/// <summary>The types are identical. Free, silent.</summary>
Identity,
/// <summary>Scalar broadcast to a vector, e.g. <c>float</c> -> <c>float3</c>. Free, silent.</summary>
Splat,
/// <summary>Lossless component widening, e.g. <c>int</c> -> <c>uint</c> or <c>half</c> -> <c>float</c>. Free, silent.</summary>
Widen,
/// <summary>Integral to floating point, e.g. <c>int</c> -> <c>float</c>. Free, silent.</summary>
IntToFloat,
/// <summary>
/// Widening with a fill, e.g. <c>float2</c> -> <c>float3</c>. Allowed with a warning; the fill
/// value is visible on the wire and overridable per edge. See <see cref="TypeRules.DefaultFill"/>.
/// </summary>
Pad,
/// <summary>
/// Lossy narrowing: vector truncation (<c>float4</c> -> <c>float2</c>, emitted as <c>.xy</c>),
/// or a lossy component conversion such as <c>float</c> -> <c>int</c>. Allowed with a warning.
/// </summary>
Truncate,
/// <summary>No implicit conversion exists. Rejected at drop time.</summary>
Illegal
}
/// <summary>
/// The implicit-conversion lattice, promotion rules and cost model. Pure functions over
/// <see cref="ShaderType"/>; the graph-wide unification pass in the compiler builds on these.
/// </summary>
public static class TypeRules
{
// ---- type-variable spellings -----------------------------------------
/// <summary>Any numeric type; unifies across every port on a node sharing the name.</summary>
public const string TypeVarT = "T";
/// <summary>The scalar component of <c>T</c> (<c>float3</c> gives <c>float</c>).</summary>
public const string TypeVarScalar = "T.scalar";
/// <summary>Any float vector of width 1..4; the width unifies.</summary>
public const string TypeVarVecN = "vecN";
/// <summary>Opts out of inference entirely (reroute, custom-code passthrough).</summary>
public const string TypeVarAny = "any";
/// <summary>
/// True when a declared port type is a type variable rather than a concrete spelling.
/// Recognises <c>T</c>, <c>T.scalar</c>, <c>vecN</c>, <c>any</c> and the <c>float{N}</c> family.
/// </summary>
public static bool IsTypeVariable( string declaredType )
{
if ( string.IsNullOrWhiteSpace( declaredType ) ) return true;
var s = declaredType.Trim();
if ( s == TypeVarAny || s == TypeVarVecN ) return true;
if ( s == TypeVarT || s.StartsWith( TypeVarT + ".", StringComparison.Ordinal ) ) return true;
if ( s.Contains( '{' ) && s.Contains( '}' ) ) return true;
return !ShaderType.TryParse( s, out _ );
}
// ---- conversion -------------------------------------------------------
/// <summary>
/// Can a value of type <paramref name="from"/> feed a port of type <paramref name="to"/>?
/// Returns false only for <see cref="ConversionKind.Illegal"/>; lossy conversions return true
/// and are expected to raise a warning at the call site.
/// </summary>
public static bool CanConvert( ShaderType from, ShaderType to, out ConversionKind kind )
{
kind = Classify( from, to );
return kind != ConversionKind.Illegal;
}
/// <summary>Classify the conversion from one type to another without the boolean shorthand.</summary>
public static ConversionKind Classify( ShaderType from, ShaderType to )
{
if ( from == to ) return ConversionKind.Identity;
// Nothing converts to or from void, and opaque types only match exactly.
if ( from.IsVoid || to.IsVoid ) return ConversionKind.Illegal;
if ( from.IsObject || to.IsObject ) return ConversionKind.Illegal;
if ( from.IsStruct || to.IsStruct ) return ConversionKind.Illegal;
// Matrices never implicitly reshape, and never convert to or from a vector without a node.
if ( from.IsMatrix != to.IsMatrix ) return ConversionKind.Illegal;
if ( from.IsMatrix )
{
if ( from.Rows != to.Rows || from.Cols != to.Cols ) return ConversionKind.Illegal;
return ScalarConversion( from.Scalar, to.Scalar );
}
var shape = from.Components == to.Components
? ConversionKind.Identity
: from.Components == 1
? ConversionKind.Splat
: from.Components > to.Components
? ConversionKind.Truncate
: ConversionKind.Pad;
var scalar = ScalarConversion( from.Scalar, to.Scalar );
if ( scalar == ConversionKind.Illegal ) return ConversionKind.Illegal;
// Report the interesting half: a shape change beats a no-op component change and vice versa,
// and when both are interesting the more severe one wins.
if ( shape == ConversionKind.Identity ) return scalar;
if ( scalar == ConversionKind.Identity ) return shape;
return Severity( shape ) >= Severity( scalar ) ? shape : scalar;
}
/// <summary>Classify a component-kind change on its own.</summary>
public static ConversionKind ScalarConversion( ScalarKind from, ScalarKind to )
{
if ( from == to ) return ConversionKind.Identity;
if ( from == ScalarKind.Void || to == ScalarKind.Void ) return ConversionKind.Illegal;
var fromFloat = from is ScalarKind.Half or ScalarKind.Float or ScalarKind.Double;
var toFloat = to is ScalarKind.Half or ScalarKind.Float or ScalarKind.Double;
// bool -> anything is a widening; float-ish targets go through the int-to-float rung.
if ( from == ScalarKind.Bool ) return toFloat ? ConversionKind.IntToFloat : ConversionKind.Widen;
// anything -> bool is a "is it non-zero" test: lossy.
if ( to == ScalarKind.Bool ) return ConversionKind.Truncate;
if ( !fromFloat && toFloat ) return ConversionKind.IntToFloat;
if ( fromFloat && !toFloat ) return ConversionKind.Truncate;
// int <-> uint is a reinterpretation of the sign bit; treated as a free widening.
if ( !fromFloat && !toFloat ) return ConversionKind.Widen;
return Rank( to ) >= Rank( from ) ? ConversionKind.Widen : ConversionKind.Truncate;
}
/// <summary>
/// Severity ordering used to pick which conversion kind to report and how loudly to complain.
/// 0 = free and silent, 2 = allowed with a warning, 3 = rejected.
/// </summary>
public static int Severity( ConversionKind kind ) => kind switch
{
ConversionKind.Identity => 0,
ConversionKind.Splat => 0,
ConversionKind.Widen => 0,
ConversionKind.IntToFloat => 0,
ConversionKind.Pad => 2,
ConversionKind.Truncate => 2,
_ => 3
};
/// <summary>True when the conversion is free and needs no user-visible marker.</summary>
public static bool IsFree( ConversionKind kind ) => Severity( kind ) == 0;
/// <summary>True when the conversion loses or invents information and must be marked on the wire.</summary>
public static bool IsLossy( ConversionKind kind ) => Severity( kind ) == 2;
/// <summary>Relative cost of a conversion, used to rank overloads and promotion candidates.</summary>
public static int Cost( ConversionKind kind ) => kind switch
{
ConversionKind.Identity => 0,
ConversionKind.Splat => 1,
ConversionKind.Widen => 2,
ConversionKind.IntToFloat => 3,
ConversionKind.Pad => 8,
ConversionKind.Truncate => 10,
_ => int.MaxValue
};
/// <summary>
/// The value Prism writes into components invented by a <see cref="ConversionKind.Pad"/>.
/// Defaults to 0, except the fourth component of a 4-component target, which defaults to 1 —
/// that is the alpha/w convention every artist expects and the built-in editor silently gets wrong.
/// The user may override this per edge.
/// </summary>
public static float DefaultFill( ShaderType from, ShaderType to, int componentIndex )
{
if ( componentIndex == 3 && to.Components == 4 && from.Components == 3 ) return 1f;
return 0f;
}
/// <summary>Human-readable description of a conversion for wire tooltips, e.g. <c>float4 -> float2 (.xy)</c>.</summary>
public static string Describe( ShaderType from, ShaderType to, ConversionKind kind ) => kind switch
{
ConversionKind.Identity => $"{from.Hlsl}",
ConversionKind.Splat => $"{from.Hlsl} -> {to.Hlsl} (splat)",
ConversionKind.Widen => $"{from.Hlsl} -> {to.Hlsl}",
ConversionKind.IntToFloat => $"{from.Hlsl} -> {to.Hlsl}",
ConversionKind.Pad => $"{from.Hlsl} -> {to.Hlsl} (padded)",
ConversionKind.Truncate => $"{from.Hlsl} -> {to.Hlsl} ({SwizzleFor( to.Components )})",
_ => $"{from.Hlsl} -> {to.Hlsl} (not allowed)"
};
/// <summary>The swizzle mask that takes the first <paramref name="components"/> channels.</summary>
public static string SwizzleFor( int components ) => components switch
{
1 => ".x",
2 => ".xy",
3 => ".xyz",
_ => ".xyzw"
};
// ---- promotion / unification -----------------------------------------
/// <summary>
/// The type both operands of a binary operation should be coerced to: the wider component kind
/// and the wider component count. A scalar operand always adopts the other operand's width.
/// Returns <see cref="ShaderType.Void"/> when no common type exists.
/// </summary>
public static ShaderType Promote( ShaderType a, ShaderType b )
{
if ( a == b ) return a;
if ( a.IsVoid ) return b;
if ( b.IsVoid ) return a;
if ( a.IsObject || b.IsObject || a.IsStruct || b.IsStruct ) return ShaderType.Void;
if ( a.IsMatrix || b.IsMatrix )
{
if ( !a.IsMatrix || !b.IsMatrix ) return ShaderType.Void;
if ( a.Rows != b.Rows || a.Cols != b.Cols ) return ShaderType.Void;
return ShaderType.Mat( PromoteScalar( a.Scalar, b.Scalar ), a.Rows, a.Cols );
}
var components = Math.Max( a.Components, b.Components );
return ShaderType.Vec( PromoteScalar( a.Scalar, b.Scalar ), components );
}
/// <summary>Promote two types, reporting whether a common type exists.</summary>
public static bool TryPromote( ShaderType a, ShaderType b, out ShaderType result )
{
result = Promote( a, b );
return !result.IsVoid;
}
/// <summary>The wider of two component kinds.</summary>
public static ScalarKind PromoteScalar( ScalarKind a, ScalarKind b )
{
if ( a == b ) return a;
if ( a == ScalarKind.Void ) return b;
if ( b == ScalarKind.Void ) return a;
// int + uint is ambiguous in HLSL; Prism resolves to int so negative literals survive.
if ( ( a == ScalarKind.Int && b == ScalarKind.UInt ) || ( a == ScalarKind.UInt && b == ScalarKind.Int ) )
return ScalarKind.Int;
return Rank( a ) >= Rank( b ) ? a : b;
}
/// <summary>
/// Unify two already-resolved types: identical types unify to themselves, otherwise the result is
/// their promotion. This is the concrete-type half of the solver; type variables are handled by
/// the compiler's unification pass, which calls into here once both sides are concrete.
/// </summary>
public static bool Unify( ShaderType a, ShaderType b, out ShaderType result )
{
if ( a == b )
{
result = a;
return true;
}
result = Promote( a, b );
return !result.IsVoid;
}
static int Rank( ScalarKind kind ) => kind switch
{
ScalarKind.Bool => 0,
ScalarKind.Int => 1,
ScalarKind.UInt => 1,
ScalarKind.Half => 2,
ScalarKind.Float => 3,
ScalarKind.Double => 4,
_ => -1
};
}