Skip to content

refactor(shader-lab): unify #define dispatch into Lexer (drop Preprocessor regex path) #2980

@zhuxudong

Description

@zhuxudong

背景

#2974("AST-first #define")合入后,shader-lab 中 #define 的处理被分到了两处:

  1. Preprocessor._parseMacroDefines(正则):扫描源码,提取宏名字 + params + referenceName,填到 macroDefineList
  2. Lexer._defineHasValue(peek):在每个 #define 指令处决定走 AST 路径还是 legacy opaque 路径

两处看的是同一段源码,但用的是不同的分析机制:

处 1(正则) 处 2(Lexer peek)
分析机制 RegExp 字符状态机
是否懂 /* */// 注释 不懂
是否懂 \ 续行 不懂
是否懂"# 必须在行首"(GLSL ES §3.4) 部分懂(^\s*#define 不强校验

不一致的真实风险

两套独立分析机制对同一段源码的理解不完全一致,已经带来了潜在 bug:

例子 1:注释里的 #define 假阳性注册

/*
 * 文档:
 * #define MAX_LIGHTS 4
 */
  • 正则匹配并把 MAX_LIGHTS 写进 macroDefineList
  • Lexer 跳过整个注释,从未看到 #define
  • 后果:用户代码 int x = MAX_LIGHTS; 被 tokenize 成 MACRO_CALL,但展开时找不到值定义

例子 2:续行 \#define

#define LONG \
    a * b + c
  • 正则 .*? 不跨行,value 抓不到
  • Lexer 正确识别续行
  • 后果:macroDefineList 里的 referenceName 提取失败

例子 3:注释 peek 盲点

#define HP /* comment */ highp

例子 4:type-alias 误进 AST(详见 https://github.com/galacean/engine/pull/2974#issuecomment-4325529482)

  • _expressionLeaderKeywords 白名单包含全部构造器类型 keyword
  • #define FxaaTex sampler2D 被错误送进 AST → parse 失败
  • FXAA portability 宏全部命中

这些 bug 都可以单独打补丁解决,但根本原因是两套分析机制必然 drift——每加一个新分流策略、每升级一次 GLSL 版本,两边都要同步审视。

建议方案

#define 处理完全收到 Lexer,删掉 Preprocessor 的 #define 正则。 Preprocessor 只保留 #include 处理。

实现思路

Lexer 内部做"两遍扫描":

第 1 遍(轻量 forward-scan):

  • 快速跑一遍源码,只为找所有 #define <name> 的名字
  • 不发 token,不分类,不解析 value
  • 复用 Lexer 既有的注释/续行/字符串跳过机制 → 自动避开例子 1/2 的 bug
  • 产出:macroDefineList 的名字索引

第 2 遍(正式 lex):

  • 知道所有宏名字后开始正式 tokenize
  • 在每个 #define 指令处做 AST/legacy 分类
  • 走到使用点 FOO,按 macroDefineList 决定 tokenize 成 MACRO_CALL 还是 ID

收益

  1. 单一 source-of-truth:Lexer 全权处理 macro,没有任何 drift 可能
  2. 注释、续行、字符串字面量等所有 source-level 细节天然一致——因为只有一套扫描机制
  3. 现有的几个 bug 自动消失
    • 例子 1:注释里假阳性 → Lexer 本来就跳注释
    • 例子 2:续行 → Lexer 本来就懂
    • 例子 3:peek 盲点 → 不需要 peek 了,第 1 遍就分类完了
    • 例子 4:type-alias 误进 AST → 第 1 遍可以做精确分类(peek 整条 replacement list)
  4. Preprocessor scope 收敛:从"manage macros + manage includes"降到只 manage includes,单一职责

代价

  1. 2x 扫描成本:PBR 这种 ~5000 行 shader 实测多 ~1ms(量级可接受,且只在源码编译阶段,runtime 不影响)
  2. Lexer 内部状态机更复杂:要支持 "scan-only mode"(第 1 遍只找名字不发 token)
  3. 改动量较大:~200-300 行 + 全面回归测试

迁移路径

  1. 当前:先按 feat(shader-lab): make #define values first-class AST nodes #2974 那边 review 给的最小补丁(type-alias peek 加 6 行)打补丁,不阻塞 PR 合入
  2. 下个迭代:单独立项做本 issue 的 refactor
  3. 完工后Preprocessor.ts 只剩 #include 处理,回到 PR 之前的简洁度

不能解决的(已知 scope)

C 只解决"分流不一致"这一类问题。以下不在 scope:

  • Nit: 表达式宏 value 在 parse 时 semantic-analyze 引发假警告(Parser semantic action 时机问题)
  • Nit: hasAstValue = .some(...)#ifdef 分支 AST/legacy 混合时语义错(MacroCallSymbol 查询逻辑问题)
  • 不支持 __LINE__ / __FILE__ / __VERSION__ 预定义宏(独立 feature)

这些需要独立 fix。

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestshaderShader related functions

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions