feat(joinir): L-5.3 Phase 1 - progress carrier guard for generic_case_a

## 実装内容

1. has_safe_progress() helper 追加 (loop_to_join.rs:186-195)
   - Phase 1: scope.progress_carrier.is_some() をチェック (保守的)
   - Phase 2 (future): MirQuery で Add 命令チェック予定

2. is_supported_case_a_loop_view() に progress guard 追加 (256-266)
   - 無限ループの可能性があるループを事前にフォールバック
   - デバッグログで reject 理由を出力

## テスト結果

 25 passed; 0 failed - JoinIR 関連テスト全通過
 skip_ws / trim / append_defs / Stage‑1 UsingResolver 全ケース PASS
 既存テストへの影響なし

## 技術メモ

- 保守的アプローチ: progress_carrier.is_some() のみチェック
- LoopScopeShape で progress_carrier を carriers の先頭として設定済み
- ignored テスト失敗は MIR 自体の PHI バグで、本変更とは無関係 (git stash で確認済み)

関連: Phase 29 L-5.3 (TASKS.md), CURRENT_TASK.md 1-00v
This commit is contained in:
nyash-codex
2025-11-26 16:08:49 +09:00
parent bde83f311e
commit b8893787dc
3 changed files with 215 additions and 17 deletions

View File

@ -40,6 +40,104 @@
## 1. 最近完了した重要タスク ## 1. 最近完了した重要タスク
### 1-00v. Phase 29 L-5.3 — JoinIR generic_case_a との統合 (Phase 1)**完了** 2025-11-26
**目的**
- generic_case_a lowering に progress carrier チェックを追加
- 無限ループの可能性があるループprogress carrier なし)を事前にフォールバック
- 保守的アプローチで minimal 4 ケースの安定性確保
**実装内容**
1. `src/mir/join_ir/lowering/loop_to_join.rs``has_safe_progress()` helper 追加Phase 1 実装)
- `scope.progress_carrier.is_some()` で保守的チェック
- Phase 2 で MirQuery による Add 命令チェック予定
2. `is_supported_case_a_loop_view()` の末尾に progress guard 追加4 番目のチェック)
- progress carrier なしループは reject してフォールバック
- デバッグログで reject 理由を出力
**テスト結果**
-**25 passed; 0 failed** — JoinIR 関連テスト全通過
- ✅ skip_ws / trim / append_defs / Stage1 UsingResolver の全ケース PASS
- ✅ 既存テストへの影響なしprogress carrier が設定されているループは全て通過)
**技術メモ**
- Phase 1 は `progress_carrier.is_some()` だけチェック(保守的)
- LoopScopeShape で progress_carrier を carriers の先頭(典型的には 'i')として設定済み
- ignored テストjoinir_vm_bridge_skip_ws Route Aの失敗は MIR 自体の PHI バグで、本変更とは無関係git stash で確認済み)
**次のステップ**
- Phase 2: MirQuery で header→latch 間に Add 命令があるかの詳細チェック追加
- verify.rs の `verify_progress_for_skip_ws()` ロジックを MIR レベルに適用
**関連ファイル**
- `src/mir/join_ir/lowering/loop_to_join.rs` (has_safe_progress + progress guard)
- `docs/private/roadmap2/phases/phase-29-longterm-joinir-full/TASKS.md` (L-5.3 完了記録)
---
### 1-00u. Phase 32 L-4.3a — llvmlite ハーネスでの JoinIR 実験(**完了** 2025-11-26
**目的**
- LLVM 経路で JoinIR が PHI 問題を解決できることを実行レベルで実証
- Route A (MIR→LLVM) vs Route B (MIR→JoinIR→MIR'→LLVM) の比較検証
**実装内容**
1. `src/config/env.rs``joinir_llvm_experiment_enabled()` 追加
- `NYASH_JOINIR_LLVM_EXPERIMENT=1` で有効化
2. `src/runner/modes/llvm.rs` に JoinIR フック追加
- `inject_method_ids` の直後で JoinIR 変換を試行
- `Main.skip/1` を JoinIR 経由で変換し、元のモジュールとマージ
- 戦略: 元の `Main.skip/1`PHI問題ありを削除 → `join_func_0``Main.skip/1` にリネーム
**テスト結果**
- **Route A** (MIR→LLVM): PHI error ❌
```
PHINode should have one entry for each predecessor of its parent basic block!
%phi_11 = phi i64 [ %.1, %bb2 ], [ %.1, %bb5 ]
```
- **Route B** (MIR→JoinIR→MIR'→LLVM): **成功 ✅**
```
[joinir/llvm] ✅ Merged module (6 functions)
✅ LLVM (harness) execution completed (exit=0)
```
**使用方法**
```bash
env NYASH_PARSER_STAGE3=1 HAKO_PARSER_STAGE3=1 \
NYASH_DISABLE_PLUGINS=1 NYASH_LLVM_USE_HARNESS=1 \
NYASH_JOINIR_EXPERIMENT=1 NYASH_JOINIR_LLVM_EXPERIMENT=1 \
./target/release/hakorune --backend llvm apps/tests/minimal_ssa_skip_ws.hako
```
**成果**: JoinIR が LLVM 経路でも PHI 問題を設計的に解決できることを実証
---
### 1-00t. Phase 32 L-3.2 — Medium 優先度 PHI 箱の削除検討(**完了** 2025-11-26
**目的**
- Medium 優先度の PHI 箱if_phi.rs / if_body_local_merge.rs / phi_invariants.rs / conservative.rsの削除可能性を調査
- 各箱の call site を A/B/C に分類A: テスト専用、B: JoinIR 候補、C: 本線必須)
**調査結果**
1. **if_phi.rs**: 9箇所で使用すべてカテゴリ C
- lifecycle.rs, if_form.rs, phi.rs, conservative.rs, loop_builder.rs で MIR Builder の If/Else PHI 生成に必須
2. **if_body_local_merge.rs**: phi_builder_box.rs で使用PhiBuilderBox に依存)
3. **phi_invariants.rs**: phi_builder_box.rs で検証用として使用PhiBuilderBox に依存)
4. **conservative.rs**: phi_merge.rs, phi.rs で MIR Builder の PHI マージに必須(カテゴリ C
**結論: Phase 32 時点では削除不可**
- すべての箱が MIR Builder の If/Else PHI 生成で必須(カテゴリ C
- if_body_local_merge.rs と phi_invariants.rs は PhiBuilderBoxHigh 優先度)に依存
- JoinIR は **Loop 専門**で、If PHI はカバーしていない
- 削除は **Phase 33+ で If JoinIR 対応後**に再検討
**ドキュメント更新**
- `PHI_BOX_INVENTORY.md` に L-3.2 詳細分析を追記
- 各箱の call site 分類テーブルと削除不可理由を明文化
---
### 1-00v. Phase 31 — LoopToJoinLowerer 統一箱(**完了** 2025-11-26 ### 1-00v. Phase 31 — LoopToJoinLowerer 統一箱(**完了** 2025-11-26
**目的** **目的**
@ -202,6 +300,20 @@ LoopScopeShape → CaseAContext::from_scope() → lower_case_a_X_core() → Join
- `joinir_runner_standalone_skip_ws` / `joinir_runner_standalone_trim` / `joinir_vm_bridge_trim_*` / JSON v0 スナップショット系テストは全て PASS。 - `joinir_runner_standalone_skip_ws` / `joinir_runner_standalone_trim` / `joinir_vm_bridge_trim_*` / JSON v0 スナップショット系テストは全て PASS。
- Stage1 / StageB については、`NYASH_JOINIR_LOWER_GENERIC` 有効時も lowering 自体は通ることを確認(意味論は簡略版のままなので VM 実行はまだ有効化しない)。 - Stage1 / StageB については、`NYASH_JOINIR_LOWER_GENERIC` 有効時も lowering 自体は通ることを確認(意味論は簡略版のままなので VM 実行はまだ有効化しない)。
5. **ExitGroup ベースの Case-A 判定 refinementL-1.4 完了 2025-11-26**
- `src/mir/control_form.rs` に `ExitGroup { target, edges, has_break }` と `ExitAnalysis { loop_exit_groups, nonlocal_exits }` を追加し、ExitEdge の生配列から「出口ブロック単位のグループ」と「非局所 exitReturn/Throw 等)」を切り出すビューを実装。
- `analyze_exits(exits: &[ExitEdge]) -> ExitAnalysis` で:
- `loop_exit_groups`: ループ本体からループ外の同じ after-block へ出る出口グループExitKind::ConditionFalse / Break のみ)
- `nonlocal_exits`: ExitKind::Return / Throw / outer break など非局所 exit
に分類。
- `loop_to_join.rs` の `is_supported_case_a_loop_view` を ExitAnalysis ベースに差し替え:
- 旧ロジック: `control.exits.len() == 1` で単一 ExitEdge を要求していたため、複雑条件 break短絡 && でブロックが分かれる)を Case-A から外していた。
- 新ロジック: `exit_analysis.loop_exit_groups.len() == 1`(出口ブロック集合が 1 種類)かつ `exit_analysis.nonlocal_exits.is_empty()`(非局所 exit なし)であれば Case-A とみなすように変更。
- 効果:
- `if c != ' ' && c != '\t' && c != '\n' { break }` のような複雑条件 break を含むループでも、出口ブロックが 1 種類であれば Case-A として LoopToJoinLowerer/JoinIR lowering の対象にできるようになった。
- Stage1 JoinIR VM bridge テスト 4 件(`joinir_vm_bridge_stage1_*`)は全て PASS を維持。
- skip_ws 実物については、JoinIR 経由の PHI 形状は引き続き正しく表現できているが、VM 実行側では `StepBudgetExceeded`(既知の別問題)が残っているため、挙動は L-4/L-5VM / progress carrier まわり)のフェーズで継続調査する。
**成果物L-2.1: Stage1 UsingResolver 本線ループ)** **成果物L-2.1: Stage1 UsingResolver 本線ループ)**
1. **本線ループを LoopToJoinLowerer 対象に昇格** 1. **本線ループを LoopToJoinLowerer 対象に昇格**
- 対象: `Stage1UsingResolverBox.resolve_for_source/5` 内の entries ループ(`lang/src/compiler/entry/using_resolver_box.hako:46-91`)。 - 対象: `Stage1UsingResolverBox.resolve_for_source/5` 内の entries ループ(`lang/src/compiler/entry/using_resolver_box.hako:46-91`)。
@ -284,7 +396,28 @@ LoopScopeShape → CaseAContext::from_scope() → lower_case_a_X_core() → Join
- 最小ケース `minimal_ssa_skip_ws.hako` で `Main.skip(" abc")` を実行 - 最小ケース `minimal_ssa_skip_ws.hako` で `Main.skip(" abc")` を実行
- Route AVM 直接): 結果 `0` ❌PHI バグで値消失) - Route AVM 直接): 結果 `0` ❌PHI バグで値消失)
- Route BJoinIR: 結果 `Int(3)` ✅(正解) - Route BJoinIR: 結果 `Int(3)` ✅(正解)
- L-3 / L-4: これからPHI レガシー削除とJoinIRVM/LLVM 前提ランナー構造への仕上げ)。 - L-3.1 Step-1: **完了2025-11-26** — PHI レガシー箱の call site 分析
- PHI_BOX_INVENTORY.md を Phase 32 向けに更新(削除優先度 High/Medium/Low 分類)
- PhiBuilderBox / PhiInputCollector / LoopSnapshotMergeBox の call site を A/B/C 分類
- 結果: カテゴリ Aテスト専用はコメント参照 1 箇所のみ
- L-3.1 Step-2: **完了2025-11-26** — PHI 経路削除可能性分析
- loopform_builder.rs: PhiInputCollector × 3, LoopSnapshotMergeBox × 1 → 非 JoinIR 経路で必須(削除不可)
- json_v0_bridge/loop_.rs: PhiInputCollector × 1 → selfhost 経路で必須(削除不可)
- **結論**: JoinIR は既に PHI 箱をバイパス。削除は L-4JoinIR 本線化)完了後
- L-3.2 / L-3.3: これからJoinIR 本線化後に PHI 箱削除)。
- L-4.1/L-4.2: **完了2025-11-26** — VM Bridge テーブル化 & 本線明示
- `join_ir_vm_bridge_dispatch.rs` に Descriptor テーブル(`JOINIR_TARGETS`)を導入し、関数名→役割のマッピングを一元管理。
- `JoinIrBridgeKind` 列挙型: **Exec**JoinIR→VM 実行まで対応)と **LowerOnly**lowering 検証のみ、実行は VM Route Aを明確に区別。
- 対象関数ごとの状態:
| 関数 | Kind | デフォルト有効 | 状態 |
|-----|------|---------------|------|
| `Main.skip/1` | Exec | ❌ | PHI canary のため本線化しないenv 必須) |
| `FuncScannerBox.trim/1` | Exec | ✅ | **唯一の本線昇格候補**A/B 実証済み) |
| `Stage1UsingResolverBox.resolve_for_source/5` | LowerOnly | ❌ | 構造検証のみ |
| `StageBBodyExtractorBox.build_body_src/2` | LowerOnly | ❌ | 構造検証のみ |
| `StageBFuncScannerBox.scan_all_boxes/1` | LowerOnly | ❌ | 構造検証のみ |
- **結論**: 現状は **trim だけが「JoinIR 実行まで含めて安全」として昇格候補**。skip は PHI canary として残し、Stage-1/Stage-B は ArrayBox/MapBox 引数の JoinValue 対応が揃うまで LowerOnly。
- L-4.3: これからLLVM ライン JoinIR 実験: まず llvmlite ハーネスで JoinIR→MIR'→LLVM の最小ケース 1 本を通し、その後 llvmc 側にミラーする計画。いずれも dev トグル前提の実験扱い)。
--- ---

View File

@ -15,7 +15,7 @@
//! let join_module = lowerer.lower(func, &loop_form, &query)?; //! let join_module = lowerer.lower(func, &loop_form, &query)?;
//! ``` //! ```
use crate::mir::control_form::{LoopControlShape, LoopId, LoopRegion}; use crate::mir::control_form::{analyze_exits, ExitEdge, LoopControlShape, LoopId, LoopRegion};
use crate::mir::join_ir::lowering::generic_case_a; use crate::mir::join_ir::lowering::generic_case_a;
use crate::mir::join_ir::lowering::loop_form_intake::intake_loop_form; use crate::mir::join_ir::lowering::loop_form_intake::intake_loop_form;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
@ -123,8 +123,8 @@ impl LoopToJoinLowerer {
); );
} }
// Phase 32 L-1.1: View ベースの Case-A サポートチェック(構造判定強化) // Phase 32 L-1.4: ExitAnalysis ベースの Case-A サポートチェック
if !self.is_supported_case_a_loop_view(func, &region, &control, &scope) { if !self.is_supported_case_a_loop_view(func, &region, &control, &exit_edges, &scope) {
if self.debug { if self.debug {
eprintln!( eprintln!(
"[LoopToJoinLowerer] rejected by view-based check: {:?}", "[LoopToJoinLowerer] rejected by view-based check: {:?}",
@ -158,22 +158,61 @@ impl LoopToJoinLowerer {
self.lower_with_scope(scope, func_name) self.lower_with_scope(scope, func_name)
} }
/// Phase 29 L-5.3: Progress carrier の安全性をチェック
///
/// 無限ループの可能性があるループprogress carrier が無い、または更新されない)
/// を事前に弾く。
///
/// # Phase 1 実装(保守的)
///
/// - `scope.progress_carrier.is_some()` をチェック
/// - progress_carrier が設定されていればループは進捗すると仮定
///
/// # Phase 2 (future)
///
/// - MirQuery で header→latch 間に Add 命令があるかチェック
/// - skip_ws verifier のロジックを MIR レベルで簡略化して適用
///
/// # Arguments
///
/// - `scope`: LoopScopeShapeprogress_carrier 情報を持つ)
/// - `func`: MIR 関数(将来の MirQuery 用)
/// - `region`: LoopRegion将来の header→latch チェック用)
///
/// # Returns
///
/// - `true`: Progress carrier ありsafe
/// - `false`: Progress carrier なしunsafe、fallback すべき)
fn has_safe_progress(
scope: &LoopScopeShape,
_func: &MirFunction, // Phase 2 で使用予定
_region: &LoopRegion, // Phase 2 で使用予定
) -> bool {
// Phase 1: 保守的チェック
// progress_carrier が設定されていれば、ループは進捗すると仮定
// (典型的には 'i' のような loop index
scope.progress_carrier.is_some()
}
/// Case-A ループとしてサポートされているかチェックView ベース版) /// Case-A ループとしてサポートされているかチェックView ベース版)
/// ///
/// Phase 32 L-1.2: 純粋な構造チェックのみ(関数名フィルタは lower() 側で適用) /// Phase 32 L-1.4: ExitGroup ベースの判定に強化
/// ///
/// # Case-A の定義 /// # Case-A の定義
/// ///
/// - 単一出口exits が 1 箇所以下) /// - **単一出口グループ**: 全ての出口辺が同じターゲットブロックに向かう
/// (例: `if c != ' ' && c != '\t' { break }` は複数 ExitEdge だが、
/// 同じ exit ブロックに向かうので Case-A として許可)
/// - **非ローカル出口なし**: Return/Throw がない
/// - ヘッダブロックの succ が 2 つcond true/false /// - ヘッダブロックの succ が 2 つcond true/false
/// - ループ変数または固定変数が存在 /// - ループ変数または固定変数が存在
/// - ネストループなし(将来チェック追加予定)
/// ///
/// # Arguments /// # Arguments
/// ///
/// - `func`: MIR 関数(ヘッダ succ チェック用) /// - `func`: MIR 関数(ヘッダ succ チェック用)
/// - `region`: LoopRegionブロック構造 /// - `region`: LoopRegionブロック構造
/// - `control`: LoopControlShape制御フロー辺 /// - `control`: LoopControlShape制御フロー辺
/// - `exit_edges`: ExitEdge のリスト(グループ化分析用)
/// - `scope`: LoopScopeShape変数分類用 /// - `scope`: LoopScopeShape変数分類用
/// ///
/// # Returns /// # Returns
@ -184,25 +223,39 @@ impl LoopToJoinLowerer {
&self, &self,
func: &MirFunction, func: &MirFunction,
region: &LoopRegion, region: &LoopRegion,
control: &LoopControlShape, _control: &LoopControlShape, // Phase 32 L-1.4: ExitEdge ベースに移行したため未使用
exit_edges: &[ExitEdge],
scope: &LoopScopeShape, scope: &LoopScopeShape,
) -> bool { ) -> bool {
// Phase 32 L-1.2: 純粋な構造チェックのみ // Phase 32 L-1.4: ExitAnalysis ベースの出口判定
// 関数名フィルタは lower() 側で generic_case_a_enabled() に応じて適用 let exit_analysis = analyze_exits(exit_edges);
// 1) 単一出口 (Case-A 限定: exits が 1 箇所以下) // 1) 単一出口グループ + 非ローカル出口なし
// Note: break_targets ベースなので、条件 false による自然な出口は含まない // 複数の ExitEdge でも、同じターゲットに向かうなら Case-A として許可
// 将来 Case-B/C で複数出口を許可する場合はここを緩める if !exit_analysis.is_single_exit_group() {
if control.exits.len() > 1 {
if self.debug { if self.debug {
eprintln!( eprintln!(
"[LoopToJoinLowerer] rejected: multiple exits ({}) - Case-A requires single exit", "[LoopToJoinLowerer] rejected: not single exit group (groups={}, nonlocal={})",
control.exits.len() exit_analysis.loop_exit_groups.len(),
exit_analysis.nonlocal_exits.len()
); );
// 詳細ログ: 各グループのターゲットを出力
for (i, group) in exit_analysis.loop_exit_groups.iter().enumerate() {
eprintln!(
" group[{}]: target={:?}, edges={}, has_break={}",
i,
group.target,
group.edges.len(),
group.has_break
);
}
} }
return false; return false;
} }
// Note: control.exits は ExitEdge の数(辺の数)なので、
// 複数でも単一グループなら OK という新しいロジック
// 2) ヘッダブロックの succ が 2 つcond true → body, cond false → exit // 2) ヘッダブロックの succ が 2 つcond true → body, cond false → exit
// これにより while(cond) 形式のループのみを対象とする // これにより while(cond) 形式のループのみを対象とする
if let Some(header_block) = func.blocks.get(&region.header) { if let Some(header_block) = func.blocks.get(&region.header) {
@ -236,6 +289,18 @@ impl LoopToJoinLowerer {
return false; return false;
} }
// 4) Phase 29 L-5.3: Progress carrier チェック
// 無限ループの可能性があるループを事前に弾く
if !Self::has_safe_progress(scope, func, region) {
if self.debug {
eprintln!(
"[LoopToJoinLowerer] rejected: no safe progress carrier (progress_carrier={:?})",
scope.progress_carrier
);
}
return false;
}
true true
} }