transpiler.mjs - <short description TODO> Copyright (C) 2022 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
10let languages = new Map();
config = { getLocations: (code: string, offset?: number) => number[][] }
see mondough.mjs for example use
the language will kick in when the code contains a template literal of type
example: mondo... will use language of type "mondo"
TODO: refactor tidal.mjs to use this
23const plugins = []; 24 25export function registerTranspilerPlugin(plugin) { 26 plugins.push(plugin); 27} 28export function getPlugins() { 29 return plugins.flat(Infinity); 30} 31 32export function transpiler(input, options = {}) { 33 options = { 34 wrapAsync: false, 35 addReturn: true, 36 emitMiniLocations: true, 37 emitWidgets: true, 38 blockBased: false, 39 range: [], 40 ...options, 41 }; 42 43 const { wrapAsync, addReturn, emitMiniLocations, emitWidgets, blockBased, range } = options; 44 45 const comments = []; 46 let ast = parse(input, { 47 ecmaVersion: 2022, 48 allowAwaitOutsideFunction: true, 49 locations: true, 50 onComment: comments, 51 }); 52 53 let miniDisableRanges = findMiniDisableRanges(comments, input.length);
Position offset for block-based evaluation
56 let nodeOffset = range && range.length > 0 ? range[0] : 0;
Track declarations to add to strudelScope for block-based eval
59 let scopeDeclarations = [];
61 let labels = []; 62 63 const context = { options, input, nodeOffset, miniDisableRanges, labels }; 64 const plugins = getPlugins().map((plugin) => plugin.walk?.(context)); 65 66 walk(ast, { 67 enter(node, parent, prop, index) { 68 // Apply position offset for block-based evaluation 69 if (blockBased && node.start !== undefined) { 70 node.start = node.start + nodeOffset; 71 node.end = node.end + nodeOffset; 72 } 73 // Collect variable and function declarations for strudelScope (block-based eval) 74 if (blockBased && parent?.type === 'Program') { 75 if (node.type === 'VariableDeclaration') { 76 for (const declarator of node.declarations) { 77 if (declarator.id?.name) { 78 scopeDeclarations.push(declarator.id.name); 79 } 80 } 81 } else if (node.type === 'FunctionDeclaration' && node.id?.name) { 82 scopeDeclarations.push(node.id.name); 83 } 84 } 85 86 for (const plugin of plugins) { 87 if (!plugin?.enter?.call(this, node, parent, prop, index)) continue; 88 return; 89 } 90 91 if (isLabelStatement(node)) { 92 // Collect label info for block-based evaluation 93 // Store positions WITHOUT offset so repl can slice the transpiler output correctly 94 if (blockBased) { 95 labels.push({ 96 name: node.label.name, 97 index: node.start - nodeOffset, 98 end: node.label.end - nodeOffset, 99 fullMatch: input.slice(node.start - nodeOffset, node.label.end - nodeOffset), 100 activeVisualizer: findVisualizerInSubtree(node.body), 101 }); 102 } 103 return this.replace(labelToP(node)); 104 } 105 // Detect all() calls as special labels for block management 106 // Store positions WITHOUT offset so repl can slice the transpiler output correctly 107 if (blockBased && isAllCall(node)) { 108 labels.push({ 109 name: 'all', 110 index: node.start - nodeOffset, 111 end: node.end - nodeOffset, 112 fullMatch: input.slice(node.start - nodeOffset, node.end - nodeOffset), 113 activeVisualizer: node.arguments[0] ? findVisualizerInSubtree(node.arguments[0]) : null, 114 }); 115 } 116 }, 117 118 leave(node, parent, prop, index) { 119 for (const plugin of plugins) { 120 if (!plugin?.leave?.call(this, node, parent, prop, index)) continue; 121 return; 122 } 123 }, 124 }); 125 126 let { body } = ast; 127 128 const silenceExpression = { 129 type: 'ExpressionStatement', 130 expression: { 131 type: 'Identifier', 132 name: 'silence', 133 }, 134 }; 135 136 if (!body.length) { 137 console.warn('empty body -> fallback to silence'); 138 body.push(silenceExpression); 139 } else if (!body?.[body.length - 1]?.expression) { 140 // Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration) 141 body.push(silenceExpression); 142 }
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
146 if (blockBased && scopeDeclarations.length > 0) { 147 const scopeAssignments = scopeDeclarations.flatMap((name) => createScopeAssignment(name)); 148 // Insert scope assignments before the last statement (which will become the return) 149 body.splice(body.length - 1, 0, ...scopeAssignments); 150 }
add return to last statement
153 if (addReturn) { 154 const { expression } = body[body.length - 1]; 155 body[body.length - 1] = { 156 type: 'ReturnStatement', 157 argument: expression, 158 }; 159 } 160 let output = escodegen.generate(ast); 161 if (wrapAsync) { 162 output = `(async ()=>{${output}})()`; 163 } 164 if (!emitMiniLocations) { 165 return { output }; 166 }
168 let pluginContext; 169 ({ options, input, miniDisableRanges, nodeOffset, ...pluginContext } = context); 170 171 return { output, ...pluginContext }; 172} 173 174function isAllCall(node) { 175 return node.type === 'CallExpression' && node.callee.name === 'all'; 176} 177 178function isLabelStatement(node) { 179 return node.type === 'LabeledStatement'; 180}
converts label expressions to p calls: "x: y" to "y.p('x')" see https://codeberg.org/uzu/strudel/issues/990
184function labelToP(node) { 185 return { 186 type: 'ExpressionStatement', 187 expression: { 188 type: 'CallExpression', 189 callee: { 190 type: 'MemberExpression', 191 object: node.body.expression, 192 property: { 193 type: 'Identifier', 194 name: 'p', 195 }, 196 }, 197 arguments: [ 198 { 199 type: 'Literal', 200 value: node.label.name, 201 raw: `'${node.label.name}'`, 202 }, 203 ], 204 }, 205 }; 206}
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)
211const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall'];
213function isVisualizerCall(node) { 214 if ( 215 node.type === 'CallExpression' && 216 node.callee.type === 'MemberExpression' && 217 nonInlineWidgets.includes(node.callee.property?.name) 218 ) { 219 return node.callee.property.name; 220 } 221 return null; 222} 223 224function findVisualizerInSubtree(node) { 225 if (!node || typeof node !== 'object') return null;
Check if this node is a visualizer call
Recursively search children
232 for (const key of Object.keys(node)) { 233 if (key === 'parent') continue; // Skip parent references to avoid cycles 234 const child = node[key]; 235 if (Array.isArray(child)) { 236 for (const item of child) { 237 const found = findVisualizerInSubtree(item); 238 if (found) return found; 239 } 240 } else if (child && typeof child === 'object' && child.type) { 241 const found = findVisualizerInSubtree(child); 242 if (found) return found; 243 } 244 } 245 return null; 246}
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
252function createScopeAssignment(name) { 253 return [ 254 // userDefinedKeys.add('name'); 255 { 256 type: 'ExpressionStatement', 257 expression: { 258 type: 'CallExpression', 259 callee: { 260 type: 'MemberExpression', 261 object: { 262 type: 'Identifier', 263 name: 'userDefinedKeys', 264 }, 265 property: { 266 type: 'Identifier', 267 name: 'add', 268 }, 269 computed: false, 270 }, 271 arguments: [ 272 { 273 type: 'Literal', 274 value: name, 275 }, 276 ], 277 }, 278 }, 279 // strudelScope.name = name; 280 { 281 type: 'ExpressionStatement', 282 expression: { 283 type: 'AssignmentExpression', 284 operator: '=', 285 left: { 286 type: 'MemberExpression', 287 object: { 288 type: 'Identifier', 289 name: 'strudelScope', 290 }, 291 property: { 292 type: 'Identifier', 293 name: name, 294 }, 295 computed: false, 296 }, 297 right: { 298 type: 'Identifier', 299 name: name, 300 }, 301 }, 302 }, 303 // globalThis.name = name; 304 { 305 type: 'ExpressionStatement', 306 expression: { 307 type: 'AssignmentExpression', 308 operator: '=', 309 left: { 310 type: 'MemberExpression', 311 object: { 312 type: 'Identifier', 313 name: 'globalThis', 314 }, 315 property: { 316 type: 'Identifier', 317 name: name, 318 }, 319 computed: false, 320 }, 321 right: { 322 type: 'Identifier', 323 name: name, 324 }, 325 }, 326 }, 327 ]; 328}
330function findMiniDisableRanges(comments, codeEnd) { 331 const ranges = []; 332 const stack = []; // used to track on/off pairs 333 for (const comment of comments) { 334 const value = comment.value.trim(); 335 if (value.startsWith('mini-off')) { 336 stack.push(comment.start); 337 } else if (value.startsWith('mini-on')) { 338 const start = stack.pop(); 339 ranges.push([start, comment.end]); 340 } 341 } 342 while (stack.length) { 343 // If no closing mini-on is found, just turn it off until `codeEnd` 344 const start = stack.pop(); 345 ranges.push([start, codeEnd]); 346 } 347 return ranges; 348}