diff --git a/src/strands/ir_builders.js b/src/strands/ir_builders.js index ff73748c9b..edbd86cf61 100644 --- a/src/strands/ir_builders.js +++ b/src/strands/ir_builders.js @@ -214,6 +214,46 @@ export function binaryOpNode( } } + const leftDim = dag.dimensions[finalLeftNodeID]; + const rightDim = dag.dimensions[finalRightNodeID]; + const leftBase = dag.baseTypes[finalLeftNodeID]; + const rightBase = dag.baseTypes[finalRightNodeID]; + + const isOrdering = [ + OpCode.Binary.LESS_THAN, + OpCode.Binary.LESS_EQUAL, + OpCode.Binary.GREATER_THAN, + OpCode.Binary.GREATER_EQUAL, + ].includes(opCode); + const isEquality = opCode === OpCode.Binary.EQUAL || opCode === OpCode.Binary.NOT_EQUAL; + const isLogical = opCode === OpCode.Binary.LOGICAL_AND || opCode === OpCode.Binary.LOGICAL_OR; + + if (isOrdering) { + if (leftDim > 1 || rightDim > 1) { + FES.userError( + 'type error', + `${OpCodeToSymbol[opCode]} is only defined for scalars. ` + + `Got ${leftBase}${leftDim} ${OpCodeToSymbol[opCode]} ${rightBase}${rightDim}.` + ); + } + } else if (isEquality) { + if ((leftDim > 1 || rightDim > 1) && (leftDim !== rightDim || leftBase !== rightBase)) { + FES.userError( + 'type error', + `Equality comparisons between vectors require matching dimensions and base types. ` + + `Got ${leftBase}${leftDim} ${OpCodeToSymbol[opCode]} ${rightBase}${rightDim}.` + ); + } + } else if (isLogical) { + if (leftBase !== BaseType.BOOL || rightBase !== BaseType.BOOL || leftDim !== 1 || rightDim !== 1) { + FES.userError( + 'type error', + `${OpCodeToSymbol[opCode]} requires two bool scalars. ` + + `Got ${leftBase}${leftDim} ${OpCodeToSymbol[opCode]} ${rightBase}${rightDim}.` + ); + } + } + if (booleanOpCode[opCode]) { cast.toType.baseType = BaseType.BOOL; cast.toType.dimension = 1; diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 06791151af..97ebb49427 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -3796,6 +3796,118 @@ suite('p5.Shader', function () { assert.include(errMsg, 'float4'); }); + test('ordering comparison with a vector operand throws a clear strands type error', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + try { + myp5.baseMaterialShader().modify( + () => { + myp5.getFinalColor(color => { + if (color < 2) { + color = [1, 1, 1, 1]; + } + return color; + }); + }, + { myp5 } + ); + } catch (e) { + /* expected */ + } + + assert.isAbove( + mockUserError.mock.calls.length, + 0, + 'FES.userError should have been called' + ); + const errMsg = mockUserError.mock.calls[0][1]; + assert.include(errMsg, '<'); + assert.include(errMsg, 'only defined for scalars'); + }); + + test('ordering comparison between scalars is allowed', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + myp5.baseMaterialShader().modify( + () => { + myp5.getFinalColor(color => { + if (color.r < 0.5) { + color = [1, 1, 1, 1]; + } + return color; + }); + }, + { myp5 } + ); + + assert.equal(mockUserError.mock.calls.length, 0); + }); + + test('equality comparison between matching vectors is allowed', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + myp5.baseMaterialShader().modify( + () => { + myp5.getFinalColor(color => { + if (color.equalTo([1, 1, 1, 1])) { + color = [1, 1, 1, 1]; + } + return color; + }); + }, + { myp5 } + ); + + assert.equal(mockUserError.mock.calls.length, 0); + }); + + test('logical and with non-boolean operands throws a clear strands type error', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + try { + myp5.baseMaterialShader().modify( + () => { + myp5.getFinalColor(color => { + if (color.r && color.g) { + color = [1, 1, 1, 1]; + } + return color; + }); + }, + { myp5 } + ); + } catch (e) { + /* expected */ + } + + assert.isAbove( + mockUserError.mock.calls.length, + 0, + 'FES.userError should have been called' + ); + const errMsg = mockUserError.mock.calls[0][1]; + assert.include(errMsg, '&&'); + assert.include(errMsg, 'requires two bool scalars'); + }); + + test('logical and between two boolean scalars is allowed', () => { + myp5.createCanvas(50, 50, myp5.WEBGL); + + myp5.baseMaterialShader().modify( + () => { + myp5.getFinalColor(color => { + if (color.r < 0.5 && color.g > 0.5) { + color = [1, 1, 1, 1]; + } + return color; + }); + }, + { myp5 } + ); + + assert.equal(mockUserError.mock.calls.length, 0); + }); + test('shows a helpful error for web editor loop protection', () => { myp5.createCanvas(50, 50, myp5.WEBGL);