diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts index fb47dade46..cde9d100ae 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts @@ -103,9 +103,7 @@ const time = root.createUniform(d.f32); const isInsideObstacle = (x: number, y: number): boolean => { 'use gpu'; - for (let obsIdx = 0; obsIdx < MAX_OBSTACLES; obsIdx++) { - const obs = obstacles.$[obsIdx]; - + for (const obs of obstacles.$) { if (obs.enabled === 0) { continue; } @@ -158,8 +156,7 @@ const computeVelocity = (x: number, y: number): d.v2f => { ]; let dirChoiceCount = 1; - for (let i = 0; i < 4; i++) { - const offset = neighborOffsets[i]; + for (const offset of neighborOffsets) { const neighborDensity = getCell(x + offset.x, y + offset.y); const cost = neighborDensity.z + d.f32(offset.y) * gravityCost; diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 3f0713365d..055494f536 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -236,12 +236,7 @@ const Transpilers: Partial< } if (node.kind === 'const') { - if (init === undefined) { - throw new Error( - 'Did not provide initial value in `const` declaration.', - ); - } - return [NODE.const, id, init]; + return init !== undefined ? [NODE.const, id, init] : [NODE.const, id]; } return init !== undefined ? [NODE.let, id, init] : [NODE.let, id]; @@ -314,6 +309,13 @@ const Transpilers: Partial< return [NODE.while, condition, body]; }, + ForOfStatement(ctx, node) { + const loopVar = transpile(ctx, node.left) as tinyest.Const | tinyest.Let; + const iterable = transpile(ctx, node.right) as tinyest.Expression; + const body = transpile(ctx, node.body) as tinyest.Statement; + return [NODE.forOf, loopVar, iterable, body]; + }, + ContinueStatement() { return [NODE.continue]; }, diff --git a/packages/tinyest/src/nodes.ts b/packages/tinyest/src/nodes.ts index 40bb78efab..79d779693e 100644 --- a/packages/tinyest/src/nodes.ts +++ b/packages/tinyest/src/nodes.ts @@ -23,6 +23,7 @@ export const NodeTypeCatalog = { while: 15, continue: 16, break: 17, + forOf: 18, // rare arrayExpr: 100, @@ -72,11 +73,13 @@ export type Let = /** * Represents a const statement */ -export type Const = readonly [ - type: NodeTypeCatalog['const'], - identifier: string, - value: Expression, -]; +export type Const = + | readonly [type: NodeTypeCatalog['const'], identifier: string] + | readonly [ + type: NodeTypeCatalog['const'], + identifier: string, + value: Expression, + ]; export type For = readonly [ type: NodeTypeCatalog['for'], @@ -96,6 +99,13 @@ export type Continue = readonly [type: NodeTypeCatalog['continue']]; export type Break = readonly [type: NodeTypeCatalog['break']]; +export type ForOf = readonly [ + type: NodeTypeCatalog['forOf'], + left: Const | Let, + right: Expression, + body: Statement, +]; + /** * A union type of all statements */ @@ -109,7 +119,8 @@ export type Statement = | For | While | Continue - | Break; + | Break + | ForOf; // // Expression diff --git a/packages/typegpu/src/data/compiledIO.ts b/packages/typegpu/src/data/compiledIO.ts index 746f79e411..2668489717 100644 --- a/packages/typegpu/src/data/compiledIO.ts +++ b/packages/typegpu/src/data/compiledIO.ts @@ -196,6 +196,10 @@ export function buildWriter( } if (wgsl.isVec(node)) { + if (wgsl.isVecBool(node)) { + throw new Error('Compiled writers do not support boolean vectors'); + } + const primitive = typeToPrimitive[node.type]; let code = ''; const writeFunc = primitiveToWriteFunction[primitive]; diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index 7e9fef27b2..942bf1d72c 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -1688,17 +1688,29 @@ export function isVec( | Vec2h | Vec2i | Vec2u + | Vec2b | Vec3f | Vec3h | Vec3i | Vec3u + | Vec3b | Vec4f | Vec4h | Vec4i - | Vec4u { + | Vec4u + | Vec4b { return isVec2(value) || isVec3(value) || isVec4(value); } +export function isVecBool( + value: unknown, +): value is + | Vec2b + | Vec3b + | Vec4b { + return isVec(value) && value.type.includes('b'); +} + export function isMatInstance(value: unknown): value is AnyMatInstance { const v = value as AnyMatInstance | undefined; return isMarkedInternal(v) && diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 900a51edc4..8d8bca327c 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -21,9 +21,8 @@ import { import * as wgsl from '../data/wgslTypes.ts'; import { invariant, ResolutionError, WgslTypeError } from '../errors.ts'; import { getName } from '../shared/meta.ts'; -import { isMarkedInternal } from '../shared/symbols.ts'; +import { $internal, isMarkedInternal } from '../shared/symbols.ts'; import { safeStringify } from '../shared/stringify.ts'; -import { $internal } from '../shared/symbols.ts'; import { pow } from '../std/numeric.ts'; import { add, div, mul, neg, sub } from '../std/operators.ts'; import { type FnArgsConversionHint, isKnownAtComptime } from '../types.ts'; @@ -44,6 +43,7 @@ import type { DualFn } from '../data/dualFn.ts'; import { createPtrFromOrigin, implicitFrom, ptrFn } from '../data/ptr.ts'; import { RefOperator } from '../data/ref.ts'; import { constant } from '../core/constant/tgpuConstant.ts'; +import { arrayLength } from '../std/array.ts'; const { NodeTypeCatalog: NODE } = tinyest; @@ -1058,6 +1058,119 @@ ${this.ctx.pre}else ${alternate}`; return `${this.ctx.pre}while (${conditionStr}) ${bodyStr}`; } + if (statement[0] === NODE.forOf) { + const [_, loopVar, iterable, body] = statement; + const iterableSnippet = this.expression(iterable); + + if (isEphemeralSnippet(iterableSnippet)) { + throw new Error( + '`for ... of ...` loops only support iterables stored in variables', + ); + } + + // Our index name will be some element from infinite sequence (i, ii, iii, ...). + // If user defines `i` and `ii` before `for ... of ...` loop, then our index name will be `iii`. + // If user defines `i` inside `for ... of ...` then it will be scoped to a new block, + // so we can safely use `i`. + let index = 'i'; // it will be valid name, no need to call this.ctx.makeNameValid + while (this.ctx.getById(index) !== null) { + index += 'i'; + } + + const elementSnippet = accessIndex( + iterableSnippet, + snip(index, u32, 'runtime'), + ); + if (!elementSnippet) { + throw new WgslTypeError( + '`for ... of ...` loops only support array or vector iterables', + ); + } + + const iterableDataType = iterableSnippet.dataType; + let elementCountSnippet: Snippet; + let elementType = elementSnippet.dataType; + if (wgsl.isWgslArray(iterableDataType)) { + elementCountSnippet = iterableDataType.elementCount > 0 + ? snip( + `${iterableDataType.elementCount}`, + u32, + 'constant', + ) + : arrayLength[$internal].gpuImpl(iterableSnippet); + } else if (wgsl.isVec(iterableDataType)) { + elementCountSnippet = snip( + `${Number(iterableDataType.type.match(/\d/))}`, + u32, + 'constant', + ); + } else { + throw new WgslTypeError( + '`for ... of ...` loops only support array or vector iterables', + ); + } + + if (loopVar[0] !== NODE.const) { + throw new WgslTypeError( + 'Only `for (const ... of ... )` loops are supported', + ); + } + + // If it's ephemeral, it's a value that cannot change. If it's a reference, we take + // an implicit pointer to it + let loopVarKind = 'let'; + const loopVarName = this.ctx.makeNameValid(loopVar[1]); + + if (!isEphemeralSnippet(elementSnippet)) { + if (elementSnippet.origin === 'constant-tgpu-const-ref') { + loopVarKind = 'const'; + } else if (elementSnippet.origin === 'runtime-tgpu-const-ref') { + loopVarKind = 'let'; + } else { + loopVarKind = 'let'; + if (!wgsl.isPtr(elementType)) { + const ptrType = createPtrFromOrigin( + elementSnippet.origin, + concretize(elementType as wgsl.AnyWgslData) as wgsl.StorableData, + ); + invariant( + ptrType !== undefined, + `Creating pointer type from origin ${elementSnippet.origin}`, + ); + elementType = ptrType; + } + + elementType = implicitFrom(elementType); + } + } + + const loopVarSnippet = snip( + loopVarName, + elementType, + elementSnippet.origin, + ); + this.ctx.defineVariable(loopVarName, loopVarSnippet); + + const forStr = stitch`${this.ctx.pre}for (var ${index} = 0; ${index} < ${ + tryConvertSnippet(elementCountSnippet, u32, false) + }; ${index}++) {`; + + this.ctx.indent(); + + const loopVarDeclStr = + stitch`${this.ctx.pre}${loopVarKind} ${loopVarName} = ${ + tryConvertSnippet(elementSnippet, elementType as AnyData, false) + };`; + + const bodyStr = `${this.ctx.pre}${ + this.block(blockifySingleStatement(body)) + }`; + + this.ctx.dedent(); + + return stitch`${forStr}\n${loopVarDeclStr}\n${bodyStr}\n${this.ctx.pre}}`; + } + if (statement[0] === NODE.continue) { return `${this.ctx.pre}continue;`; } diff --git a/packages/typegpu/tests/examples/individual/fluid-double-buffering.test.ts b/packages/typegpu/tests/examples/individual/fluid-double-buffering.test.ts index fbf3f732d8..9826333df6 100644 --- a/packages/typegpu/tests/examples/individual/fluid-double-buffering.test.ts +++ b/packages/typegpu/tests/examples/individual/fluid-double-buffering.test.ts @@ -36,17 +36,19 @@ describe('fluid double buffering example', () => { @group(0) @binding(1) var obstacles: array; fn isInsideObstacle(x: i32, y: i32) -> bool { - for (var obsIdx = 0; (obsIdx < 4i); obsIdx++) { - let obs = (&obstacles[obsIdx]); - if (((*obs).enabled == 0u)) { - continue; - } - let minX = max(0i, ((*obs).center.x - i32((f32((*obs).size.x) / 2f)))); - let maxX = min(256i, ((*obs).center.x + i32((f32((*obs).size.x) / 2f)))); - let minY = max(0i, ((*obs).center.y - i32((f32((*obs).size.y) / 2f)))); - let maxY = min(256i, ((*obs).center.y + i32((f32((*obs).size.y) / 2f)))); - if (((((x >= minX) && (x <= maxX)) && (y >= minY)) && (y <= maxY))) { - return true; + for (var i = 0; i < 4; i++) { + let obs = (&obstacles[i]); + { + if (((*obs).enabled == 0u)) { + continue; + } + let minX = max(0i, ((*obs).center.x - i32((f32((*obs).size.x) / 2f)))); + let maxX = min(256i, ((*obs).center.x + i32((f32((*obs).size.x) / 2f)))); + let minY = max(0i, ((*obs).center.y - i32((f32((*obs).size.y) / 2f)))); + let maxY = min(256i, ((*obs).center.y + i32((f32((*obs).size.y) / 2f)))); + if (((((x >= minX) && (x <= maxX)) && (y >= minY)) && (y <= maxY))) { + return true; + } } } return false; @@ -129,17 +131,19 @@ describe('fluid double buffering example', () => { @group(0) @binding(3) var obstacles: array; fn isInsideObstacle(x: i32, y: i32) -> bool { - for (var obsIdx = 0; (obsIdx < 4i); obsIdx++) { - let obs = (&obstacles[obsIdx]); - if (((*obs).enabled == 0u)) { - continue; - } - let minX = max(0i, ((*obs).center.x - i32((f32((*obs).size.x) / 2f)))); - let maxX = min(256i, ((*obs).center.x + i32((f32((*obs).size.x) / 2f)))); - let minY = max(0i, ((*obs).center.y - i32((f32((*obs).size.y) / 2f)))); - let maxY = min(256i, ((*obs).center.y + i32((f32((*obs).size.y) / 2f)))); - if (((((x >= minX) && (x <= maxX)) && (y >= minY)) && (y <= maxY))) { - return true; + for (var i = 0; i < 4; i++) { + let obs = (&obstacles[i]); + { + if (((*obs).enabled == 0u)) { + continue; + } + let minX = max(0i, ((*obs).center.x - i32((f32((*obs).size.x) / 2f)))); + let maxX = min(256i, ((*obs).center.x + i32((f32((*obs).size.x) / 2f)))); + let minY = max(0i, ((*obs).center.y - i32((f32((*obs).size.y) / 2f)))); + let maxY = min(256i, ((*obs).center.y + i32((f32((*obs).size.y) / 2f)))); + if (((((x >= minX) && (x <= maxX)) && (y >= minY)) && (y <= maxY))) { + return true; + } } } return false; @@ -174,22 +178,24 @@ describe('fluid double buffering example', () => { var leastCost = cell.z; var dirChoices = array(vec2f(), vec2f(), vec2f(), vec2f()); var dirChoiceCount = 1; - for (var i = 0; (i < 4i); i++) { + for (var i = 0; i < 4; i++) { let offset = (&neighborOffsets[i]); - var neighborDensity = getCell((x + (*offset).x), (y + (*offset).y)); - let cost = (neighborDensity.z + (f32((*offset).y) * gravityCost)); - if (!isValidFlowOut((x + (*offset).x), (y + (*offset).y))) { - continue; - } - if ((cost == leastCost)) { - dirChoices[dirChoiceCount] = vec2f(f32((*offset).x), f32((*offset).y)); - dirChoiceCount++; - } - else { - if ((cost < leastCost)) { - leastCost = cost; - dirChoices[0i] = vec2f(f32((*offset).x), f32((*offset).y)); - dirChoiceCount = 1i; + { + var neighborDensity = getCell((x + (*offset).x), (y + (*offset).y)); + let cost = (neighborDensity.z + (f32((*offset).y) * gravityCost)); + if (!isValidFlowOut((x + (*offset).x), (y + (*offset).y))) { + continue; + } + if ((cost == leastCost)) { + dirChoices[dirChoiceCount] = vec2f(f32((*offset).x), f32((*offset).y)); + dirChoiceCount++; + } + else { + if ((cost < leastCost)) { + leastCost = cost; + dirChoices[0i] = vec2f(f32((*offset).x), f32((*offset).y)); + dirChoiceCount = 1i; + } } } } @@ -305,17 +311,19 @@ describe('fluid double buffering example', () => { @group(0) @binding(3) var obstacles: array; fn isInsideObstacle(x: i32, y: i32) -> bool { - for (var obsIdx = 0; (obsIdx < 4i); obsIdx++) { - let obs = (&obstacles[obsIdx]); - if (((*obs).enabled == 0u)) { - continue; - } - let minX = max(0i, ((*obs).center.x - i32((f32((*obs).size.x) / 2f)))); - let maxX = min(256i, ((*obs).center.x + i32((f32((*obs).size.x) / 2f)))); - let minY = max(0i, ((*obs).center.y - i32((f32((*obs).size.y) / 2f)))); - let maxY = min(256i, ((*obs).center.y + i32((f32((*obs).size.y) / 2f)))); - if (((((x >= minX) && (x <= maxX)) && (y >= minY)) && (y <= maxY))) { - return true; + for (var i = 0; i < 4; i++) { + let obs = (&obstacles[i]); + { + if (((*obs).enabled == 0u)) { + continue; + } + let minX = max(0i, ((*obs).center.x - i32((f32((*obs).size.x) / 2f)))); + let maxX = min(256i, ((*obs).center.x + i32((f32((*obs).size.x) / 2f)))); + let minY = max(0i, ((*obs).center.y - i32((f32((*obs).size.y) / 2f)))); + let maxY = min(256i, ((*obs).center.y + i32((f32((*obs).size.y) / 2f)))); + if (((((x >= minX) && (x <= maxX)) && (y >= minY)) && (y <= maxY))) { + return true; + } } } return false; @@ -350,22 +358,24 @@ describe('fluid double buffering example', () => { var leastCost = cell.z; var dirChoices = array(vec2f(), vec2f(), vec2f(), vec2f()); var dirChoiceCount = 1; - for (var i = 0; (i < 4i); i++) { + for (var i = 0; i < 4; i++) { let offset = (&neighborOffsets[i]); - var neighborDensity = getCell((x + (*offset).x), (y + (*offset).y)); - let cost = (neighborDensity.z + (f32((*offset).y) * gravityCost)); - if (!isValidFlowOut((x + (*offset).x), (y + (*offset).y))) { - continue; - } - if ((cost == leastCost)) { - dirChoices[dirChoiceCount] = vec2f(f32((*offset).x), f32((*offset).y)); - dirChoiceCount++; - } - else { - if ((cost < leastCost)) { - leastCost = cost; - dirChoices[0i] = vec2f(f32((*offset).x), f32((*offset).y)); - dirChoiceCount = 1i; + { + var neighborDensity = getCell((x + (*offset).x), (y + (*offset).y)); + let cost = (neighborDensity.z + (f32((*offset).y) * gravityCost)); + if (!isValidFlowOut((x + (*offset).x), (y + (*offset).y))) { + continue; + } + if ((cost == leastCost)) { + dirChoices[dirChoiceCount] = vec2f(f32((*offset).x), f32((*offset).y)); + dirChoiceCount++; + } + else { + if ((cost < leastCost)) { + leastCost = cost; + dirChoices[0i] = vec2f(f32((*offset).x), f32((*offset).y)); + dirChoiceCount = 1i; + } } } } @@ -474,17 +484,19 @@ describe('fluid double buffering example', () => { @group(0) @binding(1) var obstacles: array; fn isInsideObstacle(x: i32, y: i32) -> bool { - for (var obsIdx = 0; (obsIdx < 4i); obsIdx++) { - let obs = (&obstacles[obsIdx]); - if (((*obs).enabled == 0u)) { - continue; - } - let minX = max(0i, ((*obs).center.x - i32((f32((*obs).size.x) / 2f)))); - let maxX = min(256i, ((*obs).center.x + i32((f32((*obs).size.x) / 2f)))); - let minY = max(0i, ((*obs).center.y - i32((f32((*obs).size.y) / 2f)))); - let maxY = min(256i, ((*obs).center.y + i32((f32((*obs).size.y) / 2f)))); - if (((((x >= minX) && (x <= maxX)) && (y >= minY)) && (y <= maxY))) { - return true; + for (var i = 0; i < 4; i++) { + let obs = (&obstacles[i]); + { + if (((*obs).enabled == 0u)) { + continue; + } + let minX = max(0i, ((*obs).center.x - i32((f32((*obs).size.x) / 2f)))); + let maxX = min(256i, ((*obs).center.x + i32((f32((*obs).size.x) / 2f)))); + let minY = max(0i, ((*obs).center.y - i32((f32((*obs).size.y) / 2f)))); + let maxY = min(256i, ((*obs).center.y + i32((f32((*obs).size.y) / 2f)))); + if (((((x >= minX) && (x <= maxX)) && (y >= minY)) && (y <= maxY))) { + return true; + } } } return false; diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 3c35557242..331e579a4a 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -326,7 +326,7 @@ describe('wgslGenerator', () => { // Check for: const value = std.atomicLoad(testUsage.value.b.aa[idx]!.y); // ^ this part should be a i32 const res = wgslGenerator.expression( - (astInfo.ast?.body[1][0] as tinyest.Const)[2], + (astInfo.ast?.body[1][0] as tinyest.Const)[2] as tinyest.Expression, ); expect(res.dataType).toStrictEqual(d.i32); @@ -336,7 +336,7 @@ describe('wgslGenerator', () => { ctx[$internal].itemStateStack.pushBlockScope(); wgslGenerator.blockVariable('var', 'value', d.i32, 'runtime'); const res2 = wgslGenerator.expression( - (astInfo.ast?.body[1][1] as tinyest.Const)[2], + (astInfo.ast?.body[1][1] as tinyest.Const)[2] as tinyest.Expression, ); ctx[$internal].itemStateStack.popBlockScope(); @@ -449,6 +449,403 @@ describe('wgslGenerator', () => { `); }); + it('parses correctly "for ... of ..." statements', () => { + const main1 = () => { + 'use gpu'; + const arr = [1, 2, 3]; + for (const foo of arr) { + // biome-ignore lint/complexity/noUselessContinue: it's a part of the test + continue; + } + }; + + const main2 = () => { + 'use gpu'; + const arr = [1, 2, 3]; + // biome-ignore lint/style/useConst: it's a part of the test + for (let foo of arr) { + // biome-ignore lint/complexity/noUselessContinue: it's a part of the test + continue; + } + }; + + const parsed1 = getMetaData(main1)?.ast?.body; + expect(JSON.stringify(parsed1)).toMatchInlineSnapshot( + `"[0,[[13,"arr",[100,[[5,"1"],[5,"2"],[5,"3"]]]],[18,[13,"foo"],"arr",[0,[[16]]]]]]"`, + ); + + const parsed2 = getMetaData(main2)?.ast?.body; + expect(JSON.stringify(parsed2)).toMatchInlineSnapshot( + `"[0,[[13,"arr",[100,[[5,"1"],[5,"2"],[5,"3"]]]],[18,[12,"foo"],"arr",[0,[[16]]]]]]"`, + ); + }); + + it('creates correct code for "for ... of ..." statement using array of primitives', () => { + const main = () => { + 'use gpu'; + const arr = d.arrayOf(d.f32, 3)([1, 2, 3]); + let res = d.f32(); + for (const foo of arr) { + res += foo; + const i = res; // this is intentional, scoping `i` to a block separate from `arr` index + } + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() { + var arr = array(1f, 2f, 3f); + var res = 0f; + for (var i = 0; i < 3; i++) { + let foo = arr[i]; + { + res += foo; + let i = res; + } + } + }" + `); + }); + + it('creates correct code for "for ... of ..." nested statements', () => { + const main = () => { + 'use gpu'; + const arr = d.arrayOf(d.f32, 3)([1, 2, 3]); + let res = d.f32(); + for (const foo of arr) { + for (const boo of arr) { + res += foo * boo; + } + } + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() { + var arr = array(1f, 2f, 3f); + var res = 0f; + for (var i = 0; i < 3; i++) { + let foo = arr[i]; + { + for (var i = 0; i < 3; i++) { + let boo = arr[i]; + { + res += (foo * boo); + } + } + } + } + }" + `); + }); + + it('creates correct code for "for ... of ..." nested statements that use the same variable name', () => { + const main = () => { + 'use gpu'; + const arr = d.arrayOf(d.f32, 3)([1, 2, 3]); + let res = d.f32(); + for (const foo of arr) { + for (const foo of arr) { + res += foo * foo; + } + } + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() { + var arr = array(1f, 2f, 3f); + var res = 0f; + for (var i = 0; i < 3; i++) { + let foo = arr[i]; + { + for (var i = 0; i < 3; i++) { + let foo2 = arr[i]; + { + res += (foo2 * foo2); + } + } + } + } + }" + `); + }); + + it('creates correct code for "for ... of ..." statement using array of non-primitives', () => { + const main = () => { + 'use gpu'; + const arr = d.arrayOf(d.vec2f, 3)([d.vec2f(1), d.vec2f(2), d.vec2f(3)]); + let res = 0; + for (const foo of arr) { + res += foo.x; + } + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() { + var arr = array(vec2f(1), vec2f(2), vec2f(3)); + var res = 0; + for (var i = 0; i < 3; i++) { + let foo = (&arr[i]); + { + res += i32((*foo).x); + } + } + }" + `); + }); + + it('creates correct code for "for ... of ..." statement using runtime size array', ({ root }) => { + const layout = tgpu.bindGroupLayout({ + arr: { storage: d.arrayOf(d.f32) }, + }); + + const main = () => { + 'use gpu'; + let res = d.f32(0); + for (const foo of layout.$.arr) { + res += foo; + } + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var arr: array; + + fn main() { + var res = 0f; + for (var i = 0; i < arrayLength((&arr)); i++) { + let foo = arr[i]; + { + res += foo; + } + } + }" + `); + }); + + it('creates correct code for "for ... of ..." statements using derived and comptime iterables', () => { + const comptimeVec = tgpu['~unstable'].comptime(() => d.vec2f(1, 2)); + + const main = () => { + 'use gpu'; + const v1 = derivedV4u.$; + for (const foo of v1) { + // biome-ignore lint/complexity/noUselessContinue: it's a part of the test + continue; + } + + const v2 = comptimeVec(); + for (const foo of v2) { + // biome-ignore lint/complexity/noUselessContinue: it's a part of the test + continue; + } + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() { + var v1 = vec4u(44, 88, 132, 176); + for (var i = 0; i < 4; i++) { + let foo = v1[i]; + { + continue; + } + } + var v2 = vec2f(1, 2); + for (var i = 0; i < 2; i++) { + let foo = v2[i]; + { + continue; + } + } + }" + `); + }); + + it('creates correct code for "for ... of ..." statements using vector iterables', () => { + const main = () => { + 'use gpu'; + const v1 = d.vec4f(1, 2, 3, 4); + const v2 = d.vec3u(5, 6, 7); + const v3 = d.vec2b(true, false); + + let res1 = d.f32(); + let res2 = d.u32(); + let res3 = d.bool(); + + for (const foo of v1) { + res1 += foo; + } + + for (const foo of v2) { + res2 *= foo; + } + + for (const foo of v3) { + res3 = foo !== res3; + } + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "fn main() { + var v1 = vec4f(1, 2, 3, 4); + var v2 = vec3u(5, 6, 7); + var v3 = vec2(true, false); + var res1 = 0f; + var res2 = 0u; + var res3 = false; + for (var i = 0; i < 4; i++) { + let foo = v1[i]; + { + res1 += foo; + } + } + for (var i = 0; i < 3; i++) { + let foo = v2[i]; + { + res2 *= foo; + } + } + for (var i = 0; i < 2; i++) { + let foo = v3[i]; + { + res3 = (foo != res3); + } + } + }" + `); + }); + + it('creates correct code for "for ... of ..." statement using a struct member iterable', () => { + const TestStruct = d.struct({ + arr: d.arrayOf(d.f32, 4), + }); + + const main = () => { + 'use gpu'; + const testStruct = TestStruct({ arr: [1, 8, 8, 2] }); + for (const foo of testStruct.arr) { + // biome-ignore lint/complexity/noUselessContinue: it's a part of the test + continue; + } + }; + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "struct TestStruct { + arr: array, + } + + fn main() { + var testStruct = TestStruct(array(1f, 8f, 8f, 2f)); + for (var i = 0; i < 4; i++) { + let foo = testStruct.arr[i]; + { + continue; + } + } + }" + `); + }); + + it('throws error when "for ... of ..." statement uses an ephemeral iterable', () => { + const main = () => { + 'use gpu'; + for (const foo of [1, 2, 3]) { + // biome-ignore lint/complexity/noUselessContinue: it's a part of the test + continue; + } + }; + + expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:main + - fn*:main(): \`for ... of ...\` loops only support iterables stored in variables] + `); + }); + + it('throws error when "for ... of ..." statement uses iterable that is not an array or a vector', () => { + const TestStruct = d.struct({ + x: d.u32, + y: d.f32, + }); + + const main = () => { + 'use gpu'; + const testStruct = TestStruct({ x: 1, y: 2 }); + //@ts-expect-error: let's assume it has an iterator + for (const foo of testStruct) { + // biome-ignore lint/complexity/noUselessContinue: it's a part of the test + continue; + } + }; + + expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:main + - fn*:main(): \`for ... of ...\` loops only support array or vector iterables] + `); + }); + + it('throws error when "for ... of ..." statement uses let declarator', () => { + const main = () => { + 'use gpu'; + const arr = [1, 2, 3]; + // biome-ignore lint/style/useConst: it's a part of the test + for (let foo of arr) { + } + }; + + expect(() => tgpu.resolve([main])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:main + - fn*:main(): Only \`for (const ... of ... )\` loops are supported] + `); + }); + + it('handles "for ... of ..." internal index variable when "i" is used by user', () => { + const f1 = () => { + 'use gpu'; + const arr = [1, 2, 3]; + for (const foo of arr) { + const i = foo; + } + }; + + expect(tgpu.resolve([f1])).toMatchInlineSnapshot(` + "fn f1() { + var arr = array(1, 2, 3); + for (var i = 0; i < 3; i++) { + let foo = arr[i]; + { + let i = foo; + } + } + }" + `); + + const f2 = () => { + 'use gpu'; + const i = 7; + const arr = [1, 2, 3]; + for (const foo of arr) { + // biome-ignore lint/complexity/noUselessContinue: it's a part of the test + continue; + } + }; + + expect(tgpu.resolve([f2])).toMatchInlineSnapshot(` + "fn f2() { + const i = 7; + var arr = array(1, 2, 3); + for (var ii = 0; ii < 3; ii++) { + let foo = arr[ii]; + { + continue; + } + } + }" + `); + }); + it('creates correct resources for derived values and slots', () => { const testFn = tgpu.fn([], d.vec4u)(() => { return derivedV4u.value;