diff options
Diffstat (limited to 'js')
38 files changed, 5683 insertions, 2105 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 e370bb1..73ccbf1 100644 --- a/js/scripting-lang/README.md +++ b/js/scripting-lang/README.md @@ -1,319 +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 -- **Basic arithmetic operations**: `+`, `-`, `*`, `/` -- **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` and `..out` +- **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 - -Basic arithmetic operations are supported: - -``` -sum : x + y; -diff : x - y; -product : x * y; -quotient : x / y; -``` - -### Function Definitions - -Functions are defined using arrow syntax (`->`): - -``` -add : x y -> x + y; -double : x -> x * 2; -``` - -### Function Calls - -Functions are called by providing arguments directly after the function name: - +### Basic Operations ``` -result : add 3 4; -doubled : double 5; -``` - -### First-Class Functions - -Functions can be passed as arguments to other functions using the `@` prefix: +/* Arithmetic */ +x : 5 + 3; +y : 10 - 2; +z : 4 * 3; +w : 15 / 3; +neg : -5; /* Unary minus */ -``` -double : x -> x * 2; -square : x -> x * x; -compose : f g x -> f g x; +/* Comparisons */ +result : x > y; +equal : a = b; +not_equal : a != b; -composed : compose @double @square 3; // double(square(3)) = 18 +/* Logical */ +and_result : true and false; +or_result : true or false; ``` -**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: - +### Variables and Functions ``` -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 +/* Immutable variables */ +x : 42; +y : "hello"; -### Input/Output Operations +/* Function definition */ +f : x -> x * 2; -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 +/* Function call */ +result : f 5; ``` -**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 actual equals expected, throws an error if they don't match - -**Note:** The `..` prefix indicates these are impure operations that have side effects. - -## 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 - +### Tables ``` -x : 5; -y : 10; -sum : x + y; -sum; // Returns 15 -``` - -### Function Definition and Call +/* Table literal */ +table : {1, 2, 3, key: "value"}; -``` -add : x y -> x + y; -result : add 3 4; -result; // Returns 7 +/* Table access */ +first : table[1]; +value : table.key; +nested : table.key.subkey; ``` ### Pattern Matching - -``` -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; // Returns "both zero" -test2 : compare 0 5; // Returns "x is zero" -test3 : compare 5 0; // Returns "y is zero" -test4 : compare 5 5; // Returns "neither zero" -``` - -### Function Composition - -``` -double : x -> x * 2; -square : x -> x * x; -compose : f g x -> f g x; -apply : f x -> f x; - -composed : compose @double @square 3; // double(square(3)) = 18 -applied : apply @double 5; // double(5) = 10 ``` - -### Recursion - -The language supports recursive function calls: - +/* Case expression */ +result : case x of + 1 : "one" + 2 : "two" + _ : "other"; ``` -factorial : n -> - case n of - 0 : 1 - 1 : 1 - _ : n * factorial n - 1; -result : factorial 5; // Returns 120 +### IO Operations ``` +/* Output */ +..out "Hello, World!"; -### Input/Output Example +/* Input */ +name : ..in; +/* Assertion */ +..assert x = 5; ``` -name : ..in; // Prompts for input -..out "Hello, "; // Outputs greeting -..out name; // Outputs the input -``` - -### First-Class Functions Example +### Standard Library ``` +/* Map */ double : x -> x * 2; -square : x -> x * x; -compose : f g x -> f g x; +squared : map @double 5; -// Function composition -composed : compose @double @square 3; // 18 +/* Filter */ +isPositive : x -> x > 0; +filtered : filter @isPositive 5; -// Higher-order functions -map : f x -> f x; -mapped : map @double 5; // 10 -``` - -### Assertion Example - -``` -x : 5; -y : 3; -sum : x + y; - -..assert x == 5; // Passes -..assert y == 3; // Passes -..assert sum == 8; // Passes -..assert add 3 4 == 7; // Passes (function call) -..assert x == 0; // Fails with error +/* 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 -**Example REPL session:** +# Run individual tests +node lang.js tests/01_lexer_basic.txt +node lang.js tests/integration_01_basic_features.txt ``` -Scripting Language REPL -Type expressions to evaluate. End with semicolon (;) to execute. -Type "exit" to quit. - -> x : 5; -> y : 10; -> + x y; -15 -> exit -Goodbye! -``` - -## 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**: Full support for recursive function calls -- **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: -- Limited arithmetic expression parsing in complex recursive functions -- Function references (`@`) not supported inside case expressions -- No data structures beyond numbers and strings -- No error handling beyond basic validation -- No modules or imports -- No standard library -- IO operations are synchronous and block execution - -## Future Enhancements - -Planned features for future versions: -- Enhanced arithmetic expression parsing for complex recursive functions -- Function references (`@`) support inside case expressions -- Lists and data structures -- Error handling -- Standard library functions -- Modules and imports -- Type system -- Performance optimizations ## 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 3ff5305..0000000 --- a/js/scripting-lang/comprehensive_test.txt +++ /dev/null @@ -1,128 +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; -..out sum; -..out diff; -..out product; -..out quotient; - -..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; - -compare : x y -> - case x y of - 0 0 : "both zero" - 0 _ : "x is zero" - _ 0 : "y is zero" - _ _ : "neither zero"; - -grade : score -> - case score of - 95 : "A" - 85 : "B" - 75 : "C" - 65 : "D" - _ : "F"; - -..out "Pattern matching:"; -test1 : compare 0 0; -test2 : compare 0 5; -test3 : compare 5 0; -test4 : compare 5 5; -..out test1; -..out test2; -..out test3; -..out test4; - -grade1 : grade 95; -grade2 : grade 85; -grade3 : grade 75; -grade4 : grade 65; -grade5 : grade 55; -..out grade1; -..out grade2; -..out grade3; -..out grade4; -..out grade5; - -countdown : n -> - case n of - 0 : 0 - _ : countdown n - 1; - -factorial : n -> - case n of - 0 : 1 - 1 : 1 - _ : n * factorial n - 1; - -..out "Recursion:"; -count : countdown 3; -fact : factorial 5; -..out count; -..out fact; - -..out "Input/Output demo:"; -..out "Hello, World!"; -..out 42; -..out "The sum of x and y is:"; -..out sum; - -complex : add double 3 square 2; -..out "Complex expression:"; -..out complex; - -..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:"; -compose : f g x -> f g x; -apply : f x -> f x; - -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 higher-order functions:"; -map : f x -> f x; -mapped1 : map @double 3; -mapped2 : map @square 4; - -..assert mapped1 == 6; -..assert mapped2 == 16; - -..out "Map results:"; -..out mapped1; -..out mapped2; - -..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/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/grammar.ebnf b/js/scripting-lang/grammar.ebnf deleted file mode 100644 index b595c87..0000000 --- a/js/scripting-lang/grammar.ebnf +++ /dev/null @@ -1,76 +0,0 @@ -(* Scripting Language Grammar *) - -(* Program Structure *) -program = statement_list -statement_list = statement { statement } -statement = assignment | function_declaration | expression | io_statement | ";" - -(* Variables and Assignment *) -assignment = identifier ":" value -value = expression | function_call - -(* Function Declarations *) -function_declaration = identifier ":" parameter_list "->" function_body -parameter_list = identifier { identifier } -function_body = expression | case_expression - -(* Expressions *) -expression = arithmetic_expression | identifier | number | string - -(* Arithmetic Expressions *) -arithmetic_expression = arithmetic_operator expression expression -arithmetic_operator = "+" | "-" | "*" | "/" - -(* Function Calls *) -function_call = identifier argument_list -argument_list = expression { expression } - -(* Case Expressions *) -case_expression = "case" value_list "of" case_list -value_list = expression { expression } -case_list = case_branch { case_branch } -case_branch = pattern ":" result -pattern = pattern_element { pattern_element } -pattern_element = number | wildcard | identifier -result = result_element { result_element } -result_element = string | number | identifier - -(* IO Statements *) -io_statement = io_operator [ expression ] -io_operator = "..in" | "..out" - -(* Terminals *) -identifier = letter { letter | digit } -number = digit { digit } -string = '"' { character } '"' -wildcard = "_" -letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" -digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -character = letter | digit | " " | "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | "=" | ">" | "?" | "@" | "[" | "\" | "]" | "^" | "`" | "{" | "|" | "}" | "~" - -(* Comments *) -(* This language does not support comments *) - -(* Examples *) - -(* Variable assignment *) -(* x : 5; *) - -(* Function declaration *) -(* add : x y -> x + y; *) - -(* Function call *) -(* result : add 3 4; *) - -(* Case expression *) -(* compare : x y -> - case x y of - 0 0 : "both zero" - 0 _ : "x is zero" - _ 0 : "y is zero" - _ _ : "neither zero"; *) - -(* IO statements *) -(* name : ..in; - ..out "Hello, "; - ..out name; *) \ No newline at end of file diff --git a/js/scripting-lang/grammar_detailed.ebnf b/js/scripting-lang/grammar_detailed.ebnf deleted file mode 100644 index 5274cd8..0000000 --- a/js/scripting-lang/grammar_detailed.ebnf +++ /dev/null @@ -1,97 +0,0 @@ -(* Detailed Scripting Language Grammar *) - -(* Program Structure *) -program = statement_list -statement_list = statement { statement } -statement = assignment | function_declaration | expression_statement | io_statement | ";" - -(* Variables and Assignment *) -assignment = identifier ":" assignment_value -assignment_value = function_call | simple_expression - -(* Function Declarations *) -function_declaration = identifier ":" parameter_list "->" function_body -parameter_list = identifier { identifier } -function_body = arithmetic_expression | case_expression - -(* Function Calls *) -function_call = identifier argument_list -argument_list = expression { expression } - -(* Expressions *) -expression = arithmetic_expression | simple_expression -simple_expression = identifier | number | string -expression_statement = expression - -(* Arithmetic Expressions - Prefix Notation *) -arithmetic_expression = arithmetic_operator expression expression -arithmetic_operator = "+" | "-" | "*" | "/" - -(* Case Expressions *) -case_expression = "case" value_list "of" case_list -value_list = expression { expression } -case_list = case_branch { case_branch } -case_branch = pattern ":" result -pattern = pattern_element { pattern_element } -pattern_element = number | wildcard | identifier -result = result_element { result_element } -result_element = string | number | identifier - -(* IO Statements *) -io_statement = io_operator [ expression ] -io_operator = "..in" | "..out" - -(* Terminals *) -identifier = letter { letter | digit } -number = digit { digit } -string = '"' { character } '"' -wildcard = "_" -letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" -digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -character = letter | digit | " " | "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | "=" | ">" | "?" | "@" | "[" | "\" | "]" | "^" | "`" | "{" | "|" | "}" | "~" - -(* Parsing Precedence and Disambiguation *) - -(* Key Insights from Debug Analysis: *) - -(* 1. Assignment parsing needs to distinguish between function calls and simple values *) -(* assignment_value = function_call | simple_expression *) - -(* 2. Function calls in assignments: "result : add 3 4;" *) -(* - "add" is an identifier *) -(* - "3" and "4" are arguments *) -(* - Should parse as: assignment(identifier("result"), function_call("add", [3, 4])) *) - -(* 3. Case expressions work correctly but function calls within them don't *) -(* - Case patterns: "0 0", "0 _", "_ 0", "_ _" *) -(* - Case results: string literals, numbers, identifiers *) - -(* 4. Arithmetic expressions use prefix notation *) -(* - "+ x y" not "x + y" *) -(* - "* 5 2" not "5 * 2" *) - -(* 5. IO statements are special *) -(* - "..in" reads from stdin *) -(* - "..out expression" writes to stdout *) - -(* Examples with Parse Trees *) - -(* Example 1: Simple assignment *) -(* Input: "x : 5;" *) -(* Parse: assignment(identifier("x"), number(5)) *) - -(* Example 2: Function call assignment *) -(* Input: "result : add 3 4;" *) -(* Parse: assignment(identifier("result"), function_call("add", [number(3), number(4)])) *) - -(* Example 3: Case expression *) -(* Input: "compare : x y -> case x y of 0 0 : \"both zero\";" *) -(* Parse: function_declaration("compare", ["x", "y"], case_expression([identifier("x"), identifier("y")], [case_branch([number(0), number(0)], [string("both zero")])])) *) - -(* Example 4: Arithmetic expression *) -(* Input: "sum : + x y;" *) -(* Parse: assignment(identifier("sum"), arithmetic_expression("+", identifier("x"), identifier("y"))) *) - -(* Example 5: IO statement *) -(* Input: "..out \"Hello\";" *) -(* Parse: io_statement("..out", string("Hello")) *) \ No newline at end of file diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index 5190b2f..1a0d77e 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,6 +1,170 @@ -// 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. +/** + * 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 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) { + if (typeof f === 'function') { + return f(x); + } else { + throw new Error('map: first argument must be a function'); + } + }; + + /** + * 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') { + 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: 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); + } else { + throw new Error('curry: first argument must be a function'); + } + }; + + /** + * 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); + } else { + throw new Error('apply: first argument must be a function'); + } + }; + + /** + * 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') { + 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 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; + } else { + throw new Error('filter: first argument must be a 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); + } else { + throw new Error('reduce: first argument must be a function'); + } + }; + + /** + * 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); + } else { + throw new Error('fold: first argument must be a function'); + } + }; +} -// 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', @@ -18,1246 +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', + COMMA: 'COMMA', + DOT: 'DOT', + STRING: 'STRING', + 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', + MODULO: 'MODULO', + POWER: 'POWER', IO_IN: 'IO_IN', IO_OUT: 'IO_OUT', IO_ASSERT: 'IO_ASSERT', - EQUALS: 'EQUALS', - FUNCTION_REF: 'FUNCTION_REF', - STRING: 'STRING', + 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]; - - 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 - }); + + // Skip whitespace + if (/\s/.test(char)) { current++; 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; + + // Handle nested comments: /* ... */ with support for /* /* ... */ */ + if (char === '/' && input[current + 1] === '*') { + let commentDepth = 1; + current += 2; // Skip /* + + while (current < input.length && commentDepth > 0) { + if (input[current] === '/' && input[current + 1] === '*') { + commentDepth++; + current += 2; + } else if (input[current] === '*' && input[current + 1] === '/') { + commentDepth--; + current += 2; + } else { + current++; + } + } continue; } - - if (/[a-z]/i.test(char)) { + + // Parse numbers (integers and decimals) + if (/[0-9]/.test(char)) { let value = ''; - while (/[a-z0-9]/i.test(char)) { - value += char; - char = input[++current]; + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; + current++; + } + + // 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.NUMBER, + value: parseFloat(value) + }); + } else { + tokens.push({ + type: TokenType.NUMBER, + value: parseInt(value) + }); } - tokens.push({ - type: TokenType.IDENTIFIER, - value - }); - continue; - } - - if (char === ':') { - 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.EQUAL - }); - 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 (input.slice(current, current + 8) === 'function') { - tokens.push({ - type: TokenType.FUNCTION - }); - current += 8; - continue; - } - - // Check for IO tokens - 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; } - - // 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'); } - } - - if (char === ';') { - tokens.push({ type: TokenType.SEMICOLON }); - current++; continue; } - - if (/[a-z]/i.test(char)) { + + // Parse identifiers and keywords + if (/[a-zA-Z_]/.test(char)) { let value = ''; - while (/[a-z0-9]/i.test(char)) { - value += char; - char = input[++current]; + 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 + }); } - tokens.push({ - type: TokenType.IDENTIFIER, - 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; - - function walk() { - if (current >= tokens.length) { - return null; // Return null when there are no more tokens - } - - let token = tokens[current]; - - if (token.type === TokenType.NUMBER) { - current++; - return { - type: 'NumberLiteral', - value: token.value, - }; - } - - if (token.type === TokenType.STRING) { - current++; - return { - type: 'StringLiteral', - value: token.value, - }; + let parsingFunctionArgs = false; // Flag to track when we're parsing function arguments + + // Reset call stack tracker for parser + callStackTracker.reset(); + + // 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 (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(); } - - if (token.type === TokenType.WILDCARD) { - current++; - return { - type: 'WildcardPattern', - }; + } + + 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 { + throw new Error('Expected closing bracket'); + } + } + + // 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(); } - - if (token.type === TokenType.IO_IN) { - current++; - return { - type: 'IOInExpression', - }; + } + + function parseArgument() { + callStackTracker.push('parseArgument', ''); + + try { + const token = tokens[current]; + if (!token) { + throw new Error('Unexpected end of input'); + } + + // 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); + } + + // Check for table access with brackets + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + return parseChainedTableAccess(identifier); + } + + 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 ":" 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 we get here, we have an unexpected token + throw new Error(`Unexpected token in parseArgument: ${token.type}`); + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.IO_OUT) { - current++; - const value = walk(); + } + + 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: 'IOOutExpression', - value, + type: 'FunctionCall', + name: functionName, + args: args }; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.FUNCTION_REF) { - current++; // Skip the @ token - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - return { - type: 'FunctionReference', - name: tokens[current].value, - }; - } else { - throw new Error('Expected function name after @'); + } + + 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.IO_ASSERT) { - current++; + } + + 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(); - // Parse the actual value (stop when we hit ==) - let actual; - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - // This might be a function call or simple identifier - const name = tokens[current].value; + 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(); - // Check if there are arguments (numbers after the identifier) - const args = []; - while (current < tokens.length && tokens[current].type === TokenType.NUMBER) { - args.push({ type: 'NumberLiteral', value: tokens[current].value }); - current++; + 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(); + } + } + + 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)) { - if (args.length > 0) { - actual = { type: 'FunctionCall', name, args }; - } else { - actual = { type: 'Identifier', value: name }; + 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; } - } else if (tokens[current] && tokens[current].type === TokenType.NUMBER) { - actual = { type: 'NumberLiteral', value: tokens[current].value }; + } + + return left; + } finally { + callStackTracker.pop(); + } + } + + 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++; - } else if (tokens[current] && tokens[current].type === TokenType.STRING) { - actual = { type: 'StringLiteral', value: tokens[current].value }; + const right = parsePrimary(); + left = { type: 'PowerExpression', left, right }; + } + + return left; + } finally { + callStackTracker.pop(); + } + } + + 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. + */ + + const token = tokens[current]; + if (!token) { + throw new Error('Unexpected end of input'); + } + + // Parse unary operators + if (token.type === TokenType.NOT) { current++; - } else { - actual = walk(); + const operand = parsePrimary(); + return { type: 'NotExpression', operand }; } - // Expect an equals sign - if (tokens[current] && tokens[current].type === TokenType.EQUALS) { - current++; // Skip the equals sign - } else { - throw new Error('Expected == in assertion'); + if (token.type === TokenType.MINUS) { + current++; + const operand = parsePrimary(); + return { type: 'UnaryMinusExpression', operand }; } - // Parse the expected value - let expected; - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - expected = { type: 'Identifier', value: tokens[current].value }; + // Parse literals + if (token.type === TokenType.NUMBER) { current++; - } else if (tokens[current] && tokens[current].type === TokenType.NUMBER) { - expected = { type: 'NumberLiteral', value: tokens[current].value }; + return { type: 'NumberLiteral', value: token.value }; + } else if (token.type === TokenType.STRING) { current++; - } else if (tokens[current] && tokens[current].type === TokenType.STRING) { - expected = { type: 'StringLiteral', value: tokens[current].value }; + return { type: 'StringLiteral', value: token.value }; + } else if (token.type === TokenType.TRUE) { current++; - } else { - expected = walk(); + 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 }; } - return { - type: 'IOAssertExpression', - actual, - expected, - }; - } - - - - if (token.type === TokenType.PLUS) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'PlusExpression', - left, - right, - }; - } + // 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; + } + + // Check for function calls + if (current < tokens.length && tokens[current].type === TokenType.LEFT_PAREN) { + return parseFunctionCall(identifier.value); + } + + // 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; + + // 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); + } + } + - if (token.type === TokenType.MINUS) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'MinusExpression', - left, - right, - }; - } + + // 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; + } + + // 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 ":" 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'); + } + } + + // Parse arrow expressions (function definitions) + if (token.type === TokenType.ARROW) { + current++; // Skip '->' + + // Parse the function body + const body = parseLogicalExpression(); + + return { type: 'ArrowExpression', body }; + } + - 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 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 { + - if (token.type === TokenType.ASSIGNMENT) { - current++; // Skip the assignment token - return walk(); // Continue parsing - } + - if (token.type === TokenType.IDENTIFIER) { - // 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; - - // 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 + + function parseLogicalExpression() { + callStackTracker.push('parseLogicalExpression', ''); - // Collect parameters until we hit an arrow - while (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.IDENTIFIER) { - params.push(tokens[tempCurrent].value); - tempCurrent++; + 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(); } + } + + function parseExpression() { + callStackTracker.push('parseExpression', ''); - // Check if next token is arrow - if (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.ARROW) { - isFunction = true; + 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(); } + } + + function parseTerm() { + callStackTracker.push('parseTerm', ''); - if (isFunction) { - // This is a function declaration - // Advance current to where tempCurrent left off - current = tempCurrent + 1; // Skip the arrow token + 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(); - // Check if the body is a case expression - if (tokens[current] && tokens[current].type === TokenType.CASE) { - current++; // Skip 'case' + while (current < tokens.length && + (tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO)) { - // 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, - }); - } - current++; + 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', ''); + + 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 (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; + + // 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; - // 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'); + while (paramIndex < lookAheadTokens.length) { + if (lookAheadTokens[paramIndex].type === TokenType.IDENTIFIER) { + parameters.push(lookAheadTokens[paramIndex].value); + paramIndex++; + } else { + // Skip non-identifier tokens (spaces, etc.) + paramIndex++; + } } - const cases = []; + // Skip the parameters and -> + current = lookAheadPos + 1; // Skip the arrow - // Parse cases until we hit a semicolon or end - while (current < tokens.length) { - 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 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' + + // 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'); + // Expect 'of' + if (current >= tokens.length || tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); } + current++; // Skip 'of' - // Parse result using walk() to handle complex expressions - debugLog('Starting case result parsing', { - current: tokens[current], - currentType: tokens[current]?.type - }); - const result = []; + const cases = []; - // Check if we're at the start of the next pattern - if ((tokens[current].type === TokenType.NUMBER || - tokens[current].type === TokenType.WILDCARD) && - tokens[current + 1] && tokens[current + 1].type === TokenType.ASSIGNMENT) { - debugLog('Found start of next pattern, stopping case result parsing'); - } else { - // Parse one expression for the case result - debugLog('Parsing case result node', { - current: tokens[current], - currentType: tokens[current]?.type + // 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] }); - const resultNode = walk(); - if (resultNode) { - result.push(resultNode); - debugLog('Added result node', resultNode); + + // 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 + } + } } } - cases.push({ pattern, result }); + functionBody = { + type: 'CaseExpression', + value: values, + cases, + }; + } else { + functionBody = parseLogicalExpression(); + } + + return { + type: 'Assignment', + identifier, + value: { + type: 'FunctionDefinition', + parameters, + body: functionBody + } + }; + } + + // Check if this is a function definition with 'function' keyword + if (current < tokens.length && tokens[current].type === TokenType.FUNCTION) { + current++; // Skip 'function' + + 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 { + throw new Error('Expected parameter name in function definition'); + } - // Skip the semicolon and break out of the case parsing loop - if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { - debugLog('Found semicolon, ending case parsing'); + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { current++; - break; // Exit the case parsing loop } } - const body = { - type: 'CaseExpression', - value, - cases, - }; + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { + throw new Error('Expected ")" after function parameters'); + } + current++; // Skip ')' - 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(); + if (current >= tokens.length || tokens[current].type !== TokenType.ASSIGNMENT) { + throw new Error('Expected ":" after function parameters'); + } + 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' - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; + // 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 + } + } + } + } - body = { - type: expressionTypes[operator], - left, - right, + functionBody = { + type: 'CaseExpression', + value: values, + cases, }; } else { - // Fallback to walk() for other cases - body = walk(); + 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)) { - // This is a function call as the value - const funcName = tokens[current].value; - current++; // Skip function name - const args = []; - - // Collect arguments until we hit a semicolon or end - while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { - // 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 { + // 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 function name after @'); + throw new Error('Expected ":" after pattern in case expression'); } - } else { - // Use walk() to parse complex arguments (including arithmetic expressions) - const arg = walk(); - if (arg) { - args.push(arg); + + 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 + } + } } } - } - - value = { - type: 'FunctionCall', - name: funcName, - args, - }; - - debugLog('Function call parsed in assignment', { name: funcName, args }); - } else if (tokens[current] && tokens[current].type === TokenType.NUMBER) { - // Simple number value - value = { - type: 'NumberLiteral', - value: tokens[current].value, - }; - current++; - } 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 the start of an infix 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 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, + return { + type: 'Assignment', + identifier, + value: { + type: 'CaseExpression', + value: values, + cases, + } }; } else { - // Simple identifier value - value = { - type: 'Identifier', - value: tokens[current].value, + // Regular assignment + const value = parseLogicalExpression(); + + return { + type: 'Assignment', + identifier, + value }; - current++; } - } 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, - }; - } else { - // Fallback to walk() for other cases - value = walk(); - } - - return { - type: 'AssignmentExpression', - name, - value, - }; - } - } - - // Check if this is a function call (identifier followed by arguments, but not operators) - 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.SEMICOLON && - (tokens[current + 1].type === TokenType.NUMBER || - tokens[current + 1].type === TokenType.IDENTIFIER)) { - // This is a function call - const funcName = token.value; - current++; // Skip function name - const args = []; - - // Collect arguments until we hit a semicolon or end - while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { - // Use walk() to parse complex arguments (including arithmetic expressions) - const arg = walk(); - if (arg) { - args.push(arg); } } - return { - type: 'FunctionCall', - name: funcName, - args, - }; - } - - // 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, - }; - } - - current++; - return { - type: 'Identifier', - value: token.value, - }; - } - - - - // Pattern matching: value : pattern1 : result1 pattern2 : result2 ... - if (token.type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the value being matched - const value = walk(); - - // 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 pattern = walk(); - const result = walk(); - cases.push({ pattern, result }); + // If it's not an assignment, put the identifier back and continue + current--; } - 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; + // 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(); } - - 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 '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]; + 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'); } - // Create a new evalNode function that uses localScope - const localEvalNode = (node) => { - if (!node) { - return undefined; + 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 { + // 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; } - 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 '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); + } + + return table; + case 'TableAccess': + const tableValue = evalNode(node.table); + let keyValue; - // 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]; + // 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}`); + } + 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(); + } + }; + 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'); + } + + 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 actual equals expected - const localActualValue = localEvalNode(node.actual); - const localExpectedValue = localEvalNode(node.expected); - if (localActualValue !== localExpectedValue) { - throw new Error(`Assertion failed: expected ${JSON.stringify(localExpectedValue)}, got ${JSON.stringify(localActualValue)}`); + + 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]; + } + 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 localActualValue; - 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 actual equals expected - const actualValue = evalNode(node.actual); - const expectedValue = evalNode(node.expected); - if (actualValue !== expectedValue) { - throw new Error(`Assertion failed: expected ${JSON.stringify(expectedValue)}, got ${JSON.stringify(actualValue)}`); - } - return actualValue; - 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; @@ -1267,367 +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 input = fs.readFileSync(filePath, 'utf-8'); - debugLog('Input content', input); + const fs = require('fs'); + const input = fs.readFileSync(filePath, 'utf8'); + + 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 = {}; - 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 '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; - 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 '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; - 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/parser_analysis.md b/js/scripting-lang/parser_analysis.md deleted file mode 100644 index 8a25df1..0000000 --- a/js/scripting-lang/parser_analysis.md +++ /dev/null @@ -1,143 +0,0 @@ -# Parser Implementation Analysis - -## Grammar vs Implementation Comparison - -### 1. Assignment Parsing Issue - -**Grammar Rule:** -``` -assignment = identifier ":" assignment_value -assignment_value = function_call | simple_expression -``` - -**Current Implementation Problem:** -- The parser calls `walk()` to get the value in assignments -- `walk()` consumes the function name before assignment parsing can detect it as a function call -- This causes `result : add 3 4;` to be parsed incorrectly - -**Expected Parse Tree:** -``` -assignment( - identifier("result"), - function_call("add", [number(3), number(4)]) -) -``` - -**Actual Parse Tree (from debug output):** -``` -assignment( - identifier("result"), - number(3) -) -// Plus separate nodes: number(4) -``` - -### 2. Function Call Detection - -**Grammar Rule:** -``` -function_call = identifier argument_list -argument_list = expression { expression } -``` - -**Current Implementation Issues:** -- Function call detection is in the wrong place in the parser -- Assignment parsing calls `walk()` which consumes tokens before function call detection -- The condition `tokens[current + 1].type !== TokenType.ASSIGNMENT` prevents function calls in assignments - -**Debug Evidence:** -``` -[DEBUG] Assignment parsing: { - "current": { "type": "NUMBER", "value": "3" }, - "next": { "type": "NUMBER", "value": "4" } -} -``` -The parser is already at the `3` token, meaning `add` has been consumed. - -### 3. Case Expression Parsing - -**Grammar Rule:** -``` -case_expression = "case" value_list "of" case_list -case_branch = pattern ":" result -``` - -**Current Implementation Status:** -✅ **Working correctly** - Case expressions parse and structure correctly -- Patterns are parsed correctly: `0 0`, `0 _`, `_ 0`, `_ _` -- Results are parsed correctly: string literals, numbers, identifiers -- The AST structure matches the grammar - -### 4. Arithmetic Expression Parsing - -**Grammar Rule:** -``` -arithmetic_expression = arithmetic_operator expression expression -``` - -**Current Implementation Status:** -✅ **Working correctly** - Prefix notation is handled correctly -- `+ x y` parses as `PlusExpression(left: x, right: y)` -- Operator precedence is handled correctly - -### 5. IO Statement Parsing - -**Grammar Rule:** -``` -io_statement = io_operator [ expression ] -io_operator = "..in" | "..out" -``` - -**Current Implementation Status:** -✅ **Working correctly** - IO statements parse correctly -- `..in` and `..out` are recognized as special tokens -- Expressions after `..out` are parsed correctly - -## Root Cause Analysis - -The fundamental issue is in the parser architecture: - -1. **Assignment parsing calls `walk()`** to get the value -2. **`walk()` consumes tokens** before assignment parsing can analyze them -3. **Function call detection happens too late** in the parsing process - -## Proposed Solutions - -### Solution 1: Restructure Assignment Parsing -Don't call `walk()` for assignment values. Instead, directly parse the value: - -```javascript -// Instead of: const value = walk(); -// Do: Parse value directly based on next tokens -``` - -### Solution 2: Add Function Call Detection to walk() -Make `walk()` detect function calls when it encounters an identifier followed by arguments: - -```javascript -if (token.type === TokenType.IDENTIFIER) { - // Check for function call first - if (isFunctionCall()) { - return parseFunctionCall(); - } - // Then check for assignment - if (isAssignment()) { - return parseAssignment(); - } -} -``` - -### Solution 3: Use Lookahead -Implement a lookahead mechanism to peek at tokens without consuming them: - -```javascript -function peek(n = 1) { - return tokens[current + n]; -} -``` - -## Recommended Approach - -**Solution 1** is the most straightforward. The assignment parsing should directly parse the value without calling `walk()`, allowing it to distinguish between function calls and simple expressions. - -This would fix the core issue and make the parser match the grammar specification. \ 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 |