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>
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:
- Condition LoopBodyLocal: Used in loop conditions (header/break/continue) → Needs Trim promotion
- 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_posis detected as LoopBodyLocal (defined in body)- Pattern2 tries to apply Trim carrier promotion
- Error: Not a Trim pattern (
indexOfvssubstring)
Desired behavior:
digit_posused in condition → Should attempt Trim promotion (and fail gracefully)- But if
digit_poswere 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 (ch → is_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:
- Filter LoopBodyLocal to only process condition LoopBodyLocal
- 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
-
Add condition detection helper (10 lines)
TrimLoopLowerer::is_used_in_condition(var_name, cond_node)- Simple AST traversal to check if variable appears
-
Update Trim detection (20 lines)
- Filter LoopBodyLocal to condition-only
- Skip body-only LoopBodyLocal
-
Add unit test (50 lines)
test_body_only_loopbodylocal_allowed- Loop with
local tempnever used in condition - Should NOT trigger Trim promotion
Task 183-3: Integration Tests
Create 3 test files demonstrating the fix:
-
phase183_p2_parse_number.hako - _parse_number pattern
digit_posin break condition → Should reject (not Trim)- Clear error message: "LoopBodyLocal in condition, but not Trim pattern"
-
phase183_p2_atoi.hako - _atoi pattern
- Similar to parse_number
- Multiple break conditions
-
phase183_p1_match_literal.hako - _match_literal pattern
- No LoopBodyLocal → Should work (baseline)
Validation Strategy
Success Criteria
- Body-only LoopBodyLocal: Loops with body-only locals compile successfully
- Condition LoopBodyLocal:
- Trim patterns → Promoted correctly
- Non-Trim patterns → Rejected with clear error
- 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:
-
Generic LoopBodyLocal promotion
- Pattern:
local x = expr; if (x op literal) break - Promotion: Evaluate
exprinline, no carrier needed - Alternative: Allow inline computation in JoinIR conditions
- Pattern:
-
String concatenation support (Phase 178 blocker)
num_str = num_str + chcurrently 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/_atoiwill 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