Skip to content

Commit 93c7ddb

Browse files
authored
Merge pull request #9235 from bhabishnu/fix/strands-hook-lifecycle-errors
fix: add friendly errors for hook lifecycle misuse
2 parents 3998034 + b650adc commit 93c7ddb

3 files changed

Lines changed: 144 additions & 2 deletions

File tree

‎src/strands/p5.strands.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { transpileStrandsToJS } from './strands_transpiler';
88
import { BlockType } from './ir_types';
9+
import * as FES from './strands_FES';
910

1011
import { createDirectedAcyclicGraph } from './ir_dag';
1112
import {
@@ -304,6 +305,12 @@ function strands(p5, fn) {
304305
strandsCallback();
305306
}
306307
if (options.hook) strandsContext.renderer._pInst[options.hook].end();
308+
if (strandsContext.activeHook) {
309+
FES.userError(
310+
'scope error',
311+
`It looks like ${strandsContext.activeHook._publicName}.begin() was called without a matching ${strandsContext.activeHook._publicName}.end().`
312+
);
313+
}
307314
popBlock(strandsContext.cfg);
308315

309316
// 3. Generate shader code hooks object from the IR

‎src/strands/strands_api.js‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,9 +1440,14 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
14401440
finishHook();
14411441
};
14421442

1443-
// In the flat strands API, this is how result-returning hooks
1444-
// are used
1443+
// In the flat strands API, result-returning hooks use set().
14451444
hook.set = function (result) {
1445+
if (!hook._active) {
1446+
FES.userError(
1447+
'scope error',
1448+
`It looks like you're trying to call ${hook._publicName}.set() outside of its begin()/end() block.`
1449+
);
1450+
}
14461451
hook._result = result;
14471452
};
14481453
hook._active = false;
@@ -1530,6 +1535,23 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
15301535
strandsContext.activeHook = undefined;
15311536

15321537
const expectedReturnType = hookType.returnType;
1538+
const expectsReturnValue =
1539+
isStructType(expectedReturnType) ||
1540+
(expectedReturnType.dataType &&
1541+
expectedReturnType.typeName?.trim() !== 'void');
1542+
1543+
if (
1544+
expectsReturnValue &&
1545+
userReturned === undefined &&
1546+
hook.earlyReturns.length === 0
1547+
) {
1548+
FES.userError(
1549+
'scope error',
1550+
`${hook._publicName} requires a value. Make sure to call ` +
1551+
`${hook._publicName}.set(value) before ${hook._publicName}.end().`
1552+
);
1553+
}
1554+
15331555
let rootNodeID = null;
15341556
const handleRetVal = retNode => {
15351557
if (isStructType(expectedReturnType)) {
@@ -1674,6 +1696,9 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
16741696
for (const name of aliases) {
16751697
augmentFnTemporary(fn, strandsContext, name, hook);
16761698
}
1699+
// Name used in error messages: an explicit alias like filterColor if
1700+
// there is one, otherwise the unprefixed alias like finalColor
1701+
hook._publicName = aliases[1] ?? hookType.name;
16771702
hook.earlyReturns = [];
16781703
}
16791704
}

‎test/unit/webgl/p5.Shader.js‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4233,6 +4233,116 @@ suite('p5.Shader', function () {
42334233
});
42344234
});
42354235

4236+
test('set() outside a hook block reports a scope error', () => {
4237+
myp5.createCanvas(50, 50, myp5.WEBGL);
4238+
4239+
try {
4240+
myp5.baseFilterShader().modify(
4241+
() => {
4242+
myp5.filterColor.begin();
4243+
myp5.filterColor.set([1, 0, 0, 1]);
4244+
myp5.filterColor.end();
4245+
myp5.filterColor.set([0, 1, 0, 1]);
4246+
},
4247+
{ myp5 }
4248+
);
4249+
} catch {
4250+
/* expected */
4251+
}
4252+
4253+
const scopeCall = mockUserError.mock.calls.find(
4254+
call => call[0] === 'scope error' && call[1].includes('set()')
4255+
);
4256+
assert.isDefined(scopeCall, 'scope error should have been called');
4257+
assert.include(scopeCall[1], 'filterColor.set()');
4258+
assert.notInclude(scopeCall[1], 'getColor');
4259+
});
4260+
4261+
test('ending a value-returning hook without a value reports a scope error', () => {
4262+
myp5.createCanvas(50, 50, myp5.WEBGL);
4263+
4264+
try {
4265+
myp5.baseFilterShader().modify(
4266+
() => {
4267+
myp5.filterColor.begin();
4268+
myp5.filterColor.end();
4269+
},
4270+
{ myp5 }
4271+
);
4272+
} catch {
4273+
/* expected */
4274+
}
4275+
4276+
const scopeCall = mockUserError.mock.calls.find(
4277+
call =>
4278+
call[0] === 'scope error' &&
4279+
call[1].includes('requires a value')
4280+
);
4281+
assert.isDefined(scopeCall, 'scope error should have been called');
4282+
assert.include(scopeCall[1], 'filterColor requires a value');
4283+
assert.include(
4284+
scopeCall[1],
4285+
'filterColor.set(value) before filterColor.end()'
4286+
);
4287+
assert.notInclude(scopeCall[1], 'getColor');
4288+
});
4289+
4290+
test('value-required error uses the hook\'s own public name', () => {
4291+
myp5.createCanvas(50, 50, myp5.WEBGL);
4292+
4293+
try {
4294+
myp5.baseMaterialShader().modify(
4295+
() => {
4296+
myp5.finalColor.begin();
4297+
myp5.finalColor.end();
4298+
},
4299+
{ myp5 }
4300+
);
4301+
} catch {
4302+
/* expected */
4303+
}
4304+
4305+
const scopeCall = mockUserError.mock.calls.find(
4306+
call =>
4307+
call[0] === 'scope error' &&
4308+
call[1].includes('requires a value')
4309+
);
4310+
assert.isDefined(scopeCall, 'scope error should have been called');
4311+
assert.include(scopeCall[1], 'finalColor requires a value');
4312+
assert.include(
4313+
scopeCall[1],
4314+
'finalColor.set(value) before finalColor.end()'
4315+
);
4316+
assert.notInclude(scopeCall[1], 'getFinalColor');
4317+
assert.notInclude(scopeCall[1], 'filterColor');
4318+
});
4319+
4320+
test('missing end() reports a scope error', () => {
4321+
myp5.createCanvas(50, 50, myp5.WEBGL);
4322+
4323+
try {
4324+
myp5.baseFilterShader().modify(
4325+
() => {
4326+
myp5.filterColor.begin();
4327+
myp5.filterColor.set([1, 0, 0, 1]);
4328+
},
4329+
{ myp5 }
4330+
);
4331+
} catch {
4332+
/* expected */
4333+
}
4334+
const scopeCall = mockUserError.mock.calls.find(
4335+
call =>
4336+
call[0] === 'scope error' &&
4337+
call[1].includes('without a matching')
4338+
);
4339+
assert.isDefined(scopeCall, 'scope error should have been called');
4340+
assert.include(
4341+
scopeCall[1],
4342+
'filterColor.begin() was called without a matching filterColor.end()'
4343+
);
4344+
});
4345+
42364346
test('scope error uses unprefixed hook name', () => {
42374347
myp5.createCanvas(50, 50, myp5.WEBGL);
42384348

0 commit comments

Comments
 (0)