typegpu 0.12.5 → 0.12.7

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 (99) hide show
  1. package/common/index.d.ts +1 -0
  2. package/common/index.js +1 -0
  3. package/common/writeChannels.d.ts +31 -0
  4. package/common/writeChannels.js +32 -0
  5. package/common/writeSoA.js +16 -0
  6. package/core/buffer/buffer.js +47 -11
  7. package/core/commandEncoder/attachments.js +5 -2
  8. package/core/commandEncoder/computePass.d.ts +13 -0
  9. package/core/commandEncoder/computePass.js +5 -2
  10. package/core/commandEncoder/renderPass.d.ts +18 -4
  11. package/core/commandEncoder/renderPass.js +6 -2
  12. package/core/declare/tgpuDeclare.d.ts +11 -0
  13. package/core/declare/tgpuDeclare.js +1 -1
  14. package/core/function/autoIO.js +4 -2
  15. package/core/function/ioSchema.d.ts +7 -2
  16. package/core/function/ioSchema.js +41 -15
  17. package/core/function/tgpuFragmentFn.d.ts +1 -1
  18. package/core/function/tgpuFragmentFn.js +2 -1
  19. package/core/function/tgpuVertexFn.js +2 -1
  20. package/core/immediate/immediateVar.d.ts +52 -0
  21. package/core/immediate/immediateVar.js +149 -0
  22. package/core/pipeline/computePipeline.d.ts +17 -0
  23. package/core/pipeline/computePipeline.js +23 -9
  24. package/core/pipeline/connectAttachmentToShader.d.ts +2 -2
  25. package/core/pipeline/connectAttachmentToShader.js +16 -21
  26. package/core/pipeline/connectTargetsToShader.d.ts +5 -1
  27. package/core/pipeline/connectTargetsToShader.js +23 -18
  28. package/core/pipeline/drawState.d.ts +8 -4
  29. package/core/pipeline/drawState.js +41 -51
  30. package/core/pipeline/priors.js +1 -0
  31. package/core/pipeline/renderPipeline.d.ts +22 -4
  32. package/core/pipeline/renderPipeline.js +25 -13
  33. package/core/resolve/namespace.js +2 -2
  34. package/core/root/init.js +3 -0
  35. package/core/root/rootTypes.d.ts +17 -2
  36. package/core/slot/accessor.d.ts +1 -1
  37. package/core/slot/accessor.js +72 -5
  38. package/core/slot/slotTypes.d.ts +2 -1
  39. package/core/texture/texture.d.ts +20 -14
  40. package/core/texture/texture.js +78 -69
  41. package/core/texture/textureUtils.d.ts +3 -6
  42. package/core/texture/textureUtils.js +142 -165
  43. package/core/texture/textureWrite.d.ts +61 -0
  44. package/core/texture/textureWrite.js +59 -0
  45. package/data/autoStruct.d.ts +1 -1
  46. package/data/autoStruct.js +8 -4
  47. package/data/dataIO.d.ts +2 -2
  48. package/data/deepEqual.js +19 -9
  49. package/data/matrix.js +10 -3
  50. package/data/partialIO.d.ts +8 -1
  51. package/data/partialIO.js +36 -19
  52. package/data/ref.js +2 -2
  53. package/data/snippet.d.ts +11 -5
  54. package/data/snippet.js +9 -4
  55. package/data/wgslTypes.d.ts +4 -1
  56. package/errors.d.ts +7 -0
  57. package/errors.js +15 -0
  58. package/indexNamedExports.d.ts +5 -2
  59. package/indexNamedExports.js +2 -1
  60. package/internal.d.ts +2 -1
  61. package/internal.js +1 -1
  62. package/nameUtils.d.ts +7 -0
  63. package/nameUtils.js +60 -0
  64. package/package.json +2 -2
  65. package/resolutionCtx.d.ts +5 -0
  66. package/resolutionCtx.js +11 -0
  67. package/serial/restore.d.ts +3 -1
  68. package/serial/restore.js +2 -0
  69. package/shared/meta.js +1 -1
  70. package/shared/symbols.js +1 -1
  71. package/shared/tseynit.d.ts +1 -0
  72. package/shared/tseynit.js +80 -44
  73. package/std/atomic.d.ts +13 -0
  74. package/std/atomic.js +63 -29
  75. package/std/boolean.js +9 -10
  76. package/std/derivative.js +14 -9
  77. package/std/index.d.ts +4 -4
  78. package/std/index.js +5 -5
  79. package/std/numeric.d.ts +5 -1
  80. package/std/numeric.js +3 -3
  81. package/std/operators.js +9 -4
  82. package/std/packing.d.ts +61 -1
  83. package/std/packing.js +214 -25
  84. package/std/subgroup.d.ts +4 -0
  85. package/std/subgroup.js +34 -0
  86. package/std/texture.d.ts +15 -2
  87. package/std/texture.js +48 -0
  88. package/tgpuUnstable.d.ts +1 -0
  89. package/tgpuUnstable.js +1 -0
  90. package/tgsl/accessIndex.js +19 -23
  91. package/tgsl/conversion.d.ts +2 -0
  92. package/tgsl/conversion.js +3 -0
  93. package/tgsl/forOfUtils.js +6 -6
  94. package/tgsl/generationHelpers.js +2 -2
  95. package/tgsl/shaderGenerator.d.ts +1 -1
  96. package/tgsl/wgslGenerator.d.ts +5 -0
  97. package/tgsl/wgslGenerator.js +288 -58
  98. package/types.d.ts +9 -1
  99. package/types.js +5 -0
@@ -4,16 +4,16 @@ 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, isStoredInMemory, 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;
@@ -442,7 +476,9 @@ export class WgslGenerator {
442
476
  validateSnippetMutation(convLhs, expression);
443
477
  this.tryMarkModified(lhs);
444
478
  // Compound assignment operators are okay, e.g. +=, -=, *=, /=, ...
445
- if (op === '=' && isAlias(rhsExpr) && !wgsl.isNaturallyEphemeral(rhsExpr.dataType)) {
479
+ if (op === '=' &&
480
+ isStoredInMemory(rhsExpr) &&
481
+ !wgsl.isNaturallyEphemeral(rhsExpr.dataType)) {
446
482
  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-----`);
447
483
  }
448
484
  }
@@ -481,8 +517,9 @@ export class WgslGenerator {
481
517
  }
482
518
  if (expression[0] === NODE.memberAccess) {
483
519
  // Member Access
484
- const [_, targetNode, property] = expression;
520
+ const [_, targetNode, propertyNode] = expression;
485
521
  const target = this._expression(targetNode);
522
+ const property = extractId(propertyNode);
486
523
  const accessed = accessProp(target, property);
487
524
  if (!accessed) {
488
525
  throw new Error(`Property '${property}' not found on '${stringifyNode(targetNode)}'`);
@@ -560,7 +597,7 @@ export class WgslGenerator {
560
597
  // We just let the argument resolve everything.
561
598
  return snip(this.ctx.resolveSnippet(arg).value, callee.value,
562
599
  // A new array, so not a reference.
563
- /* origin */ 'runtime', arg.possibleSideEffects);
600
+ /* origin */ fallthroughCopyOrigin(arg.origin), arg.possibleSideEffects);
564
601
  }
565
602
  if (callee.value === constant) {
566
603
  throw new Error('Constants cannot be defined within TypeGPU function scope. To address this, move the constant definition outside the function scope.');
@@ -628,11 +665,25 @@ export class WgslGenerator {
628
665
  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
666
  }
630
667
  if (expression[0] === NODE.objectExpr) {
631
- // Object Literal
632
- const obj = expression[1];
668
+ // Normalize to `objectProperty[]`
669
+ const properties = Array.isArray(expression[1])
670
+ ? expression[1]
671
+ : Object.entries(expression[1]).map(([key, value]) => [key, value, false]);
672
+ const seenKeys = new Map();
673
+ const resolveUniqueKey = (prop) => {
674
+ const key = this._resolveObjectPropertyKey(prop);
675
+ const dupProp = seenKeys.get(key);
676
+ if (dupProp) {
677
+ throw new WgslTypeError(`Duplicate object property key found: '${stringifyObjectProperty(dupProp)}' and '${stringifyObjectProperty(prop)}'.`);
678
+ }
679
+ seenKeys.set(key, prop);
680
+ return key;
681
+ };
633
682
  const structType = this.ctx.expectedType;
634
683
  if (structType instanceof AutoStruct) {
635
- const entries = Object.fromEntries(Object.entries(obj).map(([key, value]) => {
684
+ const keySnippetPairs = properties.map((prop) => {
685
+ const key = resolveUniqueKey(prop);
686
+ const value = prop[1];
636
687
  let accessed = structType.accessProp(key);
637
688
  let expr;
638
689
  if (accessed) {
@@ -649,21 +700,52 @@ export class WgslGenerator {
649
700
  accessed = structType.provideProp(key, unptr(concretize(expr.dataType)));
650
701
  }
651
702
  return [accessed.prop, expr];
652
- }));
703
+ });
704
+ const entries = Object.fromEntries(keySnippetPairs);
653
705
  const completeStruct = structType.completeStruct;
654
706
  const convertedSnippets = convertStructValues(this.ctx, completeStruct, entries);
655
707
  return snip(stitch `${this.ctx.resolve(structType).value}(${convertedSnippets})`, completeStruct,
656
708
  /* origin */ 'runtime');
657
709
  }
658
710
  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) {
711
+ const entries = {};
712
+ const keysWithSideEffectsInSourceOrder = [];
713
+ for (const prop of properties) {
714
+ const key = resolveUniqueKey(prop);
715
+ const value = prop[1];
716
+ const propType = structType.propTypes[key];
717
+ if (propType === undefined) {
718
+ // Evaluate every field even if it gets stripped by the struct schema
719
+ const expr = this._expression(value);
720
+ if (expr.possibleSideEffects) {
721
+ logger.warn('suspicious', `\
722
+ Object property '${stringifyObjectProperty(prop)}' in '${stringifyNode(expression)}' does not exist on type '${String(structType)}'.
723
+ The generated shader will omit it, so its runtime side effects will not occur.`);
724
+ }
725
+ continue;
726
+ }
727
+ const expr = this._typedExpression(value, propType);
728
+ entries[key] = expr;
729
+ if (expr.possibleSideEffects) {
730
+ keysWithSideEffectsInSourceOrder.push(key);
731
+ }
732
+ }
733
+ for (const key of Object.keys(structType.propTypes)) {
734
+ if (entries[key] === undefined) {
662
735
  throw new WgslTypeError(`Missing property ${key} in object literal for struct ${structType}`);
663
736
  }
664
- const result = this._typedExpression(val, value);
665
- return [key, result];
666
- }));
737
+ }
738
+ const keysWithSideEffectsInSchemaOrder = Object.keys(structType.propTypes).filter((key) => entries[key].possibleSideEffects);
739
+ const changesSideEffectsOrder = keysWithSideEffectsInSourceOrder.some((key, index) => key !== keysWithSideEffectsInSchemaOrder[index]);
740
+ if (changesSideEffectsOrder) {
741
+ logger.warn('suspicious', `\
742
+ Properties with possible side effects in '${stringifyNode(expression)}' do not match '${String(structType)}' declaration order:
743
+
744
+ Source order: [${keysWithSideEffectsInSourceOrder.join(', ')}]
745
+ Declaration order: [${keysWithSideEffectsInSchemaOrder.join(', ')}]
746
+
747
+ The generated shader will evaluate them in declaration order.`);
748
+ }
667
749
  const convertedSnippets = convertStructValues(this.ctx, structType, entries);
668
750
  return snip(stitch `${this.ctx.resolve(structType).value}(${convertedSnippets})`, structType,
669
751
  /* origin */ 'runtime', convertedSnippets.some((s) => s.possibleSideEffects));
@@ -719,8 +801,8 @@ export class WgslGenerator {
719
801
  !alt ||
720
802
  consequent.possibleSideEffects ||
721
803
  alternative.possibleSideEffects ||
722
- (isAlias(consequent) && !wgsl.isNaturallyEphemeral(consequent.dataType)) ||
723
- (isAlias(alternative) && !wgsl.isNaturallyEphemeral(alternative.dataType))) {
804
+ (isStoredInMemory(consequent) && !wgsl.isNaturallyEphemeral(consequent.dataType)) ||
805
+ (isStoredInMemory(alternative) && !wgsl.isNaturallyEphemeral(alternative.dataType))) {
724
806
  throw new Error(`Ternary operator '${stringifyNode(expression)}' is invalid. For more complex branching, please use 'std.select' or if/else statements.`);
725
807
  }
726
808
  return snip(stitch `select(${alt}, ${con}, ${convertedTest})`, con.dataType, 'runtime',
@@ -818,8 +900,7 @@ export class WgslGenerator {
818
900
  // We can make this snippet ephemeral, as we know it will be deep copied in JS
819
901
  return snip(stitch `${args[0]}`, schema, fallthroughCopyOrigin(args[0].origin), args[0].possibleSideEffects);
820
902
  }
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));
903
+ return snip(stitch `${this.ctx.resolve(schema).value}(${args})`, schema, args.every((arg) => arg.origin === 'constant') ? 'constant' : 'runtime', args.some((s) => s.possibleSideEffects));
823
904
  }
824
905
  numericLiteral(value, schema) {
825
906
  if (!Number.isFinite(value)) {
@@ -899,7 +980,7 @@ export class WgslGenerator {
899
980
  // The existence of `expectedReturnType` implies a function shell, which in turn implies that the
900
981
  // value will be copied on return anyway
901
982
  !expectedReturnType &&
902
- isAlias(returnSnippet) &&
983
+ isStoredInMemory(returnSnippet) &&
903
984
  !wgsl.isNaturallyEphemeral(returnSnippet.dataType) &&
904
985
  returnSnippet.origin !== 'local-def') {
905
986
  const str = stringifyNode(returnNode);
@@ -918,7 +999,8 @@ Try 'return ${typeStr}(${str});' instead.
918
999
  return `${this.ctx.pre}return;`;
919
1000
  }
920
1001
  _letStatement(statement) {
921
- const [_, rawId, eqNode] = statement;
1002
+ const [_, rawIdNode, eqNode] = statement;
1003
+ const rawId = extractId(rawIdNode);
922
1004
  if (eqNode === undefined) {
923
1005
  throw new Error(`'${stringifyNode(statement)}' is invalid because all variables need initializers.`);
924
1006
  }
@@ -931,14 +1013,12 @@ Try 'return ${typeStr}(${str});' instead.
931
1013
  -----`);
932
1014
  }
933
1015
  const definitionDataType = eq.dataType;
934
- if (definitionDataType === UnknownData) {
1016
+ if (definitionDataType === UnknownData || wgsl.isVoid(definitionDataType)) {
935
1017
  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
- -----`);
1018
+ const declaration = `let ${rawId}`;
1019
+ throw new WgslTypeError(`'${declaration} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'${schemaWrappingSuggestion(declaration, rhsStr, eq.value)}`);
940
1020
  }
941
- if (isAlias(eq) && !wgsl.isNaturallyEphemeral(eq.dataType)) {
1021
+ if (isStoredInMemory(eq) && !wgsl.isNaturallyEphemeral(eq.dataType)) {
942
1022
  // `let` declarations cannot store references
943
1023
  const rhsStr = stringifyNode(eqNode);
944
1024
  const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value;
@@ -967,7 +1047,8 @@ Try 'return ${typeStr}(${str});' instead.
967
1047
  };
968
1048
  }
969
1049
  _constStatement(statement) {
970
- const [_, rawId, eqNode] = statement;
1050
+ const [_, rawIdNode, eqNode] = statement;
1051
+ const rawId = extractId(rawIdNode);
971
1052
  if (eqNode === undefined) {
972
1053
  throw new Error(`'${stringifyNode(statement)}' is invalid because all variables need initializers.`);
973
1054
  }
@@ -988,12 +1069,10 @@ Try 'return ${typeStr}(${str});' instead.
988
1069
  let varOrigin = 'local-def';
989
1070
  let varType = '<deferred>';
990
1071
  let definitionDataType = eq.dataType;
991
- if (definitionDataType === UnknownData) {
1072
+ if (definitionDataType === UnknownData || wgsl.isVoid(definitionDataType)) {
992
1073
  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
- -----`);
1074
+ const declaration = `const ${rawId}`;
1075
+ throw new WgslTypeError(`'${declaration} = ${rhsStr}' is invalid, cannot determine WGSL type of '${rhsStr}'${schemaWrappingSuggestion(declaration, rhsStr, eq.value)}`);
997
1076
  }
998
1077
  if (eq.origin === 'argument') {
999
1078
  // Arguments are immutable, so we 'let' them be (kill me)
@@ -1020,7 +1099,7 @@ Try 'return ${typeStr}(${str});' instead.
1020
1099
  // function code has been processed, so at least currently, we lose that info.
1021
1100
  varOrigin = 'local-def';
1022
1101
  }
1023
- else if (!isAlias(eq)) {
1102
+ else if (!isStoredInMemory(eq)) {
1024
1103
  // Not a reference, but also not naturally ephemeral, so we cannot guarantee it won't be mutated.
1025
1104
  // We defer the decision for now.
1026
1105
  varType = '<deferred>';
@@ -1083,16 +1162,16 @@ Try 'return ${typeStr}(${str});' instead.
1083
1162
  };
1084
1163
  }
1085
1164
  _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 };
1165
+ // TODO(#3078): Remove this check.
1166
+ if (isId(statement)) {
1167
+ const item = this.ctx.getById(extractId(statement));
1168
+ if (item?.value instanceof TgpuDeclareImpl) {
1169
+ this.ctx.resolveSnippet(item);
1170
+ return { code: '', definesInNearestScope: false };
1171
+ }
1090
1172
  }
1091
- if (typeof statement === 'boolean') {
1092
- return {
1093
- code: `${this.ctx.pre}${statement ? 'true' : 'false'};`,
1094
- definesInNearestScope: false,
1095
- };
1173
+ if (isId(statement) || isBool(statement)) {
1174
+ throw new WgslForbiddenStatementError(statement);
1096
1175
  }
1097
1176
  if (statement[0] === NODE.return) {
1098
1177
  return {
@@ -1205,24 +1284,24 @@ ${this.ctx.pre}else ${alternate}`,
1205
1284
  const shouldUnroll = iterableExpr.value instanceof UnrollableIterable;
1206
1285
  const iterableSnippet = shouldUnroll ? iterableExpr.value.snippet : iterableExpr;
1207
1286
  const range = forOfUtils.getRangeSnippets(this.ctx, iterableSnippet, shouldUnroll);
1208
- const originalLoopVarName = loopVar[1];
1287
+ const originalLoopVarName = extractId(loopVar[1]);
1209
1288
  const blockified = blockifySingleStatement(body);
1210
1289
  if (shouldUnroll) {
1211
1290
  if (!isKnownAtComptime(range.end)) {
1212
1291
  throw new Error('Cannot unroll loop. Length of iterable is unknown at comptime.');
1213
1292
  }
1214
- const length = range.end.value;
1215
- if (length === 0) {
1216
- return { code: '', definesInNearestScope: false };
1217
- }
1218
1293
  const { value } = iterableSnippet;
1219
1294
  const elements = isTgpuRange(value)
1220
1295
  ? value.map((i) => coerceToSnippet(i))
1221
1296
  : value instanceof ArrayExpression
1222
1297
  ? value.elements
1223
- : Array.from({ length }, (_, i) => forOfUtils.getElementSnippet(iterableSnippet, snip(i, u32, 'constant')));
1298
+ : Array.from({ length: range.end.value }, (_, i) => forOfUtils.getElementSnippet(iterableSnippet, snip(i, u32, 'constant')));
1299
+ if (elements.length === 0) {
1300
+ return { code: '', definesInNearestScope: false };
1301
+ }
1224
1302
  const firstElement = elements[0];
1225
- if (!isAlias(firstElement) && !wgsl.isNaturallyEphemeral(firstElement.dataType)) {
1303
+ if (!isStoredInMemory(firstElement) &&
1304
+ !wgsl.isNaturallyEphemeral(firstElement.dataType)) {
1226
1305
  throw new WgslTypeError(`Cannot unroll '${stringifyNode(iterable)}'. The elements of iterable are constructed in place but are not value types.`);
1227
1306
  }
1228
1307
  let blocksCode = '';
@@ -1284,6 +1363,108 @@ ${this.ctx.pre}else ${alternate}`,
1284
1363
  this.ctx.popBlockScope();
1285
1364
  }
1286
1365
  }
1366
+ if (statement[0] === NODE.switch) {
1367
+ // Switch statement
1368
+ const [_, discriminant, cases] = statement;
1369
+ let discriminantExpr = this._expression(discriminant);
1370
+ let caseExprs = cases.map(([test, consequent]) => {
1371
+ const testExpr = test === null ? switchDefault : this._expression(test);
1372
+ return [testExpr, consequent];
1373
+ });
1374
+ // comptime folding
1375
+ let matchedCaseWasNotLast = false;
1376
+ if ([discriminantExpr, ...caseExprs.map(([test]) => test)].every(isKnownAtComptime)) {
1377
+ let matchedCaseIndex = caseExprs.findIndex(([test]) => test !== switchDefault && test.value === discriminantExpr.value);
1378
+ if (matchedCaseIndex === -1) {
1379
+ matchedCaseIndex = caseExprs.findIndex(([test]) => test === switchDefault);
1380
+ }
1381
+ if (matchedCaseIndex === -1) {
1382
+ return { code: '', definesInNearestScope: false };
1383
+ }
1384
+ const matchedConsequent = caseExprs
1385
+ .slice(matchedCaseIndex)
1386
+ .find(([, consequent]) => consequent.length !== 0)?.[1];
1387
+ if (matchedConsequent === undefined) {
1388
+ return { code: '', definesInNearestScope: false };
1389
+ }
1390
+ if (matchedConsequent !== caseExprs.at(-1)?.[1]) {
1391
+ // The JS behavior will be different if this case does not end with control flow.
1392
+ matchedCaseWasNotLast = true;
1393
+ }
1394
+ discriminantExpr = snip(0, u32, 'constant', false);
1395
+ caseExprs = [[switchDefault, matchedConsequent]];
1396
+ }
1397
+ // type concretization
1398
+ discriminantExpr = tryConvertSnippet(this.ctx, discriminantExpr, [i32, u32]);
1399
+ const switchType = discriminantExpr.dataType;
1400
+ invariant(switchType !== UnknownData);
1401
+ caseExprs = caseExprs.map(([test, consequent]) => {
1402
+ // TODO(#3148): Remove this throw.
1403
+ if (test !== switchDefault && test.dataType === UnknownData) {
1404
+ throw new Error(`Failed to convert one of the switch tests to '${switchType.type}'`);
1405
+ }
1406
+ const convertedTest = test === switchDefault ? test : tryConvertSnippet(this.ctx, test, [switchType]);
1407
+ return [convertedTest, consequent];
1408
+ });
1409
+ // consequent resolution
1410
+ const resolvedCaseExprs = caseExprs.map(([test, consequent]) => {
1411
+ // In WGSL, each case is a different block. This block scope forbids scope leaking.
1412
+ // TODO(#3001): Consider using NODE.block here
1413
+ this.ctx.pushBlockScope();
1414
+ this.ctx.indent();
1415
+ this.ctx.indent();
1416
+ try {
1417
+ const consequentStmts = consequent.map((s) => this._statement(s));
1418
+ return [test, consequentStmts];
1419
+ }
1420
+ finally {
1421
+ this.ctx.dedent();
1422
+ this.ctx.dedent();
1423
+ this.ctx.popBlockScope();
1424
+ }
1425
+ });
1426
+ const groupedCaseExprs = [];
1427
+ let currentGroup = [];
1428
+ for (const [index, [test, consequent]] of resolvedCaseExprs.entries()) {
1429
+ currentGroup.push(test);
1430
+ if (consequent.length > 0 || index === caseExprs.length - 1) {
1431
+ groupedCaseExprs.push([currentGroup, consequent]);
1432
+ currentGroup = [];
1433
+ }
1434
+ }
1435
+ // Validation
1436
+ {
1437
+ // Tests should be constant
1438
+ caseExprs.forEach(([testExpr], i) => {
1439
+ if (!isConstant(testExpr)) {
1440
+ const testNode = cases[i]?.[0];
1441
+ invariant(testNode, `Expected node to be not nullish.`);
1442
+ throw new Error(`All of switch tests must be constant.
1443
+ Test '${stringifyNode(testNode)}' is not constant, making the following switch statement invalid.
1444
+ This error may be caused by an implicit conversion.
1445
+ ${stringifyNode(statement)}`);
1446
+ }
1447
+ });
1448
+ // Tests should not have duplicates.
1449
+ // We skip this check, because WGSL errors are readable,
1450
+ // and we cannot easily access non-comptime known constants.
1451
+ // Tests should not have non-trivial fallthrough
1452
+ resolvedCaseExprs
1453
+ .slice(0, matchedCaseWasNotLast ? undefined : -1)
1454
+ .forEach(([_, consequent]) => {
1455
+ const last = consequent.at(-1);
1456
+ if (last && !last.endsWithControlFlow) {
1457
+ throw new Error(`Switch statement cannot have non-trivial fallthrough.
1458
+ The following switch statement is invalid:
1459
+ ${stringifyNode(statement)}`);
1460
+ }
1461
+ });
1462
+ }
1463
+ return {
1464
+ code: this._emitSwitchStatement(discriminantExpr, groupedCaseExprs),
1465
+ definesInNearestScope: false,
1466
+ };
1467
+ }
1287
1468
  if (statement[0] === NODE.postUpdate) {
1288
1469
  // Post-update statement
1289
1470
  const [_, op, arg] = statement;
@@ -1313,10 +1494,38 @@ ${this.ctx.pre}else ${alternate}`,
1313
1494
  definesInNearestScope: false,
1314
1495
  };
1315
1496
  }
1497
+ if (statement[0] === NODE.numericLiteral ||
1498
+ statement[0] === NODE.binaryExpr ||
1499
+ statement[0] === NODE.unaryExpr ||
1500
+ statement[0] === NODE.logicalExpr ||
1501
+ statement[0] === NODE.arrayExpr ||
1502
+ statement[0] === NODE.objectExpr ||
1503
+ statement[0] === NODE.nullLiteral) {
1504
+ // `memberAccess` is allowed for raw code snippets, `indexAccess` is allowed for consistency.
1505
+ // `stringLiteral` is forbidden by our injection prevention anyway.
1506
+ throw new WgslForbiddenStatementError(statement);
1507
+ }
1316
1508
  const expr = this._expression(statement);
1317
1509
  const resolved = expr.value !== undefined && expr.value !== null ? this.ctx.resolveSnippet(expr).value : '';
1318
1510
  return { code: resolved ? `${this.ctx.pre}${resolved};` : '', definesInNearestScope: false };
1319
1511
  }
1512
+ /**
1513
+ * Resolves the key of an object property. Handles both computed and non-computed keys.
1514
+ */
1515
+ _resolveObjectPropertyKey(property) {
1516
+ const computed = property[2];
1517
+ if (!computed) {
1518
+ return property[0];
1519
+ }
1520
+ const key = this._expression(property[0]);
1521
+ if (!isKnownAtComptime(key)) {
1522
+ throw new WgslTypeError(`Computed object property key '${stringifyObjectProperty(property)}' must be known at comptime.`);
1523
+ }
1524
+ if (typeof key.value !== 'string') {
1525
+ throw new WgslTypeError('Object property keys must be strings in TypeGPU functions.');
1526
+ }
1527
+ return key.value;
1528
+ }
1320
1529
  /**
1321
1530
  * Attempts a member access lookup to mark a variable as modified.
1322
1531
  * @example
@@ -1358,6 +1567,9 @@ function validateSnippetMutation(mutated, expr) {
1358
1567
  if (mutated.origin === 'readonly') {
1359
1568
  throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because readonly buffers cannot be mutated.`);
1360
1569
  }
1570
+ if (mutated.origin === 'immediate') {
1571
+ throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because immediate variables cannot be mutated.`);
1572
+ }
1361
1573
  if (mutated.origin === 'argument') {
1362
1574
  throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because non-pointer arguments cannot be mutated.`);
1363
1575
  }
@@ -1387,7 +1599,25 @@ function extractObject(expr) {
1387
1599
  (object[0] === NODE.memberAccess || object[0] === NODE.indexAccess)) {
1388
1600
  object = object[1];
1389
1601
  }
1390
- if (typeof object === 'string') {
1391
- return object;
1602
+ if (isId(object)) {
1603
+ return extractId(object);
1604
+ }
1605
+ }
1606
+ function isId(expr) {
1607
+ return typeof expr === 'string' || (Array.isArray(expr) && expr[0] === NODE.identifier);
1608
+ }
1609
+ function extractId(ident) {
1610
+ if (typeof ident === 'string') {
1611
+ return ident;
1612
+ }
1613
+ return ident[1];
1614
+ }
1615
+ function isBool(expr) {
1616
+ return typeof expr === 'boolean' || (Array.isArray(expr) && expr[0] === NODE.booleanLiteral);
1617
+ }
1618
+ function extractBool(ident) {
1619
+ if (typeof ident === 'boolean') {
1620
+ return ident;
1392
1621
  }
1622
+ return ident[1];
1393
1623
  }
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' ||