-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Feature/detect outside variables strands #8208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev-2.0
Are you sure you want to change the base?
Changes from 3 commits
84832ba
9bb6ec3
795e50c
4440d6e
2129ca0
1694f2d
70644dc
09c3f46
9307929
cd4b0d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -863,7 +863,89 @@ 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 }); | ||
|
|
||
| // Collect all variable declarations in the strand | ||
| const declaredVars = new Set(); | ||
|
|
||
| // First pass: collect declared variables | ||
| ancestor(ast, { | ||
| VariableDeclaration(node, state, ancestors) { | ||
| for (const decl of node.declarations) { | ||
| if (decl.id.type === 'Identifier') { | ||
| declaredVars.add(decl.id.name); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| const errors = []; | ||
|
|
||
| // Second pass: check identifier references | ||
| ancestor(ast, { | ||
| Identifier(node, state, ancestors) { | ||
| const varName = node.name; | ||
|
|
||
| // Skip built-ins | ||
| const ignoreNames = ['__p5', 'p5', 'window', 'global', 'undefined', 'null', 'this', 'arguments']; | ||
| 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 | ||
| const isParam = ancestors.some(anc => | ||
| (anc.type === 'FunctionDeclaration' || | ||
| anc.type === 'FunctionExpression' || | ||
| anc.type === 'ArrowFunctionExpression') && | ||
| anc.params && anc.params.includes(node) | ||
| ); | ||
| if (isParam) return; | ||
|
|
||
| // 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 this variable is declared anywhere in the strand | ||
| if (!declaredVars.has(varName)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think what you had before with pushing/popping scopes was not a bad approach -- currently, it looks like this will pass, when it should not: baseMaterialShader().modify(() => {
getWorldInputs(() => {
let i = 1
inputs.position.x += i // OK, i is in scope
return inputs
})
getFinalColor((color) => {
color.r += i // Not OK, i is not in scope
return color
})
}) |
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { detectOutsideVariableReferences } from '../../../src/strands/strands_transpiler.js'; | ||
| import { suite, test } from '../../../test/js/spec.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 = ` | ||
| baseMaterialShader.modify(() => { | ||
| 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 = ` | ||
| baseMaterialShader.modify(() => { | ||
| 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 = ` | ||
| baseMaterialShader.modify(() => { | ||
|
||
| 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'); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this case aiming to catch?
inputsin e.g.getWorldInputs((inputs) => { ... })?