diff --git a/client/modules/Preview/jsPreprocess.js b/client/modules/Preview/jsPreprocess.js index e417285655..8a1dd7c3f3 100644 --- a/client/modules/Preview/jsPreprocess.js +++ b/client/modules/Preview/jsPreprocess.js @@ -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, { @@ -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); @@ -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] + }; } } }); diff --git a/client/modules/Preview/jsPreprocess.unit.test.js b/client/modules/Preview/jsPreprocess.unit.test.js index 4fad33c9ae..9220a14ca6 100644 --- a/client/modules/Preview/jsPreprocess.unit.test.js +++ b/client/modules/Preview/jsPreprocess.unit.test.js @@ -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', () => {