Editor/Prism/Compiler/IrConversions.cs

Utility class that lowers implicit type conversions into IR values for the Prism shader compiler in the Editor. It classifies conversions (identity, splat, widen, pad, truncate, int-to-float) and emits appropriate IR builder operations (Cast, Swizzle) or returns Invalid when not convertible.

Native Interop
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;

namespace Editor.Prism.Compiler;

/// <summary>
/// The one lowering of an implicit conversion into IR.
/// <para>
/// Two callers need it and used to spell it differently. <c>NodeEmitContext.Coerce</c> lowered a
/// narrowing as a swizzle plus an optional convert; <c>GraphCompiler.Fit</c> lowered the identical
/// conversion as a single <c>CastKind.Truncate</c>, which the HLSL backend only renders as a mask when
/// the scalar kinds already agree and otherwise falls back to a C-style <c>( float3 )v</c>. Both are
/// legal, but two structurally different expressions for one conversion never hash-cons against each
/// other, so CSE missed and the generated text differed between two paths for no reason.
/// </para>
/// <para>
/// This class holds no diagnostics on purpose: reporting a lossy or illegal conversion is the caller's
/// job, because only the caller knows which port to attach it to. An unclassifiable conversion comes
/// back as <see cref="IrValue.Invalid"/>.
/// </para>
/// </summary>
public static class IrConversions
{
	/// <summary>
	/// Emit the conversion of <paramref name="value"/> to <paramref name="target"/>, or
	/// <see cref="IrValue.Invalid"/> when the two types cannot be converted at all.
	/// </summary>
	/// <param name="builder">The builder to emit into.</param>
	/// <param name="value">The value being converted.</param>
	/// <param name="target">The type wanted.</param>
	/// <param name="fill">What to pad a widening with, or null for <c>TypeRules.DefaultFill</c>.</param>
	public static IrValue Emit( IrBuilder builder, IrValue value, ShaderType target, float? fill = null )
	{
		if ( builder is null || !value.IsValid ) return IrValue.Invalid;
		if ( target.IsVoid || value.Type == target ) return value;

		var from = value.Type;

		switch ( TypeRules.Classify( from, target ) )
		{
			case ConversionKind.Identity:
				return value;

			case ConversionKind.Splat:
				return builder.Cast( target, value, CastKind.Splat );

			case ConversionKind.Widen:
			case ConversionKind.IntToFloat:
				return builder.Cast( target, value, CastKind.Convert );

			case ConversionKind.Pad:
			{
				var actual = fill ?? TypeRules.DefaultFill( from, target, target.Components - 1 );

				// Convert the components before widening, so the pad literal and the existing lanes are
				// already the same scalar kind by the time the constructor is printed.
				var widened = from.Scalar == target.Scalar
					? value
					: builder.Cast( from.WithScalar( target.Scalar ), value, CastKind.Convert );

				return builder.Cast( target, widened, CastKind.Pad, actual );
			}

			case ConversionKind.Truncate:
			{
				var narrowed = value;

				if ( from.IsScalarOrVector && target.IsScalarOrVector && from.Components > target.Components )
				{
					var mask = "xyzw"[..Math.Clamp( target.Components, 1, 4 )];
					narrowed = builder.Swizzle( ShaderType.Vec( from.Scalar, target.Components ), value, mask );
				}

				if ( narrowed.Type == target ) return narrowed;

				return builder.Cast( target, narrowed, CastKind.Convert );
			}

			default:
				return IrValue.Invalid;
		}
	}
}