- 编译器
- 图形学
- 编程语言
【免费下载链接】slang
Making it easier to work with shaders
本文是 Slang 着色器语言编译器中词法分析器(Lexer)所产出的全部 Token 的分类目录,面向两类读者:一是打算扩展slang-lexer.cpp的编译器开发者,二是编写消费 Slang 源码的 IDE、LSP、格式化器等工具的开发者。读完本文,你将掌握 Slang 词法层 50 余种 Token 的完整清单、每一类 Token 在 source/compiler-core 中的精确源码出处、数字字面量后缀与十六进制浮点等"解码期才定语义"的设计,以及反斜杠续行、原始字符串、#INF等特殊词法规则的确切行为。本文档在仓库中还经过了一轮与词法器源码的差距审查(gap-intake,修复了 10 处文档与实现不一致的问题),因此文中的每一条结论都可以直接对应到具体源码行与测试用例。
目录来源与源码映射
该 Token 目录是从 Slang 编译器源码反向梳理(reverse-engineered)而来,主要依据以下五个文件,读者可按图索骥:
| 源码文件 | 提供的信息 |
|---|---|
| source/compiler-core/slang-token.h | Token结构、TokenType枚举、TokenFlags位掩码定义 |
| source/compiler-core/slang-token-defs.h | 用 X-macro 列出的每一个TokenType值,被slang-token.h等多处包含 |
| source/compiler-core/slang-token.cpp | TokenTypeToString,把同一份定义列表展开为诊断消息中的 Token 拼写 |
| source/compiler-core/slang-lexer.h / source/compiler-core/slang-lexer.cpp | 真正产出这些 Token 的词法分析器 |
| source/slang/slang-preprocessor.cpp | 读取 Token 标志位的消费者,以及HandleIncludeDirective中尖括号 include 路径的组装逻辑 |
其中 slang-token-defs.h 里PUNCTUATION(id, text)宏会展开为TOKEN(id, "'<text>'"),这意味着每一个标点种类既是TokenType枚举值,又是诊断消息中使用的字面拼写——例如PUNCTUATION(OpAssign, "=")展开后描述字符串为'='。这让词法错误诊断(如"期望'='")与 Token 类型天然保持一致。
Token 种类分类法
Slang 词法层把 Token 分为五组:结束标记(end markers)、内容 Token(content tokens,即字面量与标识符)、琐碎 Token(trivia,空白与注释)、预处理标记(preprocessor markers)、标点与运算符。词法分析器核心是一个按字符分发的自由函数_lexTokenImpl(slang-lexer.cpp 约第 1966 行),由成员函数Lexer::lexToken(约第 2473 行)包装:后者负责附加 Token 标志位,并返回已经折叠掉行续接符的 Token 文本。行续接的折叠发生在字符级辅助函数_peek(越过续接符向后看)与_advance(消费一个续接符并记录ScrubbingNeeded标志)中。
结束标记与特殊 Token
| TokenKind | 词法器源码出处 | 说明 |
|---|---|---|
Unknown | 默认构造的Token | 合法输入中不应出现 |
EndOfFile | _lexTokenImpl的kEOF分支(slang-lexer.cpp 约第 1974 行) | 词法器到达输入末尾时返回 |
Invalid | _lexTokenImpl末尾的 fall-through 块(slang-lexer.cpp) | 词法器遇到没有任何分发分支匹配、且不是非 ASCII 码点(非 ASCII 会被折叠进标识符)的字符时产生。它按字符值在illegalCharacterPrint、unexpectedEndOfInput、illegalCharacterHex三种诊断间选择,且当设置了kLexerFlag_SuppressDiagnostics时(见 slang-lexer.h 中Lexer::getDiagnosticSink)直接跳过诊断。无论是否报错,InvalidToken 都会被产出 |
内容 Token(字面量与标识符)
| TokenKind | 词法器源码出处 | 说明 |
|---|---|---|
Identifier | slang-lexer.cpp 中的标识符规则 | 包含所有关键字;关键字与普通标识符的分类被推迟到解析器,通过 syntax-decl 查表完成 |
IntegerLiteral | 整数字面量规则 | 十进制、0x/0X十六进制、0b/0B二进制,或前导零的八进制数字串。任何尾随后缀都属于 Token 原始文本的一部分,见下文 |
FloatingPointLiteral | 浮点字面量规则 | 接受的形态见下方展开;同样,尾随后缀属于 Token 原始文本 |
StringLiteral | _lexStringLiteralBody(lexer, '"')(约第 2172 行)/_lexRawStringLiteralBody(约第 2166 行) | Token 原始文本包含开闭引号;转义序列的解码与校验推迟到getStringLiteralTokenValue |
CharLiteral | _lexStringLiteralBody(lexer, '\'')(约第 2177 行) | 单引号字符字面量。词法器只负责找到闭合引号;"必须恰好一个字符"的规则由解码期函数getCharLiteralValue(第 1660-1725 行)强制 |
数字字面量接受形态与"词法期无固定后缀集"设计
词法器对数字后缀采取宽松策略。_maybeLexNumberSuffix(slang-lexer.cpp)会把数字主体之后的任意ASCII 字母、数字、下划线序列都并入 Token 原始文本,含义留给消费者决定——源码注释明确写道"be liberal in what we accept here, so that figuring out the semantics of a numeric suffix is left up to the parser"。因此词法期不存在固定的后缀集合,接受哪些后缀、拒绝哪些后缀并报错,都属于解码辅助函数而非词法器本身:
- 整数:
getIntegerLiteralValue(第 789-838 行)读取基数前缀与数字串,把未消费的尾巴作为后缀交还给调用者。它只诊断溢出——LexerDiagnostics::integerLiteralTooLargeForAnyType(E10012,见 slang-lexer-diagnostic-defs.h,定义为Error级别)——当数字无法放进 64 位时触发。真正接受后缀的是其调用者parseIntegerLiteralExpr(source/slang/slang-parser.cpp 第 8614-8719 行,后缀循环在 8637-8686 行):接受u/U、l/L、ll/LL、z/Z,顺序任意,但ll的两个字母必须大小写一致;其余后缀报告Diagnostics::InvalidIntegerLiteralSuffix(invalid suffix '...' on integer literal)。这两条解码期诊断分别由 negative-integer-literal-too-large.slang 与 negative-invalid-int-suffix.slang 测试用例验证。 - 浮点:
getFloatingPointLiteralValue(第 1137-1391 行)接受空后缀或f/F(对应float)、h/H/hf/HF/fh/FH(对应half)、l/L/lf/LF/fl/FL(对应double),其余作为FloatingPointLiteralType::BadSuffix(第 1273-1287 行)。单字母后缀大小写不敏感;双字母后缀要求两个字母同大小写,因此hf、HF合法而Hf非法。对应测试如 numeric-suffix-f-is-float.slang、numeric-suffix-h-is-half.slang、numeric-suffix-lf-is-double.slang、numeric-suffix-ull-is-uint64.slang 逐一验证了这些映射。
浮点字面量的接受形态(FloatingPointLiteralNotes 列)包括:1.5(普通小数)、1.(尾随点)、.5(前导点)、1e10/0e-3(十进制指数)、0x1.8p3(十六进制有效数字配二进制p指数),以及遗留的1#INF/0#INF无穷大形式。一个值得注意的例外:点后紧跟x或r不会开始小数部分——该拼写保留给对标量字面量的 swizzle(如1.xxx),因此1.xxx会被词法切分为IntegerLiteral、Dot、Identifier三个 Token(见 slang-lexer.cpp)。
琐碎 Token(空白与注释)
词法器把这些作为独立 Token 产出,让预处理器与解析器自行决定是否跳过;大多数解析层在遍历 Token 流时会将其过滤掉。
| TokenKind | 词法器源码出处 | 说明 |
|---|---|---|
WhiteSpace | 空白规则 | 空格 / 制表符的连续序列 |
NewLine | 行尾规则 | 逻辑行终止符(在反斜杠续行折叠之后) |
LineComment | //规则 | // ...到行尾 |
BlockComment | /* ... */规则 | 注意Slang 不支持嵌套块注释 |
预处理标记
| TokenKind | 词法器源码出处 | 说明 |
|---|---|---|
Pound | #标点 | 预处理指令前缀 |
PoundPound | ##标点 | 预处理 Token 粘贴(token paste) |
CompletionRequest | #?分支(slang-lexer.cpp 约第 2367 行) | #?;在光标位置发出,用于请求补全 |
标点与结构符号
按拼写列出,每个都经由_lexTokenImpl中的逐字符switch分发:
| TokenKind | 词法器源码出处 | 说明 |
|---|---|---|
Semicolon | ;标点 | |
Comma | ,标点 | |
Dot | .标点 | |
DotDot | ..标点 | 被词法为独立种类;当前提交没有解析器消费者 |
Ellipsis | ...标点 | 被预处理器用于变参宏参数 |
LBrace/RBrace | {/} | |
LBracket/RBracket | [/] | |
LParent/RParent | (/) | |
Colon | :标点 | |
Scope | ::标点 | 命名空间 / 限定名分隔符 |
QuestionMark | ?标点 | 条件 / 可选 |
RightArrow | ->标点 | 函数返回类型、通过指针访问成员 |
DoubleRightArrow | =>标点 | Lambda 语法(其唯一解析器消费者) |
At | @标点(slang-lexer.cpp) | 被词法为独立种类;当前提交没有解析器消费者(与DotDot行的措辞一致) |
Dollar | $标点(slang-lexer.cpp) | 在spirv_asm块内作为 Slang 值操作数的前缀(如$this、$location,见 source/slang/hlsl.meta.slang 第 1215 行);也引入编译期$for语句 |
DollarDollar | $$标点(slang-lexer.cpp) | 在spirv_asm块内作为 Slang类型操作数的前缀(如result:$$float2,同见 hlsl.meta.slang 第 1215 行) |
$与$$的区分示例来自spirv_asm内联汇编的真实使用:
result:$$float2 = OpImageQueryLod $this $location其中$$float2是类型操作数、$this与$location是值操作数。
运算符
赋值、算术、比较、逻辑与位运算符构成最后一组。词法器采用最长匹配(maximal munch),且该规则通过嵌套前瞻实现而非查表:外层逐字符switch的某个分支先消费其字符,再对_peek再次switch,只有default:分支才返回较短的种类。>前缀家族是教科书式的例子(slang-lexer.cpp):
- 第二个
>被消费后,若紧跟=则产出OpShrAssign,所以>>=是单个 Token; - 没有
=时内层分支回退为OpRsh; - 第一个
>后直接是=产出OpGeq; - 其余情况产出
OpGreater。
<、=、+、-、|、&、!、%、*、^、/、.、:、#各分支都以同样方式解析。这一选择不依赖任何解析上下文;唯一的例外是OpLess,见下方表格。
| TokenKind | 词法器源码出处 | 说明 |
|---|---|---|
OpAssign | = | |
OpAdd/OpSub/OpMul/OpDiv/OpMod | +/-/*///% | |
OpNot | ! | 逻辑非 |
OpBitNot | ~ | 按位非 |
OpLsh/OpRsh | <</>> | |
OpEql/OpNeq | ==/!= | |
OpGreater/OpGeq/OpLeq | >/>=/<= | |
OpLess | < | 与泛型实例化的歧义由解析器消除,见 docs/generated/design/pipeline/02-parse-ast.md |
OpAnd/OpOr | &&/\|\| | 逻辑与 / 逻辑或 |
OpBitAnd/OpBitOr/OpBitXor | &/\|/^ | 按位与(兼作取地址)/ 或 / 异或 |
OpInc/OpDec | ++/-- | |
OpAddAssign...OpXorAssign | +=-=*=/=%=<<=>>=&=\|=^= | 复合赋值全家族 |
一个仅能从解析器源码看到、刻意未写进文档的补充事实:解析器在闭合嵌套泛型参数列表时,会把已被词法切为OpRsh的>>重新拆开(slang-parser.cpp 第 2900-2916 行)——这正是上面最大吞噬段落的最佳注脚:词法层贪心,解析层兜底。
Token 数据布局
Token结构定义于 slang-token.h:
class Token { public: TokenType type = TokenType::Unknown; TokenFlags flags = 0; SourceLoc loc; uint32_t charsCount = 0; union CharsNameUnion { const char* chars; Name* name; }; CharsNameUnion charsNameUnion; // ... };charsNameUnion是一个带标签的联合(tagged union):当Name标志位被设置时,Token 文本被驻留(intern)为Name*(用于标识符与关键字);否则 Token 持有指向原始源缓冲区中一段文本的裸指针加长度。
Token 标志位
TokenFlag(声明于 slang-token.h)是一个位掩码,记录词法属性:
| 标志位 | 含义 |
|---|---|
AtStartOfLine | Token 是某个逻辑行的第一个 Token——在发出NewLineToken 后被设置,并跨越中间的空白与注释保持;因此被折叠的转义换行(不发出NewLine)不会开启新行(预处理器用它识别指令) |
AfterWhitespace | Token 前面有空白;预处理器读取它来在字符串化(stringizing)时保留空格,并区分函数式宏定义(NAME(无间隔)与对象式宏定义 |
ScrubbingNeeded | 词法本 Token 时折叠过一个行续接符;Lexer::lexToken用该标志把续接符从存储内容中清理掉 |
Name | 判别chars/name联合 |
AtStartOfLine的直接后果是预处理指令体可以续行到下一物理行:指令体一直运行到下一个NewLineToken(IsEndOfLine,slang-preprocessor.cpp 第 2700-2712 行),而折叠后的续行不会产生NewLineToken,所以下面这个宏的宏体是完整的((x) + 1):
#define BUMP(x) \ ((x) + 1)指令识别本身也是该标志位的消费者:只有当PoundToken 携带AtStartOfLine时才启动一条指令(slang-preprocessor.cpp 第 4833 行)。这个双物理行#define的行为由 line-continuation-in-macro-definition.slang 测试验证。
特殊词法规则
slang-lexer.cpp 实现了若干上下文敏感规则:
反斜杠行续接
紧邻换行前的\被_advance消费并折叠掉,但所得 Token 的源码位置仍指向原始物理行。ScrubbingNeeded标志被设置,Lexer::lexToken据此把续接符从存储内容中剥离。
折叠发生在注释识别之下:_lexLineComment(slang-lexer.cpp)只在_peek报告换行时结束注释,而_peek(第 226-305 行)在返回前已经越过了续接符。因此以\结尾的//注释会把下一物理行一并吞掉:
// this comment continues with a backslash \ int swallowed = 0;上面的int swallowed = 0;会被注释吞掉。该行为由 line-comment-backslash-extends.slang 测试验证:测试中注释后的第一行声明被消费,只有再下一行的real成为真正的声明并被打印(CHECK: real=21)。
#include后的<...>
词法器没有 include 头文件模式:它照常发出OpLess、路径、OpGreaterToken。HandleIncludeDirective(slang-preprocessor.cpp)把<与>之间各 Token 的内容拼接起来重组成路径;引号形式则是一个完整的StringLiteral。
原始字符串字面量
以R"delimiter(打开的字符串只由)delimiter"闭合,delimiter任意。内部换行与反斜杠按字面处理,不做任何转义处理。实现位于_lexRawStringLiteralBody(slang-lexer.cpp,闭合分隔符终止检查在 1802-1812 行),由字符串分发处的R"分支(第 2166 行)调用。裸"作为分隔符会被拒绝,诊断LexerDiagnostics::quoteCannotBeDelimiter。
字符字面量
'引用的字面量主体由与字符串相同的_lexStringLiteralBody辅助函数词法化;该函数把闭合引号字符作为第二个参数(字符字面量传'\'',第 2177 行;字符串传'"',第 2172 行),而非单独的"单字符"标志。词法期间它只做定位闭合引号所需的最小转义处理——越过\'、\"、\\以防转义引号提前结束 Token——并且只报告两种导致根本找不到结尾的失败:LexerDiagnostics::endOfFileInLiteral与LexerDiagnostics::newlineInLiteral。
字面量的"值"语义全部推迟:单字符规则由getCharLiteralValue(第 1660-1725 行)强制,它对两种情形发出LexerDiagnostics::illegalCharacterLiteral:主体短到装不下字符(''),或解码后仍有未消费输入(多字符主体),两种情形都返回-1。转义序列——包括\u/\UUnicode 形式(位数错误时报invalidUnicodeStringEscape)——由共享的_decodeStringEscape解码;畸形的 UTF-8 主体报invalidUtf8ByteSequence。在上述所有情况下CharLiteralToken 依然被产出,失败只在消费者索取值时浮现。相应测试包括 negative-empty-char-literal.slang、negative-multi-char-literal.slang 与 negative-unicode-escape-digit-count.slang。
数字字面量后缀
后缀字符(u、l、f、h……)保留为字面量 Token 原始文本的一部分,Token 本身不记录请求的是哪种类型;解码是消费者的职责且发生在更晚阶段。
前导零的浮点延续
单独的0后跟十进制指数(0e10、0E5、0e+1、0e-3)或遗留的 MSVC 无穷大形式(0#INF),会被词法为FloatingPointLiteral,与1e10/1#INF形式一致。_lexTokenImpl中0分支的default:臂(slang-lexer.cpp 第 2059 行)在回退为IntegerLiteral之前先咨询_maybeLexNumberExponent(第 608 行);否则指数会被当整数后缀吞掉。同一分支的兄弟臂处理其他基数(0x/0X十六进制、0b/0B二进制,以及发出LexerDiagnostics::octalLiteral诊断后的前导数字串按 8 进制词法化)。
八进制诊断是警告而非错误——W10002,'0' prefix indicates octal literal,声明于 slang-lexer-diagnostic-defs.h(DIAGNOSTIC(10002, Warning, octalLiteral, ...)),在 slang-lexer.cpp 中于调用_lexNumber(lexer, 8)之前立即发出,因此词法不被它阻断:字面量仍按 8 进制词法与解码,八进制字面量可以正常编译。这由 negative-octal-literal-warning.slang 验证——测试中的 caret 跨度覆盖整个0777字面量且携带 10002 编号,证明该分支确实执行。
#INF的匹配先于任何基数检查(_maybeLexNumberExponent),且getFloatingPointLiteralValue在数字之后的文本一出现#INF就把值设为std::numeric_limits<double>::infinity()(slang-lexer.cpp)——#之前的数字从不被解析。因此0#INF与1#INF都解码为正无穷大。相关测试见 float-literal-zero-msvc-infinity.slang。
块注释处理
BlockCommentToken 覆盖整个/* ... */范围;不支持嵌套块注释。
标识符 / 关键字分类
每个关键字到达解析器时都是TokenType::Identifier。关键字地位由解析器 syntax-decl 表查表决定,详见 keywords-and-builtins.md。
源码位置(Source location)
每个 Token 的SourceLoc是一个 32 位整数,由SourceManager解码(slang-source-loc.h、slang-source-loc.cpp)。该整数是某一个SourceView的键,而非一对位置:解释方式由SourceLocType参数选择——Nominal(尊重#line指令与 source map)、Actual(忽略二者)、Emit(尊重#line但忽略 source map)。宏展开或其他派生视图的来源单独记录在SourceView自身上,用getInitiatingSourceLoc取回。
__LINE__是读取解码位置的用户级表面:预处理器展开它时对调用位置的发起位置调用SourceManager::getHumaneLoc(slang-preprocessor.cpp 第 2318-2348 行),而该方法的SourceLocType参数默认为Nominal(slang-source-loc.h 第 518 行,SourceLocType type = SourceLocType::Nominal)。因此#line指令——由HandleLineDirective经SourceView::addLineDirective记录(第 4233 行)——是用户可见的改变 Token 报告值的方式,既影响__LINE__也影响诊断行号。相关测试见 source-location-line-directive.slang。
本文档未覆盖的内容
- 关键字:每个关键字都以
TokenType::Identifier到达;分类与完整清单见 keywords-and-builtins.md。 - 语法产生式:见语法参考文档(grammar.md)。
此外,词法-预处理管线的整体流程(_lexTokenImpl→Lexer::lexToken→ 预处理器消费)在 docs/generated/design/pipeline/01-lex-preprocess.md 中有更宏观的叙述,可作为本文的上下游上下文。本文档配套的自动化测试全部位于 docs/generated/tests/design/syntax-reference/tokens/,可作为扩展词法器或编写消费工具的回归基线。
- 编译器
- 图形学
- 编程语言
【免费下载链接】slang
Making it easier to work with shaders
相关推荐
Slang 编译器 Token 参考:词法分析器 TokenKind 全量清单与 Token 标志位解析
Slang 编译器 Token 参考:词法分析器 TokenKind 全量清单与 Token 标志位解析 本文是面向 Slang 着色器语言编译器前端的一份 T
编译器图形学编程语言Slang 词法分析器 Token 完全参考:令牌目录、分词实现与文档验证工作流
Slang 词法分析器 Token 完全参考:令牌目录、分词实现与文档验证工作流 本篇技术指南围绕 Slang 编译器核心的词法分析(Lexer)展开,完整梳理
编译器图形学编程语言CPython 词法分析全解:从源码字符到 Token 流的语言参考指南
CPython 词法分析全解:从源码字符到 Token 流的语言参考指南 Python 程序在被真正解析执行之前,首先要经历 词法分析(lexical anal
编程语言语言运行时解释器标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考