Skip to content

fix(@typegpu/gl): Evaluate the right-hand side of an aliasing const once - #3030

Open
dchaudhari7177 wants to merge 1 commit into
software-mansion:mainfrom
dchaudhari7177:fix/3029-glsl-alias-single-evaluation
Open

dchaudhari7177 wants to merge 1 commit into
software-mansion:mainfrom
dchaudhari7177:fix/3029-glsl-alias-single-evaluation

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Fixes #3029.

Cause

For const value = values[nextIndex()]!; the GLSL generator evaluated the right-hand side three times:

  1. WgslGenerator._constStatement evaluates eqNode to decide the declaration kind. It lands in _aliasConstStatement because the value aliases a local array.
  2. #hoistIndexAccesses evaluates each index again to decide whether to hoist it. For a comptime index it keeps the original call node, since the result is constant.
  3. this._expression(...) over the rewritten access evaluates that call node a third time.

nextIndex() ran on each pass, and only the last result reached the output, so the shader read values[2].

Fix

GlslGenerator now overrides _constStatement and _expression. While a const is generated, each node of its right-hand side keeps the snippet it evaluated to the first time, keyed by the node object. The hoisting walk and the final resolution get those snippets back instead of re-running the code.

  • Only the RHS tree is cached. collectObjectNodes(eqNode) records the nodes up front, and _expression consults the cache only for those. A shellless function body generated while evaluating the RHS can be evaluated again with other argument types, so its nodes must not be cached. Nested const statements save and restore the previous cache.
  • Identifiers are strings, so they are never cached. Evaluating one has no side effects, and the hoisted idx names are new identifiers that must resolve to their new definitions.
  • The nodes #hoistIndexAccesses creates are new arrays, so they are evaluated normally. Their children are the original nodes and come from the cache.

The WGSL generator does not re-evaluate the RHS in _aliasConstStatement, so it has no such problem, and this change stays inside @typegpu/gl.

Tests

Two cases added to packages/typegpu-gl/tests/implicitPointer.test.ts. Each asserts the generated GLSL and calls === 1:

  • The snippet from the issue now resolves to return values[0].x;.
  • A runtime index with a comptime part, boids.$[nextIndex() + index], hoists to int idx = (0 + index);. The comptime part runs once, and the runtime part is still hoisted to the declaration point.
$ npx vitest run            # packages/typegpu-gl
Test Files  8 passed (8)
     Tests  75 passed | 2 skipped (77)

With only src/glslGenerator.ts reverted, both new tests fail and the other 7 in the file pass.

Also ran:

  • tsc --p ./tsconfig.json --noEmit and tsc --p ./tsconfig.test.json --noEmit for @typegpu/gl: clean.
  • oxlint -c oxlint.config.ts --max-warnings=0 --type-aware on both files: 0 warnings.
  • oxfmt --check on both files: clean.

I did not run the full monorepo pnpm test. The install here was filtered to @typegpu/gl and its dependencies, and no test outside packages/typegpu-gl imports it.

A const that aliases mutable memory evaluated its right-hand side three
times: once in _constStatement, again when _aliasConstStatement hoisted the
index accesses, and again when the rewritten access was resolved. Comptime
code in the index therefore ran three times, so values[nextIndex()] read
values[2] instead of values[0].

While a const statement is generated, cache the snippet each node of its
right-hand side evaluates to, and reuse it on the later passes. Only those
nodes are cached, since a function body generated along the way may be
evaluated again with other argument types.

Fixes software-mansion#3029
Copilot AI lite review requested due to automatic review settings September 16, 2026 16:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • RHS single-evaluation for aliasing constsGlslGenerator now overrides _constStatement and _expression to memoize each right-hand-side node's snippet, so #hoistIndexAccesses and the final resolution reuse the first evaluation instead of re-running comptime code with side effects.
  • collectObjectNodes helper — records every array/object node in the RHS tree up front; identifiers (strings) are deliberately excluded so hoisted idx names still resolve to their new definitions.
  • Nested-const safety — the active cache is saved and restored around super._constStatement via try/finally, and only RHS nodes are cached so shellless function bodies can still be regenerated with different argument types.
  • Two regression testsimplicitPointer.test.ts covers the exact issue snippet (values[0].x, calls === 1) and a hoisted runtime index with a comptime part (int idx = (0 + index);, calls === 1).

I verified both new tests fail against the pre-fix generator (they produced values[2].x and (1 + index)), so they genuinely pin the bug. The full @typegpu/gl suite and pnpm --filter @typegpu/gl test:types pass locally. The fix is correctly scoped to GLSL, since the WGSL _aliasConstStatement does not re-evaluate the RHS.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: GLSL aliases evaluate comptime index multiple times

2 participants