diff options
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r-- | js/scripting-lang/lang.js | 1049 |
1 files changed, 932 insertions, 117 deletions
diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index 4efb582..6bb0858 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -8,18 +8,25 @@ import { parser } from './parser.js'; * Initializes the standard library in the provided scope. * * @param {Object} scope - The global scope object to inject functions into + * @description Injects higher-order functions and combinator functions into the interpreter's global scope. + * These functions provide functional programming utilities and implement the combinator foundation + * that eliminates parsing ambiguity by translating all operations to function calls. * - * @description Injects higher-order functions into the interpreter's global scope. - * These functions provide functional programming utilities like map, compose, pipe, etc. + * The standard library includes: + * - Higher-order functions (map, compose, pipe, apply, filter, reduce, fold, curry) + * - Arithmetic combinators (add, subtract, multiply, divide, modulo, power, negate) + * - Comparison combinators (equals, notEquals, lessThan, greaterThan, lessEqual, greaterEqual) + * - Logical combinators (logicalAnd, logicalOr, logicalXor, logicalNot) + * - Enhanced combinators (identity, constant, flip, on, both, either) * - * @why Injecting the standard library directly into the scope ensures that user code - * can access these functions as if they were built-in, without special syntax or - * reserved keywords. This approach also allows for easy extension and testing, as - * the library is just a set of regular functions in the scope chain. + * This approach ensures that user code can access these functions as if they were built-in, + * without special syntax or reserved keywords. The combinator foundation allows the parser + * to translate all operators to function calls, eliminating ambiguity while preserving syntax. * - * @how Each function is added as a property of the scope object. Functions are written - * to check argument types at runtime, since the language is dynamically typed and - * does not enforce arity or types at parse time. + * Functions are written to check argument types at runtime since the language is dynamically + * typed and does not enforce arity or types at parse time. The combinator functions are + * designed to work seamlessly with the parser's operator translation, providing a consistent + * and extensible foundation for all language operations. */ function initializeStandardLibrary(scope) { /** @@ -28,35 +35,72 @@ function initializeStandardLibrary(scope) { * @param {*} x - Value to apply function to * @returns {*} Result of applying f to x * @throws {Error} When first argument is not a function + * @description The map function is a fundamental higher-order function that + * applies a transformation function to a value. This enables functional + * programming patterns where data transformations are expressed as function + * applications rather than imperative operations. + * + * The function supports partial application: when called with only the function, + * it returns a new function that waits for the value. This enables currying + * patterns and function composition chains. */ scope.map = function(f, x) { - if (typeof f === 'function') { - return f(x); - } else { + if (typeof f !== 'function') { throw new Error('map: first argument must be a function'); } + + if (x === undefined) { + // Partial application: return a function that waits for the second argument + return function(x) { + return f(x); + }; + } + + // Full application: apply the function to the value + return f(x); }; /** - * Compose: Compose two functions (f ∘ g)(x) = f(g(x)) - * @param {Function} f - Outer function - * @param {Function} g - Inner function - * @param {*} [x] - Optional argument to apply composed function to - * @returns {Function|*} Either a composed function or the result of applying it - * @throws {Error} When first two arguments are not functions + * Compose: Compose functions (f ∘ g)(x) = f(g(x)) + * @param {Function} f - First function + * @param {Function} [g] - Second function (optional for partial application) + * @returns {Function} Composed function or partially applied function + * @throws {Error} When first argument is not a function + * @description The compose function is a core functional programming primitive + * that combines two functions into a new function. When used with partial + * application, it enables currying patterns where functions can be built + * incrementally. This supports the 'via' operator in the language syntax + * and enables powerful function composition chains. + * + * The function implements right-associative composition, meaning that + * compose(f, compose(g, h)) creates a function that applies h, then g, then f. + * This matches mathematical function composition notation and enables + * natural reading of composition chains from right to left. */ - scope.compose = function(f, g, x) { - if (typeof f === 'function' && typeof g === 'function') { - if (arguments.length === 3) { - return f(g(x)); - } else { + scope.compose = function(f, g) { + if (typeof f !== 'function') { + throw new Error(`compose: first argument must be a function, got ${typeof f}`); + } + + if (g === undefined) { + // Partial application: return a function that waits for the second argument + return function(g) { + if (typeof g !== 'function') { + throw new Error(`compose: second argument must be a function, got ${typeof g}`); + } return function(x) { return f(g(x)); }; - } - } else { - throw new Error('compose: first two arguments must be functions'); + }; + } + + if (typeof g !== 'function') { + throw new Error(`compose: second argument must be a function, got ${typeof g}`); } + + return function(x) { + return f(g(x)); + }; }; /** @@ -66,13 +110,43 @@ function initializeStandardLibrary(scope) { * @param {*} y - Second argument * @returns {*} Result of applying f to x and y * @throws {Error} When first argument is not a function + * @description The curry function provides a simplified currying mechanism + * that allows functions to be applied to arguments incrementally. When called + * with fewer arguments than the function expects, it returns a new function + * that waits for the remaining arguments. + * + * This function is designed to work with the parser's one-by-one argument + * application system, where multi-argument function calls are translated to + * nested apply calls. The nested partial application checks ensure that + * functions return partially applied functions until all arguments are received. */ scope.curry = function(f, x, y) { - if (typeof f === 'function') { - return f(x, y); - } else { + if (typeof f !== 'function') { throw new Error('curry: first argument must be a function'); } + + if (x === undefined) { + // Partial application: return a function that waits for the remaining arguments + return function(x, y) { + if (y === undefined) { + // Still partial application + return function(y) { + return f(x, y); + }; + } + return f(x, y); + }; + } + + if (y === undefined) { + // Partial application: return a function that waits for the last argument + return function(y) { + return f(x, y); + }; + } + + // Full application: apply the function to all arguments + return f(x, y); }; /** @@ -81,35 +155,75 @@ function initializeStandardLibrary(scope) { * @param {*} x - Argument to apply function to * @returns {*} Result of applying f to x * @throws {Error} When first argument is not a function + * @description The apply function is the fundamental mechanism for function + * application in the language. It enables the juxtaposition-based function + * application syntax (f x) by providing an explicit function application + * primitive. This function is called by the parser whenever function + * application is detected, ensuring consistent semantics across all + * function calls. + * + * The function supports partial application: when called with only the function, + * it returns a new function that waits for the argument. This enables the + * parser to build function application chains incrementally, supporting + * both immediate evaluation and deferred execution patterns. */ scope.apply = function(f, x) { - if (typeof f === 'function') { - return f(x); - } else { + if (typeof f !== 'function') { throw new Error('apply: first argument must be a function'); } + + if (x === undefined) { + // Partial application: return a function that waits for the second argument + return function(x) { + return f(x); + }; + } + + // Full application: apply the function to the argument + return f(x); }; /** * Pipe: Compose functions in left-to-right order (opposite of compose) * @param {Function} f - First function - * @param {Function} g - Second function - * @param {*} [x] - Optional argument to apply piped function to - * @returns {Function|*} Either a piped function or the result of applying it - * @throws {Error} When first two arguments are not functions + * @param {Function} [g] - Second function (optional for partial application) + * @returns {Function} Function that applies the functions in left-to-right order + * @throws {Error} When first argument is not a function + * @description The pipe function provides an alternative to compose that + * applies functions in left-to-right order, which is often more intuitive + * for data processing pipelines. Like compose, it supports partial application + * for currying patterns. This enables functional programming patterns where + * data flows through a series of transformations in a natural reading order. + * + * The function implements left-associative composition, meaning that + * pipe(f, pipe(g, h)) creates a function that applies f, then g, then h. + * This is the opposite of compose and matches the natural reading order + * for data transformation pipelines. */ - scope.pipe = function(f, g, x) { - if (typeof f === 'function' && typeof g === 'function') { - if (arguments.length === 3) { - return g(f(x)); - } else { + scope.pipe = function(f, g) { + if (typeof f !== 'function') { + throw new Error(`pipe: first argument must be a function, got ${typeof f}`); + } + + if (g === undefined) { + // Partial application: return a function that waits for the second argument + return function(g) { + if (typeof g !== 'function') { + throw new Error(`pipe: second argument must be a function, got ${typeof g}`); + } return function(x) { return g(f(x)); }; - } - } else { - throw new Error('pipe: first two arguments must be functions'); + }; + } + + if (typeof g !== 'function') { + throw new Error(`pipe: second argument must be a function, got ${typeof g}`); } + + return function(x) { + return g(f(x)); + }; }; /** @@ -120,11 +234,19 @@ function initializeStandardLibrary(scope) { * @throws {Error} When first argument is not a function */ scope.filter = function(p, x) { - if (typeof p === 'function') { - return p(x) ? x : 0; - } else { + if (typeof p !== 'function') { throw new Error('filter: first argument must be a function'); } + + if (x === undefined) { + // Partial application: return a function that waits for the second argument + return function(x) { + return p(x) ? x : 0; + }; + } + + // Full application: apply the predicate and return result + return p(x) ? x : 0; }; /** @@ -134,13 +256,55 @@ function initializeStandardLibrary(scope) { * @param {*} x - Second value * @returns {*} Result of applying f to init and x * @throws {Error} When first argument is not a function + * @description The reduce function applies a binary function to an initial value + * and a second value, returning the result. This is a simplified version of + * traditional reduce that works with pairs of values rather than collections. + * + * The function supports partial application with nested checks to handle the + * parser's one-by-one argument application system. When called with only the + * function, it returns a function that waits for the initial value. When called + * with the function and initial value, it returns a function that waits for + * the second value. This enables currying patterns and incremental function + * application. */ scope.reduce = function(f, init, x) { - if (typeof f === 'function') { - return f(init, x); - } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] reduce: f =`, typeof f, f); + console.log(`[DEBUG] reduce: init =`, init); + console.log(`[DEBUG] reduce: x =`, x); + } + + if (typeof f !== 'function') { throw new Error('reduce: first argument must be a function'); } + + if (init === undefined) { + // Partial application: return a function that waits for the remaining arguments + return function(init, x) { + if (process.env.DEBUG) { + console.log(`[DEBUG] reduce returned function: f =`, typeof f, f); + console.log(`[DEBUG] reduce returned function: init =`, init); + console.log(`[DEBUG] reduce returned function: x =`, x); + } + if (x === undefined) { + // Still partial application + return function(x) { + return f(init, x); + }; + } + return f(init, x); + }; + } + + if (x === undefined) { + // Partial application: return a function that waits for the last argument + return function(x) { + return f(init, x); + }; + } + + // Full application: apply the function to all arguments + return f(init, x); }; /** @@ -152,11 +316,303 @@ function initializeStandardLibrary(scope) { * @throws {Error} When first argument is not a function */ scope.fold = function(f, init, x) { - if (typeof f === 'function') { - return f(init, x); - } else { + if (typeof f !== 'function') { throw new Error('fold: first argument must be a function'); } + + if (init === undefined) { + // Partial application: return a function that waits for the remaining arguments + return function(init, x) { + if (x === undefined) { + // Still partial application + return function(x) { + return f(init, x); + }; + } + return f(init, x); + }; + } + + if (x === undefined) { + // Partial application: return a function that waits for the last argument + return function(x) { + return f(init, x); + }; + } + + // Full application: apply the function to all arguments + return f(init, x); + }; + + // ===== ARITHMETIC COMBINATORS ===== + + /** + * Add: Add two numbers + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Sum of x and y + */ + scope.add = function(x, y) { + return x + y; + }; + + /** + * Subtract: Subtract second number from first + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Difference of x and y + */ + scope.subtract = function(x, y) { + return x - y; + }; + + /** + * Multiply: Multiply two numbers + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Product of x and y + */ + scope.multiply = function(x, y) { + return x * y; + }; + + /** + * Divide: Divide first number by second + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Quotient of x and y + * @throws {Error} When second argument is zero + */ + scope.divide = function(x, y) { + if (y === 0) { + throw new Error('Division by zero'); + } + return x / y; + }; + + /** + * Modulo: Get remainder of division + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Remainder of x divided by y + */ + scope.modulo = function(x, y) { + return x % y; + }; + + /** + * Power: Raise first number to power of second + * @param {number} x - Base number + * @param {number} y - Exponent + * @returns {number} x raised to the power of y + */ + scope.power = function(x, y) { + return Math.pow(x, y); + }; + + /** + * Negate: Negate a number + * @param {number} x - Number to negate + * @returns {number} Negated value of x + */ + scope.negate = function(x) { + return -x; + }; + + // ===== COMPARISON COMBINATORS ===== + + /** + * Equals: Check if two values are equal + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x equals y + */ + scope.equals = function(x, y) { + return x === y; + }; + + /** + * NotEquals: Check if two values are not equal + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x does not equal y + */ + scope.notEquals = function(x, y) { + return x !== y; + }; + + /** + * LessThan: Check if first value is less than second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x < y + */ + scope.lessThan = function(x, y) { + return x < y; + }; + + /** + * GreaterThan: Check if first value is greater than second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x > y + */ + scope.greaterThan = function(x, y) { + return x > y; + }; + + /** + * LessEqual: Check if first value is less than or equal to second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x <= y + */ + scope.lessEqual = function(x, y) { + return x <= y; + }; + + /** + * GreaterEqual: Check if first value is greater than or equal to second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x >= y + */ + scope.greaterEqual = function(x, y) { + return x >= y; + }; + + // ===== LOGICAL COMBINATORS ===== + + /** + * LogicalAnd: Logical AND of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if both x and y are truthy + */ + scope.logicalAnd = function(x, y) { + return !!(x && y); + }; + + /** + * LogicalOr: Logical OR of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if either x or y is truthy + */ + scope.logicalOr = function(x, y) { + return !!(x || y); + }; + + /** + * LogicalXor: Logical XOR of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if exactly one of x or y is truthy + */ + scope.logicalXor = function(x, y) { + return !!((x && !y) || (!x && y)); + }; + + /** + * LogicalNot: Logical NOT of a value + * @param {*} x - Value to negate + * @returns {boolean} True if x is falsy, false if x is truthy + */ + scope.logicalNot = function(x) { + return !x; + }; + + // ===== ASSIGNMENT COMBINATOR ===== + + /** + * Assign: Assign a value to a variable name + * @param {string} name - Variable name + * @param {*} value - Value to assign + * @returns {*} The assigned value + * @throws {Error} When trying to reassign an immutable variable + * @note This function needs access to the global scope, so it will be + * set up during interpreter initialization + */ + // Note: assign will be set up in the interpreter with access to globalScope + + // ===== ENHANCED HIGHER-ORDER COMBINATORS ===== + + /** + * Identity: Return the input unchanged + * @param {*} x - Any value + * @returns {*} The same value + */ + scope.identity = function(x) { + return x; + }; + + /** + * Constant: Create a function that always returns the same value + * @param {*} x - Value to return + * @param {*} [y] - Optional second argument (ignored) + * @returns {*} The value x, or a function if only one argument provided + */ + scope.constant = function(x, y) { + if (arguments.length === 2) { + return x; + } else { + return function(y) { + return x; + }; + } + }; + + /** + * Flip: Flip the order of arguments for a binary function + * @param {Function} f - Binary function + * @param {*} [x] - Optional first argument + * @param {*} [y] - Optional second argument + * @returns {Function|*} Function with flipped argument order, or result if arguments provided + */ + scope.flip = function(f, x, y) { + if (arguments.length === 3) { + return f(y, x); + } else { + return function(x, y) { + return f(y, x); + }; + } + }; + + /** + * On: Apply a function to the results of another function + * @param {Function} f - Outer function + * @param {Function} g - Inner function + * @returns {Function} Function that applies f to the results of g + */ + scope.on = function(f, g) { + return function(x, y) { + return f(g(x), g(y)); + }; + }; + + /** + * Both: Check if both predicates are true + * @param {Function} f - First predicate + * @param {Function} g - Second predicate + * @returns {Function} Function that returns true if both predicates are true + */ + scope.both = function(f, g) { + return function(x) { + return f(x) && g(x); + }; + }; + + /** + * Either: Check if either predicate is true + * @param {Function} f - First predicate + * @param {Function} g - Second predicate + * @returns {Function} Function that returns true if either predicate is true + */ + scope.either = function(f, g) { + return function(x) { + return f(x) || g(x); + }; }; } @@ -171,23 +627,41 @@ function initializeStandardLibrary(scope) { * corresponding operations. Manages scope, handles function calls, and supports * both synchronous and asynchronous operations. * - * @how Uses a global scope for variable storage and function definitions. Each - * function call creates a new scope (using prototypal inheritance) to implement + * The interpreter implements a combinator-based architecture where all operations + * are translated to function calls to standard library combinators. This eliminates + * parsing ambiguity while preserving the original syntax. The parser generates + * FunctionCall nodes for operators (e.g., x + y becomes add(x, y)), and the + * interpreter executes these calls using the combinator functions in the global scope. + * + * The interpreter uses a global scope for variable storage and function definitions. + * Each function call creates a new scope (using prototypal inheritance) to implement * lexical scoping. Immutability is enforced by preventing reassignment in the * global scope. * - * @why This approach allows for first-class functions, closures, and lexical - * scoping, while keeping the implementation simple. The interpreter supports - * both synchronous and asynchronous IO operations, returning Promises when necessary. - * - * @note The interpreter is split into three functions: evalNode (global), + * The interpreter is split into three functions: evalNode (global), * localEvalNodeWithScope (for function bodies), and localEvalNode (for internal * recursion). This separation allows for correct scope handling and easier debugging. + * + * Recursive function support is implemented using a forward declaration pattern: + * a placeholder function is created in the global scope before evaluation, allowing + * the function body to reference itself during evaluation. + * + * The combinator foundation ensures that all operations are executed through + * function calls, providing a consistent and extensible execution model. This + * approach enables powerful abstractions and eliminates the need for special + * handling of different operator types in the interpreter. */ function interpreter(ast) { const globalScope = {}; initializeStandardLibrary(globalScope); + // Debug: Check if combinators are available + if (process.env.DEBUG) { + console.log('[DEBUG] Available functions in global scope:', Object.keys(globalScope)); + console.log('[DEBUG] add function exists:', typeof globalScope.add === 'function'); + console.log('[DEBUG] subtract function exists:', typeof globalScope.subtract === 'function'); + } + // Reset call stack tracker at the start of interpretation callStackTracker.reset(); @@ -199,7 +673,22 @@ function interpreter(ast) { * @throws {Error} For evaluation errors * * @description Main evaluation function that handles all node types in the - * global scope context. + * global scope context. This function processes the core language constructs + * and delegates to combinator functions for all operations. + * + * The function implements the forward declaration pattern for recursive functions: + * when a function assignment is detected, a placeholder is created in the global + * scope before evaluation, allowing the function body to reference itself. + * + * This function is the primary entry point for AST evaluation and handles + * all the core language constructs including literals, operators (translated + * to combinator calls), function definitions, and control structures. It + * ensures that all operations are executed through the combinator foundation, + * providing consistent semantics across the language. + * + * The function processes legacy operator expressions (PlusExpression, MinusExpression, etc.) + * for backward compatibility, but the parser now generates FunctionCall nodes for + * all operators, which are handled by the standard library combinator functions. */ function evalNode(node) { callStackTracker.push('evalNode', node?.type || 'unknown'); @@ -303,16 +792,58 @@ function interpreter(ast) { return tableValue[keyValue]; case 'AssignmentExpression': + // Prevent reassignment of standard library functions if (globalScope.hasOwnProperty(node.name)) { throw new Error(`Cannot reassign immutable variable: ${node.name}`); } + + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; + + // Store the placeholder in global scope + globalScope[node.name] = placeholder; + + // Now evaluate the function definition with access to the placeholder + const actualFunction = evalNode(node.value); + + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; + } + const value = evalNode(node.value); globalScope[node.name] = value; return; case 'Assignment': + // Prevent reassignment of standard library functions if (globalScope.hasOwnProperty(node.identifier)) { throw new Error(`Cannot reassign immutable variable: ${node.identifier}`); } + + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.identifier} is not yet fully defined`); + }; + + // Store the placeholder in global scope + globalScope[node.identifier] = placeholder; + + // Now evaluate the function definition with access to the placeholder + const actualFunction = evalNode(node.value); + + // Replace the placeholder with the actual function + globalScope[node.identifier] = actualFunction; + return; + } + const assignmentValue = evalNode(node.value); globalScope[node.identifier] = assignmentValue; return; @@ -329,11 +860,37 @@ function interpreter(ast) { return function(...args) { callStackTracker.push('FunctionCall', node.params.join(',')); try { - let localScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - localScope[node.params[i]] = args[i]; + // If we have fewer arguments than parameters, return a curried function + if (args.length < node.params.length) { + return function(...moreArgs) { + const allArgs = [...args, ...moreArgs]; + if (allArgs.length < node.params.length) { + // Still not enough arguments, curry again + return function(...evenMoreArgs) { + const finalArgs = [...allArgs, ...evenMoreArgs]; + let localScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + localScope[node.params[i]] = finalArgs[i]; + } + return localEvalNodeWithScope(node.body, localScope); + }; + } else { + // We have enough arguments now + let localScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + localScope[node.params[i]] = allArgs[i]; + } + return localEvalNodeWithScope(node.body, localScope); + } + }; + } else { + // We have enough arguments, evaluate the function + let localScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + localScope[node.params[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, localScope); } - return localEvalNodeWithScope(node.body, localScope); } finally { callStackTracker.pop(); } @@ -353,37 +910,115 @@ function interpreter(ast) { } }; case 'FunctionCall': - let funcToCall; // Renamed from 'func' to avoid redeclaration + let funcToCall; if (typeof node.name === 'string') { // Regular function call with string name funcToCall = globalScope[node.name]; + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: looking up function '${node.name}' in globalScope, found:`, typeof funcToCall); + } } else if (node.name.type === 'Identifier') { // Function call with identifier funcToCall = globalScope[node.name.value]; - } else if (node.name.type === 'TableAccess') { - // Function call from table access (e.g., math.add) - funcToCall = evalNode(node.name); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: looking up function '${node.name.value}' in globalScope, found:`, typeof funcToCall); + } } else { - throw new Error('Invalid function name in function call'); + // Function call from expression (e.g., parenthesized function, higher-order) + funcToCall = evalNode(node.name); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: evaluated function expression, found:`, typeof funcToCall); + } } if (funcToCall instanceof Function) { let args = node.args.map(evalNode); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: calling function with args:`, args); + } return funcToCall(...args); } throw new Error(`Function is not defined or is not callable`); case 'WhenExpression': - const whenValue = evalNode(node.value); + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(evalNode) + : [evalNode(node.value)]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: whenValues =`, whenValues); + } for (const caseItem of node.cases) { - const pattern = evalNode(caseItem.pattern[0]); + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(evalNode); - // Check if pattern matches the value - let matches = false; - if (pattern === true) { // Wildcard pattern - matches = true; - } else if (whenValue === pattern) { - matches = true; + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: patterns =`, patterns); + } + + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { + matches = false; + } else { + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: comparing value ${value} with pattern ${pattern}`); + } + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: wildcard matches`); + } + continue; + } else if (typeof pattern === 'object' && pattern.type === 'FunctionCall') { + // This is a boolean expression pattern (e.g., x < 0) + // We need to substitute the current value for the pattern variable + // For now, let's assume the pattern variable is the first identifier in the function call + let patternToEvaluate = pattern; + if (pattern.args && pattern.args.length > 0 && pattern.args[0].type === 'Identifier') { + // Create a copy of the pattern with the current value substituted + patternToEvaluate = { + ...pattern, + args: [value, ...pattern.args.slice(1)] + }; + } + const patternResult = evalNode(patternToEvaluate); + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: boolean pattern result = ${patternResult}`); + } + if (!patternResult) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: boolean pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: boolean pattern matches`); + } + } + } else if (value !== pattern) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: pattern matches`); + } + } + } + } + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: case matches = ${matches}`); } if (matches) { @@ -423,6 +1058,9 @@ function interpreter(ast) { return assertionValue; case 'FunctionReference': const functionValue = globalScope[node.name]; + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionReference: looking up '${node.name}' in globalScope, found:`, typeof functionValue); + } if (functionValue === undefined) { throw new Error(`Function ${node.name} is not defined`); } @@ -450,7 +1088,21 @@ function interpreter(ast) { * @throws {Error} For evaluation errors * * @description Used for evaluating function bodies and other expressions - * that need access to both local and global variables. + * that need access to both local and global variables. This function implements + * lexical scoping by creating a local scope that prototypally inherits from + * the global scope, allowing access to both local parameters and global functions. + * + * The function handles the same node types as evalNode but uses the local scope + * for variable lookups. It also implements the forward declaration pattern for + * recursive functions, ensuring that function definitions can reference themselves + * during evaluation. + * + * This separation of global and local evaluation allows for proper scope management + * and prevents variable name conflicts between function parameters and global variables. + * + * The function prioritizes local scope lookups over global scope lookups, ensuring + * that function parameters shadow global variables with the same names. This + * implements proper lexical scoping semantics. */ const localEvalNodeWithScope = (node, scope) => { callStackTracker.push('localEvalNodeWithScope', node?.type || 'unknown'); @@ -554,9 +1206,30 @@ function interpreter(ast) { return tableValue[keyValue]; case 'AssignmentExpression': + // Prevent reassignment of standard library functions if (globalScope.hasOwnProperty(node.name)) { throw new Error(`Cannot reassign immutable variable: ${node.name}`); } + + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; + + // Store the placeholder in global scope + globalScope[node.name] = placeholder; + + // Now evaluate the function definition with access to the placeholder + const actualFunction = localEvalNodeWithScope(node.value, scope); + + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; + } + globalScope[node.name] = localEvalNodeWithScope(node.value, scope); return; case 'Identifier': @@ -607,11 +1280,9 @@ function interpreter(ast) { } else if (node.name.type === 'Identifier') { // Function call with identifier localFunc = globalScope[node.name.value]; - } else if (node.name.type === 'TableAccess') { - // Function call from table access (e.g., math.add) - localFunc = localEvalNodeWithScope(node.name, scope); } else { - throw new Error('Invalid function name in function call'); + // Function call from expression (e.g., parenthesized function, higher-order) + localFunc = localEvalNodeWithScope(node.name, scope); } if (localFunc instanceof Function) { @@ -620,17 +1291,58 @@ function interpreter(ast) { } throw new Error(`Function is not defined or is not callable`); case 'WhenExpression': - const whenValue = localEvalNodeWithScope(node.value, scope); + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(val => localEvalNodeWithScope(val, scope)) + : [localEvalNodeWithScope(node.value, scope)]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: whenValues =`, whenValues); + } for (const caseItem of node.cases) { - const pattern = localEvalNodeWithScope(caseItem.pattern[0], scope); + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope)); + + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: patterns =`, patterns); + } + + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { + matches = false; + } else { + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: comparing value ${value} with pattern ${pattern}`); + } + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: wildcard matches`); + } + continue; + } else if (value !== pattern) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern matches`); + } + } + } + } - // Check if pattern matches the value - let matches = false; - if (pattern === true) { // Wildcard pattern - matches = true; - } else if (whenValue === pattern) { - matches = true; + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: case matches = ${matches}`); } if (matches) { @@ -696,7 +1408,22 @@ function interpreter(ast) { * @throws {Error} For evaluation errors * * @description Internal helper function for recursive evaluation that - * always uses the global scope. Used to avoid circular dependencies. + * always uses the global scope. This function is used to avoid circular + * dependencies and infinite recursion when evaluating nested expressions + * that need access to the global scope. + * + * This function duplicates the logic of evalNode but without the scope + * parameter, ensuring that all variable lookups go through the global scope. + * It's primarily used for evaluating function bodies and other expressions + * that need to be isolated from local scope contexts. + * + * The function also implements the forward declaration pattern for recursive + * functions, maintaining consistency with the other evaluation functions. + * + * This function is essential for preventing scope pollution when evaluating + * nested expressions that should not inherit local scope variables, ensuring + * that global functions and variables are always accessible regardless of + * the current evaluation context. */ const localEvalNode = (node) => { callStackTracker.push('localEvalNode', node?.type || 'unknown'); @@ -800,9 +1527,30 @@ function interpreter(ast) { return tableValue[keyValue]; case 'AssignmentExpression': + // Prevent reassignment of standard library functions if (globalScope.hasOwnProperty(node.name)) { throw new Error(`Cannot reassign immutable variable: ${node.name}`); } + + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; + + // Store the placeholder in global scope + globalScope[node.name] = placeholder; + + // Now evaluate the function definition with access to the placeholder + const actualFunction = localEvalNode(node.value); + + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; + } + globalScope[node.name] = localEvalNode(node.value); return; case 'Identifier': @@ -849,11 +1597,9 @@ function interpreter(ast) { } else if (node.name.type === 'Identifier') { // Function call with identifier localFunc = globalScope[node.name.value]; - } else if (node.name.type === 'TableAccess') { - // Function call from table access (e.g., math.add) - localFunc = localEvalNode(node.name); } else { - throw new Error('Invalid function name in function call'); + // Function call from expression (e.g., parenthesized function, higher-order) + localFunc = localEvalNode(node.name); } if (localFunc instanceof Function) { @@ -862,17 +1608,32 @@ function interpreter(ast) { } throw new Error(`Function is not defined or is not callable`); case 'WhenExpression': - const whenValue = localEvalNode(node.value); + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(localEvalNode) + : [localEvalNode(node.value)]; for (const caseItem of node.cases) { - const pattern = localEvalNode(caseItem.pattern[0]); + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(localEvalNode); - // Check if pattern matches the value - let matches = false; - if (pattern === true) { // Wildcard pattern - matches = true; - } else if (whenValue === pattern) { - matches = true; + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { + matches = false; + } else { + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + continue; + } else if (value !== pattern) { + matches = false; + break; + } + } } if (matches) { @@ -955,10 +1716,18 @@ function interpreter(ast) { * @description Logs debug messages to console when DEBUG environment variable is set. * Provides verbose output during development while remaining silent in production. * - * @why Debug functions are gated by the DEBUG environment variable, allowing for + * Debug functions are gated by the DEBUG environment variable, allowing for * verbose output during development and silent operation in production. This * approach makes it easy to trace execution and diagnose issues without * cluttering normal output. + * + * This function is essential for debugging the combinator-based architecture, + * allowing developers to trace how operators are translated to function calls + * and how the interpreter executes these calls through the standard library. + * + * The function is designed to be lightweight and safe to call frequently, + * making it suitable for tracing execution flow through complex nested + * expressions and function applications. */ function debugLog(message, data = null) { if (process.env.DEBUG) { @@ -978,10 +1747,14 @@ function debugLog(message, data = null) { * @description Logs debug error messages to console when DEBUG environment variable is set. * Provides verbose error output during development while remaining silent in production. * - * @why Debug functions are gated by the DEBUG environment variable, allowing for + * Debug functions are gated by the DEBUG environment variable, allowing for * verbose output during development and silent operation in production. This * approach makes it easy to trace execution and diagnose issues without * cluttering normal output. + * + * This function is particularly useful for debugging parsing and evaluation errors, + * providing detailed context about where and why errors occur in the language + * execution pipeline. */ function debugError(message, error = null) { if (process.env.DEBUG) { @@ -996,7 +1769,22 @@ function debugError(message, error = null) { * Call stack tracking for debugging recursion issues. * * @description Tracks function calls to help identify infinite recursion - * and deep call stacks that cause stack overflow errors. + * and deep call stacks that cause stack overflow errors. This is essential + * for debugging the interpreter's recursive evaluation of AST nodes. + * + * The tracker maintains a stack of function calls with timestamps and context + * information, counts function calls to identify hot paths, and detects + * potential infinite recursion by monitoring stack depth. + * + * This tool is particularly important for the combinator-based architecture + * where function calls are the primary execution mechanism, and complex + * nested expressions can lead to deep call stacks. The tracker helps identify + * when the combinator translation creates unexpectedly deep call chains, + * enabling optimization of the function composition and application patterns. + * + * The tracker provides detailed statistics about function call patterns, + * helping developers understand the execution characteristics of their code + * and identify potential performance bottlenecks in the combinator evaluation. */ const callStackTracker = { stack: [], @@ -1077,11 +1865,21 @@ const callStackTracker = { * Cross-platform file I/O utility * * @param {string} filePath - Path to the file to read - * @returns {string} File contents + * @returns {Promise<string>} File contents as a string * @throws {Error} For file reading errors * * @description Handles file reading across different platforms (Node.js, Bun, browser) - * with appropriate fallbacks for each environment. + * with appropriate fallbacks for each environment. This function is essential for + * the language's file execution model where scripts are loaded from .txt files. + * + * The function prioritizes ES modules compatibility by using dynamic import, + * but falls back to require for older Node.js versions. Browser environments + * are not supported for file I/O operations. + * + * This cross-platform approach ensures the language can run in various JavaScript + * environments while maintaining consistent behavior. The file reading capability + * enables the language to execute scripts from files, supporting the development + * workflow where tests and examples are stored as .txt files. */ async function readFile(filePath) { // Check if we're in a browser environment @@ -1106,17 +1904,30 @@ async function readFile(filePath) { * Reads a file, tokenizes, parses, and interprets it. * * @param {string} filePath - Path to the file to execute + * @returns {Promise<*>} The result of executing the file * @throws {Error} For file reading, parsing, or execution errors * * @description Main entry point for file execution. Handles the complete language * pipeline: file reading, lexical analysis, parsing, and interpretation. * - * @why This function is the entry point for file execution, handling all stages - * of the language pipeline. Debug output is provided at each stage for - * transparency and troubleshooting. + * This function orchestrates the entire language execution process: + * 1. Reads the source file using cross-platform I/O utilities + * 2. Tokenizes the source code using the lexer + * 3. Parses tokens into an AST using the combinator-based parser + * 4. Interprets the AST using the combinator-based interpreter * - * @note Supports both synchronous and asynchronous execution, with proper - * error handling and process exit codes. + * The function provides comprehensive error handling and debug output at each + * stage for transparency and troubleshooting. It also manages the call stack + * tracker to provide execution statistics and detect potential issues. + * + * Supports both synchronous and asynchronous execution, with proper + * error handling and process exit codes. This function demonstrates the + * complete combinator-based architecture in action, showing how source code + * is transformed through each stage of the language pipeline. + * + * The function enforces the .txt file extension requirement and provides + * detailed error reporting with call stack statistics to help developers + * understand execution behavior and diagnose issues. */ async function executeFile(filePath) { try { @@ -1183,10 +1994,12 @@ async function executeFile(filePath) { * @description Processes command line arguments and executes the specified file. * Provides helpful error messages for incorrect usage. * - * @why The language is designed for file execution only (no REPL), so the CLI + * The language is designed for file execution only (no REPL), so the CLI * enforces this usage and provides helpful error messages for incorrect invocation. + * The function validates that exactly one file path is provided and that the + * file has the correct .txt extension. * - * @note Exits with appropriate error codes for different failure scenarios. + * Exits with appropriate error codes for different failure scenarios. */ async function main() { const args = process.argv.slice(2); @@ -1211,4 +2024,6 @@ async function main() { main().catch(error => { console.error('Fatal error:', error.message); process.exit(1); -}); \ No newline at end of file +}); + + |