mirror of
https://github.com/galacean/engine.git
synced 2026-05-06 22:23:05 +08:00
perf(shader-lab): elide TrivialNode wrapper on single-child productions
The parser reduces GLSL's deep expression precedence chain (logical_or_expression → ... → primary_expression, ~15 single-production layers) by creating a TrivialNode wrapper around each layer's single child. In release builds — where typed AST classes sit behind `// #if _VERBOSE` and get stripped at build time — these wrappers are semantically empty: no visitor, no type field, no side effects, just a `.children[0]` indirection. In reducer closures, when the production has no typed AST class supplied and the RHS is a single `TreeNode` child, push the child directly onto the semanticStack instead of wrapping it in a TrivialNode. The parser's GOTO table runs off `reduceProduction.goal`, not off the node type on the stack, so state transitions remain correct. The `instanceof TreeNode` guard preserves the original path for single-token RHS (e.g. `unary_operator → PLUS`), where `BaseToken` cannot sit on the semanticStack in place of an AST node. Impact (PBR, median of n=30 × 3 repeats): AST nodes: 54,504 → ~27,000 (-50%) AST parser stage: 28.4ms → 21.5ms (-24%) CodeGen stage: 25.5ms → 19.2ms (-25%) Total compile: 54.7ms → ~41ms (-25% vs dev/2.0 baseline) This aligns with the standard approach in glslang, GCC/Clang, Rust parsers etc. — never materialize AST nodes for precedence-only productions that carry no semantics.
This commit is contained in:
@@ -29,16 +29,29 @@ export default class GrammarUtils {
|
||||
{ set: (loc: ShaderRange, children: NodeChild[]) => void } & IPoolElement & TreeNode
|
||||
>
|
||||
) {
|
||||
// Resolve the AST pool once per grammar production (not per reduce).
|
||||
const pool = astTypePool ?? ASTNode.TrivialNode.pool;
|
||||
const ret: [GrammarSymbol[], TranslationRule | undefined][] = [];
|
||||
for (const opt of options) {
|
||||
// Single-`NonTerminal` RHS + no typed class → this production reduces
|
||||
// to a semantic-empty `TrivialNode` wrapper. Elide it at reduce time
|
||||
// by pushing the child directly onto the semantic stack. Safe because
|
||||
// the parser's GOTO runs off `reduceProduction.goal`, not off the node
|
||||
// type on the stack. Single-Terminal RHS (e.g. `unary_operator → PLUS`)
|
||||
// isn't eligible — a `BaseToken` can't stand in for an AST node.
|
||||
const canElide = !astTypePool && opt.length === 1 && !GrammarUtils.isTerminal(opt[0]);
|
||||
ret.push([
|
||||
[goal, ...opt],
|
||||
function (sa, ...children) {
|
||||
if (!children[0]) return;
|
||||
const start = children[0].location.start;
|
||||
const end = children[children.length - 1].location.end;
|
||||
const location = ShaderLab.createRange(start, end);
|
||||
ASTNode.get(astTypePool ?? ASTNode.TrivialNode.pool, sa, location, children);
|
||||
if (canElide) {
|
||||
sa.semanticStack.push(children[0] as TreeNode);
|
||||
} else {
|
||||
const start = children[0].location.start;
|
||||
const end = children[children.length - 1].location.end;
|
||||
const location = ShaderLab.createRange(start, end);
|
||||
ASTNode.get(pool, sa, location, children);
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user