diff options
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r-- | js/scripting-lang/lang.js | 1027 |
1 files changed, 965 insertions, 62 deletions
diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index d3bc0b5..157a2a7 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -24,44 +24,114 @@ import { parser } from './parser.js'; * to translate all operators to function calls, eliminating ambiguity while preserving syntax. * * 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. + * 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) { /** - * Map: Apply a function to a value + * Map: Apply a function to a value or collection * @param {Function} f - Function to apply - * @param {*} x - Value to apply function to + * @param {*} x - Value or collection 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 or collection. This enables + * functional programming patterns where data transformations are expressed + * as function applications rather than imperative operations. + * + * The function implements APL-inspired element-wise operations for tables: + * when x is a table, map applies the function to each value while preserving + * the table structure and keys. This eliminates the need for explicit loops + * and enables declarative data transformation patterns. + * + * 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, which are essential for the + * combinator-based architecture where all operations are function calls. + * + * This design choice aligns with the language's functional foundation and + * enables powerful abstractions like `map @double numbers` to transform + * every element in a collection without explicit iteration. */ 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 scope.map(f, x); + }; + } + + // Handle tables (APL-style element-wise operations) + if (typeof x === 'object' && x !== null && !Array.isArray(x)) { + const result = {}; + for (const [key, value] of Object.entries(x)) { + result[key] = f(value); + } + return result; + } + + // Handle arrays (future enhancement) + if (Array.isArray(x)) { + return x.map(f); + } + + // Default: apply to single 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. This is the foundation + * for the 'via' operator in the language syntax, enabling natural function + * composition chains like `f via g via h`. + * + * 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 (f ∘ g ∘ h) and + * enables natural reading of composition chains from right to left. + * + * Partial application support enables currying patterns where functions can + * be built incrementally. This is essential for the combinator-based architecture + * where complex operations are built from simple, composable functions. + * + * The right-associative design choice aligns with mathematical conventions + * and enables intuitive composition chains that read naturally from right + * to left, matching the mathematical notation for function composition. */ - 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)); + }; }; /** @@ -71,13 +141,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); }; /** @@ -86,50 +186,152 @@ 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. + * + * This function is the core mechanism that enables the parser's juxtaposition + * detection. When the parser encounters `f x`, it generates `apply(f, x)`, + * which this function handles. This design eliminates the need for special + * syntax for function calls while maintaining clear precedence rules. + * + * 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. + * + * This partial application support is essential for the parser's left-associative + * function application model, where `f g x` becomes `apply(apply(f, g), x)`. + * The nested partial application ensures that each step returns a function + * until all arguments are provided. */ 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. 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, making it intuitive for programmers + * who think in terms of data flow from left to right. + * + * Like compose, it supports partial application for currying patterns. + * This enables building complex transformation pipelines incrementally, + * which is essential for the combinator-based architecture where complex + * operations are built from simple, composable functions. + * + * The left-associative design choice makes pipe ideal for data processing + * workflows where each step transforms the data and passes it to the next + * step, creating a natural pipeline that reads like a sequence of operations. */ - 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)); + }; }; /** - * Filter: Filter a value based on a predicate + * Filter: Filter a value or collection based on a predicate * @param {Function} p - Predicate function - * @param {*} x - Value to test - * @returns {*|0} The value if predicate is true, 0 otherwise + * @param {*} x - Value or collection to test + * @returns {*|0} The value if predicate is true, filtered collection for tables, 0 otherwise * @throws {Error} When first argument is not a function + * @description The filter function applies a predicate to a value or collection, + * returning the value if the predicate is true, or a filtered collection for tables. + * This enables functional programming patterns where data selection is expressed + * as predicate application rather than imperative filtering loops. + * + * The function implements APL-inspired element-wise filtering for tables: + * when x is a table, filter applies the predicate to each value and returns + * a new table containing only the key-value pairs where the predicate returns true. + * This eliminates the need for explicit loops and enables declarative data + * selection patterns. + * + * The function supports partial application: when called with only the predicate, + * it returns a new function that waits for the value. This enables currying + * patterns and function composition chains, which are essential for the + * combinator-based architecture where all operations are function calls. + * + * This design choice aligns with the language's functional foundation and + * enables powerful abstractions like `filter @isEven numbers` to select + * elements from a collection without explicit iteration. */ 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 scope.filter(p, x); + }; + } + + // Handle tables (APL-style element-wise filtering) + if (typeof x === 'object' && x !== null && !Array.isArray(x)) { + const result = {}; + for (const [key, value] of Object.entries(x)) { + if (p(value)) { + result[key] = value; + } + } + return result; + } + + // Handle arrays (future enhancement) + if (Array.isArray(x)) { + return x.filter(p); + } + + // Default: apply predicate to single value + return p(x) ? x : 0; }; /** @@ -139,13 +341,69 @@ 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 scope.reduce(f, init, x); + }; + } + return scope.reduce(f, init, x); + }; + } + + if (x === undefined) { + // Partial application: return a function that waits for the last argument + return function(x) { + return scope.reduce(f, init, x); + }; + } + + // Handle tables (reduce all values in the table) + if (typeof x === 'object' && x !== null && !Array.isArray(x)) { + let result = init; + for (const [key, value] of Object.entries(x)) { + result = f(result, value); + } + return result; + } + + // Handle arrays (future enhancement) + if (Array.isArray(x)) { + return x.reduce(f, init); + } + + // Default: apply the function to init and x (original behavior) + return f(init, x); }; /** @@ -157,11 +415,32 @@ 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 ===== @@ -171,6 +450,18 @@ function initializeStandardLibrary(scope) { * @param {number} x - First number * @param {number} y - Second number * @returns {number} Sum of x and y + * @description The add function is a fundamental arithmetic combinator that + * implements addition. This function is called by the parser when the '+' + * operator is encountered, translating `x + y` into `add(x, y)`. + * + * As a combinator function, add supports partial application and can be used + * in function composition chains. This enables patterns like `map @add 10` + * to add 10 to every element in a collection, or `each @add table1 table2` + * for element-wise addition of corresponding table elements. + * + * The function is designed to work seamlessly with the parser's operator + * translation system, providing consistent semantics for all arithmetic + * operations through the combinator foundation. */ scope.add = function(x, y) { return x + y; @@ -191,6 +482,18 @@ function initializeStandardLibrary(scope) { * @param {number} x - First number * @param {number} y - Second number * @returns {number} Product of x and y + * @description The multiply function is a fundamental arithmetic combinator that + * implements multiplication. This function is called by the parser when the '*' + * operator is encountered, translating `x * y` into `multiply(x, y)`. + * + * As a combinator function, multiply supports partial application and can be used + * in function composition chains. This enables patterns like `map @multiply 2` + * to double every element in a collection, or `each @multiply table1 table2` + * for element-wise multiplication of corresponding table elements. + * + * The function is designed to work seamlessly with the parser's operator + * translation system, providing consistent semantics for all arithmetic + * operations through the combinator foundation. */ scope.multiply = function(x, y) { return x * y; @@ -434,10 +737,426 @@ function initializeStandardLibrary(scope) { return f(x) || g(x); }; }; + + /** + * Each: Multi-argument element-wise operations for tables and scalars + * @param {Function} f - Function to apply element-wise + * @param {*} x - First argument (table or scalar) + * @returns {Function|*} Function for partial application or result of element-wise application + * @throws {Error} When first argument is not a function + * @description The each combinator provides APL-inspired element-wise operations + * for multi-argument functions over table structures. This is the primary mechanism + * for combining multiple tables or tables with scalars in element-wise fashion. + * + * The function is designed for multi-argument operations and aligns with the parser's + * apply mechanism. When x is a table, each returns a function that waits for the + * second argument (y), enabling the parser to build `apply(apply(each, f), x)` chains + * that resolve to element-wise operations when y is provided. + * + * Key behaviors: + * - Table + Scalar: Applies f to each element of the table with the scalar as second argument + * - Table + Table: Applies f to corresponding elements from both tables + * - Scalar + Table: Uses map to apply f with the scalar as first argument to each table element + * - Scalar + Scalar: Falls back to normal function application for backward compatibility + * + * This design choice enables powerful multi-argument element-wise operations like + * `each @add table1 table2` for element-wise addition, while maintaining compatibility + * with the parser's two-argument apply model. The function is specifically designed + * for multi-argument operations, distinguishing it from map which is for single-table + * transformations. + */ + scope.each = function(f, x) { + if (process.env.DEBUG) { + console.log(`[DEBUG] each called with: f=${typeof f}, x=${typeof x}`); + console.log(`[DEBUG] x value:`, x); + } + + if (typeof f !== 'function') { + throw new Error('each: first argument must be a function, got ' + typeof f); + } + + if (x === undefined) { + // Partial application: return a function that waits for the second argument + return function(x) { + return scope.each(f, x); + }; + } + + // Check if x is a table + const isXTable = typeof x === 'object' && x !== null && !Array.isArray(x); + + if (isXTable) { + // x is a table - always return a function that can handle the second argument + return function(y) { + // Check if y is a table + const isYTable = typeof y === 'object' && y !== null && !Array.isArray(y); + + if (!isYTable) { + // x is a table, y is not a table - apply function to each element of x with y as second argument + const result = {}; + for (const [key, value] of Object.entries(x)) { + result[key] = f(value, y); + } + return result; + } + + // Both x and y are tables - they should have the same keys + const result = {}; + for (const [key, value] of Object.entries(x)) { + if (y.hasOwnProperty(key)) { + result[key] = f(value, y[key]); + } + } + return result; + }; + } + + // x is not a table, return a function that waits for the second argument + return function(y) { + // Check if y is a table + const isYTable = typeof y === 'object' && y !== null && !Array.isArray(y); + + if (!isYTable) { + // No tables, apply normally (backward compatibility) + return f(x, y); + } + + // x is not a table, y is a table - use map + return scope.map(function(val) { return f(x, val); }, y); + }; + }; + + // ===== TABLE OPERATIONS NAMESPACE (t.) ===== + + /** + * Table operations namespace (t.) + * @description Provides immutable table operations that always return new tables, + * never modifying the original. This namespace implements APL-inspired element-wise + * operations and functional table manipulation patterns. + * + * All operations in this namespace are designed to work with the language's + * immutable data philosophy, where data transformations create new structures + * rather than modifying existing ones. This enables functional programming + * patterns and eliminates side effects from table operations. + * + * The namespace provides both basic table operations (get, set, delete, merge) + * and higher-order operations (map, filter, reduce) that work element-wise + * on table values. This design choice enables powerful data transformation + * patterns while maintaining the functional programming principles of the language. + * + * Key design principles: + * - Immutability: All operations return new tables, never modify originals + * - Element-wise operations: Functions operate on table values, not structure + * - Partial application: All functions support currying patterns + * - Functional consistency: Operations work with the combinator foundation + */ + scope.t = { + /** + * Map: Apply a function to each value in a table + * @param {Function} f - Function to apply + * @param {Object} table - Table to map over + * @returns {Object} New table with transformed values + * @throws {Error} When first argument is not a function or second is not a table + */ + map: function(f, table) { + if (typeof f !== 'function') { + throw new Error('t.map: first argument must be a function'); + } + + if (table === undefined) { + // Partial application: return a function that waits for the table + return function(table) { + return scope.t.map(f, table); + }; + } + + if (typeof table !== 'object' || table === null) { + throw new Error('t.map: second argument must be a table'); + } + + const result = {}; + for (const [key, value] of Object.entries(table)) { + result[key] = f(value); + } + return result; + }, + + /** + * Filter: Filter table values based on a predicate + * @param {Function} p - Predicate function + * @param {Object} table - Table to filter + * @returns {Object} New table with only values that pass the predicate + * @throws {Error} When first argument is not a function or second is not a table + */ + filter: function(p, table) { + if (typeof p !== 'function') { + throw new Error('t.filter: first argument must be a function'); + } + + if (table === undefined) { + // Partial application: return a function that waits for the table + return function(table) { + return scope.t.filter(p, table); + }; + } + + if (typeof table !== 'object' || table === null) { + throw new Error('t.filter: second argument must be a table'); + } + + const result = {}; + for (const [key, value] of Object.entries(table)) { + if (p(value)) { + result[key] = value; + } + } + return result; + }, + + /** + * Reduce: Reduce all values in a table using a binary function + * @param {Function} f - Binary function + * @param {*} init - Initial value + * @param {Object} table - Table to reduce + * @returns {*} Result of reducing all values + * @throws {Error} When first argument is not a function or third is not a table + */ + reduce: function(f, init, table) { + if (typeof f !== 'function') { + throw new Error('t.reduce: first argument must be a function'); + } + + if (init === undefined) { + // Partial application: return a function that waits for the remaining arguments + return function(init, table) { + if (table === undefined) { + // Still partial application + return function(table) { + return scope.t.reduce(f, init, table); + }; + } + return scope.t.reduce(f, init, table); + }; + } + + if (table === undefined) { + // Partial application: return a function that waits for the table + return function(table) { + return scope.t.reduce(f, init, table); + }; + } + + if (typeof table !== 'object' || table === null) { + throw new Error('t.reduce: third argument must be a table'); + } + + let result = init; + for (const [key, value] of Object.entries(table)) { + result = f(result, value, key); + } + return result; + }, + + /** + * Set: Immutably set a key-value pair in a table + * @param {Object} table - Table to modify + * @param {*} key - Key to set + * @param {*} value - Value to set + * @returns {Object} New table with the key-value pair set + * @throws {Error} When first argument is not a table + */ + set: function(table, key, value) { + if (typeof table !== 'object' || table === null) { + throw new Error('t.set: first argument must be a table'); + } + + if (key === undefined) { + // Partial application: return a function that waits for the remaining arguments + return function(key, value) { + if (value === undefined) { + // Still partial application + return function(value) { + return scope.t.set(table, key, value); + }; + } + return scope.t.set(table, key, value); + }; + } + + if (value === undefined) { + // Partial application: return a function that waits for the value + return function(value) { + return scope.t.set(table, key, value); + }; + } + + return { ...table, [key]: value }; + }, + + /** + * Delete: Immutably delete a key from a table + * @param {Object} table - Table to modify + * @param {*} key - Key to delete + * @returns {Object} New table without the specified key + * @throws {Error} When first argument is not a table + */ + delete: function(table, key) { + if (typeof table !== 'object' || table === null) { + throw new Error('t.delete: first argument must be a table'); + } + + if (key === undefined) { + // Partial application: return a function that waits for the key + return function(key) { + return scope.t.delete(table, key); + }; + } + + const result = { ...table }; + delete result[key]; + return result; + }, + + /** + * Merge: Immutably merge two tables + * @param {Object} table1 - First table + * @param {Object} table2 - Second table (values override table1) + * @returns {Object} New merged table + * @throws {Error} When either argument is not a table + */ + merge: function(table1, table2) { + if (typeof table1 !== 'object' || table1 === null) { + throw new Error('t.merge: first argument must be a table'); + } + + if (table2 === undefined) { + // Partial application: return a function that waits for the second table + return function(table2) { + return scope.t.merge(table1, table2); + }; + } + + if (typeof table2 !== 'object' || table2 === null) { + throw new Error('t.merge: second argument must be a table'); + } + + return { ...table1, ...table2 }; + }, + + /** + * Pairs: Get all key-value pairs from a table + * @param {Object} table - Table to get pairs from + * @returns {Array} Array of [key, value] pairs + * @throws {Error} When argument is not a table + */ + pairs: function(table) { + if (typeof table !== 'object' || table === null) { + throw new Error('t.pairs: argument must be a table'); + } + return Object.entries(table); + }, + + /** + * Keys: Get all keys from a table + * @param {Object} table - Table to get keys from + * @returns {Array} Array of keys + * @throws {Error} When argument is not a table + */ + keys: function(table) { + if (typeof table !== 'object' || table === null) { + throw new Error('t.keys: argument must be a table'); + } + return Object.keys(table); + }, + + /** + * Values: Get all values from a table + * @param {Object} table - Table to get values from + * @returns {Array} Array of values + * @throws {Error} When argument is not a table + */ + values: function(table) { + if (typeof table !== 'object' || table === null) { + throw new Error('t.values: argument must be a table'); + } + return Object.values(table); + }, + + /** + * Length: Get the number of key-value pairs in a table + * @param {Object} table - Table to measure + * @returns {number} Number of key-value pairs + * @throws {Error} When argument is not a table + */ + length: function(table) { + if (typeof table !== 'object' || table === null) { + throw new Error('t.length: argument must be a table'); + } + return Object.keys(table).length; + }, + + /** + * Has: Check if a table has a specific key + * @param {Object} table - Table to check + * @param {*} key - Key to check for + * @returns {boolean} True if key exists, false otherwise + * @throws {Error} When first argument is not a table + */ + has: function(table, key) { + if (typeof table !== 'object' || table === null) { + throw new Error('t.has: first argument must be a table'); + } + + if (key === undefined) { + // Partial application: return a function that waits for the key + return function(key) { + return scope.t.has(table, key); + }; + } + + return table.hasOwnProperty(key); + }, + + /** + * Get: Safely get a value from a table with optional default + * @param {Object} table - Table to get from + * @param {*} key - Key to get + * @param {*} defaultValue - Default value if key doesn't exist + * @returns {*} Value at key or default value + * @throws {Error} When first argument is not a table + */ + get: function(table, key, defaultValue) { + if (typeof table !== 'object' || table === null) { + throw new Error('t.get: first argument must be a table'); + } + + if (key === undefined) { + // Partial application: return a function that waits for the remaining arguments + return function(key, defaultValue) { + if (defaultValue === undefined) { + // Still partial application + return function(defaultValue) { + return scope.t.get(table, key, defaultValue); + }; + } + return scope.t.get(table, key, defaultValue); + }; + } + + if (defaultValue === undefined) { + // Partial application: return a function that waits for the default value + return function(defaultValue) { + return scope.t.get(table, key, defaultValue); + }; + } + + return table.hasOwnProperty(key) ? table[key] : defaultValue; + } + }; } /** - * Interpreter: Walks the AST and evaluates each node. + * Interpreter: Walks the AST and evaluates each node using the combinator foundation. * * @param {Object} ast - Abstract Syntax Tree to evaluate * @returns {*} The result of evaluating the AST, or a Promise for async operations @@ -448,6 +1167,23 @@ function initializeStandardLibrary(scope) { * both synchronous and asynchronous operations. * * The interpreter implements a combinator-based architecture where all operations + * are executed through function calls to standard library combinators. This design + * eliminates parsing ambiguity while preserving intuitive syntax. The parser translates + * all operators (+, -, *, /, etc.) into FunctionCall nodes that reference combinator + * functions, ensuring consistent semantics across all operations. + * + * Key architectural features: + * - Combinator Foundation: All operations are function calls to standard library combinators + * - Scope Management: Prototypal inheritance for variable lookup and function definitions + * - Forward Declaration: Recursive functions are supported through placeholder creation + * - Error Handling: Comprehensive error detection and reporting with call stack tracking + * - Debug Support: Optional debug mode for development and troubleshooting + * + * The interpreter 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. This ensures that all + * operations follow the same execution model and can be extended by adding new combinator + * functions to the standard library. * 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 @@ -465,6 +1201,11 @@ function initializeStandardLibrary(scope) { * 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 = {}; @@ -481,7 +1222,7 @@ function interpreter(ast) { callStackTracker.reset(); /** - * Evaluates AST nodes in the global scope. + * Evaluates AST nodes in the global scope using the combinator foundation. * * @param {Object} node - AST node to evaluate * @returns {*} The result of evaluating the node @@ -494,6 +1235,28 @@ function interpreter(ast) { * 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 pattern enables natural recursive function definitions without requiring + * special syntax or pre-declaration. + * + * 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. + * This design ensures that all operations follow the same execution model and + * can be extended by adding new combinator functions to the standard library. + * + * Key evaluation patterns: + * - Literals: Direct value return + * - FunctionCall: Delegates to standard library combinator functions + * - Assignment: Creates variables in global scope with forward declaration support + * - WhenExpression: Pattern matching with wildcard support + * - TableLiteral: Creates immutable table structures + * - TableAccess: Safe property access with error handling */ function evalNode(node) { callStackTracker.push('evalNode', node?.type || 'unknown'); @@ -568,8 +1331,55 @@ function interpreter(ast) { // For other key types (numbers, strings), evaluate normally key = evalNode(entry.key); } - const value = evalNode(entry.value); - table[key] = value; + // Special handling for FunctionDeclaration nodes + if (process.env.DEBUG) { + console.log(`[DEBUG] TableLiteral: entry.value.type = ${entry.value.type}`); + } + if (entry.value.type === 'FunctionDeclaration') { + // Don't evaluate the function body, just create the function + const func = function(...args) { + callStackTracker.push('FunctionCall', entry.value.params.join(',')); + try { + // If we have fewer arguments than parameters, return a curried function + if (args.length < entry.value.params.length) { + return function(...moreArgs) { + const allArgs = [...args, ...moreArgs]; + if (allArgs.length < entry.value.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 < entry.value.params.length; i++) { + localScope[entry.value.params[i]] = finalArgs[i]; + } + return localEvalNodeWithScope(entry.value.body, localScope); + }; + } else { + // We have enough arguments now + let localScope = Object.create(globalScope); + for (let i = 0; i < entry.value.params.length; i++) { + localScope[entry.value.params[i]] = allArgs[i]; + } + return localEvalNodeWithScope(entry.value.body, localScope); + } + }; + } else { + // We have enough arguments, evaluate the function + let localScope = Object.create(globalScope); + for (let i = 0; i < entry.value.params.length; i++) { + localScope[entry.value.params[i]] = args[i]; + } + return localEvalNodeWithScope(entry.value.body, localScope); + } + } finally { + callStackTracker.pop(); + } + }; + table[key] = func; + } else { + const value = evalNode(entry.value); + table[key] = value; + } } } @@ -665,11 +1475,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(); } @@ -755,6 +1591,33 @@ function interpreter(ast) { 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) { @@ -810,6 +1673,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`); } @@ -848,6 +1714,10 @@ function interpreter(ast) { * * 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'); @@ -1164,6 +2034,11 @@ function interpreter(ast) { * * 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'); @@ -1460,6 +2335,14 @@ function interpreter(ast) { * 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) { @@ -1483,6 +2366,10 @@ function debugLog(message, data = null) { * 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) { @@ -1506,7 +2393,13 @@ function debugError(message, error = null) { * * 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. + * 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: [], @@ -1599,7 +2492,9 @@ const callStackTracker = { * 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. + * 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 @@ -1641,7 +2536,13 @@ async function readFile(filePath) { * tracker to provide execution statistics and detect potential issues. * * Supports both synchronous and asynchronous execution, with proper - * error handling and process exit codes. + * 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 { @@ -1738,4 +2639,6 @@ async function main() { main().catch(error => { console.error('Fatal error:', error.message); process.exit(1); -}); \ No newline at end of file +}); + + |