news 2026/9/18 15:59:42

Slang 编译器词法分析与预处理阶段深度解析:从源码字节流到扁平 Token 列表

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Slang 编译器词法分析与预处理阶段深度解析:从源码字节流到扁平 Token 列表

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 ——TokenTokenTypeTokenFlagsTokenListTokenSpanTokenReader等数据类型;
  • 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一个小字节,含AtStartOfLineAfterWhitespaceScrubbingNeededName(后者用于区分联合体)

源码位置编码

源码位置被压缩为单个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 文本)与解码(求值)分离,且这种分离几乎是干净的:扫描只做找到字面量结尾所需的最小工作,外加少量纯语法检查——invalidDigitForBaseoctalLiteralquoteCannotBeDelimiter。三者严重级别并不相同(这一细节由 gap-intake 质量审计明确补全):

检查严重级别行为
octalLiteral(如0170前缀)警告(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 转义形式。因此它只会抛出两种"根本找不到结尾"的诊断:endOfFileInLiteralnewlineInLiteral

原始字符串字面量由_lexRawStringLiteralBody(第 1782 行)单独扫描,它寻找匹配的)delimiter",且完全不进行转义处理——测试 raw-string-no-escape-processing.slang 验证了这一点。

解码:按需取值辅助函数

解码为值由 slang-lexer.h 中声明的辅助函数按需完成:

  • getIntegerLiteralValue——解析整数,可选返回后缀、十进制基数标志和溢出标志;
  • getFloatingPointLiteralValue——解析浮点值,同时负责后缀分类:把 token 拆成数值部分与后缀部分,将后缀映射为FloatingPointLiteralTypeh/hf/fhHalf,空后缀或fFloatl/lf/flDouble,大小写均可),并把解析值按该类型精度与范围取整。同一枚举还上报两种畸形情形BadSignificandBadSuffixoutErrorContent携带出错的文本。全部四个出参(outLiteralTypeoutIsOutOfRangeoutPrecisionLostoutErrorContent)都是不可省略的引用。其中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 INFINITY

1e300能放进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没有实际差别。

对应诊断(invalidUtf8ByteSequenceinvalidStringEscapeinvalidUnicodeStringEscapeoutOfRangeCodeUnitoutOfRangeCodePointForUtf8)定义在 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(ExpandedParamUnexpandedParamStringizedParam)。调用时,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: E15501

busy-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/#langHandleLanguageDirective,第 4536 行)接受可选的slang名字后跟版本:

#lang [slang] <version> #language [slang] <version>

它们设置源语言与版本,preprocessSource通过outDetectedLanguage/outLanguageVersion返回(slang-preprocessor.h)。版本操作数按名字解析而非按算术:无论它是以标识符还是整数字面量到达,其文本都被交给TypeTextUtil::findLanguageVersion,在单一表中查找——每个版本携带多个被接受的拼写:

版本接受的拼写
2018legacy/default/2018
20252025/202a
20262026/202b/latest
2027202c/next

因此#lang 2026#lang 202b#lang latest同一条指令;而编译器不认识的版本只是表中不存在,而不是"数字越界"。测试 lang-directive-version-spellings.slang 用#lang 202b编译成功来钉住202b2026共享同一表行的别名关系。注意:只有slang被接受为可选语言名,该指令已不再识别glsl

不可用操作数产生哪种诊断,取决于解析器还能从中推断出什么,因此共有三种:

操作数形态诊断
既非标识符也非整数字面量ExpectedIntegralVersionNumber
未识别的标识符,且未给slangUnknownLanguage——语言未指定时,孤立标识符被读作试图命名语言
未识别的整数字面量,或显式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 行)按第二个名字分发,目前只有两个条目:oncewarning

  • #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条件经递归下降求值器求值,其值类型是普通的带符号intPreprocessorExpressionValue,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的搜索路径。两种形态:

  • 尖括号形态HandleIncludeDirectiveOpLessOpGreater之间的原始 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 携带一个persistedAbsoluteSourceLocCounterpreprocessSource在进入时用持久化的值种子化自己的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);
  • 粘贴 tokenx##y):从全新的PathInfo::makeTokenPaste()源码视图重新词法化,其来源是##token 的位置(tokenPasteLoc)。

只有粘贴 token 情形会构建发起位置链:格式化诊断时,DiagnosticSink只要视图的PathInfo::TypeTokenPaste就沿getInitiatingSourceLoc行走,每跳发出一个seeTokenPasteLocation附注(测试 token-paste-diagnostic-notes-paste-location.slang)。粘贴后的 token 会被重新词法化,因此粘贴两个整数字面量能形成一个新的整数字面量(token-paste-relexed-numeric.slang)。

这一三分法让"宏自身有问题"的诊断能指向宏体内部,而实参 token 仍指向调用点。

失败模式与恢复行为

扫描期 vs 解码期错误

非法字符与畸形数值/字符串/字符字面量经DiagnosticSink上报,且跨两个阶段分离:

  • 扫描期错误走传入词法器initialize的 sink,覆盖扫描器无需解释字面量即可看到的问题:非法字符、畸形 UTF-8 序列、字面量内的文件尾或换行、超出进制的数字、遗留八进制字面量、作为原始字符串定界符的引号;
  • 解码期错误通过传给取值辅助函数的 sink 上报:getStringLiteralTokenValuegetCharLiteralValue报告畸形转义语法、越界码元与码点、(字符字面量的)空或多字符主体;getIntegerLiteralValue报告integerLiteralTooLargeForAnyType(测试 integer-literal-too-large-diagnostic.slang)。

getFloatingPointLiteralValue是例外:它完全不接收 sink,而是以BadSignificandBadSuffixFloatingPointLiteralType外加出错文本返回错误,由调用方选择诊断。从源码看,其唯一调用方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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 15:59:36

C 函数库手册 PDF 检索化:man 解析、Doxygen 与 SQLite

简介&#xff1a;这份 C 语言函数库手册 PDF 面向刚入门 C 语言、需要频繁查阅标准库接口的开发者与学生&#xff0c;解决函数名、参数、返回值记不牢、查文档效率低的问题。内容以函数分类为主线&#xff1a;ctype.h 中的字符分类与大小写转换函数逐一列出判断条件&#xff0c…

作者头像 李华
网站建设 2026/9/18 15:56:48

阿里云ECS搭建饥荒联机版服务器:从选型到开服全攻略

1. 为什么选择云服务器而不是本地开服1.1 本地开服和云服务器的真实差距很多人第一次接触《饥荒联机版》&#xff08;Dont Starve Together&#xff0c;简称DST&#xff09;开服&#xff0c;第一反应是用自己家里的电脑当主机。这个思路本身没错&#xff0c;但实际跑起来问题不…

作者头像 李华
网站建设 2026/9/18 15:54:51

STM32调试迁移:VS Code+OpenOCD+Cortex-Debug实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 15:54:23

多元回归模型实战:从数据清洗到结果报告的Python全流程

简介&#xff1a;多元回归模型是数学建模与统计数据分析中的核心方法。这份docx文档围绕某市粮食年销售量与常住人口、人均收入以及肉、蛋、鱼销售量等变量的关系&#xff0c;系统记录了完整实验报告&#xff0c;适合学习统计建模、经济数据分析或准备数学建模竞赛的学生参考。…

作者头像 李华