diff options
Diffstat (limited to 'js/baba-yaga/src/core/interpreter.js')
-rw-r--r-- | js/baba-yaga/src/core/interpreter.js | 2812 |
1 files changed, 2812 insertions, 0 deletions
diff --git a/js/baba-yaga/src/core/interpreter.js b/js/baba-yaga/src/core/interpreter.js new file mode 100644 index 0000000..5a2de80 --- /dev/null +++ b/js/baba-yaga/src/core/interpreter.js @@ -0,0 +1,2812 @@ + +// 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 '<function>'; + } + + // 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}<function: (${params}) -> ...> (${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 }; |