feat: Simple Object destructuring in GPU functions - #2913
Conversation
|
pkg.pr.new packages benchmark commit |
Bundle size comparison (
|
| 🟢 Decreased | ➖ Unchanged | 🔴 Increased (max 1.34%) | ❔ Unknown |
|---|---|---|---|
| 0 | 302 | 23 | 0 |
import * as ... in PR vs import * as ... in target (did bundle size increase?):
| Test | tsdown |
|---|---|
| common_fullScreenTriangle.ts | 42.88 kB ( |
| STATIC_common.ts | 59.64 kB ( |
import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):
| Test | tsdown |
|---|---|
| tgpu_init.ts | 264.40 kB ( |
| tgpu_initFromDevice.ts | 263.86 kB ( |
| tgpu_resolve.ts | 163.06 kB ( |
| tgpu_resolveWithContext.ts | 163.00 kB ( |
| tgpu_bindGroupLayout.ts | 62.32 kB ( |
| tgpu_mutableAccessor.ts | 57.61 kB ( |
| tgpu_accessor.ts | 57.61 kB ( |
| tgpu_privateVar.ts | 55.73 kB ( |
| tgpu_workgroupVar.ts | 55.73 kB ( |
| tgpu_const.ts | 55.15 kB ( |
| tgpu_lazy.ts | 54.95 kB ( |
| tgpu_fragmentFn.ts | 40.25 kB ( |
| tgpu_fn.ts | 40.19 kB ( |
| tgpu_vertexFn.ts | 40.06 kB ( |
| tgpu_computeFn.ts | 39.77 kB ( |
| tgpu_vertexLayout.ts | 28.33 kB ( |
| tgpu_comptime.ts | 15.93 kB ( |
| tgpu_unroll.ts | 1.75 kB ( |
| tgpu_slot.ts | 1.70 kB ( |
If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.
Resolution Time Benchmark---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.70, 1.46, 3.18, 4.30, 5.11, 8.22, 17.20, 17.44]
line [0.65, 1.40, 3.09, 4.04, 4.74, 8.03, 15.37, 16.88]
line [0.73, 1.36, 2.94, 4.10, 5.37, 8.42, 16.44, 15.99]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.22, 0.40, 0.52, 0.57, 0.89, 0.85, 1.02, 1.12]
line [0.26, 0.46, 0.59, 0.63, 0.90, 0.79, 1.03, 1.09]
line [0.22, 0.36, 0.55, 0.60, 0.94, 0.81, 1.01, 1.10]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.72, 1.78, 2.60, 5.12, 9.71, 18.43, 41.61, 81.47]
line [0.82, 1.63, 2.57, 4.74, 8.20, 18.61, 38.67, 76.74]
line [0.68, 1.70, 2.31, 4.77, 8.63, 18.57, 37.15, 77.91]
|
There was a problem hiding this comment.
Pull request overview
This PR adds support for simple object destructuring inside 'use gpu' functions by extending the Tinyest AST to represent variable bindings as a BindingPattern (identifier vs. destructured object), then updating parsing, WGSL generation, linting, obfuscation, and tests accordingly.
Changes:
- Extend Tinyest
let/constdeclarations to useBindingPatternand introduceBindingPatternType(with compatibility aliases for existingFuncParameter*naming). - Parse simple object destructuring for variable declarations / parameters in
tinyest-for-wgsl, and generate corresponding WGSL declarations inWgslGenerator. - Update ESLint rule coverage and test snapshots across packages to reflect the new AST shape and supported destructuring subset.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/unplugin-typegpu/test/use-gpu-directive.test.ts | Updates expected Tinyest AST snapshots for let/const bindings. |
| packages/unplugin-typegpu/test/tgsl-transpiling.test.ts | Updates transpilation snapshots to the new binding pattern representation. |
| packages/unplugin-typegpu/test/parser-options.test.ts | Adjusts parser option snapshot to the new BindingPattern form. |
| packages/unplugin-typegpu/test/obfuscation.test.ts | Adds coverage for obfuscating destructured variable declarations + updates snapshots. |
| packages/unplugin-typegpu/test/aliasing.test.ts | Updates alias-gathering snapshots to new binding representation. |
| packages/unplugin-typegpu/src/core/obfuscate.ts | Refactors binding obfuscation to support identifier vs destructured object patterns. |
| packages/typegpu/tests/tgsl/wgslGenerator.test.ts | Adds WGSL generator tests for object destructuring (const/let, single-eval, name conflicts). |
| packages/typegpu/tests/internal/tseynit.test.ts | Adds stringify coverage for destructured binding declarations. |
| packages/typegpu/tests/internal/metadata.test.ts | Updates metadata normalization tests to handle legacy vs new binding forms. |
| packages/typegpu/src/tgsl/wgslGenerator.ts | Implements object destructuring statement lowering and updates let/const handling to BindingPattern. |
| packages/typegpu/src/shared/tseynit.ts | Adds BindingPattern stringification for debugging/printing. |
| packages/typegpu/src/shared/normalizeMetadata.ts | Normalizes legacy metadata by converting string bindings into identifier BindingPatterns. |
| packages/typegpu/src/resolutionCtx.ts | Updates parameter pattern checks to use BindingPatternType. |
| packages/tinyest/src/nodes.ts | Introduces BindingPatternType/BindingPattern, updates Let/Const to use them, keeps compatibility aliases. |
| packages/tinyest-for-wgsl/tests/parsers.test.ts | Adds parser coverage for destructured declarations and unsupported patterns + snapshot updates. |
| packages/tinyest-for-wgsl/src/parsers.ts | Adds parsing for BindingPattern (identifier/object destructuring), rejects unsupported destructuring contexts. |
| packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts | Expands valid/invalid coverage for supported destructuring + unsupported binding patterns / loop headers. |
| packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts | Allows supported simple object binding patterns, adds checks for destructuring assignment and loop-header destructuring. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return { | ||
| code: declarations.map((declaration) => declaration.code).join('\n'), | ||
| definesInNearestScope: true, | ||
| }; |
| const loopBinding = loopVar[1]; | ||
| if (loopBinding.type !== tinyest.BindingPatternType.identifier) { | ||
| throw new WgslTypeError('Destructuring in for..of loops is not supported yet.'); | ||
| } |
There was a problem hiding this comment.
✅ No outstanding changes needed.
Reviewed changes
- tinyest
Let/Constbinding overhaul — The 2nd tuple element changed from astringto a structuredBindingPattern(identifier/destructuredObject);FuncParameter/FuncParameterTypekept as aliases of the new names for backward source compatibility, and every consumer of the shared node shape was updated in lockstep. - TGSL parser (
tinyest-for-wgsl) — NewparseBindingPattern(shared by variable declarations and function parameters) replaces the previous per-parameter inline mapping, and now hard-rejects nested/computed/default/rest/array patterns, destructuring assignments, and destructuring infor-loop init /for...ofheaders with clear errors. - WGSL lowering (
wgslGenerator) —_objectDestructuringStatementlowers a destructured const/let into an internal temporary (guaranteeing single evaluation of the source expression) plus one const/let per alias; temporary names are uniquified via the existingmakeUniqueIdentifiermachinery, and mutable destructuredletcorrectly lowers tovar. The GLSL generator inherits the same lowering. - Metadata migration (
normalizeMetadata) —normalizeLegacyBindingsrecursively rewrites legacy v1/v2 metadata where a let/const binding was a plain string into the new identifierBindingPatternshape, and is idempotent on the new object form; the internal metadata tests exercise real end-to-end resolve on legacy-shaped metadata as well as the new format. - ESLint rule (
noUnsupportedSyntax) — Now allows simple object-pattern variable declarations and destructured parameters while rejecting everything else; its support predicate matches what the parser accepts, so lint and transpile stay in agreement. - Obfuscation (
obfuscate.ts) —obfuscateBindingPatternrenames identifier bindings and destructured aliases while leaving property names intact (they must survive into the generated member access). - Tests across the parser, generator, metadata migration, obfuscation, and eslint rule layers, including single-evaluation, name-collision, and mutation cases.
⚠️ Pre-existing silent-drop now fails loudly (improvement, but a behavior change)
On the old code path, destructured function-parameter sub-properties that weren't the simple key: alias shape were silently dropped (props.flatMap((prop) => condition ? [entry] : [])), which could route an argument into the wrong struct field — a correctness bug. The new parseBindingPattern throws for these. That hard error is strictly better and the eslint rule now reports it earlier with a helpful message, but it is a behavioral change for any code that previously transpiled-but-mishandled such params — worth a line in the changelog/release notes.
I verified the changed packages independently: all targeted unit tests pass (tinyest-for-wgsl parsers, unplugin obfuscation, typegpu wgslGenerator/metadata/tseynit, eslint noUnsupportedSyntax) and the core typegpu typecheck (pnpm --filter typegpu test:types) is clean.
ℹ️ Nitpicks
wgslGenerator.#destructuringIndexis an instance-global counter that is never reset, so temporary names are numbered monotonically across every function in a resolve rather than per function. Harmless (each name is further uniquified in its own scope) but slightly inconsistent with how other names are scoped.- The generated temporary for a non-identifier destructing source is emitted as a mutable
let(struct values take the deferred var-type path even though the temp is never reassigned). Cosmetic;constcould be used, but there's no behavioral impact.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Adds #2318
Allows simple object destructuring like:
or
which generates:
and