block based eval
This commit is contained in:
parent
5a51b4ec71
commit
fd1e3d248a
13 changed files with 1106 additions and 200 deletions
|
|
@ -19,7 +19,14 @@ export function registerLanguage(type, config) {
|
|||
}
|
||||
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||
const {
|
||||
wrapAsync = false,
|
||||
addReturn = true,
|
||||
emitMiniLocations = true,
|
||||
emitWidgets = true,
|
||||
blockBased = false,
|
||||
range = []
|
||||
} = options;
|
||||
|
||||
let ast = parse(input, {
|
||||
ecmaVersion: 2022,
|
||||
|
|
@ -28,6 +35,13 @@ export function transpiler(input, options = {}) {
|
|||
});
|
||||
|
||||
let miniLocations = [];
|
||||
|
||||
// Position offset for block-based evaluation
|
||||
let nodeOffset = range && range.length > 0 ? range[0] : 0;
|
||||
|
||||
// Track declarations to add to strudelScope for block-based eval
|
||||
let scopeDeclarations = [];
|
||||
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const minilang = languages.get('minilang');
|
||||
if (minilang) {
|
||||
|
|
@ -40,9 +54,27 @@ export function transpiler(input, options = {}) {
|
|||
}
|
||||
};
|
||||
let widgets = [];
|
||||
let sliders = [];
|
||||
|
||||
walk(ast, {
|
||||
enter(node, parent /* , prop, index */) {
|
||||
// Apply position offset for block-based evaluation
|
||||
if (blockBased && node.start !== undefined) {
|
||||
node.start = node.start + nodeOffset;
|
||||
node.end = node.end + nodeOffset;
|
||||
}
|
||||
// Collect variable and function declarations for strudelScope (block-based eval)
|
||||
if (blockBased && parent?.type === 'Program') {
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
for (const declarator of node.declarations) {
|
||||
if (declarator.id?.name) {
|
||||
scopeDeclarations.push(declarator.id.name);
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'FunctionDeclaration' && node.id?.name) {
|
||||
scopeDeclarations.push(node.id.name);
|
||||
}
|
||||
}
|
||||
if (isLanguageLiteral(node)) {
|
||||
const { name } = node.tag;
|
||||
const language = languages.get(name);
|
||||
|
|
@ -79,22 +111,29 @@ export function transpiler(input, options = {}) {
|
|||
return this.replace(miniWithLocation(value, node));
|
||||
}
|
||||
if (isSliderFunction(node)) {
|
||||
emitWidgets &&
|
||||
widgets.push({
|
||||
from: node.arguments[0].start,
|
||||
to: node.arguments[0].end,
|
||||
value: node.arguments[0].raw, // don't use value!
|
||||
min: node.arguments[1]?.value ?? 0,
|
||||
max: node.arguments[2]?.value ?? 1,
|
||||
step: node.arguments[3]?.value,
|
||||
type: 'slider',
|
||||
});
|
||||
return this.replace(sliderWithLocation(node));
|
||||
const from = node.arguments[0].start + nodeOffset;
|
||||
const to = node.arguments[0].end + nodeOffset;
|
||||
const id = `${from}:${to}`; // Range-based ID for stability
|
||||
|
||||
const sliderConfig = {
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
value: node.arguments[0].raw, // don't use value!
|
||||
min: node.arguments[1]?.value ?? 0,
|
||||
max: node.arguments[2]?.value ?? 1,
|
||||
step: node.arguments[3]?.value,
|
||||
type: 'slider',
|
||||
};
|
||||
emitWidgets && widgets.push(sliderConfig);
|
||||
sliders.push(sliderConfig);
|
||||
return this.replace(sliderWithLocation(node, nodeOffset));
|
||||
}
|
||||
if (isWidgetMethod(node)) {
|
||||
const type = node.callee.property.name;
|
||||
const index = widgets.filter((w) => w.type === type).length;
|
||||
const widgetConfig = {
|
||||
from: node.start,
|
||||
to: node.end,
|
||||
index,
|
||||
type,
|
||||
|
|
@ -115,17 +154,33 @@ export function transpiler(input, options = {}) {
|
|||
|
||||
let { body } = ast;
|
||||
|
||||
const silenceExpression = {
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'Identifier',
|
||||
name: 'silence',
|
||||
},
|
||||
};
|
||||
|
||||
if (!body.length) {
|
||||
console.warn('empty body -> fallback to silence');
|
||||
body.push({
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'Identifier',
|
||||
name: 'silence',
|
||||
},
|
||||
});
|
||||
body.push(silenceExpression);
|
||||
} else if (!body?.[body.length - 1]?.expression) {
|
||||
throw new Error('unexpected ast format without body expression');
|
||||
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
||||
if (blockBased) {
|
||||
// For block-based eval, add silence as the return value when block ends with declaration
|
||||
body.push(silenceExpression);
|
||||
} else {
|
||||
throw new Error('unexpected ast format without body expression');
|
||||
}
|
||||
}
|
||||
|
||||
// For block-based eval, add scope assignments before the return statement
|
||||
// This allows variables/functions defined in one block to be used in other blocks
|
||||
if (blockBased && scopeDeclarations.length > 0) {
|
||||
const scopeAssignments = scopeDeclarations.flatMap((name) => createScopeAssignment(name));
|
||||
// Insert scope assignments before the last statement (which will become the return)
|
||||
body.splice(body.length - 1, 0, ...scopeAssignments);
|
||||
}
|
||||
|
||||
// add return to last statement
|
||||
|
|
@ -143,7 +198,7 @@ export function transpiler(input, options = {}) {
|
|||
if (!emitMiniLocations) {
|
||||
return { output };
|
||||
}
|
||||
return { output, miniLocations, widgets };
|
||||
return { output, miniLocations, widgets, sliders };
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
|
|
@ -190,8 +245,14 @@ function isWidgetMethod(node) {
|
|||
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
|
||||
}
|
||||
|
||||
function sliderWithLocation(node) {
|
||||
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
|
||||
function sliderWithLocation(node, nodeOffset = 0) {
|
||||
// Apply nodeOffset for block-based evaluation to generate correct range
|
||||
const from = node.arguments[0].start + nodeOffset;
|
||||
const to = node.arguments[0].end + nodeOffset;
|
||||
|
||||
// Use range-based ID for stability during block evaluation
|
||||
const id = `${from}:${to}`;
|
||||
|
||||
// add loc as identifier to first argument
|
||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||
node.arguments.unshift({
|
||||
|
|
@ -206,14 +267,26 @@ function sliderWithLocation(node) {
|
|||
export function getWidgetID(widgetConfig) {
|
||||
// the widget id is used as id for the dom element + as key for eventual resources
|
||||
// for example, for each scope widget, a new analyser + buffer (large) is created
|
||||
// that means, if we use the index index of line position as id, less garbage is generated
|
||||
// return `widget_${widgetConfig.to}`; // more gargabe
|
||||
//return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage
|
||||
return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage
|
||||
// Update: use range-based ID generation for better stability during block evaluation
|
||||
// When we have both from and to, use them together for stability
|
||||
// Otherwise fall back to position-based ID for backward compatibility
|
||||
let uniqueIdentifier;
|
||||
if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) {
|
||||
// Use range for more stable identification
|
||||
uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`;
|
||||
} else {
|
||||
// Fallback to single position (for backward compatibility)
|
||||
uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0;
|
||||
}
|
||||
const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`;
|
||||
return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`;
|
||||
}
|
||||
|
||||
function widgetWithLocation(node, widgetConfig) {
|
||||
const id = getWidgetID(widgetConfig);
|
||||
// Store the unique ID back into the config so it's available for widget management
|
||||
// This is critical for block-based evaluation to match existing widgets with new ones
|
||||
widgetConfig.id = id;
|
||||
// add loc as identifier to first argument
|
||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||
node.arguments.unshift({
|
||||
|
|
@ -327,3 +400,85 @@ function languageWithLocation(name, value, offset) {
|
|||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 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)
|
||||
// We also track the key in userDefinedKeys so clearScope() can remove it later
|
||||
function createScopeAssignment(name) {
|
||||
return [
|
||||
// userDefinedKeys.add('name');
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: 'userDefinedKeys',
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: 'add',
|
||||
},
|
||||
computed: false,
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'Literal',
|
||||
value: name,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// strudelScope.name = name;
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'AssignmentExpression',
|
||||
operator: '=',
|
||||
left: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: 'strudelScope',
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
computed: false,
|
||||
},
|
||||
right: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
// globalThis.name = name;
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'AssignmentExpression',
|
||||
operator: '=',
|
||||
left: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: 'globalThis',
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
computed: false,
|
||||
},
|
||||
right: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue