Editor/Prism/Toolchain/SlangReflectionReader.cs

Editor tool that parses Slang compiler reflection JSON into in-memory types. It defines data records for counts, attributes, bindings, types, parameters and entry points, and contains a reader that parses JsonNode output from slangc -reflection-json, tolerant of sentinel strings and single-vs-plural binding forms.

File AccessNetworking
using Editor.Prism.Core;

using System.ComponentModel;

namespace Editor.Prism.Toolchain;

/// <summary>Whether a reflected count is a real number, one of Slang's sentinels, or simply absent.</summary>
public enum SlangCountKind
{
	/// <summary>The key was not present.</summary>
	Absent,

	/// <summary>A real number.</summary>
	Known,

	/// <summary>Slang wrote the string <c>"unbounded"</c> where a number was expected.</summary>
	Unbounded,

	/// <summary>Slang wrote the string <c>"unknown"</c> where a number was expected.</summary>
	Unknown
}

/// <summary>
/// A number in the reflection JSON, which Slang is entitled to write as the string
/// <c>"unbounded"</c> or <c>"unknown"</c> instead. Every numeric field of the schema goes through this
/// so a bindless array cannot make the whole document fail to parse.
/// </summary>
public readonly record struct SlangCount( int Value, SlangCountKind Kind )
{
	/// <summary>The absent value, and the default.</summary>
	public static readonly SlangCount Absent = new( 0, SlangCountKind.Absent );

	/// <summary>True when this is a real number.</summary>
	public bool IsKnown => Kind == SlangCountKind.Known;

	/// <summary>True when Slang reported an unbounded size.</summary>
	public bool IsUnbounded => Kind == SlangCountKind.Unbounded;

	/// <summary>True when the key was present with a value we could not interpret.</summary>
	public bool IsUnknown => Kind == SlangCountKind.Unknown;

	/// <summary>True when the key was not present at all.</summary>
	public bool IsAbsent => Kind == SlangCountKind.Absent;

	/// <summary>The number, or a caller-chosen fallback when it is not one.</summary>
	public int Or( int fallback ) => IsKnown ? Value : fallback;

	/// <inheritdoc/>
	public override string ToString() => Kind switch
	{
		SlangCountKind.Known => Value.ToString(),
		SlangCountKind.Unbounded => "unbounded",
		SlangCountKind.Unknown => "unknown",
		_ => "-"
	};
}

/// <summary>
/// A user-defined attribute reflected off a parameter — the mechanism that carries material-UI metadata
/// out of a Slang module. <c>[UIRange(0,1)] uniform float g_Rough</c> arrives here as
/// <c>UIRange</c> with two numeric arguments.
/// </summary>
public sealed record SlangUserAttribute( string Name, IReadOnlyList<object> Arguments )
{
	/// <summary>An argument as a float.</summary>
	public float Float( int index, float fallback = 0f ) => Arguments is not null && index >= 0 &&
		index < Arguments.Count && Arguments[index] is IConvertible c
		? PrismLog.Guard( "Reading a Slang attribute argument",
			() => Convert.ToSingle( c, System.Globalization.CultureInfo.InvariantCulture ), fallback )
		: fallback;

	/// <summary>An argument as an int.</summary>
	public int Int( int index, int fallback = 0 ) => (int)Float( index, fallback );

	/// <summary>An argument as a string.</summary>
	public string String( int index, string fallback = null ) =>
		Arguments is not null && index >= 0 && index < Arguments.Count ? Arguments[index]?.ToString() : fallback;

	/// <inheritdoc/>
	public override string ToString() =>
		$"[{Name}({string.Join( ", ", Arguments ?? Array.Empty<object>() )})]";
}

/// <summary>One layout binding of a reflected parameter.</summary>
public sealed record SlangBinding( string Kind )
{
	/// <summary>Byte offset, for <c>uniform</c> bindings.</summary>
	public SlangCount Offset { get; init; }

	/// <summary>Byte size, for <c>uniform</c> bindings.</summary>
	public SlangCount Size { get; init; }

	/// <summary>Array element stride, for <c>uniform</c> bindings.</summary>
	public SlangCount ElementStride { get; init; }

	/// <summary>Register space.</summary>
	public SlangCount Space { get; init; }

	/// <summary>Register index.</summary>
	public SlangCount Index { get; init; }

	/// <summary>How many consecutive registers are taken.</summary>
	public SlangCount Count { get; init; }

	/// <summary>Whether the compiled kernel actually reads this. Only present inside an entry point.</summary>
	public bool? Used { get; init; }

	/// <inheritdoc/>
	public override string ToString() => Kind == "uniform"
		? $"uniform +{Offset} ({Size} bytes)"
		: $"{Kind} space {Space} index {Index}";
}

/// <summary>A reflected type or type layout, discriminated by <see cref="Kind"/>.</summary>
public sealed record SlangTypeInfo( string Kind )
{
	/// <summary>Struct, interface or generic-parameter name.</summary>
	public string Name { get; init; }

	/// <summary>Scalar type spelling, e.g. <c>float32</c>.</summary>
	public string ScalarType { get; init; }

	/// <summary>Vector width or array length.</summary>
	public SlangCount ElementCount { get; init; }

	/// <summary>Matrix rows.</summary>
	public SlangCount RowCount { get; init; }

	/// <summary>Matrix columns.</summary>
	public SlangCount ColumnCount { get; init; }

	/// <summary>Element type of a vector, matrix, array, buffer or parameter block.</summary>
	[Hide, Browsable( false )]
	public SlangTypeInfo ElementType { get; init; }

	/// <summary>Result type of a resource.</summary>
	[Hide, Browsable( false )]
	public SlangTypeInfo ResultType { get; init; }

	/// <summary>Resource shape, e.g. <c>texture2D</c>.</summary>
	public string BaseShape { get; init; }

	/// <summary>Resource access, absent when read-only.</summary>
	public string Access { get; init; }

	/// <summary>True when the resource is an array.</summary>
	public bool IsArray { get; init; }

	/// <summary>True when the resource is multisampled.</summary>
	public bool IsMultisample { get; init; }

	/// <summary>Struct fields.</summary>
	public IReadOnlyList<SlangReflectionParameter> Fields { get; init; } = Array.Empty<SlangReflectionParameter>();

	/// <summary>Attributes declared on the type.</summary>
	public IReadOnlyList<SlangUserAttribute> UserAttributes { get; init; } = Array.Empty<SlangUserAttribute>();

	/// <summary>
	/// Translate into Prism's type lattice. Returns false for anything with no Prism equivalent — a
	/// pointer, a dynamic resource, an unresolved generic — rather than guessing.
	/// </summary>
	public bool TryToShaderType( out ShaderType type )
	{
		type = ShaderType.Void;

		switch ( Kind )
		{
			case "scalar":
				type = ShaderType.Vec( Scalar( ScalarType ), 1 );
				return !type.IsVoid;

			case "vector":
				type = ShaderType.Vec( Scalar( ElementType?.ScalarType ), ElementCount.Or( 1 ) );
				return !type.IsVoid;

			case "matrix":
				type = ShaderType.Mat( Scalar( ElementType?.ScalarType ), RowCount.Or( 1 ), ColumnCount.Or( 1 ) );
				return !type.IsVoid;

			case "struct":
				if ( string.IsNullOrWhiteSpace( Name ) ) return false;

				type = ShaderType.Struct( Name );
				return true;

			case "samplerState":
				type = ShaderType.Sampler;
				return true;

			case "resource":
				var kind = ResourceKind();

				if ( kind == ObjectKind.None ) return false;

				type = ShaderType.Obj( kind );
				return true;

			case "constantBuffer":
			case "parameterBlock":
			case "textureBuffer":
			case "array":
				return ElementType is not null && ElementType.TryToShaderType( out type );

			default:
				return false;
		}
	}

	/// <summary>The Prism type, or <c>void</c> when there is no equivalent.</summary>
	public ShaderType ToShaderType() => TryToShaderType( out var type ) ? type : ShaderType.Void;

	ObjectKind ResourceKind()
	{
		var writable = Access is "write" or "readWrite" or "rasterOrdered" or "append" or "consume";

		return BaseShape switch
		{
			"texture1D" => IsArray ? ObjectKind.Texture1DArray : ObjectKind.Texture1D,
			"texture2D" when IsMultisample => ObjectKind.Texture2DMS,
			"texture2D" when writable => ObjectKind.RWTexture2D,
			"texture2D" => IsArray ? ObjectKind.Texture2DArray : ObjectKind.Texture2D,
			"texture3D" => writable ? ObjectKind.RWTexture3D : ObjectKind.Texture3D,
			"textureCube" => IsArray ? ObjectKind.TextureCubeArray : ObjectKind.TextureCube,
			"structuredBuffer" => writable ? ObjectKind.RWStructuredBuffer : ObjectKind.StructuredBuffer,
			"byteAddressBuffer" => writable ? ObjectKind.RWByteAddressBuffer : ObjectKind.ByteAddressBuffer,
			"textureBuffer" => writable ? ObjectKind.RWBuffer : ObjectKind.Buffer,
			_ => ObjectKind.None
		};
	}

	static ScalarKind Scalar( string name ) => name switch
	{
		"bool" => ScalarKind.Bool,
		"int8" or "int16" or "int32" or "int64" or "intptr" => ScalarKind.Int,
		"uint8" or "uint16" or "uint32" or "uint64" or "uintptr" => ScalarKind.UInt,
		"float16" or "bfloat16" or "float_e4m3" or "float_e5m2" => ScalarKind.Half,
		"float32" => ScalarKind.Float,
		"float64" => ScalarKind.Double,
		_ => ScalarKind.Void
	};

	/// <inheritdoc/>
	public override string ToString()
	{
		var type = ToShaderType();

		return type.IsVoid ? Kind + ( Name is null ? string.Empty : $" {Name}" ) : type.ToString();
	}
}

/// <summary>One reflected parameter — global, struct field or entry-point argument.</summary>
public sealed record SlangReflectionParameter( string Name )
{
	/// <summary>The stage this parameter belongs to, when it is stage-specific.</summary>
	public string Stage { get; init; }

	/// <summary>The single binding, when the parameter has exactly one layout category.</summary>
	public SlangBinding Binding => Bindings is { Count: > 0 } ? Bindings[0] : null;

	/// <summary>Every binding. Slang writes <c>binding</c> for one and <c>bindings</c> for several.</summary>
	public IReadOnlyList<SlangBinding> Bindings { get; init; } = Array.Empty<SlangBinding>();

	/// <summary>The parameter's type layout.</summary>
	public SlangTypeInfo Type { get; init; }

	/// <summary>User-defined attributes, which is where material-UI metadata lives.</summary>
	public IReadOnlyList<SlangUserAttribute> UserAttributes { get; init; } = Array.Empty<SlangUserAttribute>();

	/// <summary>Semantic name, for varying parameters. Slang upper-cases it.</summary>
	public string SemanticName { get; init; }

	/// <summary>Semantic index, present only when non-zero.</summary>
	public int SemanticIndex { get; init; }

	/// <summary>True when the <c>Shared</c> modifier was present.</summary>
	public bool Shared { get; init; }

	/// <summary>Image format name, for writable textures that declared one.</summary>
	public string Format { get; init; }

	/// <summary>The attribute with a given name, or null.</summary>
	public SlangUserAttribute Attribute( string name ) => UserAttributes?
		.FirstOrDefault( x => string.Equals( x.Name, name, StringComparison.OrdinalIgnoreCase ) );

	/// <inheritdoc/>
	public override string ToString() => $"{Type?.ToString() ?? "?"} {Name}";
}

/// <summary>One reflected entry point.</summary>
public sealed record SlangReflectionEntryPoint( string Name, string Stage )
{
	/// <summary>Declared parameters.</summary>
	public IReadOnlyList<SlangReflectionParameter> Parameters { get; init; } = Array.Empty<SlangReflectionParameter>();

	/// <summary>Bindings the compiled kernel resolved, each carrying a <c>used</c> flag.</summary>
	public IReadOnlyList<SlangReflectionParameter> Bindings { get; init; } = Array.Empty<SlangReflectionParameter>();

	/// <summary>The result parameter, when the entry point has one.</summary>
	public SlangReflectionParameter Result { get; init; }

	/// <summary>Thread group size, for compute entry points.</summary>
	public IReadOnlyList<int> ThreadGroupSize { get; init; } = Array.Empty<int>();

	/// <summary>Attributes declared on the entry point.</summary>
	public IReadOnlyList<SlangUserAttribute> UserAttributes { get; init; } = Array.Empty<SlangUserAttribute>();

	/// <summary>True when any input is sample-rate.</summary>
	public bool UsesAnySampleRateInput { get; init; }

	/// <summary>Translate the stage name into Prism's enum. Slang says <c>fragment</c>, we say Pixel.</summary>
	public ShaderStage ToStage() => Stage switch
	{
		"vertex" => ShaderStage.Vertex,
		"fragment" or "pixel" => ShaderStage.Pixel,
		"geometry" => ShaderStage.Geometry,
		"compute" => ShaderStage.Compute,
		_ => ShaderStage.None
	};

	/// <inheritdoc/>
	public override string ToString() => $"{Name} ({Stage})";
}

/// <summary>A parameter the graph says the module should expose, for <see cref="SlangReflectionReader.CrossCheck"/>.</summary>
public readonly record struct SlangExpectedParameter( string Name, ShaderType Type )
{
	/// <inheritdoc/>
	public override string ToString() => $"{Type.Slang} {Name}";
}

/// <summary>A parsed <c>-reflection-json</c> document.</summary>
public sealed record SlangReflection
{
	/// <summary>An empty document. Returned instead of null whenever parsing gives up.</summary>
	public static readonly SlangReflection Empty = new();

	/// <summary>Global parameters.</summary>
	public IReadOnlyList<SlangReflectionParameter> Parameters { get; init; } = Array.Empty<SlangReflectionParameter>();

	/// <summary>Entry points.</summary>
	public IReadOnlyList<SlangReflectionEntryPoint> EntryPoints { get; init; } = Array.Empty<SlangReflectionEntryPoint>();

	/// <summary>String hashes, when the module used them.</summary>
	public IReadOnlyDictionary<string, long> HashedStrings { get; init; } = new Dictionary<string, long>();

	/// <summary>Bindless space index, when present.</summary>
	public SlangCount BindlessSpaceIndex { get; init; }

	/// <summary>True when the document described nothing at all.</summary>
	public bool IsEmpty => Parameters.Count == 0 && EntryPoints.Count == 0;

	/// <summary>Find a global parameter by name.</summary>
	public SlangReflectionParameter Find( string name ) => Parameters
		.FirstOrDefault( x => string.Equals( x.Name, name, StringComparison.Ordinal ) );

	/// <summary>Find an entry point by name.</summary>
	public SlangReflectionEntryPoint FindEntryPoint( string name ) => EntryPoints
		.FirstOrDefault( x => string.Equals( x.Name, name, StringComparison.Ordinal ) );

	/// <inheritdoc/>
	public override string ToString() => $"{Parameters.Count} parameters, {EntryPoints.Count} entry points";
}

/// <summary>
/// Reads the JSON that <c>slangc -reflection-json</c> writes.
/// <para>
/// Two things make a naive deserializer fail on real output, and both are handled here. Numeric fields
/// can be the strings <c>"unbounded"</c> or <c>"unknown"</c> — Slang's sentinels for bindless and
/// not-yet-laid-out — so every number goes through <see cref="SlangCount"/>. And a parameter with one
/// layout category writes a <c>binding</c> object while one with several writes a <c>bindings</c>
/// array, so both spellings are accepted into the same list.
/// </para>
/// <para>
/// Prism uses this to cross-check that the Slang module it emitted exposes exactly the parameter set
/// the graph declares. That catches emitter bugs that produce text which reads fine and is wrong.
/// </para>
/// </summary>
public static class SlangReflectionReader
{
	/// <summary>Parse reflection JSON. Returns <see cref="SlangReflection.Empty"/> on any failure.</summary>
	public static SlangReflection Read( string json ) => TryRead( json, out var reflection, out _ )
		? reflection
		: SlangReflection.Empty;

	/// <summary>Parse reflection JSON from a file written by <c>-reflection-json</c>.</summary>
	public static SlangReflection ReadFile( string path )
	{
		var text = PrismLog.Guard( "Reading Slang reflection JSON",
			() => System.IO.File.Exists( path ) ? System.IO.File.ReadAllText( path ) : null, null );

		return Read( text );
	}

	/// <summary>Parse reflection JSON, reporting why it failed rather than throwing.</summary>
	public static bool TryRead( string json, out SlangReflection reflection, out string error )
	{
		reflection = SlangReflection.Empty;
		error = null;

		if ( string.IsNullOrWhiteSpace( json ) )
		{
			error = "The reflection document was empty.";
			return false;
		}

		JsonNode root = null;
		string failure = null;

		PrismLog.Guard( "Parsing Slang reflection JSON", () =>
		{
			try
			{
				root = JsonNode.Parse( json );
			}
			catch ( JsonException e )
			{
				failure = e.Message;
			}
		} );

		if ( root is not JsonObject obj )
		{
			error = failure ?? "The reflection document was not a JSON object.";
			return false;
		}

		reflection = PrismLog.Guard( "Reading Slang reflection JSON", () => new SlangReflection
		{
			Parameters = Parameters( obj["parameters"] ),
			EntryPoints = EntryPoints( obj["entryPoints"] ),
			HashedStrings = HashedStrings( obj["hashedStrings"] ),
			BindlessSpaceIndex = Count( obj["bindlessSpaceIndex"] )
		}, SlangReflection.Empty ) ?? SlangReflection.Empty;

		return true;
	}

	/// <summary>
	/// Compare what the module exposes against what the graph says it should. Every finding is a warning
	/// or information — Slang validation is Tier 2 and never gates rendering, so a mismatch tells the
	/// developer their emitter drifted, it does not stop the user working.
	/// </summary>
	public static IReadOnlyList<Diagnostic> CrossCheck( SlangReflection reflection,
		IEnumerable<SlangExpectedParameter> expected )
	{
		var results = new List<Diagnostic>();

		if ( reflection is null || expected is null ) return results;

		var seen = new HashSet<string>( StringComparer.Ordinal );

		foreach ( var want in expected )
		{
			if ( string.IsNullOrWhiteSpace( want.Name ) ) continue;

			seen.Add( want.Name );

			var found = reflection.Find( want.Name );

			if ( found is null )
			{
				results.Add( Diagnostic.Warning( DiagnosticCode.SlangDiagnostic,
					$"The generated Slang module does not expose '{want.Name}'", null,
					$"The graph declares {want.Type.Slang} {want.Name}, but slangc's reflection does not " +
					"list it. Either the emitter dropped it or the compiler optimised it away because " +
					"nothing reads it." ) );

				continue;
			}

			if ( want.Type.IsVoid || found.Type is null ) continue;

			var actual = found.Type.ToShaderType();

			if ( actual.IsVoid || actual == want.Type ) continue;

			results.Add( Diagnostic.Warning( DiagnosticCode.SlangDiagnostic,
				$"'{want.Name}' is {actual.Slang} in the generated Slang module, not {want.Type.Slang}",
				null, "The graph and the emitted module disagree about this parameter's type." ) );
		}

		foreach ( var parameter in reflection.Parameters )
		{
			if ( string.IsNullOrWhiteSpace( parameter.Name ) || seen.Contains( parameter.Name ) ) continue;

			results.Add( Diagnostic.Info( DiagnosticCode.SlangDiagnostic,
				$"The generated Slang module exposes '{parameter.Name}', which the graph does not declare",
				null, "Usually a helper the backend introduced; worth a look if it was not expected." ) );
		}

		return results;
	}

	// ---- readers ---------------------------------------------------------

	static IReadOnlyList<SlangReflectionParameter> Parameters( JsonNode node )
	{
		var results = new List<SlangReflectionParameter>();

		if ( node is not JsonArray array ) return results;

		foreach ( var entry in array )
		{
			var parameter = Parameter( entry );

			if ( parameter is not null ) results.Add( parameter );
		}

		return results;
	}

	static SlangReflectionParameter Parameter( JsonNode node )
	{
		if ( node is not JsonObject obj ) return null;

		return new SlangReflectionParameter( Text( obj["name"] ) )
		{
			Stage = Text( obj["stage"] ),
			Bindings = Bindings( obj ),
			Type = Type( obj["type"] ),
			UserAttributes = Attributes( obj["userAttribs"] ),
			SemanticName = Text( obj["semanticName"] ),
			SemanticIndex = Count( obj["semanticIndex"] ).Or( 0 ),
			Shared = Bool( obj["shared"] ),
			Format = Text( obj["format"] )
		};
	}

	/// <summary>Accept both spellings: <c>binding</c> for one category, <c>bindings</c> for several.</summary>
	static IReadOnlyList<SlangBinding> Bindings( JsonObject obj )
	{
		var results = new List<SlangBinding>();

		if ( obj["binding"] is JsonObject single )
		{
			var binding = Binding( single );

			if ( binding is not null ) results.Add( binding );
		}

		if ( obj["bindings"] is JsonArray array )
		{
			foreach ( var entry in array )
			{
				var binding = Binding( entry );

				if ( binding is not null ) results.Add( binding );
			}
		}

		return results;
	}

	static SlangBinding Binding( JsonNode node )
	{
		if ( node is not JsonObject obj ) return null;

		return new SlangBinding( Text( obj["kind"] ) ?? "unknown" )
		{
			Offset = Count( obj["offset"] ),
			Size = Count( obj["size"] ),
			ElementStride = Count( obj["elementStride"] ),
			Space = Count( obj["space"] ),
			Index = Count( obj["index"] ),
			Count = Count( obj["count"] ),
			Used = obj["used"] is null ? (bool?)null : Bool( obj["used"] )
		};
	}

	static SlangTypeInfo Type( JsonNode node )
	{
		if ( node is not JsonObject obj ) return null;

		return new SlangTypeInfo( Text( obj["kind"] ) ?? "unknown" )
		{
			Name = Text( obj["name"] ),
			ScalarType = Text( obj["scalarType"] ),
			ElementCount = Count( obj["elementCount"] ),
			RowCount = Count( obj["rowCount"] ),
			ColumnCount = Count( obj["columnCount"] ),
			ElementType = Type( obj["elementType"] ),
			ResultType = Type( obj["resultType"] ),
			BaseShape = Text( obj["baseShape"] ),
			Access = Text( obj["access"] ),
			IsArray = Bool( obj["array"] ),
			IsMultisample = Bool( obj["multisample"] ),
			Fields = Parameters( obj["fields"] ),
			UserAttributes = Attributes( obj["userAttribs"] )
		};
	}

	static IReadOnlyList<SlangReflectionEntryPoint> EntryPoints( JsonNode node )
	{
		var results = new List<SlangReflectionEntryPoint>();

		if ( node is not JsonArray array ) return results;

		foreach ( var entry in array )
		{
			if ( entry is not JsonObject obj ) continue;

			results.Add( new SlangReflectionEntryPoint( Text( obj["name"] ), Text( obj["stage"] ) )
			{
				Parameters = Parameters( obj["parameters"] ),
				Bindings = Parameters( obj["bindings"] ),
				Result = Parameter( obj["result"] ),
				ThreadGroupSize = Ints( obj["threadGroupSize"] ),
				UserAttributes = Attributes( obj["userAttribs"] ),
				UsesAnySampleRateInput = Bool( obj["usesAnySampleRateInput"] )
			} );
		}

		return results;
	}

	static IReadOnlyList<SlangUserAttribute> Attributes( JsonNode node )
	{
		var results = new List<SlangUserAttribute>();

		if ( node is not JsonArray array ) return results;

		foreach ( var entry in array )
		{
			if ( entry is not JsonObject obj ) continue;

			var arguments = new List<object>();

			if ( obj["arguments"] is JsonArray values )
			{
				foreach ( var value in values )
				{
					arguments.Add( Scalar( value ) );
				}
			}

			results.Add( new SlangUserAttribute( Text( obj["name"] ) ?? "?", arguments ) );
		}

		return results;
	}

	static IReadOnlyDictionary<string, long> HashedStrings( JsonNode node )
	{
		var results = new Dictionary<string, long>( StringComparer.Ordinal );

		if ( node is not JsonObject obj ) return results;

		foreach ( var pair in obj )
		{
			var count = Count( pair.Value );

			results[pair.Key] = count.IsKnown ? count.Value : 0;
		}

		return results;
	}

	static IReadOnlyList<int> Ints( JsonNode node )
	{
		var results = new List<int>();

		if ( node is not JsonArray array ) return results;

		foreach ( var entry in array )
		{
			results.Add( Count( entry ).Or( 0 ) );
		}

		return results;
	}

	// ---- primitives ------------------------------------------------------

	/// <summary>Read a number that Slang may have written as a sentinel string.</summary>
	public static SlangCount Count( JsonNode node )
	{
		if ( node is not JsonValue value ) return SlangCount.Absent;

		if ( value.TryGetValue<int>( out var number ) ) return new SlangCount( number, SlangCountKind.Known );
		if ( value.TryGetValue<long>( out var wide ) ) return new SlangCount( (int)wide, SlangCountKind.Known );

		if ( value.TryGetValue<double>( out var real ) )
		{
			return new SlangCount( (int)real, SlangCountKind.Known );
		}

		if ( value.TryGetValue<bool>( out var flag ) )
		{
			return new SlangCount( flag ? 1 : 0, SlangCountKind.Known );
		}

		if ( !value.TryGetValue<string>( out var text ) ) return new SlangCount( 0, SlangCountKind.Unknown );

		if ( string.Equals( text, "unbounded", StringComparison.OrdinalIgnoreCase ) )
		{
			return new SlangCount( 0, SlangCountKind.Unbounded );
		}

		if ( int.TryParse( text, out var parsed ) ) return new SlangCount( parsed, SlangCountKind.Known );

		return new SlangCount( 0, SlangCountKind.Unknown );
	}

	static string Text( JsonNode node ) =>
		node is JsonValue value && value.TryGetValue<string>( out var text ) ? text : null;

	static bool Bool( JsonNode node )
	{
		if ( node is not JsonValue value ) return false;
		if ( value.TryGetValue<bool>( out var flag ) ) return flag;
		if ( value.TryGetValue<int>( out var number ) ) return number != 0;
		if ( value.TryGetValue<string>( out var text ) ) return bool.TryParse( text, out var parsed ) && parsed;

		return false;
	}

	static object Scalar( JsonNode node )
	{
		if ( node is not JsonValue value ) return null;
		if ( value.TryGetValue<string>( out var text ) ) return text;
		if ( value.TryGetValue<bool>( out var flag ) ) return flag;
		if ( value.TryGetValue<int>( out var number ) ) return number;
		if ( value.TryGetValue<double>( out var real ) ) return real;

		return value.ToString();
	}
}