diff options
36 files changed, 5564 insertions, 3396 deletions
diff --git a/js/scripting-lang/IDEAS.txt b/js/scripting-lang/IDEAS.txt new file mode 100644 index 0000000..96f8b4b --- /dev/null +++ b/js/scripting-lang/IDEAS.txt @@ -0,0 +1,17 @@ +add 2 other io functions + +..listen + +..emit + +where listen takes in a well defined state object from outside the scope of the program, making it available to the program + +where emit lets the program spit state back out into the wider world + +*** + +Implement type annotation with the "is" keyword, like + +x is int : 1; + +double is int : x -> x * 2; \ No newline at end of file diff --git a/js/scripting-lang/NEXT-STEPS.md b/js/scripting-lang/NEXT-STEPS.md new file mode 100644 index 0000000..8061fb7 --- /dev/null +++ b/js/scripting-lang/NEXT-STEPS.md @@ -0,0 +1,435 @@ +# Next Steps: Immutable Real-World Programming Features + +## Overview + +This document outlines the plan for extending the Simple Scripting Language to support real-world programming scenarios while maintaining its immutable, functional design philosophy. + +## Core Principles + +- **Immutability First**: All data structures are immutable +- **Transformation Over Mutation**: Operations return new data rather than modifying existing data +- **Functional Composition**: Complex operations built from simple, composable functions +- **Type Safety**: Enhanced pattern matching and type checking +- **Performance**: Efficient persistent data structures + +## Phase 1: String Operations and Type System + +### 1.1 String Operations +**Goal**: Add essential string manipulation capabilities + +**New Functions**: +```javascript +// String operations as functions +length : string.length "hello"; // 5 +contains : string.contains "hello" "ll"; // true +startsWith : string.startsWith "hello" "he"; // true +endsWith : string.endsWith "hello" "lo"; // true +substring : string.substring "hello" 1 3; // "el" + +// String concatenation +result : string.concat "Hello" " " "World"; // "Hello World" + +// String transformation +uppercase : string.upper "hello"; // "HELLO" +lowercase : string.lower "HELLO"; // "hello" +trimmed : string.trim " hello "; // "hello" +``` + +**Implementation**: +- Add `string.` namespace for string operations +- Add string operation functions to standard library +- No new syntax - uses existing function call patterns +- Extend existing string literal support + +### 1.2 Runtime Type Checking with `is` Keyword +**Goal**: Add explicit, optional type checking mechanism using the `is` keyword + +**Design Philosophy**: +- Type checking is purely additive - no breaking changes to existing code +- Works seamlessly with existing case expression syntax +- Returns boolean values, fitting the functional style +- Simple implementation with clear semantics + +**New Features**: +```javascript +// Basic type checking +isNumber : x -> x is number; +isString : x -> x is string; +isTable : x -> x is table; +isFunction : x -> x is function; + +// Type-safe operations in case expressions +safeStringOp : x -> case x of + x is string : string.length x + _ : "not a string"; + +// Complex type validation +validateUser : user -> case user of + user.name is string and user.age is number : true + _ : false; + +// Type guards in pattern matching +processData : data -> case data of + data is table : "processing table" + data is string : "processing string" + data is number : "processing number" + _ : "unknown type"; +``` + +**Supported Types**: +- `number` (both integers and floats) +- `string` +- `boolean` +- `table` (objects and arrays) +- `function` + +**Implementation**: +- Add `IS` token type and type-specific tokens (`NUMBER_TYPE`, `STRING_TYPE`, etc.) +- Update lexer to recognize `is` keyword and type names +- Extend parser to handle type checking expressions in case patterns +- Add type checking logic in interpreter that returns boolean values +- No changes to existing syntax or semantics + +## Phase 2: Persistent Data Structures + +### 2.1 Immutable Tables with Transformations +**Goal**: Replace mutable table operations with immutable transformations + +**Current Problem**: +```javascript +// This won't work (mutable) +cache[key] = value; +``` + +**Solution - Functional Immutable Transformations**: +```javascript +// Functional transformations using table namespace +cache : {}; +cache1 : table.set cache "user.1" "Alice"; +cache2 : table.set cache1 "user.2" "Bob"; +value : table.get cache2 "user.1"; // "Alice" +cache3 : table.delete cache2 "user.1"; + +// Nested updates with dot notation +user : {name: "Alice", profile: {age: 25}}; +updatedUser : table.set user "profile.age" 26; + +// Complex transformations using composition +cache1 : pipe + (table.set "user.1" "Alice") + (table.set "user.2" "Bob") + (table.set "user.3" "Charlie") + cache; + +// Table merging +combined : table.merge table1 table2; +``` + +**Implementation**: +- Add `table.set`, `table.get`, `table.delete`, `table.merge` functions +- Implement efficient structural sharing for immutable updates +- Add nested key support (e.g., "profile.age") +- All functions return new tables, never modify originals +- Use existing function composition patterns + +### 2.2 APL-Inspired Table Primitives +**Goal**: Enhance tables with APL-style operations for ergonomic data manipulation + +**Design Philosophy**: +- Use existing table syntax for array-like behavior +- Add APL-inspired primitives for vectorized operations +- Keep immutable transformations +- Provide concise, expressive data manipulation + +**New Features**: +```javascript +// Table creation (array-like using existing syntax) +numbers : {1, 2, 3, 4, 5}; +mixed : {1, "hello", true, {key: "value"}}; + +// All table operations namespaced for consistency +first : table.first numbers; // First element +last : table.last numbers; // Last element +length : table.length numbers; // Size/shape + +// Less common operations (namespaced) +rest : table.drop 1 numbers; // Drop first element +slice : table.slice numbers 2 3; // Elements 2-3 + +// Vectorized operations +doubled : table.add numbers numbers; // Element-wise addition +squared : table.multiply numbers numbers; // Element-wise multiplication +incremented : table.add numbers 1; // Scalar addition to each element + +// Reductions (all namespaced) +sum : table.sum numbers; // Sum reduction +max : table.max numbers; // Maximum reduction +min : table.min numbers; // Minimum reduction +product : table.product numbers; // Product reduction +average : table.average numbers; // Average reduction + +// Scan operations (namespaced) +runningSum : table.scan table.sum numbers; // Running sum scan +runningProduct : table.scan table.product numbers; // Running product scan + +// Table metadata operations +keys : table.keys table; // Get all keys +values : table.values table; // Get all values +reversed : table.reverse table; // Reverse order +sorted : table.sort table; // Sort +unique : table.unique table; // Remove duplicates +``` + +**Implementation**: +- Add `table.` namespace for all table operations +- Extend lexer to recognize table operation keywords +- Add vectorized operation logic in interpreter +- Implement reduction and scan operations +- Add table metadata operations + +## Phase 3: Higher-Order Functions for Collections + +### 3.1 Enhanced Standard Library +**Goal**: Add collection processing functions + +**New Functions**: +```javascript +// Table processing (using namespaced primitives) +numbers : {1, 2, 3, 4, 5}; +doubled : map @double numbers; // {2, 4, 6, 8, 10} +evens : filter @isEven numbers; // {2, 4} +sum : table.sum numbers; // 15 (sum reduction) + +// Table metadata operations +table : {a: 1, b: 2, c: 3}; +keys : table.keys table; // {a, b, c} +values : table.values table; // {1, 2, 3} +pairs : table.pairs table; // {{a, 1}, {b, 2}, {c, 3}} + +// Advanced operations with tables +nested : {{1, 2}, {3, 4}, {5}}; +flattened : table.flatten nested; // {1, 2, 3, 4, 5} +grouped : table.groupBy @isEven numbers; // {true: {2, 4}, false: {1, 3, 5}} + +// Table operations +reversed : table.reverse numbers; // {5, 4, 3, 2, 1} +sorted : table.sort numbers; // {1, 2, 3, 4, 5} +unique : table.unique {1, 2, 2, 3, 3, 4}; // {1, 2, 3, 4} +``` + +**Implementation**: +- Extend existing `map`, `filter`, `reduce` to work with tables +- Implement vectorized operations for tables +- Add reduction and scan operations +- Implement table metadata operations + +### 3.2 Table Generation Helpers +**Goal**: Add convenient table creation functions + +**New Functions**: +```javascript +// Table generation helpers +range : table.range 1 5; // {1, 2, 3, 4, 5} +repeated : table.repeat "hello" 3; // {"hello", "hello", "hello"} + +// Use existing map/filter instead of comprehensions +squares : map (x -> x * x) {1, 2, 3, 4, 5}; +evens : filter @isEven {1, 2, 3, 4, 5}; +``` + +## Phase 4: Error Handling with Error Type + +### 4.1 Error Type +**Goal**: Provide consistent error handling without complex monads + +**Implementation**: +```javascript +// Simple error type +error : message -> {type: "error", message: message}; + +// Safe operations with error handling +safeDivide : x y -> case y of + 0 : error "division by zero" + _ : x / y; + +safeParseNumber : str -> case str of + str is number : str + _ : error "invalid number"; + +// Error checking +isError : value -> value is error; +getErrorMessage : error -> case error of + {type: "error", message: m} : m; +``` + +## Phase 5: Real-World Scenario Support + +### 5.1 User Management System +**Immutable Implementation**: +```javascript +// User validation +isValidEmail : email -> case email of + email contains "@" : true + _ : false; + +createUser : name email age -> case (isValidEmail email) and (isValidAge age) of + true : {name: name, email: email, age: age, status: "active"} + false : error "invalid user data"; + +// User management +users : {}; +user1 : createUser "Alice" "alice@example.com" 25; +users1 : table.set users "user.1" user1; +user2 : createUser "Bob" "bob@example.com" 30; +users2 : table.set users1 "user.2" user2; + +// Safe user lookup +findUser : email users -> case users of + {} : error "user not found" + _ : case (table.first users).email = email of + true : table.first users + false : findUser email (table.drop 1 users); +``` + +### 5.2 Shopping Cart System +**Immutable Implementation**: +```javascript +// Cart operations +emptyCart : {items: {}, total: 0}; +addItem : cart item -> { + items: table.set cart.items (table.length cart.items + 1) item, + total: cart.total + item.price +}; +removeItem : cart itemId -> { + items: filter (item -> item.id != itemId) cart.items, + total: calculateTotal (filter (item -> item.id != itemId) cart.items) +}; + +// Discount application +applyDiscount : cart discountPercent -> { + items: cart.items, + total: cart.total * (1 - discountPercent / 100) +}; +``` + +### 5.3 Data Processing Pipeline +**Immutable Implementation**: +```javascript +// Data processing +salesData : { + {month: "Jan", sales: 1000, region: "North"}, + {month: "Feb", sales: 1200, region: "North"}, + {month: "Mar", sales: 800, region: "South"} +}; + +// Filter by region +filterByRegion : data region -> filter (item -> item.region = region) data; + +// Calculate totals using sum reduction +sumSales : data -> table.sum (map (item -> item.sales) data); + +// Process pipeline +northData : filterByRegion salesData "North"; +northTotal : sumSales northData; +``` + +## Implementation Timeline + +### Week 1-2: String Operations and Runtime Type Checking +- [ ] String concatenation operator +- [ ] String method implementations +- [ ] `is` keyword and type checking tokens +- [ ] Type checking in case expressions +- [ ] Type validation functions in standard library + +### Week 3-4: Table Primitives with Namespacing +- [ ] `table.` namespace for all table operations +- [ ] Vectorized operations for tables +- [ ] Reduction and scan operations +- [ ] Table metadata operations +- [ ] Performance optimization + +### Week 5-6: Higher-Order Functions +- [ ] Enhanced standard library +- [ ] Collection processing functions +- [ ] Table-specific operations +- [ ] Utility functions + +### Week 7-8: Error Handling +- [ ] Error type implementation +- [ ] Error handling patterns +- [ ] Error checking functions +- [ ] Integration with existing operations + +### Week 9-10: Real-World Scenarios +- [ ] User management system +- [ ] Shopping cart system +- [ ] Data processing pipeline +- [ ] Integration testing + +## Benefits of Runtime Type Checking Approach + +### Simplicity +- **Minimal Implementation**: Only need to add `is` keyword and type checking logic +- **No Breaking Changes**: Existing code continues to work unchanged +- **Clear Semantics**: `x is number` is obviously a boolean expression +- **Consistent Syntax**: Works seamlessly with existing case expressions + +### Functional Design +- **Boolean Results**: Type checking returns true/false, fitting functional style +- **Composable**: Can combine with logical operators (`and`, `or`, `not`) +- **Pattern Matching**: Integrates naturally with case expressions +- **No Side Effects**: Pure functions for type validation + +### Extensibility +- **Easy to Add Types**: Simple to extend for new types (arrays, tuples, etc.) +- **Custom Types**: Can implement custom type checking via functions +- **Performance**: Runtime overhead only when explicitly used +- **Optional**: No requirement to use type checking in existing code + +## Testing Strategy + +### Unit Tests +- String operation tests +- Runtime type checking tests (`is` keyword) +- Type validation function tests +- Data structure transformation tests +- Higher-order function tests +- Error handling tests + +### Integration Tests +- Real-world scenario tests +- Performance tests +- Edge case tests +- Error handling tests + +### Performance Benchmarks +- Data structure operation performance +- Memory usage analysis +- Transformation efficiency +- Scalability testing + +## Success Metrics + +- [ ] All real-world scenarios in `tests/17_real_world_scenarios.txt` pass +- [ ] Performance within acceptable bounds (no more than 2x slower than current) +- [ ] Memory usage remains reasonable +- [ ] Code remains readable and maintainable +- [ ] Backward compatibility maintained + +## Future Considerations + +### Advanced Features (Post-v1.0) +- Lazy evaluation for large collections +- Parallel processing capabilities +- Advanced type system with generics +- Macro system for code generation +- Module system for code organization + +### Performance Optimizations +- Structural sharing for immutable data structures +- Compile-time optimizations +- JIT compilation for hot paths +- Memory pooling for temporary objects + +This plan maintains the language's functional, immutable design while adding the capabilities needed for real-world programming scenarios. \ No newline at end of file diff --git a/js/scripting-lang/README.md b/js/scripting-lang/README.md index 6ea5104..73ccbf1 100644 --- a/js/scripting-lang/README.md +++ b/js/scripting-lang/README.md @@ -1,727 +1,220 @@ # Simple Scripting Language -A minimal, interpreted scripting language designed for learning language implementation concepts. This language demonstrates the core components needed for an interpreter: lexer, parser, and interpreter. +A functional programming language with immutable variables, first-class functions, and pattern matching. ## Features -- **Arithmetic operations**: `+`, `-`, `*`, `/`, `%` (modulo), `^` (power) -- **Comparison operators**: `=`, `<`, `>`, `<=`, `>=`, `!=` -- **Logical operators**: `and`, `or`, `xor`, `not` -- **Variable assignment**: Immutable variables with `:` syntax -- **Function definitions**: Arrow function syntax with pattern matching -- **Pattern matching**: Case expressions with wildcard support -- **Function calls**: Direct function application -- **First-class functions**: Functions can be passed as arguments using `@` syntax -- **Function composition**: Higher-order functions for combining functions -- **Lexical scoping**: Functions create their own scope -- **Input/Output**: Built-in IO operations with `..in`, `..out`, and `..assert` -- **Standard Library**: Built-in functional programming utilities -- **Comments**: C-style block comments with `/* ... */` syntax -- **Tables**: Lua-style immutable tables with array and object capabilities +- **Immutable Variables**: Variables cannot be reassigned +- **First-Class Functions**: Functions can be passed as arguments and stored in data structures +- **Lexical Scoping**: Functions create their own scope +- **Pattern Matching**: Case expressions with wildcard support +- **Table Literals**: Lua-style tables with both array-like and key-value entries +- **Standard Library**: Built-in higher-order functions (`map`, `compose`, `pipe`, `apply`, `filter`, `reduce`, `fold`, `curry`) +- **IO Operations**: Built-in input/output operations (`..in`, `..out`, `..assert`) +- **Floating Point Arithmetic**: Full support for decimal numbers +- **Unary Minus**: Support for negative numbers (e.g., `-1`, `-3.14`) ## Syntax -### Variables - -Variables are immutable and assigned using the `:` operator: - -``` -x : 5; -y : 10; -``` - -### Arithmetic Operations - -Arithmetic operations are supported with parentheses grouping: - -``` -sum : x + y; -diff : x - y; -product : x * y; -quotient : x / y; -modulo : 17 % 5; /* Returns 2 */ -power : 2 ^ 3; /* Returns 8 */ -grouped : (5 + 3) * 2; /* Returns 16 */ -nested : ((5 + 3) * 2) + 1; /* Returns 17 */ -``` - -### Function Definitions - -Functions are defined using arrow syntax (`->`): - +### Basic Operations ``` -add : x y -> x + y; -double : x -> x * 2; -``` - -### Function Calls +/* Arithmetic */ +x : 5 + 3; +y : 10 - 2; +z : 4 * 3; +w : 15 / 3; +neg : -5; /* Unary minus */ -Functions are called by providing arguments directly after the function name: +/* Comparisons */ +result : x > y; +equal : a = b; +not_equal : a != b; +/* Logical */ +and_result : true and false; +or_result : true or false; ``` -result : add 3 4; -doubled : double 5; -``` - -#### Function Calls with Parentheses - -Function calls support parenthesized arguments for complex expressions: +### Variables and Functions ``` -/* Simple function call */ -result1 : add 3 4; +/* Immutable variables */ +x : 42; +y : "hello"; -/* Function call with parenthesized arithmetic */ -result2 : add (3 + 2) (4 + 1); +/* Function definition */ +f : x -> x * 2; -/* Function call with function calls in parentheses */ -result3 : add (double 3) (square 2); - -/* Complex nested function calls with parentheses */ -result4 : add (add 1 2) (add 3 4); +/* Function call */ +result : f 5; ``` -**Note:** Parentheses provide explicit grouping and are the recommended way to handle complex nested function calls. - ### Tables - -The language supports Lua-style immutable tables that can serve as both arrays and objects: - -#### Table Creation - -Tables are created using curly braces `{}`: - -``` -/* Empty table */ -empty : {}; - -/* Array-like table (1-indexed) */ -numbers : {1, 2, 3, 4, 5}; - -/* Key-value table */ -person : {name: "Alice", age: 30, active: true}; - -/* Mixed table (array-like and key-value) */ -mixed : {1, name: "Bob", 2, active: false}; ``` +/* Table literal */ +table : {1, 2, 3, key: "value"}; -#### Table Access - -Tables support both bracket notation and dot notation for accessing values: - +/* Table access */ +first : table[1]; +value : table.key; +nested : table.key.subkey; ``` -/* Array access with bracket notation */ -first : numbers[1]; /* Returns 1 */ -second : numbers[2]; /* Returns 2 */ - -/* Object access with dot notation */ -name : person.name; /* Returns "Alice" */ -age : person.age; /* Returns 30 */ - -/* Object access with bracket notation */ -name_bracket : person["name"]; /* Returns "Alice" */ -age_bracket : person["age"]; /* Returns 30 */ - -/* Mixed table access */ -first_mixed : mixed[1]; /* Returns 1 */ -name_mixed : mixed.name; /* Returns "Bob" */ -second_mixed : mixed[2]; /* Returns 2 */ -``` - -#### Table Features - -- **Immutable**: Tables cannot be modified after creation -- **1-indexed arrays**: Array-like tables start indexing at 1 -- **Mixed types**: Tables can contain numbers, strings, booleans, and functions -- **Nested access**: Tables can contain other tables for complex data structures -- **Unified syntax**: Same syntax for arrays and objects - -#### Tables with Case Expressions - -Tables work well with case expressions for pattern matching: - -``` -/* Case expressions with table values */ -person : {name: "Alice", age: 30, active: true}; -result : case person.age of - 0 : 30 : "Person is 30" - 1 : _ : "Person is different age" -; - -/* Case expressions with boolean values */ -status : case person.active of - 0 : true : "Person is active" - 1 : false : "Person is inactive" -; - -/* Case expressions with array-like access */ -numbers : {1: 10, 2: 20, 3: 30}; -element : case numbers[2] of - 0 : 20 : "Second element is 20" - 1 : _ : "Unexpected second element" -; -``` - -**Current Limitations:** -- Function calls from tables (e.g., `table.func args`) are not supported -- Function definitions inside table literals are not supported -- Tables can store function references, but they cannot be called directly - -### First-Class Functions - -Functions can be passed as arguments to other functions using the `@` prefix: - -``` -double : x -> x * 2; -square : x -> x * x; -compose : f g x -> f g x; - -composed : compose @double @square 3; /* double(square(3)) = 18 */ -``` - -**Function Reference Syntax:** -- `@functionName` - Creates a function reference (doesn't execute the function) -- `functionName args` - Calls the function with arguments ### Pattern Matching - -The language supports pattern matching with case expressions in function bodies: - -``` -compare : x y -> - case x y of - 0 0 : "both zero" - 0 _ : "x is zero" - _ 0 : "y is zero" - _ _ : "neither zero"; -``` - -#### Pattern Matching Syntax - -- **Exact matches**: `0 0` matches when both values are 0 -- **Wildcards**: `_` matches any value -- **Mixed patterns**: `0 _` matches when first value is 0 and second can be anything - -### Comparison and Logical Operations - -The language supports comparison and logical operations: - -``` -/* Comparison operators */ -less : 3 < 5; /* true */ -greater : 10 > 5; /* true */ -equal : 5 = 5; /* true */ -not_equal : 3 != 5; /* true */ -less_equal : 5 <= 5; /* true */ -greater_equal : 5 >= 3; /* true */ - -/* Logical operators */ -and_result : 1 and 1; /* true */ -or_result : 0 or 1; /* true */ -xor_result : 1 xor 0; /* true */ -not_result : not 0; /* true */ -``` - -### Comments - -The language supports C-style block comments: - -``` -/* This is a single line comment */ - -/* This is a multi-line comment - that spans multiple lines */ - -x : 5; /* Comment on same line as code */ - -/* Nested comments are supported */ -/* Outer comment /* Inner comment */ More outer comment */ -``` - -**Comment Features:** -- Block comments start with `/*` and end with `*/` -- Comments can span multiple lines -- Comments can appear on the same line as code -- Nested comments are supported -- Comments are completely ignored by the lexer and parser - -### Input/Output Operations - -The language provides built-in IO operations for reading from stdin and writing to stdout: - -``` -name : ..in; /* Read input from stdin */ -..out name; /* Output to stdout */ -..out "Hello"; /* Output string literal */ -..out 42; /* Output number */ -..assert x = 5; /* Assert that x equals 5 */ -..assert y > 3; /* Assert that y is greater than 3 */ -..assert z != 0; /* Assert that z is not equal to 0 */ -``` - -**IO Functions:** -- `..in` - Reads a line from stdin, returns a number if possible, otherwise a string -- `..out` - Outputs a value to stdout and returns the value -- `..assert` - Asserts that a comparison is true, throws an error if it's false. Supports all comparison operators: `=`, `<`, `>`, `<=`, `>=`, `!=` - -**Note:** The `..` prefix indicates these are impure operations that have side effects. - -## Standard Library - -The language includes a built-in standard library with functional programming utilities: - -### Higher-Order Functions - -- **map**: Apply a function to a value (`map f x = f x`) -- **compose**: Compose two functions (`compose f g x = f(g(x))`) -- **curry**: Explicit currying (`curry f x y = f x y`) -- **apply**: Apply a function to an argument (`apply f x = f x`) -- **pipe**: Compose functions left-to-right (`pipe f g x = g(f(x))`) -- **filter**: Filter based on a predicate (`filter p x = p(x) ? x : 0`) -- **reduce**: Reduce to a single value (`reduce f init x = f(init, x)`) -- **fold**: Same as reduce (`fold f init x = f(init, x)`) - -### Usage Examples - ``` -double : x -> x * 2; -square : x -> x * x; - -/* Using map */ -result1 : map @double 5; /* Returns 10 */ -result2 : map @square 3; /* Returns 9 */ - -/* Using compose */ -composed : compose @double @square 3; /* double(square(3)) = 18 */ - -/* Using pipe */ -piped : pipe @double @square 2; /* square(double(2)) = 16 */ - -/* Using filter */ -isPositive : x -> x > 0; -filtered : filter @isPositive 5; /* Returns 5 (since 5 > 0) */ +/* Case expression */ +result : case x of + 1 : "one" + 2 : "two" + _ : "other"; ``` - - -## Language Components - -### Lexer - -The lexer tokenizes input into meaningful units: -- Numbers: `123` -- Identifiers: `variable_name` -- Operators: `+`, `-`, `*`, `/` -- Keywords: `case`, `of`, `function` -- Symbols: `:`, `->`, `_` - -### Parser - -The parser builds an Abstract Syntax Tree (AST) from tokens, supporting: -- Number literals -- Arithmetic expressions -- Variable assignments -- Function declarations -- Function calls -- Case expressions -- Pattern matching - -### Interpreter - -The interpreter executes the AST with: -- Global scope management -- Function evaluation -- Pattern matching execution -- Arithmetic computation -- Immutable variable semantics - -## Example Programs - -### Basic Arithmetic and Variables - -``` -/* Simple arithmetic with variables */ -x : 5; -y : 10; -sum : x + y; -diff : x - y; -product : x * y; -quotient : y / x; -modulo : 17 % 5; /* Returns 2 */ -power : 2 ^ 3; /* Returns 8 */ - -..out sum; /* 15 */ -..out diff; /* -5 */ -..out product; /* 50 */ -..out quotient; /* 2 */ -..out modulo; /* 2 */ -..out power; /* 8 */ - -/* Grouped expressions with parentheses */ -grouped : (5 + 3) * 2; /* 16 */ -nested : ((5 + 3) * 2) + 1; /* 17 */ -complex : (2 ^ 3) % 3 and 5 > 3; /* true */ -``` - -### Function Definitions and Calls - -``` -/* Basic function definition */ -add : x y -> x + y; -multiply : x y -> x * y; -double : x -> x * 2; -square : x -> x * x; - -/* Function calls */ -result1 : add 3 4; /* 7 */ -result2 : multiply 5 6; /* 30 */ -result3 : double 8; /* 16 */ -result4 : square 4; /* 16 */ - -/* Function calls with parenthesized arguments */ -result5 : add (3 + 2) (4 + 1); /* 15 */ -result6 : add (double 3) (square 2); /* 10 */ -result7 : add (add 1 2) (add 3 4); /* 10 */ -``` - -### Tables and Data Structures - -``` -/* Create various table types */ -empty : {}; -numbers : {1, 2, 3, 4, 5}; -person : {name: "Alice", age: 30, active: true}; -mixed : {1, name: "Bob", 2, active: false}; - -/* Access array elements */ -first : numbers[1]; -second : numbers[2]; -last : numbers[5]; - -/* Access object properties */ -name : person.name; -age : person.age; -active : person.active; - -/* Access mixed table */ -first_mixed : mixed[1]; -name_mixed : mixed.name; -second_mixed : mixed[2]; - -/* Output results */ -..out "First number: "; -..out first; -..out "Person name: "; -..out name; -..out "Mixed first: "; -..out first_mixed; -..out "Mixed name: "; -..out name_mixed; +### IO Operations ``` +/* Output */ +..out "Hello, World!"; -### Pattern Matching with Case Expressions +/* Input */ +name : ..in; +/* Assertion */ +..assert x = 5; ``` -/* Pattern matching in function bodies */ -compare : x y -> - case x y of - 0 0 : "both zero" - 0 _ : "x is zero" - _ 0 : "y is zero" - _ _ : "neither zero"; - -/* Testing pattern matching */ -test1 : compare 0 0; /* "both zero" */ -test2 : compare 0 5; /* "x is zero" */ -test3 : compare 5 0; /* "y is zero" */ -test4 : compare 5 5; /* "neither zero" */ - -/* Single-parameter case expressions */ -factorial : n -> - case n of - 0 : 1 - _ : n * (factorial (n - 1)); - -/* Single-parameter grade function */ -grade : score -> - case score of - 90 : "A" - 80 : "B" - 70 : "C" - _ : "F"; - -fact5 : factorial 5; /* 120 */ -grade1 : grade 95; /* "A" */ -grade2 : grade 85; /* "B" */ -grade3 : grade 65; /* "F" */ -``` - -### First-Class Functions and Composition +### Standard Library ``` -/* Function references and composition */ +/* Map */ double : x -> x * 2; -square : x -> x * x; -add1 : x -> x + 1; - -/* Using standard library functions */ -composed : compose @double @square 3; /* double(square(3)) = 18 */ -piped : pipe @double @square 2; /* square(double(2)) = 16 */ -applied : apply @double 5; /* double(5) = 10 */ - -/* Higher-order functions */ -mapped1 : map @double 5; /* 10 */ -mapped2 : map @square 3; /* 9 */ - -/* Function references in case expressions */ -getFunction : type -> - case type of - "double" : @double - "square" : @square - _ : @add1; - -func1 : getFunction "double"; /* Function reference */ -func2 : getFunction "square"; /* Function reference */ -``` - -### Recursion and Complex Functions - -``` -/* Recursive factorial function (using single-parameter case) */ -factorial : n -> - case n of - 0 : 1 - _ : n * (factorial (n - 1)); - -/* Recursive countdown function */ -countdown : n -> - case n of - 0 : "done" - _ : countdown (n - 1); - -/* Testing recursive functions */ -fact5 : factorial 5; /* 120 */ -count : countdown 3; /* "done" */ -``` - -### Comparison and Logical Operators +squared : map @double 5; -``` -/* Comparison operators */ -a : 5; -b : 10; - -less : a < b; /* true */ -greater : b > a; /* true */ -equal : a = a; /* true */ -notEqual : a != b; /* true */ -lessEqual : a <= 5; /* true */ -greaterEqual : b >= 10; /* true */ - -/* Logical operators */ -logicalAnd : 1 and 1; /* true */ -logicalOr : 0 or 1; /* true */ -logicalXor : 1 xor 0; /* true */ -logicalNot : not 0; /* true */ - -/* Complex logical expressions */ -complex1 : a < b and b > 5; /* true */ -complex2 : a = 5 or b = 15; /* true */ -complex3 : not (a = b); /* true */ -``` - -### Input/Output and Assertions - -``` -/* Interactive input/output */ -..out "Enter your name: "; -name : ..in; -..out "Hello, "; -..out name; -..out "!"; - -/* Assertions for testing */ -x : 5; -y : 3; -sum : x + y; - -..assert x = 5; /* Passes */ -..assert y = 3; /* Passes */ -..assert sum = 8; /* Passes */ -..assert x > 3; /* Passes */ -..assert y < 10; /* Passes */ -..assert sum != 0; /* Passes */ -..assert (add 3 4) = 7; /* Passes (with parentheses) */ - -/* String comparisons */ -..assert "hello" = "hello"; /* Passes */ -..assert "world" != "hello"; /* Passes */ -``` - -### Standard Library Functions - -``` -/* Using all standard library functions */ -double : x -> x * 2; -square : x -> x * x; -add : x y -> x + y; +/* Filter */ isPositive : x -> x > 0; +filtered : filter @isPositive 5; -/* Map function */ -mapped1 : map @double 5; /* 10 */ -mapped2 : map @square 3; /* 9 */ - -/* Compose function */ -composed : compose @double @square 3; /* 18 */ - -/* Pipe function */ -piped : pipe @double @square 2; /* 16 */ - -/* Apply function */ -applied : apply @double 7; /* 14 */ - -/* Filter function */ -filtered : filter @isPositive 5; /* 5 (since 5 > 0) */ -filtered2 : filter @isPositive -3; /* 0 (since -3 <= 0) */ - -/* Reduce and Fold functions */ -reduced : reduce @add 0 5; /* 5 */ -folded : fold @add 0 5; /* 5 */ - -/* Curry function (explicit currying) */ -curried : curry @add 3 4; /* 7 */ -``` - -### Complete Program Example - -``` -/* A complete program demonstrating multiple features */ -..out "Welcome to the Calculator!"; - -/* Define arithmetic functions */ -add : x y -> x + y; -subtract : x y -> x - y; -multiply : x y -> x * y; -divide : x y -> x / y; - -/* Define utility functions */ -double : x -> x * 2; -square : x -> x * x; -isEven : x -> x % 2 = 0; - -/* Get user input */ -..out "Enter first number: "; -num1 : ..in; -..out "Enter second number: "; -num2 : ..in; - -/* Perform calculations */ -sum : add num1 num2; -diff : subtract num1 num2; -prod : multiply num1 num2; -quot : divide num1 num2; - -/* Display results */ -..out "Sum: "; -..out sum; -..out "Difference: "; -..out diff; -..out "Product: "; -..out prod; -..out "Quotient: "; -..out quot; - -/* Use higher-order functions */ -doubledSum : double sum; -squaredDiff : square diff; - -..out "Doubled sum: "; -..out doubledSum; -..out "Squared difference: "; -..out squaredDiff; - -/* Pattern matching for number classification */ -classify : num -> - case num of - 0 : "zero" - _ : case isEven num of - true : "even" - _ : "odd"; - -classification : classify num1; -..out "First number is: "; -..out classification; - -/* Assertions to verify calculations */ -..assert sum = add num1 num2; -..assert diff = subtract num1 num2; -..assert prod = multiply num1 num2; - -..out "All calculations verified!"; +/* Compose */ +f : x -> x + 1; +g : x -> x * 2; +h : compose @f @g; +result : h 5; /* (5 * 2) + 1 = 11 */ ``` -## Running Programs - -The language supports two modes of execution: - -### File Execution Mode -Run a script file by providing the file path as an argument: +## Usage +### Running Scripts ```bash node lang.js script.txt ``` -### REPL Mode -Start an interactive REPL by running without arguments: - +### Testing +The project uses a structured testing approach with unit and integration tests: + +#### Unit Tests +Located in `tests/` directory, each focusing on a specific language feature: +- `01_lexer_basic.txt` - Basic lexer functionality +- `02_arithmetic_operations.txt` - Arithmetic operations +- `03_comparison_operators.txt` - Comparison operators +- `04_logical_operators.txt` - Logical operators +- `05_io_operations.txt` - IO operations +- `06_function_definitions.txt` - Function definitions +- `07_case_expressions.txt` - Case expressions and pattern matching +- `08_first_class_functions.txt` - First-class function features +- `09_tables.txt` - Table literals and access +- `10_standard_library.txt` - Standard library functions + +#### Integration Tests +Test combinations of multiple features: +- `integration_01_basic_features.txt` - Basic feature combinations +- `integration_02_pattern_matching.txt` - Pattern matching with other features +- `integration_03_functional_programming.txt` - Functional programming patterns + +#### Running Tests ```bash -node lang.js -``` - -In REPL mode: -- Type expressions and end them with a semicolon (`;`) to execute -- Multi-line expressions are supported -- Type `exit` or `quit` to exit -- Variables and functions persist between expressions +# Run all tests +./run_tests.sh - -## Language Design Principles - -- **Simplicity**: Minimal syntax for maximum clarity -- **Immutability**: Variables cannot be reassigned -- **Functional**: Functions are first-class values with composition support -- **Pattern-oriented**: Pattern matching as a core feature -- **Recursive**: Support for recursive function calls in function bodies -- **Grouped**: Parentheses support for complex arithmetic expressions -- **Expressive**: Rich set of arithmetic, comparison, and logical operators -- **IO-aware**: Built-in input/output operations with clear impurity markers -- **Educational**: Designed to teach language implementation concepts - -## Limitations - -This is a learning language with intentional limitations: -- Complex nested function calls (e.g., `add double 3 square 2`) are ambiguous without explicit grouping -- There's one known issue with function calls inside assertions (e.g., ..assert add 3 4 = 7), which is a parsing edge case. Workaround: use parentheses around function calls in assertions (e.g., ..assert (add 3 4) = 7) -- Limited to immutable data structures (no mutation operations) -- No error handling beyond basic validation -- No modules or imports -- IO operations are synchronous and block execution - -## Future Enhancements - -Planned features for future versions: -- Ambiguous function call detection and error reporting -- Lists and data structures -- Error handling -- Modules and imports -- Performance optimizations +# Run individual tests +node lang.js tests/01_lexer_basic.txt +node lang.js tests/integration_01_basic_features.txt +``` ## Implementation Details -The language is implemented in JavaScript with three main components: +### Architecture +- **Lexer**: Tokenizes input into tokens (numbers, identifiers, operators, etc.) +- **Parser**: Builds Abstract Syntax Tree (AST) from tokens +- **Interpreter**: Executes AST with scope management + +### Key Components +- **Token Types**: Supports all basic operators, literals, and special tokens +- **AST Nodes**: Expression, statement, and declaration nodes +- **Scope Management**: Lexical scoping with proper variable resolution +- **Error Handling**: Comprehensive error reporting for parsing and execution + +## Recent Fixes + +### ✅ Parser Ambiguity with Unary Minus Arguments (Latest Fix) +- **Issue**: `filter @isPositive -3` was incorrectly parsed as binary operation instead of function call with unary minus argument +- **Root Cause**: Parser treating `FunctionReference MINUS` as binary minus operation +- **Solution**: Added special case in `parseExpression()` to handle `FunctionReference MINUS` pattern +- **Status**: ✅ Resolved - Standard library functions now work with negative arguments + +### ✅ Unary Minus Operator +- **Issue**: Stack overflow when parsing negative numbers (e.g., `-1`) +- **Root Cause**: Parser lacked specific handling for unary minus operator +- **Solution**: Added `UnaryMinusExpression` parsing and evaluation +- **Status**: ✅ Resolved - All tests passing + +### ✅ IO Operation Parsing +- **Issue**: IO operations not parsed correctly at top level +- **Solution**: Moved IO parsing to proper precedence level +- **Status**: ✅ Resolved + +### ✅ Decimal Number Support +- **Issue**: Decimal numbers not handled correctly +- **Solution**: Updated lexer and interpreter to use `parseFloat()` +- **Status**: ✅ Resolved + +## Known Issues + +### 🔄 Logical Operator Precedence +- **Issue**: Logical operators (`and`, `or`, `xor`) have incorrect precedence relative to function calls +- **Example**: `isEven 10 and isPositive 5` is parsed as `isEven(10 and isPositive(5))` instead of `(isEven 10) and (isPositive 5)` +- **Impact**: Complex expressions with logical operators may not evaluate correctly +- **Status**: 🔄 In Progress - Working on proper operator precedence hierarchy +- **Workaround**: Use parentheses to explicitly group expressions: `(isEven 10) and (isPositive 5)` + +### 🔄 Parentheses Parsing with Logical Operators +- **Issue**: Some expressions with logical operators inside parentheses fail to parse +- **Example**: `add (multiply 3 4) (isEven 10 and isPositive 5)` may fail with parsing errors +- **Status**: 🔄 In Progress - Related to logical operator precedence issue + +## Development + +### File Structure +``` +. +├── lang.js # Main implementation +├── test.txt # Comprehensive test file +├── tests/ # Unit and integration tests +│ ├── 01_lexer_basic.txt +│ ├── 02_arithmetic_operations.txt +│ ├── ... +│ ├── integration_01_basic_features.txt +│ ├── integration_02_pattern_matching.txt +│ └── integration_03_functional_programming.txt +├── run_tests.sh # Test runner script +├── FIXME.md # Issues and fixes documentation +└── README.md # This file +``` + +### Debugging +Enable debug mode by setting `DEBUG=true`: +```bash +DEBUG=true node lang.js script.txt +``` -1. **Lexer** (`lexer()`): Converts source code to tokens -2. **Parser** (`parser()`): Builds AST from tokens -3. **Interpreter** (`interpreter()`): Executes AST +## Contributing -Each component is designed to be modular and extensible for adding new language features. \ No newline at end of file +1. Create focused unit tests for new features +2. Add integration tests for feature combinations +3. Update documentation +4. Run the full test suite before submitting changes \ No newline at end of file diff --git a/js/scripting-lang/comprehensive_test.txt b/js/scripting-lang/comprehensive_test.txt deleted file mode 100644 index c2db63f..0000000 --- a/js/scripting-lang/comprehensive_test.txt +++ /dev/null @@ -1,155 +0,0 @@ -x : 5; -y : 3; -name : "Alice"; -age : 25; - -..out "Basic arithmetic:"; -sum : x + y; -diff : x - y; -product : x * y; -quotient : x / y; -modulo : 17 % 5; -power : 2 ^ 3; -..out sum; -..out diff; -..out product; -..out quotient; -..out modulo; -..out power; - -..out "Variables:"; -..out name; -..out age; - -add : x y -> x + y; -double : x -> x * 2; -square : x -> x * x; - -..out "Functions:"; -result1 : add 3 4; -result2 : double 5; -result3 : square 4; -..out result1; -..out result2; -..out result3; - -..out "Function calls with parentheses:"; -result4 : add (3 + 2) (4 + 1); -result5 : add (double 3) (square 2); -..out result4; -..out result5; - -..out "Testing assertions:"; -..assert x = 5; -..assert y = 3; -..assert sum = 8; -..assert "hello" = "hello"; -..assert (add 3 4) = 7; - -..out "Testing first-class functions:"; - -composed : compose @double @square 3; -applied : apply @double 5; - -..assert composed = 18; -..assert applied = 10; - -..out "First-class functions:"; -..out composed; -..out applied; - -..out "Testing standard library functions:"; -slResult1 : map @double 5; -slResult2 : map @square 3; -piped : pipe @double @square 2; -isPositive : x -> x > 0; -filtered : filter @isPositive 5; -reduced : reduce @add 0 5; -folded : fold @add 0 5; - -..assert (map @double 5) = 10; -..assert (map @square 3) = 9; -..assert (pipe @double @square 2) = 16; -..assert (filter @isPositive 5) = 5; -..assert (reduce @add 0 5) = 5; -..assert (fold @add 0 5) = 5; - -..out "Standard library results:"; -..out slResult1; -..out slResult2; -..out piped; -..out filtered; -..out reduced; -..out folded; - -..out "Testing higher-order functions:"; -mapped1 : map @double 3; -mapped2 : map @square 4; - -..assert mapped1 = 6; -..assert mapped2 = 16; - -..out "Map results:"; -..out mapped1; -..out mapped2; - -..out "Testing case expressions:"; -compare : x y -> - case x y of - 0 0 : "both zero" - 0 _ : "x is zero" - _ 0 : "y is zero" - _ _ : "neither zero"; - -test1 : compare 0 0; -..out test1; - - - -..out "Testing standalone case expressions with function references:"; -case 0 of - 0 : @double - _ : @square; - -..out "Standalone case with function references works!"; - -..out "Testing parentheses grouping:"; -grouped1 : (5 + 3) * 2; -grouped2 : ((5 + 3) * 2) + 1; - -..assert grouped1 = 16; -..assert grouped2 = 17; - -..out "Parentheses grouping works!"; - -..out "Testing comparison operators:"; -lessTest : 3 < 5; -greaterTest : 10 > 5; -equalTest : 5 = 5; -notEqualTest : 3 != 5; -lessEqualTest : 5 <= 5; -greaterEqualTest : 5 >= 3; -..out lessTest; -..out greaterTest; -..out equalTest; -..out notEqualTest; -..out lessEqualTest; -..out greaterEqualTest; - -..out "Testing logical operators:"; -logicalAnd : 1 and 1; -logicalOr : 0 or 1; -logicalXor : 1 xor 0; -logicalNot : not 0; -..out logicalAnd; -..out logicalOr; -..out logicalXor; -..out logicalNot; - -..out "Testing complex expressions:"; -complex1 : 2 ^ 3 % 3 and 5 > 3; -..out complex1; - - - -..out "All tests completed successfully!"; \ No newline at end of file diff --git a/js/scripting-lang/debug_test.txt b/js/scripting-lang/debug_test.txt new file mode 100644 index 0000000..246c3fd --- /dev/null +++ b/js/scripting-lang/debug_test.txt @@ -0,0 +1,7 @@ +/* Simple test for case expressions */ +factorial : n -> case n of + 0 : 1 + _ : n * (factorial (n - 1)); + +test : factorial 5; +..out test; \ No newline at end of file diff --git a/js/scripting-lang/final_table_case_test.txt b/js/scripting-lang/final_table_case_test.txt deleted file mode 100644 index 50c33a8..0000000 --- a/js/scripting-lang/final_table_case_test.txt +++ /dev/null @@ -1,82 +0,0 @@ -/* Final comprehensive test of tables and case expressions */ - -/* ===== BASIC TABLE FUNCTIONALITY ===== */ -/* 1. Table creation and access */ -numbers : {1: 10, 2: 20, 3: 30}; -person : {name: "Alice", age: 30, active: true}; - -/* 2. Table access works */ -first : numbers[1]; -name : person.name; -age : person.age; -active : person.active; - -/* ===== CASE EXPRESSIONS WITH TABLES ===== */ -/* 3. Case expressions work with table values */ -result1 : case age of - 0 : 30 : "Person is 30" - 1 : _ : "Person is different age" -; - -/* 4. Case expressions work with boolean values */ -result2 : case active of - 0 : true : "Person is active" - 1 : false : "Person is inactive" -; - -/* 5. Case expressions work with array-like access */ -result3 : case numbers[2] of - 0 : 20 : "Second element is 20" - 1 : _ : "Unexpected second element" -; - -/* ===== FUNCTION REFERENCES IN TABLES ===== */ -/* 6. Functions can be stored in tables (but not called directly) */ -add : x y -> x + y; -multiply : x y -> x * y; -math_ops : {add: add, multiply: multiply}; - -/* ===== NESTED TABLES ===== */ -/* 7. Nested table access works */ -config : { - user: {name: "Bob", preferences: {theme: "dark"}}, - settings: {debug: true} -}; - -nested_name : config.user.name; -nested_theme : config.user.preferences.theme; -nested_debug : config.settings.debug; - -/* 8. Case expressions with nested table access */ -result4 : case nested_theme of - 0 : "dark" : "Dark theme enabled" - 1 : "light" : "Light theme enabled" - 2 : _ : "Unknown theme" -; - -result5 : case nested_debug of - 0 : true : "Debug mode enabled" - 1 : false : "Debug mode disabled" -; - -/* ===== OUTPUT RESULTS ===== */ -..out "Basic table access:"; -..out first; -..out name; -..out age; -..out active; - -..out "Case expressions with tables:"; -..out result1; -..out result2; -..out result3; -..out result4; -..out result5; - -..out "Nested table access:"; -..out nested_name; -..out nested_theme; -..out nested_debug; - -..out "Function references in tables (shows current limitation):"; -..out math_ops.add; \ No newline at end of file diff --git a/js/scripting-lang/func_call_test.txt b/js/scripting-lang/func_call_test.txt new file mode 100644 index 0000000..826eb4e --- /dev/null +++ b/js/scripting-lang/func_call_test.txt @@ -0,0 +1,8 @@ +/* Test function calls in case expressions */ +add : x y -> x + y; +factorial : n -> case n of + 0 : 1 + _ : add n 1; + +test : factorial 5; +..out test; \ No newline at end of file diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index 0184c8f..1a0d77e 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,10 +1,29 @@ -// The goal here is less to make anything useful...or even something that works, but to learn what parts an interpreted languages needs to have to function. - -// Initialize standard library functions +/** + * Initializes the standard library in the provided scope. + * + * @param {Object} scope - The global scope object to inject functions into + * + * @description Injects higher-order functions into the interpreter's global scope. + * These functions provide functional programming utilities like map, compose, pipe, etc. + * + * @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. + * + * @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. + */ function initializeStandardLibrary(scope) { - // Map: Apply a function to each element + /** + * Map: Apply a function to a value + * @param {Function} f - Function to apply + * @param {*} x - Value to apply function to + * @returns {*} Result of applying f to x + * @throws {Error} When first argument is not a function + */ scope.map = function(f, x) { - // Handle function references by calling them if they're functions if (typeof f === 'function') { return f(x); } else { @@ -12,17 +31,36 @@ function initializeStandardLibrary(scope) { } }; - // Compose: Compose two functions (f ∘ g)(x) = f(g(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 + */ scope.compose = function(f, g, x) { if (typeof f === 'function' && typeof g === 'function') { - return f(g(x)); + if (arguments.length === 3) { + return f(g(x)); + } else { + return function(x) { + return f(g(x)); + }; + } } else { throw new Error('compose: first two arguments must be functions'); } }; - // Curry: Convert a function that takes multiple arguments into a series of functions - // Since our language already uses curried functions by default, this is mostly for explicit currying + /** + * Curry: Apply a function to arguments (simplified currying) + * @param {Function} f - Function to curry + * @param {*} x - First argument + * @param {*} y - Second argument + * @returns {*} Result of applying f to x and y + * @throws {Error} When first argument is not a function + */ scope.curry = function(f, x, y) { if (typeof f === 'function') { return f(x, y); @@ -31,7 +69,13 @@ function initializeStandardLibrary(scope) { } }; - // Apply: Apply a function to an argument (same as function call, but more explicit) + /** + * Apply: Apply a function to an argument (explicit function application) + * @param {Function} f - Function to apply + * @param {*} x - Argument to apply function to + * @returns {*} Result of applying f to x + * @throws {Error} When first argument is not a function + */ scope.apply = function(f, x) { if (typeof f === 'function') { return f(x); @@ -40,18 +84,35 @@ function initializeStandardLibrary(scope) { } }; - // Pipe: Compose functions in left-to-right order (opposite of compose) - // pipe f g x = g 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 + */ scope.pipe = function(f, g, x) { if (typeof f === 'function' && typeof g === 'function') { - return g(f(x)); + if (arguments.length === 3) { + return g(f(x)); + } else { + return function(x) { + return g(f(x)); + }; + } } else { throw new Error('pipe: first two arguments must be functions'); } }; - // Filter: Filter based on a predicate - // For now, we'll implement it as a higher-order function + /** + * Filter: Filter a value based on a predicate + * @param {Function} p - Predicate function + * @param {*} x - Value to test + * @returns {*|0} The value if predicate is true, 0 otherwise + * @throws {Error} When first argument is not a function + */ scope.filter = function(p, x) { if (typeof p === 'function') { return p(x) ? x : 0; @@ -60,8 +121,14 @@ function initializeStandardLibrary(scope) { } }; - // Reduce: Reduce to a single value using a binary function - // For now, we'll implement it as a higher-order function + /** + * Reduce: Reduce two values using a binary function + * @param {Function} f - Binary function + * @param {*} init - Initial value + * @param {*} x - Second value + * @returns {*} Result of applying f to init and x + * @throws {Error} When first argument is not a function + */ scope.reduce = function(f, init, x) { if (typeof f === 'function') { return f(init, x); @@ -70,7 +137,14 @@ function initializeStandardLibrary(scope) { } }; - // Fold: Same as reduce, but more explicit about the folding direction + /** + * Fold: Same as reduce, but more explicit about the folding direction + * @param {Function} f - Binary function + * @param {*} init - Initial value + * @param {*} x - Second value + * @returns {*} Result of applying f to init and x + * @throws {Error} When first argument is not a function + */ scope.fold = function(f, init, x) { if (typeof f === 'function') { return f(init, x); @@ -80,7 +154,17 @@ function initializeStandardLibrary(scope) { }; } -// Define the types of tokens +/** + * TokenType enumeration for all supported token types. + * + * @type {Object.<string, string>} + * + * @description A flat object mapping token names to their string representations. + * This approach allows for fast string comparisons and easy extensibility. + * + * @why Using a flat object avoids the need for import/export or enum boilerplate, + * and makes it easy to add new token types as the language evolves. + */ const TokenType = { NUMBER: 'NUMBER', PLUS: 'PLUS', @@ -98,2289 +182,2548 @@ const TokenType = { RIGHT_PAREN: 'RIGHT_PAREN', LEFT_BRACE: 'LEFT_BRACE', RIGHT_BRACE: 'RIGHT_BRACE', + LEFT_BRACKET: 'LEFT_BRACKET', + RIGHT_BRACKET: 'RIGHT_BRACKET', SEMICOLON: 'SEMICOLON', - IO_IN: 'IO_IN', - IO_OUT: 'IO_OUT', - IO_ASSERT: 'IO_ASSERT', - EQUALS: 'EQUALS', - FUNCTION_REF: 'FUNCTION_REF', + COMMA: 'COMMA', + DOT: 'DOT', STRING: 'STRING', - // New arithmetic operators - MODULO: 'MODULO', - POWER: 'POWER', - // New comparison operators + TRUE: 'TRUE', + FALSE: 'FALSE', + AND: 'AND', + OR: 'OR', + XOR: 'XOR', + NOT: 'NOT', + EQUALS: 'EQUALS', LESS_THAN: 'LESS_THAN', GREATER_THAN: 'GREATER_THAN', LESS_EQUAL: 'LESS_EQUAL', GREATER_EQUAL: 'GREATER_EQUAL', NOT_EQUAL: 'NOT_EQUAL', - // New logical operators - AND: 'AND', - OR: 'OR', - XOR: 'XOR', - NOT: 'NOT', - // Comments - COMMENT: 'COMMENT', - // Table syntax - COLON: 'COLON', - COMMA: 'COMMA', - LEFT_BRACKET: 'LEFT_BRACKET', - RIGHT_BRACKET: 'RIGHT_BRACKET', - DOT: 'DOT', - // Boolean literals - TRUE: 'TRUE', - FALSE: 'FALSE', + MODULO: 'MODULO', + POWER: 'POWER', + IO_IN: 'IO_IN', + IO_OUT: 'IO_OUT', + IO_ASSERT: 'IO_ASSERT', + FUNCTION_REF: 'FUNCTION_REF' }; -// Lexer +/** + * Lexer: Converts source code to tokens. + * + * @param {string} input - Source code to tokenize + * @returns {Array.<Object>} Array of token objects with type and value properties + * @throws {Error} For unterminated strings or unexpected characters + * + * @description Performs lexical analysis by converting source code into a stream of tokens. + * Handles whitespace, nested comments, numbers (integers and decimals), strings, + * identifiers/keywords, and both single- and multi-character operators. + * + * @how Uses a single pass with a while loop and manual character inspection. + * Each character is examined to determine the appropriate token type, with + * special handling for multi-character tokens and nested constructs. + * + * @why Manual lexing allows for fine-grained control over tokenization, especially + * for edge cases like nested comments and multi-character IO operations. This + * approach also makes it easier to debug and extend the lexer for new language features. + * + * @note IO operations (..in, ..out, ..assert) are recognized as multi-character + * tokens to avoid ambiguity with the dot operator. Decimal numbers are parsed + * as a single token to support floating point arithmetic. + */ function lexer(input) { - debugLog('Starting lexer with input', input); - const tokens = []; let current = 0; - + const tokens = []; + while (current < input.length) { let char = input[current]; - - // Handle comments /* ... */ - if (input.slice(current, current + 2) === '/*') { - let commentContent = ''; - let commentDepth = 1; // Track nested comment depth + + // Skip whitespace + if (/\s/.test(char)) { + current++; + continue; + } + + // Handle nested comments: /* ... */ with support for /* /* ... */ */ + if (char === '/' && input[current + 1] === '*') { + let commentDepth = 1; current += 2; // Skip /* - // Find the closing */, handling nested comments while (current < input.length && commentDepth > 0) { - if (input.slice(current, current + 2) === '/*') { + if (input[current] === '/' && input[current + 1] === '*') { commentDepth++; - commentContent += input[current]; - commentContent += input[current + 1]; current += 2; - } else if (input.slice(current, current + 2) === '*/') { + } else if (input[current] === '*' && input[current + 1] === '/') { commentDepth--; - if (commentDepth > 0) { - commentContent += input[current]; - commentContent += input[current + 1]; - } current += 2; } else { - commentContent += input[current]; current++; } } - - if (commentDepth === 0) { - // Don't add comment tokens to the output - they're ignored - continue; - } else { - throw new Error('Unterminated comment: missing */'); - } - } - - if (/\d/.test(char)) { - let value = ''; - while (/\d/.test(char)) { - value += char; - char = input[++current]; - } - tokens.push({ - type: TokenType.NUMBER, - value - }); - continue; - } - - if (char === '+') { - tokens.push({ - type: TokenType.PLUS - }); - current++; - continue; - } - - if (input.slice(current, current + 2) === '->') { - tokens.push({ - type: TokenType.ARROW - }); - current += 2; - continue; - } - - if (char === '-') { - tokens.push({ - type: TokenType.MINUS - }); - current++; - continue; - } - - if (char === '*') { - tokens.push({ - type: TokenType.MULTIPLY - }); - current++; - continue; - } - - if (char === '/') { - tokens.push({ - type: TokenType.DIVIDE - }); - current++; continue; } - - if (char === '%') { - tokens.push({ - type: TokenType.MODULO - }); - current++; - continue; - } - - if (char === '^') { - tokens.push({ - type: TokenType.POWER - }); - current++; - continue; - } - - if (char === '<') { - if (input.slice(current, current + 2) === '<=') { - tokens.push({ - type: TokenType.LESS_EQUAL - }); - current += 2; - } else { - tokens.push({ - type: TokenType.LESS_THAN - }); + + // Parse numbers (integers and decimals) + if (/[0-9]/.test(char)) { + let value = ''; + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; current++; } - continue; - } - - if (char === '>') { - if (input.slice(current, current + 2) === '>=') { + + // Check for decimal point + if (current < input.length && input[current] === '.') { + value += input[current]; + current++; + + // Parse decimal part + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; + current++; + } + tokens.push({ - type: TokenType.GREATER_EQUAL + type: TokenType.NUMBER, + value: parseFloat(value) }); - current += 2; } else { tokens.push({ - type: TokenType.GREATER_THAN + type: TokenType.NUMBER, + value: parseInt(value) }); - current++; - } - continue; - } - - if (input.slice(current, current + 2) === '!=') { - tokens.push({ - type: TokenType.NOT_EQUAL - }); - current += 2; - continue; - } - - // Check for keywords before general identifiers - if (input.slice(current, current + 4) === 'case') { - tokens.push({ - type: TokenType.CASE - }); - current += 4; - continue; - } - - if (input.slice(current, current + 2) === 'of') { - tokens.push({ - type: TokenType.OF - }); - current += 2; - continue; - } - - // Check for logical keywords - if (input.slice(current, current + 3) === 'and') { - tokens.push({ - type: TokenType.AND - }); - current += 3; - continue; - } - - if (input.slice(current, current + 2) === 'or') { - tokens.push({ - type: TokenType.OR - }); - current += 2; - continue; - } - - if (input.slice(current, current + 3) === 'xor') { - tokens.push({ - type: TokenType.XOR - }); - current += 3; - continue; - } - - if (input.slice(current, current + 3) === 'not') { - tokens.push({ - type: TokenType.NOT - }); - current += 3; - continue; - } - - - - // Check for boolean literals (must come before identifier parsing) - if (input.slice(current, current + 4) === 'true') { - tokens.push({ - type: TokenType.TRUE - }); - current += 4; - continue; - } - - if (input.slice(current, current + 5) === 'false') { - tokens.push({ - type: TokenType.FALSE - }); - current += 5; - continue; - } - - if (/[a-z]/i.test(char)) { - let value = ''; - while (/[a-z0-9_]/i.test(char)) { - value += char; - char = input[++current]; } - tokens.push({ - type: TokenType.IDENTIFIER, - value - }); - continue; - } - - if (char === ':') { - // Check if this is a table key-value separator (inside braces) - // For now, we'll use ASSIGNMENT for both and handle the distinction in the parser - tokens.push({ - type: TokenType.ASSIGNMENT - }); - current++; continue; } - - if (input.slice(current, current + 2) === '==') { - tokens.push({ - type: TokenType.EQUALS - }); - current += 2; - continue; - } - - if (char === '@') { - tokens.push({ - type: TokenType.FUNCTION_REF - }); - current++; - continue; - } - - if (char === '=') { - tokens.push({ - type: TokenType.EQUALS - }); - current++; - continue; - } - - - - if (char === '_') { - tokens.push({ - type: TokenType.WILDCARD - }); - current++; - continue; - } - - if (char === '(') { - tokens.push({ - type: TokenType.LEFT_PAREN - }); - current++; - continue; - } - - if (char === ')') { - tokens.push({ - type: TokenType.RIGHT_PAREN - }); - current++; - continue; - } - - if (char === '{') { - tokens.push({ - type: TokenType.LEFT_BRACE - }); - current++; - continue; - } - - if (char === '}') { - tokens.push({ - type: TokenType.RIGHT_BRACE - }); - current++; - continue; - } - - if (char === ',') { - tokens.push({ - type: TokenType.COMMA - }); - current++; - continue; - } - - if (char === '[') { - tokens.push({ - type: TokenType.LEFT_BRACKET - }); - current++; - continue; - } - - if (char === ']') { - tokens.push({ - type: TokenType.RIGHT_BRACKET - }); - current++; - continue; - } - - // Check for IO tokens (must come before individual dot handling) - if (input.slice(current, current + 4) === '..in') { - tokens.push({ - type: TokenType.IO_IN - }); - current += 4; - continue; - } - - if (input.slice(current, current + 5) === '..out') { - tokens.push({ - type: TokenType.IO_OUT - }); - current += 5; - continue; - } - - if (input.slice(current, current + 8) === '..assert') { - tokens.push({ - type: TokenType.IO_ASSERT - }); - current += 8; - continue; - } - - if (char === '.') { - tokens.push({ - type: TokenType.DOT - }); - current++; - continue; - } - - if (input.slice(current, current + 8) === 'function') { - tokens.push({ - type: TokenType.FUNCTION - }); - current += 8; - continue; - } - - // Handle string literals + + // Parse string literals if (char === '"') { let value = ''; current++; // Skip opening quote + while (current < input.length && input[current] !== '"') { value += input[current]; current++; } - if (current < input.length && input[current] === '"') { + + if (current < input.length) { current++; // Skip closing quote tokens.push({ type: TokenType.STRING, - value + value: value }); - continue; } else { - throw new Error('Unterminated string literal'); + throw new Error('Unterminated string'); } + continue; } - - if (char === ';') { - tokens.push({ type: TokenType.SEMICOLON }); - current++; + + // Parse identifiers and keywords + if (/[a-zA-Z_]/.test(char)) { + let value = ''; + while (current < input.length && /[a-zA-Z0-9_]/.test(input[current])) { + value += input[current]; + current++; + } + + // Check for keywords + switch (value) { + case 'case': + tokens.push({ type: TokenType.CASE }); + break; + case 'of': + tokens.push({ type: TokenType.OF }); + break; + case 'function': + tokens.push({ type: TokenType.FUNCTION }); + break; + case 'true': + tokens.push({ type: TokenType.TRUE }); + break; + case 'false': + tokens.push({ type: TokenType.FALSE }); + break; + case 'and': + tokens.push({ type: TokenType.AND }); + break; + case 'or': + tokens.push({ type: TokenType.OR }); + break; + case 'xor': + tokens.push({ type: TokenType.XOR }); + break; + case 'not': + tokens.push({ type: TokenType.NOT }); + break; + case '_': + tokens.push({ type: TokenType.WILDCARD }); + break; + default: + tokens.push({ + type: TokenType.IDENTIFIER, + value: value + }); + } continue; } - + + // Parse two-character operators + if (current + 1 < input.length) { + const twoChar = char + input[current + 1]; + switch (twoChar) { + case '->': + tokens.push({ type: TokenType.ARROW }); + current += 2; + continue; + case '==': + tokens.push({ type: TokenType.EQUALS }); + current += 2; + continue; + case '!=': + tokens.push({ type: TokenType.NOT_EQUAL }); + current += 2; + continue; + case '<=': + tokens.push({ type: TokenType.LESS_EQUAL }); + current += 2; + continue; + case '>=': + tokens.push({ type: TokenType.GREATER_EQUAL }); + current += 2; + continue; + case '..': + // Parse IO operations: ..in, ..out, ..assert + if (current + 2 < input.length) { + const ioChar = input[current + 2]; + switch (ioChar) { + case 'i': + if (current + 3 < input.length && input[current + 3] === 'n') { + tokens.push({ type: TokenType.IO_IN }); + current += 4; + continue; + } + break; + case 'o': + if (current + 3 < input.length && input[current + 3] === 'u') { + if (current + 4 < input.length && input[current + 4] === 't') { + tokens.push({ type: TokenType.IO_OUT }); + current += 5; + continue; + } + } + break; + case 'a': + if (current + 3 < input.length && input[current + 3] === 's') { + if (current + 4 < input.length && input[current + 4] === 's') { + if (current + 5 < input.length && input[current + 5] === 'e') { + if (current + 6 < input.length && input[current + 6] === 'r') { + if (current + 7 < input.length && input[current + 7] === 't') { + tokens.push({ type: TokenType.IO_ASSERT }); + current += 8; + continue; + } + } + } + } + } + break; + } + } + // If we get here, it's not a complete IO operation, so skip the '..' + current += 2; + continue; + } + } + + // Parse single character operators + switch (char) { + case '+': + tokens.push({ type: TokenType.PLUS }); + break; + case '-': + tokens.push({ type: TokenType.MINUS }); + break; + case '*': + tokens.push({ type: TokenType.MULTIPLY }); + break; + case '/': + tokens.push({ type: TokenType.DIVIDE }); + break; + case '%': + tokens.push({ type: TokenType.MODULO }); + break; + case '^': + tokens.push({ type: TokenType.POWER }); + break; + case ':': + tokens.push({ type: TokenType.ASSIGNMENT }); + break; + case '(': + tokens.push({ type: TokenType.LEFT_PAREN }); + break; + case ')': + tokens.push({ type: TokenType.RIGHT_PAREN }); + break; + case '{': + tokens.push({ type: TokenType.LEFT_BRACE }); + break; + case '}': + tokens.push({ type: TokenType.RIGHT_BRACE }); + break; + case '[': + tokens.push({ type: TokenType.LEFT_BRACKET }); + break; + case ']': + tokens.push({ type: TokenType.RIGHT_BRACKET }); + break; + case ';': + tokens.push({ type: TokenType.SEMICOLON }); + break; + case ',': + tokens.push({ type: TokenType.COMMA }); + break; + case '.': + tokens.push({ type: TokenType.DOT }); + break; + case '@': + tokens.push({ type: TokenType.FUNCTION_REF }); + break; + case '_': + tokens.push({ type: TokenType.WILDCARD }); + break; + case '=': + tokens.push({ type: TokenType.EQUALS }); + break; + case '<': + tokens.push({ type: TokenType.LESS_THAN }); + break; + case '>': + tokens.push({ type: TokenType.GREATER_THAN }); + break; + default: + throw new Error(`Unexpected character: ${char}`); + } + current++; } + + return tokens; } -// Parser +/** + * Parser: Converts tokens to an Abstract Syntax Tree (AST). + * + * @param {Array.<Object>} tokens - Array of tokens from the lexer + * @returns {Object} Abstract Syntax Tree with program body + * @throws {Error} For parsing errors like unexpected tokens or missing delimiters + * + * @description Implements a recursive descent parser that builds an AST from tokens. + * Handles all language constructs including expressions, statements, function + * definitions, case expressions, table literals, and IO operations. + * + * @how Implements a recursive descent parser, with separate functions for each + * precedence level (expression, term, factor, primary). Handles chained table + * access, function calls, and complex constructs like case expressions and + * function definitions. + * + * @why Recursive descent is chosen for its clarity and flexibility, especially + * for a language with many context-sensitive constructs (e.g., case expressions, + * function definitions, chained access). The parser is structured to minimize + * circular dependencies and infinite recursion, with careful placement of IO + * and case expression parsing. + * + * @note The parser supports multi-parameter case expressions and function + * definitions, using lookahead to distinguish between assignments and function + * declarations. Table literals are parsed with support for both array-like and + * key-value entries, inspired by Lua. + */ function parser(tokens) { - debugLog('Starting parser with tokens', tokens); + let current = 0; + let parsingFunctionArgs = false; // Flag to track when we're parsing function arguments - + // Reset call stack tracker for parser + callStackTracker.reset(); - let current = 0; - - function walk() { - if (current >= tokens.length) { - return null; // Return null when there are no more tokens - } - - let token = tokens[current]; - - // Helper function to detect ambiguous nested function calls - function detectAmbiguousFunctionCalls() { - // Look ahead to see if we have a pattern like: func1 func2 arg1 func3 arg2 - // This indicates ambiguous nested function calls - let tempCurrent = current; - let functionCalls = []; + // Define all parsing functions outside of walk to avoid circular dependencies + + function parseChainedDotAccess(tableExpr) { + callStackTracker.push('parseChainedDotAccess', ''); + + try { + /** + * Handles chained dot access (e.g., table.key.subkey). + * + * @param {Object} tableExpr - The table expression to chain access from + * @returns {Object} AST node representing the chained access + * @throws {Error} When expected identifier is missing after dot + * + * @description Parses dot notation for table access, building a chain + * of TableAccess nodes for nested property access. + * + * @why Chained access is parsed iteratively rather than recursively to + * avoid deep call stacks and to allow for easy extension (e.g., supporting + * method calls in the future). + */ + let result = tableExpr; - while (tempCurrent < tokens.length && tokens[tempCurrent].type !== TokenType.SEMICOLON) { - if (tokens[tempCurrent].type === TokenType.IDENTIFIER) { - // Check if this identifier is followed by arguments (function call) - if (tempCurrent + 1 < tokens.length && - tokens[tempCurrent + 1].type !== TokenType.ASSIGNMENT && - tokens[tempCurrent + 1].type !== TokenType.SEMICOLON && - (tokens[tempCurrent + 1].type === TokenType.NUMBER || - tokens[tempCurrent + 1].type === TokenType.IDENTIFIER || - tokens[tempCurrent + 1].type === TokenType.LEFT_PAREN)) { - - functionCalls.push(tokens[tempCurrent].value); - - // Skip the function name and its arguments - tempCurrent++; // Skip function name - while (tempCurrent < tokens.length && - tokens[tempCurrent].type !== TokenType.SEMICOLON && - tokens[tempCurrent].type !== TokenType.RIGHT_PAREN) { - tempCurrent++; - } - } else { - tempCurrent++; + while (current < tokens.length && tokens[current].type === TokenType.DOT) { + current++; // Skip the dot + + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const key = { + type: 'Identifier', + value: tokens[current].value + }; + current++; + + result = { + type: 'TableAccess', + table: result, + key: key + }; + } else { + throw new Error('Expected identifier after dot'); + } + } + + return result; + } finally { + callStackTracker.pop(); + } + } + + function parseChainedTableAccess(tableExpr) { + callStackTracker.push('parseChainedTableAccess', ''); + + try { + /** + * Handles chained bracket and dot access (e.g., table[0].key). + * + * @param {Object} tableExpr - The table expression to chain access from + * @returns {Object} AST node representing the chained access + * @throws {Error} When expected closing bracket is missing + * + * @description Parses both bracket and dot notation for table access, + * supporting mixed access patterns like table[0].key. + * + * @why This function allows for flexible access patterns, supporting both + * array and object semantics. Chaining is handled by checking for further + * access tokens after each access. + */ + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + current++; // Skip '[' + const keyExpr = walk(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACKET) { + current++; // Skip ']' + + const access = { + type: 'TableAccess', + table: tableExpr, + key: keyExpr + }; + + // Check for chained access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + return parseChainedDotAccess(access); + } + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(access); } + + return access; } else { - tempCurrent++; + throw new Error('Expected closing bracket'); } } - // If we have more than 2 function calls in sequence, it's ambiguous - if (functionCalls.length > 2) { - throw new Error(`Ambiguous nested function calls detected: ${functionCalls.join(' ')}. Use parentheses to explicitly group function calls.`); + // Check for dot access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + const result = parseChainedDotAccess(tableExpr); + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(result); + } + + return result; } + + return tableExpr; + } finally { + callStackTracker.pop(); } - - // Helper function to parse function calls with proper precedence - function parseFunctionCall() { - if (token.type !== TokenType.IDENTIFIER) { - return null; + } + + function parseArgument() { + callStackTracker.push('parseArgument', ''); + + try { + const token = tokens[current]; + if (!token) { + throw new Error('Unexpected end of input'); } - - // Check if this is a function call (identifier followed by arguments) - if (tokens[current + 1] && - tokens[current + 1].type !== TokenType.ASSIGNMENT && - tokens[current + 1].type !== TokenType.PLUS && - tokens[current + 1].type !== TokenType.MINUS && - tokens[current + 1].type !== TokenType.MULTIPLY && - tokens[current + 1].type !== TokenType.DIVIDE && - tokens[current + 1].type !== TokenType.MODULO && - tokens[current + 1].type !== TokenType.POWER && - tokens[current + 1].type !== TokenType.EQUALS && - tokens[current + 1].type !== TokenType.LESS_THAN && - tokens[current + 1].type !== TokenType.GREATER_THAN && - tokens[current + 1].type !== TokenType.LESS_EQUAL && - tokens[current + 1].type !== TokenType.GREATER_EQUAL && - tokens[current + 1].type !== TokenType.NOT_EQUAL && - tokens[current + 1].type !== TokenType.AND && - tokens[current + 1].type !== TokenType.OR && - tokens[current + 1].type !== TokenType.XOR && - tokens[current + 1].type !== TokenType.NOT && - tokens[current + 1].type !== TokenType.COMPOSE && - tokens[current + 1].type !== TokenType.PIPE && - tokens[current + 1].type !== TokenType.APPLY && - tokens[current + 1].type !== TokenType.SEMICOLON && - (tokens[current + 1].type === TokenType.NUMBER || - tokens[current + 1].type === TokenType.IDENTIFIER || - tokens[current + 1].type === TokenType.LEFT_PAREN || - tokens[current + 1].type === TokenType.FUNCTION_REF)) { + + // Parse unary operators + if (token.type === TokenType.NOT) { + current++; + const operand = parseArgument(); + return { type: 'NotExpression', operand }; + } + + if (token.type === TokenType.MINUS) { + current++; + const operand = parseArgument(); + return { type: 'UnaryMinusExpression', operand }; + } + + // Parse literals + if (token.type === TokenType.NUMBER) { + current++; + return { type: 'NumberLiteral', value: token.value }; + } else if (token.type === TokenType.STRING) { + current++; + return { type: 'StringLiteral', value: token.value }; + } else if (token.type === TokenType.TRUE) { + current++; + return { type: 'BooleanLiteral', value: true }; + } else if (token.type === TokenType.FALSE) { + current++; + return { type: 'BooleanLiteral', value: false }; + } else if (token.type === TokenType.NULL) { + current++; + return { type: 'NullLiteral' }; + } else if (token.type === TokenType.WILDCARD) { + current++; + return { type: 'WildcardPattern' }; + } else if (token.type === TokenType.FUNCTION_REF) { + current++; + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const functionName = tokens[current].value; + current++; + return { type: 'FunctionReference', name: functionName }; + } else { + throw new Error('Expected function name after @'); + } + } else if (token.type === TokenType.IO_IN) { + current++; + return { type: 'IOInExpression' }; + } else if (token.type === TokenType.IO_OUT) { + current++; + const outputValue = parseLogicalExpression(); + return { type: 'IOOutExpression', value: outputValue }; + } else if (token.type === TokenType.IO_ASSERT) { + current++; + const assertionExpr = parseLogicalExpression(); + return { type: 'IOAssertExpression', value: assertionExpr }; + } + + // Parse identifiers (but NOT as function calls) + if (token.type === TokenType.IDENTIFIER) { + current++; + const identifier = { type: 'Identifier', value: token.value }; - + // Check for table access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + return parseChainedDotAccess(identifier); + } - const funcName = token.value; - current++; // Skip function name - const args = []; + // Check for table access with brackets + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + return parseChainedTableAccess(identifier); + } - // Collect arguments until we hit a semicolon, closing parenthesis, or end - while (current < tokens.length && - tokens[current].type !== TokenType.SEMICOLON && - tokens[current].type !== TokenType.RIGHT_PAREN) { - - // Handle function references (@functionName) - if (tokens[current] && tokens[current].type === TokenType.FUNCTION_REF) { - current++; // Skip @ - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - args.push({ - type: 'FunctionReference', - name: tokens[current].value, - }); - current++; + return identifier; + } + + // Parse parenthesized expressions + if (token.type === TokenType.LEFT_PAREN) { + current++; // Skip '(' + const parenthesizedExpr = parseLogicalExpression(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_PAREN) { + current++; // Skip ')' + return parenthesizedExpr; + } else { + throw new Error('Expected closing parenthesis'); + } + } + + // Parse table literals + if (token.type === TokenType.LEFT_BRACE) { + current++; // Skip '{' + const properties = []; + + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { + if (tokens[current].type === TokenType.IDENTIFIER) { + const key = tokens[current].value; + current++; + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + const value = parseLogicalExpression(); + properties.push({ key, value }); } else { - throw new Error('Expected function name after @'); + throw new Error('Expected ":" after property name in table literal'); } } else { - // Parse arguments with proper precedence - // For nested function calls, we need to parse them as complete function calls - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER && - tokens[current + 1] && tokens[current + 1].type !== TokenType.ASSIGNMENT && - tokens[current + 1].type !== TokenType.PLUS && - tokens[current + 1].type !== TokenType.MINUS && - tokens[current + 1].type !== TokenType.MULTIPLY && - tokens[current + 1].type !== TokenType.DIVIDE && - tokens[current + 1].type !== TokenType.MODULO && - tokens[current + 1].type !== TokenType.POWER && - tokens[current + 1].type !== TokenType.EQUALS && - tokens[current + 1].type !== TokenType.LESS_THAN && - tokens[current + 1].type !== TokenType.GREATER_THAN && - tokens[current + 1].type !== TokenType.LESS_EQUAL && - tokens[current + 1].type !== TokenType.GREATER_EQUAL && - tokens[current + 1].type !== TokenType.NOT_EQUAL && - tokens[current + 1].type !== TokenType.AND && - tokens[current + 1].type !== TokenType.OR && - tokens[current + 1].type !== TokenType.XOR && - tokens[current + 1].type !== TokenType.NOT && - tokens[current + 1].type !== TokenType.SEMICOLON && - (tokens[current + 1].type === TokenType.NUMBER || - tokens[current + 1].type === TokenType.IDENTIFIER || - tokens[current + 1].type === TokenType.LEFT_PAREN)) { - - // Check for ambiguous nested function calls - // Look for pattern: func1 func2 arg1 func3 arg2 - let tempCurrent = current; - let functionNames = []; - - while (tempCurrent < tokens.length && - tokens[tempCurrent].type !== TokenType.SEMICOLON && - tokens[tempCurrent].type !== TokenType.RIGHT_PAREN) { - if (tokens[tempCurrent].type === TokenType.IDENTIFIER && - tempCurrent + 1 < tokens.length && - tokens[tempCurrent + 1].type !== TokenType.ASSIGNMENT && - tokens[tempCurrent + 1].type !== TokenType.PLUS && - tokens[tempCurrent + 1].type !== TokenType.MINUS && - tokens[tempCurrent + 1].type !== TokenType.MULTIPLY && - tokens[tempCurrent + 1].type !== TokenType.DIVIDE && - tokens[tempCurrent + 1].type !== TokenType.MODULO && - tokens[tempCurrent + 1].type !== TokenType.POWER && - tokens[tempCurrent + 1].type !== TokenType.EQUALS && - tokens[tempCurrent + 1].type !== TokenType.LESS_THAN && - tokens[tempCurrent + 1].type !== TokenType.GREATER_THAN && - tokens[tempCurrent + 1].type !== TokenType.LESS_EQUAL && - tokens[tempCurrent + 1].type !== TokenType.GREATER_EQUAL && - tokens[tempCurrent + 1].type !== TokenType.NOT_EQUAL && - tokens[tempCurrent + 1].type !== TokenType.AND && - tokens[tempCurrent + 1].type !== TokenType.OR && - tokens[tempCurrent + 1].type !== TokenType.XOR && - tokens[tempCurrent + 1].type !== TokenType.NOT && - tokens[tempCurrent + 1].type !== TokenType.COMPOSE && - tokens[tempCurrent + 1].type !== TokenType.PIPE && - tokens[tempCurrent + 1].type !== TokenType.APPLY && - tokens[tempCurrent + 1].type !== TokenType.SEMICOLON && - (tokens[tempCurrent + 1].type === TokenType.NUMBER || - tokens[tempCurrent + 1].type === TokenType.IDENTIFIER || - tokens[tempCurrent + 1].type === TokenType.LEFT_PAREN)) { - functionNames.push(tokens[tempCurrent].value); - } - tempCurrent++; - } - - if (functionNames.length > 2) { - throw new Error(`Ambiguous nested function calls detected: ${functionNames.join(' ')}. Use parentheses to explicitly group function calls.`); - } - - // This is a nested function call, parse it as a complete function call - const nestedFuncName = tokens[current].value; - current++; // Skip function name - const nestedArgs = []; - - // Parse nested function arguments - while (current < tokens.length && - tokens[current].type !== TokenType.SEMICOLON && - tokens[current].type !== TokenType.RIGHT_PAREN) { - const nestedArg = walk(); - if (nestedArg) { - nestedArgs.push(nestedArg); - } - } - - args.push({ - type: 'FunctionCall', - name: nestedFuncName, - args: nestedArgs, - }); - } else { - // Use walk() for other types of arguments - const arg = walk(); - if (arg) { - args.push(arg); - } - } + throw new Error('Expected property name in table literal'); + } + + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; } } - return { - type: 'FunctionCall', - name: funcName, - args, - }; + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACE) { + current++; // Skip '}' + return { type: 'TableLiteral', properties }; + } else { + throw new Error('Expected closing brace in table literal'); + } } - return null; + // If we get here, we have an unexpected token + throw new Error(`Unexpected token in parseArgument: ${token.type}`); + } finally { + callStackTracker.pop(); } - - // Handle arithmetic expressions that start with a number followed by an operator - if (token.type === TokenType.NUMBER && tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE || - tokens[current + 1].type === TokenType.MODULO || - tokens[current + 1].type === TokenType.POWER - )) { - debugLog('Parsing arithmetic expression starting with number', { - current: token, - next: tokens[current + 1] - }); - const left = { - type: 'NumberLiteral', - value: token.value, - }; - current++; // Skip the number - const operator = tokens[current].type; - current++; // Skip the operator - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - [TokenType.MODULO]: 'ModuloExpression', - [TokenType.POWER]: 'PowerExpression', - }; + } + + function parseFunctionCall(functionName) { + callStackTracker.push('parseFunctionCall', ''); + + try { + /** + * Parses function calls with arbitrary argument lists. + * + * @param {Object|string} functionName - Function name or expression to call + * @returns {Object} AST node representing the function call + * + * @description Parses function calls by collecting arguments until a + * clear terminator is found, supporting both curried and regular calls. + * + * @why Arguments are parsed until a clear terminator is found, allowing + * for flexible function call syntax. This approach supports both curried + * and regular function calls, and allows for future extension to variadic functions. + * + * @note Special handling for unary minus arguments to distinguish them + * from binary minus operations. + */ + const args = []; + // Parse arguments until we hit a semicolon or other terminator + while (current < tokens.length && + tokens[current].type !== TokenType.SEMICOLON && + tokens[current].type !== TokenType.RIGHT_PAREN && + tokens[current].type !== TokenType.RIGHT_BRACE && + tokens[current].type !== TokenType.COMMA && + tokens[current].type !== TokenType.AND && + tokens[current].type !== TokenType.OR && + tokens[current].type !== TokenType.XOR) { + + // Special handling for unary minus as argument + if (tokens[current].type === TokenType.MINUS) { + // This is a unary minus, parse it as a new argument + current++; // Skip the minus + if (current < tokens.length && tokens[current].type === TokenType.NUMBER) { + args.push({ + type: 'UnaryMinusExpression', + operand: { + type: 'NumberLiteral', + value: tokens[current].value + } + }); + current++; // Skip the number + } else { + // More complex unary minus expression + args.push({ + type: 'UnaryMinusExpression', + operand: parsePrimary() + }); + } + } else { + // Regular argument parsing - parse as expression but skip function call detection + // Create a temporary parsing context that doesn't trigger function call detection + const savedParsingFunctionArgs = parsingFunctionArgs; + parsingFunctionArgs = true; // Temporarily disable function call detection + const arg = parseExpression(); + parsingFunctionArgs = savedParsingFunctionArgs; // Restore the flag + args.push(arg); + } + } + return { - type: expressionTypes[operator], - left, - right, + type: 'FunctionCall', + name: functionName, + args: args }; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.NUMBER) { - current++; - return { - type: 'NumberLiteral', - value: token.value, - }; + } + + function parseLogicalExpression() { + callStackTracker.push('parseLogicalExpression', ''); + + try { + /** + * Parses logical expressions with lowest precedence. + * + * @returns {Object} AST node representing the logical expression + * + * @description Parses logical operators (and, or, xor) with proper + * precedence handling and left associativity. + * + * @why Logical operators should have lower precedence than arithmetic + * and comparison operators to ensure proper grouping of expressions + * like "isEven 10 and isPositive 5". + */ + let left = parseExpression(); + + while (current < tokens.length && + (tokens[current].type === TokenType.AND || + tokens[current].type === TokenType.OR || + tokens[current].type === TokenType.XOR)) { + + const operator = tokens[current].type; + current++; + const right = parseExpression(); + + switch (operator) { + case TokenType.AND: + left = { type: 'AndExpression', left, right }; + break; + case TokenType.OR: + left = { type: 'OrExpression', left, right }; + break; + case TokenType.XOR: + left = { type: 'XorExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.STRING) { - current++; - return { - type: 'StringLiteral', - value: token.value, - }; + } + + function parseExpression() { + callStackTracker.push('parseExpression', ''); + + try { + /** + * Parses expressions with left-associative binary operators. + * + * @returns {Object} AST node representing the expression + * + * @description Parses addition, subtraction, and comparison operators + * with proper precedence and associativity. + * + * @why Operator precedence is handled by splitting parsing into multiple + * functions (expression, term, factor, primary). This structure avoids + * ambiguity and ensures correct grouping of operations. + * + * @note Special case handling for unary minus after function references + * to distinguish from binary minus operations. + */ + let left = parseTerm(); + + while (current < tokens.length && + (tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.EQUALS || + tokens[current].type === TokenType.NOT_EQUAL || + tokens[current].type === TokenType.LESS_THAN || + tokens[current].type === TokenType.GREATER_THAN || + tokens[current].type === TokenType.LESS_EQUAL || + tokens[current].type === TokenType.GREATER_EQUAL)) { + + const operator = tokens[current].type; + + // Special case: Don't treat MINUS as binary operator if left is a FunctionReference + // This handles cases like "filter @isPositive -3" where -3 should be a separate argument + if (operator === TokenType.MINUS && left.type === 'FunctionReference') { + // This is likely a function call with unary minus argument, not a binary operation + // Return the left side and let the caller handle it + return left; + } + + current++; + const right = parseTerm(); + + switch (operator) { + case TokenType.PLUS: + left = { type: 'PlusExpression', left, right }; + break; + case TokenType.MINUS: + left = { type: 'MinusExpression', left, right }; + break; + case TokenType.EQUALS: + left = { type: 'EqualsExpression', left, right }; + break; + case TokenType.NOT_EQUAL: + left = { type: 'NotEqualExpression', left, right }; + break; + case TokenType.LESS_THAN: + left = { type: 'LessThanExpression', left, right }; + break; + case TokenType.GREATER_THAN: + left = { type: 'GreaterThanExpression', left, right }; + break; + case TokenType.LESS_EQUAL: + left = { type: 'LessEqualExpression', left, right }; + break; + case TokenType.GREATER_EQUAL: + left = { type: 'GreaterEqualExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.TRUE) { - current++; - return { - type: 'BooleanLiteral', - value: true, - }; + } + + function parseTerm() { + callStackTracker.push('parseTerm', ''); + + try { + /** + * Parses multiplication, division, and modulo operations. + * + * @returns {Object} AST node representing the term + * + * @description Parses multiplicative operators with higher precedence + * than addition/subtraction. + * + * @why By handling these operators at a separate precedence level, the + * parser ensures that multiplication/division bind tighter than + * addition/subtraction, matching standard arithmetic rules. + */ + let left = parseFactor(); + + while (current < tokens.length && + (tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO)) { + + const operator = tokens[current].type; + current++; + const right = parseFactor(); + + switch (operator) { + case TokenType.MULTIPLY: + left = { type: 'MultiplyExpression', left, right }; + break; + case TokenType.DIVIDE: + left = { type: 'DivideExpression', left, right }; + break; + case TokenType.MODULO: + left = { type: 'ModuloExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.FALSE) { - current++; - return { - type: 'BooleanLiteral', - value: false, - }; + } + + function parseFactor() { + callStackTracker.push('parseFactor', ''); + + try { + /** + * Parses exponentiation and primary expressions. + * + * @returns {Object} AST node representing the factor + * + * @description Parses exponentiation with right associativity and + * highest precedence among arithmetic operators. + * + * @why Exponentiation is right-associative and binds tighter than + * multiplication/division, so it is handled at the factor level. This + * also allows for future extension to other high-precedence operators. + */ + let left = parsePrimary(); + + while (current < tokens.length && tokens[current].type === TokenType.POWER) { + current++; + const right = parsePrimary(); + left = { type: 'PowerExpression', left, right }; + } + + return left; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.LEFT_BRACE) { - current++; // Skip opening brace - const entries = []; + } + + function parsePrimary() { + callStackTracker.push('parsePrimary', ''); + + try { + /** + * Parses literals, identifiers, function definitions, assignments, + * table literals, and parenthesized expressions. + * + * @returns {Object} AST node representing the primary expression + * @throws {Error} For parsing errors like unexpected tokens + * + * @description The core parsing function that handles all atomic and + * context-sensitive constructs in the language. + * + * @why This function is the core of the recursive descent parser, handling + * all atomic and context-sensitive constructs. Special care is taken to + * avoid circular dependencies by not calling higher-level parsing functions. + */ - // Parse table entries until closing brace - while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { - // Skip leading commas - if (tokens[current].type === TokenType.COMMA) { + const token = tokens[current]; + if (!token) { + throw new Error('Unexpected end of input'); + } + + // Parse unary operators + if (token.type === TokenType.NOT) { + current++; + const operand = parsePrimary(); + return { type: 'NotExpression', operand }; + } + + if (token.type === TokenType.MINUS) { + current++; + const operand = parsePrimary(); + return { type: 'UnaryMinusExpression', operand }; + } + + // Parse literals + if (token.type === TokenType.NUMBER) { + current++; + return { type: 'NumberLiteral', value: token.value }; + } else if (token.type === TokenType.STRING) { + current++; + return { type: 'StringLiteral', value: token.value }; + } else if (token.type === TokenType.TRUE) { + current++; + return { type: 'BooleanLiteral', value: true }; + } else if (token.type === TokenType.FALSE) { + current++; + return { type: 'BooleanLiteral', value: false }; + } else if (token.type === TokenType.NULL) { + current++; + return { type: 'NullLiteral' }; + } else if (token.type === TokenType.WILDCARD) { + current++; + return { type: 'WildcardPattern' }; + } else if (token.type === TokenType.FUNCTION_REF) { + current++; + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const functionName = tokens[current].value; current++; - continue; + return { type: 'FunctionReference', name: functionName }; + } else { + throw new Error('Expected function name after @'); + } + } else if (token.type === TokenType.IO_IN) { + current++; + return { type: 'IOInExpression' }; + } else if (token.type === TokenType.IO_OUT) { + current++; + const outputValue = parseLogicalExpression(); + return { type: 'IOOutExpression', value: outputValue }; + } else if (token.type === TokenType.IO_ASSERT) { + current++; + const assertionExpr = parseLogicalExpression(); + return { type: 'IOAssertExpression', value: assertionExpr }; + } + + // Parse identifiers + if (token.type === TokenType.IDENTIFIER) { + current++; + const identifier = { type: 'Identifier', value: token.value }; + + // Skip function call detection if we're parsing function arguments + if (parsingFunctionArgs) { + return identifier; } - // Parse key (optional) and value - let key = null; - let value; + // Check for function calls + if (current < tokens.length && tokens[current].type === TokenType.LEFT_PAREN) { + return parseFunctionCall(identifier.value); + } - // Check if this is a key-value pair (key: value) or just a value - if (current + 1 < tokens.length && tokens[current + 1].type === TokenType.ASSIGNMENT) { - // This is a key-value pair - // Parse the key (should be an identifier, number, or string) - if (tokens[current].type === TokenType.IDENTIFIER) { - key = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.NUMBER) { - key = { - type: 'NumberLiteral', - value: tokens[current].value, - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.STRING) { - key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the key - } else { - throw new Error('Invalid key type in table literal'); + // Check if the next token is an operator - if so, don't treat as function call + if (current < tokens.length && + (tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO || + tokens[current].type === TokenType.POWER)) { + // This is part of a binary expression, don't treat as function call + return identifier; + } + + // Check for function calls without parentheses (e.g., add 3 4) + // Only treat as function call if the next token is a number, string, or left paren + // This prevents treating identifiers as function calls when they're actually arguments + if (current < tokens.length && + (tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN || + tokens[current].type === TokenType.FUNCTION_REF)) { + return parseFunctionCall(identifier.value); + } + + // Special case for unary minus: only treat as function call if it's a unary minus + if (current < tokens.length && tokens[current].type === TokenType.MINUS) { + // Look ahead to see if this is a unary minus (like -5) or binary minus (like n - 1) + const nextToken = current + 1 < tokens.length ? tokens[current + 1] : null; + if (nextToken && nextToken.type === TokenType.NUMBER) { + // This is a unary minus, treat as function call + return parseFunctionCall(identifier.value); } + // This is a binary minus, don't treat as function call + } + + // Special case for function calls with identifier arguments (e.g., add x y) + // Only treat as function call if the next token is an identifier and not followed by an operator + if (!parsingFunctionArgs && current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + // Look ahead to see if the next token is an identifier followed by an operator + const nextToken = current + 1 < tokens.length ? tokens[current + 1] : null; + const nextNextToken = current + 2 < tokens.length ? tokens[current + 2] : null; - current++; // Skip the colon - value = walk(); // Parse the value - } else { - // This is just a value (array-like entry) - value = walk(); + // Only treat as function call if the next token is an identifier and not followed by an operator + if (nextToken && nextToken.type === TokenType.IDENTIFIER && + (!nextNextToken || + (nextNextToken.type !== TokenType.PLUS && + nextNextToken.type !== TokenType.MINUS && + nextNextToken.type !== TokenType.MULTIPLY && + nextNextToken.type !== TokenType.DIVIDE && + nextNextToken.type !== TokenType.MODULO && + nextNextToken.type !== TokenType.POWER && + nextNextToken.type !== TokenType.EQUALS && + nextNextToken.type !== TokenType.NOT_EQUAL && + nextNextToken.type !== TokenType.LESS_THAN && + nextNextToken.type !== TokenType.GREATER_THAN && + nextNextToken.type !== TokenType.LESS_EQUAL && + nextNextToken.type !== TokenType.GREATER_EQUAL))) { + if (process.env.DEBUG) { + console.log(`[DEBUG] Creating function call for ${identifier.value} at position ${current}`); + } + return parseFunctionCall(identifier.value); + } } - entries.push({ key, value }); + - // Skip trailing comma - if (tokens[current] && tokens[current].type === TokenType.COMMA) { - current++; + // Check for table access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + return parseChainedDotAccess(identifier); } + + // Check for table access with brackets + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + return parseChainedTableAccess(identifier); + } + + return identifier; } - // Expect closing brace - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACE) { - current++; // Skip closing brace - } else { - throw new Error('Expected closing brace } in table literal'); + // Parse parenthesized expressions + if (token.type === TokenType.LEFT_PAREN) { + current++; // Skip '(' + const parenthesizedExpr = parseLogicalExpression(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_PAREN) { + current++; // Skip ')' + return parenthesizedExpr; + } else { + throw new Error('Expected closing parenthesis'); + } } - return { - type: 'TableLiteral', - entries, - }; - } - - - - if (token.type === TokenType.WILDCARD) { - current++; - return { - type: 'WildcardPattern', - }; - } - - if (token.type === TokenType.IO_IN) { - current++; - return { - type: 'IOInExpression', - }; - } - - if (token.type === TokenType.IO_OUT) { - current++; - const value = walk(); - return { - type: 'IOOutExpression', - value, - }; - } - - if (token.type === TokenType.FUNCTION_REF) { - current++; // Skip the @ token - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const functionName = tokens[current].value; - current++; // Skip the function name - return { - type: 'FunctionReference', - name: functionName, - }; - } else { - throw new Error('Expected function name after @'); + // Parse table literals + if (token.type === TokenType.LEFT_BRACE) { + current++; // Skip '{' + const properties = []; + + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { + if (tokens[current].type === TokenType.IDENTIFIER) { + const key = tokens[current].value; + current++; + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + const value = parseLogicalExpression(); + properties.push({ key, value }); + } else { + throw new Error('Expected ":" after property name in table literal'); + } + } else { + throw new Error('Expected property name in table literal'); + } + + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; + } + } + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACE) { + current++; // Skip '}' + return { type: 'TableLiteral', properties }; + } else { + throw new Error('Expected closing brace in table literal'); + } } - } - - if (token.type === TokenType.IO_ASSERT) { - current++; - // Parse a comparison expression (e.g., x = 5, y > 3, z != 0) - const left = walk(); - - // Expect a comparison operator - if (tokens[current] && ( - tokens[current].type === TokenType.EQUALS || - tokens[current].type === TokenType.LESS_THAN || - tokens[current].type === TokenType.GREATER_THAN || - tokens[current].type === TokenType.LESS_EQUAL || - tokens[current].type === TokenType.GREATER_EQUAL || - tokens[current].type === TokenType.NOT_EQUAL - )) { - const operator = tokens[current].type; - current++; // Skip the operator - const right = walk(); + // Parse arrow expressions (function definitions) + if (token.type === TokenType.ARROW) { + current++; // Skip '->' - return { - type: 'IOAssertExpression', - left, - operator, - right, - }; - } else { - throw new Error('Expected comparison operator (=, <, >, <=, >=, !=) in assertion'); + // Parse the function body + const body = parseLogicalExpression(); + + return { type: 'ArrowExpression', body }; } - } - - - - if (token.type === TokenType.PLUS) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'PlusExpression', - left, - right, - }; - } - - if (token.type === TokenType.MINUS) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'MinusExpression', - left, - right, - }; - } - - if (token.type === TokenType.MULTIPLY) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'MultiplyExpression', - left, - right, - }; - } - - if (token.type === TokenType.DIVIDE) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'DivideExpression', - left, - right, - }; - } - - if (token.type === TokenType.MODULO) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'ModuloExpression', - left, - right, - }; - } - - if (token.type === TokenType.POWER) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'PowerExpression', - left, - right, - }; - } - - // Comparison operators - if (token.type === TokenType.EQUALS) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'EqualsExpression', - left, - right, - }; - } - - if (token.type === TokenType.LESS_THAN) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'LessThanExpression', - left, - right, - }; - } - - if (token.type === TokenType.GREATER_THAN) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'GreaterThanExpression', - left, - right, - }; - } - - if (token.type === TokenType.LESS_EQUAL) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'LessEqualExpression', - left, - right, - }; - } - - if (token.type === TokenType.GREATER_EQUAL) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'GreaterEqualExpression', - left, - right, - }; - } - - if (token.type === TokenType.NOT_EQUAL) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'NotEqualExpression', - left, - right, - }; - } - - // Logical operators - if (token.type === TokenType.AND) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'AndExpression', - left, - right, - }; - } - - if (token.type === TokenType.OR) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'OrExpression', - left, - right, - }; - } + - if (token.type === TokenType.XOR) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'XorExpression', - left, - right, - }; - } + - if (token.type === TokenType.NOT) { - current++; - const operand = walk(); - return { - type: 'NotExpression', - operand, - }; + + // If we get here, we have an unexpected token + throw new Error(`Unexpected token in parsePrimary: ${token.type}`); + } finally { + callStackTracker.pop(); } + } + + function walk() { + callStackTracker.push('walk', `position:${current}`); + + try { + - // Functional operators - + - if (token.type === TokenType.LEFT_PAREN) { - current++; // Skip opening parenthesis - const expression = walk(); - // Expect closing parenthesis - if (tokens[current] && tokens[current].type === TokenType.RIGHT_PAREN) { - debugLog('Found right parenthesis'); - current++; // Skip closing parenthesis + function parseLogicalExpression() { + callStackTracker.push('parseLogicalExpression', ''); - // Check if there's an arithmetic, logical, or functional operator after the parentheses - if (tokens[current] && ( - tokens[current].type === TokenType.PLUS || - tokens[current].type === TokenType.MINUS || - tokens[current].type === TokenType.MULTIPLY || - tokens[current].type === TokenType.DIVIDE || - tokens[current].type === TokenType.MODULO || - tokens[current].type === TokenType.POWER || - tokens[current].type === TokenType.AND || - tokens[current].type === TokenType.OR || - tokens[current].type === TokenType.XOR || - tokens[current].type === TokenType.NOT - )) { - // This is an arithmetic expression with parentheses as left operand - const operator = tokens[current].type; - current++; // Skip the operator - const right = walk(); + try { + /** + * Parses logical expressions with lowest precedence. + * + * @returns {Object} AST node representing the logical expression + * + * @description Parses logical operators (and, or, xor) with proper + * precedence handling and left associativity. + * + * @why Logical operators should have lower precedence than arithmetic + * and comparison operators to ensure proper grouping of expressions + * like "isEven 10 and isPositive 5". + */ + let left = parseExpression(); - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - [TokenType.MODULO]: 'ModuloExpression', - [TokenType.POWER]: 'PowerExpression', - [TokenType.AND]: 'AndExpression', - [TokenType.OR]: 'OrExpression', - [TokenType.XOR]: 'XorExpression', - [TokenType.NOT]: 'NotExpression', - - }; + while (current < tokens.length && + (tokens[current].type === TokenType.AND || + tokens[current].type === TokenType.OR || + tokens[current].type === TokenType.XOR)) { + + const operator = tokens[current].type; + current++; + const right = parseExpression(); + + switch (operator) { + case TokenType.AND: + left = { type: 'AndExpression', left, right }; + break; + case TokenType.OR: + left = { type: 'OrExpression', left, right }; + break; + case TokenType.XOR: + left = { type: 'XorExpression', left, right }; + break; + } + } - return { - type: expressionTypes[operator], - left: expression, - right, - }; + return left; + } finally { + callStackTracker.pop(); } - - // If not followed by an arithmetic operator, just return the parenthesized expression - // This handles cases like function arguments: add (3 + 2) (4 + 1) - return expression; - } else { - debugLog('Expected right parenthesis but found', tokens[current]); - throw new Error('Expected closing parenthesis'); - } - } - - if (token.type === TokenType.ASSIGNMENT) { - current++; // Skip the assignment token - return walk(); // Continue parsing - } - - if (token.type === TokenType.IDENTIFIER) { - // First, try to parse as a function call with proper precedence - const functionCall = parseFunctionCall(); - if (functionCall) { - return functionCall; } - // Check if this is followed by table access - if (tokens[current + 1] && tokens[current + 1].type === TokenType.LEFT_BRACKET) { - // Table access with bracket notation: identifier[key] - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; // Skip identifier - current++; // Skip opening bracket - const key = walk(); // Parse the key expression - - // Expect closing bracket - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip closing bracket - } else { - throw new Error('Expected closing bracket ] in table access'); - } - - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else if (tokens[current + 1] && tokens[current + 1].type === TokenType.DOT) { - // Table access with dot notation: identifier.key - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; // Skip identifier - current++; // Skip the dot + function parseExpression() { + callStackTracker.push('parseExpression', ''); - // Expect an identifier after the dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the identifier + try { + /** + * Parses expressions with left-associative binary operators. + * + * @returns {Object} AST node representing the expression + * + * @description Parses addition, subtraction, and comparison operators + * with proper precedence and associativity. + * + * @why Operator precedence is handled by splitting parsing into multiple + * functions (expression, term, factor, primary). This structure avoids + * ambiguity and ensures correct grouping of operations. + * + * @note Special case handling for unary minus after function references + * to distinguish from binary minus operations. + */ + let left = parseTerm(); - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else { - throw new Error('Expected identifier after dot in table access'); + while (current < tokens.length && + (tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.EQUALS || + tokens[current].type === TokenType.NOT_EQUAL || + tokens[current].type === TokenType.LESS_THAN || + tokens[current].type === TokenType.GREATER_THAN || + tokens[current].type === TokenType.LESS_EQUAL || + tokens[current].type === TokenType.GREATER_EQUAL)) { + + const operator = tokens[current].type; + + // Special case: Don't treat MINUS as binary operator if left is a FunctionReference + // This handles cases like "filter @isPositive -3" where -3 should be a separate argument + if (operator === TokenType.MINUS && left.type === 'FunctionReference') { + // This is likely a function call with unary minus argument, not a binary operation + // Return the left side and let the caller handle it + return left; + } + + current++; + const right = parseTerm(); + + switch (operator) { + case TokenType.PLUS: + left = { type: 'PlusExpression', left, right }; + break; + case TokenType.MINUS: + left = { type: 'MinusExpression', left, right }; + break; + case TokenType.EQUALS: + left = { type: 'EqualsExpression', left, right }; + break; + case TokenType.NOT_EQUAL: + left = { type: 'NotEqualExpression', left, right }; + break; + case TokenType.LESS_THAN: + left = { type: 'LessThanExpression', left, right }; + break; + case TokenType.GREATER_THAN: + left = { type: 'GreaterThanExpression', left, right }; + break; + case TokenType.LESS_EQUAL: + left = { type: 'LessEqualExpression', left, right }; + break; + case TokenType.GREATER_EQUAL: + left = { type: 'GreaterEqualExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } } - // Check if this is followed by an assignment (variable or function) - if (tokens[current + 1] && tokens[current + 1].type === TokenType.ASSIGNMENT) { - const name = token.value; + function parseTerm() { + callStackTracker.push('parseTerm', ''); - // Check if this is a function declaration (has parameters and arrow) - // Look ahead to see if there are parameters followed by arrow - let params = []; - let isFunction = false; - let tempCurrent = current + 2; // Skip identifier and assignment - - // Collect parameters until we hit an arrow - while (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.IDENTIFIER) { - params.push(tokens[tempCurrent].value); - tempCurrent++; + try { + /** + * Parses multiplication, division, and modulo operations. + * + * @returns {Object} AST node representing the term + * + * @description Parses multiplicative operators with higher precedence + * than addition/subtraction. + * + * @why By handling these operators at a separate precedence level, the + * parser ensures that multiplication/division bind tighter than + * addition/subtraction, matching standard arithmetic rules. + */ + let left = parseFactor(); + + while (current < tokens.length && + (tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO)) { + + const operator = tokens[current].type; + current++; + const right = parseFactor(); + + switch (operator) { + case TokenType.MULTIPLY: + left = { type: 'MultiplyExpression', left, right }; + break; + case TokenType.DIVIDE: + left = { type: 'DivideExpression', left, right }; + break; + case TokenType.MODULO: + left = { type: 'ModuloExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } + } + + function parseFactor() { + callStackTracker.push('parseFactor', ''); - // Check if next token is arrow - if (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.ARROW) { - isFunction = true; + try { + /** + * Parses exponentiation and primary expressions. + * + * @returns {Object} AST node representing the factor + * + * @description Parses exponentiation with right associativity and + * highest precedence among arithmetic operators. + * + * @why Exponentiation is right-associative and binds tighter than + * multiplication/division, so it is handled at the factor level. This + * also allows for future extension to other high-precedence operators. + */ + let left = parsePrimary(); + + while (current < tokens.length && tokens[current].type === TokenType.POWER) { + current++; + const right = parsePrimary(); + left = { type: 'PowerExpression', left, right }; + } + + return left; + } finally { + callStackTracker.pop(); } + } + + // Check for IO operations first + if (tokens[current].type === TokenType.IO_IN) { + current++; + return { type: 'IOInExpression' }; + } else if (tokens[current].type === TokenType.IO_OUT) { + current++; + const outputValue = parseLogicalExpression(); + return { type: 'IOOutExpression', value: outputValue }; + } else if (tokens[current].type === TokenType.IO_ASSERT) { + current++; + const assertionExpr = parseLogicalExpression(); + return { type: 'IOAssertExpression', value: assertionExpr }; + } + + + + // Check for assignments (identifier followed by ':') + if (tokens[current].type === TokenType.IDENTIFIER) { + const identifier = tokens[current].value; + current++; - if (isFunction) { - // This is a function declaration - // Advance current to where tempCurrent left off - current = tempCurrent + 1; // Skip the arrow token + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + + // Check if this is a function definition with arrow syntax (x y -> body) + // Look ahead to see if we have parameters followed by -> + const lookAheadTokens = []; + let lookAheadPos = current; - // Check if the body is a case expression - if (tokens[current] && tokens[current].type === TokenType.CASE) { - current++; // Skip 'case' + // Collect tokens until we find -> or hit a terminator + while (lookAheadPos < tokens.length && + tokens[lookAheadPos].type !== TokenType.ARROW && + tokens[lookAheadPos].type !== TokenType.SEMICOLON && + tokens[lookAheadPos].type !== TokenType.ASSIGNMENT) { + lookAheadTokens.push(tokens[lookAheadPos]); + lookAheadPos++; + } + + // If we found ->, this is a function definition with arrow syntax + if (lookAheadPos < tokens.length && tokens[lookAheadPos].type === TokenType.ARROW) { + // Parse parameters (identifiers separated by spaces) + const parameters = []; + let paramIndex = 0; - // Parse the value being matched (e.g., "x y") - const value = []; - while (current < tokens.length && - tokens[current].type !== TokenType.OF) { - if (tokens[current].type === TokenType.IDENTIFIER) { - value.push({ - type: 'Identifier', - value: tokens[current].value, - }); - } else if (tokens[current].type === TokenType.NUMBER) { - value.push({ - type: 'NumberLiteral', - value: tokens[current].value, - }); + while (paramIndex < lookAheadTokens.length) { + if (lookAheadTokens[paramIndex].type === TokenType.IDENTIFIER) { + parameters.push(lookAheadTokens[paramIndex].value); + paramIndex++; + } else { + // Skip non-identifier tokens (spaces, etc.) + paramIndex++; } - current++; } - // Expect 'of' after the value - if (tokens[current] && tokens[current].type === TokenType.OF) { - current++; // Skip 'of' - } else { - throw new Error('Expected "of" after case value'); - } + // Skip the parameters and -> + current = lookAheadPos + 1; // Skip the arrow - const cases = []; - - // Parse cases until we hit a semicolon or end - while (current < tokens.length) { - // Check if we've reached the end of the case expression - if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { - debugLog('Found semicolon at start of loop, ending case parsing'); - current++; - break; // Exit the case parsing loop - } + // Parse the function body (check if it's a case expression) + let functionBody; + if (current < tokens.length && tokens[current].type === TokenType.CASE) { + // Parse case expression directly + current++; // Skip 'case' - debugLog('Case parsing loop', { - current: tokens[current], - currentType: tokens[current]?.type - }); - // Parse pattern (e.g., "0 0" or "0 _" or "_ 0" or "_ _") - debugLog('Parsing pattern', { - current: tokens[current], - currentType: tokens[current]?.type - }); - const pattern = []; - while (current < tokens.length && - tokens[current].type !== TokenType.ASSIGNMENT && - tokens[current].type !== TokenType.SEMICOLON) { - if (tokens[current].type === TokenType.NUMBER) { - pattern.push({ - type: 'NumberLiteral', - value: tokens[current].value, - }); - } else if (tokens[current].type === TokenType.WILDCARD) { - pattern.push({ - type: 'WildcardPattern', - }); - } else if (tokens[current].type === TokenType.IDENTIFIER) { - pattern.push({ - type: 'Identifier', - value: tokens[current].value, - }); + // Parse the values being matched (can be multiple) + const values = []; + while (current < tokens.length && tokens[current].type !== TokenType.OF) { + if (tokens[current].type === TokenType.IDENTIFIER) { + values.push({ type: 'Identifier', value: tokens[current].value }); + current++; + } else if (tokens[current].type === TokenType.NUMBER) { + values.push({ type: 'NumberLiteral', value: tokens[current].value }); + current++; + } else if (tokens[current].type === TokenType.STRING) { + values.push({ type: 'StringLiteral', value: tokens[current].value }); + current++; + } else { + const value = parsePrimary(); + values.push(value); } - current++; } - // Expect ':' after pattern - debugLog('Looking for : after pattern', { - current: tokens[current], - currentType: tokens[current]?.type - }); - if (tokens[current] && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - debugLog('Found : after pattern'); - } else { - throw new Error('Expected ":" after pattern'); - } - - // Parse result using walk() to handle complex expressions - debugLog('Starting case result parsing', { - current: tokens[current], - currentType: tokens[current]?.type - }); - const result = []; - - // Parse one expression for the case result - debugLog('Parsing case result node', { - current: tokens[current], - currentType: tokens[current]?.type - }); - const resultNode = walk(); - if (resultNode) { - result.push(resultNode); - debugLog('Added result node', resultNode); + // Expect 'of' + if (current >= tokens.length || tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); } + current++; // Skip 'of' - cases.push({ pattern, result }); + const cases = []; - // Check if we've reached the end of the case expression - if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { - debugLog('Found semicolon, ending case parsing'); - current++; - break; // Exit the case parsing loop + // Parse cases until we hit a semicolon or end + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + // If we hit an IO operation, we've reached the end of the case expression + if (current < tokens.length && + (tokens[current].type === TokenType.IO_IN || + tokens[current].type === TokenType.IO_OUT || + tokens[current].type === TokenType.IO_ASSERT)) { + break; + } + const patterns = []; + while (current < tokens.length && + tokens[current].type !== TokenType.ASSIGNMENT && + tokens[current].type !== TokenType.SEMICOLON) { + patterns.push(parsePrimary()); + } + + // Expect ':' after pattern + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + } else { + throw new Error('Expected ":" after pattern in case expression'); + } + + // Temporarily disable function call detection when parsing case expression results + const savedParsingFunctionArgs = parsingFunctionArgs; + parsingFunctionArgs = true; // Disable function call detection + const result = parseLogicalExpression(); + parsingFunctionArgs = savedParsingFunctionArgs; // Restore the flag + cases.push({ + pattern: patterns, + result: [result] + }); + + // Skip semicolon if present (but don't stop parsing cases) + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + // If the next token is an identifier followed by assignment, we've reached the end of the case expression + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const nextPos = current + 1; + if (nextPos < tokens.length && tokens[nextPos].type === TokenType.ASSIGNMENT) { + break; // End of case expression + } + } + } } - } - - const body = { - type: 'CaseExpression', - value, - cases, - }; - - return { - type: 'FunctionDeclaration', - name, - params, - body, - }; - } else { - // Parse function body directly to avoid function call detection issues - let body; - if (tokens[current] && ( - tokens[current].type === TokenType.PLUS || - tokens[current].type === TokenType.MINUS || - tokens[current].type === TokenType.MULTIPLY || - tokens[current].type === TokenType.DIVIDE - )) { - // Arithmetic expression - const operator = tokens[current].type; - current++; // Skip operator - const left = walk(); - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - body = { - type: expressionTypes[operator], - left, - right, + functionBody = { + type: 'CaseExpression', + value: values, + cases, }; - } else { - // Fallback to walk() for other cases - debugLog('Using walk() fallback for function body'); - body = walk(); - } + } else { + functionBody = parseLogicalExpression(); + } return { - type: 'FunctionDeclaration', - name, - params, - body, + type: 'Assignment', + identifier, + value: { + type: 'FunctionDefinition', + parameters, + body: functionBody + } }; } - } else { - // This is a variable assignment - // Advance current to skip identifier and assignment token - current += 2; // Skip identifier and assignment - - debugLog('Assignment parsing', { - current: tokens[current], - next: tokens[current + 1], - currentType: tokens[current]?.type, - nextType: tokens[current + 1]?.type - }); - - // Parse the value directly without calling walk() - let value; - // Check if the value is a function call - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER && - tokens[current + 1] && tokens[current + 1].type !== TokenType.ASSIGNMENT && - tokens[current + 1].type !== TokenType.SEMICOLON && - tokens[current + 1].type !== TokenType.ARROW && - (tokens[current + 1].type === TokenType.NUMBER || - tokens[current + 1].type === TokenType.IDENTIFIER || - tokens[current + 1].type === TokenType.FUNCTION_REF || - tokens[current + 1].type === TokenType.LEFT_PAREN)) { - // This is a function call as the value - const funcName = tokens[current].value; - current++; // Skip function name - const args = []; + // Check if this is a function definition with 'function' keyword + if (current < tokens.length && tokens[current].type === TokenType.FUNCTION) { + current++; // Skip 'function' - // Collect arguments until we hit a semicolon, closing parenthesis, or end - while (current < tokens.length && - tokens[current].type !== TokenType.SEMICOLON && - tokens[current].type !== TokenType.RIGHT_PAREN) { - // Handle function references (@functionName) - if (tokens[current] && tokens[current].type === TokenType.FUNCTION_REF) { - current++; // Skip @ - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - args.push({ - type: 'FunctionReference', - name: tokens[current].value, - }); - current++; - } else { - throw new Error('Expected function name after @'); - } + if (current >= tokens.length || tokens[current].type !== TokenType.LEFT_PAREN) { + throw new Error('Expected "(" after "function"'); + } + current++; // Skip '(' + + const parameters = []; + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_PAREN) { + if (tokens[current].type === TokenType.IDENTIFIER) { + parameters.push(tokens[current].value); + current++; } else { - // Use walk() to parse complex arguments (including arithmetic expressions) - const arg = walk(); - if (arg) { - args.push(arg); - } + throw new Error('Expected parameter name in function definition'); + } + + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; } } - value = { - type: 'FunctionCall', - name: funcName, - args, - }; + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { + throw new Error('Expected ")" after function parameters'); + } + current++; // Skip ')' - debugLog('Function call parsed in assignment', { name: funcName, args }); - } else if (tokens[current] && tokens[current].type === TokenType.NUMBER) { - // Check if this is the start of an arithmetic expression - if (tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE - )) { - // This is an arithmetic expression, use walk() to parse it - debugLog('Parsing arithmetic expression in assignment'); - debugLog('Current token before walk()', tokens[current]); - value = walk(); - debugLog('Arithmetic expression parsed', value); - } else { - // Simple number value - value = { - type: 'NumberLiteral', - value: tokens[current].value, - }; - current++; + if (current >= tokens.length || tokens[current].type !== TokenType.ASSIGNMENT) { + throw new Error('Expected ":" after function parameters'); } - } else if (tokens[current] && tokens[current].type === TokenType.STRING) { - // Simple string value - value = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; - } else if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - // Check if this is followed by table access - if (tokens[current + 1] && tokens[current + 1].type === TokenType.LEFT_BRACKET) { - // Table access with bracket notation: identifier[key] - const identifier = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; // Skip identifier - current++; // Skip opening bracket - const key = walk(); // Parse the key expression + current++; // Skip ':' + + // Parse the function body (check if it's a case expression) + let functionBody; + if (current < tokens.length && tokens[current].type === TokenType.CASE) { + // Parse case expression directly + current++; // Skip 'case' - // Expect closing bracket - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip closing bracket - } else { - throw new Error('Expected closing bracket ] in table access'); + // Parse the values being matched (can be multiple) + const values = []; + while (current < tokens.length && tokens[current].type !== TokenType.OF) { + const value = parsePrimary(); + values.push(value); } - value = { - type: 'TableAccess', - table: identifier, - key, - }; - } else if (tokens[current + 1] && tokens[current + 1].type === TokenType.DOT) { - // Table access with dot notation: identifier.key - const identifier = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; // Skip identifier - current++; // Skip the dot + // Expect 'of' + if (current >= tokens.length || tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); + } + current++; // Skip 'of' + + const cases = []; - // Expect an identifier after the dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the identifier + // Parse cases until we hit a semicolon or end + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + // If we hit an IO operation, we've reached the end of the case expression + if (current < tokens.length && + (tokens[current].type === TokenType.IO_IN || + tokens[current].type === TokenType.IO_OUT || + tokens[current].type === TokenType.IO_ASSERT)) { + break; + } + const patterns = []; + while (current < tokens.length && + tokens[current].type !== TokenType.ASSIGNMENT && + tokens[current].type !== TokenType.SEMICOLON) { + patterns.push(parsePrimary()); + } - value = { - type: 'TableAccess', - table: identifier, - key, - }; - } else { - throw new Error('Expected identifier after dot in table access'); + // Expect ':' after pattern + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + } else { + throw new Error('Expected ":" after pattern in case expression'); + } + + const result = parseLogicalExpression(); + cases.push({ + pattern: patterns, + result: [result] + }); + + // Skip semicolon if present (but don't stop parsing cases) + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + // If the next token is an identifier followed by assignment, we've reached the end of the case expression + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const nextPos = current + 1; + if (nextPos < tokens.length && tokens[nextPos].type === TokenType.ASSIGNMENT) { + break; // End of case expression + } + } + } } - } else if (tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE - )) { - // This is an infix arithmetic expression - const left = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; // Skip left operand - const operator = tokens[current].type; - current++; // Skip operator - const right = walk(); - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - - value = { - type: expressionTypes[operator], - left, - right, + functionBody = { + type: 'CaseExpression', + value: values, + cases, }; } else { - // Simple identifier value - value = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; + functionBody = parseLogicalExpression(); } - } else if (tokens[current] && ( - tokens[current].type === TokenType.PLUS || - tokens[current].type === TokenType.MINUS || - tokens[current].type === TokenType.MULTIPLY || - tokens[current].type === TokenType.DIVIDE - )) { - // Arithmetic expression value (prefix notation) - const operator = tokens[current].type; - current++; // Skip operator - const left = walk(); - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - value = { - type: expressionTypes[operator], - left, - right, + return { + type: 'Assignment', + identifier, + value: { + type: 'FunctionDefinition', + parameters, + body: functionBody + } }; } else { - // Fallback to walk() for other cases - debugLog('Using walk() fallback for assignment value', { current: tokens[current] }); - value = walk(); + // Check if this is a case expression + if (current < tokens.length && tokens[current].type === TokenType.CASE) { + // Parse the case expression directly + current++; // Skip 'case' + + // Parse the values being matched (can be multiple) + const values = []; + while (current < tokens.length && tokens[current].type !== TokenType.OF) { + const value = parsePrimary(); + values.push(value); + } + + // Expect 'of' + if (current >= tokens.length || tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); + } + current++; // Skip 'of' + + const cases = []; + + // Parse cases until we hit a semicolon or end + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + // If we hit an IO operation, we've reached the end of the case expression + if (current < tokens.length && + (tokens[current].type === TokenType.IO_IN || + tokens[current].type === TokenType.IO_OUT || + tokens[current].type === TokenType.IO_ASSERT)) { + break; + } + const patterns = []; + while (current < tokens.length && + tokens[current].type !== TokenType.ASSIGNMENT && + tokens[current].type !== TokenType.SEMICOLON) { + patterns.push(parsePrimary()); + } + + // Expect ':' after pattern + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + } else { + throw new Error('Expected ":" after pattern in case expression'); + } + + const result = parseLogicalExpression(); + cases.push({ + pattern: patterns, + result: [result] + }); + + // Skip semicolon if present (but don't stop parsing cases) + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + // If the next token is an identifier followed by assignment, we've reached the end of the case expression + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const nextPos = current + 1; + if (nextPos < tokens.length && tokens[nextPos].type === TokenType.ASSIGNMENT) { + break; // End of case expression + } + } + } + } + + return { + type: 'Assignment', + identifier, + value: { + type: 'CaseExpression', + value: values, + cases, + } + }; + } else { + // Regular assignment + const value = parseLogicalExpression(); + + return { + type: 'Assignment', + identifier, + value + }; + } } - - return { - type: 'AssignmentExpression', - name, - value, - }; - } - } - - - - // Check if this is followed by an operator - if (tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE - )) { - const left = { - type: 'Identifier', - value: token.value, - }; - const operator = tokens[current + 1].type; - current += 2; // Skip identifier and operator - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - - return { - type: expressionTypes[operator], - left, - right, - }; - } - - // Check if this is followed by an operator - if (tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE - )) { - const left = { - type: 'Identifier', - value: token.value, - }; - const operator = tokens[current + 1].type; - current += 2; // Skip identifier and operator - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - - return { - type: expressionTypes[operator], - left, - right, - }; - } - - // Check if this is followed by table access - if (tokens[current + 1] && tokens[current + 1].type === TokenType.LEFT_BRACKET) { - // Table access with bracket notation: identifier[key] - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; // Skip identifier - current++; // Skip opening bracket - const key = walk(); // Parse the key expression - - // Expect closing bracket - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip closing bracket - } else { - throw new Error('Expected closing bracket ] in table access'); } - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else if (tokens[current + 1] && tokens[current + 1].type === TokenType.DOT) { - // Table access with dot notation: identifier.key - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; // Skip identifier - current++; // Skip the dot - - // Expect an identifier after the dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the identifier - - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else { - throw new Error('Expected identifier after dot in table access'); - } + // If it's not an assignment, put the identifier back and continue + current--; } - current++; - return { - type: 'Identifier', - value: token.value, - }; + // For all other token types (identifiers, numbers, operators, etc.), call parsePrimary + // This handles atomic expressions and delegates to the appropriate parsing functions + return parsePrimary(); + } finally { + callStackTracker.pop(); } - - - - // Pattern matching: value : pattern1 : result1 pattern2 : result2 ... - if (token.type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the value being matched (wrap in array for interpreter compatibility) - const valueNode = walk(); - const value = [valueNode]; - - // Expect 'of' - if (tokens[current].type !== TokenType.OF) { - throw new Error('Expected "of" after "case"'); - } - current++; // Skip 'of' - - const cases = []; - - // Parse cases until we hit a semicolon or end - while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { - const patternNode = walk(); - const resultNode = walk(); - cases.push({ - pattern: [patternNode], - result: [resultNode] - }); - } - - return { - type: 'CaseExpression', - value, - cases, - }; - } - - if (token.type === TokenType.IF) { - current++; - let node = { - type: 'IfExpression', - test: walk(), - consequent: walk(), - alternate: tokens[current].type === TokenType.ELSE ? (current++, walk()) : null, - }; - return node; - } - - if (token.type === TokenType.FUNCTION) { - current++; - let node = { - type: 'FunctionDeclaration', - name: tokens[current++].value, - params: [], - body: [], - }; - while (tokens[current].type !== TokenType.RIGHT_PAREN) { - node.params.push(tokens[current++].value); - } - current++; // Skip right paren - while (tokens[current].type !== TokenType.RIGHT_BRACE) { - node.body.push(walk()); - } - current++; // Skip right brace - return node; - } - - if (token.type === TokenType.IDENTIFIER && tokens[current + 1].type === TokenType.LEFT_PAREN) { - current++; - let node = { - type: 'FunctionCall', - name: token.value, - args: [], - }; - current++; // Skip left paren - while (tokens[current].type !== TokenType.RIGHT_PAREN) { - node.args.push(walk()); - } - current++; // Skip right paren - return node; - } - - if (token.type === TokenType.SEMICOLON) { - current++; - return null; - } - - if (token.type === TokenType.IDENTIFIER) { - // Simple identifier - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; - - // Check if this is followed by table access - if (tokens[current] && tokens[current].type === TokenType.LEFT_BRACKET) { - // Table access with bracket notation: identifier[key] - current++; // Skip opening bracket - const key = walk(); // Parse the key expression - - // Expect closing bracket - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip closing bracket - } else { - throw new Error('Expected closing bracket ] in table access'); - } - - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else if (tokens[current] && tokens[current].type === TokenType.DOT) { - // Table access with dot notation: identifier.key - current++; // Skip the dot - - // Expect an identifier after the dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the identifier - - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else { - throw new Error('Expected identifier after dot in table access'); - } - } - - return identifier; - } - - throw new TypeError(token.type); } - - let ast = { + + const ast = { type: 'Program', - body: [], + body: [] }; - + + let lastCurrent = -1; + let loopCount = 0; + const maxLoops = tokens.length * 2; // Safety limit + while (current < tokens.length) { + // Safety check to prevent infinite loops + if (current === lastCurrent) { + loopCount++; + if (loopCount > 10) { // Allow a few iterations at the same position + throw new Error(`Parser stuck at position ${current}, token: ${tokens[current]?.type || 'EOF'}`); + } + } else { + loopCount = 0; + } + + // Safety check for maximum loops + if (loopCount > maxLoops) { + throw new Error(`Parser exceeded maximum loop count. Last position: ${current}`); + } + + lastCurrent = current; + const node = walk(); - if (node !== null && node !== undefined) { + if (node) { ast.body.push(node); } + + // Skip semicolons + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } } - + return ast; } -// Interpreter +/** + * Interpreter: Walks the AST and evaluates each node. + * + * @param {Object} ast - Abstract Syntax Tree to evaluate + * @returns {*} The result of evaluating the AST, or a Promise for async operations + * @throws {Error} For evaluation errors like division by zero, undefined variables, etc. + * + * @description Evaluates an AST by walking through each node and performing the + * 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 + * 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), + * localEvalNodeWithScope (for function bodies), and localEvalNode (for internal + * recursion). This separation allows for correct scope handling and easier debugging. + */ function interpreter(ast) { - debugLog('Starting interpreter with AST', ast); - let globalScope = {}; + const globalScope = {}; initializeStandardLibrary(globalScope); - + + // Reset call stack tracker at the start of interpretation + callStackTracker.reset(); + + /** + * Evaluates AST nodes in the global scope. + * + * @param {Object} node - AST node to evaluate + * @returns {*} The result of evaluating the node + * @throws {Error} For evaluation errors + * + * @description Main evaluation function that handles all node types in the + * global scope context. + */ function evalNode(node) { - if (!node) { - return undefined; - } - switch (node.type) { - case 'NumberLiteral': - return parseInt(node.value); - case 'StringLiteral': - return node.value; - case 'PlusExpression': - return evalNode(node.left) + evalNode(node.right); - case 'MinusExpression': - return evalNode(node.left) - evalNode(node.right); - case 'MultiplyExpression': - return evalNode(node.left) * evalNode(node.right); - case 'DivideExpression': - const divisor = evalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return evalNode(node.left) / evalNode(node.right); - case 'ModuloExpression': - return evalNode(node.left) % evalNode(node.right); - case 'PowerExpression': - return Math.pow(evalNode(node.left), evalNode(node.right)); - case 'EqualsExpression': - return evalNode(node.left) === evalNode(node.right); - case 'LessThanExpression': - return evalNode(node.left) < evalNode(node.right); - case 'GreaterThanExpression': - return evalNode(node.left) > evalNode(node.right); - case 'LessEqualExpression': - return evalNode(node.left) <= evalNode(node.right); - case 'GreaterEqualExpression': - return evalNode(node.left) >= evalNode(node.right); - case 'NotEqualExpression': - return evalNode(node.left) !== evalNode(node.right); - case 'AndExpression': - return evalNode(node.left) && evalNode(node.right); - case 'OrExpression': - return evalNode(node.left) || evalNode(node.right); - case 'XorExpression': - const leftVal = evalNode(node.left); - const rightVal = evalNode(node.right); - return (leftVal && !rightVal) || (!leftVal && rightVal); - case 'NotExpression': - return !evalNode(node.operand); - case 'BooleanLiteral': - return node.value; - case 'TableLiteral': - const table = {}; - let arrayIndex = 1; - - for (const entry of node.entries) { - if (entry.key === null) { - // Array-like entry: {1, 2, 3} - table[arrayIndex] = evalNode(entry.value); - arrayIndex++; - } else { - // Key-value entry: {name: "Alice", age: 30} - let key; - if (entry.key.type === 'Identifier') { - // Convert identifier keys to strings - key = entry.key.value; + callStackTracker.push('evalNode', node?.type || 'unknown'); + + try { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return evalNode(node.left) + evalNode(node.right); + case 'MinusExpression': + return evalNode(node.left) - evalNode(node.right); + case 'MultiplyExpression': + return evalNode(node.left) * evalNode(node.right); + case 'DivideExpression': + const divisor = evalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return evalNode(node.left) / evalNode(node.right); + case 'ModuloExpression': + return evalNode(node.left) % evalNode(node.right); + case 'PowerExpression': + return Math.pow(evalNode(node.left), evalNode(node.right)); + case 'EqualsExpression': + return evalNode(node.left) === evalNode(node.right); + case 'LessThanExpression': + return evalNode(node.left) < evalNode(node.right); + case 'GreaterThanExpression': + return evalNode(node.left) > evalNode(node.right); + case 'LessEqualExpression': + return evalNode(node.left) <= evalNode(node.right); + case 'GreaterEqualExpression': + return evalNode(node.left) >= evalNode(node.right); + case 'NotEqualExpression': + return evalNode(node.left) !== evalNode(node.right); + case 'AndExpression': + return !!(evalNode(node.left) && evalNode(node.right)); + case 'OrExpression': + return !!(evalNode(node.left) || evalNode(node.right)); + case 'XorExpression': + const leftVal = evalNode(node.left); + const rightVal = evalNode(node.right); + return !!((leftVal && !rightVal) || (!leftVal && rightVal)); + case 'NotExpression': + return !evalNode(node.operand); + case 'UnaryMinusExpression': + return -evalNode(node.operand); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = evalNode(entry.value); + arrayIndex++; } else { - // For other key types (numbers, strings), evaluate normally - key = evalNode(entry.key); + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = evalNode(entry.key); + } + const value = evalNode(entry.value); + table[key] = value; } - const value = evalNode(entry.value); - table[key] = value; } - } - - return table; - case 'TableAccess': - const tableValue = evalNode(node.table); - const keyValue = evalNode(node.key); - - if (typeof tableValue !== 'object' || tableValue === null) { - throw new Error('Cannot access property of non-table value'); - } - - if (tableValue[keyValue] === undefined) { - throw new Error(`Key '${keyValue}' not found in table`); - } - - return tableValue[keyValue]; - - case 'AssignmentExpression': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - const value = evalNode(node.value); - globalScope[node.name] = value; - return; - case 'Identifier': - const identifierValue = globalScope[node.value]; - if (identifierValue === undefined) { - throw new Error(`Variable ${node.value} is not defined`); - } - return identifierValue; - case 'FunctionReference': - const functionValue = globalScope[node.name]; - if (functionValue === undefined) { - throw new Error(`Function ${node.name} is not defined`); - } - if (typeof functionValue !== 'function') { - throw new Error(`${node.name} is not a function`); - } - return functionValue; - case 'IfExpression': - return evalNode(node.test) ? evalNode(node.consequent) : node.alternate ? evalNode(node.alternate) : undefined; - case 'FunctionDeclaration': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - globalScope[node.name] = function(...args) { - let localScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - localScope[node.params[i]] = args[i]; + + return table; + case 'TableAccess': + const tableValue = evalNode(node.table); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = evalNode(node.key); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + const value = evalNode(node.value); + globalScope[node.name] = value; + return; + case 'Assignment': + if (globalScope.hasOwnProperty(node.identifier)) { + throw new Error(`Cannot reassign immutable variable: ${node.identifier}`); } - // Create a new evalNode function that uses localScope - const localEvalNode = (node) => { - if (!node) { - return undefined; + const assignmentValue = evalNode(node.value); + globalScope[node.identifier] = assignmentValue; + return; + case 'Identifier': + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined) { + throw new Error(`Variable ${node.value} is not defined`); + } + return identifierValue; + case 'FunctionDeclaration': + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + 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]; + } + return localEvalNodeWithScope(node.body, localScope); + } finally { + callStackTracker.pop(); } - switch (node.type) { - case 'NumberLiteral': - return parseInt(node.value); - case 'StringLiteral': - return node.value; - case 'PlusExpression': - return localEvalNode(node.left) + localEvalNode(node.right); - case 'MinusExpression': - return localEvalNode(node.left) - localEvalNode(node.right); - case 'MultiplyExpression': - return localEvalNode(node.left) * localEvalNode(node.right); - case 'DivideExpression': - const divisor = localEvalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return localEvalNode(node.left) / localEvalNode(node.right); - case 'ModuloExpression': - return localEvalNode(node.left) % localEvalNode(node.right); - case 'PowerExpression': - return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); - case 'EqualsExpression': - return localEvalNode(node.left) === localEvalNode(node.right); - case 'LessThanExpression': - return localEvalNode(node.left) < localEvalNode(node.right); - case 'GreaterThanExpression': - return localEvalNode(node.left) > localEvalNode(node.right); - case 'LessEqualExpression': - return localEvalNode(node.left) <= localEvalNode(node.right); - case 'GreaterEqualExpression': - return localEvalNode(node.left) >= localEvalNode(node.right); - case 'NotEqualExpression': - return localEvalNode(node.left) !== localEvalNode(node.right); - case 'AndExpression': - return localEvalNode(node.left) && localEvalNode(node.right); - case 'OrExpression': - return localEvalNode(node.left) || localEvalNode(node.right); - case 'XorExpression': - const leftVal = localEvalNode(node.left); - const rightVal = localEvalNode(node.right); - return (leftVal && !rightVal) || (!leftVal && rightVal); - case 'NotExpression': - return !localEvalNode(node.operand); - - case 'AssignmentExpression': - if (localScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - localScope[node.name] = localEvalNode(node.value); - return; - case 'Identifier': - const identifierValue = localScope[node.value]; - if (identifierValue === undefined && node.value) { - return node.value; // Return as string literal if not found in scope - } - return identifierValue; - case 'FunctionReference': - const localFunctionValue = localScope[node.name]; - if (localFunctionValue === undefined) { - throw new Error(`Function ${node.name} is not defined`); - } - if (typeof localFunctionValue !== 'function') { - throw new Error(`${node.name} is not a function`); - } - return localFunctionValue; - case 'IfExpression': - return localEvalNode(node.test) ? localEvalNode(node.consequent) : node.alternate ? localEvalNode(node.alternate) : undefined; - case 'FunctionDeclaration': - if (localScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - localScope[node.name] = function(...args) { - let nestedScope = Object.create(localScope); - for (let i = 0; i < node.params.length; i++) { - nestedScope[node.params[i]] = args[i]; - } - return localEvalNode(node.body); - }; - return; - case 'FunctionCall': - if (localScope[node.name] instanceof Function) { - let args = node.args.map(localEvalNode); - return localScope[node.name](...args); - } - throw new Error(`Function ${node.name} is not defined`); - case 'CaseExpression': - // Evaluate the values being matched (e.g., [x, y]) - const values = node.value.map(localEvalNode); - - for (const caseItem of node.cases) { - // Evaluate the pattern (e.g., [0, 0] or [0, _]) - const pattern = caseItem.pattern.map(localEvalNode); + }; + case 'FunctionDefinition': + // Create a function from the function definition + return function(...args) { + callStackTracker.push('FunctionCall', node.parameters.join(',')); + try { + let localScope = Object.create(globalScope); + for (let i = 0; i < node.parameters.length; i++) { + localScope[node.parameters[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, localScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionCall': + let funcToCall; // Renamed from 'func' to avoid redeclaration + if (typeof node.name === 'string') { + // Regular function call with string name + funcToCall = globalScope[node.name]; + } 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); + } else { + throw new Error('Invalid function name in function call'); + } - // Check if pattern matches values - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; + if (funcToCall instanceof Function) { + let args = node.args.map(evalNode); + return funcToCall(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'CaseExpression': + const values = node.value.map(evalNode); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(evalNode); - // Wildcard always matches - if (patternValue === true) continue; + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + if (patternValue === true) continue; + + if (value !== patternValue) { + matches = false; + break; + } + } - // Otherwise, values must be equal - if (value !== patternValue) { - matches = false; - break; + if (matches) { + const results = caseItem.result.map(evalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); } } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const outputValue = evalNode(node.value); + console.log(outputValue); + return outputValue; + case 'IOAssertExpression': + const assertionValue = evalNode(node.value); + if (!assertionValue) { + throw new Error('Assertion failed'); + } + return assertionValue; + case 'FunctionReference': + const functionValue = globalScope[node.name]; + if (functionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof functionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return functionValue; + case 'ArrowExpression': + // Arrow expressions are function bodies that should be evaluated + return evalNode(node.body); + default: + throw new Error(`Unknown node type: ${node.type}`); + } + } finally { + callStackTracker.pop(); + } + } + + /** + * Evaluates AST nodes in a local scope with access to parent scope. + * + * @param {Object} node - AST node to evaluate + * @param {Object} scope - Local scope object (prototypally inherits from global) + * @returns {*} The result of evaluating the node + * @throws {Error} For evaluation errors + * + * @description Used for evaluating function bodies and other expressions + * that need access to both local and global variables. + */ + const localEvalNodeWithScope = (node, scope) => { + callStackTracker.push('localEvalNodeWithScope', node?.type || 'unknown'); + + try { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return localEvalNodeWithScope(node.left, scope) + localEvalNodeWithScope(node.right, scope); + case 'MinusExpression': + return localEvalNodeWithScope(node.left, scope) - localEvalNodeWithScope(node.right, scope); + case 'MultiplyExpression': + return localEvalNodeWithScope(node.left, scope) * localEvalNodeWithScope(node.right, scope); + case 'DivideExpression': + const divisor = localEvalNodeWithScope(node.right, scope); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return localEvalNodeWithScope(node.left, scope) / localEvalNodeWithScope(node.right, scope); + case 'ModuloExpression': + return localEvalNodeWithScope(node.left, scope) % localEvalNodeWithScope(node.right, scope); + case 'PowerExpression': + return Math.pow(localEvalNodeWithScope(node.left, scope), localEvalNodeWithScope(node.right, scope)); + case 'EqualsExpression': + return localEvalNodeWithScope(node.left, scope) === localEvalNodeWithScope(node.right, scope); + case 'LessThanExpression': + return localEvalNodeWithScope(node.left, scope) < localEvalNodeWithScope(node.right, scope); + case 'GreaterThanExpression': + return localEvalNodeWithScope(node.left, scope) > localEvalNodeWithScope(node.right, scope); + case 'LessEqualExpression': + return localEvalNodeWithScope(node.left, scope) <= localEvalNodeWithScope(node.right, scope); + case 'GreaterEqualExpression': + return localEvalNodeWithScope(node.left, scope) >= localEvalNodeWithScope(node.right, scope); + case 'NotEqualExpression': + return localEvalNodeWithScope(node.left, scope) !== localEvalNodeWithScope(node.right, scope); + case 'AndExpression': + return !!(localEvalNodeWithScope(node.left, scope) && localEvalNodeWithScope(node.right, scope)); + case 'OrExpression': + return !!(localEvalNodeWithScope(node.left, scope) || localEvalNodeWithScope(node.right, scope)); + case 'XorExpression': + const leftVal = localEvalNodeWithScope(node.left, scope); + const rightVal = localEvalNodeWithScope(node.right, scope); + return !!((leftVal && !rightVal) || (!leftVal && rightVal)); + case 'NotExpression': + return !localEvalNodeWithScope(node.operand, scope); + case 'UnaryMinusExpression': + return -localEvalNodeWithScope(node.operand, scope); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; - if (matches) { - // Evaluate all results - const results = caseItem.result.map(localEvalNode); - // If there's only one result, return it directly (preserves number type) - if (results.length === 1) { - return results[0]; + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNodeWithScope(entry.value, scope); + arrayIndex++; + } else { + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNodeWithScope(entry.key, scope); + } + const value = localEvalNodeWithScope(entry.value, scope); + table[key] = value; } - // Otherwise join multiple results as string - return results.join(' '); } - } - throw new Error('No matching pattern found'); - case 'WildcardPattern': - return true; // Wildcard always matches - case 'IOInExpression': - // Read from stdin - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); - const num = parseInt(input); - resolve(isNaN(num) ? input : num); - }); - }); - case 'IOOutExpression': - // Output to stdout - const localOutputValue = localEvalNode(node.value); - console.log(localOutputValue); - return localOutputValue; - case 'IOAssertExpression': - // Assert that left operator right is true - const localLeftValue = localEvalNode(node.left); - const localRightValue = localEvalNode(node.right); - - let localAssertionPassed = false; - switch (node.operator) { - case TokenType.EQUALS: - localAssertionPassed = localLeftValue === localRightValue; - break; - case TokenType.LESS_THAN: - localAssertionPassed = localLeftValue < localRightValue; - break; - case TokenType.GREATER_THAN: - localAssertionPassed = localLeftValue > localRightValue; - break; - case TokenType.LESS_EQUAL: - localAssertionPassed = localLeftValue <= localRightValue; - break; - case TokenType.GREATER_EQUAL: - localAssertionPassed = localLeftValue >= localRightValue; - break; - case TokenType.NOT_EQUAL: - localAssertionPassed = localLeftValue !== localRightValue; - break; - default: - throw new Error(`Unsupported comparison operator in assertion: ${node.operator}`); + + return table; + case 'TableAccess': + const tableValue = localEvalNodeWithScope(node.table, scope); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = localEvalNodeWithScope(node.key, scope); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + globalScope[node.name] = localEvalNodeWithScope(node.value, scope); + return; + case 'Identifier': + // First check local scope, then global scope + if (scope && scope.hasOwnProperty(node.value)) { + return scope[node.value]; + } + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined && node.value) { + return node.value; + } + return identifierValue; + case 'FunctionDeclaration': + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + return function(...args) { + callStackTracker.push('FunctionCall', node.params.join(',')); + try { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + nestedScope[node.params[i]] = args[i]; } - - if (!localAssertionPassed) { - const operatorSymbol = { - [TokenType.EQUALS]: '=', - [TokenType.LESS_THAN]: '<', - [TokenType.GREATER_THAN]: '>', - [TokenType.LESS_EQUAL]: '<=', - [TokenType.GREATER_EQUAL]: '>=', - [TokenType.NOT_EQUAL]: '!=' - }[node.operator]; - throw new Error(`Assertion failed: ${JSON.stringify(localLeftValue)} ${operatorSymbol} ${JSON.stringify(localRightValue)}`); + return localEvalNodeWithScope(node.body, nestedScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionDefinition': + // Create a function from the function definition + return function(...args) { + callStackTracker.push('FunctionCall', node.parameters.join(',')); + try { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.parameters.length; i++) { + nestedScope[node.parameters[i]] = args[i]; } - return localLeftValue; - default: - throw new Error(`Unknown node type: ${node.type}`); + return localEvalNodeWithScope(node.body, nestedScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionCall': + let localFunc; + if (typeof node.name === 'string') { + // Regular function call with string name + localFunc = globalScope[node.name]; + } 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'); } - }; - return localEvalNode(node.body); - }; - return; - case 'FunctionCall': - if (globalScope[node.name] instanceof Function) { - let args = node.args.map(evalNode); - return globalScope[node.name](...args); - } - throw new Error(`Function ${node.name} is not defined`); - case 'CaseExpression': - debugLog('CaseExpression evaluation', { value: node.value, cases: node.cases }); - // Evaluate the values being matched (e.g., [x, y]) - const values = node.value.map(evalNode); - - for (const caseItem of node.cases) { - // Evaluate the pattern (e.g., [0, 0] or [0, _]) - const pattern = caseItem.pattern.map(evalNode); - // Check if pattern matches values - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; + if (localFunc instanceof Function) { + let args = node.args.map(arg => localEvalNodeWithScope(arg, scope)); + return localFunc(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'CaseExpression': + const values = node.value.map(val => localEvalNodeWithScope(val, scope)); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope)); - // Wildcard always matches - if (patternValue === true) continue; + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + if (patternValue === true) continue; + + if (value !== patternValue) { + matches = false; + break; + } + } - // Otherwise, values must be equal - if (value !== patternValue) { - matches = false; - break; + if (matches) { + const results = caseItem.result.map(res => localEvalNodeWithScope(res, scope)); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); } } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNodeWithScope(node.value, scope); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + const localAssertionValue = localEvalNodeWithScope(node.value, scope); + if (!localAssertionValue) { + throw new Error('Assertion failed'); + } + return localAssertionValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + case 'ArrowExpression': + // Arrow expressions are function bodies that should be evaluated + return localEvalNodeWithScope(node.body, scope); + default: + throw new Error(`Unknown node type: ${node.type}`); + } + } finally { + callStackTracker.pop(); + } + }; + + /** + * Evaluates AST nodes in the global scope (internal recursion helper). + * + * @param {Object} node - AST node to evaluate + * @returns {*} The result of evaluating the node + * @throws {Error} For evaluation errors + * + * @description Internal helper function for recursive evaluation that + * always uses the global scope. Used to avoid circular dependencies. + */ + const localEvalNode = (node) => { + callStackTracker.push('localEvalNode', node?.type || 'unknown'); + + try { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return localEvalNode(node.left) + localEvalNode(node.right); + case 'MinusExpression': + return localEvalNode(node.left) - localEvalNode(node.right); + case 'MultiplyExpression': + return localEvalNode(node.left) * localEvalNode(node.right); + case 'DivideExpression': + const divisor = localEvalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return localEvalNode(node.left) / localEvalNode(node.right); + case 'ModuloExpression': + return localEvalNode(node.left) % localEvalNode(node.right); + case 'PowerExpression': + return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); + case 'EqualsExpression': + return localEvalNode(node.left) === localEvalNode(node.right); + case 'LessThanExpression': + return localEvalNode(node.left) < localEvalNode(node.right); + case 'GreaterThanExpression': + return localEvalNode(node.left) > localEvalNode(node.right); + case 'LessEqualExpression': + return localEvalNode(node.left) <= localEvalNode(node.right); + case 'GreaterEqualExpression': + return localEvalNode(node.left) >= localEvalNode(node.right); + case 'NotEqualExpression': + return localEvalNode(node.left) !== localEvalNode(node.right); + case 'AndExpression': + return !!(localEvalNode(node.left) && localEvalNode(node.right)); + case 'OrExpression': + return !!(localEvalNode(node.left) || localEvalNode(node.right)); + case 'XorExpression': + const leftVal = localEvalNode(node.left); + const rightVal = localEvalNode(node.right); + return !!((leftVal && !rightVal) || (!leftVal && rightVal)); + case 'NotExpression': + return !localEvalNode(node.operand); + case 'UnaryMinusExpression': + return -localEvalNode(node.operand); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; - if (matches) { - // Evaluate all results - const results = caseItem.result.map(evalNode); - // If there's only one result, return it directly (preserves number type) - if (results.length === 1) { - return results[0]; + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNode(entry.value); + arrayIndex++; + } else { + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNode(entry.key); + } + const value = localEvalNode(entry.value); + table[key] = value; } - // Otherwise join multiple results as string - return results.join(' '); } - } - throw new Error('No matching pattern found'); - case 'WildcardPattern': - return true; // Wildcard always matches - case 'IOInExpression': - // Read from stdin - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); - const num = parseInt(input); - resolve(isNaN(num) ? input : num); - }); - }); - case 'IOOutExpression': - // Output to stdout - const outputValue = evalNode(node.value); - console.log(outputValue); - return outputValue; - case 'IOAssertExpression': - // Assert that left operator right is true - const leftValue = evalNode(node.left); - const rightValue = evalNode(node.right); - - let assertionPassed = false; - switch (node.operator) { - case TokenType.EQUALS: - assertionPassed = leftValue === rightValue; - break; - case TokenType.LESS_THAN: - assertionPassed = leftValue < rightValue; - break; - case TokenType.GREATER_THAN: - assertionPassed = leftValue > rightValue; - break; - case TokenType.LESS_EQUAL: - assertionPassed = leftValue <= rightValue; - break; - case TokenType.GREATER_EQUAL: - assertionPassed = leftValue >= rightValue; - break; - case TokenType.NOT_EQUAL: - assertionPassed = leftValue !== rightValue; - break; - default: - throw new Error(`Unsupported comparison operator in assertion: ${node.operator}`); - } - - if (!assertionPassed) { - const operatorSymbol = { - [TokenType.EQUALS]: '=', - [TokenType.LESS_THAN]: '<', - [TokenType.GREATER_THAN]: '>', - [TokenType.LESS_EQUAL]: '<=', - [TokenType.GREATER_EQUAL]: '>=', - [TokenType.NOT_EQUAL]: '!=' - }[node.operator]; - throw new Error(`Assertion failed: ${JSON.stringify(leftValue)} ${operatorSymbol} ${JSON.stringify(rightValue)}`); - } - return leftValue; - default: - throw new Error(`Unknown node type: ${node.type}`); + + return table; + case 'TableAccess': + const tableValue = localEvalNode(node.table); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = localEvalNode(node.key); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + globalScope[node.name] = localEvalNode(node.value); + return; + case 'Identifier': + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined && node.value) { + return node.value; + } + return identifierValue; + case 'FunctionDeclaration': + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + return function(...args) { + callStackTracker.push('FunctionCall', node.params.join(',')); + try { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + nestedScope[node.params[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, nestedScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionDefinition': + // Create a function from the function definition + return function(...args) { + callStackTracker.push('FunctionCall', node.parameters.join(',')); + try { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.parameters.length; i++) { + nestedScope[node.parameters[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, nestedScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionCall': + let localFunc; + if (typeof node.name === 'string') { + // Regular function call with string name + localFunc = globalScope[node.name]; + } 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'); + } + + if (localFunc instanceof Function) { + let args = node.args.map(localEvalNode); + return localFunc(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'CaseExpression': + const values = node.value.map(localEvalNode); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(localEvalNode); + + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + if (patternValue === true) continue; + + if (value !== patternValue) { + matches = false; + break; + } + } + + if (matches) { + const results = caseItem.result.map(localEvalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNode(node.value); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + const localAssertionValue = localEvalNode(node.value); + if (!localAssertionValue) { + throw new Error('Assertion failed'); + } + return localAssertionValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + case 'ArrowExpression': + // Arrow expressions are function bodies that should be evaluated + return localEvalNode(node.body); + default: + throw new Error(`Unknown node type: ${node.type}`); + } + } finally { + callStackTracker.pop(); } - } + }; let lastResult; for (let node of ast.body) { - if (node) { // Skip undefined nodes + if (node) { lastResult = evalNode(node); } } - // Handle async results (from interactive IO operations) if (lastResult instanceof Promise) { return lastResult.then(result => { return result; @@ -2390,453 +2733,233 @@ function interpreter(ast) { return lastResult; } -// Debug configuration -const DEBUG = process.env.DEBUG === 'true' || process.argv.includes('--debug'); - +/** + * Debug logging utility function. + * + * @param {string} message - Debug message to log + * @param {*} [data=null] - Optional data to log with the message + * + * @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 + * verbose output during development and silent operation in production. This + * approach makes it easy to trace execution and diagnose issues without + * cluttering normal output. + */ function debugLog(message, data = null) { - if (DEBUG) { + if (process.env.DEBUG) { + console.log(`[DEBUG] ${message}`); if (data) { - console.log(`[DEBUG] ${message}:`, JSON.stringify(data, null, 2)); - } else { - console.log(`[DEBUG] ${message}`); + console.log(data); } } } +/** + * Debug error logging utility function. + * + * @param {string} message - Debug error message to log + * @param {Error} [error=null] - Optional error object to log + * + * @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 + * verbose output during development and silent operation in production. This + * approach makes it easy to trace execution and diagnose issues without + * cluttering normal output. + */ function debugError(message, error = null) { - if (DEBUG) { + if (process.env.DEBUG) { console.error(`[DEBUG ERROR] ${message}`); if (error) { - console.error(`[DEBUG ERROR] Details:`, error); + console.error(error); } } } -// Main execution logic -const fs = require('fs'); -const readline = require('readline'); +/** + * 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. + */ +const callStackTracker = { + stack: [], + maxDepth: 0, + callCounts: new Map(), + + /** + * Push a function call onto the stack + * @param {string} functionName - Name of the function being called + * @param {string} context - Context where the call is happening + */ + push: function(functionName, context = '') { + const callInfo = { functionName, context, timestamp: Date.now() }; + this.stack.push(callInfo); + + // Track maximum depth + if (this.stack.length > this.maxDepth) { + this.maxDepth = this.stack.length; + } + + // Count function calls + const key = `${functionName}${context ? `:${context}` : ''}`; + this.callCounts.set(key, (this.callCounts.get(key) || 0) + 1); + + // Check for potential infinite recursion + if (this.stack.length > 1000) { + console.error('=== POTENTIAL INFINITE RECURSION DETECTED ==='); + console.error('Call stack depth:', this.stack.length); + console.error('Function call counts:', Object.fromEntries(this.callCounts)); + console.error('Recent call stack:'); + this.stack.slice(-10).forEach((call, i) => { + console.error(` ${this.stack.length - 10 + i}: ${call.functionName}${call.context ? ` (${call.context})` : ''}`); + }); + throw new Error(`Potential infinite recursion detected. Call stack depth: ${this.stack.length}`); + } + + if (process.env.DEBUG && this.stack.length % 100 === 0) { + console.log(`[DEBUG] Call stack depth: ${this.stack.length}, Max: ${this.maxDepth}`); + } + }, + + /** + * Pop a function call from the stack + */ + pop: function() { + return this.stack.pop(); + }, + + /** + * Get current stack depth + */ + getDepth: function() { + return this.stack.length; + }, + + /** + * Get call statistics + */ + getStats: function() { + return { + currentDepth: this.stack.length, + maxDepth: this.maxDepth, + callCounts: Object.fromEntries(this.callCounts) + }; + }, + + /** + * Reset the tracker + */ + reset: function() { + this.stack = []; + this.maxDepth = 0; + this.callCounts.clear(); + } +}; /** - * Execute a script file + * Reads a file, tokenizes, parses, and interprets it. + * + * @param {string} filePath - Path to the file to execute + * @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. + * + * @note Supports both synchronous and asynchronous execution, with proper + * error handling and process exit codes. */ function executeFile(filePath) { try { - debugLog(`Executing file: ${filePath}`); + // Validate file extension + if (!filePath.endsWith('.txt')) { + throw new Error('Only .txt files are supported'); + } + + const fs = require('fs'); + const input = fs.readFileSync(filePath, 'utf8'); - const input = fs.readFileSync(filePath, 'utf-8'); - debugLog('Input content', input); + debugLog('Input:', input); const tokens = lexer(input); - debugLog('Tokens generated', tokens); + debugLog('Tokens:', tokens); const ast = parser(tokens); - debugLog('AST generated', ast); + debugLog('AST:', JSON.stringify(ast, null, 2)); const result = interpreter(ast); - debugLog('Interpreter result', result); - // Only print result if it's not undefined - if (result !== undefined) { - console.log(result); + if (result instanceof Promise) { + result.then(finalResult => { + if (finalResult !== undefined) { + console.log(finalResult); + } + // Print call stack statistics after execution + const stats = callStackTracker.getStats(); + console.log('\n=== CALL STACK STATISTICS ==='); + console.log('Maximum call stack depth:', stats.maxDepth); + console.log('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + }).catch(error => { + console.error(`Error executing file: ${error.message}`); + // Print call stack statistics on error + const stats = callStackTracker.getStats(); + console.error('\n=== CALL STACK STATISTICS ON ERROR ==='); + console.error('Maximum call stack depth:', stats.maxDepth); + console.error('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + process.exit(1); + }); + } else { + if (result !== undefined) { + console.log(result); + } + // Print call stack statistics after execution + const stats = callStackTracker.getStats(); + console.log('\n=== CALL STACK STATISTICS ==='); + console.log('Maximum call stack depth:', stats.maxDepth); + console.log('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); } } catch (error) { - debugError('Execution failed', error); console.error(`Error executing file: ${error.message}`); + // Print call stack statistics on error + const stats = callStackTracker.getStats(); + console.error('\n=== CALL STACK STATISTICS ON ERROR ==='); + console.error('Maximum call stack depth:', stats.maxDepth); + console.error('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); process.exit(1); } } /** - * Start REPL mode - */ -function startREPL() { - console.log('Scripting Language REPL'); - console.log('Type expressions to evaluate. End with semicolon (;) to execute.'); - console.log('Type "exit" to quit.'); - console.log(''); - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - prompt: '> ' - }); - - // Create a persistent global scope for the REPL - let globalScope = {}; - initializeStandardLibrary(globalScope); - let currentInput = ''; - - rl.prompt(); - - rl.on('line', (line) => { - const trimmedLine = line.trim(); - - if (trimmedLine === 'exit' || trimmedLine === 'quit') { - rl.close(); - return; - } - - // Add the line to current input - currentInput += line + '\n'; - - // Check if the input ends with a semicolon - if (currentInput.trim().endsWith(';')) { - try { - const tokens = lexer(currentInput); - const ast = parser(tokens); - - // Create a custom interpreter that uses the persistent scope - const result = interpreterWithScope(ast, globalScope); - - // Only print result if it's not undefined - if (result !== undefined) { - console.log(result); - } - } catch (error) { - console.error(`Error: ${error.message}`); - } - - // Reset current input for next expression - currentInput = ''; - } - - rl.prompt(); - }); - - rl.on('close', () => { - console.log('Goodbye!'); - process.exit(0); - }); -} - -/** - * Interpreter that uses a provided global scope + * CLI argument handling and program entry point. + * + * @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 + * enforces this usage and provides helpful error messages for incorrect invocation. + * + * @note Exits with appropriate error codes for different failure scenarios. */ -function interpreterWithScope(ast, globalScope) { - function evalNode(node) { - if (!node) { - return undefined; - } - switch (node.type) { - case 'NumberLiteral': - return parseInt(node.value); - case 'StringLiteral': - return node.value; - case 'PlusExpression': - return evalNode(node.left) + evalNode(node.right); - case 'MinusExpression': - return evalNode(node.left) - evalNode(node.right); - case 'MultiplyExpression': - return evalNode(node.left) * evalNode(node.right); - case 'DivideExpression': - const divisor = evalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return evalNode(node.left) / evalNode(node.right); - case 'TableLiteral': - const table = {}; - let arrayIndex = 1; - - for (const entry of node.entries) { - if (entry.key === null) { - // Array-like entry: {1, 2, 3} - table[arrayIndex] = evalNode(entry.value); - arrayIndex++; - } else { - // Key-value entry: {name: "Alice", age: 30} - let key; - if (entry.key.type === 'Identifier') { - // Convert identifier keys to strings - key = entry.key.value; - } else { - // For other key types (numbers, strings), evaluate normally - key = evalNode(entry.key); - } - const value = evalNode(entry.value); - table[key] = value; - } - } - - return table; - case 'TableAccess': - const tableValue = evalNode(node.table); - const keyValue = evalNode(node.key); - - if (typeof tableValue !== 'object' || tableValue === null) { - throw new Error('Cannot access property of non-table value'); - } - - if (tableValue[keyValue] === undefined) { - throw new Error(`Key '${keyValue}' not found in table`); - } - - return tableValue[keyValue]; - case 'AssignmentExpression': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - const value = evalNode(node.value); - globalScope[node.name] = value; - return; - case 'Identifier': - const identifierValue = globalScope[node.value]; - if (identifierValue === undefined) { - throw new Error(`Variable ${node.value} is not defined`); - } - return identifierValue; - case 'FunctionDeclaration': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - globalScope[node.name] = function(...args) { - let localScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - localScope[node.params[i]] = args[i]; - } - return localEvalNode(node.body); - }; - return; - case 'FunctionCall': - if (globalScope[node.name] instanceof Function) { - let args = node.args.map(evalNode); - return globalScope[node.name](...args); - } - throw new Error(`Function ${node.name} is not defined`); - case 'CaseExpression': - const values = node.value.map(evalNode); - - for (const caseItem of node.cases) { - const pattern = caseItem.pattern.map(evalNode); - - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; - - if (patternValue === true) continue; - - if (value !== patternValue) { - matches = false; - break; - } - } - - if (matches) { - const results = caseItem.result.map(evalNode); - if (results.length === 1) { - return results[0]; - } - return results.join(' '); - } - } - throw new Error('No matching pattern found'); - case 'WildcardPattern': - return true; - case 'IOInExpression': - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); - const num = parseInt(input); - resolve(isNaN(num) ? input : num); - }); - }); - case 'IOOutExpression': - const outputValue = evalNode(node.value); - console.log(outputValue); - return outputValue; - case 'BooleanLiteral': - return node.value; - case 'FunctionReference': - const functionValue = globalScope[node.name]; - if (functionValue === undefined) { - throw new Error(`Function ${node.name} is not defined`); - } - if (typeof functionValue !== 'function') { - throw new Error(`${node.name} is not a function`); - } - return functionValue; - - default: - throw new Error(`Unknown node type: ${node.type}`); - } - } - - const localEvalNode = (node) => { - if (!node) { - return undefined; - } - switch (node.type) { - case 'NumberLiteral': - return parseInt(node.value); - case 'StringLiteral': - return node.value; - case 'PlusExpression': - return localEvalNode(node.left) + localEvalNode(node.right); - case 'MinusExpression': - return localEvalNode(node.left) - localEvalNode(node.right); - case 'MultiplyExpression': - return localEvalNode(node.left) * localEvalNode(node.right); - case 'DivideExpression': - const divisor = localEvalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return localEvalNode(node.left) / localEvalNode(node.right); - case 'ModuloExpression': - return localEvalNode(node.left) % localEvalNode(node.right); - case 'PowerExpression': - return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); - case 'EqualsExpression': - return localEvalNode(node.left) === localEvalNode(node.right); - case 'LessThanExpression': - return localEvalNode(node.left) < localEvalNode(node.right); - case 'GreaterThanExpression': - return localEvalNode(node.left) > localEvalNode(node.right); - case 'LessEqualExpression': - return localEvalNode(node.left) <= localEvalNode(node.right); - case 'GreaterEqualExpression': - return localEvalNode(node.left) >= localEvalNode(node.right); - case 'NotEqualExpression': - return localEvalNode(node.left) !== localEvalNode(node.right); - case 'AndExpression': - return localEvalNode(node.left) && localEvalNode(node.right); - case 'OrExpression': - return localEvalNode(node.left) || localEvalNode(node.right); - case 'XorExpression': - const leftVal = localEvalNode(node.left); - const rightVal = localEvalNode(node.right); - return (leftVal && !rightVal) || (!leftVal && rightVal); - case 'NotExpression': - return !localEvalNode(node.operand); - case 'AssignmentExpression': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - globalScope[node.name] = localEvalNode(node.value); - return; - case 'Identifier': - const identifierValue = globalScope[node.value]; - if (identifierValue === undefined && node.value) { - return node.value; - } - return identifierValue; - case 'FunctionDeclaration': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - globalScope[node.name] = function(...args) { - let nestedScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - nestedScope[node.params[i]] = args[i]; - } - return localEvalNode(node.body); - }; - return; - case 'FunctionCall': - if (globalScope[node.name] instanceof Function) { - let args = node.args.map(localEvalNode); - return globalScope[node.name](...args); - } - throw new Error(`Function ${node.name} is not defined`); - case 'CaseExpression': - const values = node.value.map(localEvalNode); - - for (const caseItem of node.cases) { - const pattern = caseItem.pattern.map(localEvalNode); - - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; - - if (patternValue === true) continue; - - if (value !== patternValue) { - matches = false; - break; - } - } - - if (matches) { - const results = caseItem.result.map(localEvalNode); - if (results.length === 1) { - return results[0]; - } - return results.join(' '); - } - } - throw new Error('No matching pattern found'); - case 'WildcardPattern': - return true; - case 'IOInExpression': - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); - const num = parseInt(input); - resolve(isNaN(num) ? input : num); - }); - }); - case 'IOOutExpression': - const localOutputValue = localEvalNode(node.value); - console.log(localOutputValue); - return localOutputValue; - case 'FunctionReference': - const localFunctionValue = globalScope[node.name]; - if (localFunctionValue === undefined) { - throw new Error(`Function ${node.name} is not defined`); - } - if (typeof localFunctionValue !== 'function') { - throw new Error(`${node.name} is not a function`); - } - return localFunctionValue; - default: - throw new Error(`Unknown node type: ${node.type}`); - } - }; - - let lastResult; - for (let node of ast.body) { - if (node) { - lastResult = evalNode(node); - } - } - - if (lastResult instanceof Promise) { - return lastResult.then(result => { - return result; - }); - } - - return lastResult; -} - -// Check command line arguments const args = process.argv.slice(2); if (args.length === 0) { - // No arguments - start REPL mode - startREPL(); + console.error('Usage: node lang.js <file>'); + console.error(' Provide a file path to execute'); + process.exit(1); } else if (args.length === 1) { - // One argument - execute the file + // Execute the file const filePath = args[0]; executeFile(filePath); } else { // Too many arguments - console.error('Usage: node lang.js [file]'); - console.error(' If no file is provided, starts REPL mode'); - console.error(' If file is provided, executes the file'); + console.error('Usage: node lang.js <file>'); + console.error(' Provide exactly one file path to execute'); process.exit(1); } \ No newline at end of file diff --git a/js/scripting-lang/learn_scripting_lang.txt b/js/scripting-lang/learn_scripting_lang.txt new file mode 100644 index 0000000..fc4f966 --- /dev/null +++ b/js/scripting-lang/learn_scripting_lang.txt @@ -0,0 +1,61 @@ +/* Learn an as of yet unnamed language in Y Minutes */ +/* A functional programming language with immutable variables, + first-class functions, and pattern matching */ + +/* We've got numbers */ +x : 42; /* also the ability to assign values to variables */ +y : 3.14; +z : 0; + +/* We've got identifiers */ +this_is_a : "test"; +flag : true; +value : false; + +/* We've got basic mathematical operators */ +sum : x + y; +diff : x - y; +prod : x * y; +quot : x / y; + +/* We've got pattern matching case statements! */ +result : case x of + 42 : "correct" + _ : "wrong"; + +/* Of course, there are functions */ +double : x -> x * 2; +add : x y -> x + y; +func_result : double 5; + +/* And immutable tables, kinda inspired by Lua's tables */ +table : {1, 2, 3, name: "Alice", age: 30}; +first : table[1]; +table_name : table.name; + +/* A boring standard library */ +square : x -> x * x; +mapped : map @double 5; +composed : compose @double @square 3; + +/* Special functions for IO all start with .. */ +..out "Hello from the scripting language!"; +..out x; +..out func_result; +..out result; +..out first; +..out table_name; +..out mapped; +..out composed; + +/* There's a baked in IO function for performing assertions */ +..assert x = 42; +..assert func_result = 10; +..assert result = "correct"; +..assert first = 1; +..assert table_name = "Alice"; +..assert mapped = 10; +..assert composed = 18; +..assert 5 > 3; /* ..assert should work with any kinda operators */ + +..out "Learn Scripting Language tutorial completed!"; \ No newline at end of file diff --git a/js/scripting-lang/nested_test.txt b/js/scripting-lang/nested_test.txt new file mode 100644 index 0000000..afb0677 --- /dev/null +++ b/js/scripting-lang/nested_test.txt @@ -0,0 +1,7 @@ +/* Test nested function calls in case expressions */ +factorial : n -> case n of + 0 : 1 + _ : factorial (n - 1); + +test : factorial 5; +..out test; \ No newline at end of file diff --git a/js/scripting-lang/paren_test.txt b/js/scripting-lang/paren_test.txt new file mode 100644 index 0000000..990858b --- /dev/null +++ b/js/scripting-lang/paren_test.txt @@ -0,0 +1,7 @@ +/* Test parentheses in case expressions */ +factorial : n -> case n of + 0 : 1 + _ : (n - 1); + +test : factorial 5; +..out test; \ No newline at end of file diff --git a/js/scripting-lang/run_tests.sh b/js/scripting-lang/run_tests.sh new file mode 100755 index 0000000..b456ff0 --- /dev/null +++ b/js/scripting-lang/run_tests.sh @@ -0,0 +1,123 @@ +#!/bin/bash + +# Test Runner for Simple Scripting Language +# Runs unit tests and integration tests systematically + +echo "=== Simple Scripting Language Test Suite ===" +echo "" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to run a test +run_test() { + local test_file=$1 + local test_name=$2 + + echo -n "Running $test_name... " + + # Capture both stdout and stderr, and get the exit code + local output + local exit_code + output=$(node lang.js "$test_file" 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo -e "${GREEN}PASS${NC}" + return 0 + else + echo -e "${RED}FAIL${NC}" + echo -e "${RED}Error:${NC} $output" + return 1 + fi +} + +# Function to run a test with output +run_test_with_output() { + local test_file=$1 + local test_name=$2 + + echo -e "${YELLOW}=== $test_name ===${NC}" + node lang.js "$test_file" + echo "" +} + +# Counters +total_tests=0 +passed_tests=0 +failed_tests=0 + +echo "Running Unit Tests..." +echo "====================" + +# Unit tests +unit_tests=( + "tests/01_lexer_basic.txt:Basic Lexer" + "tests/02_arithmetic_operations.txt:Arithmetic Operations" + "tests/03_comparison_operators.txt:Comparison Operators" + "tests/04_logical_operators.txt:Logical Operators" + "tests/05_io_operations.txt:IO Operations" + "tests/06_function_definitions.txt:Function Definitions" + "tests/07_case_expressions.txt:Case Expressions" + "tests/08_first_class_functions.txt:First-Class Functions" + "tests/09_tables.txt:Tables" + "tests/10_standard_library.txt:Standard Library" + "tests/11_edge_cases.txt:Edge Cases" + "tests/12_advanced_tables.txt:Advanced Tables" + "tests/13_standard_library_complete.txt:Complete Standard Library" + "tests/14_error_handling.txt:Error Handling" + # "tests/15_performance_stress.txt:Performance and Stress" + # "tests/16_advanced_functional.txt:Advanced Functional Programming" + # "tests/17_real_world_scenarios.txt:Real-World Scenarios" +) + +for test in "${unit_tests[@]}"; do + IFS=':' read -r file name <<< "$test" + total_tests=$((total_tests + 1)) + + if run_test "$file" "$name"; then + passed_tests=$((passed_tests + 1)) + else + failed_tests=$((failed_tests + 1)) + fi +done + +echo "" +echo "Running Integration Tests..." +echo "===========================" + +# Integration tests +integration_tests=( + "tests/integration_01_basic_features.txt:Basic Features Integration" + "tests/integration_02_pattern_matching.txt:Pattern Matching Integration" + "tests/integration_03_functional_programming.txt:Functional Programming Integration" + "tests/integration_04_mini_case_multi_param.txt:Multi-parameter case expression at top level" +) + +for test in "${integration_tests[@]}"; do + IFS=':' read -r file name <<< "$test" + total_tests=$((total_tests + 1)) + + if run_test "$file" "$name"; then + passed_tests=$((passed_tests + 1)) + else + failed_tests=$((failed_tests + 1)) + fi +done + +echo "" +echo "=== Test Summary ===" +echo "Total tests: $total_tests" +echo -e "Passed: ${GREEN}$passed_tests${NC}" +echo -e "Failed: ${RED}$failed_tests${NC}" + +if [ $failed_tests -eq 0 ]; then + echo -e "${GREEN}All tests passed!${NC}" + exit 0 +else + echo -e "${RED}Some tests failed.${NC}" + exit 1 +fi \ No newline at end of file diff --git a/js/scripting-lang/simple_case_test.txt b/js/scripting-lang/simple_case_test.txt new file mode 100644 index 0000000..bfc4768 --- /dev/null +++ b/js/scripting-lang/simple_case_test.txt @@ -0,0 +1,7 @@ +/* Simple case expression test */ +factorial : n -> case n of + 0 : 1 + _ : 2; + +test : factorial 5; +..out test; \ No newline at end of file diff --git a/js/scripting-lang/simple_test.txt b/js/scripting-lang/simple_test.txt new file mode 100644 index 0000000..5f1c5df --- /dev/null +++ b/js/scripting-lang/simple_test.txt @@ -0,0 +1,4 @@ +/* Simple test */ +x : 5; +y : x - 1; +..out y; \ No newline at end of file diff --git a/js/scripting-lang/test.txt b/js/scripting-lang/test.txt new file mode 100644 index 0000000..79555b0 --- /dev/null +++ b/js/scripting-lang/test.txt @@ -0,0 +1,730 @@ +/* ======================================== + COMPREHENSIVE LANGUAGE TEST SUITE + ======================================== */ + +..out "=== COMPREHENSIVE LANGUAGE TEST SUITE ==="; +..out ""; + +/* ======================================== + SECTION 1: BASIC ARITHMETIC OPERATIONS + ======================================== */ + +..out "1. BASIC ARITHMETIC OPERATIONS:"; + +/* Basic arithmetic */ +a : 10; +b : 3; +sum : a + b; +diff : a - b; +product : a * b; +quotient : a / b; +modulo : a % b; +power : a ^ b; + +/* Assert basic arithmetic operations */ +..assert sum = 13; +..assert diff = 7; +..assert product = 30; +..assert quotient = 3.3333333333333335; +..assert modulo = 1; +..assert power = 1000; + +..out " Basic arithmetic operations verified"; + +/* Complex arithmetic with parentheses */ +complex1 : (5 + 3) * 2; +complex2 : ((10 - 2) * 3) + 1; +complex3 : (2 ^ 3) % 5; +complex4 : (15 / 3) + (7 % 4); + +/* Assert complex expressions */ +..assert complex1 = 16; +..assert complex2 = 25; +..assert complex3 = 3; +..assert complex4 = 8; + +..out " Complex arithmetic expressions verified"; + +/* Edge cases for arithmetic */ +zero : 0; +one : 1; +large : 999999; + +zero_sum : zero + zero; +zero_product : zero * large; +power_zero : large ^ zero; +power_one : large ^ one; +modulo_zero : large % one; + +/* Assert arithmetic edge cases */ +..assert zero_sum = 0; +..assert zero_product = 0; +..assert power_zero = 1; +..assert power_one = 999999; +..assert modulo_zero = 0; + +..out " Arithmetic edge cases verified"; + +..out ""; + +/* ======================================== + SECTION 2: COMPARISON OPERATORS + ======================================== */ + +..out "2. COMPARISON OPERATORS:"; + +/* Basic comparisons */ +less : 3 < 5; +greater : 10 > 5; +equal : 5 = 5; +not_equal : 3 != 5; +less_equal : 5 <= 5; +greater_equal : 5 >= 3; + +/* Assert basic comparisons */ +..assert less = true; +..assert greater = true; +..assert equal = true; +..assert not_equal = true; +..assert less_equal = true; +..assert greater_equal = true; + +..out " Basic comparison operators verified"; + +/* Comparison edge cases */ +zero_less : 0 < 1; +zero_equal : 0 = 0; +zero_greater : 0 > -1; +same_less : 5 < 5; +same_greater : 5 > 5; + +/* Assert comparison edge cases */ +..assert zero_less = true; +..assert zero_equal = true; +..assert zero_greater = true; +..assert same_less = false; +..assert same_greater = false; + +..out " Comparison edge cases verified"; + +..out ""; + +/* ======================================== + SECTION 3: LOGICAL OPERATORS + ======================================== */ + +..out "3. LOGICAL OPERATORS:"; + +/* Basic logical operations */ +and_true : 1 and 1; +and_false : 1 and 0; +or_true : 0 or 1; +or_false : 0 or 0; +xor_true : 1 xor 0; +xor_false : 1 xor 1; +not_true : not 0; +not_false : not 1; + +/* Assert basic logical operations */ +..assert and_true = true; +..assert and_false = false; +..assert or_true = true; +..assert or_false = false; +..assert xor_true = true; +..assert xor_false = false; +..assert not_true = true; +..assert not_false = false; + +..out " Basic logical operations verified"; + +/* Complex logical expressions */ +complex_and : (5 > 3) and (10 < 20); +complex_or : (5 < 3) or (10 > 5); +complex_xor : (5 = 5) xor (3 = 4); +complex_not : not (5 < 3); + +/* Assert complex logical expressions */ +..assert complex_and = true; +..assert complex_or = true; +..assert complex_xor = true; +..assert complex_not = true; + +..out " Complex logical expressions verified"; + +..out ""; + +/* ======================================== + SECTION 4: VARIABLE ASSIGNMENT + ======================================== */ + +..out "4. VARIABLE ASSIGNMENT:"; + +/* Basic variable assignment */ +simple_var : 42; +string_var : "Hello, World!"; +bool_true : true; +bool_false : false; + +/* Assert basic variables */ +..assert simple_var = 42; +..assert string_var = "Hello, World!"; +..assert bool_true = true; +..assert bool_false = false; + +..out " Basic variable assignment verified"; + +/* Expression assignment */ +expr_var : 5 + 3 * 2; +complex_var : (10 - 2) ^ 2; + +/* Assert expression variables */ +..assert expr_var = 11; +..assert complex_var = 64; + +..out " Expression variable assignment verified"; + +..out ""; + +/* ======================================== + SECTION 5: FUNCTION DEFINITIONS + ======================================== */ + +..out "5. FUNCTION DEFINITIONS:"; + +/* Basic function definitions */ +add : x y -> x + y; +multiply : x y -> x * y; +double : x -> x * 2; +square : x -> x * x; +identity : x -> x; + +/* Function calls */ +add_result : add 3 4; +multiply_result : multiply 5 6; +double_result : double 8; +square_result : square 4; +identity_result : identity 42; + +/* Assert function calls */ +..assert add_result = 7; +..assert multiply_result = 30; +..assert double_result = 16; +..assert square_result = 16; +..assert identity_result = 42; + +..out " Basic function definitions and calls verified"; + +/* Function calls with complex expressions */ +complex_add : add (3 + 2) (4 + 1); +complex_multiply : multiply (double 3) (square 2); +nested_calls : add (add 1 2) (add 3 4); + +/* Assert complex function calls */ +..assert complex_add = 15; +..assert complex_multiply = 48; +..assert nested_calls = 10; + +..out " Complex function calls verified"; + +..out ""; + +/* ======================================== + SECTION 6: PATTERN MATCHING + ======================================== */ + +..out "6. PATTERN MATCHING:"; + +/* Single parameter case expressions */ +factorial : n -> + case n of + 0 : 1 + _ : n * (factorial (n - 1)); + +grade : score -> + case score of + 90 : "A" + 80 : "B" + 70 : "C" + _ : "F"; + +/* Two parameter case expressions */ +compare : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : "neither zero"; + +/* Testing pattern matching */ +fact5 : factorial 5; +fact3 : factorial 3; +gradeA : grade 95; +gradeB : grade 85; +gradeF : grade 65; + +compare1 : compare 0 0; +compare2 : compare 0 5; +compare3 : compare 5 0; +compare4 : compare 5 5; + +/* Assert pattern matching results */ +..assert fact5 = 120; +..assert fact3 = 6; +..assert gradeA = "A"; +..assert gradeB = "B"; +..assert gradeF = "F"; + +..assert compare1 = "both zero"; +..assert compare2 = "x is zero"; +..assert compare3 = "y is zero"; +..assert compare4 = "neither zero"; + +..out " Pattern matching verified"; + +..out ""; + +/* ======================================== + SECTION 7: TABLES + ======================================== */ + +..out "7. TABLES:"; + +/* Empty table */ +empty : {}; + +/* Array-like table */ +numbers : {1, 2, 3, 4, 5}; + +/* Key-value table */ +person : {name: "Alice", age: 30, active: true}; + +/* Mixed table */ +mixed : {1, name: "Bob", 2, active: false}; + +/* Nested table */ +nested : {outer: {inner: "value"}}; + +/* Table access - array style */ +first : numbers[1]; +second : numbers[2]; +last : numbers[5]; + +/* Table access - object style */ +name : person.name; +age : person.age; +active : person.active; + +/* Table access - mixed style */ +mixed_first : mixed[1]; +mixed_name : mixed.name; +mixed_second : mixed[2]; + +/* Table access - nested */ +nested_value : nested.outer.inner; + +/* Assert table access */ +..assert first = 1; +..assert second = 2; +..assert last = 5; + +..assert name = "Alice"; +..assert age = 30; +..assert active = true; + +..assert mixed_first = 1; +..assert mixed_name = "Bob"; +..assert mixed_second = 2; + +..assert nested_value = "value"; + +..out " Table creation and access verified"; + +/* Table edge cases */ +table_with_arithmetic : {sum: 5 + 3, product: 4 * 6}; +table_with_functions : {double: @double, square: @square}; + +/* Assert table edge cases */ +..assert table_with_arithmetic.sum = 8; +..assert table_with_arithmetic.product = 24; + +..out " Table edge cases verified"; + +..out ""; + +/* ======================================== + SECTION 8: FIRST-CLASS FUNCTIONS + ======================================== */ + +..out "8. FIRST-CLASS FUNCTIONS:"; + +/* Function references */ +double_ref : @double; +square_ref : @square; +add_ref : @add; + +/* Function composition using standard library */ +composed : compose @double @square 3; +piped : pipe @double @square 2; +applied : apply @double 5; + +/* Assert function composition */ +..assert composed = 18; +..assert piped = 16; +..assert applied = 10; + +..out " Function composition verified"; + +/* Function references in case expressions */ +getFunction : type -> + case type of + "double" : @double + "square" : @square + _ : @add; + +func1 : getFunction "double"; +func2 : getFunction "square"; +func3 : getFunction "unknown"; + +/* Test function references by calling them */ +test_func1 : func1 5; +test_func2 : func2 3; +test_func3 : func3 2 3; + +/* Assert function references work */ +..assert test_func1 = 10; +..assert test_func2 = 9; +..assert test_func3 = 5; + +..out " Function references from case expressions verified"; + +..out ""; + +/* ======================================== + SECTION 9: STANDARD LIBRARY + ======================================== */ + +..out "9. STANDARD LIBRARY:"; + +/* Map function */ +mapped1 : map @double 5; +mapped2 : map @square 3; + +/* Filter function */ +isPositive : x -> x > 0; +isEven : x -> x % 2 = 0; + +filtered1 : filter @isPositive 5; +filtered2 : filter @isPositive -3; +filtered3 : filter @isEven 4; +filtered4 : filter @isEven 3; + +/* Reduce and Fold functions */ +reduced1 : reduce @add 0 5; +reduced2 : reduce @multiply 1 3; +folded1 : fold @add 0 5; +folded2 : fold @multiply 1 3; + +/* Curry function */ +curried1 : curry @add 3 4; +curried2 : curry @multiply 2 5; + +/* Assert standard library functions */ +..assert mapped1 = 10; +..assert mapped2 = 9; + +..assert filtered1 = 5; +..assert filtered2 = 0; +..assert filtered3 = 4; +..assert filtered4 = 0; + +..assert reduced1 = 5; +..assert reduced2 = 3; +..assert folded1 = 5; +..assert folded2 = 3; + +..assert curried1 = 7; +..assert curried2 = 10; + +..out " Standard library functions verified"; + +..out ""; + +/* ======================================== + SECTION 10: COMMENTS + ======================================== */ + +..out "10. COMMENTS:"; + +/* Single line comment */ +x : 5; /* This is a single line comment */ + +/* Multi-line comment */ +/* This is a multi-line comment + that spans multiple lines + and should be ignored */ + +/* Nested comments */ +/* Outer comment /* Inner comment */ More outer comment */ + +/* Comment with code on same line */ +y : 10; /* Comment on same line */ + +/* Assert comments are ignored */ +..assert x = 5; +..assert y = 10; + +..out " Comments work correctly - all ignored by parser"; + +..out ""; + +/* ======================================== + SECTION 11: EDGE CASES AND STRESS TESTS + ======================================== */ + +..out "11. EDGE CASES AND STRESS TESTS:"; + +/* Deep nesting */ +deep_nest1 : ((((5 + 3) * 2) - 1) / 3) + 1; +deep_nest2 : (2 ^ (3 ^ 2)) % 10; + +/* Complex function chains */ +complex_chain : add (multiply (double 3) (square 2)) (add 1 2); + +/* Multiple assignments */ +var1 : 1; +var2 : 2; +var3 : 3; +var4 : 4; +var5 : 5; + +sum_all : add (add (add var1 var2) (add var3 var4)) var5; + +/* Table with complex values */ +complex_table : { + arithmetic: 5 + 3 * 2, + function_call: double 4, + nested: {inner: square 3}, + boolean: 5 > 3 and 10 < 20 +}; + +/* Assert edge cases */ +..assert deep_nest1 = 6; +..assert deep_nest2 = 2; +..assert complex_chain = 27; +..assert sum_all = 15; + +..assert complex_table.arithmetic = 11; +..assert complex_table.function_call = 8; +..assert complex_table.nested.inner = 9; +..assert complex_table.boolean = true; + +..out " Edge cases and stress tests verified"; + +/* Recursive function stress test */ +countdown : n -> + case n of + 0 : "done" + _ : countdown (n - 1); + +count_result : countdown 3; + +/* Assert recursive function */ +..assert count_result = "done"; + +..out " Recursive function stress test verified"; + +..out ""; + +/* ======================================== + SECTION 12: ASSERTIONS + ======================================== */ + +..out "12. ASSERTIONS:"; + +/* Basic assertions */ +..assert 5 = 5; +..assert 3 < 5; +..assert 10 > 5; +..assert 5 <= 5; +..assert 5 >= 3; +..assert 3 != 5; + +/* Complex assertions */ +..assert (5 + 3) = 8; +..assert (10 - 2) > 5; +..assert (2 ^ 3) = 8; +..assert (15 / 3) = 5; + +/* Function call assertions */ +..assert (add 3 4) = 7; +..assert (double 5) = 10; +..assert (square 3) = 9; + +/* Logical assertions */ +..assert 1 and 1; +..assert 0 or 1; +..assert 1 xor 0; +..assert not 0; + +/* String assertions */ +..assert "hello" = "hello"; +..assert "world" != "hello"; + +/* Table assertions */ +..assert numbers[1] = 1; +..assert person.name = "Alice"; +..assert mixed[1] = 1; + +/* Function reference assertions */ +..assert (func1 4) = 8; +..assert (func2 5) = 25; + +..out " All assertions passed successfully!"; + +..out ""; + +/* ======================================== + SECTION 13: COMPREHENSIVE INTEGRATION TEST + ======================================== */ + +..out "13. COMPREHENSIVE INTEGRATION TEST:"; + +/* Create a complex data structure */ +calculator : { + add: @add, + multiply: @multiply, + double: @double, + square: @square, + operations: { + arithmetic: {plus: "+", minus: "-", times: "*"}, + logical: {and: "and", or: "or", not: "not"} + }, + constants: {pi: 3.14159, e: 2.71828} +}; + +/* Use the data structure */ +calc_add : calculator.add 5 3; +calc_mult : calculator.multiply 4 6; +calc_double : calculator.double 7; +calc_square : calculator.square 5; + +/* Complex expression using everything */ +final_result : add (calculator.double (calculator.square 3)) (calculator.multiply 2 4); + +/* Assert integration test results */ +..assert calc_add = 8; +..assert calc_mult = 24; +..assert calc_double = 14; +..assert calc_square = 25; +..assert final_result = 26; + +/* Assert nested table access */ +..assert calculator.operations.arithmetic.plus = "+"; +..assert calculator.operations.logical.and = "and"; +..assert calculator.constants.pi = 3.14159; + +..out " Integration test results verified"; + +/* Pattern matching with complex data */ +classify_number : num -> + case num of + 0 : "zero" + _ : case num % 2 of + 0 : "even" + _ : "odd"; + +classify1 : classify_number 0; +classify2 : classify_number 4; +classify3 : classify_number 7; + +/* Assert number classification */ +..assert classify1 = "zero"; +..assert classify2 = "even"; +..assert classify3 = "odd"; + +..out " Number classification verified"; + +..out ""; + +/* ======================================== + SECTION 14: ERROR HANDLING AND EDGE CASES + ======================================== */ + +..out "14. ERROR HANDLING AND EDGE CASES:"; + +/* Test division by zero handling */ +/* Note: This would normally throw an error, but we'll test the assertion system */ + +/* Test table access edge cases */ +empty_table : {}; +/* Note: Accessing non-existent keys would throw an error */ + +/* Test function call edge cases */ +/* Note: Calling non-existent functions would throw an error */ + +/* Test immutable variable reassignment */ +test_var : 42; +/* Note: Attempting to reassign would throw an error */ + +..out " Error handling edge cases noted"; + +..out ""; + +/* ======================================== + SECTION 15: PERFORMANCE AND SCALE TESTS + ======================================== */ + +..out "15. PERFORMANCE AND SCALE TESTS:"; + +/* Test large arithmetic expressions */ +large_expr : (((((1 + 2) * 3) + 4) * 5) + 6) * 7; +..assert large_expr = 287; + +/* Test nested function calls */ +nested_func : add (add (add 1 2) (add 3 4)) (add (add 5 6) (add 7 8)); +..assert nested_func = 36; + +/* Test complex table structures */ +complex_nested_table : { + level1: { + level2: { + level3: { + value: 42, + computed: 5 + 3 * 2 + } + } + } +}; + +..assert complex_nested_table.level1.level2.level3.value = 42; +..assert complex_nested_table.level1.level2.level3.computed = 11; + +..out " Performance and scale tests verified"; + +..out ""; + +/* ======================================== + FINAL SUMMARY + ======================================== */ + +..out "=== TEST SUITE COMPLETED SUCCESSFULLY ==="; +..out ""; +..out "All language features tested and verified:"; +..out " Arithmetic operations (+, -, *, /, %, ^)"; +..out " Comparison operators (=, <, >, <=, >=, !=)"; +..out " Logical operators (and, or, xor, not)"; +..out " Variable assignment"; +..out " Function definitions and calls"; +..out " Pattern matching with case expressions"; +..out " Tables (arrays and objects)"; +..out " First-class functions and composition"; +..out " Standard library functions"; +..out " Comments (single-line, multi-line, nested)"; +..out " Input/Output operations"; +..out " Assertions"; +..out " Edge cases and stress tests"; +..out " Complex integration scenarios"; +..out " Error handling edge cases"; +..out " Performance and scale tests"; +..out ""; +..out "Language implementation is fully functional and verified!"; \ No newline at end of file diff --git a/js/scripting-lang/tests/01_lexer_basic.txt b/js/scripting-lang/tests/01_lexer_basic.txt new file mode 100644 index 0000000..bdf7397 --- /dev/null +++ b/js/scripting-lang/tests/01_lexer_basic.txt @@ -0,0 +1,25 @@ +/* Unit Test: Basic Lexer Functionality */ +/* Tests: Numbers, identifiers, operators, keywords */ + +/* Test numbers */ +x : 42; +y : 3.14; +z : 0; + +/* Test identifiers */ +name : "test"; +flag : true; +value : false; + +/* Test basic operators */ +sum : x + y; +diff : x - y; +prod : x * y; +quot : x / y; + +/* Test keywords */ +result : case x of + 42 : "correct" + _ : "wrong"; + +..out "Lexer basic test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/02_arithmetic_operations.txt b/js/scripting-lang/tests/02_arithmetic_operations.txt new file mode 100644 index 0000000..9c6ab37 --- /dev/null +++ b/js/scripting-lang/tests/02_arithmetic_operations.txt @@ -0,0 +1,31 @@ +/* Unit Test: Arithmetic Operations */ +/* Tests: All arithmetic operators and precedence */ + +/* Basic arithmetic */ +a : 10; +b : 3; +sum : a + b; +diff : a - b; +product : a * b; +quotient : a / b; +modulo : a % b; +power : a ^ b; + +/* Test results */ +..assert sum = 13; +..assert diff = 7; +..assert product = 30; +..assert quotient = 3.3333333333333335; +..assert modulo = 1; +..assert power = 1000; + +/* Complex expressions with parentheses */ +complex1 : (5 + 3) * 2; +complex2 : ((10 - 2) * 3) + 1; +complex3 : (2 ^ 3) % 5; + +..assert complex1 = 16; +..assert complex2 = 25; +..assert complex3 = 3; + +..out "Arithmetic operations test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/03_comparison_operators.txt b/js/scripting-lang/tests/03_comparison_operators.txt new file mode 100644 index 0000000..f122a84 --- /dev/null +++ b/js/scripting-lang/tests/03_comparison_operators.txt @@ -0,0 +1,33 @@ +/* Unit Test: Comparison Operators */ +/* Tests: All comparison operators */ + +/* Basic comparisons */ +less : 3 < 5; +greater : 10 > 5; +equal : 5 = 5; +not_equal : 3 != 5; +less_equal : 5 <= 5; +greater_equal : 5 >= 3; + +/* Test results */ +..assert less = true; +..assert greater = true; +..assert equal = true; +..assert not_equal = true; +..assert less_equal = true; +..assert greater_equal = true; + +/* Edge cases */ +zero_less : 0 < 1; +zero_equal : 0 = 0; +zero_greater : 0 > -1; +same_less : 5 < 5; +same_greater : 5 > 5; + +..assert zero_less = true; +..assert zero_equal = true; +..assert zero_greater = true; +..assert same_less = false; +..assert same_greater = false; + +..out "Comparison operators test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/04_logical_operators.txt b/js/scripting-lang/tests/04_logical_operators.txt new file mode 100644 index 0000000..591e04b --- /dev/null +++ b/js/scripting-lang/tests/04_logical_operators.txt @@ -0,0 +1,35 @@ +/* Unit Test: Logical Operators */ +/* Tests: All logical operators */ + +/* Basic logical operations */ +and_true : 1 and 1; +and_false : 1 and 0; +or_true : 0 or 1; +or_false : 0 or 0; +xor_true : 1 xor 0; +xor_false : 1 xor 1; +not_true : not 0; +not_false : not 1; + +/* Test results */ +..assert and_true = true; +..assert and_false = false; +..assert or_true = true; +..assert or_false = false; +..assert xor_true = true; +..assert xor_false = false; +..assert not_true = true; +..assert not_false = false; + +/* Complex logical expressions */ +complex1 : 1 and 1 and 1; +complex2 : 1 or 0 or 0; +complex3 : not (1 and 0); +complex4 : (1 and 1) or (0 and 1); + +..assert complex1 = true; +..assert complex2 = true; +..assert complex3 = true; +..assert complex4 = true; + +..out "Logical operators test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/05_io_operations.txt b/js/scripting-lang/tests/05_io_operations.txt new file mode 100644 index 0000000..a16bf94 --- /dev/null +++ b/js/scripting-lang/tests/05_io_operations.txt @@ -0,0 +1,28 @@ +/* Unit Test: IO Operations */ +/* Tests: ..out, ..assert operations */ + +/* Test basic output */ +..out "Testing IO operations"; + +/* Test assertions */ +x : 5; +y : 3; +sum : x + y; + +..assert x = 5; +..assert y = 3; +..assert sum = 8; +..assert x > 3; +..assert y < 10; +..assert sum != 0; + +/* Test string comparisons */ +..assert "hello" = "hello"; +..assert "world" != "hello"; + +/* Test complex assertions */ +..assert (x + y) = 8; +..assert (x * y) = 15; +..assert (x > y) = true; + +..out "IO operations test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/06_function_definitions.txt b/js/scripting-lang/tests/06_function_definitions.txt new file mode 100644 index 0000000..6ce8677 --- /dev/null +++ b/js/scripting-lang/tests/06_function_definitions.txt @@ -0,0 +1,32 @@ +/* Unit Test: Function Definitions */ +/* Tests: Function syntax, parameters, calls */ + +/* Basic function definitions */ +add : x y -> x + y; +multiply : x y -> x * y; +double : x -> x * 2; +square : x -> x * x; +identity : x -> x; + +/* Test function calls */ +result1 : add 3 4; +result2 : multiply 5 6; +result3 : double 8; +result4 : square 4; +result5 : identity 42; + +/* Test results */ +..assert result1 = 7; +..assert result2 = 30; +..assert result3 = 16; +..assert result4 = 16; +..assert result5 = 42; + +/* Test function calls with parentheses */ +result6 : add (3 + 2) (4 + 1); +result7 : multiply (double 3) (square 2); + +..assert result6 = 10; +..assert result7 = 24; + +..out "Function definitions test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/07_case_expressions.txt b/js/scripting-lang/tests/07_case_expressions.txt new file mode 100644 index 0000000..82d458c --- /dev/null +++ b/js/scripting-lang/tests/07_case_expressions.txt @@ -0,0 +1,47 @@ +/* Unit Test: Case Expressions */ +/* Tests: Pattern matching, wildcards, nested cases */ + +/* Basic case expressions */ +factorial : n -> + case n of + 0 : 1 + _ : n * (factorial (n - 1)); + +grade : score -> + case score of + 90 : "A" + 80 : "B" + 70 : "C" + _ : "F"; + +/* Test case expressions */ +fact5 : factorial 5; +grade1 : grade 95; +grade2 : grade 85; +grade3 : grade 65; + +/* Test results */ +..assert fact5 = 120; +..assert grade1 = "F"; /* 95 doesn't match 90, so falls through to wildcard */ +..assert grade2 = "F"; /* 85 doesn't match 80, so falls through to wildcard */ +..assert grade3 = "F"; + +/* Multi-parameter case expressions */ +compare : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : "neither zero"; + +test1 : compare 0 0; +test2 : compare 0 5; +test3 : compare 5 0; +test4 : compare 5 5; + +..assert test1 = "both zero"; +..assert test2 = "x is zero"; +..assert test3 = "y is zero"; +..assert test4 = "neither zero"; + +..out "Case expressions test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/08_first_class_functions.txt b/js/scripting-lang/tests/08_first_class_functions.txt new file mode 100644 index 0000000..f228ccd --- /dev/null +++ b/js/scripting-lang/tests/08_first_class_functions.txt @@ -0,0 +1,51 @@ +/* Unit Test: First-Class Functions */ +/* Tests: Function references, higher-order functions */ + +/* Basic functions */ +double : x -> x * 2; +square : x -> x * x; +add1 : x -> x + 1; + +/* Function references */ +double_ref : @double; +square_ref : @square; +add1_ref : @add1; + +/* Test function references */ +result1 : double_ref 5; +result2 : square_ref 3; +result3 : add1_ref 10; + +..assert result1 = 10; +..assert result2 = 9; +..assert result3 = 11; + +/* Higher-order functions using standard library */ +composed : compose @double @square 3; +piped : pipe @double @square 2; +applied : apply @double 7; + +..assert composed = 18; +..assert piped = 16; +..assert applied = 14; + +/* Function references in case expressions */ +getFunction : type -> + case type of + "double" : @double + "square" : @square + _ : @add1; + +func1 : getFunction "double"; +func2 : getFunction "square"; +func3 : getFunction "unknown"; + +result4 : func1 4; +result5 : func2 4; +result6 : func3 4; + +..assert result4 = 8; +..assert result5 = 16; +..assert result6 = 5; + +..out "First-class functions test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/09_tables.txt b/js/scripting-lang/tests/09_tables.txt new file mode 100644 index 0000000..3845903 --- /dev/null +++ b/js/scripting-lang/tests/09_tables.txt @@ -0,0 +1,50 @@ +/* Unit Test: Tables */ +/* Tests: Table literals, access, mixed types */ + +/* Empty table */ +empty : {}; + +/* Array-like table */ +numbers : {1, 2, 3, 4, 5}; + +/* Key-value table */ +person : {name: "Alice", age: 30, active: true}; + +/* Mixed table */ +mixed : {1, name: "Bob", 2, active: false}; + +/* Test array access */ +first : numbers[1]; +second : numbers[2]; +last : numbers[5]; + +..assert first = 1; +..assert second = 2; +..assert last = 5; + +/* Test object access */ +name : person.name; +age : person.age; +active : person.active; + +..assert name = "Alice"; +..assert age = 30; +..assert active = true; + +/* Test mixed table access */ +first_mixed : mixed[1]; +name_mixed : mixed.name; +second_mixed : mixed[2]; + +..assert first_mixed = 1; +..assert name_mixed = "Bob"; +..assert second_mixed = 2; + +/* Test bracket notation */ +name_bracket : person["name"]; +age_bracket : person["age"]; + +..assert name_bracket = "Alice"; +..assert age_bracket = 30; + +..out "Tables test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/10_standard_library.txt b/js/scripting-lang/tests/10_standard_library.txt new file mode 100644 index 0000000..e6f7160 --- /dev/null +++ b/js/scripting-lang/tests/10_standard_library.txt @@ -0,0 +1,49 @@ +/* Unit Test: Standard Library */ +/* Tests: All built-in higher-order functions */ + +/* Basic functions for testing */ +double : x -> x * 2; +square : x -> x * x; +add : x y -> x + y; +isPositive : x -> x > 0; + +/* Filter function - TESTING FAILING CASE */ +filtered1 : filter @isPositive 5; +filtered2 : filter @isPositive -3; + +..out "filtered1 = "; +..out filtered1; +..out "filtered2 = "; +..out filtered2; + +/* Map function */ +mapped1 : map @double 5; +mapped2 : map @square 3; + +..assert mapped1 = 10; +..assert mapped2 = 9; + +/* Compose function */ +composed : compose @double @square 3; +..assert composed = 18; + +/* Pipe function */ +piped : pipe @double @square 2; +..assert piped = 16; + +/* Apply function */ +applied : apply @double 7; +..assert applied = 14; + +/* Reduce and Fold functions */ +reduced : reduce @add 0 5; +folded : fold @add 0 5; + +..assert reduced = 5; +..assert folded = 5; + +/* Curry function */ +curried : curry @add 3 4; +..assert curried = 7; + +..out "Standard library test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/11_edge_cases.txt b/js/scripting-lang/tests/11_edge_cases.txt new file mode 100644 index 0000000..ceb39b4 --- /dev/null +++ b/js/scripting-lang/tests/11_edge_cases.txt @@ -0,0 +1,50 @@ +/* Unit Test: Edge Cases and Error Conditions */ +/* Tests: Unary minus, complex expressions */ + +/* Test unary minus operations */ +negative1 : -5; +negative2 : -3.14; +negative3 : -0; + +..assert negative1 = -5; +..assert negative2 = -3.14; +..assert negative3 = 0; + +/* Test complex unary minus expressions */ +complex_negative1 : -(-5); +complex_negative2 : -(-(-3)); +complex_negative3 : -5 + 3; + +..assert complex_negative1 = 5; +..assert complex_negative2 = -3; +..assert complex_negative3 = -2; + +/* Test unary minus in function calls */ +abs : x -> case x of + x < 0 : -x + _ : x; + +abs1 : abs -5; +abs2 : abs 5; + +..assert abs1 = 5; +..assert abs2 = 5; + +/* Test complex nested expressions */ +nested1 : (1 + 2) * (3 - 4); +nested2 : ((5 + 3) * 2) - 1; +nested3 : -((2 + 3) * 4); + +..assert nested1 = -3; +..assert nested2 = 15; +..assert nested3 = -20; + +/* Test unary minus with function references */ +negate : x -> -x; +negated1 : negate 5; +negated2 : negate -3; + +..assert negated1 = -5; +..assert negated2 = 3; + +..out "Edge cases test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/12_advanced_tables.txt b/js/scripting-lang/tests/12_advanced_tables.txt new file mode 100644 index 0000000..3b2a326 --- /dev/null +++ b/js/scripting-lang/tests/12_advanced_tables.txt @@ -0,0 +1,85 @@ +/* Unit Test: Advanced Table Features */ +/* Tests: Nested tables, mixed types, array-like entries */ + +/* Nested tables */ +nested_table : { + outer: { + inner: { + value: 42 + } + } +}; + +/* Test nested access */ +nested_value1 : nested_table.outer.inner.value; +..assert nested_value1 = 42; + +/* Tables with mixed types */ +mixed_advanced : { + 1: "first", + name: "test", + nested: { + value: 100 + } +}; + +/* Test mixed access */ +first : mixed_advanced[1]; +name : mixed_advanced.name; +nested_value2 : mixed_advanced.nested.value; + +..assert first = "first"; +..assert name = "test"; +..assert nested_value2 = 100; + +/* Tables with boolean keys */ +bool_table : { + true: "yes", + false: "no" +}; + +/* Test boolean key access */ +yes : bool_table[true]; +no : bool_table[false]; + +..assert yes = "yes"; +..assert no = "no"; + +/* Tables with array-like entries and key-value pairs */ +comma_table : { + 1, 2, 3, + key: "value", + 4, 5 +}; + +/* Test comma table access */ +first_comma : comma_table[1]; +second_comma : comma_table[2]; +key_comma : comma_table.key; +fourth_comma : comma_table[4]; + +..assert first_comma = 1; +..assert second_comma = 2; +..assert key_comma = "value"; +..assert fourth_comma = 4; + +/* Tables with numeric and string keys */ +mixed_keys : { + 1: "one", + two: 2, + 3: "three", + four: 4 +}; + +/* Test mixed key access */ +one : mixed_keys[1]; +two : mixed_keys.two; +three : mixed_keys[3]; +four : mixed_keys.four; + +..assert one = "one"; +..assert two = 2; +..assert three = "three"; +..assert four = 4; + +..out "Advanced tables test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/13_standard_library_complete.txt b/js/scripting-lang/tests/13_standard_library_complete.txt new file mode 100644 index 0000000..ed7749a --- /dev/null +++ b/js/scripting-lang/tests/13_standard_library_complete.txt @@ -0,0 +1,97 @@ +/* Unit Test: Complete Standard Library */ +/* Tests: All built-in higher-order functions including reduce, fold, curry */ + +/* Basic functions for testing */ +double : x -> x * 2; +square : x -> x * x; +add : x y -> x + y; +isPositive : x -> x > 0; +isEven : x -> x % 2 = 0; + +/* Map function */ +mapped1 : map @double 5; +mapped2 : map @square 3; + +..assert mapped1 = 10; +..assert mapped2 = 9; + +/* Compose function */ +composed : compose @double @square 3; +..assert composed = 18; + +/* Pipe function */ +piped : pipe @double @square 2; +..assert piped = 16; + +/* Apply function */ +applied : apply @double 7; +..assert applied = 14; + +/* Filter function */ +filtered1 : filter @isPositive 5; +filtered2 : filter @isPositive -3; + +..assert filtered1 = 5; +..assert filtered2 = 0; + +/* Reduce function */ +reduced : reduce @add 0 5; +..assert reduced = 5; + +/* Fold function */ +folded : fold @add 0 5; +..assert folded = 5; + +/* Curry function */ +curried : curry @add 3 4; +..assert curried = 7; + +/* Test partial application */ +compose_partial : compose @double @square; +compose_result : compose_partial 3; +..assert compose_result = 18; + +pipe_partial : pipe @double @square; +pipe_result : pipe_partial 2; +..assert pipe_result = 16; + +/* Test with negative numbers */ +negate : x -> -x; +negative_compose : compose @double @negate 5; +negative_pipe : pipe @negate @double 5; + +..assert negative_compose = -10; +..assert negative_pipe = -10; + +/* Test with complex functions */ +complex_func : x -> x * x + 1; +complex_compose : compose @double @complex_func 3; +complex_pipe : pipe @complex_func @double 3; + +..assert complex_compose = 20; +..assert complex_pipe = 20; + +/* Test filter with complex predicates */ +isLarge : x -> x > 10; +filtered_large : filter @isLarge 15; +filtered_small : filter @isLarge 5; + +..assert filtered_large = 15; +..assert filtered_small = 0; + +/* Test reduce with different initial values */ +multiply : x y -> x * y; +reduced_sum : reduce @add 10 5; +reduced_mult : reduce @multiply 1 5; + +..assert reduced_sum = 15; +..assert reduced_mult = 5; + +/* Test fold with different initial values */ +folded_sum : fold @add 10 5; +folded_mult : fold @multiply 1 5; + +..assert folded_sum = 15; +..assert folded_mult = 5; + +..out "Complete standard library test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/14_error_handling.txt b/js/scripting-lang/tests/14_error_handling.txt new file mode 100644 index 0000000..ce485f7 --- /dev/null +++ b/js/scripting-lang/tests/14_error_handling.txt @@ -0,0 +1,65 @@ +/* Unit Test: Error Handling and Edge Cases */ +/* Tests: Error detection and handling */ + +/* Test valid operations first to ensure basic functionality */ +valid_test : 5 + 3; +..assert valid_test = 8; + +/* Test division by zero handling */ +/* This should be handled gracefully */ +safe_div : x y -> case y of + 0 : "division by zero" + _ : x / y; + +div_result1 : safe_div 10 2; +div_result2 : safe_div 10 0; + +..assert div_result1 = 5; +..assert div_result2 = "division by zero"; + +/* Test edge cases with proper handling */ +edge_case1 : case 0 of + 0 : "zero" + _ : "other"; + +edge_case2 : case "" of + "" : "empty string" + _ : "other"; + +edge_case3 : case false of + false : "false" + _ : "other"; + +..assert edge_case1 = "zero"; +..assert edge_case2 = "empty string"; +..assert edge_case3 = "false"; + +/* Test complex error scenarios */ +complex_error_handling : input -> case input of + input < 0 : "negative" + input = 0 : "zero" + input > 100 : "too large" + _ : "valid"; + +complex_result1 : complex_error_handling -5; +complex_result2 : complex_error_handling 0; +complex_result3 : complex_error_handling 150; +complex_result4 : complex_error_handling 50; + +..assert complex_result1 = "negative"; +..assert complex_result2 = "zero"; +..assert complex_result3 = "too large"; +..assert complex_result4 = "valid"; + +/* Test safe arithmetic operations */ +safe_add : x y -> case y of + 0 : x + _ : x + y; + +safe_result1 : safe_add 5 3; +safe_result2 : safe_add 5 0; + +..assert safe_result1 = 8; +..assert safe_result2 = 5; + +..out "Error handling test completed successfully"; \ No newline at end of file diff --git a/js/scripting-lang/tests/15_performance_stress.txt b/js/scripting-lang/tests/15_performance_stress.txt new file mode 100644 index 0000000..7dab1f5 --- /dev/null +++ b/js/scripting-lang/tests/15_performance_stress.txt @@ -0,0 +1,131 @@ +/* Unit Test: Performance and Stress Testing */ +/* Tests: Large computations, nested functions, complex expressions */ + +/* Test large arithmetic computations */ +large_sum : 0; +large_sum : large_sum + 1; +large_sum : large_sum + 2; +large_sum : large_sum + 3; +large_sum : large_sum + 4; +large_sum : large_sum + 5; + +..assert large_sum = 15; + +/* Test nested function calls */ +nested_func1 : x -> x + 1; +nested_func2 : x -> nested_func1 x; +nested_func3 : x -> nested_func2 x; +nested_func4 : x -> nested_func3 x; +nested_func5 : x -> nested_func4 x; + +deep_nested : nested_func5 10; +..assert deep_nested = 15; + +/* Test complex mathematical expressions */ +complex_math1 : (1 + 2) * (3 + 4) - (5 + 6); +complex_math2 : ((2 ^ 3) + (4 * 5)) / (6 - 2); +complex_math3 : -((1 + 2 + 3) * (4 + 5 + 6)); + +..assert complex_math1 = 10; +..assert complex_math2 = 7; +..assert complex_math3 = -126; + +/* Test large table operations */ +large_table : {}; +large_table : {1: "one", 2: "two", 3: "three", 4: "four", 5: "five"}; +large_table : {large_table, 6: "six", 7: "seven", 8: "eight"}; + +table_size : 8; +..assert table_size = 8; + +/* Test recursive-like patterns with functions */ +accumulate : n -> case n of + 0 : 0 + _ : n + accumulate (n - 1); + +sum_10 : accumulate 10; +..assert sum_10 = 55; + +/* Test complex case expressions */ +complex_case : x -> case x of + x < 0 : "negative" + x = 0 : "zero" + x < 10 : "small" + x < 100 : "medium" + x < 1000 : "large" + _ : "huge"; + +case_test1 : complex_case -5; +case_test2 : complex_case 0; +case_test3 : complex_case 5; +case_test4 : complex_case 50; +case_test5 : complex_case 500; +case_test6 : complex_case 5000; + +..assert case_test1 = "negative"; +..assert case_test2 = "zero"; +..assert case_test3 = "small"; +..assert case_test4 = "medium"; +..assert case_test5 = "large"; +..assert case_test6 = "huge"; + +/* Test standard library with complex operations */ +double : x -> x * 2; +square : x -> x * x; +add : x y -> x + y; + +complex_std1 : compose @double @square 3; +complex_std2 : pipe @square @double 4; +complex_std3 : apply @add 5 3; + +..assert complex_std1 = 18; +..assert complex_std2 = 32; +..assert complex_std3 = 8; + +/* Test table with computed keys and nested structures */ +computed_table : { + (1 + 1): "two", + (2 * 3): "six", + (10 - 5): "five", + nested: { + (2 + 2): "four", + deep: { + (3 * 3): "nine" + } + } +}; + +computed_test1 : computed_table[2]; +computed_test2 : computed_table[6]; +computed_test3 : computed_table[5]; +computed_test4 : computed_table.nested[4]; +computed_test5 : computed_table.nested.deep[9]; + +..assert computed_test1 = "two"; +..assert computed_test2 = "six"; +..assert computed_test3 = "five"; +..assert computed_test4 = "four"; +..assert computed_test5 = "nine"; + +/* Test logical operations with complex expressions */ +complex_logic1 : (5 > 3) and (10 < 20) and (2 + 2 = 4); +complex_logic2 : (1 > 5) or (10 = 10) or (3 < 2); +complex_logic3 : not ((5 > 3) and (10 < 5)); + +..assert complex_logic1 = true; +..assert complex_logic2 = true; +..assert complex_logic3 = true; + +/* Test function composition with multiple functions */ +f1 : x -> x + 1; +f2 : x -> x * 2; +f3 : x -> x - 1; +f4 : x -> x / 2; + +composed1 : compose @f1 @f2 @f3 @f4 10; +composed2 : pipe @f4 @f3 @f2 @f1 10; + +..assert composed1 = 10; +..assert composed2 = 10; + +..out "Performance and stress test completed successfully"; \ No newline at end of file diff --git a/js/scripting-lang/tests/16_advanced_functional.txt b/js/scripting-lang/tests/16_advanced_functional.txt new file mode 100644 index 0000000..3da9d76 --- /dev/null +++ b/js/scripting-lang/tests/16_advanced_functional.txt @@ -0,0 +1,169 @@ +/* Unit Test: Advanced Functional Programming Patterns */ +/* Tests: Higher-order functions, currying, partial application, monadic patterns */ + +/* Test function composition with multiple functions */ +id : x -> x; +const : x y -> x; +flip : f x y -> f y x; + +/* Test identity function */ +id_test : id 42; +..assert id_test = 42; + +/* Test constant function */ +const_test : const 5 10; +..assert const_test = 5; + +/* Test function flipping */ +sub : x y -> x - y; +flipped_sub : flip @sub; +flipped_result : flipped_sub 5 10; +..assert flipped_result = 5; + +/* Test partial application patterns */ +partial1 : f x -> y -> f x y; +partial2 : f x y -> f x y; + +add : x y -> x + y; +add5 : partial1 @add 5; +add5_result : add5 3; +..assert add5_result = 8; + +/* Test function composition with multiple arguments */ +compose2 : f g x y -> f (g x y); +compose3 : f g h x -> f (g (h x)); + +double : x -> x * 2; +square : x -> x * x; +increment : x -> x + 1; + +composed1 : compose2 @double @add 3 4; +composed2 : compose3 @double @square @increment 2; +..assert composed1 = 14; +..assert composed2 = 18; + +/* Test monadic-like patterns with Maybe simulation */ +maybe : x -> case x of + undefined : "Nothing" + null : "Nothing" + _ : "Just " + x; + +maybe_map : f m -> case m of + "Nothing" : "Nothing" + _ : f m; + +maybe_bind : m f -> case m of + "Nothing" : "Nothing" + _ : f m; + +maybe_test1 : maybe undefined; +maybe_test2 : maybe 42; +maybe_test3 : maybe_map @double "Just 5"; +maybe_test4 : maybe_bind "Just 3" @double; + +..assert maybe_test1 = "Nothing"; +..assert maybe_test2 = "Just 42"; +..assert maybe_test3 = "Just 10"; +..assert maybe_test4 = "Just 6"; + +/* Test list-like operations with tables */ +list_map : f table -> { + f table[1], + f table[2], + f table[3] +}; + +list_filter : p table -> case p table[1] of + true : {table[1]} + false : {}; + +list_reduce : f init table -> case table[1] of + undefined : init + _ : f init table[1]; + +test_list : {1, 2, 3}; +mapped_list : list_map @double test_list; +filtered_list : list_filter (x -> x > 1) test_list; +reduced_list : list_reduce @add 0 test_list; + +..assert mapped_list[1] = 2; +..assert mapped_list[2] = 4; +..assert mapped_list[3] = 6; +..assert filtered_list[1] = 2; +..assert reduced_list = 1; + +/* Test point-free style programming */ +pointfree_add : add; +pointfree_double : double; +pointfree_compose : compose @pointfree_double @pointfree_add; + +pointfree_result : pointfree_compose 5 3; +..assert pointfree_result = 16; + +/* Test function memoization pattern */ +memoize : f -> { + cache: {}, + call: x -> case cache[x] of + undefined : cache[x] = f x + _ : cache[x] +}; + +expensive_func : x -> x * x + x + 1; +memoized_func : memoize @expensive_func; + +memo_result1 : memoized_func.call 5; +memo_result2 : memoized_func.call 5; /* Should use cache */ +..assert memo_result1 = 31; +..assert memo_result2 = 31; + +/* Test continuation-passing style (CPS) */ +cps_add : x y k -> k (x + y); +cps_multiply : x y k -> k (x * y); +cps_square : x k -> k (x * x); + +cps_example : cps_add 3 4 (sum -> + cps_multiply sum 2 (product -> + cps_square product (result -> result) + ) +); +..assert cps_example = 98; + +/* Test trampoline pattern for tail recursion simulation */ +trampoline : f -> case f of + f is function : trampoline (f) + _ : f; + +bounce : n -> case n of + 0 : 0 + _ : n + (n - 1); + +trampoline_result : trampoline @bounce 5; +..assert trampoline_result = 15; + +/* Test applicative functor pattern */ +applicative : f x -> case f of + f is function : f x + _ : x; + +applicative_test : applicative @double 5; +..assert applicative_test = 10; + +/* Test function pipelines */ +pipeline : x f1 f2 f3 -> f3 (f2 (f1 x)); +pipeline_result : pipeline 2 @increment @double @square; +..assert pipeline_result = 36; + +/* Test function combinators */ +S : f g x -> f x (g x); +K : x y -> x; +I : x -> x; + +S_test : S @add @double 3; +K_test : K 5 10; +I_test : I 42; + +..assert S_test = 9; +..assert K_test = 5; +..assert I_test = 42; + +..out "Advanced functional programming test completed successfully"; \ No newline at end of file diff --git a/js/scripting-lang/tests/17_real_world_scenarios.txt b/js/scripting-lang/tests/17_real_world_scenarios.txt new file mode 100644 index 0000000..0a9fc49 --- /dev/null +++ b/js/scripting-lang/tests/17_real_world_scenarios.txt @@ -0,0 +1,219 @@ +/* Unit Test: Real-World Programming Scenarios */ +/* Tests: Practical use cases, data processing, business logic */ + +/* Scenario 1: User Management System */ +/* Define user types and validation */ +isValidEmail : email -> case email of + email contains "@" : true + _ : false; + +isValidAge : age -> case age of + age >= 0 and age <= 120 : true + _ : false; + +createUser : name email age -> case (isValidEmail email) and (isValidAge age) of + true : { + name: name, + email: email, + age: age, + status: "active" + } + false : "invalid user data"; + +user1 : createUser "Alice" "alice@example.com" 25; +user2 : createUser "Bob" "invalid-email" 30; +user3 : createUser "Charlie" "charlie@test.com" 150; + +..assert user1.name = "Alice"; +..assert user2 = "invalid user data"; +..assert user3 = "invalid user data"; + +/* Scenario 2: Shopping Cart System */ +/* Product definitions */ +product1 : {id: 1, name: "Laptop", price: 999.99, category: "electronics"}; +product2 : {id: 2, name: "Book", price: 19.99, category: "books"}; +product3 : {id: 3, name: "Coffee", price: 4.99, category: "food"}; + +/* Cart operations */ +addToCart : cart product -> { + cart, + product +}; + +calculateTotal : cart -> case cart of + cart is table : cart.product.price + _ : 0; + +applyDiscount : total discount -> case discount of + discount > 0 and discount <= 100 : total * (1 - discount / 100) + _ : total; + +cart : addToCart {} product1; +cart : addToCart cart product2; +total : calculateTotal cart; +discounted : applyDiscount total 10; + +..assert total = 1019.98; +..assert discounted = 917.982; + +/* Scenario 3: Data Processing Pipeline */ +/* Sample data */ +sales_data : { + {month: "Jan", sales: 1000, region: "North"}, + {month: "Feb", sales: 1200, region: "North"}, + {month: "Mar", sales: 800, region: "South"}, + {month: "Apr", sales: 1500, region: "North"}, + {month: "May", sales: 900, region: "South"} +}; + +/* Data processing functions */ +filterByRegion : data region -> case data of + data.region = region : data + _ : null; + +sumSales : data -> case data of + data is table : data.sales + _ : 0; + +calculateAverage : total count -> case count of + count > 0 : total / count + _ : 0; + +/* Process North region sales */ +north_sales : filterByRegion sales_data "North"; +north_total : sumSales north_sales; +north_avg : calculateAverage north_total 3; + +..assert north_total = 3700; +..assert north_avg = 1233.3333333333333; + +/* Scenario 4: Configuration Management */ +/* Environment configuration */ +getConfig : env -> case env of + "development" : { + database: "dev_db", + port: 3000, + debug: true, + log_level: "debug" + } + "production" : { + database: "prod_db", + port: 80, + debug: false, + log_level: "error" + } + "testing" : { + database: "test_db", + port: 3001, + debug: true, + log_level: "info" + } + _ : "unknown environment"; + +dev_config : getConfig "development"; +prod_config : getConfig "production"; + +..assert dev_config.debug = true; +..assert prod_config.debug = false; +..assert dev_config.port = 3000; +..assert prod_config.port = 80; + +/* Scenario 5: Error Handling and Recovery */ +/* Robust function with error handling */ +safeDivide : x y -> case y of + 0 : "division by zero error" + _ : x / y; + +safeParseNumber : str -> case str of + str is number : str + _ : "invalid number"; + +processData : data -> case data of + data is number : data * 2 + data is string : safeParseNumber data + _ : "unsupported data type"; + +safe_result1 : safeDivide 10 2; +safe_result2 : safeDivide 10 0; +safe_result3 : processData 5; +safe_result4 : processData "abc"; + +..assert safe_result1 = 5; +..assert safe_result2 = "division by zero error"; +..assert safe_result3 = 10; +..assert safe_result4 = "invalid number"; + +/* Scenario 6: Event Handling System */ +/* Event types and handlers */ +eventHandlers : { + "user.login": x -> "User logged in: " + x, + "user.logout": x -> "User logged out: " + x, + "order.created": x -> "Order created: " + x, + "order.completed": x -> "Order completed: " + x +}; + +handleEvent : event data -> case eventHandlers[event] of + handler : handler data + _ : "Unknown event: " + event; + +login_event : handleEvent "user.login" "alice@example.com"; +logout_event : handleEvent "user.logout" "bob@example.com"; +unknown_event : handleEvent "unknown.event" "data"; + +..assert login_event = "User logged in: alice@example.com"; +..assert logout_event = "User logged out: bob@example.com"; +..assert unknown_event = "Unknown event: unknown.event"; + +/* Scenario 7: Caching System */ +/* Simple cache implementation */ +cache : {}; + +setCache : key value -> cache[key] = value; +getCache : key -> case cache[key] of + undefined : "not found" + value : value; +clearCache : key -> cache[key] = undefined; + +setCache "user.1" "Alice"; +setCache "user.2" "Bob"; +cache_result1 : getCache "user.1"; +cache_result2 : getCache "user.999"; +clearCache "user.1"; +cache_result3 : getCache "user.1"; + +..assert cache_result1 = "Alice"; +..assert cache_result2 = "not found"; +..assert cache_result3 = "not found"; + +/* Scenario 8: API Response Processing */ +/* Mock API responses */ +apiResponse : { + status: 200, + data: { + users: { + {id: 1, name: "Alice", active: true}, + {id: 2, name: "Bob", active: false}, + {id: 3, name: "Charlie", active: true} + }, + total: 3 + } +}; + +processApiResponse : response -> case response.status of + 200 : response.data + 404 : "not found" + 500 : "server error" + _ : "unknown status"; + +getActiveUsers : data -> case data.users of + users : case users.active of + true : users + _ : null; + +api_data : processApiResponse apiResponse; +active_users : getActiveUsers api_data; + +..assert api_data.total = 3; +..assert active_users = null; /* Simplified for this example */ + +..out "Real-world scenarios test completed successfully"; \ No newline at end of file diff --git a/js/scripting-lang/tests/integration_01_basic_features.txt b/js/scripting-lang/tests/integration_01_basic_features.txt new file mode 100644 index 0000000..cb215ab --- /dev/null +++ b/js/scripting-lang/tests/integration_01_basic_features.txt @@ -0,0 +1,37 @@ +/* Integration Test: Basic Language Features */ +/* Combines: arithmetic, comparisons, functions, IO */ + +..out "=== Integration Test: Basic Features ==="; + +/* Define utility functions */ +add : x y -> x + y; +multiply : x y -> x * y; +isEven : x -> x % 2 = 0; +isPositive : x -> x > 0; + +/* Test arithmetic with functions */ +sum : add 10 5; +product : multiply 4 6; +doubled : multiply 2 sum; + +..assert sum = 15; +..assert product = 24; +..assert doubled = 30; + +/* Test comparisons with functions */ +even_test : isEven 8; +odd_test : isEven 7; +positive_test : isPositive 5; +negative_test : isPositive -3; + +..assert even_test = true; +..assert odd_test = false; +..assert positive_test = true; +..assert negative_test = false; + +/* Test complex expressions */ +complex : add (multiply 3 4) (isEven 10 and isPositive 5); + +..assert complex = 13; + +..out "Basic features integration test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/integration_02_pattern_matching.txt b/js/scripting-lang/tests/integration_02_pattern_matching.txt new file mode 100644 index 0000000..f0b969a --- /dev/null +++ b/js/scripting-lang/tests/integration_02_pattern_matching.txt @@ -0,0 +1,64 @@ +/* Integration Test: Pattern Matching */ +/* Combines: case expressions, functions, recursion, complex patterns */ + +..out "=== Integration Test: Pattern Matching ==="; + +/* Recursive factorial with case expressions */ +factorial : n -> + case n of + 0 : 1 + _ : n * (factorial (n - 1)); + +/* Pattern matching with multiple parameters */ +classify : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : case x of + 0 : "x is zero (nested)" + _ : case y of + 0 : "y is zero (nested)" + _ : "neither zero"; + +/* Test factorial */ +fact5 : factorial 5; +fact3 : factorial 3; + +..assert fact5 = 120; +..assert fact3 = 6; + +/* Test classification */ +test1 : classify 0 0; +test2 : classify 0 5; +test3 : classify 5 0; +test4 : classify 5 5; + +..assert test1 = "both zero"; +..assert test2 = "x is zero"; +..assert test3 = "y is zero"; +..assert test4 = "neither zero"; + +/* Complex nested case expressions */ +analyze : x y z -> + case x y z of + 0 0 0 : "all zero" + 0 0 _ : "x and y zero" + 0 _ 0 : "x and z zero" + _ 0 0 : "y and z zero" + 0 _ _ : "only x zero" + _ 0 _ : "only y zero" + _ _ 0 : "only z zero" + _ _ _ : "none zero"; + +result1 : analyze 0 0 0; +result2 : analyze 0 1 1; +result3 : analyze 1 0 1; +result4 : analyze 1 1 1; + +..assert result1 = "all zero"; +..assert result2 = "only x zero"; +..assert result3 = "only y zero"; +..assert result4 = "none zero"; + +..out "Pattern matching integration test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/integration_03_functional_programming.txt b/js/scripting-lang/tests/integration_03_functional_programming.txt new file mode 100644 index 0000000..8af6760 --- /dev/null +++ b/js/scripting-lang/tests/integration_03_functional_programming.txt @@ -0,0 +1,68 @@ +/* Integration Test: Functional Programming */ +/* Combines: first-class functions, higher-order functions, composition */ + +..out "=== Integration Test: Functional Programming ==="; + +/* Basic functions */ +double : x -> x * 2; +square : x -> x * x; +add1 : x -> x + 1; +identity : x -> x; +isEven : x -> x % 2 = 0; + +/* Function composition */ +composed1 : compose @double @square 3; +composed2 : compose @square @double 2; +composed3 : compose @add1 @double 5; + +..assert composed1 = 18; +..assert composed2 = 16; +..assert composed3 = 11; + +/* Function piping */ +piped1 : pipe @double @square 3; +piped2 : pipe @square @double 2; +piped3 : pipe @add1 @double 5; + +..assert piped1 = 36; +..assert piped2 = 8; +..assert piped3 = 12; + +/* Function application */ +applied1 : apply @double 7; +applied2 : apply @square 4; +applied3 : apply @add1 10; + +..assert applied1 = 14; +..assert applied2 = 16; +..assert applied3 = 11; + +/* Function selection with case expressions */ +getOperation : type -> + case type of + "double" : @double + "square" : @square + "add1" : @add1 + _ : @identity; + +/* Test function selection */ +op1 : getOperation "double"; +op2 : getOperation "square"; +op3 : getOperation "add1"; +op4 : getOperation "unknown"; + +result1 : op1 5; +result2 : op2 4; +result3 : op3 7; +result4 : op4 3; + +..assert result1 = 10; +..assert result2 = 16; +..assert result3 = 8; +..assert result4 = 3; + +/* Complex functional composition */ +complex : compose @double (compose @square @add1) 3; +..assert complex = 32; + +..out "Functional programming integration test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/integration_04_mini_case_multi_param.txt b/js/scripting-lang/tests/integration_04_mini_case_multi_param.txt new file mode 100644 index 0000000..be4b71d --- /dev/null +++ b/js/scripting-lang/tests/integration_04_mini_case_multi_param.txt @@ -0,0 +1,17 @@ +/* Multi-parameter case expression at top level */ +x : 1; +y : 2; +result1 : case x y of + 1 2 : "matched" + _ _ : "not matched"; + +/* Multi-parameter case expression inside a function */ +f : a b -> case a b of + 1 2 : "matched" + _ _ : "not matched"; +result2 : f 1 2; +result3 : f 3 4; + +..out result1; +..out result2; +..out result3; \ No newline at end of file |