p5 2.2.2 → 2.2.3-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/accessibility/color_namer.js +5 -5
  2. package/dist/accessibility/index.js +5 -5
  3. package/dist/app.js +5 -5
  4. package/dist/color/color_conversion.js +5 -5
  5. package/dist/color/index.js +1 -1
  6. package/dist/color/setting.js +1 -1
  7. package/dist/{constants-BxjhKpTv.js → constants-D3ryGa0m.js} +1 -1
  8. package/dist/core/constants.js +1 -1
  9. package/dist/core/environment.js +7 -3
  10. package/dist/core/filterShaders.js +1 -1
  11. package/dist/core/friendly_errors/fes_core.js +1 -1
  12. package/dist/core/friendly_errors/file_errors.js +1 -1
  13. package/dist/core/friendly_errors/index.js +1 -1
  14. package/dist/core/friendly_errors/param_validator.js +2063 -2014
  15. package/dist/core/friendly_errors/sketch_verifier.js +1 -1
  16. package/dist/core/helpers.js +1 -1
  17. package/dist/core/init.js +5 -5
  18. package/dist/core/internationalization.js +1 -1
  19. package/dist/core/legacy.js +5 -5
  20. package/dist/core/main.js +5 -5
  21. package/dist/core/p5.Graphics.js +3 -3
  22. package/dist/core/p5.Renderer.js +2 -2
  23. package/dist/core/p5.Renderer2D.js +5 -5
  24. package/dist/core/p5.Renderer3D.js +3 -3
  25. package/dist/core/rendering.js +3 -3
  26. package/dist/dom/dom.js +1 -1
  27. package/dist/dom/index.js +1 -1
  28. package/dist/dom/p5.Element.js +1 -1
  29. package/dist/dom/p5.MediaElement.js +11 -4
  30. package/dist/events/pointer.js +4 -0
  31. package/dist/image/const.js +1 -1
  32. package/dist/image/filterRenderer2D.js +4 -4
  33. package/dist/image/image.js +3 -3
  34. package/dist/image/index.js +3 -3
  35. package/dist/image/loading_displaying.js +3 -3
  36. package/dist/image/p5.Image.js +2 -2
  37. package/dist/io/files.js +3 -3
  38. package/dist/io/index.js +3 -3
  39. package/dist/{ir_builders-w12-GSxu.js → ir_builders-DMfaOLIL.js} +48 -8
  40. package/dist/{main-DDs4QOnh.js → main-CGwYa9-f.js} +126 -36
  41. package/dist/math/Matrices/Matrix.js +1 -1
  42. package/dist/math/Matrices/MatrixNumjs.js +1 -1
  43. package/dist/math/index.js +1 -1
  44. package/dist/math/p5.Matrix.js +1 -1
  45. package/dist/math/p5.Vector.js +1 -1
  46. package/dist/math/trigonometry.js +1 -1
  47. package/dist/{p5.Renderer-BSGddFv7.js → p5.Renderer-C0e0XesC.js} +9 -2
  48. package/dist/{rendering-C9g7uSQ5.js → rendering-4Z2qdE_W.js} +90 -55
  49. package/dist/shape/2d_primitives.js +1 -1
  50. package/dist/shape/attributes.js +1 -1
  51. package/dist/shape/custom_shapes.js +1 -1
  52. package/dist/shape/index.js +1 -1
  53. package/dist/strands/ir_builders.js +1 -1
  54. package/dist/strands/ir_dag.js +32 -2
  55. package/dist/strands/ir_types.js +18 -11
  56. package/dist/strands/p5.strands.js +15 -2
  57. package/dist/strands/strands_api.js +86 -40
  58. package/dist/strands/strands_conditionals.js +1 -1
  59. package/dist/strands/strands_for.js +1 -1
  60. package/dist/strands/strands_node.js +1 -1
  61. package/dist/strands/strands_phi_utils.js +27 -9
  62. package/dist/strands/strands_transpiler.js +1237 -831
  63. package/dist/type/index.js +2 -2
  64. package/dist/type/p5.Font.js +7 -5
  65. package/dist/type/textCore.js +2 -2
  66. package/dist/webgl/3d_primitives.js +3 -3
  67. package/dist/webgl/GeometryBuilder.js +1 -1
  68. package/dist/webgl/ShapeBuilder.js +1 -1
  69. package/dist/webgl/enums.js +1 -1
  70. package/dist/webgl/index.js +4 -4
  71. package/dist/webgl/interaction.js +1 -1
  72. package/dist/webgl/light.js +3 -3
  73. package/dist/webgl/loading.js +41 -35
  74. package/dist/webgl/material.js +3 -3
  75. package/dist/webgl/p5.Camera.js +3 -3
  76. package/dist/webgl/p5.Framebuffer.js +3 -3
  77. package/dist/webgl/p5.Geometry.js +1 -1
  78. package/dist/webgl/p5.Quat.js +1 -1
  79. package/dist/webgl/p5.RendererGL.js +4 -4
  80. package/dist/webgl/p5.Texture.js +3 -3
  81. package/dist/webgl/strands_glslBackend.js +1 -1
  82. package/dist/webgl/text.js +3 -3
  83. package/dist/webgl/utils.js +3 -3
  84. package/dist/webgpu/index.js +2 -2
  85. package/dist/webgpu/p5.RendererWebGPU.js +2 -2
  86. package/dist/webgpu/strands_wgslBackend.js +13 -4
  87. package/lib/p5.esm.js +3634 -2870
  88. package/lib/p5.esm.min.js +1 -1
  89. package/lib/p5.js +3634 -2870
  90. package/lib/p5.min.js +1 -1
  91. package/lib/p5.webgpu.esm.js +43 -15
  92. package/lib/p5.webgpu.js +43 -15
  93. package/lib/p5.webgpu.min.js +1 -1
  94. package/package.json +1 -1
  95. package/types/global.d.ts +805 -805
  96. package/types/p5.d.ts +415 -415
@@ -27,7 +27,7 @@ function replaceBinaryOperator(codeSource) {
27
27
  }
28
28
  }
29
29
  function nodeIsUniform(ancestor) {
30
- return ancestor.type === 'CallExpression'
30
+ return ancestor && ancestor.type === 'CallExpression'
31
31
  && (
32
32
  (
33
33
  // Global mode
@@ -42,7 +42,7 @@ function nodeIsUniform(ancestor) {
42
42
  }
43
43
 
44
44
  function nodeIsVarying(node) {
45
- return node?.type === 'CallExpression'
45
+ return node && node.type === 'CallExpression'
46
46
  && (
47
47
  (
48
48
  // Global mode
@@ -282,65 +282,103 @@ const ASTCallbacks = {
282
282
  Identifier(node, _state, ancestors) {
283
283
  if (ancestors.some(nodeIsUniform)) { return; }
284
284
  if (_state.varyings[node.name]
285
- && !ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) {
286
- node.type = 'CallExpression';
287
- node.callee = {
285
+ && !ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)
286
+ ) {
287
+ node.type = 'CallExpression';
288
+ node.callee = {
289
+ type: 'MemberExpression',
290
+ object: {
291
+ type: 'Identifier',
292
+ name: node.name
293
+ },
294
+ property: {
295
+ type: 'Identifier',
296
+ name: 'getValue'
297
+ },
298
+ };
299
+ node.arguments = [];
300
+ }
301
+ },
302
+ // The callbacks for AssignmentExpression and BinaryExpression handle
303
+ // operator overloading including +=, *= assignment expressions
304
+ ArrayExpression(node, _state, ancestors) {
305
+ if (ancestors.some(nodeIsUniform)) { return; }
306
+ const original = JSON.parse(JSON.stringify(node));
307
+ node.type = 'CallExpression';
308
+ node.callee = {
309
+ type: 'Identifier',
310
+ name: '__p5.strandsNode',
311
+ };
312
+ node.arguments = [original];
313
+ },
314
+ AssignmentExpression(node, _state, ancestors) {
315
+ if (ancestors.some(nodeIsUniform)) { return; }
316
+ const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
317
+ if (node.operator !== '=') {
318
+ const methodName = replaceBinaryOperator(node.operator.replace('=',''));
319
+ const rightReplacementNode = {
320
+ type: 'CallExpression',
321
+ callee: {
322
+ type: 'MemberExpression',
323
+ object: unsafeTypes.includes(node.left.type)
324
+ ? {
325
+ type: 'CallExpression',
326
+ callee: {
327
+ type: 'Identifier',
328
+ name: '__p5.strandsNode',
329
+ },
330
+ arguments: [node.left]
331
+ }
332
+ : node.left,
333
+ property: {
334
+ type: 'Identifier',
335
+ name: methodName,
336
+ },
337
+ },
338
+ arguments: [node.right]
339
+ };
340
+ node.operator = '=';
341
+ node.right = rightReplacementNode;
342
+ }
343
+ // Handle direct varying variable assignment: myVarying = value
344
+ if (_state.varyings[node.left.name]) {
345
+ node.type = 'ExpressionStatement';
346
+ node.expression = {
347
+ type: 'CallExpression',
348
+ callee: {
288
349
  type: 'MemberExpression',
289
350
  object: {
290
351
  type: 'Identifier',
291
- name: node.name
352
+ name: node.left.name
292
353
  },
293
354
  property: {
294
355
  type: 'Identifier',
295
- name: 'getValue'
296
- },
297
- };
298
- node.arguments = [];
299
- }
300
- },
301
- // The callbacks for AssignmentExpression and BinaryExpression handle
302
- // operator overloading including +=, *= assignment expressions
303
- ArrayExpression(node, _state, ancestors) {
304
- if (ancestors.some(nodeIsUniform)) { return; }
305
- const original = JSON.parse(JSON.stringify(node));
306
- node.type = 'CallExpression';
307
- node.callee = {
308
- type: 'Identifier',
309
- name: '__p5.strandsNode',
356
+ name: 'bridge',
357
+ }
358
+ },
359
+ arguments: [node.right],
310
360
  };
311
- node.arguments = [original];
312
- },
313
- AssignmentExpression(node, _state, ancestors) {
314
- if (ancestors.some(nodeIsUniform)) { return; }
315
- const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
316
- if (node.operator !== '=') {
317
- const methodName = replaceBinaryOperator(node.operator.replace('=',''));
318
- const rightReplacementNode = {
319
- type: 'CallExpression',
320
- callee: {
321
- type: 'MemberExpression',
322
- object: unsafeTypes.includes(node.left.type)
323
- ? {
324
- type: 'CallExpression',
325
- callee: {
326
- type: 'Identifier',
327
- name: '__p5.strandsNode',
328
- },
329
- arguments: [node.left]
330
- }
331
- : node.left,
332
- property: {
333
- type: 'Identifier',
334
- name: methodName,
335
- },
336
- },
337
- arguments: [node.right]
338
- };
339
- node.operator = '=';
340
- node.right = rightReplacementNode;
361
+ }
362
+ // Handle swizzle assignment to varying variable: myVarying.xyz = value
363
+ // Note: node.left.object might be worldPos.getValue() due to prior Identifier transformation
364
+ else if (node.left.type === 'MemberExpression') {
365
+ let varyingName = null;
366
+
367
+ // Check if it's a direct identifier: myVarying.xyz
368
+ if (node.left.object.type === 'Identifier' && _state.varyings[node.left.object.name]) {
369
+ varyingName = node.left.object.name;
370
+ }
371
+ // Check if it's a getValue() call: myVarying.getValue().xyz
372
+ else if (node.left.object.type === 'CallExpression' &&
373
+ node.left.object.callee?.type === 'MemberExpression' &&
374
+ node.left.object.callee.property?.name === 'getValue' &&
375
+ node.left.object.callee.object?.type === 'Identifier' &&
376
+ _state.varyings[node.left.object.callee.object.name]) {
377
+ varyingName = node.left.object.callee.object.name;
341
378
  }
342
- // Handle direct varying variable assignment: myVarying = value
343
- if (_state.varyings[node.left.name]) {
379
+
380
+ if (varyingName) {
381
+ const swizzlePattern = node.left.property.name;
344
382
  node.type = 'ExpressionStatement';
345
383
  node.expression = {
346
384
  type: 'CallExpression',
@@ -348,889 +386,1257 @@ const ASTCallbacks = {
348
386
  type: 'MemberExpression',
349
387
  object: {
350
388
  type: 'Identifier',
351
- name: node.left.name
389
+ name: varyingName
352
390
  },
353
391
  property: {
354
392
  type: 'Identifier',
355
- name: 'bridge',
393
+ name: 'bridgeSwizzle',
356
394
  }
357
395
  },
358
- arguments: [node.right],
359
- };
360
- }
361
- // Handle swizzle assignment to varying variable: myVarying.xyz = value
362
- // Note: node.left.object might be worldPos.getValue() due to prior Identifier transformation
363
- else if (node.left.type === 'MemberExpression') {
364
- let varyingName = null;
365
-
366
- // Check if it's a direct identifier: myVarying.xyz
367
- if (node.left.object.type === 'Identifier' && _state.varyings[node.left.object.name]) {
368
- varyingName = node.left.object.name;
369
- }
370
- // Check if it's a getValue() call: myVarying.getValue().xyz
371
- else if (node.left.object.type === 'CallExpression' &&
372
- node.left.object.callee?.type === 'MemberExpression' &&
373
- node.left.object.callee.property?.name === 'getValue' &&
374
- node.left.object.callee.object?.type === 'Identifier' &&
375
- _state.varyings[node.left.object.callee.object.name]) {
376
- varyingName = node.left.object.callee.object.name;
377
- }
378
-
379
- if (varyingName) {
380
- const swizzlePattern = node.left.property.name;
381
- node.type = 'ExpressionStatement';
382
- node.expression = {
383
- type: 'CallExpression',
384
- callee: {
385
- type: 'MemberExpression',
386
- object: {
387
- type: 'Identifier',
388
- name: varyingName
389
- },
390
- property: {
391
- type: 'Identifier',
392
- name: 'bridgeSwizzle',
393
- }
396
+ arguments: [
397
+ {
398
+ type: 'Literal',
399
+ value: swizzlePattern
394
400
  },
395
- arguments: [
396
- {
397
- type: 'Literal',
398
- value: swizzlePattern
399
- },
400
- node.right
401
- ],
402
- };
403
- }
404
- }
405
- },
406
- BinaryExpression(node, _state, ancestors) {
407
- // Don't convert uniform default values to node methods, as
408
- // they should be evaluated at runtime, not compiled.
409
- if (ancestors.some(nodeIsUniform)) { return; }
410
- // If the left hand side of an expression is one of these types,
411
- // we should construct a node from it.
412
- const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
413
- if (unsafeTypes.includes(node.left.type)) {
414
- const leftReplacementNode = {
415
- type: 'CallExpression',
416
- callee: {
417
- type: 'Identifier',
418
- name: '__p5.strandsNode',
419
- },
420
- arguments: [node.left]
401
+ node.right
402
+ ],
421
403
  };
422
- node.left = leftReplacementNode;
423
404
  }
424
- // Replace the binary operator with a call expression
425
- // in other words a call to BaseNode.mult(), .div() etc.
426
- node.type = 'CallExpression';
427
- node.callee = {
428
- type: 'MemberExpression',
429
- object: node.left,
430
- property: {
405
+ }
406
+ },
407
+ BinaryExpression(node, _state, ancestors) {
408
+ // Don't convert uniform default values to node methods, as
409
+ // they should be evaluated at runtime, not compiled.
410
+ if (ancestors.some(nodeIsUniform)) { return; }
411
+ // If the left hand side of an expression is one of these types,
412
+ // we should construct a node from it.
413
+ const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
414
+ if (unsafeTypes.includes(node.left.type)) {
415
+ const leftReplacementNode = {
416
+ type: 'CallExpression',
417
+ callee: {
431
418
  type: 'Identifier',
432
- name: replaceBinaryOperator(node.operator),
419
+ name: '__p5.strandsNode',
433
420
  },
421
+ arguments: [node.left]
434
422
  };
435
- node.arguments = [node.right];
436
- },
437
- LogicalExpression(node, _state, ancestors) {
438
- // Don't convert uniform default values to node methods, as
439
- // they should be evaluated at runtime, not compiled.
440
- if (ancestors.some(nodeIsUniform)) { return; }
441
- // If the left hand side of an expression is one of these types,
442
- // we should construct a node from it.
443
- const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
444
- if (unsafeTypes.includes(node.left.type)) {
445
- const leftReplacementNode = {
446
- type: 'CallExpression',
447
- callee: {
448
- type: 'Identifier',
449
- name: '__p5.strandsNode',
450
- },
451
- arguments: [node.left]
452
- };
453
- node.left = leftReplacementNode;
454
- }
455
- // Replace the logical operator with a call expression
456
- // in other words a call to BaseNode.or(), .and() etc.
457
- node.type = 'CallExpression';
458
- node.callee = {
459
- type: 'MemberExpression',
460
- object: node.left,
461
- property: {
423
+ node.left = leftReplacementNode;
424
+ }
425
+ // Replace the binary operator with a call expression
426
+ // in other words a call to BaseNode.mult(), .div() etc.
427
+ node.type = 'CallExpression';
428
+ node.callee = {
429
+ type: 'MemberExpression',
430
+ object: node.left,
431
+ property: {
432
+ type: 'Identifier',
433
+ name: replaceBinaryOperator(node.operator),
434
+ },
435
+ };
436
+ node.arguments = [node.right];
437
+ },
438
+ LogicalExpression(node, _state, ancestors) {
439
+ // Don't convert uniform default values to node methods, as
440
+ // they should be evaluated at runtime, not compiled.
441
+ if (ancestors.some(nodeIsUniform)) { return; }
442
+ // If the left hand side of an expression is one of these types,
443
+ // we should construct a node from it.
444
+ const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
445
+ if (unsafeTypes.includes(node.left.type)) {
446
+ const leftReplacementNode = {
447
+ type: 'CallExpression',
448
+ callee: {
462
449
  type: 'Identifier',
463
- name: replaceBinaryOperator(node.operator),
450
+ name: '__p5.strandsNode',
464
451
  },
452
+ arguments: [node.left]
465
453
  };
466
- node.arguments = [node.right];
467
- },
468
- IfStatement(node, _state, ancestors) {
469
- if (ancestors.some(nodeIsUniform)) { return; }
470
- // Transform if statement into strandsIf() call
471
- // The condition is evaluated directly, not wrapped in a function
472
- const condition = node.test;
473
- // Create the then function
474
- const thenFunction = {
454
+ node.left = leftReplacementNode;
455
+ }
456
+ // Replace the logical operator with a call expression
457
+ // in other words a call to BaseNode.or(), .and() etc.
458
+ node.type = 'CallExpression';
459
+ node.callee = {
460
+ type: 'MemberExpression',
461
+ object: node.left,
462
+ property: {
463
+ type: 'Identifier',
464
+ name: replaceBinaryOperator(node.operator),
465
+ },
466
+ };
467
+ node.arguments = [node.right];
468
+ },
469
+ IfStatement(node, _state, ancestors) {
470
+ if (ancestors.some(nodeIsUniform)) { return; }
471
+ // Transform if statement into strandsIf() call
472
+ // The condition is evaluated directly, not wrapped in a function
473
+ const condition = node.test;
474
+ // Create the then function
475
+ const thenFunction = {
476
+ type: 'ArrowFunctionExpression',
477
+ params: [],
478
+ body: node.consequent.type === 'BlockStatement' ? node.consequent : {
479
+ type: 'BlockStatement',
480
+ body: [node.consequent]
481
+ }
482
+ };
483
+ // Start building the call chain: __p5.strandsIf(condition, then)
484
+ let callExpression = {
485
+ type: 'CallExpression',
486
+ callee: {
487
+ type: 'Identifier',
488
+ name: '__p5.strandsIf'
489
+ },
490
+ arguments: [condition, thenFunction]
491
+ };
492
+ // Always chain .Else() even if there's no explicit else clause
493
+ // This ensures the conditional completes and returns phi nodes
494
+ let elseFunction;
495
+ if (node.alternate) {
496
+ elseFunction = {
475
497
  type: 'ArrowFunctionExpression',
476
498
  params: [],
477
- body: node.consequent.type === 'BlockStatement' ? node.consequent : {
499
+ body: node.alternate.type === 'BlockStatement' ? node.alternate : {
478
500
  type: 'BlockStatement',
479
- body: [node.consequent]
501
+ body: [node.alternate]
480
502
  }
481
503
  };
482
- // Start building the call chain: __p5.strandsIf(condition, then)
483
- let callExpression = {
484
- type: 'CallExpression',
485
- callee: {
486
- type: 'Identifier',
487
- name: '__p5.strandsIf'
488
- },
489
- arguments: [condition, thenFunction]
504
+ } else {
505
+ // Create an empty else function
506
+ elseFunction = {
507
+ type: 'ArrowFunctionExpression',
508
+ params: [],
509
+ body: {
510
+ type: 'BlockStatement',
511
+ body: []
512
+ }
490
513
  };
491
- // Always chain .Else() even if there's no explicit else clause
492
- // This ensures the conditional completes and returns phi nodes
493
- let elseFunction;
494
- if (node.alternate) {
495
- elseFunction = {
496
- type: 'ArrowFunctionExpression',
497
- params: [],
498
- body: node.alternate.type === 'BlockStatement' ? node.alternate : {
499
- type: 'BlockStatement',
500
- body: [node.alternate]
514
+ }
515
+ callExpression = {
516
+ type: 'CallExpression',
517
+ callee: {
518
+ type: 'MemberExpression',
519
+ object: callExpression,
520
+ property: {
521
+ type: 'Identifier',
522
+ name: 'Else'
523
+ }
524
+ },
525
+ arguments: [elseFunction]
526
+ };
527
+
528
+ // Analyze which outer scope variables are assigned in any branch
529
+ const assignedVars = new Set();
530
+
531
+ const analyzeBranch = (functionBody) => {
532
+ // First pass: collect all variable declarations in the branch
533
+ const localVars = new Set();
534
+ ancestor(functionBody, {
535
+ VariableDeclarator(node, ancestors) {
536
+ // Skip if we're inside a block that contains strands control flow
537
+ if (ancestors.some(statementContainsStrandsControlFlow)) return;
538
+ if (node.id.type === 'Identifier') {
539
+ localVars.add(node.id.name);
501
540
  }
502
- };
503
- } else {
504
- // Create an empty else function
505
- elseFunction = {
506
- type: 'ArrowFunctionExpression',
507
- params: [],
508
- body: {
509
- type: 'BlockStatement',
510
- body: []
541
+ }
542
+ });
543
+
544
+ // Second pass: find assignments to non-local variables using acorn-walk
545
+ ancestor(functionBody, {
546
+ AssignmentExpression(node, ancestors) {
547
+ // Skip if we're inside a block that contains strands control flow
548
+ if (ancestors.some(statementContainsStrandsControlFlow)) return;
549
+
550
+ const left = node.left;
551
+ if (left.type === 'Identifier') {
552
+ // Direct variable assignment: x = value
553
+ if (!localVars.has(left.name)) {
554
+ assignedVars.add(left.name);
555
+ }
556
+ } else if (left.type === 'MemberExpression') {
557
+ // Property assignment: obj.prop = value or obj.a.b = value
558
+ const propertyPath = buildPropertyPath(left);
559
+ if (propertyPath) {
560
+ const baseName = propertyPath.split('.')[0];
561
+ if (!localVars.has(baseName)) {
562
+ assignedVars.add(propertyPath);
563
+ }
564
+ }
511
565
  }
512
- };
513
- }
514
- callExpression = {
515
- type: 'CallExpression',
516
- callee: {
517
- type: 'MemberExpression',
518
- object: callExpression,
519
- property: {
520
- type: 'Identifier',
521
- name: 'Else'
566
+ }
567
+ });
568
+ };
569
+
570
+ // Analyze all branches for assignments to outer scope variables
571
+ analyzeBranch(thenFunction.body);
572
+ analyzeBranch(elseFunction.body);
573
+ if (assignedVars.size > 0) {
574
+ // Add copying, reference replacement, and return statements to branch functions
575
+ const addCopyingAndReturn = (functionBody, varsToReturn) => {
576
+ if (functionBody.type === 'BlockStatement') {
577
+ // Create temporary variables and copy statements
578
+ const tempVarMap = new Map(); // property path -> temp name
579
+ const copyStatements = [];
580
+ for (const varPath of varsToReturn) {
581
+ const parts = varPath.split('.');
582
+ const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
583
+ tempVarMap.set(varPath, tempName);
584
+
585
+ // Build the member expression for the property path
586
+ let sourceExpr = { type: 'Identifier', name: parts[0] };
587
+ for (let i = 1; i < parts.length; i++) {
588
+ sourceExpr = {
589
+ type: 'MemberExpression',
590
+ object: sourceExpr,
591
+ property: { type: 'Identifier', name: parts[i] },
592
+ computed: false
593
+ };
594
+ }
595
+
596
+ // let tempName = propertyPath.copy()
597
+ copyStatements.push({
598
+ type: 'VariableDeclaration',
599
+ declarations: [{
600
+ type: 'VariableDeclarator',
601
+ id: { type: 'Identifier', name: tempName },
602
+ init: {
603
+ type: 'CallExpression',
604
+ callee: {
605
+ type: 'MemberExpression',
606
+ object: sourceExpr,
607
+ property: { type: 'Identifier', name: 'copy' },
608
+ computed: false
609
+ },
610
+ arguments: []
611
+ }
612
+ }],
613
+ kind: 'let'
614
+ });
522
615
  }
523
- },
524
- arguments: [elseFunction]
616
+ // Apply reference replacement to all statements
617
+ functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
618
+ // Insert copy statements at the beginning
619
+ functionBody.body.unshift(...copyStatements);
620
+ // Add return statement with flat object using property paths as keys
621
+ const returnObj = {
622
+ type: 'ObjectExpression',
623
+ properties: Array.from(varsToReturn).map(varPath => ({
624
+ type: 'Property',
625
+ key: { type: 'Literal', value: varPath },
626
+ value: { type: 'Identifier', name: tempVarMap.get(varPath) },
627
+ kind: 'init',
628
+ computed: false,
629
+ shorthand: false
630
+ }))
631
+ };
632
+ functionBody.body.push({
633
+ type: 'ReturnStatement',
634
+ argument: returnObj
635
+ });
636
+ }
525
637
  };
638
+ addCopyingAndReturn(thenFunction.body, assignedVars);
639
+ addCopyingAndReturn(elseFunction.body, assignedVars);
640
+ // Create a block variable to capture the return value
641
+ const blockVar = `__block_${blockVarCounter++}`;
642
+ // Replace with a block statement
643
+ const statements = [];
644
+ // Make sure every assigned variable starts as a node
645
+ for (const varPath of assignedVars) {
646
+ const parts = varPath.split('.');
647
+
648
+ // Build left side: inputs.color or just x
649
+ let leftExpr = { type: 'Identifier', name: parts[0] };
650
+ for (let i = 1; i < parts.length; i++) {
651
+ leftExpr = {
652
+ type: 'MemberExpression',
653
+ object: leftExpr,
654
+ property: { type: 'Identifier', name: parts[i] },
655
+ computed: false
656
+ };
657
+ }
658
+
659
+ // Build right side - same as left for strandsNode wrapping
660
+ let rightArgExpr = { type: 'Identifier', name: parts[0] };
661
+ for (let i = 1; i < parts.length; i++) {
662
+ rightArgExpr = {
663
+ type: 'MemberExpression',
664
+ object: rightArgExpr,
665
+ property: { type: 'Identifier', name: parts[i] },
666
+ computed: false
667
+ };
668
+ }
526
669
 
527
- // Analyze which outer scope variables are assigned in any branch
528
- const assignedVars = new Set();
529
-
530
- const analyzeBranch = (functionBody) => {
531
- // First pass: collect all variable declarations in the branch
532
- const localVars = new Set();
533
- ancestor(functionBody, {
534
- VariableDeclarator(node, ancestors) {
535
- // Skip if we're inside a block that contains strands control flow
536
- if (ancestors.some(statementContainsStrandsControlFlow)) return;
537
- if (node.id.type === 'Identifier') {
538
- localVars.add(node.id.name);
670
+ statements.push({
671
+ type: 'ExpressionStatement',
672
+ expression: {
673
+ type: 'AssignmentExpression',
674
+ operator: '=',
675
+ left: leftExpr,
676
+ right: {
677
+ type: 'CallExpression',
678
+ callee: { type: 'Identifier', name: '__p5.strandsNode' },
679
+ arguments: [rightArgExpr],
539
680
  }
540
681
  }
541
682
  });
683
+ }
684
+ statements.push({
685
+ type: 'VariableDeclaration',
686
+ declarations: [{
687
+ type: 'VariableDeclarator',
688
+ id: { type: 'Identifier', name: blockVar },
689
+ init: callExpression
690
+ }],
691
+ kind: 'const'
692
+ });
693
+ // 2. Assignments for each modified variable
694
+ for (const varPath of assignedVars) {
695
+ const parts = varPath.split('.');
696
+
697
+ // Build left side: inputs.color or just x
698
+ let leftExpr = { type: 'Identifier', name: parts[0] };
699
+ for (let i = 1; i < parts.length; i++) {
700
+ leftExpr = {
701
+ type: 'MemberExpression',
702
+ object: leftExpr,
703
+ property: { type: 'Identifier', name: parts[i] },
704
+ computed: false
705
+ };
706
+ }
542
707
 
543
- // Second pass: find assignments to non-local variables using acorn-walk
544
- ancestor(functionBody, {
545
- AssignmentExpression(node, ancestors) {
546
- // Skip if we're inside a block that contains strands control flow
547
- if (ancestors.some(statementContainsStrandsControlFlow)) return;
548
-
549
- const left = node.left;
550
- if (left.type === 'Identifier') {
551
- // Direct variable assignment: x = value
552
- if (!localVars.has(left.name)) {
553
- assignedVars.add(left.name);
554
- }
555
- } else if (left.type === 'MemberExpression') {
556
- // Property assignment: obj.prop = value or obj.a.b = value
557
- const propertyPath = buildPropertyPath(left);
558
- if (propertyPath) {
559
- const baseName = propertyPath.split('.')[0];
560
- if (!localVars.has(baseName)) {
561
- assignedVars.add(propertyPath);
562
- }
563
- }
564
- }
708
+ // Build right side: __block_2['inputs.color'] or __block_2['x']
709
+ const rightExpr = {
710
+ type: 'MemberExpression',
711
+ object: { type: 'Identifier', name: blockVar },
712
+ property: { type: 'Literal', value: varPath },
713
+ computed: true
714
+ };
715
+
716
+ statements.push({
717
+ type: 'ExpressionStatement',
718
+ expression: {
719
+ type: 'AssignmentExpression',
720
+ operator: '=',
721
+ left: leftExpr,
722
+ right: rightExpr
565
723
  }
566
724
  });
725
+ }
726
+ // Replace the if statement with a block statement
727
+ node.type = 'BlockStatement';
728
+ node.body = statements;
729
+ } else {
730
+ // No assignments, just replace with the call expression
731
+ node.type = 'ExpressionStatement';
732
+ node.expression = callExpression;
733
+ }
734
+ delete node.test;
735
+ delete node.consequent;
736
+ delete node.alternate;
737
+ },
738
+ UpdateExpression(node, _state, ancestors) {
739
+ if (ancestors.some(nodeIsUniform)) { return; }
740
+
741
+ // Transform ++var, var++, --var, var-- into assignment expressions
742
+ let operator;
743
+ if (node.operator === '++') {
744
+ operator = '+';
745
+ } else if (node.operator === '--') {
746
+ operator = '-';
747
+ } else {
748
+ return; // Unknown update operator
749
+ }
750
+
751
+ // Convert to: var = var + 1 or var = var - 1
752
+ const assignmentExpr = {
753
+ type: 'AssignmentExpression',
754
+ operator: '=',
755
+ left: node.argument,
756
+ right: {
757
+ type: 'BinaryExpression',
758
+ operator: operator,
759
+ left: node.argument,
760
+ right: {
761
+ type: 'Literal',
762
+ value: 1
763
+ }
764
+ }
765
+ };
766
+
767
+ // Replace the update expression with the assignment expression
768
+ Object.assign(node, assignmentExpr);
769
+ delete node.prefix;
770
+ this.BinaryExpression(node.right, _state, [...ancestors, node]);
771
+ this.AssignmentExpression(node, _state, ancestors);
772
+ },
773
+ ForStatement(node, _state, ancestors) {
774
+ if (ancestors.some(nodeIsUniform)) { return; }
775
+
776
+ // Transform for statement into strandsFor() call
777
+ // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars)
778
+
779
+ // Generate unique loop variable name
780
+ const uniqueLoopVar = `loopVar${loopVarCounter++}`;
781
+
782
+ // Create the initial callback from the for loop's init
783
+ let initialFunction;
784
+ if (node.init && node.init.type === 'VariableDeclaration') {
785
+ // Handle: for (let i = 0; ...)
786
+ const declaration = node.init.declarations[0];
787
+ let initValue = declaration.init;
788
+
789
+ const initAst = { body: [{ type: 'ExpressionStatement', expression: initValue }] };
790
+ initValue = initAst.body[0].expression;
791
+
792
+ initialFunction = {
793
+ type: 'ArrowFunctionExpression',
794
+ params: [],
795
+ body: {
796
+ type: 'BlockStatement',
797
+ body: [{
798
+ type: 'ReturnStatement',
799
+ argument: initValue
800
+ }]
801
+ }
567
802
  };
803
+ } else {
804
+ // Handle other cases - return a default value
805
+ initialFunction = {
806
+ type: 'ArrowFunctionExpression',
807
+ params: [],
808
+ body: {
809
+ type: 'BlockStatement',
810
+ body: [{
811
+ type: 'ReturnStatement',
812
+ argument: {
813
+ type: 'Literal',
814
+ value: 0
815
+ }
816
+ }]
817
+ }
818
+ };
819
+ }
568
820
 
569
- // Analyze all branches for assignments to outer scope variables
570
- analyzeBranch(thenFunction.body);
571
- analyzeBranch(elseFunction.body);
572
- if (assignedVars.size > 0) {
573
- // Add copying, reference replacement, and return statements to branch functions
574
- const addCopyingAndReturn = (functionBody, varsToReturn) => {
575
- if (functionBody.type === 'BlockStatement') {
576
- // Create temporary variables and copy statements
577
- const tempVarMap = new Map(); // property path -> temp name
578
- const copyStatements = [];
579
- for (const varPath of varsToReturn) {
580
- const parts = varPath.split('.');
581
- const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
582
- tempVarMap.set(varPath, tempName);
583
-
584
- // Build the member expression for the property path
585
- let sourceExpr = { type: 'Identifier', name: parts[0] };
586
- for (let i = 1; i < parts.length; i++) {
587
- sourceExpr = {
588
- type: 'MemberExpression',
589
- object: sourceExpr,
590
- property: { type: 'Identifier', name: parts[i] },
591
- computed: false
592
- };
593
- }
821
+ // Create the condition callback
822
+ let conditionBody = node.test || { type: 'Literal', value: true };
823
+ // Replace loop variable references with the parameter
824
+ if (node.init?.type === 'VariableDeclaration') {
825
+ const loopVarName = node.init.declarations[0].id.name;
826
+ conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar);
827
+ }
828
+ const conditionAst = { body: [{ type: 'ExpressionStatement', expression: conditionBody }] };
829
+ conditionBody = conditionAst.body[0].expression;
830
+
831
+ const conditionFunction = {
832
+ type: 'ArrowFunctionExpression',
833
+ params: [{ type: 'Identifier', name: uniqueLoopVar }],
834
+ body: conditionBody
835
+ };
594
836
 
595
- // let tempName = propertyPath.copy()
596
- copyStatements.push({
597
- type: 'VariableDeclaration',
598
- declarations: [{
599
- type: 'VariableDeclarator',
600
- id: { type: 'Identifier', name: tempName },
601
- init: {
602
- type: 'CallExpression',
603
- callee: {
604
- type: 'MemberExpression',
605
- object: sourceExpr,
606
- property: { type: 'Identifier', name: 'copy' },
607
- computed: false
608
- },
609
- arguments: []
610
- }
611
- }],
612
- kind: 'let'
613
- });
837
+ // Create the update callback
838
+ let updateFunction;
839
+ if (node.update) {
840
+ let updateExpr = node.update;
841
+ // Replace loop variable references with the parameter
842
+ if (node.init?.type === 'VariableDeclaration') {
843
+ const loopVarName = node.init.declarations[0].id.name;
844
+ updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar);
845
+ }
846
+ const updateAst = { body: [{ type: 'ExpressionStatement', expression: updateExpr }] };
847
+ updateExpr = updateAst.body[0].expression;
848
+
849
+ updateFunction = {
850
+ type: 'ArrowFunctionExpression',
851
+ params: [{ type: 'Identifier', name: uniqueLoopVar }],
852
+ body: {
853
+ type: 'BlockStatement',
854
+ body: [{
855
+ type: 'ReturnStatement',
856
+ argument: updateExpr
857
+ }]
858
+ }
859
+ };
860
+ } else {
861
+ updateFunction = {
862
+ type: 'ArrowFunctionExpression',
863
+ params: [{ type: 'Identifier', name: uniqueLoopVar }],
864
+ body: {
865
+ type: 'BlockStatement',
866
+ body: [{
867
+ type: 'ReturnStatement',
868
+ argument: { type: 'Identifier', name: uniqueLoopVar }
869
+ }]
870
+ }
871
+ };
872
+ }
873
+
874
+ // Create the body callback
875
+ let bodyBlock = node.body.type === 'BlockStatement' ? node.body : {
876
+ type: 'BlockStatement',
877
+ body: [node.body]
878
+ };
879
+
880
+ // Replace loop variable references in the body
881
+ if (node.init?.type === 'VariableDeclaration') {
882
+ const loopVarName = node.init.declarations[0].id.name;
883
+ bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar);
884
+ }
885
+
886
+ const bodyFunction = {
887
+ type: 'ArrowFunctionExpression',
888
+ params: [
889
+ { type: 'Identifier', name: uniqueLoopVar },
890
+ { type: 'Identifier', name: 'vars' }
891
+ ],
892
+ body: bodyBlock
893
+ };
894
+
895
+ // Analyze which outer scope variables are assigned in the loop body
896
+ const assignedVars = new Set();
897
+
898
+ // First pass: collect all variable declarations in the body
899
+ const localVars = new Set();
900
+ ancestor(bodyFunction.body, {
901
+ VariableDeclarator(node, ancestors) {
902
+ // Skip if we're inside a block that contains strands control flow
903
+ if (ancestors.some(statementContainsStrandsControlFlow)) return;
904
+ if (node.id.type === 'Identifier') {
905
+ localVars.add(node.id.name);
906
+ }
907
+ }
908
+ });
909
+
910
+ // Second pass: find assignments to non-local variables using acorn-walk
911
+ ancestor(bodyFunction.body, {
912
+ AssignmentExpression(node, ancestors) {
913
+ // Skip if we're inside a block that contains strands control flow
914
+ if (ancestors.some(statementContainsStrandsControlFlow)) {
915
+ return
916
+ }
917
+
918
+ const left = node.left;
919
+ if (left.type === 'Identifier') {
920
+ // Direct variable assignment: x = value
921
+ if (!localVars.has(left.name)) {
922
+ assignedVars.add(left.name);
923
+ }
924
+ } else if (left.type === 'MemberExpression') {
925
+ // Property assignment: obj.prop = value or obj.a.b = value
926
+ const propertyPath = buildPropertyPath(left);
927
+ if (propertyPath) {
928
+ const baseName = propertyPath.split('.')[0];
929
+ if (!localVars.has(baseName)) {
930
+ assignedVars.add(propertyPath);
614
931
  }
615
- // Apply reference replacement to all statements
616
- functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
617
- // Insert copy statements at the beginning
618
- functionBody.body.unshift(...copyStatements);
619
- // Add return statement with flat object using property paths as keys
620
- const returnObj = {
621
- type: 'ObjectExpression',
622
- properties: Array.from(varsToReturn).map(varPath => ({
623
- type: 'Property',
624
- key: { type: 'Literal', value: varPath },
625
- value: { type: 'Identifier', name: tempVarMap.get(varPath) },
626
- kind: 'init',
627
- computed: false,
628
- shorthand: false
629
- }))
630
- };
631
- functionBody.body.push({
632
- type: 'ReturnStatement',
633
- argument: returnObj
634
- });
635
932
  }
636
- };
637
- addCopyingAndReturn(thenFunction.body, assignedVars);
638
- addCopyingAndReturn(elseFunction.body, assignedVars);
639
- // Create a block variable to capture the return value
640
- const blockVar = `__block_${blockVarCounter++}`;
641
- // Replace with a block statement
642
- const statements = [];
643
- // Make sure every assigned variable starts as a node
644
- for (const varPath of assignedVars) {
645
- const parts = varPath.split('.');
933
+ }
934
+ }
935
+ });
646
936
 
647
- // Build left side: inputs.color or just x
648
- let leftExpr = { type: 'Identifier', name: parts[0] };
649
- for (let i = 1; i < parts.length; i++) {
650
- leftExpr = {
651
- type: 'MemberExpression',
652
- object: leftExpr,
653
- property: { type: 'Identifier', name: parts[i] },
654
- computed: false
655
- };
937
+ if (assignedVars.size > 0) {
938
+ // Add copying, reference replacement, and return statements similar to if statements
939
+ const addCopyingAndReturn = (functionBody, varsToReturn) => {
940
+ if (functionBody.type === 'BlockStatement') {
941
+ const tempVarMap = new Map();
942
+ const copyStatements = [];
943
+
944
+ for (const varPath of varsToReturn) {
945
+ const parts = varPath.split('.');
946
+ const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
947
+ tempVarMap.set(varPath, tempName);
948
+
949
+ // Build the member expression for vars.propertyPath
950
+ // e.g., vars.inputs.color or vars.x
951
+ let sourceExpr = { type: 'Identifier', name: 'vars' };
952
+ for (const part of parts) {
953
+ sourceExpr = {
954
+ type: 'MemberExpression',
955
+ object: sourceExpr,
956
+ property: { type: 'Identifier', name: part },
957
+ computed: false
958
+ };
959
+ }
960
+
961
+ copyStatements.push({
962
+ type: 'VariableDeclaration',
963
+ declarations: [{
964
+ type: 'VariableDeclarator',
965
+ id: { type: 'Identifier', name: tempName },
966
+ init: {
967
+ type: 'CallExpression',
968
+ callee: {
969
+ type: 'MemberExpression',
970
+ object: sourceExpr,
971
+ property: { type: 'Identifier', name: 'copy' },
972
+ computed: false
973
+ },
974
+ arguments: []
975
+ }
976
+ }],
977
+ kind: 'let'
978
+ });
656
979
  }
657
980
 
658
- // Build right side - same as left for strandsNode wrapping
659
- let rightArgExpr = { type: 'Identifier', name: parts[0] };
981
+ functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
982
+ functionBody.body.unshift(...copyStatements);
983
+
984
+ // Add return statement with flat object using property paths as keys
985
+ const returnObj = {
986
+ type: 'ObjectExpression',
987
+ properties: Array.from(varsToReturn).map(varPath => ({
988
+ type: 'Property',
989
+ key: { type: 'Literal', value: varPath },
990
+ value: { type: 'Identifier', name: tempVarMap.get(varPath) },
991
+ kind: 'init',
992
+ computed: false,
993
+ shorthand: false
994
+ }))
995
+ };
996
+
997
+ functionBody.body.push({
998
+ type: 'ReturnStatement',
999
+ argument: returnObj
1000
+ });
1001
+ }
1002
+ };
1003
+
1004
+ addCopyingAndReturn(bodyFunction.body, assignedVars);
1005
+
1006
+ // Create block variable and assignments similar to if statements
1007
+ const blockVar = `__block_${blockVarCounter++}`;
1008
+ const statements = [];
1009
+
1010
+ const initialVarsObject = {
1011
+ type: 'ObjectExpression',
1012
+ properties: Array.from(assignedVars).map(varPath => {
1013
+ const parts = varPath.split('.');
1014
+ let expr = { type: 'Identifier', name: parts[0] };
660
1015
  for (let i = 1; i < parts.length; i++) {
661
- rightArgExpr = {
1016
+ expr = {
662
1017
  type: 'MemberExpression',
663
- object: rightArgExpr,
1018
+ object: expr,
664
1019
  property: { type: 'Identifier', name: parts[i] },
665
1020
  computed: false
666
1021
  };
667
1022
  }
1023
+ const wrappedExpr = {
1024
+ type: 'CallExpression',
1025
+ callee: { type: 'Identifier', name: '__p5.strandsNode' },
1026
+ arguments: [expr]
1027
+ };
1028
+ return {
1029
+ type: 'Property',
1030
+ key: { type: 'Literal', value: varPath },
1031
+ value: wrappedExpr,
1032
+ kind: 'init',
1033
+ computed: false,
1034
+ shorthand: false
1035
+ };
1036
+ })
1037
+ };
668
1038
 
669
- statements.push({
670
- type: 'ExpressionStatement',
671
- expression: {
672
- type: 'AssignmentExpression',
673
- operator: '=',
674
- left: leftExpr,
675
- right: {
676
- type: 'CallExpression',
677
- callee: { type: 'Identifier', name: '__p5.strandsNode' },
678
- arguments: [rightArgExpr],
679
- }
680
- }
681
- });
1039
+ // Create the strandsFor call
1040
+ const callExpression = {
1041
+ type: 'CallExpression',
1042
+ callee: {
1043
+ type: 'Identifier',
1044
+ name: '__p5.strandsFor'
1045
+ },
1046
+ arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, initialVarsObject]
1047
+ };
1048
+
1049
+ statements.push({
1050
+ type: 'VariableDeclaration',
1051
+ declarations: [{
1052
+ type: 'VariableDeclarator',
1053
+ id: { type: 'Identifier', name: blockVar },
1054
+ init: callExpression
1055
+ }],
1056
+ kind: 'const'
1057
+ });
1058
+
1059
+ // Add assignments back to original variables
1060
+ for (const varPath of assignedVars) {
1061
+ const parts = varPath.split('.');
1062
+
1063
+ // Build left side: inputs.color or just x
1064
+ let leftExpr = { type: 'Identifier', name: parts[0] };
1065
+ for (let i = 1; i < parts.length; i++) {
1066
+ leftExpr = {
1067
+ type: 'MemberExpression',
1068
+ object: leftExpr,
1069
+ property: { type: 'Identifier', name: parts[i] },
1070
+ computed: false
1071
+ };
1072
+ }
1073
+
1074
+ // Build right side: __block_2.inputs.color or __block_2.x
1075
+ let rightExpr = { type: 'Identifier', name: blockVar };
1076
+ for (const part of parts) {
1077
+ rightExpr = {
1078
+ type: 'MemberExpression',
1079
+ object: rightExpr,
1080
+ property: { type: 'Identifier', name: part },
1081
+ computed: false
1082
+ };
682
1083
  }
1084
+
683
1085
  statements.push({
684
- type: 'VariableDeclaration',
685
- declarations: [{
686
- type: 'VariableDeclarator',
687
- id: { type: 'Identifier', name: blockVar },
688
- init: callExpression
689
- }],
690
- kind: 'const'
1086
+ type: 'ExpressionStatement',
1087
+ expression: {
1088
+ type: 'AssignmentExpression',
1089
+ operator: '=',
1090
+ left: leftExpr,
1091
+ right: rightExpr
1092
+ }
691
1093
  });
692
- // 2. Assignments for each modified variable
693
- for (const varPath of assignedVars) {
694
- const parts = varPath.split('.');
1094
+ }
1095
+
1096
+ node.type = 'BlockStatement';
1097
+ node.body = statements;
1098
+ } else {
1099
+ // No assignments, just replace with call expression
1100
+ node.type = 'ExpressionStatement';
1101
+ node.expression = {
1102
+ type: 'CallExpression',
1103
+ callee: {
1104
+ type: 'Identifier',
1105
+ name: '__p5.strandsFor'
1106
+ },
1107
+ arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, {
1108
+ type: 'ObjectExpression',
1109
+ properties: []
1110
+ }]
1111
+ };
1112
+ }
695
1113
 
696
- // Build left side: inputs.color or just x
697
- let leftExpr = { type: 'Identifier', name: parts[0] };
698
- for (let i = 1; i < parts.length; i++) {
699
- leftExpr = {
700
- type: 'MemberExpression',
701
- object: leftExpr,
702
- property: { type: 'Identifier', name: parts[i] },
703
- computed: false
704
- };
705
- }
1114
+ delete node.init;
1115
+ delete node.test;
1116
+ delete node.update;
1117
+ },
706
1118
 
707
- // Build right side: __block_2['inputs.color'] or __block_2['x']
708
- const rightExpr = {
709
- type: 'MemberExpression',
710
- object: { type: 'Identifier', name: blockVar },
711
- property: { type: 'Literal', value: varPath },
712
- computed: true
713
- };
1119
+ // Helper method to replace identifier references in AST nodes
1120
+ replaceIdentifierReferences(node, oldName, newName) {
1121
+ if (!node || typeof node !== 'object') return node;
714
1122
 
715
- statements.push({
716
- type: 'ExpressionStatement',
717
- expression: {
718
- type: 'AssignmentExpression',
719
- operator: '=',
720
- left: leftExpr,
721
- right: rightExpr
722
- }
723
- });
724
- }
725
- // Replace the if statement with a block statement
726
- node.type = 'BlockStatement';
727
- node.body = statements;
728
- } else {
729
- // No assignments, just replace with the call expression
730
- node.type = 'ExpressionStatement';
731
- node.expression = callExpression;
732
- }
733
- delete node.test;
734
- delete node.consequent;
735
- delete node.alternate;
736
- },
737
- UpdateExpression(node, _state, ancestors) {
738
- if (ancestors.some(nodeIsUniform)) { return; }
739
-
740
- // Transform ++var, var++, --var, var-- into assignment expressions
741
- let operator;
742
- if (node.operator === '++') {
743
- operator = '+';
744
- } else if (node.operator === '--') {
745
- operator = '-';
746
- } else {
747
- return; // Unknown update operator
1123
+ const replaceInNode = (n) => {
1124
+ if (!n || typeof n !== 'object') return n;
1125
+
1126
+ if (n.type === 'Identifier' && n.name === oldName) {
1127
+ return { ...n, name: newName };
748
1128
  }
749
1129
 
750
- // Convert to: var = var + 1 or var = var - 1
751
- const assignmentExpr = {
752
- type: 'AssignmentExpression',
753
- operator: '=',
754
- left: node.argument,
755
- right: {
756
- type: 'BinaryExpression',
757
- operator: operator,
758
- left: node.argument,
759
- right: {
760
- type: 'Literal',
761
- value: 1
1130
+ // Create a copy and recursively process properties
1131
+ const newNode = { ...n };
1132
+ for (const key in n) {
1133
+ if (n.hasOwnProperty(key) && key !== 'parent') {
1134
+ if (Array.isArray(n[key])) {
1135
+ newNode[key] = n[key].map(replaceInNode);
1136
+ } else if (typeof n[key] === 'object') {
1137
+ newNode[key] = replaceInNode(n[key]);
762
1138
  }
763
1139
  }
764
- };
1140
+ }
1141
+ return newNode;
1142
+ };
765
1143
 
766
- // Replace the update expression with the assignment expression
767
- Object.assign(node, assignmentExpr);
768
- delete node.prefix;
769
- this.BinaryExpression(node.right, _state, [...ancestors, node]);
770
- this.AssignmentExpression(node, _state, ancestors);
1144
+ return replaceInNode(node);
1145
+ }
1146
+ };
1147
+
1148
+ // Helper function to check if a function body contains return statements in control flow
1149
+ function functionHasEarlyReturns(functionNode) {
1150
+ let hasEarlyReturn = false;
1151
+ let inControlFlow = 0;
1152
+
1153
+ const checkForEarlyReturns = {
1154
+ IfStatement(node, state, c) {
1155
+ inControlFlow++;
1156
+ if (node.test) c(node.test, state);
1157
+ if (node.consequent) c(node.consequent, state);
1158
+ if (node.alternate) c(node.alternate, state);
1159
+ inControlFlow--;
771
1160
  },
772
- ForStatement(node, _state, ancestors) {
773
- if (ancestors.some(nodeIsUniform)) { return; }
1161
+ ForStatement(node, state, c) {
1162
+ inControlFlow++;
1163
+ if (node.init) c(node.init, state);
1164
+ if (node.test) c(node.test, state);
1165
+ if (node.update) c(node.update, state);
1166
+ if (node.body) c(node.body, state);
1167
+ inControlFlow--;
1168
+ },
1169
+ ReturnStatement(node) {
1170
+ if (inControlFlow > 0) {
1171
+ hasEarlyReturn = true;
1172
+ }
1173
+ }
1174
+ };
774
1175
 
775
- // Transform for statement into strandsFor() call
776
- // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars)
1176
+ if (functionNode.body && functionNode.body.type === 'BlockStatement') {
1177
+ recursive(functionNode.body, {}, checkForEarlyReturns);
1178
+ }
777
1179
 
778
- // Generate unique loop variable name
779
- const uniqueLoopVar = `loopVar${loopVarCounter++}`;
1180
+ return hasEarlyReturn;
1181
+ }
780
1182
 
781
- // Create the initial callback from the for loop's init
782
- let initialFunction;
783
- if (node.init && node.init.type === 'VariableDeclaration') {
784
- // Handle: for (let i = 0; ...)
785
- const declaration = node.init.declarations[0];
786
- let initValue = declaration.init;
1183
+ // Helper function to check if a block contains a return anywhere in it
1184
+ function blockContainsReturn(block) {
1185
+ let hasReturn = false;
1186
+ const findReturn = {
1187
+ ReturnStatement() {
1188
+ hasReturn = true;
1189
+ }
1190
+ };
1191
+ if (block) {
1192
+ recursive(block, {}, findReturn);
1193
+ }
1194
+ return hasReturn;
1195
+ }
787
1196
 
788
- const initAst = { body: [{ type: 'ExpressionStatement', expression: initValue }] };
789
- initValue = initAst.body[0].expression;
1197
+ // Transform a helper function to use __returnValue pattern instead of early returns.
1198
+ // This is necessary because we evaluate helper function *in javascript* rather than
1199
+ // converting them to functions in GLSL (which is hard because we don't know the types
1200
+ // of function parameters upfront, and they may change from use to use.) So they act
1201
+ // like macros, all contributing to build up a single function overall. An early return
1202
+ // in a helper should not be an early return of the entire hook function. Instead, we
1203
+ // just make sure helper functions always evaluate to a single value.
1204
+ function transformHelperFunction(functionNode) {
1205
+ // 1. Add __returnValue declaration at the start of function body
1206
+ const returnValueDecl = {
1207
+ type: 'VariableDeclaration',
1208
+ declarations: [{
1209
+ type: 'VariableDeclarator',
1210
+ id: { type: 'Identifier', name: '__returnValue' },
1211
+ init: null
1212
+ }],
1213
+ kind: 'let'
1214
+ };
790
1215
 
791
- initialFunction = {
792
- type: 'ArrowFunctionExpression',
793
- params: [],
794
- body: {
795
- type: 'BlockStatement',
796
- body: [{
797
- type: 'ReturnStatement',
798
- argument: initValue
799
- }]
800
- }
801
- };
802
- } else {
803
- // Handle other cases - return a default value
804
- initialFunction = {
805
- type: 'ArrowFunctionExpression',
806
- params: [],
807
- body: {
808
- type: 'BlockStatement',
809
- body: [{
810
- type: 'ReturnStatement',
811
- argument: {
812
- type: 'Literal',
813
- value: 0
814
- }
815
- }]
816
- }
817
- };
818
- }
1216
+ if (!functionNode.body || functionNode.body.type !== 'BlockStatement') {
1217
+ return; // Can't transform arrow functions with expression bodies
1218
+ }
819
1219
 
820
- // Create the condition callback
821
- let conditionBody = node.test || { type: 'Literal', value: true };
822
- // Replace loop variable references with the parameter
823
- if (node.init?.type === 'VariableDeclaration') {
824
- const loopVarName = node.init.declarations[0].id.name;
825
- conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar);
826
- }
827
- const conditionAst = { body: [{ type: 'ExpressionStatement', expression: conditionBody }] };
828
- conditionBody = conditionAst.body[0].expression;
1220
+ functionNode.body.body.unshift(returnValueDecl);
829
1221
 
830
- const conditionFunction = {
831
- type: 'ArrowFunctionExpression',
832
- params: [{ type: 'Identifier', name: uniqueLoopVar }],
833
- body: conditionBody
834
- };
1222
+ // 2. Restructure if statements: move siblings after if with return into else block
1223
+ function restructureIfStatements(statements) {
1224
+ for (let i = 0; i < statements.length; i++) {
1225
+ const stmt = statements[i];
835
1226
 
836
- // Create the update callback
837
- let updateFunction;
838
- if (node.update) {
839
- let updateExpr = node.update;
840
- // Replace loop variable references with the parameter
841
- if (node.init?.type === 'VariableDeclaration') {
842
- const loopVarName = node.init.declarations[0].id.name;
843
- updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar);
844
- }
845
- const updateAst = { body: [{ type: 'ExpressionStatement', expression: updateExpr }] };
846
- updateExpr = updateAst.body[0].expression;
1227
+ if (stmt.type === 'IfStatement' && blockContainsReturn(stmt.consequent) && !stmt.alternate) {
1228
+ // Find all subsequent statements
1229
+ const subsequentStatements = statements.slice(i + 1);
847
1230
 
848
- updateFunction = {
849
- type: 'ArrowFunctionExpression',
850
- params: [{ type: 'Identifier', name: uniqueLoopVar }],
851
- body: {
852
- type: 'BlockStatement',
853
- body: [{
854
- type: 'ReturnStatement',
855
- argument: updateExpr
856
- }]
857
- }
858
- };
859
- } else {
860
- updateFunction = {
861
- type: 'ArrowFunctionExpression',
862
- params: [{ type: 'Identifier', name: uniqueLoopVar }],
863
- body: {
1231
+ if (subsequentStatements.length > 0) {
1232
+ // Create else block with subsequent statements
1233
+ stmt.alternate = {
864
1234
  type: 'BlockStatement',
865
- body: [{
866
- type: 'ReturnStatement',
867
- argument: { type: 'Identifier', name: uniqueLoopVar }
868
- }]
869
- }
870
- };
871
- }
1235
+ body: subsequentStatements
1236
+ };
872
1237
 
873
- // Create the body callback
874
- let bodyBlock = node.body.type === 'BlockStatement' ? node.body : {
875
- type: 'BlockStatement',
876
- body: [node.body]
877
- };
1238
+ // Remove the subsequent statements from this level
1239
+ statements.splice(i + 1);
878
1240
 
879
- // Replace loop variable references in the body
880
- if (node.init?.type === 'VariableDeclaration') {
881
- const loopVarName = node.init.declarations[0].id.name;
882
- bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar);
1241
+ // Recursively process the new else block
1242
+ restructureIfStatements(stmt.alternate.body);
1243
+ }
883
1244
  }
884
1245
 
885
- const bodyFunction = {
886
- type: 'ArrowFunctionExpression',
887
- params: [
888
- { type: 'Identifier', name: uniqueLoopVar },
889
- { type: 'Identifier', name: 'vars' }
890
- ],
891
- body: bodyBlock
1246
+ // Recursively process nested blocks
1247
+ if (stmt.type === 'IfStatement') {
1248
+ if (stmt.consequent && stmt.consequent.type === 'BlockStatement') {
1249
+ restructureIfStatements(stmt.consequent.body);
1250
+ }
1251
+ if (stmt.alternate && stmt.alternate.type === 'BlockStatement') {
1252
+ restructureIfStatements(stmt.alternate.body);
1253
+ }
1254
+ } else if (stmt.type === 'ForStatement' && stmt.body && stmt.body.type === 'BlockStatement') {
1255
+ restructureIfStatements(stmt.body.body);
1256
+ } else if (stmt.type === 'BlockStatement') {
1257
+ restructureIfStatements(stmt.body);
1258
+ }
1259
+ }
1260
+ }
1261
+
1262
+ restructureIfStatements(functionNode.body.body);
1263
+
1264
+ // 3. Transform all return statements to assignments
1265
+ const transformReturns = {
1266
+ ReturnStatement(node) {
1267
+ // Convert return statement to assignment
1268
+ node.type = 'ExpressionStatement';
1269
+ node.expression = {
1270
+ type: 'AssignmentExpression',
1271
+ operator: '=',
1272
+ left: { type: 'Identifier', name: '__returnValue' },
1273
+ right: node.argument || { type: 'Identifier', name: 'undefined' }
892
1274
  };
1275
+ delete node.argument;
1276
+ }
1277
+ };
893
1278
 
894
- // Analyze which outer scope variables are assigned in the loop body
895
- const assignedVars = new Set();
1279
+ recursive(functionNode.body, {}, transformReturns);
896
1280
 
897
- // First pass: collect all variable declarations in the body
898
- const localVars = new Set();
899
- ancestor(bodyFunction.body, {
900
- VariableDeclarator(node, ancestors) {
901
- // Skip if we're inside a block that contains strands control flow
902
- if (ancestors.some(statementContainsStrandsControlFlow)) return;
903
- if (node.id.type === 'Identifier') {
904
- localVars.add(node.id.name);
905
- }
906
- }
907
- });
1281
+ // 4. Add final return statement
1282
+ const finalReturn = {
1283
+ type: 'ReturnStatement',
1284
+ argument: { type: 'Identifier', name: '__returnValue' }
1285
+ };
908
1286
 
909
- // Second pass: find assignments to non-local variables using acorn-walk
910
- ancestor(bodyFunction.body, {
911
- AssignmentExpression(node, ancestors) {
912
- // Skip if we're inside a block that contains strands control flow
913
- if (ancestors.some(statementContainsStrandsControlFlow)) {
914
- return
915
- }
1287
+ functionNode.body.body.push(finalReturn);
1288
+ }
916
1289
 
917
- const left = node.left;
918
- if (left.type === 'Identifier') {
919
- // Direct variable assignment: x = value
920
- if (!localVars.has(left.name)) {
921
- assignedVars.add(left.name);
922
- }
923
- } else if (left.type === 'MemberExpression') {
924
- // Property assignment: obj.prop = value or obj.a.b = value
925
- const propertyPath = buildPropertyPath(left);
926
- if (propertyPath) {
927
- const baseName = propertyPath.split('.')[0];
928
- if (!localVars.has(baseName)) {
929
- assignedVars.add(propertyPath);
930
- }
931
- }
932
- }
933
- }
934
- });
1290
+ // Helper function to check if a function body contains .set() calls in control flow
1291
+ function functionHasSetInControlFlow(functionNode) {
1292
+ let hasSetInControlFlow = false;
1293
+ let inControlFlow = 0;
1294
+
1295
+ const checkForSetCalls = {
1296
+ IfStatement(node, state, c) {
1297
+ inControlFlow++;
1298
+ if (node.test) c(node.test, state);
1299
+ if (node.consequent) c(node.consequent, state);
1300
+ if (node.alternate) c(node.alternate, state);
1301
+ inControlFlow--;
1302
+ },
1303
+ ForStatement(node, state, c) {
1304
+ inControlFlow++;
1305
+ if (node.init) c(node.init, state);
1306
+ if (node.test) c(node.test, state);
1307
+ if (node.update) c(node.update, state);
1308
+ if (node.body) c(node.body, state);
1309
+ inControlFlow--;
1310
+ },
1311
+ CallExpression(node) {
1312
+ // Check if this is a .set() call
1313
+ if (inControlFlow > 0 &&
1314
+ node.callee?.type === 'MemberExpression' &&
1315
+ node.callee?.property?.name === 'set') {
1316
+ hasSetInControlFlow = true;
1317
+ }
1318
+ }
1319
+ };
935
1320
 
936
- if (assignedVars.size > 0) {
937
- // Add copying, reference replacement, and return statements similar to if statements
938
- const addCopyingAndReturn = (functionBody, varsToReturn) => {
939
- if (functionBody.type === 'BlockStatement') {
940
- const tempVarMap = new Map();
941
- const copyStatements = [];
942
-
943
- for (const varPath of varsToReturn) {
944
- const parts = varPath.split('.');
945
- const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
946
- tempVarMap.set(varPath, tempName);
947
-
948
- // Build the member expression for vars.propertyPath
949
- // e.g., vars.inputs.color or vars.x
950
- let sourceExpr = { type: 'Identifier', name: 'vars' };
951
- for (const part of parts) {
952
- sourceExpr = {
953
- type: 'MemberExpression',
954
- object: sourceExpr,
955
- property: { type: 'Identifier', name: part },
956
- computed: false
957
- };
958
- }
1321
+ if (functionNode.body && functionNode.body.type === 'BlockStatement') {
1322
+ recursive(functionNode.body, {}, checkForSetCalls);
1323
+ }
959
1324
 
960
- copyStatements.push({
961
- type: 'VariableDeclaration',
962
- declarations: [{
963
- type: 'VariableDeclarator',
964
- id: { type: 'Identifier', name: tempName },
965
- init: {
966
- type: 'CallExpression',
967
- callee: {
968
- type: 'MemberExpression',
969
- object: sourceExpr,
970
- property: { type: 'Identifier', name: 'copy' },
971
- computed: false
972
- },
973
- arguments: []
974
- }
975
- }],
976
- kind: 'let'
977
- });
978
- }
1325
+ return hasSetInControlFlow;
1326
+ }
979
1327
 
980
- functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
981
- functionBody.body.unshift(...copyStatements);
982
-
983
- // Add return statement with flat object using property paths as keys
984
- const returnObj = {
985
- type: 'ObjectExpression',
986
- properties: Array.from(varsToReturn).map(varPath => ({
987
- type: 'Property',
988
- key: { type: 'Literal', value: varPath },
989
- value: { type: 'Identifier', name: tempVarMap.get(varPath) },
990
- kind: 'init',
991
- computed: false,
992
- shorthand: false
993
- }))
994
- };
1328
+ // Transform a function to use __setValue pattern instead of .set() calls in branches/loops
1329
+ function transformFunctionSetCalls(functionNode) {
1330
+ if (!functionNode.body || functionNode.body.type !== 'BlockStatement') {
1331
+ return; // Can't transform arrow functions with expression bodies
1332
+ }
995
1333
 
996
- functionBody.body.push({
997
- type: 'ReturnStatement',
998
- argument: returnObj
999
- });
1000
- }
1001
- };
1334
+ // Track which hooks have .set() calls, mapping expression string to the actual AST node
1335
+ const hooksWithSetCalls = new Map(); // exprString -> hookObjectNode
1336
+
1337
+ // First pass: find all hooks that have .set() calls in control flow
1338
+ const findSetCalls = {
1339
+ CallExpression(node) {
1340
+ if (node.callee?.type === 'MemberExpression' &&
1341
+ node.callee?.property?.name === 'set' &&
1342
+ node.callee?.object) {
1343
+ // This is something like filterColor.set(...) or myp5.filterColor.set(...)
1344
+ const hookObjectNode = node.callee.object;
1345
+ const exprString = escodegen.generate(hookObjectNode);
1346
+ if (!hooksWithSetCalls.has(exprString)) {
1347
+ hooksWithSetCalls.set(exprString, hookObjectNode);
1348
+ }
1349
+ }
1350
+ }
1351
+ };
1002
1352
 
1003
- addCopyingAndReturn(bodyFunction.body, assignedVars);
1353
+ recursive(functionNode.body, {}, findSetCalls);
1004
1354
 
1005
- // Create block variable and assignments similar to if statements
1006
- const blockVar = `__block_${blockVarCounter++}`;
1007
- const statements = [];
1355
+ if (hooksWithSetCalls.size === 0) {
1356
+ return; // No .set() calls to transform
1357
+ }
1008
1358
 
1009
- const initialVarsObject = {
1010
- type: 'ObjectExpression',
1011
- properties: Array.from(assignedVars).map(varPath => {
1012
- const parts = varPath.split('.');
1013
- let expr = { type: 'Identifier', name: parts[0] };
1014
- for (let i = 1; i < parts.length; i++) {
1015
- expr = {
1016
- type: 'MemberExpression',
1017
- object: expr,
1018
- property: { type: 'Identifier', name: parts[i] },
1019
- computed: false
1020
- };
1021
- }
1022
- const wrappedExpr = {
1023
- type: 'CallExpression',
1024
- callee: { type: 'Identifier', name: '__p5.strandsNode' },
1025
- arguments: [expr]
1026
- };
1027
- return {
1028
- type: 'Property',
1029
- key: { type: 'Literal', value: varPath },
1030
- value: wrappedExpr,
1031
- kind: 'init',
1032
- computed: false,
1033
- shorthand: false
1034
- };
1035
- })
1036
- };
1359
+ // For each hook with .set() calls, add intermediate variable and transform
1360
+ for (const [exprString, hookObjectNode] of hooksWithSetCalls) {
1361
+ // Create a safe variable name from the expression
1362
+ const safeVarName = exprString.replace(/[^a-zA-Z0-9_]/g, '_');
1363
+ const intermediateVarName = `__${safeVarName}_value`;
1364
+
1365
+ // 1. Find the .begin() call and insert intermediate variable right after it
1366
+ const intermediateVarDecl = {
1367
+ type: 'VariableDeclaration',
1368
+ declarations: [{
1369
+ type: 'VariableDeclarator',
1370
+ id: { type: 'Identifier', name: intermediateVarName },
1371
+ init: null
1372
+ }],
1373
+ kind: 'let'
1374
+ };
1037
1375
 
1038
- // Create the strandsFor call
1039
- const callExpression = {
1040
- type: 'CallExpression',
1041
- callee: {
1042
- type: 'Identifier',
1043
- name: '__p5.strandsFor'
1044
- },
1045
- arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, initialVarsObject]
1046
- };
1376
+ let beginCallIndex = -1;
1377
+ for (let i = 0; i < functionNode.body.body.length; i++) {
1378
+ const stmt = functionNode.body.body[i];
1379
+ if (stmt.type === 'ExpressionStatement' &&
1380
+ stmt.expression?.type === 'CallExpression' &&
1381
+ stmt.expression?.callee?.type === 'MemberExpression' &&
1382
+ stmt.expression?.callee?.property?.name === 'begin') {
1383
+ const beginExprString = escodegen.generate(stmt.expression.callee.object);
1384
+ if (beginExprString === exprString) {
1385
+ beginCallIndex = i;
1386
+ break;
1387
+ }
1388
+ }
1389
+ }
1047
1390
 
1048
- statements.push({
1049
- type: 'VariableDeclaration',
1050
- declarations: [{
1051
- type: 'VariableDeclarator',
1052
- id: { type: 'Identifier', name: blockVar },
1053
- init: callExpression
1054
- }],
1055
- kind: 'const'
1056
- });
1391
+ // Insert intermediate variable after .begin() if found, otherwise at the start
1392
+ if (beginCallIndex !== -1) {
1393
+ functionNode.body.body.splice(beginCallIndex + 1, 0, intermediateVarDecl);
1394
+ } else {
1395
+ functionNode.body.body.unshift(intermediateVarDecl);
1396
+ }
1057
1397
 
1058
- // Add assignments back to original variables
1059
- for (const varPath of assignedVars) {
1060
- const parts = varPath.split('.');
1398
+ // 2. Transform all .set() calls to assignments
1399
+ const transformSetToAssignment = {
1400
+ CallExpression(node, state, ancestors) {
1401
+ // Check if this is a .set() call for this hook
1402
+ if (node.callee?.type === 'MemberExpression' &&
1403
+ node.callee?.property?.name === 'set' &&
1404
+ node.callee?.object) {
1405
+ const currentExprString = escodegen.generate(node.callee.object);
1406
+ if (currentExprString === exprString && node.arguments.length > 0) {
1407
+ // Find the parent statement
1408
+ let parentStmt = null;
1409
+ for (let i = ancestors.length - 1; i >= 0; i--) {
1410
+ if (ancestors[i].type === 'ExpressionStatement') {
1411
+ parentStmt = ancestors[i];
1412
+ break;
1413
+ }
1414
+ }
1061
1415
 
1062
- // Build left side: inputs.color or just x
1063
- let leftExpr = { type: 'Identifier', name: parts[0] };
1064
- for (let i = 1; i < parts.length; i++) {
1065
- leftExpr = {
1066
- type: 'MemberExpression',
1067
- object: leftExpr,
1068
- property: { type: 'Identifier', name: parts[i] },
1069
- computed: false
1070
- };
1416
+ if (parentStmt) {
1417
+ // Replace the .set() call with an assignment
1418
+ parentStmt.type = 'ExpressionStatement';
1419
+ parentStmt.expression = {
1420
+ type: 'AssignmentExpression',
1421
+ operator: '=',
1422
+ left: { type: 'Identifier', name: intermediateVarName },
1423
+ right: node.arguments[0]
1424
+ };
1425
+ }
1071
1426
  }
1427
+ }
1428
+ }
1429
+ };
1072
1430
 
1073
- // Build right side: __block_2.inputs.color or __block_2.x
1074
- let rightExpr = { type: 'Identifier', name: blockVar };
1075
- for (const part of parts) {
1076
- rightExpr = {
1077
- type: 'MemberExpression',
1078
- object: rightExpr,
1079
- property: { type: 'Identifier', name: part },
1080
- computed: false
1081
- };
1082
- }
1431
+ ancestor(functionNode.body, transformSetToAssignment);
1083
1432
 
1084
- statements.push({
1085
- type: 'ExpressionStatement',
1086
- expression: {
1087
- type: 'AssignmentExpression',
1088
- operator: '=',
1089
- left: leftExpr,
1090
- right: rightExpr
1091
- }
1092
- });
1433
+ // 3. Find the .end() call and insert final .set() call right before it
1434
+ const finalSetCall = {
1435
+ type: 'ExpressionStatement',
1436
+ expression: {
1437
+ type: 'CallExpression',
1438
+ callee: {
1439
+ type: 'MemberExpression',
1440
+ object: JSON.parse(JSON.stringify(hookObjectNode)), // Deep copy the original node
1441
+ property: { type: 'Identifier', name: 'set' },
1442
+ computed: false
1443
+ },
1444
+ arguments: [{ type: 'Identifier', name: intermediateVarName }]
1445
+ }
1446
+ };
1447
+
1448
+ // Find the .end() call for this hook
1449
+ let endCallIndex = -1;
1450
+ for (let i = 0; i < functionNode.body.body.length; i++) {
1451
+ const stmt = functionNode.body.body[i];
1452
+ if (stmt.type === 'ExpressionStatement' &&
1453
+ stmt.expression?.type === 'CallExpression' &&
1454
+ stmt.expression?.callee?.type === 'MemberExpression' &&
1455
+ stmt.expression?.callee?.property?.name === 'end') {
1456
+ const endExprString = escodegen.generate(stmt.expression.callee.object);
1457
+ if (endExprString === exprString) {
1458
+ endCallIndex = i;
1459
+ break;
1093
1460
  }
1461
+ }
1462
+ }
1094
1463
 
1095
- node.type = 'BlockStatement';
1096
- node.body = statements;
1464
+ // Insert the final .set() call before .end() if found, otherwise at the end
1465
+ if (endCallIndex !== -1) {
1466
+ functionNode.body.body.splice(endCallIndex, 0, finalSetCall);
1467
+ } else {
1468
+ // If no .end() found, insert before return statement or at the end
1469
+ const lastStatement = functionNode.body.body[functionNode.body.body.length - 1];
1470
+ if (lastStatement && lastStatement.type === 'ReturnStatement') {
1471
+ functionNode.body.body.splice(functionNode.body.body.length - 1, 0, finalSetCall);
1097
1472
  } else {
1098
- // No assignments, just replace with call expression
1099
- node.type = 'ExpressionStatement';
1100
- node.expression = {
1101
- type: 'CallExpression',
1102
- callee: {
1103
- type: 'Identifier',
1104
- name: '__p5.strandsFor'
1105
- },
1106
- arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, {
1107
- type: 'ObjectExpression',
1108
- properties: []
1109
- }]
1110
- };
1473
+ functionNode.body.body.push(finalSetCall);
1111
1474
  }
1475
+ }
1476
+ }
1477
+ }
1112
1478
 
1113
- delete node.init;
1114
- delete node.test;
1115
- delete node.update;
1479
+ // Main transformation pass: find and transform functions with .set() calls in control flow
1480
+ function transformSetCallsInControlFlow(ast) {
1481
+ const functionsToTransform = [];
1482
+
1483
+ // Collect functions that have .set() calls in control flow
1484
+ const collectFunctions = {
1485
+ ArrowFunctionExpression(node, ancestors) {
1486
+ if (functionHasSetInControlFlow(node)) {
1487
+ functionsToTransform.push(node);
1488
+ }
1116
1489
  },
1490
+ FunctionExpression(node, ancestors) {
1491
+ if (functionHasSetInControlFlow(node)) {
1492
+ functionsToTransform.push(node);
1493
+ }
1494
+ },
1495
+ FunctionDeclaration(node, ancestors) {
1496
+ if (functionHasSetInControlFlow(node)) {
1497
+ functionsToTransform.push(node);
1498
+ }
1499
+ }
1500
+ };
1117
1501
 
1118
- // Helper method to replace identifier references in AST nodes
1119
- replaceIdentifierReferences(node, oldName, newName) {
1120
- if (!node || typeof node !== 'object') return node;
1502
+ ancestor(ast, collectFunctions);
1121
1503
 
1122
- const replaceInNode = (n) => {
1123
- if (!n || typeof n !== 'object') return n;
1504
+ // Transform each collected function
1505
+ for (const funcNode of functionsToTransform) {
1506
+ transformFunctionSetCalls(funcNode);
1507
+ }
1508
+ }
1124
1509
 
1125
- if (n.type === 'Identifier' && n.name === oldName) {
1126
- return { ...n, name: newName };
1510
+ // Main transformation pass: find and transform helper functions with early returns
1511
+ function transformHelperFunctionEarlyReturns(ast) {
1512
+ const helperFunctionsToTransform = [];
1513
+
1514
+ // Collect helper functions that need transformation
1515
+ const collectHelperFunctions = {
1516
+ VariableDeclarator(node, ancestors) {
1517
+ const init = node.init;
1518
+ if (init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
1519
+ if (functionHasEarlyReturns(init)) {
1520
+ helperFunctionsToTransform.push(init);
1127
1521
  }
1522
+ }
1523
+ },
1524
+ FunctionDeclaration(node, ancestors) {
1525
+ if (functionHasEarlyReturns(node)) {
1526
+ helperFunctionsToTransform.push(node);
1527
+ }
1528
+ },
1529
+ // Don't transform functions that are direct arguments to call expressions
1530
+ CallExpression(node, ancestors) {
1531
+ // Arguments to CallExpressions are base callbacks, not helpers
1532
+ // We skip them by not adding them to the transformation list
1533
+ }
1534
+ };
1128
1535
 
1129
- // Create a copy and recursively process properties
1130
- const newNode = { ...n };
1131
- for (const key in n) {
1132
- if (n.hasOwnProperty(key) && key !== 'parent') {
1133
- if (Array.isArray(n[key])) {
1134
- newNode[key] = n[key].map(replaceInNode);
1135
- } else if (typeof n[key] === 'object') {
1136
- newNode[key] = replaceInNode(n[key]);
1137
- }
1138
- }
1139
- }
1140
- return newNode;
1141
- };
1536
+ ancestor(ast, collectHelperFunctions);
1537
+
1538
+ // Transform each collected helper function
1539
+ for (const funcNode of helperFunctionsToTransform) {
1540
+ transformHelperFunction(funcNode);
1541
+ }
1542
+ }
1142
1543
 
1143
- return replaceInNode(node);
1544
+ function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
1545
+ // Reset counters at the start of each transpilation
1546
+ blockVarCounter = 0;
1547
+ loopVarCounter = 0;
1548
+
1549
+ const ast = parse(sourceString, {
1550
+ ecmaVersion: 2021,
1551
+ locations: srcLocations
1552
+ });
1553
+
1554
+ // First pass: transform .set() calls in control flow to use intermediate variables
1555
+ transformSetCallsInControlFlow(ast);
1556
+
1557
+ // Second pass: transform everything except if/for statements using normal ancestor traversal
1558
+ const nonControlFlowCallbacks = { ...ASTCallbacks };
1559
+ delete nonControlFlowCallbacks.IfStatement;
1560
+ delete nonControlFlowCallbacks.ForStatement;
1561
+ ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {} });
1562
+
1563
+ // Third pass: transform helper functions with early returns to use __returnValue pattern
1564
+ transformHelperFunctionEarlyReturns(ast);
1565
+
1566
+ // Fourth pass: transform if/for statements in post-order using recursive traversal
1567
+ const postOrderControlFlowTransform = {
1568
+ IfStatement(node, state, c) {
1569
+ state.inControlFlow++;
1570
+ // First recursively process children
1571
+ if (node.test) c(node.test, state);
1572
+ if (node.consequent) c(node.consequent, state);
1573
+ if (node.alternate) c(node.alternate, state);
1574
+ // Then apply the transformation to this node
1575
+ ASTCallbacks.IfStatement(node, state, []);
1576
+ state.inControlFlow--;
1577
+ },
1578
+ ForStatement(node, state, c) {
1579
+ state.inControlFlow++;
1580
+ // First recursively process children
1581
+ if (node.init) c(node.init, state);
1582
+ if (node.test) c(node.test, state);
1583
+ if (node.update) c(node.update, state);
1584
+ if (node.body) c(node.body, state);
1585
+ // Then apply the transformation to this node
1586
+ ASTCallbacks.ForStatement(node, state, []);
1587
+ state.inControlFlow--;
1588
+ },
1589
+ ReturnStatement(node, state, c) {
1590
+ if (!state.inControlFlow) return;
1591
+ // Convert return statement to strandsEarlyReturn call
1592
+ node.type = 'ExpressionStatement';
1593
+ node.expression = {
1594
+ type: 'CallExpression',
1595
+ callee: {
1596
+ type: 'Identifier',
1597
+ name: '__p5.strandsEarlyReturn'
1598
+ },
1599
+ arguments: node.argument ? [node.argument] : []
1600
+ };
1601
+ delete node.argument;
1144
1602
  }
1145
1603
  };
1146
- function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
1147
- // Reset counters at the start of each transpilation
1148
- blockVarCounter = 0;
1149
- loopVarCounter = 0;
1150
-
1151
- const ast = parse(sourceString, {
1152
- ecmaVersion: 2021,
1153
- locations: srcLocations
1154
- });
1155
- // First pass: transform everything except if/for statements using normal ancestor traversal
1156
- const nonControlFlowCallbacks = { ...ASTCallbacks };
1157
- delete nonControlFlowCallbacks.IfStatement;
1158
- delete nonControlFlowCallbacks.ForStatement;
1159
- ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {} });
1160
- // Second pass: transform if/for statements in post-order using recursive traversal
1161
- const postOrderControlFlowTransform = {
1162
- IfStatement(node, state, c) {
1163
- state.inControlFlow++;
1164
- // First recursively process children
1165
- if (node.test) c(node.test, state);
1166
- if (node.consequent) c(node.consequent, state);
1167
- if (node.alternate) c(node.alternate, state);
1168
- // Then apply the transformation to this node
1169
- ASTCallbacks.IfStatement(node, state, []);
1170
- state.inControlFlow--;
1171
- },
1172
- ForStatement(node, state, c) {
1173
- state.inControlFlow++;
1174
- // First recursively process children
1175
- if (node.init) c(node.init, state);
1176
- if (node.test) c(node.test, state);
1177
- if (node.update) c(node.update, state);
1178
- if (node.body) c(node.body, state);
1179
- // Then apply the transformation to this node
1180
- ASTCallbacks.ForStatement(node, state, []);
1181
- state.inControlFlow--;
1182
- },
1183
- ReturnStatement(node, state, c) {
1184
- if (!state.inControlFlow) return;
1185
- // Convert return statement to strandsEarlyReturn call
1186
- node.type = 'ExpressionStatement';
1187
- node.expression = {
1188
- type: 'CallExpression',
1189
- callee: {
1190
- type: 'Identifier',
1191
- name: '__p5.strandsEarlyReturn'
1192
- },
1193
- arguments: node.argument ? [node.argument] : []
1194
- };
1195
- delete node.argument;
1196
- }
1197
- };
1198
- recursive(ast, { varyings: {}, inControlFlow: 0 }, postOrderControlFlowTransform);
1199
- const transpiledSource = escodegen.generate(ast);
1200
- const scopeKeys = Object.keys(scope);
1201
- const match = /\(?\s*(?:function)?\s*\w*\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/
1202
- .exec(transpiledSource);
1203
- if (!match) {
1204
- console.log(transpiledSource);
1205
- throw new Error('Could not parse p5.strands function!');
1206
- }
1207
- const params = match[1].split(/,\s*/).filter(param => !!param.trim());
1208
- let paramVals, paramNames;
1209
- if (params.length > 0) {
1210
- paramNames = params;
1211
- paramVals = [scope];
1212
- } else {
1213
- paramNames = scopeKeys;
1214
- paramVals = scopeKeys.map(key => scope[key]);
1215
- }
1216
- const body = match[2];
1217
- try {
1218
- const internalStrandsCallback = new Function(
1219
- // Create a parameter called __p5, not just p5, because users of instance mode
1220
- // may pass in a variable called p5 as a scope variable. If we rely on a variable called
1221
- // p5, then the scope variable called p5 might accidentally override internal function
1222
- // calls to p5 static methods.
1223
- '__p5',
1224
- ...paramNames,
1225
- body,
1226
- );
1227
- return () => internalStrandsCallback(p5, ...paramVals);
1228
- } catch (e) {
1229
- console.error(e);
1230
- console.log(paramNames);
1231
- console.log(body);
1232
- throw new Error('Error transpiling p5.strands callback!');
1233
- }
1604
+ recursive(ast, { varyings: {}, inControlFlow: 0 }, postOrderControlFlowTransform);
1605
+ const transpiledSource = escodegen.generate(ast);
1606
+ const scopeKeys = Object.keys(scope);
1607
+ const match = /\(?\s*(?:function)?\s*\w*\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/
1608
+ .exec(transpiledSource);
1609
+ if (!match) {
1610
+ console.log(transpiledSource);
1611
+ throw new Error('Could not parse p5.strands function!');
1612
+ }
1613
+ const params = match[1].split(/,\s*/).filter(param => !!param.trim());
1614
+ let paramVals, paramNames;
1615
+ if (params.length > 0) {
1616
+ paramNames = params;
1617
+ paramVals = [scope];
1618
+ } else {
1619
+ paramNames = scopeKeys;
1620
+ paramVals = scopeKeys.map(key => scope[key]);
1621
+ }
1622
+ const body = match[2];
1623
+ try {
1624
+ const internalStrandsCallback = new Function(
1625
+ // Create a parameter called __p5, not just p5, because users of instance mode
1626
+ // may pass in a variable called p5 as a scope variable. If we rely on a variable called
1627
+ // p5, then the scope variable called p5 might accidentally override internal function
1628
+ // calls to p5 static methods.
1629
+ '__p5',
1630
+ ...paramNames,
1631
+ body,
1632
+ );
1633
+ return () => internalStrandsCallback(p5, ...paramVals);
1634
+ } catch (e) {
1635
+ console.error(e);
1636
+ console.log(paramNames);
1637
+ console.log(body);
1638
+ throw new Error('Error transpiling p5.strands callback!');
1234
1639
  }
1640
+ }
1235
1641
 
1236
1642
  export { transpileStrandsToJS };