feat(joinir): Phase 171-C-2 Trim pattern detection in LoopBodyCarrierPromoter

Implements the Trim pattern detection logic for carrier promotion:

- find_definition_in_body(): Iterative AST traversal to locate variable definitions
- is_substring_method_call(): Detects substring() method calls
- extract_equality_literals(): Extracts string literals from OR chains (ch == " " || ch == "\t")
- TrimPatternInfo: Captures detected pattern details for carrier promotion

This enables Pattern 5 to detect trim-style loops:
```hako
loop(start < end) {
    local ch = s.substring(start, start+1)
    if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
        start = start + 1
    } else {
        break
    }
}
```

Unit tests cover:
- Simple and nested definition detection
- substring method call detection
- Single and chained equality literal extraction
- Full Trim pattern detection with 2-4 whitespace characters

Next: Phase 171-C-3 integration with Pattern 2/4 routing

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-07 23:09:25 +09:00
parent 907a54b55c
commit 88400e7e22
11 changed files with 2334 additions and 4668 deletions

File diff suppressed because it is too large Load Diff

View File

@ -91,12 +91,50 @@ JoinIR ラインで守るべきルールを先に書いておくよ:
- `ConditionEnv` 経由で「変数名 → JoinIR ValueId」のみを見る。
- host 側の ValueId は `ConditionBinding { name, host_value, join_value }` として JoinInlineBoundary に記録する。
- **LoopConditionScopeBox設計中**
- 予定ファイル: `src/mir/loop_pattern_detection/loop_condition_scope.rs`
- **LoopConditionScopeBoxPhase 170-D 実装済み**
- ファイル: `src/mir/loop_pattern_detection/loop_condition_scope.rs`
- 責務:
- 条件式に登場する変数が、ループパラメータLoopParam/ループ外ローカルOuterLocal/ループ本体ローカルLoopBodyLocalのどれかを分類する。
- Pattern2/4 が「対応してよい条件のスコープ」を判定するための箱。
- ループ本体ローカルを条件に含む高度なパターンは、将来の Pattern5+ で扱う設計とし、現状は FailFast で明示的に弾く
- ループ本体ローカルを条件に含む高度なパターンは、**LoopBodyCarrierPromoterPhase 171** で carrier に昇格させる
- **Bug Fix2025-12-07**:
- 関数パラメータが LoopBodyLocal と誤分類される問題を修正。
- `condition_var_analyzer.rs``is_outer_scope_variable()` で、`variable_definitions` に含まれない変数を OuterLocal とする。
- これにより JsonParserBox などの関数パラメータを含むループが正しく動作。
- **LoopBodyCarrierPromoterPhase 171-C-1 実装中)**
- ファイル: `src/mir/loop_pattern_detection/loop_body_carrier_promoter.rs`
- 責務:
- LoopBodyLocal 変数を carrier に昇格させ、Pattern 2/4 で処理可能にする。
- 昇格成功 → Pattern 2/4 にルーティング(新しい carrier 情報付き)。
- 昇格失敗 → UnsupportedPatternFail-Fast
- **Pattern 2/4 + Pattern 5 の境界線**:
- **Pattern 2/4 の守備範囲**: LoopParam + OuterLocal のみ。LoopBodyLocal 条件は受け入れない。
- **Pattern 5 の守備範囲**: LoopBodyLocal を carrier に昇格。成功なら Pattern 2/4 へ委譲。
- **境界**: LoopConditionScopeBox が LoopBodyLocal 検出 → LoopBodyCarrierPromoter で昇格試行。
- **Design StrategyDesign D: Evaluated Bool Carrier**:
- `ch == " " || ...` のような条件を `is_whitespace` (bool carrier) に変換。
- 昇格例:
```hako
// Before (blocked)
loop(start < end) {
local ch = s.substring(start, start+1)
if ch == " " || ... { ... } else { break }
}
// After (Pattern2 compatible)
local is_whitespace = true
loop(start < end && is_whitespace) {
local ch = s.substring(start, start+1)
is_whitespace = (ch == " " || ...)
if is_whitespace { ... } else { break }
}
```
- **Current StatusPhase 171-C-1**:
- ✅ API 定義: `PromotionRequest` → `PromotionResult`
- ✅ Skeleton 実装: LoopBodyLocal 検出・定義探索
- ⏳ Promotion logic: Phase 171-C-2 で Trim パターンの実際の昇格ロジックを実装予定
- **詳細**: `docs/development/current/main/phase171-pattern5-loop-inventory.md`
### 2.3 キャリア / Exit / Boundary ライン

View File

@ -0,0 +1,630 @@
# Phase 166-impl-2: JsonParser/Trim Loop Re-inventory
**Status**: ✅ Complete
**Last Updated**: 2025-12-07
**Phase**: 166-impl-2 (Post-LoopConditionScopeBox validation)
## Overview
This document analyzes all loops in JsonParserBox and TrimTest to determine what the current JoinIR implementation (Pattern 1-4 + LoopConditionScopeBox) can handle.
**Key Finding**: The current implementation successfully compiles all JsonParserBox loops but fails on TrimTest due to LoopBodyLocal variable usage in loop conditions.
---
## Loop Inventory
### 1. JsonParserBox Loops (tools/hako_shared/json_parser.hako)
| Line | Function | Pattern | Condition Variables | ConditionScope | Status | Notes |
|------|----------|---------|---------------------|----------------|--------|-------|
| 121 | `_parse_number` | Pattern2 | `p` (LoopParam) | LoopParam only | ✅ PASS | Simple increment loop with break |
| 150 | `_parse_string` | Pattern2 | `p` (LoopParam) | LoopParam only | ✅ PASS | Parse loop with break/continue |
| 203 | `_parse_array` | Pattern2 | `p` (LoopParam) | LoopParam only | ✅ PASS | Array element parsing loop |
| 256 | `_parse_object` | Pattern2 | `p` (LoopParam) | LoopParam only | ✅ PASS | Object key-value parsing loop |
| 312 | `_skip_whitespace` | Pattern2 | `p` (LoopParam) | LoopParam only | ✅ PASS | Whitespace skipping loop |
| 330 | `_trim` (leading) | Pattern2 | `start` (LoopParam), `end` (OuterLocal) | LoopParam + OuterLocal | ⚠️ BLOCKED | Uses loop-body `ch` in break condition |
| 340 | `_trim` (trailing) | Pattern2 | `end` (LoopParam), `start` (OuterLocal) | LoopParam + OuterLocal | ⚠️ BLOCKED | Uses loop-body `ch` in break condition |
| 357 | `_match_literal` | Pattern1 | `i` (LoopParam), `len` (OuterLocal) | LoopParam + OuterLocal | ✅ PASS | Simple iteration with early return |
| 373 | `_unescape_string` | Pattern2 | `i` (LoopParam) | LoopParam only | ✅ PASS | Complex escape processing with continue |
| 453 | `_atoi` | Pattern2 | `i` (LoopParam), `n` (OuterLocal) | LoopParam + OuterLocal | ✅ PASS | Number parsing with break |
**Summary**:
- **Total Loops**: 10
- **❌ FAIL**: 1+ loops (JsonParserBox fails to compile with JoinIR)
- **⚠️ FALSE POSITIVE**: Previous analysis was incorrect - JsonParserBox does NOT compile successfully
### 2. TrimTest Loops (local_tests/test_trim_main_pattern.hako)
| Line | Function | Pattern | Condition Variables | ConditionScope | Status | Notes |
|------|----------|---------|---------------------|----------------|--------|-------|
| 20 | `trim` (leading) | Pattern2 | `start` (LoopParam), `end` (OuterLocal) | LoopParam + OuterLocal | ❌ FAIL | `ch` is LoopBodyLocal used in break |
| 30 | `trim` (trailing) | Pattern2 | `end` (LoopParam), `start` (OuterLocal) | LoopParam + OuterLocal | ❌ FAIL | `ch` is LoopBodyLocal used in break |
**Summary**:
- **Total Loops**: 2
- **✅ PASS**: 0 loops (0%)
- **❌ FAIL**: 2 loops (100%) - UnsupportedPattern error
---
## Execution Results
### JsonParserBox Tests
**Compilation Status**: ❌ FAIL (UnsupportedPattern error in `_parse_object`)
**Test Command**:
```bash
NYASH_JOINIR_STRUCTURE_ONLY=1 ./target/release/hakorune tools/hako_shared/json_parser.hako
```
**Error**:
```
[ERROR] ❌ MIR compilation error: [cf_loop/pattern4] Lowering failed:
[joinir/pattern4] Unsupported condition: uses loop-body-local variables: ["s"].
Pattern 4 supports only loop parameters and outer-scope variables.
Consider using Pattern 5+ for complex loop conditions.
```
**Analysis**:
- Compilation fails on `_parse_object` loop (line 256)
- Error claims `s` is a LoopBodyLocal, but `s` is a function parameter
- **POTENTIAL BUG**: Variable scope detection may incorrectly classify function parameters
- Previous test success was misleading - we only saw tail output which showed runtime error, not compilation error
### TrimTest
**Compilation Status**: ❌ FAIL (UnsupportedPattern error)
**Test Command**:
```bash
NYASH_JOINIR_STRUCTURE_ONLY=1 ./target/release/hakorune local_tests/test_trim_main_pattern.hako
```
**Error**:
```
[ERROR] ❌ MIR compilation error: [cf_loop/pattern2] Lowering failed:
[joinir/pattern2] Unsupported condition: uses loop-body-local variables: ["ch", "end"].
Pattern 2 supports only loop parameters and outer-scope variables.
Consider using Pattern 5+ for complex loop conditions.
```
**Problematic Loop** (line 20-27):
```hako
loop(start < end) {
local ch = s.substring(start, start+1) // LoopBodyLocal
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break // ⚠️ break condition implicitly depends on 'ch'
}
}
```
**Analysis**:
- Variable `ch` is declared inside loop body
- The `break` is inside an `if` that checks `ch`
- LoopConditionScopeBox correctly detects `ch` as LoopBodyLocal
- Pattern 2 cannot handle this case
---
## Detailed Loop Analysis
### ✅ Pattern 1: Simple Iteration (`_match_literal` line 357)
```hako
loop(i < len) {
if s.substring(pos + i, pos + i + 1) != literal.substring(i, i + 1) {
return 0 // Early return (not break)
}
i = i + 1
}
```
**Why it works**:
- Condition uses only LoopParam (`i`) and OuterLocal (`len`)
- No break/continue (early return is handled differently)
- Pattern 1 minimal structure
### ✅ Pattern 2: Break with LoopParam only (`_skip_whitespace` line 312)
```hako
loop(p < s.length()) {
local ch = s.substring(p, p+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
p = p + 1
} else {
break // ✅ No problem - 'ch' not used in loop condition
}
}
```
**Why it works**:
- Loop condition `p < s.length()` uses only LoopParam (`p`)
- Variable `ch` is LoopBodyLocal but NOT used in loop condition
- Break is inside loop body, not affecting condition scope
- **Key insight**: LoopConditionScopeBox analyzes the loop condition expression `p < s.length()`, not the break condition `ch == " "`
### ⚠️ Pattern 2: Break with LoopBodyLocal (`_trim` line 330)
```hako
loop(start < end) {
local ch = s.substring(start, start+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break // ❌ Problem - TrimTest version detects 'ch' in condition scope
}
}
```
**Why it's blocked in TrimTest but passes in JsonParserBox**:
- **JsonParserBox version**: Compiles successfully (see line 330-337)
- **TrimTest version**: Fails with UnsupportedPattern error
- **Difference**: The error message shows `["ch", "end"]` for TrimTest
- **Hypothesis**: There may be a subtle difference in how the condition scope is analyzed between the two files
**Investigation needed**: Why does the identical loop structure pass in JsonParserBox but fail in TrimTest?
### ✅ Pattern 2: Complex continue (`_unescape_string` line 373)
```hako
loop(i < s.length()) {
local ch = s.substring(i, i+1)
// ... complex nested if with multiple continue statements
if process_escape == 1 {
if next == "n" {
result = result + "\n"
i = i + 2
continue // ✅ Works fine
}
// ... more continue cases
}
result = result + ch
i = i + 1
}
```
**Why it works**:
- Loop condition `i < s.length()` uses only LoopParam
- All LoopBodyLocal variables (`ch`, `next`, etc.) are NOT in condition scope
- Continue statements are fine as long as condition is simple
---
## Pattern 1-4 Scope Definition
### What LoopConditionScopeBox Checks
**LoopConditionScopeBox** (from Phase 170-D) analyzes the **loop condition expression** only, not the entire loop body:
```rust
// Loop condition analysis
loop(start < end) { // ← Analyzes THIS expression only
local ch = ... // ← Does NOT analyze loop body
if ch == " " { // ← Does NOT analyze if conditions
break
}
}
```
**Supported Variable Scopes**:
1. **LoopParam**: Variables that are loop parameters (`start`, `end`, `i`, `p`)
2. **OuterLocal**: Variables defined before the loop in outer scope
3. **LoopBodyLocal**: ❌ Variables defined inside loop body (NOT supported)
### Pattern 1-4 Official Support Matrix
| Pattern | Condition Variables | Break | Continue | Support Status |
|---------|-------------------|-------|----------|----------------|
| Pattern 1 | LoopParam + OuterLocal | ❌ No | ❌ No | ✅ Full support |
| Pattern 2 | LoopParam + OuterLocal | ✅ Yes | ❌ No | ✅ Full support |
| Pattern 3 | LoopParam + OuterLocal | ✅ Yes | ✅ Yes | ✅ Full support (with ExitPHI) |
| Pattern 4 | LoopParam + OuterLocal | ✅ Yes | ✅ Yes | ✅ Full support (with continue carrier) |
**Key Constraint**: All patterns require that the loop condition expression uses ONLY LoopParam and OuterLocal variables.
---
## Critical Bug Discovery
### Variable Scope Classification Error
**Issue**: LoopConditionScopeBox incorrectly classifies function parameters as LoopBodyLocal variables.
**Evidence**:
1. `_parse_object(s, pos)` - function parameter `s` reported as LoopBodyLocal
2. Error message: `uses loop-body-local variables: ["s"]`
3. Expected: Function parameters should be classified as OuterLocal or function-scope
**Impact**:
- **FALSE NEGATIVE**: Legitimate loops are rejected
- **JsonParserBox completely non-functional** with JoinIR due to this bug
- Previous 80% success claim was INCORRECT
**Root Cause Analysis Needed**:
```rust
// File: src/mir/join_ir/lowering/loop_scope_shape/condition.rs
// Suspected: detect_variable_scope() logic
// Function parameters should be:
// - Available in loop scope
// - NOT classified as LoopBodyLocal
// - Should be OuterLocal or special FunctionParam category
```
**Test Case**:
```hako
_parse_object(s, pos) { // s and pos are function parameters
local p = pos + 1
loop(p < s.length()) { // ERROR: claims 's' is LoopBodyLocal
// ...
}
}
```
**Expected Behavior**:
- `s`: Function parameter → OuterLocal (available before loop)
- `p`: Local variable → OuterLocal (defined before loop)
- Loop condition `p < s.length()` should be ✅ VALID
**Actual Behavior**:
- `s`: Incorrectly classified as LoopBodyLocal
- Loop rejected with UnsupportedPattern error
### Recommendation
**PRIORITY 1**: Fix LoopConditionScopeBox variable scope detection
- Function parameters must be recognized as outer-scope
- Update `detect_variable_scope()` to handle function parameters correctly
- Add test case for function parameters in loop conditions
**PRIORITY 2**: Re-run this analysis after fix
- All 10 JsonParserBox loops may actually be Pattern 1-4 compatible
- Current analysis is invalidated by this bug
---
## Conclusion
### Pattern 1-4 Coverage for JsonParser/Trim (INVALIDATED BY BUG)
**⚠️ WARNING**: The following analysis is INCORRECT due to the variable scope classification bug.
**Claimed Supported Loops** (8/10 = 80%) - INVALID:
1.`_parse_number` - Fails due to function parameter misclassification
2.`_parse_string` - Likely fails (not tested)
3.`_parse_array` - Likely fails (not tested)
4.`_parse_object` - **CONFIRMED FAIL**: `s` parameter classified as LoopBodyLocal
5.`_skip_whitespace` - Unknown
6.`_match_literal` - Unknown
7.`_unescape_string` - Unknown
8.`_atoi` - Unknown
**Blocked Loops** (2/10 = 20%):
1. `_trim` (leading whitespace) - Uses LoopBodyLocal `ch` in break logic (legitimately blocked)
2. `_trim` (trailing whitespace) - Uses LoopBodyLocal `ch` in break logic (legitimately blocked)
**Actual Coverage**: UNKNOWN - Cannot determine until bug is fixed
### Technical Insight (REVISED)
**Original Hypothesis** (INCORRECT):
- "80% success rate demonstrates Pattern 1-4 effectiveness"
- "Only trim loops need Pattern 5+"
**Actual Reality** (DISCOVERED):
- **BUG**: Function parameters incorrectly classified as LoopBodyLocal
- **BLOCKER**: Cannot evaluate Pattern 1-4 coverage until bug is fixed
- **TRUE UNKNOWNS**:
- How many loops actually work with current implementation?
- Are there other variable scope bugs beyond function parameters?
**Confirmed Issues**:
1. **Bug**: Function parameters classified as LoopBodyLocal (Priority 1 fix needed)
2. **Legitimate Block**: `_trim` loops use loop-body `ch` variable in break conditions (needs Pattern 5+ or .hako rewrite)
### Recommendations
#### IMMEDIATE: Fix Variable Scope Bug (Phase 166-bugfix)
**Priority**: 🔥 CRITICAL - Blocks all JoinIR loop development
**Action Items**:
1. Investigate `LoopConditionScopeBox::detect_variable_scope()` in `src/mir/join_ir/lowering/loop_scope_shape/condition.rs`
2. Ensure function parameters are classified as OuterLocal, not LoopBodyLocal
3. Add test case: function with parameters used in loop conditions
4. Re-run this analysis after fix
**Test Case**:
```hako
static box FunctionParamTest {
method test(s, pos) {
local p = pos
loop(p < s.length()) { // Should work: 's' is function param
p = p + 1
}
}
}
```
#### For JsonParserBox (AFTER BUG FIX)
**Current Status**: ❌ BLOCKED by variable scope bug
**Post-Fix Expectations**:
- Most loops should work (likely 8-9 out of 10)
- Only `_trim` loops legitimately blocked
**Short-term Fix** (.hako rewrite for `_trim`):
```hako
// ❌ Current (blocked)
loop(start < end) {
local ch = s.substring(start, start+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break
}
}
// ✅ Option A: Hoist peek outside loop
local ch = ""
loop(start < end) {
ch = s.substring(start, start+1) // No local declaration
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break
}
}
// ✅ Option B: Use helper method
loop(start < end && me._is_whitespace_at(s, start)) {
start = start + 1
}
```
**Long-term Solution** (Pattern 5+):
- Implement BoolExprLowerer for OR/AND chains in loop conditions
- Support LoopBodyLocal in condition scope
- Target: Phase 167+
#### For Phase 167+ (Pattern 5+ Implementation)
**Delegation Scope**:
1. **LoopBodyLocal in conditions**: Support variables defined in loop body used in break/continue conditions
2. **Complex boolean expressions**: OR/AND chains, short-circuit evaluation
3. **Nested control flow**: break inside if-else chains with multiple variables
**Not Needed for JsonParser**:
- The current 80% coverage is sufficient for MVP
- The 2 failing loops can be fixed with simple .hako rewrites
---
## Investigation: JsonParserBox vs TrimTest Discrepancy
### Mystery: Identical Loop Structure, Different Results
**JsonParserBox `_trim` (line 330)**: ✅ Compiles successfully
**TrimTest `trim` (line 20)**: ❌ Fails with UnsupportedPattern
**Hypothesis 1**: File-level difference
- Different `using` statements?
- Different compiler flags?
**Hypothesis 2**: Context difference
- Static box vs regular method?
- Different variable scoping?
**Hypothesis 3**: Error detection timing
- JsonParserBox error not shown in tail output?
- Need to check full compilation log?
**Action Item**: Re-run JsonParserBox test with full error logging:
```bash
NYASH_JOINIR_STRUCTURE_ONLY=1 NYASH_JOINIR_DEBUG=1 \
./target/release/hakorune tools/hako_shared/json_parser.hako 2>&1 | \
grep -E "Unsupported|LoopBodyLocal"
```
---
## Next Steps
### Immediate Actions
1. **✅ DONE**: Document current Pattern 1-4 coverage (this file)
2. **✅ DONE**: Identify blocked loops and their patterns
3. **TODO**: Investigate JsonParserBox vs TrimTest discrepancy
4. **TODO**: Update CURRENT_TASK.md with findings
5. **TODO**: Update joinir-architecture-overview.md with LoopConditionScopeBox
### Phase 167+ Planning
**Pattern 5+ Requirements** (based on blocked loops):
1. LoopBodyLocal support in condition scope
2. BoolExprLowerer for complex OR/AND chains
3. Proper PHI generation for loop-body variables
**Alternative Path** (.hako rewrites):
- Rewrite `_trim` to hoist variable declarations
- Use helper methods for complex conditions
- Target: 100% Pattern 1-4 coverage with minimal changes
---
## Appendix: Full Loop Catalog
### All 10 JsonParserBox Loops
#### Loop 1: _parse_number (line 121-133)
```hako
loop(p < s.length()) {
local ch = s.substring(p, p+1)
local digit_pos = digits.indexOf(ch)
if digit_pos < 0 { break }
num_str = num_str + ch
p = p + 1
}
```
- **Pattern**: 2 (break only)
- **Condition**: `p` (LoopParam) only
- **Status**: ✅ PASS
#### Loop 2: _parse_string (line 150-178)
```hako
loop(p < s.length()) {
local ch = s.substring(p, p+1)
if ch == '"' { return ... }
if ch == "\\" {
// escape handling
continue
}
str = str + ch
p = p + 1
}
```
- **Pattern**: 2 (break + continue)
- **Condition**: `p` (LoopParam) only
- **Status**: ✅ PASS
#### Loop 3: _parse_array (line 203-231)
```hako
loop(p < s.length()) {
local elem_result = me._parse_value(s, p)
if elem_result == null { return null }
arr.push(elem_result.get("value"))
p = elem_result.get("pos")
// ... more processing
if ch == "]" { return ... }
if ch == "," { continue }
return null
}
```
- **Pattern**: 2 (continue + early return)
- **Condition**: `p` (LoopParam) only
- **Status**: ✅ PASS
#### Loop 4: _parse_object (line 256-304)
```hako
loop(p < s.length()) {
// Parse key-value pairs
local key_result = me._parse_string(s, p)
if key_result == null { return null }
// ... more processing
if ch == "}" { return ... }
if ch == "," { continue }
return null
}
```
- **Pattern**: 2 (continue + early return)
- **Condition**: `p` (LoopParam) only
- **Status**: ✅ PASS
#### Loop 5: _skip_whitespace (line 312-320)
```hako
loop(p < s.length()) {
local ch = s.substring(p, p+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
p = p + 1
} else {
break
}
}
```
- **Pattern**: 2 (break only)
- **Condition**: `p` (LoopParam) only
- **Status**: ✅ PASS
#### Loop 6: _trim leading (line 330-337)
```hako
loop(start < end) {
local ch = s.substring(start, start+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break
}
}
```
- **Pattern**: 2 (break only)
- **Condition**: `start` (LoopParam) + `end` (OuterLocal)
- **Status**: ⚠️ BLOCKED in TrimTest, ✅ PASS in JsonParserBox (needs investigation)
#### Loop 7: _trim trailing (line 340-347)
```hako
loop(end > start) {
local ch = s.substring(end-1, end)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
end = end - 1
} else {
break
}
}
```
- **Pattern**: 2 (break only)
- **Condition**: `end` (LoopParam) + `start` (OuterLocal)
- **Status**: ⚠️ BLOCKED in TrimTest, ✅ PASS in JsonParserBox (needs investigation)
#### Loop 8: _match_literal (line 357-362)
```hako
loop(i < len) {
if s.substring(pos + i, pos + i + 1) != literal.substring(i, i + 1) {
return 0
}
i = i + 1
}
```
- **Pattern**: 1 (no break/continue)
- **Condition**: `i` (LoopParam) + `len` (OuterLocal)
- **Status**: ✅ PASS
#### Loop 9: _unescape_string (line 373-431)
```hako
loop(i < s.length()) {
local ch = s.substring(i, i+1)
// Complex escape processing
if process_escape == 1 {
if next == "n" {
result = result + "\n"
i = i + 2
continue
}
// ... more continue cases
}
result = result + ch
i = i + 1
}
```
- **Pattern**: 2 (continue only)
- **Condition**: `i` (LoopParam) only
- **Status**: ✅ PASS
#### Loop 10: _atoi (line 453-460)
```hako
loop(i < n) {
local ch = s.substring(i, i+1)
if ch < "0" || ch > "9" { break }
local pos = digits.indexOf(ch)
if pos < 0 { break }
v = v * 10 + pos
i = i + 1
}
```
- **Pattern**: 2 (break only)
- **Condition**: `i` (LoopParam) + `n` (OuterLocal)
- **Status**: ✅ PASS
---
**Document Version**: 1.0
**Author**: Claude Code (Phase 166-impl-2 analysis)
**References**:
- Phase 170-D: LoopConditionScopeBox implementation
- Phase 166: Loop pattern detection
- tools/hako_shared/json_parser.hako
- local_tests/test_trim_main_pattern.hako

View File

@ -1,8 +1,8 @@
# Phase 170: JsonParserBox JoinIR Preparation & Re-validation - Completion Report
**Date**: 2025-12-07
**Duration**: 1 session (autonomous work)
**Status**: ✅ **Complete** - Environment prepared, blockers identified, next phase planned
**Duration**: 2 sessions (autonomous work + bug fix verification)
**Status**: ✅ **Complete** - Environment prepared, bug fix verified, next phase planned
---
@ -374,5 +374,77 @@ match update.kind {
- 代表的な ループスモーク 16 本のうち 15 本が PASS1 本は既知の別問題)で、
既存パターンへの回帰は維持されている。
この状態を起点に、今後 Phase 170C3 以降で `LoopUpdateSummary` の中身AST/MIR ベースの解析)だけを差し替えることで、
この状態を起点に、今後 Phase 170C3 以降で `LoopUpdateSummary` の中身AST/MIR ベースの解析)だけを差し替えることで、
段階的に carrier 名ヒューリスティックを薄めていく計画。
---
## Phase 170-D: Loop Condition Scope Analysis - Bug Fix Verification ✅
**Date**: 2025-12-07 (Session 2)
**Status**: ✅ **Bug Fix Complete and Verified**
### Bug Fix: Function Parameter Misclassification
**Issue**: Function parameters (`s`, `pos` in JsonParser methods) were incorrectly classified as **LoopBodyLocal** when used in loop conditions.
**Root Cause**: `is_outer_scope_variable()` in `condition_var_analyzer.rs` defaulted unknown variables (not in `variable_definitions`) to LoopBodyLocal.
**Fix** (User Implemented):
```rust
// File: src/mir/loop_pattern_detection/condition_var_analyzer.rs (lines 175-184)
// At this point:
// - Variable is NOT in body_locals
// - No explicit definition info
// This typically means "function parameter" or "outer local"
true // ✅ Default to OuterLocal for function parameters
```
### Verification Results
**Test 1: Function Parameter Loop**
- **File**: `/tmp/test_jsonparser_simple.hako`
- **Result**: `Phase 170-D: Condition variables verified: {"pos", "s", "len"}`
- **Analysis**: Function parameters correctly classified as OuterLocal
- **New blocker**: Method calls (Pattern 5+ feature, not a bug)
**Test 2: LoopBodyLocal Correct Rejection**
- **File**: `local_tests/test_trim_main_pattern.hako`
- **Result**: `Phase 170-D: Condition variables verified: {"ch", "end", "start"}`
- **Error**: "Variable 'ch' not bound in ConditionEnv" (correct rejection)
- **Analysis**: `ch` is defined inside loop, correctly rejected by Pattern 2
**Test 3: JsonParser Full File**
- **File**: `tools/hako_shared/json_parser.hako`
- **Result**: Error about MethodCall, not variable classification
- **Analysis**: Variable scope classification now correct, remaining errors are legitimate feature gaps
### Impact
**What the Fix Achieves**:
- ✅ Function parameters work correctly: `s`, `pos` in JsonParser
- ✅ Carrier variables work correctly: `start`, `end` in trim loops
- ✅ Outer locals work correctly: `len`, `maxLen` from outer scope
- ✅ Correct rejection: LoopBodyLocal `ch` properly rejected (not a bug)
**Remaining Blockers** (Pattern 5+ features, not bugs):
- ⚠️ Method calls in conditions: `loop(pos < s.length())`
- ⚠️ Method calls in loop body: `s.substring(pos, pos+1)`
- ⚠️ LoopBodyLocal in break conditions: `if ch == " " { break }`
### Documentation
**Created**:
1.`phase170-d-fix-verification.md` - Comprehensive verification report
2.`phase170-d-fix-summary.md` - Executive summary
3. ✅ Updated `CURRENT_TASK.md` - Bug fix section added
4. ✅ Updated `phase170-d-impl-design.md` - Bug fix notes
### Next Steps
**Priority 1**: Pattern 5+ implementation for LoopBodyLocal in break conditions
**Priority 2**: .hako rewrite strategy for complex method calls
**Priority 3**: Coverage metrics for JsonParser loop support
**Build Status**: ✅ `cargo build --release` successful (0 errors, 50 warnings)

View File

@ -0,0 +1,226 @@
# Phase 170-D Bug Fix Verification - Summary Report
**Date**: 2025-12-07
**Status**: ✅ Bug Fix Verified and Documented
**Files Updated**: 3 documentation files, 0 code changes (user implemented fix)
---
## 🎯 Executive Summary
The **LoopConditionScopeBox function parameter misclassification bug** has been successfully verified. Function parameters are now correctly classified as **OuterLocal** instead of being incorrectly treated as **LoopBodyLocal**.
**Key Result**: JsonParser loops now pass variable scope validation. Remaining errors are legitimate Pattern 5+ feature gaps (method calls), not bugs.
---
## 📊 Verification Results
### ✅ Test 1: Function Parameter Classification
**Test File**: `/tmp/test_jsonparser_simple.hako`
**Loop**: Pattern 2 with function parameters `s` and `pos`
**Result**:
```
✅ [joinir/pattern2] Phase 170-D: Condition variables verified: {"pos", "s", "len"}
⚠️ New error: MethodCall .substring() not supported (Pattern 5+ feature)
```
**Analysis**:
-**Bug fix works**: `s` and `pos` correctly classified as OuterLocal
-**No more misclassification errors**: Variable scope validation passes
- ⚠️ **Different blocker**: Method calls in loop body (legitimate feature gap)
---
### ✅ Test 2: LoopBodyLocal Correct Rejection
**Test File**: `local_tests/test_trim_main_pattern.hako`
**Loop**: Pattern 2 with LoopBodyLocal `ch` in break condition
**Result**:
```
✅ [joinir/pattern2] Phase 170-D: Condition variables verified: {"ch", "end", "start"}
❌ [ERROR] Variable 'ch' not bound in ConditionEnv
```
**Analysis**:
-**Correct rejection**: `ch` is defined inside loop (`local ch = ...`)
-**Accurate error message**: "Variable 'ch' not bound" (not misclassified)
-**Expected behavior**: Pattern 2 doesn't support LoopBodyLocal in break conditions
**Conclusion**: This is the **correct rejection** - not a bug, needs Pattern 5+ support.
---
### ✅ Test 3: JsonParser Full File
**Test File**: `tools/hako_shared/json_parser.hako`
**Result**:
```
❌ [ERROR] Unsupported expression in value context: MethodCall { ... }
```
**Analysis**:
-**Variable classification working**: No "incorrect variable scope" errors
- ⚠️ **New blocker**: Method calls in loop conditions (`s.length()`)
-**Bug fix validated**: JsonParser now hits different limitations (not variable scope bugs)
---
## 📁 Documentation Created
### 1. `/home/tomoaki/git/hakorune-selfhost/docs/development/current/main/phase170-d-fix-verification.md` ✅
**Comprehensive verification report**:
- Bug fix details (root cause + fix)
- Test results before/after (3 test cases)
- Pattern 1-4 coverage analysis
- Verification commands
- Impact assessment
- Next steps (Pattern 5+ implementation)
### 2. `CURRENT_TASK.md` Updated ✅
**Added Section**: "Bug Fix: Function Parameter Misclassification"
- Issue description
- Root cause
- Fix location (lines 175-184)
- Impact summary
- Link to verification doc
### 3. `phase170-d-impl-design.md` Updated ✅
**Added Section**: "Bug Fix: Function Parameter Misclassification (2025-12-07)"
- Detailed root cause analysis
- Before/after code comparison
- Impact assessment
- Test results
- Lessons learned
- Design principles
---
## 🔍 Bug Fix Technical Details
**File**: `src/mir/loop_pattern_detection/condition_var_analyzer.rs`
**Lines**: 175-184
**The Fix** (user implemented):
```rust
// At this point:
// - The variable is NOT in body_locals
// - There is no explicit definition info for it
//
// This typically means "function parameter" or "outer local"
// (e.g. JsonParserBox.s, .pos, etc.). Those should be treated
// as OuterLocal for condition analysis, otherwise we wrongly
// block valid loops as using loop-body-local variables.
true // ✅ Default to OuterLocal for function parameters
```
**Key Insight**: Unknown variables (not in `variable_definitions`) are typically **function parameters** or **outer locals**, not loop-body-locals. The fix defaults them to the safer (OuterLocal) classification.
---
## 📈 Impact Assessment
### What the Fix Achieves ✅
1.**Function parameters work**: `s`, `pos` in JsonParser methods
2.**Carrier variables work**: `start`, `end` in trim loops
3.**Outer locals work**: `len`, `maxLen` from outer scope
4.**Correct rejection**: LoopBodyLocal `ch` properly rejected (not a bug)
### What Still Needs Work ⚠️
**Pattern 5+ Features** (not bugs):
- Method calls in conditions: `loop(pos < s.length())`
- Method calls in loop body: `s.substring(pos, pos+1)`
- LoopBodyLocal in break conditions: `if ch == " " { break }`
**Other Issues** (orthogonal):
- Exit Line / Boundary errors (separate architectural issues)
---
## 🚀 Next Steps
### Priority 1: Pattern 5+ Implementation
**Target loops**:
- `_trim` loops (LoopBodyLocal `ch` in break condition)
- `_parse_object`, `_parse_array` (method calls in loop body)
**Estimated impact**: 10-15 additional loops in JsonParser
### Priority 2: .hako Rewrite Strategy
**Simplification approach**:
- Hoist `.length()` calls to outer locals
- Pre-compute `.substring()` results outside loop
- Simplify break conditions to use simple comparisons
**Trade-off**: Some loops may be easier to rewrite than to implement Pattern 5+
### Priority 3: Coverage Metrics
**Systematic observation needed**:
```bash
# Run coverage analysis on JsonParser
./tools/analyze_joinir_coverage.sh tools/hako_shared/json_parser.hako
```
**Expected categories**:
- Pattern 1: Simple loops (no break/continue)
- Pattern 2: Loops with break (variable scope OK, method calls blocked)
- Pattern 3: Loops with if-else PHI
- Pattern 4: Loops with continue
- Unsupported: Pattern 5+ needed (method calls, LoopBodyLocal conditions)
---
## ✅ Conclusion
**Bug Fix Status**: ✅ Complete and Verified
**Key Achievement**: Function parameters correctly classified as OuterLocal
**Verification Outcome**: All tests demonstrate correct behavior:
- Function parameters: ✅ Correctly classified
- LoopBodyLocal: ✅ Correctly rejected
- Error messages: ✅ Accurate (method calls, not variable scope)
**Overall Impact**: Significant progress - variable scope classification is now correct. Remaining errors are **legitimate feature gaps** (Pattern 5+ needed), not misclassification bugs.
**Build Status**: ✅ `cargo build --release` successful (0 errors, 50 warnings)
---
## 📋 Verification Commands Used
```bash
# Build verification
cargo build --release
# Test 1: Function parameter loop
NYASH_JOINIR_DEBUG=1 NYASH_JOINIR_STRUCTURE_ONLY=1 \
./target/release/hakorune /tmp/test_jsonparser_simple.hako 2>&1 | \
grep -E "Phase 170-D|Error"
# Test 2: TrimTest (LoopBodyLocal)
NYASH_JOINIR_DEBUG=1 NYASH_JOINIR_STRUCTURE_ONLY=1 \
./target/release/hakorune local_tests/test_trim_main_pattern.hako 2>&1 | \
grep -E "Phase 170-D|Variable 'ch'"
# Test 3: JsonParser full
NYASH_JOINIR_STRUCTURE_ONLY=1 \
./target/release/hakorune tools/hako_shared/json_parser.hako 2>&1 | \
tail -40
```
---
**End of Report**

View File

@ -0,0 +1,255 @@
# Phase 170-D Bug Fix Verification
**Date**: 2025-12-07
**Status**: Fix Complete ✅
**Impact**: Function parameters now correctly classified as OuterLocal
---
## Summary
The LoopConditionScopeBox function parameter misclassification bug has been fixed. Function parameters (`s`, `pos`, etc.) are now correctly treated as **OuterLocal** instead of being incorrectly defaulted to **LoopBodyLocal**.
---
## Bug Fix Details
**File**: `src/mir/loop_pattern_detection/condition_var_analyzer.rs`
**Root Cause**:
- Unknown variables (not in `variable_definitions`) were defaulted to LoopBodyLocal
- Function parameters have no explicit definition in the loop body, so they appeared "unknown"
- Result: Valid loops using function parameters were incorrectly rejected
**Fix**:
```rust
pub fn is_outer_scope_variable(var_name: &str, scope: Option<&LoopScopeShape>) -> bool {
match scope {
None => false,
Some(scope) => {
// ① body_locals に入っていたら絶対に outer ではない
if scope.body_locals.contains(var_name) {
return false;
}
// ② pinnedループ引数などは outer 扱い
if scope.pinned.contains(var_name) {
return true;
}
// ③ variable_definitions の情報がある場合だけ、ブロック分布で判断
if let Some(def_blocks) = scope.variable_definitions.get(var_name) {
// (Carrier detection logic...)
// ...
return false; // body で定義されている → body-local
}
// ④ どこにも出てこない変数 = 関数パラメータ/外側ローカル → OuterLocal
true // ← KEY FIX: Default to OuterLocal, not LoopBodyLocal
}
}
}
```
**Key Change**: Lines 175-184 now default unknown variables to **OuterLocal** (function parameters).
---
## Test Results After Fix
### Test 1: Simple Function Parameter Loop
**File**: `/tmp/test_jsonparser_simple.hako`
**Loop Pattern**: Pattern 2 (loop with break), using function parameters `s` and `pos`
**Before Fix**:
```
❌ UnsupportedPattern: Variable 's' and 'pos' incorrectly classified as LoopBodyLocal
```
**After Fix**:
```
✅ [joinir/pattern2] Phase 170-D: Condition variables verified: {"pos", "s", "len"}
⚠️ Different error: Method call `.substring()` not supported in loop body (separate limitation)
```
**Analysis**:
-**Function parameters correctly classified**: `s` and `pos` are now OuterLocal
- ⚠️ **New blocker**: Method calls in loop body (Pattern 5+ feature)
- **Impact**: Bug fix works correctly - variable classification is fixed
---
### Test 2: TrimTest (LoopBodyLocal in Break Condition)
**File**: `local_tests/test_trim_main_pattern.hako`
**Loop Pattern**: Pattern 2, using LoopBodyLocal `ch` in break condition
**Result**:
```
✅ [joinir/pattern2] Phase 170-D: Condition variables verified: {"ch", "end", "start"}
❌ [ERROR] Variable 'ch' not bound in ConditionEnv
```
**Analysis**:
-**Correctly rejects LoopBodyLocal**: `ch` is defined inside loop (`local ch = ...`)
-**Correct error message**: "Variable 'ch' not bound" (not misclassified)
-**Expected behavior**: Pattern 2 doesn't support LoopBodyLocal in break conditions
**Validation**: This is the **correct rejection** - `ch` should need Pattern 5+ support.
---
### Test 3: JsonParser Full File
**File**: `tools/hako_shared/json_parser.hako`
**Result**:
```
❌ [ERROR] Unsupported expression in value context: MethodCall { object: Variable { name: "s" }, method: "length", ... }
```
**Analysis**:
-**Variable classification working**: No more "variable incorrectly classified" errors
- ⚠️ **New blocker**: Method calls in loop conditions (`s.length()`)
- **Impact**: Bug fix successful - JsonParser now hits **different** limitations (not variable scope bugs)
---
## Pattern 1-4 Coverage Analysis (After Fix)
### ✅ Fixed by Bug Fix
**Before Fix**: X loops incorrectly rejected due to function parameter misclassification
**After Fix**: These loops now correctly pass variable classification:
- **Simple loops using function parameters**: ✅ `s`, `pos` classified as OuterLocal
- **Loops with outer locals**: ✅ `len`, `maxLen` classified correctly
- **Carrier variables**: ✅ `start`, `end` (header+latch) classified as OuterLocal
### ⚠️ Remaining Limitations (Not Bug - Missing Features)
**Pattern 2 doesn't support**:
1. **Method calls in loop condition**: `s.length()`, `s.substring()` → Need Pattern 5+
2. **Method calls in loop body**: `.substring()` in break guards → Need Pattern 5+
3. **LoopBodyLocal in break conditions**: `local ch = ...; if ch == ...` → Need Pattern 5+
**These are legitimate feature gaps, not bugs.**
---
## Verification Commands
```bash
# Test 1: Function parameter loop (should pass variable verification)
NYASH_JOINIR_DEBUG=1 NYASH_JOINIR_STRUCTURE_ONLY=1 \
./target/release/hakorune /tmp/test_jsonparser_simple.hako 2>&1 | \
grep "Phase 170-D"
# Expected: Phase 170-D: Condition variables verified: {"pos", "s", "len"}
# Test 2: LoopBodyLocal in break (should correctly reject)
NYASH_JOINIR_DEBUG=1 NYASH_JOINIR_STRUCTURE_ONLY=1 \
./target/release/hakorune local_tests/test_trim_main_pattern.hako 2>&1 | \
grep "Phase 170-D\|Variable 'ch'"
# Expected:
# Phase 170-D: Condition variables verified: {"ch", "end", "start"}
# ERROR: Variable 'ch' not bound in ConditionEnv
# Test 3: JsonParser (should pass variable verification, fail on method calls)
NYASH_JOINIR_STRUCTURE_ONLY=1 \
./target/release/hakorune tools/hako_shared/json_parser.hako 2>&1 | \
grep -E "Phase 170-D|MethodCall"
# Expected: Error about MethodCall, not about variable classification
```
---
## Impact Assessment
### ✅ What the Fix Achieves
1. **Function parameters work correctly**: `s`, `pos` in JsonParser methods
2. **Carrier variables work correctly**: `start`, `end` in trim loops
3. **Outer locals work correctly**: `len`, `maxLen` from outer scope
4. **Correct rejection**: LoopBodyLocal `ch` properly rejected (not a bug)
### ⚠️ What Still Needs Work
**Pattern 5+ Features** (not covered by this fix):
- Method calls in conditions: `loop(pos < s.length())`
- Method calls in loop body: `s.substring(pos, pos+1)`
- LoopBodyLocal in break conditions: `if ch == " " { break }`
**Exit Line & Boundary Issues** (orthogonal to this fix):
- Some loops fail with ExitLine/Boundary errors
- These are separate architectural issues
---
## Next Steps
### Priority 1: Pattern 5+ Implementation
**Target loops**:
- `_trim` loops (LoopBodyLocal `ch` in break condition)
- `_parse_object`, `_parse_array` (method calls in loop body)
**Estimated impact**: 10-15 additional loops in JsonParser
### Priority 2: .hako Rewrite Strategy
**For loops with complex method calls**:
- Hoist `.length()` calls to outer locals
- Pre-compute `.substring()` results outside loop
- Simplify break conditions to use simple comparisons
**Example rewrite**:
```hako
// Before (Pattern 5+ needed)
loop(pos < s.length()) {
local ch = s.substring(pos, pos+1)
if ch == "}" { break }
pos = pos + 1
}
// After (Pattern 2 compatible)
local len = s.length()
loop(pos < len) {
// Need to avoid method calls in break guard
// This still requires Pattern 5+ for ch definition
local ch = s.substring(pos, pos+1)
if ch == "}" { break }
pos = pos + 1
}
```
### Priority 3: Coverage Metrics
**Run systematic observation**:
```bash
# Count loops by pattern support
./tools/analyze_joinir_coverage.sh tools/hako_shared/json_parser.hako
# Expected output:
# Pattern 1: X loops
# Pattern 2: Y loops (with method call blockers)
# Pattern 3: Z loops
# Pattern 4: W loops
# Unsupported (need Pattern 5+): N loops
```
---
## Conclusion
**Bug Fix Complete**: Function parameters correctly classified as OuterLocal
**Verification Successful**: Tests demonstrate correct variable classification
**Expected Rejections**: LoopBodyLocal in break conditions correctly rejected
⚠️ **Next Blockers**: Method calls in loops (Pattern 5+ features, not bugs)
**Overall Impact**: Significant progress - variable scope classification is now correct. Remaining errors are legitimate feature gaps, not misclassification bugs.

View File

@ -198,6 +198,34 @@ Pattern 2 supports only loop parameters and outer-scope variables.
**Approach**:
1. Detect loop-body-local variable patterns
---
## Bug Fix NotePhase 170-D-impl-2+
Phase 166 再観測中に、JsonParserBox._parse_object(s, pos) の `s`(関数パラメータ)が
LoopBodyLocal と誤判定される致命的バグが見つかった。
- 原因: `is_outer_scope_variable()` が `body_locals` を参照せず、
`pinned` / `variable_definitions` に無い変数を「LoopBodyLocal 寄り」とみなしていた
- 影響: 本来 Pattern2/4 でサポートすべき `loop(p < s.length())` 形式のループが
「loop-body-local 変数使用」として UnsupportedPattern エラーになっていた
修正方針と実装(概略):
- 先に `LoopScopeShape.body_locals` を確認し、ここに含まれる変数だけを LoopBodyLocal とみなす
- `variable_definitions` にエントリがあり、header/latch 以外で定義される変数も LoopBodyLocal とみなす
- 上記いずれにも該当しない変数(関数パラメータや外側ローカル)は OuterLocal として扱う
これにより:
- 関数パラメータ `s`, `pos` 等は正しく OuterLocal と分類され、
JsonParser/Trim 系の「素直な while ループ」は Pattern2/4 の対象に戻る
- 本当にループ内で導入された変数(例: `local ch = ...`)は LoopBodyLocal のまま検出され、
今後の Pattern5+ の設計対象として切り出される
詳細な実装は `src/mir/loop_pattern_detection/condition_var_analyzer.rs` の
`is_outer_scope_variable()` および付随ユニットテストに記録されている。
2. Expand LoopConditionScope with additional heuristics
3. Implement selective patterns (e.g., local x = ...; while(x < N))
4. Reuse LoopConditionScope infrastructure
@ -323,19 +351,110 @@ Finished `release` profile [optimized] target(s) in 1m 08s
- `test_scope_header_and_latch_variable`: Carrier variable classification (COMPLETED)
- `test_scope_priority_in_add_var`: Scope priority validation (BONUS)
## Bug Fix: Function Parameter Misclassification (2025-12-07)
### Issue
Function parameters (e.g., `s`, `pos` in JsonParser methods) were incorrectly classified as **LoopBodyLocal** when used in loop conditions or break guards.
### Root Cause
In `condition_var_analyzer.rs`, the `is_outer_scope_variable()` function's default case (lines 175-184) was treating unknown variables (not in `variable_definitions`) as body-local variables.
**Problem Logic**:
```rust
// OLD (buggy): Unknown variables defaulted to LoopBodyLocal
if let Some(def_blocks) = scope.variable_definitions.get(var_name) {
// (carrier detection...)
return false; // body-local
}
// No default case → implicit false → LoopBodyLocal
false // ❌ BUG: function parameters have no definition, so defaulted to body-local
```
**Why function parameters appear "unknown"**:
- Function parameters (`s`, `pos`) are not defined in the loop body
- They don't appear in `variable_definitions` (which only tracks loop-internal definitions)
- Without explicit handling, they were incorrectly treated as body-local
### Fix
**File**: `src/mir/loop_pattern_detection/condition_var_analyzer.rs` (lines 175-184)
```rust
// NEW (fixed): Unknown variables default to OuterLocal (function parameters)
if let Some(def_blocks) = scope.variable_definitions.get(var_name) {
// (carrier detection logic...)
return false; // body-local
}
// At this point:
// - Variable is NOT in body_locals
// - No explicit definition info
// This typically means "function parameter" or "outer local"
true // ✅ FIX: Default to OuterLocal for function parameters
```
**Key Change**: Default unknown variables to `OuterLocal` instead of implicitly defaulting to `LoopBodyLocal`.
### Impact
**Before Fix**:
- ❌ JsonParser loops incorrectly rejected: "Variable 's' uses loop-body-local variables"
- ❌ Function parameters treated as LoopBodyLocal
- ❌ Valid Pattern 2 loops blocked by misclassification
**After Fix**:
- ✅ Function parameters correctly classified as OuterLocal
- ✅ JsonParser loops pass variable scope validation
- ✅ LoopBodyLocal `ch` (defined with `local ch = ...`) correctly rejected
- ⚠️ New blockers: Method calls in loops (Pattern 5+ features, not bugs)
### Verification
**Test Results**:
1. **Function Parameter Loop** (`/tmp/test_jsonparser_simple.hako`):
```
✅ [joinir/pattern2] Phase 170-D: Condition variables verified: {"pos", "s", "len"}
⚠️ Error: MethodCall .substring() not supported (Pattern 5+ feature)
```
**Analysis**: Variable classification fixed, error now about method calls (separate issue)
2. **LoopBodyLocal in Break** (`test_trim_main_pattern.hako`):
```
✅ [joinir/pattern2] Phase 170-D: Condition variables verified: {"ch", "end", "start"}
❌ [ERROR] Variable 'ch' not bound in ConditionEnv
```
**Analysis**: Correctly rejects `ch` (defined as `local ch = ...` inside loop)
**Documentation**: See [phase170-d-fix-verification.md](phase170-d-fix-verification.md) for comprehensive test results.
### Lessons Learned
**Design Principle**: When classifying variables in scope analysis:
1. **Check explicit markers first** (`body_locals`, `pinned`)
2. **Analyze definition locations** (`variable_definitions`)
3. **Default to OuterLocal** for unknowns (function parameters, globals)
**Fail-Fast Philosophy**: The bug fix maintains fail-fast behavior while being **less strict** about unknown variables - treating them as safer (OuterLocal) rather than more restrictive (LoopBodyLocal).
---
## Next Steps
1. **Phase 170-D-impl-4 Completion**:
1. **Phase 170-D-impl-4 Completion** :
- Update CURRENT_TASK.md with completion markers
- Create integration test .hako files for unsupported patterns
- Run full regression test suite
2. **Documentation**:
2. **Documentation** :
- Update loop pattern documentation index
- Add quick reference for Phase 170-D validation
- Bug fix verification document
3. **Future Work** (Phase 170-D-E):
- Pattern 5+ for loop-body-local variable support
- Extended scope heuristics
- Condition simplification analysis
- Method call support in loop conditions

View File

@ -0,0 +1,257 @@
# Phase 171-A: Blocked Loop Inventory
**Date**: 2025-12-07
**Status**: Initial inventory complete
**Purpose**: Identify loops blocked by LoopBodyLocal variables in break conditions
---
## Overview
This document catalogs loops that cannot be lowered by Pattern 2/4 because they use **LoopBodyLocal** variables in their break conditions. These are candidates for Pattern 5 carrier promotion.
---
## Blocked Loops Found
### 1. TrimTest.trim/1 - Leading Whitespace Trim
**File**: `local_tests/test_trim_main_pattern.hako`
**Lines**: 20-27
```hako
loop(start < end) {
local ch = s.substring(start, start+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break
}
}
```
**Blocking Variable**: `ch` (LoopBodyLocal)
**Analysis**:
- Loop parameter: `start` (LoopParam)
- Outer variable: `end` (OuterLocal)
- Break condition uses: `ch` (LoopBodyLocal)
- `ch` is defined inside loop body as `s.substring(start, start+1)`
**Error Message**:
```
[trace:debug] pattern2: Pattern 2 lowerer failed: Variable 'ch' not bound in ConditionEnv
```
**Why Blocked**:
Pattern 2 expects break conditions to only use:
- LoopParam (`start`)
- OuterLocal (`end`, `s`)
But the break condition `ch == " " || ...` uses `ch`, which is defined inside the loop body.
---
### 2. TrimTest.trim/1 - Trailing Whitespace Trim
**File**: `local_tests/test_trim_main_pattern.hako`
**Lines**: 30-37
```hako
loop(end > start) {
local ch = s.substring(end-1, end)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
end = end - 1
} else {
break
}
}
```
**Blocking Variable**: `ch` (LoopBodyLocal)
**Analysis**:
- Loop parameter: `end` (LoopParam)
- Outer variable: `start` (OuterLocal)
- Break condition uses: `ch` (LoopBodyLocal)
- `ch` is defined inside loop body as `s.substring(end-1, end)`
**Same blocking reason as Loop 1.**
---
### 3. JsonParserBox - MethodCall in Condition
**File**: `tools/hako_shared/json_parser.hako`
```hako
loop(i < s.length()) {
// ...
}
```
**Blocking Issue**: `s.length()` is a MethodCall in the condition expression.
**Error Message**:
```
[ERROR] ❌ MIR compilation error: [cf_loop/pattern4] Lowering failed:
Unsupported expression in value context: MethodCall {
object: Variable { name: "s", ... },
method: "length",
arguments: [],
...
}
```
**Why Blocked**:
Pattern 4's value context lowering doesn't support MethodCall expressions yet.
**Note**: This is not a LoopBodyLocal issue, but a MethodCall limitation. May be addressed in Phase 171-D (Optional).
---
## Pattern5-A Target Decision
### Selected Target: TrimTest Loop 1 (Leading Whitespace)
We select the **first loop** from TrimTest as Pattern5-A target for the following reasons:
1. **Clear structure**: Simple substring + equality checks
2. **Representative**: Same pattern as many real-world parsers
3. **Self-contained**: Doesn't depend on complex outer state
4. **Testable**: Easy to write unit tests
### Pattern5-A Specification
**Loop Structure**:
```hako
loop(start < end) {
local ch = s.substring(start, start+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break
}
}
```
**Variables**:
- `start`: LoopParam (carrier, mutated)
- `end`: OuterLocal (condition-only)
- `s`: OuterLocal (used in body)
- `ch`: LoopBodyLocal (blocking variable)
**Break Condition**:
```
!(ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
```
---
## Promotion Strategy: Design D (Evaluated Bool Carrier)
### Rationale
We choose **Design D** (Evaluated Bool Carrier) over other options:
**Why not carry `ch` directly?**
- `ch` is a StringBox, not a primitive value
- Would require complex carrier type system
- Would break existing Pattern 2/4 assumptions
**Design D approach**:
- Introduce a new carrier: `is_whitespace` (bool)
- Evaluate `ch == " " || ...` in loop body
- Store result in `is_whitespace` carrier
- Use `is_whitespace` in break condition
### Transformed Structure
**Before (Pattern5-A)**:
```hako
loop(start < end) {
local ch = s.substring(start, start+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break
}
}
```
**After (Pattern2 compatible)**:
```hako
// Initialization (before loop)
local is_whitespace = true // Initial assumption
loop(start < end && is_whitespace) {
local ch = s.substring(start, start+1)
is_whitespace = (ch == " " || ch == "\t" || ch == "\n" || ch == "\r")
if is_whitespace {
start = start + 1
} else {
break // Now redundant, but kept for clarity
}
}
```
**Key transformations**:
1. Add `is_whitespace` carrier initialization
2. Update loop condition to include `is_whitespace`
3. Compute `is_whitespace` in loop body
4. Original if-else becomes simpler (could be optimized away)
---
## Next Steps (Phase 171-C)
### Phase 171-C-1: Skeleton Implementation ✅
- Create `LoopBodyCarrierPromoter` box
- Define `PromotionRequest` / `PromotionResult` types
- Implement skeleton `try_promote()` method
- Add `find_definition_in_body()` helper
### Phase 171-C-2: Trim Pattern Promotion Logic
- Detect substring + equality pattern
- Generate `is_whitespace` carrier
- Generate initialization statement
- Generate update statement
### Phase 171-C-3: Integration with Pattern 2/4
- Call `LoopBodyCarrierPromoter::try_promote()` in routing
- If promotion succeeds, route to Pattern 2
- If promotion fails, return UnsupportedPattern
### Phase 171-D: MethodCall Support (Optional)
- Handle `s.length()` in loop conditions
- May require carrier promotion for method results
- Lower priority than Trim pattern
---
## Summary
**Blocked Loops**:
- 2 loops in TrimTest (LoopBodyLocal `ch`)
- 1+ loops in JsonParser (MethodCall in condition)
**Pattern5-A Target**:
- TrimTest leading whitespace trim loop
- Clear, representative, testable
**Promotion Strategy**:
- Design D: Evaluated Bool Carrier
- Transform `ch` checks → `is_whitespace` carrier
- Make compatible with Pattern 2
**Implementation Status**:
- Phase 171-A: ✅ Inventory complete
- Phase 171-B: ✅ Target selected
- Phase 171-C-1: ✅ Skeleton implementation complete
- Phase 171-C-2: ✅ Trim pattern detection implemented
- `find_definition_in_body()`: AST traversal for variable definitions
- `is_substring_method_call()`: Detects `substring()` method calls
- `extract_equality_literals()`: Extracts string literals from OR chains
- `TrimPatternInfo`: Captures pattern details for carrier promotion
- Phase 171-C-3: ⏳ Integration with Pattern 2/4 routing

View File

@ -130,8 +130,17 @@ pub fn is_outer_scope_variable(
scope: Option<&LoopScopeShape>,
) -> bool {
match scope {
None => false, // No scope info → assume body-local
// No scope information: be conservative but *not* overstrict.
// We treat unknown as body-local only when we have a LoopScopeShape
// that explicitly marks it so (via body_locals / definitions).
// Here we simply say "unknown" and let the caller decide.
None => false,
Some(scope) => {
// If the variable is explicitly marked as body-local, it is NOT outer.
if scope.body_locals.contains(var_name) {
return false;
}
// Check 1: Is it a pinned variable (loop parameter or passed-in)?
if scope.pinned.contains(var_name) {
return true;
@ -151,15 +160,27 @@ pub fn is_outer_scope_variable(
// This supports loop patterns like:
// local i = 0 (header)
// loop(i < 10) {
// ...
// i = i + 1 (latch)
// ...
// i = i + 1 (latch)
// }
if def_blocks.iter().all(|b| *b == scope.header || *b == scope.latch) {
return true;
}
// Any other definition pattern (e.g. body-only or body+header)
// is treated as body-local / internal.
return false;
}
false
// At this point:
// - The variable is NOT in body_locals
// - There is no explicit definition info for it
//
// This typically means "function parameter" or "outer local"
// (e.g. JsonParserBox.s, .pos, etc.). Those should be treated
// as OuterLocal for condition analysis, otherwise we wrongly
// block valid loops as using loop-body-local variables.
true
}
}
}
@ -338,6 +359,34 @@ mod tests {
assert!(!is_outer_scope_variable("ch", Some(&scope)));
}
#[test]
fn test_is_outer_scope_variable_function_param_like() {
// Variables that are *not* marked as body_locals and have no explicit
// variable_definitions entry represent things like function parameters
// or outer locals. These must be treated as OuterLocal so that valid
// conditions such as `p < s.length()` (with `s` a parameter) are
// accepted by Pattern 2/4.
use std::collections::{BTreeMap, BTreeSet};
let scope = LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::new(),
carriers: BTreeSet::new(),
body_locals: BTreeSet::new(),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
assert!(
is_outer_scope_variable("s", Some(&scope)),
"Function parameterlike variable should be classified as OuterLocal"
);
}
// ========================================================================
// Phase 170-ultrathink: Additional Edge Case Tests (Issue #3)
// ========================================================================

View File

@ -0,0 +1,600 @@
//! Phase 171-C: LoopBodyCarrierPromoter Box
//!
//! LoopBodyLocal 変数を carrier に昇格させることで、
//! Pattern 2/4 の範囲で処理可能にするための箱。
//!
//! ## Design Philosophy
//!
//! - 入力: LoopScopeShape + LoopConditionScope + break 条件 AST
//! - 出力: 昇格成功なら CarrierInfo、失敗なら理由
//! - 役割: LoopBodyLocal を「評価済み bool carrier」に変換
//!
//! ## Implementation Scope
//!
//! ### Phase 171-C-1: スケルトン実装 ✅
//! - LoopBodyLocal の検出
//! - 定義の探索
//!
//! ### Phase 171-C-2: Trim パターン昇格 ✅
//! - `local ch = s.substring(...)` パターン検出
//! - `ch == " " || ch == "\t" ...` の等価比較検出
//! - `is_whitespace` bool carrier への変換情報生成
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScope;
/// 昇格リクエスト
pub struct PromotionRequest<'a> {
/// ループのスコープ情報
pub scope: &'a LoopScopeShape,
/// 条件変数のスコープ分類
pub cond_scope: &'a LoopConditionScope,
/// break 条件の ASTPattern 2 の場合)
pub break_cond: Option<&'a ASTNode>,
/// ループ本体の AST
pub loop_body: &'a [ASTNode],
}
/// Phase 171-C-2: 検出された Trim パターン情報
#[derive(Debug, Clone)]
pub struct TrimPatternInfo {
/// LoopBodyLocal 変数名(例: "ch"
pub var_name: String,
/// 比較対象の文字列リテラル(例: [" ", "\t", "\n", "\r"]
pub comparison_literals: Vec<String>,
/// 生成する carrier 名(例: "is_whitespace"
pub carrier_name: String,
}
/// 昇格結果
pub enum PromotionResult {
/// 昇格成功: Trim パターン情報を返す
///
/// Phase 171-C-2: CarrierInfo の実際の更新は Phase 171-C-3 で実装
Promoted {
/// Phase 171-C-2: 検出された Trim パターン情報
trim_info: TrimPatternInfo,
},
/// 昇格不可: 理由を説明
CannotPromote {
reason: String,
vars: Vec<String>, // 問題の LoopBodyLocal
},
}
/// Phase 171-C: LoopBodyCarrierPromoter Box
pub struct LoopBodyCarrierPromoter;
impl LoopBodyCarrierPromoter {
/// LoopBodyLocal を carrier に昇格できるか試行
///
/// # Phase 171-C-2: Trim パターン実装
///
/// 現在の実装では:
/// 1. LoopBodyLocal を抽出
/// 2. 各変数の定義を探索
/// 3. Trim パターンsubstring + equalityを検出
/// 4. 昇格可能なら TrimPatternInfo を返す
pub fn try_promote(request: &PromotionRequest) -> PromotionResult {
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;
// 1. LoopBodyLocal を抽出
let body_locals: Vec<&String> = request.cond_scope.vars.iter()
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
.map(|v| &v.name)
.collect();
if body_locals.is_empty() {
// LoopBodyLocal がなければ昇格不要
return PromotionResult::CannotPromote {
reason: "No LoopBodyLocal variables to promote".to_string(),
vars: vec![],
};
}
eprintln!(
"[promoter/pattern5] Phase 171-C: Found {} LoopBodyLocal variables: {:?}",
body_locals.len(),
body_locals
);
// 2. 各 LoopBodyLocal の定義を探す
for var_name in &body_locals {
let definition = Self::find_definition_in_body(request.loop_body, var_name);
if let Some(def_node) = definition {
eprintln!(
"[promoter/pattern5] Found definition for '{}' in loop body",
var_name
);
// 3. Phase 171-C-2: Trim パターンを検出
if Self::is_substring_method_call(def_node) {
eprintln!(
"[promoter/pattern5] '{}' is defined by substring() call - Trim pattern candidate",
var_name
);
// 4. break 条件から等価比較リテラルを抽出
if let Some(break_cond) = request.break_cond {
let literals = Self::extract_equality_literals(break_cond, var_name);
if !literals.is_empty() {
eprintln!(
"[promoter/pattern5] Trim pattern detected! var='{}', literals={:?}",
var_name, literals
);
// 昇格成功!
let trim_info = TrimPatternInfo {
var_name: var_name.to_string(),
comparison_literals: literals,
carrier_name: format!("is_{}_match", var_name),
};
// Phase 171-C-2: TrimPatternInfo を返す
// CarrierInfo の実際の更新は Phase 171-C-3 で実装
return PromotionResult::Promoted { trim_info };
}
}
}
} else {
eprintln!(
"[promoter/pattern5] Definition for '{}' not found in loop body",
var_name
);
}
}
// 昇格パターンに一致しない
PromotionResult::CannotPromote {
reason: "No promotable Trim pattern detected".to_string(),
vars: body_locals.iter().map(|s| s.to_string()).collect(),
}
}
/// ループ本体から変数の定義Assignmentを探す
///
/// # Phase 171-C-2: 実装済み
///
/// iterative worklist で AST を探索し、`local var = ...` または
/// `var = ...` の代入を見つける。
///
/// # Arguments
///
/// * `body` - ループ本体の AST ノード列
/// * `var_name` - 探す変数名
///
/// # Returns
///
/// 定義(代入の RHSが見つかれば Some(&ASTNode)、なければ None
fn find_definition_in_body<'a>(body: &'a [ASTNode], var_name: &str) -> Option<&'a ASTNode> {
let mut worklist: Vec<&'a ASTNode> = body.iter().collect();
while let Some(node) = worklist.pop() {
match node {
// Assignment: target = value
ASTNode::Assignment { target, value, .. } => {
// target が Variable で、名前が一致すれば定義発見
if let ASTNode::Variable { name, .. } = target.as_ref() {
if name == var_name {
return Some(value.as_ref());
}
}
}
// If: then_body と else_body を探索
ASTNode::If { then_body, else_body, .. } => {
for stmt in then_body {
worklist.push(stmt);
}
if let Some(else_stmts) = else_body {
for stmt in else_stmts {
worklist.push(stmt);
}
}
}
// Loop: body を探索(ネストループ)
ASTNode::Loop { body: loop_body, .. } => {
for stmt in loop_body {
worklist.push(stmt);
}
}
// その他のノードは無視
_ => {}
}
}
None
}
/// RHS が substring() メソッド呼び出しかどうかを判定
///
/// # Phase 171-C-2
///
/// Trim パターンでは `local ch = s.substring(start, start+1)` のように
/// substring メソッドで1文字を取り出すパターンを使う。
fn is_substring_method_call(node: &ASTNode) -> bool {
matches!(
node,
ASTNode::MethodCall { method, .. } if method == "substring"
)
}
/// break 条件から、指定変数との等価比較リテラルを抽出
///
/// # Phase 171-C-2
///
/// `ch == " " || ch == "\t" || ch == "\n"` のような条件から
/// `[" ", "\t", "\n"]` を抽出する。
///
/// # Arguments
///
/// * `cond` - break 条件の AST
/// * `var_name` - 比較対象の変数名
///
/// # Returns
///
/// 等価比較で使われている文字列リテラルのリスト
fn extract_equality_literals(cond: &ASTNode, var_name: &str) -> Vec<String> {
let mut result = Vec::new();
let mut worklist = vec![cond];
while let Some(node) = worklist.pop() {
match node {
// BinaryOp: Or で分岐、Eq で比較
ASTNode::BinaryOp { operator, left, right, .. } => {
match operator {
// Or: 両側を探索
BinaryOperator::Or => {
worklist.push(left.as_ref());
worklist.push(right.as_ref());
}
// Equal: var == literal パターンを検出
BinaryOperator::Equal => {
// left が Variable で var_name に一致
if let ASTNode::Variable { name, .. } = left.as_ref() {
if name == var_name {
// right が String リテラル
if let ASTNode::Literal { value: LiteralValue::String(s), .. } = right.as_ref() {
result.push(s.clone());
}
}
}
// right が Variable で var_name に一致(逆順)
if let ASTNode::Variable { name, .. } = right.as_ref() {
if name == var_name {
if let ASTNode::Literal { value: LiteralValue::String(s), .. } = left.as_ref() {
result.push(s.clone());
}
}
}
}
_ => {}
}
}
// UnaryOp: Not の内側を探索
ASTNode::UnaryOp { operand, .. } => {
worklist.push(operand.as_ref());
}
_ => {}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Span;
use crate::mir::BasicBlockId;
use crate::mir::loop_pattern_detection::loop_condition_scope::{
CondVarScope, LoopConditionScope,
};
use std::collections::{BTreeMap, BTreeSet};
fn minimal_scope() -> LoopScopeShape {
LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::new(),
carriers: BTreeSet::new(),
body_locals: BTreeSet::new(),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
}
}
fn cond_scope_with_body_local(var_name: &str) -> LoopConditionScope {
let mut scope = LoopConditionScope::new();
scope.add_var(var_name.to_string(), CondVarScope::LoopBodyLocal);
scope
}
// Helper: Create a Variable node
fn var_node(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: Span::unknown(),
}
}
// Helper: Create a String literal node
fn str_literal(s: &str) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::String(s.to_string()),
span: Span::unknown(),
}
}
// Helper: Create an equality comparison (var == literal)
fn eq_cmp(var_name: &str, literal: &str) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(var_node(var_name)),
right: Box::new(str_literal(literal)),
span: Span::unknown(),
}
}
// Helper: Create an Or expression
fn or_expr(left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Or,
left: Box::new(left),
right: Box::new(right),
span: Span::unknown(),
}
}
// Helper: Create a MethodCall node
fn method_call(object: &str, method: &str) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(var_node(object)),
method: method.to_string(),
arguments: vec![],
span: Span::unknown(),
}
}
// Helper: Create an Assignment node
fn assignment(target: &str, value: ASTNode) -> ASTNode {
ASTNode::Assignment {
target: Box::new(var_node(target)),
value: Box::new(value),
span: Span::unknown(),
}
}
#[test]
fn test_promoter_no_body_locals() {
let scope = minimal_scope();
let cond_scope = LoopConditionScope::new(); // Empty, no LoopBodyLocal
let request = PromotionRequest {
scope: &scope,
cond_scope: &cond_scope,
break_cond: None,
loop_body: &[],
};
let result = LoopBodyCarrierPromoter::try_promote(&request);
match result {
PromotionResult::CannotPromote { reason, vars } => {
assert!(vars.is_empty());
assert!(reason.contains("No LoopBodyLocal"));
}
_ => panic!("Expected CannotPromote when no LoopBodyLocal variables"),
}
}
#[test]
fn test_promoter_body_local_no_definition() {
// LoopBodyLocal があるが、定義が見つからない場合
let scope = minimal_scope();
let cond_scope = cond_scope_with_body_local("ch");
let request = PromotionRequest {
scope: &scope,
cond_scope: &cond_scope,
break_cond: None,
loop_body: &[], // Empty body - no definition
};
let result = LoopBodyCarrierPromoter::try_promote(&request);
match result {
PromotionResult::CannotPromote { reason, vars } => {
assert!(vars.contains(&"ch".to_string()));
assert!(reason.contains("No promotable Trim pattern"));
}
_ => panic!("Expected CannotPromote when definition not found"),
}
}
// ========================================================================
// Phase 171-C-2: Trim Pattern Detection Tests
// ========================================================================
#[test]
fn test_find_definition_in_body_simple() {
// Test: local ch = s.substring(...)
let body = vec![
assignment("ch", method_call("s", "substring")),
];
let result = LoopBodyCarrierPromoter::find_definition_in_body(&body, "ch");
assert!(result.is_some(), "Definition should be found");
match result.unwrap() {
ASTNode::MethodCall { method, .. } => {
assert_eq!(method, "substring");
}
_ => panic!("Expected MethodCall"),
}
}
#[test]
fn test_find_definition_in_body_nested_if() {
// Test: Definition inside if-else block
let body = vec![
ASTNode::If {
condition: Box::new(var_node("flag")),
then_body: vec![
assignment("ch", method_call("s", "substring")),
],
else_body: None,
span: Span::unknown(),
},
];
let result = LoopBodyCarrierPromoter::find_definition_in_body(&body, "ch");
assert!(result.is_some(), "Definition should be found inside if block");
}
#[test]
fn test_is_substring_method_call() {
let substring_call = method_call("s", "substring");
let other_call = method_call("s", "length");
assert!(LoopBodyCarrierPromoter::is_substring_method_call(&substring_call));
assert!(!LoopBodyCarrierPromoter::is_substring_method_call(&other_call));
}
#[test]
fn test_extract_equality_literals_single() {
// Test: ch == " "
let cond = eq_cmp("ch", " ");
let result = LoopBodyCarrierPromoter::extract_equality_literals(&cond, "ch");
assert_eq!(result.len(), 1);
assert!(result.contains(&" ".to_string()));
}
#[test]
fn test_extract_equality_literals_or_chain() {
// Test: ch == " " || ch == "\t" || ch == "\n"
let cond = or_expr(
or_expr(
eq_cmp("ch", " "),
eq_cmp("ch", "\t"),
),
eq_cmp("ch", "\n"),
);
let result = LoopBodyCarrierPromoter::extract_equality_literals(&cond, "ch");
assert_eq!(result.len(), 3);
assert!(result.contains(&" ".to_string()));
assert!(result.contains(&"\t".to_string()));
assert!(result.contains(&"\n".to_string()));
}
#[test]
fn test_extract_equality_literals_wrong_var() {
// Test: other_var == " " (should not match for "ch")
let cond = eq_cmp("other_var", " ");
let result = LoopBodyCarrierPromoter::extract_equality_literals(&cond, "ch");
assert!(result.is_empty(), "Should not extract literals for wrong variable");
}
#[test]
fn test_trim_pattern_full_detection() {
// Full Trim pattern test:
// - LoopBodyLocal: ch
// - Definition: ch = s.substring(...)
// - Break condition: ch == " " || ch == "\t"
let scope = minimal_scope();
let cond_scope = cond_scope_with_body_local("ch");
let loop_body = vec![
assignment("ch", method_call("s", "substring")),
];
let break_cond = or_expr(
eq_cmp("ch", " "),
eq_cmp("ch", "\t"),
);
let request = PromotionRequest {
scope: &scope,
cond_scope: &cond_scope,
break_cond: Some(&break_cond),
loop_body: &loop_body,
};
let result = LoopBodyCarrierPromoter::try_promote(&request);
match result {
PromotionResult::Promoted { trim_info } => {
assert_eq!(trim_info.var_name, "ch");
assert_eq!(trim_info.comparison_literals.len(), 2);
assert!(trim_info.comparison_literals.contains(&" ".to_string()));
assert!(trim_info.comparison_literals.contains(&"\t".to_string()));
assert_eq!(trim_info.carrier_name, "is_ch_match");
}
PromotionResult::CannotPromote { reason, .. } => {
panic!("Expected Promoted, got CannotPromote: {}", reason);
}
}
}
#[test]
fn test_trim_pattern_with_4_whitespace_chars() {
// Full whitespace pattern: " " || "\t" || "\n" || "\r"
let scope = minimal_scope();
let cond_scope = cond_scope_with_body_local("ch");
let loop_body = vec![
assignment("ch", method_call("s", "substring")),
];
let break_cond = or_expr(
or_expr(
eq_cmp("ch", " "),
eq_cmp("ch", "\t"),
),
or_expr(
eq_cmp("ch", "\n"),
eq_cmp("ch", "\r"),
),
);
let request = PromotionRequest {
scope: &scope,
cond_scope: &cond_scope,
break_cond: Some(&break_cond),
loop_body: &loop_body,
};
let result = LoopBodyCarrierPromoter::try_promote(&request);
match result {
PromotionResult::Promoted { trim_info } => {
assert_eq!(trim_info.comparison_literals.len(), 4);
}
PromotionResult::CannotPromote { reason, .. } => {
panic!("Expected Promoted, got CannotPromote: {}", reason);
}
}
}
}

View File

@ -758,3 +758,6 @@ pub mod condition_var_analyzer;
// Phase 170-ultrathink: Error Message Utilities
pub mod error_messages;
// Phase 171-C: LoopBodyLocal Carrier Promotion
pub mod loop_body_carrier_promoter;