DreamShaderLang
语言核心

词法与大小写

大小写敏感性、注释、标识符、字符串与数字字面量,以及几个对空白字符看法不一致的语句切分器。

这一页讲字符级别的规则:哪些 token 在意大小写、注释和字面量怎么读、section 的内容怎么被切成语句。 大部分内容不会让人意外。真正需要记住的是三点 —— 大小写敏感性矩阵、宽容到有点危险的数字转换,以及 制表符的坑。

大小写敏感性

顶层块关键字是整门语言里唯一大小写敏感的 token。 其余所有类关键字的东西都忽略大小写匹配。

构造大小写敏感
顶层块关键字 —— ShaderShaderFunctionShaderLayerShaderLayerBlendMaterialLayerMaterialLayerBlendVirtualFunctionNamespaceFunctionGraphFunction
Section 名 —— PropertiesSettingsOutputsInputsResultsOptionsGraphCodeLayout
头部属性键 —— Name=Root=Asset=
Settings / Options 键、元数据键、UE.* 参数键、Expression( … ) 参数键、Layout 参数键否 —— 而且存储时会转成小写
类型 token —— float3vec3Texture2DScalarParameter
Properties 中的 const 前缀
Inputs 中的 opt 前缀
Function 之后的 SelfContained / Inline
in / out 参数限定符
默认值里的 true / false
Path( 资产引用关键字
Base. 绑定前缀与 Expression(
.Pin[ 引脚选择器
Group("…") 属性分组头
Slider( 元数据简写
UE. 内置前缀
#Region / #EndRegion
Node( / Comment( layout 调用
import
调用参数哨兵 default
.dsm / .dsf / .dsh 扩展名

所以 shader(Name="X")SHADER(Name="X") 都是语法错误,而 properties = { … }settings { domain = "ui"; }Shader(name="X") 都能被接受。

关键字匹配还要求右侧词边界:关键字后面的字符不能是字母、数字或 _。正是这条规则让 ShaderFunction 不会被当成 ShaderShaderLayerBlend 不会被当成 ShaderLayer

块关键字写错大小写不会被报成"大小写错误"。它根本匹配不上任何顶层关键字,失败信息是 Unexpected token near index {Index}.

语法概览

<token> := <identifier> | <keyword> | <string-literal> | <numeric-text> | <punctuation>

<identifier>     := { <letter> | _ } { <letter> | <digit> | _ }…
<qualified-name> := <identifier> :: <identifier>
<string-literal> := " { <character> | \<escape> }… "
<punctuation>    := { | } | ( | ) | [ | ] | ; | = | , | . | :: | #
记号含义示例
<x>占位符——替换成实际内容,尖括号本身不写出来。Name = <string>
[ x ]可选——整段可以整体省略。[, Root = <string>]
{ a | b }多选一——从竖线分隔的写法里取其中一个。{ Node( … ) | Comment( … ) }
可重复——前一项可以出现任意多次。<property-declaration> …

空白和注释可以出现在任意两个 token 之间,除此之外没有意义。

空白

引擎认定为空白的任何字符都能分隔 token:空格、制表符、回车、换行以及其他 Unicode 空白字符。换行在 声明语法里没有任何语法作用 —— 只有面向行的处理才关心它: import 指令、Graph 体内的 #Region 指令,以及诊断的行号。

有两种语句形式是按字面空格字符切分的,而不是按空白类。类型和名字之间写制表符会变成语法错误。见 语句切分

注释

形式规则
// …行注释;一直到下一个换行为止,不含换行本身
/* … */块注释;在第一个 */ 处结束
  • 块注释不嵌套。 /* a /* b */ c */ 在第一个 */ 处结束,后面的 c */ 又变回代码。
  • 未闭合的块注释会被静默接受。 只有 /* 没有 */ 会吞掉文件剩余部分,且不产生任何诊断。相比之下 未闭合的 { 块会失败于 Unterminated block.
  • 注释在声明语法的每个位置都被同样识别,包括统计 {}()[] 嵌套时,所以注释里的花括号或引号 永远不会让块失衡。
  • PropertiesSettingsOutputsLayout 和类型化参数 section 里切分语句之前,还会再做一次 文本级去注释。那一遍是感知字符串的,并且保留终止行注释的那个换行,所以行号不会错位。
  • 声明解析器不会去掉 Graph 体里的注释。 Graph 块中的注释原样和函数体一起存下来,之后交给表达式 分词器处理。
  • 声明层面没有 # 预处理器。#Region / #EndRegion 只在 Graph 体内被识别 —— 见 Layout 与 #Region

注释不能把文本藏起来躲开文件类型扫描,也躲不开 import 行扫描。把 .dsh 里的 Shader( 块注释掉, 它照样报错;用 /* … */ 注释掉一批 import,它们照样被导入。见 文件模型import 与命名空间

标识符

位置可用字符
首字符字母或 _
后续字符字母、数字或 _

没有长度上限,而且没有任何关键字对标识符是保留的:叫 ShaderGraphfloat 的属性、变量、 参数、函数都不会被拒绝。至于这个名字之后能不能解析成功,那是它所处上下文的事。

InputsOutputsResultsLayout 调用里的声明名字有更严的检查:整个去空白后的 token 必须 符合标识符规则。Properties 里的名字只要求非空,所以 Properties { float 1Bad = 0; } 能解析通过 —— 它只是永远无法从 Graph 里被引用。

带命名空间的名字用 ::,例如 Common::ApplyTint。见 函数

名字净化

名字进入生成的 HLSL 时会被净化:A–Z a–z 0–9 _ 之外的每个字符变成 _,开头是数字则加一个 _ 前缀, 连续的 __ 塌缩成一个,结果为空或全是下划线时变成 DreamShaderSymbol

Common::ApplyTint   →   Common__ApplyTint   →   Common_ApplyTint

正是这次塌缩,让 Common::ApplyTint 和顶层的 Common_ApplyTint 在生成代码里撞名。

字符串字面量

带引号的值以 " 开始,到下一个未转义的 " 结束,读入时完成反转义。反斜杠总是吞掉紧随其后的那个字符, 所以 \" 永远不会终结字面量。

任何可以写引号的地方 —— settings 值、元数据值、属性值、参数值 —— 引号都是可选的。原始文本先被去掉 首尾空白,只有当结果至少两个字符长、且首尾都是 " 时才剥掉引号。所以 Domain = UI;Domain = "UI"; 等价。

转义产生
\n换行
\r回车
\t制表符
\""
\\\
\<any other character>那个字符本身;反斜杠被丢弃,没有诊断
文本末尾孤零零的一个 \一个字面反斜杠

没有数字转义。\0\xNN\uNNNN 都落进"其他字符"那一行,所以 \0 得到 0\x41 得到 x41

Graph 表达式语法使用同样的五个转义和同样的透传规则;区别是那里未闭合的字符串会在输入结束时静默闭合, 而不是报错。

SettingsOptions 的值在剥掉引号之后不会再次去空白,所以 Domain = " UI "; 存下来的是带空格 的 UI。元数据的值在反引号处理后再去一次空白。"…" 里的裸换行和其他字符一样被吞掉。

数字字面量

声明语法和 Graph 表达式语法在这里并不一致,这个差异是常见的意外来源。

声明语法

声明语法没有数字 token。值以原始文本捕获,只有当某个消费者要一个数字时才转换。

消费者接受什么
标量默认值引擎 double 转换能接受的任意文本,外加 true1.0false0.0
整数参数引擎 int32 转换能接受的任意文本
布尔默认值严格的 truefalse
向量默认值<anything>( <part> [, <part>]… ) —— 见下

底层转换非常宽容:只有当解析结果为零时它才回头复核文本。下面这些全都是静默的。

写法解析为诊断
float Strength = 1.0f;1.0
float Strength = 1abc;1.0
float Strength = 0.0f;0.0
float Strength = abc;Invalid scalar default value 'abc' for property 'Strength'.

没有十六进制、八进制、二进制字面量形式。0x1F 和其他文本一样被交给同一个宽容转换。

向量字面量的形式故意写得很松。第一个 (最后一个 ) 界定分量,而 ( 之前的文本被完全忽略 —— float3(1,0,0)vec3(1,0,0)(1,0,0)Nonsense(1,0,0) 解析结果完全一样。内部按 , 切分且 不跟踪嵌套,所以某个分量里再嵌一层调用就会切错。每个分量可以是数字,也可以是 true / false

分量个数结果
1(a, a, a, 1) —— 铺满 x、y、z
2(a, b, 0, 0)
3(a, b, c, 1)
4 或更多取前四个,多出来的会被解析然后丢弃

未填充时的默认值是 (0, 0, 0, 1)

Graph 表达式语法

Graph 块里,数字是真正的 token。

元素规则
起始一个数字,或者紧跟数字的 . —— 所以 .5 合法
主体数字和 .;词法上允许出现多个 .,之后在转换阶段失败
指数最多一个 eE,后面可跟 +-
后缀f F h H u U l L 之中恰好一个
十六 / 八 / 二进制不支持 —— 0x1F 词法上是数字 0 后面跟标识符 x1F

只有当后缀后面的字符不是字母、数字或 _ 时后缀才会被消费,而且它不进入 token 文本0.55f 就是 数字 0.55。因为只消费一个后缀,1.0ul 会被读成数字 1.0 后面跟标识符 ul

表达式分词器接受这些单字符 token:( ) , . + - * / =,外加双字符的 ::

其他任何字符 —— 包括单独的 :{}[]<>%!&| —— 都会终止 表达式。 如果此时还在等一个值,解析失败于 Unexpected token '{Token}' in Graph expression.;如果 表达式已经完整,剩下的文本不带任何诊断地被丢弃。见 不支持的写法

语句切分

section 的内容是一串以 ; 分隔的语句。

  • ; 只在圆括号深度 0 方括号深度 0 时才分隔语句,字符串字面量内部永远不分。
  • 通用切分器不跟踪花括号Properties 有自己的感知花括号的遍历器,才让 Group("…") { … } 生效; 其余每个 section 都把 { 当普通字符。
  • 空语句被丢弃,所以多余的 ;; 无害。
  • 最后一条语句即使没有终止符也会被提交 —— 块内最后一个 ; 是可选的
  • section 闭合 } 之后的 ; 是可选的,section 名和它的块之间的 = 也是可选的 since 1.5.0

随后有三种切分器把一条语句拆成几部分,它们对"什么算空白"的看法并不一致

切分使用者规则
深度 0、字符串外的第一个 =SettingsOptions、元数据条目、OutputsLayout 参数、UE.* 参数Color = (R=1,G=0,B=0); 在外层 = 处切分
深度 0 的最后一个空白Properties 声明制表符没问题;UE.TexCoord(Index = 0) UV 切成类型 UE.TexCoord(Index = 0) 和名字 UV
最后一个字面空格InputsOutputsResults 的类型化参数,以及 Shader 的输出声明既不感知深度,也不感知空白类
任意一段空白Function / GraphFunction 参数列表每个参数必须切出恰好 2 或 3 个 token

制表符只在两个地方咬人。InputsOutputsResultsShaderOutputs 里写 float3<TAB>Color; 会失败于 Invalid typed declaration '{Statement}'. 同样的声明在 Properties 里 是合法的,因为它按任意空白切分。

可选的 opt 前缀被识别为 opt 加一个空格opt<TAB>float X 不会把输入标记为可选 —— 它产出的是 一个必需输入,类型 token 是 opt + 制表符 + 真正的类型,随后在生成阶段以"不支持的类型"失败。

参数列表按 , 切分,深度和字符串跟踪规则相同,所以不带引号的属性值里不能含 ,)Root=Game 可以,Name=Foo(1,2) 不行。

示例

// 顶层声明之前的行注释。
Shader(Name="Materials/M_Comments")
{
    /* 跨多行的
       块注释。 */
    Settings = {
        Domain = "UI";        // 行尾注释
        ShadingModel = Unlit; // 引号可选
    }

    Properties {
        // Properties 的类型和名字之间可以用任意空白。
        ScalarParameter Rough = 0.5 [Group="Surface"; Slider(0, 1)];
        vec3            Tint  = vec3(1.0, 0.4, 0.1);
        float           Fudge = 1.0f;   // 解析为 1.0 —— 后缀被忽略,不是被拒绝
    }

    Outputs {
        vec3 Color;                     // 这里必须是字面空格
        Base.EmissiveColor = Color
    }                                   // 块内最后一个 ';' 可省略

    Graph {
        vec2 UV = UE.TexCoord(Index = 0);
        Color = vec3(Rough, Rough, UV.x) * Tint;
    }
}

诊断

消息触发原因处理
Unexpected token near index {Index}.该位置没有匹配上任何顶层关键字 —— 包括大小写写错的正确关键字,以及被直接交给解析器的 import 行。检查块关键字的大小写。 详解
Expected identifier near index {Index}.此处需要标识符,但下一个字符不是字母或 _。
Expected '{Char}' near index {Index}.缺少一个必需的标点字符。
Unterminated block.在匹配的 } 之前就到了输入末尾。
Unterminated '{Char}' block.在匹配的定界符之前就到了输入末尾,例如未闭合的参数列表 (。
Unterminated string literal.在带引号的属性值内部到达输入末尾。
Expected value near index {Index}.属性值为空。
Expected ',' or ')' near index {Index}.属性列表格式错误。
Invalid typed declaration '{Statement}'.类型和名字之间没有字面空格、类型为空,或名字不是合法标识符。把类型和名字之间的制表符换成空格。 详解
Invalid scalar default value '{Value}' for property '{Name}'.文本无法转换成数字,且没有被解析成零。
Invalid vector default value '{Value}' for property '{Name}'.某个分量既不是数字也不是 true / false。
Invalid boolean default value '{Value}' for property '{Name}'.写了 true / false 之外的文本。
Unexpected token '{Token}' in Graph expression.在需要值的位置出现了 token,包括表达式分词器字符集之外的任何字符。详解

只有以 near index {Index} 结尾的消息才带位置,诊断映射器才能把它变成文件、行、列。见 import 与命名空间

继续阅读

本页目录