## Summary Implemented fail-fast validation for PHI ordering and value resolution in strict mode. ## Changes ### P1-1: Strict mode for "PHI after terminator" - File: `src/llvm_py/phi_wiring/wiring.py::ensure_phi` - Behavior: `NYASH_LLVM_PHI_STRICT=1` → RuntimeError if PHI created after terminator - Default: Warning only (no regression) ### P1-2: Strict mode for "fallback 0" - File: `src/llvm_py/phi_wiring/wiring.py::wire_incomings` - Behavior: Strict mode forbids silent fallback to 0 (2 locations) - Location 1: Unresolvable incoming value - Location 2: Type coercion failure - Error messages point to next debug file: `llvm_builder.py::_value_at_end_i64` ### P1-3: Connect verify_phi_ordering() to execution path - File: `src/llvm_py/builders/function_lower.py` - Behavior: Verify PHI ordering after all instructions emitted - Debug mode: Shows "✅ All N blocks have correct PHI ordering" - Strict mode: Raises RuntimeError with block list if violations found ## Testing ✅ Test 1: strict=OFF - passes without errors ✅ Test 2: strict=ON - passes without errors (no violations in test fixtures) ✅ Test 3: debug mode - verify_phi_ordering() connected and running ## Scope - LLVM harness (Python) changes only - No new environment variables (uses existing 3 from Phase 277 P2) - No JoinIR/Rust changes (root fix is Phase 279) - Default behavior unchanged (strict mode opt-in) ## Next Steps - Phase 278: Remove deprecated env var support - Phase 279: Root fix - unify "2本のコンパイラ" pipelines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Phase 269: Pattern8 への Frag 適用(P0=test-only → P1=実装)
Status: ✅ 完了(P1) Date: 2025-12-22
サブフェーズ状況
- P1(Pattern8 EdgeCFG lowering): ✅(SSA の
iPHI を含めて閉じた) - P1.1(call_method return type): ✅(署名SSOTで型注釈)
- P1.2(static box の this/me → static call 正規化): ✅(runtime receiver を禁止して SSOT 化)
目的
Pattern8(BoolPredicateScan)を EdgeCFG Fragment(Frag + emit_frag)で実装し、pattern番号の列挙を “exit配線” に収束させる。
- P0: test-only stub + 最小 fixture/smoke で “入口” を固定(DONE)
- P1: 実装(MIR CFG層で Frag を組み、emit_frag で terminator を SSOT 化)(DONE)
実装範囲(重要:スコープ境界)
✅ 触る(P1 スコープ)
src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rssrc/mir/builder/emission/loop_predicate_scan.rs(薄い入口:Frag 構築 + emit_frag)- fixture/smoke(
apps/tests/phase269_p0_pattern8_frag_min.hako,tools/smokes/v2/profiles/integration/apps/phase269_p0_pattern8_frag_vm.sh)
❌ 触らない(P1 スコープ外)
- cf_loop(JoinIR-only hard-freeze)
- merge/EdgeCFG plumbing(Phase 260-268 の SSOT は維持)
- Pattern6/7/9(Phase 269 は Pattern8 に集中)
実装戦略(Phase 268 パターン踏襲)
アーキテクチャ図
pattern8_scan_bool_predicate.rs (Pattern8 層)
↓ P1: emission 入口に委譲
emission/loop_predicate_scan.rs::emit_bool_predicate_scan_edgecfg()
↓ 内部で使用(break/continue 無しなので手配線)
Frag(branches+wires)+ emit_frag()
↓ 最終的に呼び出し
set_branch_with_edge_args() / set_jump_with_edge_args() (Phase 260 SSOT)
P1 実装のポイント(重要)
1) Return を exits に置かない
emit_frag()が emit するのはbranchesとwiresのみ(exitsは “上位に伝搬する未配線”)- したがって early-exit の
return falseはwiresにExitKind::Returnとして入れる(target=Noneを許可)
2) return true は loop 後 AST に任せる
- Frag 経路は “loop の制御” と “early-exit” のみ担当し、関数全体の return は既存 AST lowering に任せる
- P1 の最小は「失敗で return false / それ以外は after に落ちる」
3) SSA: ループ変数 i の PHI が必須
- header の条件評価は毎周回評価されるが、SSA 的には
iの “現在値” を header に合流させる必要がある - 最小の形(header に挿入、先頭に置く):
i_current = phi [i_init, preheader_bb], [i_next, step_bb]- header/body/step は
i_currentを参照 - step で
i_next = i_current + 1を作り、backedge の入力にする
完了確認(P1)
- Pattern8 Frag lower が header に PHI を挿入し、
i_currentをCompare/substring/stepの参照に使用する - integration smoke:
tools/smokes/v2/profiles/integration/apps/phase269_p0_pattern8_frag_vm.shPASStools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_vm.shPASS(回帰なし)
P1.2(DONE): static box の this/me を static call に正規化(runtime receiver 禁止)
目的(SSOT)
static box 内の this.method(...) / me.method(...) を runtime receiver(NewBox / 文字列 receiver)にしない。
compile-time に current_static_box.method/arity の canonical key を構築し、static call へ正規化する。
SSOT / 禁止(再掲)
- SSOT:
comp_ctx.current_static_box(box 名の唯一の出どころ)BoxName.method/arity(canonical key: call_method 署名注釈と共用)
- 禁止:
emit_string("StringUtils")などの文字列レシーバによる by-name 的回避- static box の this/me を
NewBoxで runtime object 化(退行の原因)
実装(責務分離)
src/mir/builder/calls/build.rs- MethodCall の共通入口で
This/Mereceiver を最優先で検出し、static call に正規化する - box 名は
comp_ctx.current_static_boxのみから取り出す(ハードコード禁止)
- MethodCall の共通入口で
src/mir/builder/stmts.rs- static/instance の文脈エラーを Fail-Fast で明確化(誤誘導のメッセージ整理)
src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs- 現状の安全策: static box 文脈の loop は Pattern8 対象外にし、汎用 lowering(Pattern1 等)へ戻す
- 目的: receiver 正規化を “1箇所” に収束させ、Pattern8 が runtime receiver を作る経路を封じる
- 撤去条件: Pattern8 が「正規化後の MethodCall(static call key)」前提で安全に動くことを fixture/smoke で確認できたら、この除外を削除する
検証(fixture/smoke)
apps/tests/phase269_p1_2_this_method_in_loop_min.hakotools/smokes/v2/profiles/integration/apps/phase269_p1_2_this_method_in_loop_vm.sh- 受け入れ条件:
- MIR dump に
const "StringUtils"が receiver として出ない call_method StringUtils.is_digit/1(または同等の static call)になる
- MIR dump に
テスト手順(固定)
cargo build --releasecargo test -p nyash-rust --lib --releaseHAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_vm.shHAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase269_p0_pattern8_frag_vm.shHAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase269_p1_2_this_method_in_loop_vm.sh./tools/smokes/v2/run.sh --profile quick(45/46 を維持)
P0(historical)
P0 の test-only 記録は履歴として残す(入口の固定に寄与)。
検出基準(Phase 259 P0 固定形式)
- Loop condition:
i < s.length() - Loop body has if statement:
- Condition:
not this.method(...)(UnaryOp::Not + MethodCall) - Then branch:
return false(early exit)
- Condition:
- Loop body has step:
i = i + 1 - Post-loop:
return true
既存実装の処理フロー
// 1. Extract pattern parts
let parts = extract_bool_predicate_scan_parts(condition, body)?;
// 2. Get host ValueIds
let s_host = variable_map[haystack];
let i_host = variable_map[loop_var];
let me_host = build_me_expression()?;
// 3. Create JoinModule
let join_module = lower_scan_bool_predicate_minimal(...);
// 4. Build boundary
let boundary = JoinInlineBoundaryBuilder::new()
.with_inputs(join_inputs, host_inputs)
.with_loop_invariants(loop_invariants)
.with_exit_bindings(exit_bindings)
.with_carrier_info(carrier_info)
.with_loop_var_name(Some(loop_var))
.with_expr_result(Some(join_exit_value))
.build();
// 5. Execute JoinIRConversionPipeline
let result = JoinIRConversionPipeline::execute(self, join_module, Some(&boundary), ...)?;
P0 Frag 版 lowerer 設計
目的
- JoinModule から MIR terminator を生成する部分を Frag 化
- 既存の JoinModule 生成・boundary 構築は維持
- emit_frag() による terminator 生成を導入
実装方針
#[cfg(test)]
pub(crate) fn lower_pattern8_frag(
builder: &mut MirBuilder,
join_module: JoinModule,
boundary: &JoinInlineBoundary,
debug: bool,
) -> Result<Option<ValueId>, String> {
// 1. JoinModule から必要な情報を取得
// - loop_step function (loop body)
// - k_exit function (return true)
// 2. Loop body Frag 構築
// - loop_step の処理を Frag で表現
// - early return (return false) は Return exit として扱う
// 3. compose::loop_() で合成
// - loop_id, header, after, body_frag
// 4. emit_frag() で MIR terminator に変換
// 5. result 返却(expr_result)
Ok(boundary.expr_result)
}
Unit Test 設計
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pattern8_frag_lowering() {
// 1. Create minimal JoinModule
// 2. Create boundary
// 3. Call lower_pattern8_frag()
// 4. Verify MIR terminator generation
// - Branch terminator exists
// - Jump terminator exists
// - Return terminator exists (early exit)
}
}
最小 fixture 設計
phase269_p0_pattern8_frag_min.hako
Phase 259 の is_integer_min.hako の縮小版:
static box StringUtils {
is_digit(ch) {
return ch == "0" or ch == "1"
}
is_integer(s) {
if s.length() == 0 {
return false
}
local i = 0
loop(i < s.length()) {
if not this.is_digit(s.substring(i, i + 1)) {
return false
}
i = i + 1
}
return true
}
}
static box Main {
main() {
return StringUtils.is_integer("01") ? 7 : 1
}
}
Smoke Test 設計
tools/smokes/v2/profiles/integration/apps/phase269_p0_pattern8_frag_vm.sh:
#!/bin/bash
set -e
cd "$(dirname "$0")/../../../../../.."
HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}"
set +e
$HAKORUNE_BIN apps/tests/phase269_p0_pattern8_frag_min.hako > /tmp/phase269_out.txt 2>&1
EXIT_CODE=$?
set -e
if [ $EXIT_CODE -eq 7 ]; then
echo "[PASS] phase269_p0_pattern8_frag_vm"
exit 0
else
echo "[FAIL] phase269_p0_pattern8_frag_vm: expected exit 7, got $EXIT_CODE"
cat /tmp/phase269_out.txt
exit 1
fi
重要な設計判断
なぜ test-only か
- 非破壊的: 既存 Pattern8 実装を壊さない
- 段階的: Frag 版の動作を先に確認してから統合
- デバッグ容易: 問題切り分けが簡単(Frag 版 vs 既存実装)
- 拡張性: Pattern6/7 にも同じパターンを適用可能
なぜ最小 fixture か
- 高速検証: 小さいコードで問題を早期発見
- デバッグ容易: MIR dump が読みやすい
- 回帰テスト: quick smoke に含めやすい
P0 での制約
- VM のみ(LLVM は P1 以降)
- Pattern8 のみ(Pattern6/7 は P1 以降)
- test-only(既存実装置換は P1 以降)
次フェーズへの橋渡し
Phase 269 P1+: 既存 Pattern8 を Frag 版に置換
cf_loop_pattern8_bool_predicate_impl()からlower_pattern8_frag()を呼び出す- JoinIRConversionPipeline を廃止(Frag 版に一本化)
- Pattern6/7 にも適用
Phase 270+: Pattern 分岐削減
- Pattern 番号による分岐を削減
- compose API による統一的なループ処理
関連ドキュメント
- Phase 268:
docs/development/current/main/phases/phase-268/README.md - 設計図:
docs/development/current/main/design/edgecfg-fragments.md - JoinIR アーキテクチャ:
docs/development/current/main/joinir-architecture-overview.md - 現在のタスク:
docs/development/current/main/10-Now.md
受け入れ基準(P0)
- ✅
cargo build --release成功 - ✅
cargo test --lib --releaseで全テスト PASS - ✅ Pattern8 Frag 版 lowerer の unit test PASS
- ✅
apps/tests/phase269_p0_pattern8_frag_min.hako作成 - ✅
tools/smokes/v2/profiles/integration/apps/phase269_p0_pattern8_frag_vm.sh作成 - ✅ smoke test PASS(exit code 7)
- ✅
tools/smokes/v2/run.sh --profile quickで 45/46 PASS 維持 - ✅ MIR dump で Branch/Jump/Return terminator 正常生成確認
- ✅ ドキュメント更新完了:
- ✅
phases/phase-269/README.md新規作成(このファイル) - ✅
10-Now.md追記 - ✅
30-Backlog.md更新
- ✅
まとめ
Phase 269 P0 の核心:
- ✅ Pattern8 を Frag 化(test-only、既存と並走)
- ✅ compose::loop_() + emit_frag() を使用
- ✅ 最小 fixture + smoke test で動作確認
- ✅ quick smoke 45/46 を悪化させない
次のステップ: P1 で既存 Pattern8 を Frag 版に置換 → Pattern6/7 にも適用