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
6 changes: 5 additions & 1 deletion src/strands/ir_builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,11 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) {
scalars.push(createStrandsNode(id, dimension, strandsContext));
}
} else {
FES.userError('type error', `Swizzle assignment: RHS vector does not match LHS vector (need ${chars.length}, got ${value.dimension}).`);
FES.dimensionMismatchError(
chars.length,
value.dimension,
`${target._originalIdentifier || 'value'}.${property}`
);
}
} else if (Array.isArray(value)) {
const flat = value.flat(Infinity);
Expand Down
7 changes: 7 additions & 0 deletions src/strands/strands_FES.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ export function internalError(errorMessage) {
export function userError(errorType, errorMessage) {
const prefixedMessage = `[p5.strands ${errorType}]: ${errorMessage}`;
throw new Error(prefixedMessage);
}

export function dimensionMismatchError(declaredDim,actualDim,varName){
userError(
'dimension mismatch',
`Cannot assign a value of dimension ${actualDim} to \`${varName}\`, which expects dimension ${declaredDim}.`
);
}
8 changes: 8 additions & 0 deletions src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,14 @@ function createHookArguments(strandsContext, parameters){
return createStrandsNode(propNode.id, propNode.dimension, strandsContext, onRebind);
},
set(val) {

if(val?.isStrandsNode&&val.dimension!==propertyType.dataType.dimension){
FES.dimensionMismatchError(
propertyType.dataType.dimension,
val.dimension,
`${param.name}.${propertyType.name}`
);
}
const oldDependsOn = dag.dependsOn[structNode.id];
const newDependsOn = [...oldDependsOn];
let newValueID;
Expand Down
15 changes: 15 additions & 0 deletions src/strands/strands_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { swizzleTrap, primitiveConstructorNode, variableNode, arrayAccessNode, a
import { BaseType, NodeType, OpCode } from './ir_types';
import { getNodeDataFromID, createNodeData, getOrCreateNode } from './ir_dag';
import { recordInBasicBlock } from './ir_cfg';
import { dimensionMismatchError } from './strands_FES';
export class StrandsNode {
constructor(id, dimension, strandsContext) {
this.id = id;
Expand Down Expand Up @@ -56,6 +57,13 @@ export class StrandsNode {

// For varying variables, we need both assignment generation AND a way to reference by identifier
if (this._originalIdentifier) {
if(value?.isStrandsNode && value.dimension!==this._originalDimension){
dimensionMismatchError(
this._originalDimension,
value.dimension,
this._originalIdentifier
);
}
// Create a variable node for the target (the varying variable)
const { id: targetVarID } = variableNode(
this.strandsContext,
Expand Down Expand Up @@ -108,6 +116,13 @@ export class StrandsNode {

// For varying variables, create swizzle assignment
if (this._originalIdentifier) {
if(value?.isStrandsNode && value.dimension!==swizzlePattern.length){
dimensionMismatchError(
swizzlePattern.length,
value.dimension,
`${this._originalIdentifier}.${swizzlePattern}`
);
}
// Create a variable node for the target with swizzle
const { id: targetVarID } = variableNode(
this.strandsContext,
Expand Down
65 changes: 58 additions & 7 deletions test/unit/webgl/p5.Shader.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import p5 from '../../../src/app.js';
import { vi } from 'vitest';
import { beforeEach, vi } from 'vitest';

const mockUserError = vi.fn();
vi.mock('../../../src/strands/strands_FES', () => ({
userError: (...args) => {
vi.mock('../../../src/strands/strands_FES', () => {
const userError = (...args) => {
mockUserError(...args);
const prefixedMessage = `[p5.strands ${args[0]}]: ${args[1]}`;
throw new Error(prefixedMessage);
},
internalError: (msg) => { throw new Error(`[p5.strands internal error]: ${msg}`); }
}));
};
return {
userError,
internalError: (msg) => { throw new Error(`[p5.strands internal error]: ${msg}`); },
dimensionMismatchError: (declaredDim, actualDim, varName) => {
userError(
'dimension mismatch',
`Cannot assign a value of dimension ${actualDim} to \`${varName}\`, which expects dimension ${declaredDim}.`
);
},
};
});

suite('p5.Shader', function() {
var myp5;
Expand Down Expand Up @@ -2608,6 +2617,48 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca
assert.approximately(pixelColor[1], 0, 5);
assert.approximately(pixelColor[2], 0, 5);
});

test('reports a friendly error when assigning a scalar to a sharedVec3 (bridge)', async () => {
await myp5.createCanvas(5, 5, myp5.WEBGL);

expect(() => {
myp5.baseMaterialShader().modify(() => {
let worldPosX = myp5.sharedVec3();
myp5.getWorldInputs(inputs => {
worldPosX = inputs.position.x; // scalar → vec3 mismatch
return inputs;
});
},{myp5});
}).toThrow(/dimension mismatch/);
});

test('reports a friendly error on dimension mismatch via swizzle write (bridgeSwizzle)', async () => {
await myp5.createCanvas(5, 5, myp5.WEBGL);

expect(() => {
myp5.baseMaterialShader().modify(() => {
let myVec = myp5.sharedVec3();
myp5.getWorldInputs(inputs => {
myVec.xy = inputs.position; // vec3 → 2-component swizzle mismatch
return inputs;
});
},{myp5});
}).toThrow(/dimension mismatch/);
});

test('does not error when shared variable assignment dimensions match', async () => {
await myp5.createCanvas(5, 5, myp5.WEBGL);

expect(() => {
myp5.baseMaterialShader().modify(() => {
let myVec = myp5.sharedVec3();
myp5.getWorldInputs(inputs => {
myVec = inputs.position; // vec3 → vec3, OK
return inputs;
});
},{myp5});
}).not.toThrow();
});
});

suite('p5.strands error messages', () => {
Expand All @@ -2625,7 +2676,7 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca
assert.include(err.message, '// noprotect');
};

afterEach(() => {
beforeEach(() => {
mockUserError.mockClear();
});

Expand Down
42 changes: 42 additions & 0 deletions test/unit/webgpu/p5.Shader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,48 @@ suite('WebGPU p5.Shader', function() {
myp5.compute(s4, 4);
}).not.toThrow();
});

test('reports a friendly error when assigning a scalar to a sharedVec3 (bridge)', async () => {
await myp5.createCanvas(5, 5, myp5.WEBGPU);

expect(() => {
myp5.baseMaterialShader().modify(() => {
let worldPosX = myp5.sharedVec3();
myp5.getWorldInputs(inputs => {
worldPosX = inputs.position.x; // scalar → vec3 mismatch
return inputs;
});
},{myp5});
}).toThrow(/dimension mismatch/);
});

test('reports a friendly error on dimension mismatch via swizzle write (bridgeSwizzle)', async () => {
await myp5.createCanvas(5, 5, myp5.WEBGPU);

expect(() => {
myp5.baseMaterialShader().modify(() => {
let myVec = myp5.sharedVec3();
myp5.getWorldInputs(inputs => {
myVec.xy = inputs.position; // vec3 → 2-component swizzle mismatch
return inputs;
});
},{myp5});
}).toThrow(/dimension mismatch/);
});

test('does not error when shared variable assignment dimensions match', async () => {
await myp5.createCanvas(5, 5, myp5.WEBGPU);

expect(() => {
myp5.baseMaterialShader().modify(() => {
let myVec = myp5.sharedVec3();
myp5.getWorldInputs(inputs => {
myVec = inputs.position; // vec3 → vec3, OK
return inputs;
});
},{myp5});
}).not.toThrow();
});
});
});
});