// interpreter.js import { tokenTypes } from './lexer.js'; import { RuntimeError, TypeError, ErrorHelpers } from './error.js'; import { createDefaultJSBridge } from './js-bridge.js'; function createInterpreter(ast, host = {}) { const scope = host.scope || new Map(); const types = new Map(); // Initialize global scope with io object const hostIo = (host && host.io) ? host.io : {}; const hostOut = typeof hostIo.out === 'function' ? hostIo.out : (...xs) => console.log(...xs); const hostIn = typeof hostIo.in === 'function' ? hostIo.in : () => ''; const hostDebug = typeof hostIo.debug === 'function' ? hostIo.debug : (...xs) => console.log('[DEBUG]', ...xs); const hostAddListener = typeof hostIo.addListener === 'function' ? hostIo.addListener : () => () => {}; const hostDeliver = typeof hostIo.deliver === 'function' ? hostIo.deliver : () => {}; // Initialize JavaScript bridge for interop const jsBridge = createDefaultJSBridge(host.jsBridgeConfig || {}); // Helper functions for Result type creation function createOkResult(value) { return { type: 'Result', variant: 'Ok', value: value }; } function createErrResult(message) { return { type: 'Result', variant: 'Err', value: String(message) }; } // Converters for IO boundaries function toPlain(value) { if (value && typeof value.value === 'number') return value.value; if (Array.isArray(value)) return value.map(toPlain); if (value && value.type === 'Object' && value.properties instanceof Map) { const obj = {}; for (const [k, v] of value.properties.entries()) obj[k] = toPlain(v); return obj; } return value; } function fromPlain(value) { if (Array.isArray(value)) return value.map(fromPlain); if (value && typeof value === 'object' && !(value.type === 'Object' && value.properties instanceof Map)) { const mapped = {}; for (const [k, v] of Object.entries(value)) mapped[k] = fromPlain(v); return createObjectFromPlain(mapped); } if (typeof value === 'number') return { value, isFloat: !Number.isInteger(value) }; return value; } scope.set('io', { type: 'Object', properties: new Map([ ['out', { type: 'NativeFunction', call: (args) => { // Convert our custom number format to regular numbers for display const displayArgs = args.map(arg => { if (arg && typeof arg.value === 'number') { return arg.value; } if (Array.isArray(arg)) { return arg.map(item => { if (item && typeof item.value === 'number') { return item.value; } return item; }); } return arg; }); hostOut(...displayArgs); }, }], ['print', { type: 'NativeFunction', call: (args) => { if (args.length === 0) { hostOut(''); return; } // Enhanced formatting for different data types const formatValue = (arg, options = {}) => { const { gridMode = false, cellAlive = '█', cellDead = '·' } = options; // Handle numbers if (arg && typeof arg.value === 'number') { return arg.value; } // Handle arrays (potential grids) if (Array.isArray(arg)) { // Check if this looks like a 2D grid (array of arrays with numbers) const isGrid = arg.length > 0 && Array.isArray(arg[0]) && arg.every(row => Array.isArray(row) && row.every(cell => typeof cell === 'number' || (cell && typeof cell.value === 'number'))); if (isGrid && gridMode) { // Format as a visual grid return arg.map(row => row.map(cell => { const value = (cell && typeof cell.value === 'number') ? cell.value : cell; return value === 1 ? cellAlive : cellDead; }).join('') ).join('\n'); } else { // Regular array formatting return arg.map(item => { if (item && typeof item.value === 'number') { return item.value; } return item; }); } } // Handle functions if (arg && (arg.type === 'NativeFunction' || arg.type === 'Function')) { return ''; } // Handle Result types if (arg && arg.type === 'Result') { return `${arg.variant}(${formatValue(arg.value, options)})`; } // Handle Objects if (arg && arg.type === 'Object' && arg.properties instanceof Map) { const obj = Object.fromEntries( Array.from(arg.properties.entries()).map(([k, v]) => [k, formatValue(v, options)]) ); return JSON.stringify(obj, null, 2); } return String(arg); }; // Process arguments if (args.length === 1) { // Single argument - try to detect if it's a grid const formatted = formatValue(args[0], { gridMode: true }); hostOut(formatted); } else if (args.length === 2 && typeof args[0] === 'string') { // Two arguments: format string and data const format = args[0]; const data = args[1]; if (format === 'grid') { const formatted = formatValue(data, { gridMode: true }); hostOut(formatted); } else if (format === 'grid-custom') { // Expected: io.print "grid-custom" { data: grid, alive: "X", dead: " " } if (data && data.type === 'Object' && data.properties instanceof Map) { const gridData = data.properties.get('data'); const alive = data.properties.get('alive') || '█'; const dead = data.properties.get('dead') || '·'; const formatted = formatValue(gridData, { gridMode: true, cellAlive: alive, cellDead: dead }); hostOut(formatted); } else { hostOut('Error: grid-custom format requires { data, alive, dead } object'); } } else { // Regular formatting with format string const formatted = formatValue(data); hostOut(`${format}: ${formatted}`); } } else { // Multiple arguments - format each normally const formatted = args.map(arg => formatValue(arg)); hostOut(...formatted); } }, }], ['in', { type: 'NativeFunction', call: (args) => { if (args.length !== 0) { throw new Error('io.in expects no arguments'); } const data = hostIn(); return typeof data === 'string' ? data : String(data ?? ''); }, }], ['emit', { type: 'NativeFunction', call: (args) => { if (args.length < 1 || args.length > 2) { throw new Error('io.emit expects 1 or 2 arguments: topic, [data]'); } const topic = String(args[0]); const data = args.length === 2 ? toPlain(args[1]) : undefined; hostDeliver({ topic, data }); }, }], ['listen', { type: 'NativeFunction', call: (args) => { if (args.length !== 2) { throw new Error('io.listen expects exactly 2 arguments: topic, handler'); } const topic = String(args[0]); const handler = args[1]; if (!handler || handler.type !== 'Function') { throw new Error('io.listen handler must be a function'); } // Wrap the language-level handler in a host-callable function const unsubscribe = hostAddListener(topic, (event) => { const ev = createObjectFromPlain({ topic: String(event && event.topic || topic), data: fromPlain(event && event.data) }); const callScope = new Map(handler.closure); const paramName = typeof handler.params[0] === 'string' ? handler.params[0] : (handler.params[0] && handler.params[0].name); if (paramName) callScope.set(paramName, ev); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } try { visit(handler.body); } catch (e) { // Surface handler errors via host deliver if available try { hostDeliver({ topic: 'cmd.render', data: { error: e && e.message ? e.message : String(e) } }); } catch {} throw e; } finally { scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } } }); // Return nothing (Unit) return undefined; }, }], // JavaScript Interop Functions ['callJS', { type: 'NativeFunction', signature: '(functionName: String, args: [Any]) -> Result', call: (args) => { if (args.length < 1 || args.length > 2) { throw new Error('io.callJS expects 1 or 2 arguments: functionName, [args]'); } const functionName = String(args[0]); let callArgs = []; if (args.length === 2) { if (Array.isArray(args[1])) { callArgs = args[1]; } else { callArgs = [args[1]]; } } // Convert Baba Yaga args to JS args const jsArgs = callArgs.map(arg => jsBridge.convertBabaValueToJS(arg)); const result = jsBridge.callFunction(functionName, jsArgs); if (result.type === 'success') { // Store the raw JavaScript value with a special marker const jsValue = { type: 'JSValue', value: result.value, // Also include a converted version for display converted: jsBridge.convertJSValueToBaba(result.value) }; return createOkResult(jsValue); } else { return createErrResult(result.error); } }, }], ['callJSAsync', { type: 'NativeFunction', signature: '(functionName: String, args: [Any]) -> Result', call: async (args) => { if (args.length < 1 || args.length > 2) { throw new Error('io.callJSAsync expects 1 or 2 arguments: functionName, [args]'); } const functionName = String(args[0]); const callArgs = args.length === 2 ? (Array.isArray(args[1]) ? args[1] : [args[1]]) : []; // Convert Baba Yaga args to JS args const jsArgs = callArgs.map(arg => jsBridge.convertBabaValueToJS(arg)); const result = await jsBridge.callFunctionAsync(functionName, jsArgs); if (result.type === 'success') { const babaValue = jsBridge.convertJSValueToBaba(result.value); return createOkResult(babaValue); } else { return createErrResult(result.error); } }, }], ['getProperty', { type: 'NativeFunction', signature: '(obj: Any, propName: String) -> Result', call: (args) => { if (args.length !== 2) { throw new Error('io.getProperty expects exactly 2 arguments: obj, propName'); } let obj; // Check if this is a JSValue from io.callJS if (args[0] && args[0].type === 'JSValue') { obj = args[0].value; // Use the raw JavaScript value } else { obj = jsBridge.convertBabaValueToJS(args[0]); } const propName = String(args[1]); const result = jsBridge.getProperty(obj, propName); if (result.type === 'success') { const babaValue = jsBridge.convertJSValueToBaba(result.value); return createOkResult(babaValue); } else { return createErrResult(result.error); } }, }], ['setProperty', { type: 'NativeFunction', signature: '(obj: Any, propName: String, value: Any) -> Result', call: (args) => { if (args.length !== 3) { throw new Error('io.setProperty expects exactly 3 arguments: obj, propName, value'); } let obj; // Check if this is a JSValue from io.callJS if (args[0] && args[0].type === 'JSValue') { obj = args[0].value; // Use the raw JavaScript value } else { obj = jsBridge.convertBabaValueToJS(args[0]); } const propName = String(args[1]); const value = jsBridge.convertBabaValueToJS(args[2]); const result = jsBridge.setProperty(obj, propName, value); if (result.type === 'success') { const babaValue = jsBridge.convertJSValueToBaba(result.value); return createOkResult(babaValue); } else { return createErrResult(result.error); } }, }], ['hasProperty', { type: 'NativeFunction', signature: '(obj: Any, propName: String) -> Bool', call: (args) => { if (args.length !== 2) { throw new Error('io.hasProperty expects exactly 2 arguments: obj, propName'); } let obj; // Check if this is a JSValue from io.callJS if (args[0] && args[0].type === 'JSValue') { obj = args[0].value; // Use the raw JavaScript value } else { obj = jsBridge.convertBabaValueToJS(args[0]); } const propName = String(args[1]); return jsBridge.hasProperty(obj, propName); }, }], ['jsArrayToList', { type: 'NativeFunction', signature: '(jsArray: Any) -> Result', call: (args) => { if (args.length !== 1) { throw new Error('io.jsArrayToList expects exactly 1 argument: jsArray'); } let jsArray; // Check if this is a JSValue from io.callJS if (args[0] && args[0].type === 'JSValue') { jsArray = args[0].value; // Use the raw JavaScript value } else { jsArray = jsBridge.convertBabaValueToJS(args[0]); } const result = jsBridge.jsArrayToList(jsArray); if (result.type === 'success') { const babaList = result.value.map(item => jsBridge.convertJSValueToBaba(item)); return createOkResult(babaList); } else { return createErrResult(result.error); } }, }], ['listToJSArray', { type: 'NativeFunction', signature: '(list: [Any]) -> Any', call: (args) => { if (args.length !== 1) { throw new Error('io.listToJSArray expects exactly 1 argument: list'); } const babaList = args[0]; const result = jsBridge.listToJSArray(babaList); if (result.type === 'success') { return result.value; } else { throw new Error(result.error); } }, }], ['objectToTable', { type: 'NativeFunction', signature: '(obj: Any) -> Result', call: (args) => { if (args.length !== 1) { throw new Error('io.objectToTable expects exactly 1 argument: obj'); } let jsObj; // Check if this is a JSValue from io.callJS if (args[0] && args[0].type === 'JSValue') { jsObj = args[0].value; // Use the raw JavaScript value } else { jsObj = jsBridge.convertBabaValueToJS(args[0]); } const result = jsBridge.objectToTable(jsObj); if (result.type === 'success') { return createOkResult(result.value); } else { return createErrResult(result.error); } }, }], ['tableToObject', { type: 'NativeFunction', signature: '(table: Table) -> Any', call: (args) => { if (args.length !== 1) { throw new Error('io.tableToObject expects exactly 1 argument: table'); } const babaTable = args[0]; const result = jsBridge.tableToObject(babaTable); if (result.type === 'success') { return result.value; } else { throw new Error(result.error); } }, }], ['getLastJSError', { type: 'NativeFunction', signature: '() -> Result', call: (args) => { if (args.length !== 0) { throw new Error('io.getLastJSError expects no arguments'); } const error = jsBridge.getLastError(); if (error) { return createOkResult(jsBridge.convertJSValueToBaba(error)); } else { return createErrResult('No JavaScript error'); } }, }], ['clearJSError', { type: 'NativeFunction', signature: '() -> Unit', call: (args) => { if (args.length !== 0) { throw new Error('io.clearJSError expects no arguments'); } jsBridge.clearLastError(); return undefined; }, }], ]), }); // Expose host-provided data binding (if present) as a global `data` // Temporarily disabled to debug member access issue // if (Object.prototype.hasOwnProperty.call(hostIo, 'data')) { // scope.set('data', fromPlain(hostIo.data)); // } // Helper to create language number literal from JS number function num(n) { return { value: n, isFloat: false }; } // Helper to create our object/table value with proxy access function createObjectFromPlain(plain) { const properties = new Map(); for (const [k, v] of Object.entries(plain)) { properties.set(k, v); } const base = { type: 'Object', properties }; return new Proxy(base, { get(target, prop, receiver) { if (prop in target) return Reflect.get(target, prop, receiver); if (typeof prop === 'string' && target.properties && target.properties.has(prop)) { return target.properties.get(prop); } return undefined; }, }); } // shape: returns a metadata table describing the argument scope.set('shape', { type: 'NativeFunction', call: (args) => { if (args.length !== 1) { throw new Error('shape expects exactly 1 argument'); } const x = args[0]; // Numbers in this language may be wrapped {value,isFloat} const isNumberWrapped = x && typeof x === 'object' && typeof x.value === 'number'; const isList = Array.isArray(x); const isString = typeof x === 'string'; const isTable = x && typeof x === 'object' && x.type === 'Object' && x.properties instanceof Map; if (isList) { const n = x.length; return createObjectFromPlain({ kind: 'List', rank: num(1), shape: [num(n)], size: num(n), isEmpty: n === 0, }); } if (isString) { const n = x.length; return createObjectFromPlain({ kind: 'String', rank: num(1), shape: [num(n)], size: num(n), isEmpty: n === 0, }); } if (isTable) { const keys = Array.from(x.properties.keys()); const k = keys.length; return createObjectFromPlain({ kind: 'Table', rank: num(1), shape: [num(k)], size: num(k), keys, isEmpty: k === 0, }); } if (isNumberWrapped || typeof x === 'number' || typeof x === 'boolean') { return createObjectFromPlain({ kind: 'Scalar', rank: num(0), shape: [], size: num(1), isEmpty: false, }); } // Fallback descriptor return createObjectFromPlain({ kind: 'Unknown', rank: num(0), shape: [], size: num(1), isEmpty: false, }); }, }); scope.set('map', { type: 'NativeFunction', call: (args) => { const func = args[0]; const list = args[1]; if (func.type !== 'Function') { throw new Error('Map expects a function as the first argument.'); } if (!Array.isArray(list)) { throw new Error('Map expects a list as the second argument.'); } return list.map(item => { const callScope = new Map(func.closure); callScope.set(func.params[0].name, item); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } const result = visit(func.body); scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } return result; }); }, }); scope.set('filter', { type: 'NativeFunction', call: (args) => { const func = args[0]; const list = args[1]; if (func.type !== 'Function') { throw new Error('Filter expects a function as the first argument.'); } if (!Array.isArray(list)) { throw new Error('Filter expects a list as the second argument.'); } return list.filter(item => { const callScope = new Map(func.closure); callScope.set(func.params[0].name, item); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } const result = visit(func.body); scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } return result; }); }, }); scope.set('length', { type: 'NativeFunction', call: (args) => { if (args.length !== 1) { throw new Error('length expects exactly 1 argument'); } const arg = args[0]; if (Array.isArray(arg)) { return { value: arg.length, isFloat: false }; } else if (typeof arg === 'string') { return { value: arg.length, isFloat: false }; } else { throw new Error('length expects a list or string as argument'); } }, }); scope.set('reduce', { type: 'NativeFunction', call: (args) => { const func = args[0]; let accumulator = args[1]; const list = args[2]; if (func.type !== 'Function') { throw new Error('Reduce expects a function as the first argument.'); } if (!Array.isArray(list)) { throw new Error('Reduce expects a list as the third argument.'); } list.forEach(item => { const callScope = new Map(func.closure); callScope.set(func.params[0].name, accumulator); callScope.set(func.params[1].name, item); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } accumulator = visit(func.body); scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } }); return accumulator; }, }); // Scan operations (cumulative operations) scope.set('scan', { type: 'NativeFunction', signature: '(func: (T, T) -> T, init: T, list: [T]) -> [T]', call: (args) => { const func = args[0]; let accumulator = args[1]; const list = args[2]; if (func.type !== 'Function') { throw new Error('Scan expects a function as the first argument.'); } if (!Array.isArray(list)) { throw new Error('Scan expects a list as the third argument.'); } const result = [accumulator]; // Start with initial value list.forEach(item => { const paramName1 = typeof func.params[0] === 'string' ? func.params[0] : func.params[0].name; const paramName2 = typeof func.params[1] === 'string' ? func.params[1] : func.params[1].name; const callScope = new Map(func.closure); callScope.set(paramName1, accumulator); callScope.set(paramName2, item); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } accumulator = visit(func.body); result.push(accumulator); scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } }); return result; }, }); // Cumulative sum utility scope.set('cumsum', { type: 'NativeFunction', signature: '(list: [Number]) -> [Number]', call: (args) => { const list = args[0]; if (!Array.isArray(list)) { throw new Error('cumsum expects a list as the first argument.'); } // Create an add function const addFunc = { type: 'Function', params: ['acc', 'x'], body: { type: 'BinaryExpression', operator: '+', left: { type: 'Identifier', name: 'acc' }, right: { type: 'Identifier', name: 'x' } }, closure: new Map() }; const scanFunc = scope.get('scan'); return scanFunc.call([addFunc, { value: 0, isFloat: false }, list]); }, }); // Cumulative product utility scope.set('cumprod', { type: 'NativeFunction', signature: '(list: [Number]) -> [Number]', call: (args) => { const list = args[0]; if (!Array.isArray(list)) { throw new Error('cumprod expects a list as the first argument.'); } // Create a multiply function const mulFunc = { type: 'Function', params: ['acc', 'x'], body: { type: 'BinaryExpression', operator: '*', left: { type: 'Identifier', name: 'acc' }, right: { type: 'Identifier', name: 'x' } }, closure: new Map() }; const scanFunc = scope.get('scan'); return scanFunc.call([mulFunc, { value: 1, isFloat: false }, list]); }, }); // List operations - all immutable scope.set('append', { type: 'NativeFunction', call: (args) => { const list = args[0]; const element = args[1]; if (!Array.isArray(list)) { throw new Error('Append expects a list as the first argument.'); } return [...list, element]; }, }); scope.set('prepend', { type: 'NativeFunction', call: (args) => { const element = args[0]; const list = args[1]; if (!Array.isArray(list)) { throw new Error('Prepend expects a list as the second argument.'); } return [element, ...list]; }, }); scope.set('concat', { type: 'NativeFunction', call: (args) => { const list1 = args[0]; const list2 = args[1]; if (!Array.isArray(list1)) { throw new Error('Concat expects a list as the first argument.'); } if (!Array.isArray(list2)) { throw new Error('Concat expects a list as the second argument.'); } return [...list1, ...list2]; }, }); scope.set('update', { type: 'NativeFunction', call: (args) => { const list = args[0]; const index = args[1]; const value = args[2]; if (!Array.isArray(list)) { throw new Error('Update expects a list as the first argument.'); } // Handle our custom number format for index const indexValue = index && typeof index.value === 'number' ? index.value : index; if (typeof indexValue !== 'number' || indexValue < 0 || indexValue >= list.length) { throw new RuntimeError( `Index out of bounds: ${indexValue}`, null, host.source || '', [`Valid indices are 0 to ${list.length - 1}`, 'Check list length before accessing elements'] ); } const newList = [...list]; newList[indexValue] = value; return newList; }, }); scope.set('removeAt', { type: 'NativeFunction', call: (args) => { const list = args[0]; const index = args[1]; if (!Array.isArray(list)) { throw new Error('RemoveAt expects a list as the first argument.'); } // Handle our custom number format for index const indexValue = index && typeof index.value === 'number' ? index.value : index; if (typeof indexValue !== 'number' || indexValue < 0 || indexValue >= list.length) { throw new RuntimeError( `Index out of bounds: ${indexValue}`, null, host.source || '', [`Valid indices are 0 to ${list.length - 1}`, 'Check list length before accessing elements'] ); } return list.filter((_, i) => i !== indexValue); }, }); scope.set('slice', { type: 'NativeFunction', call: (args) => { const list = args[0]; const start = args[1]; const end = args.length === 3 ? args[2] : list.length; if (!Array.isArray(list)) { throw new Error('Slice expects a list as the first argument.'); } // Handle our custom number format for indices const startValue = start && typeof start.value === 'number' ? start.value : start; const endValue = end && typeof end.value === 'number' ? end.value : end; if (typeof startValue !== 'number' || startValue < 0) { throw new Error(`Invalid start index: ${startValue}`); } if (typeof endValue !== 'number' || endValue < startValue || endValue > list.length) { throw new Error(`Invalid end index: ${endValue}`); } return list.slice(startValue, endValue); }, }); // Monadic operations scope.set('flatMap', { type: 'NativeFunction', signature: '(func: (T) -> [U], list: [T]) -> [U]', call: (args) => { const func = args[0]; const list = args[1]; if (func.type !== 'Function') { throw new Error('flatMap expects a function as the first argument.'); } if (!Array.isArray(list)) { throw new Error('flatMap expects a list as the second argument.'); } const result = []; list.forEach(item => { const paramName = typeof func.params[0] === 'string' ? func.params[0] : func.params[0].name; const callScope = new Map(func.closure); callScope.set(paramName, item); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } const mapped = visit(func.body); scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } if (Array.isArray(mapped)) { result.push(...mapped); } else { result.push(mapped); } }); return result; }, }); // Array broadcasting operations (APL/K inspired) scope.set('broadcast', { type: 'NativeFunction', signature: '(op: (T, U) -> V, scalar: T, array: [U]) -> [V]', call: (args) => { const op = args[0]; const scalar = args[1]; const array = args[2]; if (op.type !== 'Function') { throw new Error('broadcast expects a function as the first argument.'); } if (!Array.isArray(array)) { throw new Error('broadcast expects an array as the third argument.'); } return array.map(item => { const param1Name = typeof op.params[0] === 'string' ? op.params[0] : op.params[0].name; const param2Name = typeof op.params[1] === 'string' ? op.params[1] : op.params[1].name; const callScope = new Map(op.closure); callScope.set(param1Name, scalar); callScope.set(param2Name, item); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } try { return visit(op.body); } finally { scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } } }); }, }); scope.set('zipWith', { type: 'NativeFunction', signature: '(op: (T, U) -> V, array1: [T], array2: [U]) -> [V]', call: (args) => { const op = args[0]; const array1 = args[1]; const array2 = args[2]; if (op.type !== 'Function') { throw new Error('zipWith expects a function as the first argument.'); } if (!Array.isArray(array1)) { throw new Error('zipWith expects an array as the second argument.'); } if (!Array.isArray(array2)) { throw new Error('zipWith expects an array as the third argument.'); } const minLength = Math.min(array1.length, array2.length); const result = []; for (let i = 0; i < minLength; i++) { const param1Name = typeof op.params[0] === 'string' ? op.params[0] : op.params[0].name; const param2Name = typeof op.params[1] === 'string' ? op.params[1] : op.params[1].name; const callScope = new Map(op.closure); callScope.set(param1Name, array1[i]); callScope.set(param2Name, array2[i]); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } try { result.push(visit(op.body)); } finally { scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } } } return result; }, }); scope.set('reshape', { type: 'NativeFunction', signature: '(shape: [Int], array: [T]) -> [[T]]', call: (args) => { const shape = args[0]; const array = args[1]; if (!Array.isArray(shape)) { throw new Error('reshape expects an array of dimensions as the first argument.'); } if (!Array.isArray(array)) { throw new Error('reshape expects an array as the second argument.'); } // For now, support only 2D reshape (matrix) if (shape.length !== 2) { throw new Error('reshape currently supports only 2D reshaping.'); } const rows = shape[0] && typeof shape[0].value === 'number' ? shape[0].value : shape[0]; const cols = shape[1] && typeof shape[1].value === 'number' ? shape[1].value : shape[1]; if (rows * cols !== array.length) { throw new Error(`Cannot reshape array of length ${array.length} into ${rows}x${cols} matrix.`); } const result = []; for (let i = 0; i < rows; i++) { const row = []; for (let j = 0; j < cols; j++) { row.push(array[i * cols + j]); } result.push(row); } return result; }, }); // Advanced array indexing operations scope.set('at', { type: 'NativeFunction', signature: '(indices: [Int], array: [T]) -> [T]', call: (args) => { const indices = args[0]; const array = args[1]; if (!Array.isArray(indices)) { throw new Error('at expects an array of indices as the first argument.'); } if (!Array.isArray(array)) { throw new Error('at expects an array as the second argument.'); } return indices.map(index => { const indexValue = index && typeof index.value === 'number' ? index.value : index; if (typeof indexValue !== 'number' || indexValue < 0 || indexValue >= array.length) { throw new RuntimeError( `Index out of bounds: ${indexValue}`, null, host.source || '', [`Valid indices are 0 to ${list.length - 1}`, 'Check list length before accessing elements'] ); } return array[indexValue]; }); }, }); scope.set('where', { type: 'NativeFunction', signature: '(predicate: (T) -> Bool, array: [T]) -> [Int]', call: (args) => { const predicate = args[0]; const array = args[1]; if (predicate.type !== 'Function') { throw new Error('where expects a function as the first argument.'); } if (!Array.isArray(array)) { throw new Error('where expects an array as the second argument.'); } const result = []; array.forEach((item, index) => { const paramName = typeof predicate.params[0] === 'string' ? predicate.params[0] : predicate.params[0].name; const callScope = new Map(predicate.closure); callScope.set(paramName, item); const originalScope = new Map(scope); scope.clear(); for (const [key, value] of callScope.entries()) { scope.set(key, value); } const matches = visit(predicate.body); scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } if (matches) { result.push({ value: index, isFloat: false }); } }); return result; }, }); scope.set('take', { type: 'NativeFunction', signature: '(n: Int, array: [T]) -> [T]', call: (args) => { const n = args[0]; const array = args[1]; if (!Array.isArray(array)) { throw new Error('take expects an array as the second argument.'); } const nValue = n && typeof n.value === 'number' ? n.value : n; if (typeof nValue !== 'number' || nValue < 0) { throw new Error(`take expects a non-negative number, got: ${nValue}`); } return array.slice(0, nValue); }, }); scope.set('drop', { type: 'NativeFunction', signature: '(n: Int, array: [T]) -> [T]', call: (args) => { const n = args[0]; const array = args[1]; if (!Array.isArray(array)) { throw new Error('drop expects an array as the second argument.'); } const nValue = n && typeof n.value === 'number' ? n.value : n; if (typeof nValue !== 'number' || nValue < 0) { throw new Error(`drop expects a non-negative number, got: ${nValue}`); } return array.slice(nValue); }, }); // Table operations - all immutable scope.set('set', { type: 'NativeFunction', call: (args) => { const table = args[0]; const key = args[1]; const value = args[2]; if (table.type !== 'Object' || !table.properties) { throw new Error('Set expects a table as the first argument.'); } const newProperties = new Map(table.properties); newProperties.set(key, value); return { type: 'Object', properties: newProperties }; }, }); scope.set('remove', { type: 'NativeFunction', call: (args) => { const table = args[0]; const key = args[1]; if (table.type !== 'Object' || !table.properties) { throw new Error('Remove expects a table as the first argument.'); } const newProperties = new Map(table.properties); newProperties.delete(key); return { type: 'Object', properties: newProperties }; }, }); scope.set('merge', { type: 'NativeFunction', call: (args) => { const table1 = args[0]; const table2 = args[1]; if (table1.type !== 'Object' || !table1.properties) { throw new Error('Merge expects a table as the first argument.'); } if (table2.type !== 'Object' || !table2.properties) { throw new Error('Merge expects a table as the second argument.'); } const newProperties = new Map(table1.properties); for (const [key, value] of table2.properties.entries()) { newProperties.set(key, value); } return { type: 'Object', properties: newProperties }; }, }); scope.set('keys', { type: 'NativeFunction', call: (args) => { const table = args[0]; if (table.type !== 'Object' || !table.properties) { throw new Error('Keys expects a table as the first argument.'); } return Array.from(table.properties.keys()); }, }); scope.set('values', { type: 'NativeFunction', call: (args) => { const table = args[0]; if (table.type !== 'Object' || !table.properties) { throw new Error('Values expects a table as the first argument.'); } return Array.from(table.properties.values()); }, }); // String functions scope.set('str', { type: 'Object', properties: new Map([ ['concat', { type: 'NativeFunction', call: (args) => { if (args.length < 2) { throw new Error('str.concat expects at least 2 arguments'); } return args.map(arg => String(arg)).join(''); }, }], ['split', { type: 'NativeFunction', call: (args) => { if (args.length !== 2) { throw new Error('str.split expects exactly 2 arguments: string and delimiter'); } const str = String(args[0]); const delimiter = String(args[1]); return str.split(delimiter); }, }], ['join', { type: 'NativeFunction', call: (args) => { if (args.length !== 2) { throw new Error('str.join expects exactly 2 arguments: array and delimiter'); } if (!Array.isArray(args[0])) { throw new Error('str.join expects an array as the first argument'); } const array = args[0]; const delimiter = String(args[1]); return array.map(item => String(item)).join(delimiter); }, }], ['length', { type: 'NativeFunction', call: (args) => { if (args.length !== 1) { throw new Error('str.length expects exactly 1 argument'); } return { value: String(args[0]).length, isFloat: false }; }, }], ['substring', { type: 'NativeFunction', call: (args) => { if (args.length < 2 || args.length > 3) { throw new Error('str.substring expects 2 or 3 arguments: string, start, [end]'); } const str = String(args[0]); // Handle our custom number format for start and end const start = args[1] && typeof args[1].value === 'number' ? args[1].value : Number(args[1]); const end = args.length === 3 ? (args[2] && typeof args[2].value === 'number' ? args[2].value : Number(args[2])) : undefined; return str.substring(start, end); }, }], ['replace', { type: 'NativeFunction', call: (args) => { if (args.length !== 3) { throw new Error('str.replace expects exactly 3 arguments: string, search, replace'); } const str = String(args[0]); const search = String(args[1]); const replace = String(args[2]); return str.replace(new RegExp(search, 'g'), replace); }, }], ['trim', { type: 'NativeFunction', call: (args) => { if (args.length !== 1) { throw new Error('str.trim expects exactly 1 argument'); } return String(args[0]).trim(); }, }], ['upper', { type: 'NativeFunction', call: (args) => { if (args.length !== 1) { throw new Error('str.upper expects exactly 1 argument'); } return String(args[0]).toUpperCase(); }, }], ['lower', { type: 'NativeFunction', call: (args) => { if (args.length !== 1) { throw new Error('str.lower expects exactly 1 argument'); } return String(args[0]).toLowerCase(); }, }], ]), }); // math namespace scope.set('math', { type: 'Object', properties: new Map([ ['abs', { type: 'NativeFunction', call: ([x]) => ({ value: Math.abs((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['sign', { type: 'NativeFunction', call: ([x]) => ({ value: Math.sign((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['floor', { type: 'NativeFunction', call: ([x]) => ({ value: Math.floor((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['ceil', { type: 'NativeFunction', call: ([x]) => ({ value: Math.ceil((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['round', { type: 'NativeFunction', call: ([x]) => ({ value: Math.round((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['trunc', { type: 'NativeFunction', call: ([x]) => ({ value: Math.trunc((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['min', { type: 'NativeFunction', call: ([a, b]) => ({ value: Math.min((a && typeof a.value === 'number') ? a.value : Number(a), (b && typeof b.value === 'number') ? b.value : Number(b)), isFloat: true }) }], ['max', { type: 'NativeFunction', call: ([a, b]) => ({ value: Math.max((a && typeof a.value === 'number') ? a.value : Number(a), (b && typeof b.value === 'number') ? b.value : Number(b)), isFloat: true }) }], ['clamp', { type: 'NativeFunction', call: ([x, lo, hi]) => { const xv = (x && typeof x.value === 'number') ? x.value : Number(x); const lov = (lo && typeof lo.value === 'number') ? lo.value : Number(lo); const hiv = (hi && typeof hi.value === 'number') ? hi.value : Number(hi); return { value: Math.min(Math.max(xv, lov), hiv), isFloat: true }; }}], ['pow', { type: 'NativeFunction', call: ([x, y]) => ({ value: Math.pow((x && typeof x.value === 'number') ? x.value : Number(x), (y && typeof y.value === 'number') ? y.value : Number(y)), isFloat: true }) }], ['sqrt', { type: 'NativeFunction', call: ([x]) => { const v = (x && typeof x.value === 'number') ? x.value : Number(x); if (v < 0) throw new Error('Domain error: sqrt expects x >= 0'); return { value: Math.sqrt(v), isFloat: true }; } }], ['exp', { type: 'NativeFunction', call: ([x]) => ({ value: Math.exp((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['log', { type: 'NativeFunction', call: ([x]) => { const v = (x && typeof x.value === 'number') ? x.value : Number(x); if (v <= 0) throw new Error('Domain error: log expects x > 0'); return { value: Math.log(v), isFloat: true }; } }], ['sin', { type: 'NativeFunction', call: ([x]) => ({ value: Math.sin((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['cos', { type: 'NativeFunction', call: ([x]) => ({ value: Math.cos((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['tan', { type: 'NativeFunction', call: ([x]) => ({ value: Math.tan((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['asin', { type: 'NativeFunction', call: ([x]) => ({ value: Math.asin((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['acos', { type: 'NativeFunction', call: ([x]) => ({ value: Math.acos((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['atan', { type: 'NativeFunction', call: ([x]) => ({ value: Math.atan((x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['atan2', { type: 'NativeFunction', call: ([y, x]) => ({ value: Math.atan2((y && typeof y.value === 'number') ? y.value : Number(y), (x && typeof x.value === 'number') ? x.value : Number(x)), isFloat: true }) }], ['deg', { type: 'NativeFunction', call: ([r]) => ({ value: ((r && typeof r.value === 'number') ? r.value : Number(r)) * (180 / Math.PI), isFloat: true }) }], ['rad', { type: 'NativeFunction', call: ([d]) => ({ value: ((d && typeof d.value === 'number') ? d.value : Number(d)) * (Math.PI / 180), isFloat: true }) }], ['random', { type: 'NativeFunction', call: () => ({ value: Math.random(), isFloat: true }) }], ['randomInt', { type: 'NativeFunction', call: ([lo, hi]) => { const a = ~~((lo && typeof lo.value === 'number') ? lo.value : Number(lo)); const b = ~~((hi && typeof hi.value === 'number') ? hi.value : Number(hi)); if (a > b) throw new Error('Invalid range: lo > hi'); const n = a + Math.floor(Math.random() * (b - a + 1)); return { value: n, isFloat: false }; } }], ]) }); // validate namespace - (value: any) -> Bool scope.set('validate', { type: 'Object', properties: new Map([ ['notEmpty', { type: 'NativeFunction', signature: '(value: any) -> Bool', call: ([value]) => { if (value === null || value === undefined) return false; if (typeof value === 'string') return value.length > 0; if (Array.isArray(value)) return value.length > 0; if (value && typeof value === 'object' && value.properties instanceof Map) return value.properties.size > 0; return true; } }], ['range', { type: 'NativeFunction', signature: '(min: Number, max: Number, value: Number) -> Bool', call: ([min, max, value]) => { const minVal = (min && typeof min.value === 'number') ? min.value : Number(min); const maxVal = (max && typeof max.value === 'number') ? max.value : Number(max); const val = (value && typeof value.value === 'number') ? value.value : Number(value); return val >= minVal && val <= maxVal; } }], ['email', { type: 'NativeFunction', signature: '(email: String) -> Bool', call: ([email]) => { const emailStr = String(email); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(emailStr); } }], ['type', { type: 'NativeFunction', signature: '(expectedType: String, value: any) -> Bool', call: ([expectedType, value]) => { const expected = String(expectedType); const actual = getRuntimeType(value); return isTypeAssignable(actual, expected); } }], ]) }); // sort namespace scope.set('sort', { type: 'Object', properties: new Map([ ['by', { type: 'NativeFunction', signature: '(list: [T], keyFunc: (T) -> U) -> [T]', call: ([list, keyFunc]) => { if (!Array.isArray(list)) { throw new Error('sort.by expects an array as the first argument'); } if (!keyFunc || keyFunc.type !== 'Function') { throw new Error('sort.by expects a function as the second argument'); } return [...list].sort((a, b) => { // Helper to call a function with one argument const callFunction = (func, arg) => { const paramName = typeof func.params[0] === 'string' ? func.params[0] : func.params[0].name; const callScope = new Map(func.closure); callScope.set(paramName, arg); // Save current scope const originalScope = new Map(scope); scope.clear(); for (const [k, v] of callScope.entries()) scope.set(k, v); try { return visit(func.body); } finally { // Restore original scope scope.clear(); for (const [k, v] of originalScope.entries()) scope.set(k, v); } }; const keyA = callFunction(keyFunc, a); const keyB = callFunction(keyFunc, b); // Handle numeric comparison const numA = (keyA && typeof keyA.value === 'number') ? keyA.value : Number(keyA); const numB = (keyB && typeof keyB.value === 'number') ? keyB.value : Number(keyB); if (!isNaN(numA) && !isNaN(numB)) { return numA - numB; } // Handle string comparison const strA = String(keyA); const strB = String(keyB); return strA.localeCompare(strB); }); } }], ]) }); // group namespace scope.set('group', { type: 'Object', properties: new Map([ ['by', { type: 'NativeFunction', signature: '(list: [T], keyFunc: (T) -> U) -> Table', call: ([list, keyFunc]) => { if (!Array.isArray(list)) { throw new Error('group.by expects an array as the first argument'); } if (!keyFunc || keyFunc.type !== 'Function') { throw new Error('group.by expects a function as the second argument'); } const groups = new Map(); // Helper to call a function with one argument const callFunction = (func, arg) => { const paramName = typeof func.params[0] === 'string' ? func.params[0] : func.params[0].name; const callScope = new Map(func.closure); callScope.set(paramName, arg); // Save current scope const originalScope = new Map(scope); scope.clear(); for (const [k, v] of callScope.entries()) scope.set(k, v); try { return visit(func.body); } finally { // Restore original scope scope.clear(); for (const [k, v] of originalScope.entries()) scope.set(k, v); } }; for (const item of list) { const key = callFunction(keyFunc, item); const keyStr = String(key); if (!groups.has(keyStr)) { groups.set(keyStr, []); } groups.get(keyStr).push(item); } return { type: 'Object', properties: groups }; } }], ]) }); // text namespace - enhanced string processing scope.set('text', { type: 'Object', properties: new Map([ ['lines', { type: 'NativeFunction', signature: '(text: String) -> [String]', call: ([text]) => { const str = String(text); return str.split(/\r?\n/); } }], ['words', { type: 'NativeFunction', signature: '(text: String) -> [String]', call: ([text]) => { const str = String(text); return str.trim().split(/\s+/).filter(word => word.length > 0); } }], ['padLeft', { type: 'NativeFunction', signature: '(width: Int, text: String) -> String', call: ([width, text]) => { const w = (width && typeof width.value === 'number') ? width.value : Number(width); const str = String(text); return str.padStart(w, ' '); } }], ['padRight', { type: 'NativeFunction', signature: '(width: Int, text: String) -> String', call: ([width, text]) => { const w = (width && typeof width.value === 'number') ? width.value : Number(width); const str = String(text); return str.padEnd(w, ' '); } }], ]) }); // debug namespace - enhanced debugging tools scope.set('debug', { type: 'Object', properties: new Map([ ['print', { type: 'NativeFunction', signature: '(value: any) -> Unit', call: (args) => { if (args.length === 0) { hostDebug(''); return; } const formatDebugValue = (value, name = null) => { const prefix = name ? `${name}: ` : ''; const type = getRuntimeType(value); if (value && value.type === 'Function') { const params = value.params ? value.params.map(p => typeof p === 'string' ? p : p.name).join(', ') : ''; return `${prefix} ...> (${type})`; } if (Array.isArray(value)) { return `${prefix}[${value.map(v => String(v)).join(', ')}] (${type}, length: ${value.length})`; } if (value && typeof value === 'object' && value.properties instanceof Map) { const props = Array.from(value.properties.entries()).map(([k, v]) => `${k}: ${String(v)}`).join(', '); return `${prefix}{${props}} (${type}, size: ${value.properties.size})`; } const displayValue = (value && typeof value.value === 'number') ? value.value : value; return `${prefix}${String(displayValue)} (${type})`; }; for (let i = 0; i < args.length; i++) { const formatted = formatDebugValue(args[i]); hostDebug(formatted); } } }], ['inspect', { type: 'NativeFunction', signature: '(value: any) -> String', call: ([value]) => { const type = getRuntimeType(value); let details = `Type: ${type}\n`; // Try to get shape information, but handle errors gracefully try { const shapeFunc = scope.get('shape'); if (shapeFunc && shapeFunc.type === 'NativeFunction') { const shape = shapeFunc.call([value]); if (shape && shape.properties) { for (const [key, val] of shape.properties.entries()) { details += `${key}: ${String(val)}\n`; } } } } catch (e) { // If shape fails, just continue without shape info details += `Shape: Unable to determine (${e.message})\n`; } if (value && value.type === 'Function') { const params = value.params ? value.params.map(p => typeof p === 'string' ? p : p.name).join(', ') : ''; details += `Parameters: (${params})\n`; if (value.signature) { details += `Signature: ${value.signature}\n`; } } if (value && value.type === 'NativeFunction') { details += `Native Function: Built-in system function\n`; if (value.signature) { details += `Signature: ${value.signature}\n`; } } return details.trim(); } }], ]) }); // random namespace - enhanced random utilities scope.set('random', { type: 'Object', properties: new Map([ ['choice', { type: 'NativeFunction', signature: '(list: [T]) -> T', call: ([list]) => { if (!Array.isArray(list) || list.length === 0) { throw new Error('random.choice expects a non-empty array'); } const index = Math.floor(Math.random() * list.length); return list[index]; } }], ['shuffle', { type: 'NativeFunction', signature: '(list: [T]) -> [T]', call: ([list]) => { if (!Array.isArray(list)) { throw new Error('random.shuffle expects an array'); } const result = [...list]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; } }], ['range', { type: 'NativeFunction', signature: '(min: Int, max: Int) -> Int', call: ([min, max]) => { const minVal = (min && typeof min.value === 'number') ? min.value : Number(min); const maxVal = (max && typeof max.value === 'number') ? max.value : Number(max); if (minVal > maxVal) throw new Error('Invalid range: min > max'); const result = minVal + Math.floor(Math.random() * (maxVal - minVal + 1)); return { value: result, isFloat: false }; } }], ['seed', { type: 'NativeFunction', signature: '(seed: Int) -> Unit', call: ([seed]) => { // Note: JavaScript doesn't have a built-in seeded random, so this is a placeholder // In a real implementation, you'd use a seeded PRNG library const seedVal = (seed && typeof seed.value === 'number') ? seed.value : Number(seed); hostDebug(`Random seed set to ${seedVal} (Note: JavaScript Math.random is not seeded)`); return undefined; } }], ]) }); // Function combinators scope.set('flip', { type: 'NativeFunction', signature: '(func: (A, B) -> C) -> (B, A) -> C', call: (args) => { const func = args[0]; if (func.type !== 'Function') { throw new Error('flip expects a function as the first argument.'); } return { type: 'Function', params: func.params.length >= 2 ? [func.params[1], func.params[0], ...func.params.slice(2)] : func.params, body: func.body, closure: new Map(func.closure), signature: func.signature, }; }, }); scope.set('apply', { type: 'NativeFunction', signature: '(func: (T) -> U, value: T) -> U', call: (args) => { const func = args[0]; const value = args[1]; if (func.type !== 'Function') { throw new Error('apply expects a function as the first argument.'); } // Call the function with the value const paramName = typeof func.params[0] === 'string' ? func.params[0] : func.params[0].name; const callScope = new Map(func.closure); callScope.set(paramName, value); const originalScope = new Map(scope); scope.clear(); for (const [key, val] of callScope.entries()) { scope.set(key, val); } try { return visit(func.body); } finally { scope.clear(); for (const [key, val] of originalScope.entries()) { scope.set(key, val); } } }, }); scope.set('pipe', { type: 'NativeFunction', signature: '(value: T, func: (T) -> U) -> U', call: (args) => { const value = args[0]; const func = args[1]; if (func.type !== 'Function') { throw new Error('pipe expects a function as the second argument.'); } // Apply the function to the value (reverse of apply) const applyFunc = scope.get('apply'); return applyFunc.call([func, value]); }, }); scope.set('compose', { type: 'NativeFunction', signature: '(f: (B) -> C, g: (A) -> B) -> (A) -> C', call: (args) => { const f = args[0]; const g = args[1]; if (f.type !== 'Function' || g.type !== 'Function') { throw new Error('compose expects two functions as arguments.'); } return { type: 'Function', params: g.params, body: { type: 'FunctionCall', callee: { type: 'Identifier', name: 'f' }, arguments: [{ type: 'FunctionCall', callee: { type: 'Identifier', name: 'g' }, arguments: g.params.map(p => ({ type: 'Identifier', name: typeof p === 'string' ? p : p.name })) }] }, closure: new Map([...g.closure, ['f', f], ['g', g]]), signature: f.signature, }; }, }); // Utility functions - top-level functions scope.set('chunk', { type: 'NativeFunction', signature: '(list: [T], size: Int) -> [[T]]', call: ([list, size]) => { if (!Array.isArray(list)) { throw new Error('chunk expects an array as the first argument'); } const chunkSize = (size && typeof size.value === 'number') ? size.value : Number(size); if (chunkSize <= 0) { throw new Error('chunk size must be positive'); } const result = []; for (let i = 0; i < list.length; i += chunkSize) { result.push(list.slice(i, i + chunkSize)); } return result; } }); scope.set('range', { type: 'NativeFunction', signature: '(start: Int, end: Int) -> [Int]', call: ([start, end]) => { const startVal = (start && typeof start.value === 'number') ? start.value : Number(start); const endVal = (end && typeof end.value === 'number') ? end.value : Number(end); const result = []; if (startVal <= endVal) { for (let i = startVal; i <= endVal; i++) { result.push({ value: i, isFloat: false }); } } else { for (let i = startVal; i >= endVal; i--) { result.push({ value: i, isFloat: false }); } } return result; } }); scope.set('repeat', { type: 'NativeFunction', signature: '(count: Int, value: T) -> [T]', call: ([count, value]) => { const n = (count && typeof count.value === 'number') ? count.value : Number(count); if (n < 0) { throw new Error('repeat count must be non-negative'); } const result = []; for (let i = 0; i < n; i++) { result.push(value); } return result; } }); scope.set('assert', { type: 'NativeFunction', signature: '(condition: Bool, message: String) -> Unit', call: ([condition, message]) => { const isTrue = Boolean(condition); if (!isTrue) { const msg = message ? String(message) : 'Assertion failed'; throw new Error(`Assertion failed: ${msg}`); } return undefined; } }); function visit(node) { switch (node.type) { case 'Program': return visitProgram(node); case 'TypeDeclaration': return visitTypeDeclaration(node); case 'VariableDeclaration': return visitVariableDeclaration(node); case 'FunctionDeclaration': return visitFunctionDeclaration(node); case 'CurriedFunctionDeclaration': return visitCurriedFunctionDeclaration(node); case 'FunctionDeclarationBody': return visitFunctionDeclarationBody(node); case 'WithHeader': return visitWithHeader(node); case 'CurriedFunctionBody': return visitCurriedFunctionBody(node); case 'FunctionCall': return visitFunctionCall(node); case 'WhenExpression': return visitWhenExpression(node); case 'BinaryExpression': return visitBinaryExpression(node); case 'UnaryExpression': return visitUnaryExpression(node); case 'NumberLiteral': return { value: node.value, isFloat: node.isFloat }; case 'StringLiteral': return node.value; case 'BooleanLiteral': return node.value; case 'Identifier': return visitIdentifier(node); case 'ListLiteral': return visitListLiteral(node); case 'TableLiteral': return visitTableLiteral(node); case 'AnonymousFunction': return visitAnonymousFunction(node); case 'MemberExpression': return visitMemberExpression(node); case 'TypePattern': return node.name; case 'WildcardPattern': return '_'; // Wildcard pattern always matches case 'ResultExpression': return visitResultExpression(node); default: throw new Error(`Unknown node type: ${node.type}`); } } function visitProgram(node) { // First pass: add all function declarations to scope for mutual recursion support for (const statement of node.body) { if (statement.type === 'FunctionDeclaration') { const func = { type: 'Function', params: statement.params, body: statement.body, closure: new Map(scope), returnType: statement.returnType, }; scope.set(statement.name, func); // Add the function to its own closure for recursion support func.closure.set(statement.name, func); } } // Second pass: execute all statements in order let result; for (const statement of node.body) { if (statement.type === 'FunctionDeclaration') { // Function declarations are already handled in the first pass continue; } result = visit(statement); } return result; } function visitTypeDeclaration(node) { types.set(node.name, node.typeAnnotation); } function visitVariableDeclaration(node) { const value = visit(node.value); if (types.has(node.name)) { const expectedType = types.get(node.name); const actualType = getRuntimeType(value); if (!isTypeAssignable(actualType, expectedType)) { const displayValue = value && typeof value.value === 'number' ? value.value : value; throw new Error(`Type mismatch for ${node.name}: expected ${expectedType} but got ${actualType} (value: ${JSON.stringify(displayValue)})`); } } scope.set(node.name, value); return value; // Return the assigned value } function visitFunctionDeclaration(node) { // Function declarations are now handled in visitProgram for mutual recursion support // This function is kept for backward compatibility but should not be called directly const func = { type: 'Function', params: node.params, body: node.body, closure: new Map(scope), returnType: node.returnType, }; scope.set(node.name, func); // Add the function to its own closure for recursion support func.closure.set(node.name, func); } function visitFunctionDeclarationBody(node) { const func = { type: 'Function', params: node.params, body: node.body, closure: new Map(scope), returnType: node.returnType, }; return func; } function visitCurriedFunctionDeclaration(node) { // Create a curried function with type information const func = { type: 'Function', params: [node.param], // First typed parameter body: node.body, // CurriedFunctionBody containing remaining params closure: new Map(scope), returnType: node.returnType, // Function type like (Float -> Float) isCurried: true, }; scope.set(node.name, func); // Add the function to its own closure for recursion support func.closure.set(node.name, func); } function visitCurriedFunctionBody(node) { // Handle the remaining parameters and body of a curried function const func = { type: 'Function', params: node.params, // Remaining untyped parameters body: node.body, // Final expression closure: new Map(scope), returnType: node.returnType, // Pass along the final return type isCurriedBody: true, }; return func; } // Helper function to get the runtime type of a value function getRuntimeType(value) { // Handle our custom number format with isFloat property if (value && typeof value.value === 'number') { return value.isFloat ? 'Float' : 'Int'; } if (typeof value === 'number') { // Fallback for regular numbers return Number.isInteger(value) ? 'Int' : 'Float'; } if (typeof value === 'string') { return 'String'; } if (typeof value === 'boolean') { return 'Bool'; } if (Array.isArray(value)) { return 'List'; } if (value && value.type === 'Object' && value.properties) { return 'Table'; } if (value && value.type === 'Result') { return 'Result'; } return 'Unknown'; } // Type assignability with numeric lattice: Int ⊂ Float ⊂ Number function isTypeAssignable(actualType, expectedType) { if (!expectedType) return true; // Handle complex type objects if (typeof expectedType === 'object') { if (expectedType.type === 'PrimitiveType') { return isTypeAssignable(actualType, expectedType.name); } if (expectedType.type === 'FunctionType') { return actualType === 'Function'; // TODO: Could add deeper function signature validation } } // Handle string types if (expectedType === actualType) return true; if (expectedType === 'Number') { return actualType === 'Int' || actualType === 'Float'; } if (expectedType === 'Float') { return actualType === 'Float' || actualType === 'Int'; } return false; } // Helper function to validate argument types function validateArgumentType(argValue, expectedType, paramName, functionName) { if (!expectedType) { return; // No type annotation, skip validation } const actualType = getRuntimeType(argValue); if (!isTypeAssignable(actualType, expectedType)) { // Extract the actual value for display const displayValue = argValue && typeof argValue.value === 'number' ? argValue.value : argValue; const expectedTypeStr = typeof expectedType === 'object' ? (expectedType.type === 'PrimitiveType' ? expectedType.name : expectedType.type === 'FunctionType' ? 'Function' : JSON.stringify(expectedType)) : expectedType; throw new Error( `Type mismatch in function '${functionName}': ` + `Expected ${expectedTypeStr} for parameter '${paramName}', ` + `but got ${actualType} (value: ${JSON.stringify(displayValue)})` ); } } // Helper function to validate return type function validateReturnType(returnValue, expectedType, functionName) { if (!expectedType) { return; // No return type annotation, skip validation } // Handle function type validation if (expectedType && typeof expectedType === 'object' && expectedType.type === 'FunctionType') { if (returnValue.type !== 'Function') { throw new Error( `Return type mismatch in function '${functionName}': ` + `Expected function, but got ${getRuntimeType(returnValue)}` ); } // TODO: Could add more detailed function signature validation here return; } const actualType = getRuntimeType(returnValue); // Allow Int where Float is expected (numeric widening) if (!isTypeAssignable(actualType, expectedType)) { // Extract the actual value for display const displayValue = returnValue && typeof returnValue.value === 'number' ? returnValue.value : returnValue; const expectedTypeStr = typeof expectedType === 'object' ? (expectedType.type === 'PrimitiveType' ? expectedType.name : expectedType.type === 'FunctionType' ? 'Function' : JSON.stringify(expectedType)) : expectedType; throw new Error( `Return type mismatch in function '${functionName}': ` + `Expected ${expectedTypeStr}, but got ${actualType} (value: ${JSON.stringify(displayValue)})` ); } } function visitFunctionCall(node) { const callee = visit(node.callee); // Handle native functions (like io.out) if (callee.type === 'NativeFunction') { const args = node.arguments.map(arg => visit(arg)); return callee.call(args); } if (callee.type !== 'Function') { throw new Error(`${node.callee.name} is not a function`); } const args = node.arguments.map(arg => visit(arg)); // Validate argument types (only for typed functions) const functionName = node.callee.name || 'anonymous'; for (let i = 0; i < Math.min(args.length, callee.params.length); i++) { const param = callee.params[i]; // Handle both string parameters (anonymous functions) and object parameters (typed functions) const paramName = typeof param === 'string' ? param : (param.name || param.value); // Check if this is a typed parameter (has a type annotation) // Typed parameters have { name: string, type: string } // Untyped parameters have { type: 'Identifier', name: string } const expectedType = typeof param === 'string' ? null : (param.type && param.type !== 'Identifier' ? param.type : null); // Only validate if the parameter has a type annotation if (expectedType) { validateArgumentType(args[i], expectedType, paramName, functionName); } } if (args.length < callee.params.length) { // Partial application const newParams = callee.params.slice(args.length); const newBody = callee.body; const newClosure = new Map(callee.closure); for (let i = 0; i < args.length; i++) { // Handle both string parameters (anonymous functions) and object parameters (typed functions) const paramName = typeof callee.params[i] === 'string' ? callee.params[i] : callee.params[i].name; newClosure.set(paramName, args[i]); } return { type: 'Function', params: newParams, body: newBody, closure: newClosure, returnType: callee.returnType, }; } else if (args.length > callee.params.length) { throw new Error(`Too many arguments for function ${functionName}. Expected ${callee.params.length} but got ${args.length}`); } // Create the call scope by combining the global scope with the function's closure and arguments const callScope = new Map(scope); // Start with global scope for mutual recursion // Add function's closure variables for (const [key, value] of callee.closure.entries()) { callScope.set(key, value); } // Add function parameters for (let i = 0; i < callee.params.length; i++) { // Handle both string parameters (anonymous functions) and object parameters (typed functions) const paramName = typeof callee.params[i] === 'string' ? callee.params[i] : callee.params[i].name; callScope.set(paramName, args[i]); } // Save the current scope and set up the function's scope const originalScope = new Map(scope); scope.clear(); // Set up the function's scope with global scope, closure variables, and parameters for (const [key, value] of callScope.entries()) { scope.set(key, value); } let result; if (callee.body.type === 'FunctionDeclarationBody') { // This is a curried function, return the next nested function result = visit(callee.body); } else if (callee.body.type === 'CurriedFunctionBody') { // This is a new-style typed curried function body result = visit(callee.body); } else if (callee.body && callee.body.type === 'WithHeader') { // Execute with-header before evaluating body result = visitWithHeader(callee.body); } else { // This is the final function body, execute it result = visit(callee.body); } // Validate return type validateReturnType(result, callee.returnType, functionName); scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } return result; } // Execute a with-header node in current scope context function visitWithHeader(node) { const withTypes = new Map(); const withScope = new Map(scope); const originalScope = new Map(scope); // Helper to set scope from a map function setScopeFrom(map) { scope.clear(); for (const [k, v] of map.entries()) scope.set(k, v); } if (node.recursive) { // Pre-bind placeholders for (const entry of node.entries) { if (entry.type === 'WithTypeDecl') { withTypes.set(entry.name, entry.typeAnnotation); } else if (entry.type === 'WithAssign') { withScope.set(entry.name, undefined); } } setScopeFrom(withScope); for (const entry of node.entries) { if (entry.type !== 'WithAssign') continue; let boundValue; // If the value is an anonymous function AST, build a Function that // closes over the withScope (by reference) to enable mutual recursion if (entry.value && entry.value.type === 'AnonymousFunction') { boundValue = { type: 'Function', params: entry.value.params, body: entry.value.body, closure: withScope, }; } else { boundValue = visit(entry.value); } if (!boundValue || boundValue.type !== 'Function') { throw new Error(`with rec expects function-valued bindings. '${entry.name}' is not a function`); } withScope.set(entry.name, boundValue); } // Validate typed locals if any for (const [name, expectedType] of withTypes.entries()) { if (!withScope.has(name)) continue; const actual = withScope.get(name); if (!isTypeAssignable(getRuntimeType(actual), expectedType)) { const displayValue = actual && typeof actual.value === 'number' ? actual.value : actual; const expectedTypeStr = typeof expectedType === 'object' ? (expectedType.type === 'PrimitiveType' ? expectedType.name : 'Function') : expectedType; throw new Error(`Type mismatch for ${name}: expected ${expectedTypeStr} but got ${getRuntimeType(actual)} (value: ${JSON.stringify(displayValue)})`); } } // Evaluate body in with-scope setScopeFrom(withScope); try { return visit(node.body); } finally { setScopeFrom(originalScope); } } else { // Non-rec: process in order for (const entry of node.entries) { if (entry.type === 'WithTypeDecl') { withTypes.set(entry.name, entry.typeAnnotation); continue; } // Assign setScopeFrom(withScope); const value = visit(entry.value); // Type-check if declared if (withTypes.has(entry.name)) { const expectedType = withTypes.get(entry.name); if (!isTypeAssignable(getRuntimeType(value), expectedType)) { const displayValue = value && typeof value.value === 'number' ? value.value : value; const expectedTypeStr = typeof expectedType === 'object' ? (expectedType.type === 'PrimitiveType' ? expectedType.name : 'Function') : expectedType; throw new Error(`Type mismatch for ${entry.name}: expected ${expectedTypeStr} but got ${getRuntimeType(value)} (value: ${JSON.stringify(displayValue)})`); } } withScope.set(entry.name, value); } // Evaluate body setScopeFrom(withScope); try { return visit(node.body); } finally { setScopeFrom(originalScope); } } } function visitWhenExpression(node) { const discriminantValues = node.discriminants.map(d => visit(d)); for (const whenCase of node.cases) { const patterns = whenCase.patterns; if (patterns.length !== discriminantValues.length) { continue; } let match = true; const caseScope = new Map(); for (let i = 0; i < patterns.length; i++) { const pattern = patterns[i]; const discriminantValue = discriminantValues[i]; const discriminantName = node.discriminants[i].type === 'Identifier' ? node.discriminants[i].name : null; if (pattern.type === 'WildcardPattern') { continue; } if (pattern.type === 'GuardPattern') { // For guard patterns, the underlying pattern is treated as a binding pattern const underlyingPattern = pattern.pattern; let underlyingMatch = true; if (underlyingPattern.type === 'WildcardPattern') { // Wildcard always matches } else if (underlyingPattern.type === 'Identifier') { // Identifier patterns always match (they bind the value) underlyingMatch = true; } else if (underlyingPattern.type === 'TypePattern') { const expectedType = underlyingPattern.name; let actualType; if (types.has(discriminantName)) { actualType = types.get(discriminantName); } else { actualType = getRuntimeType(discriminantValue); } underlyingMatch = isTypeAssignable(actualType, expectedType); } else { // For literal patterns, check direct equality const patternValue = visit(underlyingPattern); const discriminantValueForComparison = discriminantValue; if (patternValue && typeof patternValue.value === 'number' && discriminantValueForComparison && typeof discriminantValueForComparison.value === 'number') { underlyingMatch = (patternValue.value === discriminantValueForComparison.value); } else { underlyingMatch = (patternValue === discriminantValueForComparison); } } if (!underlyingMatch) { match = false; break; } // Now evaluate the guard with the discriminant value in scope const originalScope = new Map(scope); // Add discriminant value to scope for guard evaluation // If the underlying pattern is an identifier, use that name if (underlyingPattern.type === 'Identifier') { scope.set(underlyingPattern.name, discriminantValue); } else if (discriminantName) { scope.set(discriminantName, discriminantValue); } try { const guardResult = visit(pattern.guard); if (!guardResult) { match = false; break; } } finally { // Restore original scope scope.clear(); for (const [key, value] of originalScope.entries()) { scope.set(key, value); } } } else if (pattern.type === 'TypePattern') { const expectedType = pattern.name; let actualType; if (types.has(discriminantName)) { actualType = types.get(discriminantName); } else { actualType = getRuntimeType(discriminantValue); } if (!isTypeAssignable(actualType, expectedType)) { match = false; break; } } else if (pattern.type === 'ResultPattern') { if (discriminantValue.variant !== pattern.variant) { match = false; break; } caseScope.set(pattern.identifier.name, discriminantValue.value); } else if (pattern.type === 'ListPattern') { if (!Array.isArray(discriminantValue) || pattern.elements.length !== discriminantValue.length) { match = false; break; } for (let j = 0; j < pattern.elements.length; j++) { const subPattern = pattern.elements[j]; const subDiscriminantValue = discriminantValue[j]; if (subPattern.type === 'WildcardPattern') { continue; } const patternValue = visit(subPattern); // Handle our custom number format for comparison if (patternValue && typeof patternValue.value === 'number' && subDiscriminantValue && typeof subDiscriminantValue.value === 'number') { if (patternValue.value !== subDiscriminantValue.value) { match = false; break; } } else if (patternValue !== subDiscriminantValue) { match = false; break; } } if (!match) break; } else if (pattern.type === 'TablePattern') { if (discriminantValue.type !== 'Object') { match = false; break; } for (const prop of pattern.properties) { if (!discriminantValue.properties.has(prop.key)) { match = false; break; } if (prop.key === '_') { let foundMatchForWildcard = false; for (const [discKey, discValue] of discriminantValue.properties.entries()) { if (prop.value.type === 'WildcardPattern') { foundMatchForWildcard = true; break; } if (visit(prop.value) === discValue) { foundMatchForWildcard = true; break; } } if (!foundMatchForWildcard) { match = false; break; } } else { if (!discriminantValue.properties.has(prop.key)) { match = false; break; } const subDiscriminantValue = discriminantValue.properties.get(prop.key); if (prop.value.type === 'WildcardPattern') { continue; } const propValue = visit(prop.value); // Handle our custom number format for comparison if (propValue && typeof propValue.value === 'number' && subDiscriminantValue && typeof subDiscriminantValue.value === 'number') { if (propValue.value !== subDiscriminantValue.value) { match = false; break; } } else if (propValue !== subDiscriminantValue) { match = false; break; } } } if (!match) break; } else { // Handle literal value comparisons const patternValue = visit(pattern); const discriminantValueForComparison = discriminantValue; // For numeric values, compare the actual values if (patternValue && typeof patternValue.value === 'number' && discriminantValueForComparison && typeof discriminantValueForComparison.value === 'number') { if (patternValue.value !== discriminantValueForComparison.value) { match = false; break; } } else if (patternValue !== discriminantValueForComparison) { match = false; break; } } } if (match) { const originalScope = new Map(scope); for (const [key, value] of caseScope.entries()) { scope.set(key, value); } const result = visit(whenCase.consequent); for (const [key, value] of caseScope.entries()) { scope.delete(key); } return result; } } return undefined; } function visitUnaryExpression(node) { const operand = visit(node.operand); if (node.operator === '-') { const operandValue = operand && typeof operand.value === 'number' ? operand.value : operand; const result = -operandValue; // Preserve the float/int type if (typeof result === 'number') { const operandIsFloat = operand && typeof operand.value === 'number' && operand.isFloat; return { value: result, isFloat: operandIsFloat }; } return result; } throw new Error(`Unknown unary operator: ${node.operator}`); } function visitBinaryExpression(node) { const left = visit(node.left); const right = visit(node.right); // Extract numeric values for arithmetic operations const leftValue = left && typeof left.value === 'number' ? left.value : left; const rightValue = right && typeof right.value === 'number' ? right.value : right; switch (node.operator) { case '+': const result = leftValue + rightValue; // For string concatenation, preserve the string type if (typeof leftValue === 'string' || typeof rightValue === 'string') { return result; } // For numeric addition, preserve the float/int type if (typeof result === 'number') { const leftIsFloat = left && typeof left.value === 'number' && left.isFloat; const rightIsFloat = right && typeof right.value === 'number' && right.isFloat; return { value: result, isFloat: leftIsFloat || rightIsFloat }; } return result; case '-': const subResult = leftValue - rightValue; if (typeof subResult === 'number') { const leftIsFloat = left && typeof left.value === 'number' && left.isFloat; const rightIsFloat = right && typeof right.value === 'number' && right.isFloat; return { value: subResult, isFloat: leftIsFloat || rightIsFloat }; } return subResult; case '*': const mulResult = leftValue * rightValue; if (typeof mulResult === 'number') { const leftIsFloat = left && typeof left.value === 'number' && left.isFloat; const rightIsFloat = right && typeof right.value === 'number' && right.isFloat; return { value: mulResult, isFloat: leftIsFloat || rightIsFloat }; } return mulResult; case '/': if (rightValue === 0) { throw new RuntimeError( 'Division by zero', node.location, host.source || '', ['Check if the divisor is zero before dividing', 'Use conditional logic to handle zero divisors'] ); } const divResult = leftValue / rightValue; if (typeof divResult === 'number') { const leftIsFloat = left && typeof left.value === 'number' && left.isFloat; const rightIsFloat = right && typeof right.value === 'number' && right.isFloat; return { value: divResult, isFloat: leftIsFloat || rightIsFloat }; } return divResult; case '%': const modResult = leftValue % rightValue; if (typeof modResult === 'number') { const leftIsFloat = left && typeof left.value === 'number' && left.isFloat; const rightIsFloat = right && typeof right.value === 'number' && right.isFloat; return { value: modResult, isFloat: leftIsFloat || rightIsFloat }; } return modResult; case '..': // String concatenation using .. operator return String(leftValue) + String(rightValue); case '=': return leftValue === rightValue; case '>': return leftValue > rightValue; case '<': return leftValue < rightValue; case '>=': return leftValue >= rightValue; case '<=': return leftValue <= rightValue; case '!=': return leftValue !== rightValue; case 'and': return Boolean(leftValue) && Boolean(rightValue); case 'or': return Boolean(leftValue) || Boolean(rightValue); case 'xor': return Boolean(leftValue) !== Boolean(rightValue); // XOR for booleans default: throw new Error(`Unknown operator: ${node.operator}`); } } function visitIdentifier(node) { if (scope.has(node.name)) { return scope.get(node.name); } throw ErrorHelpers.undefinedVariable(node.name, host.source || '', node.location); } function visitMemberExpression(node) { const object = visit(node.object); let propertyKey; if (node.property.type === 'Identifier') { propertyKey = node.property.name; } else if (node.property.type === 'NumberLiteral' || node.property.type === 'StringLiteral') { propertyKey = node.property.value; } else if (node.property.type === 'MemberExpression') { // For nested member access like e.data.input, we need to recursively visit // the property to get the intermediate object, then access the final property const intermediateObject = visit(node.property); // The intermediate object is the result of the nested access return intermediateObject; } else { throw new Error(`Unsupported property type for member access: ${node.property.type}`); } // Handle null/undefined objects if (object == null) { return null; } // Handle list element access if (Array.isArray(object) && typeof propertyKey === 'number') { if (propertyKey < 0 || propertyKey >= object.length) { throw new RuntimeError( `Index out of bounds: ${propertyKey}`, node.location, host.source || '', [`Valid indices are 0 to ${object.length - 1}`, 'Check list length before accessing elements'] ); } return object[propertyKey]; } // Handle table property access if (object.type === 'Object' && object.properties.has(propertyKey)) { return object.properties.get(propertyKey); } // Throw error for undefined properties throw ErrorHelpers.undefinedProperty(propertyKey, object, host.source || '', node.location); } function visitResultExpression(node) { return { type: 'Result', variant: node.variant, value: visit(node.value), }; } function visitListLiteral(node) { return node.elements.map(element => visit(element)); } function visitTableLiteral(node) { const properties = new Map(); for (const prop of node.properties) { properties.set(prop.key, visit(prop.value)); } const base = { type: 'Object', properties }; // Expose direct property access for convenience (e.g., result.state) return new Proxy(base, { get(target, prop, receiver) { if (prop in target) { return Reflect.get(target, prop, receiver); } if (typeof prop === 'string' && target.properties && target.properties.has(prop)) { return target.properties.get(prop); } return undefined; }, }); } function visitAnonymousFunction(node) { return { type: 'Function', params: node.params, body: node.body, closure: new Map(scope), }; } function interpret() { return visit(ast); } return { interpret, scope, // Expose scope for testing }; } export { createInterpreter };