typegpu 0.11.3 → 0.11.5

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 (59) hide show
  1. package/README.md +4 -2
  2. package/core/buffer/buffer.js +17 -53
  3. package/core/buffer/bufferUsage.js +2 -2
  4. package/core/constant/tgpuConstant.js +1 -1
  5. package/core/function/fnCore.js +12 -6
  6. package/core/function/tgpuFragmentFn.d.ts +1 -1
  7. package/core/pipeline/computePipeline.d.ts +1 -1
  8. package/core/pipeline/computePipeline.js +1 -1
  9. package/core/pipeline/renderPipeline.js +1 -1
  10. package/core/resolve/namespace.d.ts +2 -11
  11. package/core/resolve/namespace.js +7 -24
  12. package/core/resolve/resolveData.js +3 -2
  13. package/core/resolve/tgpuResolve.js +1 -1
  14. package/core/root/init.d.ts +1 -0
  15. package/core/root/init.js +6 -0
  16. package/core/root/rootTypes.d.ts +21 -15
  17. package/core/sampler/sampler.js +2 -2
  18. package/core/simulate/tgpuSimulate.js +1 -1
  19. package/core/slot/accessor.js +1 -1
  20. package/core/texture/externalTexture.js +1 -1
  21. package/core/texture/texture.js +2 -2
  22. package/core/variable/tgpuVariable.js +1 -1
  23. package/data/autoStruct.js +3 -2
  24. package/data/dataIO.d.ts +11 -0
  25. package/data/dataIO.js +45 -1
  26. package/data/dataTypes.d.ts +1 -1
  27. package/data/dataTypes.js +2 -11
  28. package/data/partialIO.d.ts +8 -0
  29. package/data/partialIO.js +6 -1
  30. package/data/struct.js +3 -2
  31. package/data/wgslTypes.d.ts +16 -16
  32. package/index.d.ts +3 -1
  33. package/index.js +3 -1
  34. package/indexNamedExports.d.ts +2 -0
  35. package/{nameRegistry.js → nameUtils.js} +46 -90
  36. package/package.js +1 -1
  37. package/package.json +1 -1
  38. package/resolutionCtx.js +64 -30
  39. package/shared/stringify.js +1 -0
  40. package/shared/tseynit.js +90 -0
  41. package/std/copy.d.ts +7 -0
  42. package/std/copy.js +27 -0
  43. package/std/index.d.ts +3 -2
  44. package/std/index.js +3 -1
  45. package/tgpuUnstable.js +1 -1
  46. package/tgsl/accessIndex.js +2 -1
  47. package/tgsl/consoleLog/deserializers.js +1 -1
  48. package/tgsl/consoleLog/logGenerator.js +1 -6
  49. package/tgsl/consoleLog/types.d.ts +4 -5
  50. package/tgsl/conversion.js +4 -1
  51. package/tgsl/generationHelpers.d.ts +2 -1
  52. package/tgsl/jsPolyfills.d.ts +25 -0
  53. package/tgsl/jsPolyfills.js +44 -0
  54. package/tgsl/wgslGenerator.d.ts +20 -2
  55. package/tgsl/wgslGenerator.js +114 -57
  56. package/types.d.ts +29 -2
  57. package/nameRegistry.d.ts +0 -30
  58. package/tgsl/consoleLog/types.js +0 -12
  59. package/tgsl/math.js +0 -45
@@ -1,13 +1,13 @@
1
1
  import { $gpuCallable, $internal, $providing, isMarkedInternal } from "../shared/symbols.js";
2
2
  import { getName } from "../shared/meta.js";
3
3
  import { Void, isBool, isNaturallyEphemeral, isNumericSchema, isPtr, isVec, isWgslArray, isWgslStruct } from "../data/wgslTypes.js";
4
- import { ConsoleLog, InfixDispatch, UnknownData, isLooseData, unptr } from "../data/dataTypes.js";
4
+ import { InfixDispatch, UnknownData, unptr } from "../data/dataTypes.js";
5
5
  import { fallthroughCopyOrigin, isEphemeralOrigin, isEphemeralSnippet, snip } from "../data/snippet.js";
6
6
  import { ResolutionError, WgslTypeError, invariant } from "../errors.js";
7
7
  import { isGPUCallable, isKnownAtComptime } from "../types.js";
8
8
  import { stitch } from "../core/resolve/stitch.js";
9
9
  import { createPtrFromOrigin, implicitFrom, ptrFn } from "../data/ptr.js";
10
- import { RefOperator } from "../data/ref.js";
10
+ import { RefOperator, _ref } from "../data/ref.js";
11
11
  import { safeStringify } from "../shared/stringify.js";
12
12
  import { convertStructValues, convertToCommonType, tryConvertSnippet } from "./conversion.js";
13
13
  import { bool, i32, u32 } from "../data/numeric.js";
@@ -23,10 +23,11 @@ import { isGenericFn } from "../core/function/tgpuFn.js";
23
23
  import { arrayOf } from "../data/array.js";
24
24
  import { pow } from "../std/numeric.js";
25
25
  import { resolveData } from "../core/resolve/resolveData.js";
26
- import { UnrollableIterable } from "../core/unroll/tgpuUnroll.js";
27
- import { mathToStd } from "./math.js";
26
+ import { UnrollableIterable, unroll } from "../core/unroll/tgpuUnroll.js";
27
+ import { mathToStd, supportedLogOps } from "./jsPolyfills.js";
28
28
  import { isTgpuRange } from "../std/range.js";
29
29
  import { getElementSnippet, getElementType, getLoopVarKind, getRangeSnippets } from "./forOfUtils.js";
30
+ import { stringifyNode } from "../shared/tseynit.js";
30
31
  import * as tinyest from "tinyest";
31
32
 
32
33
  //#region src/tgsl/wgslGenerator.ts
@@ -164,8 +165,11 @@ ${this.ctx.pre}}`;
164
165
  this.ctx.popBlockScope();
165
166
  }
166
167
  }
168
+ _blockStatement(block, externalMap) {
169
+ return `${this.ctx.pre}${this._block(block, externalMap)}`;
170
+ }
167
171
  refVariable(id, dataType) {
168
- const varName = this.ctx.makeNameValid(id);
172
+ const varName = this.ctx.makeUniqueIdentifier(id, "block");
169
173
  const ptrType = ptrFn(dataType);
170
174
  const snippet = snip(new RefOperator(snip(varName, dataType, "function"), ptrType), ptrType, "function");
171
175
  this.ctx.defineVariable(id, snippet);
@@ -180,10 +184,14 @@ ${this.ctx.pre}}`;
180
184
  else if (!naturallyEphemeral) varOrigin = isEphemeralOrigin(origin) ? "this-function" : origin;
181
185
  else if (origin === "constant" && varType === "const") varOrigin = "constant";
182
186
  else varOrigin = "runtime";
183
- const snippet = snip(this.ctx.makeNameValid(id), dataType, varOrigin);
187
+ const snippet = snip(this.ctx.makeUniqueIdentifier(id, "block"), dataType, varOrigin);
184
188
  this.ctx.defineVariable(id, snippet);
185
189
  return snippet;
186
190
  }
191
+ /**
192
+ * Creates a variable declaration string.
193
+ * `keyword` may be a placeholder filled in later.
194
+ */
187
195
  emitVarDecl(pre, keyword, name, _dataType, rhsStr) {
188
196
  return `${pre}${keyword} ${name} = ${rhsStr};`;
189
197
  }
@@ -225,8 +233,9 @@ ${this.ctx.pre}}`;
225
233
  return snip(rhsStr$1, bool, "runtime");
226
234
  }
227
235
  const rhsExpr = this._expression(rhs);
228
- if (rhsExpr.value instanceof RefOperator) throw new WgslTypeError(stitch`Cannot assign a ref to an existing variable '${lhsExpr}', define a new variable instead.`);
236
+ if (rhsExpr.value instanceof RefOperator) throw new WgslTypeError(stitch`Cannot assign a ref to an existing variable '${stringifyNode(lhs)}', define a new variable instead.`);
229
237
  if (op === "==") throw new Error("Please use the === operator instead of ==");
238
+ if (op === "!=") throw new Error("Please use the !== operator instead of !=");
230
239
  if (op === "===" && isKnownAtComptime(lhsExpr) && isKnownAtComptime(rhsExpr)) return snip(lhsExpr.value === rhsExpr.value, bool, "constant");
231
240
  if (lhsExpr.dataType === UnknownData) throw new WgslTypeError(`Left-hand side of '${op}' is of unknown type`);
232
241
  if (rhsExpr.dataType === UnknownData) throw new WgslTypeError(`Right-hand side of '${op}' is of unknown type`);
@@ -250,19 +259,14 @@ ${this.ctx.pre}}`;
250
259
  const rhsStr = this.ctx.resolve(convRhs.value, convRhs.dataType).value;
251
260
  const type = operatorToType(convLhs.dataType, op, convRhs.dataType);
252
261
  if (exprType === NODE.assignmentExpr) {
253
- if (convLhs.origin === "constant" || convLhs.origin === "constant-tgpu-const-ref" || convLhs.origin === "runtime-tgpu-const-ref") throw new WgslTypeError(`'${lhsStr} = ${rhsStr}' is invalid, because ${lhsStr} is a constant. This error may also occur when assigning to a value defined outside of a TypeGPU function's scope.`);
254
- if (lhsExpr.origin === "argument") throw new WgslTypeError(`'${lhsStr} ${op} ${rhsStr}' is invalid, because non-pointer arguments cannot be mutated.`);
255
- if (op === "=" && rhsExpr.origin === "argument" && !isNaturallyEphemeral(rhsExpr.dataType)) throw new WgslTypeError(`'${lhsStr} = ${rhsStr}' is invalid, because argument references cannot be assigned.\n-----\nTry '${lhsStr} = ${this.ctx.resolve(rhsExpr.dataType).value}(${rhsStr})' to copy the value instead.\n-----`);
256
- if (op === "=" && !isEphemeralSnippet(rhsExpr)) throw new WgslTypeError(`'${lhsStr} = ${rhsStr}' is invalid, because references cannot be assigned.\n-----\nTry '${lhsStr} = ${this.ctx.resolve(rhsExpr.dataType).value}(${rhsStr})' to copy the value instead.\n-----`);
262
+ validateSnippetMutation(convLhs, expression);
263
+ this.tryMarkModified(lhs);
264
+ if (op === "=" && rhsExpr.origin === "argument" && !isNaturallyEphemeral(rhsExpr.dataType)) throw new WgslTypeError(`'${stringifyNode(expression)}' is invalid, because argument references cannot be assigned.\n-----\nTry '${stringifyNode(lhs)} = ${this.ctx.resolve(rhsExpr.dataType).value}(${stringifyNode(rhs)})' to copy the value instead.\n-----`);
265
+ if (op === "=" && !isEphemeralSnippet(rhsExpr)) throw new WgslTypeError(`'${stringifyNode(expression)}' is invalid, because references cannot be assigned.\n-----\nTry '${stringifyNode(lhs)} = ${this.ctx.resolve(rhsExpr.dataType).value}(${stringifyNode(rhs)})' to copy the value instead.\n-----`);
257
266
  }
258
267
  return snip(parenthesizedOps.includes(op) ? `(${lhsStr} ${OP_MAP[op] ?? op} ${rhsStr})` : `${lhsStr} ${OP_MAP[op] ?? op} ${rhsStr}`, type, "runtime");
259
268
  }
260
- if (expression[0] === NODE.postUpdate) {
261
- const [_, op, arg] = expression;
262
- const argExpr = this._expression(arg);
263
- const argStr = this.ctx.resolve(argExpr.value, argExpr.dataType).value;
264
- return snip(`${argStr}${op}`, argExpr.dataType, "runtime");
265
- }
269
+ if (expression[0] === NODE.postUpdate) throw new Error(`'${stringifyNode(expression)}' is invalid because update is only allowed as a statement.`);
266
270
  if (expression[0] === NODE.unaryExpr) {
267
271
  const [_, op, arg] = expression;
268
272
  const argExpr = this._expression(arg);
@@ -274,37 +278,28 @@ ${this.ctx.pre}}`;
274
278
  }
275
279
  if (expression[0] === NODE.memberAccess) {
276
280
  const [_, targetNode, property] = expression;
277
- const target = this._expression(targetNode);
278
- if (target.value === console) return snip(new ConsoleLog(property), UnknownData, "runtime");
279
- if (target.value === Math) {
280
- if (property in mathToStd && mathToStd[property]) return snip(mathToStd[property], UnknownData, "runtime");
281
- if (typeof Math[property] === "function") throw new Error(`Unsupported functionality 'Math.${property}'. Use an std alternative, or implement the function manually.`);
282
- }
283
- const accessed = accessProp(target, property);
284
- if (!accessed) throw new Error(stitch`Property '${property}' not found on value '${target}' of type ${this.ctx.resolve(target.dataType)}`);
281
+ const accessed = accessProp(this._expression(targetNode), property);
282
+ if (!accessed) throw new Error(`Property '${property}' not found on '${stringifyNode(targetNode)}'`);
285
283
  return accessed;
286
284
  }
287
285
  if (expression[0] === NODE.indexAccess) {
288
286
  const [_, targetNode, propertyNode] = expression;
289
287
  const target = this._expression(targetNode);
290
288
  const inProperty = this._expression(propertyNode);
291
- const property = convertToCommonType(this.ctx, [inProperty], [u32, i32], false)?.[0] ?? inProperty;
292
- const accessed = accessIndex(target, property);
293
- if (!accessed) {
294
- const targetStr = this.ctx.resolve(target.value, target.dataType).value;
295
- const propertyStr = this.ctx.resolve(property.value, property.dataType).value;
296
- throw new Error(`Index access '${targetStr}[${propertyStr}]' is invalid. If the value is an array, to address this, consider one of the following approaches: (1) declare the array using 'tgpu.const', (2) store the array in a buffer, or (3) define the array within the GPU function scope.`);
297
- }
289
+ const accessed = accessIndex(target, convertToCommonType(this.ctx, [inProperty], [u32, i32], false)?.[0] ?? inProperty);
290
+ if (!accessed) throw new Error(`Index access '${stringifyNode(expression)}' is invalid. If the value is an array, to address this, consider one of the following approaches: (1) declare the array using 'tgpu.const', (2) store the array in a buffer, or (3) define the array within the GPU function scope.`);
298
291
  return accessed;
299
292
  }
300
293
  if (expression[0] === NODE.numericLiteral) {
301
294
  const type = typeof expression[1] === "string" ? numericLiteralToSnippet(parseNumericString(expression[1])) : numericLiteralToSnippet(expression[1]);
302
- if (!type) throw new Error(`Invalid numeric literal ${expression[1]}`);
295
+ invariant(type, `Expected ${stringifyNode(expression)} to be valid numeric literal`);
303
296
  return type;
304
297
  }
305
298
  if (expression[0] === NODE.call) {
306
299
  const [_, calleeNode, argNodes] = expression;
307
- const callee = this._expression(calleeNode);
300
+ const _callee = this._expression(calleeNode);
301
+ const callee = mathToStd.has(_callee.value) ? snip(mathToStd.get(_callee.value), UnknownData, "runtime") : _callee;
302
+ if (supportedLogOps().includes(callee.value)) return this.ctx.generateLog(callee.value, argNodes.map((arg) => this._expression(arg)));
308
303
  if (isWgslStruct(callee.value)) {
309
304
  if (argNodes.length > 1) throw new WgslTypeError("Struct schemas should always be called with at most 1 argument");
310
305
  if (!argNodes[0]) return snip(`${this.ctx.resolve(callee.value).value}()`, callee.value, "runtime");
@@ -324,14 +319,14 @@ ${this.ctx.pre}}`;
324
319
  const rhs = this._expression(argNodes[0]);
325
320
  return callee.value.operator(this.ctx, [callee.value.lhs, rhs]);
326
321
  }
327
- if (callee.value instanceof ConsoleLog) return this.ctx.generateLog(callee.value.op, argNodes.map((arg) => this._expression(arg)));
322
+ if ((callee.value === _ref || callee.value === unroll) && argNodes[0]) this.tryMarkModified(argNodes[0]);
328
323
  if (isGPUCallable(callee.value)) {
329
324
  const callable = callee.value[$gpuCallable];
330
325
  const strictSignature = callable.strictSignature;
331
326
  let convertedArguments;
332
327
  if (strictSignature) convertedArguments = argNodes.map((arg, i) => {
333
328
  const argType = strictSignature.argTypes[i];
334
- if (!argType) throw new WgslTypeError(`Function '${getName(callee.value)}' was called with too many arguments`);
329
+ if (!argType) throw new WgslTypeError(`Call '${stringifyNode(expression)}' is invalid since the function expected fewer arguments`);
335
330
  return this._typedExpression(arg, argType);
336
331
  });
337
332
  else convertedArguments = argNodes.map((arg) => this._expression(arg));
@@ -361,6 +356,10 @@ ${this.ctx.pre}}`;
361
356
  }));
362
357
  if (shelllessCall) return shelllessCall;
363
358
  }
359
+ const maybeMathMethod = Object.getOwnPropertyNames(Math).find((prop) => Math[prop] === callee.value);
360
+ if (maybeMathMethod) throw new Error(`Unsupported Math functionality 'Math.${maybeMathMethod}()'. Use an std alternative, or implement the function manually.`);
361
+ const maybeConsoleMethod = Object.getOwnPropertyNames(console).find((prop) => console[prop] === callee.value);
362
+ if (maybeConsoleMethod) throw new Error(`Unsupported console functionality 'console.${maybeConsoleMethod}()'.`);
364
363
  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`);
365
364
  }
366
365
  if (expression[0] === NODE.objectExpr) {
@@ -391,7 +390,7 @@ ${this.ctx.pre}}`;
391
390
  const convertedSnippets = convertStructValues(this.ctx, structType, entries);
392
391
  return snip(stitch`${this.ctx.resolve(structType).value}(${convertedSnippets})`, structType, "runtime");
393
392
  }
394
- throw new WgslTypeError(`No target type could be inferred for object with keys [${Object.keys(obj).join(", ")}], please wrap the object in the corresponding schema.`);
393
+ throw new WgslTypeError(`No target type could be inferred for object '${stringifyNode(expression)}', please wrap the object in the corresponding schema.`);
395
394
  }
396
395
  if (expression[0] === NODE.arrayExpr) {
397
396
  const [_, valueNodes] = expression;
@@ -406,7 +405,7 @@ ${this.ctx.pre}}`;
406
405
  const valuesSnippets = valueNodes.map((value) => this._expression(value));
407
406
  if (valuesSnippets.length === 0) throw new WgslTypeError("Cannot infer the type of an empty array literal.");
408
407
  const converted = convertToCommonType(this.ctx, valuesSnippets);
409
- if (!converted) throw new WgslTypeError("The given values cannot be automatically converted to a common type. Consider wrapping the array in an appropriate schema");
408
+ if (!converted) throw new WgslTypeError(`Values '${stringifyNode(expression)}' cannot be automatically converted to a common type. Consider wrapping the array in an appropriate schema`);
410
409
  values = converted;
411
410
  elemType = concretize(values[0]?.dataType);
412
411
  }
@@ -417,14 +416,21 @@ ${this.ctx.pre}}`;
417
416
  const [_, test, consequent, alternative] = expression;
418
417
  const testExpression = this._expression(test);
419
418
  if (isKnownAtComptime(testExpression)) return testExpression.value ? this._expression(consequent) : this._expression(alternative);
420
- else throw new Error(`Ternary operator is only supported for comptime-known checks (used with '${testExpression.value}'). For runtime checks, please use 'std.select' or if/else statements.`);
419
+ else throw new Error(`Ternary operator '${stringifyNode(expression)}' is invalid, because only comptime-known checks are supported. For runtime checks, please use 'std.select' or if/else statements.`);
421
420
  }
422
421
  if (expression[0] === NODE.stringLiteral) return snip(expression[1], UnknownData, "constant");
423
422
  if (expression[0] === NODE.preUpdate) throw new Error("Cannot use pre-updates in TypeGPU functions.");
424
423
  assertExhaustive(expression);
425
424
  }
426
425
  functionDefinition(options) {
427
- const body = this._block(options.body);
426
+ let body = this._block(options.body);
427
+ const scope = this.ctx.topFunctionScope;
428
+ invariant(scope, "Expected function scope to be present");
429
+ const replacements = Object.fromEntries(scope.placeholderForVariable.entries().map(([variable, placeholder]) => [placeholder, scope.modifiedVariables.has(variable) ? "var" : "let"]));
430
+ if (Object.keys(replacements).length > 0) {
431
+ const regex = new RegExp(Object.keys(replacements).join("|"), "gi");
432
+ body = body.replace(regex, (match) => replacements[match] ?? "#ERR");
433
+ }
428
434
  const returnType = options.determineReturnType();
429
435
  const argList = options.args.filter((arg) => arg.used || options.functionType === "normal").map((arg) => {
430
436
  return `${getAttributesString(arg.decoratedType)}${arg.name}: ${this.ctx.resolve(arg.decoratedType).value}`;
@@ -448,10 +454,10 @@ ${this.ctx.pre}}`;
448
454
  if (returnNode !== void 0) {
449
455
  const expectedReturnType = this.ctx.topFunctionReturnType;
450
456
  let returnSnippet = expectedReturnType ? this._typedExpression(returnNode, expectedReturnType) : this._expression(returnNode);
451
- if (returnSnippet.value instanceof RefOperator) throw new WgslTypeError(stitch`Cannot return references, returning '${returnSnippet.value.snippet}'`);
452
- if (returnSnippet.origin === "argument" && !isNaturallyEphemeral(returnSnippet.dataType) && this.ctx.topFunctionScope?.functionType === "normal") throw new WgslTypeError(stitch`Cannot return references to arguments, returning '${returnSnippet}'. Copy the argument before returning it.`);
457
+ if (returnSnippet.value instanceof RefOperator) throw new WgslTypeError(`Cannot return '${stringifyNode(returnNode)}' because it is a d.ref`);
458
+ if (returnSnippet.origin === "argument" && !isNaturallyEphemeral(returnSnippet.dataType) && this.ctx.topFunctionScope?.functionType === "normal") throw new WgslTypeError(`'${stringifyNode(statement)}' is invalid, cannot return references to arguments. Copy the argument before returning it.`);
453
459
  if (!expectedReturnType && !isEphemeralSnippet(returnSnippet) && returnSnippet.origin !== "this-function") {
454
- const str = this.ctx.resolve(returnSnippet.value, returnSnippet.dataType).value;
460
+ const str = stringifyNode(returnNode);
455
461
  const typeStr = this.ctx.resolve(unptr(returnSnippet.dataType)).value;
456
462
  throw new WgslTypeError(`'return ${str};' is invalid, cannot return references.
457
463
  -----
@@ -482,7 +488,7 @@ Try 'return ${typeStr}(${str});' instead.
482
488
  if (!Array.isArray(node)) node = blockifySingleStatement(node);
483
489
  if (node[0] === NODE.block && node[1].length === 1 && node[1][0][0] === NODE.if) return this._statement(node[1][0]);
484
490
  if (node[0] === NODE.if) return this._statement(node);
485
- return `${this.ctx.pre}${this._block(blockifySingleStatement(node))}`;
491
+ return this._blockStatement(blockifySingleStatement(node));
486
492
  }
487
493
  const consequent = this._block(blockifySingleStatement(consNode));
488
494
  const alternate = !altNode ? void 0 : this._block(blockifySingleStatement(altNode));
@@ -492,14 +498,13 @@ ${this.ctx.pre}if (${condition}) ${consequent}
492
498
  ${this.ctx.pre}else ${alternate}`;
493
499
  }
494
500
  if (statement[0] === NODE.let || statement[0] === NODE.const) {
495
- let varType = "var";
501
+ let varType = "<deferred>";
496
502
  const [stmtType, rawId, rawValue] = statement;
497
503
  const eq = rawValue !== void 0 ? this._expression(rawValue) : void 0;
498
- if (!eq) throw new Error(`Cannot create variable '${rawId}' without an initial value.`);
504
+ if (!eq) throw new Error(`'${stringifyNode(statement)}' is invalid because all variables need initializers.`);
499
505
  const ephemeral = isEphemeralSnippet(eq);
500
506
  let dataType = eq.dataType;
501
507
  const naturallyEphemeral = isNaturallyEphemeral(dataType);
502
- if (isLooseData(eq.dataType)) throw new Error(`Cannot create variable '${rawId}' with loose data type.`);
503
508
  if (eq.value instanceof RefOperator) {
504
509
  if (eq.dataType !== UnknownData) throw new WgslTypeError(`Cannot store d.ref() in a variable if it references another value. Copy the value passed into d.ref() instead.`);
505
510
  const refSnippet = eq.value.snippet;
@@ -508,7 +513,7 @@ ${this.ctx.pre}else ${alternate}`;
508
513
  }
509
514
  if (!ephemeral) {
510
515
  if (stmtType === NODE.let) {
511
- const rhsStr$1 = this.ctx.resolve(eq.value).value;
516
+ const rhsStr$1 = stringifyNode(rawValue ?? "");
512
517
  const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value;
513
518
  throw new WgslTypeError(`'let ${rawId} = ${rhsStr$1}' is invalid, because references cannot be assigned to 'let' variable declarations.
514
519
  -----
@@ -525,14 +530,17 @@ ${this.ctx.pre}else ${alternate}`;
525
530
  invariant(ptrType !== void 0, `Creating pointer type from origin ${eq.origin}`);
526
531
  dataType = ptrType;
527
532
  }
528
- if (!(eq.value instanceof RefOperator)) dataType = implicitFrom(dataType);
533
+ if (!(eq.value instanceof RefOperator)) {
534
+ dataType = implicitFrom(dataType);
535
+ this.tryMarkModified(rawValue);
536
+ }
529
537
  }
530
538
  } else if (stmtType === NODE.const) {
531
539
  if (eq.origin === "argument") varType = "let";
532
540
  else if (naturallyEphemeral) varType = eq.origin === "constant" ? "const" : "let";
533
541
  } else if (eq.origin === "argument") {
534
542
  if (!naturallyEphemeral) {
535
- const rhsStr$1 = this.ctx.resolve(eq.value).value;
543
+ const rhsStr$1 = stringifyNode(rawValue ?? "");
536
544
  const rhsTypeStr = this.ctx.resolve(unptr(eq.dataType)).value;
537
545
  throw new WgslTypeError(`'let ${rawId} = ${rhsStr$1}' is invalid, because references to arguments cannot be assigned to 'let' variable declarations.
538
546
  -----
@@ -544,9 +552,16 @@ ${this.ctx.pre}else ${alternate}`;
544
552
  const snippet = this.blockVariable(varType, rawId, concretize(dataType), eq.origin);
545
553
  const rhsSnippet = tryConvertSnippet(this.ctx, eq, dataType, false);
546
554
  const rhsStr = this.ctx.resolve(rhsSnippet.value, rhsSnippet.dataType).value;
547
- return this.emitVarDecl(this.ctx.pre, varType, snippet.value, concretize(dataType), rhsStr);
555
+ let emittedVarType;
556
+ if (varType === "<deferred>") {
557
+ const scope = this.ctx.topFunctionScope;
558
+ invariant(scope, `Expected function scope to be present for ${rawId}`);
559
+ emittedVarType = `#VAR_${scope.placeholderForVariable.size}#`;
560
+ scope.placeholderForVariable.set(snippet, emittedVarType);
561
+ } else emittedVarType = varType;
562
+ return this.emitVarDecl(this.ctx.pre, emittedVarType, snippet.value, concretize(dataType), rhsStr);
548
563
  }
549
- if (statement[0] === NODE.block) return `${this.ctx.pre}${this._block(statement)}`;
564
+ if (statement[0] === NODE.block) return this._blockStatement(statement);
550
565
  if (statement[0] === NODE.for) {
551
566
  const [_, init, condition, update, body] = statement;
552
567
  const prevUnrollingFlag = this.#unrolling;
@@ -583,6 +598,7 @@ ${this.ctx.pre}else ${alternate}`;
583
598
  if (statement[0] === NODE.forOf) {
584
599
  const [_, loopVar, iterable, body] = statement;
585
600
  if (loopVar[0] !== NODE.const) throw new WgslTypeError("Only `for (const ... of ... )` loops are supported");
601
+ this.tryMarkModified(iterable);
586
602
  let ctxIndent = false;
587
603
  const prevUnrollingFlag = this.#unrolling;
588
604
  try {
@@ -600,22 +616,22 @@ ${this.ctx.pre}else ${alternate}`;
600
616
  if (length === 0) return "";
601
617
  const { value } = iterableSnippet;
602
618
  const elements = isTgpuRange(value) ? value.map((i) => coerceToSnippet(i)) : value instanceof ArrayExpression ? value.elements : Array.from({ length }, (_$1, i) => getElementSnippet(iterableSnippet, snip(i, u32, "constant")));
603
- if (isEphemeralSnippet(elements[0]) && !isNaturallyEphemeral(elements[0]?.dataType)) throw new WgslTypeError("Cannot unroll loop. The elements of iterable are emphemeral but not naturally ephemeral.");
604
- return elements.map((e, i) => `${this.ctx.pre}// unrolled iteration #${i}\n${this.ctx.pre}${this._block(blockified, { [originalLoopVarName]: e })}`).join("\n");
619
+ if (isEphemeralSnippet(elements[0]) && !isNaturallyEphemeral(elements[0]?.dataType)) throw new WgslTypeError(`Cannot unroll '${stringifyNode(iterable)}'. The elements of iterable are constructed in place but are not value types.`);
620
+ return elements.map((e, i) => `${this.ctx.pre}// unrolled iteration #${i}\n${this._blockStatement(blockified, { [originalLoopVarName]: e })}`).join("\n");
605
621
  }
606
622
  this.#unrolling = false;
607
- const index = this.ctx.makeNameValid("i");
623
+ const index = this.ctx.makeUniqueIdentifier("i", "block");
608
624
  const forHeaderStr = stitch`${this.ctx.pre}for (var ${index} = ${range.start}; ${index} ${range.comparison} ${range.end}; ${index} += ${range.step})`;
609
625
  let bodyStr = "";
610
626
  if (isTgpuRange(iterableSnippet.value)) bodyStr = this._block(blockified, { [originalLoopVarName]: snip(index, range.start.dataType, "runtime") });
611
627
  else {
612
628
  this.ctx.indent();
613
629
  ctxIndent = true;
614
- const loopVarName = this.ctx.makeNameValid(originalLoopVarName);
630
+ const loopVarName = this.ctx.makeUniqueIdentifier(originalLoopVarName, "block");
615
631
  const elementSnippet = getElementSnippet(iterableSnippet, snip(index, u32, "runtime"));
616
632
  const loopVarKind = getLoopVarKind(elementSnippet);
617
633
  const elementType = getElementType(elementSnippet, iterableSnippet);
618
- bodyStr = `{\n${stitch`${this.ctx.pre}${loopVarKind} ${loopVarName} = ${tryConvertSnippet(this.ctx, elementSnippet, elementType, false)};`}\n${this.ctx.pre}${this._block(blockified, { [originalLoopVarName]: snip(loopVarName, elementType, elementSnippet.origin) })}\n`;
634
+ bodyStr = `{\n${stitch`${this.ctx.pre}${loopVarKind} ${loopVarName} = ${tryConvertSnippet(this.ctx, elementSnippet, elementType, false)};`}\n${this._blockStatement(blockified, { [originalLoopVarName]: snip(loopVarName, elementType, elementSnippet.origin) })}\n`;
619
635
  this.ctx.dedent();
620
636
  bodyStr += `${this.ctx.pre}}`;
621
637
  ctxIndent = false;
@@ -627,6 +643,14 @@ ${this.ctx.pre}else ${alternate}`;
627
643
  this.ctx.popBlockScope();
628
644
  }
629
645
  }
646
+ if (statement[0] === NODE.postUpdate) {
647
+ const [_, op, arg] = statement;
648
+ const argExpr = this._expression(arg);
649
+ const argStr = this.ctx.resolve(argExpr.value, argExpr.dataType).value;
650
+ validateSnippetMutation(argExpr, statement);
651
+ this.tryMarkModified(arg);
652
+ return `${this.ctx.pre}${argStr}${op};`;
653
+ }
630
654
  if (statement[0] === NODE.continue) {
631
655
  if (this.#unrolling) throw new WgslTypeError("Cannot unroll loop containing `continue`");
632
656
  return `${this.ctx.pre}continue;`;
@@ -639,7 +663,35 @@ ${this.ctx.pre}else ${alternate}`;
639
663
  const resolved = expr.value && this.ctx.resolve(expr.value).value;
640
664
  return resolved ? `${this.ctx.pre}${resolved};` : "";
641
665
  }
666
+ /**
667
+ * Attempts a member access lookup to mark a variable as modified.
668
+ * @example
669
+ * // given `let a; a = 1;`
670
+ * tryMarkModified('a') // `a` is marked in the function scope
671
+ *
672
+ * // given `const obj; obj.prop = 1;`
673
+ * tryMarkModified('obj.prop') // `obj` is marked in the function scope
674
+ *
675
+ * // given `this.buffer.$;`
676
+ * tryMarkModified('this.buffer.$') // `this` is not marked, since there is no placeholder for it
677
+ */
678
+ tryMarkModified(expr) {
679
+ if (!expr) return;
680
+ const maybeObject = extractObject(expr);
681
+ if (maybeObject !== void 0) {
682
+ const snippet = this.ctx.getById(maybeObject);
683
+ const scope = this.ctx.topFunctionScope;
684
+ if (snippet && scope && scope.placeholderForVariable.has(snippet)) scope.modifiedVariables.add(snippet);
685
+ }
686
+ }
642
687
  };
688
+ function validateSnippetMutation(mutated, expr) {
689
+ if (mutated.origin === "constant" || mutated.origin === "constant-tgpu-const-ref" || mutated.origin === "runtime-tgpu-const-ref") {
690
+ if (isKnownAtComptime(mutated)) 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.`);
691
+ throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because the left side is a constant.`);
692
+ }
693
+ if (mutated.origin === "argument") throw new WgslTypeError(`'${stringifyNode(expr)}' is invalid, because non-pointer arguments cannot be mutated.`);
694
+ }
643
695
  function assertExhaustive(value) {
644
696
  throw new Error(`'${safeStringify(value)}' was not handled by the WGSL generator.`);
645
697
  }
@@ -651,6 +703,11 @@ function parseNumericString(str) {
651
703
  function blockifySingleStatement(statement) {
652
704
  return typeof statement !== "object" || statement[0] !== NODE.block ? [NODE.block, [statement]] : statement;
653
705
  }
706
+ function extractObject(expr) {
707
+ let object = expr;
708
+ while (Array.isArray(object) && (object[0] === NODE.memberAccess || object[0] === NODE.indexAccess)) object = object[1];
709
+ if (typeof object === "string") return object;
710
+ }
654
711
  const wgslGenerator = new WgslGenerator();
655
712
  var wgslGenerator_default = wgslGenerator;
656
713
 
package/types.d.ts CHANGED
@@ -70,6 +70,14 @@ type FunctionScopeLayer = {
70
70
  * All types used in `return` statements.
71
71
  */
72
72
  reportedReturnTypes: Set<BaseData>;
73
+ /**
74
+ * Maps variables to their modifier placeholders
75
+ */
76
+ placeholderForVariable: Map<Snippet, string>;
77
+ /**
78
+ * Local variables that need `var` modifier.
79
+ */
80
+ modifiedVariables: Set<Snippet>;
73
81
  };
74
82
  type SlotBindingLayer = {
75
83
  type: 'slotBinding';
@@ -77,6 +85,7 @@ type SlotBindingLayer = {
77
85
  };
78
86
  type BlockScopeLayer = {
79
87
  type: 'blockScope';
88
+ takenLocalIdentifiers: Set<string>;
80
89
  declarations: Map<string, Snippet>;
81
90
  externals: Map<string, Snippet>;
82
91
  };
@@ -84,6 +93,7 @@ type StackLayer = ItemLayer | SlotBindingLayer | FunctionScopeLayer | BlockScope
84
93
  interface ItemStateStack {
85
94
  readonly itemDepth: number;
86
95
  readonly topItem: ItemLayer;
96
+ readonly topBlockScope: BlockScopeLayer | undefined;
87
97
  readonly topFunctionScope: FunctionScopeLayer | undefined;
88
98
  pushItem(): void;
89
99
  pushSlotBindings(pairs: SlotValuePair[]): void;
@@ -237,8 +247,25 @@ interface ResolutionCtx {
237
247
  * @param name the temporary name to assign to the item (if missing, just returns `callback()`)
238
248
  */
239
249
  withRenamed<T>(item: object, name: string | undefined, callback: () => T): T;
240
- getUniqueName(resource: object): string;
241
- makeNameValid(name: string): string;
250
+ /**
251
+ * @param primer The basis for the unique identifier. Depending on the strategy, or
252
+ * the names already taken, this may be modified to ensure uniqueness.
253
+ * @param scope The scope in which to generate the identifier. 'global' means
254
+ * the identifier is meant to be unique across the entire program, while
255
+ * 'block' means it cannot shadow any existing identifiers visible from
256
+ * within the current block. After the block is popped, any identifiers
257
+ * defined within it are no longer visible.
258
+ * @returns an identifier that is unique within the given scope
259
+ */
260
+ makeUniqueIdentifier(primer: string | undefined, scope: 'global' | 'block'): string;
261
+ isIdentifierTaken(name: string): boolean;
262
+ /**
263
+ * Makes sure the given identifier cannot be generated by {@link makeUniqueIdentifier}
264
+ * within the given scope.
265
+ * @param name The name to reserve
266
+ * @param scope See {@link makeUniqueIdentifier} for a description of the scope parameter.
267
+ */
268
+ reserveIdentifier(name: string, scope: 'global' | 'block'): void;
242
269
  }
243
270
  /**
244
271
  * Houses a method on the symbol '$resolve` that returns a
package/nameRegistry.d.ts DELETED
@@ -1,30 +0,0 @@
1
- //#region src/nameRegistry.d.ts
2
- interface NameRegistry {
3
- /**
4
- * Creates a valid WGSL identifier, each guaranteed to be unique
5
- * in the lifetime of a single resolution process
6
- * (excluding non-global identifiers from popped scopes).
7
- * Should append "_" to primer, followed by some id.
8
- * @param primer Used in the generation process, makes the identifier more recognizable.
9
- * @param global Whether the name should be registered in the global scope (true), or in the current function scope (false)
10
- */
11
- makeUnique(primer: string | undefined, global: boolean): string;
12
- /**
13
- * Creates a valid WGSL identifier.
14
- * Renames identifiers that are WGSL reserved words.
15
- * @param primer Used in the generation process.
16
- *
17
- * @example
18
- * makeValid("notAKeyword"); // "notAKeyword"
19
- * makeValid("struct"); // makeUnique("struct")
20
- * makeValid("struct_1"); // makeUnique("struct_1") (to avoid potential name collisions)
21
- * makeValid("_"); // ERROR (too difficult to make valid to care)
22
- */
23
- makeValid(primer: string): string;
24
- pushFunctionScope(): void;
25
- popFunctionScope(): void;
26
- pushBlockScope(): void;
27
- popBlockScope(): void;
28
- }
29
- //#endregion
30
- export { NameRegistry };
@@ -1,12 +0,0 @@
1
- //#region src/tgsl/consoleLog/types.ts
2
- const supportedLogOps = [
3
- "log",
4
- "debug",
5
- "info",
6
- "warn",
7
- "error",
8
- "clear"
9
- ];
10
-
11
- //#endregion
12
- export { supportedLogOps };
package/tgsl/math.js DELETED
@@ -1,45 +0,0 @@
1
- import { f32 } from "../data/numeric.js";
2
- import { abs, acos, acosh, asin, asinh, atan, atan2, atanh, ceil, cos, cosh, countLeadingZeros, exp, floor, log, log2, max, min, pow, sign, sin, sinh, sqrt, tan, tanh, trunc } from "../std/numeric.js";
3
-
4
- //#region src/tgsl/math.ts
5
- const mathToStd = {
6
- abs,
7
- acos,
8
- acosh,
9
- asin,
10
- asinh,
11
- atan,
12
- atan2,
13
- atanh,
14
- ceil,
15
- cos,
16
- cosh,
17
- exp,
18
- floor,
19
- fround: f32,
20
- clz32: countLeadingZeros,
21
- trunc,
22
- log,
23
- log2,
24
- pow,
25
- sign,
26
- sin,
27
- sinh,
28
- sqrt,
29
- tan,
30
- tanh,
31
- max,
32
- min,
33
- cbrt: void 0,
34
- log10: void 0,
35
- log1p: void 0,
36
- f16round: void 0,
37
- hypot: void 0,
38
- expm1: void 0,
39
- random: void 0,
40
- imul: void 0,
41
- round: void 0
42
- };
43
-
44
- //#endregion
45
- export { mathToStd };