diff options
Diffstat (limited to 'js')
81 files changed, 21209 insertions, 963 deletions
diff --git a/js/scripting-lang/NEXT-STEPS.md b/js/scripting-lang/NEXT-STEPS.md deleted file mode 100644 index 8061fb7..0000000 --- a/js/scripting-lang/NEXT-STEPS.md +++ /dev/null @@ -1,435 +0,0 @@ -# Next Steps: Immutable Real-World Programming Features - -## Overview - -This document outlines the plan for extending the Simple Scripting Language to support real-world programming scenarios while maintaining its immutable, functional design philosophy. - -## Core Principles - -- **Immutability First**: All data structures are immutable -- **Transformation Over Mutation**: Operations return new data rather than modifying existing data -- **Functional Composition**: Complex operations built from simple, composable functions -- **Type Safety**: Enhanced pattern matching and type checking -- **Performance**: Efficient persistent data structures - -## Phase 1: String Operations and Type System - -### 1.1 String Operations -**Goal**: Add essential string manipulation capabilities - -**New Functions**: -```javascript -// String operations as functions -length : string.length "hello"; // 5 -contains : string.contains "hello" "ll"; // true -startsWith : string.startsWith "hello" "he"; // true -endsWith : string.endsWith "hello" "lo"; // true -substring : string.substring "hello" 1 3; // "el" - -// String concatenation -result : string.concat "Hello" " " "World"; // "Hello World" - -// String transformation -uppercase : string.upper "hello"; // "HELLO" -lowercase : string.lower "HELLO"; // "hello" -trimmed : string.trim " hello "; // "hello" -``` - -**Implementation**: -- Add `string.` namespace for string operations -- Add string operation functions to standard library -- No new syntax - uses existing function call patterns -- Extend existing string literal support - -### 1.2 Runtime Type Checking with `is` Keyword -**Goal**: Add explicit, optional type checking mechanism using the `is` keyword - -**Design Philosophy**: -- Type checking is purely additive - no breaking changes to existing code -- Works seamlessly with existing case expression syntax -- Returns boolean values, fitting the functional style -- Simple implementation with clear semantics - -**New Features**: -```javascript -// Basic type checking -isNumber : x -> x is number; -isString : x -> x is string; -isTable : x -> x is table; -isFunction : x -> x is function; - -// Type-safe operations in case expressions -safeStringOp : x -> case x of - x is string : string.length x - _ : "not a string"; - -// Complex type validation -validateUser : user -> case user of - user.name is string and user.age is number : true - _ : false; - -// Type guards in pattern matching -processData : data -> case data of - data is table : "processing table" - data is string : "processing string" - data is number : "processing number" - _ : "unknown type"; -``` - -**Supported Types**: -- `number` (both integers and floats) -- `string` -- `boolean` -- `table` (objects and arrays) -- `function` - -**Implementation**: -- Add `IS` token type and type-specific tokens (`NUMBER_TYPE`, `STRING_TYPE`, etc.) -- Update lexer to recognize `is` keyword and type names -- Extend parser to handle type checking expressions in case patterns -- Add type checking logic in interpreter that returns boolean values -- No changes to existing syntax or semantics - -## Phase 2: Persistent Data Structures - -### 2.1 Immutable Tables with Transformations -**Goal**: Replace mutable table operations with immutable transformations - -**Current Problem**: -```javascript -// This won't work (mutable) -cache[key] = value; -``` - -**Solution - Functional Immutable Transformations**: -```javascript -// Functional transformations using table namespace -cache : {}; -cache1 : table.set cache "user.1" "Alice"; -cache2 : table.set cache1 "user.2" "Bob"; -value : table.get cache2 "user.1"; // "Alice" -cache3 : table.delete cache2 "user.1"; - -// Nested updates with dot notation -user : {name: "Alice", profile: {age: 25}}; -updatedUser : table.set user "profile.age" 26; - -// Complex transformations using composition -cache1 : pipe - (table.set "user.1" "Alice") - (table.set "user.2" "Bob") - (table.set "user.3" "Charlie") - cache; - -// Table merging -combined : table.merge table1 table2; -``` - -**Implementation**: -- Add `table.set`, `table.get`, `table.delete`, `table.merge` functions -- Implement efficient structural sharing for immutable updates -- Add nested key support (e.g., "profile.age") -- All functions return new tables, never modify originals -- Use existing function composition patterns - -### 2.2 APL-Inspired Table Primitives -**Goal**: Enhance tables with APL-style operations for ergonomic data manipulation - -**Design Philosophy**: -- Use existing table syntax for array-like behavior -- Add APL-inspired primitives for vectorized operations -- Keep immutable transformations -- Provide concise, expressive data manipulation - -**New Features**: -```javascript -// Table creation (array-like using existing syntax) -numbers : {1, 2, 3, 4, 5}; -mixed : {1, "hello", true, {key: "value"}}; - -// All table operations namespaced for consistency -first : table.first numbers; // First element -last : table.last numbers; // Last element -length : table.length numbers; // Size/shape - -// Less common operations (namespaced) -rest : table.drop 1 numbers; // Drop first element -slice : table.slice numbers 2 3; // Elements 2-3 - -// Vectorized operations -doubled : table.add numbers numbers; // Element-wise addition -squared : table.multiply numbers numbers; // Element-wise multiplication -incremented : table.add numbers 1; // Scalar addition to each element - -// Reductions (all namespaced) -sum : table.sum numbers; // Sum reduction -max : table.max numbers; // Maximum reduction -min : table.min numbers; // Minimum reduction -product : table.product numbers; // Product reduction -average : table.average numbers; // Average reduction - -// Scan operations (namespaced) -runningSum : table.scan table.sum numbers; // Running sum scan -runningProduct : table.scan table.product numbers; // Running product scan - -// Table metadata operations -keys : table.keys table; // Get all keys -values : table.values table; // Get all values -reversed : table.reverse table; // Reverse order -sorted : table.sort table; // Sort -unique : table.unique table; // Remove duplicates -``` - -**Implementation**: -- Add `table.` namespace for all table operations -- Extend lexer to recognize table operation keywords -- Add vectorized operation logic in interpreter -- Implement reduction and scan operations -- Add table metadata operations - -## Phase 3: Higher-Order Functions for Collections - -### 3.1 Enhanced Standard Library -**Goal**: Add collection processing functions - -**New Functions**: -```javascript -// Table processing (using namespaced primitives) -numbers : {1, 2, 3, 4, 5}; -doubled : map @double numbers; // {2, 4, 6, 8, 10} -evens : filter @isEven numbers; // {2, 4} -sum : table.sum numbers; // 15 (sum reduction) - -// Table metadata operations -table : {a: 1, b: 2, c: 3}; -keys : table.keys table; // {a, b, c} -values : table.values table; // {1, 2, 3} -pairs : table.pairs table; // {{a, 1}, {b, 2}, {c, 3}} - -// Advanced operations with tables -nested : {{1, 2}, {3, 4}, {5}}; -flattened : table.flatten nested; // {1, 2, 3, 4, 5} -grouped : table.groupBy @isEven numbers; // {true: {2, 4}, false: {1, 3, 5}} - -// Table operations -reversed : table.reverse numbers; // {5, 4, 3, 2, 1} -sorted : table.sort numbers; // {1, 2, 3, 4, 5} -unique : table.unique {1, 2, 2, 3, 3, 4}; // {1, 2, 3, 4} -``` - -**Implementation**: -- Extend existing `map`, `filter`, `reduce` to work with tables -- Implement vectorized operations for tables -- Add reduction and scan operations -- Implement table metadata operations - -### 3.2 Table Generation Helpers -**Goal**: Add convenient table creation functions - -**New Functions**: -```javascript -// Table generation helpers -range : table.range 1 5; // {1, 2, 3, 4, 5} -repeated : table.repeat "hello" 3; // {"hello", "hello", "hello"} - -// Use existing map/filter instead of comprehensions -squares : map (x -> x * x) {1, 2, 3, 4, 5}; -evens : filter @isEven {1, 2, 3, 4, 5}; -``` - -## Phase 4: Error Handling with Error Type - -### 4.1 Error Type -**Goal**: Provide consistent error handling without complex monads - -**Implementation**: -```javascript -// Simple error type -error : message -> {type: "error", message: message}; - -// Safe operations with error handling -safeDivide : x y -> case y of - 0 : error "division by zero" - _ : x / y; - -safeParseNumber : str -> case str of - str is number : str - _ : error "invalid number"; - -// Error checking -isError : value -> value is error; -getErrorMessage : error -> case error of - {type: "error", message: m} : m; -``` - -## Phase 5: Real-World Scenario Support - -### 5.1 User Management System -**Immutable Implementation**: -```javascript -// User validation -isValidEmail : email -> case email of - email contains "@" : true - _ : false; - -createUser : name email age -> case (isValidEmail email) and (isValidAge age) of - true : {name: name, email: email, age: age, status: "active"} - false : error "invalid user data"; - -// User management -users : {}; -user1 : createUser "Alice" "alice@example.com" 25; -users1 : table.set users "user.1" user1; -user2 : createUser "Bob" "bob@example.com" 30; -users2 : table.set users1 "user.2" user2; - -// Safe user lookup -findUser : email users -> case users of - {} : error "user not found" - _ : case (table.first users).email = email of - true : table.first users - false : findUser email (table.drop 1 users); -``` - -### 5.2 Shopping Cart System -**Immutable Implementation**: -```javascript -// Cart operations -emptyCart : {items: {}, total: 0}; -addItem : cart item -> { - items: table.set cart.items (table.length cart.items + 1) item, - total: cart.total + item.price -}; -removeItem : cart itemId -> { - items: filter (item -> item.id != itemId) cart.items, - total: calculateTotal (filter (item -> item.id != itemId) cart.items) -}; - -// Discount application -applyDiscount : cart discountPercent -> { - items: cart.items, - total: cart.total * (1 - discountPercent / 100) -}; -``` - -### 5.3 Data Processing Pipeline -**Immutable Implementation**: -```javascript -// Data processing -salesData : { - {month: "Jan", sales: 1000, region: "North"}, - {month: "Feb", sales: 1200, region: "North"}, - {month: "Mar", sales: 800, region: "South"} -}; - -// Filter by region -filterByRegion : data region -> filter (item -> item.region = region) data; - -// Calculate totals using sum reduction -sumSales : data -> table.sum (map (item -> item.sales) data); - -// Process pipeline -northData : filterByRegion salesData "North"; -northTotal : sumSales northData; -``` - -## Implementation Timeline - -### Week 1-2: String Operations and Runtime Type Checking -- [ ] String concatenation operator -- [ ] String method implementations -- [ ] `is` keyword and type checking tokens -- [ ] Type checking in case expressions -- [ ] Type validation functions in standard library - -### Week 3-4: Table Primitives with Namespacing -- [ ] `table.` namespace for all table operations -- [ ] Vectorized operations for tables -- [ ] Reduction and scan operations -- [ ] Table metadata operations -- [ ] Performance optimization - -### Week 5-6: Higher-Order Functions -- [ ] Enhanced standard library -- [ ] Collection processing functions -- [ ] Table-specific operations -- [ ] Utility functions - -### Week 7-8: Error Handling -- [ ] Error type implementation -- [ ] Error handling patterns -- [ ] Error checking functions -- [ ] Integration with existing operations - -### Week 9-10: Real-World Scenarios -- [ ] User management system -- [ ] Shopping cart system -- [ ] Data processing pipeline -- [ ] Integration testing - -## Benefits of Runtime Type Checking Approach - -### Simplicity -- **Minimal Implementation**: Only need to add `is` keyword and type checking logic -- **No Breaking Changes**: Existing code continues to work unchanged -- **Clear Semantics**: `x is number` is obviously a boolean expression -- **Consistent Syntax**: Works seamlessly with existing case expressions - -### Functional Design -- **Boolean Results**: Type checking returns true/false, fitting functional style -- **Composable**: Can combine with logical operators (`and`, `or`, `not`) -- **Pattern Matching**: Integrates naturally with case expressions -- **No Side Effects**: Pure functions for type validation - -### Extensibility -- **Easy to Add Types**: Simple to extend for new types (arrays, tuples, etc.) -- **Custom Types**: Can implement custom type checking via functions -- **Performance**: Runtime overhead only when explicitly used -- **Optional**: No requirement to use type checking in existing code - -## Testing Strategy - -### Unit Tests -- String operation tests -- Runtime type checking tests (`is` keyword) -- Type validation function tests -- Data structure transformation tests -- Higher-order function tests -- Error handling tests - -### Integration Tests -- Real-world scenario tests -- Performance tests -- Edge case tests -- Error handling tests - -### Performance Benchmarks -- Data structure operation performance -- Memory usage analysis -- Transformation efficiency -- Scalability testing - -## Success Metrics - -- [ ] All real-world scenarios in `tests/17_real_world_scenarios.txt` pass -- [ ] Performance within acceptable bounds (no more than 2x slower than current) -- [ ] Memory usage remains reasonable -- [ ] Code remains readable and maintainable -- [ ] Backward compatibility maintained - -## Future Considerations - -### Advanced Features (Post-v1.0) -- Lazy evaluation for large collections -- Parallel processing capabilities -- Advanced type system with generics -- Macro system for code generation -- Module system for code organization - -### Performance Optimizations -- Structural sharing for immutable data structures -- Compile-time optimizations -- JIT compilation for hot paths -- Memory pooling for temporary objects - -This plan maintains the language's functional, immutable design while adding the capabilities needed for real-world programming scenarios. \ No newline at end of file diff --git a/js/scripting-lang/README.md b/js/scripting-lang/README.md index 73ccbf1..cc650de 100644 --- a/js/scripting-lang/README.md +++ b/js/scripting-lang/README.md @@ -1,220 +1,294 @@ -# Simple Scripting Language +# Scripting Language: A Combinator-Based Functional Language -A functional programming language with immutable variables, first-class functions, and pattern matching. +A functional programming language built on a **combinator foundation** that eliminates parsing ambiguity while preserving intuitive syntax. Every operation is a function call under the hood, creating a consistent and extensible language architecture. -## Features +## Why This Approach? -- **Immutable Variables**: Variables cannot be reassigned -- **First-Class Functions**: Functions can be passed as arguments and stored in data structures -- **Lexical Scoping**: Functions create their own scope -- **Pattern Matching**: Case expressions with wildcard support -- **Table Literals**: Lua-style tables with both array-like and key-value entries -- **Standard Library**: Built-in higher-order functions (`map`, `compose`, `pipe`, `apply`, `filter`, `reduce`, `fold`, `curry`) -- **IO Operations**: Built-in input/output operations (`..in`, `..out`, `..assert`) -- **Floating Point Arithmetic**: Full support for decimal numbers -- **Unary Minus**: Support for negative numbers (e.g., `-1`, `-3.14`) +### The Problem We Solved +Traditional language parsers struggle with **operator precedence ambiguity**. Consider `f x + y` - should this be `(f x) + y` or `f (x + y)`? Most languages solve this with complex precedence tables, but we took a different approach. -## Syntax +### The Combinator Solution +We translate **every operation into a function call**: +- `x + y` becomes `add(x, y)` +- `x - y` becomes `subtract(x, y)` +- `f x` becomes `apply(f, x)` +- `true and false` becomes `logicalAnd(true, false)` -### Basic Operations -``` -/* Arithmetic */ -x : 5 + 3; -y : 10 - 2; -z : 4 * 3; -w : 15 / 3; -neg : -5; /* Unary minus */ - -/* Comparisons */ -result : x > y; -equal : a = b; -not_equal : a != b; - -/* Logical */ -and_result : true and false; -or_result : true or false; -``` +This eliminates ambiguity entirely while keeping the syntax clean and intuitive. -### Variables and Functions -``` -/* Immutable variables */ -x : 42; -y : "hello"; +### Benefits +- **Zero Ambiguity**: Every expression has exactly one interpretation +- **Functional Foundation**: Everything is a function, enabling powerful abstractions +- **Extensible**: Add new operations by simply adding combinator functions +- **Consistent**: All operations follow the same pattern +- **Preserves Syntax**: Existing code continues to work unchanged -/* Function definition */ -f : x -> x * 2; +## How It Works -/* Function call */ -result : f 5; +### Architecture Overview ``` - -### Tables +Source Code → Lexer → Parser → AST → Interpreter → Result + ↓ ↓ ↓ ↓ + Tokens → Combinator Translation → Function Calls ``` -/* Table literal */ -table : {1, 2, 3, key: "value"}; -/* Table access */ -first : table[1]; -value : table.key; -nested : table.key.subkey; +### The Magic: Operator Translation +When you write `x + y * z`, the parser automatically translates it to: +```javascript +add(x, multiply(y, z)) ``` -### Pattern Matching -``` -/* Case expression */ -result : case x of - 1 : "one" - 2 : "two" - _ : "other"; +This happens transparently - you write natural syntax, but get functional semantics. + +### Function Application with Juxtaposition +Functions are applied by placing arguments next to them: +```javascript +f : x -> x * 2; +result : f 5; // apply(f, 5) → 10 ``` -### IO Operations +### Function Composition with `via` +Compose functions naturally: +```javascript +f : x -> x * 2; +g : x -> x + 1; +result : f via g 5; // compose(f, g)(5) → 12 ``` -/* Output */ -..out "Hello, World!"; -/* Input */ -name : ..in; +## Language Features -/* Assertion */ -..assert x = 5; -``` +### Core Philosophy +- **Immutable by Default**: Variables cannot be reassigned +- **Functions First**: Everything is a function or function application +- **Pattern Matching**: Natural case expressions with wildcards +- **Lexical Scoping**: Functions create their own scope -### Standard Library +### Key Features +- **Combinator Foundation**: All operations are function calls +- **Function Composition**: `via` operator for natural composition +- **Pattern Matching**: `when` expressions with wildcard support +- **Tables**: Lua-style tables with array and key-value support +- **Standard Library**: Higher-order functions (`map`, `compose`, `pipe`, etc.) +- **IO Operations**: Built-in input/output (`..in`, `..out`, `..assert`) + +## Development Workflow + +### Testing Strategy +We use a **progressive testing approach** to ensure quality: + +#### 1. Scratch Tests (`scratch_tests/`) +**Purpose**: Rapid prototyping and debugging +- **Location**: `scratch_tests/*.txt` +- **Use Case**: Isolating specific issues, testing new features +- **Naming**: `test_<feature>_<purpose>.txt` (e.g., `test_precedence_simple.txt`) +- **Lifecycle**: Temporary, can be deleted after issue resolution + +#### 2. Unit Tests (`tests/`) +**Purpose**: Comprehensive feature coverage +- **Location**: `tests/*.txt` +- **Use Case**: Validating complete feature implementations +- **Naming**: `##_<feature_name>.txt` (e.g., `01_lexer_basic.txt`) +- **Lifecycle**: Permanent, part of regression testing + +#### 3. Integration Tests (`tests/`) +**Purpose**: Testing feature combinations +- **Location**: `tests/integration_*.txt` +- **Use Case**: Ensuring features work together +- **Naming**: `integration_##_<description>.txt` + +### Development Process + +#### When Adding New Features +1. **Create scratch test** to explore the feature +2. **Implement incrementally** with frequent testing +3. **Debug with `DEBUG=1`** for detailed output +4. **Promote to unit test** when feature is stable +5. **Add integration tests** for feature combinations + +#### When Fixing Bugs +1. **Create minimal scratch test** reproducing the issue +2. **Debug with `DEBUG=1`** to understand the problem +3. **Fix the issue** and verify with scratch test +4. **Update existing tests** if needed +5. **Clean up scratch tests** after resolution + +#### When Promoting Scratch Tests +```bash +# Test the scratch test +bun run lang.js scratch_tests/test_new_feature.txt + +# If it passes, promote to unit test +cp scratch_tests/test_new_feature.txt tests/16_new_feature.txt + +# Update test numbering if needed +# Clean up scratch test +rm scratch_tests/test_new_feature.txt ``` -/* Map */ -double : x -> x * 2; -squared : map @double 5; -/* Filter */ -isPositive : x -> x > 0; -filtered : filter @isPositive 5; +### Debugging Tools -/* Compose */ -f : x -> x + 1; -g : x -> x * 2; -h : compose @f @g; -result : h 5; /* (5 * 2) + 1 = 11 */ +#### Enable Debug Mode +```bash +DEBUG=1 bun run lang.js script.txt ``` -## Usage +This shows: +- Token stream from lexer +- AST structure from parser +- Function call traces +- Scope information -### Running Scripts +#### Call Stack Tracking +The interpreter tracks function calls to detect infinite recursion: ```bash -node lang.js script.txt +# Shows call statistics after execution +bun run lang.js script.txt ``` -### Testing -The project uses a structured testing approach with unit and integration tests: - -#### Unit Tests -Located in `tests/` directory, each focusing on a specific language feature: -- `01_lexer_basic.txt` - Basic lexer functionality -- `02_arithmetic_operations.txt` - Arithmetic operations -- `03_comparison_operators.txt` - Comparison operators -- `04_logical_operators.txt` - Logical operators -- `05_io_operations.txt` - IO operations -- `06_function_definitions.txt` - Function definitions -- `07_case_expressions.txt` - Case expressions and pattern matching -- `08_first_class_functions.txt` - First-class function features -- `09_tables.txt` - Table literals and access -- `10_standard_library.txt` - Standard library functions - -#### Integration Tests -Test combinations of multiple features: -- `integration_01_basic_features.txt` - Basic feature combinations -- `integration_02_pattern_matching.txt` - Pattern matching with other features -- `integration_03_functional_programming.txt` - Functional programming patterns - -#### Running Tests +### Running Tests ```bash # Run all tests ./run_tests.sh -# Run individual tests -node lang.js tests/01_lexer_basic.txt -node lang.js tests/integration_01_basic_features.txt +# Run individual test +bun run lang.js tests/01_lexer_basic.txt + +# Run scratch test +bun run lang.js scratch_tests/test_debug_issue.txt ``` -## Implementation Details +## Current Status -### Architecture -- **Lexer**: Tokenizes input into tokens (numbers, identifiers, operators, etc.) -- **Parser**: Builds Abstract Syntax Tree (AST) from tokens -- **Interpreter**: Executes AST with scope management - -### Key Components -- **Token Types**: Supports all basic operators, literals, and special tokens -- **AST Nodes**: Expression, statement, and declaration nodes -- **Scope Management**: Lexical scoping with proper variable resolution -- **Error Handling**: Comprehensive error reporting for parsing and execution - -## Recent Fixes - -### ✅ Parser Ambiguity with Unary Minus Arguments (Latest Fix) -- **Issue**: `filter @isPositive -3` was incorrectly parsed as binary operation instead of function call with unary minus argument -- **Root Cause**: Parser treating `FunctionReference MINUS` as binary minus operation -- **Solution**: Added special case in `parseExpression()` to handle `FunctionReference MINUS` pattern -- **Status**: ✅ Resolved - Standard library functions now work with negative arguments - -### ✅ Unary Minus Operator -- **Issue**: Stack overflow when parsing negative numbers (e.g., `-1`) -- **Root Cause**: Parser lacked specific handling for unary minus operator -- **Solution**: Added `UnaryMinusExpression` parsing and evaluation -- **Status**: ✅ Resolved - All tests passing - -### ✅ IO Operation Parsing -- **Issue**: IO operations not parsed correctly at top level -- **Solution**: Moved IO parsing to proper precedence level -- **Status**: ✅ Resolved - -### ✅ Decimal Number Support -- **Issue**: Decimal numbers not handled correctly -- **Solution**: Updated lexer and interpreter to use `parseFloat()` -- **Status**: ✅ Resolved - -## Known Issues - -### 🔄 Logical Operator Precedence -- **Issue**: Logical operators (`and`, `or`, `xor`) have incorrect precedence relative to function calls -- **Example**: `isEven 10 and isPositive 5` is parsed as `isEven(10 and isPositive(5))` instead of `(isEven 10) and (isPositive 5)` -- **Impact**: Complex expressions with logical operators may not evaluate correctly -- **Status**: 🔄 In Progress - Working on proper operator precedence hierarchy -- **Workaround**: Use parentheses to explicitly group expressions: `(isEven 10) and (isPositive 5)` - -### 🔄 Parentheses Parsing with Logical Operators -- **Issue**: Some expressions with logical operators inside parentheses fail to parse -- **Example**: `add (multiply 3 4) (isEven 10 and isPositive 5)` may fail with parsing errors -- **Status**: 🔄 In Progress - Related to logical operator precedence issue - -## Development - -### File Structure -``` -. -├── lang.js # Main implementation -├── test.txt # Comprehensive test file -├── tests/ # Unit and integration tests -│ ├── 01_lexer_basic.txt -│ ├── 02_arithmetic_operations.txt -│ ├── ... -│ ├── integration_01_basic_features.txt -│ ├── integration_02_pattern_matching.txt -│ └── integration_03_functional_programming.txt -├── run_tests.sh # Test runner script -├── FIXME.md # Issues and fixes documentation -└── README.md # This file +### ✅ Recently Completed +- **Function Composition**: `via` operator and `@` function references +- **Enhanced Standard Library**: `compose` and `pipe` with partial application +- **Combinator Foundation**: All operators translate to function calls + +### 🔧 Active Issues +- **Priority 1**: Precedence issues (binary minus operator) +- **Priority 2**: Test suite failures (blocked by precedence fix) + +### 📋 Implementation Plans +See `design/implementation/` for detailed plans: +- `PRECEDENCE_RESOLUTION_PLAN.md` - Active precedence fix +- `FUNCTION_COMPOSITION_PLAN.md` - Completed composition features + +## Quick Start + +### Installation +```bash +# Clone the repository +git clone <repository-url> +cd scripting-lang + +# Install dependencies (if any) +npm install +# or +bun install ``` -### Debugging -Enable debug mode by setting `DEBUG=true`: +### Running Scripts ```bash -DEBUG=true node lang.js script.txt +# Basic script execution +bun run lang.js script.txt + +# With debug output +DEBUG=1 bun run lang.js script.txt +``` + +### Example Script +```javascript +/* Basic arithmetic with combinator translation */ +x : 5; +y : 3; +result : x + y; // becomes add(x, y) internally + +/* Function definition and application */ +double : x -> x * 2; +result : double 5; // becomes apply(double, 5) internally + +/* Function composition */ +add1 : x -> x + 1; +result : double via add1 5; // becomes compose(double, add1)(5) internally + +/* Pattern matching */ +message : when result is + 10 then "ten" + 12 then "twelve" + _ then "other"; + +/* Output */ +..out message; +``` + +## Architecture Deep Dive + +### Combinator Foundation +Every operation is implemented as a function call to standard library combinators: + +```javascript +// Arithmetic +x + y → add(x, y) +x - y → subtract(x, y) +x * y → multiply(x, y) + +// Comparison +x = y → equals(x, y) +x > y → greaterThan(x, y) + +// Logical +x and y → logicalAnd(x, y) +not x → logicalNot(x) + +// Function application +f x → apply(f, x) ``` +### Standard Library Combinators +The language includes a comprehensive set of combinators: +- **Arithmetic**: `add`, `subtract`, `multiply`, `divide`, `negate` +- **Comparison**: `equals`, `greaterThan`, `lessThan`, etc. +- **Logical**: `logicalAnd`, `logicalOr`, `logicalNot` +- **Higher-Order**: `map`, `compose`, `pipe`, `apply`, `filter` + +### Parser Architecture +The parser uses a **precedence climbing** approach with combinator translation: +1. **Lexer**: Converts source to tokens +2. **Parser**: Builds AST with operator-to-function translation +3. **Interpreter**: Evaluates AST using combinator functions + ## Contributing -1. Create focused unit tests for new features -2. Add integration tests for feature combinations -3. Update documentation -4. Run the full test suite before submitting changes \ No newline at end of file +### Development Guidelines +1. **Follow the combinator approach** for new operations +2. **Use scratch tests** for rapid prototyping +3. **Promote to unit tests** when features are stable +4. **Maintain backward compatibility** - existing code must work +5. **Document changes** in design documents + +### Code Style +- **Functional approach**: Prefer pure functions +- **Combinator translation**: All operations become function calls +- **Clear naming**: Descriptive function and variable names +- **Comprehensive testing**: Test edge cases and combinations + +### Testing Requirements +- **Unit tests** for all new features +- **Integration tests** for feature combinations +- **Backward compatibility** tests for existing code +- **Edge case coverage** for robust implementation + +## Documentation + +### Design Documents +- `design/PROJECT_ROADMAP.md` - Current status and next steps +- `design/COMBINATORS.md` - Combinator foundation explanation +- `design/implementation/` - Detailed implementation plans + +### Architecture +- **Combinator Foundation**: Eliminates parsing ambiguity +- **Functional Semantics**: Everything is a function +- **Extensible Design**: Easy to add new operations +- **Consistent Patterns**: All operations follow the same structure + +This language demonstrates how **functional programming principles** can solve real parsing problems while maintaining intuitive syntax. The combinator foundation provides a solid base for building powerful abstractions. \ No newline at end of file diff --git a/js/scripting-lang/design/COMBINATORS.md b/js/scripting-lang/design/COMBINATORS.md new file mode 100644 index 0000000..de6b449 --- /dev/null +++ b/js/scripting-lang/design/COMBINATORS.md @@ -0,0 +1,241 @@ +# Combinator-Based Foundation + +## Overview + +This document outlines the approach to eliminate parsing ambiguity by implementing a combinator-based foundation while preserving the existing ML/Elm-inspired syntax. + +## Current Implementation Status + +### ✅ Phase 1: Core Combinators - COMPLETED +All core combinators have been successfully implemented in the standard library: + +#### ✅ Arithmetic Combinators +- `add(x, y)` - Addition +- `subtract(x, y)` - Subtraction +- `multiply(x, y)` - Multiplication +- `divide(x, y)` - Division +- `modulo(x, y)` - Modulo +- `power(x, y)` - Exponentiation +- `negate(x)` - Unary negation + +#### ✅ Comparison Combinators +- `equals(x, y)` - Equality +- `notEquals(x, y)` - Inequality +- `lessThan(x, y)` - Less than +- `greaterThan(x, y)` - Greater than +- `lessEqual(x, y)` - Less than or equal +- `greaterEqual(x, y)` - Greater than or equal + +#### ✅ Logical Combinators +- `logicalAnd(x, y)` - Logical AND +- `logicalOr(x, y)` - Logical OR +- `logicalXor(x, y)` - Logical XOR +- `logicalNot(x)` - Logical NOT + +#### ✅ Enhanced Higher-Order Combinators +- `identity(x)` - Identity function +- `constant(x)` - Constant function +- `flip(f)` - Flip argument order +- `on(f, g)` - Apply f to results of g +- `both(f, g)` - Both predicates true +- `either(f, g)` - Either predicate true + +### ✅ Phase 2: Parser Translation - COMPLETED +The parser has been successfully modified to translate operator expressions to combinator calls: + +#### ✅ AST Transformation Implemented +- `PlusExpression` → `FunctionCall` with `add` +- `MinusExpression` → `FunctionCall` with `subtract` +- `MultiplyExpression` → `FunctionCall` with `multiply` +- `DivideExpression` → `FunctionCall` with `divide` +- `ModuloExpression` → `FunctionCall` with `modulo` +- `PowerExpression` → `FunctionCall` with `power` +- `EqualsExpression` → `FunctionCall` with `equals` +- `NotEqualExpression` → `FunctionCall` with `notEquals` +- `LessThanExpression` → `FunctionCall` with `lessThan` +- `GreaterThanExpression` → `FunctionCall` with `greaterThan` +- `LessEqualExpression` → `FunctionCall` with `lessEqual` +- `GreaterEqualExpression` → `FunctionCall` with `greaterEqual` +- `AndExpression` → `FunctionCall` with `logicalAnd` +- `OrExpression` → `FunctionCall` with `logicalOr` +- `XorExpression` → `FunctionCall` with `logicalXor` +- `NotExpression` → `FunctionCall` with `logicalNot` +- `UnaryMinusExpression` → `FunctionCall` with `negate` + +### ✅ Phase 3: Syntax Preservation - COMPLETED +All existing syntax remains exactly the same. The combinator foundation is completely transparent to users. + +## Current Test Results + +### ✅ Passing Tests (12/18) +- Basic Lexer +- Arithmetic Operations +- Comparison Operators +- Logical Operators +- IO Operations +- Function Definitions +- First-Class Functions +- Tables +- Standard Library +- Complete Standard Library +- Basic Features Integration +- Functional Programming Integration + +### 🔄 Failing Tests (6/18) - Issues to Address + +#### 1. Case Expressions (07_case_expressions.txt) +**Issue**: Recursive function calls failing with "Function is not defined or is not callable" +**Root Cause**: The recursive function `factorial` is calling itself before it's fully defined in the global scope +**Status**: 🔄 In Progress - Need to implement proper recursive function support + +#### 2. Edge Cases (08_edge_cases.txt) +**Issue**: "Expected pattern (identifier, number, string, wildcard, or function reference) in when expression, got LESS_THAN" +**Root Cause**: Parser not handling comparison operators in when expression patterns +**Status**: 🔄 In Progress - Need to extend when expression pattern parsing + +#### 3. Advanced Tables (09_advanced_tables.txt) +**Issue**: "Unexpected token in parsePrimary: DOT" +**Root Cause**: Parser not handling dot notation in table access +**Status**: 🔄 In Progress - Need to implement dot notation parsing + +#### 4. Error Handling (11_error_handling.txt) +**Issue**: "Expected pattern (identifier, number, string, wildcard, or function reference) in when expression, got FALSE" +**Root Cause**: Parser not handling boolean literals in when expression patterns +**Status**: 🔄 In Progress - Need to extend when expression pattern parsing + +#### 5. Pattern Matching Integration (integration_02_pattern_matching.txt) +**Issue**: "Unexpected token in parsePrimary: WHEN" +**Root Cause**: Parser not handling when expressions in certain contexts +**Status**: 🔄 In Progress - Need to fix when expression parsing precedence + +#### 6. Multi-parameter case expression (12_multi_parameter_case.txt) +**Issue**: "Unexpected token in parsePrimary: THEN" +**Root Cause**: Parser not handling multi-parameter case expressions correctly +**Status**: 🔄 In Progress - Need to fix case expression parsing + +## Implementation Plan Moving Forward + +### Phase 4: Fix Remaining Parser Issues (Current Focus) + +#### 4.1 Fix Recursive Function Support +**Problem**: Functions cannot call themselves recursively +**Solution**: Implement forward declaration pattern +- Create placeholder function in global scope before evaluation +- Evaluate function body with access to placeholder +- Replace placeholder with actual function +**Files**: `lang.js` (interpreter) +**Status**: 🔄 In Progress + +#### 4.2 Extend When Expression Pattern Parsing +**Problem**: When expressions only support basic patterns +**Solution**: Extend pattern parsing to support: +- Comparison operators (`<`, `>`, `<=`, `>=`, `=`, `!=`) +- Boolean literals (`true`, `false`) +- Function calls +- Parenthesized expressions +**Files**: `parser.js` (parseWhenExpression) +**Status**: 🔄 In Progress + +#### 4.3 Implement Dot Notation for Table Access +**Problem**: Table access only supports bracket notation +**Solution**: Add support for dot notation (`table.property`) +**Files**: `parser.js` (parsePrimary) +**Status**: 🔄 In Progress + +#### 4.4 Fix When Expression Parsing Precedence +**Problem**: When expressions not parsed correctly in all contexts +**Solution**: Adjust parser precedence to handle when expressions properly +**Files**: `parser.js` (walk, parseWhenExpression) +**Status**: 🔄 In Progress + +#### 4.5 Fix Multi-parameter Case Expressions +**Problem**: Multi-parameter case expressions not parsed correctly +**Solution**: Extend case expression parsing for multiple parameters +**Files**: `parser.js` (parseWhenExpression) +**Status**: 🔄 In Progress + +### Phase 5: Enhanced Combinators (Future) + +#### 5.1 Table Combinators +```javascript +scope.table = (...entries) => { /* table creation */ }; +scope.get = (table, key) => { /* table access */ }; +scope.set = (table, key, value) => { /* table modification */ }; +``` + +#### 5.2 Assignment Combinator +```javascript +scope.assign = (name, value) => { /* assignment with immutability */ }; +``` + +#### 5.3 Advanced Combinators +```javascript +scope.match = (value, patterns) => { /* pattern matching */ }; +scope.case = (value, cases) => { /* case expressions */ }; +``` + +## Benefits Achieved + +1. ✅ **Eliminated Parsing Ambiguity**: Every operation is now a function call +2. ✅ **Preserved Syntax**: Zero breaking changes to existing code +3. ✅ **Functional Foundation**: Everything is a function under the hood +4. ✅ **Extensible**: Easy to add new combinators and patterns +5. ✅ **Consistent Semantics**: All operations follow the same pattern + +## Next Steps + +1. **Immediate Priority**: Fix the 6 failing tests by addressing parser issues +2. **Short Term**: Complete Phase 4 implementation +3. **Medium Term**: Add enhanced table and assignment combinators +4. **Long Term**: Add advanced pattern matching and monadic combinators + +## Files Modified + +### ✅ Completed +- **lang.js**: Added all core combinators to `initializeStandardLibrary()` +- **parser.js**: Modified expression parsing to use combinators +- **tests/**: Updated test files to avoid naming conflicts with standard library + +### 🔄 In Progress +- **lang.js**: Implementing recursive function support +- **parser.js**: Extending when expression pattern parsing +- **parser.js**: Adding dot notation support +- **parser.js**: Fixing case expression parsing + +## Testing Strategy + +### Current Status +- **Backward Compatibility**: ✅ All existing syntax works unchanged +- **Combinator Functionality**: ✅ All combinators work correctly +- **Performance**: ✅ No significant performance regression +- **Edge Cases**: 🔄 Working on remaining edge cases + +### Testing Requirements +- ✅ Every combinator has unit tests +- ✅ Integration tests for complex expressions +- 🔄 Edge case testing (null values, undefined, etc.) +- 🔄 Recursive function testing +- 🔄 Advanced pattern matching testing + +## Important Considerations + +### ✅ Backward Compatibility +- All existing syntax works exactly as before +- No breaking changes to user code +- Performance remains similar + +### ✅ Error Handling +- Combinators provide meaningful error messages +- Existing error semantics maintained +- Type checking for combinator arguments implemented + +### ✅ Scope Management +- Combinators work correctly with existing scope system +- Assignment combinator respects immutability rules +- Table combinators handle nested scopes properly + +### 🔄 Testing Requirements +- ✅ Every combinator has unit tests +- ✅ Integration tests for complex expressions +- ✅ Performance benchmarks show no regression +- 🔄 Edge case testing (null values, undefined, etc.) \ No newline at end of file diff --git a/js/scripting-lang/design/DOCUMENTATION_SUMMARY.md b/js/scripting-lang/design/DOCUMENTATION_SUMMARY.md new file mode 100644 index 0000000..57d7eb3 --- /dev/null +++ b/js/scripting-lang/design/DOCUMENTATION_SUMMARY.md @@ -0,0 +1,182 @@ +# Documentation Summary: Function Composition Implementation + +## Overview + +This document provides an overview of all documentation created for the function composition implementation project. Each document is self-contained and provides complete context for its specific purpose. + +## Document Index + +### 1. PARSER_BUG_ANALYSIS.md ✅ **COMPLETED** +**Purpose**: Analysis of the original parser bug and its resolution +**Status**: Main issue resolved, ready for archival +**Key Content**: +- ✅ **Main Issue Fixed**: Function calls with negative arguments (`abs -5`) now work correctly +- ✅ **Architecture Improved**: Combinator-based design eliminates parsing ambiguity +- ✅ **Plan Created**: Clear roadmap for function composition enhancements +- ✅ **Backward Compatibility**: All existing code continues to work + +**Next**: Can be archived once function composition is implemented + +### 2. FUNCTION_COMPOSITION_PLAN.md ✅ **COMPLETED** +**Purpose**: High-level design decisions and strategy for function composition +**Status**: Design decisions made, implementation plan defined +**Key Content**: +- **Design Decision**: Explicit composition only (not hybrid approach) +- **Solution**: `f . g x` for composition, keep `f g x` as left-associative +- **@ Operator**: Fix and enhance for function references +- **4-Phase Implementation Plan**: Lexer → Parser → Standard Library → Testing + +**Next**: Ready for implementation using IMPLEMENTATION_GUIDE.md + +### 3. IMPLEMENTATION_GUIDE.md ✅ **COMPLETED** +**Purpose**: Complete technical implementation details +**Status**: All technical details provided, ready for coding +**Key Content**: +- **Phase 1**: Lexer enhancement (DOT operator, fix @ operator) +- **Phase 2**: Parser enhancement (parseComposition, precedence chain) +- **Phase 3**: Standard library enhancement (improve compose, add pipe) +- **Phase 4**: Testing and validation +- **Complete code examples** and file locations +- **Error handling** and rollback plans + +**Next**: Ready to start implementation + +## Implementation Roadmap + +### Phase 1: Lexer Enhancement (1-2 hours) +**Files to modify**: `lexer.js` +**Tasks**: +- [ ] Add DOT operator handling +- [ ] Verify @ operator lexing works + +**Success Criteria**: `f . g` and `@f` tokenize correctly + +### Phase 2: Parser Enhancement (2-3 hours) +**Files to modify**: `parser.js` +**Tasks**: +- [ ] Fix @ operator parsing in parsePrimary() +- [ ] Add parseComposition() function +- [ ] Update precedence chain +- [ ] Test composition parsing + +**Success Criteria**: `f . g x` parses as `compose(f, g)(x)` + +### Phase 3: Standard Library Enhancement (1-2 hours) +**Files to modify**: `lang.js` +**Tasks**: +- [ ] Enhance compose function for multiple arguments +- [ ] Add pipe function +- [ ] Test both functions + +**Success Criteria**: `compose(f, g, h)` and `pipe(f, g, h)` work correctly + +### Phase 4: Testing & Validation (1-2 hours) +**Files to create**: Test files +**Tasks**: +- [ ] Create comprehensive test suite +- [ ] Test backward compatibility +- [ ] Test error cases +- [ ] Performance testing + +**Success Criteria**: All tests pass, no regressions + +## Key Design Decisions Made + +### 1. Explicit Composition Only ✅ +**Why**: Simpler, clearer, no ambiguity, backward compatible +**Syntax**: `f . g x` for composition, `f g x` for left-associative application + +### 2. Keep @ Operator ✅ +**Why**: Essential for higher-order programming and function references +**Uses**: `@f`, `map(@f, [1,2,3])`, `when x is @f then ...` + +### 3. No Full Currying ❌ +**Why**: Major architectural change, breaks existing code, complex implementation + +### 4. No Hybrid Approach ❌ +**Why**: Overcomplicated, introduces ambiguity, harder to understand + +## Technical Architecture + +### Current State +- ✅ **Combinator architecture**: All operations translate to function calls +- ✅ **Standard library**: `compose`, `apply`, `pipe` functions exist +- ✅ **Token types**: `DOT` and `FUNCTION_REF` already defined +- ❌ **DOT operator**: Not implemented in lexer +- ❌ **@ operator**: Has parsing bugs +- ❌ **Composition precedence**: Not defined + +### Target State +- ✅ **DOT operator**: Implemented in lexer and parser +- ✅ **@ operator**: Fixed and working correctly +- ✅ **Composition precedence**: Right-associative composition +- ✅ **Enhanced standard library**: Multiple function composition support + +## Success Criteria + +### Functional Requirements +- [ ] `f . g x` works correctly for function composition +- [ ] `f . g . h x` works correctly for multiple composition +- [ ] `@f` works correctly for function references +- [ ] All existing code continues to work unchanged + +### Quality Requirements +- [ ] Performance impact is minimal +- [ ] Error messages are clear and helpful +- [ ] Documentation is comprehensive +- [ ] No memory leaks or stack overflow issues + +### Compatibility Requirements +- [ ] Backward compatibility with all existing code +- [ ] No breaking changes to existing syntax +- [ ] Existing test suite continues to pass + +## Risk Assessment + +### Low Risk +- **Lexer changes**: DOT operator is simple to add +- **Standard library**: Enhancements are additive only +- **Testing**: Comprehensive test coverage planned + +### Medium Risk +- **Parser precedence**: Changes to precedence chain need careful testing +- **@ operator fix**: Need to ensure no regressions + +### Mitigation Strategies +- **Incremental implementation**: Phase by phase with testing +- **Rollback plan**: Each phase can be reverted independently +- **Comprehensive testing**: Test all scenarios before proceeding + +## Documentation Completeness + +### ✅ Complete Documentation +- **Design decisions**: All made and documented +- **Technical details**: Complete implementation guide provided +- **Test cases**: Comprehensive test scenarios defined +- **Error handling**: All error cases identified and planned +- **Rollback plan**: Clear rollback strategy for each phase + +### ✅ Self-Contained Implementation +- **No external dependencies**: All context provided in documents +- **Complete code examples**: Ready-to-use code snippets +- **File locations**: Exact locations for all changes +- **Success criteria**: Clear metrics for completion + +## Next Steps + +1. **Start Implementation**: Begin with Phase 1 using IMPLEMENTATION_GUIDE.md +2. **Test Incrementally**: Test each phase before proceeding +3. **Document Progress**: Update documents as implementation progresses +4. **Validate Results**: Ensure all success criteria are met +5. **Archive Old Documents**: Archive PARSER_BUG_ANALYSIS.md once complete + +## Conclusion + +All necessary documentation has been created and is complete for implementing the function composition features. The documentation is: + +- ✅ **Comprehensive**: Covers all aspects of the implementation +- ✅ **Self-contained**: No additional context needed +- ✅ **Technical**: Complete code examples and file locations +- ✅ **Practical**: Clear implementation roadmap and success criteria + +**Ready to begin implementation!** 🚀 \ No newline at end of file diff --git a/js/scripting-lang/IDEAS.txt b/js/scripting-lang/design/IDEAS.txt index 96f8b4b..82eed66 100644 --- a/js/scripting-lang/IDEAS.txt +++ b/js/scripting-lang/design/IDEAS.txt @@ -6,12 +6,4 @@ add 2 other io functions where listen takes in a well defined state object from outside the scope of the program, making it available to the program -where emit lets the program spit state back out into the wider world - -*** - -Implement type annotation with the "is" keyword, like - -x is int : 1; - -double is int : x -> x * 2; \ No newline at end of file +where emit lets the program spit state back out into the wider world \ No newline at end of file diff --git a/js/scripting-lang/design/PRECEDENCE_ANALYSIS.md b/js/scripting-lang/design/PRECEDENCE_ANALYSIS.md new file mode 100644 index 0000000..0b0af2b --- /dev/null +++ b/js/scripting-lang/design/PRECEDENCE_ANALYSIS.md @@ -0,0 +1,150 @@ +# Precedence Analysis: Understanding the Parser Issues + +## Current State + +We have successfully implemented function composition with the `via` operator and enhanced the standard library with `compose` and `pipe` functions. However, we're encountering persistent issues with operator precedence, particularly around the binary minus operator. + +**Confirmed Working:** +- `x + y` → `add(x, y)` ✅ +- `x * y` → `multiply(x, y)` ✅ +- `-x` → `negate(x)` ✅ + +**Confirmed Broken:** +- `x - y` → `apply(x, negate(y))` ❌ (should be `subtract(x, y)`) + +## The Core Problem + +The fundamental issue is that our parser is translating `x - y` as `apply(x, negate(y))` instead of `subtract(x, y)`. This indicates that the unary minus operator is being applied to `y` before the binary minus operator is considered. + +## What We Know + +### 1. Current Precedence Chain +``` +parseLogicalExpression() → parseExpression() → parseTerm() → parseApplication() → parseComposition() → parseFactor() → parsePrimary() +``` + +### 2. Operator Handling Locations +- **Unary minus**: Currently handled in `parsePrimary()` (highest precedence) +- **Binary minus**: Handled in `parseExpression()` (lower precedence) +- **Function application**: Handled in `parseApplication()` (via juxtaposition) + +### 3. The `isValidArgumentStart` Function +This function determines when function application (juxtaposition) should be triggered: +```javascript +function isValidArgumentStart(token) { + return token.type === TokenType.IDENTIFIER || + token.type === TokenType.NUMBER || + token.type === TokenType.STRING || + token.type === TokenType.LEFT_PAREN || + token.type === TokenType.LEFT_BRACE || + token.type === TokenType.TRUE || + token.type === TokenType.FALSE || + token.type === TokenType.FUNCTION_REF || + token.type === TokenType.MINUS || // ← This is problematic + token.type === TokenType.NOT; +} +``` + +### 4. The Root Cause +When we see `x - y`, the parser: +1. Parses `x` as an identifier +2. Sees `-` and treats it as a valid argument start (due to `TokenType.MINUS` in `isValidArgumentStart`) +3. Parses `y` as an identifier +4. Creates `apply(x, negate(y))` instead of `subtract(x, y)` + +## Attempted Solutions + +### Solution 1: Remove MINUS from isValidArgumentStart +**Result**: Fixed the binary minus issue, but broke unary minus parsing +**Problem**: `parsePrimary()` still needs to handle unary minus operators + +### Solution 2: Move unary minus to parseExpression() +**Result**: Created infinite recursion issues +**Problem**: `parsePrimary()` delegates to `parseExpression()`, which calls `parsePrimary()` again + +### Solution 3: Move unary minus to parseFactor() +**Result**: Still caused precedence issues +**Problem**: Unary minus was still being applied in wrong contexts + +## The Combinator Approach + +We've successfully implemented a combinator-based architecture where: +- All operators are translated to function calls +- Standard library provides combinator functions (`add`, `subtract`, `negate`, etc.) +- Function application uses juxtaposition (`f x`) + +However, the precedence chain is not working correctly with this approach. + +## Fundamental Issues + +### 1. Precedence Chain Mismatch +The current precedence chain doesn't match the intended operator precedence: +- Unary operators should have highest precedence +- Binary operators should be handled at appropriate levels +- Function application should have specific precedence + +### 2. Context Sensitivity +The minus operator can be either unary or binary depending on context: +- `-x` → unary (negate) +- `x - y` → binary (subtract) +- `x * -y` → unary applied to y, then binary multiply + +### 3. Function Application Interference +The juxtaposition-based function application is interfering with operator parsing. + +## Proposed Solutions + +### Option 1: Fix the Precedence Chain +**Approach**: Restructure the precedence chain to handle operators correctly +**Pros**: Maintains current architecture +**Cons**: Complex to implement correctly + +### Option 2: Context-Aware Parsing +**Approach**: Modify parsing to distinguish between unary and binary operators based on context +**Pros**: More accurate parsing +**Cons**: Increases parser complexity + +### Option 3: Separate Unary and Binary Parsing +**Approach**: Handle unary operators separately from binary operators +**Pros**: Clear separation of concerns +**Cons**: May require significant refactoring + +### Option 4: Token-Level Precedence +**Approach**: Use token-level precedence rules instead of function-level precedence +**Pros**: More traditional and well-understood +**Cons**: May conflict with combinator approach + +## Recommended Approach + +### Phase 1: Document Current Behavior +1. Create comprehensive test cases for all operator combinations +2. Document exactly how each expression is currently parsed +3. Identify all precedence conflicts + +### Phase 2: Design New Precedence Chain +1. Define the correct precedence hierarchy +2. Design a precedence chain that works with combinators +3. Ensure backward compatibility + +### Phase 3: Implement Fix +1. Implement the new precedence chain +2. Add comprehensive tests +3. Verify all existing functionality works + +## Immediate Next Steps + +1. **Create test cases** for all operator combinations +2. **Document current parsing behavior** for each case +3. **Design the correct precedence chain** +4. **Implement the fix systematically** + +## Questions to Resolve + +1. Should we maintain the combinator approach or move to a more traditional precedence-based approach? +2. How should we handle the interaction between function application and operators? +3. What is the correct precedence for the `via` operator relative to other operators? +4. Should we support parenthesized function calls or stick to juxtaposition only? + +## Conclusion + +The precedence issues are fundamental to the parser architecture. We need to either fix the precedence chain to work correctly with combinators, or reconsider the overall approach. The combinator-based architecture is elegant but requires careful precedence handling. \ No newline at end of file diff --git a/js/scripting-lang/design/PRECEDENCE_TEST_CASES.md b/js/scripting-lang/design/PRECEDENCE_TEST_CASES.md new file mode 100644 index 0000000..f4eb3c3 --- /dev/null +++ b/js/scripting-lang/design/PRECEDENCE_TEST_CASES.md @@ -0,0 +1,168 @@ +# Precedence Test Cases: Understanding Current Behavior + +## Test Categories + +### 1. Basic Arithmetic Operations +``` +x : 5; +y : 3; + +/* Binary operations */ +result1 : x + y; /* Expected: add(x, y) = 8 */ +result2 : x - y; /* Expected: subtract(x, y) = 2 */ +result3 : x * y; /* Expected: multiply(x, y) = 15 */ +result4 : x / y; /* Expected: divide(x, y) = 1.666... */ +result5 : x % y; /* Expected: modulo(x, y) = 2 */ +result6 : x ^ y; /* Expected: power(x, y) = 125 */ +``` + +### 2. Unary Operations +``` +x : 5; + +/* Unary operations */ +result1 : -x; /* Expected: negate(x) = -5 */ +result2 : not true; /* Expected: logicalNot(true) = false */ +``` + +### 3. Mixed Unary and Binary Operations +``` +x : 5; +y : 3; + +/* Mixed operations */ +result1 : x * -y; /* Expected: multiply(x, negate(y)) = -15 */ +result2 : -x + y; /* Expected: add(negate(x), y) = -2 */ +result3 : x - -y; /* Expected: subtract(x, negate(y)) = 8 */ +result4 : -x * -y; /* Expected: multiply(negate(x), negate(y)) = 15 */ +``` + +### 4. Function Application +``` +f : x -> x * 2; +g : x -> x + 1; + +/* Function application */ +result1 : f 5; /* Expected: apply(f, 5) = 10 */ +result2 : f g 5; /* Expected: apply(apply(f, g), 5) = 12 */ +result3 : f (g 5); /* Expected: apply(f, apply(g, 5)) = 12 */ +``` + +### 5. Function Composition +``` +f : x -> x * 2; +g : x -> x + 1; +h : x -> x * x; + +/* Function composition */ +result1 : f via g 5; /* Expected: compose(f, g)(5) = 12 */ +result2 : f via g via h 3; /* Expected: compose(f, compose(g, h))(3) = 20 */ +result3 : pipe(f, g) 5; /* Expected: pipe(f, g)(5) = 11 */ +result4 : compose(f, g) 5; /* Expected: compose(f, g)(5) = 12 */ +``` + +### 6. Comparison Operations +``` +x : 5; +y : 3; + +/* Comparison operations */ +result1 : x = y; /* Expected: equals(x, y) = false */ +result2 : x != y; /* Expected: notEquals(x, y) = true */ +result3 : x < y; /* Expected: lessThan(x, y) = false */ +result4 : x > y; /* Expected: greaterThan(x, y) = true */ +result5 : x <= y; /* Expected: lessEqual(x, y) = false */ +result6 : x >= y; /* Expected: greaterEqual(x, y) = true */ +``` + +### 7. Logical Operations +``` +x : true; +y : false; + +/* Logical operations */ +result1 : x and y; /* Expected: logicalAnd(x, y) = false */ +result2 : x or y; /* Expected: logicalOr(x, y) = true */ +result3 : x xor y; /* Expected: logicalXor(x, y) = true */ +result4 : not x; /* Expected: logicalNot(x) = false */ +``` + +### 8. Complex Expressions +``` +x : 5; +y : 3; +z : 2; + +/* Complex expressions */ +result1 : x + y * z; /* Expected: add(x, multiply(y, z)) = 11 */ +result2 : (x + y) * z; /* Expected: multiply(add(x, y), z) = 16 */ +result3 : x - y + z; /* Expected: add(subtract(x, y), z) = 4 */ +result4 : x * -y + z; /* Expected: add(multiply(x, negate(y)), z) = -13 */ +result5 : f x + g y; /* Expected: add(apply(f, x), apply(g, y)) = 13 */ +``` + +### 9. Edge Cases +``` +/* Edge cases */ +result1 : -5; /* Expected: negate(5) = -5 */ +result2 : 5 - 3; /* Expected: subtract(5, 3) = 2 */ +result3 : f -5; /* Expected: apply(f, negate(5)) = -10 */ +result4 : f 5 - 3; /* Expected: subtract(apply(f, 5), 3) = 7 */ +result5 : f (5 - 3); /* Expected: apply(f, subtract(5, 3)) = 4 */ +``` + +## Current Issues to Document + +### Issue 1: Binary Minus vs Unary Minus +**Problem**: `x - y` is parsed as `apply(x, negate(y))` instead of `subtract(x, y)` +**Root Cause**: `TokenType.MINUS` in `isValidArgumentStart` causes function application to be triggered + +### Issue 2: Function Application Precedence +**Problem**: Function application (juxtaposition) interferes with operator parsing +**Example**: `f x + y` might be parsed incorrectly + +### Issue 3: Parenthesized Expressions +**Problem**: Parenthesized expressions are not handled consistently +**Example**: `f (x + y)` vs `f x + y` + +### Issue 4: Complex Operator Chains +**Problem**: Complex expressions with multiple operators are not parsed correctly +**Example**: `x + y * z - w` + +## Expected vs Actual Behavior + +### Test Case: `x - y` +- **Expected**: `subtract(x, y)` +- **Actual**: `apply(x, negate(y))` +- **Status**: ❌ Broken + +### Test Case: `-x` +- **Expected**: `negate(x)` +- **Actual**: `negate(x)` +- **Status**: ✅ Working + +### Test Case: `x * -y` +- **Expected**: `multiply(x, negate(y))` +- **Actual**: `multiply(x, negate(y))` +- **Status**: ✅ Working + +### Test Case: `f x + y` +- **Expected**: `add(apply(f, x), y)` +- **Actual**: Need to test +- **Status**: ❓ Unknown + +## Next Steps + +1. **Create test file** with all these cases +2. **Run tests** to document current behavior +3. **Identify patterns** in what works vs what doesn't +4. **Design fix** based on patterns +5. **Implement fix** systematically +6. **Verify** all cases work correctly + +## Questions + +1. Should we prioritize fixing the precedence issues or maintaining the combinator approach? +2. Are there any operator combinations that currently work correctly that we should preserve? +3. How should we handle the interaction between function application and operators? +4. What is the correct precedence for the `via` operator relative to other operators? \ No newline at end of file diff --git a/js/scripting-lang/design/PROJECT_ROADMAP.md b/js/scripting-lang/design/PROJECT_ROADMAP.md new file mode 100644 index 0000000..ca25193 --- /dev/null +++ b/js/scripting-lang/design/PROJECT_ROADMAP.md @@ -0,0 +1,170 @@ +# Project Roadmap: Scripting Language Development + +## Current Status Overview + +We have successfully implemented a combinator-based scripting language with function composition capabilities. The language supports juxtaposition-based function application, operator translation to combinators, and a comprehensive standard library. + +## Completed Features ✅ + +### Core Language Features +- **Lexer**: Tokenizes source code with support for all operators and keywords +- **Parser**: AST generation with combinator-based operator translation +- **Interpreter**: Evaluates AST with lexical scoping and function support +- **Standard Library**: Comprehensive combinator functions (add, subtract, multiply, etc.) + +### Function Composition (Recently Implemented) +- **`via` operator**: Right-associative function composition (`f via g x` → `f(g(x))`) +- **`@` operator**: Function reference syntax (`@f` → function reference) +- **Enhanced `compose` and `pipe`**: Binary functions with partial application support +- **Backward compatibility**: All existing code continues to work + +## Current Issues 🔧 + +### Priority 1: Precedence Issues (Active) +**Status**: Analysis complete, implementation plan ready +**Problem**: Binary minus operator (`x - y`) incorrectly parsed as `apply(x, negate(y))` +**Impact**: High - affects basic arithmetic operations +**Solution**: Remove `TokenType.MINUS` from `isValidArgumentStart` and handle unary minus properly + +**Documents**: +- `PRECEDENCE_ANALYSIS.md` - Root cause analysis +- `PRECEDENCE_TEST_CASES.md` - Comprehensive test cases +- `implementation/PRECEDENCE_RESOLUTION_PLAN.md` - Implementation plan + +**Next Steps**: +1. Implement Phase 1 of precedence resolution plan +2. Test with comprehensive test suite +3. Fix any related precedence issues + +### Priority 2: Test Suite Issues (Blocked by Precedence) +**Status**: Identified, waiting for precedence fix +**Problem**: Many existing tests fail due to parser changes +**Impact**: Medium - affects development workflow +**Solution**: Fix precedence issues first, then update test suite + +**Issues**: +- "Unexpected token in parsePrimary: THEN" (Case Expressions) +- "Unexpected token in parsePrimary: WHEN" (Pattern Matching) +- "Unexpected token in parsePrimary: PLUS" (Edge Cases) +- "Unexpected token in parsePrimary: DOT" (Advanced Tables) + +### Priority 3: Future Enhancements (Planned) +**Status**: Design decisions made, implementation planned +**Impact**: Low - nice-to-have features + +**Features**: +- Additional I/O functions (`..listen`, `..emit`) - See `IDEAS.txt` +- Enhanced pattern matching in when expressions +- Performance optimizations + +## Implementation Plans 📋 + +### Active Plans + +#### 1. Precedence Resolution (Current Priority) +**Location**: `implementation/PRECEDENCE_RESOLUTION_PLAN.md` +**Status**: Ready for implementation +**Timeline**: 4-7 hours +**Risk**: Low-Medium + +**Phases**: +1. **Phase 1**: Fix `isValidArgumentStart` and unary minus handling +2. **Phase 2**: Comprehensive testing +3. **Phase 3**: Fix related issues and update test suite + +#### 2. Function Composition (Completed) +**Location**: `implementation/FUNCTION_COMPOSITION_PLAN.md` +**Status**: ✅ Implemented +**Achievements**: `via` operator, `@` operator, enhanced standard library + +### Completed Plans + +####ass="p">) check_eq(Lines[1].h, 128, 'F - test_draw_circle_mid_stroke/baseline/y') check_eq(#Lines[1].shapes, 0, 'F - test_draw_circle_mid_stroke/baseline/#shapes') -- draw a circle App.mouse_move(Margin_left+4, Margin_top+Drawing_padding_top+4) -- hover on drawing App.run_after_mouse_press(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_keychord('o') App.run_after_mouse_release(Margin_left+35+30, Margin_top+Drawing_padding_top+36, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_draw_circle_mid_stroke/#shapes') check_eq(#drawing.points, 1, 'F - test_draw_circle_mid_stroke/#points') check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode') check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle_mid_stroke/radius') local center = drawing.points[drawing.shapes[1].center] check_eq(center.x, 35, 'F - test_draw_circle_mid_stroke/center:x') check_eq(center.y, 36, 'F - test_draw_circle_mid_stroke/center:y') end function test_draw_arc() io.write('\ntest_draw_arc') -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'circle' App.draw() check_eq(#Lines, 2, 'F - test_draw_arc/baseline/#lines') check_eq(Lines[1].mode, 'drawing', 'F - test_draw_arc/baseline/mode') check_eq(Lines[1].y, Margin_top+Drawing_padding_top, 'F - test_draw_arc/baseline/y') check_eq(Lines[1].h, 128, 'F - test_draw_arc/baseline/y') check_eq(#Lines[1].shapes, 0, 'F - test_draw_arc/baseline/#shapes') -- draw an arc App.run_after_mouse_press(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.mouse_move(Margin_left+35+30, Margin_top+Drawing_padding_top+36) App.run_after_keychord('a') -- arc mode App.run_after_mouse_release(Margin_left+35+50, Margin_top+Drawing_padding_top+36+50, 1) -- 45° local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_draw_arc/#shapes') check_eq(#drawing.points, 1, 'F - test_draw_arc/#points') check_eq(drawing.shapes[1].mode, 'arc', 'F - test_draw_horizontal_line/shape_mode') local arc = drawing.shapes[1] check_eq(arc.radius, 30, 'F - test_draw_arc/radius') local center = drawing.points[arc.center] check_eq(center.x, 35, 'F - test_draw_arc/center:x') check_eq(center.y, 36, 'F - test_draw_arc/center:y') check_eq(arc.start_angle, 0, 'F - test_draw_arc/start:angle') check_eq(arc.end_angle, math.pi/4, 'F - test_draw_arc/end:angle') end function test_draw_polygon() io.write('\ntest_draw_polygon') -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} App.draw() check_eq(Current_drawing_mode, 'line', 'F - test_draw_polygon/baseline/drawing_mode') check_eq(#Lines, 2, 'F - test_draw_polygon/baseline/#lines') check_eq(Lines[1].mode, 'drawing', 'F - test_draw_polygon/baseline/mode') check_eq(Lines[1].y, Margin_top+Drawing_padding_top, 'F - test_draw_polygon/baseline/y') check_eq(Lines[1].h, 128, 'F - test_draw_polygon/baseline/y') check_eq(#Lines[1].shapes, 0, 'F - test_draw_polygon/baseline/#shapes') -- first point App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_keychord('g') -- polygon mode -- second point App.mouse_move(Margin_left+65, Margin_top+Drawing_padding_top+36) App.run_after_keychord('p') -- add point -- final point App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+26, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_draw_polygon/#shapes') check_eq(#drawing.points, 3, 'F - test_draw_polygon/vertices') local shape = drawing.shapes[1] check_eq(shape.mode, 'polygon', 'F - test_draw_polygon/shape_mode') check_eq(#shape.vertices, 3, 'F - test_draw_polygon/vertices') local p = drawing.points[shape.vertices[1]] check_eq(p.x, 5, 'F - test_draw_polygon/p1:x') check_eq(p.y, 6, 'F - test_draw_polygon/p1:y') local p = drawing.points[shape.vertices[2]] check_eq(p.x, 65, 'F - test_draw_polygon/p2:x') check_eq(p.y, 36, 'F - test_draw_polygon/p2:y') local p = drawing.points[shape.vertices[3]] check_eq(p.x, 35, 'F - test_draw_polygon/p3:x') check_eq(p.y, 26, 'F - test_draw_polygon/p3:y') end function test_draw_rectangle() io.write('\ntest_draw_rectangle') -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} App.draw() check_eq(Current_drawing_mode, 'line', 'F - test_draw_rectangle/baseline/drawing_mode') check_eq(#Lines, 2, 'F - test_draw_rectangle/baseline/#lines') check_eq(Lines[1].mode, 'drawing', 'F - test_draw_rectangle/baseline/mode') check_eq(Lines[1].y, Margin_top+Drawing_padding_top, 'F - test_draw_rectangle/baseline/y') check_eq(Lines[1].h, 128, 'F - test_draw_rectangle/baseline/y') check_eq(#Lines[1].shapes, 0, 'F - test_draw_rectangle/baseline/#shapes') -- first point App.run_after_mouse_press(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_keychord('r') -- rectangle mode -- second point/first edge App.mouse_move(Margin_left+42, Margin_top+Drawing_padding_top+45) App.run_after_keychord('p') -- override second point/first edge App.mouse_move(Margin_left+75, Margin_top+Drawing_padding_top+76) App.run_after_keychord('p') -- release (decides 'thickness' of rectangle perpendicular to first edge) App.run_after_mouse_release(Margin_left+15, Margin_top+Drawing_padding_top+26, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_draw_rectangle/#shapes') check_eq(#drawing.points, 5, 'F - test_draw_rectangle/#points') -- currently includes every point added local shape = drawing.shapes[1] check_eq(shape.mode, 'rectangle', 'F - test_draw_rectangle/shape_mode') check_eq(#shape.vertices, 4, 'F - test_draw_rectangle/vertices') local p = drawing.points[shape.vertices[1]] check_eq(p.x, 35, 'F - test_draw_rectangle/p1:x') check_eq(p.y, 36, 'F - test_draw_rectangle/p1:y') local p = drawing.points[shape.vertices[2]] check_eq(p.x, 75, 'F - test_draw_rectangle/p2:x') check_eq(p.y, 76, 'F - test_draw_rectangle/p2:y') local p = drawing.points[shape.vertices[3]] check_eq(p.x, 70, 'F - test_draw_rectangle/p3:x') check_eq(p.y, 81, 'F - test_draw_rectangle/p3:y') local p = drawing.points[shape.vertices[4]] check_eq(p.x, 30, 'F - test_draw_rectangle/p4:x') check_eq(p.y, 41, 'F - test_draw_rectangle/p4:y') end function test_draw_rectangle_intermediate() io.write('\ntest_draw_rectangle_intermediate') -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} App.draw() check_eq(Current_drawing_mode, 'line', 'F - test_draw_rectangle_intermediate/baseline/drawing_mode') check_eq(#Lines, 2, 'F - test_draw_rectangle_intermediate/baseline/#lines') check_eq(Lines[1].mode, 'drawing', 'F - test_draw_rectangle_intermediate/baseline/mode') check_eq(Lines[1].y, Margin_top+Drawing_padding_top, 'F - test_draw_rectangle_intermediate/baseline/y') check_eq(Lines[1].h, 128, 'F - test_draw_rectangle_intermediate/baseline/y') check_eq(#Lines[1].shapes, 0, 'F - test_draw_rectangle_intermediate/baseline/#shapes') -- first point App.run_after_mouse_press(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_keychord('r') -- rectangle mode -- second point/first edge App.mouse_move(Margin_left+42, Margin_top+Drawing_padding_top+45) App.run_after_keychord('p') -- override second point/first edge App.mouse_move(Margin_left+75, Margin_top+Drawing_padding_top+76) App.run_after_keychord('p') local drawing = Lines[1] check_eq(#drawing.points, 3, 'F - test_draw_rectangle_intermediate/#points') -- currently includes every point added local pending = drawing.pending check_eq(pending.mode, 'rectangle', 'F - test_draw_rectangle_intermediate/shape_mode') check_eq(#pending.vertices, 2, 'F - test_draw_rectangle_intermediate/vertices') local p = drawing.points[pending.vertices[1]] check_eq(p.x, 35, 'F - test_draw_rectangle_intermediate/p1:x') check_eq(p.y, 36, 'F - test_draw_rectangle_intermediate/p1:y') local p = drawing.points[pending.vertices[2]] check_eq(p.x, 75, 'F - test_draw_rectangle_intermediate/p2:x') check_eq(p.y, 76, 'F - test_draw_rectangle_intermediate/p2:y') -- outline of rectangle is drawn based on where the mouse is, but we can't check that so far end function test_draw_square() io.write('\ntest_draw_square') -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} App.draw() check_eq(Current_drawing_mode, 'line', 'F - test_draw_square/baseline/drawing_mode') check_eq(#Lines, 2, 'F - test_draw_square/baseline/#lines') check_eq(Lines[1].mode, 'drawing', 'F - test_draw_square/baseline/mode') check_eq(Lines[1].y, Margin_top+Drawing_padding_top, 'F - test_draw_square/baseline/y') check_eq(Lines[1].h, 128, 'F - test_draw_square/baseline/y') check_eq(#Lines[1].shapes, 0, 'F - test_draw_square/baseline/#shapes') -- first point App.run_after_mouse_press(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_keychord('s') -- square mode -- second point/first edge App.mouse_move(Margin_left+42, Margin_top+Drawing_padding_top+45) App.run_after_keychord('p') -- override second point/first edge App.mouse_move(Margin_left+65, Margin_top+Drawing_padding_top+66) App.run_after_keychord('p') -- release (decides which side of first edge to draw square on) App.run_after_mouse_release(Margin_left+15, Margin_top+Drawing_padding_top+26, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_draw_square/#shapes') check_eq(#drawing.points, 5, 'F - test_draw_square/#points') -- currently includes every point added check_eq(drawing.shapes[1].mode, 'square', 'F - test_draw_square/shape_mode') check_eq(#drawing.shapes[1].vertices, 4, 'F - test_draw_square/vertices') local p = drawing.points[drawing.shapes[1].vertices[1]] check_eq(p.x, 35, 'F - test_draw_square/p1:x') check_eq(p.y, 36, 'F - test_draw_square/p1:y') local p = drawing.points[drawing.shapes[1].vertices[2]] check_eq(p.x, 65, 'F - test_draw_square/p2:x') check_eq(p.y, 66, 'F - test_draw_square/p2:y') local p = drawing.points[drawing.shapes[1].vertices[3]] check_eq(p.x, 35, 'F - test_draw_square/p3:x') check_eq(p.y, 96, 'F - test_draw_square/p3:y') local p = drawing.points[drawing.shapes[1].vertices[4]] check_eq(p.x, 5, 'F - test_draw_square/p4:x') check_eq(p.y, 66, 'F - test_draw_square/p4:y') end function test_name_point() io.write('\ntest_name_point') -- create a drawing with a line Filename = 'foo' App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() -- draw a line App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_name_point/baseline/#shapes') check_eq(#drawing.points, 2, 'F - test_name_point/baseline/#points') check_eq(drawing.shapes[1].mode, 'line', 'F - test_name_point/baseline/shape:1') local p1 = drawing.points[drawing.shapes[1].p1] local p2 = drawing.points[drawing.shapes[1].p2] check_eq(p1.x, 5, 'F - test_name_point/baseline/p1:x') check_eq(p1.y, 6, 'F - test_name_point/baseline/p1:y') check_eq(p2.x, 35, 'F - test_name_point/baseline/p2:x') check_eq(p2.y, 36, 'F - test_name_point/baseline/p2:y') check_nil(p2.name, 'F - test_name_point/baseline/p2:name') -- enter 'name' mode without moving the mouse App.run_after_keychord('C-n') check_eq(Current_drawing_mode, 'name', 'F - test_name_point/mode:1') App.run_after_textinput('A') check_eq(p2.name, 'A', 'F - test_name_point') -- still in 'name' mode check_eq(Current_drawing_mode, 'name', 'F - test_name_point/mode:2') -- exit 'name' mode App.run_after_keychord('return') check_eq(Current_drawing_mode, 'line', 'F - test_name_point/mode:3') check_eq(p2.name, 'A', 'F - test_name_point') -- wait until save App.wait_fake_time(3.1) App.update(0) -- change is saved Lines = load_from_disk(Filename) local p2 = Lines[1].points[drawing.shapes[1].p2] check_eq(p2.name, 'A', 'F - test_name_point/save') end function test_move_point() io.write('\ntest_move_point') -- create a drawing with a line Filename = 'foo' App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_move_point/baseline/#shapes') check_eq(#drawing.points, 2, 'F - test_move_point/baseline/#points') check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point/baseline/shape:1') local p1 = drawing.points[drawing.shapes[1].p1] local p2 = drawing.points[drawing.shapes[1].p2] check_eq(p1.x, 5, 'F - test_move_point/baseline/p1:x') check_eq(p1.y, 6, 'F - test_move_point/baseline/p1:y') check_eq(p2.x, 35, 'F - test_move_point/baseline/p2:x') check_eq(p2.y, 36, 'F - test_move_point/baseline/p2:y') -- wait until save App.wait_fake_time(3.1) App.update(0) -- line is saved to disk Lines = load_from_disk(Filename) local drawing = Lines[1] local p2 = Lines[1].points[drawing.shapes[1].p2] check_eq(p2.x, 35, 'F - test_move_point/save/x') check_eq(p2.y, 36, 'F - test_move_point/save/y') App.draw() -- enter 'move' mode without moving the mouse App.run_after_keychord('C-u') check_eq(Current_drawing_mode, 'move', 'F - test_move_point/mode:1') -- point is lifted check_eq(drawing.pending.mode, 'move', 'F - test_move_point/mode:2') check_eq(drawing.pending.target_point, p2, 'F - test_move_point/target') -- move point App.mouse_move(Margin_left+26, Margin_top+Drawing_padding_top+44) App.update(0.05) local p2 = drawing.points[drawing.shapes[1].p2] check_eq(p2.x, 26, 'F - test_move_point/x') check_eq(p2.y, 44, 'F - test_move_point/y') -- exit 'move' mode App.run_after_mouse_click(Margin_left+26, Margin_top+Drawing_padding_top+44, 1) check_eq(Current_drawing_mode, 'line', 'F - test_move_point/mode:3') check_eq(drawing.pending, {}, 'F - test_move_point/pending') -- wait until save App.wait_fake_time(3.1) App.update(0) -- change is saved Lines = load_from_disk(Filename) local p2 = Lines[1].points[drawing.shapes[1].p2] check_eq(p2.x, 26, 'F - test_move_point/save/x') check_eq(p2.y, 44, 'F - test_move_point/save/y') end function test_move_point_on_manhattan_line() io.write('\ntest_move_point_on_manhattan_line') -- create a drawing with a manhattan line Filename = 'foo' App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'manhattan' App.draw() App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+46, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_move_point_on_manhattan_line/baseline/#shapes') check_eq(#drawing.points, 2, 'F - test_move_point_on_manhattan_line/baseline/#points') check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_move_point_on_manhattan_line/baseline/shape:1') App.draw() -- enter 'move' mode App.run_after_keychord('C-u') check_eq(Current_drawing_mode, 'move', 'F - test_move_point_on_manhattan_line/mode:1') -- move point App.mouse_move(Margin_left+26, Margin_top+Drawing_padding_top+44) App.update(0.05) -- line is no longer manhattan check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point_on_manhattan_line/baseline/shape:1') end function test_delete_lines_at_point() io.write('\ntest_delete_lines_at_point') -- create a drawing with two lines connected at a point Filename = 'foo' App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_mouse_press(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_mouse_release(Margin_left+55, Margin_top+Drawing_padding_top+26, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 2, 'F - test_delete_lines_at_point/baseline/#shapes') check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:1') check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:2') -- hover on the common point and delete App.mouse_move(Margin_left+35, Margin_top+Drawing_padding_top+36) App.run_after_keychord('C-d') check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_lines_at_point/shape:1') check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_delete_lines_at_point/shape:2') -- wait for some time App.wait_fake_time(3.1) App.update(0) -- deleted points disappear after file is reloaded Lines = load_from_disk(Filename) check_eq(#Lines[1].shapes, 0, 'F - test_delete_lines_at_point/save') end function test_delete_line_under_mouse_pointer() io.write('\ntest_delete_line_under_mouse_pointer') -- create a drawing with two lines connected at a point App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_mouse_press(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_mouse_release(Margin_left+55, Margin_top+Drawing_padding_top+26, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 2, 'F - test_delete_line_under_mouse_pointer/baseline/#shapes') check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:1') check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:2') -- hover on one of the lines and delete App.mouse_move(Margin_left+25, Margin_top+Drawing_padding_top+26) App.run_after_keychord('C-d') -- only that line is deleted check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_line_under_mouse_pointer/shape:1') check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/shape:2') end function test_delete_point_from_polygon() io.write('\ntest_delete_point_from_polygon') -- create a drawing with two lines connected at a point App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() -- first point App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_keychord('g') -- polygon mode -- second point App.mouse_move(Margin_left+65, Margin_top+Drawing_padding_top+36) App.run_after_keychord('p') -- add point -- third point App.mouse_move(Margin_left+35, Margin_top+Drawing_padding_top+26) App.run_after_keychord('p') -- add point -- fourth point App.run_after_mouse_release(Margin_left+14, Margin_top+Drawing_padding_top+16, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes') check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode') check_eq(#drawing.shapes[1].vertices, 4, 'F - test_delete_point_from_polygon/baseline/vertices') -- hover on a point and delete App.mouse_move(Margin_left+35, Margin_top+Drawing_padding_top+26) App.run_after_keychord('C-d') -- just the one point is deleted check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/shape') check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/vertices') end function test_delete_point_from_polygon() io.write('\ntest_delete_point_from_polygon') -- create a drawing with two lines connected at a point App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() -- first point App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_keychord('g') -- polygon mode -- second point App.mouse_move(Margin_left+65, Margin_top+Drawing_padding_top+36) App.run_after_keychord('p') -- add point -- third point App.run_after_mouse_release(Margin_left+14, Margin_top+Drawing_padding_top+16, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes') check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode') check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/baseline/vertices') -- hover on a point and delete App.mouse_move(Margin_left+65, Margin_top+Drawing_padding_top+36) App.run_after_keychord('C-d') -- there's < 3 points left, so the whole polygon is deleted check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_point_from_polygon') end function test_undo_name_point() io.write('\ntest_undo_name_point') -- create a drawing with a line Filename = 'foo' App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() -- draw a line App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_undo_name_point/baseline/#shapes') check_eq(#drawing.points, 2, 'F - test_undo_name_point/baseline/#points') check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_name_point/baseline/shape:1') local p1 = drawing.points[drawing.shapes[1].p1] local p2 = drawing.points[drawing.shapes[1].p2] check_eq(p1.x, 5, 'F - test_undo_name_point/baseline/p1:x') check_eq(p1.y, 6, 'F - test_undo_name_point/baseline/p1:y') check_eq(p2.x, 35, 'F - test_undo_name_point/baseline/p2:x') check_eq(p2.y, 36, 'F - test_undo_name_point/baseline/p2:y') check_nil(p2.name, 'F - test_undo_name_point/baseline/p2:name') check_eq(#History, 1, 'F - test_undo_name_point/baseline/history:1') -- enter 'name' mode without moving the mouse App.run_after_keychord('C-n') App.run_after_textinput('A') App.run_after_keychord('return') check_eq(p2.name, 'A', 'F - test_undo_name_point/baseline') check_eq(#History, 3, 'F - test_undo_name_point/baseline/history:2') check_eq(Next_history, 4, 'F - test_undo_name_point/baseline/next_history') -- undo App.run_after_keychord('C-z') local drawing = Lines[1] local p2 = drawing.points[drawing.shapes[1].p2] check_eq(Next_history, 3, 'F - test_undo_name_point/next_history') check_eq(p2.name, '', 'F - test_undo_name_point') -- not quite what it was before, but close enough -- wait until save App.wait_fake_time(3.1) App.update(0) -- undo is saved Lines = load_from_disk(Filename) local p2 = Lines[1].points[drawing.shapes[1].p2] check_eq(p2.name, '', 'F - test_undo_name_point/save') end function test_undo_move_point() io.write('\ntest_undo_move_point') -- create a drawing with a line Filename = 'foo' App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 1, 'F - test_undo_move_point/baseline/#shapes') check_eq(#drawing.points, 2, 'F - test_undo_move_point/baseline/#points') check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_move_point/baseline/shape:1') local p1 = drawing.points[drawing.shapes[1].p1] local p2 = drawing.points[drawing.shapes[1].p2] check_eq(p1.x, 5, 'F - test_undo_move_point/baseline/p1:x') check_eq(p1.y, 6, 'F - test_undo_move_point/baseline/p1:y') check_eq(p2.x, 35, 'F - test_undo_move_point/baseline/p2:x') check_eq(p2.y, 36, 'F - test_undo_move_point/baseline/p2:y') check_nil(p2.name, 'F - test_undo_move_point/baseline/p2:name') -- move p2 App.run_after_keychord('C-u') App.mouse_move(Margin_left+26, Margin_top+Drawing_padding_top+44) App.update(0.05) local p2 = drawing.points[drawing.shapes[1].p2] check_eq(p2.x, 26, 'F - test_undo_move_point/x') check_eq(p2.y, 44, 'F - test_undo_move_point/y') -- exit 'move' mode App.run_after_mouse_click(Margin_left+26, Margin_top+Drawing_padding_top+44, 1) check_eq(Next_history, 4, 'F - test_undo_move_point/next_history') -- undo App.run_after_keychord('C-z') App.run_after_keychord('C-z') -- bug: need to undo twice local drawing = Lines[1] local p2 = drawing.points[drawing.shapes[1].p2] check_eq(Next_history, 2, 'F - test_undo_move_point/next_history') check_eq(p2.x, 35, 'F - test_undo_move_point/x') check_eq(p2.y, 36, 'F - test_undo_move_point/y') -- wait until save App.wait_fake_time(3.1) App.update(0) -- undo is saved Lines = load_from_disk(Filename) local p2 = Lines[1].points[drawing.shapes[1].p2] check_eq(p2.x, 35, 'F - test_undo_move_point/save/x') check_eq(p2.y, 36, 'F - test_undo_move_point/save/y') end function test_undo_delete_point() io.write('\ntest_undo_delete_point') -- create a drawing with two lines connected at a point Filename = 'foo' App.screen.init{width=Margin_width+256, height=300} -- drawing coordinates 1:1 with pixels Lines = load_array{'```lines', '```', ''} Current_drawing_mode = 'line' App.draw() App.run_after_mouse_press(Margin_left+5, Margin_top+Drawing_padding_top+6, 1) App.run_after_mouse_release(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_mouse_press(Margin_left+35, Margin_top+Drawing_padding_top+36, 1) App.run_after_mouse_release(Margin_left+55, Margin_top+Drawing_padding_top+26, 1) local drawing = Lines[1] check_eq(#drawing.shapes, 2, 'F - test_undo_delete_point/baseline/#shapes') check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/baseline/shape:1') check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/baseline/shape:2') -- hover on the common point and delete App.mouse_move(Margin_left+35, Margin_top+Drawing_padding_top+36) App.run_after_keychord('C-d') check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_undo_delete_point/shape:1') check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_undo_delete_point/shape:2') -- undo App.run_after_keychord('C-z') local drawing = Lines[1] local p2 = drawing.points[drawing.shapes[1].p2] check_eq(Next_history, 3, 'F - test_undo_move_point/next_history') check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/shape:1') check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/shape:2') -- wait until save App.wait_fake_time(3.1) App.update(0) -- undo is saved Lines = load_from_disk(Filename) check_eq(#Lines[1].shapes, 2, 'F - test_undo_delete_point/save') end |