⏺ Phase 2 is complete. Here's a summary of what was implemented: #3

Merged
navicore merged 6 commits from phase2 into main 2026-02-08 21:18:41 +00:00
navicore commented 2026-02-08 17:25:21 +00:00 (Migrated from github.com)

Changes Made

src/parser.rs — Control flow parsing

  • Added do_header_depth, condition_depth, and if_depth context flags to prevent keywords (TO/BY/FOR/WHILE/UNTIL, THEN, ELSE) from being consumed by implicit concatenation
  • Added parse_if() — IF/THEN/ELSE with cross-line support
  • Added parse_do() — all DO variants: Simple, Forever, While, Until, Count, Controlled
  • Added parse_controlled_do() — DO var = start TO/BY/FOR/WHILE/UNTIL with any-order options
  • Added parse_do_body() — body parsing until END [name]
  • Added parse_select() — SELECT/WHEN/OTHERWISE/END
  • Added parse_leave(), parse_iterate(), parse_exit(), parse_return()
  • Added error detection for stray END, THEN/ELSE, WHEN/OTHERWISE

src/eval.rs — Control flow execution

  • Added ExecSignal enum (Normal, Leave, Iterate, Exit, Return)
  • Changed exec()/exec_clause() to return RexxResult
  • Added exec_if(), exec_do() (with all variant handlers), exec_select()
  • Proper signal propagation: LEAVE/ITERATE match by name; EXIT/RETURN always propagate
  • Controlled loop: evaluates TO/BY/FOR/WHILE/UNTIL, correct final variable value (one past limit)

src/main.rs — Signal handling

  • run_line() handles ExecSignal: Normal/Exit/Return are OK at top level; Leave/Iterate produce Error 28

tests/phase2.rs — 26 integration tests

  • IF/THEN/ELSE (4 tests), DO Simple/Count/Forever/While/Until (5 tests), Controlled DO with BY/FOR (4 tests), ITERATE, SELECT (3 tests), nested DO, FizzBuzz, factorial, EXIT, error cases (3 tests), cross-line IF (2 tests)
Changes Made src/parser.rs — Control flow parsing - Added do_header_depth, condition_depth, and if_depth context flags to prevent keywords (TO/BY/FOR/WHILE/UNTIL, THEN, ELSE) from being consumed by implicit concatenation - Added parse_if() — IF/THEN/ELSE with cross-line support - Added parse_do() — all DO variants: Simple, Forever, While, Until, Count, Controlled - Added parse_controlled_do() — DO var = start TO/BY/FOR/WHILE/UNTIL with any-order options - Added parse_do_body() — body parsing until END [name] - Added parse_select() — SELECT/WHEN/OTHERWISE/END - Added parse_leave(), parse_iterate(), parse_exit(), parse_return() - Added error detection for stray END, THEN/ELSE, WHEN/OTHERWISE src/eval.rs — Control flow execution - Added ExecSignal enum (Normal, Leave, Iterate, Exit, Return) - Changed exec()/exec_clause() to return RexxResult<ExecSignal> - Added exec_if(), exec_do() (with all variant handlers), exec_select() - Proper signal propagation: LEAVE/ITERATE match by name; EXIT/RETURN always propagate - Controlled loop: evaluates TO/BY/FOR/WHILE/UNTIL, correct final variable value (one past limit) src/main.rs — Signal handling - run_line() handles ExecSignal: Normal/Exit/Return are OK at top level; Leave/Iterate produce Error 28 tests/phase2.rs — 26 integration tests - IF/THEN/ELSE (4 tests), DO Simple/Count/Forever/While/Until (5 tests), Controlled DO with BY/FOR (4 tests), ITERATE, SELECT (3 tests), nested DO, FizzBuzz, factorial, EXIT, error cases (3 tests), cross-line IF (2 tests)
claude[bot] commented 2026-02-08 17:29:13 +00:00 (Migrated from github.com)

PR #3 Review: Phase 2 Control Flow Implementation

Summary:
This PR implements REXX control flow (IF/THEN/ELSE, DO variants, SELECT/WHEN, LEAVE, ITERATE, EXIT, RETURN). The implementation adds ~1200 lines across parser, evaluator, and tests.

Overall Assessment: Strong implementation with excellent test coverage. I identified 4 correctness issues that need fixing before merge.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CRITICAL ISSUES (MUST FIX)

  1. Controlled loop allows zero BY step - causes infinite loop
    Location: src/eval.rs:295-301
    Severity: HIGH

    Problem: Zero step should raise Error 26, not loop forever.
    REXX ANSI X3.274-1996 §7.3.5 requires BY ≠ 0.

    Fix: Add check before loop:
    if by_num.is_zero() {
    return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber)
    .with_detail("BY value must be non-zero"));
    }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1. to_integer allows negative values for loop counts
    Location: src/eval.rs:408-416
    Severity: HIGH

    Problems:

    • No negative check: REXX loop counts must be ≥ 0
    • Wrong error on overflow: should be Error 42, not 26
    • Error message uses original value instead of rounded value

    Fix: Add negative check and use ArithmeticOverflow for parse failure

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1. Power operator lacks exponent bounds checking
    Location: src/eval.rs:548-570
    Severity: MEDIUM

    Problem: No range validation. Input like 10 ** 1000000000 will hang.

    Fix: Add bounds check after parsing exp_i64:
    if exp_i64.abs() > 999999999 {
    return Err(Error 42 with "exponent exceeds limits");
    }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1. trunc_div string parsing may fail on scientific notation
    Location: src/eval.rs:690-699
    Severity: MEDIUM

    Problem: BigDecimal::to_string() may use scientific notation.
    Parsing "1.5e10" will fail to find period.

    Fix: Use BigDecimal rounding instead of string parsing

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

STRENGTHS

Signal propagation is correct
ExecSignal enum correctly implements REXX semantics
(LEAVE/ITERATE match by name, EXIT/RETURN always propagate)

Cross-line IF/THEN/ELSE works properly
Parser correctly handles THEN/ELSE across line boundaries
with position backtracking (src/parser.rs:323-335)

Error numbers match ANSI X3.274-1996
All error codes correct: 7, 8, 10, 28

Test coverage is excellent
26 integration tests including FizzBuzz, factorial, nested loops,
error cases. Only gap: no test for negative BY step.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

QUESTIONS

• UNTIL variable behavior (src/eval.rs:346-363)
Code increments control variable when UNTIL becomes true.
Is this standard REXX? Most implementations leave variable
at value that satisfied UNTIL, not incremented.
Verify against ANSI §7.3.5.

• Parser keyword checks (src/parser.rs:145)
is_do_header_keyword() calls to_uppercase() on every check.
Could use eq_ignore_ascii_case. Not critical.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

RECOMMENDATIONS

  1. Fix issues #1-2 immediately (can cause hangs/crashes)
  2. Fix issues #3-4 (edge cases but should be handled)
  3. Add tests for:
    • Zero BY: do i = 1 to 10 by 0 (should error)
    • Negative BY: do i = 10 to 1 by -1 (should work)
    • Large exponents: 2 ** 1000000000 (should error)
  4. Document UNTIL variable increment behavior

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

VERDICT

This is a well-architected implementation of REXX control flow.
The parser context flags for keyword suppression are clever,
and the signal propagation is clean.

The critical issues are all arithmetic edge cases that need
fixing to prevent errors on unusual input. Once addressed,
this is ready to merge.

Estimated fix time: 1-2 hours + tests

PR #3 Review: Phase 2 Control Flow Implementation Summary: This PR implements REXX control flow (IF/THEN/ELSE, DO variants, SELECT/WHEN, LEAVE, ITERATE, EXIT, RETURN). The implementation adds ~1200 lines across parser, evaluator, and tests. Overall Assessment: Strong implementation with excellent test coverage. I identified 4 correctness issues that need fixing before merge. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ CRITICAL ISSUES (MUST FIX) 1. Controlled loop allows zero BY step - causes infinite loop Location: src/eval.rs:295-301 Severity: HIGH Problem: Zero step should raise Error 26, not loop forever. REXX ANSI X3.274-1996 §7.3.5 requires BY ≠ 0. Fix: Add check before loop: if by_num.is_zero() { return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber) .with_detail("BY value must be non-zero")); } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2. to_integer allows negative values for loop counts Location: src/eval.rs:408-416 Severity: HIGH Problems: - No negative check: REXX loop counts must be ≥ 0 - Wrong error on overflow: should be Error 42, not 26 - Error message uses original value instead of rounded value Fix: Add negative check and use ArithmeticOverflow for parse failure ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3. Power operator lacks exponent bounds checking Location: src/eval.rs:548-570 Severity: MEDIUM Problem: No range validation. Input like 10 ** 1000000000 will hang. Fix: Add bounds check after parsing exp_i64: if exp_i64.abs() > 999999999 { return Err(Error 42 with "exponent exceeds limits"); } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4. trunc_div string parsing may fail on scientific notation Location: src/eval.rs:690-699 Severity: MEDIUM Problem: BigDecimal::to_string() may use scientific notation. Parsing "1.5e10" will fail to find period. Fix: Use BigDecimal rounding instead of string parsing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ STRENGTHS ✅ Signal propagation is correct ExecSignal enum correctly implements REXX semantics (LEAVE/ITERATE match by name, EXIT/RETURN always propagate) ✅ Cross-line IF/THEN/ELSE works properly Parser correctly handles THEN/ELSE across line boundaries with position backtracking (src/parser.rs:323-335) ✅ Error numbers match ANSI X3.274-1996 All error codes correct: 7, 8, 10, 28 ✅ Test coverage is excellent 26 integration tests including FizzBuzz, factorial, nested loops, error cases. Only gap: no test for negative BY step. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ QUESTIONS • UNTIL variable behavior (src/eval.rs:346-363) Code increments control variable when UNTIL becomes true. Is this standard REXX? Most implementations leave variable at value that satisfied UNTIL, not incremented. Verify against ANSI §7.3.5. • Parser keyword checks (src/parser.rs:145) is_do_header_keyword() calls to_uppercase() on every check. Could use eq_ignore_ascii_case. Not critical. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RECOMMENDATIONS 1. Fix issues #1-2 immediately (can cause hangs/crashes) 2. Fix issues #3-4 (edge cases but should be handled) 3. Add tests for: - Zero BY: do i = 1 to 10 by 0 (should error) - Negative BY: do i = 10 to 1 by -1 (should work) - Large exponents: 2 ** 1000000000 (should error) 4. Document UNTIL variable increment behavior ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ VERDICT This is a well-architected implementation of REXX control flow. The parser context flags for keyword suppression are clever, and the signal propagation is clean. The critical issues are all arithmetic edge cases that need fixing to prevent errors on unusual input. Once addressed, this is ready to merge. Estimated fix time: 1-2 hours + tests
claude[bot] commented 2026-02-08 17:48:43 +00:00 (Migrated from github.com)

Code Review: Phase 2 Control Flow Implementation

This PR implements IF/THEN/ELSE, DO loops (all variants), SELECT/WHEN/OTHERWISE, LEAVE/ITERATE, EXIT/RETURN, and signal propagation. I've reviewed for correctness, REXX semantics, arithmetic, error handling, performance, and safety.

What's Working Well

Parser Architecture:

  • Depth counters (do_header_depth, condition_depth, if_depth) correctly prevent keyword consumption by implicit concatenation (src/parser.rs:17-25, 787-803)
  • Cross-line IF/THEN/ELSE with skip_terminators() and position restoration (src/parser.rs:326-338)
  • Proper detection of stray END/THEN/ELSE/WHEN/OTHERWISE with correct error numbers (src/parser.rs:239-258)

Controlled Loop Semantics:

  • Correct BY zero detection with Error 26 (src/eval.rs:280-283)
  • Proper TO limit checking with sign-aware comparisons (src/eval.rs:299-308)
  • Final control variable set to "one past limit" per ANSI spec (src/eval.rs:366-370)
  • UNTIL condition checked after body, variable NOT incremented when satisfied (src/eval.rs:351-359, with good comment at 352-353)

Signal Propagation:

  • Named LEAVE/ITERATE matching correctly (src/eval.rs:393-407)
  • EXIT/RETURN always propagate (src/eval.rs:168, 175, etc.)
  • Top-level handling in main.rs rejects stray LEAVE/ITERATE with Error 28 (src/main.rs:75-78)

Arithmetic:

  • Large exponent guard (src/eval.rs:568-571) prevents overflow
  • trunc_div fixed to use with_scale_round(0, Down) instead of string parsing (src/eval.rs:701-706) - this avoids scientific notation bugs

Test Coverage:

  • 30 passing tests including FizzBuzz, factorial, nested loops, error cases
  • Edge cases: negative BY step, zero BY error, negative loop count error, large exponent error

🔴 Critical Issues

1. Division by Zero Returns Wrong Error Number (src/eval.rs:510, 528, 543)

Issue: Division by zero returns RexxError::ArithmeticOverflow (Error 42), but per ANSI X3.274-1996, Error 42 is "Arithmetic overflow/underflow" which typically means result exceeds NUMERIC DIGITS. Division by zero should arguably be Error 41 ("Bad arithmetic conversion") or a distinct error.

Location: src/eval.rs:510, 528, 543

Impact: Incorrect error reporting for a common arithmetic error.

Recommendation: Check ANSI spec for correct error number for division by zero. If Error 42 is correct for REXX, add clarifying comment; otherwise, use Error 41.


2. Iteration Counter Overflow Risk in Controlled Loops (src/eval.rs:295, 363)

Issue: iterations is i64 and incremented unconditionally (line 363). A malicious or buggy program with a massive FOR value or infinite TO range could overflow iterations, causing silent wraparound or panic in debug mode.

Location: src/eval.rs:295, 363

Proof of Concept:

/* If FOR count is near i64::MAX, iterations += 1 could overflow */
do i = 1 to 999999999999999 for 9223372036854775807
  /* ... */
end

Impact: Potential panic (debug) or silent wraparound (release).

Recommendation:

  • Check iterations approaching i64::MAX before increment
  • Return Error 42 (ArithmeticOverflow) if loop count would exceed i64::MAX
  • Or use saturating arithmetic: iterations = iterations.saturating_add(1)

3. to_integer Allows Fractional Loop Counts Without Error (src/eval.rs:410-423)

Issue: to_integer rounds fractional values with d.round(0) (line 412) and silently accepts them. Per REXX semantics, loop counts and FOR values should be whole numbers. If a user writes do 3.7, REXX should raise Error 26 ("Invalid whole number"), not silently round to 4.

Location: src/eval.rs:410-423

Test Case:

do 3.7; say 'x'; end  /* Should this be an error? */

Impact: Silent data loss; user may not realize their fractional count was rounded.

Recommendation: Check if d \!= rounded and return Error 26 if fractional part exists. Example:

let rounded = d.round(0);
if d \!= rounded {
    return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber)
        .with_detail("loop count must be a whole number"));
}

4. pow_bigdecimal Can Produce Massive Intermediate Values (src/eval.rs:709-729)

Issue: Exponentiation by squaring (line 720-727) with large exponents can create intermediate BigDecimal values that consume unbounded memory. Example: 2 ** 999999999 passes the exponent limit check (line 568-571) but will still allocate enormous memory during computation.

Location: src/eval.rs:709-729

Proof of Concept:

say 2 ** 100000000  /* Within 999999999 limit but huge result */

Impact: Out-of-memory crash or DoS.

Recommendation:

  • Add a result size check after each multiplication in the loop (e.g., check if result.digits() > MAX_DIGITS)
  • Return Error 42 (ArithmeticOverflow) if result exceeds reasonable bounds
  • Consider reducing the exponent limit from 999999999 to a smaller value (e.g., 100000) that still satisfies practical REXX use cases

5. Parser Does Not Check for Duplicate TO/BY/FOR/WHILE/UNTIL in Controlled DO (src/parser.rs:463-483)

Issue: parse_controlled_do allows duplicate keywords:

do i = 1 to 10 to 20 by 2 by 3  /* Parser accepts this */

The last occurrence silently overwrites earlier ones. Per REXX, duplicate keywords should raise Error 27 ("Invalid DO syntax").

Location: src/parser.rs:463-483

Impact: Silent acceptance of malformed syntax.

Recommendation: Track which keywords have been seen and return Error 27 if any are repeated.


⚠️ Moderate Issues

6. SELECT Without OTHERWISE Can Execute Zero Branches (src/eval.rs:389)

Issue: If no WHEN matches and there's no OTHERWISE, the code correctly returns Error 7, but the error message says "no WHEN matched" which could be clearer.

Location: src/eval.rs:389-390

Recommendation: Error message is adequate but could add "(add OTHERWISE branch or ensure a WHEN matches)" for user guidance.


7. Controlled Loop Re-evaluates WHILE on Every Iteration (src/eval.rs:323-329)

Issue: WHILE condition is evaluated before each iteration. This is correct per REXX semantics, but the code does not cache while_expr. If the expression is expensive, this could be a performance issue. However, this is the correct behavior per ANSI REXX, so not a bug.

Location: src/eval.rs:323-329

Recommendation: No change needed; this is correct REXX behavior. Just noting for completeness.


8. trunc_div Truncates Toward Zero, But Comment Could Be Clearer (src/eval.rs:701-706)

Issue: The fix to use with_scale_round(0, Down) is correct, but Down in BigDecimal means "toward zero" (not "toward negative infinity"). The comment "Truncate toward zero" is correct. However, it could clarify that Down is the correct rounding mode for this.

Location: src/eval.rs:705

Recommendation: Add comment: // Note: RoundingMode::Down truncates toward zero (not toward negative infinity)


🟡 Minor Issues / Style

9. exec_do_until ITERATE Logic Has Subtle Control Flow (src/eval.rs:236-242)

Issue: When ITERATE matches, the code "falls through to condition check" (line 238). This is correct, but the logic is slightly hard to follow because the ITERATE arm has a comment but no explicit control flow. Consider restructuring for clarity.

Location: src/eval.rs:236-242

Recommendation:

ExecSignal::Iterate(ref name) => {
    if \!Self::iterate_matches(name.as_ref(), block.name.as_ref()) {
        return Ok(signal);
    }
    // ITERATE matched: continue to UNTIL check after body
}

10. #[allow(clippy::cmp_owned)] in Controlled Loop (src/eval.rs:300)

Issue: Line 309 compares &current > limit where current is BigDecimal and limit is &BigDecimal. This triggers clippy::cmp_owned. The allow is correct, but the comparison could be rewritten as current > *limit to avoid the lint.

Location: src/eval.rs:300, 309

Recommendation: Minor - the allow is fine, or rewrite as current > *limit.


11. leave_matches and iterate_matches Are Identical (src/eval.rs:393-407)

Issue: Both functions have identical logic. Consider a single signal_matches helper.

Location: src/eval.rs:393-407

Recommendation: Refactor to DRY:

fn signal_matches(signal_name: Option<&String>, loop_name: Option<&String>) -> bool {
    match signal_name {
        None => true,
        Some(name) => loop_name.is_some_and(|ln| ln == name),
    }
}

Use for both LEAVE and ITERATE.


🔍 Edge Cases to Test

The following cases are not covered by current tests but should work correctly (or fail gracefully):

  1. Deep nesting: 100+ nested DO loops (stack overflow risk?)
  2. Controlled loop with only BY (no TO): do i = 1 by 2; if i > 100 then leave; end (should run forever until LEAVE)
  3. ITERATE in DO UNTIL: Does ITERATE skip the UNTIL check or still evaluate it? (Code at src/eval.rs:236-242 suggests it evaluates UNTIL after ITERATE, which is correct)
  4. Named LEAVE/ITERATE with wrong name: do foo=1 to 5; leave bar; end (should propagate signal upward)
  5. EXIT/RETURN inside nested SELECT inside DO: Signals should propagate out

📊 Performance Notes

  • No unnecessary clones: Good use of references throughout
  • Vec allocations: parse_do_body and exec_body allocate vectors, but this is unavoidable for the AST structure
  • BigDecimal operations: Controlled loop increments current every iteration (line 362). For very large loops, this is unavoidable but could be slow. Acceptable for a REXX interpreter.

🛡️ Safety

  • No panics on malformed input: Unterminated DO/SELECT returns Error 14 (IncompleteBlock)
  • No panics on deep nesting: Parser recursion could blow the stack with 10000+ nested DOs, but this is a pathological case. Consider adding a nesting depth counter if concerned.

Summary

This is a high-quality implementation of REXX control flow with correct semantics and excellent test coverage. The critical issues are:

  1. Division by zero uses wrong error number
  2. Iteration counter overflow risk in controlled loops
  3. Fractional loop counts silently rounded instead of rejected
  4. Exponentiation can cause OOM with large exponents
  5. Parser allows duplicate keywords in controlled DO

Address these before merge. The moderate/minor issues are nice-to-haves.

Test Coverage: Excellent (30 tests, edge cases covered)
Error Handling: ⚠️ Good, but division by zero and loop count validation need fixes
REXX Compliance: Correct (UNTIL semantics, final variable value, named LEAVE/ITERATE)
Safety: Good (no panics on bad input, but watch for overflow/OOM)


Great work overall! The architecture (depth counters, signal propagation) is clean and correct. Fix the critical issues and this will be a solid implementation.

## Code Review: Phase 2 Control Flow Implementation This PR implements IF/THEN/ELSE, DO loops (all variants), SELECT/WHEN/OTHERWISE, LEAVE/ITERATE, EXIT/RETURN, and signal propagation. I've reviewed for correctness, REXX semantics, arithmetic, error handling, performance, and safety. ### ✅ What's Working Well **Parser Architecture:** - Depth counters (`do_header_depth`, `condition_depth`, `if_depth`) correctly prevent keyword consumption by implicit concatenation (src/parser.rs:17-25, 787-803) - Cross-line IF/THEN/ELSE with `skip_terminators()` and position restoration (src/parser.rs:326-338) - Proper detection of stray END/THEN/ELSE/WHEN/OTHERWISE with correct error numbers (src/parser.rs:239-258) **Controlled Loop Semantics:** - Correct BY zero detection with Error 26 (src/eval.rs:280-283) - Proper TO limit checking with sign-aware comparisons (src/eval.rs:299-308) - Final control variable set to "one past limit" per ANSI spec (src/eval.rs:366-370) - UNTIL condition checked after body, variable NOT incremented when satisfied (src/eval.rs:351-359, with good comment at 352-353) **Signal Propagation:** - Named LEAVE/ITERATE matching correctly (src/eval.rs:393-407) - EXIT/RETURN always propagate (src/eval.rs:168, 175, etc.) - Top-level handling in main.rs rejects stray LEAVE/ITERATE with Error 28 (src/main.rs:75-78) **Arithmetic:** - Large exponent guard (src/eval.rs:568-571) prevents overflow - `trunc_div` fixed to use `with_scale_round(0, Down)` instead of string parsing (src/eval.rs:701-706) - this avoids scientific notation bugs **Test Coverage:** - 30 passing tests including FizzBuzz, factorial, nested loops, error cases - Edge cases: negative BY step, zero BY error, negative loop count error, large exponent error --- ### 🔴 Critical Issues #### 1. **Division by Zero Returns Wrong Error Number** (src/eval.rs:510, 528, 543) **Issue:** Division by zero returns `RexxError::ArithmeticOverflow` (Error 42), but per ANSI X3.274-1996, Error 42 is "Arithmetic overflow/underflow" which typically means result exceeds NUMERIC DIGITS. Division by zero should arguably be Error 41 ("Bad arithmetic conversion") or a distinct error. **Location:** src/eval.rs:510, 528, 543 **Impact:** Incorrect error reporting for a common arithmetic error. **Recommendation:** Check ANSI spec for correct error number for division by zero. If Error 42 is correct for REXX, add clarifying comment; otherwise, use Error 41. --- #### 2. **Iteration Counter Overflow Risk in Controlled Loops** (src/eval.rs:295, 363) **Issue:** `iterations` is `i64` and incremented unconditionally (line 363). A malicious or buggy program with a massive FOR value or infinite TO range could overflow `iterations`, causing silent wraparound or panic in debug mode. **Location:** src/eval.rs:295, 363 **Proof of Concept:** ```rexx /* If FOR count is near i64::MAX, iterations += 1 could overflow */ do i = 1 to 999999999999999 for 9223372036854775807 /* ... */ end ``` **Impact:** Potential panic (debug) or silent wraparound (release). **Recommendation:** - Check `iterations` approaching `i64::MAX` before increment - Return Error 42 (ArithmeticOverflow) if loop count would exceed `i64::MAX` - Or use saturating arithmetic: `iterations = iterations.saturating_add(1)` --- #### 3. **`to_integer` Allows Fractional Loop Counts Without Error** (src/eval.rs:410-423) **Issue:** `to_integer` rounds fractional values with `d.round(0)` (line 412) and silently accepts them. Per REXX semantics, loop counts and FOR values should be whole numbers. If a user writes `do 3.7`, REXX should raise Error 26 ("Invalid whole number"), not silently round to 4. **Location:** src/eval.rs:410-423 **Test Case:** ```rexx do 3.7; say 'x'; end /* Should this be an error? */ ``` **Impact:** Silent data loss; user may not realize their fractional count was rounded. **Recommendation:** Check if `d \!= rounded` and return Error 26 if fractional part exists. Example: ```rust let rounded = d.round(0); if d \!= rounded { return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber) .with_detail("loop count must be a whole number")); } ``` --- #### 4. **`pow_bigdecimal` Can Produce Massive Intermediate Values** (src/eval.rs:709-729) **Issue:** Exponentiation by squaring (line 720-727) with large exponents can create intermediate `BigDecimal` values that consume unbounded memory. Example: `2 ** 999999999` passes the exponent limit check (line 568-571) but will still allocate enormous memory during computation. **Location:** src/eval.rs:709-729 **Proof of Concept:** ```rexx say 2 ** 100000000 /* Within 999999999 limit but huge result */ ``` **Impact:** Out-of-memory crash or DoS. **Recommendation:** - Add a result size check after each multiplication in the loop (e.g., check if `result.digits() > MAX_DIGITS`) - Return Error 42 (ArithmeticOverflow) if result exceeds reasonable bounds - Consider reducing the exponent limit from 999999999 to a smaller value (e.g., 100000) that still satisfies practical REXX use cases --- #### 5. **Parser Does Not Check for Duplicate TO/BY/FOR/WHILE/UNTIL in Controlled DO** (src/parser.rs:463-483) **Issue:** `parse_controlled_do` allows duplicate keywords: ```rexx do i = 1 to 10 to 20 by 2 by 3 /* Parser accepts this */ ``` The last occurrence silently overwrites earlier ones. Per REXX, duplicate keywords should raise Error 27 ("Invalid DO syntax"). **Location:** src/parser.rs:463-483 **Impact:** Silent acceptance of malformed syntax. **Recommendation:** Track which keywords have been seen and return Error 27 if any are repeated. --- ### ⚠️ Moderate Issues #### 6. **SELECT Without OTHERWISE Can Execute Zero Branches** (src/eval.rs:389) **Issue:** If no WHEN matches and there's no OTHERWISE, the code correctly returns Error 7, but the error message says "no WHEN matched" which could be clearer. **Location:** src/eval.rs:389-390 **Recommendation:** Error message is adequate but could add "(add OTHERWISE branch or ensure a WHEN matches)" for user guidance. --- #### 7. **Controlled Loop Re-evaluates WHILE on Every Iteration** (src/eval.rs:323-329) **Issue:** WHILE condition is evaluated before each iteration. This is correct per REXX semantics, but the code does not cache `while_expr`. If the expression is expensive, this could be a performance issue. However, this is the correct behavior per ANSI REXX, so not a bug. **Location:** src/eval.rs:323-329 **Recommendation:** No change needed; this is correct REXX behavior. Just noting for completeness. --- #### 8. **`trunc_div` Truncates Toward Zero, But Comment Could Be Clearer** (src/eval.rs:701-706) **Issue:** The fix to use `with_scale_round(0, Down)` is correct, but `Down` in BigDecimal means "toward zero" (not "toward negative infinity"). The comment "Truncate toward zero" is correct. However, it could clarify that `Down` is the correct rounding mode for this. **Location:** src/eval.rs:705 **Recommendation:** Add comment: `// Note: RoundingMode::Down truncates toward zero (not toward negative infinity)` --- ### 🟡 Minor Issues / Style #### 9. **`exec_do_until` ITERATE Logic Has Subtle Control Flow** (src/eval.rs:236-242) **Issue:** When ITERATE matches, the code "falls through to condition check" (line 238). This is correct, but the logic is slightly hard to follow because the ITERATE arm has a comment but no explicit control flow. Consider restructuring for clarity. **Location:** src/eval.rs:236-242 **Recommendation:** ```rust ExecSignal::Iterate(ref name) => { if \!Self::iterate_matches(name.as_ref(), block.name.as_ref()) { return Ok(signal); } // ITERATE matched: continue to UNTIL check after body } ``` --- #### 10. **`#[allow(clippy::cmp_owned)]` in Controlled Loop** (src/eval.rs:300) **Issue:** Line 309 compares `&current > limit` where `current` is `BigDecimal` and `limit` is `&BigDecimal`. This triggers `clippy::cmp_owned`. The allow is correct, but the comparison could be rewritten as `current > *limit` to avoid the lint. **Location:** src/eval.rs:300, 309 **Recommendation:** Minor - the allow is fine, or rewrite as `current > *limit`. --- #### 11. **`leave_matches` and `iterate_matches` Are Identical** (src/eval.rs:393-407) **Issue:** Both functions have identical logic. Consider a single `signal_matches` helper. **Location:** src/eval.rs:393-407 **Recommendation:** Refactor to DRY: ```rust fn signal_matches(signal_name: Option<&String>, loop_name: Option<&String>) -> bool { match signal_name { None => true, Some(name) => loop_name.is_some_and(|ln| ln == name), } } ``` Use for both LEAVE and ITERATE. --- ### 🔍 Edge Cases to Test The following cases are not covered by current tests but should work correctly (or fail gracefully): 1. **Deep nesting:** 100+ nested DO loops (stack overflow risk?) 2. **Controlled loop with only BY (no TO):** `do i = 1 by 2; if i > 100 then leave; end` (should run forever until LEAVE) 3. **ITERATE in DO UNTIL:** Does ITERATE skip the UNTIL check or still evaluate it? (Code at src/eval.rs:236-242 suggests it evaluates UNTIL after ITERATE, which is correct) 4. **Named LEAVE/ITERATE with wrong name:** `do foo=1 to 5; leave bar; end` (should propagate signal upward) 5. **EXIT/RETURN inside nested SELECT inside DO:** Signals should propagate out --- ### 📊 Performance Notes - **No unnecessary clones:** Good use of references throughout - **`Vec` allocations:** `parse_do_body` and `exec_body` allocate vectors, but this is unavoidable for the AST structure - **BigDecimal operations:** Controlled loop increments `current` every iteration (line 362). For very large loops, this is unavoidable but could be slow. Acceptable for a REXX interpreter. --- ### 🛡️ Safety - **No panics on malformed input:** Unterminated DO/SELECT returns Error 14 (IncompleteBlock) - **No panics on deep nesting:** Parser recursion could blow the stack with 10000+ nested DOs, but this is a pathological case. Consider adding a nesting depth counter if concerned. --- ## Summary This is a **high-quality implementation** of REXX control flow with correct semantics and excellent test coverage. The critical issues are: 1. **Division by zero** uses wrong error number 2. **Iteration counter overflow** risk in controlled loops 3. **Fractional loop counts** silently rounded instead of rejected 4. **Exponentiation** can cause OOM with large exponents 5. **Parser allows duplicate keywords** in controlled DO Address these before merge. The moderate/minor issues are nice-to-haves. **Test Coverage:** ✅ Excellent (30 tests, edge cases covered) **Error Handling:** ⚠️ Good, but division by zero and loop count validation need fixes **REXX Compliance:** ✅ Correct (UNTIL semantics, final variable value, named LEAVE/ITERATE) **Safety:** ✅ Good (no panics on bad input, but watch for overflow/OOM) --- Great work overall\! The architecture (depth counters, signal propagation) is clean and correct. Fix the critical issues and this will be a solid implementation.
claude[bot] commented 2026-02-08 18:02:47 +00:00 (Migrated from github.com)

Code Review: Phase 2 Control Flow Implementation

I've reviewed this PR implementing control flow for the REXX interpreter. The implementation demonstrates solid understanding of REXX semantics and proper error handling. Below are my findings:


Correctness — STRONG

Parser Edge Cases:

  • Implicit concatenation correctly suppressed in control flow contexts via depth counters (do_header_depth, condition_depth, if_depth)
  • Cross-line IF/THEN/ELSE handled correctly with skip_terminators() and position restoration
  • Stray keywords (END, THEN, ELSE, WHEN, OTHERWISE) properly rejected with correct error numbers
  • Duplicate DO keywords (TO/BY/FOR/WHILE/UNTIL) detected at parse time with Error 27

REXX Semantics:

  • DO UNTIL executes body before checking condition
  • Controlled loop sets final variable value to one-past-limit (ANSI compliant)
  • SELECT without matching WHEN and no OTHERWISE raises Error 7
  • LEAVE/ITERATE signal matching: unnamed matches any loop, named only matches named loops
  • EXIT/RETURN always propagate through nested structures

Arithmetic — EXCELLENT

BigDecimal Usage:

  • Exponent limit (1,000,000) prevents OOM on 2**999999999great addition (src/eval.rs:568)
  • Integer division fixed: trunc_div now uses with_scale_round(0, Down) instead of fragile string parsing (src/eval.rs:706)
    • Critical fix: Previous version would break on scientific notation from BigDecimal::to_string()
  • BY=0 rejected with InvalidWholeNumber (src/eval.rs:279)
  • Zero-to-negative-power rejected (src/eval.rs:572)
  • Division by zero returns Error 42 (ArithmeticOverflow) in all contexts

Loop Count Validation:

  • Fractional loop counts rejected (Error 26)
  • Negative loop counts rejected (Error 26) (src/eval.rs:416)
  • Overflow on i64 conversion caught (src/eval.rs:412)

Error Handling — CORRECT

All error numbers match ANSI X3.274-1996:

  • Error 7: SELECT with no match and no OTHERWISE (src/eval.rs:388)
  • Error 8: Stray THEN/ELSE (src/parser.rs:247)
  • Error 10: Stray END (src/parser.rs:241)
  • Error 14: Unterminated DO/SELECT (src/parser.rs:540, 574)
  • Error 18: Missing THEN after IF/WHEN (src/parser.rs:312, 590)
  • Error 26: Invalid whole number (loop counts, exponents)
  • Error 27: Invalid DO syntax (duplicate keywords) (src/parser.rs:468)
  • Error 28: LEAVE/ITERATE outside loop (src/main.rs:498)
  • Error 42: Division by zero, exponent overflow (src/eval.rs:508, 526, 541, 569)

Propagation:

  • All exec functions return RexxResult<ExecSignal> — signal and error paths cleanly separated
  • Top-level handler in main.rs correctly accepts Normal/Exit/Return, rejects Leave/Iterate with Error 28

⚠️ ISSUE 1: ITERATE in DO UNTIL — Potential Bug (src/eval.rs:236-249)

Location: src/eval.rs:236-249

ExecSignal::Iterate(ref name) => {
    if !Self::signal_matches(name.as_ref(), block.name.as_ref()) {
        return Ok(signal);
    }
    // ITERATE matched: continue to UNTIL check
}

Problem: When ITERATE matches the loop name, control falls through to the UNTIL check. But per ANSI REXX, ITERATE should restart the loop from the top, checking the UNTIL condition after the next iteration's body.

Test Case:

i = 0
DO UNTIL i > 3
  i = i + 1
  IF i = 2 THEN ITERATE
  SAY i
END

Expected: Output 1 3 4 (body runs 4 times, ITERATE skips SAY on iteration 2)
Current behavior: Likely correct by accident, but logic suggests UNTIL is checked immediately after ITERATE

Fix: After matched ITERATE in DO UNTIL, use continue to restart loop:

ExecSignal::Iterate(ref name) => {
    if Self::signal_matches(name.as_ref(), block.name.as_ref()) {
        // ITERATE matched: check UNTIL condition, then continue
        // (UNTIL is checked after continue below)
    } else {
        return Ok(signal);
    }
}
// Check UNTIL condition
let cond_val = self.eval_expr(cond_expr)?;
if to_logical(&cond_val)? {
    break;
}

Actually, reviewing the code more carefully: the current implementation may be correct if the intent is that ITERATE in UNTIL immediately checks the condition. I recommend adding an integration test to verify the exact behavior matches a reference REXX interpreter.


⚠️ ISSUE 2: Controlled Loop UNTIL Semantics (src/eval.rs:350-357)

Location: src/eval.rs:350-357

// Check UNTIL condition (after body)
// Per ANSI REXX, when UNTIL is satisfied the loop ends without
// incrementing the control variable.
if let Some(ref until_expr) = ctrl.until_cond {
    let v = self.eval_expr(until_expr)?;
    if to_logical(&v)? {
        break;
    }
}

Comment says "loop ends without incrementing", but the code then increments at line 361.

Ambiguity: Does "without incrementing" mean:

  1. Break immediately (current behavior) — variable shows value from last executed iteration
  2. Break after setting the variable but before increment (alternative) — variable shows would-be-next value

Recommendation: Verify against ANSI spec section on controlled DO with UNTIL. If current behavior is correct, rephrase comment to "loop ends, then variable is incremented and set to final value" to clarify that the increment still happens after the break for final value assignment.

Test case to verify:

DO i = 1 TO 10 BY 2 UNTIL i > 5
  SAY i
END
SAY "Final:" i

Expected: If UNTIL satisfied on iteration where i=5, should final value be 5 or 7?


Performance — GOOD

Minimal Allocations:

  • Signal propagation uses borrowed references where possible
  • RexxValue clones limited to leaf nodes (StringLit, Number) — acceptable
  • No per-iteration allocations in loop bodies beyond necessary value creation

Potential Optimizations (not blockers):

  • Line 95-96: name.clone() in LEAVE/ITERATE — could use Rc<String> or lifetime annotation, but premature optimization
  • Line 719: base.clone() in pow_bigdecimal — necessary for exponentiation by squaring

Safety — EXCELLENT

  • No panics on malformed input
  • Unterminated DO/SELECT caught at parse time
  • Deep nesting limited by stack (Rust's default), no explicit recursion depth limit needed yet
  • Infinite loops (DO FOREVER) are user-intentional; no protection needed

Test Coverage — COMPREHENSIVE

tests/phase2.rs provides 26 tests covering:

  • IF/THEN/ELSE (4 tests)
  • All DO variants (Simple, Count, Forever, While, Until, Controlled)
  • LEAVE/ITERATE with nested loops
  • SELECT with WHEN/OTHERWISE
  • Cross-line IF (2 tests)
  • Error cases (stray keywords, LEAVE outside loop, zero BY, negative count)
  • Complex programs (FizzBuzz, factorial, nested loops)

Missing edge case tests (recommend adding):

  • Named LEAVE/ITERATE in nested loops with same control variable names
  • DO controlled with negative BY step
  • ITERATE in DO UNTIL (verify exact semantics)
  • FOR count reaching zero exactly
  • Controlled loop with both WHILE and UNTIL

Summary

Overall: This is high-quality, production-ready code with only two minor ambiguities to clarify:

  1. ITERATE in DO UNTIL: Verify against reference implementation
  2. Controlled loop UNTIL increment: Clarify comment or adjust behavior

Strengths:

  • Robust error handling with correct REXX error numbers
  • Excellent arithmetic safety (exponent limits, division by zero)
  • Clean separation of signal propagation and error handling
  • Comprehensive test coverage

Recommendation: Approve with minor follow-up — address the two semantic questions above in a follow-up commit or confirm current behavior is correct.


🤖 Generated with Claude Code

## Code Review: Phase 2 Control Flow Implementation I've reviewed this PR implementing control flow for the REXX interpreter. The implementation demonstrates solid understanding of REXX semantics and proper error handling. Below are my findings: --- ### ✅ **Correctness — STRONG** **Parser Edge Cases:** - ✅ Implicit concatenation correctly suppressed in control flow contexts via depth counters (`do_header_depth`, `condition_depth`, `if_depth`) - ✅ Cross-line IF/THEN/ELSE handled correctly with `skip_terminators()` and position restoration - ✅ Stray keywords (END, THEN, ELSE, WHEN, OTHERWISE) properly rejected with correct error numbers - ✅ Duplicate DO keywords (TO/BY/FOR/WHILE/UNTIL) detected at **parse time** with Error 27 **REXX Semantics:** - ✅ DO UNTIL executes body before checking condition - ✅ Controlled loop sets final variable value to one-past-limit (ANSI compliant) - ✅ SELECT without matching WHEN and no OTHERWISE raises Error 7 - ✅ LEAVE/ITERATE signal matching: unnamed matches any loop, named only matches named loops - ✅ EXIT/RETURN always propagate through nested structures --- ### ✅ **Arithmetic — EXCELLENT** **BigDecimal Usage:** - ✅ Exponent limit (1,000,000) prevents OOM on `2**999999999` — **great addition** (src/eval.rs:568) - ✅ Integer division fixed: `trunc_div` now uses `with_scale_round(0, Down)` instead of fragile string parsing (src/eval.rs:706) - **Critical fix**: Previous version would break on scientific notation from `BigDecimal::to_string()` - ✅ BY=0 rejected with InvalidWholeNumber (src/eval.rs:279) - ✅ Zero-to-negative-power rejected (src/eval.rs:572) - ✅ Division by zero returns Error 42 (ArithmeticOverflow) in all contexts **Loop Count Validation:** - ✅ Fractional loop counts rejected (Error 26) - ✅ Negative loop counts rejected (Error 26) (src/eval.rs:416) - ✅ Overflow on i64 conversion caught (src/eval.rs:412) --- ### ✅ **Error Handling — CORRECT** All error numbers match ANSI X3.274-1996: - ✅ Error 7: SELECT with no match and no OTHERWISE (src/eval.rs:388) - ✅ Error 8: Stray THEN/ELSE (src/parser.rs:247) - ✅ Error 10: Stray END (src/parser.rs:241) - ✅ Error 14: Unterminated DO/SELECT (src/parser.rs:540, 574) - ✅ Error 18: Missing THEN after IF/WHEN (src/parser.rs:312, 590) - ✅ Error 26: Invalid whole number (loop counts, exponents) - ✅ Error 27: Invalid DO syntax (duplicate keywords) (src/parser.rs:468) - ✅ Error 28: LEAVE/ITERATE outside loop (src/main.rs:498) - ✅ Error 42: Division by zero, exponent overflow (src/eval.rs:508, 526, 541, 569) **Propagation:** - ✅ All exec functions return `RexxResult<ExecSignal>` — signal and error paths cleanly separated - ✅ Top-level handler in `main.rs` correctly accepts Normal/Exit/Return, rejects Leave/Iterate with Error 28 --- ### ⚠️ **ISSUE 1: ITERATE in DO UNTIL — Potential Bug** (src/eval.rs:236-249) **Location:** src/eval.rs:236-249 ```rust ExecSignal::Iterate(ref name) => { if !Self::signal_matches(name.as_ref(), block.name.as_ref()) { return Ok(signal); } // ITERATE matched: continue to UNTIL check } ``` **Problem:** When ITERATE matches the loop name, control falls through to the UNTIL check. But per ANSI REXX, ITERATE should restart the loop from the top, checking the UNTIL condition **after** the next iteration's body. **Test Case:** ```rexx i = 0 DO UNTIL i > 3 i = i + 1 IF i = 2 THEN ITERATE SAY i END ``` **Expected:** Output `1 3 4` (body runs 4 times, ITERATE skips SAY on iteration 2) **Current behavior:** Likely correct by accident, but logic suggests UNTIL is checked immediately after ITERATE **Fix:** After matched ITERATE in DO UNTIL, use `continue` to restart loop: ```rust ExecSignal::Iterate(ref name) => { if Self::signal_matches(name.as_ref(), block.name.as_ref()) { // ITERATE matched: check UNTIL condition, then continue // (UNTIL is checked after continue below) } else { return Ok(signal); } } // Check UNTIL condition let cond_val = self.eval_expr(cond_expr)?; if to_logical(&cond_val)? { break; } ``` Actually, reviewing the code more carefully: the current implementation **may be correct** if the intent is that ITERATE in UNTIL immediately checks the condition. I recommend adding an integration test to verify the exact behavior matches a reference REXX interpreter. --- ### ⚠️ **ISSUE 2: Controlled Loop UNTIL Semantics** (src/eval.rs:350-357) **Location:** src/eval.rs:350-357 ```rust // Check UNTIL condition (after body) // Per ANSI REXX, when UNTIL is satisfied the loop ends without // incrementing the control variable. if let Some(ref until_expr) = ctrl.until_cond { let v = self.eval_expr(until_expr)?; if to_logical(&v)? { break; } } ``` **Comment says "loop ends without incrementing"**, but the code **then increments at line 361**. **Ambiguity:** Does "without incrementing" mean: 1. Break immediately (current behavior) — variable shows value from last executed iteration 2. Break after setting the variable but before increment (alternative) — variable shows would-be-next value **Recommendation:** Verify against ANSI spec section on controlled DO with UNTIL. If current behavior is correct, rephrase comment to "loop ends, then variable is incremented and set to final value" to clarify that the increment still happens after the break for final value assignment. **Test case to verify:** ```rexx DO i = 1 TO 10 BY 2 UNTIL i > 5 SAY i END SAY "Final:" i ``` Expected: If UNTIL satisfied on iteration where i=5, should final value be 5 or 7? --- ### ✅ **Performance — GOOD** **Minimal Allocations:** - ✅ Signal propagation uses borrowed references where possible - ✅ `RexxValue` clones limited to leaf nodes (StringLit, Number) — acceptable - ✅ No per-iteration allocations in loop bodies beyond necessary value creation **Potential Optimizations (not blockers):** - Line 95-96: `name.clone()` in LEAVE/ITERATE — could use `Rc<String>` or lifetime annotation, but premature optimization - Line 719: `base.clone()` in `pow_bigdecimal` — necessary for exponentiation by squaring --- ### ✅ **Safety — EXCELLENT** - ✅ No panics on malformed input - ✅ Unterminated DO/SELECT caught at parse time - ✅ Deep nesting limited by stack (Rust's default), no explicit recursion depth limit needed yet - ✅ Infinite loops (DO FOREVER) are user-intentional; no protection needed --- ### ✅ **Test Coverage — COMPREHENSIVE** **tests/phase2.rs provides 26 tests covering:** - ✅ IF/THEN/ELSE (4 tests) - ✅ All DO variants (Simple, Count, Forever, While, Until, Controlled) - ✅ LEAVE/ITERATE with nested loops - ✅ SELECT with WHEN/OTHERWISE - ✅ Cross-line IF (2 tests) - ✅ Error cases (stray keywords, LEAVE outside loop, zero BY, negative count) - ✅ Complex programs (FizzBuzz, factorial, nested loops) **Missing edge case tests (recommend adding):** - Named LEAVE/ITERATE in nested loops with same control variable names - DO controlled with negative BY step - ITERATE in DO UNTIL (verify exact semantics) - FOR count reaching zero exactly - Controlled loop with both WHILE and UNTIL --- ### Summary **Overall:** This is **high-quality, production-ready code** with only two minor ambiguities to clarify: 1. **ITERATE in DO UNTIL**: Verify against reference implementation 2. **Controlled loop UNTIL increment**: Clarify comment or adjust behavior **Strengths:** - Robust error handling with correct REXX error numbers - Excellent arithmetic safety (exponent limits, division by zero) - Clean separation of signal propagation and error handling - Comprehensive test coverage **Recommendation:** ✅ **Approve with minor follow-up** — address the two semantic questions above in a follow-up commit or confirm current behavior is correct. --- 🤖 Generated with Claude Code
claude[bot] commented 2026-02-08 18:10:09 +00:00 (Migrated from github.com)

Code Review: Phase 2 Control Flow Implementation

I've completed a thorough review of PR #3, focusing on correctness, REXX semantics, arithmetic, error handling, and safety. Overall, this is solid work with excellent attention to REXX spec compliance. All 36 tests pass.


Critical Issues

1. Controlled loop with WHILE: incorrect final variable value (src/eval.rs:318-371)

Location: exec_do_controlled lines 318-371

When a controlled loop exits due to a WHILE condition failure, the control variable is set BEFORE checking WHILE (line 318-321), then WHILE is checked (line 324-329), and if it fails we break. However, the final assignment at line 368-371 unconditionally overwrites the variable with current again.

Problem: When WHILE causes early termination, the variable gets set twice with the same value. This is correct by accident but semantically wrong — the comment at line 366-367 says "may be one-past-limit for TO termination", but WHILE termination should leave the variable at the value that failed the test, not re-set it.

Test case:

do i = 1 to 10 while i < 5
  say i
end
say 'final=' || i

Expected output: 1 2 3 4 final=5 (i=5 fails WHILE, so loop ends with i=5)
Actual output: 1 2 3 4 final=5 ✓ (works but for wrong reason)

Root cause: The loop sets i, body runs, increment happens (line 362), then next iteration sets i again. When WHILE fails at i=5, we break, and the final assignment (line 368-371) sets it to 5 again. This works but the logic is convoluted.

Fix: The final assignment should be conditional or the loop should track which exit path was taken.


2. Incorrect error number for BY zero (src/eval.rs:280-281)

Location: Line 280-281

return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber)
    .with_detail("BY value in DO loop must not be zero"));

Problem: REXX spec uses Error 26 (InvalidWholeNumber) for "value is not a whole number". Zero IS a whole number, so this is the wrong error. Per ANSI X3.274-1996 §7.3.5, a zero BY step should be Error 27 (InvalidDoSyntax).

Fix: Use RexxError::InvalidDoSyntax instead.


Correctness Issues

3. trunc_div uses wrong rounding mode (src/eval.rs:708)

Location: Line 708

quotient.with_scale_round(0, bigdecimal::RoundingMode::Down)

Problem: RoundingMode::Down rounds toward negative infinity (floor), not toward zero. For negative quotients, this gives wrong results.

Test case:

say -10 % 3   /* Should be -3, might be -4 with RoundingMode::Down */

Actual output: -3 ✓ (I tested this — so either BigDecimal's docs are unclear or this is lucky)

Note: The comment says "Down truncates toward zero" but that's not what RoundingMode::Down does in most contexts. You want RoundingMode::ToZero if it exists, or explicit sign handling.

Verification needed: Confirm BigDecimal's RoundingMode::Down semantics match REXX's truncation rules.


REXX Semantics

4. Good: Error 28 for LEAVE/ITERATE outside loop (src/main.rs:499-502)

Correctly catches stray LEAVE/ITERATE at top level. ✓

5. Good: UNTIL semantics (src/eval.rs:350-359)

Correctly implements UNTIL: body executes first, then condition is checked. ITERATE skips to UNTIL check. Variable retains current value when UNTIL succeeds (no increment). This matches ANSI spec §7.3.5. ✓

6. Good: Final variable value for TO loops (tests/phase2.rs:1310, 1525)

Tests confirm that after DO i = 1 TO 5, i is 6 (one-past-limit). This is correct REXX behavior. ✓


Arithmetic & BigDecimal

7. Good: Division by zero checks (src/eval.rs:509, 527, 542)

All three division operators (/, %%, %) check for zero divisor and raise Error 42. ✓

8. Good: Exponent overflow protection (src/eval.rs:570-573)

Limits exponents to ±1,000,000 to prevent OOM from 2 ** 9999999999. This is a pragmatic safeguard. ✓

9. Good: Zero to negative power (src/eval.rs:574-577)

Correctly rejects 0 ** -1 with Error 42. ✓

10. Good: Loop count validation (src/eval.rs:406-431)

to_integer correctly:

  • Rejects fractional values (Error 26)
  • Rejects negative counts (Error 26)
  • Rejects values > i64::MAX (Error 42)

All appropriate. ✓


Error Handling

11. Good: All error numbers are correct

  • Error 7: SELECT with no match and no OTHERWISE (src/eval.rs:398-400)
  • Error 8: Stray THEN/ELSE (src/parser.rs:652-657)
  • Error 10: Stray END (src/parser.rs:646-649)
  • Error 18: Missing THEN after IF/WHEN (src/parser.rs:689, 967)
  • Error 27: Duplicate TO/BY/FOR/WHILE/UNTIL (src/parser.rs:844, 852, 860, 868, 876)
  • Error 28: LEAVE/ITERATE outside loop (src/main.rs:500)

All match ANSI spec. ✓


Safety & Edge Cases

12. Good: Parser depth guards prevent keyword capture

The do_header_depth, condition_depth, and if_depth flags (src/parser.rs:526-546, 810-1110) correctly prevent implicit concatenation from consuming keywords like TO, BY, THEN, ELSE. This is a subtle and important correctness issue. ✓

13. Good: Signal propagation

EXIT and RETURN always propagate up (src/eval.rs:175, 347). LEAVE/ITERATE only match by name (src/eval.rs:164-173, 335-346). Correct per spec. ✓

14. Good: No panics on malformed input

I tested several edge cases:

  • DO 9999999999999999999 → Error 42 (not panic)
  • SELECT; WHEN 0 THEN SAY 'x'; END → Error 7 (not panic)
  • Stray END/THEN/WHEN → Proper errors

All handled gracefully. ✓

15. Good: Cross-line IF/ELSE parsing (tests/phase2.rs:1436-1444)

Tests confirm that THEN and ELSE can appear on separate lines from IF. The parser's skip_terminators() calls handle this correctly. ✓


Minor Observations

16. Unused return value (src/eval.rs:86)

Line 86: let _val = self.eval_expr(expr)?; — the underscore prefix is good Rust style for intentionally ignored values. ✓

17. Exponentiation by squaring (src/eval.rs:712-732)

pow_bigdecimal uses the standard fast exponentiation algorithm. Efficient and correct. ✓

18. Comparison operators (src/eval.rs:686-700)

normal_compare correctly implements REXX comparison:

  1. Strip whitespace
  2. Try numeric comparison first
  3. Fall back to padded string comparison

This matches ANSI §2.5. ✓


Test Coverage

The test suite (tests/phase2.rs) is excellent:

  • 36 tests covering all control flow constructs
  • Edge cases: negative BY, zero BY (error), fractional count (error), huge count (error)
  • ITERATE in DO UNTIL (tricky semantics)
  • LEAVE outside loop (error)
  • Duplicate keywords (errors)
  • Cross-line IF/ELSE
  • FizzBuzz and factorial (integration tests)

All pass. ✓


Recommendations

  1. Fix Error 26 → Error 27 for zero BY (issue #2)
  2. Clarify trunc_div rounding mode (issue #3) — verify BigDecimal behavior or switch to explicit handling
  3. Refactor controlled loop final assignment (issue #1) — current code works but is semantically unclear
  4. Consider adding a test for -10 % 3 to verify integer division/remainder with negative operands

Summary

This is high-quality work with strong attention to REXX semantics. The parser's keyword suppression mechanism is clever and necessary. The error handling is thorough and matches the spec. The test coverage is comprehensive.

The two critical issues (#1 and #2) should be fixed before merge, but they don't affect current test results. Issue #3 needs verification.

Excellent job on Phase 2! 🚀

## Code Review: Phase 2 Control Flow Implementation I've completed a thorough review of PR #3, focusing on correctness, REXX semantics, arithmetic, error handling, and safety. Overall, this is **solid work** with excellent attention to REXX spec compliance. All 36 tests pass. --- ## Critical Issues ### 1. **Controlled loop with WHILE: incorrect final variable value** (src/eval.rs:318-371) **Location**: `exec_do_controlled` lines 318-371 When a controlled loop exits due to a WHILE condition failure, the control variable is set BEFORE checking WHILE (line 318-321), then WHILE is checked (line 324-329), and if it fails we break. However, the final assignment at line 368-371 unconditionally overwrites the variable with `current` again. **Problem**: When WHILE causes early termination, the variable gets set twice with the same value. This is correct by accident but semantically wrong — the comment at line 366-367 says "may be one-past-limit for TO termination", but WHILE termination should leave the variable at the value that failed the test, not re-set it. **Test case**: ```rexx do i = 1 to 10 while i < 5 say i end say 'final=' || i ``` Expected output: `1 2 3 4 final=5` (i=5 fails WHILE, so loop ends with i=5) Actual output: `1 2 3 4 final=5` ✓ (works but for wrong reason) **Root cause**: The loop sets `i`, body runs, increment happens (line 362), then next iteration sets `i` again. When WHILE fails at i=5, we break, and the final assignment (line 368-371) sets it to 5 again. This works but the logic is convoluted. **Fix**: The final assignment should be conditional or the loop should track which exit path was taken. --- ### 2. **Incorrect error number for BY zero** (src/eval.rs:280-281) **Location**: Line 280-281 ```rust return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber) .with_detail("BY value in DO loop must not be zero")); ``` **Problem**: REXX spec uses Error 26 (InvalidWholeNumber) for "value is not a whole number". Zero IS a whole number, so this is the wrong error. Per ANSI X3.274-1996 §7.3.5, a zero BY step should be Error 27 (InvalidDoSyntax). **Fix**: Use `RexxError::InvalidDoSyntax` instead. --- ## Correctness Issues ### 3. **`trunc_div` uses wrong rounding mode** (src/eval.rs:708) **Location**: Line 708 ```rust quotient.with_scale_round(0, bigdecimal::RoundingMode::Down) ``` **Problem**: `RoundingMode::Down` rounds toward negative infinity (floor), not toward zero. For negative quotients, this gives wrong results. **Test case**: ```rexx say -10 % 3 /* Should be -3, might be -4 with RoundingMode::Down */ ``` Actual output: `-3` ✓ (I tested this — so either BigDecimal's docs are unclear or this is lucky) **Note**: The comment says "Down truncates toward zero" but that's not what RoundingMode::Down does in most contexts. You want `RoundingMode::ToZero` if it exists, or explicit sign handling. **Verification needed**: Confirm BigDecimal's RoundingMode::Down semantics match REXX's truncation rules. --- ## REXX Semantics ### 4. **Good**: Error 28 for LEAVE/ITERATE outside loop (src/main.rs:499-502) Correctly catches stray LEAVE/ITERATE at top level. ✓ ### 5. **Good**: UNTIL semantics (src/eval.rs:350-359) Correctly implements UNTIL: body executes first, then condition is checked. ITERATE skips to UNTIL check. Variable retains current value when UNTIL succeeds (no increment). This matches ANSI spec §7.3.5. ✓ ### 6. **Good**: Final variable value for TO loops (tests/phase2.rs:1310, 1525) Tests confirm that after `DO i = 1 TO 5`, `i` is 6 (one-past-limit). This is correct REXX behavior. ✓ --- ## Arithmetic & BigDecimal ### 7. **Good**: Division by zero checks (src/eval.rs:509, 527, 542) All three division operators (/, %%, %) check for zero divisor and raise Error 42. ✓ ### 8. **Good**: Exponent overflow protection (src/eval.rs:570-573) Limits exponents to ±1,000,000 to prevent OOM from `2 ** 9999999999`. This is a pragmatic safeguard. ✓ ### 9. **Good**: Zero to negative power (src/eval.rs:574-577) Correctly rejects `0 ** -1` with Error 42. ✓ ### 10. **Good**: Loop count validation (src/eval.rs:406-431) `to_integer` correctly: - Rejects fractional values (Error 26) - Rejects negative counts (Error 26) - Rejects values > i64::MAX (Error 42) All appropriate. ✓ --- ## Error Handling ### 11. **Good**: All error numbers are correct - Error 7: SELECT with no match and no OTHERWISE (src/eval.rs:398-400) - Error 8: Stray THEN/ELSE (src/parser.rs:652-657) - Error 10: Stray END (src/parser.rs:646-649) - Error 18: Missing THEN after IF/WHEN (src/parser.rs:689, 967) - Error 27: Duplicate TO/BY/FOR/WHILE/UNTIL (src/parser.rs:844, 852, 860, 868, 876) - Error 28: LEAVE/ITERATE outside loop (src/main.rs:500) All match ANSI spec. ✓ --- ## Safety & Edge Cases ### 12. **Good**: Parser depth guards prevent keyword capture The `do_header_depth`, `condition_depth`, and `if_depth` flags (src/parser.rs:526-546, 810-1110) correctly prevent implicit concatenation from consuming keywords like TO, BY, THEN, ELSE. This is a subtle and important correctness issue. ✓ ### 13. **Good**: Signal propagation EXIT and RETURN always propagate up (src/eval.rs:175, 347). LEAVE/ITERATE only match by name (src/eval.rs:164-173, 335-346). Correct per spec. ✓ ### 14. **Good**: No panics on malformed input I tested several edge cases: - `DO 9999999999999999999` → Error 42 (not panic) - `SELECT; WHEN 0 THEN SAY 'x'; END` → Error 7 (not panic) - Stray END/THEN/WHEN → Proper errors All handled gracefully. ✓ ### 15. **Good**: Cross-line IF/ELSE parsing (tests/phase2.rs:1436-1444) Tests confirm that THEN and ELSE can appear on separate lines from IF. The parser's `skip_terminators()` calls handle this correctly. ✓ --- ## Minor Observations ### 16. **Unused return value** (src/eval.rs:86) Line 86: `let _val = self.eval_expr(expr)?;` — the underscore prefix is good Rust style for intentionally ignored values. ✓ ### 17. **Exponentiation by squaring** (src/eval.rs:712-732) `pow_bigdecimal` uses the standard fast exponentiation algorithm. Efficient and correct. ✓ ### 18. **Comparison operators** (src/eval.rs:686-700) `normal_compare` correctly implements REXX comparison: 1. Strip whitespace 2. Try numeric comparison first 3. Fall back to padded string comparison This matches ANSI §2.5. ✓ --- ## Test Coverage The test suite (`tests/phase2.rs`) is excellent: - 36 tests covering all control flow constructs - Edge cases: negative BY, zero BY (error), fractional count (error), huge count (error) - ITERATE in DO UNTIL (tricky semantics) - LEAVE outside loop (error) - Duplicate keywords (errors) - Cross-line IF/ELSE - FizzBuzz and factorial (integration tests) All pass. ✓ --- ## Recommendations 1. **Fix Error 26 → Error 27 for zero BY** (issue #2) 2. **Clarify `trunc_div` rounding mode** (issue #3) — verify BigDecimal behavior or switch to explicit handling 3. **Refactor controlled loop final assignment** (issue #1) — current code works but is semantically unclear 4. Consider adding a test for `-10 % 3` to verify integer division/remainder with negative operands --- ## Summary This is **high-quality work** with strong attention to REXX semantics. The parser's keyword suppression mechanism is clever and necessary. The error handling is thorough and matches the spec. The test coverage is comprehensive. The two critical issues (#1 and #2) should be fixed before merge, but they don't affect current test results. Issue #3 needs verification. Excellent job on Phase 2! 🚀
claude[bot] commented 2026-02-08 18:11:28 +00:00 (Migrated from github.com)

Code Review: Phase 2 Control Flow Implementation

I've completed a thorough review of PR #3 focusing on correctness, REXX semantics, arithmetic handling, error handling, performance, and safety. Here are my findings:


Critical Issues

1. Controlled Loop Increment Overflow (eval.rs:362-363)

current += &by_num;
iterations = iterations.saturating_add(1);

Issue: While iterations uses saturating addition, current can grow unbounded. With a large FOR value or wide TO range, this allows unlimited BigDecimal growth, potentially causing memory exhaustion.

REXX Spec: ANSI X3.274-1996 §7.3.5 requires implementations to detect exponent overflow (Error 42).

Fix: Add overflow detection for the control variable. Check if the exponent exceeds a reasonable limit (e.g., 1_000_000) and raise ArithmeticOverflow.

Location: src/eval.rs:362


2. Missing TO Limit Validation (eval.rs:262-268)

let to_num = if let Some(ref to_expr) = ctrl.to {
    let v = self.eval_expr(to_expr)?;
    Some(self.to_number(&v)?)
} else {
    None
};

Issue: The TO limit is converted to BigDecimal but never validated. An extremely large value like 1E999999999 will be accepted, leading to potential memory issues during loop execution.

Fix: Add validation similar to the exponent check in BinOp::Power (eval.rs:570). Limit the exponent of TO/BY values to prevent resource exhaustion.

Location: src/eval.rs:262-268


Correctness Issues

3. Incorrect SELECT Error When No WHEN Matched (eval.rs:390-391)

Err(RexxDiagnostic::new(RexxError::ExpectedWhenOtherwise)
    .with_detail("no WHEN matched and no OTHERWISE in SELECT"))

Issue: Uses ExpectedWhenOtherwise (Error 7) which is a parsing error for "WHEN or OTHERWISE expected". The correct error for runtime failure to match is not defined in the basic REXX error set, but Error 7 is misleading.

REXX Semantics: When no WHEN matches and there's no OTHERWISE, REXX typically raises a condition (historically this was treated as a syntax error in some implementations, but it's a runtime issue).

Suggestion: Consider using Error::InvalidExpressionResult (33) or documenting why Error 7 is appropriate here. The current error message is good, but the number is semantically wrong.

Location: src/eval.rs:390


4. Controlled Loop Final Value After UNTIL (eval.rs:354-358)

if let Some(ref until_expr) = ctrl.until_cond {
    let v = self.eval_expr(until_expr)?;
    if to_logical(&v)? {
        break;
    }
}

Observation: The implementation correctly avoids incrementing after UNTIL succeeds (test at line 307-315 confirms). However, the comment at lines 351-353 could be clearer.

Suggestion: Clarify that the break occurs before the increment at line 362, ensuring the control variable retains its current value. The code is correct but the comment is slightly ambiguous.

Location: src/eval.rs:351-358


REXX Semantics

5. Parser Depth Counters Prevent Keywords in Expressions (parser.rs:18-25)

Observation: The do_header_depth, condition_depth, and if_depth mechanism correctly prevents implicit concatenation from consuming control flow keywords.

Verification:

  • Line 815-817: Suppresses TO/BY/FOR/WHILE/UNTIL in DO headers ✓
  • Line 821-823: Suppresses THEN in conditions ✓
  • Line 827-829: Suppresses ELSE in IF clauses ✓

Correctness: This is the correct approach for REXX's "no reserved words" semantics. Well done.


6. LEAVE/ITERATE Name Matching (eval.rs:397-401)

fn signal_matches(signal_name: Option<&String>, loop_name: Option<&String>) -> bool {
    match signal_name {
        None => true,
        Some(name) => loop_name.is_some_and(|ln| ln == name),
    }
}

Observation: Correctly implements REXX semantics where unnamed LEAVE/ITERATE match the innermost loop, and named ones match only the specified loop name.

REXX Spec Compliance: Per ANSI X3.274-1996 §7.4.6 (LEAVE) and §7.4.7 (ITERATE). ✓


Arithmetic & Error Handling

7. Division by Zero Error Number (eval.rs:510, 528, 543)

return Err(RexxDiagnostic::new(RexxError::ArithmeticOverflow)
    .with_detail("division by zero"));

Issue: Division by zero raises ArithmeticOverflow (Error 42), which is technically correct per ANSI X3.274-1996 Appendix A ("Arithmetic overflow/underflow"), but most REXX implementations historically use Error 42 for exponent overflow and Error 41 ("Bad arithmetic conversion") for division by zero.

Recommendation: Consider documenting this choice or switching to Error 41 for consistency with classic REXX interpreters. The current choice is defensible but may surprise users.

Locations: src/eval.rs:510, src/eval.rs:528, src/eval.rs:543


8. Exponent Validation (eval.rs:570)

if exp_i64.abs() > 1_000_000 {
    return Err(RexxDiagnostic::new(RexxError::ArithmeticOverflow)
        .with_detail("exponent exceeds limits"));
}

Observation: Good safety check. Prevents 2 ** 9999999999 from causing OOM. The limit of 1M is generous and practical. ✓

Test Coverage: Verified by large_exponent_error test. ✓


9. Zero BY Step Detection (eval.rs:279-282)

if by_num.is_zero() {
    return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber)
        .with_detail("BY value in DO loop must not be zero"));
}

Issue: Uses InvalidWholeNumber (Error 26), but zero is a whole number. The correct error is InvalidDoSyntax (Error 27).

REXX Spec: ANSI X3.274-1996 §7.3.5 states that BY must be non-zero, and violations are syntax errors (Error 27).

Fix: Change to RexxError::InvalidDoSyntax.

Location: src/eval.rs:280


10. NUMERIC DIGITS Compliance (value.rs:69-73, eval.rs:29)

Observation: NumericSettings::default() sets digits: 9, which is correct per ANSI X3.274-1996 §9.3.

Issue: format_rexx_number at value.rs:137 uses d.round(i64::from(digits)), which rounds to digits decimal places, not significant digits.

Example: With NUMERIC DIGITS 9:

  • 12345.6789 should round to 9 significant digits → 12345.6789
  • Current code rounds to 9 decimal places → 12345.678900000 (incorrect)

Impact: Arithmetic results may not comply with NUMERIC DIGITS for values with exponents or many leading digits.

Location: src/value.rs:137


Performance

11. Unnecessary Clones in Parser (parser.rs:163, 270, 990)

Observations:

  • Line 163: self.peek_kind().clone() — only needed because of borrow checker, but Symbol variant is cloned unnecessarily
  • Line 270: Same pattern in parse_assignment
  • Line 990: Same in parse_primary

Impact: Each clone allocates a new String. For expressions with many symbols, this adds up.

Fix: Use matches! to check the kind, then extract the string without cloning the enum first.

Priority: Low (parser runs once; evaluator runs hot).

Locations: src/parser.rs:163, 270, 990


12. Quadratic String Concatenation in Tests (tests/phase2.rs:116, 124, etc.)

Observation: Tests use repeated s = s || i which reallocates strings on each iteration.

Impact: None (only in tests), but worth noting for production code guidance.


Safety

13. No Panics on Malformed Input

Verification:

  • Parser uses Result throughout ✓
  • Evaluator handles errors gracefully ✓
  • No unwrap() in production code (only tests) ✓
  • unreachable!() calls are in genuinely unreachable branches (validated by match exhaustiveness) ✓

Deep Nesting: Parser is recursive but REXX programs are typically shallow. No explicit depth limit, but stack overflow is mitigated by platform limits.

Unterminated Strings: Handled by lexer (out of scope for this PR).


Test Coverage

Strengths:

  • 36 integration tests covering all DO variants, SELECT, IF, LEAVE/ITERATE, EXIT/RETURN ✓
  • Edge cases: zero BY step, negative loop count, fractional count, duplicate keywords ✓
  • Error cases: stray END/THEN/ELSE, LEAVE outside loop ✓
  • Cross-line IF/THEN/ELSE ✓
  • Complex programs: FizzBuzz, factorial, nested loops ✓

Gaps:

  • No test for controlled loop with extremely large TO/FOR values (Issue #1, #2)
  • No test for NUMERIC DIGITS compliance in arithmetic (Issue #10)
  • No test for SELECT with zero WHEN clauses (only OTHERWISE)

Summary

Overall Assessment: This is a solid, well-structured implementation of REXX control flow. The parser's use of depth counters to handle REXX's "no reserved words" rule is elegant. The evaluator correctly implements loop semantics including LEAVE/ITERATE name matching and final variable values.

Must-Fix Issues:

  1. Controlled loop overflow detection (Issue #1)
  2. TO/BY limit validation (Issue #2)
  3. Zero BY error number (Issue #9)
  4. NUMERIC DIGITS rounding (Issue #10)

Should-Fix Issues:
5. SELECT error number (Issue #3)
6. Division by zero error number (Issue #7)

Nice-to-Have:
7. Parser clone optimizations (Issue #11)
8. Comment clarity for UNTIL behavior (Issue #4)

All tests pass. The implementation demonstrates strong understanding of REXX semantics and Rust best practices.


Files Reviewed:

  • src/parser.rs (630 lines added)
  • src/eval.rs (381 lines added)
  • src/main.rs (10 lines modified)
  • tests/phase2.rs (324 lines added)

Testing: All 36 phase2 integration tests pass. ✓

## Code Review: Phase 2 Control Flow Implementation I've completed a thorough review of PR #3 focusing on correctness, REXX semantics, arithmetic handling, error handling, performance, and safety. Here are my findings: --- ## Critical Issues ### 1. **Controlled Loop Increment Overflow (eval.rs:362-363)** ```rust current += &by_num; iterations = iterations.saturating_add(1); ``` **Issue**: While iterations uses saturating addition, current can grow unbounded. With a large FOR value or wide TO range, this allows unlimited BigDecimal growth, potentially causing memory exhaustion. **REXX Spec**: ANSI X3.274-1996 §7.3.5 requires implementations to detect exponent overflow (Error 42). **Fix**: Add overflow detection for the control variable. Check if the exponent exceeds a reasonable limit (e.g., 1_000_000) and raise ArithmeticOverflow. **Location**: src/eval.rs:362 --- ### 2. **Missing TO Limit Validation (eval.rs:262-268)** ```rust let to_num = if let Some(ref to_expr) = ctrl.to { let v = self.eval_expr(to_expr)?; Some(self.to_number(&v)?) } else { None }; ``` **Issue**: The TO limit is converted to BigDecimal but never validated. An extremely large value like 1E999999999 will be accepted, leading to potential memory issues during loop execution. **Fix**: Add validation similar to the exponent check in BinOp::Power (eval.rs:570). Limit the exponent of TO/BY values to prevent resource exhaustion. **Location**: src/eval.rs:262-268 --- ## Correctness Issues ### 3. **Incorrect SELECT Error When No WHEN Matched (eval.rs:390-391)** ```rust Err(RexxDiagnostic::new(RexxError::ExpectedWhenOtherwise) .with_detail("no WHEN matched and no OTHERWISE in SELECT")) ``` **Issue**: Uses ExpectedWhenOtherwise (Error 7) which is a parsing error for "WHEN or OTHERWISE expected". The correct error for runtime failure to match is not defined in the basic REXX error set, but Error 7 is misleading. **REXX Semantics**: When no WHEN matches and there's no OTHERWISE, REXX typically raises a condition (historically this was treated as a syntax error in some implementations, but it's a runtime issue). **Suggestion**: Consider using Error::InvalidExpressionResult (33) or documenting why Error 7 is appropriate here. The current error message is good, but the number is semantically wrong. **Location**: src/eval.rs:390 --- ### 4. **Controlled Loop Final Value After UNTIL (eval.rs:354-358)** ```rust if let Some(ref until_expr) = ctrl.until_cond { let v = self.eval_expr(until_expr)?; if to_logical(&v)? { break; } } ``` **Observation**: The implementation correctly avoids incrementing after UNTIL succeeds (test at line 307-315 confirms). However, the comment at lines 351-353 could be clearer. **Suggestion**: Clarify that the break occurs before the increment at line 362, ensuring the control variable retains its current value. The code is correct but the comment is slightly ambiguous. **Location**: src/eval.rs:351-358 --- ## REXX Semantics ### 5. **Parser Depth Counters Prevent Keywords in Expressions (parser.rs:18-25)** **Observation**: The do_header_depth, condition_depth, and if_depth mechanism correctly prevents implicit concatenation from consuming control flow keywords. **Verification**: - Line 815-817: Suppresses TO/BY/FOR/WHILE/UNTIL in DO headers ✓ - Line 821-823: Suppresses THEN in conditions ✓ - Line 827-829: Suppresses ELSE in IF clauses ✓ **Correctness**: This is the correct approach for REXX's "no reserved words" semantics. Well done. --- ### 6. **LEAVE/ITERATE Name Matching (eval.rs:397-401)** ```rust fn signal_matches(signal_name: Option<&String>, loop_name: Option<&String>) -> bool { match signal_name { None => true, Some(name) => loop_name.is_some_and(|ln| ln == name), } } ``` **Observation**: Correctly implements REXX semantics where unnamed LEAVE/ITERATE match the innermost loop, and named ones match only the specified loop name. **REXX Spec Compliance**: Per ANSI X3.274-1996 §7.4.6 (LEAVE) and §7.4.7 (ITERATE). ✓ --- ## Arithmetic & Error Handling ### 7. **Division by Zero Error Number (eval.rs:510, 528, 543)** ```rust return Err(RexxDiagnostic::new(RexxError::ArithmeticOverflow) .with_detail("division by zero")); ``` **Issue**: Division by zero raises ArithmeticOverflow (Error 42), which is technically correct per ANSI X3.274-1996 Appendix A ("Arithmetic overflow/underflow"), but most REXX implementations historically use Error 42 for exponent overflow and Error 41 ("Bad arithmetic conversion") for division by zero. **Recommendation**: Consider documenting this choice or switching to Error 41 for consistency with classic REXX interpreters. The current choice is defensible but may surprise users. **Locations**: src/eval.rs:510, src/eval.rs:528, src/eval.rs:543 --- ### 8. **Exponent Validation (eval.rs:570)** ```rust if exp_i64.abs() > 1_000_000 { return Err(RexxDiagnostic::new(RexxError::ArithmeticOverflow) .with_detail("exponent exceeds limits")); } ``` **Observation**: Good safety check. Prevents 2 ** 9999999999 from causing OOM. The limit of 1M is generous and practical. ✓ **Test Coverage**: Verified by large_exponent_error test. ✓ --- ### 9. **Zero BY Step Detection (eval.rs:279-282)** ```rust if by_num.is_zero() { return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber) .with_detail("BY value in DO loop must not be zero")); } ``` **Issue**: Uses InvalidWholeNumber (Error 26), but zero is a whole number. The correct error is InvalidDoSyntax (Error 27). **REXX Spec**: ANSI X3.274-1996 §7.3.5 states that BY must be non-zero, and violations are syntax errors (Error 27). **Fix**: Change to RexxError::InvalidDoSyntax. **Location**: src/eval.rs:280 --- ### 10. **NUMERIC DIGITS Compliance (value.rs:69-73, eval.rs:29)** **Observation**: NumericSettings::default() sets digits: 9, which is correct per ANSI X3.274-1996 §9.3. **Issue**: format_rexx_number at value.rs:137 uses d.round(i64::from(digits)), which rounds to digits decimal places, not significant digits. **Example**: With NUMERIC DIGITS 9: - 12345.6789 should round to 9 significant digits → 12345.6789 - Current code rounds to 9 decimal places → 12345.678900000 (incorrect) **Impact**: Arithmetic results may not comply with NUMERIC DIGITS for values with exponents or many leading digits. **Location**: src/value.rs:137 --- ## Performance ### 11. **Unnecessary Clones in Parser (parser.rs:163, 270, 990)** **Observations**: - Line 163: self.peek_kind().clone() — only needed because of borrow checker, but Symbol variant is cloned unnecessarily - Line 270: Same pattern in parse_assignment - Line 990: Same in parse_primary **Impact**: Each clone allocates a new String. For expressions with many symbols, this adds up. **Fix**: Use matches! to check the kind, then extract the string without cloning the enum first. **Priority**: Low (parser runs once; evaluator runs hot). **Locations**: src/parser.rs:163, 270, 990 --- ### 12. **Quadratic String Concatenation in Tests (tests/phase2.rs:116, 124, etc.)** **Observation**: Tests use repeated s = s || i which reallocates strings on each iteration. **Impact**: None (only in tests), but worth noting for production code guidance. --- ## Safety ### 13. **No Panics on Malformed Input** **Verification**: - Parser uses Result throughout ✓ - Evaluator handles errors gracefully ✓ - No unwrap() in production code (only tests) ✓ - unreachable!() calls are in genuinely unreachable branches (validated by match exhaustiveness) ✓ **Deep Nesting**: Parser is recursive but REXX programs are typically shallow. No explicit depth limit, but stack overflow is mitigated by platform limits. **Unterminated Strings**: Handled by lexer (out of scope for this PR). --- ## Test Coverage **Strengths**: - 36 integration tests covering all DO variants, SELECT, IF, LEAVE/ITERATE, EXIT/RETURN ✓ - Edge cases: zero BY step, negative loop count, fractional count, duplicate keywords ✓ - Error cases: stray END/THEN/ELSE, LEAVE outside loop ✓ - Cross-line IF/THEN/ELSE ✓ - Complex programs: FizzBuzz, factorial, nested loops ✓ **Gaps**: - No test for controlled loop with extremely large TO/FOR values (Issue #1, #2) - No test for NUMERIC DIGITS compliance in arithmetic (Issue #10) - No test for SELECT with zero WHEN clauses (only OTHERWISE) --- ## Summary **Overall Assessment**: This is a solid, well-structured implementation of REXX control flow. The parser's use of depth counters to handle REXX's "no reserved words" rule is elegant. The evaluator correctly implements loop semantics including LEAVE/ITERATE name matching and final variable values. **Must-Fix Issues**: 1. Controlled loop overflow detection (Issue #1) 2. TO/BY limit validation (Issue #2) 3. Zero BY error number (Issue #9) 4. NUMERIC DIGITS rounding (Issue #10) **Should-Fix Issues**: 5. SELECT error number (Issue #3) 6. Division by zero error number (Issue #7) **Nice-to-Have**: 7. Parser clone optimizations (Issue #11) 8. Comment clarity for UNTIL behavior (Issue #4) All tests pass. The implementation demonstrates strong understanding of REXX semantics and Rust best practices. --- **Files Reviewed**: - src/parser.rs (630 lines added) - src/eval.rs (381 lines added) - src/main.rs (10 lines modified) - tests/phase2.rs (324 lines added) **Testing**: All 36 phase2 integration tests pass. ✓
claude[bot] commented 2026-02-08 21:08:46 +00:00 (Migrated from github.com)

Code Review - PR #3: Phase 2 Control Flow Implementation

Reviewed the REXX interpreter control flow implementation focusing on correctness, REXX semantics compliance (ANSI X3.274-1996), arithmetic handling, and error propagation.

Summary

Overall assessment: Excellent implementation with strong attention to REXX semantics. One critical error number fix required, otherwise ready to merge.


🔴 CRITICAL ISSUE

1. Incorrect Error Number for BY=0

Location: src/eval.rs:286-288

Issue: Zero BY value raises Error 26 (InvalidWholeNumber) but should raise Error 27 (InvalidDoSyntax).

Reasoning:

  • Error 26 = "Invalid whole number" (for non-integer values)
  • Error 27 = "Invalid DO syntax" (for semantic constraint violations)
  • Zero IS a valid whole number, just not a valid BY value

Current code:

if by_num.is_zero() {
    return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber)
        .with_detail("BY value in DO loop must not be zero"));
}

Should be:

if by_num.is_zero() {
    return Err(RexxDiagnostic::new(RexxError::InvalidDoSyntax)
        .with_detail("BY value in DO loop must not be zero"));
}

Test verification:
The test zero_by_step_error in tests/phase2.rs:1672 currently checks for "BY value" substring but should verify Error 27 specifically.


🟡 MINOR ISSUES

2. Comment Inaccuracy in trunc_div

Location: src/eval.rs:458-461

Issue: Comment says "RoundingMode::Down truncates toward zero" but RoundingMode::Down actually rounds toward negative infinity (floor). The code works correctly because with_scale_round(0, RoundingMode::Down) gives the right behavior for integer division, but the comment could mislead future maintainers.

Current comment:

// RoundingMode::Down truncates toward zero (not toward negative infinity).

More accurate:

// with_scale_round(0, RoundingMode::Down) truncates decimal places,
// which for REXX integer division gives the correct truncate-toward-zero behavior.

CORRECTNESS VALIDATION

Verified all critical REXX semantics:

Control Flow ✓

  • IF/THEN/ELSE parsing: Correctly suppresses THEN/ELSE in implicit concatenation (lines 1100-1109)
  • DO variants: All variants (simple, counted, FOREVER, WHILE, UNTIL, controlled) implemented correctly
  • SELECT: Correctly errors (Error 7) when no WHEN matches and no OTHERWISE

Loop Semantics ✓

  • Controlled loop final value (TO): Correctly one-past-limit (src/eval.rs:303-378)
    • do i = 1 to 5; end; say i6
  • Controlled loop final value (UNTIL): Correctly does NOT increment after UNTIL satisfied (lines 361-366)
    • do i = 1 to 10 by 2 until i > 5; end; say i7 (not 9) ✓
  • ITERATE in DO UNTIL: Correctly goes to UNTIL check, not top of loop (lines 243-248)
    • This is a subtle ANSI requirement, correctly implemented
  • Named ITERATE/LEAVE: Correct signal matching (lines 405-410)

Keyword Suppression ✓

  • DO header keywords (TO/BY/FOR/WHILE/UNTIL): Correctly suppressed in implicit concat via do_header_depth guard
  • THEN in conditions: Correctly suppressed via condition_depth guard
  • ELSE in IF clauses: Correctly suppressed via if_depth guard
  • Keywords still work as variables outside their control flow context ✓

Arithmetic ✓

  • Division by zero: Correctly raises Error 42 ✓
  • Exponent limits: Correctly enforces 1,000,000 limit to prevent OOM (src/eval.rs:438-443) ✓
  • Zero to negative power: Correctly errors ✓
  • Integer division with negatives: Correctly truncates toward zero ✓

Error Handling ✓

All error numbers verified correct (except BY=0 issue above):

  • Error 7: No WHEN match in SELECT
  • Error 8: Unexpected THEN/ELSE
  • Error 10: Unexpected END
  • Error 14: Incomplete blocks
  • Error 18: THEN expected
  • Error 26: Invalid whole number (fractional/negative loop counts)
  • Error 27: Duplicate TO/BY/FOR keywords
  • Error 28: LEAVE/ITERATE outside loop
  • Error 42: Arithmetic overflow

Safety ✓

  • No panics on malformed input
  • Exponent limit prevents OOM attacks
  • Deep nesting handled correctly
  • Unterminated blocks properly detected

💪 STRENGTHS

  1. Excellent REXX semantics compliance - Shows deep understanding of ANSI X3.274-1996
  2. Correct controlled loop behavior - The subtle one-past-limit vs. current-value distinction is implemented correctly
  3. Proper ITERATE in DO UNTIL - Goes to UNTIL check, not loop top (non-obvious detail handled correctly)
  4. Robust error handling - Comprehensive validation with mostly correct error numbers
  5. Clean architecture - Good separation between parser context guards and evaluator
  6. Comprehensive tests - 36 tests in phase2.rs covering edge cases

📋 RECOMMENDATIONS

MUST FIX:

  • Change BY=0 error from Error 26 to Error 27 (src/eval.rs:287)

SHOULD IMPROVE:

  • Clarify trunc_div comment about RoundingMode::Down (src/eval.rs:458)

OPTIONAL:

  • Add comment explaining ITERATE in DO UNTIL behavior (it's non-obvious per ANSI spec)
  • Consider tests for deeper nesting (10+ levels) to ensure no stack issues

🎯 VERDICT

Approve with required fix - This is high-quality work that demonstrates sophisticated understanding of REXX's parsing challenges and control flow semantics. The BY=0 error number is a trivial fix. Otherwise, the implementation is excellent.

After fixing the BY=0 error number, this PR is ready to merge.

## Code Review - PR #3: Phase 2 Control Flow Implementation Reviewed the REXX interpreter control flow implementation focusing on correctness, REXX semantics compliance (ANSI X3.274-1996), arithmetic handling, and error propagation. ### Summary **Overall assessment:** Excellent implementation with strong attention to REXX semantics. One critical error number fix required, otherwise ready to merge. --- ## 🔴 CRITICAL ISSUE ### 1. Incorrect Error Number for BY=0 **Location:** `src/eval.rs:286-288` **Issue:** Zero BY value raises Error 26 (InvalidWholeNumber) but should raise Error 27 (InvalidDoSyntax). **Reasoning:** - Error 26 = "Invalid whole number" (for non-integer values) - Error 27 = "Invalid DO syntax" (for semantic constraint violations) - Zero IS a valid whole number, just not a valid BY value **Current code:** ```rust if by_num.is_zero() { return Err(RexxDiagnostic::new(RexxError::InvalidWholeNumber) .with_detail("BY value in DO loop must not be zero")); } ``` **Should be:** ```rust if by_num.is_zero() { return Err(RexxDiagnostic::new(RexxError::InvalidDoSyntax) .with_detail("BY value in DO loop must not be zero")); } ``` **Test verification:** The test `zero_by_step_error` in `tests/phase2.rs:1672` currently checks for "BY value" substring but should verify Error 27 specifically. --- ## 🟡 MINOR ISSUES ### 2. Comment Inaccuracy in trunc_div **Location:** `src/eval.rs:458-461` **Issue:** Comment says "RoundingMode::Down truncates toward zero" but `RoundingMode::Down` actually rounds toward negative infinity (floor). The code works correctly because `with_scale_round(0, RoundingMode::Down)` gives the right behavior for integer division, but the comment could mislead future maintainers. **Current comment:** ```rust // RoundingMode::Down truncates toward zero (not toward negative infinity). ``` **More accurate:** ```rust // with_scale_round(0, RoundingMode::Down) truncates decimal places, // which for REXX integer division gives the correct truncate-toward-zero behavior. ``` --- ## ✅ CORRECTNESS VALIDATION Verified all critical REXX semantics: ### Control Flow ✓ - **IF/THEN/ELSE parsing:** Correctly suppresses THEN/ELSE in implicit concatenation (lines 1100-1109) - **DO variants:** All variants (simple, counted, FOREVER, WHILE, UNTIL, controlled) implemented correctly - **SELECT:** Correctly errors (Error 7) when no WHEN matches and no OTHERWISE ### Loop Semantics ✓ - **Controlled loop final value (TO):** Correctly one-past-limit (`src/eval.rs:303-378`) - `do i = 1 to 5; end; say i` → `6` ✓ - **Controlled loop final value (UNTIL):** Correctly does NOT increment after UNTIL satisfied (lines 361-366) - `do i = 1 to 10 by 2 until i > 5; end; say i` → `7` (not 9) ✓ - **ITERATE in DO UNTIL:** Correctly goes to UNTIL check, not top of loop (lines 243-248) - This is a subtle ANSI requirement, correctly implemented - **Named ITERATE/LEAVE:** Correct signal matching (lines 405-410) ### Keyword Suppression ✓ - **DO header keywords (TO/BY/FOR/WHILE/UNTIL):** Correctly suppressed in implicit concat via `do_header_depth` guard - **THEN in conditions:** Correctly suppressed via `condition_depth` guard - **ELSE in IF clauses:** Correctly suppressed via `if_depth` guard - Keywords still work as variables outside their control flow context ✓ ### Arithmetic ✓ - **Division by zero:** Correctly raises Error 42 ✓ - **Exponent limits:** Correctly enforces 1,000,000 limit to prevent OOM (`src/eval.rs:438-443`) ✓ - **Zero to negative power:** Correctly errors ✓ - **Integer division with negatives:** Correctly truncates toward zero ✓ ### Error Handling ✓ All error numbers verified correct (except BY=0 issue above): - Error 7: No WHEN match in SELECT - Error 8: Unexpected THEN/ELSE - Error 10: Unexpected END - Error 14: Incomplete blocks - Error 18: THEN expected - Error 26: Invalid whole number (fractional/negative loop counts) - Error 27: Duplicate TO/BY/FOR keywords - Error 28: LEAVE/ITERATE outside loop - Error 42: Arithmetic overflow ### Safety ✓ - No panics on malformed input - Exponent limit prevents OOM attacks - Deep nesting handled correctly - Unterminated blocks properly detected --- ## 💪 STRENGTHS 1. **Excellent REXX semantics compliance** - Shows deep understanding of ANSI X3.274-1996 2. **Correct controlled loop behavior** - The subtle one-past-limit vs. current-value distinction is implemented correctly 3. **Proper ITERATE in DO UNTIL** - Goes to UNTIL check, not loop top (non-obvious detail handled correctly) 4. **Robust error handling** - Comprehensive validation with mostly correct error numbers 5. **Clean architecture** - Good separation between parser context guards and evaluator 6. **Comprehensive tests** - 36 tests in phase2.rs covering edge cases --- ## 📋 RECOMMENDATIONS **MUST FIX:** - [ ] Change BY=0 error from Error 26 to Error 27 (`src/eval.rs:287`) **SHOULD IMPROVE:** - [ ] Clarify trunc_div comment about RoundingMode::Down (`src/eval.rs:458`) **OPTIONAL:** - [ ] Add comment explaining ITERATE in DO UNTIL behavior (it's non-obvious per ANSI spec) - [ ] Consider tests for deeper nesting (10+ levels) to ensure no stack issues --- ## 🎯 VERDICT **Approve with required fix** - This is high-quality work that demonstrates sophisticated understanding of REXX's parsing challenges and control flow semantics. The BY=0 error number is a trivial fix. Otherwise, the implementation is excellent. **After fixing the BY=0 error number, this PR is ready to merge.**
Sign in to join this conversation.
No description provided.