Editor/Prism/Text/Lexer/VfxLexer.cs

A lexer for s&box .shader VFX files. It extends an HLSL lexer to recognize top-level VFX blocks (HEADER, MODES, FEATURES, COMMON, VS, PS, etc.), map blocks to shader stages, and classify identifiers differently depending on current VFX block context.

Reflection
using Editor.Prism.Core;
using Editor.Prism.Text.LanguageDb;

namespace Editor.Prism.Text.Lexer;

/// <summary>
/// The top-level blocks of a VFX <c>.shader</c> file. The numeric values are the 1-based index into
/// <see cref="SboxSymbols.BlockNames"/> and are what <see cref="LexFlags.Block"/> stores, so they
/// must not be reordered.
/// </summary>
public enum VfxBlock
{
	/// <summary>File scope — outside every block. This is where the <c>struct VertexInput</c> and
	/// <c>struct PixelInput</c> declarations live.</summary>
	None = 0,
	/// <summary>The <c>HEADER</c> metadata block.</summary>
	Header = 1,
	/// <summary>The <c>MODES</c> render-pass block.</summary>
	Modes = 2,
	/// <summary>The <c>FEATURES</c> permutation block.</summary>
	Features = 3,
	/// <summary>The <c>COMMON</c> block, shared HLSL.</summary>
	Common = 4,
	/// <summary>The <c>VS</c> vertex program.</summary>
	Vertex = 5,
	/// <summary>The <c>PS</c> pixel program.</summary>
	Pixel = 6,
	/// <summary>The <c>GS</c> geometry program.</summary>
	Geometry = 7,
	/// <summary>The <c>HS</c> hull program. The engine's parser throws on this block.</summary>
	Hull = 8,
	/// <summary>The <c>DS</c> domain program. The engine's parser throws on this block.</summary>
	Domain = 9,
	/// <summary>The <c>CS</c> compute program.</summary>
	Compute = 10,
	/// <summary>The <c>PS_RENDER_STATE</c> block.</summary>
	PixelRenderState = 11,
	/// <summary>The <c>RTX</c> ray-tracing program.</summary>
	RayTracing = 12
}

/// <summary>
/// The lexer for s&amp;box <c>.shader</c> files. A <c>.shader</c> is a small block language wrapping
/// HLSL, so this derives from <see cref="HlslLexer"/> and the embedded HLSL inside <c>COMMON</c>,
/// <c>VS</c>, <c>PS</c>, <c>GS</c> and <c>CS</c> is lexed by exactly the same scanner — including the
/// <c>&lt; … &gt;</c> annotation grammar, which the base lexer already understands because
/// <c>common_samplers.fxc</c> and friends use it in plain <c>.hlsl</c> files too.
///
/// What this class adds is the block structure: the block name is recognised only at file scope, the
/// brace that follows it opens the block, and <c>HEADER</c>, <c>MODES</c> and <c>FEATURES</c> get
/// their own word classes because their contents are declarations rather than code.
/// </summary>
public sealed class VfxLexer : HlslLexer
{
	/// <inheritdoc/>
	public override string Language => "vfx";

	/// <inheritdoc/>
	protected override LanguageDefinition Definition => LanguageDefinition.Vfx;

	/// <summary>Which block a lexer state sits inside. <see cref="VfxBlock.None"/> means file scope.</summary>
	public static VfxBlock BlockOf( LexState state )
	{
		var index = LexFlags.Block( state.Flags );
		return index is >= 0 and <= (int)VfxBlock.RayTracing ? (VfxBlock)index : VfxBlock.None;
	}

	/// <summary>True when the state sits inside a block whose body is HLSL rather than declarations.</summary>
	public static bool IsCodeBlock( LexState state ) => IsCodeBlock( BlockOf( state ) );

	/// <summary>True when the block's body is HLSL rather than VFX declarations.</summary>
	public static bool IsCodeBlock( VfxBlock block ) => block >= VfxBlock.Common;

	/// <summary>
	/// The shader stage a block compiles to, or <see cref="ShaderStage.None"/> for the metadata
	/// blocks and for the two programs the engine rejects.
	/// </summary>
	public static ShaderStage StageOf( VfxBlock block )
	{
		switch ( block )
		{
			case VfxBlock.Vertex: return ShaderStage.Vertex;
			case VfxBlock.Pixel: return ShaderStage.Pixel;
			case VfxBlock.PixelRenderState: return ShaderStage.Pixel;
			case VfxBlock.Geometry: return ShaderStage.Geometry;
			case VfxBlock.Compute: return ShaderStage.Compute;
			default: return ShaderStage.None;
		}
	}

	/// <inheritdoc/>
	protected override TokenKind ClassifyIdentifier( string word, IdentifierContext ctx )
	{
		if ( ctx.AfterDot || ctx.AfterScope || ctx.InAttribute )
			return base.ClassifyIdentifier( word, ctx );

		var block = (VfxBlock)LexFlags.Block( ctx.Flags );

		// The metadata blocks are declaration languages of their own.
		if ( !ctx.InAnnotation && !ctx.InDirective )
		{
			switch ( block )
			{
				case VfxBlock.Header:
					if ( SboxSymbols.TryGet( word, out var key ) && key.Kind == SboxSymbolKind.HeaderKey )
						return TokenKind.Keyword;
					break;

				case VfxBlock.Modes:
					if ( SboxSymbols.IsModeFunction( word ) )
						return TokenKind.FunctionName;
					break;

				case VfxBlock.Features:
					if ( SboxSymbols.IsComboDeclaration( word ) )
						return TokenKind.Keyword;
					break;
			}
		}

		return base.ClassifyIdentifier( word, ctx );
	}
}