Slang 编译器词法分析与预处理阶段深度解析:从源码字节流到扁平 Token 列表
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
本篇技术指南聚焦 Slang 着色器编译器编译流水线的第一阶段——词法分析(Lexing)与预处理(Preprocessing)。该阶段负责把源码缓冲区转换为解析器可直接消费的扁平Token数组,涵盖字节编码解码、字面量扫描与取值分离、宏展开、条件编译指令、#include解析以及宏展开后的源码位置保留策略。读完本文,你将掌握 Slang 编译器第一阶段的完整实现脉络、每条核心指令与诊断的具体行为边界,以及如何在 docs/generated/design/pipeline/01-lex-preprocess.md 对应的源码与测试资产中定位并验证这些行为。
第一阶段全景:从源码字节流到扁平 Token 列表
整个编译流水线以 "Lex / Preprocess → Parse → Semantic check → AST to IR → IR passes → Emit" 的顺序推进(参见 docs/generated/design/pipeline/overview.md),而词法与预处理是这条流水线的起点。
输入:SourceFile::decodeContentBlob与编码归一化
该阶段的输入是一个源码缓冲区(通常由 include 系统从磁盘加载),外加当前激活的Linkage配置(预定义宏、include 搜索路径)。原始文件字节通过SourceFile::decodeContentBlob转换为源码文本,其核心行为(实现在 source/compiler-core/slang-source-loc.cpp):
- 剥离开头的 Unicode 字节序标记(BOM);
- 将非 UTF-8 编码解码到一块全新缓冲区;
- 无 BOM 的 UTF-8 文件则原样透传。
SourceFile::setContents也经由同一辅助函数路由,因此词法器看到的永远是"无 BOM 的 UTF-8"文本。从源码结构看,CharEncoding::determineEncoding/getEncoding(type)->decode(见 source/compiler-core/slang-source-loc.cpp)负责编码判定,具体支持的编码集合定义在 source/core/slang-char-encode.cpp,这也正是文档维护过程中被标记为"待补充"(deferred)的一个缺口:文档层面尚未逐一列出decodeContentBlob识别的编码名。
输出:完全展开的TokenList,不做流式输送
输出是一个完全展开的TokenList——即所有#include、宏展开、条件预处理指令都已解析完毕的Token序列(TokenList类型见 source/compiler-core/slang-lexer.h)。需要特别强调的是:
预处理器不会向解析器流式推送 token。第一阶段必须完整跑完并产出扁平 token 列表,第二阶段(解析)才开始。
正是这种解耦,让解析器可以使用任意长度的前瞻(lookahead)而无需与预处理逻辑交错。相关测试资产见 docs/generated/tests/design/pipeline/01-lex-preprocess/README.md。
词法分析器(Lexer)实现与数据模型
源码文件布局
词法器实现位于 source/compiler-core 目录下,核心文件包括:
- slang-lexer.h / slang-lexer.cpp ——
Lexer结构体及其initialize/ 词法驱动循环; - slang-token.h / slang-token.cpp ——
Token、TokenType、TokenFlags、TokenList、TokenSpan、TokenReader等数据类型; - slang-token-defs.h —— 用 X-macro 列出全部
TokenType值的目录文件。
完整的 token 种类目录(含分类)位于 docs/generated/design/syntax-reference/tokens.md,本文不重复罗列。
Token 数据模型
每个 token 携带四类信息:
| 字段 | 说明 |
|---|---|
TokenType | 一个字节,通过 slang-token-defs.h 中的 X-macro 声明 |
| 原始文本 | charsCount加上一个联合体:要么指向原始字符,要么指向驻留(interned)的Name* |
SourceLoc | 单个 32 位整数,由 slang-source-loc.h 解码 |
TokenFlags | 一个小字节,含AtStartOfLine、AfterWhitespace、ScrubbingNeeded、Name(后者用于区分联合体) |
源码位置编码
源码位置被压缩为单个uint32_t,由SourceManager(slang-source-loc.h、slang-source-loc.cpp)在需要时按需解码回文件/行/列。这样每个 token 只需携带一个廉价字段,而不是三个。SourceLoc的拷贝构造函数被显式声明为= default而非手写,从而保证该类型在嵌入联合体时仍保持平凡可拷贝(trivially copyable)。
LexerFlags与特殊规则
LexerFlags(声明于 slang-lexer.h)当前包含kLexerFlag_SuppressDiagnostics,用于在遇到非法或不支持的字符时不产生诊断——典型场景是在非激活(inactive)的#if块内部进行分词。该标志通过Lexer::getDiagnosticSink生效:当标志被设置时,该访问器返回 null 而非真实 sink,于是所有经由该访问器的扫描期诊断点都会被静默。
例外情况:畸形 UTF-8 诊断绕过这一抑制机制——它直接通过lexer->m_sink上报,因此在非激活分支内依然会触发。
词法器还处理若干 C 语言遗留特性:
- 反斜杠续行:
\<newline>被折叠,但结果 token 的源码位置仍映射回原始的物理行; - 标识符与关键字不做区分:每个关键字 token 都以
TokenType::Identifier到达解析器(见 slang-token-defs.h 的 X-macro 列表),关键字状态由解析器通过查找解析,详见 docs/generated/design/pipeline/02-parse-ast.md; - 数值、字符串、字符字面量的文本原样保留在 token 的原始文本中:词法器只扫描字面量的边界,不解码其值,取值延迟到下述辅助函数;
- 非 ASCII 输入按 UTF-8 解码:字节级
_advance辅助函数(slang-lexer.cpp)把一个合法的多字节序列折叠成单个码点,供标识符规则接受。畸形序列会被诊断为invalidUtf8ByteSequence(以十六进制格式化出错字节),然后替换为一个空格——坏字节终结当前 token,而不会破坏后续扫描。_advance停在第一个非法的续接字节之前而非消费它,使恢复点保持在下一个字符的起始处。
字面量扫描与取值分离
扫描期只做纯语法检查(严重级别:警告 vs 错误)
词法器把字面量的扫描(记录其边界为原始 token 文本)与解码(求值)分离,且这种分离几乎是干净的:扫描只做找到字面量结尾所需的最小工作,外加少量纯语法检查——invalidDigitForBase、octalLiteral、quoteCannotBeDelimiter。三者严重级别并不相同(这一细节由 gap-intake 质量审计明确补全):
| 检查 | 严重级别 | 行为 |
|---|---|---|
octalLiteral(如017的0前缀) | 警告(W10002) | 扫描继续进入_lexNumber(lexer, 8),017依然产出可用的八进制字面量 token(slang-lexer.cpp 第 2097 行) |
invalidDigitForBase(超出进制的数字) | 错误(E10003) | 第 551 行 |
quoteCannotBeDelimiter(原始字符串定界符中出现引号) | 错误(E10010) | 第 1797 行 |
对应测试 octal-literal-scan-warning.slang 用DIAGNOSTIC_TEST:SIMPLE将警告钉死在 W10002,并断言 caret 覆盖整个字面量 token:
static const int legacy = 017; //CHECK: ^^^ '0' prefix indicates octal literal // CHECK: W10002字符串 / 字符 / 原始字符串的扫描
扫描发生在词法驱动循环中。_lexStringLiteralBody(lexer, char quote)(slang-lexer.cpp 第 1731 行)同时处理"..."和'...'——闭合引号字符是参数,因此不存在独立的字符字面量模式。它的唯一转义处理是跳过\'、\"、\\,使被转义的引号不会误终结 token;它不尝试识别八进制、十六进制或 Unicode 转义形式。因此它只会抛出两种"根本找不到结尾"的诊断:endOfFileInLiteral和newlineInLiteral。
原始字符串字面量由_lexRawStringLiteralBody(第 1782 行)单独扫描,它寻找匹配的)delimiter",且完全不进行转义处理——测试 raw-string-no-escape-processing.slang 验证了这一点。
解码:按需取值辅助函数
解码为值由 slang-lexer.h 中声明的辅助函数按需完成:
getIntegerLiteralValue——解析整数,可选返回后缀、十进制基数标志和溢出标志;getFloatingPointLiteralValue——解析浮点值,同时负责后缀分类:把 token 拆成数值部分与后缀部分,将后缀映射为FloatingPointLiteralType(h/hf/fh→Half,空后缀或f→Float,l/lf/fl→Double,大小写均可),并把解析值按该类型精度与范围取整。同一枚举还上报两种畸形情形BadSignificand与BadSuffix,outErrorContent携带出错的文本。全部四个出参(outLiteralType、outIsOutOfRange、outPrecisionLost、outErrorContent)都是不可省略的引用。其中outIsOutOfRange表示结果为0(下溢)或INFINITY(超过字面量类型最大值的上溢,而非超过double);outPrecisionLost仅对十六进制浮点且值在范围内时上报;getStringLiteralTokenValue(token, sink)——把转义解码为结果字节;getFileNameTokenValue是用于#include风格文件名的变体,不处理转义(对应测试 include-filename-escapes-not-processed.slang);getCharLiteralValue(token, sink)——返回 32 位码点,失败时返回-1(同时给出诊断)。该辅助函数独占"单字符规则":对太短无法容纳字符的''报illegalCharacterLiteral,当解码后仍有未消费输入时再次上报——这正是多字符字面量被捕获的方式(slang-lexer.cpp 第 1660-1725 行)。
空后缀分类为Float而非Double,因此后缀单独即可决定一个字面量是否可表示。设计文档给出了这个最反直觉的例子:
double a = 1e300lf; // Double: stays finite float b = 1e300; // Float: out of range, decodes to INFINITY1e300能放进double,但远高于float最大值,所以后缀单独改变了解码值(slang-lexer.cpp 第 1273-1284 行,float分支解析见第 1331 行)。边界测试 float-double-suffix-preserves-double-range.slang 用"有限值减半会改变自身、而 INFINITY 减半不变"的判定谓词同时验证两条路径:
double withSuffix = 1e300lf; printf("double-finite=%d\n", (withSuffix > 0.0 && withSuffix * 0.5 != withSuffix) ? 1 : 0); // 1 float noSuffix = 1e300; printf("float-inf=%d\n", (noSuffix > 0.0 && noSuffix * 0.5 == noSuffix) ? 1 : 0); // 1转义语法:_decodeStringEscape
字符串/字符辅助函数接收DiagnosticSink*,因为所有字面量取值错误都在解码时上报:越界的码元与码点、畸形转义语法、字符计数规则。转义处理集中在_decodeStringEscape(slang-lexer.cpp 第 1440 行),它接收 sink 与 token 的SourceLoc,自行诊断失败情形(未知转义字母、无数字或未终止的\x{...}/\u{...}形式、溢出值,对应invalidStringEscape;\u不是恰好 4 个十六进制数字或\U不是恰好 8 个,对应invalidUnicodeStringEscape),然后返回-1。
其文法遵循 docs/language-reference/expressions-literal.md:八进制\NNN、十六进制\xNN(任意位数)、Unicode 转义\uNNNN(4 位十六进制)、\UNNNNNNNN(8 位十六进制)以及带花括号的\u{...}(最长 32 位)。在字符串字面量中,\xNN映射为单个字节,而 Unicode 码点转义被编码为 UTF-8 字节序列(经encodeUnicodePointToUTF8)。getStringLiteralTokenValue对裸非转义字节直接逐字节复制、不复查 UTF-8 良构性,因为词法器的_advance已在扫描字面量体时诊断过畸形序列。在字符字面量中,主体字节会经getUnicodePointFromUTF8二次重新解码,畸形 UTF-8 会被再次拒绝,因此在字符字面量中\x与\u没有实际差别。
对应诊断(invalidUtf8ByteSequence、invalidStringEscape、invalidUnicodeStringEscape、outOfRangeCodeUnit、outOfRangeCodePointForUtf8)定义在 slang-lexer-diagnostic-defs.h。解码期错误上限测试资产覆盖了\x无数字(E10007)、\u位数不足(E10008)、越界码元(E10009)、多字符字面量(E10001)等边界。
词法器不做什么
- 不分类关键字(推迟到查找阶段);
- 扫描期间不求值数值字面量(取值是独立、按需的步骤);
- 默认不跳过空白与注释:
lexToken会把它们作为独立 token 类型发出,由lexAllSemanticTokens与预处理器的ReadAllTokens过滤,因此解析器永远不会看到它们(测试 comments-not-in-parser-token-list.slang)。
预处理器(Preprocessor)
预处理器实现在 source/slang/slang-preprocessor.cpp,公共接口在 slang-preprocessor.h。
PreprocessorDesc配置
Preprocessor通过PreprocessorDesc配置,其字段包括:
DiagnosticSink*——消息输出;NamePool*——标识符文本驻留;ISlangFileSystemExt*与SourceManager*——I/O;- 可选
IncludeSystem*——#include解析(slang-include-system.h); - 可选的
Dictionary<String, String>预定义宏; - 可选
PreprocessorHandler*——事件回调(翻译单元结束、文件依赖等,构建系统用它记录 include 依赖); - 可选
PreprocessorContentAssistInfo*——语言服务器在预处理时收集代码辅助信息用。
输入流栈
预处理器维护一个输入流栈:原始源文件位于栈底,#include的文件与宏展开压入其上。token 向上流动时经过指令识别;每个被#include的文件拥有自己的InputFile、自己的词法器、以及自己独立的Conditional状态栈,因此条件跳过与指令诊断都是按文件隔离的,不会扰动父文件。测试 include-conditional-stack-is-per-file.slang 验证:头文件里开启的#if在头文件文件尾被诊断,而父文件中的#endif没有可闭合的条件。
宏展开
Op 编译与参数回放
宏定义保存一份已经词法化的 token 序列,预先切分为MacroDefinition::Op条目;展开即"回放"这些 op。参数引用被编译为携带参数索引的参数 op(ExpandedParam、UnexpandedParam或StringizedParam)。调用时,MacroInvocation按索引回放匹配实参的 token 范围(_getArgTokens,见 slang-preprocessor.cpp),ExpandedParam情形下用ExpansionInputStream包裹,使实参 token 自身也参与宏展开——不存在按调用建立的伪宏环境。
相关功能测试覆盖:对象宏展开(object-macro-expansion.slang)、函数宏参数替换(function-macro-argument-substitution.slang)、实参 token 再次展开(function-macro-argument-tokens-are-expanded.slang)、零参数宏与空实参等边界。
参数数量不匹配:E15501
实参数量与形参列表不匹配的调用会被WrongNumberOfArgumentsToMacro诊断并跳过,因此不贡献任何 token(slang-preprocessor.cpp 第 1870 行非变参、第 1892 行变参供给不足)。注意细节:变参宏只要求其非变参形参被满足,尾部实参数量任意。测试 macro-wrong-argument-count-diagnostic.slang 把 2 形参 1 实参的下供边界钉死在 E15501,并断言消息中的期望/实际数量:
#define BIN(a, b) ((a) + (b)) static const int q = BIN(1); //CHECK: ^ wrong number of arguments to macro (expected 2, got 1) // CHECK: E15501busy-list 自抑制递归
展开是自抑制(self-suppressing)而非深度受限的:每个进行中的调用都在一张"忙碌列表"上,当MacroInvocation::isBusy发现自己的宏在该列表上时,_maybeBeginMacroInvocation就不再展开而是直接返回,把m_lookaheadToken当作标识符交给上层(第 1462 与 1728 行)。因此:
在自身宏体里引用自己的宏,到达解析器时是一个普通标识符——既不会重新展开,也不会被诊断;同一张列表也切断了嵌套展开间的相互递归。
非激活的#if分支仍然流经词法器(保证列/行计数正确),但其内容不会被展开;只有可能切换激活/非激活状态的指令(#if、#ifdef、#ifndef、#else、#elif、#endif)才会在非激活块内被真正求值(测试 inactive-block-skips-non-conditional-directives.slang、ifndef-nesting-tracked-inside-inactive-block.slang)。
预处理指令
指令按名字在预处理器状态上的回调表中查找,因此新增指令(#pragma、自定义扩展)就是向表中注册新回调。kDirectives表(slang-preprocessor.cpp)持有 C / HLSL 风格指令集(#if一族、#include、#define、#undef、#warning、#error、#line、#pragma),外加 Slang 语言选择指令#language/#lang与 GLSL 指令#version/#extension。
每条指令消费自己的整行,因此不向输出列表贡献任何 token。
#language/#lang:版本按名字解析而非按算术
#language/#lang(HandleLanguageDirective,第 4536 行)接受可选的slang名字后跟版本:
#lang [slang] <version> #language [slang] <version>它们设置源语言与版本,preprocessSource通过outDetectedLanguage/outLanguageVersion返回(slang-preprocessor.h)。版本操作数按名字解析而非按算术:无论它是以标识符还是整数字面量到达,其文本都被交给TypeTextUtil::findLanguageVersion,在单一表中查找——每个版本携带多个被接受的拼写:
| 版本 | 接受的拼写 |
|---|---|
| 2018 | legacy/default/2018 |
| 2025 | 2025/202a |
| 2026 | 2026/202b/latest |
| 2027 | 202c/next |
因此#lang 2026、#lang 202b与#lang latest是同一条指令;而编译器不认识的版本只是表中不存在,而不是"数字越界"。测试 lang-directive-version-spellings.slang 用#lang 202b编译成功来钉住202b与2026共享同一表行的别名关系。注意:只有slang被接受为可选语言名,该指令已不再识别glsl。
不可用操作数产生哪种诊断,取决于解析器还能从中推断出什么,因此共有三种:
| 操作数形态 | 诊断 |
|---|---|
| 既非标识符也非整数字面量 | ExpectedIntegralVersionNumber |
未识别的标识符,且未给slang名 | UnknownLanguage——语言未指定时,孤立标识符被读作试图命名语言 |
未识别的整数字面量,或显式slang之后出现未识别标识符 | UnknownLanguageVersion——语言已确定,该 token 只可能是版本 |
测试 lang-directive-unknown-version-after-language.slang 验证:显式slang名之后的不识别标识符报 E15207(未知语言版本)而非 E15208(未知语言)。
#version与#extension
#version(第 4501 行)接受一个整数,当它命名有效的 GLSL 版本时把检测到的语言切换到 GLSL;#extension(第 4496 行)被接受然后丢弃。
#pragma once与未知子指令策略
#pragma通过kPragmaDirectives(第 4445 行)按第二个名字分发,目前只有两个条目:once与warning。
#pragma once把所在文件的唯一身份记录进pragmaOnceUniqueIdentities(第 4272 行);之后某次#include解析到同一身份时,HandleIncludeDirective提前返回(第 3680 行),从而防止重复包含。测试 pragma-once-prevents-redefinition.slang 两次包含同一#pragma once头文件并成功编译验证。- 未知子指令不是错误:
findPragmaDirective回退到handleUnknownPragmaDirective(第 4242 行),以UnknownPragmaDirectiveIgnored警告并跳过该行(警告严重级别见 source/slang/slang-diagnostics.lua)——这与未知#指令形成鲜明对比:后者由HandleInvalidDirective(第 4614 行)作为错误上报。测试 pragma-unknown-emits-warning.slang 与 unknown-directive-diagnostic.slang 分别钉住这两种失败形态。
#if表达式求值模型
#if与#elif条件经递归下降求值器求值,其值类型是普通的带符号int(PreprocessorExpressionValue,slang-preprocessor.cpp 第 2984 行):没有无符号或 64 位模式,任何非零结果即选中该分支。关键规则(均由 gap-intake 审计确认并补进文档):
- 命名了某个非对象宏的标识符,在发出
UndefinedIdentifierInPreprocessorExpression警告后求值为0(第 3117 行)——测试 if-undefined-macro-is-zero.slang; EvaluateInfixOp(第 3194 行)直接套用 C++ 运算符,只有/和%检查操作数(除数为零会被诊断并产生0),因此+、-、*、<<的溢出既不被检测也不被诊断——测试 if-expression-signed-overflow-wraps.slang 验证(INT_MAX + 1)回绕为负值,if-expression-uint32-max-is-true.slang 验证0xFFFFFFFF作为带符号值求值仍非零并选中激活分支。
#include解析
#include字符串由IncludeSystem(slang-include-system.cpp)解析,它查询Linkage的搜索路径。两种形态:
- 尖括号形态:
HandleIncludeDirective把OpLess与OpGreater之间的原始 token 拼接为路径字符串,并选择IncludeSystem::Mode::System(测试 include-angle-bracket-mode.slang); - 引号形态:路径来自单个
StringLiteraltoken。
解析返回一个SourceFile,预处理器随后为该文件压入全新的输入流。处理器还会收到handleFileDependency回调,使前端能为构建系统建立依赖记录。测试资产还包括 5 层嵌套 include 的压测(include-nested-5deep.slang)与"include 压入全新流"的行为验证(include-pushes-fresh-stream.slang)。
#pragma warning跨文件状态跟踪
#pragma warning(push/pop/disable/...)的状态由WarningStateTracker(slang-preprocessor.cpp)跟踪:它按诊断 id 记录一条以绝对源码位置轴为键的时间线。由于每个__include的文件都在一次独立的preprocessSource流程(携带全新的Preprocessor)中预处理,tracker 携带一个persistedAbsoluteSourceLocCounter:preprocessSource在进入时用持久化的值种子化自己的absoluteSourceLocCounter,退出时把推进后的值交还,从而保证时间线的绝对轴在文件间全局单调,而不是每次流程从 0 重启导致碰撞。交还计数器上的SLANG_RELEASE_ASSERT守卫单调性(例如防止超大型翻译单元上uint32_t回绕),因为一旦违反,发布构建中#pragma warning状态会被静默地错误解析。
测试 pragma-warning-disable-timeline.slang 验证同一构造在指令之前警告、之后静默;pragma-warning-push-pop-restores.slang 验证 push/pop 恢复。
源码位置保留:宏展开 token 的三类位置策略
宏展开产出的 token 按源码位置的选取方式分为三类,这也是诊断定位质量的关键(诊断系统详见 docs/generated/design/cross-cutting/diagnostics.md):
1. 宏体原始 token
从宏定义逐字复制的 token,其SourceLoc是宏定义中对应 token 的位置,由MacroInvocation::readToken(slang-preprocessor.cpp)回放。不会为调用创建新的SourceView,因此这类诊断指向宏体内部而非调用点。测试 macro-body-diagnostic-points-into-macro-body.slang 验证之。
2. 实参 token
取自调用点实参列表的 token。_getArgTokens通过PretokenizedInputStream回放记录的 token 范围且不做任何重写(第 2351 与 2494 行),因此每个 token 保留其在调用点被物理词法化时的SourceLoc。参数被多次使用时,每次都回放同一个位置。可证伪的经典示例:
#define TWICE(x) ((x) + (x)) TWICE(undeclared)对实参undeclared的诊断指向调用点的undeclared,而不是宏体中任何一个(x)——后者正是第 1 类规则会放置的位置。
3. 构造 token
全新合成的 token,有三种不同的位置规则:
- 内建宏(
__LINE__、__FILE__):由_pushStreamForSourceLocBuiltin压入,合成的 token 获得m_macroInvocationLoc,归属于调用点(但报告的行/文件值来自发起的最顶层位置)。测试 line-macro-through-nested-expansion.slang 验证嵌套展开中__LINE__报告的是发起调用的最顶层行; - 字符串化参数(
#x):取宏定义中#token 的位置(m_macro->tokens.m_tokens[tokenIndex].loc); - 粘贴 token(
x##y):从全新的PathInfo::makeTokenPaste()源码视图重新词法化,其来源是##token 的位置(tokenPasteLoc)。
只有粘贴 token 情形会构建发起位置链:格式化诊断时,DiagnosticSink只要视图的PathInfo::Type是TokenPaste就沿getInitiatingSourceLoc行走,每跳发出一个seeTokenPasteLocation附注(测试 token-paste-diagnostic-notes-paste-location.slang)。粘贴后的 token 会被重新词法化,因此粘贴两个整数字面量能形成一个新的整数字面量(token-paste-relexed-numeric.slang)。
这一三分法让"宏自身有问题"的诊断能指向宏体内部,而实参 token 仍指向调用点。
失败模式与恢复行为
扫描期 vs 解码期错误
非法字符与畸形数值/字符串/字符字面量经DiagnosticSink上报,且跨两个阶段分离:
- 扫描期错误走传入词法器
initialize的 sink,覆盖扫描器无需解释字面量即可看到的问题:非法字符、畸形 UTF-8 序列、字面量内的文件尾或换行、超出进制的数字、遗留八进制字面量、作为原始字符串定界符的引号; - 解码期错误通过传给取值辅助函数的 sink 上报:
getStringLiteralTokenValue与getCharLiteralValue报告畸形转义语法、越界码元与码点、(字符字面量的)空或多字符主体;getIntegerLiteralValue报告integerLiteralTooLargeForAnyType(测试 integer-literal-too-large-diagnostic.slang)。
getFloatingPointLiteralValue是例外:它完全不接收 sink,而是以BadSignificand或BadSuffix的FloatingPointLiteralType外加出错文本返回错误,由调用方选择诊断。从源码看,其唯一调用方parseFloatingPointLiteralExpr(source/slang/slang-parser.cpp)把BadSignificand映射为InvalidFloatingPointLiteralNumber、把BadSuffix映射为InvalidFloatingPointLiteralSuffix。
两个阶段都经由 slang-lexer.cpp 中文件局部的diagnose辅助函数汇入:它丢弃 null sink,并且一旦 sink 的错误计数超过kMaxLexErrorCount(100)就停止转发,从而防止病态畸形文件淹没 sink。
抑制诊断下的行为
设置kLexerFlag_SuppressDiagnostics后,词法器对畸形输入仍会产出TokenType::Invalidtoken,但抑制诊断——用于被跳过的预处理块内部(那里的 token 会被非激活分支过滤器丢弃)。由于抑制机制是让Lexer::getDiagnosticSink交出 null sink,它只覆盖使用该访问器的扫描期位置(畸形 UTF-8 绕过它);如果调用方之后向取值辅助函数请求字面量值,传入自己的 sink,仍会看到解码期诊断。
预处理错误与继续策略
预处理错误(不配对的#if、未知指令、缺失 include、循环 include、没有打开条件的#else/#elif/#endif)同样经 sink 上报,且预处理器会继续运行:
- 畸形指令被跳到其行尾;
- 失败的
#include从其处理器返回; - 坏的宏调用可能展开为无 token;
- 未闭合的条件在文件尾对每个仍打开的条件各诊断一次,即
EndOfFileInPreprocessorConditional(slang-preprocessor.cpp 第 4773 行;测试 unbalanced-if-diagnostic.slang); - 游离的闭合器是独立的诊断
DirectiveWithoutIf(第 3530 行); - 循环 include:当解析出的 include 已经在 include 栈上打开时,
CyclicInclude触发(第 3710 行),该#include被跳过。测试 include-self-cycle-diagnostic.slang 让文件包含自身并钉住 E15302:
#include "include-self-cycle-diagnostic.slang" //CHECK: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cyclic `#include` of file '"include-self-cycle-diagnostic.slang"' // CHECK: E15302输出中唯一的EndOfFiletoken 由ReadAllTokens在整个输入栈弹出之后才追加。
文档质量保障:gap-intake 审计与测试资产
本主题对应的设计文档 docs/generated/design/pipeline/01-lex-preprocess.md 经过一轮系统性质量审计(见 docs/generated/design/_meta/gap-intake/pipeline/01-lex-preprocess.md.gap-intake.md),共识别 11 处缺口:9 处已修复、2 处推迟、0 处驳回、0 处升级为缺陷——没有任何观察与受监视源码矛盾。修复分布在五个章节:扫描检查严重级别与1e300/1e300lf后缀示例、宏展开(参数数量不匹配与 busy-list 递归规则)、预处理指令(#language/#lang/#version/#extension的效果、#pragma once与未知子指令策略、#if算术模型)、源码位置保留(第 2 类改写为围绕_getArgTokens并附可证伪示例)、失败模式(循环 include 与游离条件闭合器)。两处推迟项均因佐证源码位于本文档监视路径之外:编码集合命名需 source/core/slang-char-encode.cpp,BadSignificand/BadSuffix的用户级诊断命名需 source/slang/slang-parser.cpp。
围绕该文档生成的测试套件(docs/generated/tests/design/pipeline/01-lex-preprocess/README.md)采用"一条正向主张一个功能测试、数值/转义/后缀/嵌套轴上的边界探测、每条拒绝主张一个钉死诊断码的诊断测试"的覆盖策略,全部测试与目标无关,可通过INTERPRET、-target cpp文本发射与DIAGNOSTIC_TEST运行。这也是阅读上述每一处行为细节最直接的验证入口:先读设计文档定位语义,再打开对应.slang测试查看可观察断言,最后在 source/compiler-core 与 source/slang 的源码中确认实现。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考