Slang 语句 AST 参考指南:Stmt 类层次结构、解析流程与诊断错误码全解析
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
导读
本文是 Slang 编译器源码中语句(Statement)AST 体系的权威参考指南,面向需要阅读或修改解析器(parser)、语义检查器(checker)或 IR 降级(IR lowering)代码的贡献者。文章完整梳理了Stmt的所有具体子类、Parser::ParseStatement的调度与两阶段解析策略、switch/for/__target_switch/defer等语句的底层解析行为,以及与之配套的E20001、E20004、E20101、E29110、E36105、E36109六个用户可见诊断错误码。读完本文,你将能够在源码中快速定位任意语句节点的定义、解析入口与相关测试,并理解 HLSL 输入下for循环变量泄漏等微妙行为。
文档来源与定位
本文主体对应仓库中的自动生成文档 docs/generated/design/ast-reference/statements.md,并依据 docs/generated/design/_meta/gap-intake/ast-reference/statements.md.gap-intake.md 中记录的 8 项缺口(gap)处理结果,将 7 项已修复的文档缺口(诊断码、defer可观察语义、switch单作用域结论等)与 1 项暂缓缺口(HLSL 输入语言选择机制)一并整合进正文,同时结合 source/slang/slang-parser.cpp、source/slang/slang-ast-stmt.h 等源码与docs/generated/tests/design/ast-reference/statements/下的 bundle 测试做交叉印证。
语句 AST 的整体框架
类的声明位置
语句(Statement)类全部声明在 source/slang/slang-ast-stmt.h 中;而所有语句的抽象根Stmt并不在该文件里——它是 source/slang/slang-ast-base.h 中的抽象基类之一(与Expr、Decl等并列)。文档对Stmt本身的说明位于 base.md。
解析入口
语句的解析入口是Parser::ParseStatement(slang-parser.cpp),它是一个基于关键字前瞻(keyword-lookahead)的调度器,根据下一个 token 选择对应的Parse*Statement/parse*Stmt辅助函数;Parser::parseBlockStatement(slang-parser.cpp)则负责解析{ ... }代码块。
两者都接收一个AllowCaseDefaultStatements参数,这正是case/default写在switch体外时能先被解析成节点、再被诊断(而非直接报"unexpected token")的原因,参见 keywords-and-builtins.md。
两阶段解析策略
函数体并不会在解析声明的同时被解析:解析器运行在ParsingStage::Decl与ParsingStage::Body两个阶段之一(ParsingStage枚举位于 slang-parser.cpp)。parseOptBody(slang-parser.cpp)在遇到{限定的函数体时,不递归进入parseBlockStatement,而是将其记录为一个UnparsedStmt;parseUnparsedStmt(slang-parser.cpp)稍后以stage = ParsingStage::Body重新进入,把这些 token 真正变成BlockStmt。整体策略参见 02-parse-ast.md。
家族层次结构(Family hierarchy)
语句节点沿两个轴划分:其一,是否是一个ScopeStmt分组(结构基类,只有部分子类真正持有ScopeDecl);其二,是否可以作为break(循环与switch)或continue(仅循环)的目标。此外还有一组ChildStmt,保存对某个外围BreakableStmt的引用。
BreakableStmt携带一个UniqueStmtIDNode* uniqueID,ChildStmt通过targetOuterStmtID引用它。UniqueStmtIDNode在头文件中被声明为Decl子类,仅仅是为了序列化的便利——它并不是真正的声明;虽然不在Stmt层次内、也不被当作语句解析,但由于头文件用FIDDLE()宏声明它,它仍会出现在下文## Nodes表中作为辅助节点。
节点总表(Nodes)
下表是全部语句节点的速查表,字段与文法一应俱全:
| Class | Parent | Key fields | Grammar | Summary |
|---|---|---|---|---|
SeqStmt | Stmt | stmts: List<Stmt*> | (none) | 扁平的语句序列,用于在只允许单条语句的位置(如{ ... }块的若干语句)填充多条语句。 |
LabelStmt | Stmt | label: Token,innerStmt: Stmt* | labeled stmt | label:后跟一条语句。 |
BlockStmt | ScopeStmt | body: Stmt*,closingSourceLoc: SourceLoc | block | { ... };为块级声明引入一个ScopeDecl。 |
UnparsedStmt | Stmt | tokens: List<Token>,sourceLanguage: SourceLanguage,currentScope: Scope*,outerScope: Scope* | (none) | 在ParsingStage::Decl阶段以原始 token 捕获的{ ... }函数体,按需重新解析。 |
EmptyStmt | Stmt | (无附加状态) | empty stmt | 裸;。 |
DiscardStmt | Stmt | (无附加状态) | discard | discard(片元着色器像素丢弃)。 |
DeclStmt | Stmt | decl: DeclBase* | decl stmt | 在语句位置使用声明(例如函数体内的int x = 1;)。 |
IfStmt | Stmt | predicate: Expr*,positiveStatement: Stmt*,negativeStatement: Stmt*,afterLoc: SourceLoc | if | if (...) ... else ...;也是if (let x = ...)脱糖后的节点。 |
SwitchStmt | BreakableStmt | condition: Expr*,body: Stmt* | switch | switch (cond) { ... }。 |
TargetCaseStmt | ChildStmt | capability: int32_t,capabilityToken: Token,body: Stmt* | __target_switch | __target_switch内的case <capability>:(或default:);capability保存一个CapabilityName代码。 |
TargetSwitchStmt | BreakableStmt | targetCases: List<TargetCaseStmt*> | __target_switch | 按 capability 集合静态分发。 |
StageSwitchStmt | TargetSwitchStmt | (继承) | __stage_switch | 按管线阶段静态分发。 |
IntrinsicAsmStmt | Stmt | asmText: String,args: List<Expr*> | __intrinsic_asm | 内联 intrinsic 汇编语句,供核心模块 intrinsic 使用。 |
CaseStmt | CaseStmtBase | expr: Expr*,exprVal: Val* | case | switch内的case <expr>:。 |
DefaultStmt | CaseStmtBase | (无附加状态) | default | switch内的default:。 |
GpuForeachStmt | ScopeStmt | device: Expr*,gridDims: Expr*,dispatchThreadID: VarDecl*,kernelCall: Expr* | __GPU_FOREACH | 宿主机侧对网格的计算式 foreach,写法为__GPU_FOREACH(device, gridDims, LAMBDA(...) { ... })。 |
ForStmt | LoopStmt | initialStatement: Stmt*,sideEffectExpression: Expr*,predicateExpression: Expr*,statement: Stmt* | for | for (init; cond; step) body;循环变量限定在函数体内作用域。 |
UnscopedForStmt | ForStmt | (继承) | for | 语法与ForStmt相同,仅对 HLSL 输入产生——此时循环变量泄漏到外围作用域。 |
WhileStmt | LoopStmt | predicate: Expr*,statement: Stmt* | while | while (cond) body。 |
DoWhileStmt | LoopStmt | statement: Stmt*,predicate: Expr* | do-while | do body while (cond);。 |
CompileTimeForStmt | ScopeStmt | varDecl: VarDecl*,rangeBeginExpr: Expr*,rangeEndExpr: Expr*,body: Stmt*;外加检查期填充的rangeBeginVal/rangeEndValIntVal* | compile-time for | 编译期展开的基于范围的循环;不产生运行时循环。 |
BreakStmt | JumpStmt | targetLabel: Token | break | break(可带目标标签)。 |
ContinueStmt | JumpStmt | (继承) | continue | continue;与break不同,不带标签。 |
ReturnStmt | Stmt | expression: Expr* | return | return(可带表达式)。 |
DeferStmt | Stmt | statement: Stmt* | defer | defer S;statement保存被延迟的语句。 |
ThrowStmt | Stmt | expression: Expr* | throw | 可错误函数(errorable function)中的throw e(ParseThrowStatement本身不消费尾随;)。 |
CatchStmt | Stmt | errorVar: ParamDecl*,tryBody: Stmt*,handleBody: Stmt* | do-catch | do { ... } catch (e) { ... };tryBody是被保护体,handleBody是处理器;errorVar == null表示 catch-all。 |
ExpressionStmt | Stmt | expression: Expr* | expression stmt | 以副作用为目的的表达式(f();、a = b;)。 |
RequireCapabilityStmt | Stmt | requiredCaps: List<Token> | __requireCapability | __requireCapability(...);语句级 capability 要求,作用域限定于所在函数。 |
UniqueStmtIDNode | Decl | (无解析状态) | (none) | 综合产生的身份辅助节点,为语句提供稳定的唯一 id;用于序列化与控制流追踪,而非被当作语句解析。 |
重点节点详解(Notable nodes)
BlockStmt 与 SeqStmt:作用域从何而来
ScopeStmt是所有可能拥有词法作用域的控制流分组的结构基类。它唯一的字段scopeDecl: ScopeDecl*是块内局部声明所挂载的容器声明,名字查找从语句出发向外遍历时正是通过它找到这些声明。只有四个解析例程会填充它:
Parser::parseBlockStatement为BlockStmt填充(slang-parser.cpp);Parser::ParseForStatement为有作用域的for填充(slang-parser.cpp);parseGpuForeachStmt(slang-parser.cpp);parseCompileTimeForStmt(slang-parser.cpp)。
SwitchStmt和TargetSwitchStmt通过BreakableStmt继承ScopeStmt,但scopeDecl保持为 null——switch的词法作用域取自解析器为其函数体构建的BlockStmt;WhileStmt/DoWhileStmt则不引入自己的作用域。
由此得出一个用户可见的重要语义:整个switch函数体是一个单一作用域。在case 1:下声明的变量在case 2:和default:下依然在作用域内,并且会在函数体其余部分遮蔽函数体外同名变量。这是 gap-intake 修复7050023ee030的结论,其证据链为:slang-parser.cpp 用parseBlockStatement(AllowCaseDefaultStatements::Allow)解析整个switch函数体,ParseCaseStmt/ParseDefaultStmt(:6592、:6602)只读取case <expr> :/default :,因此标签下的声明是同一个BlockStmt作用域内的兄弟节点。bundle 测试 switchstmt-body-block-scope.slang 固化了这一可观察行为:default:中的读取解析到case 1:的声明而非外层变量。
提示:
SwitchStmt::body永远是BlockStmt——ParseSwitchStmt(slang-parser.cpp)用parseBlockStatement解析{ ... },该块的body通常是混合了CaseStmt、DefaultStmt与其他语句的SeqStmt。case 标签并不是其下语句的父节点:ParseCaseStmt与ParseDefaultStmt(slang-parser.cpp)只读取case <expr> :/default :,后续语句作为兄弟留在序列中,所以每个CaseStmt/DefaultStmt只是序列里的一个标记。SwitchStmt内的BreakStmt通过BreakableStmt::uniqueID匹配。
BlockStmt是最简单的ScopeStmt——一个body为单条Stmt的{ ... }块。SeqStmt则相反,是一个没有自身作用域的扁平容器,存在的意义就是让多条语句能填入只放得下一条Stmt的槽位。Parser::parseBlockStatement正是这样使用它:块中第一条语句直接成为body,第二条出现时创建一个SeqStmt并把两条都移进去;空块获得EmptyStmt作为body而非 null。另外两处会构建SeqStmt:Parser::parseIfLetStatement(slang-parser.cpp)与parseTargetSwitchStmtImpl(slang-parser.cpp)中每个 case 的函数体循环。
UnparsedStmt:函数体的第一形态
UnparsedStmt并非什么奇特构造——它就是函数体在解析第一阶段最普通的表现形式。当解析器在期望函数体的位置遇到{时,parseOptBody(slang-parser.cpp)不递归进入parseBlockStatement,而是把直到匹配}的 token 复制进UnparsedStmt::tokens(以合成的文件结束 token 结尾),并记录当时生效的两个作用域currentScope与outerScope。像 slang-parser.cpp 处的函数声明解析器这样的调用方,会把结果当作该 decl 的body。
被捕获的作用域正是延迟(deferral)安全性的关键:当函数体最终被需要时,parseUnparsedStmt(slang-parser.cpp)在一个以stage = ParsingStage::Body配置的全新Parser上重新安装这两个作用域并调用parseBlockStatement,于是函数体在与其原始位置完全相同的查找环境中被解析。结果是普通的BlockStmt,UnparsedStmt不会存活到后续阶段。触发重新解析的调用点位于 checker 中(在本文 watched paths 之外)。
IfStmt:谓词与两分支
IfStmt持有谓词与两个分支的原始Stmt*。没有else的if,其negativeStatement槽位为 null。afterLoc字段是解析器消费完两个分支后正在查看的位置(Parser::parseIfStatement末尾的tokenReader.peekLoc(),slang-parser.cpp),即整个if之后的第一个 token。
if let没有专属节点。当ParseStatement在if后两个 token 处看到let时,调用Parser::parseIfLetStatement,在解析期把该形式脱糖成一个SeqStmt:其中包含一个DeclStmt,绑定合成的$OptVar,后随一个普通IfStmt;另一个合成的LetDecl(绑定到$OptVar.value)被前置到正分支。因此追踪if let的读者应在 AST 中找SeqStmt,而不是某个独立的类。
循环家族(Loop family)
ForStmt、WhileStmt、DoWhileStmt都派生自LoopStmt,后者派生自BreakableStmt,再往上派生自ScopeStmt——因此每个循环都可break,但只有for拥有自己的ScopeDecl。
ForStmt::statement是循环体;initialStatement按语句(而非表达式)解析,以便DeclStmt能引入循环变量;Parser::ParseForStatement(slang-parser.cpp)会诊断该位置任何既非DeclStmt也非ExpressionStmt的内容,同时保留已构建的循环节点以便解析恢复。写在该位置的块——for ({ int i = 0; } n < 3; n = n + 1)——正是触发它的形态,用户看到的是E20001,"unexpected statement, expected expression",报告在该违规语句上。这是 gap-intake 修复81ec1b4a473c的结论:证据在 slang-parser.cpp,ParseStatement解析初始语句,任何非DeclStmt/ExpressionStmt的内容触发Diagnostics::UnexpectedTokenExpectedTokenType,其actualToken = "statement"、expectedToken = "expression";对应错误码定义于 slang-diagnostics.lua(id 20001,"unexpected ~actualToken, expected ~expectedToken")。bundle 测试 forstmt-init-non-expression-rejected.slang 正是用for ({ int i = 0; } n < 3; n = n + 1)固化 "unexpected statement, expected expression" 输出。
UnscopedForStmt是 HLSL 兼容形态:同一函数在getSourceLanguage()为SourceLanguage::HLSL时创建它而非ForStmt。这里有一个容易踩坑的重要事实:输入语言由输入文件的扩展名决定,不能用-lang选择。在OptionsParser::addInputPath(slang-options.cpp)中,扩展名测试先于语言值生效:以.slang结尾的路径直接进入addInputSlangPath,因此命令行上的-lang hlsl(只设置CompilerOptionName::Language)永远不会到达它;语言值只用于扩展名未决定的情况,否则findSourceLanguageFromPath也会从扩展名推导。所以同一份源码编译为foo.hlsl时循环变量泄漏,编译为foo.slang时则不会,且给.slang编译加-lang hlsl也不会改变这一点。在 HLSL 情形下解析器会填充scopeDecl但从不压栈该作用域,于是循环变量泄漏进外围作用域。
关于"用户如何选择 HLSL 输入"这一缺口(gap
9f664f7cf462)被暂缓(deferred):watched_paths中没有决定输入源语言的内容——slang-parser.cpp 只读取getSourceLanguage(),而Parser是从其调用方(slang-parser.h,同样未受监视)收到该值的。真正回答该问题的扩展名表位于 slang-options.cpp({".hlsl", SLANG_SOURCE_LANGUAGE_HLSL, SLANG_STAGE_NONE}),不在watched_paths内,且 bundle 中没有编译 HLSL 输入的测试。若把watched_paths扩展到source/slang/slang-options.cpp即可闭环此缺口。
do语句由Parser::ParseDoStatement(slang-parser.cpp)解析,它先解析函数体、再决定构建什么:后续有while则产生DoWhileStmt,后续有catch则产生CatchStmt,其余情况报错。
ReturnStmt
return既是控制流终结符,也是函数结果的载体:ReturnStmt::expression是被返回的Expr*,裸return(void 函数中)则为 null。checker 会把该表达式与外围函数声明的返回类型比对。
CompileTimeForStmt:编译期展开的循环
基于范围的循环,其边界必须是编译期常量(rangeBeginVal与rangeEndVal是由检查阶段填充的IntVal*)。解析器从$for (name in Range(...))语法产生它:parseCompileTimeStmt(slang-parser.cpp)消费$,parseCompileTimeForStmt(slang-parser.cpp)消费其余部分。
Range在该位置是必需的、字面意义上的关键字:解析器用ReadToken("Range")读取它(slang-parser.cpp),其失败路径是Unexpected(parser, expected)(slang-parser.cpp)→Diagnostics::UnexpectedTokenExpectedTokenName,即E20004,"unexpected identifier, expected 'Range'"(定义于 slang-diagnostics.lua,id 20004,消息 "unexpected ~actualToken, expected '~expectedTokenName'")。这是 gap-intake 修复acf54503f940的结论。
两种拼写及其迭代值(均半开区间):
- 单参形式
$for (i in Range(4)):rangeBeginExpr保持 null,迭代 0、1、2、3(共 4 次)。bundle 测试 compiletimefor-unrolls.slang 用Range(4)求和 0+1+2+3 固化该行为。 - 双参形式
$for (i in Range(2, 5)):逗号把第一个参数移入rangeBeginExpr,迭代 2、3、4。测试 compiletimefor-two-argument-range.slang 固化之。 - 其他标识符出现在该位置则报
E20004,由测试 compiletimefor-range-keyword-required.slang("unexpected identifier, expected 'Range'")固化。
循环变量是解析器创建的VarDecl,被加入语句自身的ScopeDecl,因此函数体可以引用它。
TargetSwitchStmt、StageSwitchStmt、TargetCaseStmt
TargetSwitchStmt是编译期按 capability 匹配的静态分发;每个TargetCaseStmt携带一个 capability 代码和一个函数体。StageSwitchStmt形状相同但按管线阶段(pipeline stage)分发,两者共享同一解析器:parseTargetSwitchStmt与parseStageSwitchStmt(slang-parser.cpp)仅在调用parseTargetSwitchStmtImpl前分配哪个节点上不同。
该共享实现有两个从表格中看不到的细节:
- capability 名字在解析期就被解析:
findCapabilityName把 token 映射为CapabilityName,存入TargetCaseStmt::capability(int32_t);未识别的名字立即被诊断为Diagnostics::UnknownTargetName——E29110,"unknown target name ' '"(slang-diagnostics.lua,id 29110,消息 "unknown target name '~name'")。该 case 仍会被记录,但capability留在CapabilityName::Invalid,于是语义检查在同一标签上再报第二个错误:E36109,"'Invalid' cannot be used as a target_switch case."(slang-diagnostics.lua,id 36109,消息 "'~capability' cannot be used as a target_switch case.")。这是 gap-intake 修复8d82d193316d的结论:证据在 slang-parser.cpp(诊断UnknownTargetName后仍存储capability = (int32_t)CapabilityName::Invalid),而该Invalid存储值正是 checker 在 slang-check-stmt.cpp(在本文 watched paths 之外)添加Diagnostics::InvalidTargetSwitchCase的原因。bundle 测试 targetcase-unknown-name-rejected.slang 在同一标签上固化了两条消息。default:标签被记录为capabilityToken内容已清空的TargetCaseStmt。 - 堆叠标签:
case a: case b: ...每个标签产生一个TargetCaseStmt,全部指向同一个函数体Stmt,因此遍历targetCases的消费者会多次看到该函数体。
DeferStmt:何时真正执行由 IR 降级决定
Parser::ParseDeferStatement(slang-parser.cpp)读取defer关键字后跟一条Stmt——因此延迟块不需要尾随分号——并存入DeferStmt::statement。节点不携带其他状态;延迟语句真正运行的时机由 IR 降级(IR lowering)决定,而非 AST。
可观察的规则(gap-intake 修复20449a33ee45的结论)是:延迟语句在封闭作用域退出时运行——即包含该defer的块结束时,也包括函数通过return提前离开该作用域的情形。注意:break与throw虽出现在建议措辞中,但没有任何 bundle 测试固化它们,因此文档有意没有写入。相关 bundle 测试:
- deferstmt-scope-exit-order.slang:固化 body → deferred → after-block 的顺序;
- deferstmt-at-function-entry.slang 与 deferstmt-runs-on-early-return.slang:固化提前
return时延迟打印落在被调用方最后一条打印与调用方下一条打印之间。
降级的完整说明参见 04-ast-to-ir.md。
ThrowStmt 与 CatchStmt:可错误函数模型的两半
CatchStmt同时持有被保护体(tryBody)与处理器(handleBody);错误参数是ParamDecl,使 catch 处理器拥有完整类型的局部变量。errorVar为 null 表示不绑定错误值的 catch-all。
注意语句级拼写是do { ... } catch (e) { ... }——没有try语句;语句位置的try会被路由到Parser::ParseExpressionStatement,因为try是表达式关键字。一个do后面可以跟多个catch子句,Parser::ParseDoCatchStatement(slang-parser.cpp)会将其链接起来:当下一个 token 是catch时循环继续,把刚构建的CatchStmt作为下一个的tryBody,于是n个 catch 子句变成n层嵌套的CatchStmt,最外层被返回。
DeclStmt、ExpressionStmt 与 EmptyStmt:样板包装器
这些样板包装器的存在是为了让语句文法保持统一。DeclStmt允许任意DeclBase出现在语句位置(典型用途是局部变量声明);ExpressionStmt允许任意Expr以副作用为目的使用。checker 分别校验每种类型。EmptyStmt是解析器为裸;产出的节点,因此空的语句槽位(例如循环体只有;)仍有具体的Stmt。
前两者的选择需要回溯(backtracking):对以标识符或::开头的语句,ParseStatement先试探性解析一个类型,若随后是标识符则回卷 token 读取器,通过Parser::parseVarDeclrStatement(slang-parser.cpp)把整条重新解析为声明;否则回卷并调用ParseExpressionStatement。
if之后紧跟;是可疑而非非法:该情形仍产出EmptyStmt,但同时报告Diagnostics::UnintendedEmptyStatement——警告E20101,"potentially unintended empty statement at this location; use {} instead."。这是 gap-intake 修复df583f6f8952的结论:证据在 slang-parser.cpp——只有当父节点是IfStmt时才抛出该诊断,随后仍构建EmptyStmt;条目以warning(...)声明(slang-diagnostics.lua,id 20101),bundle 测试 emptystmt-after-if-diagnosed.slang 固化了该打印文本。因为是警告而非错误,程序仍会继续编译。
LabelStmt、BreakStmt、ContinueStmt 与 DiscardStmt
Slang 支持带标签语句与带标签 break。LabelStmt把标签 token 附着到内部语句上,ParseStatement看到标识符紧跟:时选择Parser::parseLabelStatement(slang-parser.cpp);BreakStmt::targetLabel可选地命名要跳出的封闭带标签循环或switch。targetLabel到BreakableStmt::uniqueID的解析由语义检查完成。ContinueStmt是重启最近封闭循环的兄弟JumpStmt,且不接受标签。DiscardStmt是仅限片元着色器的控制流语句(discard),丢弃当前像素,不携带操作数。
RequireCapabilityStmt:语句级 capability 断言
断言外围函数要求列出的 capability 原子。Parser::ParseRequireCapabilityStatement(slang-parser.cpp)识别__requireCapability关键字,并在读取每个名字时通过findCapabilityName校验;无法解析的名字被丢弃并诊断为Diagnostics::UnknownCapability——E36105,"unknown capability name ' '."(slang-diagnostics.lua,id 36105,消息 "unknown capability name '~capability'.")。
函数体内可接受的拼写是__requireCapability(hlsl);。gap-intake 修复98d151850f5e给出的细节:名字在while (true)循环中读取,遇到逗号时AdvanceIf(this, TokenType::Comma)(slang-parser.cpp)继续,因此一条语句可以列出多个原子;语句以ReadToken(TokenType::RParent)/ReadToken(TokenType::Semicolon)(slang-parser.cpp)收尾,即)之后必须跟;。只有被接受的 token 才以原始Token存入requiredCaps,交由 targets.md 所述 capability 系统解释。相关测试: requirecapability-in-function.slang 在函数体内编译__requireCapability(hlsl);;requirecapability-unknown-name-rejected.slang 固化该消息。
诊断错误码速查
本文(连同 gap-intake 报告)共涉及六个用户可见的诊断码,全部定义于 source/slang/slang-diagnostics.lua,可在 slang-diagnostics-helpers.lua 的 E20001–E20012 注释范围内对照:
| 错误码 | 类别 | 消息 | 触发位置 |
|---|---|---|---|
E20001 | error | unexpected statement, expected expression | for头部的初始位置出现非声明/非表达式语句 |
E20004 | error | unexpected identifier, expected 'Range' | $for的Range位置出现其他标识符 |
E20101 | warning | potentially unintended empty statement at this location; use {} instead. | if后紧跟裸; |
E29110 | error | unknown target name ' ' | __target_switch中出现未识别的 capability 名 |
E36105 | error | unknown capability name ' '. | __requireCapability中出现无法解析的名字 |
E36109 | error | ' ' cannot be used as a target_switch case. | 同一标签在E29110之后由 checker 追加 |
需要说明的边界:gap-intake 报告指出,诊断 id、严重级别与消息文本都位于 source/slang/slang-diagnostics.lua,但它不在本文页面的watched_paths中,因此这五个新增代码(E20001、E20004、E20101、E29110、E36105、E36109)目前是未被追踪的;将其加入watched_paths可以闭环验证,04-ast-to-ir.md 已在相同条件下引用这些代码。此外,本文指向的 04-ast-to-ir.md 完全未提及DeferStmt,该指针目前是悬空的,值得在该页开一个缺口。
扩展阅读
- base.md —
Stmt、ModifiableSyntaxNode基类。 - declarations.md —
DeclStmt包装任意DeclBase。 - expressions.md — 语句中的每个
Expr*槽位。 - 02-parse-ast.md — 两阶段函数体解析。
- scopes.md —
ScopeStmt持有的ScopeDecl如何参与查找。 - 04-ast-to-ir.md —
DeferStmt、ThrowStmt、CompileTimeForStmt、TargetSwitchStmt的 IR 降级。 - grammar.md#statements — 语句文法产生式。
- 验证用 bundle 测试目录:docs/generated/tests/design/ast-reference/statements/。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考