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>
This commit is contained in:
nyash-codex
2025-12-08 03:06:20 +09:00
parent 14c84fc583
commit 5ba11468e4
4 changed files with 733 additions and 20 deletions

View File

@ -247,3 +247,7 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ
- 185188: Strict mode / LoopBuilder 削除 / Pattern14 基盤
- 189193: Multi-function merge / Select bridge / ExitLine 箱化
- 171172 / 3310/13: ConditionEnv, ConditionBinding, JoinFragmentMeta, ExitLineRefactor 等
- `docs/development/current/main/loop_pattern_space.md`
- JoinIR ループパターン空間の整理メモ。
どの軸(継続条件 / break / continue / PHI / 条件変数スコープ / 更新パターン)でパターンを分けるか、
そして P1P4 / Trim(P5) の位置づけと、今後追加候補のパターン一覧がまとまっている。

View File

@ -0,0 +1,170 @@
# JoinIR Loop Pattern Space (Phase 171 UltraThink Memo)
このメモは「JoinIR のループパターン空間」を構造的に整理したものだよ。
総当たりではなく、**直交する少数の軸**を組み合わせることで有限のパターンに収束することを確認する。
---
## 1. ループを構成する直交軸
ループはだいたい次の 6 軸の組み合わせで記述できる:
| 軸 | 選択肢 | 説明 |
|-------------------|------------------------------------|-------------------------------|
| A. 継続条件 | ① なし ② 単純 ③ 複合 | `loop(cond)``cond` |
| B. 早期終了 | ① なし ② break ③ 条件付き break | ループを「抜ける」経路 |
| C. スキップ | ① なし ② continue ③ 条件付き cont | 次のイテレーションに「飛ぶ」 |
| D. PHI 分岐 | ① なし ② ifPHI ③ matchPHI | 条件に応じて値が変わるパターン |
| E. 条件変数のスコープ | ① OuterLocal ② LoopBodyLocal | 条件で参照される変数の定義位置 |
| F. キャリア更新 | ① 単一 ② 複数 ③ 条件付き | ループ内での状態更新パターン |
この 6 軸は、それぞれ 2〜3 通りしかないので、理論上は最大 3×3×3×3×2×3=486 通りの組み合わせになるが、
実際に意味のあるパターンはずっと少ない10〜20 程度)ことが分かった。
---
## 2. 現在 JoinIR が正規化済みのパターン
代表ループと対応する Pattern はだいたいこうなっている:
| Pattern | 代表例 | A継続 | B終了 | Cスキップ | DPHI | E変数 | F更新 |
|----------------|------------------------------|---------|--------------|-----------------|---------|------------|----------|
| P1: Minimal | `loop_min_while.hako` | 単純 | なし | なし | なし | Outer | 単一 |
| P2: Break | `joinir_min_loop.hako` | 単純 | 条件付きbreak | なし | なし | Outer | 単一 |
| P3: IfPHI | `loop_if_phi.hako` | 単純 | なし | なし | ifPHI | Outer | 条件付き |
| P4: Continue | `loop_continue_pattern4` | 単純 | なし | 条件付きcont | なし | Outer | 単一 |
| P5: Trimlike* | `TrimTest.trim`(設計中) | 単純 | 条件付きbreak | なし | なし | BodyLocal | 単一 |
\*P5 は Phase 171 時点では「検出+安全性判定」まで。実際の JoinIR lower は Phase 172 以降の仕事。
ここまでで:
- OuterLocal 条件(関数パラメータや外側ローカル)を持つ基本的な while/break/continue/ifPHI は JoinIR で正規化済み。
- LoopBodyLocal 条件(`local ch = ...; if ch == ' ' { break }`)は **LoopConditionScopeBox で FailFast** し、
Trim パターンだけを LoopBodyCarrierPromoter/TrimLoopHelper で特例として扱う設計にしている。
---
## 3. これから増やす候補パターン
実際の Nyash コードを見た上で、「出そうだな」と分かっているパターンをいくつか挙げておく。
### P6: break + continue 同時
```hako
loop (cond) {
if x { continue }
if y { break }
...
}
```
- A: 単純
- B: 条件付き break
- C: 条件付き continue
- E: OuterLocal 条件(が理想)
### P7: 複数キャリア + 条件付き更新
```hako
loop (i < n) {
if arr[i] > max { max = arr[i] }
if arr[i] < min { min = arr[i] }
i = i + 1
}
```
- A: 単純
- B: なし
- C: なし
- D: なし or ifPHI に相当
- F: 複数+条件付き
### P8: ネストループ(外側ループへの break/continue
```hako
loop (i < n) {
loop (j < m) {
if cond { break outer } // labeled break
}
}
```
※ Nyash 言語仕様で outer break をどう扱うか次第で、JoinIR 側も変わる。
### P9: matchPHI
```hako
loop (i < n) {
result = match state {
"A" => compute_a()
"B" => compute_b()
}
}
```
これは ifPHI を複数ケースに拡張した形。JoinIR の If/Select/PHI パスを再利用できるはず。
### P10: 無限ループ + 内部 break
```hako
loop (true) {
if done { break }
}
```
構造としては P2 の特殊ケースA: 継続条件なしB: breakとして扱える。
### P11: 複合継続条件 + LoopBodyLocal
```hako
loop (i < n && is_valid) {
local x = compute()
is_valid = check(x)
}
```
これは LoopBodyLocal 状態を継続条件に折り込むパターン。
BoolExprLowerer + LoopBodyCarrierPromoter の拡張で扱える可能性がある。
### P12: early return 内包
```hako
loop (i < n) {
if error { return null } // ループを抜けて関数終了
}
```
ループ exit と関数 exit が混ざるパターン。ExitLine とは別に「関数全体の戻り値ライン」とどう噛ませるかの設計が必要。
---
## 4. 収束性について
- 各軸が有限個の状態しか取らないこと、
- 多くの組み合わせが意味を持たない/禁止パターンであること(無限ループ、進まないループなど)
から、JoinIR で真面目に扱うべきループ形は **高々数十種類** に収束する。
実運用上の優先順としては:
1. P1P4: 基本パターン(すでに実装済み)
2. P5: Trim / JsonParser で現実に必要な LoopBodyLocal 条件の一部(昇格可能なもの)
3. P6, P7, P12: パーサ/集計/エラー処理で頻出
4. P8, P9, P11: 言語仕様や実アプリのニーズを見ながら段階的に
という順で Box を増やしていけば、「パターン爆発」にはならずに済む想定だよ。
---
## 5. 関連ドキュメント
- `joinir-architecture-overview.md`
JoinIR 全体の箱と契約。Loop/If/ExitLine/Boundary/条件式ラインの全体図。
- `phase33-16-design.md`, `PHASE_33_16_SUMMARY.md`
Loop header PHI / ExitLine / Boundary 再設計の詳細。
- `phase166-jsonparser-loop-recheck.md`
JsonParserBox / Trim 系ループのインベントリと、どの Pattern に入るかの観測ログ。
- `phase171-pattern5-loop-inventory.md`
Trim/JsonParser 向け Pattern5LoopBodyLocal 条件)設計の進捗。

View File

@ -0,0 +1,345 @@
# 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**:
```nyash
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**:
```rust
// 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):
```rust
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**:
```rust
// 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**:
```rust
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**:
```rust
// 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**:
```rust
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**:
```rust
// 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**:
```bash
./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**:
```bash
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**:
```bash
./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:
```rust
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`