diff --git a/packages/shader-lab/src/Preprocessor.ts b/packages/shader-lab/src/Preprocessor.ts index 4a9aaa31a..0a288bdb4 100644 --- a/packages/shader-lab/src/Preprocessor.ts +++ b/packages/shader-lab/src/Preprocessor.ts @@ -4,31 +4,18 @@ import { ShaderLib } from "@galacean/engine"; import type { ASTNode } from "./parser/AST"; /** - * Record for a single `#define` directive. - * - * Two shapes coexist and can be combined on the same entry: - * - * - **Expression macros** — `valueAst` holds an `AssignmentExpression` subtree - * produced by the `macro_define` CFG rule. These participate in type - * inference, varying flattening, and visitor-based codegen. - * - **Legacy opaque macros** — the directive is emitted verbatim as a - * `MACRO_DEFINE_EXPRESSION` token and expanded by the GLSL driver. `valueAst` - * is undefined. - * - * `referenceName` captures the top-level external identifier the value refers - * to, if any — the callee of a function-call value (`#define F foo(a,b)` - * → `foo`) or the single-identifier value (`#define F foo` → `foo`). Empty - * for numeric literals, qualifier fragments, or structurally complex values. - * Populated by the regex pass; drives symbol-table lookup for macro call - * sites, independent of whether the value also has an AST form. + * Record for a single `#define` directive. `valueAst` is set for expression + * macros (joined into the AST pipeline); opaque macros leave it undefined and + * are emitted verbatim to the GLSL driver. */ export interface MacroDefineInfo { isFunction: boolean; name: string; params: string[]; - /** AST for expression-style macros. Absent for opaque macros. */ valueAst?: ASTNode.AssignmentExpression; - /** Top-level external identifier the value references, or empty string. */ + /** Leading identifier of the value (`#define F foo` → `foo`, `#define F foo(a)` + * → `foo`), or empty for literals / operator expressions. Drives symbol-table + * lookup at macro call sites. */ referenceName: string; } @@ -40,34 +27,34 @@ export class Preprocessor { private static readonly _includeReg = /^[ \t]*#include +"([\w\d./]+)"/gm; private static readonly _macroRegex = /^\s*#define\s+(\w+)[ ]*(\(([^)]*)\))?[ ]+(\(?\w+\)?.*?)(?:\/\/.*|\/\*.*?\*\/)?\s*$/gm; - private static readonly _symbolReg = /^[a-zA-Z_][a-zA-Z0-9_]*$/; - private static readonly _funcCallReg = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(.*\)$/; - private static readonly _macroDefineIncludeMap = new Map(); + // Matches a bare identifier or a function-call whole-value form only. + // Mixed-operator values (`a + b`) are intentionally rejected. + private static readonly _referenceReg = /^([a-zA-Z_]\w*)(?:\s*\(.*\))?$/; + private static readonly _chunkCache = new Map(); /** * @internal */ static _repeatIncludeSet = new Set(); - static parse( - source: string, - basePathForIncludeKey: string, - outMacroDefineList: MacroDefineList, - parseMacro = true - ): string { - if (parseMacro) { - this._parseMacroDefines(source, outMacroDefineList); - } + static parse(source: string, basePathForIncludeKey: string, outMacroDefineList: MacroDefineList): string { + this._parseMacroDefines(source, outMacroDefineList); return source.replace(this._includeReg, (_, includeName) => this._replace(includeName, basePathForIncludeKey, outMacroDefineList) ); } - /** - * For each registered definition of `macroName`, push its `referenceName` - * (when non-empty and not a macro parameter) so call sites can drive - * symbol-table lookup. - */ + private static _parseMacroDefines(source: string, outMacroList: MacroDefineList): void { + this._macroRegex.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = this._macroRegex.exec(source)) !== null) { + const [, name, paramsGroup, paramsStr, valueRaw] = match; + this._registerDefine(outMacroList, name, paramsGroup ? paramsStr : undefined, valueRaw); + } + } + + /** Collect unique `referenceName`s of `macroName`'s definitions, skipping + * names that shadow a macro parameter. */ static getReferenceSymbolNames(macroDefineList: MacroDefineList, macroName: string, out: string[]): void { out.length = 0; const infos = macroDefineList[macroName]; @@ -82,15 +69,9 @@ export class Preprocessor { } } - /** - * Extract the top-level identifier a value refers to, or empty string if - * the value is a numeric literal, qualifier fragment, or structurally - * complex form we don't try to introspect. - */ private static _extractReferenceName(value: string): string { - if (this._symbolReg.test(value)) return value; - const callMatch = this._funcCallReg.exec(value); - return callMatch ? callMatch[1] : ""; + const match = this._referenceReg.exec(value); + return match ? match[1] : ""; } private static _isExist(list: MacroDefineInfo[], item: MacroDefineInfo): boolean { @@ -103,45 +84,36 @@ export class Preprocessor { ); } - private static _parseMacroDefines(source: string, outMacroList: MacroDefineList): void { - let match: RegExpExecArray | null; - this._macroRegex.lastIndex = 0; + private static _registerDefine( + outMacroList: MacroDefineList, + name: string, + paramsStr: string | undefined, + valueRaw: string + ): void { + // `paramsStr` is `undefined` for object-like macros and the `()` contents + // (possibly empty) for function-like ones. + const isFunction = paramsStr !== undefined; + const params = paramsStr ? paramsStr.split(",").map((p) => p.trim()).filter(Boolean) : []; + const value = valueRaw.trim(); + const referenceName = value ? this._extractReferenceName(value) : ""; - while ((match = this._macroRegex.exec(source)) !== null) { - const [, name, paramsGroup, paramsStr, valueRaw] = match; - const isFunction = !!paramsGroup && !!valueRaw; - const params = - isFunction && paramsStr - ? paramsStr - .split(",") - .map((p) => p.trim()) - .filter(Boolean) - : []; - const value = valueRaw ? valueRaw.trim() : ""; - const referenceName = value ? this._extractReferenceName(value) : ""; + const info: MacroDefineInfo = { isFunction, name, params, referenceName }; - const info: MacroDefineInfo = { isFunction, name, params, referenceName }; - - const arr = outMacroList[name]; - if (arr) { - if (!this._isExist(arr, info)) arr.push(info); - } else { - outMacroList[name] = [info]; - } + const arr = outMacroList[name]; + if (arr) { + if (!this._isExist(arr, info)) arr.push(info); + } else { + outMacroList[name] = [info]; } } private static _mergeMacroDefineLists(from: MacroDefineList, to: MacroDefineList): void { - for (const macroName in from) { - if (to[macroName]) { - const target = to[macroName]; - const src = from[macroName]; - for (let i = 0; i < src.length; i++) { - const info = src[i]; - if (!this._isExist(target, info)) target.push(info); - } - } else { - to[macroName] = from[macroName]; + for (const name in from) { + const target = to[name] ?? (to[name] = []); + const src = from[name]; + for (let i = 0, n = src.length; i < n; i++) { + const info = src[i]; + if (!this._isExist(target, info)) target.push(info); } } } @@ -170,15 +142,14 @@ export class Preprocessor { } this._repeatIncludeSet.add(path); - if (this._macroDefineIncludeMap.has(path)) { - this._mergeMacroDefineLists(this._macroDefineIncludeMap.get(path)!, outMacroDefineList); - } else { - const chunkMacroDefineList: MacroDefineList = {}; - this._parseMacroDefines(chunk, chunkMacroDefineList); - this._macroDefineIncludeMap.set(path, chunkMacroDefineList); - this._mergeMacroDefineLists(chunkMacroDefineList, outMacroDefineList); + let entry = this._chunkCache.get(path); + if (!entry) { + const macros: MacroDefineList = {}; + const output = this.parse(chunk, basePathForIncludeKey, macros); + entry = { output, macros }; + this._chunkCache.set(path, entry); } - - return this.parse(chunk, basePathForIncludeKey, outMacroDefineList, false); + this._mergeMacroDefineLists(entry.macros, outMacroDefineList); + return entry.output; } }