Files
hakorune/docs/development/current/main/phase172-trim-lowering-impl.md
nyash-codex 5ba11468e4 feat(joinir): Phase 172 - Trim pattern JoinIR lowering implementation
Implement actual MIR generation for Trim pattern loops, enabling
LoopBodyLocal variables to work through bool carrier promotion.

## Implementation (pattern2_with_break.rs)
- emit_whitespace_check(): Generate OR chain for whitespace comparison
- extract_substring_args(): Extract s, start from substring call
- Initial carrier generation: ch0 = s.substring(start, start+1)
- Whitespace check: is_ch_match0 = (ch0 == " " || "\t" || "\n" || "\r")
- ConditionEnv integration: Register carrier for JoinIR condition
- Break condition replacement: !is_ch_match instead of original ch checks

## Architecture
- Host MIR: substring calls, OR chain evaluation, BoxCall
- JoinIR: Only sees bool carrier for break control
- No new JoinIR instructions added

## Documentation
- phase172-trim-lowering-impl.md: Design and implementation details
- loop_pattern_space.md: Analysis of all loop pattern combinations

Test: Trim loops compile and generate JoinIR successfully
Build: 0 errors, clean compilation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 03:06:20 +09:00

10 KiB

Phase 172: Trim Pattern JoinIR Lowering Implementation

Objective

Implement actual JoinIR → MIR lowering for Trim pattern loops, making test_trim_main_pattern.hako work end-to-end.

Target Loop

File: local_tests/test_trim_main_pattern.hako

Leading whitespace trim loop:

loop(start < end) {
  local ch = s.substring(start, start+1)
  if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
    start = start + 1
  } else {
    break
  }
}

Using Boxes from Phase 171

Phase 171 Infrastructure

  • LoopConditionScopeBox: Detects ch as LoopBodyLocal
  • LoopBodyCarrierPromoter: Promotes ch to bool carrier is_ch_match
  • TrimPatternInfo: Contains promotion metadata
  • TrimLoopHelper: Attached to CarrierInfo for lowering guidance
  • Pattern2 validation: Currently returns informative error at line 234-239

Design Philosophy

Responsibility Separation

Host MIR Side (before/after JoinIR):

  • Execute substring() method calls
  • Evaluate OR chain comparisons (ch == " " || ch == "\t" || ...)
  • Generate bool result for carrier
  • BoxCall operations

JoinIR Side (pure control flow):

  • bool carrier-based loop control
  • break condition: !is_ch_match
  • No knowledge of substring/OR chain
  • No new JoinIR instructions

Key Insight

The Trim pattern transformation:

Original:
  local ch = s.substring(start, start+1)
  if (ch == " " || ch == "\t" || ...) { start++ } else { break }

Transformed:
  is_ch_match = (s.substring(start, start+1) == " " || ...)  # Host MIR
  if (!is_ch_match) { break }                                  # JoinIR

Implementation Steps

Step 1: Loop Pre-Initialization (Task 172-2)

Location: pattern2_with_break.rs, before JoinIR generation

Goal: Initialize bool carrier before entering loop

Implementation:

// After Line 264 (after carrier_info.merge_from)
if let Some(helper) = carrier_info.trim_helper() {
    if helper.is_safe_trim() {
        // Generate initial whitespace check
        // ch0 = s.substring(start, start+1)
        let ch0 = self.emit_method_call("substring", s_id, vec![start_id, start_plus_1_id])?;

        // is_ch_match0 = (ch0 == " " || ch0 == "\t" || ...)
        let is_ch_match0 = emit_whitespace_check(self, ch0, &helper.whitespace_chars)?;

        // Update carrier_info initial value
        // (Will be used in JoinIR input mapping)
    }
}

Helper Function (new):

fn emit_whitespace_check(
    builder: &mut MirBuilder,
    ch_value: ValueId,
    whitespace_chars: &[String],
) -> Result<ValueId, String> {
    // Build OR chain: ch == " " || ch == "\t" || ...
    let mut result = None;
    for ws_char in whitespace_chars {
        let ws_const = emit_string(builder, ws_char.clone());
        let eq_check = emit_eq_to(builder, ch_value, ws_const)?;

        result = Some(if let Some(prev) = result {
            // prev || eq_check
            emit_binary_op(builder, BinaryOp::Or, prev, eq_check)?
        } else {
            eq_check
        });
    }
    result.ok_or("Empty whitespace_chars".to_string())
}

Step 2: Loop Body Update Logic (Task 172-3)

Location: Pattern2 lowerer's latch generation (after start = start + 1)

Goal: Update bool carrier for next iteration

Implementation:

// In latch block generation (conceptually after line 267+)
if let Some(helper) = carrier_info.trim_helper() {
    if helper.is_safe_trim() {
        // ch_next = s.substring(start_next, start_next+1)
        let ch_next = self.emit_method_call("substring", s_id, vec![start_next, start_next_plus_1])?;

        // is_ch_match_next = (ch_next == " " || ...)
        let is_ch_match_next = emit_whitespace_check(self, ch_next, &helper.whitespace_chars)?;

        // Store for JoinIR latch → header PHI
        // (Will be used in carrier update mapping)
    }
}

Note: The actual MIR emission happens in host space, not JoinIR space. The JoinIR only sees the bool carrier as a loop parameter.

Step 3: JoinIR break Condition Replacement (Task 172-4)

Location: lower_loop_with_break_minimal call at line 267

Goal: Replace break condition with !is_ch_match

Current Code:

let (join_module, fragment_meta) = match lower_loop_with_break_minimal(
    scope,
    condition,        // loop condition: start < end
    &break_condition_node,  // if condition: ch == " " || ...
    &env,
    &loop_var_name
) { ... }

Modified Approach:

// Phase 172: Trim pattern special route
if let Some(helper) = carrier_info.trim_helper() {
    if helper.is_safe_trim() {
        // Create negated carrier check: !is_ch_match
        let carrier_var_node = ASTNode::Variable {
            name: helper.carrier_name.clone(), // "is_ch_match"
            span: Span::unknown(),
        };

        let negated_carrier_check = ASTNode::UnaryOp {
            operator: UnaryOperator::Not,
            operand: Box::new(carrier_var_node),
            span: Span::unknown(),
        };

        // Use negated carrier as break condition
        let (join_module, fragment_meta) = match lower_loop_with_break_minimal(
            scope,
            condition,  // start < end (unchanged)
            &negated_carrier_check,  // !is_ch_match (REPLACED)
            &env,
            &loop_var_name
        ) { ... }

        // Continue with normal merge flow...
    }
}

Critical Design Decision:

  • The break condition AST is replaced, but the loop condition (start < end) remains unchanged
  • JoinIR sees bool carrier as just another loop parameter
  • No new JoinIR instructions needed!

Step 4: Carrier Mapping Integration

Location: Boundary setup (line 283-291)

Goal: Ensure bool carrier is properly mapped between host and JoinIR

Current boundary setup:

let mut boundary = JoinInlineBoundary::new_inputs_only(
    vec![ValueId(0)],  // JoinIR: loop param
    vec![loop_var_id], // Host: "start"
);
boundary.condition_bindings = condition_bindings;
boundary.exit_bindings = exit_bindings.clone();

Enhanced with Trim carrier:

// Phase 172: Add bool carrier to condition_bindings
if let Some(helper) = carrier_info.trim_helper() {
    let carrier_host_id = self.variable_map.get(&helper.carrier_name)
        .copied()
        .ok_or_else(|| format!("Carrier '{}' not in variable_map", helper.carrier_name))?;

    let carrier_join_id = alloc_join_value(); // Allocate JoinIR-local ID

    env.insert(helper.carrier_name.clone(), carrier_join_id);
    condition_bindings.push(ConditionBinding {
        name: helper.carrier_name.clone(),
        host_value: carrier_host_id,
        join_value: carrier_join_id,
    });
}

Implementation Constraints

What We DON'T Change

  1. ExitLine Architecture: No changes to ExitLineReconnector/ExitMetaCollector
  2. Header PHI Generation: Existing LoopHeaderPhiBuilder handles bool carrier automatically
  3. JoinIR Instruction Set: No new instructions added
  4. Pattern2 Basic Logic: Core control flow unchanged

What We DO Change

  1. Pre-loop initialization: Add bool carrier setup
  2. Latch update logic: Add bool carrier update
  3. Break condition AST: Replace with !is_ch_match
  4. Condition bindings: Add bool carrier mapping

Testing Strategy

E2E Test

Command:

./target/release/hakorune local_tests/test_trim_main_pattern.hako

Expected Behavior:

  • Before: Error message " Trim pattern validation successful! ... JoinIR lowering: TODO"
  • After: Result: [hello] and PASS: Trimmed correctly

Debug Traces

With JoinIR debug:

NYASH_JOINIR_DEBUG=1 ./target/release/hakorune local_tests/test_trim_main_pattern.hako 2>&1 | grep -E "\[pattern2\]|\[trim\]"

Expected log patterns:

[pattern2/promotion] LoopBodyLocal detected in condition scope
[pattern2/promoter] LoopBodyLocal 'ch' promoted to carrier 'is_ch_match'
[pattern2/trim] Safe Trim pattern detected, bypassing LoopBodyLocal restriction
[pattern2/trim] Carrier: 'is_ch_match', original var: 'ch', whitespace chars: [" ", "\t", "\n", "\r"]
[pattern2/trim] Emitting whitespace check initialization
[pattern2/trim] Replacing break condition with !is_ch_match

MIR Validation

Dump MIR:

./target/release/hakorune --dump-mir local_tests/test_trim_main_pattern.hako 2>&1 | less

Check for:

  • Pre-loop: substring() call + OR chain comparison
  • Loop header: PHI for is_ch_match
  • Loop body: Conditional branch on !is_ch_match
  • Loop latch: substring() call + OR chain update
  • Exit block: Proper carrier value propagation

Success Criteria

  1. test_trim_main_pattern.hako executes without errors
  2. Output: Result: [hello] (correctly trimmed)
  3. Test result: PASS: Trimmed correctly
  4. cargo build --release succeeds with 0 errors
  5. cargo test all tests pass
  6. No new warnings introduced
  7. MIR dump shows proper SSA form

Implementation Status

  • Task 172-1: Design document (THIS FILE)
  • Task 172-2: Loop pre-initialization
  • Task 172-3: Loop body update logic
  • Task 172-4: JoinIR break condition replacement
  • Task 172-5: E2E test validation
  • Task 172-6: Documentation update

Future Work (Out of Scope for Phase 172)

Pattern 4 Support

  • Apply same Trim lowering to Pattern 4 (loop with continue)
  • Reuse emit_whitespace_check() helper

JsonParser Integration

  • Apply to all trim loops in JsonParser._trim
  • Validate with JSON parsing smoke tests

P6+ Patterns

  • Complex control flow with Trim-like patterns
  • Multi-carrier bool promotion

Notes

Why This Design Works

  1. Minimal Invasiveness: Changes only affect Trim-specific paths
  2. Reusability: emit_whitespace_check() can be shared with Pattern 4
  3. Maintainability: Clear separation between host MIR and JoinIR concerns
  4. Testability: Each step can be validated independently

Phase 171 Validation Hook

Current code (line 234-239) already validates Trim pattern and returns early:

return Err(format!(
    "[cf_loop/pattern2] ✅ Trim pattern validation successful! \
     Carrier '{}' ready for Phase 172 implementation. \
     (Pattern detection: PASS, Safety check: PASS, JoinIR lowering: TODO)",
    helper.carrier_name
));

Phase 172 will replace this early return with actual lowering logic.

References

  • Phase 171-C: LoopBodyCarrierPromoter implementation
  • Phase 171-C-5: TrimLoopHelper design
  • Pattern2 Lowerer: src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs
  • Test File: local_tests/test_trim_main_pattern.hako