From 1a7f464998e3dd797ff821c89a5754133f4c93b1 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Tue, 30 Dec 2025 18:34:26 -0600 Subject: [PATCH] requested revisions 1 (and fix failing test) --- packages/codemirror/block_utilities.mjs | 2 +- packages/core/repl.mjs | 103 +++++------------------- packages/transpiler/transpiler.mjs | 71 +++++++++++++++- test/examples.test.mjs | 1 + 4 files changed, 90 insertions(+), 87 deletions(-) diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs index 4b53755d..0d13259a 100644 --- a/packages/codemirror/block_utilities.mjs +++ b/packages/codemirror/block_utilities.mjs @@ -44,7 +44,7 @@ export const evalBlock = (strudelMirror) => { if (block) { // Flash the block being evaluated strudelMirror.flash(200, { from: a, to: b }); - strudelMirror.repl.evaluate(block, true, true, true, { range }); + strudelMirror.repl.evaluate(block, true, true, { range }); } } return true; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 4c730515..d1bb1b1a 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -126,76 +126,11 @@ export function repl({ ); } - // // Helper function to filter meta (widgets, sliders, miniLocations) by block range - // function splitCodeByRange(meta, blockStart, blockEnd) { - // } - - // Helper function to extract labels from code with their positions - function extractLabelsFromCode(code) { - const labels = []; - // Regex to find label patterns like "d1:" or "myLabel:" - const labelRegex = /^(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:)/gm; - let match; - while ((match = labelRegex.exec(code)) !== null) { - labels.push({ - name: match[2], - index: match.index, - end: match.index + match[1].length, - fullMatch: match[1], - }); - } - - // Also check for 'all()' and treat it as a special label - // just for management purposes - const allRegex = /all\s*\(\s*([^)]+)\s*\)/; - const allMatch = allRegex.exec(code); - if (allMatch) { - // Check if the argument contains a widget call - const argCode = allMatch[1]; - const activeVisualizer = detectActiveVisualizer(argCode); - - labels.push({ - name: 'all', - index: allMatch.index, - end: allMatch.index + allMatch[0].length, - fullMatch: allMatch[0], - activeVisualizer: activeVisualizer, - }); - } - - return labels; - } - - // Helper function to detect non-inline widget calls in code - function detectActiveVisualizer(code) { - // List of non-inline widgets that need cleanup - // These are Pattern.prototype methods that create persistent visualizations - // I don't like this approach, feels hacky, but since these methods don't create ids that - // can be read through the transpiler, I'm not sure how best to detect them. - // Would probably be better to register these methods through some function that would - // tag them better - const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall']; - - for (const widget of nonInlineWidgets) { - const widgetRegex = new RegExp(`\\.${widget}\\s*\\(`); - if (widgetRegex.test(code)) { - return widget; - } - } - return null; - } - // Helper function to handle single label code block storage function handleSingleLabelBlock(label, code, options, meta) { // Detect if this block contains a non-inline widget - // For 'all' label, widget info is already on the label object from extractLabelsFromCode - - // As mentioned in detectActiveVisualizer, this is a bad approach to - // managing non-inline widgets (or widgets that don't have ids) - // and a proper solution would give them widgets in the transpiler, or at least - // track where they are so we don't have to futz around with regexes - const activeVisualizer = - label.activeVisualizer !== undefined ? label.activeVisualizer : detectActiveVisualizer(code); + // The activeVisualizer is now provided by the transpiler for all labels + const activeVisualizer = label.activeVisualizer || null; if (activeVisualizer !== null) { lastActiveVisualizerLabel = label.name; @@ -371,7 +306,7 @@ export function repl({ }); }; - const evaluate = async (code, autostart = true, shouldHush = true, blockBased = false, options = {}) => { + const evaluate = async (code, autostart = true, blockBased = false, options = {}) => { if (!code) { throw new Error('no code to evaluate'); } @@ -390,7 +325,7 @@ export function repl({ if (!blockBased) { codeBlocks = {}; - shouldHush && hush(); + hush(); } if (mondo) { @@ -403,7 +338,7 @@ export function repl({ let widgetRemoved = false; if (blockBased) { - const labels = extractLabelsFromCode(code); + const labels = meta.labels || []; // Store code blocks in dictionary using labels as keys if (labels.length > 0) { @@ -526,18 +461,18 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); + } + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); - } - }; + }; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 24dd3f1f..e7d4acfa 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -55,6 +55,7 @@ export function transpiler(input, options = {}) { }; let widgets = []; let sliders = []; + let labels = []; walk(ast, { enter(node, parent /* , prop, index */) { @@ -146,10 +147,32 @@ export function transpiler(input, options = {}) { return this.replace(withAwait(node)); } if (isLabelStatement(node)) { + // Collect label info for block-based evaluation + // Store positions WITHOUT offset so repl can slice the transpiler output correctly + if (blockBased) { + labels.push({ + name: node.label.name, + index: node.start - nodeOffset, + end: node.label.end - nodeOffset, + fullMatch: input.slice(node.start - nodeOffset, node.label.end - nodeOffset), + activeVisualizer: findVisualizerInSubtree(node.body), + }); + } return this.replace(labelToP(node)); } + // Detect all() calls as special labels for block management + // Store positions WITHOUT offset so repl can slice the transpiler output correctly + if (blockBased && isAllCall(node)) { + labels.push({ + name: 'all', + index: node.start - nodeOffset, + end: node.end - nodeOffset, + fullMatch: input.slice(node.start - nodeOffset, node.end - nodeOffset), + activeVisualizer: node.arguments[0] ? findVisualizerInSubtree(node.arguments[0]) : null, + }); + } }, - leave(node, parent, prop, index) {}, + leave(node, parent, prop, index) { }, }); let { body } = ast; @@ -198,7 +221,7 @@ export function transpiler(input, options = {}) { if (!emitMiniLocations) { return { output }; } - return { output, miniLocations, widgets, sliders }; + return { output, miniLocations, widgets, sliders, labels }; } function isStringWithDoubleQuotes(node, locations, code) { @@ -301,6 +324,10 @@ function isBareSamplesCall(node, parent) { return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; } +function isAllCall(node) { + return node.type === 'CallExpression' && node.callee.name === 'all'; +} + function withAwait(node) { return { type: 'AwaitExpression', @@ -401,6 +428,46 @@ function languageWithLocation(name, value, offset) { }; } +// List of non-inline widgets that need cleanup +// These are Pattern.prototype methods that create persistent visualizations +// (should be repalced by a function call producing an actual list of registered widgets) +const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall']; + +function isVisualizerCall(node) { + if ( + node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + nonInlineWidgets.includes(node.callee.property?.name) + ) { + return node.callee.property.name; + } + return null; +} + +function findVisualizerInSubtree(node) { + if (!node || typeof node !== 'object') return null; + + // Check if this node is a visualizer call + const viz = isVisualizerCall(node); + if (viz) return viz; + + // Recursively search children + for (const key of Object.keys(node)) { + if (key === 'parent') continue; // Skip parent references to avoid cycles + const child = node[key]; + if (Array.isArray(child)) { + for (const item of child) { + const found = findVisualizerInSubtree(item); + if (found) return found; + } + } else if (child && typeof child === 'object' && child.type) { + const found = findVisualizerInSubtree(child); + if (found) return found; + } + } + return null; +} + // Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name; // Used in block-based evaluation to persist variables/functions across blocks // We add to both strudelScope (for internal lookups) and globalThis (for direct access) diff --git a/test/examples.test.mjs b/test/examples.test.mjs index 896a02f8..18f12bb4 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -20,6 +20,7 @@ const skippedExamples = [ 'accelerationX', 'defaultmidimap', 'midimaps', + 'clearScope' ]; describe('runs examples', () => {