Skip to content
Draft
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
8 changes: 7 additions & 1 deletion src/core/p5.Renderer3D.js
Original file line number Diff line number Diff line change
Expand Up @@ -2722,7 +2722,13 @@ function renderer3D(p5, fn) {
`buildComputeShader() is only available with the WebGPU renderer. ${webGPUAddonMessage}`
);
}
return this.baseComputeShader().modify(cb, context, { hook: 'iteration' });
const wasInternal = this._isUserCall;
this._isUserCall = false;
try {
return this.baseComputeShader().modify(cb, context, { hook: 'iteration' });
} finally {
this._isUserCall = wasInternal;
}
};
p5.registerDecorator('p5.prototype.buildComputeShader', markExperimental('webgpu', p5));

Expand Down
30 changes: 26 additions & 4 deletions src/friendly_errors/param_validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ let documentationData = dataDoc;
function validateParams(p5, fn, lifecycles) {
// Cache for Zod schemas
let schemaRegistry = new Map();
let strandsSchemaRegistry = new Map();

// Mapping names of p5 types to their constructor functions.
// p5Constructors:
Expand Down Expand Up @@ -200,6 +201,17 @@ function validateParams(p5, fn, lifecycles) {
// For primitive types and web API objects.
else if (schemaMap[baseType]) {
typeSchema = schemaMap[baseType];
if (
p5._isStrandsContextActive &&
['Number', 'Integer', 'Boolean', 'String'].includes(baseType)
) {
typeSchema = z.union([
typeSchema,
z.custom(val => val && val.isStrandsNode, {
message: 'Expected StrandsNode'
})
]);
}
}
// Tuple types
else if (
Expand Down Expand Up @@ -572,7 +584,7 @@ function validateParams(p5, fn, lifecycles) {
errorStackParser.parse(Error()).slice(3)
);
const locationStr = getFriendlyStack(stacktrace, true);
FES.log`${locationStr} ${message}`();
FES.log`${locationStr ? locationStr + ' ' : ''}${message}`();
}
return message;
};
Expand Down Expand Up @@ -615,11 +627,21 @@ function validateParams(p5, fn, lifecycles) {
};
}

let funcSchemas = schemaRegistry.get(func);
let activeRegistry = p5._isStrandsContextActive ? strandsSchemaRegistry : schemaRegistry;
let funcSchemas = activeRegistry.get(func);

if (!funcSchemas) {
funcSchemas = generateZodSchemasForFunc(func);
// Check _strandsSignatures for explicit bypass entries (value === true)
// that only apply inside an active strands context.
if (p5._isStrandsContextActive && p5._strandsSignatures?.has(func)) {
funcSchemas = z.any();
}

if (!funcSchemas) {
funcSchemas = generateZodSchemasForFunc(func);
}
if (!funcSchemas) return;
schemaRegistry.set(func, funcSchemas);
activeRegistry.set(func, funcSchemas);
}

if (z.validate(funcSchemas, args)) {
Expand Down
59 changes: 44 additions & 15 deletions src/friendly_errors/stacktrace.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,27 +279,29 @@ export const processStack = (error, stacktrace) => {
let isInternal = false;
let p5FileName, friendlyStack;

// Helper to strip query strings and hashes from URLs for consistent matching
const cleanUrl = url => (typeof url === 'string' ? url.split('?')[0].split('#')[0] : '');

// Intentionally throw an error that we catch so that we can check the name
// of the current file. Any errors we see from this file, we treat as
// internal errors.
const testStacktrace = errorStackParser.parse(Error());
p5FileName = testStacktrace[0].fileName;
p5FileName = cleanUrl(testStacktrace[0]?.fileName);
const p5BaseName = p5FileName ? p5FileName.split('/').slice(-1)[0] : '';
const isP5File = url => {
const cleaned = cleanUrl(url);
if (!cleaned) return false;
if (cleaned === p5FileName) return true;
if (p5BaseName && cleaned.split('/').slice(-1)[0] === p5BaseName) return true;
return false;
};

for (let i = stacktrace.length - 1; i >= 0; i--) {
let splitted = stacktrace[i].functionName.split('.');
if (entryPoints.includes(splitted[splitted.length - 1])) {
// remove everything below an entry point function (setup, draw, etc).
// (it's usually the internal initialization calls)
friendlyStack = stacktrace.slice(0, i + 1);
// We call the error "internal" if the source of the error was a
// function from within the p5.js library file, but called from the
// user's code directly. We only need to check the topmost frame in
// the stack trace since any function internal to p5 should pass this
// check, not just public p5 functions.
if (stacktrace[0].fileName === p5FileName) {
isInternal = true;
break;
}
break;
}
}
Expand All @@ -309,6 +311,14 @@ export const processStack = (error, stacktrace) => {
// entire stacktrace for friendlyStack
if (!friendlyStack) friendlyStack = stacktrace;

// We call the error "internal" if the source of the error was a
// function from within the p5.js library file, but called from the
// user's code directly. When error is null (called from param_validator),
// it is always an internal validation call.
if (error === null || isP5File(stacktrace[0]?.fileName)) {
isInternal = true;
}

if (isInternal) {
// the frameIndex property is added before the filter, so frameIndex
// corresponds to the index of a frame in the original stacktrace.
Expand All @@ -319,7 +329,15 @@ export const processStack = (error, stacktrace) => {
frame.frameIndex = index;
return frame;
})
.filter(frame => frame.fileName !== p5FileName);
.filter(frame => {
const cleaned = cleanUrl(frame.fileName);
return (
cleaned &&
!isP5File(frame.fileName) &&
cleaned !== '<anonymous>' &&
!cleaned.startsWith('eval')
);
});

// a weird case, if for some reason we can't identify the function called
// from user's code
Expand All @@ -332,16 +350,19 @@ export const processStack = (error, stacktrace) => {
friendlyStack[0].lineNumber &&
friendlyStack[0].columnNumber
) {
const cleanFile = cleanUrl(friendlyStack[0].fileName);
locationObj = {
location: `${friendlyStack[0].fileName}:${
location: `${cleanFile}:${
friendlyStack[0].lineNumber
}:${friendlyStack[0].columnNumber}`,
file: friendlyStack[0].fileName.split('/').slice(-1),
file: cleanFile.split('/').slice(-1)[0],
line: friendlyStack[0].lineNumber
};

// if already handled by another part of the FES, don't handle again
if (p5._fesLogCache[locationObj.location]) return [true, null];
if (typeof p5 !== 'undefined' && p5?._fesLogCache?.[locationObj.location]) {
return [true, null];
}
}

// Library error
Expand Down Expand Up @@ -394,8 +415,16 @@ export function printFriendlyStack(friendlyStack) {
}

export function getFriendlyStack(stacktrace, compact = false) {
if (!stacktrace || !stacktrace.length || !stacktrace[0]?.fileName) {
return null;
}
const rawFile = stacktrace[0].fileName;
const cleanFile = (typeof rawFile === 'string' ? rawFile.split('?')[0].split('#')[0] : '').split('/').slice(-1)[0];
if (!cleanFile || cleanFile === '<anonymous>' || cleanFile.startsWith('eval')) {
return null;
}
if (compact) {
return `[${stacktrace[0].fileName.split('/').slice(-1)}, line ${stacktrace[0].lineNumber}]`;
return `[${cleanFile}, line ${stacktrace[0].lineNumber}]`;
} else {
if (stacktrace.length > 1) {
let stacktraceMsg = '';
Expand Down
6 changes: 2 additions & 4 deletions src/strands/p5.strands.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ function strands(p5, fn) {
ctx.backend = backend;
ctx.active = active;
ctx.renderer = renderer;
ctx.baseShader = baseShader;
ctx.previousFES = p5.disableFriendlyErrors;
ctx.tempProps = [];
ctx._noiseOctaves = null;
Expand All @@ -144,7 +143,7 @@ function strands(p5, fn) {
// the pass is done since FES is switched off while it runs (see below).
ctx.experimentalFeaturesUsed = new Set();
if (active) {
p5.disableFriendlyErrors = true;
p5._isStrandsContextActive = true;
}
}

Expand All @@ -167,7 +166,6 @@ function strands(p5, fn) {
ctx.backend = undefined;
ctx.active = false;
ctx.renderer = null;
ctx.baseShader = null;
ctx.previousFES = p5.disableFriendlyErrors;
ctx.tempProps = [];
ctx._noiseOctaves = null;
Expand All @@ -193,7 +191,7 @@ function strands(p5, fn) {
* @return {Error[]} Errors from temporary writes that could not be undone.
*/
function deinitStrandsContext(ctx) {
p5.disableFriendlyErrors = ctx.previousFES;
p5._isStrandsContextActive = false;
// Undo hooks, hook aliases and options.hook properties (incl. #8549's
// p5.Graphics.prototype entries) exactly, newest first.
const restoreErrors = restoreTemporaryProperties(ctx);
Expand Down
Loading