Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d2d8a83
basic for ... of ... loop compilation
cieplypolar Dec 14, 2025
a2bb186
naming convention fix
cieplypolar Dec 15, 2025
edfaea7
refactor, var vs let, support for vectors
cieplypolar Dec 16, 2025
4909740
Merge branch 'main' into feat/for-of-loop
cieplypolar Dec 16, 2025
f8ac4b8
biome fixes
cieplypolar Dec 16, 2025
8be4dd7
for of in some examples
cieplypolar Dec 16, 2025
d74b998
updated wgsl tests
cieplypolar Dec 16, 2025
934bacf
cleanup
cieplypolar Dec 16, 2025
38bee47
minor test cleanup
cieplypolar Dec 17, 2025
581f7d2
Taking a reference to the iterated value if it's a non-ephemeral element
iwoplaza Dec 18, 2025
5e8cfe0
Revert example change
iwoplaza Dec 18, 2025
c386c45
Free test!
Dec 18, 2025
9b0da3d
more review changes + vector type guard impr
cieplypolar Dec 18, 2025
17c34dc
review changes
cieplypolar Dec 20, 2025
70d515d
Merge branch 'main' into feat/for-of-loop
cieplypolar Dec 20, 2025
41a0165
nested forOf test
cieplypolar Dec 21, 2025
3990078
Merge branch 'feat/for-of-loop' of github.com:software-mansion/TypeGP…
cieplypolar Dec 21, 2025
e47a2cc
the last test, I promise
cieplypolar Dec 21, 2025
80e60bd
test cleanup
cieplypolar Dec 22, 2025
26e0f98
I lied
cieplypolar Dec 22, 2025
3b90675
Merge branch 'main' into feat/for-of-loop
cieplypolar Dec 23, 2025
46ef92a
merge main
cieplypolar Dec 23, 2025
a374551
Merge branch 'main' into feat/for-of-loop
cieplypolar Jan 8, 2026
cd6c194
Merge branch 'main' into feat/for-of-loop
cieplypolar Jan 9, 2026
57bfbae
Merge branch 'main' into feat/for-of-loop
cieplypolar Jan 12, 2026
b6d8b3b
Merge branch 'main' into feat/for-of-loop
cieplypolar Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;

Expand Down
14 changes: 8 additions & 6 deletions packages/tinyest-for-wgsl/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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];
},
Expand Down
23 changes: 17 additions & 6 deletions packages/tinyest/src/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const NodeTypeCatalog = {
while: 15,
continue: 16,
break: 17,
forOf: 18,

// rare
arrayExpr: 100,
Expand Down Expand Up @@ -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'],
Expand All @@ -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
*/
Expand All @@ -109,7 +119,8 @@ export type Statement =
| For
| While
| Continue
| Break;
| Break
| ForOf;

//
// Expression
Expand Down
4 changes: 4 additions & 0 deletions packages/typegpu/src/data/compiledIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
14 changes: 13 additions & 1 deletion packages/typegpu/src/data/wgslTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) &&
Expand Down
117 changes: 115 additions & 2 deletions packages/typegpu/src/tgsl/wgslGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;

Expand Down Expand Up @@ -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;`;
}
Expand Down
Loading