diff --git a/CLAUDE.md b/CLAUDE.md
index 074c0cd8..b80c3ef8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -19,11 +19,17 @@
#### 🔍 **MIRデバッグ完全ガイド**(超重要!)
```bash
-# 基本MIR確認(最優先!)
-./target/release/hakorune --dump-mir program.hako
-NYASH_VM_DUMP_MIR=1 ./target/release/hakorune program.hako
+# 基本MIR確認(最優先! / 実行経路SSOT)
+# - VM実行経路で「実際に走るMIR」を見る(推奨)
+NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm program.hako
-# 詳細MIR + エフェクト情報
+# 詳細MIR + エフェクト情報(実行経路SSOT)
+NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm --mir-verbose --mir-verbose-effects program.hako
+
+# 参考: コンパイルだけでMIRを見る(実行しない / 入口確認用)
+# - 実行経路SSOT(最適化/検証/バックエンド差分)を追う目的では、上の NYASH_VM_DUMP_MIR を優先
+# - `--dump-mir` は “compile-only” のため、実行時の挙動や backend 差分確認の主導線にはしない
+./target/release/hakorune --dump-mir program.hako
./target/release/hakorune --dump-mir --mir-verbose --mir-verbose-effects program.hako
# JSON形式で詳細解析
@@ -574,11 +580,11 @@ src/runner/modes/common_util/resolve/strip.rs # コード生成
./target/release/hakorune apps/tests/string_ops_basic.hako # StringBox
# MIR確認用テスト
-./target/release/hakorune --dump-mir apps/tests/loop_min_while.hako
-./target/release/hakorune --dump-mir apps/tests/esc_dirname_smoke.hako
+NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm apps/tests/loop_min_while.hako
+NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm apps/tests/esc_dirname_smoke.hako
# 統一Call テスト(Phase A完成!)
-NYASH_MIR_UNIFIED_CALL=1 ./target/release/hakorune --dump-mir test_simple_call.hako
+NYASH_MIR_UNIFIED_CALL=1 NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm test_simple_call.hako
NYASH_MIR_UNIFIED_CALL=1 ./target/release/hakorune --emit-mir-json test.json test.hako
```
diff --git a/CODEX_QUESTION.md b/CODEX_QUESTION.md
index 8d83bb54..39bfcd67 100644
--- a/CODEX_QUESTION.md
+++ b/CODEX_QUESTION.md
@@ -92,7 +92,7 @@ NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1 ./target/release/hakorune test_stringbox.hako
NYASH_CLI_VERBOSE=1 ./target/release/hakorune test_stringbox.hako
# MIRダンプ確認
-./target/release/hakorune --dump-mir test_stringbox.hako
+NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm test_stringbox.hako
# 具体的な問題箇所の確認
rg "M_BIRTH" plugins/nyash-string-plugin/src/lib.rs # 該当箇所を特定
diff --git a/apps/tests/phase274_p1_typeop_is_as_min.hako b/apps/tests/phase274_p1_typeop_is_as_min.hako
new file mode 100644
index 00000000..6d24e47f
--- /dev/null
+++ b/apps/tests/phase274_p1_typeop_is_as_min.hako
@@ -0,0 +1,28 @@
+// Phase 274 P1: TypeOp (is/as) runnable on Rust VM
+
+box Foo {
+ birth() { }
+}
+
+static box Main {
+ main() {
+ // Primitive check + cast
+ local x = 1
+ if !x.is("Integer") { return 101 }
+ local y = x.as("Integer")
+ if y != 1 { return 102 }
+
+ // String check
+ local s = "a"
+ if !s.is("String") { return 103 }
+
+ // User-defined box check + cast
+ local f = new Foo()
+ if !f.is("Foo") { return 104 }
+ local g = f.as("Foo")
+ if !g.is("Foo") { return 105 }
+
+ return 3
+ }
+}
+
diff --git a/apps/tests/phase274_p2_typeop_dynamic.hako b/apps/tests/phase274_p2_typeop_dynamic.hako
new file mode 100644
index 00000000..a6d70bc0
--- /dev/null
+++ b/apps/tests/phase274_p2_typeop_dynamic.hako
@@ -0,0 +1,29 @@
+// Phase 274 P2: TypeOp (is/as) with dynamic values (prevents constant folding)
+
+static box Main {
+ // Helper function that takes a parameter (prevents constant folding)
+ check_integer(x) {
+ if !x.is("Integer") { return 101 }
+ local y = x.as("Integer")
+ if y != x { return 102 }
+ return 0
+ }
+
+ check_string(s) {
+ if !s.is("String") { return 103 }
+ local t = s.as("String")
+ if t != s { return 104 }
+ return 0
+ }
+
+ main() {
+ // Use function calls to prevent constant folding
+ local r1 = me.check_integer(42)
+ if r1 != 0 { return r1 }
+
+ local r2 = me.check_string("hello")
+ if r2 != 0 { return r2 }
+
+ return 3
+ }
+}
diff --git a/apps/tests/phase274_p2_typeop_primitives_only.hako b/apps/tests/phase274_p2_typeop_primitives_only.hako
new file mode 100644
index 00000000..2d9f4acd
--- /dev/null
+++ b/apps/tests/phase274_p2_typeop_primitives_only.hako
@@ -0,0 +1,19 @@
+// Phase 274 P2: TypeOp (is/as) with primitives only (no user boxes)
+
+static box Main {
+ main() {
+ // Integer check + cast
+ local x = 42
+ if !x.is("Integer") { return 101 }
+ local y = x.as("Integer")
+ if y != 42 { return 102 }
+
+ // String check + cast
+ local s = "hello"
+ if !s.is("String") { return 103 }
+ local t = s.as("String")
+ if t != "hello" { return 104 }
+
+ return 3
+ }
+}
diff --git a/apps/tests/phase274_p2_typeop_union_no_fold.hako b/apps/tests/phase274_p2_typeop_union_no_fold.hako
new file mode 100644
index 00000000..3bc04231
--- /dev/null
+++ b/apps/tests/phase274_p2_typeop_union_no_fold.hako
@@ -0,0 +1,26 @@
+// Phase 274 P2: TypeOp (is/as) on LLVM harness
+// Goal: prevent compile-time folding by creating a runtime-unknown union value.
+
+static box Main {
+ main() {
+ // Runtime-unknown condition: depends on script args.
+ // The CLI writes script args into env as JSON when invoked with `-- ...`.
+ // (The smoke passes one arg to force the "then" path at runtime.)
+ local args_json = env.env.get("NYASH_SCRIPT_ARGS_JSON")
+
+ local x
+ if args_json {
+ x = 1
+ } else {
+ x = "nope"
+ }
+
+ // Must remain as TypeOp at MIR level (not constant-folded).
+ if !x.is("Integer") { return 101 }
+
+ local y = x.as("Integer")
+ if y != 1 { return 102 }
+
+ return 3
+ }
+}
diff --git a/apps/tests/phase275_p0_eq_number_only_min.hako b/apps/tests/phase275_p0_eq_number_only_min.hako
new file mode 100644
index 00000000..19ee2ec6
--- /dev/null
+++ b/apps/tests/phase275_p0_eq_number_only_min.hako
@@ -0,0 +1,20 @@
+// Phase 275 P0: Test B2 - Number-only equality (Bool↔Int removed, precise Int↔Float)
+// Goal: Demonstrate precise equality semantics
+
+static box Main {
+ main() {
+ // Int↔Float exact comparison
+ local a = (1 == 1.0) // true (exact)
+ local b = (1 == 1.1) // false (non-integral)
+
+ // Bool↔Int no coercion (now false, was true)
+ local c = (true == 1) // false (no coercion)
+ local d = (false == 0) // false (no coercion)
+
+ if a and not b and not c and not d {
+ return 3 // Success
+ }
+
+ return 1 // Failure
+ }
+}
diff --git a/apps/tests/phase275_p0_plus_number_only_min.hako b/apps/tests/phase275_p0_plus_number_only_min.hako
new file mode 100644
index 00000000..e16fa3e4
--- /dev/null
+++ b/apps/tests/phase275_p0_plus_number_only_min.hako
@@ -0,0 +1,19 @@
+// Phase 275 P0: Test C2 - Plus with number-only promotion
+// Goal: Int+Float promotion, String+String only
+
+static box Main {
+ main() {
+ // Number promotion
+ local a = 1 + 2.0 // 3.0 (Float)
+ local b = 2.5 + 1 // 3.5 (Float)
+
+ // String concat
+ local c = "a" + "b" // "ab"
+
+ if a == 3.0 and b == 3.5 and c == "ab" {
+ return 3 // Success
+ }
+
+ return 1 // Failure
+ }
+}
diff --git a/apps/tests/phase275_p0_truthiness_void_error_min.hako b/apps/tests/phase275_p0_truthiness_void_error_min.hako
new file mode 100644
index 00000000..65594c4b
--- /dev/null
+++ b/apps/tests/phase275_p0_truthiness_void_error_min.hako
@@ -0,0 +1,20 @@
+// Phase 275 P0: Test A1 - Void in boolean context → TypeError
+// Goal: Demonstrate Void in condition is a runtime error (fail-fast)
+
+static box Main {
+ _nop() {
+ // Returns Void implicitly
+ }
+
+ main() {
+ local v = me._nop() // v is Void
+
+ // ❌ This should fail (Void in if condition)
+ if v {
+ print("ERROR: Void was truthy")
+ return 1
+ }
+
+ return 0 // Should not reach here
+ }
+}
diff --git a/crates/nyash-llvm-compiler/src/main.rs b/crates/nyash-llvm-compiler/src/main.rs
index d0cb9565..d55f2af6 100644
--- a/crates/nyash-llvm-compiler/src/main.rs
+++ b/crates/nyash-llvm-compiler/src/main.rs
@@ -376,8 +376,13 @@ fn link_executable(
let libnyrt = nyrt_dir.join("libnyash_kernel.a");
if !libnyrt.exists() {
bail!(
- "libnyash_kernel.a not found in {} (use --nyrt to specify)",
- nyrt_dir.display()
+ "libnyash_kernel.a not found in {}.\n\
+ hint: build the kernel staticlib first:\n\
+ cargo build --release -p nyash_kernel\n\
+ expected output (workspace default): target/release/libnyash_kernel.a\n\
+ or pass an explicit directory via --nyrt
.\n\
+ note: the llvmlite harness path (NYASH_LLVM_USE_HARNESS=1) does not need libnyash_kernel.a.",
+ nyrt_dir.display(),
);
}
diff --git a/crates/nyash_kernel/README.md b/crates/nyash_kernel/README.md
index 56f460ae..31caa2ed 100644
--- a/crates/nyash_kernel/README.md
+++ b/crates/nyash_kernel/README.md
@@ -105,9 +105,15 @@ else:
```bash
# Build with LLVM integration
cargo build --release -p nyash_kernel
-# Output: crates/nyash_kernel/target/release/libnyash_kernel.a
+# Output (workspace default):
+# target/release/libnyash_kernel.a
+# Note: if you set `CARGO_TARGET_DIR`, the output is under `$CARGO_TARGET_DIR/release/`.
```
+Notes:
+- `libnyash_kernel.a` is required for **native executable linking** (AOT/`--emit-exe`/`ny-llvmc --emit exe`).
+- The Python llvmlite **harness** path (`NYASH_LLVM_USE_HARNESS=1`) does not require the static library.
+
### For VM Backend
```bash
# Runtime integration (automatic)
diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md
index d7fb984f..c021e71c 100644
--- a/docs/development/current/main/10-Now.md
+++ b/docs/development/current/main/10-Now.md
@@ -1,6 +1,26 @@
# Self Current Task — Now (main)
-## 2025-12-22: Phase 277(P0/P1)— Phase 275/276 残タスク完全実装 ✅
+## Current Focus (next)
+
+- Phase 277 P0/P1(docs+validation): `docs/development/current/main/phases/phase-277/README.md`
+ - PHI型推論の導線/責務/SSOT を docs に固定(Phase 275/276 の実装を「読める形」にする)
+ - PHI順序(PHI → non-PHI → terminator)検証の fail-fast を強化
+- Phase 278(cleanup): `docs/development/current/main/phases/phase-278/README.md`
+ - Phase 277 P2 の後方互換(旧PHI env var)を撤去して、1セットに収束させる
+- Phase 279(impl): `docs/development/current/main/phases/phase-279/README.md`
+ - “2本のコンパイラ” にならないように、型伝播パイプラインの入口/順序を SSOT で一本化する
+- Phase 273(design-first): `docs/development/current/main/30-Backlog.md`
+ - Pattern を Plan Extractor(pure)へ降格し、`Plan → Frag → emit_frag()` に収束させる
+
+## Recently Completed (2025-12-22)
+
+- Phase 275 P0(A1/B2/C2 coercion SSOT): `docs/development/current/main/phases/phase-275/README.md`
+- Phase 276 P0(quick wins / type_helper SSOT): `docs/development/current/main/phases/phase-276/README.md`
+- Phase 277 P2(PHI env var 統合): `docs/development/current/main/phases/phase-277/README.md`
+
+---
+
+## 2025-12-22: Follow-ups(post Phase 275/276/277)
- 目的: Phase 275/276 で積み残した改善タスクを完全実装(デッドコード削除・SSOT使用推進・Void検出)
- 達成内容:
@@ -19,7 +39,7 @@
- ✅ **LLVM Smoke Tests 完全実施**:
- Test 1 (simple Int+Float): ✅ PASS (exit=3, VM/LLVM parity)
- Test 2 (two Int+Float ops): ✅ PASS (exit=3, VM/LLVM parity)
- - Test 3 (Float + String): ⚠️ exit=0 (String 問題は Phase 275 範囲外)
+ - Test 3 (Float + String): C2 では **TypeError** が期待(文字列混在 `+` は禁止)。ここが通るならバグとして扱う
- 効果:
- Float PHI 完全動作(VM/LLVM parity 達成)
- SSOT 原則完全適用(型変換・環境変数)
@@ -80,9 +100,9 @@
### 過去の Blocker: 型伝播パイプラインの二重化(lifecycle vs JoinIR)
- 現状、型伝播/PHI 型解決の順序が経路により異なり、同一 fixture が別ルートで壊れ得る(実質 "2本のコンパイラ")。
-- 対処(SSOT): Phase 276 P0 で型取得ロジックをSSOT化(部分対応完了)
-- Phase 276 本体: type propagation pipeline 完全統一(長期計画)
-- 予定: `docs/development/current/main/phases/phase-276/README.md`
+- 対処(SSOT, short-term): Phase 276 P0 で型取得ロジックをSSOT化(部分対応)
+- 根治(SSOT, long-term): Phase 279 で type propagation pipeline の入口/順序を完全統一
+- 予定: `docs/development/current/main/phases/phase-279/README.md`
## 2025-12-22:Phase 274(P1)— TypeOp(is/as)を Rust VM で実行可能にする ✅
diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md
index 2368835e..c46cf911 100644
--- a/docs/development/current/main/30-Backlog.md
+++ b/docs/development/current/main/30-Backlog.md
@@ -8,21 +8,63 @@ Related:
## 直近(JoinIR/selfhost)
+- **Phase 277 P0/P1(planned, docs+validation): PHI型推論ドキュメント整備 + PHI順序検証強化**
+ - 入口: `docs/development/current/main/phases/phase-277/README.md`
+ - 目的:
+ - Phase 275/276 で入った PHI 型推論の “導線/責務/SSOT” を docs に固定する
+ - PHI 配置順序(PHI → non-PHI → terminator)違反を fail-fast で検出しやすくする
+
+- **Phase 278(planned, cleanup): PHI旧環境変数の後方互換性削除**
+ - 目的: Phase 277 P2 で deprecated 扱いにした旧 env var を削除し、1セットに収束させる
+ - 入口: `docs/development/current/main/phases/phase-278/README.md`
+ - 実装ガイド: `docs/development/current/main/phases/phase-278/P0-INSTRUCTIONS.md`
+
+- **Phase 279(planned, impl): Type propagation pipeline SSOT 統一(lifecycle / JoinIR / LLVM の二重化解消)**
+ - 目的: 型伝播(Copy/BinOp/PHI/Compare など)の “順序/入口” を 1 本に固定し、経路差による二重バグを根絶する
+ - 入口: `docs/development/current/main/phases/phase-279/README.md`
+ - 実装ガイド: `docs/development/current/main/phases/phase-279/P0-INSTRUCTIONS.md`
+
+- **Phase 272(✅ complete): Pattern6/7 を Frag+emit_frag へ吸収(段階適用)**
+ - 目的: scan系 loop の CFG 構築を `Frag/ExitKind` 合成へ寄せ、pattern列挙の増殖を止める
+ - 完了: P0.1(Pattern6)✅ + P0.2(Pattern7)✅
+ - 入口: fixture/smoke を SSOT として固定(Pattern6→Pattern7 の順で段階適用)
+ - 詳細: `phases/phase-272/README.md`
+
+- **Phase 273(planned, design-first): Pattern → Plan Extractor(pure)→ PlanLowerer で収束**
+ - 目的: pattern の裾広がりを止め、`Plan → Frag → emit_frag()` の本線へ一本化する(terminator SSOT は維持)
+ - 相談メモ: `docs/development/current/main/investigations/phase-272-frag-plan-architecture-consult.md`
+ - 受け入れ(最小):
+ - extractor が builder を触らない(ID採番/PHI挿入禁止)
+ - Plan 語彙を固定(`seq/if/loop/exit/effect/let`)
+ - PlanLowerer が block/value/phi を作る唯一の箱になる
+
+
+- **Phase 274(active, design-first): Type SSOT Alignment(local + dynamic runtime)**
+ - 入口SSOT: `docs/reference/language/types.md`
+ - P1(✅完了): Rust VM が `TypeOp(Check/Cast)` を実行可能(`is/as` が動く)
+ - P2(✅完了): LLVM ライン(llvmlite harness)の `TypeOp` を SSOT に合わせる
+ - P3(decision): truthiness / equality / compare / `+` の coercion を SSOT として固定(必要なら “仕様変更フェーズ” を追加)
+ - 詳細: `phases/phase-274/README.md`
+
- **Phase 270(✅ 完了): JoinIR-only minimal loop SSOT**
- `apps/tests/phase270_p0_loop_min_const.hako` + VM smoke で “最小 const loop” を固定(exit=3)
- Pattern1 は test-only stub のため不適合 → Pattern9(AccumConstLoop)を橋渡しとして追加
- 詳細: `phases/phase-270/README.md`
-- **Phase 271(planned, docs-only): Bridge pattern 撤去条件SSOT**
+- **Phase 271(✅ 完了, docs-only): Bridge pattern 撤去条件SSOT**
- 対象: bridge pattern(例: `Pattern9_AccumConstLoop`)
- 目的: 「汎用化しない」「Frag 合成へ吸収して削除する」を SSOT 化
+ - 成果物:
+ - `docs/development/current/main/design/edgecfg-fragments.md` の `Bridge patterns(撤去条件SSOT)` に “bridge contract” テンプレを追加
+ - `Pattern9_AccumConstLoop` の撤去条件(fixture/smoke/手順)を同セクションに明文化
- SSOT: `docs/development/current/main/design/edgecfg-fragments.md`
-- **Phase 269 P1(in progress): Pattern8 を EdgeCFG で実装(SSA を閉じる)**
+- **Phase 269 P1(✅ 完了): Pattern8 を EdgeCFG で実装(SSA を閉じる)**
- 方針: emission 入口で Frag 構築(break/continue 無しなので `compose::loop_()` は使わず手配線)
- - 残件: header に `i` の PHI を追加して SSA を閉じる(`i_current = phi [i_init, preheader], [i_next, step]`)
+ - 完了: header に `i` の PHI を追加して SSA を閉じた(`i_current = phi [i_init, preheader], [i_next, step]`)
- early-exit の `return false` は Return wire、`return true` は loop 後 AST に任せる
- Pattern8 の返り値は当面 `void`(loop-statement 扱い)
+ - 補足(DONE): static box の `this/me` は MethodCall 共通入口で static call に正規化済み(Pattern8 は現状 static box 文脈を対象外)
- 詳細: `phases/phase-269/README.md`
- **Phase 270+(planned): Pattern6/7 への Frag 適用**
diff --git a/docs/development/current/main/design/edgecfg-fragments.md b/docs/development/current/main/design/edgecfg-fragments.md
index c67fa6d8..35d42dd6 100644
--- a/docs/development/current/main/design/edgecfg-fragments.md
+++ b/docs/development/current/main/design/edgecfg-fragments.md
@@ -146,12 +146,41 @@ Frag = { entry_block, exits: Map> }
- 原則:
- bridge pattern は **汎用化しない**(固定形SSOT + fixture/smoke で仕様を固定するだけ)。
- 将来は `Frag/ExitKind` 合成側へ **吸収して削除**する前提で追加する。
-- 撤去条件(最低限):
- 1. loop の EdgeCFG 実戦投入が 1 箇所以上済み(BasicBlockId 層で `Frag + emit_frag` を使っている)
- 2. bridge pattern の fixture が “Frag 合成経路” で PASS する
- 3. quick/integration の FAIL 位置が悪化しないことを確認済み
-- 撤去手順(最小):
- - router から bridge pattern を外す → fixture/smoke で PASS 維持 → ファイル削除
+
+### Bridge contract(テンプレ / SSOT)
+
+bridge pattern を追加する場合は、最低限この “撤去条件” を先に書く(書けないなら追加しない)。
+
+- **固定する fixture/smoke(SSOT)**
+ - fixture(最小)と smoke(integration)を必ず紐づける
+ - 「何が通れば撤去できるか」を machine-checkable にする
+- **置換先(吸収先)の SSOT がある**
+ - Pattern番号列挙の反対側に、必ず “吸収先” を書く(例: `Frag/ExitKind` 合成、もしくは emission 入口)
+ - 吸収先が未確定な場合でも “層” は確定させる(pattern層にロジックを増やさない)
+- **撤去条件(最低限)**
+ 1. 置換先(吸収先)で同じ fixture/smoke が PASS する
+ 2. bridge pattern 依存の分岐が router から消せる(最小差分で削除できる)
+ 3. quick/integration の FAIL 位置が悪化しない(既知Failは増やさない)
+- **撤去手順(最小)**
+ - router から bridge pattern を外す
+ - fixture/smoke(+ quick)で PASS 維持
+ - ファイル削除(または historical へ隔離)し、SSOT から参照を外す
+
+### Phase 271: `Pattern9_AccumConstLoop` 撤去条件(SSOT)
+
+Phase 270 の “JoinIR-only minimal loop” を通すための橋渡し。将来は Frag 合成側へ吸収して削除する。
+
+- **固定 fixture/smoke**
+ - fixture: `apps/tests/phase270_p0_loop_min_const.hako`(exit=3)
+ - smoke: `tools/smokes/v2/profiles/integration/apps/phase270_p0_loop_min_const_vm.sh`
+- **吸収先(層)**
+ - Structured→CFG lowering 層(`Frag/ExitKind` 合成)またはその emission 入口(pattern層は extractor に縮退)
+- **撤去条件**
+ 1. 上記 fixture/smoke が、bridge pattern を使わない経路で PASS する(Frag/emit_frag 側で loop を構築できる)
+ 2. Pattern9 が router から削除されても coverage が落ちない(同 fixture が同じルートで通る)
+ 3. `tools/smokes/v2/run.sh --profile quick` が悪化しない
+- **撤去手順**
+ - Pattern9 の router 分岐を削除 → smoke PASS → Pattern9 実装を削除(または historical 化)
## 実装入口(コード SSOT)
diff --git a/docs/development/current/main/investigations/phase-272-frag-plan-architecture-consult.md b/docs/development/current/main/investigations/phase-272-frag-plan-architecture-consult.md
new file mode 100644
index 00000000..f179c695
--- /dev/null
+++ b/docs/development/current/main/investigations/phase-272-frag-plan-architecture-consult.md
@@ -0,0 +1,186 @@
+Status: Active
+Date: 2025-12-22
+Scope: Phase 272 の設計相談(この Markdown だけで完結する要約 + 質問集)
+Audience: 外部相談(ChatGPT Pro 等)向け
+
+# Phase 272: Pattern 裾広がりを止める設計相談(Frag + Plan 収束)
+
+## 相談の目的(結論)
+
+`Frag + emit_frag()`(terminator SSOT)は導入できたが、上流が “Pattern 群” のままだと **loop 形が増えるたびに pattern 実装が裾広がり**になりやすい。
+Phase 272(Pattern6/7 を Frag へ段階移行)のタイミングで、**pattern を「Plan 抽出」に降格**し、lowering の本線を収束させたい。
+
+この文書は、外部相談先に渡して「設計の方向性」「最小の収束案」「段階移行の手順」をレビューしてもらうためのもの。
+
+---
+
+## 現状(2025-12-22)
+
+### 参照SSOT(コード/設計)
+
+- 設計(Structured→CFG 合成SSOT候補): `docs/development/current/main/design/edgecfg-fragments.md`
+- Phase 272(実装チェックリスト): `docs/development/current/main/phases/phase-272/README.md`
+- loop pattern router(優先順位SSOT): `src/mir/builder/control_flow/joinir/patterns/router.rs`
+- Frag API(型/emit SSOT): `src/mir/builder/control_flow/edgecfg/api/`
+ - `emit_frag()`(terminator SSOT): `src/mir/builder/control_flow/edgecfg/api/emit.rs`
+- 参照実装(Frag 経路が既に動作):
+ - Pattern8: `src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs`
+ - Pattern8 emission: `src/mir/builder/emission/loop_predicate_scan.rs`
+
+### Phase 269(完了): Pattern8 が Frag 経路で動作中(参照実装)
+
+Pattern8 は “pattern 層で block/value を構築” し、terminator は `Frag + emit_frag()` に集約している。
+
+- block を明示割り当て(`next_block_id()`)
+- PHI は `insert_phi_at_head_spanned()` で header 先頭へ(SSA を閉じる)
+- wiring は emission 層で `Frag` を組み、`emit_frag()` で Branch/Jump/Return を落とす
+
+### Phase 270(完了): Pattern9 を bridge として追加
+
+Pattern1(simple_while_minimal)が test-only stub のため、JoinIR-only minimal loop を通す橋として `Pattern9_AccumConstLoop` を追加。
+Phase 271(docs-only)で撤去条件を SSOT 化済み(`edgecfg-fragments.md` の Bridge patterns セクション)。
+
+### Phase 272 P0(進行中): Pattern6→Pattern7 の順で Frag 化
+
+- Pattern6: `index_of` 形(scan with init)
+ - fixture/smoke: `apps/tests/phase254_p0_index_of_min.hako` / `tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh`
+- Pattern7: `split` 形(tokenization with variable step + side effects)
+ - fixture/smoke: `apps/tests/phase256_p0_split_min.hako` / `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh`
+
+---
+
+## 問題意識(相談ポイント)
+
+### 1) Frag の入口が小さくても、pattern 群が増えれば裾広がりになる
+
+Frag/emit は SSOT 化できたが、上流の loop 処理が “pattern番号の列挙” を中心に残ると:
+
+- 新しい loop 形(reverse, dynamic needle, extra side effects, nested if 等)が出るたびに pattern が増える
+- “どのパターンで通すか” が本質になり、Structured→CFG 合成則(ExitKind/Frag)が中心に戻らない
+- 結果としてフローが読みにくくなり、設計が収束しにくい
+
+### 2) compiler flow(責務分離)がまだ不安定
+
+収束させたい本線は以下:
+
+`AST(loop)` → **Plan(正規化された抽出結果)** → `Frag(wires/branches)` → `emit_frag()`(terminator SSOT)
+
+pattern は “検出して Plan を返すだけ” に降格し、CFG の組み立て・配線の本体は Plan→Frag の共通 lowerer に集約したい。
+
+---
+
+## 現状の実装形(要約)
+
+### EdgeCFG Frag(terminator SSOT)
+
+- `Frag` は “entry + branches + wires (+ exits)” を持つ
+- `emit_frag()` が以下を保証(Fail-Fast):
+ - 1 block = 1 terminator(wire/branch の衝突禁止、複数wire禁止)
+ - `set_jump_with_edge_args` / `set_branch_with_edge_args` を使用し successors/preds を同期
+ - Return は `target=None` を許可(意味を持たない)
+
+### Pattern6/7(現状): JoinIRConversionPipeline 依存
+
+現時点では `JoinIRConversionPipeline` に依存しており、
+JoinModule → MirModule → merge… という暗黙の変換で terminator が作られる。
+これが “terminator SSOT” を弱め、pattern 増殖も誘発しやすい。
+
+---
+
+## 制約(ポリシー)
+
+- by-name ハードコード禁止(Box名文字列一致で分岐など)
+- 環境変数トグル増殖禁止(今回の相談では新設しない)
+- Fail-Fast 原則(fallback は原則避け、`Ok(None)` の “不適用” と `Err` の “契約違反” を分ける)
+- 大規模設計変更は避け、段階移行(P0/P1…)で可逆に進める
+
+---
+
+## 収束のための案(たたき台)
+
+### 案A: “Pattern = Plan Extractor” へ降格(推奨)
+
+pattern を増やすのではなく、**Plan の種類を少数に固定**し、pattern は Plan 抽出だけを担当する。
+
+例(概念):
+
+- `LoopPlan::ScanEq`(Pattern6の本質)
+ - `i`(loop var), `s`(haystack), `needle`
+ - `step`(P0は 1 のみ、逆走は P1 で追加)
+ - `found_exit` / `not_found_exit`(Return / afterへ落とす等)
+ - `effects`: なし(P0)
+- `LoopPlan::SplitScan`(Pattern7の本質)
+ - carriers: `i`, `start`
+ - invariants: `s`, `sep`, `result`
+ - side-effects: `result.push(segment)`(順序固定)
+
+Plan→Frag の lowerer を共通化:
+
+1. block/value の生成(pattern or lowerer)
+2. PHI insertion(`insert_phi_at_head_spanned`)
+3. wiring(emission で `Frag` を組む)
+4. `emit_frag()`(terminator SSOT)
+
+### 案B: Plan は 1 種に寄せ、差分は “語彙” に寄せる
+
+Scan/Predicate/Split を全部 “Loop + If + Step + Exit” の語彙に落とし、
+Plan は「どの基本語彙をどう繋ぐか」だけにする。
+
+利点: Plan 種類が増えにくい
+欠点: 設計が抽象化しすぎると P0 の実装が重くなる
+
+---
+
+## 相談したい質問(ChatGPT Pro への問い)
+
+### Q1. Plan の粒度
+
+Pattern6/7/8 の裾広がりを止めるために、Plan の型はどの粒度が適切か?
+
+- `ScanPlan` / `SplitPlan` のような “中粒度” がよいか
+- もっと小さく `LoopPlan { header, body, step, exits }` に寄せるべきか
+- Plan 種類を増やさず “パラメータ” で吸収する設計案はあるか
+
+### Q2. 責務分離(フォルダ/モジュール)
+
+どこに Plan を置くべきか?
+
+- 候補: `src/mir/builder/control_flow/` 配下に `plans/`(Extractor/Plan/Lowerer)
+- pattern フォルダは “extractor” 専用へ縮退させるべきか
+- emission 層は “wiring only” を守るべきか(Pattern8 と同様)
+
+### Q3. `Ok(None)` と `Err` の境界(Fail-Fast)
+
+「不適用」は `Ok(None)` で通常loweringへ戻すとして、`Err` にすべき契約違反は何か?
+
+- 例: extractor が “形は一致” と判断した後に、必要な var が存在しない等
+- “close but unsupported” を Err(Fail-Fast)にし、形が違うだけなら Ok(None) にする方針は妥当か
+
+### Q4. side effects(Pattern7)の扱い
+
+副作用(`result.push`)を含む loop を Plan/Frag で表す際、
+評価順・SSA・ブロック配置の設計で注意すべき点は?
+
+### Q5. bridge patterns(Pattern9)の扱い
+
+bridge pattern は撤去条件SSOTを作ったが、設計としてどこまで許すべきか?
+(例: “bridge を増やさない運用” の現実的なルール)
+
+---
+
+## 期待する回答フォーマット(外部相談用)
+
+1. 推奨する収束アーキテクチャ(1ページ図 + 箇条書き)
+2. Phase 272 以降の段階移行手順(P0→P1→撤去)
+3. Plan 型の提案(最小フィールド、増殖しない理由)
+4. Fail-Fast の境界(Ok(None)/Err のガイドライン)
+5. 副作用を含む loop の設計チェックリスト
+
+---
+
+## Non-goals(今回やらない)
+
+- 言語仕様の拡張(大きな機能追加は一時停止中)
+- merge/EdgeCFG plumbing の広域改変
+- 新しい環境変数トグル追加
+
diff --git a/docs/development/current/main/investigations/phase-274-p3-coercion-ssot-consult.md b/docs/development/current/main/investigations/phase-274-p3-coercion-ssot-consult.md
new file mode 100644
index 00000000..88c00c19
--- /dev/null
+++ b/docs/development/current/main/investigations/phase-274-p3-coercion-ssot-consult.md
@@ -0,0 +1,130 @@
+# Phase 274 P3 — coercion SSOT consultation memo (truthiness / `==` / `+`)
+
+Status: for external consultation (self-contained)
+
+Goal: decide and freeze **language-level coercion semantics** as SSOT, then make VM/LLVM/docs consistent.
+
+Repo context (Nyash/Hakorune):
+- Philosophy: **Fail-Fast**, **no guessing**, **SSOT-first** (runtime semantics must not “accidentally” emerge from resolver/type-facts).
+- Current SSOT doc draft: `docs/reference/language/types.md`
+- Phase 274 overview: `docs/development/current/main/phases/phase-274/README.md`
+
+This memo asks you to recommend a clean, modern coercion policy and (if needed) a migration plan.
+
+---
+
+## 0) Why we need P3
+
+We have a dynamic runtime, but coercions are currently a mix of “historical convenience” and “implementation drift”.
+If we don’t freeze these rules, type facts / resolver heuristics can start acting like semantics.
+
+P3 is the decision phase for these three coercion points:
+1) truthiness (conditions)
+2) equality (`==`)
+3) `+` (add/concat)
+
+---
+
+## 1) Current observed behavior (summary)
+
+Treat this as “current reality”, not necessarily desired design.
+
+### 1.1 truthiness (boolean context)
+
+Current doc says (Rust VM behavior):
+- Bool → itself
+- Integer/Float → 0 is false, non-zero is true
+- String → empty false, otherwise true
+- Void → false
+- BoxRef:
+ - some “bridge boxes” (BoolBox/IntegerBox/StringBox/VoidBox) behave like their primitives
+ - other BoxRef types currently **TypeError** (fail-fast)
+
+### 1.2 `==` (equality)
+
+Current doc says:
+- same-kind primitives compare normally
+- some cross-kind coercions exist (best-effort):
+ - Integer ↔ Bool (non-zero == true)
+ - Integer ↔ Float (numeric comparison)
+- BoxRef == BoxRef is identity
+- mixed kinds often return false (not an error)
+
+### 1.3 `+` (add / concat)
+
+Current doc says:
+- Integer+Integer, Float+Float are numeric add
+- if either side is String, it concatenates (stringifies the other operand)
+- other combos are TypeError
+
+---
+
+## 2) Target design constraints
+
+We want:
+- Minimal, composable semantics
+- Predictable failure (fail-fast where it prevents silent bugs)
+- Avoid “JS-style surprise coercions”
+- Keep language dynamic (no full static typing required)
+
+---
+
+## 3) Decision questions (please answer A/B/C)
+
+### A) truthiness: should `Void` be allowed in conditions?
+
+Options:
+- A1 (Fail-Fast): `if void` → TypeError
+- A2 (Compatibility): `if void` → false (but maybe add lint/warn later)
+
+Question:
+- Which should be SSOT, and why?
+- If you choose A1, suggest the recommended explicit pattern (`x != Void`? `bool(x)`?).
+
+### B) `==`: should we keep any cross-type coercions?
+
+We want to avoid half-coercions that become “spec by accident”.
+
+Options:
+- B1 (Strict): cross-type equality is always false (no coercion)
+- B2 (Number-only): allow Int↔Float numeric compare, but **do not** allow Bool↔Int
+- B3 (Legacy): keep both Int↔Float and Bool↔Int
+
+Question:
+- Which option is the best “modern + safe” SSOT, and why?
+- If you choose B2 or B3, define the exact rule (edge cases).
+
+### C) `+`: should `"String" + 1` be allowed?
+
+Options:
+- C1 (Strict): only same-kind `+` is allowed:
+ - Int+Int, Float+Float, String+String
+ - anything else is TypeError
+- C2 (Number-only): allow Int↔Float numeric add (promotion), but String mixed is TypeError
+- C3 (Legacy): if either side is String, concatenate (stringify the other side)
+
+Question:
+- Which option is the best SSOT, and why?
+- If you choose C2, define the promotion rule precisely (Int→Float only?).
+
+---
+
+## 4) Implementation impact / migration plan (if SSOT changes)
+
+If your recommended SSOT differs from current behavior, please propose:
+- Whether to do a “compatibility freeze” phase (document current behavior first), then a separate “breaking change” phase.
+- What minimum tests/fixtures should exist to lock the SSOT:
+ - truthiness cases (Bool/Int/Float/String/Void/BoxRef)
+ - equality matrix (same-type + selected cross-type)
+ - `+` matrix (including failure cases)
+
+---
+
+## 5) Preferred final output format
+
+Please respond with:
+1) Final SSOT decision table (truthiness / `==` / `+`)
+2) Rationale (short)
+3) Migration plan (if needed)
+4) Suggested test matrix (minimum)
+
diff --git a/docs/development/current/main/joinir-architecture-overview.md b/docs/development/current/main/joinir-architecture-overview.md
index 86162bfd..d87b8794 100644
--- a/docs/development/current/main/joinir-architecture-overview.md
+++ b/docs/development/current/main/joinir-architecture-overview.md
@@ -15,6 +15,72 @@
---
+## 0.0 収束形(Target Shape / Convergence)
+
+JoinIR/CFG 合成が「裾広がり」せずに収束していくための **目標形(target shape)** をここで固定する。
+通称: **Plan→Frag パイプライン**(会話・作業ログではこの短い呼び名を使う)。
+
+狙いは「要素が増えても、本線が増えない」こと:
+- 増えてよい: extractor(検出)だけ
+- 増えてはいけない: block/value/PHI/terminator 生成の分岐点(= 生成本線の if/分岐増殖)
+
+### 0.0.1 パイプライン(収束形)
+
+```
+ ┌──────────────────────────────┐
+AST/Stmt → │ Plan Extractor Box (pure) │
+ │ - patterns are just extractors
+ │ - no block/value allocation
+ │ - returns: Ok(None)/Ok(Plan)/Err
+ └──────────────┬───────────────┘
+ │
+ v
+ ┌──────────────────────────────┐
+ │ Plan Verifier Box (fail-fast)│
+ │ - phase gating (P0/P1)
+ │ - invariants (no ambiguity)
+ └──────────────┬───────────────┘
+ │
+ v
+ ┌──────────────────────────────┐
+ │ Plan Lowerer Box (only builder)│
+ │ - alloc blocks/values/phi
+ │ - builds Frag via small comb.
+ │ - uses Expr/Scope boxes inside blocks
+ └──────────────┬───────────────┘
+ │
+ v
+ ┌──────────────────────────────┐
+ │ EdgeCFG Frag + emit_frag() │
+ │ - terminator SSOT
+ │ - succ/pred sync
+ └──────────────┬───────────────┘
+ │
+ v
+ Verify / DCE / CFG update / print
+ (terminator operand only)
+```
+
+### 0.0.2 「収束している」と呼ぶ条件(定義)
+
+この定義を満たしている状態を、JoinIR/CFG 合成の「収束」と呼ぶ:
+
+1) **pattern は Plan 抽出へ降格する**
+ - pattern は「一致判定 + Plan を返す」だけ(builder を触らない)。
+
+2) **CFG 生成(block/value/PHI)は PlanLowerer に一本化する**
+ - `next_block_id()` / `next_value_id()` / PHI 挿入 / `emit_frag()` 呼び出しは PlanLowerer 側だけに置く。
+
+3) **terminator 生成点は `emit_frag()` のみ(SSOT)**
+ - Branch/Jump/Return を “別の場所で” 生成しない。
+
+4) **増えても本線が増えない(成長境界)**
+ - 新しい loop/if の形が増えても、増えるのは extractor(薄いファイル)だけ。
+ - Plan の語彙と Lowerer/emit の本線は増殖しない(分岐増殖は設計上のバグとして扱う)。
+
+参照(相談メモ / 背景):
+- `docs/development/current/main/investigations/phase-272-frag-plan-architecture-consult.md`
+
## 0. 読み方ガイド(Reader's Guide)
このファイルは情報量が多いので、「何を知りたいか」で読む場所を分けると楽だよ:
diff --git a/docs/development/current/main/phases/phase-269/README.md b/docs/development/current/main/phases/phase-269/README.md
index ef7dc128..0f78bd5a 100644
--- a/docs/development/current/main/phases/phase-269/README.md
+++ b/docs/development/current/main/phases/phase-269/README.md
@@ -1,14 +1,20 @@
# Phase 269: Pattern8 への Frag 適用(P0=test-only → P1=実装)
-Status: 🚧 進行中(P1)
-Date: 2025-12-21
+Status: ✅ 完了(P1)
+Date: 2025-12-22
+
+## サブフェーズ状況
+
+- **P1(Pattern8 EdgeCFG lowering)**: ✅(SSA の `i` PHI を含めて閉じた)
+- **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 化)(IN PROGRESS)
+- **P1**: 実装(MIR CFG層で Frag を組み、emit_frag で terminator を SSOT 化)(DONE)
## 実装範囲(重要:スコープ境界)
@@ -52,13 +58,57 @@ set_branch_with_edge_args() / set_jump_with_edge_args() (Phase 260 SSOT)
- 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.sh` PASS
+ - `tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_vm.sh` PASS(回帰なし)
+
+## 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/Me` receiver を最優先で検出し、static call に正規化する
+ - box 名は `comp_ctx.current_static_box` のみから取り出す(ハードコード禁止)
+- `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.hako`
+- `tools/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)になる
+
## テスト手順(固定)
1. `cargo build --release`
2. `cargo test -p nyash-rust --lib --release`
3. `HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_vm.sh`
4. `HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase269_p0_pattern8_frag_vm.sh`
-5. `./tools/smokes/v2/run.sh --profile quick`(45/46 を維持)
+5. `HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase269_p1_2_this_method_in_loop_vm.sh`
+6. `./tools/smokes/v2/run.sh --profile quick`(45/46 を維持)
## P0(historical)
diff --git a/docs/development/current/main/phases/phase-272/README.md b/docs/development/current/main/phases/phase-272/README.md
new file mode 100644
index 00000000..4956a428
--- /dev/null
+++ b/docs/development/current/main/phases/phase-272/README.md
@@ -0,0 +1,166 @@
+Status: Active
+Date: 2025-12-22
+Scope: Pattern6/7 を `Frag + emit_frag()` へ段階吸収(pattern列挙の増殖を止める)
+Related:
+- Design SSOT: `docs/development/current/main/design/edgecfg-fragments.md`
+- Phase 269(Pattern8 Frag): `docs/development/current/main/phases/phase-269/README.md`
+- Phase 270(Pattern9 bridge): `docs/development/current/main/phases/phase-270/README.md`
+
+# Phase 272(P0): Pattern6/7 を Frag+emit_frag へ吸収(段階適用)
+
+## ステータス
+
+- **P0.1(Pattern6)**: ✅ 完了(Frag+emit_frag 経路へ移行)
+- **P0.2(Pattern7)**: ✅ 完了(Frag+emit_frag 経路へ移行)
+
+## 目的
+
+- Pattern6/7(scan系)の CFG 構築を “pattern番号ごとの推測分岐” から外し、**EdgeCFG Frag 合成(ExitKind/wires/branches)**に収束させる。
+- terminator emission を SSOT(`emit_frag()`)へ集約し、block の successors/preds 同期漏れを構造で防ぐ。
+
+## スコープ境界
+
+### ✅ 触る
+- `src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs`
+- `src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs`
+- `src/mir/builder/emission/`(Pattern8 と同じ “薄い入口” の追加)
+
+### ❌ 触らない
+- merge/EdgeCFG plumbing(Phase 260-268 の SSOT は維持)
+- cf_loop の非JoinIR経路追加(JoinIR-only hard-freeze 維持)
+- by-name ハードコード(Box名/Pattern名文字列での分岐増殖など)
+
+## 入口SSOT(fixture/smoke)
+
+### Pattern6(index_of)
+- fixture: `apps/tests/phase254_p0_index_of_min.hako`(exit=1)
+- smoke (VM): `tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh`
+
+### Pattern7(split)
+- fixture: `apps/tests/phase256_p0_split_min.hako`(exit=3)
+- smoke (VM): `tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh`
+
+## 方針(P0)
+
+P0 は “両方一気に” ではなく、以下の順で段階適用する。
+
+1. Pattern6 を `Frag + emit_frag()` に切り替え(wiring を SSOT 化)
+2. Pattern7 を `Frag + emit_frag()` に切り替え(副作用 push を含む)
+3. 旧 JoinIR 経路の撤去条件が満たせた時点で削除(本READMEに明記)
+
+## 実装ガイド(共通)
+
+- PHI は block 先頭(after existing phis)へ挿入し、入力を `[(pred_bb, val)]` の形で固定する:
+ - `crate::mir::ssot::cf_common::insert_phi_at_head_spanned`
+- terminator emission は `crate::mir::builder::control_flow::edgecfg::api::emit_frag` に集約する。
+- Pattern8 の構造(参考):
+ - emission 入口: `src/mir/builder/emission/loop_predicate_scan.rs`
+
+## P0.1: Pattern6(index_of)— Frag 化
+
+### 狙い
+- loop 骨格(header/body/step/after + early return)を Frag に落とし、Jump/Branch/Return を `emit_frag()` に集約する。
+
+### 実装結果(✅ 完了)
+
+- emission 入口を新設し、Pattern6 の terminator emission を `emit_frag()`(SSOT)へ集約
+ - 新規: `src/mir/builder/emission/loop_scan_with_init.rs`
+ - 更新: `src/mir/builder/emission/mod.rs`
+- Pattern6 の JoinIRConversionPipeline 経路を撤去し、Frag 経路へ切り替え
+ - 更新: `src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs`
+- P0 スコープ:
+ - forward scan(`step=1`)のみ適用
+ - reverse/dynamic needle 等は `Ok(None)` で不適用(既定挙動不変)
+- 旧 DCE 対策(exit PHI 用の post-loop guard)を撤去(Frag が Return を直接 emit するため)
+
+### 最小 CFG 形(forward scan)
+- blocks: `header`, `body`, `step`, `after`, `ret_found`
+- header:
+ - `i_current = phi [i_init, preheader], [i_next, step_bb]`
+ - `cond_loop = (i_current < len)`
+ - branch: true→body, false→after
+- body:
+ - `ch = s.substring(i_current, i_current+1)`
+ - `cond_match = (ch == needle)`
+ - branch: true→ret_found, false→step
+- step:
+ - `i_next = i_current + 1`
+ - jump: →header
+- ret_found:
+ - wire: `Return(i_current)`
+- after:
+ - P0 では `return -1` を既存 AST lowering に任せてもよい(after を current_block にする)
+
+### 受け入れ
+- `cargo test -p nyash-rust --lib --release`
+- `HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh`
+
+### 追加の検証(推奨)
+
+- `NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm apps/tests/phase254_p0_index_of_min.hako` で PHI/terminator を確認(任意)
+
+## P0.2: Pattern7(split)— Frag 化
+
+### 狙い
+- Pattern7 の terminator 配線(if/loop の遷移)を Frag に集約し、副作用(`result.push`)を含む形でも CFG を壊さない。
+
+### 注意点(Fail-Fast)
+- carriers が複数(`i`, `start`)なので、header の PHI を 2 本(以上)で SSA を閉じる必要がある。
+- `result.push` は副作用なので、block 配置(評価順)を壊さない(P0は固定形のみ受理)。
+
+### 実装結果(✅ 完了)
+
+- Pattern7 の JoinIRConversionPipeline 経路を撤去し、Frag+emit_frag 経路へ切り替え
+ - 更新: `src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs`
+- emission 入口を新設し、terminator emission を `emit_frag()`(SSOT)へ集約
+ - 新規: `src/mir/builder/emission/loop_split_scan.rs`
+ - 更新: `src/mir/builder/emission/mod.rs`
+- CFG 形(P0):
+ - blocks: `header`, `body`, `then`, `else`, `step`, `after`(+ 入口 preheader)
+ - header: PHI(`i_current`, `start_current`) + loop condition
+ - body: delimiter match check
+ - then: `result.push(segment)` + `start_next_then` 計算(dominance 安全)
+ - else: `i_next_else = i_current + 1`
+ - step: PHI(`i_next`, `start_next`) + jump header
+- Compare は `CompareOp::Le`(`i <= limit`)を使用(固定形)
+
+### リファクタ結果(共通SSOT)
+
+Phase 272 P0.2 完了後、Pattern6/7/8 の重複を以下へ収束した(仕様不変):
+
+- PHI 挿入の薄いラッパ: `src/mir/builder/emission/phi.rs`
+- variable_map の fail-fast 取得: `src/mir/builder/variable_context.rs`(`require(name, ctx)`)
+- can_lower の戦略メモ: `src/mir/builder/control_flow/joinir/patterns/router.rs`(`CanLowerStrategy`)
+
+### 受け入れ
+- `HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh`
+ - PASS(exit=3)
+ - MIR 形(PHI/Branch/Jump/BoxCall(push))を目視確認できること(任意)
+
+## Next(planned): Phase 273(design-first)— Pattern を Plan Extractor に降格して裾広がりを止める
+
+Phase 272(P0)で “terminator SSOT(emit_frag)へ寄せる” を完了したら、次は上流の収束(compiler flow の一本化)を行う。
+
+- 相談メモ(外部レビュー用): `docs/development/current/main/investigations/phase-272-frag-plan-architecture-consult.md`
+- ねらい:
+ - Pattern = **Plan 抽出(pure)** に降格(builder を触らない)
+ - Plan = `seq/if/loop/exit/effect/let` の固定語彙(増殖しない)
+ - PlanLowerer が block/value/phi を作る唯一の箱(emit_frag は SSOT のまま)
+- 受け入れ(最小):
+ - extractor が `next_block_id/next_value_id/insert_phi_*` を呼ばない(純関数)
+ - Plan→Frag→emit_frag の本線が 1 本になる(pattern番号列挙を中心にしない)
+
+## 旧 JoinIR 経路の撤去条件(SSOT)
+
+旧 `JoinIRConversionPipeline` 系の経路を削るのは、以下を満たした後に行う。
+
+1. Pattern6/7 の fixture/smoke が Frag 経路で PASS
+2. `tools/smokes/v2/run.sh --profile quick` が悪化しない
+3. router から該当 pattern の “旧経路” が消せる(最小差分で削除可能)
+
+## テスト手順(固定)
+
+1. `cargo build --release`
+2. `cargo test -p nyash-rust --lib --release`
+3. `HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh`
+4. `HAKORUNE_BIN=./target/release/hakorune bash tools/smokes/v2/profiles/integration/apps/phase256_p0_split_vm.sh`
diff --git a/docs/development/current/main/phases/phase-274/P1-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-274/P1-INSTRUCTIONS.md
new file mode 100644
index 00000000..eced6a21
--- /dev/null
+++ b/docs/development/current/main/phases/phase-274/P1-INSTRUCTIONS.md
@@ -0,0 +1,90 @@
+# Phase 274 P1 — Implement `TypeOp` on Rust VM (Instruction Guide)
+
+Goal: make `x.is("T")` / `x.as("T")` runnable on the primary backend (Rust VM), aligning language docs and runtime behavior.
+
+Scope: small, behavior-preserving where possible; fail-fast on unsupported type operations.
+
+SSOT:
+- Language semantics: `docs/reference/language/types.md`
+- MIR instruction: `MirInstruction::TypeOp` (`src/mir/instruction.rs`)
+- Rust VM executor: `src/backend/mir_interpreter/*`
+
+---
+
+## 1) What already exists
+
+- Frontend lowering emits `MirInstruction::TypeOp` for `.is("Type")` / `.as("Type")`:
+ - `src/mir/builder/exprs.rs`
+ - type-name mapping: `src/mir/builder/calls/special_handlers.rs` (`parse_type_name_to_mir`)
+- A fixture exists for P1 acceptance:
+ - `apps/tests/phase274_p1_typeop_is_as_min.hako`
+ - smoke: `tools/smokes/v2/profiles/integration/apps/phase274_p1_typeop_is_as_vm.sh`
+
+---
+
+## 2) Implementation plan (Rust VM)
+
+### 2.1 Add instruction execution
+
+Implement execution of:
+- `MirInstruction::TypeOp { dst, op: Check, value, ty }` → `dst = Bool(matches(value, ty))`
+- `MirInstruction::TypeOp { dst, op: Cast, value, ty }` → `dst = value` if matches, else `TypeError`
+
+Files:
+- `src/backend/mir_interpreter/handlers/type_ops.rs` (new module is OK)
+- `src/backend/mir_interpreter/handlers/mod.rs` (dispatch arm)
+- `src/backend/mir_interpreter/mod.rs` (import `MirType`, `TypeOpKind` into interpreter module)
+
+### 2.2 Matching rules (minimal, fail-fast)
+
+Match by `MirType`:
+- `Integer/Float/Bool/String/Void`: accept both primitive VM values and their core Box equivalents when present.
+- `Box("Foo")`: accept:
+ - user-defined `InstanceBox` where `class_name == "Foo"`
+ - builtin/plugin boxes where `type_name() == "Foo"`
+ - (best-effort) builtin `InstanceBox(from_any_box)` inner `type_name() == "Foo"`
+- Others:
+ - `Unknown` matches anything (diagnostic-friendly).
+
+Do not add new environment variables. Keep behavior deterministic and fail-fast.
+
+---
+
+## 3) Testing / Verification
+
+### 3.1 Build
+
+```bash
+cargo build --release
+```
+
+### 3.2 Smoke (required)
+
+```bash
+HAKORUNE_BIN=./target/release/hakorune bash \
+ tools/smokes/v2/profiles/integration/apps/phase274_p1_typeop_is_as_vm.sh
+```
+
+Expected:
+- PASS (exit=3)
+
+### 3.3 Optional MIR inspection
+
+```bash
+NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm \
+ apps/tests/phase274_p1_typeop_is_as_min.hako
+```
+
+Confirm:
+- MIR contains `TypeOp(check, ...)` and `TypeOp(cast, ...)`.
+
+---
+
+## 4) Acceptance criteria (P1)
+
+- Rust VM executes `TypeOp` (no “unimplemented instruction”).
+- `phase274_p1_typeop_is_as_vm.sh` passes.
+- No new env vars are introduced.
+- Docs remain consistent with runtime:
+ - `docs/reference/language/types.md` describes runtime `TypeOp` behavior.
+
diff --git a/docs/development/current/main/phases/phase-274/P2-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-274/P2-INSTRUCTIONS.md
new file mode 100644
index 00000000..5d1701a7
--- /dev/null
+++ b/docs/development/current/main/phases/phase-274/P2-INSTRUCTIONS.md
@@ -0,0 +1,151 @@
+# Phase 274 P2 (impl): LLVM (llvmlite harness) TypeOp alignment
+
+Status: planned / design-first
+
+Goal: make LLVM harness execution match the SSOT semantics in `docs/reference/language/types.md` for:
+- `TypeOp(Check, value, ty)` → `Bool`
+- `TypeOp(Cast, value, ty)` → `value` or `TypeError`
+
+Primary reference implementation (SSOT runtime): `src/backend/mir_interpreter/handlers/type_ops.rs`
+
+---
+
+## 0. What is currently wrong (must-fix)
+
+LLVM harness TypeOp is stubbed in `src/llvm_py/instructions/typeop.py`:
+- `is`: returns 0 for most types (IntegerBox is “non-zero” heuristic)
+- `cast/as`: pass-through (never errors)
+
+This conflicts with SSOT:
+- `is` must reflect actual runtime type match.
+- `as` must fail-fast (`TypeError`) on mismatch.
+
+Note:
+- It is OK if the compiler constant-folds trivial cases (e.g. `1.is("Integer")`).
+- For P2 verification, you must use a fixture that keeps `TypeOp` in MIR (runtime-unknown / union value).
+
+---
+
+## 1. Acceptance criteria (minimum)
+
+1) Behavior parity with Rust VM (SSOT)
+- With `NYASH_LLVM_USE_HARNESS=1` and `--backend llvm`, this fixture behaves the same as VM:
+ - `apps/tests/phase274_p2_typeop_primitives_only.hako` (recommended: harness-safe baseline)
+
+2) Fail-fast
+- `as` on mismatch must raise a TypeError (not return 0 / pass-through).
+- `is` must return `0/1` deterministically (no “unknown → 0” unless it is truly not a match).
+
+3) No hardcode / no new env sprawl
+- No “BoxName string match special-cases” except small alias normalization shared with frontend (`IntegerBox`/`StringBox` etc.).
+- Do not add new environment variables for behavior.
+
+---
+
+## 2. Design constraint: LLVM harness value representation (key risk)
+
+In llvmlite harness, a runtime “value” is currently represented as an `i64`, but it mixes:
+- raw integers (from `const i64`)
+- boxed handles (e.g. strings are boxed to handles via `nyash.box.from_i8_string`)
+- various call/bridge conventions
+
+Because a handle is also an `i64`, **the harness cannot reliably decide at runtime** whether an `i64` is “raw int” or “handle”, unless the value representation is made uniform.
+
+This means TypeOp parity cannot be achieved reliably without addressing representation.
+
+---
+
+## 3. Recommended implementation strategy (P2): make representation uniform for TypeOp
+
+### Strategy A (recommended): “all values are handles” in LLVM harness
+
+Make every runtime value in llvmlite harness be a handle (i64) to a boxed value:
+- integers: `nyash.box.from_i64(i64) -> handle`
+- floats: `nyash.box.from_f64(f64) -> handle`
+- strings: already boxed (`nyash.box.from_i8_string`)
+- bool/void: use existing conventions (or add kernel shims if needed)
+
+Then TypeOp becomes implementable via runtime introspection on handles.
+
+#### A.1 Kernel helper needed (small, SSOT-friendly)
+
+Add a kernel export (in `crates/nyash_kernel/src/lib.rs`) that checks a handle’s runtime type:
+- `nyash.any.is_type_h(handle: i64, type_name: *const i8) -> i64` (0/1)
+- optionally `nyash.any.cast_h(handle: i64, type_name: *const i8) -> i64` (handle or 0; but prefer fail-fast at caller)
+
+Implementation rule:
+- Must use actual runtime object type (builtins + plugin boxes + InstanceBox class name).
+- Must not guess via resolver/type facts.
+
+#### A.2 LLVM harness lowering
+
+Update `src/llvm_py/instructions/typeop.py`:
+- Always resolve `src_val` as handle (`i64`).
+- `check/is`: call `nyash.any.is_type_h(src_val, type_name_ptr)` → i64 0/1
+- `cast/as`: call `is_type_h`; if false, emit a runtime error (use existing “panic”/error path if available) or call a kernel `nyash.panic.type_error` style function (add if missing).
+
+Also update other lowerers incrementally so that values feeding TypeOp are handles (start with the fixture path).
+
+### Strategy B (fallback): keep mixed representation, but document divergence (not parity)
+
+If Strategy A is too large for P2, constrain scope:
+- Implement `TypeOp` using compile-time `resolver.value_types` hints.
+- Document clearly in Phase 274 README: LLVM harness TypeOp is “best-effort using type facts” and is not SSOT-correct under re-assignment.
+
+This keeps the harness useful for SSA/CFG validation, but is not runtime-parity.
+
+Note: Strategy B should be treated as temporary and must be called out as backend divergence in docs.
+
+---
+
+## 4. Concrete work items (P2)
+
+1) Audit current failure path
+- Identify how LLVM harness reports runtime errors today (type errors, asserts).
+- Prefer a single runtime helper rather than sprinkling Python exceptions.
+
+1.5) Fix MIR JSON emission for TypeOp (required)
+
+The LLVM harness consumes MIR JSON emitted by the Rust runner.
+If `TypeOp` is missing in that JSON, the harness will never see it (and the JSON can become invalid due to missing defs).
+
+Checklist:
+- `src/runner/mir_json_emit.rs` must emit `{"op":"typeop", ...}` in **both** emitters:
+ - `emit_mir_json_for_harness` (nyash_rust::mir) ✅ already supports TypeOp
+ - `emit_mir_json_for_harness_bin` (crate::mir) ⚠️ ensure TypeOp is included
+
+2) Add kernel introspection helper(s)
+- `crates/nyash_kernel/src/lib.rs`: add `nyash.any.is_type_h`.
+- It must handle:
+ - primitives boxed (`IntegerBox`, `FloatBox`, `BoolBox`, `StringBox`, `VoidBox`)
+ - `InstanceBox` user classes (by `class_name`)
+ - plugin boxes (by metadata / resolved type name)
+
+3) Implement real `TypeOp` lowering
+- `src/llvm_py/instructions/typeop.py`:
+ - normalize `target_type` aliases (same mapping as frontend docs: `Int` → `IntegerBox`, etc.)
+ - `is` → call kernel check
+ - `as`/`cast` → check then return src or TypeError
+
+4) Add LLVM smoke (integration)
+- New script (name suggestion):
+ - `tools/smokes/v2/profiles/integration/apps/phase274_p2_typeop_is_as_llvm.sh`
+- Run:
+ - `NYASH_LLVM_USE_HARNESS=1 ./target/release/hakorune --backend llvm apps/tests/phase274_p2_typeop_primitives_only.hako`
+- Expect: exit code `3` (same as VM).
+
+---
+
+## 5. Notes / non-goals (P2)
+
+- Do not implement a full static type system here.
+- Do not add rule logic to the resolver (no “guessing chains”).
+- Do not add new environment variables for behavior selection.
+- If you must limit scope, limit it by fixtures and document it in Phase 274 README as explicit divergence.
+
+### Fixture rule (important)
+
+To avoid “TypeOp disappeared” false negatives:
+- Do not use pure compile-time constants for `is/as` checks.
+- Prefer a union value formed by a runtime-unknown branch (e.g. `process.argv().size() > 0`).
+ - Note: `env.process.argv` is currently not supported on Rust VM, and `env.get` is not linked for LLVM AOT yet; keep harness fixtures minimal unless the required externs are implemented in NyRT.
diff --git a/docs/development/current/main/phases/phase-274/P3-DECISIONS.md b/docs/development/current/main/phases/phase-274/P3-DECISIONS.md
new file mode 100644
index 00000000..4736a644
--- /dev/null
+++ b/docs/development/current/main/phases/phase-274/P3-DECISIONS.md
@@ -0,0 +1,162 @@
+# Phase 274 P3 (decision): Coercion SSOT (truthiness / `==` / `+`)
+
+Status: accepted (2025-12-22) / pending implementation
+
+This document freezes **what coercions mean** at the language level, so runtime behavior cannot “emerge” from resolver/type-facts.
+
+SSOT anchor (current executable behavior): `docs/reference/language/types.md`
+Phase overview: `docs/development/current/main/phases/phase-274/README.md`
+Implementation phase: `docs/development/current/main/phases/phase-275/README.md`
+
+---
+
+## 1) Terms
+
+In this doc, “coercion” means: when operands/types differ, do we:
+- convert implicitly,
+- return a deterministic result (e.g. `false`),
+- or fail-fast (`TypeError`)?
+
+Target constraints:
+- Fail-Fast where it prevents silent bugs
+- No “JS-style surprise coercion”
+- Dynamic runtime remains (no static type system required)
+- Backend parity (VM/LLVM) or explicit divergence, never accidental drift
+
+---
+
+## 2) Proposed SSOT (recommended)
+
+Based on the project philosophy, the recommended SSOT choice is:
+- **truthiness: A1**
+- **`==`: B2 (Number-only)**
+- **`+`: C2 (Number-only promotion)**
+
+The sections below define each choice precisely.
+
+---
+
+## 3) truthiness (boolean context)
+
+### Decision: A1 (Fail-Fast)
+
+`Void` in condition is **TypeError**.
+
+Allowed in boolean context:
+- `Bool` → itself
+- `Integer` → `0` false, non-zero true
+- `Float` → `0.0` false, non-zero true
+- `String` → empty false, otherwise true
+
+Disallowed (TypeError):
+- `Void` (always error)
+- `BoxRef` (by default)
+ - Exception: only **explicit bridge boxes** may be unboxed to the corresponding primitive for truthiness:
+ - `BoolBox` / `IntegerBox` / `StringBox`
+ - `VoidBox` is treated as `Void` → TypeError
+
+Recommended explicit patterns:
+- existence check: `x != Void`
+- type check: `x.is("T")` / `x.as("T")`
+- explicit conversion (if we add it): `bool(x)` (but `bool(Void)` remains TypeError)
+
+Implementation impact (where to change):
+- Rust VM: `src/backend/abi_util.rs::to_bool_vm`
+- LLVM harness: must match the VM semantics used for branch conditions
+
+---
+
+## 4) `==` (equality)
+
+### Decision: B2 (Number-only)
+
+Rules:
+- Same-kind primitives compare normally.
+- `Int` ↔ `Float` comparisons are allowed (Number-only).
+- `Bool` is **not** a number: `Bool` ↔ `Int/Float` has no coercion.
+- Other mixed kinds: deterministic **`false`** (not an error).
+- `BoxRef == BoxRef`: identity only.
+
+#### Precise rule for `Int == Float` (avoid “accidental true”)
+
+To avoid float rounding making `true` incorrectly:
+
+For `Int == Float` (or `Float == Int`):
+1) If Float is NaN → `false`
+2) If Float is finite, integral (fractional part is 0), and within `i64` exact range:
+ - convert Float → Int exactly, then compare Ints
+3) Otherwise → `false`
+
+Migration note:
+- If legacy behavior existed for `1 == true`, prefer a transition phase where it becomes **TypeError first** (to surface bugs), then settle to `false` if desired.
+
+Implementation impact:
+- Rust VM: `src/backend/abi_util.rs::eq_vm` (and any helpers)
+- LLVM harness: must mirror the same decision for `compare ==` lowering
+
+---
+
+## 5) `+` (add / concat)
+
+### Decision: C2 (Number-only promotion)
+
+Rules:
+- `Int + Int` → `Int`
+- `Float + Float` → `Float`
+- `Int + Float` / `Float + Int` → `Float` (promote Int→Float)
+- `String + String` → concat
+- `String + non-string` / `non-string + String` → **TypeError** (no implicit stringify)
+- Other combos → TypeError
+
+Implementation impact:
+- Rust VM: `src/backend/mir_interpreter/helpers.rs::eval_binop` (BinaryOp::Add)
+- LLVM harness: binop `+` lowering must follow the same coercion rules
+
+---
+
+## 6) Minimum test matrix (SSOT lock)
+
+### 6.1 truthiness
+
+- Bool: `if true`, `if false`
+- Int: `if 0`, `if 1`, `if -1`
+- Float: `if 0.0`, `if 0.5`, `if NaN` (define if NaN counts as truthy)
+- String: `if ""`, `if "a"`
+- Void: `if Void` → TypeError (A1)
+- BoxRef:
+ - bridge: `BoolBox(true)`, `IntegerBox(0)`, `StringBox("")`
+ - non-bridge: `Foo()` → TypeError
+
+### 6.2 equality
+
+- same-kind primitives
+- Int↔Float:
+ - `1 == 1.0` true
+ - `1 == 1.1` false
+ - `NaN == NaN` false
+- Bool↔Int:
+ - `true == 1` (explicitly decide: TypeError during migration vs final false)
+- BoxRef identity:
+ - same handle true, different handles false
+
+### 6.3 plus
+
+- Int/Float add
+- Int+Float promotion
+- String+String concat
+- String mixed TypeError
+
+---
+
+## 7) Migration plan (if changing behavior)
+
+Recommended two-step approach:
+
+1) Compatibility freeze (Phase 274)
+- Document current behavior (already in `types.md`)
+- Add warnings / diagnostics where possible (no new env sprawl)
+
+2) Switch semantics (Phase 275 or later)
+- Implement A1/B2/C2 in VM and LLVM
+- Add fixtures to lock the SSOT
+- Ensure error messages provide “fix-it” guidance (`str(x)`, `x != Void`, etc.)
diff --git a/docs/development/current/main/phases/phase-274/README.md b/docs/development/current/main/phases/phase-274/README.md
new file mode 100644
index 00000000..537b3a6a
--- /dev/null
+++ b/docs/development/current/main/phases/phase-274/README.md
@@ -0,0 +1,123 @@
+# Phase 274 (active): Type SSOT Alignment (local + dynamic runtime)
+
+Status: active / design-first
+
+Goal: make the **language-level type semantics** and the **runtime behavior** consistent and easy to reason about, without turning Nyash into a statically typed language.
+
+This phase is about:
+- clarifying SSOT docs,
+- closing “frontend emits it but VM can’t run it” gaps,
+- and preventing “type facts / resolver guessing” from silently becoming language semantics.
+
+---
+
+## Background (why this exists)
+
+Current state:
+- Nyash is dynamic at runtime (VM executes tagged values).
+- MIR builder attaches type metadata (`value_types`, `value_origin_newbox`) for routing/optimization.
+- Some docs describe stricter semantics than the VM actually implements.
+- `TypeOp` is emitted by the frontend for `is/as`.
+
+Problems:
+- Spec drift: quick docs vs runtime behavior differ (truthiness / equality / compare / `+`).
+- Capability drift across backends: “VM is correct” but “LLVM harness differs” (TypeOp).
+- Type metadata risks becoming implicit semantics via resolver fallback chains.
+
+SSOT decisions should be expressed in:
+- language docs (meaning),
+- runtime (execution),
+- and only then optimization facts (rewrite / routing).
+
+---
+
+## SSOT references
+
+- Language type semantics (SSOT): `docs/reference/language/types.md`
+- VM semantics source: `src/backend/abi_util.rs`, `src/backend/mir_interpreter/helpers.rs`
+- MIR type vocabulary: `src/mir/types.rs`
+- Call certainty vocabulary: `src/mir/definitions/call_unified.rs`
+
+---
+
+## Scope (P0/P1/P2/P3)
+
+### P0 (docs-only): establish SSOT and remove contradictions
+
+Deliverables:
+- `docs/reference/language/types.md` (SSOT)
+- Quick-reference points to SSOT and stops contradicting runtime
+
+Acceptance:
+- docs no longer claim semantics that the Rust VM clearly violates.
+
+### P1 (impl): make `TypeOp` runnable on Rust VM
+
+Goal:
+- `x.is("T")` / `x.as("T")` lowering exists already; make it executable on the primary backend (Rust VM).
+
+Acceptance (minimum):
+- Rust VM implements `MirInstruction::TypeOp { op: Check|Cast, value, ty }`
+- Add a small executable fixture/smoke that exercises `is/as`
+- No new env vars; fail-fast errors on unsupported casts/checks
+
+Implementation guide:
+- `docs/development/current/main/phases/phase-274/P1-INSTRUCTIONS.md`
+
+Status: ✅ done (2025-12-22)
+
+Artifacts:
+- Fixture: `apps/tests/phase274_p1_typeop_is_as_min.hako`
+- Smoke: `tools/smokes/v2/profiles/integration/apps/phase274_p1_typeop_is_as_vm.sh`
+- VM handler: `src/backend/mir_interpreter/handlers/type_ops.rs`
+
+### P2 (impl): align LLVM (llvmlite harness) `TypeOp` to SSOT
+
+Goal:
+- Make LLVM harness behavior match Rust VM (SSOT) for `TypeOp(Check/Cast)`.
+
+Current mismatch:
+- `src/llvm_py/instructions/typeop.py` is stubbed:
+ - `is` returns 0 for most types (special-cases `IntegerBox` as “non-zero”)
+ - `cast/as` are pass-through
+
+Acceptance (minimum):
+- With `NYASH_LLVM_USE_HARNESS=1` + `--backend llvm`, the P1 fixture has the same observable result as Rust VM.
+- Unsupported cases fail-fast (TypeError), not silent 0/“passthrough”.
+- No new environment-variable toggles; differences must be fixed or explicitly documented.
+
+Implementation guide:
+- `docs/development/current/main/phases/phase-274/P2-INSTRUCTIONS.md`
+
+Status: ✅ done (2025-12-22)
+
+Artifacts:
+- Kernel type check helper: `crates/nyash_kernel/src/lib.rs` (`nyash.any.is_type_h`)
+- LLVM TypeOp lowering: `src/llvm_py/instructions/typeop.py`
+- MIR JSON emission fix (bin): `src/runner/mir_json_emit.rs` (emit `op:"typeop"`)
+- Fixture (LLVM-safe): `apps/tests/phase274_p2_typeop_primitives_only.hako`
+- Smoke (LLVM): `tools/smokes/v2/profiles/integration/apps/phase274_p2_typeop_is_as_llvm.sh`
+
+### P3 (decision + optional impl): tighten or document coercions
+
+Decision points to settle (SSOT):
+- Truthiness for arbitrary BoxRef (allow “any object is truthy” vs fail-fast)
+- Equality cross-coercions (`int↔bool`, `int↔float`) — keep, restrict, or gate behind a profile
+- `+` mixed numeric types (`int+float`) — keep TypeError or add explicit conversions
+
+Acceptance:
+- whichever behavior is chosen becomes consistent across backends and docs.
+
+Decision memo (P3):
+- `docs/development/current/main/phases/phase-274/P3-DECISIONS.md`
+
+Status:
+- P3 decisions are ✅ accepted; implementation is tracked in Phase 275.
+
+---
+
+## Non-goals
+
+- Full static typing / inference engine
+- Widening the language surface area (new keywords) as the first move
+- Adding more environment-variable toggles as a long-term solution
diff --git a/docs/development/current/main/phases/phase-275/P0-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-275/P0-INSTRUCTIONS.md
new file mode 100644
index 00000000..65a081d1
--- /dev/null
+++ b/docs/development/current/main/phases/phase-275/P0-INSTRUCTIONS.md
@@ -0,0 +1,147 @@
+# Phase 275 P0 (impl): Coercion SSOT rollout
+
+Status: planned / implementation guide
+
+This is the “next instruction sheet” for implementing the accepted coercion SSOT (A1/B2/C2) across backends.
+
+SSOT decisions:
+- `docs/development/current/main/phases/phase-274/P3-DECISIONS.md`
+
+---
+
+## Scope (P0)
+
+Implement and lock these three rule sets:
+- truthiness: `Void`/`BoxRef` fail-fast (with bridge-box exceptions)
+- equality: B2 (Number-only, precise Int↔Float)
+- `+`: C2 (Number-only promotion; String+String only; String mixed → TypeError)
+
+Backends in scope:
+- Rust VM (primary SSOT)
+- LLVM harness (llvmlite path) parity with Rust VM
+
+Out of scope:
+- adding new language features (keywords)
+- expanding env var toggles
+- rewriting the optimizer broadly (only touch what is required to enforce semantics)
+
+---
+
+## Step 0: Lock reference + plan
+
+- Keep `docs/reference/language/types.md` as “current executable SSOT” until the implementation is complete.
+- After implementation + tests land, update `types.md` to the new semantics (it becomes SSOT again).
+
+---
+
+## Step 1: Rust VM — truthiness (A1)
+
+Target:
+- `src/backend/abi_util.rs::to_bool_vm` (or equivalent truthiness entry)
+
+Changes:
+- `Void` in boolean context → return `TypeError`
+- `BoxRef`:
+ - allow only explicit bridge boxes: BoolBox/IntegerBox/StringBox
+ - treat VoidBox as Void (→ TypeError)
+ - other BoxRef types → TypeError
+
+Acceptance:
+- A dedicated fixture demonstrates `if Void { ... }` is a runtime error (fail-fast).
+
+---
+
+## Step 2: Rust VM — equality (B2)
+
+Target:
+- `src/backend/abi_util.rs::eq_vm` (and any helpers it relies on)
+
+Changes:
+- Remove Bool↔Int coercion.
+- Keep Int↔Float comparison, but make it precise:
+ - if Float is NaN → false
+ - if Float is integral and within i64 exact range → compare as Int exactly
+ - otherwise → false
+- Mixed kinds (except Int↔Float) → false (not error).
+- BoxRef equality stays identity.
+
+Acceptance:
+- Tests cover:
+ - `1 == 1.0` true
+ - `1 == 1.1` false
+ - `true == 1` false (or TypeError if you choose a transition rule; document explicitly)
+
+---
+
+## Step 3: Rust VM — `+` (C2)
+
+Target:
+- `src/backend/mir_interpreter/helpers.rs::eval_binop` for `BinaryOp::Add`
+
+Changes:
+- Numeric:
+ - Int+Int → Int
+ - Float+Float → Float
+ - Int+Float / Float+Int → Float (Int promoted to Float)
+- String:
+ - String+String → concat
+ - String mixed → TypeError (no implicit stringify)
+- Everything else → TypeError
+
+Acceptance:
+- Tests cover:
+ - `1 + 2.0` → `3.0`
+ - `"a" + "b"` → `"ab"`
+ - `"a" + 1` → TypeError
+
+---
+
+## Step 4: LLVM harness parity
+
+Targets (likely):
+- `src/llvm_py/instructions/binop.py` for `+`
+- `src/llvm_py/instructions/compare.py` for `==`
+- truthiness for branch conditions (inspect branch lowering):
+ - `src/llvm_py/instructions/controlflow/branch.py`
+ - and any “truthy” conversions used in control-flow lowering
+
+Notes:
+- LLVM harness uses MIR JSON metadata (`value_types`) for discriminating raw vs handle.
+- Keep behavior identical to Rust VM; do not re-introduce “string mixed concat”.
+
+Acceptance:
+- VM and LLVM smokes use the same fixtures and produce identical exit codes.
+
+---
+
+## Step 5: Fixtures + smoke tests (SSOT lock)
+
+Create minimal, self-contained fixtures under `apps/tests/` and add smokes under `tools/smokes/v2/profiles/integration/apps/`.
+
+Suggested fixtures (names; adjust as needed):
+- `apps/tests/phase275_p0_truthiness_void_error_min.hako`
+- `apps/tests/phase275_p0_eq_number_only_min.hako`
+- `apps/tests/phase275_p0_plus_number_only_min.hako`
+
+Smoke targets:
+- VM: `..._vm.sh`
+- LLVM: `..._llvm.sh` (harness/EXE path consistent with current infra)
+
+Rules:
+- No reliance on hidden env toggles.
+- If a test needs runtime-unknown values, avoid externs that aren’t supported on VM/LLVM.
+
+---
+
+## Step 6: Update SSOT docs
+
+After the implementation is complete and tests pass:
+- Update `docs/reference/language/types.md` to the new semantics:
+ - truthiness: Void/BoxRef fail-fast
+ - equality: B2
+ - `+`: C2
+- Update Phase 274/275 status:
+ - `docs/development/current/main/phases/phase-274/README.md`
+ - `docs/development/current/main/10-Now.md`
+ - `docs/development/current/main/30-Backlog.md`
+
diff --git a/docs/development/current/main/phases/phase-275/P1-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-275/P1-INSTRUCTIONS.md
new file mode 100644
index 00000000..0ee4d661
--- /dev/null
+++ b/docs/development/current/main/phases/phase-275/P1-INSTRUCTIONS.md
@@ -0,0 +1,74 @@
+# Phase 275 P1 (docs/lock): Update types.md + lock coercion matrix
+
+Status: planned / instruction sheet
+
+This is the follow-up after Phase 275 P0 (implementation) lands.
+
+Prereq:
+- Phase 275 P0 is complete and VM/LLVM smokes are green.
+- Decisions are frozen in `docs/development/current/main/phases/phase-274/P3-DECISIONS.md`.
+
+---
+
+## 1) Update the language SSOT doc
+
+File:
+- `docs/reference/language/types.md`
+
+Edits:
+- Move “Accepted (Phase 275)” rules from “pending” into the **current executable SSOT** sections:
+ - truthiness: `Void` and non-bridge `BoxRef` become `TypeError`
+ - `==`: B2 (Number-only) with precise Int↔Float; Bool↔Number has no coercion
+ - `+`: C2 (Number-only promotion) + String+String only; String mixed `TypeError`
+- Add a short “Migration notes” subsection:
+ - how to rewrite old code (`x != Void`, `str(x)`, interpolation)
+ - explicitly call out any intentionally-breaking changes
+
+Acceptance:
+- `types.md` no longer describes the legacy behavior (e.g. `Void -> false` in conditions, `"a"+1` concat).
+
+---
+
+## 2) Add the minimum test matrix fixtures (SSOT lock)
+
+Goal:
+- prevent semantic drift by locking the truthiness / `==` / `+` matrix in fixtures + smokes.
+
+Add fixtures under `apps/tests/` (minimal, self-contained):
+- `apps/tests/phase275_p0_truthiness_min.hako`
+- `apps/tests/phase275_p0_eq_min.hako`
+- `apps/tests/phase275_p0_plus_min.hako`
+
+Rules:
+- no env toggles required
+- no dependency on unsupported externs/symbols on LLVM line
+
+Add smokes under `tools/smokes/v2/profiles/integration/apps/`:
+- `phase275_p0_truthiness_vm.sh` / `phase275_p0_truthiness_llvm.sh`
+- `phase275_p0_eq_vm.sh` / `phase275_p0_eq_llvm.sh`
+- `phase275_p0_plus_vm.sh` / `phase275_p0_plus_llvm.sh`
+
+Acceptance:
+- all 6 smokes pass (expected exit codes fixed and documented in the scripts).
+
+---
+
+## 3) Update “Now / Backlog”
+
+Files:
+- `docs/development/current/main/10-Now.md`
+- `docs/development/current/main/30-Backlog.md`
+- `docs/development/current/main/phases/phase-275/README.md`
+
+Edits:
+- Mark Phase 275 as ✅ complete (P0 done; P1 done).
+- Move any remaining work (warnings/lints, broader coercion coverage) into Phase 276+ entries.
+
+---
+
+## 4) Optional: add diagnostics without new env vars (future phase)
+
+If you want to surface legacy patterns proactively (without changing semantics again), create a new phase and keep it strictly “diagnostic-only”:
+- warnings for `if Void`, string-mixed `+`, etc.
+- no behavior toggles via new env vars
+
diff --git a/docs/development/current/main/phases/phase-275/README.md b/docs/development/current/main/phases/phase-275/README.md
new file mode 100644
index 00000000..85407c1e
--- /dev/null
+++ b/docs/development/current/main/phases/phase-275/README.md
@@ -0,0 +1,50 @@
+# Phase 275: Coercion Implementation (truthiness / `==` / `+`)
+
+Status: ✅ completed (Phase 275 P0)
+
+Goal: implement the accepted coercion SSOT across backends (Rust VM + LLVM harness) and update language docs and fixtures so behavior cannot drift.
+
+Accepted SSOT decisions:
+- `docs/development/current/main/phases/phase-274/P3-DECISIONS.md`
+
+Language SSOT (updated after Phase 275):
+- `docs/reference/language/types.md`
+
+---
+
+## What changes in this phase
+
+Implement these semantics (A1/B2/C2):
+
+1) truthiness (boolean context)
+- `Void` in condition → **TypeError**
+- `BoxRef` in condition → **TypeError**, except explicit bridge boxes (BoolBox/IntegerBox/StringBox)
+- “object is always truthy” remains prohibited
+
+2) equality (`==`)
+- allow Int↔Float numeric comparison only (precise rule; no accidental true via float rounding)
+- Bool is not a number: no Bool↔Int coercion
+- other mixed kinds: deterministic `false` (not error)
+- BoxRef equality: identity only
+
+3) `+`
+- numeric add: Int+Int, Float+Float, Int↔Float promotion to Float
+- string concat: **String+String only**
+- String mixed (`"a"+1`, `1+"a"`) → TypeError (no implicit stringify)
+
+---
+
+## Acceptance criteria (minimum)
+
+- Rust VM behavior matches `P3-DECISIONS.md` for truthiness / `==` / `+`.
+- LLVM harness behavior matches Rust VM (no backend divergence).
+- `docs/reference/language/types.md` is updated to reflect the new executable SSOT.
+- New fixtures + smokes lock behavior (VM + LLVM) without introducing environment-variable sprawl.
+- No by-name hardcoding for “special cases”; if something must be special, it must be a documented bridge rule.
+
+---
+
+## Implementation guide
+
+- `docs/development/current/main/phases/phase-275/P0-INSTRUCTIONS.md`
+- `docs/development/current/main/phases/phase-275/P1-INSTRUCTIONS.md`
diff --git a/docs/development/current/main/phases/phase-276/P0-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-276/P0-INSTRUCTIONS.md
new file mode 100644
index 00000000..32cc7371
--- /dev/null
+++ b/docs/development/current/main/phases/phase-276/P0-INSTRUCTIONS.md
@@ -0,0 +1,65 @@
+# Phase 276 P0: Quick wins (LLVM harness maintainability)
+
+Status: ✅ completed (2025-12-22)
+
+This sheet documents what Phase 276 P0 targeted (and can be used to reproduce the intent for future refactors).
+
+Reference completion:
+- `docs/development/current/main/phases/phase-276/P0-COMPLETION.md`
+
+Non-goals:
+- pipeline unification / order drift fixes (tracked as Phase 279)
+- new language features
+- new env vars
+
+---
+
+## Task 1: Remove noisy debug leftovers
+
+Target:
+- `src/llvm_py/phi_wiring/wiring.py`
+
+Acceptance:
+- no stacktrace spam in normal runs
+
+---
+
+## Task 2: Consolidate PHI dst type lookup into SSOT
+
+Create:
+- `src/llvm_py/phi_wiring/type_helper.py`
+
+Provide:
+- `get_phi_dst_type(...)` (SSOT for “what is the intended type of this ValueId?”)
+- `dst_type_to_llvm_type(...)` (SSOT for MIR dst_type → LLVM IR type)
+
+Integrate (remove duplicate logic):
+- `src/llvm_py/phi_wiring/tagging.py`
+- `src/llvm_py/llvm_builder.py`
+- `src/llvm_py/phi_wiring/wiring.py`
+
+Acceptance:
+- the same ValueId gets the same effective type across these entry points
+- adding a new MIR dst_type requires changing only `type_helper.py`
+
+---
+
+## Task 3: Make PHI type mismatch visible
+
+Target:
+- `src/llvm_py/phi_wiring/wiring.py`
+
+Policy:
+- important mismatch warnings should be visible even without debug env vars
+- keep the full trace behind the existing debug gate
+
+---
+
+## Task 4: Confirm the change does not regress existing smokes
+
+Minimum:
+- build still succeeds
+- representative LLVM harness runs still pass (no new failures)
+
+Note:
+- If a failure points to “two pipelines / ordering drift”, treat it as Phase 279 scope.
diff --git a/docs/development/current/main/phases/phase-276/README.md b/docs/development/current/main/phases/phase-276/README.md
new file mode 100644
index 00000000..fa77a6b2
--- /dev/null
+++ b/docs/development/current/main/phases/phase-276/README.md
@@ -0,0 +1,22 @@
+# Phase 276 P0: Quick wins (type helper SSOT)
+
+Status: ✅ completed (2025-12-22)
+
+Goal: keep the LLVM harness maintainable by removing “local heuristics” and consolidating type lookup into a single SSOT helper.
+
+Scope (P0):
+- Consolidate PHI destination type lookup into `type_helper.py` (SSOT).
+- Remove noisy debug leftovers.
+- Make “PHI type mismatch” more visible (fail-fast friendly diagnostics).
+
+Docs:
+- Instructions: `docs/development/current/main/phases/phase-276/P0-INSTRUCTIONS.md`
+- Completion: `docs/development/current/main/phases/phase-276/P0-COMPLETION.md`
+
+Note (important):
+- The “two pipelines / order drift” root cause is tracked separately as **Phase 279** (pipeline SSOT unification).
+
+Non-goals:
+- introduce new language features
+- widen the type system (no Union/Any work here)
+- broad optimizer rewrite
diff --git a/docs/development/current/main/phases/phase-277/P0-DESIGN.md b/docs/development/current/main/phases/phase-277/P0-DESIGN.md
new file mode 100644
index 00000000..eb9ea84e
--- /dev/null
+++ b/docs/development/current/main/phases/phase-277/P0-DESIGN.md
@@ -0,0 +1,74 @@
+# Phase 277 P0: PHI型推論ドキュメント整備(design-first)
+
+Status: planned / docs
+
+Goal: Phase 275/276 で導入・修正された PHI 型推論(MIR→LLVM harness)について、導線・責務・SSOT を “迷子にならない形” で固定する。
+
+Scope:
+- 実装のリファクタではなく **ドキュメントSSOT**。
+- “どのファイルが何の責務か” を明確にし、次回のデバッグで「触る場所」を 1 本化する。
+
+Non-goals:
+- 新しい env var 追加
+- PHI アルゴリズム変更(仕様変更)
+- 既定挙動変更
+
+---
+
+## 1) SSOT の入口を定義する
+
+必ず最初に入口を 1 箇所に固定する:
+- `docs/development/current/main/phases/phase-277/README.md` を「入口SSOT」にする
+- PHI 関連の “概念” と “コードの参照先” を README から辿れるようにする
+
+---
+
+## 2) “2本のパイプライン” を防ぐ説明を入れる
+
+このセッションで顕在化した事故:
+- ルートAでは BinOp 型伝播→PHI 型解決
+- ルートBでは PHI 型解決→BinOp 型伝播
+
+結果:
+- 同じ fixture が片方で PASS、片方で FAIL(実質 “2本のコンパイラ”)
+
+P0 では、これを README で明示し、根治フェーズ(Phase 279)へ誘導する文言を固定する。
+
+---
+
+## 3) コード責務マップ(最低限)
+
+以下の “責務の箱” を docs に書き出す(箇条書きでよい):
+
+- **MIR 側(型情報のSSOT)**
+ - `MirInstruction.dst_type` の意味(instruction-local SSOT)
+ - `value_types`(propagation/inference の結果)の意味(analysis SSOT)
+ - PHI の `dst_type` の意味(PHI-local SSOT)
+
+- **JoinIR / bridge 側**
+ - “どのルートで type propagation が走るか”
+ - “どの段で value_types が更新されるか”
+
+- **LLVM harness 側(消費者)**
+ - `type_helper.py` が SSOT であること(Phase 276 P0)
+ - `dst_type_to_llvm_type` の contract(`f64` / `i64` handle / `void`)
+
+---
+
+## 4) デバッグ導線(最小)
+
+“迷子防止” のため、以下を docs に固定する:
+- PHI関連の推奨 env var(Phase 277 P2 の統合版)
+ - `NYASH_LLVM_DEBUG_PHI=1`
+ - `NYASH_LLVM_DEBUG_PHI_TRACE=1`
+ - `NYASH_LLVM_PHI_STRICT=1`
+- 典型的な確認コマンド(1〜2本だけ)
+- 失敗時に「次に見るファイル」を 1 行で指示(type_helper → wiring → resolver の順など)
+
+---
+
+## 5) 完了条件
+
+- README から “PHI型推論の導線” が 1 本で読める
+- “2本のパイプライン” の危険と、根治フェーズ(Phase 279)へのリンクが明示されている
+- 既存 docs との矛盾がない(Phase 275/276/277 の整合)
diff --git a/docs/development/current/main/phases/phase-277/P0-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-277/P0-INSTRUCTIONS.md
new file mode 100644
index 00000000..459610a5
--- /dev/null
+++ b/docs/development/current/main/phases/phase-277/P0-INSTRUCTIONS.md
@@ -0,0 +1,79 @@
+# Phase 277 P0: PHI型推論 SSOT docs を完成させる(Claude Code 指示書)
+
+Status: instructions / docs-only
+
+目的:
+- PHI型推論(MIR→LLVM harness)の導線・責務・SSOT を **1本に固定**し、次回のデバッグで迷子にならないようにする。
+
+スコープ:
+- docs のみ(設計と導線の固定)
+- 実装変更・仕様変更は行わない
+
+Non-goals:
+- “2本のコンパイラ(パイプライン差)” の根治(Phase 279)
+- env var 追加(禁止)
+
+入口SSOT:
+- `docs/development/current/main/phases/phase-277/README.md`
+
+参照:
+- P0設計メモ: `docs/development/current/main/phases/phase-277/P0-DESIGN.md`
+- P2完了(env var 統合): `docs/development/current/main/phases/phase-277/P2-COMPLETION.md`
+- env vars: `docs/reference/environment-variables.md`
+- type helper SSOT(Phase 276 P0): `src/llvm_py/phi_wiring/type_helper.py`
+
+---
+
+## Step 1: README を “PHI型推論の地図” にする
+
+`docs/development/current/main/phases/phase-277/README.md` に以下の節を追加/更新して、READMEだけ読めば導線が分かる状態にする。
+
+必須内容(短くてOK):
+- 何が SSOT か(どのファイルが “決める” か)
+- どこが consumer か(LLVM harness が何を期待するか)
+- どこを見れば原因が特定できるか(迷子防止)
+
+最低限の “責務マップ”:
+- MIR 側:
+ - `MirInstruction.dst_type`(instruction-local)
+ - propagated `value_types`(analysis)
+ - PHI `dst_type`(PHI-local)
+- LLVM harness 側:
+ - PHI env var SSOT: `src/llvm_py/phi_wiring/debug_helper.py`
+ - 型取得 SSOT: `src/llvm_py/phi_wiring/type_helper.py`
+ - PHI placeholder SSOT: `src/llvm_py/phi_wiring/wiring.py::ensure_phi`
+ - 順序検証: `src/llvm_py/phi_placement.py`(現状は verify/report)
+
+注意:
+- llvmlite は基本 “命令の並べ替え” ができないことを明記する(PHI-first は生成時に守る)。
+- “2本のパイプライン” 問題は Phase 279 へリンクし、P0 で根治しないことを明確化する。
+
+---
+
+## Step 2: デバッグ導線(最小)を README に固定
+
+README に以下を固定する(1〜2コマンドだけ、冗長にしない):
+
+- 推奨 env var(Phase 277 P2 統合版)
+ - `NYASH_LLVM_DEBUG_PHI=1`
+ - `NYASH_LLVM_DEBUG_PHI_TRACE=1`
+ - `NYASH_LLVM_PHI_STRICT=1`
+
+- 典型コマンド(例)
+ - `NYASH_LLVM_DEBUG_PHI=1 NYASH_LLVM_USE_HARNESS=1 ./target/release/hakorune --backend llvm apps/tests/.hako`
+
+- 失敗時の “次に見るファイル” を 1 行で指示(固定順)
+ - `type_helper.py → wiring.py → llvm_builder.py → resolver.py`
+
+---
+
+## Step 3: docs 整合チェック(最小)
+
+確認:
+- `docs/development/current/main/10-Now.md` の “次にやる” が Phase 277 を指していること
+- `docs/development/current/main/30-Backlog.md` の Phase 277/278/279 が矛盾していないこと
+
+Acceptance:
+- README が入口SSOTとして成立(READMEだけで導線が追える)
+- P0-DESIGN/P1-VALIDATION/P2-COMPLETION へリンクがある
+- 新しい env var を増やしていない
diff --git a/docs/development/current/main/phases/phase-277/P1-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-277/P1-INSTRUCTIONS.md
new file mode 100644
index 00000000..72a9ff91
--- /dev/null
+++ b/docs/development/current/main/phases/phase-277/P1-INSTRUCTIONS.md
@@ -0,0 +1,100 @@
+# Phase 277 P1: PHI順序検証を fail-fast 導線に接続する(Claude Code 指示書)
+
+Status: instructions / validation
+
+目的:
+- PHI の “順序違反/配線欠落/型不整合” を **原因箇所で止める**(fail-fast)。
+- strict mode(`NYASH_LLVM_PHI_STRICT=1`)が “実際に効く” 状態にする。
+
+重要な制約:
+- Phase 277 P1 は **LLVM harness(Python)側の検証強化**が主。JoinIR/Rust の型伝播パイプラインには踏み込まない(根治は Phase 279)。
+- env var 増殖禁止(既存の3つのみ)。
+- by-name hardcode 禁止(特定関数名の例外分岐などは増やさない)。
+
+参照:
+- 検証方針: `docs/development/current/main/phases/phase-277/P1-VALIDATION.md`
+- PHI placeholder SSOT: `src/llvm_py/phi_wiring/wiring.py::ensure_phi`
+- PHI ordering verifier: `src/llvm_py/phi_placement.py::verify_phi_ordering`
+- 実行導線(現状SSOT): `src/llvm_py/builders/function_lower.py`(`_finalize_phis` 経路)
+- env vars SSOT: `src/llvm_py/phi_wiring/debug_helper.py`
+
+---
+
+## Step 1: strict mode で “PHIが遅く作られた” を即死にする
+
+対象:
+- `src/llvm_py/phi_wiring/wiring.py::ensure_phi`
+
+現状:
+- `bb.terminator` がある状態で PHI を作ろうとすると warning を出すだけ。
+
+P1の変更:
+- `NYASH_LLVM_PHI_STRICT=1` のときは fail-fast:
+ - 例: `raise RuntimeError(...)`(block_id/dst_vid を含める)
+- strict 以外は従来どおり warning + 継続(既定挙動を壊さない)。
+
+Acceptance:
+- “PHI after terminator” が strict で必ず落ちる。
+- エラー文に `block_id`, `dst_vid`, “next file” を含める(迷子防止)。
+
+---
+
+## Step 2: strict mode で “fallback 0” を禁止する
+
+対象候補(実態に合わせて最小1箇所から):
+- `src/llvm_py/phi_wiring/wiring.py::wire_incomings`
+- もしくは `src/llvm_py/llvm_builder.py::finalize_phis`(ローカル実装が残っているので注意)
+
+方針:
+- incoming が解決できずに `0` を入れる分岐があるなら、strict で Err にする。
+- Err には `block_id/dst_vid/pred_bid` を必ず含める。
+
+注意:
+- どの finalize 経路が SSOT かを明確にする(現状は `builders/function_lower.py` が実行導線)。
+- “2本の finalize 実装” を統合するのは Phase 279 のスコープ。P1では SSOT 経路に検証を接続する。
+
+Acceptance:
+- strict で silent fallback が残っていない(少なくとも PHI incoming の “解決不能→0” は落ちる)。
+
+---
+
+## Step 3: `verify_phi_ordering()` を実行導線に接続する
+
+対象:
+- `src/llvm_py/phi_placement.py::verify_phi_ordering(builder)`
+
+現状:
+- 定義されているが、実行導線から呼ばれていない。
+- llvmlite は reorder ができないため、verify/report の位置が重要。
+
+接続点(推奨):
+- `src/llvm_py/builders/function_lower.py` の関数 lowering の終盤:
+ - `lower_terminators(...)` の後(全命令が出揃った後)
+ - strict のときは NG を Err にする
+ - debug のときは block ごとのサマリを stderr に出す(`NYASH_LLVM_DEBUG_PHI=1`)
+
+Acceptance:
+- strict で ordering NG を確実に検出して落とせる。
+- debug で NG block の数と block_id が出る(過剰ログは避ける)。
+
+---
+
+## Step 4: 最小の回帰確認
+
+目的:
+- “検証が増えたせいで全部が落ちる” を避けつつ、狙った違反を確実に捕まえる。
+
+推奨:
+- 代表 fixture を1つ選び、まず strict=OFF で PASS、strict=ON でも PASS を確認(正常系)。
+- 既知の壊れ方(PHI late create / missing incoming)を意図的に起こす最小再現があるなら、それで strict で落ちることも確認。
+
+No new env vars.
+
+---
+
+## Completion criteria
+
+- strict mode が “順序違反/配線欠落” を原因箇所で fail-fast できる
+- `verify_phi_ordering()` が実行導線に接続されている
+- 既定(strict=OFF)での挙動は壊さない
+- Phase 279(根治)へ繋がる前提が docs で明確になっている
diff --git a/docs/development/current/main/phases/phase-277/P1-VALIDATION.md b/docs/development/current/main/phases/phase-277/P1-VALIDATION.md
new file mode 100644
index 00000000..3f0933f1
--- /dev/null
+++ b/docs/development/current/main/phases/phase-277/P1-VALIDATION.md
@@ -0,0 +1,102 @@
+# Phase 277 P1: PHI順序検証強化(validation)
+
+Status: planned / validation
+
+Goal: PHI placement/order を fail-fast で検出しやすくし、LLVM harness の “後段で壊れる” ではなく “原因箇所で止まる” を実現する。
+
+Scope:
+- 検証とエラーメッセージの改善(実装は最小・局所)
+- “順序違反” と “型不整合” の可観測性を上げる
+
+Non-goals:
+- 新しい env var 追加
+- 大規模なパイプライン統一(Phase 279)
+
+---
+
+## 1) 何を検証するか(契約SSOT)
+
+最低限、この契約を SSOT として明文化する:
+
+- **Block内順序**:
+ - PHI 群
+ - non-PHI 命令群
+ - terminator(Branch/Jump/Return)
+ - この順序以外は “バグ” として扱う
+
+- **PHI 入力の完全性**:
+ - incoming が欠ける場合は fail-fast(既定で silent fallback をしない)
+ - strict mode(`NYASH_LLVM_PHI_STRICT=1`)では必ず Err
+
+- **型整合**:
+ - `dst_type` と実際に生成する LLVM type が一致していること
+ - mismatch を “CRITICAL” として可視化する(Phase 276 P0 の方針を踏襲)
+
+---
+
+## 2) 実装ポイント(現状コードに合わせた最小)
+
+現状の構造(要点):
+- llvmlite は “命令の並べ替え” が基本できないため、PHI-first は **生成時**に守る必要がある
+ - `src/llvm_py/phi_placement.py` は “reorder” ではなく “verify/report” が主
+- PHI 配線は `finalize_phis` で行われる(PHI placeholder 作成→incoming 配線)
+ - 実際のSSOT呼び出しは `src/llvm_py/builders/function_lower.py` の `_finalize_phis(builder, context)` 経路
+ - `NyashLLVMBuilder.finalize_phis()` は別実装が残っており、P1では **どちらをSSOTにするか**を明示する
+
+実装点(推奨):
+
+1) **“PHIを遅く作ってしまった” を strict で即死**
+- 対象: `src/llvm_py/phi_wiring/wiring.py::ensure_phi`
+ - すでに `bb.terminator` を検知して warning を出している
+ - P1では `NYASH_LLVM_PHI_STRICT=1` のとき、ここを fail-fast(例: `raise` / `unreachable` 相当)にする
+- 期待効果: “順序違反の原因” で止まる
+
+2) **fallback 0 の採用を strict で禁止**
+- 対象: PHI incoming を解決できず `0` を選ぶ箇所
+ - `src/llvm_py/llvm_builder.py` および `src/llvm_py/phi_wiring/wiring.py::wire_incomings` に存在
+- P1では strict のとき:
+ - “missing snapshot / unresolved” を明示エラーにする
+ - エラー文に `block_id / dst_vid / pred_bid` を含める
+
+3) **PHI ordering verifier を “実行経路に接続”**
+- 現状 `src/llvm_py/phi_placement.py::verify_phi_ordering(builder)` が未使用
+- P1では呼び出し点を 1 箇所に固定する:
+ - 候補: `src/llvm_py/builders/function_lower.py` の `lower_terminators(...)` 後
+ - strict のときは ordering NG を Err にする
+ - debug のときは詳細を stderr に出す(`NYASH_LLVM_DEBUG_PHI=1`)
+
+補足:
+- ここで reorder はできないので、verifier は “最後に怒る” ではなく
+ “生成時の契約が破られていないことを確認する” 目的で使う
+
+---
+
+## 3) エラーメッセージ(迷子防止)
+
+エラー文は必ず以下を含める:
+- block id
+- dst ValueId(PHIの対象)
+- expected vs actual(型/順序)
+- 次に見るファイル(1つ、固定)
+
+推奨:
+- ordering なら `src/llvm_py/phi_wiring/wiring.py`(PHI生成の入口)
+- missing incoming なら `src/llvm_py/llvm_builder.py`(snapshot/value解決の入口)
+
+---
+
+## 4) 最小テスト
+
+P1 の目的は “検証が働くこと” なので、最小の再現でよい:
+- 既存の PHI を含む fixture を 1 つ選ぶ(Phase 275 のものなど)
+- strict mode で実行して、違反があれば落ちることを確認する
+
+No new CI jobs.
+
+---
+
+## 5) 完了条件
+
+- PHI順序違反が “原因箇所で” fail-fast する
+- strict mode が意味を持つ(silent fallback が残っていない)
+- 既存の正常ケース(代表スモーク)が退行しない
diff --git a/docs/development/current/main/phases/phase-277/README.md b/docs/development/current/main/phases/phase-277/README.md
index 803df2e6..864d80d1 100644
--- a/docs/development/current/main/phases/phase-277/README.md
+++ b/docs/development/current/main/phases/phase-277/README.md
@@ -4,6 +4,15 @@
Phase 275/276で完了したFloat型PHI対応・型取得SSOT化の後続改善として、PHI関連の環境変数統合・ドキュメント整備を実施。
+このPhaseの狙いは「PHIまわりの迷子を無くす」こと:
+- どの層が何を決めるか(SSOT)を固定する
+- PHI順序/配線の違反を “後段で壊れる” ではなく “原因で止まる” に寄せる
+- そして根治として「2本のコンパイラ(パイプライン差による二重バグ)」を Phase 279 で潰せるように導線を引く
+
+入口(関連):
+- Now: `docs/development/current/main/10-Now.md`
+- Backlog: `docs/development/current/main/30-Backlog.md`
+
---
## サブフェーズ一覧
@@ -13,8 +22,12 @@ Phase 275/276で完了したFloat型PHI対応・型取得SSOT化の後続改善
- 目的: Phase 275/276で実装したPHI型推論ロジックのドキュメント化
- 内容:
- MIR型伝播 → LLVM IR型生成のフロー図
- - type_helper.py の設計ドキュメント
+ - type_helper.py(LLVM harness 側の型取得SSOT)の設計ドキュメント
- PHI型推論のベストプラクティス
+- 設計メモ(このPhase配下のSSOT案):
+ - `docs/development/current/main/phases/phase-277/P0-DESIGN.md`
+- 指示書(Claude Code):
+ - `docs/development/current/main/phases/phase-277/P0-INSTRUCTIONS.md`
### Phase 277 P1: PHI順序検証強化(予定)
@@ -23,6 +36,10 @@ Phase 275/276で完了したFloat型PHI対応・型取得SSOT化の後続改善
- phi_placement.py の検証ロジック強化
- LLVM IR仕様準拠チェック(PHI → 非PHI → terminator)
- 順序違反時のエラーメッセージ改善
+- 検証メモ(このPhase配下のSSOT案):
+ - `docs/development/current/main/phases/phase-277/P1-VALIDATION.md`
+- 指示書(Claude Code):
+ - `docs/development/current/main/phases/phase-277/P1-INSTRUCTIONS.md`
### Phase 277 P2: PHI関連環境変数の統合・整理 ✅
@@ -65,6 +82,7 @@ NYASH_LLVM_PHI_STRICT=1
- **Phase 275**: Float型PHI対応(MIR型伝播 → LLVM IR double生成)
- **Phase 276**: 型取得SSOT化(type_helper.py)
- **Phase 278**: 後方互換性削除(旧環境変数サポート削除予定)
+- **Phase 279**: パイプラインSSOT統一(“2本のコンパイラ” 根治)
---
@@ -74,12 +92,36 @@ NYASH_LLVM_PHI_STRICT=1
phase-277/
├── README.md # 本ファイル(Phase 277概要)
├── P2-COMPLETION.md # P2完了報告
-├── P0-DESIGN.md # P0設計ドキュメント(予定)
-└── P1-VALIDATION.md # P1検証強化ドキュメント(予定)
+├── P0-DESIGN.md # P0設計ドキュメント(docs)
+└── P1-VALIDATION.md # P1検証強化ドキュメント(validation)
```
---
+## 重要なSSOT(どこが何を決めるか)
+
+最小の地図(迷ったらここから辿る):
+
+- **PHI env var(統合SSOT)**: `src/llvm_py/phi_wiring/debug_helper.py`
+ - 使う側は `is_phi_debug_enabled()` / `is_phi_trace_enabled()` / `is_phi_strict_enabled()` だけを見る
+ - 旧 env var の撤去は Phase 278
+
+- **LLVM harness 側の型取得SSOT**: `src/llvm_py/phi_wiring/type_helper.py`
+ - `get_phi_dst_type(...)` と `dst_type_to_llvm_type(...)` が入口
+ - “PHIのdst_typeをどこから取るか” をここに集約する(Phase 276 P0)
+
+- **PHI placeholder を block head に作るSSOT**: `src/llvm_py/phi_wiring/wiring.py::ensure_phi`
+ - llvmlite は “後から命令を並べ替える” が基本できない
+ - よって PHI は “作るタイミング” が勝負(PHI-first の契約をここで守る)
+
+- **順序検証(verifier)**: `src/llvm_py/phi_placement.py`
+ - 現状は “並べ替え” ではなく “検証/レポート” のみ(llvmlite制約)
+ - Phase 277 P1 で fail-fast 導線を強化する(strict mode の意味を強くする)
+
+- **根治(パイプライン二重化の解消)**: `docs/development/current/main/phases/phase-279/README.md`
+
+---
+
## 今後の予定
1. **Phase 277 P0**: PHI型推論ドキュメント整備
diff --git a/docs/development/current/main/phases/phase-278/P0-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-278/P0-INSTRUCTIONS.md
new file mode 100644
index 00000000..e050c521
--- /dev/null
+++ b/docs/development/current/main/phases/phase-278/P0-INSTRUCTIONS.md
@@ -0,0 +1,89 @@
+# Phase 278 P0: Remove deprecated PHI debug env vars
+
+Status: planned / cleanup
+
+Goal: remove legacy PHI-related environment variables that were consolidated in Phase 277 P2, so the ecosystem converges to a single set of PHI debug knobs.
+
+SSOT references:
+- Consolidation completion: `docs/development/current/main/phases/phase-277/P2-COMPLETION.md`
+- Env var reference (current): `docs/reference/environment-variables.md`
+
+Target SSOT (post-Phase 278):
+- `NYASH_LLVM_DEBUG_PHI=1`
+- `NYASH_LLVM_DEBUG_PHI_TRACE=1`
+- `NYASH_LLVM_PHI_STRICT=1`
+
+Non-goals:
+- introduce new debug toggles
+- change PHI behavior (only remove deprecated inputs)
+- change CLI defaults
+
+---
+
+## 1) Identify deprecated env vars (remove inputs)
+
+Remove support for the legacy variables that were “merged” in Phase 277 P2.
+
+Expected deprecated set (verify in `docs/reference/environment-variables.md`):
+- `NYASH_LLVM_PHI_DEBUG`
+- `NYASH_PHI_TYPE_DEBUG`
+- `NYASH_PHI_ORDERING_DEBUG`
+- `NYASH_LLVM_TRACE_PHI`
+- `NYASH_LLVM_VMAP_TRACE`
+- `NYASH_PYVM_DEBUG_PHI` (if still present in code; PyVM line is historical)
+
+Policy:
+- Do not keep “silent compatibility”.
+- If a deprecated var is detected, print a one-line error with the replacement and exit non-zero.
+
+Rationale:
+- Keeping deprecated behavior is how env var sprawl comes back.
+
+---
+
+## 2) Update the runtime checks to accept only the SSOT set
+
+Target:
+- `src/llvm_py/phi_wiring/debug_helper.py` (SSOT entry for PHI debug flags)
+
+Acceptance:
+- only the SSOT variables are read
+- deprecated variables trigger a clear failure message (replacement shown)
+
+---
+
+## 3) Update documentation (SSOT)
+
+Update:
+- `docs/reference/environment-variables.md`
+
+Requirements:
+- Remove deprecated entries from tables (or move them to a “Removed in Phase 278” section).
+- Keep examples only with `NYASH_LLVM_DEBUG_PHI`, `NYASH_LLVM_DEBUG_PHI_TRACE`, `NYASH_LLVM_PHI_STRICT`.
+- Add a short migration note:
+ - “If you used X, replace with Y.”
+
+---
+
+## 4) Add/Update smoke coverage
+
+Minimum:
+- One LLVM harness smoke that runs with:
+ - `NYASH_LLVM_DEBUG_PHI=1`
+ - `NYASH_LLVM_DEBUG_PHI_TRACE=1`
+ - `NYASH_LLVM_PHI_STRICT=1`
+ and verifies the run completes (no strict-mode violations in the fixture).
+
+Deprecation enforcement:
+- One smoke (or a small shell snippet in an existing test) that sets a deprecated var and expects a non-zero exit.
+
+No new env vars.
+
+---
+
+## 5) Completion criteria
+
+- Deprecated env vars no longer affect behavior (removed from code paths).
+- Deprecated env vars cause a fail-fast error with a replacement hint.
+- Docs reflect only the SSOT set.
+- The representative smokes remain green.
diff --git a/docs/development/current/main/phases/phase-278/README.md b/docs/development/current/main/phases/phase-278/README.md
new file mode 100644
index 00000000..a89ec19a
--- /dev/null
+++ b/docs/development/current/main/phases/phase-278/README.md
@@ -0,0 +1,18 @@
+# Phase 278 (planned): Remove deprecated PHI debug env vars
+
+Status: planned / cleanup
+
+Goal: remove legacy PHI debug environment variables that were consolidated in Phase 277 P2, so the ecosystem converges to a single, memorable set.
+
+Reference:
+- Consolidation doc: `docs/development/current/main/phases/phase-277/P2-COMPLETION.md`
+- Env var reference: `docs/reference/environment-variables.md` (PHI デバッグ関連)
+
+Target SSOT (post-Phase 278):
+- `NYASH_LLVM_DEBUG_PHI=1`
+- `NYASH_LLVM_DEBUG_PHI_TRACE=1`
+- `NYASH_LLVM_PHI_STRICT=1`
+
+Implementation guide:
+- `docs/development/current/main/phases/phase-278/P0-INSTRUCTIONS.md`
+
diff --git a/docs/development/current/main/phases/phase-279/P0-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-279/P0-INSTRUCTIONS.md
new file mode 100644
index 00000000..14488fff
--- /dev/null
+++ b/docs/development/current/main/phases/phase-279/P0-INSTRUCTIONS.md
@@ -0,0 +1,96 @@
+# Phase 279 P0: Unify type propagation pipeline (SSOT)
+
+Status: planned / implementation
+
+Problem:
+- Type propagation and PHI type resolution are executed in multiple routes with different orderings.
+- This creates “double bugs”: the same fixture can pass in one route and fail in another, even when the frontend and MIR are identical.
+
+Goal:
+- Define a single **TypePropagationPipeline** (SSOT) with a fixed order, and make all routes call it.
+
+Constraints:
+- No new environment variables.
+- No by-name hardcode dispatch.
+- Fail-fast on invariants (no silent fallback).
+
+---
+
+## 1) Define the SSOT pipeline (single entry)
+
+Create one entry function (name is flexible, keep it unambiguous):
+- `run_type_propagation_pipeline(module: &mut MirModule, mode: ...) -> Result<(), String>`
+
+Fixed order (SSOT):
+1. Copy type propagation
+2. BinOp type re-propagation
+3. PHI type resolution
+4. Minimal follow-ups required for downstream typing (Compare / TypeOp / etc.), only if already needed by current backends
+
+Hard rule:
+- No route may run PHI type resolution before BinOp re-propagation.
+
+Rationale:
+- PHI type inference depends on stabilized incoming value types.
+
+---
+
+## 2) Route integration (remove local ordering)
+
+Make all relevant routes call the SSOT entry and remove/disable any local ordering logic.
+
+Known routes to check (examples; confirm in code):
+- Builder lifecycle path (emits MIR directly)
+- JoinIR → MIR bridge path
+- Any “analysis/json emit” path that downstream LLVM harness relies on for `value_types`
+
+Acceptance:
+- Each route calls the same SSOT entry.
+- There is no remaining “partial pipeline” that can reorder steps.
+
+---
+
+## 3) Fail-fast order guard (prevent regressions)
+
+Add an invariant checker that makes order drift obvious:
+- If PHI type resolution is invoked while BinOp re-propagation has not run, return `Err(...)`.
+
+This is not a feature toggle. It is a structural guard.
+
+---
+
+## 4) Backends: define the contract for `value_types`
+
+Document (in code/doc) what the downstream expects:
+- A `ValueId` that is `f64` must be consistently typed as `f64` across:
+ - MIR instruction dst_type
+ - propagated/inferred `value_types`
+ - PHI dst_type
+- “i64 handle” vs “unboxed f64” must be consistent for the LLVM harness.
+
+Avoid “best-effort” inference at the harness layer. If the type is unknown, fail-fast where the SSOT contract is violated.
+
+---
+
+## 5) Minimal acceptance tests
+
+Minimum:
+- A representative fixture that exercises:
+ - Copy chain
+ - BinOp promotion (e.g. Int+Float)
+ - PHI over that promoted value
+ and is executed via all relevant routes.
+- The MIR JSON `value_types` is consistent across routes.
+
+Suggested validation commands (keep local, do not add CI jobs):
+- `cargo build --release`
+- relevant smoke(s): `tools/smokes/v2/run.sh --profile quick`
+
+---
+
+## 6) Completion criteria
+
+- No route-specific ordering logic remains.
+- Order guard prevents PHI-before-BinOp execution.
+- A reproduction fixture cannot diverge across routes due to type propagation ordering.
+- Documentation points to this phase as the SSOT for “pipeline unification”.
diff --git a/docs/development/current/main/phases/phase-279/README.md b/docs/development/current/main/phases/phase-279/README.md
new file mode 100644
index 00000000..44907c65
--- /dev/null
+++ b/docs/development/current/main/phases/phase-279/README.md
@@ -0,0 +1,25 @@
+# Phase 279 (planned): Type propagation pipeline SSOT unification
+
+Status: planned / implementation
+
+Goal: eliminate “two compiler pipelines” by making type propagation run through **one SSOT entry** with a fixed order across routes, so the same fixture cannot pass in one route and fail in another purely due to ordering drift.
+
+Background trigger:
+- A real incident occurred where PHI type resolution ran before BinOp re-propagation in one route, but after it in another, producing LLVM parity breakage. This is effectively “two compilers”.
+
+Scope:
+- Define a single type propagation pipeline entry (SSOT).
+- Make every route that emits MIR/LLVM metadata call that SSOT entry.
+- Add fail-fast guards that make order drift impossible to miss.
+
+SSOT references:
+- Current status log: `docs/development/current/main/10-Now.md`
+- Backlog entry: `docs/development/current/main/30-Backlog.md`
+
+Implementation guide:
+- `docs/development/current/main/phases/phase-279/P0-INSTRUCTIONS.md`
+
+Non-goals:
+- new language features (no Union/Any)
+- broad optimizer rewrite
+- adding new environment variables
diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md
index 99e07a74..5de80378 100644
--- a/docs/guides/testing-guide.md
+++ b/docs/guides/testing-guide.md
@@ -102,10 +102,10 @@ python3 tools/phi_trace_check.py --file tmp/phi_trace.jsonl --summary
### 1. CLI レベルの MIR ダンプ
-- ソースから直接 MIR を確認:
+- VM 実行経路(SSOT)で「実際に走るMIR」を一緒に吐く:
+ - `NYASH_VM_DUMP_MIR=1 ./target/release/hakorune --backend vm path/to/program.hako`
+- 参考: 実行せずにコンパイルだけで MIR を確認(入口確認用 / compile-only):
- `./target/release/hakorune --dump-mir path/to/program.hako`
-- VM 実行経路で MIR を一緒に吐く:
- - `NYASH_VM_DUMP_MIR=1 ./target/release/hakorune path/to/program.hako`
- JSON で詳細解析したい場合:
- `./target/release/hakorune --emit-mir-json mir.json path/to/program.hako`
- 例: `jq '.functions[0].blocks' mir.json` でブロック構造を確認。
diff --git a/docs/reference/language/LANGUAGE_REFERENCE_2025.md b/docs/reference/language/LANGUAGE_REFERENCE_2025.md
index fef4b4cb..d48f85a9 100644
--- a/docs/reference/language/LANGUAGE_REFERENCE_2025.md
+++ b/docs/reference/language/LANGUAGE_REFERENCE_2025.md
@@ -72,7 +72,7 @@ Rust製インタープリターによる高性能実行と、直感的な構文
```nyash
box ClassName {
# フィールド宣言(Phase 12.7形式)
- field1: TypeBox # フィールド型アノテーション(P0では無視)
+ field1: TypeBox # 型アノテーション(現状は契約として未強制。SSOT: docs/reference/language/types.md)
field2: TypeBox
field3 # 型なしも可
@@ -133,10 +133,9 @@ static box Main {
```
注意(静的Boxのメソッド引数規約)
-- 静的Boxのメソッドは先頭に暗黙の `self`(= Singleton)が存在する。
-- つまり呼び出し側は `Main.main()` のように書いても、意味論上は `Main.main(self, ...)` の形になる。
-- VM/MIR/LLVM いずれのバックエンドでも、この規約に基づき引数個数(arity)を判定する。
-- 開発時のガイドライン: 静的Box内に定義する全メソッドは「先頭に `self` を取る」形で設計すること(将来の最適化や検証で一貫性を保つため)。
+- 静的Boxのメソッドは **暗黙の receiver(`me/self`)を持たない**(既定)。
+- 呼び出し側の `Main.main()` は、`Main.main/` のような **receiver無しの関数呼び出し**へ正規化される。
+- 静的Box内で `me/this` を instance receiver として扱うのは禁止(Fail-Fast)。必要なら `Main.some_static()` の形で呼び出す。
📌 **構文に関する注意(`static method` について)**
diff --git a/docs/reference/language/README.md b/docs/reference/language/README.md
index 02dfe404..b2330d1a 100644
--- a/docs/reference/language/README.md
+++ b/docs/reference/language/README.md
@@ -19,6 +19,9 @@ Imports and namespaces
Variables and scope
- See: reference/language/variables-and-scope.md — Block-scoped locals, assignment resolution, and strong/weak reference guidance.
+Type system (SSOT)
+- See: reference/language/types.md — runtime truthiness, `+`/compare/equality semantics, and the role/limits of MIR type facts.
+
Grammar (EBNF)
- See: reference/language/EBNF.md — Stage‑2 grammar specification used by parser implementations.
- Unified Members (stored/computed/once/birth_once): see reference/language/EBNF.md “Box Members (Phase 15)” and the Language Reference section. Default ON (disable with `NYASH_ENABLE_UNIFIED_MEMBERS=0`).
diff --git a/docs/reference/language/quick-reference.md b/docs/reference/language/quick-reference.md
index e0aa3eec..a82ef845 100644
--- a/docs/reference/language/quick-reference.md
+++ b/docs/reference/language/quick-reference.md
@@ -40,21 +40,18 @@ Semicolons and ASI (Automatic Semicolon Insertion)
- Ambiguous continuations; parser must Fail‑Fast with a clear message.
Truthiness (boolean context)
-- `Bool` → itself
-- `Integer` → `0` is false; non‑zero is true
-- `String` → empty string is false; otherwise true
-- `Array`/`Map` → non‑null is true (size is not consulted)
-- `null`/`void` → false
+- SSOT: `reference/language/types.md`(runtime truthiness)
+- 実行上は `Bool/Integer/Float/String/Void` が中心。`BoxRef` は一部のコアBoxのみ許可され、その他は `TypeError`(Fail-Fast)。
Equality and Comparison
-- `==` and `!=` compare primitive values (Integer/Bool/String). No implicit cross‑type coercion.
-- Box/Instance comparisons should use explicit methods (`equals`), or be normalized by the builder.
-- Compare operators `< <= > >=` are defined on integers (MVP).
+- SSOT: `reference/language/types.md`(`==`/`!=` と `< <= > >=` の runtime 仕様)
+- `==` は一部の cross-kind(`Integer↔Bool`, `Integer↔Float`)を best-effort で扱う。その他は `false`。
+- `< <= > >=` は `Integer/Float/String` の **同型同士**のみ(異型は `TypeError`)。
String and Numeric `+`
-- If either side is `String`, `+` is string concatenation.
-- If both sides are numeric, `+` is addition.
-- Other mixes are errors (dev: warn; prod: error) — keep it explicit(必要なら `str(x)` を使う)。
+- SSOT: `reference/language/types.md`(runtime `+` 仕様)
+- `Integer+Integer`, `Float+Float` は加算。片側が `String` なら文字列連結(相手は文字列化)。
+- それ以外(例: `Integer+Bool`, `Integer+Float`)は `TypeError`(Fail-Fast)。
Blocks and Control
- `if (cond) { ... } [else { ... }]`
diff --git a/docs/reference/language/types.md b/docs/reference/language/types.md
new file mode 100644
index 00000000..ec9d61d6
--- /dev/null
+++ b/docs/reference/language/types.md
@@ -0,0 +1,133 @@
+# Type System (SSOT)
+
+Status: Draft / active (2025-12)
+
+This document defines the **current, executable** type semantics of Nyash/Hakorune.
+Implementation is currently anchored to the Rust VM (`src/backend/mir_interpreter/*`).
+
+If another backend differs from this document, treat it as a bug unless explicitly noted.
+
+Coercion SSOT status:
+- Decisions: `docs/development/current/main/phases/phase-274/P3-DECISIONS.md`
+- Implemented (VM + LLVM parity): Phase 275 P0
+
+---
+
+## 1. Big Picture
+
+- Nyash is **dynamically typed**: runtime values carry a tag (Integer/Float/Bool/String/Void/BoxRef/Future).
+- `local` declares a variable; **re-assignment is allowed** (single keyword policy).
+- There is currently no static type checker. Some parts of MIR carry **type facts** as metadata for optimization / routing, not for semantics.
+
+Terminology (SSOT):
+- **Runtime type**: what the VM executes on (`VMValue`).
+- **MIR type facts**: builder annotations (`MirType`, `value_types`, `value_origin_newbox`, `TypeCertainty`).
+
+---
+
+## 2. Variables and Re-assignment
+
+- `local x` / `local x = expr` introduces a mutable local variable.
+- Re-assignment is always allowed: `x = expr`.
+- “Immutable locals” (let/const) are not part of the language today; they can be introduced later as lint/strict checks without changing core semantics.
+
+Note: Field type annotations like `field: TypeBox` exist in syntax, but are currently **not enforced** as a type contract (docs-only) — see `LANGUAGE_REFERENCE_2025.md`.
+
+---
+
+## 3. Boolean Context (truthiness)
+
+Boolean context means:
+- `if (cond) { ... }`
+- `loop(cond) { ... }`
+- `!cond`
+- branch conditions generated from `&&` / `||` lowering
+
+Runtime rule (SSOT) is implemented by `to_bool_vm` (`src/backend/abi_util.rs`):
+
+- `Bool` → itself
+- `Integer` → `0` is false; non-zero is true
+- `Float` → `0.0` is false; non-zero is true
+- `String` → empty string is false; otherwise true
+- `Void` → **TypeError** (fail-fast)
+- `BoxRef`:
+ - bridge boxes only:
+ - `BoolBox` / `IntegerBox` / `StringBox` are unboxed and coerced like their primitive equivalents
+ - `VoidBox` is treated as `Void` → **TypeError**
+ - other BoxRef types → **TypeError**
+- `Future` → error (`TypeError`)
+
+This is intentionally fail-fast: “any object is truthy” is **not** assumed by default today.
+
+---
+
+## 4. Operators: `+`, comparisons, equality
+
+### 4.1 `+` (BinaryOp::Add)
+
+Runtime semantics are defined in the Rust VM (`eval_binop` in `src/backend/mir_interpreter/helpers.rs`):
+
+- Numeric addition:
+ - `Integer + Integer` → `Integer`
+ - `Float + Float` → `Float`
+- Numeric promotion:
+ - `Integer + Float` / `Float + Integer` → `Float` (promote int→float)
+- String concatenation:
+ - `String + String` → `String`
+- Other combinations are `TypeError` (e.g., `Integer + Bool`, `Integer + Float`, `Bool + Bool`, `BoxRef + ...`).
+
+Dev-only note:
+- `NYASH_VM_TOLERATE_VOID=1` (or `--dev` paths) may tolerate `Void` in some arithmetic as a safety valve; do not rely on it for spec.
+
+### 4.2 `< <= > >=` (CompareOp)
+
+Runtime semantics (`eval_cmp` in `src/backend/mir_interpreter/helpers.rs`):
+
+- `Integer <=> Integer`
+- `Float <=> Float`
+- `String <=> String` (lexicographic)
+- Other combinations are `TypeError`.
+
+### 4.3 `==` / `!=`
+
+Equality is implemented as `eq_vm` (`src/backend/abi_util.rs`) and used by comparisons:
+
+- Same-kind equality for primitives: `Integer/Float/Bool/String/Void`.
+- Cross-kind coercions (Number-only):
+ - `Integer` ↔ `Float` only, with a precise rule (avoid accidental true via float rounding)
+- `BoxRef == BoxRef` is pointer identity (`Arc::ptr_eq`).
+- `Void` is treated as equal to `BoxRef(VoidBox)` and `BoxRef(MissingBox)` for backward compatibility.
+- Other mixed kinds are `false` (not an error).
+
+Precise rule for `Int == Float` (or `Float == Int`):
+- if Float is NaN → false
+- if Float is finite, integral, and exactly representable as i64 → compare as i64
+- otherwise → false
+
+---
+
+## 5. `is` / `as` and TypeOp
+
+Source patterns like `x.is("TypeName")` / `x.as("TypeName")` are lowered to MIR `TypeOp(Check/Cast)` (see `src/mir/builder/exprs.rs`).
+
+Runtime behavior (Rust VM):
+- `TypeOp(Check, value, ty)` produces a `Bool`.
+- `TypeOp(Cast, value, ty)` returns the input value if it matches; otherwise `TypeError`.
+
+Backend note:
+- LLVM (llvmlite harness) must match this SSOT; if it differs, treat it as a bug.
+- Tracking: Phase 274 P2 (`docs/development/current/main/phases/phase-274/P2-INSTRUCTIONS.md`).
+
+---
+
+## 6. MIR Type Facts (non-semantic metadata)
+
+MIR has a lightweight type vocabulary (`MirType` in `src/mir/types.rs`) and per-value metadata:
+- `value_types: ValueId -> MirType` (type annotations / inferred hints)
+- `value_origin_newbox: ValueId -> BoxName` (origin facts for “Known receiver”)
+- `TypeCertainty::{Known, Union}` used by call routing (`src/mir/definitions/call_unified.rs`)
+
+Important rule:
+- These facts are for **optimization/routing** (e.g., Known-only rewrite, callee resolution) and must not be treated as semantic truth.
+
+If you need semantics, define it at the runtime layer (VM) and then optionally optimize by using these facts.
diff --git a/docs/tools/cli-options.md b/docs/tools/cli-options.md
index 102b6563..8644f741 100644
--- a/docs/tools/cli-options.md
+++ b/docs/tools/cli-options.md
@@ -8,7 +8,7 @@
- `--debug-fuel {N|unlimited}`: パーサーのデバッグ燃料(無限ループ対策)
## MIR関連
-- `--dump-mir`: MIRを出力(実行はしない)
+- `--dump-mir`: MIRを出力(実行はしない / compile-only。実行経路SSOTの確認は `NYASH_VM_DUMP_MIR=1 --backend vm` を優先)
- `--verify`: MIR検証を実施
- `--mir-verbose`: 詳細MIR出力(統計など)
diff --git a/src/backend/abi_util.rs b/src/backend/abi_util.rs
index 2b9613e8..ded70d93 100644
--- a/src/backend/abi_util.rs
+++ b/src/backend/abi_util.rs
@@ -12,14 +12,16 @@ use std::sync::Arc;
/// Opaque handle type used by JIT/runtime bridges.
pub type Handle = u64;
-/// Convert a VMValue to boolean using unified, permissive semantics.
+/// Convert a VMValue to boolean using unified, fail-fast semantics (Phase 275 A1).
pub fn to_bool_vm(v: &VMValue) -> Result {
match v {
VMValue::Bool(b) => Ok(*b),
VMValue::Integer(i) => Ok(*i != 0),
- VMValue::Void => Ok(false),
+ VMValue::Void => Err("Void in boolean context".to_string()),
VMValue::String(s) => Ok(!s.is_empty()),
+ VMValue::Float(f) => Ok(*f != 0.0),
VMValue::BoxRef(b) => {
+ // Bridge boxes: allow unboxing to primitive for truthiness
if let Some(bb) = b.as_any().downcast_ref::() {
return Ok(bb.value);
}
@@ -29,17 +31,17 @@ pub fn to_bool_vm(v: &VMValue) -> Result {
if let Some(sb) = b.as_any().downcast_ref::() {
return Ok(!sb.value.is_empty());
}
+ // VoidBox treated as Void → TypeError
if b.as_any().downcast_ref::().is_some() {
- return Ok(false);
+ return Err("VoidBox in boolean context".to_string());
}
Err(format!("cannot coerce BoxRef({}) to bool", b.type_name()))
}
- VMValue::Float(f) => Ok(*f != 0.0),
VMValue::Future(_) => Err("cannot coerce Future to bool".to_string()),
}
}
-/// Nyash-style equality on VMValue (best-effort for core primitives).
+/// Nyash-style equality on VMValue with precise number-only coercion (Phase 275 B2).
pub fn eq_vm(a: &VMValue, b: &VMValue) -> bool {
use VMValue::*;
match (a, b) {
@@ -48,10 +50,20 @@ pub fn eq_vm(a: &VMValue, b: &VMValue) -> bool {
(Bool(x), Bool(y)) => x == y,
(String(x), String(y)) => x == y,
(Void, Void) => true,
- // Cross-kind simple coercions commonly used in MIR compare
- (Integer(x), Bool(y)) | (Bool(y), Integer(x)) => (*x != 0) == *y,
- (Integer(x), Float(y)) => (*x as f64) == *y,
- (Float(x), Integer(y)) => *x == (*y as f64),
+ // Precise Int↔Float equality (avoid accidental true via float rounding)
+ (Integer(x), Float(y)) | (Float(y), Integer(x)) => {
+ if y.is_nan() {
+ return false;
+ }
+ if y.is_finite() && y.fract() == 0.0 {
+ // Float is integral - check exact representability
+ let y_int = *y as i64;
+ if (y_int as f64) == *y {
+ return x == &y_int;
+ }
+ }
+ false
+ }
(BoxRef(ax), BoxRef(by)) => Arc::ptr_eq(ax, by),
// Treat BoxRef(VoidBox/MissingBox) as equal to Void (null) for backward compatibility
(BoxRef(bx), Void) => {
diff --git a/src/backend/mir_interpreter/handlers/mod.rs b/src/backend/mir_interpreter/handlers/mod.rs
index 579418d6..62e7b50d 100644
--- a/src/backend/mir_interpreter/handlers/mod.rs
+++ b/src/backend/mir_interpreter/handlers/mod.rs
@@ -23,6 +23,7 @@ mod extern_provider;
mod externals;
mod memory;
mod misc;
+mod type_ops;
impl MirInterpreter {
pub(super) fn execute_instruction(&mut self, inst: &MirInstruction) -> Result<(), VMError> {
@@ -73,6 +74,9 @@ impl MirInterpreter {
MirInstruction::Compare { dst, op, lhs, rhs } => {
self.handle_compare(*dst, *op, *lhs, *rhs)?
}
+ MirInstruction::TypeOp { dst, op, value, ty } => {
+ self.handle_type_op(*dst, *op, *value, ty)?
+ }
MirInstruction::Copy { dst, src } => self.handle_copy(*dst, *src)?,
MirInstruction::Load { dst, ptr } => self.handle_load(*dst, *ptr)?,
MirInstruction::Store { ptr, value } => self.handle_store(*ptr, *value)?,
diff --git a/src/backend/mir_interpreter/handlers/type_ops.rs b/src/backend/mir_interpreter/handlers/type_ops.rs
new file mode 100644
index 00000000..f9900cbb
--- /dev/null
+++ b/src/backend/mir_interpreter/handlers/type_ops.rs
@@ -0,0 +1,114 @@
+use super::*;
+
+fn matches_mir_type(value: &VMValue, ty: &MirType) -> bool {
+ match ty {
+ MirType::Unknown => true,
+ MirType::Void => match value {
+ VMValue::Void => true,
+ VMValue::BoxRef(bx) => {
+ bx.as_any().downcast_ref::().is_some()
+ || bx
+ .as_any()
+ .downcast_ref::()
+ .is_some()
+ }
+ _ => false,
+ },
+ MirType::Bool => match value {
+ VMValue::Bool(_) => true,
+ VMValue::BoxRef(bx) => bx.as_any().downcast_ref::().is_some(),
+ _ => false,
+ },
+ MirType::Integer => match value {
+ VMValue::Integer(_) => true,
+ VMValue::BoxRef(bx) => {
+ bx.as_any().downcast_ref::().is_some()
+ }
+ _ => false,
+ },
+ MirType::Float => match value {
+ VMValue::Float(_) => true,
+ VMValue::BoxRef(bx) => bx.as_any().downcast_ref::().is_some(),
+ _ => false,
+ },
+ MirType::String => match value {
+ VMValue::String(_) => true,
+ VMValue::BoxRef(bx) => {
+ bx.as_any().downcast_ref::().is_some()
+ }
+ _ => false,
+ },
+ MirType::Future(_) => matches!(value, VMValue::Future(_)),
+ MirType::Array(_) => {
+ // Current VM representation is BoxRef(ArrayBox) (not a distinct VMValue variant).
+ // Keep this as a conservative name match to avoid guessing.
+ match value {
+ VMValue::BoxRef(bx) => {
+ if let Some(inst) = bx.as_any().downcast_ref::()
+ {
+ inst.class_name == "ArrayBox"
+ } else {
+ bx.type_name() == "ArrayBox"
+ }
+ }
+ _ => false,
+ }
+ }
+ MirType::Box(name) => match value {
+ VMValue::BoxRef(bx) => {
+ // User-defined boxes are represented as InstanceBox (type_name is not stable for user boxes).
+ if let Some(inst) = bx.as_any().downcast_ref::() {
+ if inst.class_name == *name {
+ return true;
+ }
+ // Builtin inner box name match (best-effort).
+ if let Some(inner) = inst.inner_content.as_ref() {
+ if inner.type_name() == name {
+ return true;
+ }
+ }
+ false
+ } else {
+ bx.type_name() == name
+ }
+ }
+ // Allow primitive to satisfy core "Box types" by name when user writes "IntegerBox" etc.
+ VMValue::Integer(_) => name == "IntegerBox",
+ VMValue::Float(_) => name == "FloatBox",
+ VMValue::Bool(_) => name == "BoolBox",
+ VMValue::String(_) => name == "StringBox",
+ VMValue::Void => name == "VoidBox" || name == "NullBox",
+ _ => false,
+ },
+ }
+}
+
+impl MirInterpreter {
+ pub(super) fn handle_type_op(
+ &mut self,
+ dst: ValueId,
+ op: TypeOpKind,
+ value: ValueId,
+ ty: &MirType,
+ ) -> Result<(), VMError> {
+ let v = self.reg_load(value)?;
+ match op {
+ TypeOpKind::Check => {
+ let ok = matches_mir_type(&v, ty);
+ self.regs.insert(dst, VMValue::Bool(ok));
+ Ok(())
+ }
+ TypeOpKind::Cast => {
+ if matches_mir_type(&v, ty) {
+ self.regs.insert(dst, v);
+ Ok(())
+ } else {
+ Err(VMError::TypeError(format!(
+ "type cast failed: expected {:?}, got {:?}",
+ ty, v
+ )))
+ }
+ }
+ }
+ }
+}
diff --git a/src/backend/mir_interpreter/helpers.rs b/src/backend/mir_interpreter/helpers.rs
index f0ec4ac8..5fedeb64 100644
--- a/src/backend/mir_interpreter/helpers.rs
+++ b/src/backend/mir_interpreter/helpers.rs
@@ -147,21 +147,19 @@ impl MirInterpreter {
// Dev-only safety valve for Sub (guarded): treat Void as 0
(Sub, Integer(x), VMValue::Void) if tolerate => Integer(x),
(Sub, VMValue::Void, Integer(y)) if tolerate => Integer(0 - y),
+ // Phase 275 C2: Number-only promotion
(Add, Integer(x), Integer(y)) => Integer(x + y),
- (Add, String(s), Integer(y)) => String(format!("{}{}", s, y)),
- (Add, String(s), Float(y)) => String(format!("{}{}", s, y)),
- (Add, String(s), Bool(y)) => String(format!("{}{}", s, y)),
+ (Add, Integer(x), Float(y)) => Float(x as f64 + y),
+ (Add, Float(x), Integer(y)) => Float(x + y as f64),
+ (Add, Float(x), Float(y)) => Float(x + y),
+ // String concat (String+String only)
(Add, String(s), String(t)) => String(format!("{}{}", s, t)),
- (Add, Integer(x), String(t)) => String(format!("{}{}", x, t)),
- (Add, Float(x), String(t)) => String(format!("{}{}", x, t)),
- (Add, Bool(x), String(t)) => String(format!("{}{}", x, t)),
(Sub, Integer(x), Integer(y)) => Integer(x - y),
(Mul, Integer(x), Integer(y)) => Integer(x * y),
(Div, Integer(_), Integer(0)) => return Err(VMError::DivisionByZero),
(Div, Integer(x), Integer(y)) => Integer(x / y),
(Mod, Integer(_), Integer(0)) => return Err(VMError::DivisionByZero),
(Mod, Integer(x), Integer(y)) => Integer(x % y),
- (Add, Float(x), Float(y)) => Float(x + y),
(Sub, Float(x), Float(y)) => Float(x - y),
(Mul, Float(x), Float(y)) => Float(x * y),
(Div, Float(_), Float(y)) if y == 0.0 => return Err(VMError::DivisionByZero),
diff --git a/src/backend/mir_interpreter/mod.rs b/src/backend/mir_interpreter/mod.rs
index 205174b9..f0edfb5b 100644
--- a/src/backend/mir_interpreter/mod.rs
+++ b/src/backend/mir_interpreter/mod.rs
@@ -14,7 +14,7 @@ pub(super) use crate::backend::abi_util::{eq_vm, to_bool_vm};
pub(super) use crate::backend::vm::{VMError, VMValue};
pub(super) use crate::mir::{
BasicBlockId, BinaryOp, Callee, CompareOp, ConstValue, MirFunction, MirInstruction, MirModule,
- ValueId,
+ MirType, TypeOpKind, ValueId,
};
mod exec;
diff --git a/src/cli/args.rs b/src/cli/args.rs
index 6a11606b..907ff02e 100644
--- a/src/cli/args.rs
+++ b/src/cli/args.rs
@@ -55,7 +55,7 @@ pub fn build_command() -> Command {
.arg(Arg::new("hako-run").long("hako-run").help("Run via Stage-1 (.hako) stub (equivalent to NYASH_USE_STAGE1_CLI=1)").action(clap::ArgAction::SetTrue))
.arg(Arg::new("program-json-to-mir").long("program-json-to-mir").value_name("FILE").help("Convert Program(JSON v0) to MIR(JSON) and exit (use with --json-file)"))
.arg(Arg::new("emit-exe").long("emit-exe").value_name("FILE").help("Emit native executable via ny-llvmc and exit"))
- .arg(Arg::new("emit-exe-nyrt").long("emit-exe-nyrt").value_name("DIR").help("Directory containing libnyash_kernel.a (used with --emit-exe)"))
+ .arg(Arg::new("emit-exe-nyrt").long("emit-exe-nyrt").value_name("DIR").help("Directory containing libnyash_kernel.a (used with --emit-exe). Hint: build via `cargo build -p nyash_kernel --release` (default output: target/release/libnyash_kernel.a)"))
.arg(Arg::new("emit-exe-libs").long("emit-exe-libs").value_name("FLAGS").help("Extra linker flags for ny-llvmc when emitting executable"))
.arg(Arg::new("stage3").long("stage3").help("Enable Stage-3 syntax acceptance for selfhost parser").action(clap::ArgAction::SetTrue))
.arg(Arg::new("ny-compiler-args").long("ny-compiler-args").value_name("ARGS").help("Pass additional args to selfhost child compiler"))
diff --git a/src/grammar/generated.rs b/src/grammar/generated.rs
index 25bb6e51..d94cb9c1 100644
--- a/src/grammar/generated.rs
+++ b/src/grammar/generated.rs
@@ -36,15 +36,37 @@ pub static OPERATORS_DIV_RULES: &[(&str, &str, &str, &str)] = &[
];
pub fn lookup_keyword(word: &str) -> Option<&'static str> {
for (k, t) in KEYWORDS {
- if *k == word {
- return Some(*t);
- }
+ if *k == word { return Some(*t); }
}
None
}
pub static SYNTAX_ALLOWED_STATEMENTS: &[&str] = &[
- "box", "global", "function", "static", "if", "loop", "break", "return", "print", "nowait",
- "include", "local", "outbox", "try", "throw", "using", "from",
+ "box",
+ "global",
+ "function",
+ "static",
+ "if",
+ "loop",
+ "break",
+ "return",
+ "print",
+ "nowait",
+ "include",
+ "local",
+ "outbox",
+ "try",
+ "throw",
+ "using",
+ "from",
];
-pub static SYNTAX_ALLOWED_BINOPS: &[&str] = &["add", "sub", "mul", "div", "and", "or", "eq", "ne"];
+pub static SYNTAX_ALLOWED_BINOPS: &[&str] = &[
+ "add",
+ "sub",
+ "mul",
+ "div",
+ "and",
+ "or",
+ "eq",
+ "ne",
+];
\ No newline at end of file
diff --git a/src/llvm_py/builders/function_lower.py b/src/llvm_py/builders/function_lower.py
index eeb95122..50a9c069 100644
--- a/src/llvm_py/builders/function_lower.py
+++ b/src/llvm_py/builders/function_lower.py
@@ -334,6 +334,31 @@ def lower_function(builder, func_data: Dict[str, Any]):
from builders.block_lower import lower_terminators as _lower_terminators
_lower_terminators(builder, func)
+ # Phase 277 P1: Verify PHI ordering after all instructions are emitted
+ from phi_placement import verify_phi_ordering
+ from phi_wiring.debug_helper import is_phi_strict_enabled, is_phi_debug_enabled
+ import sys
+
+ ordering_results = verify_phi_ordering(builder)
+
+ # Check results
+ failed_blocks = [bid for bid, ok in ordering_results.items() if not ok]
+
+ if failed_blocks:
+ msg = f"[function_lower/PHI] {len(failed_blocks)} blocks have incorrect PHI ordering: {failed_blocks}"
+
+ if is_phi_strict_enabled():
+ print(f"[CRITICAL] {msg}", file=sys.stderr)
+ print(f" → Blocks: {failed_blocks}", file=sys.stderr)
+ print(f" → Required order: PHI → non-PHI → terminator", file=sys.stderr)
+ raise RuntimeError(msg)
+
+ if is_phi_debug_enabled():
+ print(f"[WARNING] {msg}", file=sys.stderr)
+
+ elif is_phi_debug_enabled():
+ print(f"[function_lower/PHI] ✅ All {len(ordering_results)} blocks have correct PHI ordering", file=sys.stderr)
+
# Safety pass: ensure every basic block ends with a terminator.
# This avoids llvmlite IR parse errors like "expected instruction opcode" on empty blocks.
try:
diff --git a/src/llvm_py/instructions/typeop.py b/src/llvm_py/instructions/typeop.py
index a72f27f6..0bb62c63 100644
--- a/src/llvm_py/instructions/typeop.py
+++ b/src/llvm_py/instructions/typeop.py
@@ -1,12 +1,27 @@
"""
-TypeOp instruction lowering
-Handles type conversions and type checks
+TypeOp instruction lowering (Phase 274 P2)
+Handles type conversions and type checks with SSOT alignment to Rust VM
"""
import llvmlite.ir as ir
from typing import Dict, Optional, Any
from utils.values import safe_vmap_write
+# Type aliases for frontend normalization (shared with MIR builder)
+TYPE_ALIASES = {
+ "Int": "IntegerBox",
+ "Integer": "IntegerBox",
+ "String": "StringBox",
+ "Bool": "BoolBox",
+ "Float": "FloatBox",
+ "Array": "ArrayBox",
+ "Void": "VoidBox",
+}
+
+def normalize_type_name(ty: str) -> str:
+ """Normalize frontend type aliases to Box names"""
+ return TYPE_ALIASES.get(ty, ty)
+
def lower_typeop(
builder: ir.IRBuilder,
op: str,
@@ -21,13 +36,18 @@ def lower_typeop(
ctx: Optional[Any] = None,
) -> None:
"""
- Lower MIR TypeOp instruction
-
+ Lower MIR TypeOp instruction with SSOT alignment to Rust VM
+
+ Phase 274 P2: Implements runtime type introspection using kernel helper
+ - Primitives (Integer, String, Bool): immediate check via resolver.value_types
+ - Boxes: runtime check via kernel nyash.any.is_type_h
+ - Fail-fast: TypeOp::Cast emits trap on mismatch
+
Operations:
- - cast: Type conversion
- - is: Type check
- - as: Safe cast
-
+ - cast: Type conversion (fail-fast on mismatch)
+ - is: Type check (returns 0/1)
+ - as: Safe cast (alias for cast)
+
Args:
builder: Current LLVM IR builder
op: Operation type (cast, is, as)
@@ -50,44 +70,274 @@ def lower_typeop(
bb_map = ctx.bb_map
except Exception:
pass
+
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
src_val = resolver.resolve_i64(src_vid, builder.block, preds, block_end_values, vmap, bb_map)
else:
src_val = vmap.get(src_vid, ir.Constant(ir.IntType(64), 0))
-
- if op == "cast":
- # Type casting - for now just pass through
- # In real implementation, would check/convert box types
- safe_vmap_write(vmap, dst_vid, src_val, "typeop_cast", resolver=resolver)
- elif op == "is":
- # Type check - returns boolean (i64: 1 or 0)
- # For now, simplified implementation
- if target_type == "IntegerBox":
- # Check if it's a valid integer box handle
- # Simplified: non-zero value
- if hasattr(src_val, 'type') and src_val.type == ir.IntType(64):
- zero = ir.Constant(ir.IntType(64), 0)
- result = builder.icmp_unsigned('!=', src_val, zero)
- # Convert i1 to i64
- result = builder.zext(result, ir.IntType(64))
- else:
- result = ir.Constant(ir.IntType(64), 0)
- else:
- # For other types, would need runtime type info
- result = ir.Constant(ir.IntType(64), 0)
+ # Normalize type name
+ normalized_type = normalize_type_name(target_type) if target_type else "Unknown"
- safe_vmap_write(vmap, dst_vid, result, "typeop_is", resolver=resolver)
+ op_l = (op or "").lower()
- elif op == "as":
- # Safe cast - returns value or null/0
- # For now, same as cast
- safe_vmap_write(vmap, dst_vid, src_val, "typeop_as", resolver=resolver)
+ if op_l in ("is", "check"):
+ # Type check operation - returns i64 (0 or 1)
+ _lower_typeop_is(builder, src_vid, dst_vid, src_val, normalized_type, vmap, resolver)
+
+ elif op_l in ("cast", "as"):
+ # Cast/as operation - fail-fast on mismatch
+ _lower_typeop_cast(builder, src_vid, dst_vid, src_val, normalized_type, vmap, resolver)
else:
- # Unknown operation
+ # Unknown operation - return 0
safe_vmap_write(vmap, dst_vid, ir.Constant(ir.IntType(64), 0), "typeop_unknown")
+def _lower_typeop_is(
+ builder: ir.IRBuilder,
+ src_vid: int,
+ dst_vid: int,
+ src_val: ir.Value,
+ normalized_type: str,
+ vmap: Dict[int, ir.Value],
+ resolver=None,
+) -> None:
+ """
+ Lower TypeOp::Check (is operation)
+ Returns 1 if type matches, 0 otherwise
+
+ Strategy:
+ 1. Check resolver.value_types for raw vs handle discrimination
+ 2. Primitives (Integer, String, Bool): immediate check (no kernel call)
+ 3. Boxes: call kernel nyash.any.is_type_h
+ """
+ # Step 1: Check resolver.value_types for raw vs handle discrimination
+ mir_type = None
+ if resolver is not None and hasattr(resolver, 'value_types') and isinstance(resolver.value_types, dict):
+ mir_type = resolver.value_types.get(src_vid)
+ # Debug: log type information
+ import os
+ if os.environ.get('NYASH_TYPEOP_DEBUG') == '1':
+ print(f"[TypeOp is] src_vid={src_vid}, mir_type={mir_type}, target={normalized_type}")
+
+ # Step 2: Primitive immediate check (raw i64)
+ if _is_primitive_type(mir_type, 'Integer'):
+ # Raw i64: immediate check
+ if normalized_type in ("Integer", "IntegerBox"):
+ result = ir.Constant(ir.IntType(64), 1) # Match
+ else:
+ result = ir.Constant(ir.IntType(64), 0) # No match
+ safe_vmap_write(vmap, dst_vid, result, "typeop_is_primitive")
+ return
+
+ if _is_primitive_type(mir_type, 'String'):
+ # String check
+ if normalized_type in ("String", "StringBox"):
+ result = ir.Constant(ir.IntType(64), 1)
+ else:
+ result = ir.Constant(ir.IntType(64), 0)
+ safe_vmap_write(vmap, dst_vid, result, "typeop_is_string")
+ return
+
+ if _is_primitive_type(mir_type, 'Bool'):
+ # Bool check
+ if normalized_type in ("Bool", "BoolBox"):
+ result = ir.Constant(ir.IntType(64), 1)
+ else:
+ result = ir.Constant(ir.IntType(64), 0)
+ safe_vmap_write(vmap, dst_vid, result, "typeop_is_bool")
+ return
+
+ # Step 3: Handle (Box) → call kernel is_type_h
+ result = _emit_kernel_type_check(builder, src_val, normalized_type)
+ safe_vmap_write(vmap, dst_vid, result, "typeop_is_handle")
+
+def _lower_typeop_cast(
+ builder: ir.IRBuilder,
+ src_vid: int,
+ dst_vid: int,
+ src_val: ir.Value,
+ normalized_type: str,
+ vmap: Dict[int, ir.Value],
+ resolver=None,
+) -> None:
+ """
+ Lower TypeOp::Cast (cast/as operation)
+ Returns value if type matches, traps on mismatch (fail-fast)
+
+ Strategy:
+ 1. Check resolver.value_types for raw vs handle discrimination
+ 2. Primitives: immediate check, trap on mismatch
+ 3. Boxes: kernel check, trap on mismatch
+ """
+ # Step 1: Check resolver.value_types for raw vs handle discrimination
+ mir_type = None
+ if resolver is not None and hasattr(resolver, 'value_types') and isinstance(resolver.value_types, dict):
+ mir_type = resolver.value_types.get(src_vid)
+
+ # Step 2: Primitive immediate check (raw i64)
+ if _is_primitive_type(mir_type, 'Integer'):
+ # Raw i64: check target type
+ if normalized_type in ("Integer", "IntegerBox"):
+ # Match: pass through
+ safe_vmap_write(vmap, dst_vid, src_val, "typeop_cast_primitive_ok")
+ return
+ else:
+ # Mismatch: fail-fast (trap)
+ _emit_trap(builder)
+ # Unreachable after trap, but emit defensive value
+ safe_vmap_write(vmap, dst_vid, ir.Constant(ir.IntType(64), 0), "typeop_cast_primitive_trap")
+ return
+
+ if _is_primitive_type(mir_type, 'String'):
+ # String check
+ if normalized_type in ("String", "StringBox"):
+ safe_vmap_write(vmap, dst_vid, src_val, "typeop_cast_string_ok")
+ return
+ else:
+ _emit_trap(builder)
+ safe_vmap_write(vmap, dst_vid, ir.Constant(ir.IntType(64), 0), "typeop_cast_string_trap")
+ return
+
+ if _is_primitive_type(mir_type, 'Bool'):
+ # Bool check
+ if normalized_type in ("Bool", "BoolBox"):
+ safe_vmap_write(vmap, dst_vid, src_val, "typeop_cast_bool_ok")
+ return
+ else:
+ _emit_trap(builder)
+ safe_vmap_write(vmap, dst_vid, ir.Constant(ir.IntType(64), 0), "typeop_cast_bool_trap")
+ return
+
+ # Step 3: Handle (Box) → call kernel is_type_h for runtime check
+ check_result = _emit_kernel_type_check(builder, src_val, normalized_type)
+
+ # Check if type matches
+ zero = ir.Constant(ir.IntType(64), 0)
+ is_match = builder.icmp_unsigned('!=', check_result, zero, name=f"typeop_cast_match_{dst_vid}")
+
+ # Fail-fast: if mismatch, trap
+ # Get current function for block allocation
+ fn = builder.function
+ trap_bb = fn.append_basic_block(name=f"typeop_cast_fail_{dst_vid}")
+ ok_bb = fn.append_basic_block(name=f"typeop_cast_ok_{dst_vid}")
+
+ builder.cbranch(is_match, ok_bb, trap_bb)
+
+ # Trap block
+ builder.position_at_end(trap_bb)
+ _emit_trap(builder)
+
+ # OK block
+ builder.position_at_end(ok_bb)
+ safe_vmap_write(vmap, dst_vid, src_val, "typeop_cast_handle_ok")
+
+def _is_primitive_type(mir_type, expected: str) -> bool:
+ """
+ Check if MIR type metadata indicates a primitive type
+
+ Args:
+ mir_type: Value from resolver.value_types (str or dict)
+ expected: Expected primitive name ("Integer", "String", "Bool")
+
+ Returns:
+ True if mir_type indicates the expected primitive
+ """
+ if mir_type is None:
+ return False
+
+ # JSON metadata vocabulary (from Rust runner):
+ # - "i64" for integers
+ # - "i1" for booleans
+ # - {"kind":"string"} for strings (even when runtime value is a StringBox handle)
+ if expected == "Integer":
+ if mir_type in ("i64", "Integer"):
+ return True
+ if isinstance(mir_type, dict) and mir_type.get("kind") in ("i64", "Integer"):
+ return True
+ return False
+
+ if expected == "Bool":
+ if mir_type in ("i1", "Bool"):
+ return True
+ if isinstance(mir_type, dict) and mir_type.get("kind") in ("i1", "Bool"):
+ return True
+ return False
+
+ if expected == "String":
+ if mir_type == "String":
+ return True
+ if isinstance(mir_type, dict):
+ k = mir_type.get("kind")
+ if k == "string":
+ return True
+ if k == "handle" and mir_type.get("box_type") == "StringBox":
+ return True
+ if mir_type.get("box_type") == "StringBox":
+ return True
+ return False
+
+ return False
+
+def _emit_kernel_type_check(builder: ir.IRBuilder, src_val: ir.Value, type_name: str) -> ir.Value:
+ """
+ Emit call to kernel nyash.any.is_type_h for runtime type check
+
+ Args:
+ builder: LLVM IR builder
+ src_val: Source value (i64 handle)
+ type_name: Normalized type name (e.g., "IntegerBox")
+
+ Returns:
+ i64 result (1 if match, 0 otherwise)
+ """
+ # Create global string for type name
+ type_name_bytes = bytearray(type_name.encode('utf-8') + b'\0')
+ type_name_str = ir.GlobalVariable(
+ builder.module,
+ ir.ArrayType(ir.IntType(8), len(type_name_bytes)),
+ name=f"type_name_{type_name}"
+ )
+ type_name_str.initializer = ir.Constant(
+ ir.ArrayType(ir.IntType(8), len(type_name_bytes)),
+ type_name_bytes
+ )
+ type_name_str.linkage = 'internal'
+ type_name_str.global_constant = True
+
+ # Get pointer to string
+ type_name_ptr = builder.gep(
+ type_name_str,
+ [ir.Constant(ir.IntType(32), 0), ir.Constant(ir.IntType(32), 0)],
+ name=f"type_name_ptr_{type_name}"
+ )
+
+ # Declare kernel function (check if already exists to avoid duplicates)
+ func_name = "nyash.any.is_type_h"
+ try:
+ is_type_h = builder.module.get_global(func_name)
+ except KeyError:
+ # Function doesn't exist, create it
+ is_type_h_ty = ir.FunctionType(ir.IntType(64), [ir.IntType(64), ir.IntType(8).as_pointer()])
+ is_type_h = ir.Function(builder.module, is_type_h_ty, name=func_name)
+
+ # Call kernel helper
+ result = builder.call(is_type_h, [src_val, type_name_ptr], name=f"is_{type_name}")
+
+ return result
+
+def _emit_trap(builder: ir.IRBuilder) -> None:
+ """
+ Emit unreachable instruction for fail-fast TypeOp error
+
+ Args:
+ builder: LLVM IR builder
+
+ Note: Uses unreachable directly instead of llvm.trap intrinsic
+ for simplicity and llvmlite compatibility
+ """
+ builder.unreachable()
+
def lower_convert(
builder: ir.IRBuilder,
src_vid: int,
@@ -103,7 +353,7 @@ def lower_convert(
) -> None:
"""
Lower type conversion between primitive types
-
+
Args:
builder: Current LLVM IR builder
src_vid: Source value ID
@@ -142,7 +392,7 @@ def lower_convert(
else:
safe_vmap_write(vmap, dst_vid, ir.Constant(ir.IntType(64), 0), "convert_default_i64")
return
-
+
# Perform conversion
if from_type == "i64" and to_type == "f64":
# int to float
diff --git a/src/llvm_py/phi_wiring/wiring.py b/src/llvm_py/phi_wiring/wiring.py
index 9b3a2021..dafe50b8 100644
--- a/src/llvm_py/phi_wiring/wiring.py
+++ b/src/llvm_py/phi_wiring/wiring.py
@@ -22,6 +22,9 @@ def ensure_phi(builder, block_id: int, dst_vid: int, bb: ir.Block, dst_type=None
Phase 275 P0: Support Float type PHIs (double) based on dst_type from MIR JSON.
"""
+ # Phase 277 P1: Import debug helpers at function scope
+ from .debug_helper import is_phi_debug_enabled, is_phi_strict_enabled, is_phi_trace_enabled
+
# Always place PHI at block start to keep LLVM invariant "PHI nodes at top"
# Check if PHI already exists in vmap for this block
@@ -90,9 +93,18 @@ def ensure_phi(builder, block_id: int, dst_vid: int, bb: ir.Block, dst_type=None
if block_has_terminator:
# This should not happen - PHIs must be created before terminators
import sys
+ msg = (f"[phi_wiring/CRITICAL] PHI v{dst_vid} created after terminator in bb{block_id}! "
+ f"This violates LLVM IR PHI-first invariant.")
+
+ # Phase 277 P1: Fail-fast in strict mode
+ if is_phi_strict_enabled():
+ print(msg, file=sys.stderr)
+ print(f" → Next file to check: phi_placement.py::verify_phi_ordering", file=sys.stderr)
+ raise RuntimeError(msg)
+
+ # Default: warning only (preserve existing behavior)
if is_phi_debug_enabled():
- print(f"[phi_wiring] WARNING: Attempting to create PHI in bb{block_id} "
- f"after terminator already exists! This will cause LLVM IR errors.", file=sys.stderr)
+ print(f"[phi_wiring] WARNING: {msg}", file=sys.stderr)
# Try to create PHI anyway at the start, but log the issue
# Create PHI at block start
@@ -292,15 +304,33 @@ def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[in
trace({"phi": "wire_resolve_fail", "vs": vs, "pred": pred_match, "error": str(e)})
val = None
resolved_ok = val is not None
- # Normalize to a well-typed LLVM value (i64)
+ # Phase 277 P1: Strict mode forbids silent fallback to 0
if val is None:
+ from .debug_helper import is_phi_strict_enabled
+ if is_phi_strict_enabled():
+ import sys
+ msg = (f"[phi_wiring/CRITICAL] PHI v{dst_vid} incoming from bb{pred_match} "
+ f"could not be resolved (vs={vs}). "
+ f"Silent fallback to 0 is forbidden in strict mode.")
+ print(msg, file=sys.stderr)
+ print(f" → Next file: llvm_builder.py::_value_at_end_i64 (value resolution)", file=sys.stderr)
+ raise RuntimeError(msg)
+ # Default: silent fallback (existing behavior)
val = _const_i64(builder, 0)
else:
try:
# Some paths can accidentally pass plain integers; coerce to i64 const
if not hasattr(val, 'type'):
val = _const_i64(builder, int(val))
- except Exception:
+ except Exception as e:
+ from .debug_helper import is_phi_strict_enabled
+ if is_phi_strict_enabled():
+ import sys
+ msg = (f"[phi_wiring/CRITICAL] PHI v{dst_vid} incoming type coercion failed "
+ f"(vs={vs}, pred={pred_match}): {e}")
+ print(msg, file=sys.stderr)
+ raise RuntimeError(msg)
+ # Default: silent fallback (existing behavior)
val = _const_i64(builder, 0)
# SSOT for ambiguous PHI incoming (same pred_match multiple times):
# - prefer a non-zero / successfully-resolved value over a synthesized zero,
diff --git a/src/mir/builder/calls/build.rs b/src/mir/builder/calls/build.rs
index 4fa12d72..2b56bdb8 100644
--- a/src/mir/builder/calls/build.rs
+++ b/src/mir/builder/calls/build.rs
@@ -99,36 +99,16 @@ impl MirBuilder {
method: String,
arguments: Vec,
) -> Result {
- if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") {
- let kind = match &object {
- ASTNode::Variable { .. } => "Variable",
- ASTNode::FieldAccess { .. } => "FieldAccess",
- ASTNode::This { .. } => "This",
- ASTNode::Me { .. } => "Me",
- _ => "Other",
- };
- eprintln!(
- "[builder] method-call object kind={} method={}",
- kind, method
- );
- }
+ self.trace_method_call_if_enabled(&object, &method);
// 0. Dev-only: __mir__.log / __mir__.mark → MirInstruction::DebugLog へ直接 lowering
- if let ASTNode::Variable { name: obj_name, .. } = &object {
- if obj_name == "__mir__" {
- if let Some(result) = self.try_build_mir_debug_call(&method, &arguments)? {
- return Ok(result);
- }
- }
+ if let Some(result) = self.try_build_mir_debug_method_call(&object, &method, &arguments)? {
+ return Ok(result);
}
// 1. Static box method call: BoxName.method(args)
- if let ASTNode::Variable { name: obj_name, .. } = &object {
- if let Some(result) =
- self.try_build_static_method_call(obj_name, &method, &arguments)?
- {
- return Ok(result);
- }
+ if let Some(result) = self.try_build_static_receiver_method_call(&object, &method, &arguments)? {
+ return Ok(result);
}
// 2. Handle env.* methods
@@ -136,25 +116,9 @@ impl MirBuilder {
return res;
}
- // 3. Handle this/me.method() calls
- // Phase 269 P1.2: ReceiverNormalizeBox - MethodCall 共通入口 SSOT
- // Static box context check for This/Me receiver
- if matches!(object, ASTNode::This { .. } | ASTNode::Me { .. }) {
- // Priority 1: Static box → compile-time static call normalization
- if let Some(box_name) = self.comp_ctx.current_static_box.clone() {
- // Debug trace
- if std::env::var("NYASH_TRACE_NORMALIZE").is_ok() {
- eprintln!("[trace:normalize] this.{}() → {}.{}() (static call)", method, box_name, method);
- }
- // Static call normalization (no runtime receiver object needed)
- // this.method(args) → current_static_box.method/arity(args)
- return self.handle_static_method_call(&box_name, &method, &arguments);
- }
-
- // Instance method fallback (requires variable_map["me"])
- if let Some(result) = self.handle_me_method_call(&method, &arguments)? {
- return Ok(result);
- }
+ // 3. Phase 269 P1.2: ReceiverNormalizeBox - MethodCall 共通入口 SSOT
+ if let Some(result) = self.try_normalize_this_me_method_call(&object, &method, &arguments)? {
+ return Ok(result);
}
// 4. Build object value
@@ -172,6 +136,75 @@ impl MirBuilder {
self.handle_standard_method_call(object_value, method, &arguments)
}
+ fn trace_method_call_if_enabled(&self, object: &ASTNode, method: &str) {
+ if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() != Some("1") {
+ return;
+ }
+ let kind = match object {
+ ASTNode::Variable { .. } => "Variable",
+ ASTNode::FieldAccess { .. } => "FieldAccess",
+ ASTNode::This { .. } => "This",
+ ASTNode::Me { .. } => "Me",
+ _ => "Other",
+ };
+ eprintln!("[builder] method-call object kind={} method={}", kind, method);
+ }
+
+ fn try_build_mir_debug_method_call(
+ &mut self,
+ object: &ASTNode,
+ method: &str,
+ arguments: &[ASTNode],
+ ) -> Result