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
16 changes: 15 additions & 1 deletion src/strands/p5.strands.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { glslBackend } from './strands_glslBackend';

import { transpileStrandsToJS } from './strands_transpiler';
import { transpileStrandsToJS, detectOutsideVariableReferences } from './strands_transpiler';
import { BlockType } from './ir_types';

import { createDirectedAcyclicGraph } from './ir_dag'
Expand Down Expand Up @@ -72,6 +72,20 @@ function strands(p5, fn) {
// TODO: expose this, is internal for debugging for now.
const options = { parser: true, srcLocations: false };

// 0. Detect outside variable references in uniforms (before transpilation)
if (options.parser) {
const sourceString = `(${shaderModifier.toString()})`;
const errors = detectOutsideVariableReferences(sourceString);
if (errors.length > 0) {
// Show errors to the user
for (const error of errors) {
p5._friendlyError(
`p5.strands: ${error.message}`
);
}
}
}

// 1. Transpile from strands DSL to JS
let strandsCallback;
if (options.parser) {
Expand Down
95 changes: 94 additions & 1 deletion src/strands/strands_transpiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,100 @@ const ASTCallbacks = {
return replaceInNode(node);
}
}
export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
/**
* Analyzes strand code to detect outside variable references
* This runs before transpilation to provide helpful errors to users
*
* @param {string} sourceString - The strand code to analyze
* @returns {Array<{variable: string, message: string}>} - Array of errors if any
*/
export function detectOutsideVariableReferences(sourceString) {
try {
const ast = parse(sourceString, { ecmaVersion: 2021 });

const errors = [];
const declaredVars = new Set();

// First pass: collect all declared variables
ancestor(ast, {
VariableDeclaration(node) {
for (const declarator of node.declarations) {
if (declarator.id.type === 'Identifier') {
declaredVars.add(declarator.id.name);
}
}
}
});

// Second pass: check identifier references
ancestor(ast, {
Identifier(node, state, ancestors) {
const varName = node.name;

// Skip built-ins and p5.strands functions
const ignoreNames = [
'__p5', 'p5', 'window', 'global', 'undefined', 'null', 'this', 'arguments',
// p5.strands built-in functions
'getWorldPosition', 'getWorldNormal', 'getWorldTangent', 'getWorldBinormal',
'getLocalPosition', 'getLocalNormal', 'getLocalTangent', 'getLocalBinormal',
'getUV', 'getColor', 'getTime', 'getDeltaTime', 'getFrameCount',
'uniformFloat', 'uniformVec2', 'uniformVec3', 'uniformVec4',
'uniformInt', 'uniformBool', 'uniformMat2', 'uniformMat3', 'uniformMat4'
];
if (ignoreNames.includes(varName)) return;

// Skip if it's a property access (obj.prop)
const isProperty = ancestors.some(anc =>
anc.type === 'MemberExpression' && anc.property === node
);
if (isProperty) return;

// Skip if it's a function parameter
// Find the immediate function scope and check if this identifier is a parameter
for (let i = ancestors.length - 1; i >= 0; i--) {
const anc = ancestors[i];
if (anc.type === 'FunctionDeclaration' ||
anc.type === 'FunctionExpression' ||
anc.type === 'ArrowFunctionExpression') {
if (anc.params && anc.params.some(param => param.name === varName)) {
return; // It's a function parameter
}
break; // Only check the immediate function scope
}
}

// Skip if it's its own declaration
const isDeclaration = ancestors.some(anc =>
anc.type === 'VariableDeclarator' && anc.id === node
);
if (isDeclaration) return;

// Check if we're inside a uniform callback (OK to access outer scope)
const inUniformCallback = ancestors.some(anc =>
anc.type === 'CallExpression' &&
anc.callee.type === 'Identifier' &&
anc.callee.name.startsWith('uniform')
);
if (inUniformCallback) return; // Allow outer scope access in uniform callbacks

// Check if variable is declared
if (!declaredVars.has(varName)) {
errors.push({
variable: varName,
message: `Variable "${varName}" is not declared in the strand context.`
});
}
}
});

return errors;
} catch (error) {
// If parsing fails, return empty array - transpilation will catch it
return [];
}
}

export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
const ast = parse(sourceString, {
ecmaVersion: 2021,
locations: srcLocations
Expand Down
49 changes: 49 additions & 0 deletions test/unit/strands/strands_transpiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { detectOutsideVariableReferences } from '../../../src/strands/strands_transpiler.js';

suite('Strands Transpiler - Outside Variable Detection', function() {
test('should allow outer scope variables in uniform callbacks', function() {
// OK: mouseX in uniform callback is allowed
const code = `
const myUniform = uniformFloat(() => mouseX);
getWorldPosition((inputs) => {
inputs.position.x += myUniform;
return inputs;
});
`;

const errors = detectOutsideVariableReferences(code);
assert.equal(errors.length, 0, 'Should not error - mouseX is OK in uniform callback');
});

test('should detect undeclared variable in strand code', function() {
// ERROR: mouseX in strand code is not declared
const code = `
getWorldPosition((inputs) => {
inputs.position.x += mouseX; // mouseX not declared in strand!
return inputs;
});
`;

const errors = detectOutsideVariableReferences(code);
assert.ok(errors.length > 0, 'Should detect error');
assert.ok(errors.some(e => e.variable === 'mouseX'), 'Should detect mouseX');
});

test('should not error when variable is declared', function() {
const code = `
let myVar = 5;
getWorldPosition((inputs) => {
inputs.position.x += myVar; // myVar is declared
return inputs;
});
`;

const errors = detectOutsideVariableReferences(code);
assert.equal(errors.length, 0, 'Should not detect errors');
});

test('should handle empty code', function() {
const errors = detectOutsideVariableReferences('');
assert.equal(errors.length, 0, 'Empty code should have no errors');
});
});