DreamShaderLang
The Graph Language

Expressions and Conversions

The four operators and their precedence, literals, name lookup, constructors, swizzles, the conversion rules that fit a value to its place, and the merging of identical nodes.

Expressions

DreamShader » Graph » Expressions

The value-producing grammar of a Graph block: literals, identifiers, calls, member access and the four arithmetic operators, evaluated into UMaterialExpression nodes.

Declared in.dsm, .dsf — inside a Graph { … } body, an Outputs binding expression, or an Outputs declaration default
Kindexpression grammar
GeneratesUMaterialExpressionAdd, UMaterialExpressionSubtract, UMaterialExpressionMultiply, UMaterialExpressionDivide — one node per binary operator; identical ones are merged (see Notes)

Synopsis

<expression>     := <additive>
<additive>       := <multiplicative> [ { + | - } <multiplicative> ] …
<multiplicative> := <unary> [ { * | / } <unary> ] …
<unary>          := { + | - } <unary> | <postfix>
<postfix>        := <primary> [ <postfix-op> ] …
<postfix-op>     := . <identifier> | :: <identifier> | ( [ <argument> [ , <argument> ] … ] )
<primary>        := <identifier> | <number-literal> | <string-literal> | ( <additive> )
<argument>       := [ <identifier> = ] <additive>

( ) , . :: = + - * / are literal DreamShaderLang punctuation. [ … ], { a | b } and … are meta-notation and are never typed.

This is the 1.x grammar. The legacy front end parses an expression with the 2.0 grammar, which is a superset, and refuses everything outside the grammar above with a code (since 2.0.0) — see Operators that do not exist.

Precedence and associativity

Highest binding first.

LevelOperatorsArityAssociativityNotes
1f(…) call, .member, ::namepostfixleftChains freely: A::F(x).rgb.b
2+ -unary, prefixrightRecursive: - -x parses and is legal. --x without the space is the decrement operator, DSH2207
3* /binarylefta / b / c is (a / b) / c
4+ -binarylefta - b - c is (a - b) - c
—( … ) grouping——Overrides the levels above

Operators that do not exist

The 1.x expression grammar has no other operators. Each of the following parses, and is then refused:

CategorySpellingsCode
Modulo% — use the fmod builtinDSH2201
Comparison== != < > <= >= — legal only in an if conditionDSH2200
Logical&& || !DSH2200
Bitwise / shift& | ^ ~ << >>DSH2201
Conditional? :DSH2200
Increment / decrement++ --DSH2207
Compound assignment+= -= *= /= and the restDSH2205
Assignment inside an expression= (outside a named call argument)DSH2204
Indexing[ ] — use a swizzleDSH2202
Cast(float3)x — use a constructorDSH2203
Comma operator, (outside a call argument list)none of its own: the statement ends there, so what follows is usually DSH2154
Matrix types / operatorsnone exist; see ConstructorsDSH4361 for a matrix value

The full catalogue, with what to write instead, is in Unsupported constructs.

[!WARNING] 1.x truncated these silently. Its tokenizer mapped every character it did not know (%, !, <, >, &, |, ^, ~, ?, [, …) to the end of the input, so the parser kept the expression before it and dropped the rest without a message. (since 2.0.0) the whole expression is read and the operator is refused:

Written1.x builtCode now
a % baDSH2201
a && baDSH2200
a ? b : caDSH2200
v[0]vDSH2202
a << 2aDSH2201

A character that is no token at all (@, $, a backtick) is DSH2101. Parenthesising a suspect expression is no longer needed to make a truncation visible.

Operand rules

The binder applies these tests to both operands of + - * /:

#TestCode when it fails
1Both operands are numbers or bools — not a texture, a sampler or a MaterialAttributes valueDSH4226
2A Substrate operand: + between two Substrate values and * by a scalar build Substrate.Add / Substrate.Weight (since 2.0.0; 1.x refused both); any other operatorDSH5293
3The widths agree: the result is as wide as the widest operand, a scalar operand spreads to that width, and a narrower vector does notDSH4226
4For /: not both operands integer constructorsDSH4243 — see Integer division

An operator never narrows an operand: float3 * float4 is DSH4226, so that channels are never dropped silently. (since 2.0.0) nor does it widen one: 1.x widened an operand to the width of an "authoritative" value in some cases; operands are typed now, and the scalar spread is the only way two widths meet. See Conversions.

There is no separate scalar-promotion step: a scalar operand is passed to the material node as-is and Unreal replicates it, so A * K with A a vec3 and K a float produces a single Multiply node, not an AppendVector splat.

Result of a binary operator

PropertyValue
NodeAdd / Subtract / Multiply / Divide (per operator)
Widththe widest operand's
Kindthe operands' kinds promoted as in HLSL — an int with a float is a float; in the graph every value is a float
All-constant operandsfolded into one Constant node by the constant-folding pass (since 2.0.0)

Unary operators

FormLoweringNode costResult
+xidentityno nodex unchanged
-xMultiply(x, Constant(-1))one Multiply and one Constant holding -1the type of x

!x is DSH2200, ~x is DSH2201, ++x and --x are DSH2207.

A negated constant is folded: -2.0 becomes one Constant holding -2 (since 2.0.0), as does any arithmetic over constants. See Constructors.

Integer division

/ is the only operator with an extra type rule: DSH4243 when both operands are integer constructor calls (int, int2..4, ivec2..4, uint, uint2..4, uvec2..4). A material graph has no integer division, so the compiler refuses to pick between HLSL's truncating answer and the graph's fractional one.

Every other / is the graph's float division, as in 1.x, which typed every number literal float — even where both sides are integers to the compiler: an integer literal (7), an int / uint variable or property (legacy rule L27). dsc migrate writes such a division as float(7) / 2, because a .dss refuses every / between integers.

ExpressionResult
int(7) / int(2)DSH4243
int(7) / 23.5 (2.0.0 – 2.1.0 refused it with DSH4243)
7 / 23.5 (2.0.0 – 2.1.0 refused it with DSH4243)
7.0 / 23.5
float(int(7)) / int(2)3.5 — one operand is a float
int a = 7; int b = 2; a / b3.5 (2.0.0 – 2.1.0 refused it with DSH4243)

[!NOTE] int, uint, bool and half are kinds of their own to the compiler, and this rule is what the difference is for. In the generated graph every one of them is a float — there is no integer arithmetic and no truncation. See Conversions.

Compound assignment

+= -= *= /= are not 1.x operators. (since 2.0.0) each is DSH2205, whatever the spacing.

Written1.x read it asCode now
a += b;a declaration of a variable + of type aDSH2205
a+=b;an assignment to a new variable named a+; a unchanged, no messageDSH2205

Write the expansion instead: a = a + b;.

Increment and decrement

++ and -- are not 1.x operators. (since 2.0.0) every form is DSH2207: a++;, a--;, ++a;, --a;, and x = --a; inside a larger expression, which 1.x read as -(-a). Write a = a + 1; instead.

Notes

  • Identical subexpressions are merged after lowering: the deduplication pass keeps one node for every set of nodes with the same operation and the same operands in the same order, so A + B written twice yields one Add, while B + A is a second one. Constants are merged the same way. See Node reuse.
  • Grouping parentheses generate nothing; they only affect parse order.
  • A call may be swizzled directly: UE.TexCoord().x is a postfix chain of a call followed by a member access. See Swizzle.
  • :: is the namespace separator in a callee path: N::F names the function F of Namespace(Name="N"). It must be followed by a name (DSH5260). See Name resolution.
  • Named call arguments (Coordinates = uv) are part of the argument grammar, not an assignment operator. See Calls.

Diagnostics

CodeRaised when
DSH2151an operand was expected and something else was found, e.g. x = ; or a * )
DSH2152a ) is missing from a call or a parenthesized expression; also a missing , between arguments
DSH2161. is not followed by a name
DSH5260:: is not followed by a name
DSH2101a character that is no token
DSH2200a comparison, a logical operator or ?: outside an if condition
DSH2201%, a shift or a bitwise operator
DSH2202[ ]
DSH2203a C-style cast
DSH2204an assignment inside an expression
DSH2205a compound assignment
DSH2207++ or --
DSH4226an operand is not a number, or the widths do not agree
DSH4243both operands of / are integer constructor calls
DSH5293an operator a Substrate value does not have
DSH4200an operand name is not declared
DSH4202a string used as a value
DSH2211an expression statement that is not a call or an assignment, e.g. a + b;

The complete list lives in the diagnostics index.

Example

Shader(Name="Docs/M_Expressions")
{
    Properties {
        vec3 A = vec3(1.0, 0.5, 0.2);
        vec3 B = vec3(0.1, 0.2, 0.3);
        float K = 2.0;
    }
    Settings { Domain = "UI"; ShadingModel = "Unlit"; }
    Outputs { vec3 Color; Base.EmissiveColor = Color; }
    Graph {
        vec3 Sum    = A + B;
        vec3 Diff   = A - B;
        vec3 Scaled = A * K;
        vec3 Ratio  = A / K;
        vec3 Neg    = -Scaled;
        Color = (Sum + Diff - Scaled + Ratio) * 0.25 + Neg;
    }
}

Generated nodes:

VectorParameter A, VectorParameter B             (property nodes)
ScalarParameter K                                (property node)
Add(A, B)                                        -> Sum
Subtract(A, B)                                   -> Diff
Multiply(A, K)                                   -> Scaled
Divide(A, K)                                     -> Ratio
Multiply(Scaled, Constant(-1))                   -> Neg
Add / Subtract / Add chain, Multiply(..., 0.25), Add(..., Neg)

See also

  • Statements — the statement forms an expression can appear in
  • Unsupported constructs — every refused construct and its code
  • Literals — numeric, string and boolean literal forms
  • Constructors — float3(…), vec4(…) and the integer constructors
  • Swizzle — .rgb, .bgr, channel masks
  • Conversions — widening, narrowing and kinds
  • if / else — the only place comparison operators are accepted
  • Calls — call syntax, named arguments, out arguments
  • Name resolution — how an identifier or callee is looked up
  • Node reuse — why repeated subexpressions produce one node
  • Math builtins — fmod, pow, min, max and the rest

On this page