about summary refs log tree commit diff stats
path: root/js/scripting-lang/lang.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r--js/scripting-lang/lang.js1049
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
+});
+
+