typegpu 0.12.4 → 0.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/bin.mjs +39 -67
  2. package/common/index.d.ts +1 -0
  3. package/common/index.js +1 -0
  4. package/common/writeChannels.d.ts +31 -0
  5. package/common/writeChannels.js +32 -0
  6. package/core/commandEncoder/computePass.d.ts +13 -0
  7. package/core/commandEncoder/computePass.js +5 -2
  8. package/core/commandEncoder/renderPass.d.ts +18 -4
  9. package/core/commandEncoder/renderPass.js +6 -2
  10. package/core/declare/tgpuDeclare.d.ts +11 -0
  11. package/core/declare/tgpuDeclare.js +1 -1
  12. package/core/immediate/immediateVar.d.ts +52 -0
  13. package/core/immediate/immediateVar.js +149 -0
  14. package/core/pipeline/computePipeline.d.ts +23 -0
  15. package/core/pipeline/computePipeline.js +26 -9
  16. package/core/pipeline/drawState.d.ts +8 -4
  17. package/core/pipeline/drawState.js +41 -51
  18. package/core/pipeline/priors.js +1 -0
  19. package/core/pipeline/renderPipeline.d.ts +24 -1
  20. package/core/pipeline/renderPipeline.js +18 -1
  21. package/core/root/init.js +3 -0
  22. package/core/root/rootTypes.d.ts +11 -0
  23. package/core/slot/slotTypes.d.ts +2 -1
  24. package/core/texture/texture.d.ts +20 -14
  25. package/core/texture/texture.js +70 -65
  26. package/core/texture/textureUtils.d.ts +3 -6
  27. package/core/texture/textureUtils.js +142 -165
  28. package/core/texture/textureWrite.d.ts +61 -0
  29. package/core/texture/textureWrite.js +59 -0
  30. package/data/dataIO.d.ts +2 -2
  31. package/data/generalizeFn.d.ts +17 -1
  32. package/data/generalizeFn.js +39 -2
  33. package/data/snippet.d.ts +5 -1
  34. package/data/snippet.js +1 -0
  35. package/data/wgslTypes.d.ts +1 -1
  36. package/errors.d.ts +7 -0
  37. package/errors.js +15 -0
  38. package/indexNamedExports.d.ts +5 -2
  39. package/indexNamedExports.js +2 -1
  40. package/internal.d.ts +1 -1
  41. package/internal.js +1 -1
  42. package/package.json +2 -2
  43. package/resolutionCtx.d.ts +5 -0
  44. package/resolutionCtx.js +11 -0
  45. package/serial/restore.d.ts +3 -1
  46. package/serial/restore.js +2 -0
  47. package/shared/meta.js +1 -1
  48. package/shared/symbols.js +1 -1
  49. package/shared/tseynit.d.ts +1 -0
  50. package/shared/tseynit.js +39 -7
  51. package/shared/utilityTypes.d.ts +1 -1
  52. package/std/atomic.d.ts +13 -0
  53. package/std/atomic.js +63 -29
  54. package/std/bitcast.js +4 -0
  55. package/std/boolean.d.ts +1 -1
  56. package/std/boolean.js +35 -21
  57. package/std/derivative.js +17 -12
  58. package/std/index.d.ts +4 -4
  59. package/std/index.js +5 -5
  60. package/std/numeric.d.ts +5 -1
  61. package/std/numeric.js +75 -8
  62. package/std/operators.d.ts +2 -2
  63. package/std/operators.js +29 -17
  64. package/std/packing.d.ts +61 -1
  65. package/std/packing.js +214 -25
  66. package/std/subgroup.d.ts +4 -0
  67. package/std/subgroup.js +34 -0
  68. package/std/texture.d.ts +13 -0
  69. package/std/texture.js +48 -0
  70. package/tgpuUnstable.d.ts +1 -0
  71. package/tgpuUnstable.js +1 -0
  72. package/tgsl/conversion.d.ts +2 -0
  73. package/tgsl/conversion.js +3 -0
  74. package/tgsl/shaderGenerator.d.ts +1 -1
  75. package/tgsl/wgslGenerator.d.ts +5 -0
  76. package/tgsl/wgslGenerator.js +218 -50
  77. package/types.d.ts +9 -1
  78. package/types.js +5 -0
@@ -6,14 +6,14 @@ import { bool, i32, u32 } from "../data/numeric.js";
6
6
  import { vec2u, vec3u, vec4u } from "../data/vector.js";
7
7
  import { fallthroughCopyOrigin, isAlias, snip, } from "../data/snippet.js";
8
8
  import * as wgsl from "../data/wgslTypes.js";
9
- import { invariant, ResolutionError, WgslTypeError } from "../errors.js";
9
+ import { invariant, ResolutionError, WgslForbiddenStatementError, WgslTypeError, } from "../errors.js";
10
10
  import { getName } from "../shared/meta.js";
11
11
  import { $gpuCallable, $internal, $providing, isMarkedInternal } from "../shared/symbols.js";
12
12
  import { safeStringify } from "../shared/stringify.js";
13
13
  import { pow } from "../std/numeric.js";
14
14
  import { add, div, mul, neg, sub } from "../std/operators.js";
15
15
  import { eq, ne, lt, le, gt, ge, not } from "../std/boolean.js";
16
- import { isGPUCallable, isKnownAtComptime, } from "../types.js";
16
+ import { isConstant, isGPUCallable, isKnownAtComptime, } from "../types.js";
17
17
  import { convertStructValues, convertToCommonType, tryConvertSnippet } from "./conversion.js";
18
18
  import { ArrayExpression, coerceToSnippet, concretize, numericLiteralToSnippet, } from "./generationHelpers.js";
19
19
  import { accessIndex } from "./accessIndex.js";
@@ -28,11 +28,12 @@ import { AutoStruct } from "../data/autoStruct.js";
28
28
  import { mathToStd, supportedLogOps } from "./jsPolyfills.js";
29
29
  import * as forOfUtils from "./forOfUtils.js";
30
30
  import { isTgpuRange } from "../std/range.js";
31
- import { stringifyNode } from "../shared/tseynit.js";
31
+ import { stringifyNode, stringifyObjectProperty } from "../shared/tseynit.js";
32
32
  import { getAttributesString } from "../data/attributes.js";
33
33
  import { validSelectBranchTypes } from "../std/boolean.js";
34
34
  import { isInfixDispatch } from "./infixDispatch.js";
35
35
  import { logger } from "../tgpuLogger.js";
36
+ import { TgpuDeclareImpl } from "../core/declare/tgpuDeclare.js";
36
37
  const { NodeTypeCatalog: NODE } = tinyest;
37
38
  const parenthesizedOps = [
38
39
  '==',
@@ -151,11 +152,23 @@ const usageToVarTemplateMap = {
151
152
  uniform: 'uniform',
152
153
  mutable: 'storage, read_write',
153
154
  readonly: 'storage, read',
155
+ immediate: 'immediate',
154
156
  };
155
157
  /**
156
158
  * The block depth that we can expect when generating code in the function scope, not in any nested blocks.
157
159
  */
158
160
  const functionInitialBlockDepth = 2;
161
+ // Used for switch case.
162
+ const switchDefault = snip('default', UnknownData, 'constant');
163
+ function schemaWrappingSuggestion(declaration, rhs, value) {
164
+ if (value === null || value === undefined || typeof value === 'string') {
165
+ return '';
166
+ }
167
+ return `
168
+ -----
169
+ - Try using or defining a schema that matches your desired value the most, and wrap the value with it: '${declaration} = Schema(${rhs})'
170
+ -----`;
171
+ }
159
172
  export class WgslGenerator {
160
173
  #ctx = undefined;
161
174
  // used to detect `continue` and `break` nodes in loop body, as well as label
@@ -265,6 +278,27 @@ export class WgslGenerator {
265
278
  }
266
279
  return res;
267
280
  }
281
+ _emitSwitchStatement(discriminantExpr, groupedCaseExprs) {
282
+ if (groupedCaseExprs.flatMap(([tests]) => tests).every((test) => test.value !== 'default')) {
283
+ // default clause is required in WGSL
284
+ groupedCaseExprs.push([[switchDefault], []]);
285
+ }
286
+ this.ctx.indent();
287
+ const cases = groupedCaseExprs.map(([tests, consequent]) => {
288
+ const resolvedTests = tests
289
+ .map((test) => (test.value === 'default' ? 'default' : this.ctx.resolveSnippet(test).value))
290
+ .join(', ');
291
+ // Purely cosmetic break pruning (it is legal, but there's no need to generate it).
292
+ const last = consequent.at(-1);
293
+ if (last && /^\s*break\s*;\s*$/.test(last.code) && last.endsWithControlFlow === 'break') {
294
+ consequent.pop();
295
+ }
296
+ const resolvedConsequent = consequent.map((s) => s.code).join('\n');
297
+ return stitch `${this.ctx.pre}case ${resolvedTests}: {\n${resolvedConsequent}\n${this.ctx.pre}}`;
298
+ });
299
+ this.ctx.dedent();
300
+ return stitch `${this.ctx.pre}switch ${discriminantExpr} {\n${cases.join('\n')}\n${this.ctx.pre}}`;
301
+ }
268
302
  _callShellless(callee, args) {
269
303
  const isGeneric = isGenericFn(callee);
270
304
  const slotPairs = isGeneric ? (callee[$providing]?.pairs ?? []) : [];
@@ -308,11 +342,11 @@ export class WgslGenerator {
308
342
  }
309
343
  }
310
344
  _expression(expression) {
311
- if (typeof expression === 'string') {
312
- return this._identifier(expression);
345
+ if (isId(expression)) {
346
+ return this._identifier(extractId(expression));
313
347
  }
314
- if (typeof expression === 'boolean') {
315
- return snip(expression, bool, /* origin */ 'constant', false);
348
+ if (isBool(expression)) {
349
+ return snip(extractBool(expression), bool, /* origin */ 'constant', false);
316
350
  }
317
351
  if (expression[0] === NODE.logicalExpr) {
318
352
  const [_, lhs, op, rhs] = expression;
@@ -481,8 +515,9 @@ export class WgslGenerator {
481
515
  }
482
516
  if (expression[0] === NODE.memberAccess) {
483
517
  // Member Access
484
- const [_, targetNode, property] = expression;
518
+ const [_, targetNode, propertyNode] = expression;
485
519
  const target = this._expression(targetNode);
520
+ const property = extractId(propertyNode);
486
521
  const accessed = accessProp(target, property);
487
522
  if (!accessed) {
488
523
  throw new Error(`Property '${property}' not found on '${stringifyNode(targetNode)}'`);
@@ -560,7 +595,7 @@ export class WgslGenerator {
560
595
  // We just let the argument resolve everything.
561
596
  return snip(this.ctx.resolveSnippet(arg).value, callee.value,
562
597
  // A new array, so not a reference.
563
- /* origin */ 'runtime', arg.possibleSideEffects);
598
+ /* origin */ fallthroughCopyOrigin(arg.origin), arg.possibleSideEffects);
564
599
  }
565
600
  if (callee.value === constant) {
566
601
  throw new Error('Constants cannot be defined within TypeGPU function scope. To address this, move the constant definition outside the function scope.');
@@ -628,11 +663,25 @@ export class WgslGenerator {
628
663
  throw new Error(`Function '${getName(callee.value) ?? String(callee.value)}' is not marked with the 'use gpu' directive and cannot be used in a shader`);
629
664
  }
630
665
  if (expression[0] === NODE.objectExpr) {
631
- // Object Literal
632
- const obj = expression[1];
666
+ // Normalize to `objectProperty[]`
667
+ const properties = Array.isArray(expression[1])
668
+ ? expression[1]
669
+ : Object.entries(expression[1]).map(([key, value]) => [key, value, false]);
670
+ const seenKeys = new Map();
671
+ const resolveUniqueKey = (prop) => {
672
+ const key = this._resolveObjectPropertyKey(prop);
673
+ const dupProp = seenKeys.get(key);
674
+ if (dupProp) {
675
+ throw new WgslTypeError(`Duplicate object property key found: '${stringifyObjectProperty(dupProp)}' and '${stringifyObjectProperty(prop)}'.`);
676
+ }
677
+ seenKeys.set(key, prop);
678
+ return key;
679
+ };
633
680
  const structType = this.ctx.expectedType;
634
681
  if (structType instanceof AutoStruct) {
635
- const entries = Object.fromEntries(Object.entries(obj).map(([key, value]) => {
682
+ const keySnippetPairs = properties.map((prop) => {
683
+ const key = resolveUniqueKey(prop);
684
+ const value = prop[1];
636
685
  let accessed = structType.accessProp(key);
637
686
  let expr;
638
687
  if (accessed) {
@@ -649,21 +698,32 @@ export class WgslGenerator {
649
698
  accessed = structType.provideProp(key, unptr(concretize(expr.dataType)));
650
699
  }
651
700
  return [accessed.prop, expr];
652
- }));
701
+ });
702
+ const entries = Object.fromEntries(keySnippetPairs);
653
703
  const completeStruct = structType.completeStruct;
654
704
  const convertedSnippets = convertStructValues(this.ctx, completeStruct, entries);
655
705
  return snip(stitch `${this.ctx.resolve(structType).value}(${convertedSnippets})`, completeStruct,
656
706
  /* origin */ 'runtime');
657
707
  }
658
708
  if (wgsl.isWgslStruct(structType)) {
659
- const entries = Object.fromEntries(Object.entries(structType.propTypes).map(([key, value]) => {
660
- const val = obj[key];
661
- if (val === undefined) {
709
+ const entries = {};
710
+ for (const prop of properties) {
711
+ const key = resolveUniqueKey(prop);
712
+ const value = prop[1];
713
+ const propType = structType.propTypes[key];
714
+ if (propType === undefined) {
715
+ // Evaluate every field even if it gets stripped by the struct schema
716
+ void this._expression(value);
717
+ continue;
718
+ }
719
+ const expr = this._typedExpression(value, propType);
720
+ entries[key] = expr;
721
+ }
722
+ for (const key of Object.keys(structType.propTypes)) {
723
+ if (entries[key] === undefined) {
662
724
  throw new WgslTypeError(`Missing property ${key} in object literal for struct ${structType}`);
663
725
  }
664
- const result = this._typedExpression(val, value);
665
- return [key, result];
666
- }));
726
+ }
667
727
  const convertedSnippets = convertStructValues(this.ctx, structType, entries);
668
728
  return snip(stitch `${this.ctx.resolve(structType).value}(${convertedSnippets})`, structType,
669
729
  /* origin */ 'runtime', convertedSnippets.some((s) => s.possibleSideEffects));
@@ -818,8 +878,7 @@ export class WgslGenerator {
818
878
  // We can make this snippet ephemeral, as we know it will be deep copied in JS
819
879
  return snip(stitch `${args[0]}`, schema, fallthroughCopyOrigin(args[0].origin), args[0].possibleSideEffects);
820
880
  }
821
- // Creating a 'runtime' snippet, since it's instantiating a new value
822
- return snip(stitch `${this.ctx.resolve(schema).value}(${args})`, schema, 'runtime', args.some((s) => s.possibleSideEffects));
881
+ return snip(stitch `${this.ctx.resolve(schema).value}(${args})`, schema, args.every((arg) => arg.origin === 'constant') ? 'constant' : 'runtime', args.some((s) => s.possibleSideEffects));
823
882
  }
824
883
  numericLiteral(value, schema) {
825
884
  if (!Number.isFinite(value)) {
@@ -918,7 +977,8 @@ Try 'return ${typeStr}(${str});' instead.
918
977
  return `${this.ctx.pre}return;`;
919
978
  }
920
979
  _letStatement(statement) {
921
- const [_, rawId, eqNode] = statement;
980
+ const [_, rawIdNode, eqNode] = statement;
981
+ const rawId = extractId(rawIdNode);
922
982
  if (eqNode === undefined) {
923
983
  throw new Error(`'${stringifyNode(statement)}' is invalid because all variables need initializers.`);
924
984
  }
@@ -931,12 +991,10 @@ Try 'return ${typeStr}(${str});' instead.
931
991
  -----`);
932
992
  }
933
993
  const definitionDataType = eq.dataType;
934
- if (definitionDataType === UnknownData) {
994
+ if (definitionDataType === UnknownData || wgsl.isVoid(definitionDataType)) {
935
995
  const rhsStr = stringifyNode(eqNode);
936
- throw new WgslTypeError(`'let ${rawId} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'
937
- -----
938
- - Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'let ${rawId} = Schema(${rhsStr})'
939
- -----`);
996
+ const declaration = `let ${rawId}`;
997
+ throw new WgslTypeError(`'${declaration} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'${schemaWrappingSuggestion(declaration, rhsStr, eq.value)}`);
940
998
  }
941
999
  if (isAlias(eq) && !wgsl.isNaturallyEphemeral(eq.dataType)) {
942
1000
  // `let` declarations cannot store references
@@ -967,7 +1025,8 @@ Try 'return ${typeStr}(${str});' instead.
967
1025
  };
968
1026
  }
969
1027
  _constStatement(statement) {
970
- const [_, rawId, eqNode] = statement;
1028
+ const [_, rawIdNode, eqNode] = statement;
1029
+ const rawId = extractId(rawIdNode);
971
1030
  if (eqNode === undefined) {
972
1031
  throw new Error(`'${stringifyNode(statement)}' is invalid because all variables need initializers.`);
973
1032
  }
@@ -988,12 +1047,10 @@ Try 'return ${typeStr}(${str});' instead.
988
1047
  let varOrigin = 'local-def';
989
1048
  let varType = '<deferred>';
990
1049
  let definitionDataType = eq.dataType;
991
- if (definitionDataType === UnknownData) {
1050
+ if (definitionDataType === UnknownData || wgsl.isVoid(definitionDataType)) {
992
1051
  const rhsStr = stringifyNode(eqNode);
993
- throw new WgslTypeError(`'const ${rawId} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'
994
- -----
995
- - Try using or defining a schema that matches your desired value the most, and wrap the value with it: 'const ${rawId} = Schema(${rhsStr})'
996
- -----`);
1052
+ const declaration = `const ${rawId}`;
1053
+ throw new WgslTypeError(`'${declaration} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'${schemaWrappingSuggestion(declaration, rhsStr, eq.value)}`);
997
1054
  }
998
1055
  if (eq.origin === 'argument') {
999
1056
  // Arguments are immutable, so we 'let' them be (kill me)
@@ -1083,16 +1140,16 @@ Try 'return ${typeStr}(${str});' instead.
1083
1140
  };
1084
1141
  }
1085
1142
  _statement(statement) {
1086
- if (typeof statement === 'string') {
1087
- const id = this._identifier(statement);
1088
- const resolved = id.value !== undefined && id.value !== null ? this.ctx.resolveSnippet(id).value : '';
1089
- return { code: resolved ? `${this.ctx.pre}${resolved};` : '', definesInNearestScope: false };
1143
+ // TODO(#3078): Remove this check.
1144
+ if (isId(statement)) {
1145
+ const item = this.ctx.getById(extractId(statement));
1146
+ if (item?.value instanceof TgpuDeclareImpl) {
1147
+ this.ctx.resolveSnippet(item);
1148
+ return { code: '', definesInNearestScope: false };
1149
+ }
1090
1150
  }
1091
- if (typeof statement === 'boolean') {
1092
- return {
1093
- code: `${this.ctx.pre}${statement ? 'true' : 'false'};`,
1094
- definesInNearestScope: false,
1095
- };
1151
+ if (isId(statement) || isBool(statement)) {
1152
+ throw new WgslForbiddenStatementError(statement);
1096
1153
  }
1097
1154
  if (statement[0] === NODE.return) {
1098
1155
  return {
@@ -1205,22 +1262,21 @@ ${this.ctx.pre}else ${alternate}`,
1205
1262
  const shouldUnroll = iterableExpr.value instanceof UnrollableIterable;
1206
1263
  const iterableSnippet = shouldUnroll ? iterableExpr.value.snippet : iterableExpr;
1207
1264
  const range = forOfUtils.getRangeSnippets(this.ctx, iterableSnippet, shouldUnroll);
1208
- const originalLoopVarName = loopVar[1];
1265
+ const originalLoopVarName = extractId(loopVar[1]);
1209
1266
  const blockified = blockifySingleStatement(body);
1210
1267
  if (shouldUnroll) {
1211
1268
  if (!isKnownAtComptime(range.end)) {
1212
1269
  throw new Error('Cannot unroll loop. Length of iterable is unknown at comptime.');
1213
1270
  }
1214
- const length = range.end.value;
1215
- if (length === 0) {
1216
- return { code: '', definesInNearestScope: false };
1217
- }
1218
1271
  const { value } = iterableSnippet;
1219
1272
  const elements = isTgpuRange(value)
1220
1273
  ? value.map((i) => coerceToSnippet(i))
1221
1274
  : value instanceof ArrayExpression
1222
1275
  ? value.elements
1223
- : Array.from({ length }, (_, i) => forOfUtils.getElementSnippet(iterableSnippet, snip(i, u32, 'constant')));
1276
+ : Array.from({ length: range.end.value }, (_, i) => forOfUtils.getElementSnippet(iterableSnippet, snip(i, u32, 'constant')));
1277
+ if (elements.length === 0) {
1278
+ return { code: '', definesInNearestScope: false };
1279
+ }
1224
1280
  const firstElement = elements[0];
1225
1281
  if (!isAlias(firstElement) && !wgsl.isNaturallyEphemeral(firstElement.dataType)) {
1226
1282
  throw new WgslTypeError(`Cannot unroll '${stringifyNode(iterable)}'. The elements of iterable are constructed in place but are not value types.`);
@@ -1284,6 +1340,69 @@ ${this.ctx.pre}else ${alternate}`,
1284
1340
  this.ctx.popBlockScope();
1285
1341
  }
1286
1342
  }
1343
+ if (statement[0] === NODE.switch) {
1344
+ // Switch statement
1345
+ const [_, discriminant, cases] = statement;
1346
+ const discriminantExpr = this._typedExpression(discriminant, [i32, u32]);
1347
+ const switchType = discriminantExpr.dataType;
1348
+ invariant(switchType !== UnknownData);
1349
+ const caseExprs = cases.map(([test, consequent]) => {
1350
+ const testExpr = test === null ? switchDefault : this._typedExpression(test, [switchType]);
1351
+ // In WGSL, each case is a different block. This block scope forbids scope leaking.
1352
+ // TODO(#3001): Consider using NODE.block here
1353
+ this.ctx.pushBlockScope();
1354
+ this.ctx.indent();
1355
+ this.ctx.indent();
1356
+ try {
1357
+ const consequentStmts = consequent.map((s) => this._statement(s));
1358
+ return [testExpr, consequentStmts];
1359
+ }
1360
+ finally {
1361
+ this.ctx.dedent();
1362
+ this.ctx.dedent();
1363
+ this.ctx.popBlockScope();
1364
+ }
1365
+ });
1366
+ // Validation
1367
+ {
1368
+ // Tests should be constant
1369
+ caseExprs.forEach(([testExpr], i) => {
1370
+ if (!isConstant(testExpr)) {
1371
+ const testNode = cases[i]?.[0];
1372
+ invariant(testNode, `Expected node to be not nullish.`);
1373
+ throw new Error(`All of switch tests must be constant.
1374
+ Test '${stringifyNode(testNode)}' is not constant, making the following switch statement invalid.
1375
+ This error may be caused by an implicit conversion.
1376
+ ${stringifyNode(statement)}`);
1377
+ }
1378
+ });
1379
+ // Tests should not have duplicates.
1380
+ // We skip this check, because WGSL errors are readable,
1381
+ // and we cannot easily access non-comptime known constants.
1382
+ // Tests should not have non-trivial fallthrough
1383
+ caseExprs.slice(0, -1).forEach(([_, consequent]) => {
1384
+ const last = consequent.at(-1);
1385
+ if (last && !last.endsWithControlFlow) {
1386
+ throw new Error(`Switch statement cannot have non-trivial fallthrough.
1387
+ The following switch statement is invalid:
1388
+ ${stringifyNode(statement)}`);
1389
+ }
1390
+ });
1391
+ }
1392
+ const groupedCaseExprs = [];
1393
+ let currentGroup = [];
1394
+ for (const [index, [test, consequent]] of caseExprs.entries()) {
1395
+ currentGroup.push(test);
1396
+ if (consequent.length > 0 || index === caseExprs.length - 1) {
1397
+ groupedCaseExprs.push([currentGroup, consequent]);
1398
+ currentGroup = [];
1399
+ }
1400
+ }
1401
+ return {
1402
+ code: this._emitSwitchStatement(discriminantExpr, groupedCaseExprs),
1403
+ definesInNearestScope: false,
1404
+ };
1405
+ }
1287
1406
  if (statement[0] === NODE.postUpdate) {
1288
1407
  // Post-update statement
1289
1408
  const [_, op, arg] = statement;
@@ -1313,10 +1432,38 @@ ${this.ctx.pre}else ${alternate}`,
1313
1432
  definesInNearestScope: false,
1314
1433
  };
1315
1434
  }
1435
+ if (statement[0] === NODE.numericLiteral ||
1436
+ statement[0] === NODE.binaryExpr ||
1437
+ statement[0] === NODE.unaryExpr ||
1438
+ statement[0] === NODE.logicalExpr ||
1439
+ statement[0] === NODE.arrayExpr ||
1440
+ statement[0] === NODE.objectExpr ||
1441
+ statement[0] === NODE.nullLiteral) {
1442
+ // `memberAccess` is allowed for raw code snippets, `indexAccess` is allowed for consistency.
1443
+ // `stringLiteral` is forbidden by our injection prevention anyway.
1444
+ throw new WgslForbiddenStatementError(statement);
1445
+ }
1316
1446
  const expr = this._expression(statement);
1317
1447
  const resolved = expr.value !== undefined && expr.value !== null ? this.ctx.resolveSnippet(expr).value : '';
1318
1448
  return { code: resolved ? `${this.ctx.pre}${resolved};` : '', definesInNearestScope: false };
1319
1449
  }
1450
+ /**
1451
+ * Resolves the key of an object property. Handles both computed and non-computed keys.
1452
+ */
1453
+ _resolveObjectPropertyKey(property) {
1454
+ const computed = property[2];
1455
+ if (!computed) {
1456
+ return property[0];
1457
+ }
1458
+ const key = this._expression(property[0]);
1459
+ if (!isKnownAtComptime(key)) {
1460
+ throw new WgslTypeError(`Computed object property key '${stringifyObjectProperty(property)}' must be known at comptime.`);
1461
+ }
1462
+ if (typeof key.value !== 'string') {
1463
+ throw new WgslTypeError('Object property keys must be strings in TypeGPU functions.');
1464
+ }
1465
+ return key.value;
1466
+ }
1320
1467
  /**
1321
1468
  * Attempts a member access lookup to mark a variable as modified.
1322
1469
  * @example
@@ -1358,6 +1505,9 @@ function validateSnippetMutation(mutated, expr) {
1358
1505
  if (mutated.origin === 'readonly') {
1359
1506
  throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because readonly buffers cannot be mutated.`);
1360
1507
  }
1508
+ if (mutated.origin === 'immediate') {
1509
+ throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because immediate variables cannot be mutated.`);
1510
+ }
1361
1511
  if (mutated.origin === 'argument') {
1362
1512
  throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because non-pointer arguments cannot be mutated.`);
1363
1513
  }
@@ -1387,7 +1537,25 @@ function extractObject(expr) {
1387
1537
  (object[0] === NODE.memberAccess || object[0] === NODE.indexAccess)) {
1388
1538
  object = object[1];
1389
1539
  }
1390
- if (typeof object === 'string') {
1391
- return object;
1540
+ if (isId(object)) {
1541
+ return extractId(object);
1542
+ }
1543
+ }
1544
+ function isId(expr) {
1545
+ return typeof expr === 'string' || (Array.isArray(expr) && expr[0] === NODE.identifier);
1546
+ }
1547
+ function extractId(ident) {
1548
+ if (typeof ident === 'string') {
1549
+ return ident;
1550
+ }
1551
+ return ident[1];
1552
+ }
1553
+ function isBool(expr) {
1554
+ return typeof expr === 'boolean' || (Array.isArray(expr) && expr[0] === NODE.booleanLiteral);
1555
+ }
1556
+ function extractBool(ident) {
1557
+ if (typeof ident === 'boolean') {
1558
+ return ident;
1392
1559
  }
1560
+ return ident[1];
1393
1561
  }
package/types.d.ts CHANGED
@@ -13,6 +13,7 @@ import type { TgpuSampler } from './core/sampler/sampler.ts';
13
13
  import { type Eventual, type SlotValuePair, type TgpuAccessor, type TgpuLazy, type TgpuSlot } from './core/slot/slotTypes.ts';
14
14
  import type { TgpuExternalTexture } from './core/texture/externalTexture.ts';
15
15
  import type { TgpuTexture, TgpuTextureView } from './core/texture/texture.ts';
16
+ import type { TgpuImmediateVar } from './core/immediate/immediateVar.ts';
16
17
  import type { TgpuVar } from './core/variable/tgpuVariable.ts';
17
18
  import { type AnyData, UnknownData } from './data/dataTypes.ts';
18
19
  import type { MapValueToSnippet, ResolvedSnippet, Snippet } from './data/snippet.ts';
@@ -26,7 +27,7 @@ import type { StorageFlag } from './extension.ts';
26
27
  import type { TgpuBufferBinding } from './core/buffer/bufferBinding.ts';
27
28
  import type { ShelllessRepository } from './tgsl/shellless.ts';
28
29
  import type { SupportedLogOp } from './tgsl/consoleLog/types.ts';
29
- export type ResolvableObject = SelfResolvable | TgpuLazy<unknown> | TgpuConst | TgpuDeclare | TgpuBindGroupLayout | TgpuFn | TgpuComputeFn | TgpuFragmentFn | TgpuComputePipeline | TgpuRenderPipeline | TgpuVertexFn | TgpuSampler | TgpuAccessor | TgpuExternalTexture | TgpuTexture | TgpuTextureView | TgpuBufferBinding<BaseData> | TgpuVar | AnyVecInstance | AnyMatInstance | AnyData | ((...args: never[]) => unknown);
30
+ export type ResolvableObject = SelfResolvable | TgpuLazy<unknown> | TgpuConst | TgpuDeclare | TgpuBindGroupLayout | TgpuFn | TgpuComputeFn | TgpuFragmentFn | TgpuComputePipeline | TgpuRenderPipeline | TgpuVertexFn | TgpuSampler | TgpuAccessor | TgpuExternalTexture | TgpuTexture | TgpuTextureView | TgpuBufferBinding<BaseData> | TgpuVar | TgpuImmediateVar | AnyVecInstance | AnyMatInstance | AnyData | ((...args: never[]) => unknown);
30
31
  export type Wgsl = Eventual<number | boolean | ResolvableObject>;
31
32
  export type ShaderStage = 'compute' | 'vertex' | 'fragment';
32
33
  export interface ResolveFunctionOptions {
@@ -281,6 +282,12 @@ export interface ResolutionCtx {
281
282
  * @returns an identifier that is unique within the given scope
282
283
  */
283
284
  makeUniqueIdentifier(primer: string | undefined, scope: 'global' | 'block'): string;
285
+ /**
286
+ * Registers the use of an immediate variable in the current resolution.
287
+ * At most one `var<immediate>` is allowed per resolution.
288
+ * @throws When a different immediate variable has already been registered.
289
+ */
290
+ registerImmediate(immediate: TgpuImmediateVar): void;
284
291
  isIdentifierBanned(name: string): boolean;
285
292
  /**
286
293
  * @param name The name to check.
@@ -352,6 +359,7 @@ export declare function hasCast(value: unknown): value is WithCast;
352
359
  type AnyFn = (...args: never[]) => unknown;
353
360
  export type DualFn<T extends AnyFn> = T & GPUCallable<Parameters<T>>;
354
361
  export declare function isKnownAtComptime(snippet: Snippet): boolean;
362
+ export declare function isConstant(snippet: Snippet): boolean;
355
363
  export declare function isWgsl(value: unknown): value is Wgsl;
356
364
  export type BindableBufferUsage = 'uniform' | 'readonly' | 'mutable';
357
365
  export type BufferUsage = 'uniform' | 'readonly' | 'mutable' | 'vertex';
package/types.js CHANGED
@@ -33,6 +33,11 @@ export function isKnownAtComptime(snippet) {
33
33
  return ((typeof snippet.value !== 'string' || snippet.dataType === UnknownData) &&
34
34
  getOwnSnippet(snippet.value) === undefined);
35
35
  }
36
+ export function isConstant(snippet) {
37
+ return (isKnownAtComptime(snippet) ||
38
+ snippet.origin === 'constant' ||
39
+ snippet.origin === 'constant-immutable-def');
40
+ }
36
41
  export function isWgsl(value) {
37
42
  return (typeof value === 'number' ||
38
43
  typeof value === 'boolean' ||