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