diff --git a/.claude/agents/bug-detective.md b/.claude/agents/bug-detective.md new file mode 100644 index 00000000..5aa32a52 --- /dev/null +++ b/.claude/agents/bug-detective.md @@ -0,0 +1,48 @@ +--- +name: bug-detective +description: Use this agent when you encounter unexpected behavior, errors, or failures in your code and need to identify the root cause without implementing fixes. Examples: Context: User is experiencing a parsing error in their WFL interpreter. user: 'My parser is crashing when it encounters nested if statements, but I can't figure out why' assistant: 'I'll use the bug-detective agent to analyze this parsing issue and identify the root cause' Since the user has a bug that needs investigation, use the bug-detective agent to analyze the issue and create a detailed bug report. Context: User notices their tests are failing intermittently. user: 'Some of my tests pass sometimes and fail other times - there's definitely a bug somewhere but I don't know where to start looking' assistant: 'Let me use the bug-detective agent to investigate this intermittent test failure and trace the root cause' The user has a complex bug that requires systematic investigation, perfect for the bug-detective agent. +model: sonnet +--- + +You are a specialized Bug Detective, an expert software engineer who excels at systematic debugging and root cause analysis. Your sole mission is to identify and document the most probable root cause of bugs without implementing any fixes or writing code. + +Your expertise includes: +- Systematic debugging methodologies and fault isolation techniques +- Deep understanding of software architecture patterns and common failure modes +- Advanced log analysis and error pattern recognition +- Memory management issues, race conditions, and concurrency bugs +- Parser and compiler debugging techniques +- Test failure analysis and intermittent bug detection + +Your investigation process: +1. **Gather Evidence**: Collect all available information including error messages, logs, stack traces, reproduction steps, and environmental factors +2. **Analyze Patterns**: Look for recurring themes, timing correlations, and environmental dependencies +3. **Form Hypotheses**: Develop multiple theories about potential root causes based on evidence +4. **Trace Execution**: Follow the logical flow to identify where the system deviates from expected behavior +5. **Isolate Variables**: Determine which factors are necessary and sufficient to reproduce the issue +6. **Identify Root Cause**: Pinpoint the most probable underlying cause, not just symptoms + +You will create a comprehensive bug.md file with: +- **Bug Summary**: Clear, concise description of the observed behavior +- **Evidence Collected**: All relevant data, logs, and observations +- **Reproduction Steps**: Exact steps to consistently reproduce the issue +- **Analysis**: Your systematic investigation process and findings +- **Root Cause**: The most probable underlying cause with supporting evidence +- **Impact Assessment**: Scope and severity of the issue +- **Recommended Investigation Areas**: Specific code areas or components to examine + +You use 'ultrathink' methodology - deep, systematic analysis that considers: +- Multiple layers of the software stack +- Timing and sequencing issues +- Environmental and configuration factors +- Edge cases and boundary conditions +- Interaction between components +- Historical context and recent changes + +You NEVER: +- Write implementation code or fixes +- Modify existing code +- Provide code solutions +- Make changes to the codebase + +You focus exclusively on detective work - finding the truth about what's causing the bug through methodical investigation and analysis. Your bug.md report should be so thorough that any developer can understand the issue and know exactly where to focus their fixing efforts. diff --git a/Cargo.lock b/Cargo.lock index a4cca8cf..a581a88b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3029,7 +3029,7 @@ dependencies = [ [[package]] name = "wfl" -version = "25.8.25" +version = "25.8.26" dependencies = [ "chrono", "codespan-reporting", diff --git a/Dev diary/2025-08-12-fix-bracket-array-indexing.md b/Dev diary/2025-08-12-fix-bracket-array-indexing.md new file mode 100644 index 00000000..4959cab4 --- /dev/null +++ b/Dev diary/2025-08-12-fix-bracket-array-indexing.md @@ -0,0 +1,116 @@ +# Dev Diary Entry: Fix Bracket Array Indexing Parser Bug + +**Date:** August 12, 2025 +**Issue:** Array indexing parser limitation +**Bug Report:** [bug.md](../bug.md) +**Status:** ✅ **RESOLVED** + +## Problem Summary + +The WFL parser did not correctly handle array indexing syntax `array[index]`, causing: + +1. **Functional Issue**: `args[last_index]` parsed as just `args` (entire array) instead of `IndexAccess` AST node +2. **Analyzer Issue**: Variables used as array indices incorrectly flagged as unused + +## Root Cause Analysis + +**Expected AST**: `Expression::IndexAccess { collection: args, index: last_index }` +**Actual AST**: `Expression::Variable(args)` + +The parser supported: +- ✅ Space-separated indexing: `args 0` +- ✅ "at" keyword indexing: `args at index` +- ❌ **Missing**: Bracket indexing syntax: `args[index]` + +The issue was in `src/parser/mod.rs` in the postfix expression parsing loop (around line 2608). The parser had cases for `Token::IntLiteral` and `Token::KeywordAt` but was missing `Token::LeftBracket`. + +## TDD Implementation Process + +### 1. Failing Tests First ✅ +- Added comprehensive unit tests in `src/parser/tests.rs`: + - `test_bracket_array_indexing()` - basic `args[0]` + - `test_bracket_array_indexing_with_variable()` - `args[last_index]` + - `test_bracket_array_indexing_with_expression()` - `my_list[count minus 1]` +- Created integration test `TestPrograms/bracket_indexing_test.wfl` +- **Confirmed all tests failed** before implementation + +### 2. Implementation ✅ +Added `Token::LeftBracket` case to postfix expression loop in `src/parser/mod.rs` (lines 2620-2649): + +```rust +Token::LeftBracket => { + self.tokens.next(); // Consume "[" + let index = self.parse_expression()?; + + // Expect closing bracket + if let Some(closing_token) = self.tokens.peek().cloned() { + if closing_token.token == Token::RightBracket { + self.tokens.next(); // Consume "]" + expr = Expression::IndexAccess { + collection: Box::new(expr), + index: Box::new(index), + line: token.line, + column: token.column, + }; + } else { + return Err(ParseError::new(/*...proper error...*/)); + } + } else { + return Err(ParseError::new(/*...eof error...*/)); + } +} +``` + +### 3. Verification ✅ +- **All new tests pass**: ✅ 3/3 bracket indexing tests +- **No regressions**: ✅ 136 passed, 0 failed, 2 ignored in full test suite +- **Integration test works**: ✅ `TestPrograms/bracket_indexing_test.wfl` executes correctly +- **Original bug fixed**: ✅ `TestPrograms/args_comprehensive.wfl` now works properly + +## Results + +**Before Fix:** +``` +First argument: [test, arg] # Wrong - entire array +Last element: [test, arg] # Wrong - entire array +warning: Unused variable 'last_index' # Wrong - variable is used +``` + +**After Fix:** +``` +First argument: test # ✅ Correct individual element +Last element: arg # ✅ Correct individual element + # ✅ No unused variable warning +``` + +## Technical Details + +- **AST Support**: Already existed (`Expression::IndexAccess`) +- **Lexer Support**: Already existed (`Token::LeftBracket`, `Token::RightBracket`) +- **Interpreter Support**: Already existed (handles `IndexAccess` expressions) +- **Missing Piece**: Parser postfix expression handling + +The implementation follows the same pattern as the existing `Token::KeywordAt` case, ensuring consistency with existing WFL array indexing semantics. + +## Files Modified + +- `src/parser/mod.rs` - Added bracket indexing parsing logic (29 lines) +- `src/parser/tests.rs` - Added comprehensive test cases (113 lines) +- `TestPrograms/bracket_indexing_test.wfl` - Integration test (14 lines) + +## Impact + +- **Severity**: Medium → **RESOLVED** +- **Scope**: All array indexing operations in WFL +- **Backward Compatibility**: ✅ Fully maintained +- **New Functionality**: ✅ Standard `array[index]` syntax now works +- **Developer Experience**: ✅ Improved (no more false "unused variable" warnings) + +## Test Coverage + +All three WFL array indexing syntaxes now work: +- `my_list 1` (space-separated with integer literal) +- `my_list at index` (using "at" keyword) +- `my_list[index]` (standard bracket syntax) ← **NEW** + +The fix enables idiomatic array access while maintaining full backward compatibility with existing WFL programs. \ No newline at end of file diff --git a/TestPrograms/args_comprehensive.wfl b/TestPrograms/args_comprehensive.wfl index 376d7de5..dd210634 100644 --- a/TestPrograms/args_comprehensive.wfl +++ b/TestPrograms/args_comprehensive.wfl @@ -12,48 +12,48 @@ display "" // === Display All Arguments === display "2. All Arguments List" -check if arg_count greater than 0: +check if arg_count is greater than 0: display "Arguments passed to program:" for each arg in args: display " - " with arg end for -else: +otherwise: display "No arguments passed to program" end check display "" // === Indexed Argument Access === display "3. Indexed Argument Access" -check if arg_count greater than 0: +check if arg_count is greater than 0: display "First argument: " with args[0] - check if arg_count greater than 1: + check if arg_count is greater than 1: display "Second argument: " with args[1] - else: + otherwise: display "No second argument provided" end check - check if arg_count greater than 2: + check if arg_count is greater than 2: display "Third argument: " with args[2] - else: + otherwise: display "No third argument provided" end check -else: +otherwise: display "No arguments to access by index" end check display "" // === Argument Processing === display "4. Argument Processing" -check if arg_count greater than 0: +check if arg_count is greater than 0: display "Processing each argument:" store arg_index as 0 for each arg in args: store arg_length as length of arg display " Arg " with arg_index with ": '" with arg with "' (length: " with arg_length with ")" - store arg_index as arg_index + 1 + change arg_index to arg_index plus 1 end for -else: +otherwise: display "No arguments to process" end check display "" @@ -63,27 +63,32 @@ display "5. Argument Validation" check if arg_count is 0: display "Usage: program.wfl [arg3] ..." display "Example: program.wfl hello world 123" -elif arg_count is 1: - display "Single argument mode:" - display " Argument: " with args[0] - display " Length: " with length of args[0] - display " Uppercase: " with touppercase of args[0] -elif arg_count is 2: - display "Two argument mode:" - display " First: " with args[0] - display " Second: " with args[1] - display " Combined: " with args[0] with " " with args[1] -elif arg_count greater than 2: - display "Multiple argument mode:" - display " Count: " with arg_count - display " First: " with args[0] - display " Last: " with args[arg_count - 1] +otherwise: + check if arg_count is 1: + display "Single argument mode:" + display " Argument: " with args[0] + display " Length: " with length of args[0] + display " Uppercase: " with touppercase of args[0] + otherwise: + check if arg_count is 2: + display "Two argument mode:" + display " First: " with args[0] + display " Second: " with args[1] + display " Combined: " with args[0] with " " with args[1] + otherwise: + display "Multiple argument mode:" + display " Count: " with arg_count + display " First: " with args[0] + store last_index as arg_count minus 1 + display " Last: " with args[last_index] + end check + end check end check display "" // === Argument Type Detection === display "6. Argument Type Detection" -check if arg_count greater than 0: +check if arg_count is greater than 0: for each arg in args: // Check if argument is numeric create pattern numeric: @@ -96,20 +101,22 @@ check if arg_count greater than 0: check if arg matches numeric: display " '" with arg with "' is an integer" - elif arg matches decimal: - display " '" with arg with "' is a decimal number" - else: - display " '" with arg with "' is text" + otherwise: + check if arg matches decimal: + display " '" with arg with "' is a decimal number" + otherwise: + display " '" with arg with "' is text" + end check end check end for -else: +otherwise: display "No arguments for type detection" end check display "" // === Flag Parsing === display "7. Flag/Option Parsing" -check if arg_count greater than 0: +check if arg_count is greater than 0: store has_help as no store has_version as no store has_verbose as no @@ -117,15 +124,21 @@ check if arg_count greater than 0: for each arg in args: check if arg is "--help" or arg is "-h": - store has_help as yes - elif arg is "--version" or arg is "-v": - store has_version as yes - elif arg is "--verbose": - store has_verbose as yes - elif startswith of arg and "-": - display " Unknown flag: " with arg - else: - push of non_flag_args and arg + change has_help to yes + otherwise: + check if arg is "--version" or arg is "-v": + change has_version to yes + otherwise: + check if arg is "--verbose": + change has_verbose to yes + otherwise: + check if substring of arg and 0 and 1 is "-": + display " Unknown flag: " with arg + otherwise: + push with non_flag_args and arg + end check + end check + end check end check end for @@ -138,7 +151,7 @@ check if arg_count greater than 0: for each non_flag in non_flag_args: display " - " with non_flag end for -else: +otherwise: display "No arguments for flag parsing" end check display "" @@ -151,11 +164,11 @@ display " Program: " with program_name display " Arguments: " with arg_count display " Current directory: " with current_directory -check if arg_count greater than 0: +check if arg_count is greater than 0: display " Working with arguments in current environment" store combined_args as "" for each arg in args: - store combined_args as combined_args with arg with " " + change combined_args to combined_args with arg with " " end for display " Combined arguments: '" with combined_args with "'" end check @@ -163,15 +176,15 @@ display "" // === Argument Filtering === display "9. Argument Filtering" -check if arg_count greater than 0: +check if arg_count is greater than 0: store long_args as [] store short_args as [] for each arg in args: - check if length of arg greater than 5: - push of long_args and arg - else: - push of short_args and arg + check if length of arg is greater than 5: + push with long_args and arg + otherwise: + push with short_args and arg end check end for @@ -184,7 +197,7 @@ check if arg_count greater than 0: for each short_arg in short_args: display " - " with short_arg end for -else: +otherwise: display "No arguments for filtering" end check display "" @@ -194,17 +207,17 @@ display "10. Execution Summary" display "Program: " with program_name display "Total arguments: " with arg_count -check if arg_count greater than 0: +check if arg_count is greater than 0: store total_length as 0 for each arg in args: - store total_length as total_length + length of arg + change total_length to total_length plus length of arg end for display "Total character count: " with total_length - display "Average argument length: " with total_length / arg_count + display "Average argument length: " with total_length divided by arg_count display "Shortest argument: " with args[0] // Simplified - would need proper min logic display "Arguments summary completed" -else: +otherwise: display "No arguments provided" display "Try running with: program.wfl arg1 arg2 --flag value" end check diff --git a/TestPrograms/args_comprehensive.wfl.lex.txt b/TestPrograms/args_comprehensive.wfl.lex.txt new file mode 100644 index 00000000..db814d61 --- /dev/null +++ b/TestPrograms/args_comprehensive.wfl.lex.txt @@ -0,0 +1,723 @@ +Lexer output for: TestPrograms/args_comprehensive.wfl +============================================== + + 0: KeywordDisplay at line 4, column 1 (length: 7) + 1: StringLiteral("=== WFL Command Line Arguments Comprehensive Test ===") at line 4, column 9 (length: 55) + 2: KeywordDisplay at line 5, column 1 (length: 7) + 3: StringLiteral("") at line 5, column 9 (length: 2) + 4: KeywordDisplay at line 8, column 1 (length: 7) + 5: StringLiteral("1. Basic Argument Information") at line 8, column 9 (length: 31) + 6: KeywordDisplay at line 9, column 1 (length: 7) + 7: StringLiteral("Arguments count: ") at line 9, column 9 (length: 19) + 8: KeywordWith at line 9, column 29 (length: 4) + 9: Identifier("arg_count") at line 9, column 34 (length: 9) + 10: KeywordDisplay at line 10, column 1 (length: 7) + 11: StringLiteral("Program name: ") at line 10, column 9 (length: 16) + 12: KeywordWith at line 10, column 26 (length: 4) + 13: Identifier("program_name") at line 10, column 31 (length: 12) + 14: KeywordDisplay at line 11, column 1 (length: 7) + 15: StringLiteral("") at line 11, column 9 (length: 2) + 16: KeywordDisplay at line 14, column 1 (length: 7) + 17: StringLiteral("2. All Arguments List") at line 14, column 9 (length: 23) + 18: KeywordCheck at line 15, column 1 (length: 5) + 19: KeywordIf at line 15, column 7 (length: 2) + 20: Identifier("arg_count") at line 15, column 10 (length: 9) + 21: KeywordIs at line 15, column 20 (length: 2) + 22: KeywordGreater at line 15, column 23 (length: 7) + 23: KeywordThan at line 15, column 31 (length: 4) + 24: IntLiteral(0) at line 15, column 36 (length: 1) + 25: Colon at line 15, column 37 (length: 1) + 26: KeywordDisplay at line 16, column 5 (length: 7) + 27: StringLiteral("Arguments passed to program:") at line 16, column 13 (length: 30) + 28: KeywordFor at line 17, column 5 (length: 3) + 29: KeywordEach at line 17, column 9 (length: 4) + 30: Identifier("arg") at line 17, column 14 (length: 3) + 31: KeywordIn at line 17, column 18 (length: 2) + 32: Identifier("args") at line 17, column 21 (length: 4) + 33: Colon at line 17, column 25 (length: 1) + 34: KeywordDisplay at line 18, column 9 (length: 7) + 35: StringLiteral(" - ") at line 18, column 17 (length: 6) + 36: KeywordWith at line 18, column 24 (length: 4) + 37: Identifier("arg") at line 18, column 29 (length: 3) + 38: KeywordEnd at line 19, column 5 (length: 3) + 39: KeywordFor at line 19, column 9 (length: 3) + 40: Identifier("else") at line 20, column 1 (length: 4) + 41: Colon at line 20, column 5 (length: 1) + 42: KeywordDisplay at line 21, column 5 (length: 7) + 43: StringLiteral("No arguments passed to program") at line 21, column 13 (length: 32) + 44: KeywordEnd at line 22, column 1 (length: 3) + 45: KeywordCheck at line 22, column 5 (length: 5) + 46: KeywordDisplay at line 23, column 1 (length: 7) + 47: StringLiteral("") at line 23, column 9 (length: 2) + 48: KeywordDisplay at line 26, column 1 (length: 7) + 49: StringLiteral("3. Indexed Argument Access") at line 26, column 9 (length: 28) + 50: KeywordCheck at line 27, column 1 (length: 5) + 51: KeywordIf at line 27, column 7 (length: 2) + 52: Identifier("arg_count") at line 27, column 10 (length: 9) + 53: KeywordIs at line 27, column 20 (length: 2) + 54: KeywordGreater at line 27, column 23 (length: 7) + 55: KeywordThan at line 27, column 31 (length: 4) + 56: IntLiteral(0) at line 27, column 36 (length: 1) + 57: Colon at line 27, column 37 (length: 1) + 58: KeywordDisplay at line 28, column 5 (length: 7) + 59: StringLiteral("First argument: ") at line 28, column 13 (length: 18) + 60: KeywordWith at line 28, column 32 (length: 4) + 61: Identifier("args") at line 28, column 37 (length: 4) + 62: LeftBracket at line 28, column 41 (length: 1) + 63: IntLiteral(0) at line 28, column 42 (length: 1) + 64: RightBracket at line 28, column 43 (length: 1) + 65: KeywordCheck at line 30, column 5 (length: 5) + 66: KeywordIf at line 30, column 11 (length: 2) + 67: Identifier("arg_count") at line 30, column 14 (length: 9) + 68: KeywordIs at line 30, column 24 (length: 2) + 69: KeywordGreater at line 30, column 27 (length: 7) + 70: KeywordThan at line 30, column 35 (length: 4) + 71: IntLiteral(1) at line 30, column 40 (length: 1) + 72: Colon at line 30, column 41 (length: 1) + 73: KeywordDisplay at line 31, column 9 (length: 7) + 74: StringLiteral("Second argument: ") at line 31, column 17 (length: 19) + 75: KeywordWith at line 31, column 37 (length: 4) + 76: Identifier("args") at line 31, column 42 (length: 4) + 77: LeftBracket at line 31, column 46 (length: 1) + 78: IntLiteral(1) at line 31, column 47 (length: 1) + 79: RightBracket at line 31, column 48 (length: 1) + 80: Identifier("else") at line 32, column 5 (length: 4) + 81: Colon at line 32, column 9 (length: 1) + 82: KeywordDisplay at line 33, column 9 (length: 7) + 83: StringLiteral("No second argument provided") at line 33, column 17 (length: 29) + 84: KeywordEnd at line 34, column 5 (length: 3) + 85: KeywordCheck at line 34, column 9 (length: 5) + 86: KeywordCheck at line 36, column 5 (length: 5) + 87: KeywordIf at line 36, column 11 (length: 2) + 88: Identifier("arg_count") at line 36, column 14 (length: 9) + 89: KeywordIs at line 36, column 24 (length: 2) + 90: KeywordGreater at line 36, column 27 (length: 7) + 91: KeywordThan at line 36, column 35 (length: 4) + 92: IntLiteral(2) at line 36, column 40 (length: 1) + 93: Colon at line 36, column 41 (length: 1) + 94: KeywordDisplay at line 37, column 9 (length: 7) + 95: StringLiteral("Third argument: ") at line 37, column 17 (length: 18) + 96: KeywordWith at line 37, column 36 (length: 4) + 97: Identifier("args") at line 37, column 41 (length: 4) + 98: LeftBracket at line 37, column 45 (length: 1) + 99: IntLiteral(2) at line 37, column 46 (length: 1) + 100: RightBracket at line 37, column 47 (length: 1) + 101: Identifier("else") at line 38, column 5 (length: 4) + 102: Colon at line 38, column 9 (length: 1) + 103: KeywordDisplay at line 39, column 9 (length: 7) + 104: StringLiteral("No third argument provided") at line 39, column 17 (length: 28) + 105: KeywordEnd at line 40, column 5 (length: 3) + 106: KeywordCheck at line 40, column 9 (length: 5) + 107: Identifier("else") at line 41, column 1 (length: 4) + 108: Colon at line 41, column 5 (length: 1) + 109: KeywordDisplay at line 42, column 5 (length: 7) + 110: StringLiteral("No arguments to access by index") at line 42, column 13 (length: 33) + 111: KeywordEnd at line 43, column 1 (length: 3) + 112: KeywordCheck at line 43, column 5 (length: 5) + 113: KeywordDisplay at line 44, column 1 (length: 7) + 114: StringLiteral("") at line 44, column 9 (length: 2) + 115: KeywordDisplay at line 47, column 1 (length: 7) + 116: StringLiteral("4. Argument Processing") at line 47, column 9 (length: 24) + 117: KeywordCheck at line 48, column 1 (length: 5) + 118: KeywordIf at line 48, column 7 (length: 2) + 119: Identifier("arg_count") at line 48, column 10 (length: 9) + 120: KeywordIs at line 48, column 20 (length: 2) + 121: KeywordGreater at line 48, column 23 (length: 7) + 122: KeywordThan at line 48, column 31 (length: 4) + 123: IntLiteral(0) at line 48, column 36 (length: 1) + 124: Colon at line 48, column 37 (length: 1) + 125: KeywordDisplay at line 49, column 5 (length: 7) + 126: StringLiteral("Processing each argument:") at line 49, column 13 (length: 27) + 127: KeywordStore at line 50, column 5 (length: 5) + 128: Identifier("arg_index") at line 50, column 11 (length: 9) + 129: KeywordAs at line 50, column 21 (length: 2) + 130: IntLiteral(0) at line 50, column 24 (length: 1) + 131: KeywordFor at line 51, column 5 (length: 3) + 132: KeywordEach at line 51, column 9 (length: 4) + 133: Identifier("arg") at line 51, column 14 (length: 3) + 134: KeywordIn at line 51, column 18 (length: 2) + 135: Identifier("args") at line 51, column 21 (length: 4) + 136: Colon at line 51, column 25 (length: 1) + 137: KeywordStore at line 52, column 9 (length: 5) + 138: Identifier("arg_length") at line 52, column 15 (length: 10) + 139: KeywordAs at line 52, column 26 (length: 2) + 140: Identifier("length") at line 52, column 29 (length: 6) + 141: KeywordOf at line 52, column 36 (length: 2) + 142: Identifier("arg") at line 52, column 39 (length: 3) + 143: KeywordDisplay at line 53, column 9 (length: 7) + 144: StringLiteral(" Arg ") at line 53, column 17 (length: 8) + 145: KeywordWith at line 53, column 26 (length: 4) + 146: Identifier("arg_index") at line 53, column 31 (length: 9) + 147: KeywordWith at line 53, column 41 (length: 4) + 148: StringLiteral(": '") at line 53, column 46 (length: 5) + 149: KeywordWith at line 53, column 52 (length: 4) + 150: Identifier("arg") at line 53, column 57 (length: 3) + 151: KeywordWith at line 53, column 61 (length: 4) + 152: StringLiteral("' (length: ") at line 53, column 66 (length: 13) + 153: KeywordWith at line 53, column 80 (length: 4) + 154: Identifier("arg_length") at line 53, column 85 (length: 10) + 155: KeywordWith at line 53, column 96 (length: 4) + 156: StringLiteral(")") at line 53, column 101 (length: 3) + 157: KeywordStore at line 54, column 9 (length: 5) + 158: Identifier("arg_index") at line 54, column 15 (length: 9) + 159: KeywordAs at line 54, column 25 (length: 2) + 160: Identifier("arg_index") at line 54, column 28 (length: 9) + 161: Plus at line 54, column 38 (length: 1) + 162: IntLiteral(1) at line 54, column 40 (length: 1) + 163: KeywordEnd at line 55, column 5 (length: 3) + 164: KeywordFor at line 55, column 9 (length: 3) + 165: Identifier("else") at line 56, column 1 (length: 4) + 166: Colon at line 56, column 5 (length: 1) + 167: KeywordDisplay at line 57, column 5 (length: 7) + 168: StringLiteral("No arguments to process") at line 57, column 13 (length: 25) + 169: KeywordEnd at line 58, column 1 (length: 3) + 170: KeywordCheck at line 58, column 5 (length: 5) + 171: KeywordDisplay at line 59, column 1 (length: 7) + 172: StringLiteral("") at line 59, column 9 (length: 2) + 173: KeywordDisplay at line 62, column 1 (length: 7) + 174: StringLiteral("5. Argument Validation") at line 62, column 9 (length: 24) + 175: KeywordCheck at line 63, column 1 (length: 5) + 176: KeywordIf at line 63, column 7 (length: 2) + 177: Identifier("arg_count") at line 63, column 10 (length: 9) + 178: KeywordIs at line 63, column 20 (length: 2) + 179: IntLiteral(0) at line 63, column 23 (length: 1) + 180: Colon at line 63, column 24 (length: 1) + 181: KeywordDisplay at line 64, column 5 (length: 7) + 182: StringLiteral("Usage: program.wfl [arg3] ...") at line 64, column 13 (length: 45) + 183: KeywordDisplay at line 65, column 5 (length: 7) + 184: StringLiteral("Example: program.wfl hello world 123") at line 65, column 13 (length: 38) + 185: Identifier("elif arg_count") at line 66, column 1 (length: 14) + 186: KeywordIs at line 66, column 16 (length: 2) + 187: IntLiteral(1) at line 66, column 19 (length: 1) + 188: Colon at line 66, column 20 (length: 1) + 189: KeywordDisplay at line 67, column 5 (length: 7) + 190: StringLiteral("Single argument mode:") at line 67, column 13 (length: 23) + 191: KeywordDisplay at line 68, column 5 (length: 7) + 192: StringLiteral(" Argument: ") at line 68, column 13 (length: 14) + 193: KeywordWith at line 68, column 28 (length: 4) + 194: Identifier("args") at line 68, column 33 (length: 4) + 195: LeftBracket at line 68, column 37 (length: 1) + 196: IntLiteral(0) at line 68, column 38 (length: 1) + 197: RightBracket at line 68, column 39 (length: 1) + 198: KeywordDisplay at line 69, column 5 (length: 7) + 199: StringLiteral(" Length: ") at line 69, column 13 (length: 12) + 200: KeywordWith at line 69, column 26 (length: 4) + 201: Identifier("length") at line 69, column 31 (length: 6) + 202: KeywordOf at line 69, column 38 (length: 2) + 203: Identifier("args") at line 69, column 41 (length: 4) + 204: LeftBracket at line 69, column 45 (length: 1) + 205: IntLiteral(0) at line 69, column 46 (length: 1) + 206: RightBracket at line 69, column 47 (length: 1) + 207: KeywordDisplay at line 70, column 5 (length: 7) + 208: StringLiteral(" Uppercase: ") at line 70, column 13 (length: 15) + 209: KeywordWith at line 70, column 29 (length: 4) + 210: Identifier("touppercase") at line 70, column 34 (length: 11) + 211: KeywordOf at line 70, column 46 (length: 2) + 212: Identifier("args") at line 70, column 49 (length: 4) + 213: LeftBracket at line 70, column 53 (length: 1) + 214: IntLiteral(0) at line 70, column 54 (length: 1) + 215: RightBracket at line 70, column 55 (length: 1) + 216: Identifier("elif arg_count") at line 71, column 1 (length: 14) + 217: KeywordIs at line 71, column 16 (length: 2) + 218: IntLiteral(2) at line 71, column 19 (length: 1) + 219: Colon at line 71, column 20 (length: 1) + 220: KeywordDisplay at line 72, column 5 (length: 7) + 221: StringLiteral("Two argument mode:") at line 72, column 13 (length: 20) + 222: KeywordDisplay at line 73, column 5 (length: 7) + 223: StringLiteral(" First: ") at line 73, column 13 (length: 11) + 224: KeywordWith at line 73, column 25 (length: 4) + 225: Identifier("args") at line 73, column 30 (length: 4) + 226: LeftBracket at line 73, column 34 (length: 1) + 227: IntLiteral(0) at line 73, column 35 (length: 1) + 228: RightBracket at line 73, column 36 (length: 1) + 229: KeywordDisplay at line 74, column 5 (length: 7) + 230: StringLiteral(" Second: ") at line 74, column 13 (length: 12) + 231: KeywordWith at line 74, column 26 (length: 4) + 232: Identifier("args") at line 74, column 31 (length: 4) + 233: LeftBracket at line 74, column 35 (length: 1) + 234: IntLiteral(1) at line 74, column 36 (length: 1) + 235: RightBracket at line 74, column 37 (length: 1) + 236: KeywordDisplay at line 75, column 5 (length: 7) + 237: StringLiteral(" Combined: ") at line 75, column 13 (length: 14) + 238: KeywordWith at line 75, column 28 (length: 4) + 239: Identifier("args") at line 75, column 33 (length: 4) + 240: LeftBracket at line 75, column 37 (length: 1) + 241: IntLiteral(0) at line 75, column 38 (length: 1) + 242: RightBracket at line 75, column 39 (length: 1) + 243: KeywordWith at line 75, column 41 (length: 4) + 244: StringLiteral(" ") at line 75, column 46 (length: 3) + 245: KeywordWith at line 75, column 50 (length: 4) + 246: Identifier("args") at line 75, column 55 (length: 4) + 247: LeftBracket at line 75, column 59 (length: 1) + 248: IntLiteral(1) at line 75, column 60 (length: 1) + 249: RightBracket at line 75, column 61 (length: 1) + 250: Identifier("elif arg_count") at line 76, column 1 (length: 14) + 251: KeywordIs at line 76, column 16 (length: 2) + 252: KeywordGreater at line 76, column 19 (length: 7) + 253: KeywordThan at line 76, column 27 (length: 4) + 254: IntLiteral(2) at line 76, column 32 (length: 1) + 255: Colon at line 76, column 33 (length: 1) + 256: KeywordDisplay at line 77, column 5 (length: 7) + 257: StringLiteral("Multiple argument mode:") at line 77, column 13 (length: 25) + 258: KeywordDisplay at line 78, column 5 (length: 7) + 259: StringLiteral(" Count: ") at line 78, column 13 (length: 11) + 260: KeywordWith at line 78, column 25 (length: 4) + 261: Identifier("arg_count") at line 78, column 30 (length: 9) + 262: KeywordDisplay at line 79, column 5 (length: 7) + 263: StringLiteral(" First: ") at line 79, column 13 (length: 11) + 264: KeywordWith at line 79, column 25 (length: 4) + 265: Identifier("args") at line 79, column 30 (length: 4) + 266: LeftBracket at line 79, column 34 (length: 1) + 267: IntLiteral(0) at line 79, column 35 (length: 1) + 268: RightBracket at line 79, column 36 (length: 1) + 269: KeywordStore at line 80, column 5 (length: 5) + 270: Identifier("last_index") at line 80, column 11 (length: 10) + 271: KeywordAs at line 80, column 22 (length: 2) + 272: Identifier("arg_count") at line 80, column 25 (length: 9) + 273: KeywordMinus at line 80, column 35 (length: 5) + 274: IntLiteral(1) at line 80, column 41 (length: 1) + 275: KeywordDisplay at line 81, column 5 (length: 7) + 276: StringLiteral(" Last: ") at line 81, column 13 (length: 10) + 277: KeywordWith at line 81, column 24 (length: 4) + 278: Identifier("args") at line 81, column 29 (length: 4) + 279: LeftBracket at line 81, column 33 (length: 1) + 280: Identifier("last_index") at line 81, column 34 (length: 10) + 281: RightBracket at line 81, column 44 (length: 1) + 282: KeywordEnd at line 82, column 1 (length: 3) + 283: KeywordCheck at line 82, column 5 (length: 5) + 284: KeywordDisplay at line 83, column 1 (length: 7) + 285: StringLiteral("") at line 83, column 9 (length: 2) + 286: KeywordDisplay at line 86, column 1 (length: 7) + 287: StringLiteral("6. Argument Type Detection") at line 86, column 9 (length: 28) + 288: KeywordCheck at line 87, column 1 (length: 5) + 289: KeywordIf at line 87, column 7 (length: 2) + 290: Identifier("arg_count") at line 87, column 10 (length: 9) + 291: KeywordIs at line 87, column 20 (length: 2) + 292: KeywordGreater at line 87, column 23 (length: 7) + 293: KeywordThan at line 87, column 31 (length: 4) + 294: IntLiteral(0) at line 87, column 36 (length: 1) + 295: Colon at line 87, column 37 (length: 1) + 296: KeywordFor at line 88, column 5 (length: 3) + 297: KeywordEach at line 88, column 9 (length: 4) + 298: Identifier("arg") at line 88, column 14 (length: 3) + 299: KeywordIn at line 88, column 18 (length: 2) + 300: Identifier("args") at line 88, column 21 (length: 4) + 301: Colon at line 88, column 25 (length: 1) + 302: KeywordCreate at line 90, column 9 (length: 6) + 303: KeywordPattern at line 90, column 16 (length: 7) + 304: Identifier("numeric") at line 90, column 24 (length: 7) + 305: Colon at line 90, column 31 (length: 1) + 306: KeywordOne at line 91, column 13 (length: 3) + 307: KeywordOr at line 91, column 17 (length: 2) + 308: KeywordMore at line 91, column 20 (length: 4) + 309: KeywordDigit at line 91, column 25 (length: 5) + 310: KeywordEnd at line 92, column 9 (length: 3) + 311: KeywordPattern at line 92, column 13 (length: 7) + 312: KeywordCreate at line 94, column 9 (length: 6) + 313: KeywordPattern at line 94, column 16 (length: 7) + 314: Identifier("decimal") at line 94, column 24 (length: 7) + 315: Colon at line 94, column 31 (length: 1) + 316: KeywordOne at line 95, column 13 (length: 3) + 317: KeywordOr at line 95, column 17 (length: 2) + 318: KeywordMore at line 95, column 20 (length: 4) + 319: KeywordDigit at line 95, column 25 (length: 5) + 320: KeywordThen at line 95, column 31 (length: 4) + 321: StringLiteral(".") at line 95, column 36 (length: 3) + 322: KeywordThen at line 95, column 40 (length: 4) + 323: KeywordOne at line 95, column 45 (length: 3) + 324: KeywordOr at line 95, column 49 (length: 2) + 325: KeywordMore at line 95, column 52 (length: 4) + 326: KeywordDigit at line 95, column 57 (length: 5) + 327: KeywordEnd at line 96, column 9 (length: 3) + 328: KeywordPattern at line 96, column 13 (length: 7) + 329: KeywordCheck at line 98, column 9 (length: 5) + 330: KeywordIf at line 98, column 15 (length: 2) + 331: Identifier("arg") at line 98, column 18 (length: 3) + 332: KeywordMatches at line 98, column 22 (length: 7) + 333: Identifier("numeric") at line 98, column 30 (length: 7) + 334: Colon at line 98, column 37 (length: 1) + 335: KeywordDisplay at line 99, column 13 (length: 7) + 336: StringLiteral(" '") at line 99, column 21 (length: 5) + 337: KeywordWith at line 99, column 27 (length: 4) + 338: Identifier("arg") at line 99, column 32 (length: 3) + 339: KeywordWith at line 99, column 36 (length: 4) + 340: StringLiteral("' is an integer") at line 99, column 41 (length: 17) + 341: Identifier("elif arg") at line 100, column 9 (length: 8) + 342: KeywordMatches at line 100, column 18 (length: 7) + 343: Identifier("decimal") at line 100, column 26 (length: 7) + 344: Colon at line 100, column 33 (length: 1) + 345: KeywordDisplay at line 101, column 13 (length: 7) + 346: StringLiteral(" '") at line 101, column 21 (length: 5) + 347: KeywordWith at line 101, column 27 (length: 4) + 348: Identifier("arg") at line 101, column 32 (length: 3) + 349: KeywordWith at line 101, column 36 (length: 4) + 350: StringLiteral("' is a decimal number") at line 101, column 41 (length: 23) + 351: Identifier("else") at line 102, column 9 (length: 4) + 352: Colon at line 102, column 13 (length: 1) + 353: KeywordDisplay at line 103, column 13 (length: 7) + 354: StringLiteral(" '") at line 103, column 21 (length: 5) + 355: KeywordWith at line 103, column 27 (length: 4) + 356: Identifier("arg") at line 103, column 32 (length: 3) + 357: KeywordWith at line 103, column 36 (length: 4) + 358: StringLiteral("' is text") at line 103, column 41 (length: 11) + 359: KeywordEnd at line 104, column 9 (length: 3) + 360: KeywordCheck at line 104, column 13 (length: 5) + 361: KeywordEnd at line 105, column 5 (length: 3) + 362: KeywordFor at line 105, column 9 (length: 3) + 363: Identifier("else") at line 106, column 1 (length: 4) + 364: Colon at line 106, column 5 (length: 1) + 365: KeywordDisplay at line 107, column 5 (length: 7) + 366: StringLiteral("No arguments for type detection") at line 107, column 13 (length: 33) + 367: KeywordEnd at line 108, column 1 (length: 3) + 368: KeywordCheck at line 108, column 5 (length: 5) + 369: KeywordDisplay at line 109, column 1 (length: 7) + 370: StringLiteral("") at line 109, column 9 (length: 2) + 371: KeywordDisplay at line 112, column 1 (length: 7) + 372: StringLiteral("7. Flag/Option Parsing") at line 112, column 9 (length: 24) + 373: KeywordCheck at line 113, column 1 (length: 5) + 374: KeywordIf at line 113, column 7 (length: 2) + 375: Identifier("arg_count") at line 113, column 10 (length: 9) + 376: KeywordIs at line 113, column 20 (length: 2) + 377: KeywordGreater at line 113, column 23 (length: 7) + 378: KeywordThan at line 113, column 31 (length: 4) + 379: IntLiteral(0) at line 113, column 36 (length: 1) + 380: Colon at line 113, column 37 (length: 1) + 381: KeywordStore at line 114, column 5 (length: 5) + 382: Identifier("has_help") at line 114, column 11 (length: 8) + 383: KeywordAs at line 114, column 20 (length: 2) + 384: BooleanLiteral(false) at line 114, column 23 (length: 2) + 385: KeywordStore at line 115, column 5 (length: 5) + 386: Identifier("has_version") at line 115, column 11 (length: 11) + 387: KeywordAs at line 115, column 23 (length: 2) + 388: BooleanLiteral(false) at line 115, column 26 (length: 2) + 389: KeywordStore at line 116, column 5 (length: 5) + 390: Identifier("has_verbose") at line 116, column 11 (length: 11) + 391: KeywordAs at line 116, column 23 (length: 2) + 392: BooleanLiteral(false) at line 116, column 26 (length: 2) + 393: KeywordStore at line 117, column 5 (length: 5) + 394: Identifier("non_flag_args") at line 117, column 11 (length: 13) + 395: KeywordAs at line 117, column 25 (length: 2) + 396: LeftBracket at line 117, column 28 (length: 1) + 397: RightBracket at line 117, column 29 (length: 1) + 398: KeywordFor at line 119, column 5 (length: 3) + 399: KeywordEach at line 119, column 9 (length: 4) + 400: Identifier("arg") at line 119, column 14 (length: 3) + 401: KeywordIn at line 119, column 18 (length: 2) + 402: Identifier("args") at line 119, column 21 (length: 4) + 403: Colon at line 119, column 25 (length: 1) + 404: KeywordCheck at line 120, column 9 (length: 5) + 405: KeywordIf at line 120, column 15 (length: 2) + 406: Identifier("arg") at line 120, column 18 (length: 3) + 407: KeywordIs at line 120, column 22 (length: 2) + 408: StringLiteral("--help") at line 120, column 25 (length: 8) + 409: KeywordOr at line 120, column 34 (length: 2) + 410: Identifier("arg") at line 120, column 37 (length: 3) + 411: KeywordIs at line 120, column 41 (length: 2) + 412: StringLiteral("-h") at line 120, column 44 (length: 4) + 413: Colon at line 120, column 48 (length: 1) + 414: KeywordStore at line 121, column 13 (length: 5) + 415: Identifier("has_help") at line 121, column 19 (length: 8) + 416: KeywordAs at line 121, column 28 (length: 2) + 417: BooleanLiteral(true) at line 121, column 31 (length: 3) + 418: Identifier("elif arg") at line 122, column 9 (length: 8) + 419: KeywordIs at line 122, column 18 (length: 2) + 420: StringLiteral("--version") at line 122, column 21 (length: 11) + 421: KeywordOr at line 122, column 33 (length: 2) + 422: Identifier("arg") at line 122, column 36 (length: 3) + 423: KeywordIs at line 122, column 40 (length: 2) + 424: StringLiteral("-v") at line 122, column 43 (length: 4) + 425: Colon at line 122, column 47 (length: 1) + 426: KeywordStore at line 123, column 13 (length: 5) + 427: Identifier("has_version") at line 123, column 19 (length: 11) + 428: KeywordAs at line 123, column 31 (length: 2) + 429: BooleanLiteral(true) at line 123, column 34 (length: 3) + 430: Identifier("elif arg") at line 124, column 9 (length: 8) + 431: KeywordIs at line 124, column 18 (length: 2) + 432: StringLiteral("--verbose") at line 124, column 21 (length: 11) + 433: Colon at line 124, column 32 (length: 1) + 434: KeywordStore at line 125, column 13 (length: 5) + 435: Identifier("has_verbose") at line 125, column 19 (length: 11) + 436: KeywordAs at line 125, column 31 (length: 2) + 437: BooleanLiteral(true) at line 125, column 34 (length: 3) + 438: Identifier("elif substring") at line 126, column 9 (length: 14) + 439: KeywordOf at line 126, column 24 (length: 2) + 440: Identifier("arg") at line 126, column 27 (length: 3) + 441: KeywordAnd at line 126, column 31 (length: 3) + 442: IntLiteral(0) at line 126, column 35 (length: 1) + 443: KeywordAnd at line 126, column 37 (length: 3) + 444: IntLiteral(1) at line 126, column 41 (length: 1) + 445: KeywordIs at line 126, column 43 (length: 2) + 446: StringLiteral("-") at line 126, column 46 (length: 3) + 447: Colon at line 126, column 49 (length: 1) + 448: KeywordDisplay at line 127, column 13 (length: 7) + 449: StringLiteral(" Unknown flag: ") at line 127, column 21 (length: 18) + 450: KeywordWith at line 127, column 40 (length: 4) + 451: Identifier("arg") at line 127, column 45 (length: 3) + 452: Identifier("else") at line 128, column 9 (length: 4) + 453: Colon at line 128, column 13 (length: 1) + 454: KeywordPush at line 129, column 13 (length: 4) + 455: KeywordWith at line 129, column 18 (length: 4) + 456: Identifier("non_flag_args") at line 129, column 23 (length: 13) + 457: KeywordAnd at line 129, column 37 (length: 3) + 458: Identifier("arg") at line 129, column 41 (length: 3) + 459: KeywordEnd at line 130, column 9 (length: 3) + 460: KeywordCheck at line 130, column 13 (length: 5) + 461: KeywordEnd at line 131, column 5 (length: 3) + 462: KeywordFor at line 131, column 9 (length: 3) + 463: KeywordDisplay at line 133, column 5 (length: 7) + 464: StringLiteral("Flags detected:") at line 133, column 13 (length: 17) + 465: KeywordDisplay at line 134, column 5 (length: 7) + 466: StringLiteral(" Help flag: ") at line 134, column 13 (length: 15) + 467: KeywordWith at line 134, column 29 (length: 4) + 468: Identifier("has_help") at line 134, column 34 (length: 8) + 469: KeywordDisplay at line 135, column 5 (length: 7) + 470: StringLiteral(" Version flag: ") at line 135, column 13 (length: 18) + 471: KeywordWith at line 135, column 32 (length: 4) + 472: Identifier("has_version") at line 135, column 37 (length: 11) + 473: KeywordDisplay at line 136, column 5 (length: 7) + 474: StringLiteral(" Verbose flag: ") at line 136, column 13 (length: 18) + 475: KeywordWith at line 136, column 32 (length: 4) + 476: Identifier("has_verbose") at line 136, column 37 (length: 11) + 477: KeywordDisplay at line 138, column 5 (length: 7) + 478: StringLiteral("Non-flag arguments:") at line 138, column 13 (length: 21) + 479: KeywordFor at line 139, column 5 (length: 3) + 480: KeywordEach at line 139, column 9 (length: 4) + 481: Identifier("non_flag") at line 139, column 14 (length: 8) + 482: KeywordIn at line 139, column 23 (length: 2) + 483: Identifier("non_flag_args") at line 139, column 26 (length: 13) + 484: Colon at line 139, column 39 (length: 1) + 485: KeywordDisplay at line 140, column 9 (length: 7) + 486: StringLiteral(" - ") at line 140, column 17 (length: 6) + 487: KeywordWith at line 140, column 24 (length: 4) + 488: Identifier("non_flag") at line 140, column 29 (length: 8) + 489: KeywordEnd at line 141, column 5 (length: 3) + 490: KeywordFor at line 141, column 9 (length: 3) + 491: Identifier("else") at line 142, column 1 (length: 4) + 492: Colon at line 142, column 5 (length: 1) + 493: KeywordDisplay at line 143, column 5 (length: 7) + 494: StringLiteral("No arguments for flag parsing") at line 143, column 13 (length: 31) + 495: KeywordEnd at line 144, column 1 (length: 3) + 496: KeywordCheck at line 144, column 5 (length: 5) + 497: KeywordDisplay at line 145, column 1 (length: 7) + 498: StringLiteral("") at line 145, column 9 (length: 2) + 499: KeywordDisplay at line 148, column 1 (length: 7) + 500: StringLiteral("8. Environment Integration Test") at line 148, column 9 (length: 33) + 501: KeywordDisplay at line 150, column 1 (length: 7) + 502: StringLiteral("Program execution context:") at line 150, column 9 (length: 28) + 503: KeywordDisplay at line 151, column 1 (length: 7) + 504: StringLiteral(" Program: ") at line 151, column 9 (length: 13) + 505: KeywordWith at line 151, column 23 (length: 4) + 506: Identifier("program_name") at line 151, column 28 (length: 12) + 507: KeywordDisplay at line 152, column 1 (length: 7) + 508: StringLiteral(" Arguments: ") at line 152, column 9 (length: 15) + 509: KeywordWith at line 152, column 25 (length: 4) + 510: Identifier("arg_count") at line 152, column 30 (length: 9) + 511: KeywordDisplay at line 153, column 1 (length: 7) + 512: StringLiteral(" Current directory: ") at line 153, column 9 (length: 23) + 513: KeywordWith at line 153, column 33 (length: 4) + 514: Identifier("current_directory") at line 153, column 38 (length: 17) + 515: KeywordCheck at line 155, column 1 (length: 5) + 516: KeywordIf at line 155, column 7 (length: 2) + 517: Identifier("arg_count") at line 155, column 10 (length: 9) + 518: KeywordIs at line 155, column 20 (length: 2) + 519: KeywordGreater at line 155, column 23 (length: 7) + 520: KeywordThan at line 155, column 31 (length: 4) + 521: IntLiteral(0) at line 155, column 36 (length: 1) + 522: Colon at line 155, column 37 (length: 1) + 523: KeywordDisplay at line 156, column 5 (length: 7) + 524: StringLiteral(" Working with arguments in current environment") at line 156, column 13 (length: 49) + 525: KeywordStore at line 157, column 5 (length: 5) + 526: Identifier("combined_args") at line 157, column 11 (length: 13) + 527: KeywordAs at line 157, column 25 (length: 2) + 528: StringLiteral("") at line 157, column 28 (length: 2) + 529: KeywordFor at line 158, column 5 (length: 3) + 530: KeywordEach at line 158, column 9 (length: 4) + 531: Identifier("arg") at line 158, column 14 (length: 3) + 532: KeywordIn at line 158, column 18 (length: 2) + 533: Identifier("args") at line 158, column 21 (length: 4) + 534: Colon at line 158, column 25 (length: 1) + 535: KeywordStore at line 159, column 9 (length: 5) + 536: Identifier("combined_args") at line 159, column 15 (length: 13) + 537: KeywordAs at line 159, column 29 (length: 2) + 538: Identifier("combined_args") at line 159, column 32 (length: 13) + 539: KeywordWith at line 159, column 46 (length: 4) + 540: Identifier("arg") at line 159, column 51 (length: 3) + 541: KeywordWith at line 159, column 55 (length: 4) + 542: StringLiteral(" ") at line 159, column 60 (length: 3) + 543: KeywordEnd at line 160, column 5 (length: 3) + 544: KeywordFor at line 160, column 9 (length: 3) + 545: KeywordDisplay at line 161, column 5 (length: 7) + 546: StringLiteral(" Combined arguments: '") at line 161, column 13 (length: 25) + 547: KeywordWith at line 161, column 39 (length: 4) + 548: Identifier("combined_args") at line 161, column 44 (length: 13) + 549: KeywordWith at line 161, column 58 (length: 4) + 550: StringLiteral("'") at line 161, column 63 (length: 3) + 551: KeywordEnd at line 162, column 1 (length: 3) + 552: KeywordCheck at line 162, column 5 (length: 5) + 553: KeywordDisplay at line 163, column 1 (length: 7) + 554: StringLiteral("") at line 163, column 9 (length: 2) + 555: KeywordDisplay at line 166, column 1 (length: 7) + 556: StringLiteral("9. Argument Filtering") at line 166, column 9 (length: 23) + 557: KeywordCheck at line 167, column 1 (length: 5) + 558: KeywordIf at line 167, column 7 (length: 2) + 559: Identifier("arg_count") at line 167, column 10 (length: 9) + 560: KeywordIs at line 167, column 20 (length: 2) + 561: KeywordGreater at line 167, column 23 (length: 7) + 562: KeywordThan at line 167, column 31 (length: 4) + 563: IntLiteral(0) at line 167, column 36 (length: 1) + 564: Colon at line 167, column 37 (length: 1) + 565: KeywordStore at line 168, column 5 (length: 5) + 566: Identifier("long_args") at line 168, column 11 (length: 9) + 567: KeywordAs at line 168, column 21 (length: 2) + 568: LeftBracket at line 168, column 24 (length: 1) + 569: RightBracket at line 168, column 25 (length: 1) + 570: KeywordStore at line 169, column 5 (length: 5) + 571: Identifier("short_args") at line 169, column 11 (length: 10) + 572: KeywordAs at line 169, column 22 (length: 2) + 573: LeftBracket at line 169, column 25 (length: 1) + 574: RightBracket at line 169, column 26 (length: 1) + 575: KeywordFor at line 171, column 5 (length: 3) + 576: KeywordEach at line 171, column 9 (length: 4) + 577: Identifier("arg") at line 171, column 14 (length: 3) + 578: KeywordIn at line 171, column 18 (length: 2) + 579: Identifier("args") at line 171, column 21 (length: 4) + 580: Colon at line 171, column 25 (length: 1) + 581: KeywordCheck at line 172, column 9 (length: 5) + 582: KeywordIf at line 172, column 15 (length: 2) + 583: Identifier("length") at line 172, column 18 (length: 6) + 584: KeywordOf at line 172, column 25 (length: 2) + 585: Identifier("arg") at line 172, column 28 (length: 3) + 586: KeywordIs at line 172, column 32 (length: 2) + 587: KeywordGreater at line 172, column 35 (length: 7) + 588: KeywordThan at line 172, column 43 (length: 4) + 589: IntLiteral(5) at line 172, column 48 (length: 1) + 590: Colon at line 172, column 49 (length: 1) + 591: KeywordPush at line 173, column 13 (length: 4) + 592: KeywordWith at line 173, column 18 (length: 4) + 593: Identifier("long_args") at line 173, column 23 (length: 9) + 594: KeywordAnd at line 173, column 33 (length: 3) + 595: Identifier("arg") at line 173, column 37 (length: 3) + 596: Identifier("else") at line 174, column 9 (length: 4) + 597: Colon at line 174, column 13 (length: 1) + 598: KeywordPush at line 175, column 13 (length: 4) + 599: KeywordWith at line 175, column 18 (length: 4) + 600: Identifier("short_args") at line 175, column 23 (length: 10) + 601: KeywordAnd at line 175, column 34 (length: 3) + 602: Identifier("arg") at line 175, column 38 (length: 3) + 603: KeywordEnd at line 176, column 9 (length: 3) + 604: KeywordCheck at line 176, column 13 (length: 5) + 605: KeywordEnd at line 177, column 5 (length: 3) + 606: KeywordFor at line 177, column 9 (length: 3) + 607: KeywordDisplay at line 179, column 5 (length: 7) + 608: StringLiteral("Long arguments (>5 chars):") at line 179, column 13 (length: 28) + 609: KeywordFor at line 180, column 5 (length: 3) + 610: KeywordEach at line 180, column 9 (length: 4) + 611: Identifier("long_arg") at line 180, column 14 (length: 8) + 612: KeywordIn at line 180, column 23 (length: 2) + 613: Identifier("long_args") at line 180, column 26 (length: 9) + 614: Colon at line 180, column 35 (length: 1) + 615: KeywordDisplay at line 181, column 9 (length: 7) + 616: StringLiteral(" - ") at line 181, column 17 (length: 6) + 617: KeywordWith at line 181, column 24 (length: 4) + 618: Identifier("long_arg") at line 181, column 29 (length: 8) + 619: KeywordEnd at line 182, column 5 (length: 3) + 620: KeywordFor at line 182, column 9 (length: 3) + 621: KeywordDisplay at line 184, column 5 (length: 7) + 622: StringLiteral("Short arguments (≤5 chars):") at line 184, column 13 (length: 31) + 623: KeywordFor at line 185, column 5 (length: 3) + 624: KeywordEach at line 185, column 9 (length: 4) + 625: Identifier("short_arg") at line 185, column 14 (length: 9) + 626: KeywordIn at line 185, column 24 (length: 2) + 627: Identifier("short_args") at line 185, column 27 (length: 10) + 628: Colon at line 185, column 37 (length: 1) + 629: KeywordDisplay at line 186, column 9 (length: 7) + 630: StringLiteral(" - ") at line 186, column 17 (length: 6) + 631: KeywordWith at line 186, column 24 (length: 4) + 632: Identifier("short_arg") at line 186, column 29 (length: 9) + 633: KeywordEnd at line 187, column 5 (length: 3) + 634: KeywordFor at line 187, column 9 (length: 3) + 635: Identifier("else") at line 188, column 1 (length: 4) + 636: Colon at line 188, column 5 (length: 1) + 637: KeywordDisplay at line 189, column 5 (length: 7) + 638: StringLiteral("No arguments for filtering") at line 189, column 13 (length: 28) + 639: KeywordEnd at line 190, column 1 (length: 3) + 640: KeywordCheck at line 190, column 5 (length: 5) + 641: KeywordDisplay at line 191, column 1 (length: 7) + 642: StringLiteral("") at line 191, column 9 (length: 2) + 643: KeywordDisplay at line 194, column 1 (length: 7) + 644: StringLiteral("10. Execution Summary") at line 194, column 9 (length: 23) + 645: KeywordDisplay at line 195, column 1 (length: 7) + 646: StringLiteral("Program: ") at line 195, column 9 (length: 11) + 647: KeywordWith at line 195, column 21 (length: 4) + 648: Identifier("program_name") at line 195, column 26 (length: 12) + 649: KeywordDisplay at line 196, column 1 (length: 7) + 650: StringLiteral("Total arguments: ") at line 196, column 9 (length: 19) + 651: KeywordWith at line 196, column 29 (length: 4) + 652: Identifier("arg_count") at line 196, column 34 (length: 9) + 653: KeywordCheck at line 198, column 1 (length: 5) + 654: KeywordIf at line 198, column 7 (length: 2) + 655: Identifier("arg_count") at line 198, column 10 (length: 9) + 656: KeywordIs at line 198, column 20 (length: 2) + 657: KeywordGreater at line 198, column 23 (length: 7) + 658: KeywordThan at line 198, column 31 (length: 4) + 659: IntLiteral(0) at line 198, column 36 (length: 1) + 660: Colon at line 198, column 37 (length: 1) + 661: KeywordStore at line 199, column 5 (length: 5) + 662: Identifier("total_length") at line 199, column 11 (length: 12) + 663: KeywordAs at line 199, column 24 (length: 2) + 664: IntLiteral(0) at line 199, column 27 (length: 1) + 665: KeywordFor at line 200, column 5 (length: 3) + 666: KeywordEach at line 200, column 9 (length: 4) + 667: Identifier("arg") at line 200, column 14 (length: 3) + 668: KeywordIn at line 200, column 18 (length: 2) + 669: Identifier("args") at line 200, column 21 (length: 4) + 670: Colon at line 200, column 25 (length: 1) + 671: KeywordStore at line 201, column 9 (length: 5) + 672: Identifier("total_length") at line 201, column 15 (length: 12) + 673: KeywordAs at line 201, column 28 (length: 2) + 674: Identifier("total_length") at line 201, column 31 (length: 12) + 675: Plus at line 201, column 44 (length: 1) + 676: Identifier("length") at line 201, column 46 (length: 6) + 677: KeywordOf at line 201, column 53 (length: 2) + 678: Identifier("arg") at line 201, column 56 (length: 3) + 679: KeywordEnd at line 202, column 5 (length: 3) + 680: KeywordFor at line 202, column 9 (length: 3) + 681: KeywordDisplay at line 203, column 5 (length: 7) + 682: StringLiteral("Total character count: ") at line 203, column 13 (length: 25) + 683: KeywordWith at line 203, column 39 (length: 4) + 684: Identifier("total_length") at line 203, column 44 (length: 12) + 685: KeywordDisplay at line 204, column 5 (length: 7) + 686: StringLiteral("Average argument length: ") at line 204, column 13 (length: 27) + 687: KeywordWith at line 204, column 41 (length: 4) + 688: Identifier("total_length arg_count") at line 204, column 46 (length: 22) + 689: KeywordDisplay at line 206, column 5 (length: 7) + 690: StringLiteral("Shortest argument: ") at line 206, column 13 (length: 21) + 691: KeywordWith at line 206, column 35 (length: 4) + 692: Identifier("args") at line 206, column 40 (length: 4) + 693: LeftBracket at line 206, column 44 (length: 1) + 694: IntLiteral(0) at line 206, column 45 (length: 1) + 695: RightBracket at line 206, column 46 (length: 1) + 696: KeywordDisplay at line 207, column 5 (length: 7) + 697: StringLiteral("Arguments summary completed") at line 207, column 13 (length: 29) + 698: Identifier("else") at line 208, column 1 (length: 4) + 699: Colon at line 208, column 5 (length: 1) + 700: KeywordDisplay at line 209, column 5 (length: 7) + 701: StringLiteral("No arguments provided") at line 209, column 13 (length: 23) + 702: KeywordDisplay at line 210, column 5 (length: 7) + 703: StringLiteral("Try running with: program.wfl arg1 arg2 --flag value") at line 210, column 13 (length: 54) + 704: KeywordEnd at line 211, column 1 (length: 3) + 705: KeywordCheck at line 211, column 5 (length: 5) + 706: KeywordDisplay at line 212, column 1 (length: 7) + 707: StringLiteral("") at line 212, column 9 (length: 2) + 708: KeywordDisplay at line 214, column 1 (length: 7) + 709: StringLiteral("=== Command Line Arguments Tests Completed ===") at line 214, column 9 (length: 48) + 710: KeywordDisplay at line 215, column 1 (length: 7) + 711: StringLiteral("") at line 215, column 9 (length: 2) + 712: KeywordDisplay at line 216, column 1 (length: 7) + 713: StringLiteral("To test this program, run it with various arguments:") at line 216, column 9 (length: 54) + 714: KeywordDisplay at line 217, column 1 (length: 7) + 715: StringLiteral(" cargo run -- TestPrograms/args_comprehensive.wfl hello world 123") at line 217, column 9 (length: 68) + 716: KeywordDisplay at line 218, column 1 (length: 7) + 717: StringLiteral(" cargo run -- TestPrograms/args_comprehensive.wfl --help --verbose file.txt") at line 218, column 9 (length: 78) + 718: KeywordDisplay at line 219, column 1 (length: 7) + 719: StringLiteral(" cargo run -- TestPrograms/args_comprehensive.wfl") at line 219, column 9 (length: 52) diff --git a/TestPrograms/arity_bug_test.wfl b/TestPrograms/arity_bug_test.wfl new file mode 100644 index 00000000..8a46c88d --- /dev/null +++ b/TestPrograms/arity_bug_test.wfl @@ -0,0 +1,46 @@ +// Arity Bug Test - WFL +// This test should expose the typechecker arity bug where multi-argument +// builtin functions incorrectly default to expecting 1 argument + +display "=== WFL Builtin Function Arity Bug Test ===" +display "" + +// Test 0-argument functions (should accept 0 args) +display "Testing 0-argument functions:" +store rand_val as random +display "Random value: " with rand_val + +// Test 2-argument functions that currently fail with arity errors +display "" +display "Testing 2-argument functions:" + +// Text functions with 2 arguments +store sample_text as "Hello World" +store search_text as "World" +store contains_result as contains of sample_text and search_text +display "Contains test: " with contains_result + +// List functions with 2 arguments +store my_list as [1 and 2 and 3] +push of my_list and 4 +display "List after push: " with my_list + +store list_contains as contains of my_list and 2 +display "List contains 2: " with list_contains + +store index_result as indexof of my_list and 3 +display "Index of 3: " with index_result + +// Math functions with 2+ arguments (not implemented yet, but should be recognized) +// These will fail both due to arity bug AND because functions aren't implemented +// But the arity error should come first + +// Note: min and max are not implemented yet, so these would fail anyway +// But we want to test the arity checking specifically +// store min_result as min of 5 and 3 +// store max_result as max of 5 and 3 +// store power_result as power of 2 and 3 + +display "" +display "Test completed successfully!" +display "If you see this message, the arity bug has been fixed!" \ No newline at end of file diff --git a/TestPrograms/arity_bug_trigger.wfl b/TestPrograms/arity_bug_trigger.wfl new file mode 100644 index 00000000..65494175 --- /dev/null +++ b/TestPrograms/arity_bug_trigger.wfl @@ -0,0 +1,12 @@ +// Test to trigger the specific arity bug +// Using builtin functions that should take 2+ arguments but typechecker thinks they take 1 + +store a as 5 +store b as 10 + +// Test functions that are in builtins but incorrectly default to 1 argument +// This should show type checking errors, not runtime errors + +// Try to use min function with 2 arguments +// According to bug report, this should fail with "Function expects 1 arguments, but 2 were provided" +display min of a and b \ No newline at end of file diff --git a/TestPrograms/arity_success_test.wfl b/TestPrograms/arity_success_test.wfl new file mode 100644 index 00000000..44122120 --- /dev/null +++ b/TestPrograms/arity_success_test.wfl @@ -0,0 +1,19 @@ +// Test that confirms the arity bug is fixed + +// Test 0-argument function (was incorrectly expecting 1) +store rand_val as random +display "Random: " with rand_val + +// Test 3-argument function (was correctly defined) +store mytext as "Hello World" +store sub as substring of mytext and 0 and 5 +display "Substring: " with sub + +// Test another 3-argument function +store value as 7 +store min_val as 1 +store max_val as 10 +store clamped as clamp of value and min_val and max_val +display "Clamped: " with clamped + +display "All tests passed! Arity bug is fixed!" \ No newline at end of file diff --git a/TestPrograms/bracket_indexing_test.wfl b/TestPrograms/bracket_indexing_test.wfl new file mode 100644 index 00000000..7ce42548 --- /dev/null +++ b/TestPrograms/bracket_indexing_test.wfl @@ -0,0 +1,16 @@ +// Test bracket array indexing syntax +// This test should fail until the parser supports bracket indexing + +store my_list as ["apple" and "banana" and "cherry" and "date"] +store index as 2 + +display "List: " with my_list +display "Index: " with index +display "Element at index: " with my_list[index] + +// Test with integer literal +display "First element: " with my_list[0] + +// Test with expression +store last_index as (length of my_list) minus 1 +display "Last element: " with my_list[last_index] \ No newline at end of file diff --git a/TestPrograms/minimal_arity_test.wfl b/TestPrograms/minimal_arity_test.wfl new file mode 100644 index 00000000..913ea9e4 --- /dev/null +++ b/TestPrograms/minimal_arity_test.wfl @@ -0,0 +1,20 @@ +// Minimal test to expose the arity bug + +// Test 0-argument function (should work) +store rand_val as random +display rand_val + +// Test functions that should take 2 arguments but the typechecker thinks take 1 +// This should cause typechecker errors, not parse errors + +// Using substring which is correctly defined (3 args) +store mytext as "Hello" +store sub as substring of mytext and 1 and 3 +display sub + +// Using functions that default to 1 arg incorrectly +// These should trigger type checking errors +store a as 5 +store b as 10 +// Test min function (not implemented but should show arity error) +// display min of a and b \ No newline at end of file diff --git a/TestPrograms/test_fixed_arity.wfl b/TestPrograms/test_fixed_arity.wfl new file mode 100644 index 00000000..3aea72e9 --- /dev/null +++ b/TestPrograms/test_fixed_arity.wfl @@ -0,0 +1,20 @@ +// Test that the arity fix works for implemented functions + +// Test 0-argument function +store rand1 as random +store rand2 as random +display "Random 1: " with rand1 +display "Random 2: " with rand2 + +// Test 2-argument functions that should now work +store text as "Hello World" +store result as contains of text and "World" +display "Contains 'World': " with result + +store text2 as "Testing" +store result2 as contains of text2 and "xyz" +display "Contains 'xyz': " with result2 + +// Test 3-argument function (was already working) +store sub as substring of text and 0 and 5 +display "Substring: " with sub \ No newline at end of file diff --git a/bug.md b/bug.md new file mode 100644 index 00000000..b84db716 --- /dev/null +++ b/bug.md @@ -0,0 +1,190 @@ +# Bug Report: Incomplete Builtin Function Arity Definitions + +## Bug Summary +The WFL typechecker has incomplete builtin function arity definitions, causing false argument-count errors. When builtin function names are converted to Function types, they use a small hardcoded match set to determine param_count, defaulting to 1 for unknown functions. This causes many builtin functions that require 2 or more arguments to incorrectly report type errors claiming they only expect 1 argument. + +## Evidence Collected + +### 1. Problematic Code Location +**File**: `C:\logbie\wfl\src\typechecker\mod.rs` +**Lines**: 1230-1237 + +```rust +let param_count = match name.as_str() { + "substring" => 3, // text, start, length + "replace" => 3, // text, old, new + "clamp" => 3, // value, min, max + "padleft" | "padright" => 2, // text, length + "indexof" | "index_of" | "lastindexof" | "last_index_of" => 2, // text, substring + _ => 1, // Most functions take 1 parameter +}; +``` + +### 2. Current Match Cases and Their Param Counts +Only 6 functions have correct arity definitions: +- `substring`: 3 arguments (text, start, length) +- `replace`: 3 arguments (text, old, new) +- `clamp`: 3 arguments (value, min, max) +- `padleft`, `padright`: 2 arguments (text, length) +- `indexof`, `index_of`, `lastindexof`, `last_index_of`: 2 arguments (text, substring) +- **All others default to 1 argument** + +### 3. Complete List of Builtin Functions by Module + +**From `C:\logbie\wfl\src\builtins.rs` (Complete Registry)**: + +#### Implemented Functions (with verified arities): + +**Math Module** (`src/stdlib/math.rs`): +- `abs`: 1 argument ✓ (correctly defaulted) +- `round`: 1 argument ✓ (correctly defaulted) +- `floor`: 1 argument ✓ (correctly defaulted) +- `ceil`: 1 argument ✓ (correctly defaulted) +- `random`: 0 arguments ❌ (incorrectly defaulted to 1) +- `clamp`: 3 arguments ✓ (correctly defined) + +**Text Module** (`src/stdlib/text.rs`): +- `touppercase`, `to_uppercase`: 1 argument ✓ (correctly defaulted) +- `tolowercase`, `to_lowercase`: 1 argument ✓ (correctly defaulted) +- `contains`: 2 arguments ❌ (incorrectly defaulted to 1) +- `substring`: 3 arguments ✓ (correctly defined) + +**List Module** (`src/stdlib/list.rs`): +- `length`: 1 argument ✓ (correctly defaulted) +- `push`: 2 arguments ❌ (incorrectly defaulted to 1) +- `pop`: 1 argument ✓ (correctly defaulted) +- `contains`: 2 arguments ❌ (incorrectly defaulted to 1) +- `indexof`, `index_of`: 2 arguments ✓ (correctly defined) + +#### Recognized but NOT Implemented Functions: + +**Math Functions** (listed in builtins but not implemented): +- `min`: **2+ arguments** ❌ (incorrectly defaulted to 1) +- `max`: **2+ arguments** ❌ (incorrectly defaulted to 1) +- `power`: **2 arguments** ❌ (incorrectly defaulted to 1) +- `sqrt`, `sin`, `cos`, `tan`: 1 argument ✓ (correctly defaulted) + +**Text Functions** (listed in builtins but not implemented): +- `replace`: 3 arguments ✓ (correctly defined) +- `trim`: 1 argument ✓ (correctly defaulted) +- `padleft`, `padright`: 2 arguments ✓ (correctly defined) +- `capitalize`, `reverse`: 1 argument ✓ (correctly defaulted) +- `startswith`, `starts_with`: **2 arguments** ❌ (incorrectly defaulted to 1) +- `endswith`, `ends_with`: **2 arguments** ❌ (incorrectly defaulted to 1) +- `split`: **2 arguments** ❌ (incorrectly defaulted to 1) +- `join`: **2 arguments** ❌ (incorrectly defaulted to 1) + +**List Functions** (listed in builtins but not implemented): +- Most list operations: varying arities, mostly defaulted incorrectly + +## Reproduction Steps + +1. Create a simple WFL program with multi-argument builtin function calls: +```wfl +store a as 10 +store b as 5 +display min of a and b +``` + +2. Run with: `./target/release/wfl.exe test.wfl` + +3. Observe the type checking error: +``` +Type checking warnings: +error: Function expects 1 arguments, but 2 were provided +``` + +## Analysis + +### Root Cause Investigation + +The issue stems from a **hardcoded lookup table** approach in the typechecker that only covers a small subset of builtin functions. The code at lines 1230-1237 in `src/typechecker/mod.rs` uses a match statement to determine parameter counts, but it only handles 6 specific cases and defaults everything else to 1 argument. + +### Key Findings + +1. **Incomplete Coverage**: Only 6 out of 80+ builtin functions have correct arity definitions +2. **Default is Wrong**: The default of 1 argument is incorrect for many functions +3. **Disconnect**: The arity definitions are disconnected from the actual function implementations +4. **Scale of Impact**: This affects most multi-argument builtin functions + +### Impact Assessment + +**Severity**: High - Prevents use of essential builtin functions + +**Scope**: Affects all builtin functions requiring 2+ arguments that are not in the hardcoded list: + +**Incorrectly Defaulted Functions (Major Impact)**: +- `min`, `max`: Basic math operations requiring 2+ arguments +- `power`: Exponentiation requiring 2 arguments +- `contains`: Text/list searching requiring 2 arguments +- `push`: List manipulation requiring 2 arguments +- `starts_with`, `ends_with`: Text checking requiring 2 arguments +- `split`, `join`: String manipulation requiring 2 arguments +- `random`: Actually requires 0 arguments but defaults to 1 + +**Test Case Evidence**: The test program `TestPrograms/stdlib_comprehensive.wfl` contains multiple calls to these functions that would trigger this bug, including: +- `min of a and b` +- `max of a and b` +- `power of 5 and 2` +- `contains of sample_text and search_text` + +## Root Cause + +The fundamental issue is that **parameter count determination is hardcoded and incomplete**. The typechecker attempts to create Function types for builtin functions using a small lookup table, but this table covers less than 10% of the declared builtin functions. + +The problem occurs in the identifier type checking logic where builtin function names are converted to Function types. The param_count is determined by a match statement that only handles a few specific cases and defaults to 1 for everything else. + +## Recommended Investigation Areas + +### 1. Immediate Fix Areas +- **File**: `C:\logbie\wfl\src\typechecker\mod.rs`, lines 1230-1237 +- **Issue**: Expand the match statement to include correct arities for all builtin functions +- **Priority**: High - This is the core bug location + +### 2. Design Improvements +- **Synchronization**: Create a centralized arity registry that both the typechecker and interpreter can use +- **Validation**: Ensure arity definitions match actual function implementations +- **Testing**: Add comprehensive tests for builtin function arity validation + +### 3. Implementation Gaps +- **Missing Functions**: Many builtin functions are declared but not implemented (`min`, `max`, `power`, etc.) +- **File**: Various stdlib modules need to implement missing functions +- **Priority**: Medium - These are functionality gaps beyond the arity bug + +### 4. Test Coverage +- **File**: Need specific tests for builtin function arity validation +- **Focus**: Create test cases that specifically verify correct argument count handling +- **Location**: Add to existing test suites or create new arity-specific tests + +## Suggested Fix Approach + +### Minimal Expansion (Quick Fix) +Expand the match statement in `src/typechecker/mod.rs` to include the most commonly used multi-argument functions: + +```rust +let param_count = match name.as_str() { + // Existing entries... + "substring" => 3, + "replace" => 3, + "clamp" => 3, + "padleft" | "padright" => 2, + "indexof" | "index_of" | "lastindexof" | "last_index_of" => 2, + + // Critical additions: + "min" | "max" => 2, // Note: Actually variadic but minimum 2 + "power" => 2, + "contains" => 2, + "push" => 2, + "starts_with" | "startswith" => 2, + "ends_with" | "endswith" => 2, + "split" | "join" => 2, + "random" => 0, + + _ => 1, +}; +``` + +### Comprehensive Solution (Recommended) +Create a centralized arity registry that can be shared between the typechecker and function implementations, eliminating the possibility of drift between the two systems. + +This bug significantly impacts the usability of WFL's builtin functions and should be prioritized for fixing. \ No newline at end of file diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 688095a3..d740f74e 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -255,6 +255,55 @@ impl Analyzer { }; let _ = global_scope.define(loop_symbol); + // Define runtime command-line argument variables + // These are defined at runtime by the interpreter but need to be known + // to the static analyzer to avoid false undefined variable errors + + let arg_count_symbol = Symbol { + name: "arg_count".to_string(), + kind: SymbolKind::Variable { mutable: false }, + symbol_type: Some(Type::Number), + line: 0, + column: 0, + }; + let _ = global_scope.define(arg_count_symbol); + + let args_symbol = Symbol { + name: "args".to_string(), + kind: SymbolKind::Variable { mutable: false }, + symbol_type: Some(Type::List(Box::new(Type::Text))), + line: 0, + column: 0, + }; + let _ = global_scope.define(args_symbol); + + let program_name_symbol = Symbol { + name: "program_name".to_string(), + kind: SymbolKind::Variable { mutable: false }, + symbol_type: Some(Type::Text), + line: 0, + column: 0, + }; + let _ = global_scope.define(program_name_symbol); + + let current_directory_symbol = Symbol { + name: "current_directory".to_string(), + kind: SymbolKind::Variable { mutable: false }, + symbol_type: Some(Type::Text), + line: 0, + column: 0, + }; + let _ = global_scope.define(current_directory_symbol); + + let positional_args_symbol = Symbol { + name: "positional_args".to_string(), + kind: SymbolKind::Variable { mutable: false }, + symbol_type: Some(Type::List(Box::new(Type::Text))), + line: 0, + column: 0, + }; + let _ = global_scope.define(positional_args_symbol); + Analyzer { current_scope: global_scope, errors: Vec::new(), diff --git a/src/analyzer/static_analyzer.rs b/src/analyzer/static_analyzer.rs index d2a6b8b9..b7894811 100644 --- a/src/analyzer/static_analyzer.rs +++ b/src/analyzer/static_analyzer.rs @@ -1467,4 +1467,150 @@ mod tests { "Expected no unused variable diagnostics for RepeatUntilLoop" ); } + + #[test] + fn test_variable_used_in_array_access() { + // This test reproduces the false positive for 'last_index' in args_comprehensive.wfl + // where a variable is declared and immediately used in array access within nested conditionals + let program = Program { + statements: vec![ + Statement::VariableDeclaration { + name: "arg_count".to_string(), + value: Expression::Literal(Literal::Integer(5), 1, 1), + is_constant: false, + line: 1, + column: 1, + }, + Statement::VariableDeclaration { + name: "args".to_string(), + value: Expression::Literal( + Literal::List(vec![ + Expression::Literal(Literal::String("a".to_string()), 1, 1), + Expression::Literal(Literal::String("b".to_string()), 1, 1), + Expression::Literal(Literal::String("c".to_string()), 1, 1), + ]), + 2, + 1, + ), + is_constant: false, + line: 2, + column: 1, + }, + // Triple-nested conditional structure similar to args_comprehensive.wfl + Statement::IfStatement { + condition: Expression::BinaryOperation { + left: Box::new(Expression::Variable("arg_count".to_string(), 3, 10)), + operator: Operator::Equals, + right: Box::new(Expression::Literal(Literal::Integer(0), 3, 20)), + line: 3, + column: 10, + }, + then_block: vec![Statement::DisplayStatement { + value: Expression::Literal(Literal::String("No args".to_string()), 4, 1), + line: 4, + column: 1, + }], + else_block: Some(vec![Statement::IfStatement { + condition: Expression::BinaryOperation { + left: Box::new(Expression::Variable("arg_count".to_string(), 5, 10)), + operator: Operator::Equals, + right: Box::new(Expression::Literal(Literal::Integer(1), 5, 20)), + line: 5, + column: 10, + }, + then_block: vec![Statement::DisplayStatement { + value: Expression::Literal( + Literal::String("One arg".to_string()), + 6, + 1, + ), + line: 6, + column: 1, + }], + else_block: Some(vec![Statement::IfStatement { + condition: Expression::BinaryOperation { + left: Box::new(Expression::Variable( + "arg_count".to_string(), + 7, + 10, + )), + operator: Operator::Equals, + right: Box::new(Expression::Literal(Literal::Integer(2), 7, 20)), + line: 7, + column: 10, + }, + then_block: vec![Statement::DisplayStatement { + value: Expression::Literal( + Literal::String("Two args".to_string()), + 8, + 1, + ), + line: 8, + column: 1, + }], + else_block: Some(vec![ + Statement::VariableDeclaration { + name: "last_index".to_string(), + value: Expression::BinaryOperation { + left: Box::new(Expression::Variable( + "arg_count".to_string(), + 9, + 30, + )), + operator: Operator::Minus, + right: Box::new(Expression::Literal( + Literal::Integer(1), + 9, + 40, + )), + line: 9, + column: 30, + }, + is_constant: false, + line: 9, + column: 1, + }, + Statement::DisplayStatement { + value: Expression::IndexAccess { + collection: Box::new(Expression::Variable( + "args".to_string(), + 10, + 20, + )), + index: Box::new(Expression::Variable( + "last_index".to_string(), + 10, + 25, + )), + line: 10, + column: 20, + }, + line: 10, + column: 1, + }, + ]), + line: 7, + column: 1, + }]), + line: 5, + column: 1, + }]), + line: 3, + column: 1, + }, + ], + }; + + let analyzer = Analyzer::new(); + let file_id = 0; + + let diagnostics = analyzer.check_unused_variables(&program, file_id); + + // last_index should NOT be reported as unused since it's used in the IndexAccess + // This test currently FAILS because of the bug + assert!( + !diagnostics.iter().any(|d| d.message.contains("last_index")), + "last_index should not be reported as unused when used in array access" + ); + } } diff --git a/src/builtins.rs b/src/builtins.rs index c2d33e00..f4143d4a 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -190,6 +190,93 @@ pub fn builtin_functions() -> impl Iterator { BUILTIN_FUNCTIONS.iter().copied() } +/// Get the parameter count (arity) for a builtin function +/// Returns the correct number of parameters each function expects +pub fn get_function_arity(name: &str) -> usize { + match name { + // === CORE FUNCTIONS === + "print" => 1, + "typeof" | "type_of" => 1, + "isnothing" | "is_nothing" => 1, + + // === MATH FUNCTIONS === + // Single argument functions + "abs" | "round" | "floor" | "ceil" | "sqrt" | "sin" | "cos" | "tan" => 1, + // Zero argument functions + "random" => 0, + // Two argument functions + "min" | "max" | "power" => 2, + // Three argument functions + "clamp" => 3, + + // === TEXT FUNCTIONS === + // Single argument functions + "length" | "touppercase" | "to_uppercase" | "tolowercase" | "to_lowercase" | "trim" + | "capitalize" | "reverse" => 1, + // Two argument functions + "contains" | "indexof" | "index_of" | "lastindexof" | "last_index_of" | "padleft" + | "padright" | "startswith" | "starts_with" | "endswith" | "ends_with" | "split" + | "join" => 2, + // Three argument functions + "substring" | "replace" => 3, + + // === LIST FUNCTIONS === + // Single argument functions + "pop" | "shift" | "sort" | "reverse_list" | "unique" | "clear" | "count" | "size" => 1, + // Two argument functions + "push" | "unshift" | "remove_at" | "removeat" | "includes" | "find" | "find_index" => 2, + // Three argument functions + "insert_at" | "insertat" | "slice" => 3, + // Variable argument functions (using 2 as minimum for now) + "filter" | "map" | "reduce" | "foreach" | "every" | "some" | "fill" | "concat" => 2, + + // === TIME FUNCTIONS === + // Zero argument functions + "now" | "today" | "datetime_now" | "time" | "current_date" => 0, + // Single argument functions + "year" | "month" | "day" | "hour" | "minute" | "second" | "dayofweek" | "day_of_week" + | "isleapyear" | "is_leap_year" | "sleep" => 1, + // Two argument functions + "format_date" | "format_time" | "format_datetime" | "parse_date" | "parse_time" + | "add_days" | "days_between" | "adddays" | "formatdate" | "formattime" | "parsedate" + | "daysbetween" | "add_hours" | "addhours" | "add_minutes" | "addminutes" + | "add_seconds" | "addseconds" | "add_months" | "addmonths" | "add_years" | "addyears" + | "months_between" | "monthsbetween" | "years_between" | "yearsbetween" => 2, + // Three argument functions + "create_time" | "create_date" => 3, + + // === PATTERN FUNCTIONS === + // Single argument functions + "compile_pattern" => 1, + // Two argument functions + "pattern_matches" | "pattern_find" | "match_pattern" | "pattern" | "match" | "test" + | "extract" | "ismatch" | "is_match" => 2, + // Three argument functions + "pattern_find_all" | "replace_pattern" | "findall" | "find_all" => 3, + + // === FILE SYSTEM FUNCTIONS === + // Single argument functions + "list_dir" | "path_basename" | "path_dirname" | "makedirs" | "file_mtime" + | "path_exists" | "is_file" | "is_dir" | "read_file" | "file_exists" | "delete_file" + | "create_directory" | "list_directory" | "is_directory" => 1, + // Two argument functions + "glob" | "rglob" | "path_join" | "write_file" => 2, + + // === SPECIAL TEST FUNCTIONS === + "helper_function" | "nested_function" => 1, + + // Default case for unknown functions + // This should not happen if all builtins are properly catalogued above + _ => { + eprintln!( + "Warning: Unknown builtin function '{}' - defaulting to 1 argument", + name + ); + 1 + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -243,4 +330,72 @@ mod tests { "Duplicate builtin function names detected" ); } + + #[test] + fn test_function_arity_mappings() { + // Test critical functions that were causing the bug + assert_eq!( + get_function_arity("random"), + 0, + "random should take 0 arguments" + ); + assert_eq!(get_function_arity("min"), 2, "min should take 2 arguments"); + assert_eq!(get_function_arity("max"), 2, "max should take 2 arguments"); + assert_eq!( + get_function_arity("power"), + 2, + "power should take 2 arguments" + ); + assert_eq!( + get_function_arity("contains"), + 2, + "contains should take 2 arguments" + ); + assert_eq!( + get_function_arity("push"), + 2, + "push should take 2 arguments" + ); + + // Test correctly defined functions still work + assert_eq!( + get_function_arity("substring"), + 3, + "substring should take 3 arguments" + ); + assert_eq!( + get_function_arity("clamp"), + 3, + "clamp should take 3 arguments" + ); + assert_eq!(get_function_arity("abs"), 1, "abs should take 1 argument"); + + // Test both versions of function names + assert_eq!( + get_function_arity("indexof"), + 2, + "indexof should take 2 arguments" + ); + assert_eq!( + get_function_arity("index_of"), + 2, + "index_of should take 2 arguments" + ); + } + + #[test] + fn test_all_builtins_have_arity_definition() { + // Ensure all builtin functions have arity definitions + // This prevents regression where new functions are added but arity is not defined + for function_name in BUILTIN_FUNCTIONS { + let arity = get_function_arity(function_name); + // Just verify it doesn't panic and returns a reasonable value + assert!( + arity <= 10, + "Function '{}' has unreasonable arity: {}", + function_name, + arity + ); + } + } } diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index c93135ac..9c472eef 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -699,6 +699,26 @@ impl Interpreter { // Store argument count let _ = env.define("arg_count", Value::Number(self.script_args.len() as f64)); + // Store program name (first argument or empty string) + let program_name = if self.script_args.is_empty() { + "wfl".to_string() + } else { + // Extract just the filename from the path + std::path::Path::new(&self.script_args[0]) + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned() + }; + let _ = env.define("program_name", Value::Text(Rc::from(program_name))); + + // Store current directory + let current_dir = std::env::current_dir() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + let _ = env.define("current_directory", Value::Text(Rc::from(current_dir))); + // Store flags as individual variables with flag_ prefix for (key, value) in flags_map { let _ = env.define(&format!("flag_{key}"), value); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 389474c9..9b80b1fd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2617,6 +2617,40 @@ impl<'a> Parser<'a> { column: token.column, }; } + Token::LeftBracket => { + self.tokens.next(); // Consume "[" + + let index = self.parse_expression()?; + + // Expect closing bracket + if let Some(closing_token) = self.tokens.peek().cloned() { + if closing_token.token == Token::RightBracket { + self.tokens.next(); // Consume "]" + expr = Expression::IndexAccess { + collection: Box::new(expr), + index: Box::new(index), + line: token.line, + column: token.column, + }; + } else { + return Err(ParseError::new( + format!( + "Expected ']' after array index, found {:?}", + closing_token.token + ), + closing_token.line, + closing_token.column, + )); + } + } else { + return Err(ParseError::new( + "Expected ']' after array index, found end of input" + .to_string(), + token.line, + token.column, + )); + } + } // Handle static member access: "Container.staticMember" Token::Identifier(id) if id == "." => { self.tokens.next(); // Consume "." diff --git a/src/parser/tests.rs b/src/parser/tests.rs index 64ae099b..6e2e31da 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -905,3 +905,140 @@ fn test_unary_minus_with_complex_expression() { panic!("Expected display statement, got: {result:?}"); } } +#[test] +fn test_bracket_array_indexing() { + // Test basic bracket indexing with integer literal + let input = r#"display args[0]"#; + let tokens = lex_wfl_with_positions(input); + let mut parser = Parser::new(&tokens); + + let result = parser.parse_statement(); + assert!( + result.is_ok(), + "Failed to parse basic bracket indexing: {result:?}" + ); + + if let Ok(Statement::DisplayStatement { value, .. }) = result { + if let Expression::IndexAccess { + collection, index, .. + } = value + { + // Collection should be a variable "args" + if let Expression::Variable(var_name, ..) = *collection { + assert_eq!(var_name, "args", "Expected collection to be 'args'"); + } else { + panic!("Expected collection to be Variable, got: {collection:?}"); + } + + // Index should be integer literal 0 + if let Expression::Literal(Literal::Integer(n), ..) = *index { + assert_eq!(n, 0, "Expected index to be 0"); + } else { + panic!("Expected index to be integer literal 0, got: {index:?}"); + } + } else { + panic!("Expected IndexAccess expression, got: {value:?}"); + } + } else { + panic!("Expected DisplayStatement, got: {result:?}"); + } +} + +#[test] +fn test_bracket_array_indexing_with_variable() { + // Test bracket indexing with variable as index + let input = r#"display args[last_index]"#; + let tokens = lex_wfl_with_positions(input); + let mut parser = Parser::new(&tokens); + + let result = parser.parse_statement(); + assert!( + result.is_ok(), + "Failed to parse bracket indexing with variable: {result:?}" + ); + + if let Ok(Statement::DisplayStatement { value, .. }) = result { + if let Expression::IndexAccess { + collection, index, .. + } = value + { + // Collection should be a variable "args" + if let Expression::Variable(var_name, ..) = *collection { + assert_eq!(var_name, "args", "Expected collection to be 'args'"); + } else { + panic!("Expected collection to be Variable, got: {collection:?}"); + } + + // Index should be variable "last_index" + if let Expression::Variable(var_name, ..) = *index { + assert_eq!(var_name, "last_index", "Expected index to be 'last_index'"); + } else { + panic!("Expected index to be variable 'last_index', got: {index:?}"); + } + } else { + panic!("Expected IndexAccess expression, got: {value:?}"); + } + } else { + panic!("Expected DisplayStatement, got: {result:?}"); + } +} + +#[test] +fn test_bracket_array_indexing_with_expression() { + // Test bracket indexing with expression as index + let input = r#"display my_list[count minus 1]"#; + let tokens = lex_wfl_with_positions(input); + let mut parser = Parser::new(&tokens); + + let result = parser.parse_statement(); + assert!( + result.is_ok(), + "Failed to parse bracket indexing with expression: {result:?}" + ); + + if let Ok(Statement::DisplayStatement { value, .. }) = result { + if let Expression::IndexAccess { + collection, index, .. + } = value + { + // Collection should be a variable "my_list" + if let Expression::Variable(var_name, ..) = *collection { + assert_eq!(var_name, "my_list", "Expected collection to be 'my_list'"); + } else { + panic!("Expected collection to be Variable, got: {collection:?}"); + } + + // Index should be a binary operation "count minus 1" + if let Expression::BinaryOperation { + left, + operator, + right, + .. + } = *index + { + // Left should be variable "count" + if let Expression::Variable(var_name, ..) = *left { + assert_eq!(var_name, "count", "Expected left operand to be 'count'"); + } else { + panic!("Expected left operand to be Variable, got: {left:?}"); + } + + // Operator should be Minus + assert_eq!(operator, Operator::Minus, "Expected operator to be Minus"); + + // Right should be integer literal 1 + if let Expression::Literal(Literal::Integer(n), ..) = *right { + assert_eq!(n, 1, "Expected right operand to be 1"); + } else { + panic!("Expected right operand to be integer literal 1, got: {right:?}"); + } + } else { + panic!("Expected index to be BinaryOperation, got: {index:?}"); + } + } else { + panic!("Expected IndexAccess expression, got: {value:?}"); + } + } else { + panic!("Expected DisplayStatement, got: {result:?}"); + } +} diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index 79b2c5c4..addbc85d 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -1,4 +1,5 @@ use crate::analyzer::Analyzer; +use crate::builtins; use crate::parser::ast::{Expression, Literal, Operator, Program, Statement, Type, UnaryOperator}; use std::fmt; @@ -1224,6 +1225,18 @@ impl TypeChecker { // Special case for loopcounter - it's a Number return Type::Number; } + + // For builtin functions, return their proper type + if Analyzer::is_builtin_function(name) { + let param_count = builtins::get_function_arity(name); + return Type::Function { + parameters: vec![Type::Any; param_count], + return_type: Box::new( + self.get_builtin_function_type(name, param_count), + ), + }; + } + Type::Unknown } else { // The analyzer already reports undefined variables, so we don't need to duplicate the error @@ -1649,27 +1662,35 @@ impl TypeChecker { // This matches the interpreter's behavior which converts values to strings Type::Text } - Expression::PatternMatch { text, pattern, .. } => { + Expression::PatternMatch { + text, + pattern, + line, + column, + } => { let text_type = self.infer_expression_type(text); let pattern_type = self.infer_expression_type(pattern); - if text_type != Type::Text { + if text_type != Type::Text && text_type != Type::Unknown { self.type_error( format!("Expected Text for pattern matching, got {text_type}"), Some(Type::Text), Some(text_type), - 0, - 0, + *line, + *column, ); } - if pattern_type != Type::Pattern && pattern_type != Type::Text { + if pattern_type != Type::Pattern + && pattern_type != Type::Text + && pattern_type != Type::Unknown + { self.type_error( format!("Expected Pattern for pattern matching, got {pattern_type}"), Some(Type::Pattern), Some(pattern_type), - 0, - 0, + *line, + *column, ); } @@ -2307,6 +2328,10 @@ impl TypeChecker { (a, b) if a == b => true, (Type::Unknown, _) => true, + (_, Type::Unknown) => true, // Unknown can be assigned to any type + + (Type::Any, _) => true, // Any can accept any type + (_, Type::Any) => true, // Any can be assigned to any type (_, Type::Nothing) => true, @@ -2560,4 +2585,7 @@ mod tests { .are_types_compatible(&Type::Text, &Type::Async(Box::new(Type::Number))) ); } + + // TODO: Add test for type inference in for-each loops + // Test currently commented out due to analyzer access limitations }