Skip to content

Commit a1788ad

Browse files
johnnytclaude
andauthored
Evaluates arithmetic and unary operators (#21)
Completes the arithmetic expression evaluation pipeline by implementing instruction execution in the stack-based evaluator. Arithmetic expressions now work end-to-end from parsing to evaluation. - Arithmetic instruction handlers: add, subtract, multiply, divide, modulo - Unary instruction handlers: unary_minus, unary_bang - Comprehensive error handling with type checking and division-by-zero protection - Pattern-matching implementation for clean, idiomatic Elixir code - 7 new instruction execution functions in evaluator - 100 new tests (69 arithmetic + 31 unary) with full pipeline integration - Error messages for type mismatches and stack underflow conditions - Maintained 92.2% test coverage, all quality checks passing - `2 + 3 * 4` → 14 (correct precedence) - `(10 - 5) / 2` → 2 (parentheses and integer division) - `-score > -100` → evaluates unary minus with comparisons - `!active AND ready` → unary bang in logical expressions Resolves arithmetic evaluation requirement from SCXML implementation phases 1.2-1.4. All arithmetic and unary expressions now fully functional in production. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent ea59f4a commit a1788ad

File tree

6 files changed

+665
-62
lines changed

6 files changed

+665
-62
lines changed

CHANGELOG.md

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020

2121
#### Operator Support Details
2222
```elixir
23-
# Arithmetic operators (parsing complete, evaluation pending)
24-
2 + 3 # Addition - generates ["add"] instruction
25-
5 - 2 # Subtraction - generates ["subtract"] instruction
26-
3 * 4 # Multiplication - generates ["multiply"] instruction
27-
8 / 2 # Division - generates ["divide"] instruction
28-
7 % 3 # Modulo - generates ["modulo"] instruction
23+
# Arithmetic operators (FULLY IMPLEMENTED - parsing and evaluation complete)
24+
2 + 3 # Addition - evaluates to 5
25+
5 - 2 # Subtraction - evaluates to 3
26+
3 * 4 # Multiplication - evaluates to 12
27+
8 / 2 # Division - evaluates to 4 (integer division)
28+
7 % 3 # Modulo - evaluates to 1
29+
-5 # Unary minus - evaluates to -5
2930

3031
# Logical operators (fully functional)
3132
true && false # Logical AND - works completely
@@ -36,20 +37,29 @@ true || false # Logical OR - works completely
3637
x == y # Strict equality - works completely
3738
```
3839

40+
#### Arithmetic Evaluation Implementation
41+
- **Complete Pipeline**: Full arithmetic evaluation now implemented in stack machine evaluator
42+
- **Instruction Handlers**: Added execution support for `["add"]`, `["subtract"]`, `["multiply"]`, `["divide"]`, `["modulo"]` instructions
43+
- **Unary Operations**: Implemented `["unary_minus"]` and `["unary_bang"]` instruction evaluation
44+
- **Error Handling**: Comprehensive type checking and division-by-zero protection
45+
- **Pattern Matching**: Idiomatic Elixir implementation using pattern matching for each operation
46+
- **Integration Testing**: Full pipeline testing from expression strings to computed results
47+
3948
#### Foundation for SCXML Value Expressions
40-
- **Parser Foundation**: Completed lexer, parser, and AST phases (1.2-1.4) of SCXML datamodel support
41-
- **Instruction Generation**: Arithmetic expressions now generate proper stack machine instructions
42-
- **Ready for Evaluation**: Next phase will implement instruction execution in evaluator
49+
- **Complete Implementation**: Finished lexer, parser, AST, and evaluation phases (1.2-1.4) of SCXML datamodel support
50+
- **Full Expression Support**: Arithmetic expressions now work end-to-end from parsing to evaluation
51+
- **Production Ready**: Complete arithmetic expression evaluation ready for SCXML integration
4352
- **Backward Compatibility**: All existing functionality remains unchanged
4453

4554
### Technical Implementation
4655
- **Lexer Enhancement**: Extended tokenization with 9 new token types
4756
- **Parser Grammar**: Implemented arithmetic precedence hierarchy (unary → multiplication → addition → equality → comparison)
4857
- **AST Extensions**: Added 4 new AST node types (:arithmetic, :equality, :unary, plus enhanced visitor support)
4958
- **Instruction Generation**: Arithmetic expressions compile to proper stack machine instructions
59+
- **Evaluator Enhancement**: Added 7 new instruction handlers with pattern matching for type safety
5060
- **Error Recovery**: Comprehensive error messages for parsing and evaluation phases
51-
- **Test Coverage**: 604 tests passing with updated expectations for parser success
52-
- **Code Quality**: Eliminated code duplication in StringVisitor through helper function extraction
61+
- **Test Coverage**: 747 tests passing (92.2% coverage) with comprehensive arithmetic evaluation testing
62+
- **Code Quality**: Idiomatic Elixir pattern matching implementation, all quality checks passing
5363

5464
## [2.0.0] - 2025-08-21
5565

lib/predicator/evaluator.ex

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ defmodule Predicator.Evaluator do
1414
- `["not"]` - Logical NOT of top boolean value
1515
- `["in"]` - Membership test (element in collection)
1616
- `["contains"]` - Membership test (collection contains element)
17+
- `["add"]` - Add top two integer values
18+
- `["subtract"]` - Subtract top two integer values
19+
- `["multiply"]` - Multiply top two integer values
20+
- `["divide"]` - Divide top two integer values (integer division)
21+
- `["modulo"]` - Modulo operation on top two integer values
22+
- `["unary_minus"]` - Negate top integer value
23+
- `["unary_bang"]` - Logical NOT of top boolean value
1724
- `["call", function_name, arg_count]` - Call function with arguments from stack
1825
"""
1926

@@ -222,6 +229,36 @@ defmodule Predicator.Evaluator do
222229
execute_membership(evaluator, :contains)
223230
end
224231

232+
# Arithmetic instructions
233+
defp execute_instruction(%__MODULE__{} = evaluator, ["add"]) do
234+
execute_arithmetic(evaluator, :add)
235+
end
236+
237+
defp execute_instruction(%__MODULE__{} = evaluator, ["subtract"]) do
238+
execute_arithmetic(evaluator, :subtract)
239+
end
240+
241+
defp execute_instruction(%__MODULE__{} = evaluator, ["multiply"]) do
242+
execute_arithmetic(evaluator, :multiply)
243+
end
244+
245+
defp execute_instruction(%__MODULE__{} = evaluator, ["divide"]) do
246+
execute_arithmetic(evaluator, :divide)
247+
end
248+
249+
defp execute_instruction(%__MODULE__{} = evaluator, ["modulo"]) do
250+
execute_arithmetic(evaluator, :modulo)
251+
end
252+
253+
# Unary instructions
254+
defp execute_instruction(%__MODULE__{} = evaluator, ["unary_minus"]) do
255+
execute_unary(evaluator, :minus)
256+
end
257+
258+
defp execute_instruction(%__MODULE__{} = evaluator, ["unary_bang"]) do
259+
execute_unary(evaluator, :bang)
260+
end
261+
225262
# Function call instruction
226263
defp execute_instruction(%__MODULE__{} = evaluator, ["call", function_name, arg_count])
227264
when is_binary(function_name) and is_integer(arg_count) and arg_count >= 0 do
@@ -377,6 +414,75 @@ defmodule Predicator.Evaluator do
377414
"#{String.upcase(to_string(operation))} requires two values on stack, got: #{length(stack)}"}
378415
end
379416

417+
@spec execute_arithmetic(t(), :add | :subtract | :multiply | :divide | :modulo) ::
418+
{:ok, t()} | {:error, term()}
419+
defp execute_arithmetic(%__MODULE__{stack: [right | [left | rest]]} = evaluator, :add)
420+
when is_integer(left) and is_integer(right) do
421+
{:ok, %__MODULE__{evaluator | stack: [left + right | rest]}}
422+
end
423+
424+
defp execute_arithmetic(%__MODULE__{stack: [right | [left | rest]]} = evaluator, :subtract)
425+
when is_integer(left) and is_integer(right) do
426+
{:ok, %__MODULE__{evaluator | stack: [left - right | rest]}}
427+
end
428+
429+
defp execute_arithmetic(%__MODULE__{stack: [right | [left | rest]]} = evaluator, :multiply)
430+
when is_integer(left) and is_integer(right) do
431+
{:ok, %__MODULE__{evaluator | stack: [left * right | rest]}}
432+
end
433+
434+
defp execute_arithmetic(%__MODULE__{stack: [0 | [_left | _rest]]}, :divide) do
435+
{:error, "Division by zero"}
436+
end
437+
438+
defp execute_arithmetic(%__MODULE__{stack: [right | [left | rest]]} = evaluator, :divide)
439+
when is_integer(left) and is_integer(right) do
440+
{:ok, %__MODULE__{evaluator | stack: [div(left, right) | rest]}}
441+
end
442+
443+
defp execute_arithmetic(%__MODULE__{stack: [0 | [_left | _rest]]}, :modulo) do
444+
{:error, "Modulo by zero"}
445+
end
446+
447+
defp execute_arithmetic(%__MODULE__{stack: [right | [left | rest]]} = evaluator, :modulo)
448+
when is_integer(left) and is_integer(right) do
449+
{:ok, %__MODULE__{evaluator | stack: [rem(left, right) | rest]}}
450+
end
451+
452+
defp execute_arithmetic(%__MODULE__{stack: [right | [left | _rest]]}, operation) do
453+
{:error,
454+
"Arithmetic #{operation} requires two integer values, got: #{inspect(left)} and #{inspect(right)}"}
455+
end
456+
457+
defp execute_arithmetic(%__MODULE__{stack: stack}, operation) do
458+
{:error, "Arithmetic #{operation} requires two values on stack, got: #{length(stack)}"}
459+
end
460+
461+
@spec execute_unary(t(), :minus | :bang) :: {:ok, t()} | {:error, term()}
462+
defp execute_unary(%__MODULE__{stack: [value | rest]} = evaluator, :minus)
463+
when is_integer(value) do
464+
result = -value
465+
{:ok, %__MODULE__{evaluator | stack: [result | rest]}}
466+
end
467+
468+
defp execute_unary(%__MODULE__{stack: [value | rest]} = evaluator, :bang)
469+
when is_boolean(value) do
470+
result = not value
471+
{:ok, %__MODULE__{evaluator | stack: [result | rest]}}
472+
end
473+
474+
defp execute_unary(%__MODULE__{stack: [value | _rest]}, :minus) do
475+
{:error, "Unary minus requires an integer value, got: #{inspect(value)}"}
476+
end
477+
478+
defp execute_unary(%__MODULE__{stack: [value | _rest]}, :bang) do
479+
{:error, "Unary bang (!) requires a boolean value, got: #{inspect(value)}"}
480+
end
481+
482+
defp execute_unary(%__MODULE__{stack: []}, operation) do
483+
{:error, "Unary #{operation} requires one value on stack, got: 0"}
484+
end
485+
380486
@spec execute_function_call(t(), binary(), non_neg_integer()) :: {:ok, t()} | {:error, term()}
381487
defp execute_function_call(
382488
%__MODULE__{stack: stack, functions: functions} = evaluator,

0 commit comments

Comments
 (0)