Slang 词法分析器 Token 完全参考:Token 分类、源码映射与特殊词法规则
2026/9/19 21:41:42 网站建设 项目流程
  • 编译器
  • 图形学
  • 编程语言

【免费下载链接】slang

Making it easier to work with shaders

项目地址:https://gitcode.com/GitHub_Trending/sl/slang
点击查看免费下载

本文是 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.hToken结构、TokenType枚举、TokenFlags位掩码定义
source/compiler-core/slang-token-defs.h用 X-macro 列出的每一个TokenType值,被slang-token.h等多处包含
source/compiler-core/slang-token.cppTokenTypeToString,把同一份定义列表展开为诊断消息中的 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_lexTokenImplkEOF分支(slang-lexer.cpp 约第 1974 行)词法器到达输入末尾时返回
Invalid_lexTokenImpl末尾的 fall-through 块(slang-lexer.cpp)词法器遇到没有任何分发分支匹配、且不是非 ASCII 码点(非 ASCII 会被折叠进标识符)的字符时产生。它按字符值在illegalCharacterPrintunexpectedEndOfInputillegalCharacterHex三种诊断间选择,且当设置了kLexerFlag_SuppressDiagnostics时(见 slang-lexer.h 中Lexer::getDiagnosticSink)直接跳过诊断。无论是否报错,InvalidToken 都会被产出

内容 Token(字面量与标识符)

TokenKind词法器源码出处说明
Identifierslang-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/Ul/Lll/LLz/Z,顺序任意,但ll的两个字母必须大小写一致;其余后缀报告Diagnostics::InvalidIntegerLiteralSuffixinvalid 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 行)。单字母后缀大小写不敏感;双字母后缀要求两个字母同大小写,因此hfHF合法而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无穷大形式。一个值得注意的例外:点后紧跟xr不会开始小数部分——该拼写保留给对标量字面量的 swizzle(如1.xxx),因此1.xxx会被词法切分为IntegerLiteralDotIdentifier三个 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)是一个位掩码,记录词法属性:

标志位含义
AtStartOfLineToken 是某个逻辑行的第一个 Token——在发出NewLineToken 后被设置,并跨越中间的空白与注释保持;因此被折叠的转义换行(不发出NewLine)不会开启新行(预处理器用它识别指令)
AfterWhitespaceToken 前面有空白;预处理器读取它来在字符串化(stringizing)时保留空格,并区分函数式宏定义(NAME(无间隔)与对象式宏定义
ScrubbingNeeded词法本 Token 时折叠过一个行续接符;Lexer::lexToken用该标志把续接符从存储内容中清理掉
Name判别chars/name联合

AtStartOfLine的直接后果是预处理指令体可以续行到下一物理行:指令体一直运行到下一个NewLineTokenIsEndOfLine,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::endOfFileInLiteralLexerDiagnostics::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。

数字字面量后缀

后缀字符(ulfh……)保留为字面量 Token 原始文本的一部分,Token 本身不记录请求的是哪种类型;解码是消费者的职责且发生在更晚阶段。

前导零的浮点延续

单独的0后跟十进制指数(0e100E50e+10e-3)或遗留的 MSVC 无穷大形式(0#INF),会被词法为FloatingPointLiteral,与1e10/1#INF形式一致。_lexTokenImpl0分支的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#INF1#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指令——由HandleLineDirectiveSourceView::addLineDirective记录(第 4233 行)——是用户可见的改变 Token 报告值的方式,既影响__LINE__也影响诊断行号。相关测试见 source-location-line-directive.slang。

本文档未覆盖的内容

  • 关键字:每个关键字都以TokenType::Identifier到达;分类与完整清单见 keywords-and-builtins.md。
  • 语法产生式:见语法参考文档(grammar.md)。

此外,词法-预处理管线的整体流程(_lexTokenImplLexer::lexToken→ 预处理器消费)在 docs/generated/design/pipeline/01-lex-preprocess.md 中有更宏观的叙述,可作为本文的上下游上下文。本文档配套的自动化测试全部位于 docs/generated/tests/design/syntax-reference/tokens/,可作为扩展词法器或编写消费工具的回归基线。

  • 编译器
  • 图形学
  • 编程语言

【免费下载链接】slang

Making it easier to work with shaders

项目地址:https://gitcode.com/GitHub_Trending/sl/slang
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询