RFC: Shader Static Analyzer
1. 改造背景
1.1 当前架构
@galacean/engine-shader-compiler 同时承担两个职责:
Runtime compilation — ShaderLab/GLSL → IShaderProgramSource,给引擎运行时用
Static diagnostics — verbose 构建下收集错误/警告
两个职责混在一份代码里,靠 jscc 条件编译(#if _VERBOSE)切换。
flowchart LR
subgraph SC["📦 @galacean/engine-shader-compiler (单一代码库)"]
direction TB
R["🏃 Runtime Compilation<br/>ShaderLab → IShaderProgramSource"]
D["🔍 Static Diagnostics<br/>~97 处 #if _VERBOSE"]
R -. 同源代码<br/>jscc 条件编译切换 .- D
end
SC --> B1["main.js"]
SC --> B2["module.js"]
SC --> B3["browser.js"]
SC --> B4["browser.min.js"]
SC --> V1["main.verbose.js"]
SC --> V2["module.verbose.js"]
SC --> V3["browser.verbose.js"]
SC --> V4["browser.verbose.min.js"]
classDef release fill:#d4edda,stroke:#28a745
classDef verbose fill:#fff3cd,stroke:#ffc107
classDef problem fill:#f8d7da,stroke:#dc3545
class B1,B2,B3,B4 release
class V1,V2,V3,V4 verbose
class D problem
Loading
1.2 改造原因
按重要性排列。核心是诊断与运行时编译耦合 (原因 1);附带"诊断被边角化"(原因 2)和"维护成本"(原因 3):
#
原因
子因 / 表现
量化证据
1
诊断与运行时编译耦合
A. 编译链路无法早停 :ShaderCompiler._parseShaderPass 把 Lexer → Preprocessor → Parser → Semantic → CodeGen 全绑在一个函数链路;要拿"语法/语义错误"必须先跑完 CodeGen(流水线图见下方)
后 2 阶段对诊断无意义但强制执行
B. 诊断状态散落 :错误分散在 ShaderSourceParser.errors / parser.errors / codeGen.errors 3 个独立队列;外部消费者要拿全量诊断只能复刻流程
3 个独立队列
C. 诊断逻辑绑定运行时 :SemanticAnalyzer 依赖 @galacean/engine-core.Logger + @galacean/engine enum;Node 调用需 mock 与诊断无关的运行时符号
需 mock 多个运行时符号
2
诊断能力被边角化
A. release 产物零诊断 :_VERBOSE 块外无错误记录,错误只走 console.error 自由文本
0 结构化错误
B. 当前能检测的语义错误仅 8 处 :AST.ts L243/L466/L395-400/L713/L717/L780/L1482
8 处
C. 完全没覆盖 :类型推断 / 函数签名 / 矩阵维度 / 变量重定义 / 副作用 / 采样器(不是 parser 算法问题,是 SemanticAnalyzer 没写 )
6 类未覆盖
3
维护成本
#if _VERBOSE 块跨 5 阶段(词法 / 预处理 / 语法 / 语义 / codegen + AST 节点池 + 调试打印):CFG.ts 31 + AST.ts 13 + ShaderSourceParser.ts 13 + SemanticAnalyzer.ts 3 + 其他 ~37
~97 处 TS 源码 / 8 份产物变体
原因 1.A 流水线图 (CodeGen 是诊断无关的浪费阶段):
flowchart LR
Input["源码"] --> L["Lexer"]
L --> P["Preprocessor"]
P --> SP["Parser<br/>(LALR)"]
SP --> SE["Semantic<br/>Analyzer"]
SE --> CG["CodeGen<br/>(GLSL string)"]
CG --> Out["IShaderProgramSource"]
SP -.错误.-> Err["console.error<br/>(_VERBOSE 内才收集)"]
SE -.错误.-> Err
Stage["💡 诊断真正需要的"] -.- L
Stage -.- P
Stage -.- SP
Stage -.- SE
Waste["⚠️ 诊断不需要"] -.- CG
Waste -.- Out
classDef need fill:#d4edda,stroke:#28a745
classDef waste fill:#f8d7da,stroke:#dc3545
classDef err fill:#fff3cd,stroke:#ffc107
class L,P,SP,SE need
class CG,Out waste
class Err err
Loading
verbose minified 与 release 仅差 +11K(160K vs 149K),拆分不是体积驱动,是维护性驱动 。
1.3 预期收益(能力变化全景)
完成本 RFC 后的能力清单。类型 列标识"保留 / 增强 / 新增":
维度
当前(dev/2.0)
目标
类型
架构
Lexer/Parser/AST + 诊断 + codegen 全耦合在 shader-compiler 一包
拆三包:shader-parser(中性 AST,唯一来源)→ shader-compiler(codegen)+ shader-analyzer(诊断),依赖结构上不可能 drift
增强
诊断形态
verbose-only 裸字符串,release 不可见,无定位,同错多遍
结构化 Diagnostic(DiagnosticType 分类 + severity + 行列 range + 上下文),release 可见,每错一遍
增强
诊断范围
零星语义检查(散布 AST.ts)
完整框架层规则目录(§3,45 类,对齐 Naga/Tint/glslang),并为 WebGPU 复用预留
增强 + 新增
消费方
仅引擎运行时
编辑器 / CI / LLM 均可独立调用,零运行时依赖 (无需 mock 引擎)
新增
公共 API
仅私有方法
稳定 ShaderAnalyzer.analyze() → 结构化 Diagnostic[];注入式诊断走 Logger
新增
运行时编译
codegen 不变
codegen 不变;失败报告升级为全 release 行列结构化(不再依赖 verbose 构建)
保留 + 增强
构建产物
8 份(4 release + 4 verbose)
取消 4 份 verbose 变体,单构建
增强
2. 改造方案
2.0 架构总览(三类信息 → 三层归属)
溯源元数据 (行列号、lexeme):Lexer 产出,附着在每个 token / AST 节点上。
语义线索 (类型、符号绑定、作用域、完整类型化 AST、IO 角色):parser 一趟产出并挂在 AST 上,中立通用、与目标平台无关,codegen 与 analyzer 共享同一份。
诊断判断 :非法判定内化在 parser 的线索计算 里(非法 = 线索取 error 值,自带 code + reason);analyzer 通用收集 error 线索 → Diagnostic,不进入 parser / codegen、零 per-node 特例 。
包职责
shader-parser:CFG → 带完整语义线索的中性 AST(唯一来源),含 IO 角色推断(ShaderIOAnalyzer)。
shader-compiler:消费 AST + 线索 → GLSL,零诊断、零角色派生 (IO 结构 / 角色全部消费自 parser)。
shader-analyzer:消费 AST + 线索 → 结构化诊断,与 shader-compiler 无依赖 (不驱动 LALR / codegen)。
shader-compiler 与 shader-analyzer 是 parser 之上两个平等的消费者 (对标 Naga 的 IR + 独立 Validator + 多 backend),互不感知。诊断在中性 IR 上进行、与 backend 无关:同一套框架层规则对 GLSL ES 与未来 WGSL 复用;目标分叉 的构造(如 array-of-array:WGSL 允许、GLSL ES 禁)analyzer 判不了对错 → 归 backend 生成时报错;平台内建(gl_FragColor)的形态转换 也归 backend emit。
设计参照:取 Naga / Tint / glslang 之长
本 RFC 的架构与诊断规则不是凭空设计,而是从业界三个成熟着色器编译器提炼、再裁剪适配 Galacean 的图形 vertex/fragment 场景。源码出处见文末「参考资料」。
先把借鉴接到痛点上 :§1.2 的核心病是诊断与运行时编译耦合 (拿错误必须先跑完无关的 codegen,诊断逻辑还绑死运行时)。两个 WebGPU 参考编译器对"校验放哪"给出了两种已验证的答案 —— Naga 分离、Tint 内联 。我们的诉求是"诊断可独立于编译、运行时零成本",故选 Naga 的分离式 。
Naga(与我们同构)—— 前端 → 中性 IR →(独立 Validator + 多后端):
flowchart LR
subgraph N["Naga(wgpu / Firefox WebGPU)"]
direction LR
NF["前端<br/>WGSL · GLSL · SPIR-V"] --> NIR["naga::Module<br/>中性 IR"]
NIR --> NV["valid::Validator<br/>独立校验模块"]
NIR --> NB["后端 Writer<br/>GLSL · MSL · HLSL · SPIR-V"]
end
subgraph U["本 RFC(同构)"]
direction LR
UF["ShaderLab 前端"] --> UIR["typed AST<br/>中性 IR · shader-parser"]
UIR --> UA["shader-analyzer<br/>独立诊断"]
UIR --> UC["shader-compiler<br/>GLSL(+ 未来 WGSL)"]
end
NIR -. 对应 .- UIR
NV -. 对应 .- UA
NB -. 对应 .- UC
classDef ir fill:#fff9c4,stroke:#f57f17
classDef val fill:#e3f2fd,stroke:#1976d2
classDef be fill:#d4edda,stroke:#28a745
class NIR,UIR ir
class NV,UA val
class NB,UC be
Loading
可核依据 :Naga 的 valid 是与前端/后端并列的独立模块 ;Validator::validate() 产出 ModuleInfo,后端 Writer 必须 消费它才能生成代码 —— "中性 IR + 独立校验 + 多后端"是 Naga 的真实结构,非本文断言。源码见文末。
Tint(对照:校验内联在 Resolver)—— 我们没选这条:
flowchart LR
TF["WGSL"] --> TR["Resolver<br/>类型解析 + 校验 交织"]
TR --> TIR["IR"] --> TB["后端<br/>HLSL · MSL · SPIR-V"]
classDef inline fill:#f8d7da,stroke:#dc3545
class TR inline
Loading
可核依据 :Tint 的校验在 resolver_validation.cc,与名称/类型解析交织在 Resolver 内 (内联式,类似 glslang)。这是另一种合法做法,但诊断无法独立于编译、运行时也省不掉 —— 不符合我们"诊断 dev-only、运行时零成本"的诉求 ,故取 Naga 的分离而非 Tint 的内联。
不是一言堂 :分离式(Naga / rustc / TypeScript)与内联式(Tint / glslang / Clang)都是真实编译器的成熟选择;我们按引擎诉求选分离式,理由见下「裁剪适配」。
参照
是什么
取它什么
Naga (wgpu 的着色器编译器,Firefox WebGPU 后端)
多前端 → 中性 IR(Module)→ 独立 Validator → 多后端
架构 :三包 + 平等消费者 + 独立诊断层;诊断分类 :按 IR 实体分(Type / Constant / GlobalVariable / Function / EntryPoint),每条规则为携带数据的 enum 变体
Tint (Dawn 的 WGSL 编译器,Chrome WebGPU 后端)
WGSL → IR → 多后端,校验在 Resolver 内
校验规则的真实条件与报错文案 (印证并补全 Naga 分类);未来 WGSL 目标层规则
glslang (Khronos GLSL/ESSL 官方参考编译器)
GLSL/ESSL → SPIR-V,权威校验
GLSL ES 100/300 的权威语义规则 —— 这是引擎的编译目标,以它为 ground truth
ANGLE (浏览器 WebGL 实现层)
GL ES → 原生 API,含 GLSL ES 校验器
WebGL 实际 GLSL ES 校验参照(多为平台 limits,本 RFC 不取)
裁剪适配 Galacean(为什么不照搬):
诊断 dev-only :Naga/Tint 在浏览器里校验是强制的(浏览器是安全边界);Galacean 不是安全边界(有 GPU 驱动 + glCompileShader 兜底),故诊断只在注册 analyzer 时跑,运行时零成本。
框架层与 backend 无关 :诊断在中性 IR 上做,不绑定 GLSL/WGSL → 未来加 WebGPU 时框架层规则零改动复用(学 Naga 的 IR 中立)。
目标相关归 backend :array-of-array 这种目标分叉 的合法性(WGSL 允许、GLSL ES 禁,analyzer 判不了对错)、以及 gl_FragColor→ES300 out 的形态转换 ,都归 backend;analyzer 只判 target 无关的合法性。
按范围裁剪 :本 PR 聚焦图形 vertex/fragment 框架层诊断 —— 含类型类诊断(先建类型系统,§4.1 S7);WGSL 目标层为后续后端、compute 等范围外(见 §3)。
诊断模型(参考 Khronos glslang)
不用数字错误码 —— 对齐 glslang(仅严重级别 + 位置 + token + 消息)。对外分类用 DiagnosticType (语义枚举,ESLint/Clang 流派,人可读、IDE 可用、稳定)。
severity :仅 error / warning。
消息 :glslang 风格 'token' : 说明 + 精确行列。
呈现 :出错行 + 前后 2 行上下文 + ^^^ 波浪线。
DiagnosticType
实现采用 Naga 式分层 typed-enum(类别 → 规则,变体携带诊断数据 — 位置、类型)。完整规则目录(按实体类别分类、锚定 Naga/Tint/glslang 真实源码、含范围判定与 ShaderLab 正误示例)见 §3 诊断规则目录 。
相对 dev/2.0
形态升级 :dev/2.0 诊断 verbose-only、release 不可见、同错多遍、无定位 → 本次结构化 + 可定位(行列 + 波浪线)+ 每错 1 遍 + release 可见 。
范围升级 :dev/2.0 零星检查 → 完整框架层规则目录(§3,对齐 Naga/Tint/glslang)。本 PR 落地 ✅ 直接档 (结构 / swizzle / 符号 / 控制流 / IO / 常量 / 资源)+ ⏸️ 类型类档 (先建类型系统,S7);WGSL 为后续后端、compute 范围外 —— 见 §3。
行为校准 :UseBeforeDeclaration 为 error(不软化);DuplicateEntryAssignment 正确检测同一入口的重复赋值。
运行时形态
运行时只走 parser(产线索)+ compiler(codegen),不引 analyzer → 零诊断、零诊断成本 。诊断逻辑全在 analyzer,仅编辑器 / CI / dev 注册 analyzer 时才跑;故运行时 bundle 不含诊断逻辑与错误字符串,取消 verbose 条件构建,单构建即可 。合入门槛见 §4.3。
2.1 包结构与依赖关系
抽出共享基础包 shader-parser ,让 shader-compiler 和 shader-analyzer 在物理层共用一套 Lexer / Parser / AST ,杜绝 drift:
flowchart TB
SP["📦 shader-parser(新基础包)<br/>Lexer · Preprocessor · Parser (LALR)<br/>LALR1 · AST 类型 · Grammar · SourceParser"]
SC["⚙️ shader-compiler(重构)<br/>依赖 shader-parser<br/>+ CodeGenVisitor<br/>+ ShaderCompiler runtime entry<br/>⚡ 尽可能快"]
SA["🔍 shader-analyzer(新增)<br/>依赖 shader-parser<br/>+ 通用收集 error 线索<br/>+ §3 诊断目录 + Diagnostic API<br/>📋 尽可能全面"]
SP --> SC
SP --> SA
classDef base fill:#fff9c4,stroke:#f57f17,font-weight:bold
classDef compiler fill:#d4edda,stroke:#28a745
classDef analyzer fill:#e3f2fd,stroke:#1976d2
class SP base
class SC compiler
class SA analyzer
Loading
三包目录布局:
packages/
├── shader-parser/ ← 新建(从现有 shader-compiler 抽出)
│ └── src/
│ ├── sourceParser/ ShaderSourceParser(手写,ShaderLab 顶层结构)
│ ├── lexer/ GLSL Lexer(Pass 内部)
│ ├── parser/ ShaderTargetParser + AST 类型 + Grammar
│ ├── lalr/ LALR1 状态机
│ └── Preprocessor.ts #include / 宏处理
├── shader-compiler/ ← 重构:删除被抽出的部分
│ └── src/
│ ├── codeGen/ (CodeGenVisitor,保留)
│ └── ShaderCompiler.ts (runtime entry)
└── shader-analyzer/ ← 新建
└── src/
├── ShaderAnalyzer.ts analyze() 入口:驱动 parse + ShaderIOAnalyzer,收集 error 线索
├── Diagnostic.ts Diagnostic / DiagnosticSeverity / AnalyzerOptions
└── convert.ts error 线索(GSError)→ 结构化 Diagnostic(类型系统在 parser,不在此)
两套 Parser 架构说明 (by design 的两层):
Parser
语言 / 范围
实现
输出
ShaderSourceParser
ShaderLab 顶层结构(Shader { SubShader { Pass { ... } } } / RenderState / Tags 块)
手写 + 正则扫描
IShaderSource(结构化 SubShader / Pass 数组)
ShaderTargetParser + LALR
Pass 内部的 GLSL 代码
LALR(1) 生成
GLSL AST
两层独立——SourceParser 先把 ShaderLab 拆出每个 Pass 的源码,再用 LALR Parser 对每个 Pass 内部的 GLSL 单独解析。两套各自的诊断也独立剥离到 analyzer :SourceParser 的诊断(缺 Shader / SubShader / Pass、RenderState 字段错)→ §3 的 A 类(结构 + RenderState);LALR Parser + AST 的诊断 → §3 的 B–G 类(类型 / 常量 / 资源 / 符号 / 控制流 / IO)。
两个目标对应 §4.1 的实施步骤 :
拆分(S1–S5,功能等价) :抽出 shader-parser 作为 single source of truth;shader-compiler 出清诊断;shader-analyzer 独立承载诊断,搬迁现有 verbose 模式可检测的所有错误 (不引入新检查)
增强(S6–S7) :shader-analyzer 补齐 §1.2 原因 2.C 的 6 类未覆盖语义检查 + 诊断增强(结构 / 类型 / 控制流 / IO);其中类型类诊断由 S7 类型系统解锁
职责契约 :
模块
内容
错误处理
shader-parser (新基础包)
Lexer / Preprocessor / Parser (LALR) / LALR1 / AST 类型 / Grammar / SourceParser;AST 节点的 semanticAnalyze 仅保留 codegen + 诊断都需要的语义动作 (类型推断、符号表注册、scope 管理)——剥离所有 reportError 诊断逻辑
parser 解析失败时 return null,不输出错误(错误处理在上层)
shader-compiler (重构)
依赖 shader-parser;保留 CodeGenVisitor + ShaderCompiler runtime entry;删除现有 lexer/ parser/ lalr/ sourceParser/ Preprocessor.ts(迁出到 shader-parser);删除 8 处 reportError + 大部分 #if _VERBOSE 块
编译失败 → 返回 undefined;强制 [shader-compiler] Compile failed at line N (col M): <reason> 到 console.error;行列追踪移出 #if _VERBOSE
shader-analyzer (新增)
依赖 shader-parser;通用收集 parser 算出的 error 线索(+ ShaderIOAnalyzer 全局 IO)→ §3 诊断;公共 Diagnostic API(analyze())
结构化诊断 / 多错误由 analyzer 提供,编辑器 / CI / LLM agent 走此路径
三条核心决策 :
抽 shader-parser 基础包 ——parser/AST 是 single source of truth,physical 上不可能 drift(不是靠"约定"或"CI 校验",是靠依赖结构);compiler 和 analyzer 平级,方向清晰
三个包都放 monorepo(packages/)——共生关系太紧,独立 repo 工程开销不划算
shader-compiler 完全出清诊断职责 ——assumed valid input(已被 analyzer 校验),runtime 只做 codegen,不重复跑诊断检查
2.2 AST 复用与诊断分离机制(本 RFC 命脉)
问题 :当前诊断逻辑耦合在 AST 节点的 semanticAnalyze() 方法里 ——类型推断 / 符号表注册(codegen 必需)和 reportError(诊断)混在同一个方法。要拆开,又不能让 AST drift。
做法(error-as-clue) :诊断不再是 AST 节点里的 reportError 调用,而是线索的一种取值 。semanticAnalyze 算类型 / 符号 / 角色线索时,非法 = 该线索取 error 值 (自带 code + reason + 位置)。analyzer 一段通用收集 逻辑「扫 error 线索 → 包成 Diagnostic」——零 instanceof、零 per-node 特例、不重走 AST。需跨函数全局信息的(IO 角色 / entry 签名)由 ShaderIOAnalyzer 一趟算出 error 线索,analyzer 一并收。
flowchart LR
subgraph PARSER["📦 shader-parser(基础包,single source of truth)"]
P["parse → AST"] --> SA["semanticAnalyze<br/>算 类型/符号/角色 线索<br/>(非法=线索取 error 值;<br/>codegen 与诊断共享)"]
end
SA --> CG["shader-compiler<br/>CodeGenVisitor<br/>⚡ 尽可能快,0 诊断检查"]
SA --> DV["shader-analyzer<br/>通用收集 error 线索<br/>🔍 扫线索→Diagnostic,零特例"]
CG --> OUT["最终 GLSL(IShaderProgramSource)"]
DV --> DIAG["Diagnostic[]"]
classDef shared fill:#fff9c4,stroke:#f57f17
classDef compiler fill:#d4edda,stroke:#28a745
classDef analyzer fill:#e3f2fd,stroke:#1976d2
class P,SA shared
class CG,OUT compiler
class DV,DIAG analyzer
Loading
层
归属
说明
Lexer / Preprocessor / Parser (LALR)
shader-parser 基础包
compiler 和 analyzer 都依赖它,single source of truth
AST 类型定义 + semanticAnalyze(算 类型/符号/角色 线索,非法=error 值)
shader-parser 基础包
codegen 和诊断都需要"每个表达式是什么类型";判定内化为线索取值,不再 reportError
诊断收集(扫线索 error 值 + ShaderIOAnalyzer 全局 IO)
shader-analyzer(通用收集器)
compiler 不读 error 线索 → 快;analyzer 一段通用扫描 → 全面,零 per-node 特例
CodeGen
shader-compiler 的 CodeGenVisitor
不变
这一机制同时回答三件事 :
怎么复用 AST ——parser 一趟产 AST + 线索(含 error 值)放 shader-parser;codegen 读有效线索生成、analyzer 扫 error 线索报诊断,同一份、无重复遍历
为什么 compiler 快 ——runtime 路径不读 error 线索(假设输入已被 analyzer 校验过)
为什么 analyzer 全面 ——加诊断 = 在 parser 让更多非法落到线索 error 值 + analyzer 通用扫描自动覆盖,不写 per-node 特例、不碰 codegen
改 Lexer / Parser / AST → 两边自动同步 :shader-parser 是唯一实现,analyzer 和 compiler 都通过依赖关系拿到同一份代码——物理上不可能 drift (不是靠"约定",是靠依赖结构)。任何 lexer / parser / AST 改动都在 shader-parser 内做,compiler / analyzer 重新构建即自动继承。
诊断增强 = 给 parser 补线索,不是给 analyzer 加特例 :要多报一类错,就让 semanticAnalyze 算线索时多覆盖一种非法取值(落到 error 线索),analyzer 的通用扫描自动报出 —— analyzer 永远是「扫线索」一段逻辑,不随规则增多长出 per-node 分支。
两个诊断入口、同一套收集 :error 线索的通用收集逻辑 两个入口共用、不重复实现 —— ① 独立 analyze(src)(编辑器 squiggles / CI,自己 parse);② 引擎注入(§2.3,复用 compiler 同一次 parse)。
实施约束(代码勘察)
抽包不是"零成本复用"——勘察发现四个必须处理的障碍。RFC 接受这些成本,对应方案如下:
障碍
现状
方案
反向依赖 :parser 代码 import 了"留在 compiler"的符号
10+ 处:ShaderCompiler.createPosition/createRange(静态工厂)、ShaderCompilerUtils.createObjectPool/createGSError、ShaderCompiler._processingPassText(静态字段)、AST.ts 对 CodeGenVisitor 的类型引用
这些符号本质是 parser 基础设施,随 parser 一起迁到 shader-parser (createPosition/Range/ObjectPool/GSError/_processingPassText 都归 shader-parser);AST.ts 对 CodeGenVisitor 的引用改为接口 (ICodeGenVisitor,定义在 shader-parser,compiler 实现),切断对具体 codegen 类的依赖
诊断在 parse 时就地产出 :semanticAnalyze 随 shift-reduce 归约即时算线索,无"先建完整 AST 再遍历"这一步
ASTNode.get() 创建节点即调 semanticAnalyze
正合 error-as-clue :非法落到线索 error 值、随归约收进 error 列表(IO 类由 ShaderIOAnalyzer 一趟补),analyzer 直接收集 —— 不需要 visitor、不重走 AST ,无遍历重复
运行时依赖 :parser 模块 import @galacean/engine 运行时符号
Logger(3 处,诊断日志)/ 对象池接口(7 处)/ ShaderLanguage enum(1 处)/ Color(3 处,RenderState 序列化)
Logger → 删除或回调注入;对象池 → shader-parser 内置最小 IObjectPool 接口;ShaderLanguage enum → 复制进 shader-parser;Color → 改用 [r,g,b,a] 数组(RenderState 序列化层转换)
共享可变状态的生命周期 :单例 parser / AST 节点池 / processingPassText 等"每次 parse 重置"的状态
AST 与语义线索是池化对象 ,仅在"本次 parse 到下次 parse 之间"有效
健壮性约束(必须遵守) :诊断须在下次 parse 前消费完;analyzer 不得跨 parse 缓存 AST 引用;processingPassText 等共享静态状态用 try/finally 复位,避免一次失败污染下一次。注入路径(§2.3)在同一次 parse 内完成诊断 + codegen,天然满足
Preprocessor.parse() 已是无状态纯入参(includeMap / chunkOutputCache 通过参数传递),§4.1 的 includeMap 接口改造成本低。
2.3 API 设计
export interface AnalyzerOptions {
includeMap ?: Map < string , string > ; // #include 解析
}
export enum DiagnosticSeverity { Error = "error" , Warning = "warning" }
export interface Diagnostic {
severity : DiagnosticSeverity ; // 枚举 Error / Warning
code : DiagnosticType ; // 枚举,命中 §3 哪条规则
message : string ; // glslang 风格 'token' : 说明
range : { start : Position ; end : Position } ; // Position = { line, column, offset }
relatedSource ?: string ; // 出错 pass 源码,供"上下文 + ^^^"呈现
}
export interface AnalyzedPass {
program : ShaderProgramAst ; // 该 pass 解析出的 GLSL AST,喂给 compiler 做 codegen
vertexEntry : string ;
fragmentEntry : string ;
}
export interface AnalysisResult {
diagnostics : Diagnostic [ ] ;
passes : AnalyzedPass [ ] ; // 每个 pass 已解析的 AST,供编辑器复用做 codegen,只解析一次
}
export class ShaderAnalyzer {
analyze ( source : string , options ?: AnalyzerOptions ) : AnalysisResult ;
}
引擎集成:注入即诊断(编辑器 / CI / dev)
诊断与编译通过 WebGLEngine.create 注入达成 —— 注入 shaderAnalyzer 即开诊断,省略则纯编译、零诊断成本:
const engine = await WebGLEngine . create ( {
canvas,
shaderCompiler : new ShaderCompiler ( ) ,
shaderAnalyzer : new ShaderAnalyzer ( ) , // 注入 = 开诊断(→ Logger);省略 = 纯编译
} ) ;
Shader . create ( src ) ; // 一次解析 → analyzer 诊断(→ Logger)+ compiler 出 GLSL,不二次解析
诊断怎么拿 :注入路径诊断走 Logger (Logger.enable() 控制台可见);要结构化 诊断(编辑器画波浪线 / CI),用独立入口 analyzer.analyze(src),直接拿返回的 Diagnostic[]。
一次解析、两个产物 :compiler 持有注入的 analyzer,_parseShaderPass 解析后、codegen 前让 analyzer 诊断同一份 AST ,省掉二次解析。
依赖方向 :compiler 仅依赖 analyzer 的接口(IShaderAnalyzer,design 包),不依赖其实现;analyzer 不依赖 compiler。
关键决策 :
决策项
内容
诊断码
code 用语义枚举 DiagnosticType(如 InvalidSwizzle),非数字码——自解释、对标 glslang 文案风格;§3 目录即其全集
诊断位置约定
Diagnostic.range 全文件绝对坐标(line / column 从 source 开头);多 SubShader / Pass 不切分坐标系
性能预算
IDE < 100ms(keystroke 响应);CI < 5s / corpus 全量 < 5min;LLM agent 无硬性要求
版本兼容
shader-analyzer 与 @galacean/engine 绑定 major(2.x ↔ 2.x);内置引擎符号表随 engine 实际声明,engine major 升级时 analyzer 必须同步发版 (CI sync check 见 §4.3)
包体积预算
analyzer minified < 200 KB ;compiler release ~149K 基本不变 + 取消 4 份 verbose 变体 ;CI > 10% 阻塞 merge
3. 诊断规则目录
本 RFC 的核心交付物。诊断为图形 vertex/fragment 框架层 (与 backend 无关)。每条规则都标了来源 (可回源码核对)与范围 (本 PR 是否实现),并给出 ShaderLab 正/误写法 —— 既是错误文案的依据,也是 §4.2 ABTest 的雏形。
「来源」列简写说明:
简写
含义
[N:变体名]
Naga valid 模块的真实错误 enum 变体(如 [N:InvalidSwitchType]),可回 GitHub 源码逐条核对
[T:函数名]
Tint resolver_validation.cc 的真实校验函数(如 [T:ValidateSwitch])
[GLSL]
GLSL ES(ESSL)规范 / glslang 校验
自有
Galacean ShaderLab 特有,业界无对应
「范围」列说明(✅ 与 ⏸️ 都是本 PR):
✅ 本 PR · 直接 :拆包后直接落地(结构 / swizzle / 类型成员 / 符号 / 控制流 / IO / 常量 / 资源)。
⏸️ 本 PR · 需类型系统 :运算 / 赋值 / 返回等类型类 诊断 —— 本 PR 先建类型系统(含泛型函数重载解析)后落地。
⛔ 范围外 :ShaderLab 仅图形 vertex/fragment,无 compute / atomic / 光追 / mesh 管线(WebGPU/WGSL 为后续后端,见 §5)。
✅ 与 ⏸️ 每条都须有 ABTest(见 §4.2);⏸️ 仅表示需先建类型系统,仍属本 PR。
DiagnosticType 列 = 每条规则的诊断码,也是 ABTest 目录名(tests/fixtures/<DiagnosticType>/)。复用 dev/2.0 已有 27 个,其余本 PR 新增;带 * 为一族(如 InvalidRenderState* 含属性 / 枚举值 / 值类型等多码)。
A · ShaderLab 结构(DSL 层)
DiagnosticType
规则
来源
范围
✅ 正确
❌ 错误
MissingEntry
Pass 须绑 vertex+fragment 入口
自有
✅
VertexShader=vert; FragmentShader=frag;
VertexShader=vert;
DuplicateEntryAssignment
入口不重复赋值
[N:EntryPointError.Conflict]
✅
VertexShader=vert;
VertexShader=vert; VertexShader=v2;
EntryNotFound
入口须指向已定义函数
自有
✅
Varyings vert(){} VertexShader=vert;
VertexShader=notDefined;
InvalidRenderState*
RenderState 属性 / 枚举值 / 值类型合法
自有
✅
BlendOperation=Add;
BlendOperation=Foo;
B · 类型与表达式
DiagnosticType
规则
来源
范围
✅ 正确
❌ 错误
InvalidSwizzle
swizzle 仅用于向量、分量不超维度
[N:InvalidVectorType/InvalidSwizzleComponent]
✅
vec3 v; v.xy
vec2 v; v.xyz
UndeclaredStructMember
struct 成员须存在
[GLSL]
✅
struct S{float a;}; s.a
s.b
ConstructorArgCount
构造函数参数数量匹配
[T:ValidateVariableConstructorOrCast]
✅
vec3(1.,2.,3.) / vec3(1.)
vec3(1.,2.)
InvalidSplat
splat 仅用于标量
[N:InvalidSplatType]
✅
vec3(1.0)
—
NonIndexableType
下标对象须可索引
[N:InvalidBaseType]
✅
vec3 v; v[0]
float f; f[0]
IndexOutOfBounds
常量下标不越界 / 非负
[N:IndexOutOfBounds/NegativeIndex]
✅
vec3 v; v[2]
v[3] / v[-1]
ConstDivideByZero
常量除零
[N:DivideByZero]
✅
1.0/2.0
1.0/0.0
ShiftOutOfRange
移位量不超位宽
[N:ShiftAmountTooLarge]
✅
i << 3
i << 999
InvalidBinaryOperands
二元运算操作数类型兼容
[N:InvalidBinaryOperandTypes]
⏸️
vec3 a,b; a+b
vec3 v; mat4 m; v+m
InvalidUnaryOperand
一元运算操作数类型合法
[N:InvalidUnaryOperandType]
⏸️
-f / !b
bool b; -b
ConstructorArgType
构造函数参数类型匹配
[GLSL]
⏸️
vec4(v3,1.)
vec4(v3, b)
NonIntegerIndex
下标须整型
[N:InvalidIndexType]
⏸️
a[i]
a[1.5]
InvalidConversion
类型转换合法
[N:InvalidCastArgument]
⏸️
float(i)
float(sampler)
C · 常量
DiagnosticType
规则
来源
范围
✅ 正确
❌ 错误
NonConstArraySize
数组大小须常量
[GLSL]
✅
const int N=4; float a[N];
int n=f(); float a[n];
NonConstInitializer
const 须常量初始化
[N:LocalVariableError.NonConstOrOverrideInitializer]
✅
const float PI=3.14;
const float x=v_uv.x;
D · 资源 / 全局
DiagnosticType
规则
来源
范围
✅ 正确
❌ 错误
InvalidUniformType
uniform / sampler 声明类型合法
[N:GlobalVariableError.InvalidType]
✅
uniform sampler2D u_tex;
uniform void u;
UniformInitializer
uniform 不可初始化
[N:GlobalVariableError.InitializerNotAllowed] [GLSL]
✅
uniform float u;
uniform float u=1.0;
ExpectedSampler
采样目标须是 sampler
[N:ExpressionError.ExpectedSamplerType]
✅
texture(u_tex, uv)
texture(u_time, uv)
E · 符号与作用域
DiagnosticType
规则
来源
范围
✅ 正确
❌ 错误
UseBeforeDeclaration
使用前须声明
[N:ExpressionError.NotInScope]
✅
float x=1.; float y=x;
float y=x; float x=1.;
Redefinition
同作用域不重定义
[GLSL]
✅
float x; { float x; }
float x; float x;
UndefinedFunction
函数调用须解析到定义
[N:FunctionError.InvalidCall]
✅
float f(){} f();
notAFunc();
NoMatchingOverload
调用实参数量匹配
[N:CallError.ArgumentCount] [T:ValidateFunctionCall]
✅
f(float x){} f(1.)
f() / f(1.,2.)
NoMatchingOverload
调用实参类型匹配
[N:CallError.ArgumentType]
⏸️
f(1.0)
f(a+b)(算术实参无类型)
F · 函数与控制流
DiagnosticType
规则
来源
范围
✅ 正确
❌ 错误
ReturnInVoidFunction / MissingReturn
void 不返值 / 非 void 须返回
[T:ValidateFunction] [GLSL]
✅
void f(){return;} / float g(){return 1.;}
void f(){return 1.;} / float g(){}
NonConstructibleReturnType
返回类型须可构造
[N:NonConstructibleReturnType]
✅
float f()
sampler2D f()
NonBoolCondition
if 条件须 bool
[N:InvalidIfType]
✅
if(x>0.){}
if(x){}(float)
NonIntegerSwitch
switch selector 须整型
[N:InvalidSwitchType] [T:ValidateSwitch]
✅
switch(i){}
switch(f){}
DuplicateSwitchCase
switch case 不重复
[N:ConflictingSwitchCase] [T:ValidateSwitch]
✅
case 1: case 2:
case 1: case 1:
InvalidSwitchDefault
switch 恰一个 default
[N:MissingDefaultCase/MultipleDefaultCases] [T:ValidateSwitch]
✅
... default:
无 / 多 default
MisplacedControlFlow
break/continue 须在循环或 switch
[N:BreakOutsideOfLoopOrSwitch/ContinueOutsideOfLoop] [T:ValidateBreak/Continue]
✅
for(){break;}
void f(){break;}
RecursiveFunction
禁递归
[GLSL/WGSL 通则]
✅
非递归
float f(){return f();}
UnreachableCode
return 之后死代码
[N:UnvisitedExpression 类]
✅
—
return x; y=1.;
ReturnTypeMismatch
返回类型匹配
[N:InvalidReturnType] [T:ValidateReturn]
⏸️
float f(){return 1.;}
float f(){return v3;}(算术返回值无类型时漏报)
AssignTypeMismatch
赋值左值可写 + 类型兼容
[N:InvalidStoreTypes] [T:ValidateAssignment]
⏸️
float x; x=1.0;
vec3 v; v=1.0;(算术右值无类型时漏报)
G · 管线 IO
DiagnosticType
规则
来源
范围
✅ 正确
❌ 错误
VertexEntryReturnType / FragmentEntryReturnType
入口签名合法(vert 返 varying;frag 收 varying)
[N:EntryPointError]
✅
Varyings vert(Attributes a){}
float vert(Attributes a){}
InvalidVaryingStruct
IO 结构成员须 IO 可共享类型
[N:VaryingError.NotIOShareableType]
✅
struct V{vec4 pos; vec2 uv;}
struct V{sampler2D s;}
NestedIOStruct
不以嵌套结构作 IO
[T:ValidateEntryPoint]
✅
扁平 struct
struct 套 struct 作 IO
StructRoleConflict
struct 角色不冲突(不同时作 attribute 与 varying)
[N:VaryingError]
✅
分开 Attributes/Varyings
同 struct 既输入又输出
GlFragColorWithMrt*
输出一致性(单色输出与 MRT 互斥)
自有
✅
gl_FragColor 或 MRT 二选一
两者并用
MissingVertexPosition
vertex 须输出位置
[N:EntryPointError.MissingVertexOutputPosition] [T:ValidateFunction]
✅
vert 写 position
vert 不写 position
NonFlatIntegerVarying
整型 varying 须 flat 插值
[N:VaryingError.InvalidInterpolationForInteger] [T:ValidateEntryPoint]
✅
flat int id;
整型 varying 不标 flat
范围外 / 后续后端(编目)
诊断族
归属
说明
来源(编目)
atomic / subgroup / workgroup / 光追 / mesh / uniformity
范围外
ShaderLab 仅图形 vertex/fragment,无 compute 管线
[N:FunctionError 高级变体]
WGSL 目标层诊断、@group/@binding
后续后端
需 WGSL 后端;中性 IR 已为复用预留(见 §5)
[N:VaryingError.BindingCollision]
array-of-array(a[2][2])
backend 生成时报
目标分叉 :WGSL 允许、GLSL ES 禁 → 平台无关的 analyzer 判不了对错 → GLES100/300 lower 不出来时报错
Naga valid/ 无此规则、back/glsl::Error 有
判据 :同一写法在某 target 合法、另一 target 非法(target-分叉)→ analyzer 不判 ,归 backend 生成时报错(对标 Naga:中性 valid/ 忽略 array-of-array,back/glsl emit 时才报)。never-flips 的(如输出机制不可混用、整型 varying 须 flat)仍属 analyzer。
类型系统(本 PR,⏸️ 类型类诊断的实现基础) :重载解析器已存在 —— shader-parser 的 builtin/functions.ts::resolveOverload 已实现完整泛型家族重载(GenType / size·scalarType 维度锁定 / 返回投影 / TypeAny 通配)。S7 = 重启用 AST.ts 被注释的二元运算 type-deduce(MultiplicativeExpression / AdditiveExpression,原 exp1.type === exp2.type 太糙:vec3 * float 合法但类型不同、泛型返回 TypeAny 会算错)→ 换成正经 GLSL 算子类型规则(scalar/vec/mat,GLSL ES spec §5 一张固定小表)+ 接住 resolveOverload 的 TypeAny 传播 + functions.ts 按 spec §8 补缺 builtin。纯本地实现、无外部研究 (见 §4.1 S7)。
错误传播策略(跨 check)
跨 check 的失败传播采用 continue-with-unknown 而非 fail-fast:
上游 check 失败时,受影响的 AST 节点标记为 unknown 状态(例:类型推断失败 → Expression.type = "unknown")
下游 check 遇到 unknown 时跳过该检查 ,不重复报错 (避免一个根因被多次报告造成噪音)
例子:函数调用 foo(undefined_var)——作用域检查已报"未定义变量"后,类型推断将 undefined_var.type 置为 unknown,函数签名检查遇 unknown 参数则跳过
4. 实施计划
一个 PR 内的步骤序列,不是多个 PR 。每步保持纯粹、可独立验收、现有测试全绿;中间态遵循"先加后删"(先在 analyzer 建好诊断并验收对等,再删 shader-parser 旧诊断),任何时刻 verbose 消费者都有可用诊断。
为什么是这套拆法 :shader-compiler 是叶子包 ——没有任何引擎内部包在 package.json 依赖它(core/Shader.ts 仅注释提及),抽包对引擎 runtime 零影响 ,风险集中在外部消费者(editor / CI)与根 rollup.config.js 的 verbose 变体。相较"保持现状继续维护 #if _VERBOSE"(诊断与编译永久耦合 / 8 份产物 / release 零诊断)或"完全重写"(工程量翻倍、丢现有 reportError 经验),抽 shader-parser 基础包 + 新建 shader-analyzer 让依赖结构物理上不可能 drift ,是 single source of truth 的最小代价路径。
4.1 实施步骤
步骤
内容
风险
前序
验收
S1
消除反向依赖(compiler 内部,不改包结构)
低
—
现有测试全绿,零行为变化
S2
解耦运行时依赖(compiler 内部)
中
S1
现有测试全绿,编译输出字节对等
S3
物理抽出 shader-parser 包(诊断暂随迁)
中高
S2
现有测试全绿,行为完全不变
S4
新建 shader-analyzer + 搬迁现有诊断(先加 )
中
S3
analyzer 诊断 ≥ 原 verbose(corpus 对等)
S5
剥离 shader-parser 诊断 + 取消 verbose 变体(后删 )
低
S4
single source of truth;verbose 消费者已切 analyzer
S6
增强诊断范围(§3 目录 ✅ 直接落地项)
中
S5
§3 每个 ✅ 类型有 ABTest 且通过
S7
建类型系统(含泛型函数重载解析)→ 解锁 ⏸️ 类型类诊断
高
S6
§3 每个 ⏸️ 类型有 ABTest 且通过
S1 — 消除反向依赖 (纯内部重构,不新建包、不改行为,为抽包扫清障碍)
S2 — 解耦运行时依赖 (parser 相关模块不再 import @galacean/engine 运行时符号)
S3 — 物理抽出 shader-parser 包 (只搬不改,诊断逻辑暂随迁,行为完全不变)
S4 — 新建 shader-analyzer + 搬迁现有诊断 (先加,行为对等原 verbose,不引入新检查)
S5 — 剥离 shader-parser 诊断 + 取消 verbose (后删,达成 single source of truth)
S6 — 增强诊断范围 (§3 目录 ✅ 直接落地项,analyzer 内新增,不动 compiler / parser)
S7 — 建类型系统 (解锁 ⏸️ 类型类诊断)
4.2 测试:每个诊断类型一组 ABTest
核心要求:§3 的每个诊断类型(✅ 与 ⏸️)的每个 case 都有一组 ABTest —— 正确版 (应通过、零该类型诊断)+ 错误版 (应失败、精确命中该 DiagnosticType)。既是验收标准,也支撑实现期循环自检(改一条规则 → 跑对应 ABTest → 立即知对错)。
类型
适用对象
验证标准
实施载体
ABTest per-case(核心)
§3 每个类型的每个 case(「✅ 正确 / ❌ 错误」两列即雏形)
正确版 :analyze() 不含该类型诊断;错误版 :精确命中(code = 对应 DiagnosticType + severity + range),忽略 message 文本
tests/fixtures/<DiagnosticType>/{ok.shader, err.shader, expected.diag.json}
集成测试 end-to-end
真实 corpus(examples/ 下 shader)
golden file 比对;新增类型不应在已 pass 的 corpus 上误报
tests/corpus/
性能基准
small (~50) / medium (~200) / large (~1000 行)
small < 10ms / medium < 50ms / large < 100ms;CI 回归 > 50% 退化阻塞 merge
vitest bench;同时记录包体积 + 内存峰值
覆盖目标 :§3 的 45 个本 PR 类型全部有 ABTest (37 ✅ 直接 + 8 ⏸️ 需类型系统)。复合规则(如 swizzle「非向量 / 超维度」、构造函数「参数过多 / 过少」、switch「重复 case / 缺 default / 多 default」)拆成多 case,故 case 数 > 类型数(约 60+ 组正/误 fixture)。CI 强制:新增类型无 ABTest 不得 merge ,保证 0 skip。
4.3 合入门槛
门槛
标准
功能对等
S5 完成时 analyzer 诊断 ≥ dev/2.0 verbose(golden corpus 逐条对等,不退化)
编译字节对等
S1–S5 每步 shader-compiler runtime 输出 IShaderProgramSource 不变
包体积
analyzer minified < 200 KB;compiler ~149K 不变;取消 4 份 verbose 变体;CI > 10% 阻塞
性能
§4.2 基准全过;CI 回归 > 50% 阻塞
ABTest 覆盖
§3 每个类型有 ABTest(CI 强制,无则不得 merge)
引擎符号表同步
CI 比对 analyzer 内置引擎符号表与 packages/core 声明,drift 时 fail
4.4 失败模式分析(FMEA)
逐一枚举失败场景与应对:
ID
失败场景
触发条件
应对
残余风险
F1
引擎符号表 drift
core 加新 builtin 未同步 analyzer
§4.3 CI sync check fail
升级时需手动修同步
F2
runtime 编译失败无诊断
取消 verbose 后用户找不到原因
S5 错误协议:最小 console.error(含行列)
信息不如 verbose 详细
F3
多诊断重复报错(噪音)
一个根因触发多个 check
§3 continue-with-unknown + unknown 标记
unknown 节点可能漏报下游
F4
analyzer 性能不达标
IDE 场景 > 100ms 失交互感
§2.3 性能预算 + §4.2 基准卡 CI
低
F5
测试 corpus 不充分
真实 shader 出现未覆盖语法
§4.2 集成 corpus 持续扩充
中(依赖 corpus 维护)
F6
二元运算推断重启用后回归
重启用 deduce 与现有 resolveOverload 的 TypeAny 交互出错
S7 复用现有 functions.ts::resolveOverload(已实现),只补 GLSL 算子类型规则;ABTest 守 ⏸️ 类型
中(S7 复杂度高)
F7
analyzer 与引擎版本不匹配
analyzer 2.0 + engine 2.1
§2.3 版本兼容(major 绑定)+ §4.3 sync check
低
4.5 关键文件
路径以 dev/2.0 现状给出;拆包后 parser 相关文件迁到 packages/shader-parser/src/。
路径
用途
packages/shader-compiler/src/parser/AST.ts
8 处 reportError(L243/375/398/466/713/717/780/1482)的剥离起点;L2 import CodeGenVisitor 是要切断的反向依赖(改 ICodeGenVisitor 接口)
packages/shader-compiler/src/parser/SemanticAnalyzer.ts
诊断收集机制(errors[] / reportError);新 Diagnostic 模板;依赖 ShaderCompiler._processingPassText(反向依赖,要迁出)
packages/shader-compiler/src/ShaderCompiler.ts
主入口;createPosition / createRange / _processingPassText / _includeMap 都在此——这些 parser 基础设施要迁到 shader-parser;_parseShaderPass 的 codegen 调度留下
packages/shader-compiler/src/ShaderCompilerUtils.ts
createObjectPool / createGSError——parser 侧也依赖,随 shader-parser 迁出
packages/shader-compiler/src/common/BaseLexer.ts
BaseLexer / BaseToken 反向依赖 ShaderCompiler.createPosition + ShaderCompilerUtils;迁出时一并处理
packages/shader-compiler/src/sourceParser/ShaderSourceParser.ts
ShaderLab 顶层 parser;errors[] 诊断剥离起点(§3 A 类:结构 + RenderState)
packages/shader-compiler/src/Preprocessor.ts
实测已无状态纯入参;迁出后导出 IncludeMap / ChunkOutputCache 类型
packages/shader-compiler/src/lalr/LALR1.ts
conflict 处理(_isKnownShiftPreferred);Logger 运行时依赖(要解耦)
packages/shader-compiler/rollup.config.js + 根 rollup.config.js
jscc _VERBOSE 配置 + verbose 变体生成;取消 4 份 verbose 变体在此改
5. 未来工作(Out of Scope)
本 RFC 范围之外的演进方向:
方向
项
描述 / 触发条件
多 backend
WGSL 支持
新建 WGSL 后端 + 其 target-specific 检查(中性 IR 已为复用预留;engine 支持 WebGPU 后立项)。本 PR 不出任何 WGSL 接口 / 参数
Cross-target 诊断
ShaderLab → GLSL ES 100/300 转换的 target-specific 错误(如 in/out 仅 ES 300 / layout 限制 / mediump 行为差异);可作为新诊断族
IDE 生态
LSP server
@galacean/engine-shader-language-server 包装 ShaderAnalyzer,含 incremental + go-to-definition + semantic tokens
VSCode 扩展
基于 LSP server + 语法高亮 + Diagnostic 渲染
Cursor / Continue 集成
让 LLM IDE 拿到结构化 shader 诊断
性能优化
增量分析
analyzeIncremental API(本 PR 不导出;LSP server 需要时引入),用于 IDE keystroke 响应
AST 缓存
单 shader 多次 analyze 复用 AST(基于源码 hash)
诊断分组并行
独立诊断族可 Web Worker 并行(需评估 worker 启停成本)
工具链生态
@galacean/shader-lint CLI
包装 ShaderAnalyzer + lint 规则,给 pre-commit / CI 用
AI-assisted fix
基于结构化诊断 + LLM 自动生成修复建议
shader registry
社区 Galacean shader 包索引,analyzer 验证合规性
第三方 Lint 规则市场
基于自定义规则 API 的社区规则包(@galacean/shader-lint-recommended / mobile-perf 等),ESLint plugin 心智模型
开发工具
Shader Playground
examples/shader-playground/(Vite + Monaco)实时校验,shader-analyzer dogfooding;浏览器内纯静态分析、无需 defines 上下文
自定义 Lint 规则 API
analyzer.registerRule() + RuleContext(symbolTable / builtins / report),ESLint plugin 心智模型
6. 参考资料(Sources)
诊断规则与架构的真实出处,§3 每条 [N:] / [T:] 标注均可回此核对:
RFC: Shader Static Analyzer
1. 改造背景
1.1 当前架构
@galacean/engine-shader-compiler同时承担两个职责:IShaderProgramSource,给引擎运行时用两个职责混在一份代码里,靠 jscc 条件编译(
#if _VERBOSE)切换。flowchart LR subgraph SC["📦 @galacean/engine-shader-compiler (单一代码库)"] direction TB R["🏃 Runtime Compilation<br/>ShaderLab → IShaderProgramSource"] D["🔍 Static Diagnostics<br/>~97 处 #if _VERBOSE"] R -. 同源代码<br/>jscc 条件编译切换 .- D end SC --> B1["main.js"] SC --> B2["module.js"] SC --> B3["browser.js"] SC --> B4["browser.min.js"] SC --> V1["main.verbose.js"] SC --> V2["module.verbose.js"] SC --> V3["browser.verbose.js"] SC --> V4["browser.verbose.min.js"] classDef release fill:#d4edda,stroke:#28a745 classDef verbose fill:#fff3cd,stroke:#ffc107 classDef problem fill:#f8d7da,stroke:#dc3545 class B1,B2,B3,B4 release class V1,V2,V3,V4 verbose class D problem1.2 改造原因
按重要性排列。核心是诊断与运行时编译耦合(原因 1);附带"诊断被边角化"(原因 2)和"维护成本"(原因 3):
ShaderCompiler._parseShaderPass把 Lexer → Preprocessor → Parser → Semantic → CodeGen 全绑在一个函数链路;要拿"语法/语义错误"必须先跑完 CodeGen(流水线图见下方)ShaderSourceParser.errors/parser.errors/codeGen.errors3 个独立队列;外部消费者要拿全量诊断只能复刻流程SemanticAnalyzer依赖@galacean/engine-core.Logger+@galacean/engineenum;Node 调用需 mock 与诊断无关的运行时符号_VERBOSE块外无错误记录,错误只走console.error自由文本AST.tsL243/L466/L395-400/L713/L717/L780/L1482#if _VERBOSE块跨 5 阶段(词法 / 预处理 / 语法 / 语义 / codegen + AST 节点池 + 调试打印):CFG.ts31 +AST.ts13 +ShaderSourceParser.ts13 +SemanticAnalyzer.ts3 + 其他 ~37原因 1.A 流水线图(CodeGen 是诊断无关的浪费阶段):
flowchart LR Input["源码"] --> L["Lexer"] L --> P["Preprocessor"] P --> SP["Parser<br/>(LALR)"] SP --> SE["Semantic<br/>Analyzer"] SE --> CG["CodeGen<br/>(GLSL string)"] CG --> Out["IShaderProgramSource"] SP -.错误.-> Err["console.error<br/>(_VERBOSE 内才收集)"] SE -.错误.-> Err Stage["💡 诊断真正需要的"] -.- L Stage -.- P Stage -.- SP Stage -.- SE Waste["⚠️ 诊断不需要"] -.- CG Waste -.- Out classDef need fill:#d4edda,stroke:#28a745 classDef waste fill:#f8d7da,stroke:#dc3545 classDef err fill:#fff3cd,stroke:#ffc107 class L,P,SP,SE need class CG,Out waste class Err err1.3 预期收益(能力变化全景)
完成本 RFC 后的能力清单。类型列标识"保留 / 增强 / 新增":
shader-parser(中性 AST,唯一来源)→shader-compiler(codegen)+shader-analyzer(诊断),依赖结构上不可能 driftDiagnostic(DiagnosticType分类 + severity + 行列 range + 上下文),release 可见,每错一遍AST.ts)ShaderAnalyzer.analyze()→ 结构化Diagnostic[];注入式诊断走 Logger2. 改造方案
2.0 架构总览(三类信息 → 三层归属)
包职责
shader-parser:CFG → 带完整语义线索的中性 AST(唯一来源),含 IO 角色推断(ShaderIOAnalyzer)。shader-compiler:消费 AST + 线索 → GLSL,零诊断、零角色派生(IO 结构 / 角色全部消费自 parser)。shader-analyzer:消费 AST + 线索 → 结构化诊断,与 shader-compiler 无依赖(不驱动 LALR / codegen)。shader-compiler与shader-analyzer是 parser 之上两个平等的消费者(对标 Naga 的 IR + 独立 Validator + 多 backend),互不感知。诊断在中性 IR 上进行、与 backend 无关:同一套框架层规则对 GLSL ES 与未来 WGSL 复用;目标分叉的构造(如 array-of-array:WGSL 允许、GLSL ES 禁)analyzer 判不了对错 → 归 backend 生成时报错;平台内建(gl_FragColor)的形态转换也归 backend emit。设计参照:取 Naga / Tint / glslang 之长
本 RFC 的架构与诊断规则不是凭空设计,而是从业界三个成熟着色器编译器提炼、再裁剪适配 Galacean 的图形 vertex/fragment 场景。源码出处见文末「参考资料」。
先把借鉴接到痛点上:§1.2 的核心病是诊断与运行时编译耦合(拿错误必须先跑完无关的 codegen,诊断逻辑还绑死运行时)。两个 WebGPU 参考编译器对"校验放哪"给出了两种已验证的答案 —— Naga 分离、Tint 内联。我们的诉求是"诊断可独立于编译、运行时零成本",故选 Naga 的分离式。
Naga(与我们同构)—— 前端 → 中性 IR →(独立 Validator + 多后端):
flowchart LR subgraph N["Naga(wgpu / Firefox WebGPU)"] direction LR NF["前端<br/>WGSL · GLSL · SPIR-V"] --> NIR["naga::Module<br/>中性 IR"] NIR --> NV["valid::Validator<br/>独立校验模块"] NIR --> NB["后端 Writer<br/>GLSL · MSL · HLSL · SPIR-V"] end subgraph U["本 RFC(同构)"] direction LR UF["ShaderLab 前端"] --> UIR["typed AST<br/>中性 IR · shader-parser"] UIR --> UA["shader-analyzer<br/>独立诊断"] UIR --> UC["shader-compiler<br/>GLSL(+ 未来 WGSL)"] end NIR -. 对应 .- UIR NV -. 对应 .- UA NB -. 对应 .- UC classDef ir fill:#fff9c4,stroke:#f57f17 classDef val fill:#e3f2fd,stroke:#1976d2 classDef be fill:#d4edda,stroke:#28a745 class NIR,UIR ir class NV,UA val class NB,UC beTint(对照:校验内联在 Resolver)—— 我们没选这条:
flowchart LR TF["WGSL"] --> TR["Resolver<br/>类型解析 + 校验 交织"] TR --> TIR["IR"] --> TB["后端<br/>HLSL · MSL · SPIR-V"] classDef inline fill:#f8d7da,stroke:#dc3545 class TR inlineModule)→ 独立 Validator → 多后端裁剪适配 Galacean(为什么不照搬):
glCompileShader兜底),故诊断只在注册 analyzer 时跑,运行时零成本。gl_FragColor→ES300out的形态转换,都归 backend;analyzer 只判 target 无关的合法性。诊断模型(参考 Khronos glslang)
DiagnosticType(语义枚举,ESLint/Clang 流派,人可读、IDE 可用、稳定)。error/warning。'token' : 说明+ 精确行列。^^^波浪线。DiagnosticType
实现采用 Naga 式分层 typed-enum(
类别 → 规则,变体携带诊断数据 — 位置、类型)。完整规则目录(按实体类别分类、锚定 Naga/Tint/glslang 真实源码、含范围判定与 ShaderLab 正误示例)见 §3 诊断规则目录。相对 dev/2.0
UseBeforeDeclaration为 error(不软化);DuplicateEntryAssignment正确检测同一入口的重复赋值。运行时形态
2.1 包结构与依赖关系
抽出共享基础包
shader-parser,让 shader-compiler 和 shader-analyzer 在物理层共用一套 Lexer / Parser / AST,杜绝 drift:flowchart TB SP["📦 shader-parser(新基础包)<br/>Lexer · Preprocessor · Parser (LALR)<br/>LALR1 · AST 类型 · Grammar · SourceParser"] SC["⚙️ shader-compiler(重构)<br/>依赖 shader-parser<br/>+ CodeGenVisitor<br/>+ ShaderCompiler runtime entry<br/>⚡ 尽可能快"] SA["🔍 shader-analyzer(新增)<br/>依赖 shader-parser<br/>+ 通用收集 error 线索<br/>+ §3 诊断目录 + Diagnostic API<br/>📋 尽可能全面"] SP --> SC SP --> SA classDef base fill:#fff9c4,stroke:#f57f17,font-weight:bold classDef compiler fill:#d4edda,stroke:#28a745 classDef analyzer fill:#e3f2fd,stroke:#1976d2 class SP base class SC compiler class SA analyzer三包目录布局:
两套 Parser 架构说明(by design 的两层):
ShaderSourceParserShader { SubShader { Pass { ... } } }/RenderState/Tags块)IShaderSource(结构化 SubShader / Pass 数组)ShaderTargetParser+ LALR两层独立——SourceParser 先把 ShaderLab 拆出每个 Pass 的源码,再用 LALR Parser 对每个 Pass 内部的 GLSL 单独解析。两套各自的诊断也独立剥离到 analyzer:SourceParser 的诊断(缺 Shader / SubShader / Pass、RenderState 字段错)→ §3 的 A 类(结构 + RenderState);LALR Parser + AST 的诊断 → §3 的 B–G 类(类型 / 常量 / 资源 / 符号 / 控制流 / IO)。
两个目标对应 §4.1 的实施步骤:
shader-parser作为 single source of truth;shader-compiler 出清诊断;shader-analyzer 独立承载诊断,搬迁现有 verbose 模式可检测的所有错误(不引入新检查)职责契约:
shader-parser(新基础包)semanticAnalyze仅保留 codegen + 诊断都需要的语义动作(类型推断、符号表注册、scope 管理)——剥离所有reportError诊断逻辑return null,不输出错误(错误处理在上层)shader-compiler(重构)lexer/parser/lalr/sourceParser/Preprocessor.ts(迁出到 shader-parser);删除 8 处reportError+ 大部分#if _VERBOSE块undefined;强制[shader-compiler] Compile failed at line N (col M): <reason>到console.error;行列追踪移出#if _VERBOSEshader-analyzer(新增)ShaderIOAnalyzer全局 IO)→ §3 诊断;公共 Diagnostic API(analyze())三条核心决策:
shader-parser基础包——parser/AST 是 single source of truth,physical 上不可能 drift(不是靠"约定"或"CI 校验",是靠依赖结构);compiler 和 analyzer 平级,方向清晰packages/)——共生关系太紧,独立 repo 工程开销不划算2.2 AST 复用与诊断分离机制(本 RFC 命脉)
问题:当前诊断逻辑耦合在 AST 节点的
semanticAnalyze()方法里——类型推断 / 符号表注册(codegen 必需)和reportError(诊断)混在同一个方法。要拆开,又不能让 AST drift。做法(error-as-clue):诊断不再是 AST 节点里的
reportError调用,而是线索的一种取值。semanticAnalyze算类型 / 符号 / 角色线索时,非法 = 该线索取 error 值(自带 code + reason + 位置)。analyzer 一段通用收集逻辑「扫 error 线索 → 包成 Diagnostic」——零instanceof、零 per-node 特例、不重走 AST。需跨函数全局信息的(IO 角色 / entry 签名)由ShaderIOAnalyzer一趟算出 error 线索,analyzer 一并收。flowchart LR subgraph PARSER["📦 shader-parser(基础包,single source of truth)"] P["parse → AST"] --> SA["semanticAnalyze<br/>算 类型/符号/角色 线索<br/>(非法=线索取 error 值;<br/>codegen 与诊断共享)"] end SA --> CG["shader-compiler<br/>CodeGenVisitor<br/>⚡ 尽可能快,0 诊断检查"] SA --> DV["shader-analyzer<br/>通用收集 error 线索<br/>🔍 扫线索→Diagnostic,零特例"] CG --> OUT["最终 GLSL(IShaderProgramSource)"] DV --> DIAG["Diagnostic[]"] classDef shared fill:#fff9c4,stroke:#f57f17 classDef compiler fill:#d4edda,stroke:#28a745 classDef analyzer fill:#e3f2fd,stroke:#1976d2 class P,SA shared class CG,OUT compiler class DV,DIAG analyzershader-parser基础包semanticAnalyze(算 类型/符号/角色 线索,非法=error 值)shader-parser基础包reportErrorShaderIOAnalyzer全局 IO)shader-analyzer(通用收集器)shader-compiler的 CodeGenVisitor这一机制同时回答三件事:
shader-parser;codegen 读有效线索生成、analyzer 扫 error 线索报诊断,同一份、无重复遍历改 Lexer / Parser / AST → 两边自动同步:shader-parser 是唯一实现,analyzer 和 compiler 都通过依赖关系拿到同一份代码——物理上不可能 drift(不是靠"约定",是靠依赖结构)。任何 lexer / parser / AST 改动都在
shader-parser内做,compiler / analyzer 重新构建即自动继承。诊断增强 = 给 parser 补线索,不是给 analyzer 加特例:要多报一类错,就让
semanticAnalyze算线索时多覆盖一种非法取值(落到 error 线索),analyzer 的通用扫描自动报出 —— analyzer 永远是「扫线索」一段逻辑,不随规则增多长出 per-node 分支。两个诊断入口、同一套收集:error 线索的通用收集逻辑两个入口共用、不重复实现 —— ① 独立
analyze(src)(编辑器 squiggles / CI,自己 parse);② 引擎注入(§2.3,复用 compiler 同一次 parse)。实施约束(代码勘察)
抽包不是"零成本复用"——勘察发现四个必须处理的障碍。RFC 接受这些成本,对应方案如下:
ShaderCompiler.createPosition/createRange(静态工厂)、ShaderCompilerUtils.createObjectPool/createGSError、ShaderCompiler._processingPassText(静态字段)、AST.ts对CodeGenVisitor的类型引用_processingPassText都归 shader-parser);AST.ts对 CodeGenVisitor 的引用改为接口(ICodeGenVisitor,定义在 shader-parser,compiler 实现),切断对具体 codegen 类的依赖semanticAnalyze随 shift-reduce 归约即时算线索,无"先建完整 AST 再遍历"这一步ASTNode.get()创建节点即调semanticAnalyzeShaderIOAnalyzer一趟补),analyzer 直接收集 —— 不需要 visitor、不重走 AST,无遍历重复@galacean/engine运行时符号Logger(3 处,诊断日志)/ 对象池接口(7 处)/ShaderLanguageenum(1 处)/Color(3 处,RenderState 序列化)IObjectPool接口;ShaderLanguage enum → 复制进 shader-parser;Color → 改用[r,g,b,a]数组(RenderState 序列化层转换)processingPassText等"每次 parse 重置"的状态processingPassText等共享静态状态用try/finally复位,避免一次失败污染下一次。注入路径(§2.3)在同一次 parse 内完成诊断 + codegen,天然满足2.3 API 设计
引擎集成:注入即诊断(编辑器 / CI / dev)
诊断与编译通过
WebGLEngine.create注入达成 —— 注入shaderAnalyzer即开诊断,省略则纯编译、零诊断成本:Logger.enable()控制台可见);要结构化诊断(编辑器画波浪线 / CI),用独立入口analyzer.analyze(src),直接拿返回的Diagnostic[]。_parseShaderPass解析后、codegen 前让 analyzer 诊断同一份 AST,省掉二次解析。IShaderAnalyzer,design 包),不依赖其实现;analyzer 不依赖 compiler。关键决策:
code用语义枚举DiagnosticType(如InvalidSwizzle),非数字码——自解释、对标 glslang 文案风格;§3 目录即其全集Diagnostic.range全文件绝对坐标(line / column 从 source 开头);多 SubShader / Pass 不切分坐标系@galacean/engine绑定 major(2.x ↔ 2.x);内置引擎符号表随 engine 实际声明,engine major 升级时 analyzer 必须同步发版(CI sync check 见 §4.3)3. 诊断规则目录
「来源」列简写说明:
[N:变体名]valid模块的真实错误 enum 变体(如[N:InvalidSwitchType]),可回 GitHub 源码逐条核对[T:函数名]resolver_validation.cc的真实校验函数(如[T:ValidateSwitch])[GLSL]「范围」列说明(✅ 与 ⏸️ 都是本 PR):
A · ShaderLab 结构(DSL 层)
MissingEntryVertexShader=vert; FragmentShader=frag;VertexShader=vert;DuplicateEntryAssignment[N:EntryPointError.Conflict]VertexShader=vert;VertexShader=vert; VertexShader=v2;EntryNotFoundVaryings vert(){} VertexShader=vert;VertexShader=notDefined;InvalidRenderState*BlendOperation=Add;BlendOperation=Foo;B · 类型与表达式
InvalidSwizzle[N:InvalidVectorType/InvalidSwizzleComponent]vec3 v; v.xyvec2 v; v.xyzUndeclaredStructMember[GLSL]struct S{float a;}; s.as.bConstructorArgCount[T:ValidateVariableConstructorOrCast]vec3(1.,2.,3.)/vec3(1.)vec3(1.,2.)InvalidSplat[N:InvalidSplatType]vec3(1.0)NonIndexableType[N:InvalidBaseType]vec3 v; v[0]float f; f[0]IndexOutOfBounds[N:IndexOutOfBounds/NegativeIndex]vec3 v; v[2]v[3]/v[-1]ConstDivideByZero[N:DivideByZero]1.0/2.01.0/0.0ShiftOutOfRange[N:ShiftAmountTooLarge]i << 3i << 999InvalidBinaryOperands[N:InvalidBinaryOperandTypes]vec3 a,b; a+bvec3 v; mat4 m; v+mInvalidUnaryOperand[N:InvalidUnaryOperandType]-f/!bbool b; -bConstructorArgType[GLSL]vec4(v3,1.)vec4(v3, b)NonIntegerIndex[N:InvalidIndexType]a[i]a[1.5]InvalidConversion[N:InvalidCastArgument]float(i)float(sampler)C · 常量
NonConstArraySize[GLSL]const int N=4; float a[N];int n=f(); float a[n];NonConstInitializer[N:LocalVariableError.NonConstOrOverrideInitializer]const float PI=3.14;const float x=v_uv.x;D · 资源 / 全局
InvalidUniformType[N:GlobalVariableError.InvalidType]uniform sampler2D u_tex;uniform void u;UniformInitializer[N:GlobalVariableError.InitializerNotAllowed][GLSL]uniform float u;uniform float u=1.0;ExpectedSampler[N:ExpressionError.ExpectedSamplerType]texture(u_tex, uv)texture(u_time, uv)E · 符号与作用域
UseBeforeDeclaration[N:ExpressionError.NotInScope]float x=1.; float y=x;float y=x; float x=1.;Redefinition[GLSL]float x; { float x; }float x; float x;UndefinedFunction[N:FunctionError.InvalidCall]float f(){} f();notAFunc();NoMatchingOverload[N:CallError.ArgumentCount][T:ValidateFunctionCall]f(float x){} f(1.)f()/f(1.,2.)NoMatchingOverload[N:CallError.ArgumentType]f(1.0)f(a+b)(算术实参无类型)F · 函数与控制流
ReturnInVoidFunction/MissingReturn[T:ValidateFunction][GLSL]void f(){return;}/float g(){return 1.;}void f(){return 1.;}/float g(){}NonConstructibleReturnType[N:NonConstructibleReturnType]float f()sampler2D f()NonBoolCondition[N:InvalidIfType]if(x>0.){}if(x){}(float)NonIntegerSwitch[N:InvalidSwitchType][T:ValidateSwitch]switch(i){}switch(f){}DuplicateSwitchCase[N:ConflictingSwitchCase][T:ValidateSwitch]case 1: case 2:case 1: case 1:InvalidSwitchDefault[N:MissingDefaultCase/MultipleDefaultCases][T:ValidateSwitch]... default:MisplacedControlFlow[N:BreakOutsideOfLoopOrSwitch/ContinueOutsideOfLoop][T:ValidateBreak/Continue]for(){break;}void f(){break;}RecursiveFunction[GLSL/WGSL 通则]float f(){return f();}UnreachableCode[N:UnvisitedExpression 类]return x; y=1.;ReturnTypeMismatch[N:InvalidReturnType][T:ValidateReturn]float f(){return 1.;}float f(){return v3;}(算术返回值无类型时漏报)AssignTypeMismatch[N:InvalidStoreTypes][T:ValidateAssignment]float x; x=1.0;vec3 v; v=1.0;(算术右值无类型时漏报)G · 管线 IO
VertexEntryReturnType/FragmentEntryReturnType[N:EntryPointError]Varyings vert(Attributes a){}float vert(Attributes a){}InvalidVaryingStruct[N:VaryingError.NotIOShareableType]struct V{vec4 pos; vec2 uv;}struct V{sampler2D s;}NestedIOStruct[T:ValidateEntryPoint]StructRoleConflict[N:VaryingError]GlFragColorWithMrt*gl_FragColor或 MRT 二选一MissingVertexPosition[N:EntryPointError.MissingVertexOutputPosition][T:ValidateFunction]NonFlatIntegerVarying[N:VaryingError.InvalidInterpolationForInteger][T:ValidateEntryPoint]flat int id;范围外 / 后续后端(编目)
[N:FunctionError 高级变体]@group/@binding[N:VaryingError.BindingCollision]a[2][2])valid/无此规则、back/glsl::Error有类型系统(本 PR,⏸️ 类型类诊断的实现基础):重载解析器已存在 ——
shader-parser的builtin/functions.ts::resolveOverload已实现完整泛型家族重载(GenType / size·scalarType 维度锁定 / 返回投影 / TypeAny 通配)。S7 = 重启用AST.ts被注释的二元运算 type-deduce(MultiplicativeExpression/AdditiveExpression,原exp1.type === exp2.type太糙:vec3 * float合法但类型不同、泛型返回 TypeAny 会算错)→ 换成正经 GLSL 算子类型规则(scalar/vec/mat,GLSL ES spec §5 一张固定小表)+ 接住resolveOverload的 TypeAny 传播 +functions.ts按 spec §8 补缺 builtin。纯本地实现、无外部研究(见 §4.1 S7)。错误传播策略(跨 check)
跨 check 的失败传播采用 continue-with-unknown 而非 fail-fast:
unknown状态(例:类型推断失败 →Expression.type = "unknown")unknown时跳过该检查,不重复报错(避免一个根因被多次报告造成噪音)foo(undefined_var)——作用域检查已报"未定义变量"后,类型推断将undefined_var.type置为unknown,函数签名检查遇 unknown 参数则跳过4. 实施计划
4.1 实施步骤
shader-parser包(诊断暂随迁)shader-analyzer+ 搬迁现有诊断(先加)S1 — 消除反向依赖(纯内部重构,不新建包、不改行为,为抽包扫清障碍)
ShaderCompiler.createPosition / createRange(静态工厂)→ 独立模块(如common/PositionFactory),parser 不再反向 importShaderCompilerShaderCompiler._processingPassText(静态字段,错误上下文)→ parse 调用链传参 / 独立 context 对象,解除SemanticAnalyzer/ShaderTargetParser对它的依赖ShaderCompilerUtils.createObjectPool / createGSError→ 确认归属 parser 基础设施(S3 随 parser 迁出,此处先确保 codegen 侧不强依赖其挂在 ShaderCompiler 上)AST.ts对CodeGenVisitor的具体类引用 → 改ICodeGenVisitor接口npm run test全绿;编译产物 diff 为空S2 — 解耦运行时依赖(parser 相关模块不再 import
@galacean/engine运行时符号)Logger(3 处:SemanticAnalyzer/LALR1/SymbolTable,均诊断日志)→ 删除或回调注入@galacean/engine-core的ClearableObjectPool/IPoolElement)→ shader-parser 内置最小IObjectPool接口ShaderLanguageenum → 复制定义进包Color(3 处,RenderState 序列化)→ 改[r,g,b,a]数组,序列化层做转换S3 — 物理抽出
shader-parser包(只搬不改,诊断逻辑暂随迁,行为完全不变)packages/shader-parser/(package.json / tsconfig / rollup config)lexer/parser/(含 AST + Grammar)lalr/sourceParser/Preprocessor.tscommon/ParserUtils.tsGSError.ts+ S1 拆出的基础设施IncludeMap/ChunkOutputCache类型 + Preprocessor(实测已无状态纯入参,工作量极小)CFG.ts+AST.ts的 verbose-only 中间节点(StorageQualifier/InterpolationQualifier等)默认无条件生成,AST 始终完整 → analyzer 拿全集;compiler 对性能敏感可在 CodeGenVisitor 内 skipICodeGenVisitorbundler/rollup预编译插件相应调整依赖S4 — 新建
shader-analyzer+ 搬迁现有诊断(先加,行为对等原 verbose,不引入新检查)packages/shader-analyzer/,依赖shader-parserDiagnostic/AnalyzerOptions/AnalysisResult/ShaderAnalyzer(§2.3)ShaderIOAnalyzer全局 IO,零 per-node 特例、零 re-walkanalyze(source):只跑 parse(产 error 线索)+ 通用收集,跳过 CodeGenreportError(数组嵌套 / 整型常量 / return 匹配 / 未定义函数等)改为线索取 error 值;+ShaderSourceParser.errors(缺 Shader/SubShader/Pass/Tags、RenderState 字段错、声明顺序)统一收集为结构化Diagnosticok.shader/err.shader/expected.diag.json比对器,见 §4.2)+ 验收:迁移后对 dev/2.0 已有诊断行为对等(不退化)S5 — 剥离 shader-parser 诊断 + 取消 verbose(后删,达成 single source of truth)
semanticAnalyze的 8 处reportError(5 处易 / 2 处保留 returnStatement 收集 /FunctionCallGeneric保留 early-return 错误恢复,仅移报错)ShaderSourceParser.errors收集逻辑null+ 暴露lastError: { line, col, token, reason };analyzer 据此产出具体诊断,compiler 据此产出 runtimeconsole.error#if _VERBOSE块(实测 ~97 处 TS 源码)+ 取消 4 份 verbose 构建变体(根rollup.config.js+ package.jsonexports)console.error(行列追踪移出#if _VERBOSE为非条件代码)S6 — 增强诊断范围(§3 目录 ✅ 直接落地项,analyzer 内新增,不动 compiler / parser)
#include/ 禁手动重定义引擎 uniform / include 顺序(依赖内置引擎符号表 + §4.3 sync check)unknown标记 + 下游 check 跳过(continue-with-unknown,见 §3 错误传播策略)S7 — 建类型系统(解锁 ⏸️ 类型类诊断)
builtin/functions.ts::resolveOverload(泛型家族重载已实现)—— 不重写AST.ts被注释的MultiplicativeExpression/AdditiveExpressiontype-deduce → 换正经 GLSL 算子类型规则(scalar/vec/mat)+ 接resolveOverload的 TypeAny 传播functions.ts按 GLSL ES spec §8 补缺 builtin(纯数据)4.2 测试:每个诊断类型一组 ABTest
核心要求:§3 的每个诊断类型(✅ 与 ⏸️)的每个 case 都有一组 ABTest —— 正确版(应通过、零该类型诊断)+ 错误版(应失败、精确命中该
DiagnosticType)。既是验收标准,也支撑实现期循环自检(改一条规则 → 跑对应 ABTest → 立即知对错)。analyze()不含该类型诊断;错误版:精确命中(code= 对应DiagnosticType+severity+range),忽略message文本tests/fixtures/<DiagnosticType>/{ok.shader, err.shader, expected.diag.json}examples/下 shader)tests/corpus/4.3 合入门槛
IShaderProgramSource不变packages/core声明,drift 时 fail4.4 失败模式分析(FMEA)
逐一枚举失败场景与应对:
core加新 builtin 未同步 analyzerconsole.error(含行列)unknown标记resolveOverload的 TypeAny 交互出错functions.ts::resolveOverload(已实现),只补 GLSL 算子类型规则;ABTest 守 ⏸️ 类型4.5 关键文件
packages/shader-compiler/src/parser/AST.tsreportError(L243/375/398/466/713/717/780/1482)的剥离起点;L2 importCodeGenVisitor是要切断的反向依赖(改ICodeGenVisitor接口)packages/shader-compiler/src/parser/SemanticAnalyzer.tserrors[]/reportError);新 Diagnostic 模板;依赖ShaderCompiler._processingPassText(反向依赖,要迁出)packages/shader-compiler/src/ShaderCompiler.tscreatePosition/createRange/_processingPassText/_includeMap都在此——这些 parser 基础设施要迁到 shader-parser;_parseShaderPass的 codegen 调度留下packages/shader-compiler/src/ShaderCompilerUtils.tscreateObjectPool/createGSError——parser 侧也依赖,随 shader-parser 迁出packages/shader-compiler/src/common/BaseLexer.tsBaseLexer/BaseToken反向依赖ShaderCompiler.createPosition+ShaderCompilerUtils;迁出时一并处理packages/shader-compiler/src/sourceParser/ShaderSourceParser.tserrors[]诊断剥离起点(§3 A 类:结构 + RenderState)packages/shader-compiler/src/Preprocessor.tsIncludeMap/ChunkOutputCache类型packages/shader-compiler/src/lalr/LALR1.ts_isKnownShiftPreferred);Logger运行时依赖(要解耦)packages/shader-compiler/rollup.config.js+ 根rollup.config.js_VERBOSE配置 + verbose 变体生成;取消 4 份 verbose 变体在此改5. 未来工作(Out of Scope)
本 RFC 范围之外的演进方向:
in/out仅 ES 300 /layout限制 / mediump 行为差异);可作为新诊断族@galacean/engine-shader-language-server包装 ShaderAnalyzer,含 incremental + go-to-definition + semantic tokensanalyzeIncrementalAPI(本 PR 不导出;LSP server 需要时引入),用于 IDE keystroke 响应@galacean/shader-lintCLI@galacean/shader-lint-recommended/mobile-perf等),ESLint plugin 心智模型examples/shader-playground/(Vite + Monaco)实时校验,shader-analyzer dogfooding;浏览器内纯静态分析、无需defines上下文analyzer.registerRule()+RuleContext(symbolTable / builtins / report),ESLint plugin 心智模型6. 参考资料(Sources)
诊断规则与架构的真实出处,§3 每条
[N:]/[T:]标注均可回此核对:valid/function.rs(FunctionError / CallError / LocalVariableError):https://github.com/gfx-rs/wgpu/blob/trunk/naga/src/valid/function.rsvalid/expression.rs(ExpressionError):https://github.com/gfx-rs/wgpu/blob/trunk/naga/src/valid/expression.rsvalid/interface.rs(VaryingError / EntryPointError / GlobalVariableError):https://github.com/gfx-rs/wgpu/blob/trunk/naga/src/valid/interface.rs