Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 28 additions & 8 deletions client/modules/Preview/jsPreprocess.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,18 +144,25 @@ function collectLoopsToProtect(ast, shaderNames) {
if (isInsideShader) return;

let parentBlock = null;
// directParent is the immediate ancestor that contains this loop as its
// body (e.g. an outer for-loop whose body is this loop without braces).
let directParent = null;
for (let i = ancestors.length - 1; i >= 0; i--) {
const ancestor = ancestors[i];
if (
ancestor !== node &&
(ancestor.type === 'BlockStatement' || ancestor.type === 'Program')
) {
parentBlock = ancestor;
break;
if (ancestor !== node) {
// Record the first non-self ancestor as the direct parent
if (directParent === null) {
directParent = ancestor;
}

if (ancestor.type === 'BlockStatement' || ancestor.type === 'Program') {
parentBlock = ancestor;
break;
}
}
}

loops.push({ loop: node, parentBlock });
loops.push({ loop: node, parentBlock, directParent });
}

walk.ancestor(ast, {
Expand All @@ -168,7 +175,7 @@ function collectLoopsToProtect(ast, shaderNames) {
}

function injectProtection(loops) {
loops.forEach(({ loop, parentBlock }, idx) => {
loops.forEach(({ loop, parentBlock, directParent }, idx) => {
const varName = `_LP${idx}`;
const { line } = loop.loc.start;
const check = makeCheckStatement(varName, line);
Expand All @@ -184,6 +191,19 @@ function injectProtection(loops) {
const nodeIdx = parentBlock.body.indexOf(loop);
if (nodeIdx !== -1) {
parentBlock.body.splice(nodeIdx, 0, varDecl);
} else if (directParent && directParent.body === loop) {
// The loop is the un-braced body of a parent control statement
// (e.g. `for(...) for(...) stmt` — inner loop without braces).
// We cannot splice into parentBlock because the loop is not directly
// in its body array, so the varDecl would never be declared while the
// check inside the loop still references it → ReferenceError: _LP${idx}
// is not defined.
// Fix: wrap the directParent's body in a BlockStatement that prepends
// the varDecl so the variable is always in scope.
directParent.body = {
type: 'BlockStatement',
body: [varDecl, loop]
};
}
}
});
Expand Down
26 changes: 26 additions & 0 deletions client/modules/Preview/jsPreprocess.unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ describe('jsPreprocess', () => {
const result = jsPreprocess(code, '');
expect(result).toContain('window.loopProtect.hit');
});

it('adds loop protection to nested for loops without braces', () => {
// Regression: inner loop of `for (...) for (...) stmt` is not directly in
// a BlockStatement, so its timer variable declaration was silently dropped
// while the check still referenced it → ReferenceError: _LP0 is not defined.
const code = `
for (let h = 0; h < 40; h++)
for (let i = 0; i < 50; i++)
doSomething();
`;
const result = jsPreprocess(code, '');
expect(result).toContain('window.loopProtect.hit');
// Both loops must be protected — two hit-checks in output
expect(result.match(/window\.loopProtect\.hit/g).length).toBe(2);
});

it('adds loop protection to a while loop nested in a for loop without braces', () => {
const code = `
for (let h = 0; h < 10; h++)
while (condition())
doSomething();
`;
const result = jsPreprocess(code, '');
expect(result).toContain('window.loopProtect.hit');
expect(result.match(/window\.loopProtect\.hit/g).length).toBe(2);
});
});

describe('shader strings', () => {
Expand Down