fix(joinir): Phase 177-3 ValueId collision fix for multi-carrier loops

Root cause: JoinIR ValueId collision between function parameters and condition bindings
- Same ValueId used for both `result_init` (carrier param) and `limit` (condition var)
- Phase 33-21 was overwriting condition bindings when remapping carrier PHIs

Fix implemented (Option B - immediate protection):
1. Phase 177-3: Protect condition-only variables from Phase 33-21 override
   - Collect condition_bindings that are NOT carriers (by checking exit_bindings)
   - Skip remapping for these protected ValueIds
2. Phase 177-3-B: Handle body-only carriers explicitly
   - Carriers that appear in condition_bindings (added by Phase 176-5)
   - Map them to correct PHI dsts by name lookup

Investigation tools added:
- [DEBUG-177] trace logs for remapper state tracking
- Phase 177-3 protection logging
- BoundaryInjector PHI collision detection

Test results:
-  Integer multi-carrier test: Output 3 (expected)
- ⚠️ String test: RC=0 but empty output (separate issue - string concat emit)

Design docs created:
- phase177-parse-string-design.md: _parse_string loop analysis
- phase177-carrier-evolution.md: Carrier progression Phase 174-179

Next: Investigate string concatenation emit for full _parse_string support
This commit is contained in:
nyash-codex
2025-12-08 16:34:04 +09:00
parent 99d329096f
commit 7a01ffe522
6 changed files with 646 additions and 40 deletions

View File

@ -0,0 +1,165 @@
# Phase 177: Carrier Evolution - min から Production へ
## 視覚的比較: ループ構造の進化
### Phase 174: min1-carrier
```
Input: "hello world""
^start ^target
loop(pos < len) {
ch = s[pos]
if ch == '"' → break ✓
else → pos++
}
Carriers: 1
- pos (ループ変数)
Pattern: 4 (Loop + If PHI)
Exit: pos = 11
```
### Phase 175/176: min22-carrier
```
Input: "hello world""
^start ^target
loop(pos < len) {
ch = s[pos]
if ch == '"' → break ✓
else → result += ch
pos++
}
Carriers: 2
- pos (ループ変数)
- result (バッファ)
Pattern: 4 (Loop + If PHI)
Exit: pos = 11, result = "hello world"
```
### Phase 177-A: Simple Case2-carrier, Production-like
```
Input: "hello world""
^start ^target
loop(p < len) {
ch = s[p]
if ch == '"' → break ✓
else → str += ch
p++
}
Carriers: 2
- p (ループ変数)
- str (バッファ)
Pattern: 4 (Loop + If PHI)
Exit: p = 11, str = "hello world"
```
### Phase 178: Escape Handling2-carrier + continue
```
Input: "hello \"world\"""
^start ^escape ^target
loop(p < len) {
ch = s[p]
if ch == '"' → break ✓
if ch == '\\' → str += ch
p++
str += s[p]
p++
continue ←← 新要素
else → str += ch
p++
}
Carriers: 2 (変わらず)
- p (ループ変数)
- str (バッファ)
Pattern: 4? (continue 対応は未検証)
Exit: p = 15, str = "hello \"world\""
```
### Phase 179: Full Production2-carrier + early return
```
Input: "hello \"world\"""
^start ^escape ^target
loop(p < len) {
ch = s[p]
if ch == '"' → return MapBox {...} ✓ ←← early return
if ch == '\\' → if p+1 >= len → return null ←← error
str += ch
p++
str += s[p]
p++
continue
else → str += ch
p++
}
return null ←← ループ終端エラー
Carriers: 2 (変わらず)
- p (ループ変数)
- str (バッファ)
Pattern: 4? (early return 対応は未検証)
Exit: 正常: MapBox, 異常: null
```
## Carrier 安定性分析
### 重要な発見
**Phase 174 → 179 を通じて Carrier 数は安定1 → 2**
| Phase | Carriers | 新要素 | P5昇格候補 |
|-------|----------|--------|-----------|
| 174 | 1 (pos) | - | なし |
| 175/176 | 2 (pos + result) | バッファ追加 | なし |
| 177-A | 2 (p + str) | - | なし |
| 178 | 2 (p + str) | continue | なし(確認中) |
| 179 | 2 (p + str) | early return | なし(確認中) |
### P5昇格候補の不在
**Trim と異なり、`is_ch_match` 相当は不要**
理由:
- Trim: `is_ch_match` が「次も空白か?」を決定(**次の判断に影響**
- _parse_string: `ch == '"'` は「今終了か?」のみ(**次に影響しない**
```
Trim の制御フロー:
is_ch_match = (ch == ' ')
if is_ch_match → pos++ → 次も is_ch_match を評価 ←← 連鎖
_parse_string の制御フロー:
if ch == '"' → break → 終了
else → str += ch, p++ → 次は独立判断 ←← 連鎖なし
```
## JoinIR Pattern 対応予測
| Phase | Pattern 候補 | 理由 |
|-------|-------------|------|
| 177-A | Pattern4 | Loop + If PHI + break実装済み |
| 178 | Pattern4? | continue は Pattern4-with-continue要実装確認 |
| 179 | Pattern5? | early return は新パターン候補(要設計) |
## まとめ
### 段階的検証戦略
1. **Phase 177-A**: min2 と同型 → P5 安定性確認
2. **Phase 178**: continue 追加 → JoinIR 拡張必要性評価
3. **Phase 179**: early return 追加 → Pattern5 設計判断
### Carrier 設計の教訓
- **最小構成で開始**: 1-carrier (Phase 174)
- **段階的拡張**: 2-carrier (Phase 175/176)
- **Production 適用**: 構造は変えず、制御フローのみ追加Phase 177+
**「Carrier 数を固定して制御フローを段階的に複雑化」が正解**

View File

@ -0,0 +1,178 @@
# Phase 177: _parse_string 本体 JoinIR 適用設計
## 目的
Production `JsonParserBox._parse_string` メソッドに JoinIR P5 パイプラインを適用する。
Phase 174/175/176 で確立した「1-carrier → 2-carrier」検証の成果を、実際の JSON パーサーに展開する。
## 本番ループ構造lines 144-181
### 前提条件
- 開始位置 `pos``"` を指しているline 145 でチェック)
- ループ開始時 `p = pos + 1`(引用符の次の文字から開始)
### ループ本体lines 150-178
```hako
loop(p < s.length()) {
local ch = s.substring(p, p+1)
if ch == '"' {
// End of string (lines 153-160)
local result = new MapBox()
result.set("value", me._unescape_string(str))
result.set("pos", p + 1)
result.set("type", "string")
return result // ← Early return (通常の exit)
}
if ch == "\\" {
// Escape sequence (lines 162-174)
local has_next = 0
if p + 1 < s.length() { has_next = 1 }
if has_next == 0 { return null } // ← Early return (エラー)
str = str + ch
p = p + 1
str = str + s.substring(p, p+1)
p = p + 1
continue // ← ループ継続
}
str = str + ch // 通常文字の追加
p = p + 1
}
return null // ← ループ終端に到達(エラー)
```
### 制御フロー分岐
1. **終端クォート検出** (`ch == '"'`): 成功 return
2. **エスケープ検出** (`ch == "\\"`) + continue: 2文字消費してループ継続
3. **通常文字**: バッファ蓄積 + 位置進行
4. **ループ終端**: 引用符が閉じていない(エラー)
## Carriers 分析
| 変数 | 初期値 | 役割 | 更新式 | P5 昇格対象? | 備考 |
|------|--------|------|--------|---------------|------|
| `p` | `pos + 1` | カウンタ(位置) | `p = p + 1` | **Nループ変数** | 条件式に使用 |
| `str` | `""` | バッファ(蓄積) | `str = str + ch` | **N通常キャリア** | エスケープ時2回更新 |
| `has_next` | - | 一時変数(フラグ) | - | - | ループ内のみ有効 |
| `is_escape` | - | (潜在的フラグ) | - | - | 明示的変数なし |
### 重要な発見
- **エスケープ処理は continue 経由**: `p``str` を 2 回更新してから continue
- **Early return が 2 箇所**: 成功 return (line 159) とエラー return (line 167)
- **通常キャリアのみ**: P5 昇格対象(`is_ch_match` 相当)は**不要**
## min ケースとの差分
### 共通点
| 項目 | min (Phase 174) | min2 (Phase 175) | 本番 (Phase 177) |
|------|-----------------|------------------|------------------|
| ループ変数 | `pos` | `pos` | `p` |
| バッファ | なし | `result` | `str` |
| 終端条件 | `ch == '"'` → break | `ch == '"'` → break | `ch == '"'` → return |
| 通常処理 | `pos++` | `result += ch; pos++` | `str += ch; p++` |
### 差分
| 項目 | min/min2 | 本番 |
|------|----------|------|
| 終端処理 | `break` のみ | Early `return` (MapBox 返却) |
| エスケープ処理 | なし | `continue` を使った複雑な分岐 |
| エラー処理 | なし | ループ内・外に `return null` |
## 方針決定: 段階的アプローチ
### Phase 177-A: Simple Case今回
**対象**: エスケープ**なし**・終端クォート検出のみ
- **ループ構造**: min2 と完全同型(`p` + `str` の 2-carrier
- **終端処理**: `break` → 直後に MapBox 構築return 代替)
- **目的**: P5 パイプラインが「2-carrier + break」で動作することを確認
```hako
// Simplified for Phase 177-A
loop(p < s.length()) {
local ch = s.substring(p, p+1)
if ch == '"' {
break // P5: Pattern4 対応Loop+If PHI merge
} else {
str = str + ch
p = p + 1
}
}
// break 後: MapBox 構築
```
### Phase 178: Escape Handling次回
**追加要素**:
- `continue` 分岐(エスケープ処理)
- Early returnエラーハンドリング
- P5 昇格キャリア候補の検討(`is_escape` フラグ?)
### Phase 179: Full Production最終
**統合**:
- `_unescape_string()` 呼び出し
- 完全なエラーハンドリング
- 本番同等の MapBox 返却
## Phase 177-A 実装計画
### Test Case: `test_jsonparser_parse_string_simple.hako`
```hako
// Phase 177-A: Production-like simple case
static box JsonParserStringTest3 {
parse_string_simple() {
local s = "hello world\""
local p = 0 // pos + 1 相当(簡略化のため 0 から開始)
local str = ""
local len = s.length()
// 2-carrier loop (min2 と同型)
loop(p < len) {
local ch = s.substring(p, p+1)
if ch == "\"" {
break
} else {
str = str + ch
p = p + 1
}
}
// Post-loop: 結果出力MapBox 構築の代替)
print("Parsed string: ")
print(str)
print(", final pos: ")
print(p)
}
main() {
me.parse_string_simple()
return "OK"
}
}
```
### 期待される MIR 構造
- **Pattern4 検出**: Loop + If PHI mergePhase 170 実装済み)
- **2 carriers**: `p` (ループ変数) + `str` (バッファ)
- **Exit PHI**: ループ後の `p``str` が正しく伝播
### 検証項目
1. ✅ P5 パイプライン通過JoinIR → MIR
2. ✅ 2-carrier の正しい伝播(`p``str`
3.`break` 後の変数値が正しく使用可能
## 成功基準
- [ ] `test_jsonparser_parse_string_simple.hako` が実行成功
- [ ] MIR ダンプで Pattern4 検出確認
- [ ] 出力: `Parsed string: hello world, final pos: 11`
## 次のステップPhase 178
- `continue` 分岐の追加(エスケープ処理)
- P5 昇格キャリア候補の検討(必要性を再評価)
- Early return 対応JoinIR での処理検討)
## まとめ
**Phase 177-A では、min2 と同型の Simple Case を Production 環境に適用する。**
エスケープ処理は Phase 178 以降に回し、まず「2-carrier + break」の動作確認を優先する。

View File

@ -12,15 +12,20 @@ use super::super::trace;
/// Phase 1: Allocate new block IDs for ALL functions (Phase 189) /// Phase 1: Allocate new block IDs for ALL functions (Phase 189)
/// ///
/// DETERMINISM: Sort functions and blocks by name/ID to ensure consistent iteration order /// DETERMINISM: Sort functions and blocks by name/ID to ensure consistent iteration order
///
/// Returns: (JoinIrIdRemapper, exit_block_id)
pub(super) fn allocate_blocks( pub(super) fn allocate_blocks(
builder: &mut crate::mir::builder::MirBuilder, builder: &mut crate::mir::builder::MirBuilder,
mir_module: &MirModule, mir_module: &MirModule,
debug: bool, debug: bool,
) -> Result<JoinIrIdRemapper, String> { ) -> Result<(JoinIrIdRemapper, crate::mir::BasicBlockId), String> {
let mut remapper = JoinIrIdRemapper::new(); let mut remapper = JoinIrIdRemapper::new();
// Create exit block for Return conversion (single for all functions) // Phase 177-3: Allocate exit block FIRST to ensure it doesn't conflict with JoinIR blocks
let _exit_block_id = builder.block_gen.next(); // This exit_block_id will be returned and used by instruction_rewriter and exit_phi_builder
let exit_block_id = builder.block_gen.next();
eprintln!("[cf_loop/joinir/block_allocator] Phase 177-3: Allocated exit_block_id = {:?}", exit_block_id);
// Phase 195: Use unified trace // Phase 195: Use unified trace
trace::trace().blocks("allocator", "Phase 189: Allocating block IDs for all functions"); trace::trace().blocks("allocator", "Phase 189: Allocating block IDs for all functions");
@ -61,5 +66,6 @@ pub(super) fn allocate_blocks(
); );
} }
Ok(remapper) // Phase 177-3: Return both remapper and exit_block_id
Ok((remapper, exit_block_id))
} }

View File

@ -30,6 +30,11 @@ use std::collections::HashMap;
/// Uses extracted modules: /// Uses extracted modules:
/// - tail_call_classifier: TailCallKind classification /// - tail_call_classifier: TailCallKind classification
/// - merge_result: MergeResult data structure /// - merge_result: MergeResult data structure
///
/// # Phase 177-3
///
/// `exit_block_id` is now passed in from block_allocator to ensure it doesn't
/// conflict with JoinIR function blocks.
pub(super) fn merge_and_rewrite( pub(super) fn merge_and_rewrite(
builder: &mut crate::mir::builder::MirBuilder, builder: &mut crate::mir::builder::MirBuilder,
mir_module: &MirModule, mir_module: &MirModule,
@ -38,13 +43,11 @@ pub(super) fn merge_and_rewrite(
function_params: &HashMap<String, Vec<ValueId>>, function_params: &HashMap<String, Vec<ValueId>>,
boundary: Option<&JoinInlineBoundary>, boundary: Option<&JoinInlineBoundary>,
loop_header_phi_info: &mut LoopHeaderPhiInfo, loop_header_phi_info: &mut LoopHeaderPhiInfo,
exit_block_id: BasicBlockId,
debug: bool, debug: bool,
)-> Result<MergeResult, String> { )-> Result<MergeResult, String> {
// Create exit block for Return conversion (single for all functions) // Phase 177-3: exit_block_id is now passed in from block_allocator
let exit_block_id = builder.block_gen.next(); eprintln!("[cf_loop/joinir/instruction_rewriter] Phase 177-3: Using exit_block_id = {:?}", exit_block_id);
if debug {
eprintln!("[cf_loop/joinir] Exit block: {:?}", exit_block_id);
}
// Phase 189 FIX: Build set of boundary join_inputs to skip their Const initializers // Phase 189 FIX: Build set of boundary join_inputs to skip their Const initializers
let boundary_input_set: std::collections::HashSet<ValueId> = boundary let boundary_input_set: std::collections::HashSet<ValueId> = boundary
@ -172,8 +175,58 @@ pub(super) fn merge_and_rewrite(
let mut found_tail_call = false; let mut found_tail_call = false;
let mut tail_call_target: Option<(BasicBlockId, Vec<ValueId>)> = None; let mut tail_call_target: Option<(BasicBlockId, Vec<ValueId>)> = None;
// Phase 177-3: Check if this block is the loop header with PHI nodes
let is_loop_header_with_phi = is_loop_entry_point && !loop_header_phi_info.carrier_phis.is_empty();
if is_loop_entry_point {
eprintln!(
"[cf_loop/joinir] Phase 177-3 DEBUG: is_loop_entry_point={}, carrier_phis.len()={}, is_loop_header_with_phi={}",
is_loop_entry_point,
loop_header_phi_info.carrier_phis.len(),
is_loop_header_with_phi
);
}
// Phase 177-3: Collect PHI dst IDs that will be created for this block
// (if this is the loop header)
let phi_dst_ids_for_block: std::collections::HashSet<ValueId> = if is_loop_header_with_phi {
loop_header_phi_info.carrier_phis.values()
.map(|entry| entry.phi_dst)
.collect()
} else {
std::collections::HashSet::new()
};
if is_loop_header_with_phi && !phi_dst_ids_for_block.is_empty() {
eprintln!(
"[cf_loop/joinir] Phase 177-3: Loop header with {} PHI dsts to protect: {:?}",
phi_dst_ids_for_block.len(),
phi_dst_ids_for_block
);
}
// First pass: Process all instructions, identify tail calls // First pass: Process all instructions, identify tail calls
for inst in &old_block.instructions { for inst in &old_block.instructions {
// Phase 177-3: Skip Copy instructions that would overwrite PHI dsts
// The loop header PHIs will provide these values, so copies are redundant/harmful
if is_loop_header_with_phi {
if let MirInstruction::Copy { dst, src } = inst {
// Check if this copy's dst is a PHI dst (after remapping)
let dst_remapped = remapper.get_value(*dst).unwrap_or(*dst);
eprintln!(
"[cf_loop/joinir] Phase 177-3 DEBUG: Copy {:?} = {:?}, dst_remapped = {:?}, in phi_dsts = {}",
dst, src, dst_remapped, phi_dst_ids_for_block.contains(&dst_remapped)
);
if phi_dst_ids_for_block.contains(&dst_remapped) {
eprintln!(
"[cf_loop/joinir] Phase 177-3: ✅ Skipping loop header Copy to PHI dst {:?} (original {:?})",
dst_remapped, dst
);
continue; // Skip - would overwrite PHI
}
}
}
// Phase 189: Skip Const String instructions that define function names // Phase 189: Skip Const String instructions that define function names
if let MirInstruction::Const { dst, value } = inst { if let MirInstruction::Const { dst, value } = inst {
if let crate::mir::types::ConstValue::String(_) = value { if let crate::mir::types::ConstValue::String(_) = value {
@ -653,15 +706,30 @@ pub(super) fn merge_and_rewrite(
} }
} }
// Phase 177-3: Collect PHI dst IDs from loop_header_phi_info
let phi_dst_ids: std::collections::HashSet<ValueId> = loop_header_phi_info
.carrier_phis
.values()
.map(|entry| entry.phi_dst)
.collect();
// Use BoundaryInjector to inject Copy instructions // Use BoundaryInjector to inject Copy instructions
// Phase 177-3 Option B: Returns reallocations map for condition_bindings with PHI collisions
if let Some(ref mut current_func) = builder.current_function { if let Some(ref mut current_func) = builder.current_function {
BoundaryInjector::inject_boundary_copies( let _reallocations = BoundaryInjector::inject_boundary_copies(
current_func, current_func,
entry_block_remapped, entry_block_remapped,
boundary, boundary,
&value_map_for_injector, &value_map_for_injector,
&phi_dst_ids,
debug, debug,
)?; )?;
// Note: reallocations map is returned but not currently used.
// If condition variables need to be referenced after this point,
// the reallocations map would need to be applied to update references.
// For now, condition_bindings are read-only variables used only in
// the loop condition, so no further updates are needed.
} }
} }

View File

@ -80,7 +80,8 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
} }
// Phase 1: Allocate block IDs for all functions // Phase 1: Allocate block IDs for all functions
let mut remapper = block_allocator::allocate_blocks(builder, mir_module, debug)?; // Phase 177-3: block_allocator now returns exit_block_id to avoid conflicts
let (mut remapper, exit_block_id) = block_allocator::allocate_blocks(builder, mir_module, debug)?;
// Phase 2: Collect values from all functions // Phase 2: Collect values from all functions
let (mut used_values, value_to_func_name, function_params) = let (mut used_values, value_to_func_name, function_params) =
@ -113,6 +114,30 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
// Phase 3: Remap ValueIds // Phase 3: Remap ValueIds
remap_values(builder, &used_values, &mut remapper, debug)?; remap_values(builder, &used_values, &mut remapper, debug)?;
// Phase 177-3 DEBUG: Verify remapper state after Phase 3
eprintln!("[DEBUG-177] === Remapper state after Phase 3 ===");
eprintln!("[DEBUG-177] used_values count: {}", used_values.len());
for value_id in &used_values {
if let Some(remapped) = remapper.get_value(*value_id) {
eprintln!("[DEBUG-177] JoinIR {:?} → Host {:?}", value_id, remapped);
} else {
eprintln!("[DEBUG-177] JoinIR {:?} → NOT FOUND ❌", value_id);
}
}
// Check condition_bindings specifically
if let Some(boundary) = boundary {
eprintln!("[DEBUG-177] === Condition bindings check ===");
for binding in &boundary.condition_bindings {
let lookup_result = remapper.get_value(binding.join_value);
eprintln!(
"[DEBUG-177] '{}': JoinIR {:?}{:?}",
binding.name, binding.join_value, lookup_result
);
}
}
eprintln!("[DEBUG-177] ==============================");
// Phase 3.5: Build loop header PHIs (if loop pattern with loop_var_name) // Phase 3.5: Build loop header PHIs (if loop pattern with loop_var_name)
// //
// We need to know PHI dsts before instruction_rewriter runs, so that: // We need to know PHI dsts before instruction_rewriter runs, so that:
@ -215,9 +240,47 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
// 2. loop body uses PHI result // 2. loop body uses PHI result
// 3. tail call args are correctly routed // 3. tail call args are correctly routed
// Map main's parameters // Phase 177-3 fix: Protect condition-ONLY bindings from being overridden to PHI dsts
// MIR function keys use join_func_N format from join_func_name() //
// Problem: condition_bindings may contain:
// 1. True condition-only variables (e.g., 'limit' in loop(i < limit)) - NOT carriers
// 2. Body-only carriers added by Phase 176-5 (e.g., 'result') - ARE carriers
//
// We must ONLY protect (1), not (2), because:
// - Condition-only vars should keep their HOST mapping (e.g., limit = %8)
// - Body-only carriers MUST be remapped to PHI dsts (e.g., result = %24)
//
// Solution: Protect condition_bindings that are NOT in exit_bindings (i.e., not carriers)
let carrier_names: std::collections::HashSet<&str> = boundary
.exit_bindings
.iter()
.map(|eb| eb.carrier_name.as_str())
.collect();
let condition_binding_ids: std::collections::HashSet<ValueId> = boundary
.condition_bindings
.iter()
.filter(|cb| !carrier_names.contains(cb.name.as_str()))
.map(|cb| cb.join_value)
.collect();
if !condition_binding_ids.is_empty() {
eprintln!(
"[cf_loop/joinir] Phase 177-3: Protected ValueIds (condition-only, not carriers): {:?}",
condition_binding_ids
);
for cb in &boundary.condition_bindings {
let is_carrier = carrier_names.contains(cb.name.as_str());
eprintln!(
"[cf_loop/joinir] Phase 177-3: '{}': JoinIR {:?} (carrier={})",
cb.name, cb.join_value, is_carrier
);
}
}
let main_func_name = "join_func_0"; let main_func_name = "join_func_0";
let loop_step_func_name = "join_func_1";
if function_params.get(main_func_name).is_none() { if function_params.get(main_func_name).is_none() {
eprintln!( eprintln!(
"[cf_loop/joinir] WARNING: function_params.get('{}') returned None. Available keys: {:?}", "[cf_loop/joinir] WARNING: function_params.get('{}') returned None. Available keys: {:?}",
@ -226,30 +289,57 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
); );
} }
if let Some(main_params) = function_params.get(main_func_name) { if let Some(main_params) = function_params.get(main_func_name) {
if debug {
eprintln!( eprintln!(
"[cf_loop/joinir] Phase 33-21: main ({}) params: {:?}", "[DEBUG-177] Phase 33-21: main ({}) params: {:?}",
main_func_name, main_params main_func_name, main_params
); );
} eprintln!(
"[DEBUG-177] Phase 33-21: carrier_phis count: {}, names: {:?}",
phi_info.carrier_phis.len(),
phi_info.carrier_phis.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>()
);
// Map main's parameters to header PHI dsts // Map main's parameters to header PHI dsts
// main params: [i_init, carrier1_init, ...] // main params: [i_init, carrier1_init, ...]
// carrier_phis: [("i", entry), ("sum", entry), ...] // carrier_phis: [("i", entry), ("sum", entry), ...]
for (idx, (carrier_name, entry)) in phi_info.carrier_phis.iter().enumerate() { for (idx, (carrier_name, entry)) in phi_info.carrier_phis.iter().enumerate() {
if let Some(&main_param) = main_params.get(idx) { if let Some(&main_param) = main_params.get(idx) {
if debug { // Phase 177-3: Don't override condition_bindings
if condition_binding_ids.contains(&main_param) {
eprintln!( eprintln!(
"[cf_loop/joinir] Phase 33-21: REMAP main param {:?} {:?} ('{}')", "[cf_loop/joinir] Phase 177-3: Skipping override for condition_binding {:?} ('{}')",
main_param, entry.phi_dst, carrier_name main_param, carrier_name
); );
continue;
} }
eprintln!(
"[DEBUG-177] Phase 33-21: REMAP main param[{}] {:?}{:?} ('{}')",
idx, main_param, entry.phi_dst, carrier_name
);
remapper.set_value(main_param, entry.phi_dst); remapper.set_value(main_param, entry.phi_dst);
} }
} }
} }
// Phase 177-3-B: Handle body-only carriers
// These are carriers in carrier_phis that are NOT in main function params.
// They appear in condition_bindings (added by Phase 176-5) but need PHI remapping.
for (carrier_name, entry) in &phi_info.carrier_phis {
// Check if this carrier has a condition_binding
if let Some(binding) = boundary.condition_bindings.iter().find(|cb| cb.name == *carrier_name) {
// Skip if it's a true condition-only variable (already protected above)
if condition_binding_ids.contains(&binding.join_value) {
continue;
}
// This is a body-only carrier - remap it to PHI dst
eprintln!(
"[cf_loop/joinir] Phase 177-3-B: Body-only carrier '{}': JoinIR {:?} → PHI {:?}",
carrier_name, binding.join_value, entry.phi_dst
);
remapper.set_value(binding.join_value, entry.phi_dst);
}
}
// Map loop_step's parameters // Map loop_step's parameters
let loop_step_func_name = "join_func_1";
if debug { if debug {
eprintln!( eprintln!(
"[cf_loop/joinir] Phase 33-21: function_params keys: {:?}", "[cf_loop/joinir] Phase 33-21: function_params keys: {:?}",
@ -275,6 +365,14 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
// carrier_phis: [("i", entry), ("sum", entry), ...] // carrier_phis: [("i", entry), ("sum", entry), ...]
for (idx, (carrier_name, entry)) in phi_info.carrier_phis.iter().enumerate() { for (idx, (carrier_name, entry)) in phi_info.carrier_phis.iter().enumerate() {
if let Some(&loop_step_param) = loop_step_params.get(idx) { if let Some(&loop_step_param) = loop_step_params.get(idx) {
// Phase 177-3: Don't override condition_bindings
if condition_binding_ids.contains(&loop_step_param) {
eprintln!(
"[cf_loop/joinir] Phase 177-3: Skipping override for condition_binding {:?} ('{}')",
loop_step_param, carrier_name
);
continue;
}
if debug { if debug {
eprintln!( eprintln!(
"[cf_loop/joinir] Phase 33-21: REMAP loop_step param {:?}{:?} ('{}')", "[cf_loop/joinir] Phase 33-21: REMAP loop_step param {:?}{:?} ('{}')",
@ -290,6 +388,8 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
// Fallback: Use old behavior (ValueId(0), ValueId(1), ...) // Fallback: Use old behavior (ValueId(0), ValueId(1), ...)
// This handles patterns that don't have loop_step function // This handles patterns that don't have loop_step function
if let Some(phi_dst) = phi_info.get_carrier_phi(loop_var_name) { if let Some(phi_dst) = phi_info.get_carrier_phi(loop_var_name) {
// Phase 177-3: Don't override condition_bindings
if !condition_binding_ids.contains(&ValueId(0)) {
remapper.set_value(ValueId(0), phi_dst); remapper.set_value(ValueId(0), phi_dst);
if debug { if debug {
eprintln!( eprintln!(
@ -297,12 +397,19 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
phi_dst phi_dst
); );
} }
} else {
eprintln!(
"[cf_loop/joinir] Phase 177-3 fallback: Skipping override for condition_binding ValueId(0)"
);
}
} }
for (idx, (carrier_name, entry)) in phi_info.carrier_phis.iter().enumerate() { for (idx, (carrier_name, entry)) in phi_info.carrier_phis.iter().enumerate() {
if carrier_name == loop_var_name { if carrier_name == loop_var_name {
continue; continue;
} }
let join_value_id = ValueId(idx as u32); let join_value_id = ValueId(idx as u32);
// Phase 177-3: Don't override condition_bindings
if !condition_binding_ids.contains(&join_value_id) {
remapper.set_value(join_value_id, entry.phi_dst); remapper.set_value(join_value_id, entry.phi_dst);
if debug { if debug {
eprintln!( eprintln!(
@ -310,8 +417,24 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
join_value_id, entry.phi_dst, carrier_name join_value_id, entry.phi_dst, carrier_name
); );
} }
} else {
eprintln!(
"[cf_loop/joinir] Phase 177-3 fallback: Skipping override for condition_binding {:?} ('{}')",
join_value_id, carrier_name
);
} }
} }
}
// Phase 177-3 DEBUG: Check remapper after Phase 33-21 overrides
eprintln!("[DEBUG-177] === Remapper state after Phase 33-21 ===");
for binding in &boundary.condition_bindings {
let lookup_result = remapper.get_value(binding.join_value);
eprintln!(
"[DEBUG-177] '{}': JoinIR {:?}{:?} (after 33-21)",
binding.name, binding.join_value, lookup_result
);
}
phi_info phi_info
} else { } else {
@ -323,6 +446,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
// Phase 4: Merge blocks and rewrite instructions // Phase 4: Merge blocks and rewrite instructions
// Phase 33-16: Pass mutable loop_header_phi_info for latch_incoming tracking // Phase 33-16: Pass mutable loop_header_phi_info for latch_incoming tracking
// Phase 177-3: Pass exit_block_id from allocator to avoid conflicts
let merge_result = instruction_rewriter::merge_and_rewrite( let merge_result = instruction_rewriter::merge_and_rewrite(
builder, builder,
mir_module, mir_module,
@ -331,6 +455,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
&function_params, &function_params,
boundary, boundary,
&mut loop_header_phi_info, &mut loop_header_phi_info,
exit_block_id,
debug, debug,
)?; )?;

View File

@ -39,13 +39,26 @@ impl BoundaryInjector {
/// When `boundary.loop_var_name` is set, ALL carriers (loop var + other carriers /// When `boundary.loop_var_name` is set, ALL carriers (loop var + other carriers
/// from exit_bindings) are handled by header PHIs. We skip ALL join_inputs /// from exit_bindings) are handled by header PHIs. We skip ALL join_inputs
/// Copy instructions to avoid overwriting the PHI results. /// Copy instructions to avoid overwriting the PHI results.
///
/// # Phase 177-3: PHI Collision Avoidance (Option B)
///
/// When `phi_dst_ids` contains ValueIds of existing PHI dsts in the header block,
/// we allocate a NEW ValueId for condition_bindings instead of skipping the copy.
/// This ensures the condition variable is available even when its remapped ValueId
/// collides with a PHI dst.
///
/// # Returns
///
/// Returns a HashMap mapping original ValueIds to their reallocated ValueIds
/// (only for condition_bindings that had collisions).
pub fn inject_boundary_copies( pub fn inject_boundary_copies(
func: &mut MirFunction, func: &mut MirFunction,
entry_block_id: BasicBlockId, entry_block_id: BasicBlockId,
boundary: &JoinInlineBoundary, boundary: &JoinInlineBoundary,
value_map: &HashMap<ValueId, ValueId>, value_map: &HashMap<ValueId, ValueId>,
phi_dst_ids: &std::collections::HashSet<ValueId>,
debug: bool, debug: bool,
) -> Result<(), String> { ) -> Result<HashMap<ValueId, ValueId>, String> {
// Phase 33-20: When loop_var_name is set, ALL join_inputs are handled by header PHIs // Phase 33-20: When loop_var_name is set, ALL join_inputs are handled by header PHIs
// This includes the loop variable AND all other carriers from exit_bindings. // This includes the loop variable AND all other carriers from exit_bindings.
// We skip ALL join_inputs Copy instructions, only condition_bindings remain. // We skip ALL join_inputs Copy instructions, only condition_bindings remain.
@ -59,7 +72,8 @@ impl BoundaryInjector {
}; };
let total_inputs = effective_join_inputs + boundary.condition_bindings.len(); let total_inputs = effective_join_inputs + boundary.condition_bindings.len();
if total_inputs == 0 { if total_inputs == 0 {
return Ok(()); // No inputs to process, return empty reallocations map
return Ok(HashMap::new());
} }
if debug { if debug {
@ -73,7 +87,38 @@ impl BoundaryInjector {
); );
} }
// Entry block を取得 // Phase 177-3: Use PHI dst IDs passed from caller
if debug && !phi_dst_ids.is_empty() {
eprintln!(
"[BoundaryInjector] Phase 177-3: Received {} PHI dst IDs to avoid: {:?}",
phi_dst_ids.len(),
phi_dst_ids
);
}
// Phase 177-3 Option B: First pass - allocate all new ValueIds for PHI collisions
// We need to do this BEFORE acquiring the entry_block reference to avoid borrow conflicts
let mut reallocations = HashMap::new();
for binding in &boundary.condition_bindings {
let remapped_join = value_map.get(&binding.join_value).copied().unwrap_or(binding.join_value);
if phi_dst_ids.contains(&remapped_join) {
// Collision detected! Allocate a fresh ValueId
let fresh_dst = func.next_value_id();
if debug {
eprintln!(
"[BoundaryInjector] Phase 177-3 Option B: PHI collision for condition binding '{}': {:?} → reallocated to {:?}",
binding.name, remapped_join, fresh_dst
);
}
reallocations.insert(binding.join_value, fresh_dst);
}
}
// Now get entry block reference (after all ValueId allocations are done)
let entry_block = func let entry_block = func
.get_block_mut(entry_block_id) .get_block_mut(entry_block_id)
.ok_or(format!("Entry block {:?} not found", entry_block_id))?; .ok_or(format!("Entry block {:?} not found", entry_block_id))?;
@ -110,17 +155,34 @@ impl BoundaryInjector {
} }
} }
// Phase 177-3 DEBUG: Check value_map in BoundaryInjector
if debug {
eprintln!("[DEBUG-177] === BoundaryInjector value_map ===");
for binding in &boundary.condition_bindings {
let lookup = value_map.get(&binding.join_value);
eprintln!(
"[DEBUG-177] '{}': JoinIR {:?}{:?}",
binding.name, binding.join_value, lookup
);
}
}
// Phase 171-fix: Inject Copy instructions for condition_bindings (condition-only variables) // Phase 171-fix: Inject Copy instructions for condition_bindings (condition-only variables)
// These variables are read-only and used ONLY in the loop condition. // These variables are read-only and used ONLY in the loop condition.
// Each binding explicitly specifies HOST ValueId → JoinIR ValueId mapping. // Each binding explicitly specifies HOST ValueId → JoinIR ValueId mapping.
// We inject Copy: remapped_join_value = Copy host_value // We inject Copy: remapped_join_value = Copy host_value
//
// Phase 177-3 Option B: Use pre-allocated reallocations for PHI collision cases
for binding in &boundary.condition_bindings { for binding in &boundary.condition_bindings {
// Look up the remapped JoinIR ValueId from value_map // Look up the remapped JoinIR ValueId from value_map
let remapped_join = value_map.get(&binding.join_value).copied().unwrap_or(binding.join_value); let remapped_join = value_map.get(&binding.join_value).copied().unwrap_or(binding.join_value);
// Copy instruction: remapped_join_value = Copy host_value // Phase 177-3 Option B: Check if this binding was reallocated (PHI collision case)
let final_dst = reallocations.get(&binding.join_value).copied().unwrap_or(remapped_join);
// Copy instruction: final_dst = Copy host_value
let copy_inst = MirInstruction::Copy { let copy_inst = MirInstruction::Copy {
dst: remapped_join, dst: final_dst,
src: binding.host_value, src: binding.host_value,
}; };
@ -128,8 +190,9 @@ impl BoundaryInjector {
if debug { if debug {
eprintln!( eprintln!(
"[BoundaryInjector] Condition binding '{}': Copy {:?} = Copy {:?} (JoinIR {:?} → remapped {:?})", "[BoundaryInjector] Condition binding '{}': Copy {:?} = Copy {:?} (JoinIR {:?} → remapped {:?}{})",
binding.name, remapped_join, binding.host_value, binding.join_value, remapped_join binding.name, final_dst, binding.host_value, binding.join_value, remapped_join,
if final_dst != remapped_join { format!(" → reallocated {:?}", final_dst) } else { String::new() }
); );
} }
} }
@ -145,7 +208,8 @@ impl BoundaryInjector {
entry_block.instruction_spans.insert(0, default_span); entry_block.instruction_spans.insert(0, default_span);
} }
Ok(()) // Return reallocations map for condition_bindings that had PHI collisions
Ok(reallocations)
} }
} }