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, String> { + let ASTNode::Variable { name: obj_name, .. } = object else { + return Ok(None); + }; + if obj_name != "__mir__" { + return Ok(None); + } + self.try_build_mir_debug_call(method, arguments) + } + + fn try_build_static_receiver_method_call( + &mut self, + object: &ASTNode, + method: &str, + arguments: &[ASTNode], + ) -> Result, String> { + let ASTNode::Variable { name: obj_name, .. } = object else { + return Ok(None); + }; + self.try_build_static_method_call(obj_name, method, arguments) + } + + fn try_normalize_this_me_method_call( + &mut self, + object: &ASTNode, + method: &str, + arguments: &[ASTNode], + ) -> Result, String> { + if !matches!(object, ASTNode::This { .. } | ASTNode::Me { .. }) { + return Ok(None); + } + + // Priority 1: static box → compile-time static call normalization + if let Some(box_name) = self.comp_ctx.current_static_box.clone() { + if std::env::var("NYASH_TRACE_NORMALIZE").is_ok() { + eprintln!( + "[trace:normalize] this.{}() → {}.{}() (static call)", + method, box_name, method + ); + } + // this.method(args) → current_static_box.method/arity(args) + return Ok(Some(self.handle_static_method_call( + &box_name, method, arguments, + )?)); + } + + // Instance method fallback (requires variable_map["me"]) + self.handle_me_method_call(method, arguments) + } + // Build from expression: from Parent.method(arguments) pub fn build_from_expression( &mut self, diff --git a/src/mir/builder/calls/lowering.rs b/src/mir/builder/calls/lowering.rs index 19ba87a9..941a8bba 100644 --- a/src/mir/builder/calls/lowering.rs +++ b/src/mir/builder/calls/lowering.rs @@ -114,7 +114,7 @@ impl MirBuilder { } /// 🎯 箱理論: Step 3 - パラメータ設定 - /// Phase 269 P1.2: MeBindingInitializerBox - Initializes "me" for static methods + /// Phase 269 P1.2: static call 正規化により、static box では "me" の擬似初期化を行わない(receiver は compile-time で確定) #[allow(deprecated)] fn setup_function_params(&mut self, params: &[String]) { // Phase 136 Step 3/7: Clear scope_ctx (SSOT) diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs index e76c1d81..2a33ce5a 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs @@ -528,9 +528,13 @@ pub(crate) fn lower( } impl MirBuilder { - /// Phase 254 P1: Pattern 6 (ScanWithInit) implementation + /// Phase 272 P0.1: Pattern 6 (ScanWithInit) Frag-based implementation /// - /// Lowers index_of-style loops to JoinIR using scan_with_init_minimal lowerer. + /// Lowers index_of-style loops using EdgeCFG Frag construction (replacing JoinIRConversionPipeline). + /// + /// # P0 Scope + /// - Forward scan only (step = 1) + /// - Reverse scan / dynamic needle → fallback to Ok(None) /// /// # Arguments /// @@ -547,15 +551,15 @@ impl MirBuilder { debug: bool, fn_body: Option<&[ASTNode]>, ) -> Result, String> { - use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; - use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; + use crate::mir::{BinaryOp, CompareOp, ConstValue, EffectMask, Effect, MirInstruction, MirType}; + use crate::mir::ssot::cf_common::insert_phi_at_head_spanned; let trace = trace::trace(); if debug { trace.debug( "pattern6/lower", - &format!("Phase 254 P1: ScanWithInit lowering for {}", func_name), + &format!("Phase 272 P0.1: ScanWithInit Frag lowering for {}", func_name), ); } @@ -563,6 +567,27 @@ impl MirBuilder { let parts = extract_scan_with_init_parts(condition, body, fn_body)? .ok_or_else(|| format!("[pattern6] Not a scan-with-init pattern in {}", func_name))?; + // P0 Scope: Forward scan (step=1) only + if parts.step_lit != 1 { + // Reverse scan / dynamic needle: Pattern6 not applicable + // Return Ok(None) to let other patterns/generic lowering handle this + if debug { + trace.debug( + "pattern6/lower", + &format!("P0 fallback: step_lit={} (not forward scan)", parts.step_lit), + ); + } + return Ok(None); + } + + if parts.dynamic_needle { + // Dynamic needle not supported in P0 + if debug { + trace.debug("pattern6/lower", "P0 fallback: dynamic_needle=true"); + } + return Ok(None); + } + if debug { trace.debug( "pattern6/lower", @@ -581,198 +606,189 @@ impl MirBuilder { .copied() .ok_or_else(|| format!("[pattern6] Variable {} not found", parts.haystack))?; - let ch_host = self + let needle_host = self .variable_ctx .variable_map .get(&parts.needle) .copied() .ok_or_else(|| format!("[pattern6] Variable {} not found", parts.needle))?; - let i_host = self + // Step 3: Get initial loop variable value from variable_map (Pattern8 方式) + let i_init_val = self .variable_ctx .variable_map .get(&parts.loop_var) .copied() - .ok_or_else(|| format!("[pattern6] Variable {} not found", parts.loop_var))?; + .ok_or_else(|| format!("[pattern6] Loop variable {} not found", parts.loop_var))?; if debug { trace.debug( "pattern6/lower", &format!( - "Host ValueIds: s={:?}, ch={:?}, i={:?}", - s_host, ch_host, i_host + "Host ValueIds: s={:?}, needle={:?}, i_init={:?}", + s_host, needle_host, i_init_val ), ); } - // Step 3: Create JoinModule based on scan direction - let mut join_value_space = JoinValueSpace::new(); - let join_module = match parts.scan_direction { - ScanDirection::Forward => { - use crate::mir::join_ir::lowering::scan_with_init_minimal::lower_scan_with_init_minimal; - // Phase 258 P0: Pass dynamic_needle to forward lowerer - lower_scan_with_init_minimal(&mut join_value_space, parts.dynamic_needle) - } - ScanDirection::Reverse => { - use crate::mir::join_ir::lowering::scan_with_init_reverse::lower_scan_with_init_reverse; - // P0: Reverse lowerer does not support dynamic needle yet - lower_scan_with_init_reverse(&mut join_value_space) - } - }; + // Step 4a: Capture preheader block (entry to loop) for PHI input + let preheader_bb = self.current_block + .ok_or_else(|| "[pattern6] No current block for loop entry".to_string())?; - // Phase 255 P2: Build CarrierInfo for loop variable only - // Step 1: Create CarrierInfo with loop variable (i) only - // s and ch are now loop_invariants (not carriers) - use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierRole}; + // Step 4b: Allocate PHI destination for loop variable BEFORE generating blocks + let i_current = self.next_value_id(); + self.type_ctx.value_types.insert(i_current, MirType::Integer); - let carrier_info = CarrierInfo::with_carriers( - parts.loop_var.clone(), // loop_var_name: "i" - i_host, // loop_var_id (LoopState - header PHI + exit PHI) - vec![], // Empty carriers - only loop_var - ); + // Step 4c: Allocate BasicBlockIds for 5 blocks + let header_bb = self.next_block_id(); + let body_bb = self.next_block_id(); + let step_bb = self.next_block_id(); + let after_bb = self.next_block_id(); + let ret_found_bb = self.next_block_id(); - // Phase 255 P2: Create loop_invariants for ch and s - // CRITICAL: Order MUST match JoinModule loop_step params: [i, needle, haystack] - // carrier_order is built as: [loop_var] + loop_invariants - // So loop_invariants order determines param-to-PHI mapping for invariants! - // Phase 258 P0: In both fixed and dynamic modes, order is [needle, haystack] - let loop_invariants = vec![ - (parts.needle.clone(), ch_host), // needle (ch or substr) → JoinIR param 1 - (parts.haystack.clone(), s_host), // haystack (s) → JoinIR param 2 - ]; - - if debug { - trace.debug( - "pattern6/lower", - &format!( - "Phase 255 P2: CarrierInfo with loop_var only (i: LoopState), {} loop_invariants (s, ch)", - loop_invariants.len() - ), - ); + // Add Jump from current block to header_bb (to terminate the previous block) + if let Some(_current) = self.current_block { + self.emit_instruction(MirInstruction::Jump { + target: header_bb, + edge_args: None, + })?; } - // Phase 256.8.5: Use JoinModule.entry.params as SSOT (no hardcoded ValueIds) - use super::common::get_entry_function; - let main_func = get_entry_function(&join_module, "pattern6")?; + // Build header_bb: len = s.length(), cond_loop = (i < len) + self.start_new_block(header_bb)?; - // SSOT: Use actual params allocated by JoinIR lowerer - let join_inputs = main_func.params.clone(); + // Note: PHI node for i_current will be inserted AFTER all blocks are generated + // (see Step 7 below, after step_bb generates i_next_val) - // Step 2: Build host_inputs in same order: [i, ch, s] (alphabetical) - // CRITICAL: Order must match JoinModule main() params: [i, ch, s] (alphabetical) - // Phase 255 P0: CarrierInfo sorts carriers alphabetically, so params must match - let host_inputs = vec![i_host, ch_host, s_host]; // [i, ch, s] alphabetical + let len_val = self.next_value_id(); + self.emit_instruction(MirInstruction::BoxCall { + dst: Some(len_val), + box_val: s_host, + method: "length".to_string(), + method_id: None, + args: vec![], + effects: EffectMask::PURE.add(Effect::Io), + })?; + self.type_ctx.value_types.insert(len_val, MirType::Integer); - // Verify count consistency (fail-fast) - if join_inputs.len() != host_inputs.len() { - return Err(format!( - "[pattern6] Params count mismatch: join_inputs={}, host_inputs={}", - join_inputs.len(), host_inputs.len() - )); - } + let cond_loop = self.next_value_id(); + self.emit_instruction(MirInstruction::Compare { + dst: cond_loop, + lhs: i_current, // Use PHI result, not initial value + op: CompareOp::Lt, + rhs: len_val, + })?; + self.type_ctx.value_types.insert(cond_loop, MirType::Bool); - // Step 3: Build exit_bindings manually - // Phase 255 P2: Only LoopState variables (i) need exit bindings - // Loop invariants (s, ch) do NOT need exit bindings - use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; + // Build body_bb: ch = s.substring(i, i+1), cond_match = (ch == needle) + self.start_new_block(body_bb)?; - let k_exit_func = join_module.require_function( - crate::mir::join_ir::lowering::canonical_names::K_EXIT, - "Pattern 6", - ); - let join_exit_value_i = k_exit_func - .params - .first() - .copied() - .expect("k_exit must have parameter for exit value"); + let one_val = self.next_value_id(); + self.emit_instruction(MirInstruction::Const { + dst: one_val, + value: ConstValue::Integer(1), + })?; + self.type_ctx.value_types.insert(one_val, MirType::Integer); - let i_exit_binding = LoopExitBinding { - carrier_name: parts.loop_var.clone(), - join_exit_value: join_exit_value_i, - host_slot: i_host, - role: CarrierRole::LoopState, - }; + let i_plus_one = self.next_value_id(); + self.emit_instruction(MirInstruction::BinOp { + dst: i_plus_one, + lhs: i_current, // Use PHI result, not initial value + op: BinaryOp::Add, + rhs: one_val, + })?; + self.type_ctx.value_types.insert(i_plus_one, MirType::Integer); - // Phase 255 P2: Only i (LoopState) in exit_bindings - // s and ch are loop_invariants, not carriers - let exit_bindings = vec![i_exit_binding]; + let ch_val = self.next_value_id(); + self.emit_instruction(MirInstruction::BoxCall { + dst: Some(ch_val), + box_val: s_host, + method: "substring".to_string(), + method_id: None, + args: vec![i_current, i_plus_one], // Use PHI result, not initial value + effects: EffectMask::PURE.add(Effect::Io), + })?; + self.type_ctx.value_types.insert(ch_val, MirType::String); - if debug { - trace.debug( - "pattern6/lower", - &format!("Phase 255 P2: Generated {} exit_bindings (i only)", exit_bindings.len()), - ); - } + let cond_match = self.next_value_id(); + self.emit_instruction(MirInstruction::Compare { + dst: cond_match, + lhs: ch_val, + op: CompareOp::Eq, + rhs: needle_host, + })?; + self.type_ctx.value_types.insert(cond_match, MirType::Bool); - // Step 4: Build boundary with carrier_info and loop_invariants - let boundary = JoinInlineBoundaryBuilder::new() - .with_inputs(join_inputs, host_inputs) - .with_loop_invariants(loop_invariants) // Phase 255 P2: Add loop invariants - .with_exit_bindings(exit_bindings) - .with_loop_var_name(Some(parts.loop_var.clone())) - .with_carrier_info(carrier_info.clone()) // ✅ Key: carrier_info for multi-PHI - .build(); + // Build step_bb: i_next = i + 1 + self.start_new_block(step_bb)?; - // Step 5: Build PostLoopEarlyReturnPlan for exit PHI usage (Phase 255 P1) - // This forces the exit PHI value to be used, preventing DCE from eliminating it - use crate::mir::builder::control_flow::joinir::patterns::policies::post_loop_early_return_plan::PostLoopEarlyReturnPlan; - use crate::ast::Span; + let i_next_val = self.next_value_id(); + self.emit_instruction(MirInstruction::BinOp { + dst: i_next_val, + lhs: i_current, // Use PHI result, not initial value + op: BinaryOp::Add, + rhs: one_val, // Reuse one_val from body_bb + })?; + self.type_ctx.value_types.insert(i_next_val, MirType::Integer); + // Note: Do NOT update variable_map here - PHI will handle SSA renaming - let post_loop_plan = PostLoopEarlyReturnPlan { - cond: ASTNode::BinaryOp { - operator: BinaryOperator::NotEqual, - left: Box::new(var(&parts.loop_var)), // i - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(parts.not_found_return_lit), // -1 - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ret_expr: var(&parts.loop_var), // return i - }; + // Ensure ret_found_bb and after_bb exist (they don't have instructions, but must exist for emit_frag) + self.ensure_block_exists(ret_found_bb)?; + self.ensure_block_exists(after_bb)?; - if debug { - trace.debug( - "pattern6/lower", - "Phase 255 P1: Built PostLoopEarlyReturnPlan (cond: i != -1, ret: i)", - ); - } + // Step 7: Insert PHI at head of header_bb - Phase 272 P0.2 Refactoring: use emission/phi.rs + use crate::mir::builder::emission::phi::insert_loop_phi; - // Step 6: Execute JoinIRConversionPipeline - use super::conversion_pipeline::JoinIRConversionPipeline; - let _ = JoinIRConversionPipeline::execute( + insert_loop_phi( self, - join_module, - Some(&boundary), - "pattern6", - debug, + header_bb, + i_current, + vec![ + (preheader_bb, i_init_val), // Entry edge: initial value + (step_bb, i_next_val), // Latch edge: updated value + ], + "pattern6/header_phi", )?; - // Step 6.5: Emit post-loop early return guard (Phase 255 P1) - // This prevents exit PHI from being DCE'd by using the value - use super::pattern2_steps::post_loop_early_return_step_box::PostLoopEarlyReturnStepBox; - PostLoopEarlyReturnStepBox::maybe_emit(self, Some(&post_loop_plan))?; - if debug { - trace.debug( - "pattern6/lower", - "Phase 255 P1: Emitted post-loop early return guard (if i != -1 { return i })", - ); + trace.debug("pattern6/lower", "PHI inserted at header_bb"); } - // Note: The post-loop guard ensures exit PHI is used: - // - k_exit with i (found case) - // - k_exit with -1 (not found case) - // The original "return -1" statement after the loop is unreachable - // and will be optimized away by DCE. + // Step 8: Call emission entrypoint + use crate::mir::builder::emission::loop_scan_with_init::emit_scan_with_init_edgecfg; - // Step 7: Return Void (loops don't produce values) - let void_val = crate::mir::builder::emission::constant::emit_void(self); + emit_scan_with_init_edgecfg( + self, + header_bb, + body_bb, + step_bb, + after_bb, + ret_found_bb, + cond_loop, + cond_match, + i_current, // Return value for found case + )?; + + if debug { + trace.debug("pattern6/lower", "Frag emitted successfully"); + } + + // Step 9: Update variable_map to use final loop variable value + // (This is the value when loop exits normally via i >= len) + self.variable_ctx.variable_map.insert(parts.loop_var.clone(), i_current); + + // Step 10: Setup after_bb for subsequent AST lowering (return -1) + // CRITICAL: Use start_new_block() to create actual block, not just set current_block + self.start_new_block(after_bb)?; + + // Step 11: Return Void (pattern applied successfully) + use crate::mir::builder::emission::constant::emit_void; + let void_val = emit_void(self); if debug { trace.debug( "pattern6/lower", - &format!("Pattern 6 complete, returning Void {:?}", void_val), + &format!("Pattern 6 Frag complete, returning Void {:?}", void_val), ); } diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs b/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs index 49e71600..9040ed26 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern7_split_scan.rs @@ -243,70 +243,30 @@ pub(crate) fn can_lower( _builder: &MirBuilder, ctx: &super::router::LoopPatternContext, ) -> bool { - use crate::mir::loop_pattern_detection::LoopPatternKind; - - // Phase 256 P0: Accept Pattern2Break OR Pattern3IfPhi (same as Pattern 6) - match ctx.pattern_kind { - LoopPatternKind::Pattern2Break | LoopPatternKind::Pattern3IfPhi => { - // Continue to structure checks + // Phase 272 P0.2: SSOT between detect and extract (follow Pattern6 approach) + // Try extraction - if it succeeds, pattern matches + match extract_split_scan_parts(ctx.condition, ctx.body, &[]) { + Ok(_) => { + // Pattern is extractable + if ctx.debug { + trace::trace().debug( + "pattern7/can_lower", + "accept: pattern extractable (SSOT verified)", + ); + } + true } - _ => return false, - } - - // Check for if statement with MethodCall in condition - let has_if_with_methodcall = ctx.body.iter().any(|stmt| { - matches!(stmt, ASTNode::If { condition, .. } if contains_methodcall(condition)) - }); - - if !has_if_with_methodcall { - if ctx.debug { - trace::trace().debug( - "pattern7/can_lower", - "reject: no if with MethodCall in condition", - ); + Err(e) => { + // Extraction failed - pattern doesn't match + if ctx.debug { + trace::trace().debug( + "pattern7/can_lower", + &format!("reject: pattern not extractable - {}", e), + ); + } + false } - return false; } - - // Check for VARIABLE STEP pattern (i = start, where start = i + separator.length()) - // This distinguishes Pattern 7 from Pattern 6 (which has i = i + 1) - let has_variable_step = ctx.body.iter().any(|stmt| { - matches!(stmt, ASTNode::If { then_body, .. } if contains_variable_step(then_body)) - }); - - if !has_variable_step { - if ctx.debug { - trace::trace().debug( - "pattern7/can_lower", - "reject: no variable step pattern found", - ); - } - return false; - } - - // Check for push() operation in then branch - let has_push_operation = ctx.body.iter().any(|stmt| { - matches!(stmt, ASTNode::If { then_body, .. } if contains_push(then_body)) - }); - - if !has_push_operation { - if ctx.debug { - trace::trace().debug( - "pattern7/can_lower", - "reject: no push() operation in then branch", - ); - } - return false; - } - - if ctx.debug { - trace::trace().debug( - "pattern7/can_lower", - "MATCHED: SplitScan pattern detected", - ); - } - - true } /// Check if AST node contains MethodCall @@ -356,16 +316,17 @@ pub(crate) fn lower( } impl MirBuilder { - /// Phase 256 P0: Pattern 7 (SplitScan) implementation + /// Phase 272 P0.2: Pattern 7 (SplitScan) Frag implementation /// - /// Lowers split/tokenization loops to JoinIR using split_scan_minimal lowerer. + /// Direct EdgeCFG Frag construction (no JoinIRConversionPipeline). /// /// # Architecture /// /// - 2 carriers: i (loop index), start (segment start) - /// - 3 invariants: s (haystack), sep (separator), result (accumulator) - /// - Conditional step via Select (P0 pragmatic approach) - /// - Post-loop segment push stays in host AST (k_exit is pure return) + /// - 4 PHI nodes: header (i_current, start_current) + step (i_next, start_next) + /// - 6 blocks: header, body, then, else, step, after + /// - Side effect: result.push(segment) in then_bb + /// - Frag: 2 branches (header, body) + 3 wires (then→step, else→step, step→header) pub(crate) fn cf_loop_pattern7_split_scan_impl( &mut self, condition: &ASTNode, @@ -374,16 +335,12 @@ impl MirBuilder { debug: bool, fn_body: Option<&[ASTNode]>, ) -> Result, String> { - use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; - use crate::mir::join_ir::lowering::split_scan_minimal::lower_split_scan_minimal; - use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; - let trace = trace::trace(); if debug { trace.debug( "pattern7/lower", - &format!("Phase 256 P0: SplitScan lowering for {}", func_name), + &format!("Phase 272 P0.2: SplitScan Frag lowering for {}", func_name), ); } @@ -394,7 +351,8 @@ impl MirBuilder { if debug { trace.debug("pattern7/lower", &format!("extraction failed: {}", e)); } - return Err(format!("Pattern 7 extraction failed: {}", e)); + // Pattern not applicable - return Ok(None) to allow fallback + return Ok(None); } }; @@ -408,213 +366,289 @@ impl MirBuilder { ); } - // Step 2: Get host ValueIds for all variables - let s_host = self - .variable_ctx - .variable_map - .get(&parts.s_var) - .copied() - .ok_or_else(|| format!("[pattern7] Variable {} not found", parts.s_var))?; - - let sep_host = self - .variable_ctx - .variable_map - .get(&parts.sep_var) - .copied() - .ok_or_else(|| format!("[pattern7] Variable {} not found", parts.sep_var))?; - - let result_host = self - .variable_ctx - .variable_map - .get(&parts.result_var) - .copied() - .ok_or_else(|| format!("[pattern7] Variable {} not found", parts.result_var))?; - - let i_host = self - .variable_ctx - .variable_map - .get(&parts.i_var) - .copied() - .ok_or_else(|| format!("[pattern7] Variable {} not found", parts.i_var))?; - - let start_host = self - .variable_ctx - .variable_map - .get(&parts.start_var) - .copied() - .ok_or_else(|| format!("[pattern7] Variable {} not found", parts.start_var))?; + // Step 3.1: Get host ValueIds from variable_map (Phase 272 P0.2 Refactoring: use require()) + let i_init_val = self.variable_ctx.require(&parts.i_var, "pattern7")?; + let start_init_val = self.variable_ctx.require(&parts.start_var, "pattern7")?; + let s_host = self.variable_ctx.require(&parts.s_var, "pattern7")?; + let sep_host = self.variable_ctx.require(&parts.sep_var, "pattern7")?; + let result_host = self.variable_ctx.require(&parts.result_var, "pattern7")?; if debug { trace.debug( "pattern7/lower", &format!( - "Host ValueIds: i={:?}, result={:?}, s={:?}, sep={:?}, start={:?}", - i_host, result_host, s_host, sep_host, start_host + "Host ValueIds: i={:?}, start={:?}, s={:?}, sep={:?}, result={:?}", + i_init_val, start_init_val, s_host, sep_host, result_host ), ); } - // Step 3: Create JoinModule - let mut join_value_space = JoinValueSpace::new(); - let join_module = lower_split_scan_minimal(&mut join_value_space); + // Step 3.2: Block allocation (6 blocks) + let preheader_bb = self.current_block.ok_or("[pattern7] No current block")?; - // Phase 255 P2: Build CarrierInfo for 2 carriers (i, start) - use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierVar, CarrierRole}; + // Allocate PHI destinations BEFORE blocks (Pattern8 style) + use crate::mir::MirType; + let i_current = self.next_value_id(); + self.type_ctx.value_types.insert(i_current, MirType::Integer); + let start_current = self.next_value_id(); + self.type_ctx + .value_types + .insert(start_current, MirType::Integer); + let i_next = self.next_value_id(); + self.type_ctx.value_types.insert(i_next, MirType::Integer); + let start_next = self.next_value_id(); + self.type_ctx + .value_types + .insert(start_next, MirType::Integer); - let carrier_info = CarrierInfo::with_carriers( - parts.i_var.clone(), // loop_var_name: "i" - i_host, // loop_var_id (LoopState) - vec![CarrierVar::with_role( - parts.start_var.clone(), // second carrier: "start" - start_host, // start_id (LoopState) - CarrierRole::LoopState, - )], - ); + let header_bb = self.next_block_id(); + let body_bb = self.next_block_id(); + let then_bb = self.next_block_id(); + let else_bb = self.next_block_id(); + let step_bb = self.next_block_id(); + let after_bb = self.next_block_id(); - // Phase 255 P2: Create loop_invariants for result, s, sep - // CRITICAL: Order MUST match JoinModule loop_step params: [i, start, result, s, sep] - // carrier_order is built as: [loop_var (i), carriers (start)] + loop_invariants - // So loop_invariants order must be [result, s, sep] to match param indices 2, 3, 4! - // Phase 256 P1.5: result needs to be in BOTH loop_invariants (for initial value) AND exit_bindings (for return) - let loop_invariants = vec![ - (parts.result_var.clone(), result_host), // result: JoinIR param 2 - (parts.s_var.clone(), s_host), // s: JoinIR param 3 (haystack, read-only) - (parts.sep_var.clone(), sep_host), // sep: JoinIR param 4 (separator, read-only) - ]; + // Terminate preheader + use crate::mir::MirInstruction; + self.emit_instruction(MirInstruction::Jump { + target: header_bb, + edge_args: None, + })?; - if debug { - trace.debug( - "pattern7/lower", - &format!( - "Phase 255 P2: CarrierInfo with 2 carriers (i, start), {} loop_invariants (s, sep, result)", - loop_invariants.len() - ), - ); - } + // Step 3.3: header_bb - loop condition + self.start_new_block(header_bb)?; - // Phase 256.8.5: Use JoinModule.entry.params as SSOT (no hardcoded ValueIds) - use super::common::get_entry_function; - let main_func = get_entry_function(&join_module, "pattern7")?; + // sep_len = sep.length() + let sep_len = self.next_value_id(); + self.emit_instruction(MirInstruction::BoxCall { + dst: Some(sep_len), + box_val: sep_host, + method: "length".to_string(), + method_id: None, + args: vec![], + effects: crate::mir::EffectMask::PURE.add(crate::mir::Effect::Io), + })?; + self.type_ctx.value_types.insert(sep_len, MirType::Integer); - // SSOT: Use actual params allocated by JoinIR lowerer - let join_inputs = main_func.params.clone(); + // s_len = s.length() + let s_len = self.next_value_id(); + self.emit_instruction(MirInstruction::BoxCall { + dst: Some(s_len), + box_val: s_host, + method: "length".to_string(), + method_id: None, + args: vec![], + effects: crate::mir::EffectMask::PURE.add(crate::mir::Effect::Io), + })?; + self.type_ctx.value_types.insert(s_len, MirType::Integer); - // Step 4: Build host_inputs in same order: [i, start, result, s, sep] - // Phase 256 P1.5: Order must match main() params (line 166 in split_scan_minimal.rs): [i, start, result, s, sep] - // CRITICAL: NOT allocation order [i(100), result(101), s(102), sep(103), start(104)] - // But Carriers-First order: [i, start, result, s, sep] = [100, 104, 101, 102, 103] - let host_inputs = vec![ - i_host, // i (loop var) - start_host, // start (carrier) - result_host, // result (carried) - s_host, // s (invariant) - sep_host, // sep (invariant) - ]; + // limit = s_len - sep_len + use crate::mir::BinaryOp; + let limit = self.next_value_id(); + self.emit_instruction(MirInstruction::BinOp { + dst: limit, + lhs: s_len, + op: BinaryOp::Sub, + rhs: sep_len, + })?; + self.type_ctx.value_types.insert(limit, MirType::Integer); - // Verify count consistency (fail-fast) - if join_inputs.len() != host_inputs.len() { - return Err(format!( - "[pattern7] Params count mismatch: join_inputs={}, host_inputs={}", - join_inputs.len(), host_inputs.len() - )); - } + // cond_loop = (i <= limit) + use crate::mir::CompareOp; + let cond_loop = self.next_value_id(); + self.emit_instruction(MirInstruction::Compare { + dst: cond_loop, + lhs: i_current, + op: CompareOp::Le, // ← CompareOp::Le (user correction) + rhs: limit, + })?; + self.type_ctx.value_types.insert(cond_loop, MirType::Bool); - // Step 5: Build exit_bindings for 2 carriers (Phase 256 P1: Required!) - // Phase 256 P1: k_exit params are [i, start, result, s] (Carriers-First!) - // We need exit_bindings for carriers i and start - use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; + // Step 3.4: body_bb - match check + self.start_new_block(body_bb)?; - let k_exit_func = join_module.require_function("k_exit", "Pattern 7"); - // k_exit params (Carriers-First order): - // params[0] = i_exit_param - // params[1] = start_exit_param - // params[2] = result_exit_param - // params[3] = s_exit_param + // i_plus_sep = i + sep_len + let i_plus_sep = self.next_value_id(); + self.emit_instruction(MirInstruction::BinOp { + dst: i_plus_sep, + lhs: i_current, + op: BinaryOp::Add, + rhs: sep_len, + })?; + self.type_ctx + .value_types + .insert(i_plus_sep, MirType::Integer); - // Get exit values for both carriers - let join_exit_value_i = k_exit_func - .params - .get(0) - .copied() - .expect("k_exit must have parameter 0 for loop variable i"); + // chunk = s.substring(i, i_plus_sep) + let chunk = self.next_value_id(); + self.emit_instruction(MirInstruction::BoxCall { + dst: Some(chunk), + box_val: s_host, + method: "substring".to_string(), + method_id: None, + args: vec![i_current, i_plus_sep], + effects: crate::mir::EffectMask::PURE.add(crate::mir::Effect::Io), + })?; + self.type_ctx.value_types.insert(chunk, MirType::String); - let join_exit_value_start = k_exit_func - .params - .get(1) - .copied() - .expect("k_exit must have parameter 1 for carrier start"); + // cond_match = (chunk == sep) + let cond_match = self.next_value_id(); + self.emit_instruction(MirInstruction::Compare { + dst: cond_match, + lhs: chunk, + op: CompareOp::Eq, + rhs: sep_host, + })?; + self.type_ctx.value_types.insert(cond_match, MirType::Bool); - let i_exit_binding = LoopExitBinding { - carrier_name: parts.i_var.clone(), - join_exit_value: join_exit_value_i, - host_slot: i_host, - role: CarrierRole::LoopState, - }; + // Step 3.5: then_bb - push + updates + self.start_new_block(then_bb)?; - let start_exit_binding = LoopExitBinding { - carrier_name: parts.start_var.clone(), - join_exit_value: join_exit_value_start, - host_slot: start_host, - role: CarrierRole::LoopState, - }; + // segment = s.substring(start, i) + let segment = self.next_value_id(); + self.emit_instruction(MirInstruction::BoxCall { + dst: Some(segment), + box_val: s_host, + method: "substring".to_string(), + method_id: None, + args: vec![start_current, i_current], + effects: crate::mir::EffectMask::PURE.add(crate::mir::Effect::Io), + })?; + self.type_ctx.value_types.insert(segment, MirType::String); - // Phase 256 P1.5: result is modified by k_exit.push(), so it must be in exit_bindings too! - // result is params[2] in k_exit, and we need to map its return value to result_host - let join_exit_value_result = k_exit_func - .params - .get(2) - .copied() - .expect("k_exit must have parameter 2 for result (accumulator)"); + // result.push(segment) - Side effect! + self.emit_instruction(MirInstruction::BoxCall { + dst: None, // push returns Void + box_val: result_host, + method: "push".to_string(), + method_id: None, + args: vec![segment], + effects: crate::mir::EffectMask::MUT, + })?; - let result_exit_binding = LoopExitBinding { - carrier_name: parts.result_var.clone(), - join_exit_value: join_exit_value_result, - host_slot: result_host, - role: CarrierRole::LoopState, // Phase 256 P1.5: result acts like a carrier even though it's an accumulator - }; + // start_next_then = i + sep_len (recalculated in then_bb - dominance safety, user correction) + let start_next_then = self.next_value_id(); + self.emit_instruction(MirInstruction::BinOp { + dst: start_next_then, + lhs: i_current, + op: BinaryOp::Add, + rhs: sep_len, + })?; + self.type_ctx + .value_types + .insert(start_next_then, MirType::Integer); - let exit_bindings = vec![i_exit_binding, start_exit_binding, result_exit_binding]; + let i_next_then = start_next_then; // i = start (for PHI) - if debug { - trace.debug( - "pattern7/lower", - &format!("Phase 256 P1: Generated {} exit_bindings (i, start)", exit_bindings.len()), - ); - } + // Step 3.6: else_bb - increment i + self.start_new_block(else_bb)?; - // Step 6: Build boundary with carrier_info and loop_invariants - // Phase 256 P1.5: Set expr_result to result_exit_param so the loop expression returns the result - // Phase 256 P1.7: Register k_exit as continuation function for proper merging - let boundary = JoinInlineBoundaryBuilder::new() - .with_inputs(join_inputs, host_inputs) - .with_loop_invariants(loop_invariants) // Phase 255 P2: Add loop invariants - .with_exit_bindings(exit_bindings) - .with_expr_result(Some(join_exit_value_result)) // Phase 256 P1.5: Loop expression returns result - .with_loop_var_name(Some(parts.i_var.clone())) - .with_carrier_info(carrier_info.clone()) // ✅ Key: carrier_info for multi-PHI - .with_k_exit_continuation() // Phase 256 P1.7: Convenience API for k_exit registration - .build(); + // one = const 1 + use crate::mir::ConstValue; + let one = self.next_value_id(); + self.emit_instruction(MirInstruction::Const { + dst: one, + value: ConstValue::Integer(1), + })?; + self.type_ctx.value_types.insert(one, MirType::Integer); - if debug { - trace.debug("pattern7/lower", "Built JoinInlineBoundary with carrier_info"); - } + // i_next_else = i + 1 + let i_next_else = self.next_value_id(); + self.emit_instruction(MirInstruction::BinOp { + dst: i_next_else, + lhs: i_current, + op: BinaryOp::Add, + rhs: one, + })?; + self.type_ctx + .value_types + .insert(i_next_else, MirType::Integer); - // Step 7: Execute JoinIRConversionPipeline - use super::conversion_pipeline::JoinIRConversionPipeline; - let _ = JoinIRConversionPipeline::execute( + let start_next_else = start_current; // Unchanged (for PHI) + + // Step 3.7: step_bb - ensure exists + self.ensure_block_exists(step_bb)?; + self.ensure_block_exists(after_bb)?; + + // Step 3.8: PHI insertion (4 PHIs) - Phase 272 P0.2 Refactoring: use emission/phi.rs + use crate::mir::builder::emission::phi::insert_loop_phi; + + // Header PHI 1: i_current + insert_loop_phi( self, - join_module, - Some(&boundary), - "pattern7", - debug, + header_bb, + i_current, + vec![(preheader_bb, i_init_val), (step_bb, i_next)], + "pattern7/header_phi_i", )?; - if debug { - trace.debug("pattern7/lower", "JoinIRConversionPipeline executed successfully"); + // Header PHI 2: start_current + insert_loop_phi( + self, + header_bb, + start_current, + vec![(preheader_bb, start_init_val), (step_bb, start_next)], + "pattern7/header_phi_start", + )?; + + // Step PHI 1: i_next + insert_loop_phi( + self, + step_bb, + i_next, + vec![(then_bb, i_next_then), (else_bb, i_next_else)], + "pattern7/step_phi_i", + )?; + + // Step PHI 2: start_next + insert_loop_phi( + self, + step_bb, + start_next, + vec![(then_bb, start_next_then), (else_bb, start_next_else)], + "pattern7/step_phi_start", + )?; + + // Step 3.9: Emission call + use crate::mir::builder::emission::loop_split_scan::emit_split_scan_edgecfg; + + if let Some(ref mut func) = self.scope_ctx.current_function { + emit_split_scan_edgecfg( + func, + header_bb, + body_bb, + then_bb, + else_bb, + step_bb, + after_bb, + cond_loop, + cond_match, + )?; + } else { + return Err("[pattern7] No current function for emit_frag".to_string()); } - // Step 8: Return result ValueId - Ok(Some(result_host)) + if debug { + trace.debug("pattern7/lower", "Frag emitted successfully (4 PHIs, 6 blocks)"); + } + + // Step 3.10: Post-loop setup + // Update variable_map (post-loop needs start for final push) + self.variable_ctx + .variable_map + .insert(parts.i_var.clone(), i_current); + self.variable_ctx + .variable_map + .insert(parts.start_var.clone(), start_current); + + // Setup after_bb for subsequent AST lowering + self.start_new_block(after_bb)?; + + // Return Void (pattern applied successfully) + use crate::mir::builder::emission::constant::emit_void; + let void_val = emit_void(self); + + Ok(Some(void_val)) } } diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs b/src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs index a6ffb9e1..d7c71d86 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs @@ -487,26 +487,19 @@ pub(crate) fn lower( builder.ensure_block_exists(ret_false_bb)?; builder.ensure_block_exists(after_bb)?; - // Step 4: Insert PHI at head of header_bb with proper span synchronization - use crate::mir::ssot::cf_common::insert_phi_at_head_spanned; + // Step 4: Insert PHI at head of header_bb - Phase 272 P0.2 Refactoring: use emission/phi.rs + use crate::mir::builder::emission::phi::insert_loop_phi; - let phi_inputs = vec![ - (preheader_bb, i_init_val), // Entry edge: initial value - (step_bb, i_next_val), // Latch edge: updated value - ]; - - // Access current_function for PHI insertion - if let Some(ref mut func) = builder.scope_ctx.current_function { - insert_phi_at_head_spanned( - func, - header_bb, - i_current, // PHI destination - phi_inputs, - builder.metadata_ctx.current_span(), - ); - } else { - return Err("[pattern8] No current function for PHI insertion".to_string()); - } + insert_loop_phi( + builder, + header_bb, + i_current, + vec![ + (preheader_bb, i_init_val), // Entry edge: initial value + (step_bb, i_next_val), // Latch edge: updated value + ], + "pattern8/header_phi", + )?; // Step 5: Call emission entrypoint use crate::mir::builder::emission::loop_predicate_scan::emit_bool_predicate_scan_edgecfg; diff --git a/src/mir/builder/control_flow/joinir/patterns/router.rs b/src/mir/builder/control_flow/joinir/patterns/router.rs index 60b6b9a0..c3125d2b 100644 --- a/src/mir/builder/control_flow/joinir/patterns/router.rs +++ b/src/mir/builder/control_flow/joinir/patterns/router.rs @@ -138,6 +138,42 @@ impl<'a> LoopPatternContext<'a> { /// Phase 193: Feature extraction moved to ast_feature_extractor module /// See: src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs +/// Phase 272 P0.2 Refactoring: can_lower() strategy classification +/// +/// Clarifies the two main detection strategies used across patterns: +/// +/// ## ExtractionBased (SSOT Approach) +/// - Used by: Pattern6, Pattern7 +/// - Strategy: Try pattern extraction, if successful → match +/// - Pros: Single source of truth (extract function defines pattern) +/// - Cons: Extraction can be expensive (but amortized over lowering) +/// +/// ## StructureBased (Feature Classification) +/// - Used by: Pattern1, Pattern2, Pattern3, Pattern4, Pattern5, Pattern8, Pattern9 +/// - Strategy: Check pattern_kind (from LoopPatternContext), plus optional structural checks +/// - Pros: Fast classification, reuses centralized feature detection +/// - Cons: Two sources of truth (classify + structural checks) +/// +/// ## Rationale for Dual Strategy: +/// - Pattern6/7: Complex extraction logic (variable step, carrier tracking) +/// → ExtractionBased avoids duplication between detect and extract +/// - Other patterns: Simple structural features (break/continue/if-phi) +/// → StructureBased leverages centralized LoopFeatures classification +/// +/// This documentation prevents bugs like Phase 272 P0.2's Pattern7 issue +/// (pattern_kind check was too restrictive, extraction-based approach fixed it). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] // Documentation purpose - not enforced in code yet +pub(crate) enum CanLowerStrategy { + /// Extraction-based detection: Try extract(), success → match + /// Used by Pattern6, Pattern7 + ExtractionBased, + + /// Structure-based detection: Check pattern_kind from LoopPatternContext + /// Used by Pattern1, Pattern2, Pattern3, Pattern4, Pattern5, Pattern8, Pattern9 + StructureBased, +} + /// Entry in the loop pattern router table. /// Each pattern registers a detect function and a lower function. pub(crate) struct LoopPatternEntry { diff --git a/src/mir/builder/emission/loop_scan_with_init.rs b/src/mir/builder/emission/loop_scan_with_init.rs new file mode 100644 index 00000000..8c4f3eda --- /dev/null +++ b/src/mir/builder/emission/loop_scan_with_init.rs @@ -0,0 +1,143 @@ +//! Phase 272 P0.1: Pattern6 Scan with Init - Emission Entrypoint +//! +//! ## Purpose +//! Thin entrypoint for Pattern6 Frag construction and MIR terminator emission. +//! This module only handles terminator wiring via EdgeCFG Frag API. +//! Block allocation and value computation (len, substring, match check) are done by Pattern6. +//! +//! ## Pattern Structure +//! ```nyash +//! index_of(s, ch) { +//! local i = 0 +//! loop(i < s.length()) { +//! if s.substring(i, i + 1) == ch { +//! return i +//! } +//! i = i + 1 +//! } +//! return -1 +//! } +//! ``` +//! +//! ## P0 Scope +//! - Forward scan only (step = 1) +//! - Fixed needle (ch) +//! - No ret_not_found_bb (after_bb handles AST return -1) +//! +//! ## Critical SSOT +//! 1. Return in wires (not exits) - emit_frag() generates terminators from wires/branches only +//! 2. after_bb has no terminator - let subsequent AST lowering handle "return -1" +//! 3. Frag assembly is direct field access (no with_* API) +//! 4. BranchStub/EdgeStub field names match current implementation +//! 5. Return Void (loop as statement, not expression) + +use crate::mir::builder::MirBuilder; +use crate::mir::builder::control_flow::edgecfg::api::{ + BranchStub, EdgeStub, ExitKind, Frag, emit_frag, +}; +use crate::mir::basic_block::{BasicBlockId, EdgeArgs}; +use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout; +use crate::mir::ValueId; + +/// Emit Scan with Init EdgeCFG Fragment +/// +/// ## Arguments +/// - `b`: MirBuilder (for emit_frag access to current_function) +/// - `header_bb`: Loop condition check block (i < s.length()) +/// - `body_bb`: Substring + match check + early return branch +/// - `step_bb`: Increment i and jump back to header +/// - `after_bb`: Normal loop exit (no terminator - subsequent AST lowering handles "return -1") +/// - `ret_found_bb`: Early exit Return(i) block +/// - `cond_loop`: ValueId for (i < s.length()) +/// - `cond_match`: ValueId for (ch == needle) +/// - `i_found`: ValueId for loop variable i (return value for found case) +/// +/// ## Frag Structure +/// - **branches**: +/// 1. header: cond_loop true→body, false→after +/// 2. body: cond_match true→ret_found, false→step +/// - **wires**: +/// - step → header (Normal Jump) +/// - ret_found_bb → Return(i) - **IN WIRES, NOT EXITS** +/// - **exits**: empty (no upward propagation in P0) +/// +/// ## Returns +/// `Ok(())` - Frag emitted successfully +/// `Err` - emit_frag failed or current_function is None +pub(in crate::mir::builder) fn emit_scan_with_init_edgecfg( + b: &mut MirBuilder, + header_bb: BasicBlockId, + body_bb: BasicBlockId, + step_bb: BasicBlockId, + after_bb: BasicBlockId, + ret_found_bb: BasicBlockId, + cond_loop: ValueId, + cond_match: ValueId, + i_found: ValueId, +) -> Result<(), String> { + // EdgeArgs::empty() helper + let empty_args = EdgeArgs { + layout: JumpArgsLayout::CarriersOnly, + values: vec![], + }; + + // Return(i) arguments (contains found index) + let ret_found_args = EdgeArgs { + layout: JumpArgsLayout::CarriersOnly, + values: vec![i_found], + }; + + // branches (BranchStub) - current field names + let branches = vec![ + BranchStub { + from: header_bb, + cond: cond_loop, + then_target: body_bb, + then_args: empty_args.clone(), + else_target: after_bb, + else_args: empty_args.clone(), + }, + BranchStub { + from: body_bb, + cond: cond_match, + then_target: ret_found_bb, + then_args: empty_args.clone(), + else_target: step_bb, + else_args: empty_args.clone(), + }, + ]; + + // wires (EdgeStub) - current field names + let wires = vec![ + // step_bb → header_bb Jump (Normal) + EdgeStub { + from: step_bb, + kind: ExitKind::Normal, + target: Some(header_bb), + args: empty_args, + }, + // ret_found_bb Return(i) - THIS GOES IN WIRES! + EdgeStub { + from: ret_found_bb, + kind: ExitKind::Return, + target: None, + args: ret_found_args, + }, + // P0: No ret_not_found wire - after_bb handles AST return -1 + ]; + + // Frag assembly (direct field access - no with_* API exists) + let mut frag = Frag::new(header_bb); + frag.branches = branches; + frag.wires = wires; + // exits is empty (no upward propagation in P0) + + // emit_frag generates MIR terminators + if let Some(ref mut func) = b.scope_ctx.current_function { + emit_frag(func, &frag)?; + } else { + return Err("[emit_scan_with_init_edgecfg] current_function is None".to_string()); + } + + Ok(()) +} diff --git a/src/mir/builder/emission/loop_split_scan.rs b/src/mir/builder/emission/loop_split_scan.rs new file mode 100644 index 00000000..8c370748 --- /dev/null +++ b/src/mir/builder/emission/loop_split_scan.rs @@ -0,0 +1,133 @@ +//! Pattern7 Split Scan - EdgeCFG Frag Emission (Phase 272 P0.2) +//! +//! Purpose: Construct and emit Frag for Pattern7 (split scan with 2 carriers + side effect) +//! +//! CFG Structure (6 blocks): +//! ``` +//! preheader_bb +//! ↓ Jump +//! header_bb (PHI: i_current, start_current) +//! ↓ Branch(cond_loop: i <= s.len - sep.len) +//! ├─ true → body_bb +//! │ ↓ Branch(cond_match: substring == sep) +//! │ ├─ true → then_bb (result.push + updates) +//! │ │ ↓ Jump → step_bb +//! │ └─ false → else_bb (i++) +//! │ ↓ Jump → step_bb +//! └─ false → after_bb (post-loop) +//! +//! step_bb (PHI: i_next, start_next) +//! ↓ Jump → header_bb (latch) +//! ``` +//! +//! Frag Components: +//! - **2 branches**: header (loop condition), body (match condition) +//! - **3 wires**: then→step, else→step, step→header +//! - **0 exits**: after_bb handled by AST (no terminator) +//! +//! Phase 272 P0.2: Pattern7 Frag migration from JoinIRConversionPipeline + +use crate::mir::builder::control_flow::edgecfg::api::{ + BranchStub, EdgeStub, ExitKind, Frag, emit_frag, +}; +use crate::mir::basic_block::{BasicBlockId, EdgeArgs}; +use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout; +use crate::mir::{MirFunction, ValueId}; + +/// Emit EdgeCFG Frag for Pattern7 split scan loop +/// +/// # CFG Invariants +/// - header_bb: 2 PHI nodes (i_current, start_current) must be inserted by caller +/// - step_bb: 2 PHI nodes (i_next, start_next) must be inserted by caller +/// - after_bb: No terminator (AST will handle post-loop code) +/// +/// # Terminators Generated (SSOT via emit_frag) +/// - header_bb: Branch(cond_loop) → body_bb / after_bb +/// - body_bb: Branch(cond_match) → then_bb / else_bb +/// - then_bb: Jump → step_bb +/// - else_bb: Jump → step_bb +/// - step_bb: Jump → header_bb +/// +/// # Side Effects +/// - result.push(segment) must be emitted in then_bb by caller (before calling this function) +/// +/// # Arguments +/// - `func`: Current MirFunction (for emit_frag) +/// - `header_bb`: Loop header block (PHI merge point) +/// - `body_bb`: Match check block +/// - `then_bb`: Match found block (segment push + start update) +/// - `else_bb`: Match not found block (i increment) +/// - `step_bb`: Increment merge block (PHI for i_next, start_next) +/// - `after_bb`: Post-loop block (no terminator) +/// - `cond_loop`: Loop condition value (i <= limit) +/// - `cond_match`: Match condition value (chunk == sep) +pub(in crate::mir::builder) fn emit_split_scan_edgecfg( + func: &mut MirFunction, + header_bb: BasicBlockId, + body_bb: BasicBlockId, + then_bb: BasicBlockId, + else_bb: BasicBlockId, + step_bb: BasicBlockId, + after_bb: BasicBlockId, + cond_loop: ValueId, + cond_match: ValueId, +) -> Result<(), String> { + // EdgeArgs: CarriersOnly layout (no explicit values, PHI handles propagation) + let empty_args = EdgeArgs { + layout: JumpArgsLayout::CarriersOnly, + values: vec![], + }; + + // 2 branches: header (loop condition), body (match condition) + let branches = vec![ + BranchStub { + from: header_bb, + cond: cond_loop, + then_target: body_bb, + then_args: empty_args.clone(), + else_target: after_bb, + else_args: empty_args.clone(), + }, + BranchStub { + from: body_bb, + cond: cond_match, + then_target: then_bb, + then_args: empty_args.clone(), + else_target: else_bb, + else_args: empty_args.clone(), + }, + ]; + + // 3 wires (resolved internal edges): then→step, else→step, step→header + let wires = vec![ + EdgeStub { + from: then_bb, + kind: ExitKind::Normal, + target: Some(step_bb), + args: empty_args.clone(), + }, + EdgeStub { + from: else_bb, + kind: ExitKind::Normal, + target: Some(step_bb), + args: empty_args.clone(), + }, + EdgeStub { + from: step_bb, + kind: ExitKind::Normal, + target: Some(header_bb), + args: empty_args, + }, + ]; + + // Construct Frag (no exits - after_bb is handled by AST) + let mut frag = Frag::new(header_bb); + frag.branches = branches; + frag.wires = wires; + // frag.exits remains empty (after_bb has no terminator) + + // Emit terminators (SSOT) + emit_frag(func, &frag)?; + + Ok(()) +} diff --git a/src/mir/builder/emission/mod.rs b/src/mir/builder/emission/mod.rs index 9ac427d2..ac32426c 100644 --- a/src/mir/builder/emission/mod.rs +++ b/src/mir/builder/emission/mod.rs @@ -2,9 +2,15 @@ //! - constant.rs: Const発行を一箇所に集約 //! - compare.rs: Compare命令の薄い発行 //! - branch.rs: Branch/Jump 発行の薄い関数 +//! - phi.rs: PHI挿入の薄いラッパー(builder context extraction) //! - loop_predicate_scan.rs: Pattern8 bool predicate scan EdgeCFG Frag (Phase 269 P1) +//! - loop_scan_with_init.rs: Pattern6 scan with init EdgeCFG Frag (Phase 272 P0.1) +//! - loop_split_scan.rs: Pattern7 split scan EdgeCFG Frag (Phase 272 P0.2) pub mod branch; pub mod compare; pub mod constant; +pub(in crate::mir::builder) mod phi; // Phase 272 P0.2 Refactoring pub(in crate::mir::builder) mod loop_predicate_scan; // Phase 269 P1 +pub(in crate::mir::builder) mod loop_scan_with_init; // Phase 272 P0.1 +pub(in crate::mir::builder) mod loop_split_scan; // Phase 272 P0.2 diff --git a/src/mir/builder/emission/phi.rs b/src/mir/builder/emission/phi.rs new file mode 100644 index 00000000..2a20bd47 --- /dev/null +++ b/src/mir/builder/emission/phi.rs @@ -0,0 +1,57 @@ +//! PHI Insertion - Thin wrapper for builder context +//! +//! Purpose: Eliminate PHI insertion boilerplate across Pattern6/7/8 +//! +//! Architecture: +//! - SSOT: insert_phi_at_head_spanned() in src/mir/ssot/cf_common.rs +//! - This module: Builder context extraction (current_function, span) + fail-fast +//! +//! Refactoring Context: +//! - Before: Pattern6/7/8 each have ~15 lines of boilerplate (if let Some(ref mut func) ...) +//! - After: Single function call with error propagation + +use crate::mir::builder::MirBuilder; +use crate::mir::ssot::cf_common::insert_phi_at_head_spanned; +use crate::mir::{BasicBlockId, ValueId}; + +/// Insert PHI node at block header (builder wrapper) +/// +/// # Arguments +/// - `builder`: MirBuilder (for current_function, span extraction) +/// - `block`: Target block for PHI insertion +/// - `phi_dst`: Destination ValueId for PHI result +/// - `phi_inputs`: Vec of (predecessor_block, value) pairs +/// +/// # Errors +/// Returns Err if current_function is None (fail-fast) +/// +/// # Example +/// ```ignore +/// // Pattern6/7/8 header PHI insertion +/// insert_loop_phi( +/// builder, +/// header_bb, +/// i_current, +/// vec![(preheader_bb, i_init_val), (step_bb, i_next)], +/// "pattern7", +/// )?; +/// ``` +pub(in crate::mir::builder) fn insert_loop_phi( + builder: &mut MirBuilder, + block: BasicBlockId, + phi_dst: ValueId, + phi_inputs: Vec<(BasicBlockId, ValueId)>, + context: &str, +) -> Result<(), String> { + let func = builder + .scope_ctx + .current_function + .as_mut() + .ok_or_else(|| format!("[{}] insert_loop_phi: No current function", context))?; + + let span = builder.metadata_ctx.current_span(); + + insert_phi_at_head_spanned(func, block, phi_dst, phi_inputs, span); + + Ok(()) +} diff --git a/src/mir/builder/variable_context.rs b/src/mir/builder/variable_context.rs index d9914f4e..d1c0654d 100644 --- a/src/mir/builder/variable_context.rs +++ b/src/mir/builder/variable_context.rs @@ -92,6 +92,28 @@ impl VariableContext { self.variable_map.get(name).copied() } + /// Require a variable's ValueId (fail-fast variant of lookup) + /// + /// Returns the ValueId if the variable exists, otherwise returns Err. + /// + /// ## Use cases: + /// - Pattern6/7/8: Variable extraction with clear error messages + /// - JoinIR lowering: Carrier/invariant resolution + /// + /// ## Example: + /// ```ignore + /// let i_init = builder.variable_ctx.require(&parts.i_var, "pattern7")?; + /// let s_host = builder.variable_ctx.require(&parts.s_var, "pattern7")?; + /// ``` + /// + /// Phase 272 P0.2 Refactoring: Eliminate variable_map.get().ok_or() boilerplate + pub fn require(&self, name: &str, context: &str) -> Result { + self.variable_map + .get(name) + .copied() + .ok_or_else(|| format!("[{}] Variable '{}' not found in variable_map", context, name)) + } + /// Insert or update a variable's ValueId /// /// ## Important notes: diff --git a/src/mir/phi_core/phi_type_resolver.rs b/src/mir/phi_core/phi_type_resolver.rs index ee6ad2d9..c523e27f 100644 --- a/src/mir/phi_core/phi_type_resolver.rs +++ b/src/mir/phi_core/phi_type_resolver.rs @@ -76,10 +76,16 @@ impl<'f> PhiTypeResolver<'f> { match self.find_definition(v) { Some(DefKind::Copy { src }) => { // Copy → src へ進む + if std::env::var("NYASH_PHI_RESOLVER_DEBUG").is_ok() { + eprintln!("[phi_resolver] {:?} -> Copy from {:?}", v, src); + } stack.push(src); } Some(DefKind::Phi { inputs }) => { // Phi → 各 incoming ValueId へ進む + if std::env::var("NYASH_PHI_RESOLVER_DEBUG").is_ok() { + eprintln!("[phi_resolver] {:?} -> Phi with {} inputs", v, inputs.len()); + } for (_, incoming) in inputs { stack.push(incoming); } @@ -91,9 +97,14 @@ impl<'f> PhiTypeResolver<'f> { if !matches!(ty, MirType::Unknown | MirType::Void) { // 重複を避けて追加(eq で比較) if !base_types.iter().any(|t| t == ty) { + if std::env::var("NYASH_PHI_RESOLVER_DEBUG").is_ok() { + eprintln!("[phi_resolver] {:?} -> Base type {:?}", v, ty); + } base_types.push(ty.clone()); } } + } else if std::env::var("NYASH_PHI_RESOLVER_DEBUG").is_ok() { + eprintln!("[phi_resolver] {:?} -> No type in value_types", v); } } } diff --git a/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh index aaeef984..9e934380 100644 --- a/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh +++ b/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh @@ -3,7 +3,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../../../.." && pwd)" HAKORUNE_BIN="${HAKORUNE_BIN:-$PROJECT_ROOT/target/release/hakorune}" HAKO_PATH="$PROJECT_ROOT/apps/tests/phase254_p0_index_of_min.hako" diff --git a/tools/smokes/v2/profiles/integration/apps/phase274_p1_typeop_is_as_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase274_p1_typeop_is_as_vm.sh new file mode 100644 index 00000000..be09201e --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase274_p1_typeop_is_as_vm.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail + +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" +HAKO_PATH="apps/tests/phase274_p1_typeop_is_as_min.hako" +EXPECTED_EXIT=3 + +set +e +$HAKORUNE_BIN --backend vm "$HAKO_PATH" >/dev/null 2>&1 +actual_exit=$? +set -e + +if [[ $actual_exit -eq $EXPECTED_EXIT ]]; then + echo "✅ phase274_p1_typeop_is_as_vm: PASS (exit=$actual_exit)" + exit 0 +else + echo "❌ phase274_p1_typeop_is_as_vm: FAIL (expected=$EXPECTED_EXIT, got=$actual_exit)" + exit 1 +fi + diff --git a/tools/smokes/v2/profiles/integration/apps/phase274_p2_typeop_is_as_llvm.sh b/tools/smokes/v2/profiles/integration/apps/phase274_p2_typeop_is_as_llvm.sh new file mode 100644 index 00000000..f685f005 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase274_p2_typeop_is_as_llvm.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Phase 274 P2: LLVM TypeOp is/as smoke test +# Tests SSOT alignment with Rust VM for TypeOp semantics +set -e + +cd "$(dirname "$0")/../../../../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" + +set +e +NYASH_LLVM_USE_HARNESS=1 $HAKORUNE_BIN --backend llvm \ + apps/tests/phase274_p2_typeop_primitives_only.hako > /tmp/phase274_p2_llvm.txt 2>&1 +EXIT_CODE=$? +set -e + +if [ $EXIT_CODE -eq 3 ]; then + echo "[PASS] phase274_p2_typeop_is_as_llvm" + exit 0 +else + echo "[FAIL] phase274_p2_typeop_is_as_llvm: expected exit 3, got $EXIT_CODE" + cat /tmp/phase274_p2_llvm.txt + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase275_p0_eq_number_only_llvm.sh b/tools/smokes/v2/profiles/integration/apps/phase275_p0_eq_number_only_llvm.sh new file mode 100644 index 00000000..4b4daf28 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase275_p0_eq_number_only_llvm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Phase 275 P0: Test B2 - Number-only equality (LLVM) +set -e +cd "$(dirname "$0")/../../../../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" + +set +e +NYASH_LLVM_USE_HARNESS=1 $HAKORUNE_BIN --backend llvm apps/tests/phase275_p0_eq_number_only_min.hako > /tmp/phase275_eq_llvm.txt 2>&1 +EXIT_CODE=$? +set -e + +if [ $EXIT_CODE -eq 3 ]; then + echo "[PASS] phase275_p0_eq_number_only_llvm" + exit 0 +else + echo "[FAIL] expected exit=3, got $EXIT_CODE" + cat /tmp/phase275_eq_llvm.txt + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase275_p0_eq_number_only_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase275_p0_eq_number_only_vm.sh new file mode 100644 index 00000000..a1790867 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase275_p0_eq_number_only_vm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Phase 275 P0: Test B2 - Number-only equality (VM) +set -e +cd "$(dirname "$0")/../../../../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" + +set +e +$HAKORUNE_BIN --backend vm apps/tests/phase275_p0_eq_number_only_min.hako > /tmp/phase275_eq_vm.txt 2>&1 +EXIT_CODE=$? +set -e + +if [ $EXIT_CODE -eq 3 ]; then + echo "[PASS] phase275_p0_eq_number_only_vm" + exit 0 +else + echo "[FAIL] expected exit=3, got $EXIT_CODE" + cat /tmp/phase275_eq_vm.txt + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase275_p0_plus_number_only_llvm.sh b/tools/smokes/v2/profiles/integration/apps/phase275_p0_plus_number_only_llvm.sh new file mode 100644 index 00000000..764f1759 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase275_p0_plus_number_only_llvm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Phase 275 P0: Test C2 - Plus number-only promotion (LLVM) +set -e +cd "$(dirname "$0")/../../../../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" + +set +e +NYASH_LLVM_USE_HARNESS=1 $HAKORUNE_BIN --backend llvm apps/tests/phase275_p0_plus_number_only_min.hako > /tmp/phase275_plus_llvm.txt 2>&1 +EXIT_CODE=$? +set -e + +if [ $EXIT_CODE -eq 3 ]; then + echo "[PASS] phase275_p0_plus_number_only_llvm" + exit 0 +else + echo "[FAIL] expected exit=3, got $EXIT_CODE" + cat /tmp/phase275_plus_llvm.txt + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase275_p0_plus_number_only_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase275_p0_plus_number_only_vm.sh new file mode 100644 index 00000000..2a00fc77 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase275_p0_plus_number_only_vm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Phase 275 P0: Test C2 - Plus number-only promotion (VM) +set -e +cd "$(dirname "$0")/../../../../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" + +set +e +$HAKORUNE_BIN --backend vm apps/tests/phase275_p0_plus_number_only_min.hako > /tmp/phase275_plus_vm.txt 2>&1 +EXIT_CODE=$? +set -e + +if [ $EXIT_CODE -eq 3 ]; then + echo "[PASS] phase275_p0_plus_number_only_vm" + exit 0 +else + echo "[FAIL] expected exit=3, got $EXIT_CODE" + cat /tmp/phase275_plus_vm.txt + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase275_p0_truthiness_llvm.sh b/tools/smokes/v2/profiles/integration/apps/phase275_p0_truthiness_llvm.sh new file mode 100644 index 00000000..326079c1 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase275_p0_truthiness_llvm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Phase 275 P0: Test A1 - Void in boolean context → TypeError (LLVM) +set -e +cd "$(dirname "$0")/../../../../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" + +set +e +NYASH_LLVM_USE_HARNESS=1 $HAKORUNE_BIN --backend llvm apps/tests/phase275_p0_truthiness_void_error_min.hako > /tmp/phase275_truthiness_llvm.txt 2>&1 +EXIT_CODE=$? +set -e + +if [ $EXIT_CODE -ne 0 ]; then + echo "[PASS] phase275_p0_truthiness_llvm" + exit 0 +else + echo "[FAIL] expected error, got exit $EXIT_CODE" + cat /tmp/phase275_truthiness_llvm.txt + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase275_p0_truthiness_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase275_p0_truthiness_vm.sh new file mode 100644 index 00000000..e5318fc4 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase275_p0_truthiness_vm.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Phase 275 P0: Test A1 - Void in boolean context → TypeError (VM) +set -e +cd "$(dirname "$0")/../../../../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" + +set +e +$HAKORUNE_BIN --backend vm apps/tests/phase275_p0_truthiness_void_error_min.hako > /tmp/phase275_truthiness_vm.txt 2>&1 +EXIT_CODE=$? +set -e + +if [ $EXIT_CODE -ne 0 ]; then + echo "[PASS] phase275_p0_truthiness_vm" + exit 0 +else + echo "[FAIL] expected error, got exit $EXIT_CODE" + cat /tmp/phase275_truthiness_vm.txt + exit 1 +fi