Skip to content

RFC: Shader Static Analyzer #3017

Description

@zhuxudong

RFC: Shader Static Analyzer

1. 改造背景

1.1 当前架构

@galacean/engine-shader-compiler 同时承担两个职责:

  1. Runtime compilation — ShaderLab/GLSL → IShaderProgramSource,给引擎运行时用
  2. 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 不可见,无定位,同错多遍 结构化 DiagnosticDiagnosticType 分类 + 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-compilershader-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 不变

这一机制同时回答三件事

  1. 怎么复用 AST——parser 一趟产 AST + 线索(含 error 值)放 shader-parser;codegen 读有效线索生成、analyzer 扫 error 线索报诊断,同一份、无重复遍历
  2. 为什么 compiler 快——runtime 路径不读 error 线索(假设输入已被 analyzer 校验过)
  3. 为什么 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/createGSErrorShaderCompiler._processingPassText(静态字段)、AST.tsCodeGenVisitor 的类型引用 这些符号本质是 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,不二次解析
  • 诊断怎么拿:注入路径诊断走 LoggerLogger.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-parserbuiltin/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 — 消除反向依赖(纯内部重构,不新建包、不改行为,为抽包扫清障碍)

  • ShaderCompiler.createPosition / createRange(静态工厂)→ 独立模块(如 common/PositionFactory),parser 不再反向 import ShaderCompiler
  • ShaderCompiler._processingPassText(静态字段,错误上下文)→ parse 调用链传参 / 独立 context 对象,解除 SemanticAnalyzer / ShaderTargetParser 对它的依赖
  • ShaderCompilerUtils.createObjectPool / createGSError → 确认归属 parser 基础设施(S3 随 parser 迁出,此处先确保 codegen 侧不强依赖其挂在 ShaderCompiler 上)
  • AST.tsCodeGenVisitor 的具体类引用 → 改 ICodeGenVisitor 接口
  • 验收:npm run test 全绿;编译产物 diff 为空

S2 — 解耦运行时依赖(parser 相关模块不再 import @galacean/engine 运行时符号)

  • Logger(3 处:SemanticAnalyzer / LALR1 / SymbolTable,均诊断日志)→ 删除或回调注入
  • 对象池(7 处 @galacean/engine-coreClearableObjectPool / IPoolElement)→ shader-parser 内置最小 IObjectPool 接口
  • ShaderLanguage enum → 复制定义进包
  • Color(3 处,RenderState 序列化)→ 改 [r,g,b,a] 数组,序列化层做转换
  • 验收:测试全绿;RenderState 序列化结果字节对等

S3 — 物理抽出 shader-parser(只搬不改,诊断逻辑暂随迁,行为完全不变)

  • 新建 packages/shader-parser/(package.json / tsconfig / rollup config)
  • 迁移 lexer/ parser/(含 AST + Grammar)lalr/ sourceParser/ Preprocessor.ts common/ ParserUtils.ts GSError.ts + S1 拆出的基础设施
  • 导出 IncludeMap / ChunkOutputCache 类型 + Preprocessor(实测已无状态纯入参,工作量极小)
  • verbose-only AST 节点归属CFG.ts + AST.ts 的 verbose-only 中间节点(StorageQualifier / InterpolationQualifier 等)默认无条件生成,AST 始终完整 → analyzer 拿全集;compiler 对性能敏感可在 CodeGenVisitor 内 skip
  • shader-compiler 改依赖 shader-parser,删除被迁出源码,实现 ICodeGenVisitor
  • bundler/rollup 预编译插件相应调整依赖
  • 验收:测试全绿;编译产物字节对等(纯代码搬家)

S4 — 新建 shader-analyzer + 搬迁现有诊断(先加,行为对等原 verbose,不引入新检查)

  • 新建 packages/shader-analyzer/,依赖 shader-parser
  • 公共 API:Diagnostic / AnalyzerOptions / AnalysisResult / ShaderAnalyzer(§2.3)
  • 通用收集器(error-as-clue,见 §2.2):一段「扫 error 线索 → Diagnostic」+ ShaderIOAnalyzer 全局 IO,零 per-node 特例、零 re-walk
  • analyze(source):只跑 parse(产 error 线索)+ 通用收集,跳过 CodeGen
  • 搬迁现有诊断:AST 原 8 处 reportError(数组嵌套 / 整型常量 / return 匹配 / 未定义函数等)改为线索取 error 值;+ ShaderSourceParser.errors(缺 Shader/SubShader/Pass/Tags、RenderState 字段错、声明顺序)统一收集为结构化 Diagnostic
  • ABTest 框架骨架ok.shader / err.shader / expected.diag.json 比对器,见 §4.2)+ 验收:迁移后对 dev/2.0 已有诊断行为对等(不退化)
  • 此时诊断"两边都有"(parser 旧 reportError + analyzer error-线索收集),临时态,S5 删旧

S5 — 剥离 shader-parser 诊断 + 取消 verbose(后删,达成 single source of truth)

  • 删 AST semanticAnalyze 的 8 处 reportError(5 处易 / 2 处保留 returnStatement 收集 / FunctionCallGeneric 保留 early-return 错误恢复,仅移报错
  • ShaderSourceParser.errors 收集逻辑
  • shader-parser 错误协议:lexer/parser 失败返回 null + 暴露 lastError: { line, col, token, reason };analyzer 据此产出具体诊断,compiler 据此产出 runtime console.error
  • #if _VERBOSE 块(实测 ~97 处 TS 源码)+ 取消 4 份 verbose 构建变体(根 rollup.config.js + package.json exports
  • runtime 错误报告契约:compiler 编译失败输出最小位置信息到 console.error(行列追踪移出 #if _VERBOSE 为非条件代码)
  • 验收:测试全绿;verbose 消费者已迁 analyzer;shader-parser 零诊断逻辑

S6 — 增强诊断范围(§3 目录 ✅ 直接落地项,analyzer 内新增,不动 compiler / parser)

  • GLSL 语义:作用域(未定义引用 / 同作用域重定义 / shadowing)、函数(调用解析 / 实参数量 / 返回路径完整性)、swizzle / struct 成员 / 构造函数参数数、常量(数组大小须常量 / const 初始化 / 越界 / 除零 / 移位)、资源(uniform 类型合法 / 不可初始化 / 采样目标须 sampler)、控制流(if / switch / break / continue / 死代码 / 禁递归)
  • ShaderLab 结构(A 类)补全:入口绑定 / 不重复赋值 / 指向已定义函数 / RenderState 合法
  • 管线 IO(G 类):入口签名 / IO 可共享类型 / 不嵌套结构作 IO / struct 角色不冲突 / 输出一致性 / vertex 须输出位置 / 整型 varying 须 flat
  • Cross-stage 一致性:Varyings 赋值完整性 / 引用一致性 / attribute 原名保留
  • 内置符号联动:引擎 uniform 须有对应 #include / 禁手动重定义引擎 uniform / include 顺序(依赖内置引擎符号表 + §4.3 sync check)
  • 错误传播框架unknown 标记 + 下游 check 跳过(continue-with-unknown,见 §3 错误传播策略)
  • 每个 ✅ 类型补 ABTest(§4.2)

S7 — 建类型系统(解锁 ⏸️ 类型类诊断)

  • 复用现有 builtin/functions.ts::resolveOverload(泛型家族重载已实现)—— 不重写
  • 重启用 AST.ts 被注释的 MultiplicativeExpression / AdditiveExpression type-deduce → 换正经 GLSL 算子类型规则(scalar/vec/mat)+ 接 resolveOverload 的 TypeAny 传播
  • functions.ts 按 GLSL ES spec §8 补缺 builtin(纯数据)
  • 落地 ⏸️ 类型类诊断:二元 / 一元运算操作数类型、构造函数参数类型、下标须整型、类型转换合法、调用实参类型、返回类型匹配、赋值左值可写 + 类型兼容
  • 每个 ⏸️ 类型补 ABTest(§4.2)

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:] 标注均可回此核对:

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFCdesignengine designenhancementNew feature or requestshaderShader related functions

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions