DreamShaderLang
Plugin referenceC++ API

`DreamShaderLang`

`DreamShaderLang`

DreamShaderLang

DreamShader » C++ API » DreamShaderLang

DreamShaderLang is the front end and the middle end: the text goes in, a lowered graph comes out — and the other way round. Lexer, preprocessor, both parsers (2.0 and the 1.x legacy front end), AST, printer, binder, the graph IR with its passes, validator and comparator, the writer that turns an IR back into source, the 1.x → 2.0 migrator, and the diagnostic types live here — and nothing else does. It was split out of DreamShader for 2.0 so the language can be exercised without an editor, reused by a language service, and reasoned about as a language rather than as a plugin.

The line the module does not cross is the engine: an FIRModule names no UObject, no asset and no expression class object. Engine knowledge arrives as plain data in an FBuiltinCatalog, which the Editor module fills by reflection and can also save to, and load from, JSON.

ModuleDreamShaderLang
Type / loading phaseRuntime / PostConfigInit
DependenciesCore only
Export macroDREAMSHADERLANG_API
Public headers30: 4 at the module root + 11 under Lang/ + 1 under Semantic/ + 12 under IR/ + 1 under Decompile/ + 1 under Migrate/
NamespacesUE::DreamShader::Lang (2.0 front end and binder) · UE::DreamShader::IR (the graph IR and the builtin catalog) · UE::DreamShader (preprocessor, define table, FDreamShaderError)
Reflected typesnone — no UCLASS, no USTRUCT, no UENUM
Statusnew in 2.0: the lexer, the 2.0 parser and the printer; the binder, the IR, its builder, passes and validator, and the custom-HLSL builder; the legacy front end, .dsi sources, comment trivia, the IR comparator, IR → source, and the migrator. .dsp pipelines (since 2.1.0)

The Core-only rule

DreamShaderLang.Build.cs declares exactly one dependency:

PublicDependencyModuleNames.AddRange(new[] { "Core" });

This is a hard boundary, not a current state of affairs:

  • No UObject, no Engine, no CoreUObject. Nothing in the module can name a UMaterial, a UMaterialExpression or an asset path type.
  • No IPluginManager, no GetDefault<>(), no config access. The module cannot look anything up about the project it is running in.
  • Engine facts arrive as parameters. The DS_* preprocessor constants, asset roots and expression class names are supplied by the host as plain data — DreamShaderDefineResolution.h in the DreamShader module is the half that builds a define table by asking the engine; DreamShaderLang only consumes one.
  • Every header compiles in a commandlet, a test, or a future standalone language server. The front end creates no objects and touches no files: FLangSourceText is handed text that somebody else read.

Public headers

Lang/ — the 2.0 front end

Everything here is in UE::DreamShader::Lang. Include paths keep the Lang/ prefix, e.g. #include "Lang/LangParser.h".

HeaderPurpose
Lang/LangSource.hFLangSpan (offset + length + resolved 1-based line/column), ELangFileKind (Dss, Dsh, Dsm, Dsf, Dsi, Dsp (since 2.1.0), Unknown), GetLangFileKindFromPath, and FLangSourceText — one source text with its line table, the thing every span points into.
Lang/LangDiagnostic.hELangSeverity, FLangDiagnostic (a DSHnnnn code, a severity, an FText message, a span, a file path) and FLangDiagnosticSink, which collects them for one run. FLangDiagnosticSink::ToWireString renders DSHnnnn: message in the invariant (source) form for logs, JSON and test goldens.
Lang/LangToken.hELangTokenKind, ELangKeyword, FLangToken, TryGetLangKeyword, GetLangTokenSpelling. Type names are not keywords: float3, Texture2D, material and user struct names all reach the parser as identifiers.
Lang/LangLexer.hLexDreamShaderLang and FLangLexOptions. One lexer serves both syntaxes.
Lang/LangAst.hThe tree both front ends produce: FTypeRef, FDocBlock/FDocDirective, ENodeKind, the expression / statement / declaration node structs, and FModule. Ownership is TUniquePtr down the tree; sub-kinds are told apart with As<T>() — no RTTI, no visitors. A .dsp adds FBufferDecl, FPassDecl with its FPassStmts (EPassStmtKind), FPipelineKeyValue and EPragmaKind::Pipeline (since 2.1.0); their keys and values are ordinary expressions, and what they mean is the binder's.
Lang/LangParser.hParseDreamShaderLang, ParseDreamShaderLangExpression, ELangFrontend, FLangParseOptions (Frontend, bKeepTrivia), FLangParseResult (Module, Diagnostics, and Legacy for a 1.x source).
Lang/LangPrinter.hPrintDreamShaderLang and the per-node overloads (PrintDreamShaderLangExpr, …), plus FLangPrintOptions.
Lang/LangLegacy.hFLegacyMigrationInfo — what a 1.x text said, beside the AST it became: FLegacyBlock, FLegacySection, FLegacyParameterDeclaration, FLegacyAssetReference, FLegacyOutputSelection, FLegacyRename, the synthesized initializers and directives. The binder reads it to apply the 1.x rules; the migrator reads it to write them out.
Lang/LangInstanceSource.h.dsi text: BuildDreamShaderInstanceModule / PrintDreamShaderInstance (an IR::FIRInstance → tree → text), RewriteDreamShaderInstanceSource and RewriteDreamShaderUniformDefaults (value-by-value edits of an existing file, as FLangSourceEdits, so comments and order survive), FormatDreamShaderFloatLiteral, MakeDreamShaderIdentifier.
Lang/LangPipelineSource.h (since 2.1.0).dsp text without the compiler: CollectDreamShaderPipelineReferences (every Material = "..." and Shader = "..." of a parsed .dsp, once each, in source order — what the host resolves before the bind), BuildDreamShaderPipelineModule / PrintDreamShaderPipeline (an IR::FIRPassPipeline → tree → text, defaults left out: the decompiler's text), RewriteDreamShaderPipelineSource (Adopt: edits that make an existing file state a payload, touching only the declarations and keys that differ), CompareDreamShaderPipelines (structural equality of two payloads, source references ignored, one line per difference), and GetDreamShaderPassInjectionNames / GetDreamShaderPassFormatNames — the canonical spellings, in the order of the runtime's enums.
Lang/LangFormat.hdsc fmt without the editor: FormatDreamShaderLangSource, FLangFormatOptions and ELangFormatOutcome (Unchanged, Changed, Skipped, Failed) — the printer over a parse that kept its trivia, refusing to write a file it cannot vouch for.

Semantic/ — the binder

UE::DreamShader::Lang, one header. Binding is the step between "it parses" and "it can be lowered": every name resolved, every expression typed, every call matched against a signature or the catalog, and the file's products decided.

HeaderPurpose
Semantic/LangBound.hBindDreamShaderLang, FBindOptions, FLangBindResult, and FBoundModule — the AST plus side tables keyed by const FNode*: FBoundStruct, FBoundGlobal, FBoundFunction (with EBoundFunctionKind: entry, helper, export, extern, custom), FBoundProduct, FBoundExpr (EBoundExprKind), FBoundDirectives and the Directive:: key names, the region tree, the resolved include paths, and the catalog the module was bound against. Also BuildDreamShaderSymbolIndexJson — the language service's symbol index, schema dreamshader-symbol-index version 1. For a .dsp (since 2.1.0): FBoundModule::Pipeline, an FBoundPipeline (the canonical payload with every default applied, and the declaration each part came from), and what the host resolves before the bind — FPipelineReferences with one FPipelineMaterialInfo per material, one FPipelineShaderInfo per .usf, the project's pass layers and bCustomPassAvailable — passed as FBindOptions::PipelineReferences.

A bound module keeps pointers into the FModule it bound and into whatever the include resolver returned; both have to outlive it. Nothing downstream of the builder does — an FIRModule points back at neither.

IR/ — the graph IR

UE::DreamShader::IR, twelve headers. The IR is the material graph before it is a material graph: nodes, operands, properties and products, with no engine type anywhere in it. Include paths keep the IR/ prefix.

HeaderPurpose
IR/IRTypes.hEIRTypeKind, FIRType (kind + component count + texture kind + struct index), TypeFromBuiltinRef, EIRConversion and ClassifyConversion — the one place a "does this value fit that pin" question is answered.
IR/IRCoreOps.hEIROp, EIRTypingRule, FIRCoreOpInfo and the lookups (GetCoreOpInfo, FindCoreOpByHlslName, FindCoreOpByGlslAlias, FindCoreOpForBinary, FindCoreOpForUnary, IsCoreMathOp). One table holds each core op's arity, typing rule, engine class and pin names; the binder reads the typing, the builder builds from it, the emitter reads the class and pins.
IR/IRCatalog.hFBuiltinCatalog and its JSON schema: FCatalogExpression (class, pins, properties, outputs, aliases, bIsCustomOutput), FCatalogPin, FCatalogProperty, FCatalogMaterialAttribute, ECatalogValueType, LoadBuiltinCatalogFromJson / SaveBuiltinCatalogToJson, TypeFromCatalogValueType. The only channel through which engine knowledge reaches this module.
IR/IR.hFIRValue, FIRInput, FIRProperty / FIRPropertyValue (EIRPropertyKind), the Prop:: property-name constants, FIRSourceRef, FIRNode, FIRRegion, FIRLayoutHint, FIRGraph, FIRProduct (EIRProductKind, EIRBackend) and FIRModule. A .dsp's product is EIRProductKind::PassPipeline (since 2.1.0): an empty graph, and FIRProduct::PassPipeline — an FIRPassPipeline of FIRPassParameters, FIRPassBuffers and FIRPasses (with FIRPassBinding, FIRPassParam, FIRPassFilterClause / FIRPassFilterTerm), every enumerated value a string in its .dsp spelling. The passes and the validator's graph checks skip it (a pipeline must be the only product of its module, DSH4334); the comparator compares the payloads.
IR/IRBuilder.hFIRBuildOptions (MaxUnrolledIterations, MaxInlineDepth, bKeepDebugNames, an optional Catalog override) and BuildDreamShaderIR.
IR/IRPasses.hFIRPassOptions (bFoldConstants, bDedupe, bPrune) and RunDreamShaderIRPasses.
IR/IRValidator.hValidateDreamShaderIR — one lowered module against the IR's own rules and against the catalog. An empty catalog skips the checks that need engine knowledge rather than failing them, so a hand-built graph can be validated in a unit test.
IR/IRDump.hDumpDreamShaderIRText (the dsc dump-ir rendering, and the golden the IR corpus diffs) and DumpDreamShaderIRJson.
IR/IRCompare.hAreDreamShaderIRModulesEquivalent and FIRCompareOptions — two modules compared from their roots, with the first difference in words, followed down to the deepest node that differs. What "the decompiled text means the same" and "the migrated text builds the same graph" are measured with. The options say which differences are one asset written two ways: debug names, regions, Custom-code markers and spacing, a constant against its Const* twin, an identity swizzle.
IR/IRInstanceSchema.hBuildParameterSchemaFromIR — the parameter schema a .dsi binds against, taken from its parent's IR (the editor side builds the same schema from an asset).
IR/IRCustomHlsl.hFCustomNodeCode (Code, IncludeFilePaths, InlinedFunctions), BuildDreamShaderCustomNodeCode, and the CustomCodeMarker grammar with TryParseCustomCodeBodyMarker — the line markers a Custom node's body carries, which dsc check -Shaders maps shader-compile errors back through.
IR/IRLayout.hGraph layout before any node exists: EIRLayoutStyle (Blocks, SourceBands, Layered) with TryParseIRLayoutStyle, FIRLayoutOptions, FIRLayoutResult and its parts, LayoutDreamShaderIRGraph, FindDreamShaderIRLayoutOwner, the engine-free size estimates, and DumpDreamShaderIRLayoutSvg / …Json (schema dreamshader-ir-layout version 1, what dsc dump-layout writes). Deterministic, and it never changes the graph.

The IR holds no source positions in its identity: FIRSourceRef rides on a node for tooling (the asset's DreamShader.SourceSpans table, node ↔ source navigation) and is never part of a dedupe key. Two structurally identical nodes merge no matter where they were written.

Decompile/ and Migrate/ — the way back

UE::DreamShader::Lang, one header each. Neither knows about assets: the editor module imports a graph into an FIRModule and hands it over.

HeaderPurpose
Decompile/IRToAst.hRaiseDreamShaderIR — the emitter's lowered shapes read back as source-level ones, in place (a StaticSwitch over a static bool is an if, a Set-attributes chain is m.X = …) — and BuildDreamShaderAstFromIR with FIRToAstOptions (bEmitLayout, bEmitBackend, bReadable, the extern interfaces and product paths the host knows, header comments). The result is an ordinary FModule; PrintDreamShaderLang makes it text.
Migrate/LangMigrate.hMigrateDreamShaderLegacyModule — rewrites a bound 1.x module in place into one that means the same without the legacy rules (the header's table lists each rewrite with its rule number) — and CollectDreamShaderComments, which the host uses to prove no comment was lost.

Module root — what moved here in 2.0

Three of the four headers below were in the DreamShader module through 1.9.x and moved into DreamShaderLang for 2.0. They kept their UE::DreamShader namespace and their include paths, so existing callers need no source change — only a DreamShaderLang entry in their Build.cs dependency list. DreamShaderLangModule.h is new in 2.0.

HeaderPurpose
DreamShaderPreprocessor.h (moved in 2.0)PreprocessDreamShaderSource, FDreamShaderPreprocessResult, DreamShaderSourceHasPreprocessorDirectives, EvaluateDreamShaderConditionExpression, BuildDreamShaderDefineKeyFragment. #if resolution, line-count-conserving. See Preprocessor.
DreamShaderDefineTable.h (moved in 2.0)EDreamShaderDefineSource, FDreamShaderDefineEntry, the case-sensitive FDreamShaderDefineMap / FDreamShaderDefineValueMap aliases, FDreamShaderDefineTable, IsReservedDreamShaderDefineName, IsValidDreamShaderDefineName. Pure text; the engine-facing half that fills a table stayed behind as DreamShaderDefineResolution.h in the DreamShader module.
DreamShaderDiagnostic.h (moved in 2.0)FDreamShaderError / FDreamShaderTextError — the 1.x code-carrying error struct, and the doc comment that records what each DSHnnnn leading digit means.
DreamShaderLangModule.hFDreamShaderLangModule. Both lifecycle methods are empty: the module object exists only so the plugin descriptor can name it and dependents can link it. There is no singleton accessor and nothing to initialise.

FDreamShaderError (1.x, FString/FText message) and FLangDiagnostic (2.0, code + severity + FText + span) coexist on purpose: the 1.x pipeline still carries the former, and the 2.0 front end never produces one. They are not converted implicitly.

Entry points

The module is a pipeline of six calls — preprocess, lex, parse, bind, lower, run the passes — plus a validator, two dumps, a printer, and one call for parsing a single expression on its own.

CallHeaderContract
PreprocessDreamShaderSource(...)DreamShaderPreprocessor.hResolves #if / #define against a define table, conserving line counts, so every later span still points at the author's line.
LexDreamShaderLang(Source, Options, OutTokens, Diagnostics)Lang/LangLexer.hCuts a whole text into tokens. Returns false when any error was recorded; the array always ends with EndOfFile, so a caller can keep going.
ParseDreamShaderLang(Source, Options)Lang/LangParser.hOne text → one FModule. The result's Module is present even after errors, holding whatever parsed cleanly, so a language service keeps working on a broken file; Succeeded() is Module.IsValid() && !Diagnostics.HasErrors(). Lexer and parser diagnostics are merged in source order, and a lexical diagnostic whose span falls inside a /// @custom body is dropped — that text is not this language (see Opaque bodies below).
ParseDreamShaderLangExpression(Source, Diagnostics)Lang/LangParser.hOne text → one FExpr, for tests, the language service and #pragma values. A token left over after the expression loses the whole result (DSH3211) rather than silently truncating it; this is the only entry point that can raise that code.
PrintDreamShaderLang(Module, Options)Lang/LangPrinter.hFModule → .dss text.
BindDreamShaderLang(Module, Options)Semantic/LangBound.hOne FModule → one FBoundModule. FBindOptions::Catalog is required; without an IncludeResolver every #include is refused. Like the parser it keeps going after an error, so ask Succeeded() rather than whether the sink is empty. The result borrows from Module and from the resolver's modules; both must outlive it.
BuildDreamShaderSymbolIndexJson(Bound)Semantic/LangBound.hDeclarations, references, resolved includes and the parameter schema as JSON, for the language service. Works on a module that has errors — which is when an editor needs navigation most.
BuildDreamShaderIR(Bound, Options, Diagnostics)IR/IRBuilder.hFBoundModule → TUniquePtr<FIRModule>, every product lowered. Returns a module even after errors (the partial graph is what dsc dump-ir shows), so callers ask Diagnostics.HasErrors(); null means the bound module had no FModule at all. Nothing in the result points back into the AST.
RunDreamShaderIRPasses(Module, Options, Diagnostics)IR/IRPasses.hConstant folding, structural dedupe and dead-node pruning, in place. FIRValues stay valid across it. Its diagnostics are informational only; a pass never fails a build.
ValidateDreamShaderIR(Module, Catalog, Diagnostics)IR/IRValidator.htrue when the module is emittable. Diagnostics land in the sink either way, all stamped with the sink's own file path.
DumpDreamShaderIRText(Module) / …Json(Module)IR/IRDump.hThe human rendering (dsc dump-ir, and the golden the IR corpus diffs) and the machine one (dsc dump-ir -Json).
BuildDreamShaderCustomNodeCode(Bound, FunctionIndex, Out, Diagnostics)IR/IRCustomHlsl.hOne /// @custom function → the HLSL a Custom node carries, with its hoisted #includes and its embedded helpers. Deterministic: two calls for the same function produce the same string, which is what lets the dedupe key run over Prop::Code.
LoadBuiltinCatalogFromJson(...) / SaveBuiltinCatalogToJson(...)IR/IRCatalog.hThe catalog's on-disk form. dsc export-catalog writes it; a tool that binds outside the editor reads it.
AreDreamShaderIRModulesEquivalent(A, B, Options, OutDifference)IR/IRCompare.hTrue when two modules describe the same products with equivalent graphs; otherwise the first difference, in words.
RaiseDreamShaderIR(Module, Diagnostics) → BuildDreamShaderAstFromIR(Module, Catalog, Options, Diagnostics)Decompile/IRToAst.hAn FIRModule → an FModule that lowers back to an equivalent IR. What the language cannot say is a diagnostic (DSH9075–DSH9084), never a silent drop.
MigrateDreamShaderLegacyModule(Module, Legacy, Bound, Options, Diagnostics)Migrate/LangMigrate.hRewrites a 1.x module in place. Bound was bound against Module and is only read; its node pointers are stale once this returns. False after an error: the module is then not worth printing.
PrintDreamShaderInstance(Instance, FilePath, AssetPathOverride)Lang/LangInstanceSource.hThe .dsi text for an instance payload: optional /// @name, #pragma instance(...), one uniform per override.
CollectDreamShaderPipelineReferences(Module, OutMaterials, OutShaders) (since 2.1.0)Lang/LangPipelineSource.hEvery Material = "..." and Shader = "..." of a parsed .dsp, once each, in source order. The host resolves them into an FPipelineReferences and binds with it as FBindOptions::PipelineReferences; without one the bind is an engine-free check — references taken as written, every check that needs an engine fact skipped, and an info (DSH7360) that says so. The product is the payload of FBoundModule::Pipeline, named after the file; BuildDreamShaderIR copies it into FIRProduct::PassPipeline and lowers nothing.
PrintDreamShaderPipeline(Pipeline, FilePath, Options) (since 2.1.0)Lang/LangPipelineSource.hThe .dsp text for a pipeline payload — #pragma pipeline, uniforms, buffers, passes, in the payload's order, defaults left out. What the decompiler writes.
RewriteDreamShaderPipelineSource(Original, Parsed, Bound, Desired, OutEdits, OutText, Diagnostics) (since 2.1.0)Lang/LangPipelineSource.hAdopt for a .dsp: span edits that make an existing file state Desired, touching only what differs, so comments and order survive. False, with OutText left as the original, for a file that is not a bound .dsp (DSH9109), a declaration shared with other names that would have to change (DSH9107) or overlapping edits (DSH9108).
CompareDreamShaderPipelines(A, B, OutDifferences) (since 2.1.0)Lang/LangPipelineSource.hStructural equality of two payloads, source references ignored, one line per difference — what the .dsp → asset → .dsp round trip and AreDreamShaderIRModulesEquivalent measure a pipeline with.
// MyTooling.Build.cs:  PrivateDependencyModuleNames.AddRange(new[] { "Core", "DreamShaderLang" });

#include "Lang/LangParser.h"
#include "Lang/LangPrinter.h"

using namespace UE::DreamShader::Lang;

void Normalise(const FString& Path, const FString& Text)
{
    const FLangSourceText Source(Path, Text);
    const FLangParseResult Result = ParseDreamShaderLang(Source);

    for (const FLangDiagnostic& Diagnostic : Result.Diagnostics.GetDiagnostics())
    {
        UE_LOG(LogTemp, Warning, TEXT("%s(%d,%d): %s"),
            *Diagnostic.FilePath, Diagnostic.Span.Line, Diagnostic.Span.Column,
            *FLangDiagnosticSink::ToWireString(Diagnostic));
    }

    if (Result.Succeeded())
    {
        const FString Printed = PrintDreamShaderLang(*Result.Module);
    }
}

Front-end selection

FLangParseOptions::Frontend defaults to ELangFrontend::Auto, which decides from the file extension carried by FLangSourceText::GetPath():

ExtensionELangFileKindFront end
.dssDss2.0
.dsiDsi2.0 — a material instance: a #pragma instance and uniform overrides, nothing else
.dshDshdecided per declaration: a shared header may hold both syntaxes while a project migrates. export and asset blocks are rejected
.dsp (since 2.1.0)Dsp2.0 — a Custom Pass pipeline: #pragma pipeline, uniform and static const declarations, buffer and pass. buffer and pass are declaration words in a .dsp only (DSH3310 elsewhere); a function, a struct or an #include in a .dsp is DSH3314
.dsmDsmlegacy
.dsfDsflegacy

The legacy front end lowers 1.x constructs onto the same AST node kinds — a Shader block becomes uniforms, a #pragma material and one exported function — so nothing downstream has to know which parser produced a module. What the 1.x text said beyond that (block names, sections, renames, output selections) rides beside the tree in FLangParseResult::Legacy, and the declarations carry bLegacy, which is what makes the binder apply the documented 1.x rules to them and to nothing else.

Printer contract

The printer's promise is structural fidelity, not textual: parse(print(parse(X))) yields the same tree as parse(X). Concretely — parentheses are emitted where FParenExpr recorded them and wherever precedence requires; /// blocks are re-emitted one directive per line after the free text; an opaque (/// @custom) body is written verbatim from RawBody; pragmas and includes keep their spelling; literals keep the lexeme as written, so 1.0f, 0x10 and 2u survive. Comments other than /// are reproduced when the parse kept them: with FLangParseOptions::bKeepTrivia every comment and blank line is attached to the declaration or statement it stood by (FModule::Trivia), and the printer writes them back — which is what lets dsc migrate promise that no comment is lost. Without the option they are dropped, as before.

FLangPrintOptions::NewLine defaults to \n, but an opaque body is a slice of its source and keeps that file's terminators, so printing a CRLF file yields mixed terminators. That is stable: the next parse slices the same bytes back out, so the fixed point still holds.

Opaque bodies

A function whose /// block carries @custom has its body captured verbatim: FFunctionDecl::Body is null, bOpaqueBody is true, RawBody is the text between the braces and BodySpan covers the braces inclusive. The parser reads only the braces — they must balance, and strings, comments and # lines inside cannot unbalance them because the lexer already made each one a single token.

Because the text is not DreamShaderLang, ParseDreamShaderLang discards every lexical diagnostic whose span starts inside a BodySpan. A $, a @, a ' or a mid-line # in there is not a fact about the program, and the shader compiler is the one that gets to judge it. The suppression is scoped to those spans: the same character one line outside is still DSH2101 / DSH2106.

Diagnostics

Every raise site names its code literally, so .skill/gen-diagnostics.ps1 can find it:

Diagnostics.Error(TEXT("DSH2151"), Span, LOCTEXT("ExpectedExpression", "expected an expression"));

The messages are LOCTEXT under a per-file LOCTEXT_NAMESPACE of the form "DreamShader.Lang.<Unit>", so the editor can localise them. Assert on the code, never on the message text.

RangeRaised byFixed anchors
DSH2101–DSH2119the lexer (allocated: 2101–2106)2101 unknown character · 2102 unterminated block comment · 2103 unterminated string · 2104 unknown escape · 2105 malformed number · 2106 a # that is not the first thing on its line
DSH2150–DSH2189expressions and statements (allocated: 2150–2155, 2157–2165)2150 unexpected end of file · 2158 positional argument after a named one · 2160 unsupported statement (switch, and a # line inside a body) · 2162 an initializer list used as an expression
DSH2199front-end selectionretired when the legacy front end arrived
DSH2200–DSH2258the legacy front end — blocks, attribute lists, import, Graph statements2200 an operator 1.x silently truncated at · 2248 2.0 syntax in a .dsm / .dsf · 2249 an asset block in a .dsh · 2252 an import the include resolver does not read
DSH2300–DSH2349.dsp syntax — buffer and pass declarations, pass bodies, bindings
DSH3250–DSH3278the legacy front end — Properties, Settings, Outputs, Inputs, Layout sections
DSH3300–DSH3349.dsp declarations — #pragma pipeline, buffer and pass keys, .dsp-only declarations in another source
DSH6300–DSH6330the legacy front end and binder — Function / GraphFunction, lifted UE. calls6326 a lifted call reads a body local
DSH3200–DSH3249declarations, types, directives, doc blocks (allocated: 3200–3208, 3210, 3211, 3213–3218, 3220–3222)3201 stray preprocessor directive · 3203 #include / import without a double-quoted path · 3204 expected a type name · 3208 function without a body · 3213 bad storage/linkage combination · 3220 malformed @ directive (warning) · 3221 orphan /// block (warning) · 3222 a 1.x declaration word

The binder, the builder, the passes, the validator and the custom-HLSL builder raise through the same sink, with their own ranges:

RangeRaised by
DSH4200–DSH4299the binder — names, types, expressions, statements, loops, regions
DSH4300–DSH4349the IR validator
DSH4350–DSH4399lowering refusals — a matrix that survives folding, a loop with no provable trip count, discard inside a branch, a material attribute read before it is written
DSH4400–DSH4449.dsp names and references — buffers, materials, HLSL files, entries, layers
DSH5200–DSH5299reflected calls, the catalog, material attributes
DSH5300–DSH5314the Custom Pass material nodes in a .dss — the engine gate, where they may stand, the new translator
DSH6200–DSH6219function kinds, the entry, export / extern
DSH6220–DSH6249helper inlining
DSH6250–DSH6299/// @custom HLSL
DSH7200–DSH7249uniforms, /// directives, #pragma material
DSH7250–DSH7270.dsi: #pragma instance, overrides against the parent's schema
DSH7300–DSH7379.dsp checks (V1–V13): pass kind against injection point, buffer use in frame order, bindings against the material or HLSL file, limits
DSH5250–DSH5292the documented 1.x rules (L1–L27), each one a diagnostic where it rewrites or drops something
DSH9042–DSH9044the formatter: a 1.x file and a file with preprocessor lines are skipped (info), a formatted text that fails its own check is refused
DSH9075–DSH9084IR → source: what the writer renamed, could not place, or left at a default
DSH9107–DSH9109the in-place rewrites of a .dsi and, since 2.1.0, a .dsp (Adopt): a shared declaration, overlapping edits, the wrong kind of file
DSH9091, DSH9094the migrator (the host's proofs around it, in the editor module, are the rest of DSH9090–DSH9099)

DSH1030–DSH1042 (the preprocessor's own codes) moved into this module with the preprocessor and kept their numbers. The prose for every code lives under Diagnostics.

Testing: the Lang corpus

The front end is covered by a data-driven corpus, not by hand-written per-feature tests. Fixtures live under Tests/Corpus/Lang/, one .dss or .dsh per feature, each optionally paired with a <name>.expected.json golden; a complex automation test enumerates the tree at runtime and surfaces every fixture as its own sub-test.

TestDreamShader.Lang2.Corpus.<Area>.<Fixture>
RunnerRunDreamShaderLangCorpusCase in Source/DreamShaderEditor/Private/Tests/DreamShaderTestCommon.h
RegistrationSource/DreamShaderEditor/Private/Tests/DreamShaderCorpusLangTests.cpp
FixturesTests/Corpus/Lang/{Lexical,Expressions,Statements,Declarations,Examples}/ — .dss and .dsh, plus one .dsm that pins front-end selection: it reaches the legacy front end and fails there (DSH2241)
Golden schema"entryPoint": "lang" — see Tests/Corpus/README.md

Each case is parsed once through ParseDreamShaderLang, then checked against its golden:

  • outcome — ok or error. Without a golden the default is "a positive fixture parses, a *.bad.* fixture fails".
  • errorContains / warningsContain — substrings (case-sensitive) of FLangDiagnosticSink::ToWireString of each diagnostic of that severity. In practice: the DSHnnnn code.
  • module — structural counts over the parsed FModule: declarations (pragmas and includes count), pragmas, includes, structs, uniforms (Uniform storage only), constants (StaticConst only), functions (all three linkages), exports, externs, opaqueBodies, entry (the first function whose signature is void (inout material), regardless of linkage) and roundtrip.
  • roundtrip — print → parse → print is byte-identical, which is the printer's contract expressed as a test.

Adding coverage for a new construct is dropping a file in the tree; no C++ changes, no recompile. Run the whole layer headlessly:

"F:\UnrealEngine\UE_Moon\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" ^
  "I:\UnrealProject_Moon\DEV_58\MoonEngineSample\MoonEngineSample.uproject" ^
  -ExecCmds="Automation RunTests DreamShader.Lang2; Quit" ^
  -nullrhi -unattended -nopause -nosplash -log

Adding -DreamShaderUpdateGolden rewrites every golden from the actual result instead of asserting. Review the diff by hand before committing — that switch is how a regression becomes a baseline.

Alongside the corpus, each front-end unit has its own focused test file (DreamShaderLangLexerTests.cpp, …DeclarationTests.cpp, …ExpressionTests.cpp, …PrinterTests.cpp), all under DreamShader.Lang2.*.

See also

On this page