diff options
36 files changed, 4354 insertions, 2197 deletions
diff --git a/js/scripting-lang/FIXME.md b/js/scripting-lang/FIXME.md deleted file mode 100644 index e93674a..0000000 --- a/js/scripting-lang/FIXME.md +++ /dev/null @@ -1,100 +0,0 @@ -# FIXME: IO Operation Parsing Bug - -## Problem Summary - -- The parser fails with `Unexpected token in parsePrimary: DOT` when processing IO operations like `..out`, `..assert`, or `..in` at the top level in a script. -- This occurs in the comprehensive test suite and any script that uses IO operations as standalone statements. - -## Observations - -- The **lexer** correctly emits `IO_OUT`, `IO_ASSERT`, and `IO_IN` tokens for `..out`, `..assert`, and `..in`. -- The **parser**'s main loop (`walk()`) now delegates to `parsePrimary()`, which only handles IO tokens if they are the first token in a statement. -- If a statement starts with a DOT (as in `..out`), and the parser is not expecting it, it throws an error. -- The bug is not in the lexer (no stray DOT tokens for IO ops), but in the parser's handling of top-level statements. -- The bug manifests after a block of expressions/statements, when the next statement is an IO operation. - -## What We Have in Place - -- Lexer emits correct IO tokens for `..out`, `..assert`, `..in`. -- `parsePrimary()` handles IO tokens if they are the first token in a statement. -- Table access, dot notation, and function calls with table access are all working. -- The parser's main loop is too generic and does not explicitly handle IO tokens at the top level. - -## Step-by-Step Plan to Fix - -1. **Confirm Lexer Output** ✅ **COMPLETED** - - Run the lexer in debug mode on a failing script to confirm that IO operations are tokenized as `IO_OUT`, `IO_ASSERT`, or `IO_IN` (not as DOT tokens). - - **Result:** Lexer correctly emits IO tokens. - -2. **Fix Decimal Number Lexing** ✅ **COMPLETED** - - **Issue Found:** Lexer was incorrectly tokenizing decimal numbers like `3.3333333333333335` as separate tokens: `NUMBER(3)`, `DOT`, `NUMBER(3333333333333335)`. - - **Fix Applied:** Updated lexer to handle decimal numbers as single `NUMBER` tokens with `parseFloat()`. - - **Result:** Decimal numbers are now correctly tokenized. - -3. **Fix Interpreter Number Evaluation** ✅ **COMPLETED** - - **Issue Found:** Interpreter was using `parseInt()` for `NumberLiteral` evaluation, truncating decimal values. - - **Fix Applied:** Changed all three `NumberLiteral` cases to use `parseFloat()` instead of `parseInt()`. - - **Result:** Decimal numbers are now correctly evaluated. - -4. **Patch Parser IO Handling** ✅ **COMPLETED** - - **Issue Found:** Parser's main loop wasn't properly handling IO tokens at the top level. - - **Fix Applied:** Added explicit IO token handling in the `walk()` function before calling `parsePrimary()`. - - **Result:** IO operations are now correctly parsed as standalone statements. - -5. **Test Comprehensive Suite** ❌ **BLOCKED** - - **Issue:** Stack overflow error when running the full comprehensive test suite. - - **Status:** Need to investigate what's causing the stack overflow in the comprehensive test. - -## Current Status - -**Fixed Issues:** -- ✅ IO operation parsing (lexer, parser, interpreter) -- ✅ Decimal number handling (lexer and interpreter) -- ✅ Basic arithmetic operations with floating point -- ✅ IO operations as standalone statements -- ✅ Case expression parsing (including wildcard patterns) -- ✅ Function definitions and calls -- ✅ Wildcard token recognition in lexer - -**Remaining Issues:** -- ❌ Stack overflow in comprehensive test suite -- ❌ Need to identify what specific pattern in the comprehensive test is causing the stack overflow - -## Investigation Results - -**Successfully Isolated and Fixed:** -1. **Case Expression Parsing** - Fixed wildcard token recognition and case expression parsing -2. **Function Definitions** - Confirmed working correctly -3. **Basic Language Features** - All core features (arithmetic, IO, functions, case expressions) work correctly - -**Stack Overflow Analysis:** -- The stack overflow occurs in the comprehensive test suite but not in isolated tests -- All individual language constructs work correctly when tested in isolation -- The issue is likely caused by a specific combination or pattern of constructs in the comprehensive test - -## Next Steps - -1. **Systematic Comprehensive Test Analysis** - - Test the comprehensive test file section by section to identify the exact location of the stack overflow - - Look for patterns that might cause infinite recursion (e.g., complex nested expressions, specific function call patterns) - - Check for any language constructs that might not be handled correctly in combination - -2. **Parser Debugging** - - Add more detailed debugging to identify the exact parsing step that causes the stack overflow - - Check for any circular dependencies or infinite loops in the parser logic - -3. **Complete Testing** - - Once stack overflow is resolved, run the full comprehensive test suite - - Verify all language features work correctly together - ---- - -**Status:** -- Lexer: ✅ Fixed (decimal numbers, IO tokens, wildcards) -- Parser: ✅ Fixed (IO handling, case expressions, wildcards) but has stack overflow issue -- Interpreter: ✅ Fixed (decimal number evaluation, case expressions) -- Test suite: ❌ Blocked by stack overflow in comprehensive test - ---- - -**Next step:** Systematically analyze the comprehensive test to identify the exact cause of the stack overflow. \ No newline at end of file diff --git a/js/scripting-lang/IDEAS.txt b/js/scripting-lang/IDEAS.txt index 82eed66..96f8b4b 100644 --- a/js/scripting-lang/IDEAS.txt +++ b/js/scripting-lang/IDEAS.txt @@ -6,4 +6,12 @@ add 2 other io functions 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 \ No newline at end of file +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 index 7cb1e75..8061fb7 100644 --- a/js/scripting-lang/NEXT-STEPS.md +++ b/js/scripting-lang/NEXT-STEPS.md @@ -1,229 +1,435 @@ -# Next Steps: Table Features Implementation +# Next Steps: Immutable Real-World Programming Features -## Current State Analysis +## Overview -### What Works ✅ -- Basic table creation: `{name: "Alice", age: 30}` -- Simple table access: `person.name` (single level) -- Basic function definitions: `inc : x -> x + 1` -- Basic function calls: `inc 5` -- Table literals with functions: `{inc: x -> x + 1}` (parsed but not fully functional) +This document outlines the plan for extending the Simple Scripting Language to support real-world programming scenarios while maintaining its immutable, functional design philosophy. -### What's Broken ❌ -1. **Chained table access**: `config.user.name` fails with "Unexpected dot (.) token" -2. **Function calls from tables**: `m.inc 1` doesn't work -3. **Functions in table literals**: May not be properly interpreted +## Core Principles -## Root Cause Analysis +- **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 -### Issue 1: Chained Table Access -**Problem**: Parser encounters standalone DOT tokens when parsing `config.user.name` -**Location**: Assignment parsing logic in `walk()` function -**Debug Evidence**: -- Tokens: `[IDENTIFIER "config", DOT, IDENTIFIER "user", DOT, IDENTIFIER "name"]` -- Parser fails at position 17 (second DOT) because it's not in table access context +## Phase 1: String Operations and Type System -### Issue 2: Function Calls from Tables -**Problem**: Parser doesn't recognize `m.inc 1` as a function call -**Location**: Function call parsing logic -**Expected**: Should parse as `FunctionCall` with `name: TableAccess` and `args: [1]` +### 1.1 String Operations +**Goal**: Add essential string manipulation capabilities -## Implementation Plan +**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" +``` -### Phase 1: Fix Chained Table Access Parser +**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 -#### Step 1.1: Update Assignment Value Parsing -**File**: `lang.js` around line 1300-1400 -**Change**: Modify assignment parsing to handle chained dot notation before falling back to `walk()` +### 1.2 Runtime Type Checking with `is` Keyword +**Goal**: Add explicit, optional type checking mechanism using the `is` keyword -**Current Logic**: -```javascript -// Assignment parsing falls back to walk() for value -const value = walk(); // This fails on DOT tokens -``` +**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 Logic**: +**New Features**: ```javascript -// Check if value is a chained table access -if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER && - tokens[current + 1] && tokens[current + 1].type === TokenType.DOT) { - // Parse chained table access - const tableAccess = parseChainedTableAccess(); - return { type: 'AssignmentExpression', name, value: tableAccess }; -} -// Fall back to walk() for other cases -const value = walk(); +// 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"; ``` -#### Step 1.2: Create parseChainedTableAccess Helper -**File**: `lang.js` in `walk()` function -**Purpose**: Parse arbitrary length dot notation chains +**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 -function parseChainedTableAccess() { - let tableExpr = { - type: 'Identifier', - value: tokens[current].value - }; - current++; // Skip first identifier - - while (tokens[current] && tokens[current].type === TokenType.DOT) { - current++; // Skip dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value - }; - current++; // Skip identifier - - tableExpr = { - type: 'TableAccess', - table: tableExpr, - key - }; - } else { - throw new Error('Expected identifier after dot in table access'); - } - } - - return tableExpr; -} +// This won't work (mutable) +cache[key] = value; ``` -#### Step 1.3: Update Function Call Parsing -**File**: `lang.js` around line 600-700 -**Change**: Allow `TableAccess` nodes as function names - -**Current Logic**: +**Solution - Functional Immutable Transformations**: ```javascript -// Only handles string function names -func = globalScope[node.name]; +// 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; ``` -**New Logic**: +**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 -if (typeof node.name === 'string') { - func = globalScope[node.name]; -} else if (node.name.type === 'TableAccess') { - // Evaluate table access to get function - func = evalNode(node.name); - if (typeof func !== 'function') { - throw new Error('Table access did not resolve to a function'); - } -} +// 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 ``` -### Phase 2: Fix Function Calls from Tables +**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 -#### Step 2.1: Update Function Call Detection -**File**: `lang.js` in `parseFunctionCall()` function -**Change**: Detect when a table access is followed by arguments +### 3.1 Enhanced Standard Library +**Goal**: Add collection processing functions -**Current Logic**: +**New Functions**: ```javascript -// Only checks for identifier followed by arguments -if (tokens[current + 1] && tokens[current + 1].type === TokenType.NUMBER) { - // Function call -} +// 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} ``` -**New Logic**: +**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 -// Check for identifier followed by arguments OR table access followed by arguments -if ((tokens[current + 1] && tokens[current + 1].type === TokenType.NUMBER) || - (tokens[current + 1] && tokens[current + 1].type === TokenType.DOT)) { - // Parse table access first, then check for arguments - const tableAccess = parseChainedTableAccess(); - if (tokens[current] && isArgumentToken(tokens[current])) { - // This is a function call from table - return parseFunctionCallFromTable(tableAccess); - } - return tableAccess; -} +// 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}; ``` -#### Step 2.2: Create parseFunctionCallFromTable Helper -**Purpose**: Parse function calls where the function is a table access +## Phase 4: Error Handling with Error Type + +### 4.1 Error Type +**Goal**: Provide consistent error handling without complex monads **Implementation**: ```javascript -function parseFunctionCallFromTable(tableAccess) { - const args = []; - while (current < tokens.length && isArgumentToken(tokens[current])) { - args.push(walk()); - } - return { - type: 'FunctionCall', - name: tableAccess, - args - }; -} +// 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 3: Test and Validate - -#### Step 3.1: Create Comprehensive Test Suite -**File**: `table_features_test.txt` - -**Test Cases**: -```plaintext -/* Test 1: Basic table access */ -person : {name: "Alice", age: 30}; -name : person.name; -..out name; /* Should output: Alice */ +## Phase 5: Real-World Scenario Support -/* Test 2: Chained table access */ -config : {user: {profile: {name: "Bob"}}}; -deep_name : config.user.profile.name; -..out deep_name; /* Should output: Bob */ +### 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); +``` -/* Test 3: Functions in tables */ -math : { - add : x y -> x + y, - sub : x y -> x - y, - double : x -> x * 2 +### 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) }; -/* Test 4: Function calls from tables */ -result1 : math.add 3 4; /* Should be 7 */ -result2 : math.sub 10 3; /* Should be 7 */ -result3 : math.double 5; /* Should be 10 */ -..out result1; -..out result2; -..out result3; - -/* Test 5: Nested function calls from tables */ -nested : math.double(math.add 2 3); /* Should be 10 */ -..out nested; +// Discount application +applyDiscount : cart discountPercent -> { + items: cart.items, + total: cart.total * (1 - discountPercent / 100) +}; ``` -#### Step 3.2: Debug and Fix Issues -- Run tests and identify any remaining issues -- Add debug logging as needed -- Fix edge cases and error handling - -## Implementation Order - -1. **Phase 1.1**: Update assignment value parsing -2. **Phase 1.2**: Create parseChainedTableAccess helper -3. **Phase 1.3**: Update function call parsing -4. **Phase 2.1**: Update function call detection -5. **Phase 2.2**: Create parseFunctionCallFromTable helper -6. **Phase 3.1**: Create comprehensive test suite -7. **Phase 3.2**: Debug and validate +### 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"} +}; -## Success Criteria +// Filter by region +filterByRegion : data region -> filter (item -> item.region = region) data; -- ✅ `config.user.name` parses and evaluates correctly -- ✅ `m.inc 1` parses and evaluates to 2 -- ✅ `m.inc(m.dec(5))` works with nested calls -- ✅ Functions defined in table literals work correctly -- ✅ No regression in existing functionality +// Calculate totals using sum reduction +sumSales : data -> table.sum (map (item -> item.sales) data); -## Risk Mitigation +// Process pipeline +northData : filterByRegion salesData "North"; +northTotal : sumSales northData; +``` -- **Minimal changes**: Each change targets a specific issue -- **Debug logging**: Keep debug output to trace issues -- **Incremental testing**: Test each phase before proceeding -- **Fallback logic**: Maintain existing `walk()` fallback for non-table cases \ No newline at end of file +## 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 5630e93..73ccbf1 100644 --- a/js/scripting-lang/README.md +++ b/js/scripting-lang/README.md @@ -1,722 +1,220 @@ # Simple Scripting Language -A minimal, interpreted scripting language designed for learning language implementation concepts. This language demonstrates the core components needed for an interpreter: lexer, parser, and interpreter. +A functional programming language with immutable variables, first-class functions, and pattern matching. ## Features -- **Arithmetic operations**: `+`, `-`, `*`, `/`, `%` (modulo), `^` (power) -- **Comparison operators**: `=`, `<`, `>`, `<=`, `>=`, `!=` -- **Logical operators**: `and`, `or`, `xor`, `not` -- **Variable assignment**: Immutable variables with `:` syntax -- **Function definitions**: Arrow function syntax with pattern matching -- **Pattern matching**: Case expressions with wildcard support -- **Function calls**: Direct function application -- **First-class functions**: Functions can be passed as arguments using `@` syntax -- **Function composition**: Higher-order functions for combining functions -- **Lexical scoping**: Functions create their own scope -- **Input/Output**: Built-in IO operations with `..in`, `..out`, and `..assert` -- **Standard Library**: Built-in functional programming utilities -- **Comments**: C-style block comments with `/* ... */` syntax -- **Tables**: Lua-style immutable tables with array and object capabilities +- **Immutable Variables**: Variables cannot be reassigned +- **First-Class Functions**: Functions can be passed as arguments and stored in data structures +- **Lexical Scoping**: Functions create their own scope +- **Pattern Matching**: Case expressions with wildcard support +- **Table Literals**: Lua-style tables with both array-like and key-value entries +- **Standard Library**: Built-in higher-order functions (`map`, `compose`, `pipe`, `apply`, `filter`, `reduce`, `fold`, `curry`) +- **IO Operations**: Built-in input/output operations (`..in`, `..out`, `..assert`) +- **Floating Point Arithmetic**: Full support for decimal numbers +- **Unary Minus**: Support for negative numbers (e.g., `-1`, `-3.14`) ## Syntax -### Variables - -Variables are immutable and assigned using the `:` operator: - -``` -x : 5; -y : 10; +### Basic Operations ``` +/* Arithmetic */ +x : 5 + 3; +y : 10 - 2; +z : 4 * 3; +w : 15 / 3; +neg : -5; /* Unary minus */ -### Arithmetic Operations +/* Comparisons */ +result : x > y; +equal : a = b; +not_equal : a != b; -Arithmetic operations are supported with parentheses grouping: - -``` -sum : x + y; -diff : x - y; -product : x * y; -quotient : x / y; -modulo : 17 % 5; /* Returns 2 */ -power : 2 ^ 3; /* Returns 8 */ -grouped : (5 + 3) * 2; /* Returns 16 */ -nested : ((5 + 3) * 2) + 1; /* Returns 17 */ +/* Logical */ +and_result : true and false; +or_result : true or false; ``` -### Function Definitions - -Functions are defined using arrow syntax (`->`): - +### Variables and Functions ``` -add : x y -> x + y; -double : x -> x * 2; -``` - -### Function Calls - -Functions are called by providing arguments directly after the function name: - -``` -result : add 3 4; -doubled : double 5; -``` - -#### Function Calls with Parentheses - -Function calls support parenthesized arguments for complex expressions: - -``` -/* Simple function call */ -result1 : add 3 4; - -/* Function call with parenthesized arithmetic */ -result2 : add (3 + 2) (4 + 1); +/* Immutable variables */ +x : 42; +y : "hello"; -/* Function call with function calls in parentheses */ -result3 : add (double 3) (square 2); +/* Function definition */ +f : x -> x * 2; -/* Complex nested function calls with parentheses */ -result4 : add (add 1 2) (add 3 4); +/* Function call */ +result : f 5; ``` -**Note:** Parentheses provide explicit grouping and are the recommended way to handle complex nested function calls. - ### Tables - -The language supports Lua-style immutable tables that can serve as both arrays and objects: - -#### Table Creation - -Tables are created using curly braces `{}`: - -``` -/* Empty table */ -empty : {}; - -/* Array-like table (1-indexed) */ -numbers : {1, 2, 3, 4, 5}; - -/* Key-value table */ -person : {name: "Alice", age: 30, active: true}; - -/* Mixed table (array-like and key-value) */ -mixed : {1, name: "Bob", 2, active: false}; -``` - -#### Table Access - -Tables support both bracket notation and dot notation for accessing values: - -``` -/* Array access with bracket notation */ -first : numbers[1]; /* Returns 1 */ -second : numbers[2]; /* Returns 2 */ - -/* Object access with dot notation */ -name : person.name; /* Returns "Alice" */ -age : person.age; /* Returns 30 */ - -/* Object access with bracket notation */ -name_bracket : person["name"]; /* Returns "Alice" */ -age_bracket : person["age"]; /* Returns 30 */ - -/* Mixed table access */ -first_mixed : mixed[1]; /* Returns 1 */ -name_mixed : mixed.name; /* Returns "Bob" */ -second_mixed : mixed[2]; /* Returns 2 */ -``` - -#### Table Features - -- **Immutable**: Tables cannot be modified after creation -- **1-indexed arrays**: Array-like tables start indexing at 1 -- **Mixed types**: Tables can contain numbers, strings, booleans, and functions -- **Nested access**: Tables can contain other tables for complex data structures -- **Unified syntax**: Same syntax for arrays and objects - -#### Tables with Case Expressions - -Tables work well with case expressions for pattern matching: - -``` -/* Case expressions with table values */ -person : {name: "Alice", age: 30, active: true}; -result : case person.age of - 0 : 30 : "Person is 30" - 1 : _ : "Person is different age" -; - -/* Case expressions with boolean values */ -status : case person.active of - 0 : true : "Person is active" - 1 : false : "Person is inactive" -; - -/* Case expressions with array-like access */ -numbers : {1: 10, 2: 20, 3: 30}; -element : case numbers[2] of - 0 : 20 : "Second element is 20" - 1 : _ : "Unexpected second element" -; -``` - -**Current Limitations:** -- Function calls from tables (e.g., `table.func args`) are not supported -- Function definitions inside table literals are not supported -- Tables can store function references, but they cannot be called directly - -### First-Class Functions - -Functions can be passed as arguments to other functions using the `@` prefix: - ``` -double : x -> x * 2; -square : x -> x * x; -compose : f g x -> f g x; +/* Table literal */ +table : {1, 2, 3, key: "value"}; -composed : compose @double @square 3; /* double(square(3)) = 18 */ +/* Table access */ +first : table[1]; +value : table.key; +nested : table.key.subkey; ``` -**Function Reference Syntax:** -- `@functionName` - Creates a function reference (doesn't execute the function) -- `functionName args` - Calls the function with arguments - ### Pattern Matching - -The language supports pattern matching with case expressions in function bodies: - -``` -compare : x y -> - case x y of - 0 0 : "both zero" - 0 _ : "x is zero" - _ 0 : "y is zero" - _ _ : "neither zero"; -``` - -#### Pattern Matching Syntax - -- **Exact matches**: `0 0` matches when both values are 0 -- **Wildcards**: `_` matches any value -- **Mixed patterns**: `0 _` matches when first value is 0 and second can be anything - -### Comparison and Logical Operations - -The language supports comparison and logical operations: - -``` -/* Comparison operators */ -less : 3 < 5; /* true */ -greater : 10 > 5; /* true */ -equal : 5 = 5; /* true */ -not_equal : 3 != 5; /* true */ -less_equal : 5 <= 5; /* true */ -greater_equal : 5 >= 3; /* true */ - -/* Logical operators */ -and_result : 1 and 1; /* true */ -or_result : 0 or 1; /* true */ -xor_result : 1 xor 0; /* true */ -not_result : not 0; /* true */ -``` - -### Comments - -The language supports C-style block comments: - -``` -/* This is a single line comment */ - -/* This is a multi-line comment - that spans multiple lines */ - -x : 5; /* Comment on same line as code */ - -/* Nested comments are supported */ -/* Outer comment /* Inner comment */ More outer comment */ -``` - -**Comment Features:** -- Block comments start with `/*` and end with `*/` -- Comments can span multiple lines -- Comments can appear on the same line as code -- Nested comments are supported -- Comments are completely ignored by the lexer and parser - -### Input/Output Operations - -The language provides built-in IO operations for reading from stdin and writing to stdout: - -``` -name : ..in; /* Read input from stdin */ -..out name; /* Output to stdout */ -..out "Hello"; /* Output string literal */ -..out 42; /* Output number */ -..assert x = 5; /* Assert that x equals 5 */ -..assert y > 3; /* Assert that y is greater than 3 */ -..assert z != 0; /* Assert that z is not equal to 0 */ -``` - -**IO Functions:** -- `..in` - Reads a line from stdin, returns a number if possible, otherwise a string -- `..out` - Outputs a value to stdout and returns the value -- `..assert` - Asserts that a comparison is true, throws an error if it's false. Supports all comparison operators: `=`, `<`, `>`, `<=`, `>=`, `!=` - -**Note:** The `..` prefix indicates these are impure operations that have side effects. - -## Standard Library - -The language includes a built-in standard library with functional programming utilities: - -### Higher-Order Functions - -- **map**: Apply a function to a value (`map f x = f x`) -- **compose**: Compose two functions (`compose f g x = f(g(x))`) -- **curry**: Explicit currying (`curry f x y = f x y`) -- **apply**: Apply a function to an argument (`apply f x = f x`) -- **pipe**: Compose functions left-to-right (`pipe f g x = g(f(x))`) -- **filter**: Filter based on a predicate (`filter p x = p(x) ? x : 0`) -- **reduce**: Reduce to a single value (`reduce f init x = f(init, x)`) -- **fold**: Same as reduce (`fold f init x = f(init, x)`) - -### Usage Examples - -``` -double : x -> x * 2; -square : x -> x * x; - -/* Using map */ -result1 : map @double 5; /* Returns 10 */ -result2 : map @square 3; /* Returns 9 */ - -/* Using compose */ -composed : compose @double @square 3; /* double(square(3)) = 18 */ - -/* Using pipe */ -piped : pipe @double @square 2; /* square(double(2)) = 16 */ - -/* Using filter */ -isPositive : x -> x > 0; -filtered : filter @isPositive 5; /* Returns 5 (since 5 > 0) */ -``` - - -## Language Components - -### Lexer - -The lexer tokenizes input into meaningful units: -- Numbers: `123` -- Identifiers: `variable_name` -- Operators: `+`, `-`, `*`, `/` -- Keywords: `case`, `of`, `function` -- Symbols: `:`, `->`, `_` - -### Parser - -The parser builds an Abstract Syntax Tree (AST) from tokens, supporting: -- Number literals -- Arithmetic expressions -- Variable assignments -- Function declarations -- Function calls -- Case expressions -- Pattern matching - -### Interpreter - -The interpreter executes the AST with: -- Global scope management -- Function evaluation -- Pattern matching execution -- Arithmetic computation -- Immutable variable semantics - -## Example Programs - -### Basic Arithmetic and Variables - ``` -/* Simple arithmetic with variables */ -x : 5; -y : 10; -sum : x + y; -diff : x - y; -product : x * y; -quotient : y / x; -modulo : 17 % 5; /* Returns 2 */ -power : 2 ^ 3; /* Returns 8 */ - -..out sum; /* 15 */ -..out diff; /* -5 */ -..out product; /* 50 */ -..out quotient; /* 2 */ -..out modulo; /* 2 */ -..out power; /* 8 */ - -/* Grouped expressions with parentheses */ -grouped : (5 + 3) * 2; /* 16 */ -nested : ((5 + 3) * 2) + 1; /* 17 */ -complex : (2 ^ 3) % 3 and 5 > 3; /* true */ +/* Case expression */ +result : case x of + 1 : "one" + 2 : "two" + _ : "other"; ``` -### Function Definitions and Calls - +### IO Operations ``` -/* Basic function definition */ -add : x y -> x + y; -multiply : x y -> x * y; -double : x -> x * 2; -square : x -> x * x; - -/* Function calls */ -result1 : add 3 4; /* 7 */ -result2 : multiply 5 6; /* 30 */ -result3 : double 8; /* 16 */ -result4 : square 4; /* 16 */ - -/* Function calls with parenthesized arguments */ -result5 : add (3 + 2) (4 + 1); /* 15 */ -result6 : add (double 3) (square 2); /* 10 */ -result7 : add (add 1 2) (add 3 4); /* 10 */ -``` - -### Tables and Data Structures +/* Output */ +..out "Hello, World!"; -``` -/* Create various table types */ -empty : {}; -numbers : {1, 2, 3, 4, 5}; -person : {name: "Alice", age: 30, active: true}; -mixed : {1, name: "Bob", 2, active: false}; - -/* Access array elements */ -first : numbers[1]; -second : numbers[2]; -last : numbers[5]; - -/* Access object properties */ -name : person.name; -age : person.age; -active : person.active; - -/* Access mixed table */ -first_mixed : mixed[1]; -name_mixed : mixed.name; -second_mixed : mixed[2]; - -/* Output results */ -..out "First number: "; -..out first; -..out "Person name: "; -..out name; -..out "Mixed first: "; -..out first_mixed; -..out "Mixed name: "; -..out name_mixed; -``` - -### Pattern Matching with Case Expressions +/* Input */ +name : ..in; -``` -/* Pattern matching in function bodies */ -compare : x y -> - case x y of - 0 0 : "both zero" - 0 _ : "x is zero" - _ 0 : "y is zero" - _ _ : "neither zero"; - -/* Testing pattern matching */ -test1 : compare 0 0; /* "both zero" */ -test2 : compare 0 5; /* "x is zero" */ -test3 : compare 5 0; /* "y is zero" */ -test4 : compare 5 5; /* "neither zero" */ - -/* Single-parameter case expressions */ -factorial : n -> - case n of - 0 : 1 - _ : n * (factorial (n - 1)); - -/* Single-parameter grade function */ -grade : score -> - case score of - 90 : "A" - 80 : "B" - 70 : "C" - _ : "F"; - -fact5 : factorial 5; /* 120 */ -grade1 : grade 95; /* "A" */ -grade2 : grade 85; /* "B" */ -grade3 : grade 65; /* "F" */ +/* Assertion */ +..assert x = 5; ``` -### First-Class Functions and Composition - +### Standard Library ``` -/* Function references and composition */ +/* Map */ double : x -> x * 2; -square : x -> x * x; -add1 : x -> x + 1; - -/* Using standard library functions */ -composed : compose @double @square 3; /* double(square(3)) = 18 */ -piped : pipe @double @square 2; /* square(double(2)) = 16 */ -applied : apply @double 5; /* double(5) = 10 */ - -/* Higher-order functions */ -mapped1 : map @double 5; /* 10 */ -mapped2 : map @square 3; /* 9 */ - -/* Function references in case expressions */ -getFunction : type -> - case type of - "double" : @double - "square" : @square - _ : @add1; - -func1 : getFunction "double"; /* Function reference */ -func2 : getFunction "square"; /* Function reference */ -``` - -### Recursion and Complex Functions - -``` -/* Recursive factorial function (using single-parameter case) */ -factorial : n -> - case n of - 0 : 1 - _ : n * (factorial (n - 1)); - -/* Recursive countdown function */ -countdown : n -> - case n of - 0 : "done" - _ : countdown (n - 1); - -/* Testing recursive functions */ -fact5 : factorial 5; /* 120 */ -count : countdown 3; /* "done" */ -``` +squared : map @double 5; -### Comparison and Logical Operators - -``` -/* Comparison operators */ -a : 5; -b : 10; - -less : a < b; /* true */ -greater : b > a; /* true */ -equal : a = a; /* true */ -notEqual : a != b; /* true */ -lessEqual : a <= 5; /* true */ -greaterEqual : b >= 10; /* true */ - -/* Logical operators */ -logicalAnd : 1 and 1; /* true */ -logicalOr : 0 or 1; /* true */ -logicalXor : 1 xor 0; /* true */ -logicalNot : not 0; /* true */ - -/* Complex logical expressions */ -complex1 : a < b and b > 5; /* true */ -complex2 : a = 5 or b = 15; /* true */ -complex3 : not (a = b); /* true */ -``` - -### Input/Output and Assertions - -``` -/* Interactive input/output */ -..out "Enter your name: "; -name : ..in; -..out "Hello, "; -..out name; -..out "!"; - -/* Assertions for testing */ -x : 5; -y : 3; -sum : x + y; - -..assert x = 5; /* Passes */ -..assert y = 3; /* Passes */ -..assert sum = 8; /* Passes */ -..assert x > 3; /* Passes */ -..assert y < 10; /* Passes */ -..assert sum != 0; /* Passes */ -..assert (add 3 4) = 7; /* Passes (with parentheses) */ - -/* String comparisons */ -..assert "hello" = "hello"; /* Passes */ -..assert "world" != "hello"; /* Passes */ -``` - -### Standard Library Functions - -``` -/* Using all standard library functions */ -double : x -> x * 2; -square : x -> x * x; -add : x y -> x + y; +/* Filter */ isPositive : x -> x > 0; +filtered : filter @isPositive 5; -/* Map function */ -mapped1 : map @double 5; /* 10 */ -mapped2 : map @square 3; /* 9 */ - -/* Compose function */ -composed : compose @double @square 3; /* 18 */ - -/* Pipe function */ -piped : pipe @double @square 2; /* 16 */ - -/* Apply function */ -applied : apply @double 7; /* 14 */ - -/* Filter function */ -filtered : filter @isPositive 5; /* 5 (since 5 > 0) */ -filtered2 : filter @isPositive -3; /* 0 (since -3 <= 0) */ - -/* Reduce and Fold functions */ -reduced : reduce @add 0 5; /* 5 */ -folded : fold @add 0 5; /* 5 */ - -/* Curry function (explicit currying) */ -curried : curry @add 3 4; /* 7 */ -``` - -### Complete Program Example - -``` -/* A complete program demonstrating multiple features */ -..out "Welcome to the Calculator!"; - -/* Define arithmetic functions */ -add : x y -> x + y; -subtract : x y -> x - y; -multiply : x y -> x * y; -divide : x y -> x / y; - -/* Define utility functions */ -double : x -> x * 2; -square : x -> x * x; -isEven : x -> x % 2 = 0; - -/* Get user input */ -..out "Enter first number: "; -num1 : ..in; -..out "Enter second number: "; -num2 : ..in; - -/* Perform calculations */ -sum : add num1 num2; -diff : subtract num1 num2; -prod : multiply num1 num2; -quot : divide num1 num2; - -/* Display results */ -..out "Sum: "; -..out sum; -..out "Difference: "; -..out diff; -..out "Product: "; -..out prod; -..out "Quotient: "; -..out quot; - -/* Use higher-order functions */ -doubledSum : double sum; -squaredDiff : square diff; - -..out "Doubled sum: "; -..out doubledSum; -..out "Squared difference: "; -..out squaredDiff; - -/* Pattern matching for number classification */ -classify : num -> - case num of - 0 : "zero" - _ : case isEven num of - true : "even" - _ : "odd"; - -classification : classify num1; -..out "First number is: "; -..out classification; - -/* Assertions to verify calculations */ -..assert sum = add num1 num2; -..assert diff = subtract num1 num2; -..assert prod = multiply num1 num2; - -..out "All calculations verified!"; +/* Compose */ +f : x -> x + 1; +g : x -> x * 2; +h : compose @f @g; +result : h 5; /* (5 * 2) + 1 = 11 */ ``` -## Running Programs - -The language supports file execution mode only: - -### File Execution Mode -Run a script file by providing the file path as an argument: +## Usage +### Running Scripts ```bash node lang.js script.txt ``` -**Note:** REPL mode has been removed in the simplified version. The language now only supports file execution. - -## Language Design Principles - -- **Simplicity**: Minimal syntax for maximum clarity -- **Immutability**: Variables cannot be reassigned -- **Functional**: Functions are first-class values with composition support -- **Pattern-oriented**: Pattern matching as a core feature -- **Recursive**: Support for recursive function calls in function bodies -- **Grouped**: Parentheses support for complex arithmetic expressions -- **Expressive**: Rich set of arithmetic, comparison, and logical operators -- **IO-aware**: Built-in input/output operations with clear impurity markers -- **Educational**: Designed to teach language implementation concepts - -## Limitations - -This is a learning language with intentional limitations: -- Complex nested function calls (e.g., `add double 3 square 2`) are ambiguous without explicit grouping -- There's one known issue with function calls inside assertions (e.g., ..assert add 3 4 = 7), which is a parsing edge case. Workaround: use parentheses around function calls in assertions (e.g., ..assert (add 3 4) = 7) -- Limited to immutable data structures (no mutation operations) -- No error handling beyond basic validation -- No modules or imports -- IO operations are synchronous and block execution - -## Future Enhancements +### 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 +# Run all tests +./run_tests.sh -Planned features for future versions: -- Ambiguous function call detection and error reporting -- Lists and data structures -- Error handling -- Modules and imports -- Performance optimizations +# Run individual tests +node lang.js tests/01_lexer_basic.txt +node lang.js tests/integration_01_basic_features.txt +``` ## Implementation Details -The language is implemented in JavaScript with three main components: - -1. **Lexer** (`lexer()`): Converts source code to tokens -2. **Parser** (`parser()`): Builds AST from tokens -3. **Interpreter** (`interpreter()`): Executes AST - -Each component is designed to be modular and extensible for adding new language features. +### 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 +``` -## Files +## Contributing -- `lang.js` - Main language implementation -- `table_basic_test.txt` - Basic table functionality tests -- `table_edge_cases_test.txt` - Advanced table edge cases (some features may not work) -- `README.md` - This documentation -- `NEXT-STEPS.md` - Development roadmap and notes \ 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/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/lang.js b/js/scripting-lang/lang.js index 3de7a0e..1a0d77e 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,10 +1,29 @@ -// The goal here is less to make anything useful...or even something that works, but to learn what parts an interpreted languages needs to have to function. - -// Initialize standard library functions +/** + * Initializes the standard library in the provided scope. + * + * @param {Object} scope - The global scope object to inject functions into + * + * @description Injects higher-order functions into the interpreter's global scope. + * These functions provide functional programming utilities like map, compose, pipe, etc. + * + * @why Injecting the standard library directly into the scope ensures that user code + * can access these functions as if they were built-in, without special syntax or + * reserved keywords. This approach also allows for easy extension and testing, as + * the library is just a set of regular functions in the scope chain. + * + * @how Each function is added as a property of the scope object. Functions are written + * to check argument types at runtime, since the language is dynamically typed and + * does not enforce arity or types at parse time. + */ function initializeStandardLibrary(scope) { - // Map: Apply a function to each element + /** + * Map: Apply a function to a value + * @param {Function} f - Function to apply + * @param {*} x - Value to apply function to + * @returns {*} Result of applying f to x + * @throws {Error} When first argument is not a function + */ scope.map = function(f, x) { - // Handle function references by calling them if they're functions if (typeof f === 'function') { return f(x); } else { @@ -12,17 +31,36 @@ function initializeStandardLibrary(scope) { } }; - // Compose: Compose two functions (f ∘ g)(x) = f(g(x)) + /** + * Compose: Compose two functions (f ∘ g)(x) = f(g(x)) + * @param {Function} f - Outer function + * @param {Function} g - Inner function + * @param {*} [x] - Optional argument to apply composed function to + * @returns {Function|*} Either a composed function or the result of applying it + * @throws {Error} When first two arguments are not functions + */ scope.compose = function(f, g, x) { if (typeof f === 'function' && typeof g === 'function') { - return f(g(x)); + if (arguments.length === 3) { + return f(g(x)); + } else { + return function(x) { + return f(g(x)); + }; + } } else { throw new Error('compose: first two arguments must be functions'); } }; - // Curry: Convert a function that takes multiple arguments into a series of functions - // Since our language already uses curried functions by default, this is mostly for explicit currying + /** + * Curry: Apply a function to arguments (simplified currying) + * @param {Function} f - Function to curry + * @param {*} x - First argument + * @param {*} y - Second argument + * @returns {*} Result of applying f to x and y + * @throws {Error} When first argument is not a function + */ scope.curry = function(f, x, y) { if (typeof f === 'function') { return f(x, y); @@ -31,7 +69,13 @@ function initializeStandardLibrary(scope) { } }; - // Apply: Apply a function to an argument (same as function call, but more explicit) + /** + * Apply: Apply a function to an argument (explicit function application) + * @param {Function} f - Function to apply + * @param {*} x - Argument to apply function to + * @returns {*} Result of applying f to x + * @throws {Error} When first argument is not a function + */ scope.apply = function(f, x) { if (typeof f === 'function') { return f(x); @@ -40,18 +84,35 @@ function initializeStandardLibrary(scope) { } }; - // Pipe: Compose functions in left-to-right order (opposite of compose) - // pipe f g x = g f x + /** + * Pipe: Compose functions in left-to-right order (opposite of compose) + * @param {Function} f - First function + * @param {Function} g - Second function + * @param {*} [x] - Optional argument to apply piped function to + * @returns {Function|*} Either a piped function or the result of applying it + * @throws {Error} When first two arguments are not functions + */ scope.pipe = function(f, g, x) { if (typeof f === 'function' && typeof g === 'function') { - return g(f(x)); + if (arguments.length === 3) { + return g(f(x)); + } else { + return function(x) { + return g(f(x)); + }; + } } else { throw new Error('pipe: first two arguments must be functions'); } }; - // Filter: Filter based on a predicate - // For now, we'll implement it as a higher-order function + /** + * Filter: Filter a value based on a predicate + * @param {Function} p - Predicate function + * @param {*} x - Value to test + * @returns {*|0} The value if predicate is true, 0 otherwise + * @throws {Error} When first argument is not a function + */ scope.filter = function(p, x) { if (typeof p === 'function') { return p(x) ? x : 0; @@ -60,8 +121,14 @@ function initializeStandardLibrary(scope) { } }; - // Reduce: Reduce to a single value using a binary function - // For now, we'll implement it as a higher-order function + /** + * Reduce: Reduce two values using a binary function + * @param {Function} f - Binary function + * @param {*} init - Initial value + * @param {*} x - Second value + * @returns {*} Result of applying f to init and x + * @throws {Error} When first argument is not a function + */ scope.reduce = function(f, init, x) { if (typeof f === 'function') { return f(init, x); @@ -70,7 +137,14 @@ function initializeStandardLibrary(scope) { } }; - // Fold: Same as reduce, but more explicit about the folding direction + /** + * Fold: Same as reduce, but more explicit about the folding direction + * @param {Function} f - Binary function + * @param {*} init - Initial value + * @param {*} x - Second value + * @returns {*} Result of applying f to init and x + * @throws {Error} When first argument is not a function + */ scope.fold = function(f, init, x) { if (typeof f === 'function') { return f(init, x); @@ -80,7 +154,17 @@ function initializeStandardLibrary(scope) { }; } -// Define the types of tokens +/** + * TokenType enumeration for all supported token types. + * + * @type {Object.<string, string>} + * + * @description A flat object mapping token names to their string representations. + * This approach allows for fast string comparisons and easy extensibility. + * + * @why Using a flat object avoids the need for import/export or enum boilerplate, + * and makes it easy to add new token types as the language evolves. + */ const TokenType = { NUMBER: 'NUMBER', PLUS: 'PLUS', @@ -124,7 +208,29 @@ const TokenType = { FUNCTION_REF: 'FUNCTION_REF' }; -// Lexer - converts source code to tokens +/** + * 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) { let current = 0; const tokens = []; @@ -138,7 +244,7 @@ function lexer(input) { continue; } - // Skip comments + // Handle nested comments: /* ... */ with support for /* /* ... */ */ if (char === '/' && input[current + 1] === '*') { let commentDepth = 1; current += 2; // Skip /* @@ -157,7 +263,7 @@ function lexer(input) { continue; } - // Numbers + // Parse numbers (integers and decimals) if (/[0-9]/.test(char)) { let value = ''; while (current < input.length && /[0-9]/.test(input[current])) { @@ -189,7 +295,7 @@ function lexer(input) { continue; } - // Strings + // Parse string literals if (char === '"') { let value = ''; current++; // Skip opening quote @@ -211,7 +317,7 @@ function lexer(input) { continue; } - // Identifiers and keywords + // Parse identifiers and keywords if (/[a-zA-Z_]/.test(char)) { let value = ''; while (current < input.length && /[a-zA-Z0-9_]/.test(input[current])) { @@ -260,7 +366,7 @@ function lexer(input) { continue; } - // Two-character operators + // Parse two-character operators if (current + 1 < input.length) { const twoChar = char + input[current + 1]; switch (twoChar) { @@ -285,7 +391,7 @@ function lexer(input) { current += 2; continue; case '..': - // Check for IO operations + // Parse IO operations: ..in, ..out, ..assert if (current + 2 < input.length) { const ioChar = input[current + 2]; switch (ioChar) { @@ -328,7 +434,7 @@ function lexer(input) { } } - // Single character operators + // Parse single character operators switch (char) { case '+': tokens.push({ type: TokenType.PLUS }); @@ -400,15 +506,65 @@ function lexer(input) { current++; } + + return tokens; } -// Parser - converts tokens to AST +/** + * 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) { let current = 0; + let parsingFunctionArgs = false; // Flag to track when we're parsing function arguments - function walk() { - function parseChainedDotAccess(tableExpr) { + // 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) { @@ -432,9 +588,29 @@ function parser(tokens) { } return result; + } finally { + callStackTracker.pop(); } + } + + function parseChainedTableAccess(tableExpr) { + callStackTracker.push('parseChainedTableAccess', ''); - function parseChainedTableAccess(tableExpr) { + 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(); @@ -485,14 +661,167 @@ function parser(tokens) { } return tableExpr; + } finally { + callStackTracker.pop(); } + } + + function parseArgument() { + callStackTracker.push('parseArgument', ''); - function detectAmbiguousFunctionCalls() { - // This is a placeholder for future ambiguous function call detection - // For now, we'll assume the parser handles function calls correctly + 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(); } + } + + function parseFunctionCall(functionName) { + callStackTracker.push('parseFunctionCall', ''); - function parseFunctionCall(functionName) { + 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 @@ -500,18 +829,117 @@ function parser(tokens) { tokens[current].type !== TokenType.SEMICOLON && tokens[current].type !== TokenType.RIGHT_PAREN && tokens[current].type !== TokenType.RIGHT_BRACE && - tokens[current].type !== TokenType.COMMA) { - args.push(parseExpression()); + 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: 'FunctionCall', name: functionName, args: args }; + } finally { + callStackTracker.pop(); + } + } + + 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(); } + } + + function parseExpression() { + callStackTracker.push('parseExpression', ''); - function parseExpression() { + try { + /** + * Parses expressions with left-associative binary operators. + * + * @returns {Object} AST node representing the expression + * + * @description Parses addition, subtraction, and comparison operators + * with proper precedence and associativity. + * + * @why Operator precedence is handled by splitting parsing into multiple + * functions (expression, term, factor, primary). This structure avoids + * ambiguity and ensures correct grouping of operations. + * + * @note Special case handling for unary minus after function references + * to distinguish from binary minus operations. + */ let left = parseTerm(); while (current < tokens.length && @@ -522,12 +950,18 @@ function parser(tokens) { tokens[current].type === TokenType.LESS_THAN || tokens[current].type === TokenType.GREATER_THAN || tokens[current].type === TokenType.LESS_EQUAL || - tokens[current].type === TokenType.GREATER_EQUAL || - tokens[current].type === TokenType.AND || - tokens[current].type === TokenType.OR || - tokens[current].type === TokenType.XOR)) { + 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(); @@ -556,22 +990,31 @@ function parser(tokens) { case TokenType.GREATER_EQUAL: left = { type: 'GreaterEqualExpression', left, right }; break; - 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 parseTerm() { + callStackTracker.push('parseTerm', ''); - function 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 && @@ -597,9 +1040,27 @@ function parser(tokens) { } return left; + } finally { + callStackTracker.pop(); } + } + + function parseFactor() { + callStackTracker.push('parseFactor', ''); - function 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) { @@ -609,52 +1070,186 @@ function parser(tokens) { } return left; + } finally { + callStackTracker.pop(); } + } + + function parsePrimary() { + callStackTracker.push('parsePrimary', ''); - function 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++; const operand = parsePrimary(); return { type: 'NotExpression', operand }; } - if (token.type === TokenType.NUMBER) { + if (token.type === TokenType.MINUS) { current++; - return { - type: 'NumberLiteral', - value: token.value - }; + const operand = parsePrimary(); + return { type: 'UnaryMinusExpression', operand }; } - if (token.type === TokenType.STRING) { + // Parse literals + if (token.type === TokenType.NUMBER) { current++; - return { - type: 'StringLiteral', - value: token.value - }; - } - - if (token.type === TokenType.TRUE) { + return { type: 'NumberLiteral', value: token.value }; + } else if (token.type === TokenType.STRING) { current++; - return { - type: 'BooleanLiteral', - value: true - }; + 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 }; } - if (token.type === TokenType.FALSE) { + // Parse identifiers + if (token.type === TokenType.IDENTIFIER) { current++; - return { - type: 'BooleanLiteral', - value: false - }; + 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); + } + } + + + + // 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 = parseExpression(); + const parenthesizedExpr = parseLogicalExpression(); if (current < tokens.length && tokens[current].type === TokenType.RIGHT_PAREN) { current++; // Skip ')' @@ -664,272 +1259,636 @@ function parser(tokens) { } } - if (token.type === TokenType.IDENTIFIER) { - const identifier = { - type: 'Identifier', - value: token.value - }; - current++; + // Parse table literals + if (token.type === TokenType.LEFT_BRACE) { + current++; // Skip '{' + const properties = []; - // Check if this is an assignment - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - - // Check if this is a function definition - let isFunction = false; - let params = []; - - // Look ahead to see if this is a function definition - let lookAhead = current; - while (lookAhead < tokens.length && - tokens[lookAhead].type !== TokenType.ARROW && - tokens[lookAhead].type !== TokenType.SEMICOLON) { - if (tokens[lookAhead].type === TokenType.IDENTIFIER) { - params.push(tokens[lookAhead].value); + 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'); } - lookAhead++; - } - - if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.ARROW) { - isFunction = true; + } else { + throw new Error('Expected property name in table literal'); } - if (isFunction) { - // Clear params array and parse function parameters - params = []; - while (current < tokens.length && tokens[current].type !== TokenType.ARROW) { - if (tokens[current].type === TokenType.IDENTIFIER) { - params.push(tokens[current].value); - } - current++; - } - - current++; // Skip '->' - - // Parse the function body (which could be a case expression or other expression) - const functionBody = parseExpression(); - - return { - type: 'AssignmentExpression', - name: identifier.value, - value: { - type: 'FunctionDeclaration', - name: null, // Anonymous function - params, - body: functionBody, - } - }; - } else { - // Regular assignment - const value = parseExpression(); - return { - type: 'AssignmentExpression', - name: identifier.value, - value: value - }; + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; } } - // Check if this is table access - if (current < tokens.length && - (tokens[current].type === TokenType.LEFT_BRACKET || - tokens[current].type === TokenType.DOT)) { - return parseChainedTableAccess(identifier); + 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 '->' - // 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(identifier); - } + // Parse the function body + const body = parseLogicalExpression(); - return identifier; + return { type: 'ArrowExpression', body }; } - if (token.type === TokenType.FUNCTION_REF) { - current++; // Skip '@' - if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { - const funcName = tokens[current].value; - current++; - return { - type: 'FunctionReference', - name: funcName - }; - } else { - throw new Error('Expected function name after @'); - } - } - if (token.type === TokenType.WILDCARD) { - current++; // Skip '_' - return { type: 'WildcardPattern' }; - } + - if (token.type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the value being matched - const value = parseExpression(); + + // 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 { + + + + + + function parseLogicalExpression() { + callStackTracker.push('parseLogicalExpression', ''); - // Expect 'of' - if (tokens[current].type !== TokenType.OF) { - throw new Error('Expected "of" after "case"'); + 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(); } - current++; // Skip 'of' + } + + function parseExpression() { + callStackTracker.push('parseExpression', ''); - const cases = []; + 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', ''); - // Parse cases until we hit a semicolon or end - while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { - const pattern = parseExpression(); + 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(); - // Expect ':' after pattern - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - } else { - throw new Error('Expected ":" after pattern in case expression'); + while (current < tokens.length && + (tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO)) { + + const operator = tokens[current].type; + current++; + const right = parseFactor(); + + switch (operator) { + case TokenType.MULTIPLY: + left = { type: 'MultiplyExpression', left, right }; + break; + case TokenType.DIVIDE: + left = { type: 'DivideExpression', left, right }; + break; + case TokenType.MODULO: + left = { type: 'ModuloExpression', left, right }; + break; + } } - const result = parseExpression(); - cases.push({ - pattern: [pattern], - result: [result] - }); + return left; + } finally { + callStackTracker.pop(); } + } + + function parseFactor() { + callStackTracker.push('parseFactor', ''); - return { - type: 'CaseExpression', - value: [value], - cases, - }; + 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 }; } - // If we get here, it's an operator token that should be handled by parseExpression - // But we need to handle it here to avoid circular dependency - if (token.type === TokenType.LEFT_BRACE) { - current++; // Skip '{' - const entries = []; - let arrayIndex = 1; + // Check for assignments (identifier followed by ':') + if (tokens[current].type === TokenType.IDENTIFIER) { + const identifier = tokens[current].value; + current++; - while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { - // Skip leading commas - if (tokens[current].type === TokenType.COMMA) { - current++; - continue; - } + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' - let key = null; - let value; + // Check if this is a function definition with arrow syntax (x y -> body) + // Look ahead to see if we have parameters followed by -> + const lookAheadTokens = []; + let lookAheadPos = current; - // Check if this is a key-value pair or just a value - if (current + 1 < tokens.length && tokens[current + 1].type === TokenType.ASSIGNMENT) { - // This is a key-value pair: key: value - if (tokens[current].type === TokenType.IDENTIFIER) { - key = { - type: 'Identifier', - value: tokens[current].value - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.NUMBER) { - key = { - type: 'NumberLiteral', - value: tokens[current].value, - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.STRING) { - key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.TRUE) { - key = { - type: 'BooleanLiteral', - value: true, - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.FALSE) { - key = { - type: 'BooleanLiteral', - value: false, + // 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; + + while (paramIndex < lookAheadTokens.length) { + if (lookAheadTokens[paramIndex].type === TokenType.IDENTIFIER) { + parameters.push(lookAheadTokens[paramIndex].value); + paramIndex++; + } else { + // Skip non-identifier tokens (spaces, etc.) + paramIndex++; + } + } + + // Skip the parameters and -> + current = lookAheadPos + 1; // Skip the arrow + + // 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); + } + } + + // 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'); + } + + // Temporarily disable function call detection when parsing case expression results + const savedParsingFunctionArgs = parsingFunctionArgs; + parsingFunctionArgs = true; // Disable function call detection + const result = parseLogicalExpression(); + parsingFunctionArgs = savedParsingFunctionArgs; // Restore the flag + cases.push({ + pattern: patterns, + result: [result] + }); + + // Skip semicolon if present (but don't stop parsing cases) + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + // If the next token is an identifier followed by assignment, we've reached the end of the case expression + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const nextPos = current + 1; + if (nextPos < tokens.length && tokens[nextPos].type === TokenType.ASSIGNMENT) { + break; // End of case expression + } + } + } + } + + functionBody = { + type: 'CaseExpression', + value: values, + cases, }; - current++; // Skip the key } else { - throw new Error('Invalid key type in table literal'); + functionBody = parseLogicalExpression(); } - current++; // Skip ':' - value = parseExpression(); - } else { - // This is just a value (array-like entry) - value = parseExpression(); + return { + type: 'Assignment', + identifier, + value: { + type: 'FunctionDefinition', + parameters, + body: functionBody + } + }; } - entries.push({ key, value }); - - // Skip trailing commas - if (current < tokens.length && tokens[current].type === TokenType.COMMA) { - current++; + // 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 comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; + } + } + + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { + throw new Error('Expected ")" after function parameters'); + } + current++; // Skip ')' + + 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' + + // 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 + } + } + } + } + + functionBody = { + type: 'CaseExpression', + value: values, + cases, + }; + } else { + functionBody = parseLogicalExpression(); + } + + return { + type: 'Assignment', + identifier, + value: { + type: 'FunctionDefinition', + parameters, + body: functionBody + } + }; + } 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 ":" after pattern in case expression'); + } + + const result = parseLogicalExpression(); + cases.push({ + pattern: patterns, + result: [result] + }); + + // Skip semicolon if present (but don't stop parsing cases) + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + // If the next token is an identifier followed by assignment, we've reached the end of the case expression + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const nextPos = current + 1; + if (nextPos < tokens.length && tokens[nextPos].type === TokenType.ASSIGNMENT) { + break; // End of case expression + } + } + } + } + + return { + type: 'Assignment', + identifier, + value: { + type: 'CaseExpression', + value: values, + cases, + } + }; + } else { + // Regular assignment + const value = parseLogicalExpression(); + + return { + type: 'Assignment', + identifier, + value + }; + } } } - if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACE) { - current++; // Skip '}' - return { - type: 'TableLiteral', - entries: entries - }; - } else { - throw new Error('Expected closing brace'); - } - } - - // If we get here, it's an operator token that should be handled by parseExpression - // But we need to handle it here to avoid circular dependency - if (token.type === TokenType.PLUS || - token.type === TokenType.MINUS || - token.type === TokenType.MULTIPLY || - token.type === TokenType.DIVIDE || - token.type === TokenType.MODULO || - token.type === TokenType.POWER || - token.type === TokenType.EQUALS || - token.type === TokenType.NOT_EQUAL || - token.type === TokenType.LESS_THAN || - token.type === TokenType.GREATER_THAN || - token.type === TokenType.LESS_EQUAL || - token.type === TokenType.GREATER_EQUAL || - token.type === TokenType.AND || - token.type === TokenType.OR || - token.type === TokenType.XOR) { - // Reset current to parse the expression properly - return parseExpression(); + // If it's not an assignment, put the identifier back and continue + current--; } - // If we get here, we have an unexpected token - throw new Error(`Unexpected token in parsePrimary: ${token.type}`); - } - - // Check for IO operations before calling parsePrimary - if (tokens[current].type === TokenType.IO_IN) { - current++; - return { type: 'IOInExpression' }; - } else if (tokens[current].type === TokenType.IO_OUT) { - current++; - const outputValue = parseExpression(); - return { type: 'IOOutExpression', value: outputValue }; - } else if (tokens[current].type === TokenType.IO_ASSERT) { - current++; - const assertionExpr = parseExpression(); - return { type: 'IOAssertExpression', value: assertionExpr }; + // 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(); } - - // Simple wrapper that calls parsePrimary for all token types - return parsePrimary(); } const ast = { @@ -937,7 +1896,28 @@ function parser(tokens) { 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) { ast.body.push(node); @@ -952,634 +1932,788 @@ function parser(tokens) { 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) { 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 parseFloat(node.value); - case 'StringLiteral': - return node.value; - case 'BooleanLiteral': - return node.value; - case 'PlusExpression': - return evalNode(node.left) + evalNode(node.right); - case 'MinusExpression': - return evalNode(node.left) - evalNode(node.right); - case 'MultiplyExpression': - return evalNode(node.left) * evalNode(node.right); - case 'DivideExpression': - const divisor = evalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return evalNode(node.left) / evalNode(node.right); - case 'ModuloExpression': - return evalNode(node.left) % evalNode(node.right); - case 'PowerExpression': - return Math.pow(evalNode(node.left), evalNode(node.right)); - case 'EqualsExpression': - return evalNode(node.left) === evalNode(node.right); - case 'LessThanExpression': - return evalNode(node.left) < evalNode(node.right); - case 'GreaterThanExpression': - return evalNode(node.left) > evalNode(node.right); - case 'LessEqualExpression': - return evalNode(node.left) <= evalNode(node.right); - case 'GreaterEqualExpression': - return evalNode(node.left) >= evalNode(node.right); - case 'NotEqualExpression': - return evalNode(node.left) !== evalNode(node.right); - case 'AndExpression': - return evalNode(node.left) && evalNode(node.right); - case 'OrExpression': - return evalNode(node.left) || evalNode(node.right); - case 'XorExpression': - const leftVal = evalNode(node.left); - const rightVal = evalNode(node.right); - return (leftVal && !rightVal) || (!leftVal && rightVal); - case 'NotExpression': - return !evalNode(node.operand); - case 'TableLiteral': - const table = {}; - let arrayIndex = 1; - - for (const entry of node.entries) { - if (entry.key === null) { - // Array-like entry: {1, 2, 3} - table[arrayIndex] = evalNode(entry.value); - arrayIndex++; - } else { - // Key-value entry: {name: "Alice", age: 30} - let key; - if (entry.key.type === 'Identifier') { - // Convert identifier keys to strings - key = entry.key.value; + callStackTracker.push('evalNode', node?.type || 'unknown'); + + try { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return evalNode(node.left) + evalNode(node.right); + case 'MinusExpression': + return evalNode(node.left) - evalNode(node.right); + case 'MultiplyExpression': + return evalNode(node.left) * evalNode(node.right); + case 'DivideExpression': + const divisor = evalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return evalNode(node.left) / evalNode(node.right); + case 'ModuloExpression': + return evalNode(node.left) % evalNode(node.right); + case 'PowerExpression': + return Math.pow(evalNode(node.left), evalNode(node.right)); + case 'EqualsExpression': + return evalNode(node.left) === evalNode(node.right); + case 'LessThanExpression': + return evalNode(node.left) < evalNode(node.right); + case 'GreaterThanExpression': + return evalNode(node.left) > evalNode(node.right); + case 'LessEqualExpression': + return evalNode(node.left) <= evalNode(node.right); + case 'GreaterEqualExpression': + return evalNode(node.left) >= evalNode(node.right); + case 'NotEqualExpression': + return evalNode(node.left) !== evalNode(node.right); + case 'AndExpression': + return !!(evalNode(node.left) && evalNode(node.right)); + case 'OrExpression': + return !!(evalNode(node.left) || evalNode(node.right)); + case 'XorExpression': + const leftVal = evalNode(node.left); + const rightVal = evalNode(node.right); + return !!((leftVal && !rightVal) || (!leftVal && rightVal)); + case 'NotExpression': + return !evalNode(node.operand); + case 'UnaryMinusExpression': + return -evalNode(node.operand); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = evalNode(entry.value); + arrayIndex++; } else { - // For other key types (numbers, strings), evaluate normally - key = evalNode(entry.key); + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = evalNode(entry.key); + } + const value = evalNode(entry.value); + table[key] = value; } - const value = evalNode(entry.value); - table[key] = value; } - } - - return table; - case 'TableAccess': - const tableValue = evalNode(node.table); - let keyValue; - - // Handle different key types - if (node.key.type === 'Identifier') { - // For dot notation, use the identifier name as the key - keyValue = node.key.value; - } else { - // For bracket notation, evaluate the key expression - keyValue = evalNode(node.key); - } - - if (typeof tableValue !== 'object' || tableValue === null) { - throw new Error('Cannot access property of non-table value'); - } - - if (tableValue[keyValue] === undefined) { - throw new Error(`Key '${keyValue}' not found in table`); - } - - return tableValue[keyValue]; - case 'AssignmentExpression': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - const value = evalNode(node.value); - globalScope[node.name] = value; - return; - case '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) { - let localScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - localScope[node.params[i]] = args[i]; + + return table; + case 'TableAccess': + const tableValue = evalNode(node.table); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = evalNode(node.key); } - return localEvalNodeWithScope(node.body, localScope); - }; - 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); - 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 (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 (matches) { - const results = caseItem.result.map(evalNode); - if (results.length === 1) { - return results[0]; + 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); + + 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(' '); } - 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); + 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 }); - }); - 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; - default: - throw new Error(`Unknown node type: ${node.type}`); + + 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) => { - 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 'TableLiteral': - const table = {}; - let arrayIndex = 1; - - 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; + 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; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNodeWithScope(entry.value, scope); + arrayIndex++; } else { - // For other key types (numbers, strings), evaluate normally - key = localEvalNodeWithScope(entry.key, scope); + // 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; } - const value = localEvalNodeWithScope(entry.value, scope); - table[key] = value; } - } - - 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) { - let nestedScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - nestedScope[node.params[i]] = args[i]; + + 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); } - return localEvalNodeWithScope(node.body, nestedScope); - }; - 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'); - } - - 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)); - 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 (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 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'); + } + + 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)); - if (matches) { - const results = caseItem.result.map(res => localEvalNodeWithScope(res, scope)); - if (results.length === 1) { - return results[0]; + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope)); + + 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(res => localEvalNodeWithScope(res, scope)); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); } - 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); + 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 }); - }); - 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; - default: - throw new Error(`Unknown node type: ${node.type}`); + + 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) => { - 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 'TableLiteral': - const table = {}; - let arrayIndex = 1; - - 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; + 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; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNode(entry.value); + arrayIndex++; } else { - // For other key types (numbers, strings), evaluate normally - key = localEvalNode(entry.key); + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNode(entry.key); + } + const value = localEvalNode(entry.value); + table[key] = value; } - const value = localEvalNode(entry.value); - table[key] = value; } - } - - 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) { - let nestedScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - nestedScope[node.params[i]] = args[i]; + + 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); } - return localEvalNodeWithScope(node.body, nestedScope); - }; - 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 (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 (matches) { - const results = caseItem.result.map(localEvalNode); - if (results.length === 1) { - return results[0]; + 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(' '); } - 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); + 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 }); - }); - 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; - default: - throw new Error(`Unknown node type: ${node.type}`); + + 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(); } }; @@ -1599,7 +2733,20 @@ function interpreter(ast) { return lastResult; } -// Debug logging function +/** + * 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 (process.env.DEBUG) { console.log(`[DEBUG] ${message}`); @@ -1609,7 +2756,20 @@ function debugLog(message, data = null) { } } -// Debug error function +/** + * 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 (process.env.DEBUG) { console.error(`[DEBUG ERROR] ${message}`); @@ -1619,9 +2779,110 @@ function debugError(message, error = null) { } } -// Execute a file +/** + * 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(); + } +}; + +/** + * 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 { + // Validate file extension + if (!filePath.endsWith('.txt')) { + throw new Error('Only .txt files are supported'); + } + const fs = require('fs'); const input = fs.readFileSync(filePath, 'utf8'); @@ -1640,20 +2901,52 @@ function executeFile(filePath) { 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) { 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); } } -// Check command line arguments +/** + * 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. + */ const args = process.argv.slice(2); if (args.length === 0) { diff --git a/js/scripting-lang/learn_scripting_lang.txt b/js/scripting-lang/learn_scripting_lang.txt new file mode 100644 index 0000000..fc4f966 --- /dev/null +++ b/js/scripting-lang/learn_scripting_lang.txt @@ -0,0 +1,61 @@ +/* Learn an as of yet unnamed language in Y Minutes */ +/* A functional programming language with immutable variables, + first-class functions, and pattern matching */ + +/* We've got numbers */ +x : 42; /* also the ability to assign values to variables */ +y : 3.14; +z : 0; + +/* We've got identifiers */ +this_is_a : "test"; +flag : true; +value : false; + +/* We've got basic mathematical operators */ +sum : x + y; +diff : x - y; +prod : x * y; +quot : x / y; + +/* We've got pattern matching case statements! */ +result : case x of + 42 : "correct" + _ : "wrong"; + +/* Of course, there are functions */ +double : x -> x * 2; +add : x y -> x + y; +func_result : double 5; + +/* And immutable tables, kinda inspired by Lua's tables */ +table : {1, 2, 3, name: "Alice", age: 30}; +first : table[1]; +table_name : table.name; + +/* A boring standard library */ +square : x -> x * x; +mapped : map @double 5; +composed : compose @double @square 3; + +/* Special functions for IO all start with .. */ +..out "Hello from the scripting language!"; +..out x; +..out func_result; +..out result; +..out first; +..out table_name; +..out mapped; +..out composed; + +/* There's a baked in IO function for performing assertions */ +..assert x = 42; +..assert func_result = 10; +..assert result = "correct"; +..assert first = 1; +..assert table_name = "Alice"; +..assert mapped = 10; +..assert composed = 18; +..assert 5 > 3; /* ..assert should work with any kinda operators */ + +..out "Learn Scripting Language tutorial completed!"; \ No newline at end of file diff --git a/js/scripting-lang/nested_test.txt b/js/scripting-lang/nested_test.txt new file mode 100644 index 0000000..afb0677 --- /dev/null +++ b/js/scripting-lang/nested_test.txt @@ -0,0 +1,7 @@ +/* Test nested function calls in case expressions */ +factorial : n -> case n of + 0 : 1 + _ : factorial (n - 1); + +test : factorial 5; +..out test; \ No newline at end of file diff --git a/js/scripting-lang/paren_test.txt b/js/scripting-lang/paren_test.txt new file mode 100644 index 0000000..990858b --- /dev/null +++ b/js/scripting-lang/paren_test.txt @@ -0,0 +1,7 @@ +/* Test parentheses in case expressions */ +factorial : n -> case n of + 0 : 1 + _ : (n - 1); + +test : factorial 5; +..out test; \ No newline at end of file diff --git a/js/scripting-lang/run_tests.sh b/js/scripting-lang/run_tests.sh new file mode 100755 index 0000000..b456ff0 --- /dev/null +++ b/js/scripting-lang/run_tests.sh @@ -0,0 +1,123 @@ +#!/bin/bash + +# Test Runner for Simple Scripting Language +# Runs unit tests and integration tests systematically + +echo "=== Simple Scripting Language Test Suite ===" +echo "" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to run a test +run_test() { + local test_file=$1 + local test_name=$2 + + echo -n "Running $test_name... " + + # Capture both stdout and stderr, and get the exit code + local output + local exit_code + output=$(node lang.js "$test_file" 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo -e "${GREEN}PASS${NC}" + return 0 + else + echo -e "${RED}FAIL${NC}" + echo -e "${RED}Error:${NC} $output" + return 1 + fi +} + +# Function to run a test with output +run_test_with_output() { + local test_file=$1 + local test_name=$2 + + echo -e "${YELLOW}=== $test_name ===${NC}" + node lang.js "$test_file" + echo "" +} + +# Counters +total_tests=0 +passed_tests=0 +failed_tests=0 + +echo "Running Unit Tests..." +echo "====================" + +# Unit tests +unit_tests=( + "tests/01_lexer_basic.txt:Basic Lexer" + "tests/02_arithmetic_operations.txt:Arithmetic Operations" + "tests/03_comparison_operators.txt:Comparison Operators" + "tests/04_logical_operators.txt:Logical Operators" + "tests/05_io_operations.txt:IO Operations" + "tests/06_function_definitions.txt:Function Definitions" + "tests/07_case_expressions.txt:Case Expressions" + "tests/08_first_class_functions.txt:First-Class Functions" + "tests/09_tables.txt:Tables" + "tests/10_standard_library.txt:Standard Library" + "tests/11_edge_cases.txt:Edge Cases" + "tests/12_advanced_tables.txt:Advanced Tables" + "tests/13_standard_library_complete.txt:Complete Standard Library" + "tests/14_error_handling.txt:Error Handling" + # "tests/15_performance_stress.txt:Performance and Stress" + # "tests/16_advanced_functional.txt:Advanced Functional Programming" + # "tests/17_real_world_scenarios.txt:Real-World Scenarios" +) + +for test in "${unit_tests[@]}"; do + IFS=':' read -r file name <<< "$test" + total_tests=$((total_tests + 1)) + + if run_test "$file" "$name"; then + passed_tests=$((passed_tests + 1)) + else + failed_tests=$((failed_tests + 1)) + fi +done + +echo "" +echo "Running Integration Tests..." +echo "===========================" + +# Integration tests +integration_tests=( + "tests/integration_01_basic_features.txt:Basic Features Integration" + "tests/integration_02_pattern_matching.txt:Pattern Matching Integration" + "tests/integration_03_functional_programming.txt:Functional Programming Integration" + "tests/integration_04_mini_case_multi_param.txt:Multi-parameter case expression at top level" +) + +for test in "${integration_tests[@]}"; do + IFS=':' read -r file name <<< "$test" + total_tests=$((total_tests + 1)) + + if run_test "$file" "$name"; then + passed_tests=$((passed_tests + 1)) + else + failed_tests=$((failed_tests + 1)) + fi +done + +echo "" +echo "=== Test Summary ===" +echo "Total tests: $total_tests" +echo -e "Passed: ${GREEN}$passed_tests${NC}" +echo -e "Failed: ${RED}$failed_tests${NC}" + +if [ $failed_tests -eq 0 ]; then + echo -e "${GREEN}All tests passed!${NC}" + exit 0 +else + echo -e "${RED}Some tests failed.${NC}" + exit 1 +fi \ No newline at end of file diff --git a/js/scripting-lang/simple_case_test.txt b/js/scripting-lang/simple_case_test.txt new file mode 100644 index 0000000..bfc4768 --- /dev/null +++ b/js/scripting-lang/simple_case_test.txt @@ -0,0 +1,7 @@ +/* Simple case expression test */ +factorial : n -> case n of + 0 : 1 + _ : 2; + +test : factorial 5; +..out test; \ No newline at end of file diff --git a/js/scripting-lang/simple_test.txt b/js/scripting-lang/simple_test.txt new file mode 100644 index 0000000..5f1c5df --- /dev/null +++ b/js/scripting-lang/simple_test.txt @@ -0,0 +1,4 @@ +/* Simple test */ +x : 5; +y : x - 1; +..out y; \ No newline at end of file diff --git a/js/scripting-lang/table_basic_test.txt b/js/scripting-lang/table_basic_test.txt deleted file mode 100644 index 172d95c..0000000 --- a/js/scripting-lang/table_basic_test.txt +++ /dev/null @@ -1,51 +0,0 @@ -/* Basic Table Tests */ - -/* Test 1: Simple table creation */ -..out "=== Basic Table Tests ==="; -empty : {}; -numbers : {1, 2, 3}; -person : {name: "Alice", age: 30}; - -..out "Empty table: "; -..out empty; -..out "Numbers: "; -..out numbers; -..out "Person: "; -..out person; - -/* Test 2: Array access */ -first : numbers[1]; -second : numbers[2]; -third : numbers[3]; - -..out "First: "; -..out first; -..out "Second: "; -..out second; -..out "Third: "; -..out third; - -/* Test 3: Object access */ -name : person.name; -age : person.age; - -..out "Name: "; -..out name; -..out "Age: "; -..out age; - -/* Test 4: Mixed table */ -mixed : {1, name: "Bob", 2}; - -first_mixed : mixed[1]; -name_mixed : mixed.name; -second_mixed : mixed[2]; - -..out "Mixed first: "; -..out first_mixed; -..out "Mixed name: "; -..out name_mixed; -..out "Mixed second: "; -..out second_mixed; - -..out "Basic tests complete!"; \ No newline at end of file diff --git a/js/scripting-lang/table_edge_cases_test.txt b/js/scripting-lang/table_edge_cases_test.txt deleted file mode 100644 index 268f271..0000000 --- a/js/scripting-lang/table_edge_cases_test.txt +++ /dev/null @@ -1,304 +0,0 @@ -/* Table Edge Cases Tests */ - -/* Test 1: Nested tables */ -..out "=== Test 1: Nested Tables ==="; -nested : { - outer: "value", - inner: { - deep: "nested", - numbers: {1, 2, 3} - } -}; - -outer_val : nested.outer; -inner_table : nested.inner; -deep_val : nested.inner.deep; -inner_nums : nested.inner.numbers; -first_num : nested.inner.numbers[1]; - -..out "Outer: "; -..out outer_val; -..out "Inner table: "; -..out inner_table; -..out "Deep: "; -..out deep_val; -..out "Inner numbers: "; -..out inner_nums; -..out "First number: "; -..out first_num; - -/* Test 2: Tables with different value types */ -..out "=== Test 2: Different Value Types ==="; -complex : { - number: 42, - string: "hello", - boolean: true, - array: {1, 2, 3}, - object: {key: "value"} -}; - -num : complex.number; -str : complex.string; -bool : complex.boolean; -arr : complex.array; -obj : complex.object; - -..out "Number: "; -..out num; -..out "String: "; -..out str; -..out "Boolean: "; -..out bool; -..out "Array: "; -..out arr; -..out "Object: "; -..out obj; - -/* Test 3: Tables with function references */ -..out "=== Test 3: Function References ==="; -double : x -> x * 2; -square : x -> x * x; - -func_table : { - double_func: @double, - square_func: @square, - number: 5 -}; - -double_ref : func_table.double_func; -square_ref : func_table.square_func; -table_num : func_table.number; - -..out "Double ref: "; -..out double_ref; -..out "Square ref: "; -..out square_ref; -..out "Table number: "; -..out table_num; - -/* Test 4: Tables with arithmetic expressions */ -..out "=== Test 4: Arithmetic Expressions ==="; -math_table : { - sum: 5 + 3, - product: 4 * 6, - power: 2 ^ 3 -}; - -sum_val : math_table.sum; -prod_val : math_table.product; -pow_val : math_table.power; - -..out "Sum: "; -..out sum_val; -..out "Product: "; -..out prod_val; -..out "Power: "; -..out pow_val; - -/* Test 5: Tables with function calls */ -..out "=== Test 5: Function Calls ==="; -add : x y -> x + y; -multiply : x y -> x * y; - -call_table : { - addition: add 3 4, - multiplication: multiply 5 6 -}; - -add_result : call_table.addition; -mult_result : call_table.multiplication; - -..out "Addition: "; -..out add_result; -..out "Multiplication: "; -..out mult_result; - -/* Test 6: Tables with bracket notation access */ -..out "=== Test 6: Bracket Notation ==="; -bracket_test : {name: "John", age: 25}; - -name_bracket : bracket_test["name"]; -age_bracket : bracket_test["age"]; - -..out "Name (bracket): "; -..out name_bracket; -..out "Age (bracket): "; -..out age_bracket; - -/* Test 7: Tables with string keys */ -..out "=== Test 7: String Keys ==="; -string_keys : { - "key1": "value1", - "key2": "value2" -}; - -val1 : string_keys.key1; -val2 : string_keys["key2"]; - -..out "Value 1: "; -..out val1; -..out "Value 2: "; -..out val2; - -/* Test 8: Tables with numeric keys */ -..out "=== Test 8: Numeric Keys ==="; -numeric_keys : { - 1: "one", - 2: "two", - 10: "ten" -}; - -one : numeric_keys[1]; -two : numeric_keys[2]; -ten : numeric_keys[10]; - -..out "One: "; -..out one; -..out "Two: "; -..out two; -..out "Ten: "; -..out ten; - -/* Test 9: Tables with boolean keys */ -..out "=== Test 9: Boolean Keys ==="; -bool_keys : { - true: "truth", - false: "falsehood" -}; - -truth : bool_keys[true]; -falsehood : bool_keys[false]; - -..out "Truth: "; -..out truth; -..out "Falsehood: "; -..out falsehood; - -/* Test 10: Tables with trailing commas */ -..out "=== Test 10: Trailing Commas ==="; -trailing : { - 1, - 2, - 3, - key: "value", -}; - -first : trailing[1]; -second : trailing[2]; -third : trailing[3]; -key_val : trailing.key; - -..out "First: "; -..out first; -..out "Second: "; -..out second; -..out "Third: "; -..out third; -..out "Key: "; -..out key_val; - -/* Test 11: Tables with leading commas */ -..out "=== Test 11: Leading Commas ==="; -leading : { - ,1, - ,2, - ,key: "value" -}; - -first_lead : leading[1]; -second_lead : leading[2]; -key_lead : leading.key; - -..out "First (leading): "; -..out first_lead; -..out "Second (leading): "; -..out second_lead; -..out "Key (leading): "; -..out key_lead; - -/* Test 12: Tables with function definitions inside */ -..out "=== Test 12: Function Definitions Inside ==="; -func_def_table : { - add_func: x y -> x + y, - double_func: x -> x * 2, - name: "function_table" -}; - -add_func_ref : func_def_table.add_func; -double_func_ref : func_def_table.double_func; -func_name : func_def_table.name; - -..out "Add func ref: "; -..out add_func_ref; -..out "Double func ref: "; -..out double_func_ref; -..out "Func name: "; -..out func_name; - -/* Test 13: Tables with case expressions inside */ -..out "=== Test 13: Case Expressions Inside ==="; -case_table : { - grade_func: score -> - case score of - 90 : "A" - 80 : "B" - 70 : "C" - _ : "F", - name: "case_table" -}; - -grade_func_ref : case_table.grade_func; -case_name : case_table.name; - -..out "Grade func ref: "; -..out grade_func_ref; -..out "Case name: "; -..out case_name; - -/* Test 14: Tables with standard library functions */ -..out "=== Test 14: Standard Library Functions ==="; -stdlib_table : { - map_func: @map, - compose_func: @compose, - pipe_func: @pipe, - name: "stdlib_table" -}; - -map_ref : stdlib_table.map_func; -compose_ref : stdlib_table.compose_func; -pipe_ref : stdlib_table.pipe_func; -stdlib_name : stdlib_table.name; - -..out "Map ref: "; -..out map_ref; -..out "Compose ref: "; -..out compose_ref; -..out "Pipe ref: "; -..out pipe_ref; -..out "Stdlib name: "; -..out stdlib_name; - -/* Test 15: Tables with IO operations */ -..out "=== Test 15: IO Operations ==="; -io_table : { - input_func: @..in, - output_func: @..out, - assert_func: @..assert, - name: "io_table" -}; - -input_ref : io_table.input_func; -output_ref : io_table.output_func; -assert_ref : io_table.assert_func; -io_name : io_table.name; - -..out "Input ref: "; -..out input_ref; -..out "Output ref: "; -..out output_ref; -..out "Assert ref: "; -..out assert_ref; -..out "IO name: "; -..out io_name; - -..out "Edge cases tests complete!"; \ 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 |