diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/scripting-lang/README.md | 414 | ||||
-rw-r--r-- | js/scripting-lang/comprehensive_test.txt | 153 | ||||
-rw-r--r-- | js/scripting-lang/grammar.ebnf | 76 | ||||
-rw-r--r-- | js/scripting-lang/grammar_detailed.ebnf | 97 | ||||
-rw-r--r-- | js/scripting-lang/input.txt | 3 | ||||
-rw-r--r-- | js/scripting-lang/lang.js | 2160 | ||||
-rw-r--r-- | js/scripting-lang/parser_analysis.md | 143 | ||||
-rw-r--r-- | js/scripting-lang/test_ambiguous_cases.txt | 23 | ||||
-rw-r--r-- | js/seed/README.md | 15 | ||||
-rwxr-xr-x | js/seed/seed | 75 | ||||
-rw-r--r-- | js/seed/src/app.js | 8 | ||||
-rw-r--r-- | js/seed/src/dev.js | 4 | ||||
-rw-r--r-- | js/seed/src/view.js | 6 |
13 files changed, 3107 insertions, 70 deletions
diff --git a/js/scripting-lang/README.md b/js/scripting-lang/README.md new file mode 100644 index 0000000..d662fcc --- /dev/null +++ b/js/scripting-lang/README.md @@ -0,0 +1,414 @@ +# 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 + +## 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. + +### 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 +``` + +### 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 + +``` +x : 5; +y : 10; +sum : x + y; +sum; // Returns 15 +``` + +### Function Definition and Call + +``` +add : x y -> x + y; +result : add 3 4; +result; // Returns 7 +``` + +### Pattern Matching + +``` +compare : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : "neither zero"; + +test1 : compare 0 0; // Returns "both zero" +test2 : compare 0 5; // Returns "x is zero" +test3 : compare 5 0; // Returns "y is zero" +test4 : compare 5 5; // Returns "neither zero" +``` + +### Function Composition + +``` +double : x -> x * 2; +square : x -> x * x; +compose : f g x -> f g x; +apply : f x -> f x; + +composed : compose @double @square 3; // double(square(3)) = 18 +applied : apply @double 5; // double(5) = 10 +``` + +### Recursion + +The language supports recursive function calls in function body case expressions: + +``` +countdown : n -> + case n of + 0 : 0 + _ : countdown n - 1; + +result : countdown 3; // Returns 0 + +// Note: Complex arithmetic in recursive functions may require careful expression design +// due to the language's lack of explicit grouping (parentheses) +``` + +### Input/Output Example + +``` +name : ..in; // Prompts for input +..out "Hello, "; // Outputs greeting +..out name; // Outputs the input +``` + +### First-Class Functions Example + +``` +double : x -> x * 2; +square : x -> x * x; +compose : f g x -> f g x; + +// Function composition +composed : compose @double @square 3; // 18 + +// Higher-order functions +map : f x -> f x; +mapped : map @double 5; // 10 + +// Function references in standalone case expressions +case 0 of + 0 : @double + _ : @square; +``` + +### Assertion Example + +``` +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 x = 0; // Fails with error +``` + +## Running Programs + +The language supports two modes of execution: + +### File Execution Mode +Run a script file by providing the file path as an argument: + +```bash +node lang.js script.txt +``` + +### REPL Mode +Start an interactive REPL by running without arguments: + +```bash +node lang.js +``` + +In REPL mode: +- Type expressions and end them with a semicolon (`;`) to execute +- Multi-line expressions are supported +- Type `exit` or `quit` to exit +- Variables and functions persist between expressions + +**Example REPL session:** +``` +Scripting Language REPL +Type expressions to evaluate. End with semicolon (;) to execute. +Type "exit" to quit. + +> x : 5; +> y : 10; +> + x y; +15 +> exit +Goodbye! +``` + +## Language Design Principles + +- **Simplicity**: Minimal syntax for maximum clarity +- **Immutability**: Variables cannot be reassigned +- **Functional**: Functions are first-class values with composition support +- **Pattern-oriented**: Pattern matching as a core feature +- **Recursive**: 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) +- No data structures beyond numbers and strings +- No error handling beyond basic validation +- No modules or imports +- No standard library +- IO operations are synchronous and block execution + +## Future Enhancements + +Planned features for future versions: +- Ambiguous function call detection and error reporting +- Lists and data structures +- Error handling +- Standard library functions +- Modules and imports +- Type system +- Performance optimizations + +## Implementation Details + +The language is implemented in JavaScript with three main components: + +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. \ No newline at end of file diff --git a/js/scripting-lang/comprehensive_test.txt b/js/scripting-lang/comprehensive_test.txt new file mode 100644 index 0000000..1b4aae8 --- /dev/null +++ b/js/scripting-lang/comprehensive_test.txt @@ -0,0 +1,153 @@ +x : 5; +y : 3; +name : "Alice"; +age : 25; + +..out "Basic arithmetic:"; +sum : x + y; +diff : x - y; +product : x * y; +quotient : x / y; +modulo : 17 % 5; +power : 2 ^ 3; +..out sum; +..out diff; +..out product; +..out quotient; +..out modulo; +..out power; + +..out "Variables:"; +..out name; +..out age; + +add : x y -> x + y; +double : x -> x * 2; +square : x -> x * x; + +..out "Functions:"; +result1 : add 3 4; +result2 : double 5; +result3 : square 4; +..out result1; +..out result2; +..out result3; + +..out "Function calls with parentheses:"; +result4 : add (3 + 2) (4 + 1); +result5 : add (double 3) (square 2); +..out result4; +..out result5; + +..out "Testing assertions:"; +..assert x = 5; +..assert y = 3; +..assert sum = 8; +..assert "hello" = "hello"; +..assert (add 3 4) = 7; + +..out "Testing first-class functions:"; + +composed : compose @double @square 3; +applied : apply @double 5; + +..assert composed = 18; +..assert applied = 10; + +..out "First-class functions:"; +..out composed; +..out applied; + +..out "Testing standard library functions:"; +slResult1 : map @double 5; +slResult2 : map @square 3; +piped : pipe @double @square 2; +isPositive : x -> x > 0; +filtered : filter @isPositive 5; +reduced : reduce @add 0 5; +folded : fold @add 0 5; + +..assert (map @double 5) = 10; +..assert (map @square 3) = 9; +..assert (pipe @double @square 2) = 16; +..assert (filter @isPositive 5) = 5; +..assert (reduce @add 0 5) = 5; +..assert (fold @add 0 5) = 5; + +..out "Standard library results:"; +..out slResult1; +..out slResult2; +..out piped; +..out filtered; +..out reduced; +..out folded; + +..out "Testing higher-order functions:"; +mapped1 : map @double 3; +mapped2 : map @square 4; + +..assert mapped1 = 6; +..assert mapped2 = 16; + +..out "Map results:"; +..out mapped1; +..out mapped2; + +..out "Testing case expressions:"; +compare : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : "neither zero"; + +test1 : compare 0 0; +..out test1; + + + +..out "Testing standalone case expressions with function references:"; +case 0 of + 0 : @double + _ : @square; + +..out "Standalone case with function references works!"; + +..out "Testing parentheses grouping:"; +grouped1 : (5 + 3) * 2; +grouped2 : ((5 + 3) * 2) + 1; + +..assert grouped1 = 16; +..assert grouped2 = 17; + +..out "Parentheses grouping works!"; + +..out "Testing comparison operators:"; +lessTest : 3 < 5; +greaterTest : 10 > 5; +equalTest : 5 = 5; +notEqualTest : 3 != 5; +lessEqualTest : 5 <= 5; +greaterEqualTest : 5 >= 3; +..out lessTest; +..out greaterTest; +..out equalTest; +..out notEqualTest; +..out lessEqualTest; +..out greaterEqualTest; + +..out "Testing logical operators:"; +logicalAnd : 1 and 1; +logicalOr : 0 or 1; +logicalXor : 1 xor 0; +logicalNot : not 0; +..out logicalAnd; +..out logicalOr; +..out logicalXor; +..out logicalNot; + +..out "Testing complex expressions:"; +complex1 : 2 ^ 3 % 3 and 5 > 3; +..out complex1; + +..out "All tests completed successfully!"; \ No newline at end of file diff --git a/js/scripting-lang/grammar.ebnf b/js/scripting-lang/grammar.ebnf new file mode 100644 index 0000000..b595c87 --- /dev/null +++ b/js/scripting-lang/grammar.ebnf @@ -0,0 +1,76 @@ +(* Scripting Language Grammar *) + +(* Program Structure *) +program = statement_list +statement_list = statement { statement } +statement = assignment | function_declaration | expression | io_statement | ";" + +(* Variables and Assignment *) +assignment = identifier ":" value +value = expression | function_call + +(* Function Declarations *) +function_declaration = identifier ":" parameter_list "->" function_body +parameter_list = identifier { identifier } +function_body = expression | case_expression + +(* Expressions *) +expression = arithmetic_expression | identifier | number | string + +(* Arithmetic Expressions *) +arithmetic_expression = arithmetic_operator expression expression +arithmetic_operator = "+" | "-" | "*" | "/" + +(* Function Calls *) +function_call = identifier argument_list +argument_list = expression { expression } + +(* Case Expressions *) +case_expression = "case" value_list "of" case_list +value_list = expression { expression } +case_list = case_branch { case_branch } +case_branch = pattern ":" result +pattern = pattern_element { pattern_element } +pattern_element = number | wildcard | identifier +result = result_element { result_element } +result_element = string | number | identifier + +(* IO Statements *) +io_statement = io_operator [ expression ] +io_operator = "..in" | "..out" + +(* Terminals *) +identifier = letter { letter | digit } +number = digit { digit } +string = '"' { character } '"' +wildcard = "_" +letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" +digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +character = letter | digit | " " | "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | "=" | ">" | "?" | "@" | "[" | "\" | "]" | "^" | "`" | "{" | "|" | "}" | "~" + +(* Comments *) +(* This language does not support comments *) + +(* Examples *) + +(* Variable assignment *) +(* x : 5; *) + +(* Function declaration *) +(* add : x y -> x + y; *) + +(* Function call *) +(* result : add 3 4; *) + +(* Case expression *) +(* compare : x y -> + case x y of + 0 0 : "both zero" + 0 _ : "x is zero" + _ 0 : "y is zero" + _ _ : "neither zero"; *) + +(* IO statements *) +(* name : ..in; + ..out "Hello, "; + ..out name; *) \ No newline at end of file diff --git a/js/scripting-lang/grammar_detailed.ebnf b/js/scripting-lang/grammar_detailed.ebnf new file mode 100644 index 0000000..5274cd8 --- /dev/null +++ b/js/scripting-lang/grammar_detailed.ebnf @@ -0,0 +1,97 @@ +(* Detailed Scripting Language Grammar *) + +(* Program Structure *) +program = statement_list +statement_list = statement { statement } +statement = assignment | function_declaration | expression_statement | io_statement | ";" + +(* Variables and Assignment *) +assignment = identifier ":" assignment_value +assignment_value = function_call | simple_expression + +(* Function Declarations *) +function_declaration = identifier ":" parameter_list "->" function_body +parameter_list = identifier { identifier } +function_body = arithmetic_expression | case_expression + +(* Function Calls *) +function_call = identifier argument_list +argument_list = expression { expression } + +(* Expressions *) +expression = arithmetic_expression | simple_expression +simple_expression = identifier | number | string +expression_statement = expression + +(* Arithmetic Expressions - Prefix Notation *) +arithmetic_expression = arithmetic_operator expression expression +arithmetic_operator = "+" | "-" | "*" | "/" + +(* Case Expressions *) +case_expression = "case" value_list "of" case_list +value_list = expression { expression } +case_list = case_branch { case_branch } +case_branch = pattern ":" result +pattern = pattern_element { pattern_element } +pattern_element = number | wildcard | identifier +result = result_element { result_element } +result_element = string | number | identifier + +(* IO Statements *) +io_statement = io_operator [ expression ] +io_operator = "..in" | "..out" + +(* Terminals *) +identifier = letter { letter | digit } +number = digit { digit } +string = '"' { character } '"' +wildcard = "_" +letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" +digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +character = letter | digit | " " | "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | "=" | ">" | "?" | "@" | "[" | "\" | "]" | "^" | "`" | "{" | "|" | "}" | "~" + +(* Parsing Precedence and Disambiguation *) + +(* Key Insights from Debug Analysis: *) + +(* 1. Assignment parsing needs to distinguish between function calls and simple values *) +(* assignment_value = function_call | simple_expression *) + +(* 2. Function calls in assignments: "result : add 3 4;" *) +(* - "add" is an identifier *) +(* - "3" and "4" are arguments *) +(* - Should parse as: assignment(identifier("result"), function_call("add", [3, 4])) *) + +(* 3. Case expressions work correctly but function calls within them don't *) +(* - Case patterns: "0 0", "0 _", "_ 0", "_ _" *) +(* - Case results: string literals, numbers, identifiers *) + +(* 4. Arithmetic expressions use prefix notation *) +(* - "+ x y" not "x + y" *) +(* - "* 5 2" not "5 * 2" *) + +(* 5. IO statements are special *) +(* - "..in" reads from stdin *) +(* - "..out expression" writes to stdout *) + +(* Examples with Parse Trees *) + +(* Example 1: Simple assignment *) +(* Input: "x : 5;" *) +(* Parse: assignment(identifier("x"), number(5)) *) + +(* Example 2: Function call assignment *) +(* Input: "result : add 3 4;" *) +(* Parse: assignment(identifier("result"), function_call("add", [number(3), number(4)])) *) + +(* Example 3: Case expression *) +(* Input: "compare : x y -> case x y of 0 0 : \"both zero\";" *) +(* Parse: function_declaration("compare", ["x", "y"], case_expression([identifier("x"), identifier("y")], [case_branch([number(0), number(0)], [string("both zero")])])) *) + +(* Example 4: Arithmetic expression *) +(* Input: "sum : + x y;" *) +(* Parse: assignment(identifier("sum"), arithmetic_expression("+", identifier("x"), identifier("y"))) *) + +(* Example 5: IO statement *) +(* Input: "..out \"Hello\";" *) +(* Parse: io_statement("..out", string("Hello")) *) \ No newline at end of file diff --git a/js/scripting-lang/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..b8b6fed 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,21 +1,129 @@ // 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', SEMICOLON: 'SEMICOLON', + IO_IN: 'IO_IN', + IO_OUT: 'IO_OUT', + IO_ASSERT: 'IO_ASSERT', + EQUALS: 'EQUALS', + FUNCTION_REF: 'FUNCTION_REF', + STRING: 'STRING', + // New arithmetic operators + MODULO: 'MODULO', + POWER: 'POWER', + // New comparison operators + LESS_THAN: 'LESS_THAN', + GREATER_THAN: 'GREATER_THAN', + LESS_EQUAL: 'LESS_EQUAL', + GREATER_EQUAL: 'GREATER_EQUAL', + NOT_EQUAL: 'NOT_EQUAL', + // New logical operators + AND: 'AND', + OR: 'OR', + XOR: 'XOR', + NOT: 'NOT', }; // Lexer function lexer(input) { + debugLog('Starting lexer with input', input); const tokens = []; let current = 0; @@ -43,9 +151,145 @@ function lexer(input) { continue; } + if (input.slice(current, current + 2) === '->') { + tokens.push({ + type: TokenType.ARROW + }); + current += 2; + continue; + } + + if (char === '-') { + tokens.push({ + type: TokenType.MINUS + }); + current++; + continue; + } + + if (char === '*') { + tokens.push({ + type: TokenType.MULTIPLY + }); + current++; + continue; + } + + if (char === '/') { + tokens.push({ + type: TokenType.DIVIDE + }); + current++; + continue; + } + + if (char === '%') { + tokens.push({ + type: TokenType.MODULO + }); + current++; + continue; + } + + if (char === '^') { + tokens.push({ + type: TokenType.POWER + }); + current++; + continue; + } + + if (char === '<') { + if (input.slice(current, current + 2) === '<=') { + tokens.push({ + type: TokenType.LESS_EQUAL + }); + current += 2; + } else { + tokens.push({ + type: TokenType.LESS_THAN + }); + current++; + } + continue; + } + + if (char === '>') { + if (input.slice(current, current + 2) === '>=') { + tokens.push({ + type: TokenType.GREATER_EQUAL + }); + current += 2; + } else { + tokens.push({ + type: TokenType.GREATER_THAN + }); + current++; + } + continue; + } + + if (input.slice(current, current + 2) === '!=') { + tokens.push({ + type: TokenType.NOT_EQUAL + }); + current += 2; + continue; + } + + // Check for keywords before general identifiers + if (input.slice(current, current + 4) === 'case') { + tokens.push({ + type: TokenType.CASE + }); + current += 4; + continue; + } + + if (input.slice(current, current + 2) === 'of') { + tokens.push({ + type: TokenType.OF + }); + current += 2; + continue; + } + + // Check for logical keywords + if (input.slice(current, current + 3) === 'and') { + tokens.push({ + type: TokenType.AND + }); + current += 3; + continue; + } + + if (input.slice(current, current + 2) === 'or') { + tokens.push({ + type: TokenType.OR + }); + current += 2; + continue; + } + + if (input.slice(current, current + 3) === 'xor') { + tokens.push({ + type: TokenType.XOR + }); + current += 3; + continue; + } + + if (input.slice(current, current + 3) === 'not') { + tokens.push({ + type: TokenType.NOT + }); + current += 3; + continue; + } + if (/[a-z]/i.test(char)) { let value = ''; - while (/[a-z]/i.test(char)) { + while (/[a-z0-9]/i.test(char)) { value += char; char = input[++current]; } @@ -64,27 +308,37 @@ function lexer(input) { continue; } - if (char === '=') { + if (input.slice(current, current + 2) === '==') { + tokens.push({ + type: TokenType.EQUALS + }); + current += 2; + continue; + } + + if (char === '@') { tokens.push({ - type: TokenType.EQUAL + type: TokenType.FUNCTION_REF }); current++; continue; } - if (input.slice(current, current + 2) === 'if') { + if (char === '=') { tokens.push({ - type: TokenType.IF + type: TokenType.EQUALS }); - current += 2; + current++; continue; } - if (input.slice(current, current + 4) === 'else') { + + + if (char === '_') { tokens.push({ - type: TokenType.ELSE + type: TokenType.WILDCARD }); - current += 4; + current++; continue; } @@ -128,12 +382,70 @@ function lexer(input) { continue; } + // Check for IO tokens + if (input.slice(current, current + 4) === '..in') { + tokens.push({ + type: TokenType.IO_IN + }); + current += 4; + continue; + } + + if (input.slice(current, current + 5) === '..out') { + tokens.push({ + type: TokenType.IO_OUT + }); + current += 5; + continue; + } + + if (input.slice(current, current + 8) === '..assert') { + tokens.push({ + type: TokenType.IO_ASSERT + }); + current += 8; + continue; + } + + // Handle string literals + if (char === '"') { + let value = ''; + current++; // Skip opening quote + while (current < input.length && input[current] !== '"') { + value += input[current]; + current++; + } + if (current < input.length && input[current] === '"') { + current++; // Skip closing quote + tokens.push({ + type: TokenType.STRING, + value + }); + continue; + } else { + throw new Error('Unterminated string literal'); + } + } + if (char === ';') { tokens.push({ type: TokenType.SEMICOLON }); current++; continue; } + if (/[a-z]/i.test(char)) { + let value = ''; + while (/[a-z0-9]/i.test(char)) { + value += char; + char = input[++current]; + } + tokens.push({ + type: TokenType.IDENTIFIER, + value + }); + continue; + } + current++; } @@ -142,6 +454,10 @@ function lexer(input) { // Parser function parser(tokens) { + debugLog('Starting parser with tokens', tokens); + + + let current = 0; function walk() { @@ -151,6 +467,244 @@ function parser(tokens) { let token = tokens[current]; + // Helper function to detect ambiguous nested function calls + function detectAmbiguousFunctionCalls() { + // Look ahead to see if we have a pattern like: func1 func2 arg1 func3 arg2 + // This indicates ambiguous nested function calls + let tempCurrent = current; + let functionCalls = []; + + while (tempCurrent < tokens.length && tokens[tempCurrent].type !== TokenType.SEMICOLON) { + if (tokens[tempCurrent].type === TokenType.IDENTIFIER) { + // Check if this identifier is followed by arguments (function call) + if (tempCurrent + 1 < tokens.length && + tokens[tempCurrent + 1].type !== TokenType.ASSIGNMENT && + tokens[tempCurrent + 1].type !== TokenType.SEMICOLON && + (tokens[tempCurrent + 1].type === TokenType.NUMBER || + tokens[tempCurrent + 1].type === TokenType.IDENTIFIER || + tokens[tempCurrent + 1].type === TokenType.LEFT_PAREN)) { + + functionCalls.push(tokens[tempCurrent].value); + + // Skip the function name and its arguments + tempCurrent++; // Skip function name + while (tempCurrent < tokens.length && + tokens[tempCurrent].type !== TokenType.SEMICOLON && + tokens[tempCurrent].type !== TokenType.RIGHT_PAREN) { + tempCurrent++; + } + } else { + tempCurrent++; + } + } else { + tempCurrent++; + } + } + + // If we have more than 2 function calls in sequence, it's ambiguous + if (functionCalls.length > 2) { + throw new Error(`Ambiguous nested function calls detected: ${functionCalls.join(' ')}. Use parentheses to explicitly group function calls.`); + } + } + + // Helper function to parse function calls with proper precedence + function parseFunctionCall() { + if (token.type !== TokenType.IDENTIFIER) { + return null; + } + + // Check if this is a function call (identifier followed by arguments) + if (tokens[current + 1] && + tokens[current + 1].type !== TokenType.ASSIGNMENT && + tokens[current + 1].type !== TokenType.PLUS && + tokens[current + 1].type !== TokenType.MINUS && + tokens[current + 1].type !== TokenType.MULTIPLY && + tokens[current + 1].type !== TokenType.DIVIDE && + tokens[current + 1].type !== TokenType.MODULO && + tokens[current + 1].type !== TokenType.POWER && + tokens[current + 1].type !== TokenType.EQUALS && + tokens[current + 1].type !== TokenType.LESS_THAN && + tokens[current + 1].type !== TokenType.GREATER_THAN && + tokens[current + 1].type !== TokenType.LESS_EQUAL && + tokens[current + 1].type !== TokenType.GREATER_EQUAL && + tokens[current + 1].type !== TokenType.NOT_EQUAL && + tokens[current + 1].type !== TokenType.AND && + tokens[current + 1].type !== TokenType.OR && + tokens[current + 1].type !== TokenType.XOR && + tokens[current + 1].type !== TokenType.NOT && + tokens[current + 1].type !== TokenType.SEMICOLON && + (tokens[current + 1].type === TokenType.NUMBER || + tokens[current + 1].type === TokenType.IDENTIFIER || + tokens[current + 1].type === TokenType.LEFT_PAREN || + tokens[current + 1].type === TokenType.FUNCTION_REF)) { + + + + const funcName = token.value; + current++; // Skip function name + const args = []; + + // Collect arguments until we hit a semicolon, closing parenthesis, or end + while (current < tokens.length && + tokens[current].type !== TokenType.SEMICOLON && + tokens[current].type !== TokenType.RIGHT_PAREN) { + + // Handle function references (@functionName) + if (tokens[current] && tokens[current].type === TokenType.FUNCTION_REF) { + current++; // Skip @ + if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { + args.push({ + type: 'FunctionReference', + name: tokens[current].value, + }); + current++; + } else { + throw new Error('Expected function name after @'); + } + } else { + // Parse arguments with proper precedence + // For nested function calls, we need to parse them as complete function calls + if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER && + tokens[current + 1] && tokens[current + 1].type !== TokenType.ASSIGNMENT && + tokens[current + 1].type !== TokenType.PLUS && + tokens[current + 1].type !== TokenType.MINUS && + tokens[current + 1].type !== TokenType.MULTIPLY && + tokens[current + 1].type !== TokenType.DIVIDE && + tokens[current + 1].type !== TokenType.MODULO && + tokens[current + 1].type !== TokenType.POWER && + tokens[current + 1].type !== TokenType.EQUALS && + tokens[current + 1].type !== TokenType.LESS_THAN && + tokens[current + 1].type !== TokenType.GREATER_THAN && + tokens[current + 1].type !== TokenType.LESS_EQUAL && + tokens[current + 1].type !== TokenType.GREATER_EQUAL && + tokens[current + 1].type !== TokenType.NOT_EQUAL && + tokens[current + 1].type !== TokenType.AND && + tokens[current + 1].type !== TokenType.OR && + tokens[current + 1].type !== TokenType.XOR && + tokens[current + 1].type !== TokenType.NOT && + tokens[current + 1].type !== TokenType.SEMICOLON && + (tokens[current + 1].type === TokenType.NUMBER || + tokens[current + 1].type === TokenType.IDENTIFIER || + tokens[current + 1].type === TokenType.LEFT_PAREN)) { + + // Check for ambiguous nested function calls + // Look for pattern: func1 func2 arg1 func3 arg2 + let tempCurrent = current; + let functionNames = []; + + while (tempCurrent < tokens.length && + tokens[tempCurrent].type !== TokenType.SEMICOLON && + tokens[tempCurrent].type !== TokenType.RIGHT_PAREN) { + if (tokens[tempCurrent].type === TokenType.IDENTIFIER && + tempCurrent + 1 < tokens.length && + tokens[tempCurrent + 1].type !== TokenType.ASSIGNMENT && + tokens[tempCurrent + 1].type !== TokenType.PLUS && + tokens[tempCurrent + 1].type !== TokenType.MINUS && + tokens[tempCurrent + 1].type !== TokenType.MULTIPLY && + tokens[tempCurrent + 1].type !== TokenType.DIVIDE && + tokens[tempCurrent + 1].type !== TokenType.MODULO && + tokens[tempCurrent + 1].type !== TokenType.POWER && + tokens[tempCurrent + 1].type !== TokenType.EQUALS && + tokens[tempCurrent + 1].type !== TokenType.LESS_THAN && + tokens[tempCurrent + 1].type !== TokenType.GREATER_THAN && + tokens[tempCurrent + 1].type !== TokenType.LESS_EQUAL && + tokens[tempCurrent + 1].type !== TokenType.GREATER_EQUAL && + tokens[tempCurrent + 1].type !== TokenType.NOT_EQUAL && + tokens[tempCurrent + 1].type !== TokenType.AND && + tokens[tempCurrent + 1].type !== TokenType.OR && + tokens[tempCurrent + 1].type !== TokenType.XOR && + tokens[tempCurrent + 1].type !== TokenType.NOT && + tokens[tempCurrent + 1].type !== TokenType.SEMICOLON && + (tokens[tempCurrent + 1].type === TokenType.NUMBER || + tokens[tempCurrent + 1].type === TokenType.IDENTIFIER || + tokens[tempCurrent + 1].type === TokenType.LEFT_PAREN)) { + functionNames.push(tokens[tempCurrent].value); + } + tempCurrent++; + } + + if (functionNames.length > 2) { + throw new Error(`Ambiguous nested function calls detected: ${functionNames.join(' ')}. Use parentheses to explicitly group function calls.`); + } + + // This is a nested function call, parse it as a complete function call + const nestedFuncName = tokens[current].value; + current++; // Skip function name + const nestedArgs = []; + + // Parse nested function arguments + while (current < tokens.length && + tokens[current].type !== TokenType.SEMICOLON && + tokens[current].type !== TokenType.RIGHT_PAREN) { + const nestedArg = walk(); + if (nestedArg) { + nestedArgs.push(nestedArg); + } + } + + args.push({ + type: 'FunctionCall', + name: nestedFuncName, + args: nestedArgs, + }); + } else { + // Use walk() for other types of arguments + const arg = walk(); + if (arg) { + args.push(arg); + } + } + } + } + + return { + type: 'FunctionCall', + name: funcName, + args, + }; + } + + return null; + } + + // Handle arithmetic expressions that start with a number followed by an operator + if (token.type === TokenType.NUMBER && tokens[current + 1] && ( + tokens[current + 1].type === TokenType.PLUS || + tokens[current + 1].type === TokenType.MINUS || + tokens[current + 1].type === TokenType.MULTIPLY || + tokens[current + 1].type === TokenType.DIVIDE || + tokens[current + 1].type === TokenType.MODULO || + tokens[current + 1].type === TokenType.POWER + )) { + debugLog('Parsing arithmetic expression starting with number', { + current: token, + next: tokens[current + 1] + }); + const left = { + type: 'NumberLiteral', + value: token.value, + }; + current++; // Skip the number + const operator = tokens[current].type; + current++; // Skip the operator + const right = walk(); + + const expressionTypes = { + [TokenType.PLUS]: 'PlusExpression', + [TokenType.MINUS]: 'MinusExpression', + [TokenType.MULTIPLY]: 'MultiplyExpression', + [TokenType.DIVIDE]: 'DivideExpression', + [TokenType.MODULO]: 'ModuloExpression', + [TokenType.POWER]: 'PowerExpression', + }; + + return { + type: expressionTypes[operator], + left, + right, + }; + } + if (token.type === TokenType.NUMBER) { current++; return { @@ -159,16 +713,755 @@ function parser(tokens) { }; } + if (token.type === TokenType.STRING) { + current++; + return { + type: 'StringLiteral', + value: token.value, + }; + } + + if (token.type === TokenType.WILDCARD) { + current++; + return { + type: 'WildcardPattern', + }; + } + + if (token.type === TokenType.IO_IN) { + current++; + return { + type: 'IOInExpression', + }; + } + + if (token.type === TokenType.IO_OUT) { + current++; + const value = walk(); + return { + type: 'IOOutExpression', + value, + }; + } + + if (token.type === TokenType.FUNCTION_REF) { + current++; // Skip the @ token + if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { + const functionName = tokens[current].value; + current++; // Skip the function name + return { + type: 'FunctionReference', + name: functionName, + }; + } else { + throw new Error('Expected function name after @'); + } + } + + if (token.type === TokenType.IO_ASSERT) { + current++; + + // Parse a comparison expression (e.g., x = 5, y > 3, z != 0) + const left = walk(); + + // Expect a comparison operator + if (tokens[current] && ( + tokens[current].type === TokenType.EQUALS || + tokens[current].type === TokenType.LESS_THAN || + tokens[current].type === TokenType.GREATER_THAN || + tokens[current].type === TokenType.LESS_EQUAL || + tokens[current].type === TokenType.GREATER_EQUAL || + tokens[current].type === TokenType.NOT_EQUAL + )) { + const operator = tokens[current].type; + current++; // Skip the operator + const right = walk(); + + return { + type: 'IOAssertExpression', + left, + operator, + right, + }; + } else { + throw new Error('Expected comparison operator (=, <, >, <=, >=, !=) in assertion'); + } + } + + + if (token.type === TokenType.PLUS) { current++; + const left = walk(); + const right = walk(); return { type: 'PlusExpression', - left: walk(), - right: walk(), + left, + right, }; } + if (token.type === TokenType.MINUS) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'MinusExpression', + left, + right, + }; + } + + if (token.type === TokenType.MULTIPLY) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'MultiplyExpression', + left, + right, + }; + } + + if (token.type === TokenType.DIVIDE) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'DivideExpression', + left, + right, + }; + } + + if (token.type === TokenType.MODULO) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'ModuloExpression', + left, + right, + }; + } + + if (token.type === TokenType.POWER) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'PowerExpression', + left, + right, + }; + } + + // Comparison operators + if (token.type === TokenType.EQUALS) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'EqualsExpression', + left, + right, + }; + } + + if (token.type === TokenType.LESS_THAN) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'LessThanExpression', + left, + right, + }; + } + + if (token.type === TokenType.GREATER_THAN) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'GreaterThanExpression', + left, + right, + }; + } + + if (token.type === TokenType.LESS_EQUAL) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'LessEqualExpression', + left, + right, + }; + } + + if (token.type === TokenType.GREATER_EQUAL) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'GreaterEqualExpression', + left, + right, + }; + } + + if (token.type === TokenType.NOT_EQUAL) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'NotEqualExpression', + left, + right, + }; + } + + // Logical operators + if (token.type === TokenType.AND) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'AndExpression', + left, + right, + }; + } + + if (token.type === TokenType.OR) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'OrExpression', + left, + right, + }; + } + + if (token.type === TokenType.XOR) { + current++; + const left = walk(); + const right = walk(); + return { + type: 'XorExpression', + left, + right, + }; + } + + if (token.type === TokenType.NOT) { + current++; + const operand = walk(); + return { + type: 'NotExpression', + operand, + }; + } + + if (token.type === TokenType.LEFT_PAREN) { + current++; // Skip opening parenthesis + const expression = walk(); + + // Expect closing parenthesis + if (tokens[current] && tokens[current].type === TokenType.RIGHT_PAREN) { + debugLog('Found right parenthesis'); + current++; // Skip closing parenthesis + + // Check if there's an arithmetic operator after the parentheses + if (tokens[current] && ( + tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO || + tokens[current].type === TokenType.POWER || + tokens[current].type === TokenType.AND || + tokens[current].type === TokenType.OR || + tokens[current].type === TokenType.XOR || + tokens[current].type === TokenType.NOT + )) { + // This is an arithmetic expression with parentheses as left operand + const operator = tokens[current].type; + current++; // Skip the operator + const right = walk(); + + const expressionTypes = { + [TokenType.PLUS]: 'PlusExpression', + [TokenType.MINUS]: 'MinusExpression', + [TokenType.MULTIPLY]: 'MultiplyExpression', + [TokenType.DIVIDE]: 'DivideExpression', + [TokenType.MODULO]: 'ModuloExpression', + [TokenType.POWER]: 'PowerExpression', + [TokenType.AND]: 'AndExpression', + [TokenType.OR]: 'OrExpression', + [TokenType.XOR]: 'XorExpression', + [TokenType.NOT]: 'NotExpression', + }; + + return { + type: expressionTypes[operator], + left: expression, + right, + }; + } + + // If not followed by an arithmetic operator, just return the parenthesized expression + // This handles cases like function arguments: add (3 + 2) (4 + 1) + return expression; + } else { + debugLog('Expected right parenthesis but found', tokens[current]); + throw new Error('Expected closing parenthesis'); + } + } + + if (token.type === TokenType.ASSIGNMENT) { + current++; // Skip the assignment token + return walk(); // Continue parsing + } + if (token.type === TokenType.IDENTIFIER) { + // First, try to parse as a function call with proper precedence + const functionCall = parseFunctionCall(); + if (functionCall) { + return functionCall; + } + + // Check if this is followed by an assignment (variable or function) + if (tokens[current + 1] && tokens[current + 1].type === TokenType.ASSIGNMENT) { + const name = token.value; + + // Check if this is a function declaration (has parameters and arrow) + // Look ahead to see if there are parameters followed by arrow + let params = []; + let isFunction = false; + let tempCurrent = current + 2; // Skip identifier and assignment + + // Collect parameters until we hit an arrow + while (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.IDENTIFIER) { + params.push(tokens[tempCurrent].value); + tempCurrent++; + } + + // Check if next token is arrow + if (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.ARROW) { + isFunction = true; + } + + if (isFunction) { + // This is a function declaration + // Advance current to where tempCurrent left off + current = tempCurrent + 1; // Skip the arrow token + + // Check if the body is a case expression + if (tokens[current] && tokens[current].type === TokenType.CASE) { + current++; // Skip 'case' + + // Parse the value being matched (e.g., "x y") + const value = []; + while (current < tokens.length && + tokens[current].type !== TokenType.OF) { + if (tokens[current].type === TokenType.IDENTIFIER) { + value.push({ + type: 'Identifier', + value: tokens[current].value, + }); + } else if (tokens[current].type === TokenType.NUMBER) { + value.push({ + type: 'NumberLiteral', + value: tokens[current].value, + }); + } + current++; + } + + // Expect 'of' after the value + if (tokens[current] && tokens[current].type === TokenType.OF) { + current++; // Skip 'of' + } else { + throw new Error('Expected "of" after case value'); + } + + const cases = []; + + // Parse cases until we hit a semicolon or end + while (current < tokens.length) { + // Check if we've reached the end of the case expression + if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { + debugLog('Found semicolon at start of loop, ending case parsing'); + current++; + break; // Exit the case parsing loop + } + + debugLog('Case parsing loop', { + current: tokens[current], + currentType: tokens[current]?.type + }); + // Parse pattern (e.g., "0 0" or "0 _" or "_ 0" or "_ _") + debugLog('Parsing pattern', { + current: tokens[current], + currentType: tokens[current]?.type + }); + const pattern = []; + while (current < tokens.length && + tokens[current].type !== TokenType.ASSIGNMENT && + tokens[current].type !== TokenType.SEMICOLON) { + if (tokens[current].type === TokenType.NUMBER) { + pattern.push({ + type: 'NumberLiteral', + value: tokens[current].value, + }); + } else if (tokens[current].type === TokenType.WILDCARD) { + pattern.push({ + type: 'WildcardPattern', + }); + } else if (tokens[current].type === TokenType.IDENTIFIER) { + pattern.push({ + type: 'Identifier', + value: tokens[current].value, + }); + } + current++; + } + + // Expect ':' after pattern + debugLog('Looking for : after pattern', { + current: tokens[current], + currentType: tokens[current]?.type + }); + if (tokens[current] && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + debugLog('Found : after pattern'); + } else { + throw new Error('Expected ":" after pattern'); + } + + // Parse result using walk() to handle complex expressions + debugLog('Starting case result parsing', { + current: tokens[current], + currentType: tokens[current]?.type + }); + const result = []; + + // Check if we're at the start of the next pattern + if ((tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.WILDCARD) && + tokens[current + 1] && tokens[current + 1].type === TokenType.ASSIGNMENT) { + debugLog('Found start of next pattern, stopping case result parsing'); + } else { + // Parse one expression for the case result + debugLog('Parsing case result node', { + current: tokens[current], + currentType: tokens[current]?.type + }); + const resultNode = walk(); + if (resultNode) { + result.push(resultNode); + debugLog('Added result node', resultNode); + } + + // Check if we've reached the end of the case expression + if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { + debugLog('Found semicolon after result, ending case parsing'); + current++; + break; // Exit the case parsing loop + } + } + + cases.push({ pattern, result }); + + // Check if we've reached the end of the case expression + if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { + debugLog('Found semicolon, ending case parsing'); + current++; + break; // Exit the case parsing loop + } + } + + const body = { + type: 'CaseExpression', + value, + cases, + }; + + return { + type: 'FunctionDeclaration', + name, + params, + body, + }; + } else { + // Parse function body directly to avoid function call detection issues + let body; + if (tokens[current] && ( + tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE + )) { + // Arithmetic expression + const operator = tokens[current].type; + current++; // Skip operator + const left = walk(); + const right = walk(); + + const expressionTypes = { + [TokenType.PLUS]: 'PlusExpression', + [TokenType.MINUS]: 'MinusExpression', + [TokenType.MULTIPLY]: 'MultiplyExpression', + [TokenType.DIVIDE]: 'DivideExpression', + }; + + body = { + type: expressionTypes[operator], + left, + right, + }; + } else { + // Fallback to walk() for other cases + debugLog('Using walk() fallback for function body'); + body = walk(); + } + + return { + type: 'FunctionDeclaration', + name, + params, + body, + }; + } + } else { + // This is a variable assignment + // Advance current to skip identifier and assignment token + current += 2; // Skip identifier and assignment + + debugLog('Assignment parsing', { + current: tokens[current], + next: tokens[current + 1], + currentType: tokens[current]?.type, + nextType: tokens[current + 1]?.type + }); + + // Parse the value directly without calling walk() + let value; + + // Check if the value is a function call + if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER && + tokens[current + 1] && tokens[current + 1].type !== TokenType.ASSIGNMENT && + tokens[current + 1].type !== TokenType.SEMICOLON && + tokens[current + 1].type !== TokenType.ARROW && + (tokens[current + 1].type === TokenType.NUMBER || + tokens[current + 1].type === TokenType.IDENTIFIER || + tokens[current + 1].type === TokenType.FUNCTION_REF || + tokens[current + 1].type === TokenType.LEFT_PAREN)) { + // This is a function call as the value + const funcName = tokens[current].value; + current++; // Skip function name + const args = []; + + // Collect arguments until we hit a semicolon, closing parenthesis, or end + while (current < tokens.length && + tokens[current].type !== TokenType.SEMICOLON && + tokens[current].type !== TokenType.RIGHT_PAREN) { + // Handle function references (@functionName) + if (tokens[current] && tokens[current].type === TokenType.FUNCTION_REF) { + current++; // Skip @ + if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { + args.push({ + type: 'FunctionReference', + name: tokens[current].value, + }); + current++; + } else { + throw new Error('Expected function name after @'); + } + } else { + // Use walk() to parse complex arguments (including arithmetic expressions) + const arg = walk(); + if (arg) { + args.push(arg); + } + } + } + + value = { + type: 'FunctionCall', + name: funcName, + args, + }; + + debugLog('Function call parsed in assignment', { name: funcName, args }); + } else if (tokens[current] && tokens[current].type === TokenType.NUMBER) { + // Check if this is the start of an arithmetic expression + if (tokens[current + 1] && ( + tokens[current + 1].type === TokenType.PLUS || + tokens[current + 1].type === TokenType.MINUS || + tokens[current + 1].type === TokenType.MULTIPLY || + tokens[current + 1].type === TokenType.DIVIDE + )) { + // This is an arithmetic expression, use walk() to parse it + debugLog('Parsing arithmetic expression in assignment'); + debugLog('Current token before walk()', tokens[current]); + value = walk(); + debugLog('Arithmetic expression parsed', value); + } else { + // Simple number value + value = { + type: 'NumberLiteral', + value: tokens[current].value, + }; + current++; + } + } else if (tokens[current] && tokens[current].type === TokenType.STRING) { + // Simple string value + value = { + type: 'StringLiteral', + value: tokens[current].value, + }; + current++; + } else if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { + // Check if this is the start of an infix arithmetic expression + if (tokens[current + 1] && ( + tokens[current + 1].type === TokenType.PLUS || + tokens[current + 1].type === TokenType.MINUS || + tokens[current + 1].type === TokenType.MULTIPLY || + tokens[current + 1].type === TokenType.DIVIDE + )) { + // This is an infix arithmetic expression + const left = { + type: 'Identifier', + value: tokens[current].value, + }; + current++; // Skip left operand + const operator = tokens[current].type; + current++; // Skip operator + const right = walk(); + + const expressionTypes = { + [TokenType.PLUS]: 'PlusExpression', + [TokenType.MINUS]: 'MinusExpression', + [TokenType.MULTIPLY]: 'MultiplyExpression', + [TokenType.DIVIDE]: 'DivideExpression', + }; + + value = { + type: expressionTypes[operator], + left, + right, + }; + } else { + // Simple identifier value + value = { + type: 'Identifier', + value: tokens[current].value, + }; + current++; + } + } else if (tokens[current] && ( + tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE + )) { + // Arithmetic expression value (prefix notation) + const operator = tokens[current].type; + current++; // Skip operator + const left = walk(); + const right = walk(); + + const expressionTypes = { + [TokenType.PLUS]: 'PlusExpression', + [TokenType.MINUS]: 'MinusExpression', + [TokenType.MULTIPLY]: 'MultiplyExpression', + [TokenType.DIVIDE]: 'DivideExpression', + }; + + value = { + type: expressionTypes[operator], + left, + right, + }; + } else { + // Fallback to walk() for other cases + debugLog('Using walk() fallback for assignment value', { current: tokens[current] }); + value = walk(); + } + + return { + type: 'AssignmentExpression', + name, + value, + }; + } + } + + + + // Check if this is followed by an operator + if (tokens[current + 1] && ( + tokens[current + 1].type === TokenType.PLUS || + tokens[current + 1].type === TokenType.MINUS || + tokens[current + 1].type === TokenType.MULTIPLY || + tokens[current + 1].type === TokenType.DIVIDE + )) { + const left = { + type: 'Identifier', + value: token.value, + }; + const operator = tokens[current + 1].type; + current += 2; // Skip identifier and operator + const right = walk(); + + const expressionTypes = { + [TokenType.PLUS]: 'PlusExpression', + [TokenType.MINUS]: 'MinusExpression', + [TokenType.MULTIPLY]: 'MultiplyExpression', + [TokenType.DIVIDE]: 'DivideExpression', + }; + + return { + type: expressionTypes[operator], + left, + right, + }; + } + + // Check if this is followed by an operator + if (tokens[current + 1] && ( + tokens[current + 1].type === TokenType.PLUS || + tokens[current + 1].type === TokenType.MINUS || + tokens[current + 1].type === TokenType.MULTIPLY || + tokens[current + 1].type === TokenType.DIVIDE + )) { + const left = { + type: 'Identifier', + value: token.value, + }; + const operator = tokens[current + 1].type; + current += 2; // Skip identifier and operator + const right = walk(); + + const expressionTypes = { + [TokenType.PLUS]: 'PlusExpression', + [TokenType.MINUS]: 'MinusExpression', + [TokenType.MULTIPLY]: 'MultiplyExpression', + [TokenType.DIVIDE]: 'DivideExpression', + }; + + return { + type: expressionTypes[operator], + left, + right, + }; + } + current++; return { type: 'Identifier', @@ -176,12 +1469,38 @@ function parser(tokens) { }; } - if (token.type === TokenType.ASSIGNMENT) { - current++; + + + // Pattern matching: value : pattern1 : result1 pattern2 : result2 ... + if (token.type === TokenType.CASE) { + current++; // Skip 'case' + + // Parse the value being matched (wrap in array for interpreter compatibility) + const valueNode = walk(); + const value = [valueNode]; + + // Expect 'of' + if (tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); + } + current++; // Skip 'of' + + const cases = []; + + // Parse cases until we hit a semicolon or end + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + const patternNode = walk(); + const resultNode = walk(); + cases.push({ + pattern: [patternNode], + result: [resultNode] + }); + } + return { - type: 'AssignmentExpression', - name: tokens[current - 2].value, - value: walk(), + type: 'CaseExpression', + value, + cases, }; } @@ -232,7 +1551,7 @@ function parser(tokens) { if (token.type === TokenType.SEMICOLON) { current++; - return; + return null; } throw new TypeError(token.type); @@ -245,7 +1564,7 @@ function parser(tokens) { while (current < tokens.length) { const node = walk(); - if (node !== null) { + if (node !== null && node !== undefined) { ast.body.push(node); } } @@ -255,66 +1574,819 @@ function parser(tokens) { // Interpreter function interpreter(ast) { + debugLog('Starting interpreter with AST', ast); let globalScope = {}; + initializeStandardLibrary(globalScope); function evalNode(node) { + if (!node) { + return undefined; + } switch (node.type) { case 'NumberLiteral': return parseInt(node.value); + case 'StringLiteral': + return node.value; case 'PlusExpression': return evalNode(node.left) + evalNode(node.right); + case 'MinusExpression': + return evalNode(node.left) - evalNode(node.right); + case 'MultiplyExpression': + return evalNode(node.left) * evalNode(node.right); + case 'DivideExpression': + const divisor = evalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return evalNode(node.left) / evalNode(node.right); + case '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 '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]; + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined) { + throw new Error(`Variable ${node.value} is not defined`); + } + return identifierValue; + case 'FunctionReference': + const functionValue = globalScope[node.name]; + if (functionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof functionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return functionValue; case 'IfExpression': return evalNode(node.test) ? evalNode(node.consequent) : node.alternate ? evalNode(node.alternate) : undefined; case 'FunctionDeclaration': - globalScope[node.name] = function() { + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + globalScope[node.name] = function(...args) { let localScope = Object.create(globalScope); for (let i = 0; i < node.params.length; i++) { - localScope[node.params[i]] = arguments[i]; + localScope[node.params[i]] = args[i]; + } + // Create a new evalNode function that uses localScope + const localEvalNode = (node) => { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseInt(node.value); + case 'StringLiteral': + return node.value; + case 'PlusExpression': + return localEvalNode(node.left) + localEvalNode(node.right); + case 'MinusExpression': + return localEvalNode(node.left) - localEvalNode(node.right); + case 'MultiplyExpression': + return localEvalNode(node.left) * localEvalNode(node.right); + case 'DivideExpression': + const divisor = localEvalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return localEvalNode(node.left) / localEvalNode(node.right); + case 'ModuloExpression': + return localEvalNode(node.left) % localEvalNode(node.right); + case 'PowerExpression': + return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); + case 'EqualsExpression': + return localEvalNode(node.left) === localEvalNode(node.right); + case 'LessThanExpression': + return localEvalNode(node.left) < localEvalNode(node.right); + case 'GreaterThanExpression': + return localEvalNode(node.left) > localEvalNode(node.right); + case 'LessEqualExpression': + return localEvalNode(node.left) <= localEvalNode(node.right); + case 'GreaterEqualExpression': + return localEvalNode(node.left) >= localEvalNode(node.right); + case 'NotEqualExpression': + return localEvalNode(node.left) !== localEvalNode(node.right); + case 'AndExpression': + return localEvalNode(node.left) && localEvalNode(node.right); + case 'OrExpression': + return localEvalNode(node.left) || localEvalNode(node.right); + case 'XorExpression': + const leftVal = localEvalNode(node.left); + const rightVal = localEvalNode(node.right); + return (leftVal && !rightVal) || (!leftVal && rightVal); + case 'NotExpression': + return !localEvalNode(node.operand); + case 'AssignmentExpression': + if (localScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + localScope[node.name] = localEvalNode(node.value); + return; + case 'Identifier': + const identifierValue = localScope[node.value]; + if (identifierValue === undefined && node.value) { + return node.value; // Return as string literal if not found in scope + } + return identifierValue; + case 'FunctionReference': + const localFunctionValue = localScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + case 'IfExpression': + return localEvalNode(node.test) ? localEvalNode(node.consequent) : node.alternate ? localEvalNode(node.alternate) : undefined; + case 'FunctionDeclaration': + if (localScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + localScope[node.name] = function(...args) { + let nestedScope = Object.create(localScope); + for (let i = 0; i < node.params.length; i++) { + nestedScope[node.params[i]] = args[i]; + } + return localEvalNode(node.body); + }; + return; + case 'FunctionCall': + if (localScope[node.name] instanceof Function) { + let args = node.args.map(localEvalNode); + return localScope[node.name](...args); + } + throw new Error(`Function ${node.name} is not defined`); + case 'CaseExpression': + // Evaluate the values being matched (e.g., [x, y]) + const values = node.value.map(localEvalNode); + + for (const caseItem of node.cases) { + // Evaluate the pattern (e.g., [0, 0] or [0, _]) + const pattern = caseItem.pattern.map(localEvalNode); + + // Check if pattern matches values + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + // Wildcard always matches + if (patternValue === true) continue; + + // Otherwise, values must be equal + if (value !== patternValue) { + matches = false; + break; + } } - let lastResult; - for (let bodyNode of node.body) { - lastResult = evalNode(bodyNode); + + if (matches) { + // Evaluate all results + const results = caseItem.result.map(localEvalNode); + // If there's only one result, return it directly (preserves number type) + if (results.length === 1) { + return results[0]; + } + // Otherwise join multiple results as string + return results.join(' '); } - return lastResult; + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; // Wildcard always matches + case 'IOInExpression': + // Read from stdin + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + // Output to stdout + const localOutputValue = localEvalNode(node.value); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + // Assert that left operator right is true + const localLeftValue = localEvalNode(node.left); + const localRightValue = localEvalNode(node.right); + + let localAssertionPassed = false; + switch (node.operator) { + case TokenType.EQUALS: + localAssertionPassed = localLeftValue === localRightValue; + break; + case TokenType.LESS_THAN: + localAssertionPassed = localLeftValue < localRightValue; + break; + case TokenType.GREATER_THAN: + localAssertionPassed = localLeftValue > localRightValue; + break; + case TokenType.LESS_EQUAL: + localAssertionPassed = localLeftValue <= localRightValue; + break; + case TokenType.GREATER_EQUAL: + localAssertionPassed = localLeftValue >= localRightValue; + break; + case TokenType.NOT_EQUAL: + localAssertionPassed = localLeftValue !== localRightValue; + break; + default: + throw new Error(`Unsupported comparison operator in assertion: ${node.operator}`); + } + + if (!localAssertionPassed) { + const operatorSymbol = { + [TokenType.EQUALS]: '=', + [TokenType.LESS_THAN]: '<', + [TokenType.GREATER_THAN]: '>', + [TokenType.LESS_EQUAL]: '<=', + [TokenType.GREATER_EQUAL]: '>=', + [TokenType.NOT_EQUAL]: '!=' + }[node.operator]; + throw new Error(`Assertion failed: ${JSON.stringify(localLeftValue)} ${operatorSymbol} ${JSON.stringify(localRightValue)}`); + } + return localLeftValue; + default: + throw new Error(`Unknown node type: ${node.type}`); + } + }; + return localEvalNode(node.body); }; return; case 'FunctionCall': if (globalScope[node.name] instanceof Function) { let args = node.args.map(evalNode); - return globalScope[node.name].apply(null, args); + return globalScope[node.name](...args); } throw new Error(`Function ${node.name} is not defined`); + case 'CaseExpression': + debugLog('CaseExpression evaluation', { value: node.value, cases: node.cases }); + // Evaluate the values being matched (e.g., [x, y]) + const values = node.value.map(evalNode); + + for (const caseItem of node.cases) { + // Evaluate the pattern (e.g., [0, 0] or [0, _]) + const pattern = caseItem.pattern.map(evalNode); + + // Check if pattern matches values + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + // Wildcard always matches + if (patternValue === true) continue; + + // Otherwise, values must be equal + if (value !== patternValue) { + matches = false; + break; + } + } + + if (matches) { + // Evaluate all results + const results = caseItem.result.map(evalNode); + // If there's only one result, return it directly (preserves number type) + if (results.length === 1) { + return results[0]; + } + // Otherwise join multiple results as string + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; // Wildcard always matches + case 'IOInExpression': + // Read from stdin + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + // Output to stdout + const outputValue = evalNode(node.value); + console.log(outputValue); + return outputValue; + case 'IOAssertExpression': + // Assert that left operator right is true + const leftValue = evalNode(node.left); + const rightValue = evalNode(node.right); + + let assertionPassed = false; + switch (node.operator) { + case TokenType.EQUALS: + assertionPassed = leftValue === rightValue; + break; + case TokenType.LESS_THAN: + assertionPassed = leftValue < rightValue; + break; + case TokenType.GREATER_THAN: + assertionPassed = leftValue > rightValue; + break; + case TokenType.LESS_EQUAL: + assertionPassed = leftValue <= rightValue; + break; + case TokenType.GREATER_EQUAL: + assertionPassed = leftValue >= rightValue; + break; + case TokenType.NOT_EQUAL: + assertionPassed = leftValue !== rightValue; + break; + default: + throw new Error(`Unsupported comparison operator in assertion: ${node.operator}`); + } + + if (!assertionPassed) { + const operatorSymbol = { + [TokenType.EQUALS]: '=', + [TokenType.LESS_THAN]: '<', + [TokenType.GREATER_THAN]: '>', + [TokenType.LESS_EQUAL]: '<=', + [TokenType.GREATER_EQUAL]: '>=', + [TokenType.NOT_EQUAL]: '!=' + }[node.operator]; + throw new Error(`Assertion failed: ${JSON.stringify(leftValue)} ${operatorSymbol} ${JSON.stringify(rightValue)}`); + } + return leftValue; default: throw new Error(`Unknown node type: ${node.type}`); } } - return evalNode(ast.body[0]); + let lastResult; + for (let node of ast.body) { + if (node) { // Skip undefined nodes + lastResult = evalNode(node); + } + } + + // Handle async results (from interactive IO operations) + 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 configuration +const DEBUG = process.env.DEBUG === 'true' || process.argv.includes('--debug'); -// const tokens2 = lexer('x : 2 + 2'); -// const ast2 = parser(tokens2); -// const result2 = interpreter(ast2); -// console.log(result2); +function debugLog(message, data = null) { + if (DEBUG) { + if (data) { + console.log(`[DEBUG] ${message}:`, JSON.stringify(data, null, 2)); + } else { + console.log(`[DEBUG] ${message}`); + } + } +} +function debugError(message, error = null) { + if (DEBUG) { + console.error(`[DEBUG ERROR] ${message}`); + if (error) { + console.error(`[DEBUG ERROR] Details:`, error); + } + } +} + +// Main execution logic const fs = require('fs'); +const readline = require('readline'); + +/** + * Execute a script file + */ +function executeFile(filePath) { + try { + debugLog(`Executing file: ${filePath}`); + + const input = fs.readFileSync(filePath, 'utf-8'); + debugLog('Input content', input); + + const tokens = lexer(input); + debugLog('Tokens generated', tokens); + + const ast = parser(tokens); + debugLog('AST generated', ast); + + const result = interpreter(ast); + debugLog('Interpreter result', result); + + // Only print result if it's not undefined + if (result !== undefined) { + console.log(result); + } + } catch (error) { + debugError('Execution failed', error); + console.error(`Error executing file: ${error.message}`); + process.exit(1); + } +} + +/** + * Start REPL mode + */ +function startREPL() { + console.log('Scripting Language REPL'); + console.log('Type expressions to evaluate. End with semicolon (;) to execute.'); + console.log('Type "exit" to quit.'); + console.log(''); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: '> ' + }); + + // Create a persistent global scope for the REPL + let globalScope = {}; + initializeStandardLibrary(globalScope); + let currentInput = ''; + + rl.prompt(); + + rl.on('line', (line) => { + const trimmedLine = line.trim(); + + if (trimmedLine === 'exit' || trimmedLine === 'quit') { + rl.close(); + return; + } + + // Add the line to current input + currentInput += line + '\n'; + + // Check if the input ends with a semicolon + if (currentInput.trim().endsWith(';')) { + try { + const tokens = lexer(currentInput); + const ast = parser(tokens); + + // Create a custom interpreter that uses the persistent scope + const result = interpreterWithScope(ast, globalScope); + + // Only print result if it's not undefined + if (result !== undefined) { + console.log(result); + } + } catch (error) { + console.error(`Error: ${error.message}`); + } + + // Reset current input for next expression + currentInput = ''; + } + + rl.prompt(); + }); + + rl.on('close', () => { + console.log('Goodbye!'); + process.exit(0); + }); +} + +/** + * Interpreter that uses a provided global scope + */ +function interpreterWithScope(ast, globalScope) { + function evalNode(node) { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseInt(node.value); + case 'StringLiteral': + return node.value; + case 'PlusExpression': + return evalNode(node.left) + evalNode(node.right); + case 'MinusExpression': + return evalNode(node.left) - evalNode(node.right); + case 'MultiplyExpression': + return evalNode(node.left) * evalNode(node.right); + case 'DivideExpression': + const divisor = evalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return evalNode(node.left) / evalNode(node.right); + case 'AssignmentExpression': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + const value = evalNode(node.value); + globalScope[node.name] = value; + return; + case 'Identifier': + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined) { + throw new Error(`Variable ${node.value} is not defined`); + } + return identifierValue; + case 'FunctionDeclaration': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + globalScope[node.name] = function(...args) { + let localScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + localScope[node.params[i]] = args[i]; + } + return localEvalNode(node.body); + }; + return; + case 'FunctionCall': + if (globalScope[node.name] instanceof Function) { + let args = node.args.map(evalNode); + return globalScope[node.name](...args); + } + throw new Error(`Function ${node.name} is not defined`); + case 'CaseExpression': + const values = node.value.map(evalNode); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(evalNode); + + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + if (patternValue === true) continue; + + if (value !== patternValue) { + matches = false; + break; + } + } + + if (matches) { + const results = caseItem.result.map(evalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const outputValue = evalNode(node.value); + console.log(outputValue); + return outputValue; + case 'FunctionReference': + const functionValue = globalScope[node.name]; + if (functionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof functionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return functionValue; + default: + throw new Error(`Unknown node type: ${node.type}`); + } + } + + const localEvalNode = (node) => { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseInt(node.value); + case 'StringLiteral': + return node.value; + case 'PlusExpression': + return localEvalNode(node.left) + localEvalNode(node.right); + case 'MinusExpression': + return localEvalNode(node.left) - localEvalNode(node.right); + case 'MultiplyExpression': + return localEvalNode(node.left) * localEvalNode(node.right); + case 'DivideExpression': + const divisor = localEvalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return localEvalNode(node.left) / localEvalNode(node.right); + case 'ModuloExpression': + return localEvalNode(node.left) % localEvalNode(node.right); + case 'PowerExpression': + return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); + case 'EqualsExpression': + return localEvalNode(node.left) === localEvalNode(node.right); + case 'LessThanExpression': + return localEvalNode(node.left) < localEvalNode(node.right); + case 'GreaterThanExpression': + return localEvalNode(node.left) > localEvalNode(node.right); + case 'LessEqualExpression': + return localEvalNode(node.left) <= localEvalNode(node.right); + case 'GreaterEqualExpression': + return localEvalNode(node.left) >= localEvalNode(node.right); + case 'NotEqualExpression': + return localEvalNode(node.left) !== localEvalNode(node.right); + case 'AndExpression': + return localEvalNode(node.left) && localEvalNode(node.right); + case 'OrExpression': + return localEvalNode(node.left) || localEvalNode(node.right); + case 'XorExpression': + const leftVal = localEvalNode(node.left); + const rightVal = localEvalNode(node.right); + return (leftVal && !rightVal) || (!leftVal && rightVal); + case 'NotExpression': + return !localEvalNode(node.operand); + case 'AssignmentExpression': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + globalScope[node.name] = localEvalNode(node.value); + return; + case 'Identifier': + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined && node.value) { + return node.value; + } + return identifierValue; + case 'FunctionDeclaration': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + globalScope[node.name] = function(...args) { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + nestedScope[node.params[i]] = args[i]; + } + return localEvalNode(node.body); + }; + return; + case 'FunctionCall': + if (globalScope[node.name] instanceof Function) { + let args = node.args.map(localEvalNode); + return globalScope[node.name](...args); + } + throw new Error(`Function ${node.name} is not defined`); + case 'CaseExpression': + const values = node.value.map(localEvalNode); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(localEvalNode); + + let matches = true; + for (let i = 0; i < Math.max(values.length, pattern.length); i++) { + const value = values[i]; + const patternValue = pattern[i]; + + if (patternValue === true) continue; + + if (value !== patternValue) { + matches = false; + break; + } + } + + if (matches) { + const results = caseItem.result.map(localEvalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } + } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNode(node.value); + console.log(localOutputValue); + return localOutputValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + default: + throw new Error(`Unknown node type: ${node.type}`); + } + }; + + let lastResult; + for (let node of ast.body) { + if (node) { + lastResult = evalNode(node); + } + } + + if (lastResult instanceof Promise) { + return lastResult.then(result => { + return result; + }); + } + + return lastResult; +} -// 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) { + // No arguments - start REPL mode + startREPL(); +} else if (args.length === 1) { + // One argument - execute the file + const filePath = args[0]; + executeFile(filePath); +} else { + // Too many arguments + console.error('Usage: node lang.js [file]'); + console.error(' If no file is provided, starts REPL mode'); + console.error(' If file is provided, executes the file'); + process.exit(1); +} \ No newline at end of file diff --git a/js/scripting-lang/parser_analysis.md b/js/scripting-lang/parser_analysis.md new file mode 100644 index 0000000..8a25df1 --- /dev/null +++ b/js/scripting-lang/parser_analysis.md @@ -0,0 +1,143 @@ +# Parser Implementation Analysis + +## Grammar vs Implementation Comparison + +### 1. Assignment Parsing Issue + +**Grammar Rule:** +``` +assignment = identifier ":" assignment_value +assignment_value = function_call | simple_expression +``` + +**Current Implementation Problem:** +- The parser calls `walk()` to get the value in assignments +- `walk()` consumes the function name before assignment parsing can detect it as a function call +- This causes `result : add 3 4;` to be parsed incorrectly + +**Expected Parse Tree:** +``` +assignment( + identifier("result"), + function_call("add", [number(3), number(4)]) +) +``` + +**Actual Parse Tree (from debug output):** +``` +assignment( + identifier("result"), + number(3) +) +// Plus separate nodes: number(4) +``` + +### 2. Function Call Detection + +**Grammar Rule:** +``` +function_call = identifier argument_list +argument_list = expression { expression } +``` + +**Current Implementation Issues:** +- Function call detection is in the wrong place in the parser +- Assignment parsing calls `walk()` which consumes tokens before function call detection +- The condition `tokens[current + 1].type !== TokenType.ASSIGNMENT` prevents function calls in assignments + +**Debug Evidence:** +``` +[DEBUG] Assignment parsing: { + "current": { "type": "NUMBER", "value": "3" }, + "next": { "type": "NUMBER", "value": "4" } +} +``` +The parser is already at the `3` token, meaning `add` has been consumed. + +### 3. Case Expression Parsing + +**Grammar Rule:** +``` +case_expression = "case" value_list "of" case_list +case_branch = pattern ":" result +``` + +**Current Implementation Status:** +✅ **Working correctly** - Case expressions parse and structure correctly +- Patterns are parsed correctly: `0 0`, `0 _`, `_ 0`, `_ _` +- Results are parsed correctly: string literals, numbers, identifiers +- The AST structure matches the grammar + +### 4. Arithmetic Expression Parsing + +**Grammar Rule:** +``` +arithmetic_expression = arithmetic_operator expression expression +``` + +**Current Implementation Status:** +✅ **Working correctly** - Prefix notation is handled correctly +- `+ x y` parses as `PlusExpression(left: x, right: y)` +- Operator precedence is handled correctly + +### 5. IO Statement Parsing + +**Grammar Rule:** +``` +io_statement = io_operator [ expression ] +io_operator = "..in" | "..out" +``` + +**Current Implementation Status:** +✅ **Working correctly** - IO statements parse correctly +- `..in` and `..out` are recognized as special tokens +- Expressions after `..out` are parsed correctly + +## Root Cause Analysis + +The fundamental issue is in the parser architecture: + +1. **Assignment parsing calls `walk()`** to get the value +2. **`walk()` consumes tokens** before assignment parsing can analyze them +3. **Function call detection happens too late** in the parsing process + +## Proposed Solutions + +### Solution 1: Restructure Assignment Parsing +Don't call `walk()` for assignment values. Instead, directly parse the value: + +```javascript +// Instead of: const value = walk(); +// Do: Parse value directly based on next tokens +``` + +### Solution 2: Add Function Call Detection to walk() +Make `walk()` detect function calls when it encounters an identifier followed by arguments: + +```javascript +if (token.type === TokenType.IDENTIFIER) { + // Check for function call first + if (isFunctionCall()) { + return parseFunctionCall(); + } + // Then check for assignment + if (isAssignment()) { + return parseAssignment(); + } +} +``` + +### Solution 3: Use Lookahead +Implement a lookahead mechanism to peek at tokens without consuming them: + +```javascript +function peek(n = 1) { + return tokens[current + n]; +} +``` + +## Recommended Approach + +**Solution 1** is the most straightforward. The assignment parsing should directly parse the value without calling `walk()`, allowing it to distinguish between function calls and simple expressions. + +This would fix the core issue and make the parser match the grammar specification. \ No newline at end of file diff --git a/js/scripting-lang/test_ambiguous_cases.txt b/js/scripting-lang/test_ambiguous_cases.txt new file mode 100644 index 0000000..96ae555 --- /dev/null +++ b/js/scripting-lang/test_ambiguous_cases.txt @@ -0,0 +1,23 @@ +..out "Testing ambiguous function call detection:"; + +add : x y -> x + y; +double : x -> x * 2; +square : x -> x * x; + +..out "1. Simple function call (should work):"; +result1 : add 3 4; +..out result1; + +..out "2. Two function calls (should work):"; +result2 : add double 3 4; +..out result2; + +..out "3. Ambiguous nested function calls (should error):"; +result3 : add double 3 square 2; +..out result3; + +..out "4. More ambiguous calls (should error):"; +result4 : add double 3 square 2 multiply 4; +..out result4; + +..out "Test complete"; \ 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. |