typegpu 0.12.6 → 0.12.8

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 (87) hide show
  1. package/common/writeSoA.js +16 -0
  2. package/core/buffer/buffer.js +47 -11
  3. package/core/commandEncoder/attachments.js +5 -2
  4. package/core/function/autoIO.js +5 -3
  5. package/core/function/comptime.js +7 -5
  6. package/core/function/createCallableSchema.js +4 -3
  7. package/core/function/dualImpl.js +20 -19
  8. package/core/function/ioSchema.d.ts +17 -2
  9. package/core/function/ioSchema.js +74 -27
  10. package/core/function/tgpuFn.d.ts +11 -1
  11. package/core/function/tgpuFn.js +9 -2
  12. package/core/function/tgpuFragmentFn.d.ts +1 -1
  13. package/core/function/tgpuFragmentFn.js +2 -1
  14. package/core/function/tgpuVertexFn.js +2 -1
  15. package/core/pipeline/connectAttachmentToShader.d.ts +2 -2
  16. package/core/pipeline/connectAttachmentToShader.js +16 -21
  17. package/core/pipeline/connectTargetsToShader.d.ts +5 -1
  18. package/core/pipeline/connectTargetsToShader.js +23 -18
  19. package/core/pipeline/renderPipeline.d.ts +9 -6
  20. package/core/pipeline/renderPipeline.js +22 -22
  21. package/core/pipeline/timeable.js +15 -2
  22. package/core/resolve/namespace.js +2 -2
  23. package/core/root/configurableImpl.js +1 -2
  24. package/core/root/rootTypes.d.ts +6 -2
  25. package/core/slot/accessor.d.ts +1 -1
  26. package/core/slot/accessor.js +72 -5
  27. package/core/texture/externalTexture.d.ts +1 -1
  28. package/core/texture/externalTexture.js +3 -1
  29. package/core/texture/texture.d.ts +1 -1
  30. package/core/texture/texture.js +12 -5
  31. package/core/unroll/tgpuUnroll.d.ts +4 -3
  32. package/core/unroll/tgpuUnroll.js +7 -1
  33. package/core/valueProxyUtils.d.ts +4 -2
  34. package/core/valueProxyUtils.js +4 -2
  35. package/data/autoStruct.d.ts +1 -1
  36. package/data/autoStruct.js +8 -4
  37. package/data/compiledIO.js +12 -9
  38. package/data/dataIO.d.ts +10 -6
  39. package/data/dataIO.js +19 -4
  40. package/data/dataTypes.d.ts +0 -5
  41. package/data/dataTypes.js +0 -6
  42. package/data/deepEqual.js +19 -9
  43. package/data/matrix.js +10 -3
  44. package/data/matrixColumnsAccess.d.ts +8 -0
  45. package/data/matrixColumnsAccess.js +15 -0
  46. package/data/offsetUtils.js +181 -102
  47. package/data/partialIO.d.ts +8 -1
  48. package/data/partialIO.js +36 -19
  49. package/data/ref.d.ts +12 -5
  50. package/data/ref.js +51 -24
  51. package/data/snippet.d.ts +13 -4
  52. package/data/snippet.js +17 -4
  53. package/data/struct.js +3 -0
  54. package/data/wgslTypes.d.ts +6 -3
  55. package/internal.d.ts +1 -0
  56. package/nameUtils.d.ts +7 -0
  57. package/nameUtils.js +60 -0
  58. package/package.json +1 -1
  59. package/resolutionCtx.js +1 -1
  60. package/shared/meta.js +1 -1
  61. package/shared/symbols.d.ts +5 -0
  62. package/shared/symbols.js +6 -1
  63. package/shared/tseynit.js +54 -50
  64. package/std/boolean.js +9 -10
  65. package/std/numeric.js +1 -4
  66. package/std/operators.js +9 -4
  67. package/std/packing.js +10 -10
  68. package/std/texture.d.ts +2 -2
  69. package/tgpuLogger.d.ts +1 -1
  70. package/tgpuLogger.js +0 -2
  71. package/tgsl/accessIndex.js +37 -38
  72. package/tgsl/accessProp.js +10 -7
  73. package/tgsl/conversion.js +3 -2
  74. package/tgsl/forOfUtils.js +8 -8
  75. package/tgsl/generationHelpers.d.ts +4 -3
  76. package/tgsl/generationHelpers.js +18 -5
  77. package/tgsl/infixDispatch.d.ts +4 -1
  78. package/tgsl/infixDispatch.js +13 -1
  79. package/tgsl/makeDereferenceable.d.ts +1 -1
  80. package/tgsl/makeDereferenceable.js +3 -1
  81. package/tgsl/shellless.js +5 -9
  82. package/tgsl/stringifySnippet.d.ts +9 -0
  83. package/tgsl/stringifySnippet.js +24 -0
  84. package/tgsl/wgslGenerator.d.ts +1 -0
  85. package/tgsl/wgslGenerator.js +180 -86
  86. package/types.d.ts +23 -2
  87. package/types.js +30 -5
@@ -4,7 +4,7 @@ import { arrayOf } from "../data/array.js";
4
4
  import { UnknownData, unptr } from "../data/dataTypes.js";
5
5
  import { bool, i32, u32 } from "../data/numeric.js";
6
6
  import { vec2u, vec3u, vec4u } from "../data/vector.js";
7
- import { fallthroughCopyOrigin, isAlias, snip, } from "../data/snippet.js";
7
+ import { fallthroughCopyOrigin, isMutableOrigin, isStoredInMemory, snip, } from "../data/snippet.js";
8
8
  import * as wgsl from "../data/wgslTypes.js";
9
9
  import { invariant, ResolutionError, WgslForbiddenStatementError, WgslTypeError, } from "../errors.js";
10
10
  import { getName } from "../shared/meta.js";
@@ -13,7 +13,7 @@ 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 { isConstant, isGPUCallable, isKnownAtComptime, } from "../types.js";
16
+ import { asComptime, isConstant, isGPUCallable, } 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";
@@ -29,6 +29,7 @@ import { mathToStd, supportedLogOps } from "./jsPolyfills.js";
29
29
  import * as forOfUtils from "./forOfUtils.js";
30
30
  import { isTgpuRange } from "../std/range.js";
31
31
  import { stringifyNode, stringifyObjectProperty } from "../shared/tseynit.js";
32
+ import { setSourceNode, stringifySnippet } from "./stringifySnippet.js";
32
33
  import { getAttributesString } from "../data/attributes.js";
33
34
  import { validSelectBranchTypes } from "../std/boolean.js";
34
35
  import { isInfixDispatch } from "./infixDispatch.js";
@@ -127,12 +128,13 @@ const unaryOpCodeToCodegen = {
127
128
  if (argExpr === undefined) {
128
129
  throw new Error('The unary operator `!` expects 1 argument, but 0 were provided.');
129
130
  }
130
- if (isKnownAtComptime(argExpr)) {
131
- return snip(!argExpr.value, bool, 'constant', false);
131
+ const knownArg = asComptime(argExpr);
132
+ if (knownArg) {
133
+ return snip(!knownArg.value, bool, 'constant', false);
132
134
  }
133
135
  const argStr = ctx.resolveSnippet(argExpr).value;
134
136
  if (!wgsl.isBool(argExpr.dataType)) {
135
- throw new WgslTypeError(`Unary operator ! requires boolean operand. Got ${String(argExpr.dataType)}.${wgsl.isVecBool(argExpr.dataType)
137
+ throw new WgslTypeError(`Unary operator ! requires boolean operand. Got '${stringifySnippet(argExpr)}' of type ${String(argExpr.dataType)}.${wgsl.isVecBool(argExpr.dataType)
136
138
  ? ` For component-wise negation, use 'std.${not.toString()}'.`
137
139
  : ''}`);
138
140
  }
@@ -279,25 +281,41 @@ export class WgslGenerator {
279
281
  return res;
280
282
  }
281
283
  _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
284
  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).
285
+ // It is legal to generate a break at the end of a consequent, but there is no need to.
286
+ // Also, removing this lets us simplify more pruned switch statements to blocks.
292
287
  const last = consequent.at(-1);
293
288
  if (last && /^\s*break\s*;\s*$/.test(last.code) && last.endsWithControlFlow === 'break') {
294
289
  consequent.pop();
295
290
  }
296
291
  const resolvedConsequent = consequent.map((s) => s.code).join('\n');
297
- return stitch `${this.ctx.pre}case ${resolvedTests}: {\n${resolvedConsequent}\n${this.ctx.pre}}`;
292
+ return { tests, resolvedConsequent };
293
+ });
294
+ if (cases.flatMap(({ tests }) => tests).every((test) => test !== switchDefault)) {
295
+ // default clause is required in WGSL
296
+ cases.push({ tests: [switchDefault], resolvedConsequent: '' });
297
+ }
298
+ // If the switch was pruned, and it has no breaks, we can simplify it to a block.
299
+ const onlyCase = cases.length === 1 ? cases.at(0) : undefined;
300
+ if (asComptime(discriminantExpr) !== undefined &&
301
+ onlyCase &&
302
+ onlyCase.tests.length === 1 &&
303
+ onlyCase.tests.at(0) === switchDefault) {
304
+ const { resolvedConsequent } = onlyCase;
305
+ if (!/\bbreak\b/.test(resolvedConsequent)) {
306
+ return `${this.ctx.pre}{\n${this.ctx.getDedented(resolvedConsequent)}\n${this.ctx.pre}}`;
307
+ }
308
+ }
309
+ this.ctx.indent();
310
+ const resolvedCases = cases.map(({ tests, resolvedConsequent }) => {
311
+ const resolvedTests = tests
312
+ .map((test) => (test.value === 'default' ? 'default' : this.ctx.resolveSnippet(test).value))
313
+ .join(', ');
314
+ return `${this.ctx.pre}case ${resolvedTests}: {\n${resolvedConsequent}\n${this.ctx.pre}}`;
298
315
  });
299
316
  this.ctx.dedent();
300
- return stitch `${this.ctx.pre}switch ${discriminantExpr} {\n${cases.join('\n')}\n${this.ctx.pre}}`;
317
+ const body = resolvedCases.join('\n');
318
+ return stitch `${this.ctx.pre}switch ${discriminantExpr} {\n${body}\n${this.ctx.pre}}`;
301
319
  }
302
320
  _callShellless(callee, args) {
303
321
  const isGeneric = isGenericFn(callee);
@@ -335,13 +353,17 @@ export class WgslGenerator {
335
353
  // convert the result.
336
354
  return result;
337
355
  }
338
- return tryConvertSnippet(this.ctx, result, expectedType);
356
+ return setSourceNode(tryConvertSnippet(this.ctx, result, expectedType), expression);
339
357
  }
340
358
  finally {
341
359
  this.ctx.expectedType = prevExpectedType;
342
360
  }
343
361
  }
344
362
  _expression(expression) {
363
+ // Remembering which JS expression produced the snippet, for more descriptive errors
364
+ return setSourceNode(this._expressionImpl(expression), expression);
365
+ }
366
+ _expressionImpl(expression) {
345
367
  if (isId(expression)) {
346
368
  return this._identifier(extractId(expression));
347
369
  }
@@ -352,21 +374,23 @@ export class WgslGenerator {
352
374
  const [_, lhs, op, rhs] = expression;
353
375
  const lhsExpr = this._expression(lhs);
354
376
  // Short Circuit Evaluation
355
- if (isKnownAtComptime(lhsExpr)) {
377
+ const knownLhs = asComptime(lhsExpr);
378
+ if (knownLhs) {
356
379
  const castToBool = wgsl.isBool(this.ctx.expectedType);
357
- const evalRhs = op === '&&' ? lhsExpr.value : !lhsExpr.value;
380
+ const evalRhs = op === '&&' ? knownLhs.value : !knownLhs.value;
358
381
  if (!evalRhs) {
359
382
  return castToBool
360
383
  ? snip(op === '||', bool, 'constant', false)
361
- : coerceToSnippet(lhsExpr.value);
384
+ : coerceToSnippet(knownLhs.value);
362
385
  }
363
386
  const rhsExpr = this._expression(rhs);
364
- if (isKnownAtComptime(rhsExpr)) {
365
- const rhsSnippet = coerceToSnippet(rhsExpr.value);
387
+ const knownRhs = asComptime(rhsExpr);
388
+ if (knownRhs) {
389
+ const rhsSnippet = coerceToSnippet(knownRhs.value);
366
390
  return castToBool ? tryConvertSnippet(this.ctx, rhsSnippet, bool, false) : rhsSnippet;
367
391
  }
368
392
  if (rhsExpr.dataType === UnknownData) {
369
- throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`);
393
+ throw new WgslTypeError(`Right-hand side '${stringifySnippet(rhsExpr)}' of '${op}' is of unknown type.`);
370
394
  }
371
395
  // we can skip lhs
372
396
  return castToBool ? tryConvertSnippet(this.ctx, rhsExpr, bool, false) : rhsExpr;
@@ -374,13 +398,13 @@ export class WgslGenerator {
374
398
  const rhsExpr = this._expression(rhs);
375
399
  // they are not known at comptime
376
400
  if (lhsExpr.dataType === UnknownData) {
377
- throw new WgslTypeError(`Left-hand side of '${op}' is of unknown type`);
401
+ throw new WgslTypeError(`Left-hand side '${stringifySnippet(lhsExpr)}' of '${op}' is of unknown type.`);
378
402
  }
379
- if (!isKnownAtComptime(rhsExpr) && rhsExpr.dataType === UnknownData) {
380
- throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`);
403
+ if (!asComptime(rhsExpr) && rhsExpr.dataType === UnknownData) {
404
+ throw new WgslTypeError(`Right-hand side '${stringifySnippet(rhsExpr)}' of '${op}' is of unknown type.`);
381
405
  }
382
406
  if (!wgsl.isBool(lhsExpr.dataType) || !wgsl.isBool(rhsExpr.dataType)) {
383
- throw new WgslTypeError(`Logical expression '${op}' requires boolean operands. Got '${String(lhsExpr.dataType)}' and '${String(rhsExpr.dataType)}'.`);
407
+ throw new WgslTypeError(`Logical expression '${stringifyNode(expression)}' requires boolean operands. Got '${String(lhsExpr.dataType)}' and '${String(rhsExpr.dataType)}'.`);
384
408
  }
385
409
  const lhsStr = this.ctx.resolveSnippet(lhsExpr).value;
386
410
  const rhsStr = this.ctx.resolveSnippet(rhsExpr).value;
@@ -393,7 +417,7 @@ export class WgslGenerator {
393
417
  const lhsExpr = this._expression(lhs);
394
418
  const rhsExpr = this._expression(rhs);
395
419
  if (rhsExpr.value instanceof RefOperator) {
396
- throw new WgslTypeError(stitch `Cannot assign a ref to an existing variable '${stringifyNode(lhs)}', define a new variable instead.`);
420
+ throw new WgslTypeError(`Cannot assign a ref to an existing variable '${stringifyNode(lhs)}', define a new variable instead.`);
397
421
  }
398
422
  if (op === '==') {
399
423
  throw new Error('Please use the === operator instead of ==');
@@ -402,9 +426,11 @@ export class WgslGenerator {
402
426
  throw new Error('Please use the !== operator instead of !=');
403
427
  }
404
428
  const stdBinaryRelationalOp = binaryRelationalOpToStdMap[op];
405
- if (stdBinaryRelationalOp && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) {
406
- const left = lhsExpr.value;
407
- const right = rhsExpr.value;
429
+ const knownLhs = stdBinaryRelationalOp && asComptime(lhsExpr);
430
+ const knownRhs = knownLhs && asComptime(rhsExpr);
431
+ if (stdBinaryRelationalOp && knownLhs && knownRhs) {
432
+ const left = knownLhs.value;
433
+ const right = knownRhs.value;
408
434
  switch (op) {
409
435
  case '===':
410
436
  return snip(left === right, bool, 'constant', false);
@@ -413,7 +439,7 @@ export class WgslGenerator {
413
439
  }
414
440
  if (typeof left !== 'number' || typeof right !== 'number') {
415
441
  const bothVectors = wgsl.isVec(lhsExpr.dataType) && wgsl.isVec(rhsExpr.dataType);
416
- throw new WgslTypeError(`Comparison '${op}' requires numeric operands.${bothVectors
442
+ throw new WgslTypeError(`Comparison '${stringifyNode(expression)}' requires numeric operands.${bothVectors
417
443
  ? ` For component-wise comparison, use 'std.${stdBinaryRelationalOp}'.`
418
444
  : ''}`);
419
445
  }
@@ -429,10 +455,10 @@ export class WgslGenerator {
429
455
  }
430
456
  }
431
457
  if (lhsExpr.dataType === UnknownData) {
432
- throw new WgslTypeError(`Left-hand side of '${op}' is of unknown type`);
458
+ throw new WgslTypeError(`Left-hand side '${stringifySnippet(lhsExpr)}' of '${op}' is of unknown type.`);
433
459
  }
434
460
  if (rhsExpr.dataType === UnknownData) {
435
- throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`);
461
+ throw new WgslTypeError(`Right-hand side '${stringifySnippet(rhsExpr)}' of '${op}' is of unknown type.`);
436
462
  }
437
463
  const codegen = binaryOpCodeToCodegen[op];
438
464
  if (codegen) {
@@ -476,7 +502,9 @@ export class WgslGenerator {
476
502
  validateSnippetMutation(convLhs, expression);
477
503
  this.tryMarkModified(lhs);
478
504
  // Compound assignment operators are okay, e.g. +=, -=, *=, /=, ...
479
- if (op === '=' && isAlias(rhsExpr) && !wgsl.isNaturallyEphemeral(rhsExpr.dataType)) {
505
+ if (op === '=' &&
506
+ isStoredInMemory(rhsExpr) &&
507
+ !wgsl.isNaturallyEphemeral(rhsExpr.dataType)) {
480
508
  throw new WgslTypeError(`'${stringifyNode(expression)}' is invalid, because references cannot be assigned.\n-----\nTry '${stringifyNode(lhs)} = ${this.ctx.resolve(unptr(rhsExpr.dataType)).value}(${stringifyNode(rhs)})' to copy the value instead.\n-----`);
481
509
  }
482
510
  }
@@ -486,7 +514,7 @@ export class WgslGenerator {
486
514
  (equalityCheck && wgsl.isBool(convLhs.dataType) && wgsl.isBool(convRhs.dataType));
487
515
  if (!correctOperandTypes) {
488
516
  const bothVectors = wgsl.isVec(convLhs.dataType) && wgsl.isVec(convRhs.dataType);
489
- throw new WgslTypeError(`Comparison '${op}' requires numeric${equalityCheck ? ' or boolean' : ''} operands. Got '${String(convLhs.dataType)}' and '${String(convRhs.dataType)}'.${bothVectors
517
+ throw new WgslTypeError(`Comparison '${stringifyNode(expression)}' requires numeric${equalityCheck ? ' or boolean' : ''} operands. Got '${String(convLhs.dataType)}' and '${String(convRhs.dataType)}'.${bothVectors
490
518
  ? ` For component-wise comparison, use 'std.${stdBinaryRelationalOp}'.`
491
519
  : ''}`);
492
520
  }
@@ -660,7 +688,7 @@ export class WgslGenerator {
660
688
  if (maybeConsoleMethod) {
661
689
  throw new Error(`Unsupported console functionality 'console.${maybeConsoleMethod}()'.`);
662
690
  }
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`);
691
+ throw new Error(`Function '${getName(callee.value) ?? stringifySnippet(callee)}' is not marked with the 'use gpu' directive and cannot be used in a shader`);
664
692
  }
665
693
  if (expression[0] === NODE.objectExpr) {
666
694
  // Normalize to `objectProperty[]`
@@ -692,7 +720,7 @@ export class WgslGenerator {
692
720
  // Generating the expression and inferring the type instead
693
721
  expr = this._expression(value);
694
722
  if (expr.dataType === UnknownData) {
695
- throw new WgslTypeError(stitch `Property ${key} in object literal has a value of unknown type: '${expr}'`);
723
+ throw new WgslTypeError(`Value '${stringifySnippet(expr)}' of property '${key}' in object literal is of unknown type.`);
696
724
  }
697
725
  // Taking care of abstract numerics and implicit pointers
698
726
  accessed = structType.provideProp(key, unptr(concretize(expr.dataType)));
@@ -707,23 +735,43 @@ export class WgslGenerator {
707
735
  }
708
736
  if (wgsl.isWgslStruct(structType)) {
709
737
  const entries = {};
738
+ const keysWithSideEffectsInSourceOrder = [];
710
739
  for (const prop of properties) {
711
740
  const key = resolveUniqueKey(prop);
712
741
  const value = prop[1];
713
742
  const propType = structType.propTypes[key];
714
743
  if (propType === undefined) {
715
744
  // Evaluate every field even if it gets stripped by the struct schema
716
- void this._expression(value);
745
+ const expr = this._expression(value);
746
+ if (expr.possibleSideEffects) {
747
+ logger.warn('suspicious', `\
748
+ Object property '${stringifyObjectProperty(prop)}' in '${stringifyNode(expression)}' does not exist on type '${String(structType)}'.
749
+ The generated shader will omit it, so its runtime side effects will not occur.`);
750
+ }
717
751
  continue;
718
752
  }
719
753
  const expr = this._typedExpression(value, propType);
720
754
  entries[key] = expr;
755
+ if (expr.possibleSideEffects) {
756
+ keysWithSideEffectsInSourceOrder.push(key);
757
+ }
721
758
  }
722
759
  for (const key of Object.keys(structType.propTypes)) {
723
760
  if (entries[key] === undefined) {
724
761
  throw new WgslTypeError(`Missing property ${key} in object literal for struct ${structType}`);
725
762
  }
726
763
  }
764
+ const keysWithSideEffectsInSchemaOrder = Object.keys(structType.propTypes).filter((key) => entries[key].possibleSideEffects);
765
+ const changesSideEffectsOrder = keysWithSideEffectsInSourceOrder.some((key, index) => key !== keysWithSideEffectsInSchemaOrder[index]);
766
+ if (changesSideEffectsOrder) {
767
+ logger.warn('suspicious', `\
768
+ Properties with possible side effects in '${stringifyNode(expression)}' do not match '${String(structType)}' declaration order:
769
+
770
+ Source order: [${keysWithSideEffectsInSourceOrder.join(', ')}]
771
+ Declaration order: [${keysWithSideEffectsInSchemaOrder.join(', ')}]
772
+
773
+ The generated shader will evaluate them in declaration order.`);
774
+ }
727
775
  const convertedSnippets = convertStructValues(this.ctx, structType, entries);
728
776
  return snip(stitch `${this.ctx.resolve(structType).value}(${convertedSnippets})`, structType,
729
777
  /* origin */ 'runtime', convertedSnippets.some((s) => s.possibleSideEffects));
@@ -767,8 +815,11 @@ export class WgslGenerator {
767
815
  // ternary operator
768
816
  const [_, testNode, consequentNode, alternativeNode] = expression;
769
817
  const test = this._expression(testNode);
770
- if (isKnownAtComptime(test)) {
771
- return test.value ? this._expression(consequentNode) : this._expression(alternativeNode);
818
+ const knownTest = asComptime(test);
819
+ if (knownTest) {
820
+ return knownTest.value
821
+ ? this._expression(consequentNode)
822
+ : this._expression(alternativeNode);
772
823
  }
773
824
  else {
774
825
  const convertedTest = tryConvertSnippet(this.ctx, test, bool, false);
@@ -779,8 +830,8 @@ export class WgslGenerator {
779
830
  !alt ||
780
831
  consequent.possibleSideEffects ||
781
832
  alternative.possibleSideEffects ||
782
- (isAlias(consequent) && !wgsl.isNaturallyEphemeral(consequent.dataType)) ||
783
- (isAlias(alternative) && !wgsl.isNaturallyEphemeral(alternative.dataType))) {
833
+ (isStoredInMemory(consequent) && !wgsl.isNaturallyEphemeral(consequent.dataType)) ||
834
+ (isStoredInMemory(alternative) && !wgsl.isNaturallyEphemeral(alternative.dataType))) {
784
835
  throw new Error(`Ternary operator '${stringifyNode(expression)}' is invalid. For more complex branching, please use 'std.select' or if/else statements.`);
785
836
  }
786
837
  return snip(stitch `select(${alt}, ${con}, ${convertedTest})`, con.dataType, 'runtime',
@@ -958,7 +1009,7 @@ export class WgslGenerator {
958
1009
  // The existence of `expectedReturnType` implies a function shell, which in turn implies that the
959
1010
  // value will be copied on return anyway
960
1011
  !expectedReturnType &&
961
- isAlias(returnSnippet) &&
1012
+ isStoredInMemory(returnSnippet) &&
962
1013
  !wgsl.isNaturallyEphemeral(returnSnippet.dataType) &&
963
1014
  returnSnippet.origin !== 'local-def') {
964
1015
  const str = stringifyNode(returnNode);
@@ -996,7 +1047,7 @@ Try 'return ${typeStr}(${str});' instead.
996
1047
  const declaration = `let ${rawId}`;
997
1048
  throw new WgslTypeError(`'${declaration} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'${schemaWrappingSuggestion(declaration, rhsStr, eq.value)}`);
998
1049
  }
999
- if (isAlias(eq) && !wgsl.isNaturallyEphemeral(eq.dataType)) {
1050
+ if (isStoredInMemory(eq) && !wgsl.isNaturallyEphemeral(eq.dataType)) {
1000
1051
  // `let` declarations cannot store references
1001
1052
  const rhsStr = stringifyNode(eqNode);
1002
1053
  const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value;
@@ -1033,9 +1084,11 @@ Try 'return ${typeStr}(${str});' instead.
1033
1084
  const eq = this._expression(eqNode);
1034
1085
  if (eq.value instanceof RefOperator) {
1035
1086
  // We're assigning a newly created `d.ref()`
1036
- if (eq.dataType !== UnknownData) {
1037
- throw new WgslTypeError(`Cannot store d.ref() in a variable if it references another value. Copy the value passed into d.ref() instead.`);
1087
+ if (eq.value.addressable) {
1088
+ const refStr = stringifyNode(eqNode);
1089
+ throw new WgslTypeError(`'const ${rawId} = ${refStr}' is invalid, cannot store ${refStr} in a variable if it references another value. Copy the value passed into d.ref() instead, or inline ${refStr}.`);
1038
1090
  }
1091
+ // Unwrapping the ref, and storing the value it was created from in a new variable
1039
1092
  const refSnippet = eq.value.snippet;
1040
1093
  const varName = this.refVariable(rawId, concretize(refSnippet.dataType));
1041
1094
  return {
@@ -1077,7 +1130,7 @@ Try 'return ${typeStr}(${str});' instead.
1077
1130
  // function code has been processed, so at least currently, we lose that info.
1078
1131
  varOrigin = 'local-def';
1079
1132
  }
1080
- else if (!isAlias(eq)) {
1133
+ else if (!isStoredInMemory(eq)) {
1081
1134
  // Not a reference, but also not naturally ephemeral, so we cannot guarantee it won't be mutated.
1082
1135
  // We defer the decision for now.
1083
1136
  varType = '<deferred>';
@@ -1265,7 +1318,8 @@ ${this.ctx.pre}else ${alternate}`,
1265
1318
  const originalLoopVarName = extractId(loopVar[1]);
1266
1319
  const blockified = blockifySingleStatement(body);
1267
1320
  if (shouldUnroll) {
1268
- if (!isKnownAtComptime(range.end)) {
1321
+ const knownEnd = asComptime(range.end);
1322
+ if (!knownEnd) {
1269
1323
  throw new Error('Cannot unroll loop. Length of iterable is unknown at comptime.');
1270
1324
  }
1271
1325
  const { value } = iterableSnippet;
@@ -1273,12 +1327,13 @@ ${this.ctx.pre}else ${alternate}`,
1273
1327
  ? value.map((i) => coerceToSnippet(i))
1274
1328
  : value instanceof ArrayExpression
1275
1329
  ? value.elements
1276
- : Array.from({ length: range.end.value }, (_, i) => forOfUtils.getElementSnippet(iterableSnippet, snip(i, u32, 'constant')));
1330
+ : Array.from({ length: knownEnd.value }, (_, i) => forOfUtils.getElementSnippet(iterableSnippet, snip(i, u32, 'constant')));
1277
1331
  if (elements.length === 0) {
1278
1332
  return { code: '', definesInNearestScope: false };
1279
1333
  }
1280
1334
  const firstElement = elements[0];
1281
- if (!isAlias(firstElement) && !wgsl.isNaturallyEphemeral(firstElement.dataType)) {
1335
+ if (!isStoredInMemory(firstElement) &&
1336
+ !wgsl.isNaturallyEphemeral(firstElement.dataType)) {
1282
1337
  throw new WgslTypeError(`Cannot unroll '${stringifyNode(iterable)}'. The elements of iterable are constructed in place but are not value types.`);
1283
1338
  }
1284
1339
  let blocksCode = '';
@@ -1343,11 +1398,50 @@ ${this.ctx.pre}else ${alternate}`,
1343
1398
  if (statement[0] === NODE.switch) {
1344
1399
  // Switch statement
1345
1400
  const [_, discriminant, cases] = statement;
1346
- const discriminantExpr = this._typedExpression(discriminant, [i32, u32]);
1401
+ let discriminantExpr = this._expression(discriminant);
1402
+ let caseExprs = cases.map(([test, consequent]) => {
1403
+ const testExpr = test === null ? switchDefault : this._expression(test);
1404
+ return [testExpr, consequent];
1405
+ });
1406
+ // comptime folding
1407
+ let matchedCaseWasNotLast = false;
1408
+ const knownDiscriminant = asComptime(discriminantExpr);
1409
+ const knownTests = knownDiscriminant && caseExprs.map(([test]) => asComptime(test));
1410
+ if (knownDiscriminant && knownTests?.every((test) => test !== undefined)) {
1411
+ let matchedCaseIndex = knownTests.findIndex((test) => test !== switchDefault && test?.value === knownDiscriminant.value);
1412
+ if (matchedCaseIndex === -1) {
1413
+ matchedCaseIndex = caseExprs.findIndex(([test]) => test === switchDefault);
1414
+ }
1415
+ if (matchedCaseIndex === -1) {
1416
+ return { code: '', definesInNearestScope: false };
1417
+ }
1418
+ const matchedConsequent = caseExprs
1419
+ .slice(matchedCaseIndex)
1420
+ .find(([, consequent]) => consequent.length !== 0)?.[1];
1421
+ if (matchedConsequent === undefined) {
1422
+ return { code: '', definesInNearestScope: false };
1423
+ }
1424
+ if (matchedConsequent !== caseExprs.at(-1)?.[1]) {
1425
+ // The JS behavior will be different if this case does not end with control flow.
1426
+ matchedCaseWasNotLast = true;
1427
+ }
1428
+ discriminantExpr = snip(0, u32, 'constant', false);
1429
+ caseExprs = [[switchDefault, matchedConsequent]];
1430
+ }
1431
+ // type concretization
1432
+ discriminantExpr = tryConvertSnippet(this.ctx, discriminantExpr, [i32, u32]);
1347
1433
  const switchType = discriminantExpr.dataType;
1348
1434
  invariant(switchType !== UnknownData);
1349
- const caseExprs = cases.map(([test, consequent]) => {
1350
- const testExpr = test === null ? switchDefault : this._typedExpression(test, [switchType]);
1435
+ caseExprs = caseExprs.map(([test, consequent]) => {
1436
+ // TODO(#3148): Remove this throw.
1437
+ if (test !== switchDefault && test.dataType === UnknownData) {
1438
+ throw new Error(`Failed to convert one of the switch tests to '${switchType.type}'`);
1439
+ }
1440
+ const convertedTest = test === switchDefault ? test : tryConvertSnippet(this.ctx, test, [switchType]);
1441
+ return [convertedTest, consequent];
1442
+ });
1443
+ // consequent resolution
1444
+ const resolvedCaseExprs = caseExprs.map(([test, consequent]) => {
1351
1445
  // In WGSL, each case is a different block. This block scope forbids scope leaking.
1352
1446
  // TODO(#3001): Consider using NODE.block here
1353
1447
  this.ctx.pushBlockScope();
@@ -1355,7 +1449,7 @@ ${this.ctx.pre}else ${alternate}`,
1355
1449
  this.ctx.indent();
1356
1450
  try {
1357
1451
  const consequentStmts = consequent.map((s) => this._statement(s));
1358
- return [testExpr, consequentStmts];
1452
+ return [test, consequentStmts];
1359
1453
  }
1360
1454
  finally {
1361
1455
  this.ctx.dedent();
@@ -1363,6 +1457,15 @@ ${this.ctx.pre}else ${alternate}`,
1363
1457
  this.ctx.popBlockScope();
1364
1458
  }
1365
1459
  });
1460
+ const groupedCaseExprs = [];
1461
+ let currentGroup = [];
1462
+ for (const [index, [test, consequent]] of resolvedCaseExprs.entries()) {
1463
+ currentGroup.push(test);
1464
+ if (consequent.length > 0 || index === caseExprs.length - 1) {
1465
+ groupedCaseExprs.push([currentGroup, consequent]);
1466
+ currentGroup = [];
1467
+ }
1468
+ }
1366
1469
  // Validation
1367
1470
  {
1368
1471
  // Tests should be constant
@@ -1380,7 +1483,9 @@ ${stringifyNode(statement)}`);
1380
1483
  // We skip this check, because WGSL errors are readable,
1381
1484
  // and we cannot easily access non-comptime known constants.
1382
1485
  // Tests should not have non-trivial fallthrough
1383
- caseExprs.slice(0, -1).forEach(([_, consequent]) => {
1486
+ resolvedCaseExprs
1487
+ .slice(0, matchedCaseWasNotLast ? undefined : -1)
1488
+ .forEach(([_, consequent]) => {
1384
1489
  const last = consequent.at(-1);
1385
1490
  if (last && !last.endsWithControlFlow) {
1386
1491
  throw new Error(`Switch statement cannot have non-trivial fallthrough.
@@ -1389,15 +1494,6 @@ ${stringifyNode(statement)}`);
1389
1494
  }
1390
1495
  });
1391
1496
  }
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
1497
  return {
1402
1498
  code: this._emitSwitchStatement(discriminantExpr, groupedCaseExprs),
1403
1499
  definesInNearestScope: false,
@@ -1455,8 +1551,8 @@ ${stringifyNode(statement)}`);
1455
1551
  if (!computed) {
1456
1552
  return property[0];
1457
1553
  }
1458
- const key = this._expression(property[0]);
1459
- if (!isKnownAtComptime(key)) {
1554
+ const key = asComptime(this._expression(property[0]));
1555
+ if (!key) {
1460
1556
  throw new WgslTypeError(`Computed object property key '${stringifyObjectProperty(property)}' must be known at comptime.`);
1461
1557
  }
1462
1558
  if (typeof key.value !== 'string') {
@@ -1490,27 +1586,25 @@ ${stringifyNode(statement)}`);
1490
1586
  }
1491
1587
  }
1492
1588
  }
1589
+ const immutabilityReasons = {
1590
+ constant: 'the left side is a constant',
1591
+ 'constant-immutable-def': 'the left side is a constant',
1592
+ 'runtime-immutable-def': 'the left side is immutable',
1593
+ uniform: 'uniform buffers cannot be mutated',
1594
+ readonly: 'readonly buffers cannot be mutated',
1595
+ immediate: 'immediate variables cannot be mutated',
1596
+ argument: 'non-pointer arguments cannot be mutated',
1597
+ handle: 'textures and samplers cannot be mutated',
1598
+ };
1493
1599
  function validateSnippetMutation(mutated, expr) {
1494
- if (mutated.origin === 'constant' ||
1495
- mutated.origin === 'constant-immutable-def' ||
1496
- mutated.origin === 'runtime-immutable-def') {
1497
- if (isKnownAtComptime(mutated)) {
1498
- throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because the left side is defined outside of the shader, and therefore is immutable during its execution. Try using tgpu.privateVar or buffers.`);
1499
- }
1500
- throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because the left side is a constant.`);
1501
- }
1502
- if (mutated.origin === 'uniform') {
1503
- throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because uniform buffers cannot be mutated.`);
1504
- }
1505
- if (mutated.origin === 'readonly') {
1506
- throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because readonly buffers cannot be mutated.`);
1507
- }
1508
- if (mutated.origin === 'immediate') {
1509
- throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because immediate variables cannot be mutated.`);
1510
- }
1511
- if (mutated.origin === 'argument') {
1512
- throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because non-pointer arguments cannot be mutated.`);
1600
+ if (isMutableOrigin(mutated.origin)) {
1601
+ return;
1513
1602
  }
1603
+ const reason = asComptime(mutated)
1604
+ ? 'the left side is immutable during shader execution. Try using tgpu.privateVar or buffers'
1605
+ : (immutabilityReasons[mutated.origin] ??
1606
+ 'the left side is not a reference to an existing value');
1607
+ throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because ${reason}.`);
1514
1608
  }
1515
1609
  function assertExhaustive(value) {
1516
1610
  throw new Error(`'${safeStringify(value)}' was not handled by the WGSL generator.`);
package/types.d.ts CHANGED
@@ -18,7 +18,7 @@ import type { TgpuVar } from './core/variable/tgpuVariable.ts';
18
18
  import { type AnyData, UnknownData } from './data/dataTypes.ts';
19
19
  import type { MapValueToSnippet, ResolvedSnippet, Snippet } from './data/snippet.ts';
20
20
  import { type AnyMatInstance, type AnyVecInstance, type BaseData } from './data/wgslTypes.ts';
21
- import { $cast, $gpuCallable, $gpuValueOf, $internal, $ownSnippet, $resolve } from './shared/symbols.ts';
21
+ import { $cast, $comptimeValueOf, $gpuCallable, $gpuValueOf, $internal, $ownSnippet, $resolve } from './shared/symbols.ts';
22
22
  import type { TgpuBindGroupLayout, TgpuLayoutEntry } from './tgpuBindGroupLayout.ts';
23
23
  import type { WgslEnableExtension } from './wgslExtensions.ts';
24
24
  import type { Infer } from './shared/repr.ts';
@@ -342,6 +342,23 @@ export interface WithOwnSnippet {
342
342
  readonly [$ownSnippet]: Snippet;
343
343
  }
344
344
  export declare function getOwnSnippet(value: unknown): Snippet | undefined;
345
+ export type ComptimeValue = {
346
+ comptime: false;
347
+ } | {
348
+ comptime: true;
349
+ value: unknown;
350
+ };
351
+ export interface WithComptimeValue {
352
+ /**
353
+ * @returns The value that this object represents at comptime,
354
+ * or `{ comptime: false }` if it is only known at runtime.
355
+ */
356
+ [$comptimeValueOf](): ComptimeValue;
357
+ }
358
+ /**
359
+ * A `[$comptimeValueOf]` implementation for resources that are only known at runtime
360
+ */
361
+ export declare function runtimeOnly(): ComptimeValue;
345
362
  export interface GPUCallable<TArgs extends unknown[] = unknown[]> {
346
363
  [$gpuCallable]: {
347
364
  strictSignature?: {
@@ -358,7 +375,11 @@ export type WithCast<T = BaseData> = GPUCallable<[v?: Infer<T>]> & {
358
375
  export declare function hasCast(value: unknown): value is WithCast;
359
376
  type AnyFn = (...args: never[]) => unknown;
360
377
  export type DualFn<T extends AnyFn> = T & GPUCallable<Parameters<T>>;
361
- export declare function isKnownAtComptime(snippet: Snippet): boolean;
378
+ /**
379
+ * @returns A snippet holding the JS value that `snippet` represents at comptime,
380
+ * or `undefined` if it is only known at runtime.
381
+ */
382
+ export declare function asComptime(snippet: Snippet): Snippet | undefined;
362
383
  export declare function isConstant(snippet: Snippet): boolean;
363
384
  export declare function isWgsl(value: unknown): value is Wgsl;
364
385
  export type BindableBufferUsage = 'uniform' | 'readonly' | 'mutable';
package/types.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { isLazy, isProviding, isSlot, } from "./core/slot/slotTypes.js";
2
2
  import { UnknownData } from "./data/dataTypes.js";
3
+ import { withValue } from "./data/snippet.js";
3
4
  import { isWgslData, } from "./data/wgslTypes.js";
4
- import { $cast, $gpuCallable, $gpuValueOf, $internal, $ownSnippet, $resolve, } from "./shared/symbols.js";
5
+ import { $cast, $comptimeValueOf, $gpuCallable, $gpuValueOf, $internal, $ownSnippet, $resolve, } from "./shared/symbols.js";
5
6
  export class NormalState {
6
7
  type = 'normal';
7
8
  }
@@ -23,18 +24,42 @@ export function isSelfResolvable(value) {
23
24
  export function getOwnSnippet(value) {
24
25
  return value?.[$ownSnippet];
25
26
  }
27
+ /**
28
+ * A `[$comptimeValueOf]` implementation for resources that are only known at runtime
29
+ */
30
+ export function runtimeOnly() {
31
+ return { comptime: false };
32
+ }
26
33
  export function isGPUCallable(value) {
27
34
  return !!value?.[$gpuCallable];
28
35
  }
29
36
  export function hasCast(value) {
30
37
  return !!value?.[$cast];
31
38
  }
32
- export function isKnownAtComptime(snippet) {
33
- return ((typeof snippet.value !== 'string' || snippet.dataType === UnknownData) &&
34
- getOwnSnippet(snippet.value) === undefined);
39
+ /**
40
+ * @returns A snippet holding the JS value that `snippet` represents at comptime,
41
+ * or `undefined` if it is only known at runtime.
42
+ */
43
+ export function asComptime(snippet) {
44
+ const { value } = snippet;
45
+ if (typeof value === 'string' && snippet.dataType !== UnknownData) {
46
+ // Resolved WGSL code
47
+ return undefined;
48
+ }
49
+ const getter = value?.[$comptimeValueOf];
50
+ if (!getter) {
51
+ // Objects that meet the criteria and didn't explicitly opt out of
52
+ // being known at comptime, are treated as known at comptime.
53
+ // That's mainly because there are far more objects that are eligible
54
+ // and we want to be treated as known at comptime than those that are
55
+ // special cases and need to opt out.
56
+ return snippet;
57
+ }
58
+ const result = getter.call(value);
59
+ return result.comptime ? withValue(result.value, snippet) : undefined;
35
60
  }
36
61
  export function isConstant(snippet) {
37
- return (isKnownAtComptime(snippet) ||
62
+ return (asComptime(snippet) !== undefined ||
38
63
  snippet.origin === 'constant' ||
39
64
  snippet.origin === 'constant-immutable-def');
40
65
  }