diff options
Diffstat (limited to 'js')
29 files changed, 4620 insertions, 248 deletions
diff --git a/js/scripting-lang/FIXME.md b/js/scripting-lang/FIXME.md new file mode 100644 index 0000000..98aec38 --- /dev/null +++ b/js/scripting-lang/FIXME.md @@ -0,0 +1,109 @@ +# 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 +- ✅ Comprehensive test suite implementation +- ✅ All language features working correctly in isolation and integration + +**Remaining Issues:** +- ❌ Stack overflow in the original comprehensive test file (`test.txt`) +- ❌ Need to identify what specific pattern in the original test is causing the stack overflow + +## Investigation Results + +**Successfully Implemented:** +1. **Structured Testing Strategy** - Created comprehensive unit and integration test suite +2. **All Language Features Working** - Confirmed that all individual features work correctly +3. **Integration Testing** - Verified that feature combinations work correctly +4. **Test Automation** - Implemented automated test runner with pass/fail reporting + +**Test Results:** +- ✅ **13/13 tests passing** (10 unit tests + 3 integration tests) +- ✅ All core language features verified working +- ✅ All feature combinations verified working +- ✅ No stack overflow issues in the new test suite + +**Stack Overflow Analysis:** +- The stack overflow only occurs in the original `test.txt` file +- All language constructs work correctly when tested in isolation or in controlled combinations +- The issue is likely caused by a specific pattern or combination in the original comprehensive test + +## Next Steps + +1. **Compare Test Files** + - Analyze the differences between the working test suite and the problematic `test.txt` + - Identify specific patterns or constructs that exist in `test.txt` but not in the working tests + +2. **Incremental Analysis** + - Test sections of `test.txt` individually to isolate the problematic pattern + - Look for any language constructs or combinations that might cause infinite recursion + +3. **Parser Edge Case Investigation** + - Check for any edge cases in the parser that might be triggered by specific patterns + - Verify that all language constructs are handled correctly in all contexts + +--- + +**Status:** +- Lexer: ✅ Fixed (decimal numbers, IO tokens, wildcards) +- Parser: ✅ Fixed (IO handling, case expressions, wildcards) +- Interpreter: ✅ Fixed (decimal number evaluation, case expressions) +- Test Suite: ✅ Implemented and working (13/13 tests passing) +- Original Test File: ❌ Still has stack overflow issue + +--- + +**Next step:** Analyze the differences between the working test suite and the problematic `test.txt` to identify the specific 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 new file mode 100644 index 0000000..82eed66 --- /dev/null +++ b/js/scripting-lang/IDEAS.txt @@ -0,0 +1,9 @@ +add 2 other io functions + +..listen + +..emit + +where listen takes in a well defined state object from outside the scope of the program, making it available to the program + +where emit lets the program spit state back out into the wider world \ No newline at end of file diff --git a/js/scripting-lang/NEXT-STEPS.md b/js/scripting-lang/NEXT-STEPS.md new file mode 100644 index 0000000..7cb1e75 --- /dev/null +++ b/js/scripting-lang/NEXT-STEPS.md @@ -0,0 +1,229 @@ +# Next Steps: Table Features Implementation + +## Current State Analysis + +### 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) + +### 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 + +## Root Cause Analysis + +### 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 + +### 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]` + +## Implementation Plan + +### Phase 1: Fix Chained Table Access Parser + +#### 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()` + +**Current Logic**: +```javascript +// Assignment parsing falls back to walk() for value +const value = walk(); // This fails on DOT tokens +``` + +**New Logic**: +```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(); +``` + +#### Step 1.2: Create parseChainedTableAccess Helper +**File**: `lang.js` in `walk()` function +**Purpose**: Parse arbitrary length dot notation chains + +**Implementation**: +```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; +} +``` + +#### Step 1.3: Update Function Call Parsing +**File**: `lang.js` around line 600-700 +**Change**: Allow `TableAccess` nodes as function names + +**Current Logic**: +```javascript +// Only handles string function names +func = globalScope[node.name]; +``` + +**New Logic**: +```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'); + } +} +``` + +### Phase 2: Fix Function Calls from Tables + +#### Step 2.1: Update Function Call Detection +**File**: `lang.js` in `parseFunctionCall()` function +**Change**: Detect when a table access is followed by arguments + +**Current Logic**: +```javascript +// Only checks for identifier followed by arguments +if (tokens[current + 1] && tokens[current + 1].type === TokenType.NUMBER) { + // Function call +} +``` + +**New Logic**: +```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; +} +``` + +#### Step 2.2: Create parseFunctionCallFromTable Helper +**Purpose**: Parse function calls where the function is a table access + +**Implementation**: +```javascript +function parseFunctionCallFromTable(tableAccess) { + const args = []; + while (current < tokens.length && isArgumentToken(tokens[current])) { + args.push(walk()); + } + return { + type: 'FunctionCall', + name: tableAccess, + args + }; +} +``` + +### 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 */ + +/* Test 2: Chained table access */ +config : {user: {profile: {name: "Bob"}}}; +deep_name : config.user.profile.name; +..out deep_name; /* Should output: Bob */ + +/* Test 3: Functions in tables */ +math : { + add : x y -> x + y, + sub : x y -> x - y, + double : x -> x * 2 +}; + +/* 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; +``` + +#### 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 + +## Success Criteria + +- ✅ `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 + +## Risk Mitigation + +- **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 diff --git a/js/scripting-lang/README.md b/js/scripting-lang/README.md new file mode 100644 index 0000000..296c39f --- /dev/null +++ b/js/scripting-lang/README.md @@ -0,0 +1,769 @@ +# 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. + +## 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 + +## Syntax + +### Variables + +Variables are immutable and assigned using the `:` operator: + +``` +x : 5; +y : 10; +``` + +### Arithmetic Operations + +Arithmetic operations are supported with parentheses grouping: + +``` +sum : x + y; +diff : x - y; +product : x * y; +quotient : x / y; +modulo : 17 % 5; /* Returns 2 */ +power : 2 ^ 3; /* Returns 8 */ +grouped : (5 + 3) * 2; /* Returns 16 */ +nested : ((5 + 3) * 2) + 1; /* Returns 17 */ +``` + +### Function Definitions + +Functions are defined using arrow syntax (`->`): + +``` +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); + +/* Function call with function calls in parentheses */ +result3 : add (double 3) (square 2); + +/* Complex nested function calls with parentheses */ +result4 : add (add 1 2) (add 3 4); +``` + +**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; + +composed : compose @double @square 3; /* double(square(3)) = 18 */ +``` + +**Function Reference Syntax:** +- `@functionName` - Creates a function reference (doesn't execute the function) +- `functionName args` - Calls the function with arguments + +### Pattern Matching + +The language supports pattern matching with case expressions in function bodies: + +``` +compare : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : "neither zero"; +``` + +#### Pattern Matching Syntax + +- **Exact matches**: `0 0` matches when both values are 0 +- **Wildcards**: `_` matches any value +- **Mixed patterns**: `0 _` matches when first value is 0 and second can be anything + +### Comparison and Logical Operations + +The language supports comparison and logical operations: + +``` +/* Comparison operators */ +less : 3 < 5; /* true */ +greater : 10 > 5; /* true */ +equal : 5 = 5; /* true */ +not_equal : 3 != 5; /* true */ +less_equal : 5 <= 5; /* true */ +greater_equal : 5 >= 3; /* true */ + +/* Logical operators */ +and_result : 1 and 1; /* true */ +or_result : 0 or 1; /* true */ +xor_result : 1 xor 0; /* true */ +not_result : not 0; /* true */ +``` + +### Comments + +The language supports C-style block comments: + +``` +/* This is a single line comment */ + +/* This is a multi-line comment + that spans multiple lines */ + +x : 5; /* Comment on same line as code */ + +/* Nested comments are supported */ +/* Outer comment /* Inner comment */ More outer comment */ +``` + +**Comment Features:** +- Block comments start with `/*` and end with `*/` +- Comments can span multiple lines +- Comments can appear on the same line as code +- Nested comments are supported +- Comments are completely ignored by the lexer and parser + +### Input/Output Operations + +The language provides built-in IO operations for reading from stdin and writing to stdout: + +``` +name : ..in; /* Read input from stdin */ +..out name; /* Output to stdout */ +..out "Hello"; /* Output string literal */ +..out 42; /* Output number */ +..assert x = 5; /* Assert that x equals 5 */ +..assert y > 3; /* Assert that y is greater than 3 */ +..assert z != 0; /* Assert that z is not equal to 0 */ +``` + +**IO Functions:** +- `..in` - Reads a line from stdin, returns a number if possible, otherwise a string +- `..out` - Outputs a value to stdout and returns the value +- `..assert` - Asserts that a comparison is true, throws an error if it's false. Supports all comparison operators: `=`, `<`, `>`, `<=`, `>=`, `!=` + +**Note:** The `..` prefix indicates these are impure operations that have side effects. + +## Standard Library + +The language includes a built-in standard library with functional programming utilities: + +### Higher-Order Functions + +- **map**: Apply a function to a value (`map f x = f x`) +- **compose**: Compose two functions (`compose f g x = f(g(x))`) +- **curry**: Explicit currying (`curry f x y = f x y`) +- **apply**: Apply a function to an argument (`apply f x = f x`) +- **pipe**: Compose functions left-to-right (`pipe f g x = g(f(x))`) +- **filter**: Filter based on a predicate (`filter p x = p(x) ? x : 0`) +- **reduce**: Reduce to a single value (`reduce f init x = f(init, x)`) +- **fold**: Same as reduce (`fold f init x = f(init, x)`) + +### Usage Examples + +``` +double : x -> x * 2; +square : x -> x * x; + +/* Using map */ +result1 : map @double 5; /* Returns 10 */ +result2 : map @square 3; /* Returns 9 */ + +/* Using compose */ +composed : compose @double @square 3; /* double(square(3)) = 18 */ + +/* Using pipe */ +piped : pipe @double @square 2; /* square(double(2)) = 16 */ + +/* Using filter */ +isPositive : x -> x > 0; +filtered : filter @isPositive 5; /* Returns 5 (since 5 > 0) */ +``` + + +## Language Components + +### Lexer + +The lexer tokenizes input into meaningful units: +- Numbers: `123` +- Identifiers: `variable_name` +- Operators: `+`, `-`, `*`, `/` +- Keywords: `case`, `of`, `function` +- Symbols: `:`, `->`, `_` + +### Parser + +The parser builds an Abstract Syntax Tree (AST) from tokens, supporting: +- Number literals +- Arithmetic expressions +- Variable assignments +- Function declarations +- Function calls +- Case expressions +- Pattern matching + +### Interpreter + +The interpreter executes the AST with: +- Global scope management +- Function evaluation +- Pattern matching execution +- Arithmetic computation +- Immutable variable semantics + +## Example Programs + +### Basic Arithmetic and Variables + +``` +/* Simple arithmetic with variables */ +x : 5; +y : 10; +sum : x + y; +diff : x - y; +product : x * y; +quotient : y / x; +modulo : 17 % 5; /* Returns 2 */ +power : 2 ^ 3; /* Returns 8 */ + +..out sum; /* 15 */ +..out diff; /* -5 */ +..out product; /* 50 */ +..out quotient; /* 2 */ +..out modulo; /* 2 */ +..out power; /* 8 */ + +/* Grouped expressions with parentheses */ +grouped : (5 + 3) * 2; /* 16 */ +nested : ((5 + 3) * 2) + 1; /* 17 */ +complex : (2 ^ 3) % 3 and 5 > 3; /* true */ +``` + +### Function Definitions and Calls + +``` +/* Basic function definition */ +add : x y -> x + y; +multiply : x y -> x * y; +double : x -> x * 2; +square : x -> x * x; + +/* Function calls */ +result1 : add 3 4; /* 7 */ +result2 : multiply 5 6; /* 30 */ +result3 : double 8; /* 16 */ +result4 : square 4; /* 16 */ + +/* Function calls with parenthesized arguments */ +result5 : add (3 + 2) (4 + 1); /* 15 */ +result6 : add (double 3) (square 2); /* 10 */ +result7 : add (add 1 2) (add 3 4); /* 10 */ +``` + +### Tables and Data Structures + +``` +/* Create various table types */ +empty : {}; +numbers : {1, 2, 3, 4, 5}; +person : {name: "Alice", age: 30, active: true}; +mixed : {1, name: "Bob", 2, active: false}; + +/* Access array elements */ +first : numbers[1]; +second : numbers[2]; +last : numbers[5]; + +/* Access object properties */ +name : person.name; +age : person.age; +active : person.active; + +/* Access mixed table */ +first_mixed : mixed[1]; +name_mixed : mixed.name; +second_mixed : mixed[2]; + +/* Output results */ +..out "First number: "; +..out first; +..out "Person name: "; +..out name; +..out "Mixed first: "; +..out first_mixed; +..out "Mixed name: "; +..out name_mixed; +``` + +### Pattern Matching with Case Expressions + +``` +/* Pattern matching in function bodies */ +compare : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : "neither zero"; + +/* Testing pattern matching */ +test1 : compare 0 0; /* "both zero" */ +test2 : compare 0 5; /* "x is zero" */ +test3 : compare 5 0; /* "y is zero" */ +test4 : compare 5 5; /* "neither zero" */ + +/* Single-parameter case expressions */ +factorial : n -> + case n of + 0 : 1 + _ : n * (factorial (n - 1)); + +/* Single-parameter grade function */ +grade : score -> + case score of + 90 : "A" + 80 : "B" + 70 : "C" + _ : "F"; + +fact5 : factorial 5; /* 120 */ +grade1 : grade 95; /* "A" */ +grade2 : grade 85; /* "B" */ +grade3 : grade 65; /* "F" */ +``` + +### First-Class Functions and Composition + +``` +/* Function references and composition */ +double : x -> x * 2; +square : x -> x * x; +add1 : x -> x + 1; + +/* Using standard library functions */ +composed : compose @double @square 3; /* double(square(3)) = 18 */ +piped : pipe @double @square 2; /* square(double(2)) = 16 */ +applied : apply @double 5; /* double(5) = 10 */ + +/* Higher-order functions */ +mapped1 : map @double 5; /* 10 */ +mapped2 : map @square 3; /* 9 */ + +/* Function references in case expressions */ +getFunction : type -> + case type of + "double" : @double + "square" : @square + _ : @add1; + +func1 : getFunction "double"; /* Function reference */ +func2 : getFunction "square"; /* Function reference */ +``` + +### Recursion and Complex Functions + +``` +/* Recursive factorial function (using single-parameter case) */ +factorial : n -> + case n of + 0 : 1 + _ : n * (factorial (n - 1)); + +/* Recursive countdown function */ +countdown : n -> + case n of + 0 : "done" + _ : countdown (n - 1); + +/* Testing recursive functions */ +fact5 : factorial 5; /* 120 */ +count : countdown 3; /* "done" */ +``` + +### Comparison and Logical Operators + +``` +/* 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; +isPositive : x -> x > 0; + +/* 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!"; +``` + +## Running Programs + +The language supports file execution mode only: + +### File Execution Mode +Run a script file by providing the file path as an argument: + +```bash +node lang.js script.txt +``` + +**Note:** REPL mode has been removed in the simplified version. The language now only supports file execution. + +## Testing + +The language includes a comprehensive test suite to verify functionality: + +### Test Structure + +- **Unit Tests** (`tests/01_*.txt` to `tests/10_*.txt`): Test individual language features in isolation +- **Integration Tests** (`tests/integration_*.txt`): Test combinations of multiple features +- **Test Runner** (`run_tests.sh`): Automated test execution with pass/fail reporting + +### Running Tests + +**Run all tests:** +```bash +./run_tests.sh +``` + +**Run individual tests:** +```bash +node lang.js tests/01_lexer_basic.txt +node lang.js tests/07_case_expressions.txt +node lang.js tests/integration_01_basic_features.txt +``` + +### Test Coverage + +The test suite covers: +- Basic lexer functionality (numbers, identifiers, operators, keywords) +- Arithmetic operations (all operators, precedence, parentheses) +- Comparison operators (all comparison types, edge cases) +- Logical operators (and, or, xor, not, complex expressions) +- IO operations (output, assertions, string comparisons) +- Function definitions (syntax, parameters, calls, parentheses) +- Case expressions (pattern matching, wildcards, recursion) +- First-class functions (references, higher-order functions) +- Tables (literals, access, mixed types) +- Standard library (all built-in higher-order functions) +- Integration scenarios (feature combinations, complex patterns) + +### Testing Strategy + +The testing approach focuses on: +1. **Isolation**: Each unit test focuses on a single language feature +2. **Integration**: Integration tests verify feature combinations work correctly +3. **Edge Cases**: Tests include boundary conditions and error scenarios +4. **Automation**: Test runner provides systematic execution and reporting + +## Language Design Principles + +- **Simplicity**: Minimal syntax for maximum clarity +- **Immutability**: Variables cannot be reassigned +- **Functional**: Functions are first-class values with composition support +- **Pattern-oriented**: Pattern matching as a core feature +- **Recursive**: Support for recursive function calls in function bodies +- **Grouped**: Parentheses support for complex arithmetic expressions +- **Expressive**: Rich set of arithmetic, comparison, and logical operators +- **IO-aware**: Built-in input/output operations with clear impurity markers +- **Educational**: Designed to teach language implementation concepts + +## Limitations + +This is a learning language with intentional limitations: +- Complex nested function calls (e.g., `add double 3 square 2`) are ambiguous without explicit grouping +- There's one known issue with function calls inside assertions (e.g., ..assert add 3 4 = 7), which is a parsing edge case. Workaround: use parentheses around function calls in assertions (e.g., ..assert (add 3 4) = 7) +- Limited to immutable data structures (no mutation operations) +- No error handling beyond basic validation +- No modules or imports +- IO operations are synchronous and block execution + +## Future Enhancements + +Planned features for future versions: +- Ambiguous function call detection and error reporting +- Lists and data structures +- Error handling +- Modules and imports +- Performance optimizations + +## 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. + +## Files + +- `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 diff --git a/js/scripting-lang/analyze_test_differences.sh b/js/scripting-lang/analyze_test_differences.sh new file mode 100755 index 0000000..41a2ced --- /dev/null +++ b/js/scripting-lang/analyze_test_differences.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Script to analyze differences between working tests and problematic test.txt + +echo "=== Test Analysis Tool ===" +echo "" + +echo "1. Checking file sizes:" +echo " Working test files:" +for file in tests/*.txt; do + if [ -f "$file" ]; then + lines=$(wc -l < "$file") + echo " - $(basename "$file"): $lines lines" + fi +done + +echo "" +echo " Original test.txt:" +if [ -f "test.txt" ]; then + lines=$(wc -l < "test.txt") + echo " - test.txt: $lines lines" +else + echo " - test.txt: not found" +fi + +echo "" +echo "2. Testing sections of test.txt:" + +if [ -f "test.txt" ]; then + # Extract first 50 lines and test + echo " Testing first 50 lines..." + head -50 test.txt > temp_section1.txt + if node lang.js temp_section1.txt > /dev/null 2>&1; then + echo " ✅ First 50 lines: PASS" + else + echo " ❌ First 50 lines: FAIL" + fi + rm temp_section1.txt + + # Extract lines 51-100 and test + echo " Testing lines 51-100..." + sed -n '51,100p' test.txt > temp_section2.txt + if [ -s temp_section2.txt ]; then + if node lang.js temp_section2.txt > /dev/null 2>&1; then + echo " ✅ Lines 51-100: PASS" + else + echo " ❌ Lines 51-100: FAIL" + fi + else + echo " ⚠️ Lines 51-100: Empty" + fi + rm temp_section2.txt + + # Extract lines 101-150 and test + echo " Testing lines 101-150..." + sed -n '101,150p' test.txt > temp_section3.txt + if [ -s temp_section3.txt ]; then + if node lang.js temp_section3.txt > /dev/null 2>&1; then + echo " ✅ Lines 101-150: PASS" + else + echo " ❌ Lines 101-150: FAIL" + fi + else + echo " ⚠️ Lines 101-150: Empty" + fi + rm temp_section3.txt + + # Continue with more sections if needed + echo " Testing lines 151-200..." + sed -n '151,200p' test.txt > temp_section4.txt + if [ -s temp_section4.txt ]; then + if node lang.js temp_section4.txt > /dev/null 2>&1; then + echo " ✅ Lines 151-200: PASS" + else + echo " ❌ Lines 151-200: FAIL" + fi + else + echo " ⚠️ Lines 151-200: Empty" + fi + rm temp_section4.txt + +else + echo " test.txt not found" +fi + +echo "" +echo "3. Unique constructs in test.txt:" +if [ -f "test.txt" ]; then + echo " Checking for unique patterns..." + + # Look for unique function call patterns + echo " - Function calls with complex nesting:" + grep -n "add.*add.*add" test.txt | head -3 + + # Look for unique case expression patterns + echo " - Complex case expressions:" + grep -n "case.*case.*case" test.txt | head -3 + + # Look for unique table patterns + echo " - Complex table literals:" + grep -n "\\{.*\\{.*\\}" test.txt | head -3 +fi + +echo "" +echo "Analysis complete." \ No newline at end of file diff --git a/js/scripting-lang/input.txt b/js/scripting-lang/input.txt deleted file mode 100644 index d343086..0000000 --- a/js/scripting-lang/input.txt +++ /dev/null @@ -1,3 +0,0 @@ -x : 10; -y : 20; -x + y; \ No newline at end of file diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index f91f842..3de7a0e 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,320 +1,1672 @@ // 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 +function initializeStandardLibrary(scope) { + // Map: Apply a function to each element + scope.map = function(f, x) { + // Handle function references by calling them if they're functions + if (typeof f === 'function') { + return f(x); + } else { + throw new Error('map: first argument must be a function'); + } + }; + + // Compose: Compose two functions (f ∘ g)(x) = f(g(x)) + scope.compose = function(f, g, x) { + if (typeof f === 'function' && typeof g === 'function') { + 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 + scope.curry = function(f, x, y) { + if (typeof f === 'function') { + return f(x, y); + } else { + throw new Error('curry: first argument must be a function'); + } + }; + + // Apply: Apply a function to an argument (same as function call, but more explicit) + scope.apply = function(f, x) { + if (typeof f === 'function') { + return f(x); + } else { + throw new Error('apply: first argument must be a function'); + } + }; + + // Pipe: Compose functions in left-to-right order (opposite of compose) + // pipe f g x = g f x + scope.pipe = function(f, g, x) { + if (typeof f === 'function' && typeof g === 'function') { + 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 + scope.filter = function(p, x) { + if (typeof p === 'function') { + return p(x) ? x : 0; + } else { + throw new Error('filter: first argument must be a function'); + } + }; + + // Reduce: Reduce to a single value using a binary function + // For now, we'll implement it as a higher-order function + scope.reduce = function(f, init, x) { + if (typeof f === 'function') { + return f(init, x); + } else { + throw new Error('reduce: first argument must be a function'); + } + }; + + // Fold: Same as reduce, but more explicit about the folding direction + scope.fold = function(f, init, x) { + if (typeof f === 'function') { + return f(init, x); + } else { + throw new Error('fold: first argument must be a function'); + } + }; +} + // Define the types of tokens const TokenType = { NUMBER: 'NUMBER', PLUS: 'PLUS', + MINUS: 'MINUS', + MULTIPLY: 'MULTIPLY', + DIVIDE: 'DIVIDE', IDENTIFIER: 'IDENTIFIER', ASSIGNMENT: 'ASSIGNMENT', + ARROW: 'ARROW', + CASE: 'CASE', + OF: 'OF', + WILDCARD: 'WILDCARD', FUNCTION: 'FUNCTION', LEFT_PAREN: 'LEFT_PAREN', RIGHT_PAREN: 'RIGHT_PAREN', LEFT_BRACE: 'LEFT_BRACE', RIGHT_BRACE: 'RIGHT_BRACE', + LEFT_BRACKET: 'LEFT_BRACKET', + RIGHT_BRACKET: 'RIGHT_BRACKET', SEMICOLON: 'SEMICOLON', + COMMA: 'COMMA', + DOT: 'DOT', + STRING: 'STRING', + TRUE: 'TRUE', + FALSE: 'FALSE', + AND: 'AND', + OR: 'OR', + XOR: 'XOR', + NOT: 'NOT', + EQUALS: 'EQUALS', + LESS_THAN: 'LESS_THAN', + GREATER_THAN: 'GREATER_THAN', + LESS_EQUAL: 'LESS_EQUAL', + GREATER_EQUAL: 'GREATER_EQUAL', + NOT_EQUAL: 'NOT_EQUAL', + MODULO: 'MODULO', + POWER: 'POWER', + IO_IN: 'IO_IN', + IO_OUT: 'IO_OUT', + IO_ASSERT: 'IO_ASSERT', + FUNCTION_REF: 'FUNCTION_REF' }; -// Lexer +// Lexer - converts source code to tokens function lexer(input) { - const tokens = []; let current = 0; - + const tokens = []; + while (current < input.length) { let char = input[current]; - - if (/\d/.test(char)) { - let value = ''; - while (/\d/.test(char)) { - value += char; - char = input[++current]; - } - tokens.push({ - type: TokenType.NUMBER, - value - }); - continue; - } - - if (char === '+') { - tokens.push({ - type: TokenType.PLUS - }); + + // Skip whitespace + if (/\s/.test(char)) { current++; continue; } - - if (/[a-z]/i.test(char)) { - let value = ''; - while (/[a-z]/i.test(char)) { - value += char; - char = input[++current]; + + // Skip comments + if (char === '/' && input[current + 1] === '*') { + let commentDepth = 1; + current += 2; // Skip /* + + while (current < input.length && commentDepth > 0) { + if (input[current] === '/' && input[current + 1] === '*') { + commentDepth++; + current += 2; + } else if (input[current] === '*' && input[current + 1] === '/') { + commentDepth--; + current += 2; + } else { + current++; + } } - tokens.push({ - type: TokenType.IDENTIFIER, - value - }); - continue; - } - - if (char === ':') { - tokens.push({ - type: TokenType.ASSIGNMENT - }); - current++; - continue; - } - - if (char === '=') { - tokens.push({ - type: TokenType.EQUAL - }); - current++; - continue; - } - - if (input.slice(current, current + 2) === 'if') { - tokens.push({ - type: TokenType.IF - }); - current += 2; - continue; - } - - if (input.slice(current, current + 4) === 'else') { - tokens.push({ - type: TokenType.ELSE - }); - current += 4; continue; } - - if (char === '(') { - tokens.push({ - type: TokenType.LEFT_PAREN - }); - current++; - continue; - } - - if (char === ')') { - tokens.push({ - type: TokenType.RIGHT_PAREN - }); - current++; + + // Numbers + if (/[0-9]/.test(char)) { + let value = ''; + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; + current++; + } + + // Check for decimal point + if (current < input.length && input[current] === '.') { + value += input[current]; + current++; + + // Parse decimal part + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; + current++; + } + + tokens.push({ + type: TokenType.NUMBER, + value: parseFloat(value) + }); + } else { + tokens.push({ + type: TokenType.NUMBER, + value: parseInt(value) + }); + } continue; } - - if (char === '{') { - tokens.push({ - type: TokenType.LEFT_BRACE - }); - current++; + + // Strings + if (char === '"') { + let value = ''; + current++; // Skip opening quote + + while (current < input.length && input[current] !== '"') { + value += input[current]; + current++; + } + + if (current < input.length) { + current++; // Skip closing quote + tokens.push({ + type: TokenType.STRING, + value: value + }); + } else { + throw new Error('Unterminated string'); + } continue; } - - if (char === '}') { - tokens.push({ - type: TokenType.RIGHT_BRACE - }); - current++; + + // Identifiers and keywords + if (/[a-zA-Z_]/.test(char)) { + let value = ''; + while (current < input.length && /[a-zA-Z0-9_]/.test(input[current])) { + value += input[current]; + current++; + } + + // Check for keywords + switch (value) { + case 'case': + tokens.push({ type: TokenType.CASE }); + break; + case 'of': + tokens.push({ type: TokenType.OF }); + break; + case 'function': + tokens.push({ type: TokenType.FUNCTION }); + break; + case 'true': + tokens.push({ type: TokenType.TRUE }); + break; + case 'false': + tokens.push({ type: TokenType.FALSE }); + break; + case 'and': + tokens.push({ type: TokenType.AND }); + break; + case 'or': + tokens.push({ type: TokenType.OR }); + break; + case 'xor': + tokens.push({ type: TokenType.XOR }); + break; + case 'not': + tokens.push({ type: TokenType.NOT }); + break; + case '_': + tokens.push({ type: TokenType.WILDCARD }); + break; + default: + tokens.push({ + type: TokenType.IDENTIFIER, + value: value + }); + } continue; } - - if (input.slice(current, current + 8) === 'function') { - tokens.push({ - type: TokenType.FUNCTION - }); - current += 8; - continue; + + // Two-character operators + if (current + 1 < input.length) { + const twoChar = char + input[current + 1]; + switch (twoChar) { + case '->': + tokens.push({ type: TokenType.ARROW }); + current += 2; + continue; + case '==': + tokens.push({ type: TokenType.EQUALS }); + current += 2; + continue; + case '!=': + tokens.push({ type: TokenType.NOT_EQUAL }); + current += 2; + continue; + case '<=': + tokens.push({ type: TokenType.LESS_EQUAL }); + current += 2; + continue; + case '>=': + tokens.push({ type: TokenType.GREATER_EQUAL }); + current += 2; + continue; + case '..': + // Check for IO operations + if (current + 2 < input.length) { + const ioChar = input[current + 2]; + switch (ioChar) { + case 'i': + if (current + 3 < input.length && input[current + 3] === 'n') { + tokens.push({ type: TokenType.IO_IN }); + current += 4; + continue; + } + break; + case 'o': + if (current + 3 < input.length && input[current + 3] === 'u') { + if (current + 4 < input.length && input[current + 4] === 't') { + tokens.push({ type: TokenType.IO_OUT }); + current += 5; + continue; + } + } + break; + case 'a': + if (current + 3 < input.length && input[current + 3] === 's') { + if (current + 4 < input.length && input[current + 4] === 's') { + if (current + 5 < input.length && input[current + 5] === 'e') { + if (current + 6 < input.length && input[current + 6] === 'r') { + if (current + 7 < input.length && input[current + 7] === 't') { + tokens.push({ type: TokenType.IO_ASSERT }); + current += 8; + continue; + } + } + } + } + } + break; + } + } + // If we get here, it's not a complete IO operation, so skip the '..' + current += 2; + continue; + } } - - if (char === ';') { - tokens.push({ type: TokenType.SEMICOLON }); - current++; - continue; + + // Single character operators + switch (char) { + case '+': + tokens.push({ type: TokenType.PLUS }); + break; + case '-': + tokens.push({ type: TokenType.MINUS }); + break; + case '*': + tokens.push({ type: TokenType.MULTIPLY }); + break; + case '/': + tokens.push({ type: TokenType.DIVIDE }); + break; + case '%': + tokens.push({ type: TokenType.MODULO }); + break; + case '^': + tokens.push({ type: TokenType.POWER }); + break; + case ':': + tokens.push({ type: TokenType.ASSIGNMENT }); + break; + case '(': + tokens.push({ type: TokenType.LEFT_PAREN }); + break; + case ')': + tokens.push({ type: TokenType.RIGHT_PAREN }); + break; + case '{': + tokens.push({ type: TokenType.LEFT_BRACE }); + break; + case '}': + tokens.push({ type: TokenType.RIGHT_BRACE }); + break; + case '[': + tokens.push({ type: TokenType.LEFT_BRACKET }); + break; + case ']': + tokens.push({ type: TokenType.RIGHT_BRACKET }); + break; + case ';': + tokens.push({ type: TokenType.SEMICOLON }); + break; + case ',': + tokens.push({ type: TokenType.COMMA }); + break; + case '.': + tokens.push({ type: TokenType.DOT }); + break; + case '@': + tokens.push({ type: TokenType.FUNCTION_REF }); + break; + case '_': + tokens.push({ type: TokenType.WILDCARD }); + break; + case '=': + tokens.push({ type: TokenType.EQUALS }); + break; + case '<': + tokens.push({ type: TokenType.LESS_THAN }); + break; + case '>': + tokens.push({ type: TokenType.GREATER_THAN }); + break; + default: + throw new Error(`Unexpected character: ${char}`); } - + current++; } - + return tokens; } -// Parser +// Parser - converts tokens to AST function parser(tokens) { let current = 0; - + function walk() { - if (current >= tokens.length) { - return null; // Return null when there are no more tokens + function parseChainedDotAccess(tableExpr) { + let result = tableExpr; + + while (current < tokens.length && tokens[current].type === TokenType.DOT) { + current++; // Skip the dot + + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const key = { + type: 'Identifier', + value: tokens[current].value + }; + current++; + + result = { + type: 'TableAccess', + table: result, + key: key + }; + } else { + throw new Error('Expected identifier after dot'); + } + } + + return result; } - - let token = tokens[current]; - - if (token.type === TokenType.NUMBER) { - current++; - return { - type: 'NumberLiteral', - value: token.value, - }; + + function parseChainedTableAccess(tableExpr) { + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + current++; // Skip '[' + const keyExpr = walk(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACKET) { + current++; // Skip ']' + + const access = { + type: 'TableAccess', + table: tableExpr, + key: keyExpr + }; + + // Check for chained access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + return parseChainedDotAccess(access); + } + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(access); + } + + return access; + } else { + throw new Error('Expected closing bracket'); + } + } + + // Check for dot access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + const result = parseChainedDotAccess(tableExpr); + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(result); + } + + return result; + } + + return tableExpr; } - - if (token.type === TokenType.PLUS) { - current++; - return { - type: 'PlusExpression', - left: walk(), - right: walk(), - }; + + function detectAmbiguousFunctionCalls() { + // This is a placeholder for future ambiguous function call detection + // For now, we'll assume the parser handles function calls correctly } - - if (token.type === TokenType.IDENTIFIER) { - current++; + + function parseFunctionCall(functionName) { + const args = []; + + // Parse arguments until we hit a semicolon or other terminator + while (current < tokens.length && + tokens[current].type !== TokenType.SEMICOLON && + tokens[current].type !== TokenType.RIGHT_PAREN && + tokens[current].type !== TokenType.RIGHT_BRACE && + tokens[current].type !== TokenType.COMMA) { + args.push(parseExpression()); + } + return { - type: 'Identifier', - value: token.value, + type: 'FunctionCall', + name: functionName, + args: args }; } - - if (token.type === TokenType.ASSIGNMENT) { - current++; - return { - type: 'AssignmentExpression', - name: tokens[current - 2].value, - value: walk(), - }; + + function parseExpression() { + 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 || + tokens[current].type === TokenType.AND || + tokens[current].type === TokenType.OR || + tokens[current].type === TokenType.XOR)) { + + const operator = tokens[current].type; + 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; + 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; } - - if (token.type === TokenType.IF) { - current++; - let node = { - type: 'IfExpression', - test: walk(), - consequent: walk(), - alternate: tokens[current].type === TokenType.ELSE ? (current++, walk()) : null, - }; - return node; + + function parseTerm() { + let left = parseFactor(); + + while (current < tokens.length && + (tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO)) { + + const operator = tokens[current].type; + current++; + const right = parseFactor(); + + switch (operator) { + case TokenType.MULTIPLY: + left = { type: 'MultiplyExpression', left, right }; + break; + case TokenType.DIVIDE: + left = { type: 'DivideExpression', left, right }; + break; + case TokenType.MODULO: + left = { type: 'ModuloExpression', left, right }; + break; + } + } + + return left; } + + function parseFactor() { + let left = parsePrimary(); + + while (current < tokens.length && tokens[current].type === TokenType.POWER) { + current++; + const right = parsePrimary(); + left = { type: 'PowerExpression', left, right }; + } + + return left; + } + + function parsePrimary() { + const token = tokens[current]; + + if (token.type === TokenType.NOT) { + current++; + const operand = parsePrimary(); + return { type: 'NotExpression', operand }; + } + + if (token.type === TokenType.NUMBER) { + current++; + return { + type: 'NumberLiteral', + value: token.value + }; + } + + if (token.type === TokenType.STRING) { + current++; + return { + type: 'StringLiteral', + value: token.value + }; + } + + if (token.type === TokenType.TRUE) { + current++; + return { + type: 'BooleanLiteral', + value: true + }; + } + + if (token.type === TokenType.FALSE) { + current++; + return { + type: 'BooleanLiteral', + value: false + }; + } + + if (token.type === TokenType.LEFT_PAREN) { + current++; // Skip '(' + const parenthesizedExpr = parseExpression(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_PAREN) { + current++; // Skip ')' + return parenthesizedExpr; + } else { + throw new Error('Expected closing parenthesis'); + } + } + + if (token.type === TokenType.IDENTIFIER) { + const identifier = { + type: 'Identifier', + value: token.value + }; + current++; + + // 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); + } + lookAhead++; + } + + if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.ARROW) { + isFunction = true; + } + + 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 + }; + } + } + + // Check if this is table access + if (current < tokens.length && + (tokens[current].type === TokenType.LEFT_BRACKET || + tokens[current].type === TokenType.DOT)) { + return parseChainedTableAccess(identifier); + } + + // 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); + } + + return identifier; + } + + 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.FUNCTION) { - current++; - let node = { - type: 'FunctionDeclaration', - name: tokens[current++].value, - params: [], - body: [], - }; - while (tokens[current].type !== TokenType.RIGHT_PAREN) { - node.params.push(tokens[current++].value); + if (token.type === TokenType.WILDCARD) { + current++; // Skip '_' + return { type: 'WildcardPattern' }; } - current++; // Skip right paren - while (tokens[current].type !== TokenType.RIGHT_BRACE) { - node.body.push(walk()); + + if (token.type === TokenType.CASE) { + current++; // Skip 'case' + + // Parse the value being matched + const value = parseExpression(); + + // Expect 'of' + if (tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); + } + current++; // Skip 'of' + + const cases = []; + + // Parse cases until we hit a semicolon or end + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + const pattern = parseExpression(); + + // 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 = parseExpression(); + cases.push({ + pattern: [pattern], + result: [result] + }); + } + + return { + type: 'CaseExpression', + value: [value], + cases, + }; } - current++; // Skip right brace - return node; - } + - if (token.type === TokenType.IDENTIFIER && tokens[current + 1].type === TokenType.LEFT_PAREN) { - current++; - let node = { - type: 'FunctionCall', - name: token.value, - args: [], - }; - current++; // Skip left paren - while (tokens[current].type !== TokenType.RIGHT_PAREN) { - node.args.push(walk()); + + // 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; + + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { + // Skip leading commas + if (tokens[current].type === TokenType.COMMA) { + current++; + continue; + } + + let key = null; + let value; + + // 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, + }; + current++; // Skip the key + } else { + throw new Error('Invalid key type in table literal'); + } + + current++; // Skip ':' + value = parseExpression(); + } else { + // This is just a value (array-like entry) + value = parseExpression(); + } + + entries.push({ key, value }); + + // Skip trailing commas + 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', + 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(); } - current++; // Skip right paren - return node; + + // If we get here, we have an unexpected token + throw new Error(`Unexpected token in parsePrimary: ${token.type}`); } - - if (token.type === TokenType.SEMICOLON) { + + // 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++; - return; + 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 }; } - - throw new TypeError(token.type); + + // Simple wrapper that calls parsePrimary for all token types + return parsePrimary(); } - - let ast = { + + const ast = { type: 'Program', - body: [], + body: [] }; - + while (current < tokens.length) { const node = walk(); - if (node !== null) { + if (node) { ast.body.push(node); } + + // Skip semicolons + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } } - + return ast; } // Interpreter function interpreter(ast) { - let globalScope = {}; - + const globalScope = {}; + initializeStandardLibrary(globalScope); + function evalNode(node) { + if (!node) { + return undefined; + } switch (node.type) { case 'NumberLiteral': - return parseInt(node.value); + 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; + } else { + // For other key types (numbers, strings), evaluate normally + key = evalNode(entry.key); + } + const value = evalNode(entry.value); + table[key] = value; + } + } + + return table; + case 'TableAccess': + const tableValue = evalNode(node.table); + 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': - globalScope[node.name] = evalNode(node.value); + 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': - return globalScope[node.value]; - case 'IfExpression': - return evalNode(node.test) ? evalNode(node.consequent) : node.alternate ? evalNode(node.alternate) : undefined; + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined) { + throw new Error(`Variable ${node.value} is not defined`); + } + return identifierValue; case 'FunctionDeclaration': - globalScope[node.name] = function() { + // 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]] = arguments[i]; + localScope[node.params[i]] = args[i]; } - let lastResult; - for (let bodyNode of node.body) { - lastResult = evalNode(bodyNode); - } - return lastResult; + return localEvalNodeWithScope(node.body, localScope); }; - return; case 'FunctionCall': - if (globalScope[node.name] instanceof Function) { + 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 globalScope[node.name].apply(null, args); + 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(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const outputValue = evalNode(node.value); + console.log(outputValue); + return outputValue; + case 'IOAssertExpression': + const assertionValue = evalNode(node.value); + if (!assertionValue) { + throw new Error('Assertion failed'); + } + return assertionValue; + case 'FunctionReference': + const functionValue = globalScope[node.name]; + if (functionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); } - 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 evalNode(ast.body[0]); + 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; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNodeWithScope(entry.key, scope); + } + 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 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 (matches) { + const results = caseItem.result.map(res => localEvalNodeWithScope(res, scope)); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNodeWithScope(node.value, scope); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + const localAssertionValue = localEvalNodeWithScope(node.value, scope); + if (!localAssertionValue) { + throw new Error('Assertion failed'); + } + return localAssertionValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + default: + throw new Error(`Unknown node type: ${node.type}`); + } + }; + + 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; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNode(entry.key); + } + 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 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 (matches) { + const results = caseItem.result.map(localEvalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNode(node.value); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + const localAssertionValue = localEvalNode(node.value); + if (!localAssertionValue) { + throw new Error('Assertion failed'); + } + return localAssertionValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + default: + throw new Error(`Unknown node type: ${node.type}`); + } + }; + + let lastResult; + for (let node of ast.body) { + if (node) { + lastResult = evalNode(node); + } + } + + if (lastResult instanceof Promise) { + return lastResult.then(result => { + return result; + }); + } + + return lastResult; } -// Usage -// const tokens = lexer('2 + 2'); -// const ast = parser(tokens); -// const result = interpreter(ast); -// console.log(result); // 4 +// Debug logging function +function debugLog(message, data = null) { + if (process.env.DEBUG) { + console.log(`[DEBUG] ${message}`); + if (data) { + console.log(data); + } + } +} -// const tokens2 = lexer('x : 2 + 2'); -// const ast2 = parser(tokens2); -// const result2 = interpreter(ast2); -// console.log(result2); +// Debug error function +function debugError(message, error = null) { + if (process.env.DEBUG) { + console.error(`[DEBUG ERROR] ${message}`); + if (error) { + console.error(error); + } + } +} -const fs = require('fs'); +// Execute a file +function executeFile(filePath) { + try { + const fs = require('fs'); + const input = fs.readFileSync(filePath, 'utf8'); + + debugLog('Input:', input); + + const tokens = lexer(input); + debugLog('Tokens:', tokens); + + const ast = parser(tokens); + debugLog('AST:', JSON.stringify(ast, null, 2)); + + const result = interpreter(ast); + + if (result instanceof Promise) { + result.then(finalResult => { + if (finalResult !== undefined) { + console.log(finalResult); + } + }).catch(error => { + console.error(`Error executing file: ${error.message}`); + }); + } else { + if (result !== undefined) { + console.log(result); + } + } + } catch (error) { + console.error(`Error executing file: ${error.message}`); + } +} -// Read the input from a file -const input = fs.readFileSync('input.txt', 'utf-8'); +// Check command line arguments +const args = process.argv.slice(2); -// Usage -const tokens = lexer(input); -const ast = parser(tokens); -const result = interpreter(ast); -console.log(result); \ No newline at end of file +if (args.length === 0) { + console.error('Usage: node lang.js <file>'); + console.error(' Provide a file path to execute'); + process.exit(1); +} else if (args.length === 1) { + // Execute the file + const filePath = args[0]; + executeFile(filePath); +} else { + // Too many arguments + console.error('Usage: node lang.js <file>'); + console.error(' Provide exactly one file path to execute'); + process.exit(1); +} \ No newline at end of file diff --git a/js/scripting-lang/run_tests.sh b/js/scripting-lang/run_tests.sh new file mode 100755 index 0000000..473f67a --- /dev/null +++ b/js/scripting-lang/run_tests.sh @@ -0,0 +1,108 @@ +#!/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... " + + if node lang.js "$test_file" > /dev/null 2>&1; then + echo -e "${GREEN}PASS${NC}" + return 0 + else + echo -e "${RED}FAIL${NC}" + 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" +) + +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" +) + +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/table_basic_test.txt b/js/scripting-lang/table_basic_test.txt new file mode 100644 index 0000000..172d95c --- /dev/null +++ b/js/scripting-lang/table_basic_test.txt @@ -0,0 +1,51 @@ +/* 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 new file mode 100644 index 0000000..268f271 --- /dev/null +++ b/js/scripting-lang/table_edge_cases_test.txt @@ -0,0 +1,304 @@ +/* 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/test.txt b/js/scripting-lang/test.txt new file mode 100644 index 0000000..79555b0 --- /dev/null +++ b/js/scripting-lang/test.txt @@ -0,0 +1,730 @@ +/* ======================================== + COMPREHENSIVE LANGUAGE TEST SUITE + ======================================== */ + +..out "=== COMPREHENSIVE LANGUAGE TEST SUITE ==="; +..out ""; + +/* ======================================== + SECTION 1: BASIC ARITHMETIC OPERATIONS + ======================================== */ + +..out "1. BASIC ARITHMETIC OPERATIONS:"; + +/* Basic arithmetic */ +a : 10; +b : 3; +sum : a + b; +diff : a - b; +product : a * b; +quotient : a / b; +modulo : a % b; +power : a ^ b; + +/* Assert basic arithmetic operations */ +..assert sum = 13; +..assert diff = 7; +..assert product = 30; +..assert quotient = 3.3333333333333335; +..assert modulo = 1; +..assert power = 1000; + +..out " Basic arithmetic operations verified"; + +/* Complex arithmetic with parentheses */ +complex1 : (5 + 3) * 2; +complex2 : ((10 - 2) * 3) + 1; +complex3 : (2 ^ 3) % 5; +complex4 : (15 / 3) + (7 % 4); + +/* Assert complex expressions */ +..assert complex1 = 16; +..assert complex2 = 25; +..assert complex3 = 3; +..assert complex4 = 8; + +..out " Complex arithmetic expressions verified"; + +/* Edge cases for arithmetic */ +zero : 0; +one : 1; +large : 999999; + +zero_sum : zero + zero; +zero_product : zero * large; +power_zero : large ^ zero; +power_one : large ^ one; +modulo_zero : large % one; + +/* Assert arithmetic edge cases */ +..assert zero_sum = 0; +..assert zero_product = 0; +..assert power_zero = 1; +..assert power_one = 999999; +..assert modulo_zero = 0; + +..out " Arithmetic edge cases verified"; + +..out ""; + +/* ======================================== + SECTION 2: COMPARISON OPERATORS + ======================================== */ + +..out "2. COMPARISON OPERATORS:"; + +/* Basic comparisons */ +less : 3 < 5; +greater : 10 > 5; +equal : 5 = 5; +not_equal : 3 != 5; +less_equal : 5 <= 5; +greater_equal : 5 >= 3; + +/* Assert basic comparisons */ +..assert less = true; +..assert greater = true; +..assert equal = true; +..assert not_equal = true; +..assert less_equal = true; +..assert greater_equal = true; + +..out " Basic comparison operators verified"; + +/* Comparison edge cases */ +zero_less : 0 < 1; +zero_equal : 0 = 0; +zero_greater : 0 > -1; +same_less : 5 < 5; +same_greater : 5 > 5; + +/* Assert comparison edge cases */ +..assert zero_less = true; +..assert zero_equal = true; +..assert zero_greater = true; +..assert same_less = false; +..assert same_greater = false; + +..out " Comparison edge cases verified"; + +..out ""; + +/* ======================================== + SECTION 3: LOGICAL OPERATORS + ======================================== */ + +..out "3. LOGICAL OPERATORS:"; + +/* Basic logical operations */ +and_true : 1 and 1; +and_false : 1 and 0; +or_true : 0 or 1; +or_false : 0 or 0; +xor_true : 1 xor 0; +xor_false : 1 xor 1; +not_true : not 0; +not_false : not 1; + +/* Assert basic logical operations */ +..assert and_true = true; +..assert and_false = false; +..assert or_true = true; +..assert or_false = false; +..assert xor_true = true; +..assert xor_false = false; +..assert not_true = true; +..assert not_false = false; + +..out " Basic logical operations verified"; + +/* Complex logical expressions */ +complex_and : (5 > 3) and (10 < 20); +complex_or : (5 < 3) or (10 > 5); +complex_xor : (5 = 5) xor (3 = 4); +complex_not : not (5 < 3); + +/* Assert complex logical expressions */ +..assert complex_and = true; +..assert complex_or = true; +..assert complex_xor = true; +..assert complex_not = true; + +..out " Complex logical expressions verified"; + +..out ""; + +/* ======================================== + SECTION 4: VARIABLE ASSIGNMENT + ======================================== */ + +..out "4. VARIABLE ASSIGNMENT:"; + +/* Basic variable assignment */ +simple_var : 42; +string_var : "Hello, World!"; +bool_true : true; +bool_false : false; + +/* Assert basic variables */ +..assert simple_var = 42; +..assert string_var = "Hello, World!"; +..assert bool_true = true; +..assert bool_false = false; + +..out " Basic variable assignment verified"; + +/* Expression assignment */ +expr_var : 5 + 3 * 2; +complex_var : (10 - 2) ^ 2; + +/* Assert expression variables */ +..assert expr_var = 11; +..assert complex_var = 64; + +..out " Expression variable assignment verified"; + +..out ""; + +/* ======================================== + SECTION 5: FUNCTION DEFINITIONS + ======================================== */ + +..out "5. FUNCTION DEFINITIONS:"; + +/* Basic function definitions */ +add : x y -> x + y; +multiply : x y -> x * y; +double : x -> x * 2; +square : x -> x * x; +identity : x -> x; + +/* Function calls */ +add_result : add 3 4; +multiply_result : multiply 5 6; +double_result : double 8; +square_result : square 4; +identity_result : identity 42; + +/* Assert function calls */ +..assert add_result = 7; +..assert multiply_result = 30; +..assert double_result = 16; +..assert square_result = 16; +..assert identity_result = 42; + +..out " Basic function definitions and calls verified"; + +/* Function calls with complex expressions */ +complex_add : add (3 + 2) (4 + 1); +complex_multiply : multiply (double 3) (square 2); +nested_calls : add (add 1 2) (add 3 4); + +/* Assert complex function calls */ +..assert complex_add = 15; +..assert complex_multiply = 48; +..assert nested_calls = 10; + +..out " Complex function calls verified"; + +..out ""; + +/* ======================================== + SECTION 6: PATTERN MATCHING + ======================================== */ + +..out "6. PATTERN MATCHING:"; + +/* Single parameter case expressions */ +factorial : n -> + case n of + 0 : 1 + _ : n * (factorial (n - 1)); + +grade : score -> + case score of + 90 : "A" + 80 : "B" + 70 : "C" + _ : "F"; + +/* Two parameter case expressions */ +compare : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : "neither zero"; + +/* Testing pattern matching */ +fact5 : factorial 5; +fact3 : factorial 3; +gradeA : grade 95; +gradeB : grade 85; +gradeF : grade 65; + +compare1 : compare 0 0; +compare2 : compare 0 5; +compare3 : compare 5 0; +compare4 : compare 5 5; + +/* Assert pattern matching results */ +..assert fact5 = 120; +..assert fact3 = 6; +..assert gradeA = "A"; +..assert gradeB = "B"; +..assert gradeF = "F"; + +..assert compare1 = "both zero"; +..assert compare2 = "x is zero"; +..assert compare3 = "y is zero"; +..assert compare4 = "neither zero"; + +..out " Pattern matching verified"; + +..out ""; + +/* ======================================== + SECTION 7: TABLES + ======================================== */ + +..out "7. TABLES:"; + +/* Empty table */ +empty : {}; + +/* Array-like table */ +numbers : {1, 2, 3, 4, 5}; + +/* Key-value table */ +person : {name: "Alice", age: 30, active: true}; + +/* Mixed table */ +mixed : {1, name: "Bob", 2, active: false}; + +/* Nested table */ +nested : {outer: {inner: "value"}}; + +/* Table access - array style */ +first : numbers[1]; +second : numbers[2]; +last : numbers[5]; + +/* Table access - object style */ +name : person.name; +age : person.age; +active : person.active; + +/* Table access - mixed style */ +mixed_first : mixed[1]; +mixed_name : mixed.name; +mixed_second : mixed[2]; + +/* Table access - nested */ +nested_value : nested.outer.inner; + +/* Assert table access */ +..assert first = 1; +..assert second = 2; +..assert last = 5; + +..assert name = "Alice"; +..assert age = 30; +..assert active = true; + +..assert mixed_first = 1; +..assert mixed_name = "Bob"; +..assert mixed_second = 2; + +..assert nested_value = "value"; + +..out " Table creation and access verified"; + +/* Table edge cases */ +table_with_arithmetic : {sum: 5 + 3, product: 4 * 6}; +table_with_functions : {double: @double, square: @square}; + +/* Assert table edge cases */ +..assert table_with_arithmetic.sum = 8; +..assert table_with_arithmetic.product = 24; + +..out " Table edge cases verified"; + +..out ""; + +/* ======================================== + SECTION 8: FIRST-CLASS FUNCTIONS + ======================================== */ + +..out "8. FIRST-CLASS FUNCTIONS:"; + +/* Function references */ +double_ref : @double; +square_ref : @square; +add_ref : @add; + +/* Function composition using standard library */ +composed : compose @double @square 3; +piped : pipe @double @square 2; +applied : apply @double 5; + +/* Assert function composition */ +..assert composed = 18; +..assert piped = 16; +..assert applied = 10; + +..out " Function composition verified"; + +/* Function references in case expressions */ +getFunction : type -> + case type of + "double" : @double + "square" : @square + _ : @add; + +func1 : getFunction "double"; +func2 : getFunction "square"; +func3 : getFunction "unknown"; + +/* Test function references by calling them */ +test_func1 : func1 5; +test_func2 : func2 3; +test_func3 : func3 2 3; + +/* Assert function references work */ +..assert test_func1 = 10; +..assert test_func2 = 9; +..assert test_func3 = 5; + +..out " Function references from case expressions verified"; + +..out ""; + +/* ======================================== + SECTION 9: STANDARD LIBRARY + ======================================== */ + +..out "9. STANDARD LIBRARY:"; + +/* Map function */ +mapped1 : map @double 5; +mapped2 : map @square 3; + +/* Filter function */ +isPositive : x -> x > 0; +isEven : x -> x % 2 = 0; + +filtered1 : filter @isPositive 5; +filtered2 : filter @isPositive -3; +filtered3 : filter @isEven 4; +filtered4 : filter @isEven 3; + +/* Reduce and Fold functions */ +reduced1 : reduce @add 0 5; +reduced2 : reduce @multiply 1 3; +folded1 : fold @add 0 5; +folded2 : fold @multiply 1 3; + +/* Curry function */ +curried1 : curry @add 3 4; +curried2 : curry @multiply 2 5; + +/* Assert standard library functions */ +..assert mapped1 = 10; +..assert mapped2 = 9; + +..assert filtered1 = 5; +..assert filtered2 = 0; +..assert filtered3 = 4; +..assert filtered4 = 0; + +..assert reduced1 = 5; +..assert reduced2 = 3; +..assert folded1 = 5; +..assert folded2 = 3; + +..assert curried1 = 7; +..assert curried2 = 10; + +..out " Standard library functions verified"; + +..out ""; + +/* ======================================== + SECTION 10: COMMENTS + ======================================== */ + +..out "10. COMMENTS:"; + +/* Single line comment */ +x : 5; /* This is a single line comment */ + +/* Multi-line comment */ +/* This is a multi-line comment + that spans multiple lines + and should be ignored */ + +/* Nested comments */ +/* Outer comment /* Inner comment */ More outer comment */ + +/* Comment with code on same line */ +y : 10; /* Comment on same line */ + +/* Assert comments are ignored */ +..assert x = 5; +..assert y = 10; + +..out " Comments work correctly - all ignored by parser"; + +..out ""; + +/* ======================================== + SECTION 11: EDGE CASES AND STRESS TESTS + ======================================== */ + +..out "11. EDGE CASES AND STRESS TESTS:"; + +/* Deep nesting */ +deep_nest1 : ((((5 + 3) * 2) - 1) / 3) + 1; +deep_nest2 : (2 ^ (3 ^ 2)) % 10; + +/* Complex function chains */ +complex_chain : add (multiply (double 3) (square 2)) (add 1 2); + +/* Multiple assignments */ +var1 : 1; +var2 : 2; +var3 : 3; +var4 : 4; +var5 : 5; + +sum_all : add (add (add var1 var2) (add var3 var4)) var5; + +/* Table with complex values */ +complex_table : { + arithmetic: 5 + 3 * 2, + function_call: double 4, + nested: {inner: square 3}, + boolean: 5 > 3 and 10 < 20 +}; + +/* Assert edge cases */ +..assert deep_nest1 = 6; +..assert deep_nest2 = 2; +..assert complex_chain = 27; +..assert sum_all = 15; + +..assert complex_table.arithmetic = 11; +..assert complex_table.function_call = 8; +..assert complex_table.nested.inner = 9; +..assert complex_table.boolean = true; + +..out " Edge cases and stress tests verified"; + +/* Recursive function stress test */ +countdown : n -> + case n of + 0 : "done" + _ : countdown (n - 1); + +count_result : countdown 3; + +/* Assert recursive function */ +..assert count_result = "done"; + +..out " Recursive function stress test verified"; + +..out ""; + +/* ======================================== + SECTION 12: ASSERTIONS + ======================================== */ + +..out "12. ASSERTIONS:"; + +/* Basic assertions */ +..assert 5 = 5; +..assert 3 < 5; +..assert 10 > 5; +..assert 5 <= 5; +..assert 5 >= 3; +..assert 3 != 5; + +/* Complex assertions */ +..assert (5 + 3) = 8; +..assert (10 - 2) > 5; +..assert (2 ^ 3) = 8; +..assert (15 / 3) = 5; + +/* Function call assertions */ +..assert (add 3 4) = 7; +..assert (double 5) = 10; +..assert (square 3) = 9; + +/* Logical assertions */ +..assert 1 and 1; +..assert 0 or 1; +..assert 1 xor 0; +..assert not 0; + +/* String assertions */ +..assert "hello" = "hello"; +..assert "world" != "hello"; + +/* Table assertions */ +..assert numbers[1] = 1; +..assert person.name = "Alice"; +..assert mixed[1] = 1; + +/* Function reference assertions */ +..assert (func1 4) = 8; +..assert (func2 5) = 25; + +..out " All assertions passed successfully!"; + +..out ""; + +/* ======================================== + SECTION 13: COMPREHENSIVE INTEGRATION TEST + ======================================== */ + +..out "13. COMPREHENSIVE INTEGRATION TEST:"; + +/* Create a complex data structure */ +calculator : { + add: @add, + multiply: @multiply, + double: @double, + square: @square, + operations: { + arithmetic: {plus: "+", minus: "-", times: "*"}, + logical: {and: "and", or: "or", not: "not"} + }, + constants: {pi: 3.14159, e: 2.71828} +}; + +/* Use the data structure */ +calc_add : calculator.add 5 3; +calc_mult : calculator.multiply 4 6; +calc_double : calculator.double 7; +calc_square : calculator.square 5; + +/* Complex expression using everything */ +final_result : add (calculator.double (calculator.square 3)) (calculator.multiply 2 4); + +/* Assert integration test results */ +..assert calc_add = 8; +..assert calc_mult = 24; +..assert calc_double = 14; +..assert calc_square = 25; +..assert final_result = 26; + +/* Assert nested table access */ +..assert calculator.operations.arithmetic.plus = "+"; +..assert calculator.operations.logical.and = "and"; +..assert calculator.constants.pi = 3.14159; + +..out " Integration test results verified"; + +/* Pattern matching with complex data */ +classify_number : num -> + case num of + 0 : "zero" + _ : case num % 2 of + 0 : "even" + _ : "odd"; + +classify1 : classify_number 0; +classify2 : classify_number 4; +classify3 : classify_number 7; + +/* Assert number classification */ +..assert classify1 = "zero"; +..assert classify2 = "even"; +..assert classify3 = "odd"; + +..out " Number classification verified"; + +..out ""; + +/* ======================================== + SECTION 14: ERROR HANDLING AND EDGE CASES + ======================================== */ + +..out "14. ERROR HANDLING AND EDGE CASES:"; + +/* Test division by zero handling */ +/* Note: This would normally throw an error, but we'll test the assertion system */ + +/* Test table access edge cases */ +empty_table : {}; +/* Note: Accessing non-existent keys would throw an error */ + +/* Test function call edge cases */ +/* Note: Calling non-existent functions would throw an error */ + +/* Test immutable variable reassignment */ +test_var : 42; +/* Note: Attempting to reassign would throw an error */ + +..out " Error handling edge cases noted"; + +..out ""; + +/* ======================================== + SECTION 15: PERFORMANCE AND SCALE TESTS + ======================================== */ + +..out "15. PERFORMANCE AND SCALE TESTS:"; + +/* Test large arithmetic expressions */ +large_expr : (((((1 + 2) * 3) + 4) * 5) + 6) * 7; +..assert large_expr = 287; + +/* Test nested function calls */ +nested_func : add (add (add 1 2) (add 3 4)) (add (add 5 6) (add 7 8)); +..assert nested_func = 36; + +/* Test complex table structures */ +complex_nested_table : { + level1: { + level2: { + level3: { + value: 42, + computed: 5 + 3 * 2 + } + } + } +}; + +..assert complex_nested_table.level1.level2.level3.value = 42; +..assert complex_nested_table.level1.level2.level3.computed = 11; + +..out " Performance and scale tests verified"; + +..out ""; + +/* ======================================== + FINAL SUMMARY + ======================================== */ + +..out "=== TEST SUITE COMPLETED SUCCESSFULLY ==="; +..out ""; +..out "All language features tested and verified:"; +..out " Arithmetic operations (+, -, *, /, %, ^)"; +..out " Comparison operators (=, <, >, <=, >=, !=)"; +..out " Logical operators (and, or, xor, not)"; +..out " Variable assignment"; +..out " Function definitions and calls"; +..out " Pattern matching with case expressions"; +..out " Tables (arrays and objects)"; +..out " First-class functions and composition"; +..out " Standard library functions"; +..out " Comments (single-line, multi-line, nested)"; +..out " Input/Output operations"; +..out " Assertions"; +..out " Edge cases and stress tests"; +..out " Complex integration scenarios"; +..out " Error handling edge cases"; +..out " Performance and scale tests"; +..out ""; +..out "Language implementation is fully functional and verified!"; \ No newline at end of file diff --git a/js/scripting-lang/tests/01_lexer_basic.txt b/js/scripting-lang/tests/01_lexer_basic.txt new file mode 100644 index 0000000..bdf7397 --- /dev/null +++ b/js/scripting-lang/tests/01_lexer_basic.txt @@ -0,0 +1,25 @@ +/* Unit Test: Basic Lexer Functionality */ +/* Tests: Numbers, identifiers, operators, keywords */ + +/* Test numbers */ +x : 42; +y : 3.14; +z : 0; + +/* Test identifiers */ +name : "test"; +flag : true; +value : false; + +/* Test basic operators */ +sum : x + y; +diff : x - y; +prod : x * y; +quot : x / y; + +/* Test keywords */ +result : case x of + 42 : "correct" + _ : "wrong"; + +..out "Lexer basic test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/02_arithmetic_operations.txt b/js/scripting-lang/tests/02_arithmetic_operations.txt new file mode 100644 index 0000000..9c6ab37 --- /dev/null +++ b/js/scripting-lang/tests/02_arithmetic_operations.txt @@ -0,0 +1,31 @@ +/* Unit Test: Arithmetic Operations */ +/* Tests: All arithmetic operators and precedence */ + +/* Basic arithmetic */ +a : 10; +b : 3; +sum : a + b; +diff : a - b; +product : a * b; +quotient : a / b; +modulo : a % b; +power : a ^ b; + +/* Test results */ +..assert sum = 13; +..assert diff = 7; +..assert product = 30; +..assert quotient = 3.3333333333333335; +..assert modulo = 1; +..assert power = 1000; + +/* Complex expressions with parentheses */ +complex1 : (5 + 3) * 2; +complex2 : ((10 - 2) * 3) + 1; +complex3 : (2 ^ 3) % 5; + +..assert complex1 = 16; +..assert complex2 = 25; +..assert complex3 = 3; + +..out "Arithmetic operations test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/03_comparison_operators.txt b/js/scripting-lang/tests/03_comparison_operators.txt new file mode 100644 index 0000000..f122a84 --- /dev/null +++ b/js/scripting-lang/tests/03_comparison_operators.txt @@ -0,0 +1,33 @@ +/* Unit Test: Comparison Operators */ +/* Tests: All comparison operators */ + +/* Basic comparisons */ +less : 3 < 5; +greater : 10 > 5; +equal : 5 = 5; +not_equal : 3 != 5; +less_equal : 5 <= 5; +greater_equal : 5 >= 3; + +/* Test results */ +..assert less = true; +..assert greater = true; +..assert equal = true; +..assert not_equal = true; +..assert less_equal = true; +..assert greater_equal = true; + +/* Edge cases */ +zero_less : 0 < 1; +zero_equal : 0 = 0; +zero_greater : 0 > -1; +same_less : 5 < 5; +same_greater : 5 > 5; + +..assert zero_less = true; +..assert zero_equal = true; +..assert zero_greater = true; +..assert same_less = false; +..assert same_greater = false; + +..out "Comparison operators test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/04_logical_operators.txt b/js/scripting-lang/tests/04_logical_operators.txt new file mode 100644 index 0000000..591e04b --- /dev/null +++ b/js/scripting-lang/tests/04_logical_operators.txt @@ -0,0 +1,35 @@ +/* Unit Test: Logical Operators */ +/* Tests: All logical operators */ + +/* Basic logical operations */ +and_true : 1 and 1; +and_false : 1 and 0; +or_true : 0 or 1; +or_false : 0 or 0; +xor_true : 1 xor 0; +xor_false : 1 xor 1; +not_true : not 0; +not_false : not 1; + +/* Test results */ +..assert and_true = true; +..assert and_false = false; +..assert or_true = true; +..assert or_false = false; +..assert xor_true = true; +..assert xor_false = false; +..assert not_true = true; +..assert not_false = false; + +/* Complex logical expressions */ +complex1 : 1 and 1 and 1; +complex2 : 1 or 0 or 0; +complex3 : not (1 and 0); +complex4 : (1 and 1) or (0 and 1); + +..assert complex1 = true; +..assert complex2 = true; +..assert complex3 = true; +..assert complex4 = true; + +..out "Logical operators test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/05_io_operations.txt b/js/scripting-lang/tests/05_io_operations.txt new file mode 100644 index 0000000..a16bf94 --- /dev/null +++ b/js/scripting-lang/tests/05_io_operations.txt @@ -0,0 +1,28 @@ +/* Unit Test: IO Operations */ +/* Tests: ..out, ..assert operations */ + +/* Test basic output */ +..out "Testing IO operations"; + +/* Test assertions */ +x : 5; +y : 3; +sum : x + y; + +..assert x = 5; +..assert y = 3; +..assert sum = 8; +..assert x > 3; +..assert y < 10; +..assert sum != 0; + +/* Test string comparisons */ +..assert "hello" = "hello"; +..assert "world" != "hello"; + +/* Test complex assertions */ +..assert (x + y) = 8; +..assert (x * y) = 15; +..assert (x > y) = true; + +..out "IO operations test completed"; \ No newline at end of file diff --git a/js/scripting-lang/tests/06_function_definitions.txt b/js/scripting-lang/tests/06_function_definitions.txt new file mode 100644 index 0000000..3bd3eee --- /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 = 15; +..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..d0ed2fe --- /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 = "A"; +..assert grade2 = "B"; +..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..222b005 --- /dev/null +++ b/js/scripting-lang/tests/10_standard_library.txt @@ -0,0 +1,47 @@ +/* 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; + +/* 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 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/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..d0c6682 --- /dev/null +++ b/js/scripting-lang/tests/integration_03_functional_programming.txt @@ -0,0 +1,67 @@ +/* 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; +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 = 11; + +/* 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/seed/README.md b/js/seed/README.md index 8159cb3..981ca7d 100644 --- a/js/seed/README.md +++ b/js/seed/README.md @@ -1,6 +1,6 @@ # Seed: Minimal FRP/TEA Web App Starter Kit -This is an opinionated, minimal starting point for browser-native web apps using a functional, Elm-style architecture (FRP/TEA) and only browser APIs. No frameworks, no build step, just ES modules. +This is an opinionated, hopefully simple starting point for browser-native web apps using a functional, Elm-style architecture (FRP/TEA) and only browser APIs. No rulers, no kings, no frameworks, no build step, only ES modules. ## Architecture - **state.js**: App state definition and helpers @@ -15,14 +15,9 @@ This is an opinionated, minimal starting point for browser-native web apps using - **View**: Pure function `(state) => html` - **Entrypoint**: Handles events, dispatches actions, triggers re-render -## Why? -- Simple, testable, and maintainable -- No dependencies -- Encourages functional, declarative code - ## How to Extend and Use This Template -This template is designed to be a flexible, opinionated starting point for any browser-native app. +This template is designed to be a flexible, opinionated starting point for any kinda app, especially proofs of concept, toys, and prototypes. ### Key Files to Extend - **src/state.js**: Define the app's state shape and any helper functions for cloning or initializing state. @@ -69,9 +64,9 @@ Suppose you want to add a button that increments a counter: ``` ### Tips -- Keep all state transitions in `update.js` for predictability. -- Keep all DOM rendering in `view.js` for clarity. -- Use the `postRender` hook for accessibility or focus management. +- Keep all state transitions in `update.js`. +- Keep all DOM rendering in `view.js`. +- Use the `postRender` hook for accessibility or focus management stuff. - Add new features by extending state, update, view, and wiring up events in `app.js`. --- diff --git a/js/seed/seed b/js/seed/seed new file mode 100755 index 0000000..15276e7 --- /dev/null +++ b/js/seed/seed @@ -0,0 +1,75 @@ +#!/bin/bash + +set -euo pipefail + +# Usage: seed plant + +if [ "$#" -ne 1 ] || [ "$1" != "plant" ]; then + echo "Usage: $0 plant" + exit 1 +fi + +if [ "$EUID" -eq 0 ]; then + echo "Do not run this script as root." + exit 1 +fi + +if ! command -v git >/dev/null 2>&1; then + echo "Warning: git is not installed. You won't be able to initialize a git repo." +fi + +SRC_DIR="$(cd "$(dirname "$0")" && pwd)" + +read -rp "Enter new project name: " DEST_DIR + +if [ -z "$DEST_DIR" ]; then + echo "Project name cannot be empty." + exit 1 +fi + +DEST_PATH="$PWD/$DEST_DIR" + +if [ -e "$DEST_PATH" ]; then + echo "Error: '$DEST_PATH' already exists." + exit 1 +fi + +cleanup() { + if [ -d "$DEST_PATH" ]; then + echo "Cleaning up partial directory..." + rm -rf "$DEST_PATH" + fi +} +trap cleanup INT TERM + +echo "Copying seed template to '$DEST_PATH'..." +mkdir "$DEST_PATH" + +if command -v rsync >/dev/null 2>&1; then + rsync -a --exclude='.git' --exclude='seed' "$SRC_DIR/" "$DEST_PATH/" +else + cp -r "$SRC_DIR/"* "$DEST_PATH/" + cp -r "$SRC_DIR/".* "$DEST_PATH/" 2>/dev/null || true + rm -rf "$DEST_PATH/.git" "$DEST_PATH/seed" +fi + +cd "$DEST_PATH" + +# Optionally, update README +if [ -f README.md ]; then + sed -i '' "1s/.*/# $DEST_DIR/" README.md 2>/dev/null || sed -i "1s/.*/# $DEST_DIR/" README.md +fi + +echo "Initialized new project in '$DEST_PATH'." + +read -rp "Do you want to initialize a git repository? (y/n): " INIT_GIT +if [[ "$INIT_GIT" =~ ^[Yy]$ ]]; then + git init + echo "Git repository initialized." +else + echo "Skipping git initialization." +fi + +echo "Next steps:" +echo " cd \"$DEST_PATH\"" +echo " and tend to the seed you've planted..." \ No newline at end of file diff --git a/js/seed/src/app.js b/js/seed/src/app.js index 34b4579..49ad9d1 100644 --- a/js/seed/src/app.js +++ b/js/seed/src/app.js @@ -155,11 +155,3 @@ function setHistoryPointer(idx) { updateHistoryInfo(); } } - -function handleSliderChange(e) { - setHistoryPointer(Number(e.target.value)); -} - -function handleStepperChange(e) { - setHistoryPointer(Number(e.target.value)); -} \ No newline at end of file diff --git a/js/seed/src/dev.js b/js/seed/src/dev.js index ee1a6e7..173fc3c 100644 --- a/js/seed/src/dev.js +++ b/js/seed/src/dev.js @@ -1,8 +1,8 @@ // devMode.js -// Minimal, single-file dev mode with scriptable console API +// Simpleish, single-file dev mode with interactive console API /** - * Initialize dev mode: exposes a scriptable API for stepping through state history. + * Initialize dev mode: exposes an API for stepping through state history. * @param {object} opts * @param {function} opts.getState - returns current app state * @param {function} opts.setState - sets app state diff --git a/js/seed/src/view.js b/js/seed/src/view.js index 5feef6e..4c6e680 100644 --- a/js/seed/src/view.js +++ b/js/seed/src/view.js @@ -4,10 +4,10 @@ /** * Pure view functions for the application. * - * Why pure functions returning HTML strings? + * Why pure functions returning HTML strings? Because Elm does it, tbh. * - Keeps rendering logic stateless and easy to test. - * - Ensures the UI is always a direct function of state, avoiding UI bugs from incremental DOM updates. - * - Using template literals is minimal and browser-native, with no dependencies. + * - Ensures the UI is always a direct function of state, which should in theory totally avoid bugs from incremental DOM updates. + * - Using template literals is minimal and browser-native, with no dependencies, and is fun. * * Why escape output? * - Prevents XSS and ensures all user/content data is safely rendered. |