move evaluate/evaluateBlock to different functions

This commit is contained in:
Dsm0 2025-12-30 20:14:32 -06:00
parent 1a7f464998
commit 160ef84b47
4 changed files with 152 additions and 109 deletions

View file

@ -44,7 +44,7 @@ export const evalBlock = (strudelMirror) => {
if (block) { if (block) {
// Flash the block being evaluated // Flash the block being evaluated
strudelMirror.flash(200, { from: a, to: b }); strudelMirror.flash(200, { from: a, to: b });
strudelMirror.repl.evaluate(block, true, true, { range }); strudelMirror.repl.evaluateBlock(block, true, { range });
} }
} }
return true; return true;

View file

@ -187,6 +187,48 @@ export function repl({
}; };
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
// Helper function to apply pattern transformations (solo, each, all)
// this should be abstracted more
function applyPatternTransforms(pattern) {
const allPatterns = Object.values(pPatterns);
if (allPatterns.length) {
let patterns = [];
let soloActive = false;
for (const [key, value] of Object.entries(pPatterns)) {
// handle soloed patterns ex: S$: s("bd!4")
const isSolod = key.length > 1 && key.startsWith('S');
if (isSolod && soloActive === false) {
// first time we see a soloed pattern, clear existing patterns
patterns = [];
soloActive = true;
}
if (!soloActive || (soloActive && isSolod)) {
const valWithState = value.withState((state) => state.setControls({ id: key }));
patterns.push(valWithState);
}
}
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
}
pattern = stack(...patterns);
} else if (eachTransform) {
pattern = eachTransform(pattern);
}
if (allTransforms.length) {
for (const transform of allTransforms) {
pattern = transform(pattern);
}
}
if (!isPattern(pattern)) {
pattern = silence;
}
return pattern;
}
const stop = () => { const stop = () => {
codeBlocks = {}; codeBlocks = {};
blockPatterns.clear(); blockPatterns.clear();
@ -306,7 +348,7 @@ export function repl({
}); });
}; };
const evaluate = async (code, autostart = true, blockBased = false, options = {}) => { const evaluate = async (code, autostart = true) => {
if (!code) { if (!code) {
throw new Error('no code to evaluate'); throw new Error('no code to evaluate');
} }
@ -314,20 +356,60 @@ export function repl({
updateState({ code, pending: true }); updateState({ code, pending: true });
await injectPatternMethods(); await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
await beforeEval?.({ code, blockBased }); await beforeEval?.({ code, blockBased: false });
allTransforms = []; // reset all transforms
codeBlocks = {};
hush();
if (mondo) {
code = `mondolang\`${code}\``;
}
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
pattern = applyPatternTransforms(pattern);
logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart);
updateState({
miniLocations: meta?.miniLocations || [],
widgets: meta?.widgets || [],
sliders: meta?.sliders || [],
activeCode: code,
pattern,
evalError: undefined,
schedulerError: undefined,
pending: false,
});
afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false });
return pattern;
} catch (err) {
logger(`[eval] error: ${err.message}`, 'error');
console.error(err);
updateState({ evalError: err, pending: false });
onEvalError?.(err);
}
};
const evaluateBlock = async (code, autostart = true, options = {}) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
updateState({ code, pending: true });
await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor?
await beforeEval?.({ code, blockBased: true });
allTransforms = []; // reset all transforms allTransforms = []; // reset all transforms
const transpilerOptionsWithBlock = { const transpilerOptionsWithBlock = {
...transpilerOptions, ...transpilerOptions,
blockBased, blockBased: true,
range: options.range || [], range: options.range || [],
}; };
if (!blockBased) {
codeBlocks = {};
hush();
}
if (mondo) { if (mondo) {
code = `mondolang\`${code}\``; code = `mondolang\`${code}\``;
} }
@ -337,101 +419,61 @@ export function repl({
// Track activeVisualizer cleanup: check if any block's visualizer was removed // Track activeVisualizer cleanup: check if any block's visualizer was removed
let widgetRemoved = false; let widgetRemoved = false;
if (blockBased) { const labels = meta.labels || [];
const labels = meta.labels || [];
// Store code blocks in dictionary using labels as keys // Store code blocks in dictionary using labels as keys
if (labels.length > 0) { if (labels.length > 0) {
for (let i = 0; i < labels.length; i++) { for (let i = 0; i < labels.length; i++) {
// processLabeledBlock(labels, i, code, options, meta); // processing transpiler output instead of code is simply to avoid
// processing transpiler output instead of code is simply to avoid // extra regex in detecting whether or not an inline widget has been commented out
// extra regex in detecting whether or not an inline widget has been commented out processLabeledBlock(labels, i, meta.output, options, meta);
processLabeledBlock(labels, i, meta.output, options, meta);
}
} else if (pattern !== silence) {
// variable/function declarations that return silence are allowed,
// but anonymous pattern blocks pose an issue for block-based evaluation
// if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder
// it's very common for users to write code prefixed with '$'
// but to modify and override existing patterns, the patterns must be labeled,
// otherwise we'll have no idea of which pattern is being overridden
// (we probably need to update the docs on this)
// we could easily enable it, but it would confuse a lot of people
throw new Error(
'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
);
} }
} else if (pattern !== silence) {
// variable/function declarations that return silence are allowed,
// but anonymous pattern blocks pose an issue for block-based evaluation
// if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder
// For block-based evaluation, collect from all blocks // it's very common for users to write code prefixed with '$'
// The highlight/widget/slider systems will handle selective updates based on the range // but to modify and override existing patterns, the patterns must be labeled,
meta.miniLocations = collectFromBlocks('miniLocations'); // otherwise we'll have no idea of which pattern is being overridden
meta.widgets = collectFromBlocks('widgets');
meta.sliders = collectFromBlocks('sliders');
// Track activeVisualizer cleanup: check if any block's visualizer was removed // (we probably need to update the docs on this)
const blocksToUpdate = labels.map((label) => label.name);
for (const [key, block] of Object.entries(codeBlocks)) { // we could easily enable it, but it would confuse a lot of people
if (blocksToUpdate.includes(key)) { throw new Error(
// This block was just updated 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
if (block.activeVisualizer !== null) {
// Block now has a visualizer, update tracking
lastActiveVisualizerLabel = key;
} else if (lastActiveVisualizerLabel === key) {
// This block lost its visualizer, trigger cleanup
widgetRemoved = true;
lastActiveVisualizerLabel = null;
}
}
}
}
const allPatterns = Object.values(pPatterns);
if (blockBased) {
pPatterns = Object.fromEntries(
Object.entries(pPatterns).filter(([key]) => {
return Object.keys(codeBlocks).includes(key);
}),
); );
} }
if (allPatterns.length) { meta.miniLocations = collectFromBlocks('miniLocations');
let patterns = []; meta.widgets = collectFromBlocks('widgets');
let soloActive = false; meta.sliders = collectFromBlocks('sliders');
for (const [key, value] of Object.entries(pPatterns)) {
// handle soloed patterns ex: S$: s("bd!4") // Track activeVisualizer cleanup: check if any block's visualizer was removed
const isSolod = key.length > 1 && key.startsWith('S'); const blocksToUpdate = labels.map((label) => label.name);
if (isSolod && soloActive === false) {
// first time we see a soloed pattern, clear existing patterns // this is the hackiest bit
patterns = []; for (const [key, block] of Object.entries(codeBlocks)) {
soloActive = true; if (blocksToUpdate.includes(key)) {
// This block was just updated
if (block.activeVisualizer !== null) {
// Block now has a visualizer, update tracking
lastActiveVisualizerLabel = key;
} else if (lastActiveVisualizerLabel === key) {
// This block lost its visualizer, trigger cleanup
widgetRemoved = true;
lastActiveVisualizerLabel = null;
} }
if (!soloActive || (soloActive && isSolod)) {
const valWithState = value.withState((state) => state.setControls({ id: key }));
patterns.push(valWithState);
}
}
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
}
pattern = stack(...patterns);
} else if (eachTransform) {
pattern = eachTransform(pattern);
}
if (allTransforms.length) {
for (const transform of allTransforms) {
pattern = transform(pattern);
} }
} }
if (!isPattern(pattern)) { pPatterns = Object.fromEntries(
pattern = silence; Object.entries(pPatterns).filter(([key]) => {
} return Object.keys(codeBlocks).includes(key);
}),
);
pattern = applyPatternTransforms(pattern);
logger(`[eval] code updated`); logger(`[eval] code updated`);
pattern = await setPattern(pattern, autostart); pattern = await setPattern(pattern, autostart);
@ -455,24 +497,25 @@ export function repl({
onEvalError?.(err); onEvalError?.(err);
} }
}; };
const setCode = (code) => updateState({ code }); const setCode = (code) => updateState({ code });
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
} }
export const getTrigger = export const getTrigger =
({ getTime, defaultOutput }) => ({ getTime, defaultOutput }) =>
async (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) // ^ 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 // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
try { try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) { if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps, t); 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');
}
};

View file

@ -172,7 +172,7 @@ export function transpiler(input, options = {}) {
}); });
} }
}, },
leave(node, parent, prop, index) { }, leave(node, parent, prop, index) {},
}); });
let { body } = ast; let { body } = ast;

View file

@ -20,7 +20,7 @@ const skippedExamples = [
'accelerationX', 'accelerationX',
'defaultmidimap', 'defaultmidimap',
'midimaps', 'midimaps',
'clearScope' 'clearScope',
]; ];
describe('runs examples', () => { describe('runs examples', () => {