Files
hakorune/docs/development/current/main/phase183-loopbodylocal-role-separation.md
nyash-codex a3df5ecc7a feat(joinir): Phase 183 LoopBodyLocal role separation
Implements role-based separation of LoopBodyLocal variables to prevent
inappropriate Trim promotion for body-only local variables.

## Changes

### Task 183-1: Design Documentation
- Created `phase183-loopbodylocal-role-separation.md` with role taxonomy:
  - Condition LoopBodyLocal: Used in loop conditions → Trim promotion target
  - Body-only LoopBodyLocal: Only in body → No promotion needed
- Documented architectural approach and implementation strategy

### Task 183-2: Implementation
- Added `TrimLoopLowerer::is_var_used_in_condition()` helper
  - Recursively checks if variable appears in condition AST
  - Handles BinaryOp, UnaryOp, MethodCall node types
- Updated `try_lower_trim_like_loop()` to filter condition LoopBodyLocal
  - Only processes LoopBodyLocal that appear in break conditions
  - Skips body-only LoopBodyLocal (returns Ok(None) early)
- Added 5 unit tests for variable detection logic

### Task 183-3: Test Files
- Created `phase183_body_only_loopbodylocal.hako`
  - Demonstrates body-only LoopBodyLocal (`temp`) not triggering Trim
  - Verified trace output: "No LoopBodyLocal detected, skipping Trim lowering"
- Created additional test files (phase183_p1_match_literal, phase183_p2_atoi, phase183_p2_parse_number)

### Task 183-4: Documentation Updates
- Updated `joinir-architecture-overview.md` with Phase 183 results
- Updated `CURRENT_TASK.md` with Phase 183 completion status

## Results

 LoopBodyLocal role separation complete
 Body-only LoopBodyLocal skips Trim promotion
 5 unit tests passing
 Trace verification successful

## Next Steps (Phase 184+)

- Body-local variable MIR lowering support
- String concatenation filter relaxation
- Full _parse_number/_atoi implementation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-08 23:14:10 +09:00

7.4 KiB

Phase 183: LoopBodyLocal Role Separation Design

Overview

Phase 182 discovered Blocker 1: LoopBodyLocal variables are currently always routed to Trim-specific carrier promotion logic, which is inappropriate for JsonParser integer loops where these variables are simple local computations.

This phase separates LoopBodyLocal variables into two categories based on their usage pattern:

  1. Condition LoopBodyLocal: Used in loop conditions (header/break/continue) → Needs Trim promotion
  2. Body-only LoopBodyLocal: Only used in loop body, never in conditions → No promotion needed

Problem Statement

Current Behavior (Phase 182 Blockers)

// Example: _parse_number
loop(p < s.length()) {
    local digit_pos = "0123456789".indexOf(ch)  // LoopBodyLocal: digit_pos
    if (digit_pos < 0) {
        break  // digit_pos used in BREAK condition
    }
    num_str = num_str + ch
    p = p + 1
}

Current routing:

  • digit_pos is detected as LoopBodyLocal (defined in body)
  • Pattern2 tries to apply Trim carrier promotion
  • Error: Not a Trim pattern (indexOf vs substring)

Desired behavior:

  • digit_pos used in condition → Should attempt Trim promotion (and fail gracefully)
  • But if digit_pos were only in body → Should be allowed as pure local variable

Use Cases

Case A: Condition LoopBodyLocal (Trim Pattern)

// _trim_leading_whitespace
loop(pos < s.length()) {
    local ch = s.substring(pos, pos + 1)  // LoopBodyLocal: ch
    if (ch == " " || ch == "\t") {        // ch in BREAK condition
        pos = pos + 1
    } else {
        break
    }
}

Routing: Needs Trim promotion (chis_whitespace carrier)

Case B: Body-only LoopBodyLocal (Pure Local)

// Hypothetical simple loop
loop(i < n) {
    local temp = i * 2      // LoopBodyLocal: temp (not in any condition!)
    result = result + temp
    i = i + 1
}

Routing: No promotion needed (temp never used in conditions)

Case C: Condition LoopBodyLocal (Non-Trim)

// _parse_number
loop(p < s.length()) {
    local digit_pos = "0123456789".indexOf(ch)  // LoopBodyLocal: digit_pos
    if (digit_pos < 0) {                         // digit_pos in BREAK condition
        break
    }
    p = p + 1
}

Current: Tries Trim promotion → Fails Desired: Recognize non-Trim pattern → Block with clear error message

Design Solution

Architecture: Two-Stage Check

LoopConditionScopeBox
    ↓
has_loop_body_local() ?
    ↓ YES
Check: Where is LoopBodyLocal used?
    ├─ In CONDITION (header/break/continue) → Try Trim promotion
    │   ├─ Success → Pattern2/4 with Trim carrier
    │   └─ Fail → Reject loop (not supported yet)
    └─ Body-only (NOT in any condition) → Allow as pure local

Implementation Strategy

Step 1: Extend LoopConditionScope Analysis

Add is_in_condition() check to differentiate:

  • Condition LoopBodyLocal: Used in header/break/continue conditions
  • Body-only LoopBodyLocal: Only in body assignments/expressions
impl LoopConditionScope {
    /// Check if a LoopBodyLocal is used in any condition
    pub fn is_body_local_in_condition(&self, var_name: &str) -> bool {
        // Implementation: Check if var_name appears in condition_nodes
    }
}

Step 2: Update TrimLoopLowerer

Modify try_lower_trim_like_loop() to:

  1. Filter LoopBodyLocal to only process condition LoopBodyLocal
  2. Skip body-only LoopBodyLocal (let Pattern1/2 handle naturally)
impl TrimLoopLowerer {
    pub fn try_lower_trim_like_loop(...) -> Result<Option<TrimLoweringResult>, String> {
        // Extract condition LoopBodyLocal only
        let cond_body_locals: Vec<_> = cond_scope.vars.iter()
            .filter(|v| v.scope == CondVarScope::LoopBodyLocal)
            .filter(|v| Self::is_used_in_condition(v.name, break_cond))
            .collect();

        if cond_body_locals.is_empty() {
            // No condition LoopBodyLocal → Not a Trim pattern
            return Ok(None);
        }

        // Try promotion for condition LoopBodyLocal
        // ...
    }
}

Step 3: Update Pattern2 can_lower

Ensure Pattern2 accepts loops with body-only LoopBodyLocal:

pub fn can_lower(builder: &MirBuilder, ctx: &LoopPatternContext) -> bool {
    // Existing checks...

    // NEW: Allow body-only LoopBodyLocal
    let cond_scope = &ctx.preprocessing.cond_scope;
    if cond_scope.has_loop_body_local() {
        // Check if all LoopBodyLocal are body-only (not in conditions)
        let all_body_only = cond_scope.vars.iter()
            .filter(|v| v.scope == CondVarScope::LoopBodyLocal)
            .all(|v| !is_in_any_condition(v.name, ctx));

        if !all_body_only {
            // Some LoopBodyLocal in conditions → Must be Trim pattern
            // Trim lowering will handle this
        }
    }

    true
}

Implementation Plan

Task 183-2: Core Implementation

  1. Add condition detection helper (10 lines)

    • TrimLoopLowerer::is_used_in_condition(var_name, cond_node)
    • Simple AST traversal to check if variable appears
  2. Update Trim detection (20 lines)

    • Filter LoopBodyLocal to condition-only
    • Skip body-only LoopBodyLocal
  3. Add unit test (50 lines)

    • test_body_only_loopbodylocal_allowed
    • Loop with local temp never used in condition
    • Should NOT trigger Trim promotion

Task 183-3: Integration Tests

Create 3 test files demonstrating the fix:

  1. phase183_p2_parse_number.hako - _parse_number pattern

    • digit_pos in break condition → Should reject (not Trim)
    • Clear error message: "LoopBodyLocal in condition, but not Trim pattern"
  2. phase183_p2_atoi.hako - _atoi pattern

    • Similar to parse_number
    • Multiple break conditions
  3. phase183_p1_match_literal.hako - _match_literal pattern

    • No LoopBodyLocal → Should work (baseline)

Validation Strategy

Success Criteria

  1. Body-only LoopBodyLocal: Loops with body-only locals compile successfully
  2. Condition LoopBodyLocal:
    • Trim patterns → Promoted correctly
    • Non-Trim patterns → Rejected with clear error
  3. No regression: Existing Trim tests still pass

Test Commands

# Structure trace (verify no freeze)
NYASH_JOINIR_STRUCTURE_ONLY=1 ./target/release/hakorune apps/tests/phase183_p2_parse_number.hako

# Execution test (once promotion logic is ready)
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase183_p1_match_literal.hako

Future Work (Out of Scope)

Phase 184+: Non-Trim LoopBodyLocal Patterns

To support _parse_number and _atoi fully, we need:

  1. Generic LoopBodyLocal promotion

    • Pattern: local x = expr; if (x op literal) break
    • Promotion: Evaluate expr inline, no carrier needed
    • Alternative: Allow inline computation in JoinIR conditions
  2. String concatenation support (Phase 178 blocker)

    • num_str = num_str + ch currently rejected
    • Need string carrier update support in Pattern2/4

Decision for Phase 183:

  • Focus on architectural separation (condition vs body-only)
  • Accept that _parse_number/_atoi will still fail (but with better error)
  • Unblock body-only LoopBodyLocal use cases

References

  • Phase 182: JsonParser P1/P2 pattern validation (discovered blockers)
  • Phase 181: JsonParser loop inventory
  • Phase 171-C: LoopBodyCarrierPromoter original design
  • Phase 170-D: LoopConditionScopeBox implementation