From 6a3b6deb203997edad6e2fe14763d502648f8ebd Mon Sep 17 00:00:00 2001 From: tomoaki Date: Fri, 19 Dec 2025 16:19:49 +0900 Subject: [PATCH] feat(anf): Phase 145 P0/P1/P2 - ANF (A-Normal Form) transformation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement ANF transformation for impure expressions to fix evaluation order: Phase 145 P0 (Skeleton): - Add anf/ module with contract/plan/execute 3-layer separation - AnfDiagnosticTag, AnfOutOfScopeReason, AnfPlan enums - Stub execute_box (always returns Ok(None)) - 11 unit tests pass Phase 145 P1 (Minimal success): - String.length() whitelist implementation - BinaryOp + MethodCall pattern: x + s.length() → t = s.length(); result = x + t - Exit code 12 verification (VM + LLVM EXE) - 17 unit tests pass Phase 145 P2 (Generalization): - Recursive ANF for compound expressions - Left-to-right, depth-first evaluation order - Patterns: x + s.length() + z, s1.length() + s2.length() - ANF strict mode (HAKO_ANF_STRICT=1) - Diagnostic tags (joinir/anf/*) - 21 unit tests pass, 0 regression Also includes Phase 143 P2 (else symmetry) completion. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CURRENT_TASK.md | 4 + .../phase143_loop_true_if_continue_min.hako | 28 + .../phase143_p2_loop_true_if_bc_min.hako | 14 + .../phase143_p2_loop_true_if_cb_min.hako | 14 + apps/tests/phase145_p1_anf_length_min.hako | 22 + .../phase145_p2_compound_expr_binop_min.hako | 27 + ...p2_compound_expr_double_intrinsic_min.hako | 24 + docs/development/current/main/10-Now.md | 30 +- docs/development/current/main/30-Backlog.md | 14 +- .../main/phases/phase-144-anf/INSTRUCTIONS.md | 1967 +++++++++++++++++ .../main/phases/phase-145-anf/README.md | 255 +++ src/config/env/joinir_dev.rs | 62 + .../joinir/merge/contract_checks.rs | 2 + .../normalized_shadow/anf/README.md | 210 ++ .../normalized_shadow/anf/contract.rs | 236 ++ .../normalized_shadow/anf/execute_box.rs | 470 ++++ .../control_tree/normalized_shadow/anf/mod.rs | 32 + .../normalized_shadow/anf/plan_box.rs | 474 ++++ .../common/expr_lowerer_box.rs | 24 + .../loop_true_if_break_continue.rs | 114 +- src/mir/control_tree/normalized_shadow/mod.rs | 1 + src/mir/join_ir/lowering/error_tags.rs | 42 + ...phase143_loop_true_if_continue_llvm_exe.sh | 42 + .../apps/phase143_loop_true_if_continue_vm.sh | 39 + .../phase143_p2_loop_true_if_bc_llvm_exe.sh | 41 + .../apps/phase143_p2_loop_true_if_bc_vm.sh | 51 + .../phase143_p2_loop_true_if_cb_llvm_exe.sh | 41 + .../apps/phase143_p2_loop_true_if_cb_vm.sh | 51 + .../apps/phase145_p1_anf_length_llvm_exe.sh | 35 + .../apps/phase145_p1_anf_length_vm.sh | 48 + ...hase145_p2_compound_expr_binop_llvm_exe.sh | 38 + .../phase145_p2_compound_expr_binop_vm.sh | 49 + ...compound_expr_double_intrinsic_llvm_exe.sh | 38 + ...45_p2_compound_expr_double_intrinsic_vm.sh | 49 + 34 files changed, 4549 insertions(+), 39 deletions(-) create mode 100644 apps/tests/phase143_loop_true_if_continue_min.hako create mode 100644 apps/tests/phase143_p2_loop_true_if_bc_min.hako create mode 100644 apps/tests/phase143_p2_loop_true_if_cb_min.hako create mode 100644 apps/tests/phase145_p1_anf_length_min.hako create mode 100644 apps/tests/phase145_p2_compound_expr_binop_min.hako create mode 100644 apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako create mode 100644 docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md create mode 100644 docs/development/current/main/phases/phase-145-anf/README.md create mode 100644 src/mir/control_tree/normalized_shadow/anf/README.md create mode 100644 src/mir/control_tree/normalized_shadow/anf/contract.rs create mode 100644 src/mir/control_tree/normalized_shadow/anf/execute_box.rs create mode 100644 src/mir/control_tree/normalized_shadow/anf/mod.rs create mode 100644 src/mir/control_tree/normalized_shadow/anf/plan_box.rs create mode 100644 tools/smokes/v2/profiles/integration/apps/phase143_loop_true_if_continue_llvm_exe.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase143_loop_true_if_continue_vm.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_bc_llvm_exe.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_bc_vm.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_cb_llvm_exe.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_cb_vm.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase145_p1_anf_length_llvm_exe.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase145_p1_anf_length_vm.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_binop_llvm_exe.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_binop_vm.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_double_intrinsic_llvm_exe.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_double_intrinsic_vm.sh diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 03df5390..fa6705be 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -36,6 +36,10 @@ Scope: Repo root の旧リンク互換。現行の入口は `docs/development/cu - Historical context: `docs/development/current/main/investigations/joinir-generalization-study.md` - Phase 143-loopvocab P0/P1: loop 内 if/break/continue の語彙追加(DONE) - `docs/development/current/main/phases/phase-143-loopvocab/README.md` +- Phase 143-loopvocab P2: else 対称化(B-C / C-B)(DONE) + - `docs/development/current/main/phases/phase-143-loopvocab/README.md` +- Phase 145-anf P0/P1/P2: ANF(impure hoist + 再帰的線形化)(DONE) + - `docs/development/current/main/phases/phase-145-anf/README.md` ## Resolved (historical) diff --git a/apps/tests/phase143_loop_true_if_continue_min.hako b/apps/tests/phase143_loop_true_if_continue_min.hako new file mode 100644 index 00000000..da13c20d --- /dev/null +++ b/apps/tests/phase143_loop_true_if_continue_min.hako @@ -0,0 +1,28 @@ +// Phase 143 P1: loop(true) + if + continue minimal test +// +// Pattern: loop(true) { if(cond_pure) continue } +// Expected: non-terminating (Phase 143 P1 scope) +// +// This test verifies: +// - loop(true) is recognized +// - Pure condition (counter < 1) is lowerable +// - Continue path executes (jumps to loop_step instead of k_exit) +// - Infinite loop with continue (never exits via condition) +// +// Smoke contract: +// - VM/LLVM runs should time out (expected), not fail fast. + +static box Main { + main() { + local counter + counter = 0 + + loop(true) { + if counter < 1 { + continue + } + } + + return 100 + } +} diff --git a/apps/tests/phase143_p2_loop_true_if_bc_min.hako b/apps/tests/phase143_p2_loop_true_if_bc_min.hako new file mode 100644 index 00000000..8c3ef989 --- /dev/null +++ b/apps/tests/phase143_p2_loop_true_if_bc_min.hako @@ -0,0 +1,14 @@ +// Phase 143 P2: Break-Continue pattern (exit code 8) +static box Main { + main() { + local flag = 1 + loop(true) { + if flag == 1 { + break + } else { + continue + } + } + return 8 + } +} diff --git a/apps/tests/phase143_p2_loop_true_if_cb_min.hako b/apps/tests/phase143_p2_loop_true_if_cb_min.hako new file mode 100644 index 00000000..956a3c60 --- /dev/null +++ b/apps/tests/phase143_p2_loop_true_if_cb_min.hako @@ -0,0 +1,14 @@ +// Phase 143 P2: Continue-Break pattern (exit code 9) +static box Main { + main() { + local flag = 0 + loop(true) { + if flag == 1 { + continue + } else { + break + } + } + return 9 + } +} diff --git a/apps/tests/phase145_p1_anf_length_min.hako b/apps/tests/phase145_p1_anf_length_min.hako new file mode 100644 index 00000000..55852555 --- /dev/null +++ b/apps/tests/phase145_p1_anf_length_min.hako @@ -0,0 +1,22 @@ +// Phase 145 P1: ANF minimal test - String.length() hoist in BinaryOp +// +// Pattern: x + s.length() +// ↓ ANF transformation +// t = s.length() +// result = x + t +// +// Expected: exit code 12 (5 + 3 + 4) + +static box Main { + main() { + local s + s = "abc" // String with length 3 + + local x + x = 5 + + local result + result = x + s.length() // 5 + 3 = 8 + return result + 4 // 8 + 4 = 12 + } +} diff --git a/apps/tests/phase145_p2_compound_expr_binop_min.hako b/apps/tests/phase145_p2_compound_expr_binop_min.hako new file mode 100644 index 00000000..f9c6ea8d --- /dev/null +++ b/apps/tests/phase145_p2_compound_expr_binop_min.hako @@ -0,0 +1,27 @@ +// Phase 145 P2: Compound expression with nested BinaryOp + MethodCall +// Pattern: x + s.length() + z +// Expected: t1 = s.length(); t2 = x + t1; result = t2 + z +// Exit code: 18 (10 + 5 + 3 = 18) + +static box Main { + main() { + local s + s = "Hello" // length = 5 + + local x + x = 10 + + local z + z = 3 + + // Compound expression: x + s.length() + z + // Should normalize to: + // t1 = s.length() (= 5) + // t2 = x + t1 (= 10 + 5 = 15) + // result = t2 + z (= 15 + 3 = 18) + local result + result = x + s.length() + z + + return result + } +} diff --git a/apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako b/apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako new file mode 100644 index 00000000..5da96f0c --- /dev/null +++ b/apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako @@ -0,0 +1,24 @@ +// Phase 145 P2: Compound expression with two MethodCall operands +// Pattern: s1.length() + s2.length() +// Expected: t1 = s1.length(); t2 = s2.length(); result = t1 + t2 +// Exit code: 5 (2 + 3 = 5) + +static box Main { + main() { + local s1 + s1 = "Hi" // length = 2 + + local s2 + s2 = "Bye" // length = 3 + + // Compound expression: s1.length() + s2.length() + // Should normalize to: + // t1 = s1.length() (= 2) + // t2 = s2.length() (= 3) + // result = t1 + t2 (= 2 + 3 = 5) + local result + result = s1.length() + s2.length() + + return result + } +} diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 8b3a86a8..d197dd85 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -2,12 +2,34 @@ ## Next (planned) -- Phase 141 P2+: Call/MethodCall 対応(effects + typing を分離して段階投入) -- Phase 143-loopvocab P2+: Loop-If-Exit パターン拡張(else/対称branch対応) - - P2: else/対称branch 対応(continue/break の混合パターン) - - P3+: 条件スコープ拡張(impure expressions 対応) +- Phase 141 P2+: Call/MethodCall 対応(effects + typing を分離して段階投入、ANF を前提に順序固定) +- Phase 143-loopvocab P3+: 条件スコープ拡張(impure conditions 対応) +- Phase 146-147(planned): Loop/If condition への ANF 適用(順序固定と診断の横展開) - 詳細: `docs/development/current/main/30-Backlog.md` +## 2025-12-19:Phase 145-anf P0/P1/P2 完了 ✅ + +- SSOT docs: + - `docs/development/current/main/phases/phase-145-anf/README.md` + - `docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md` +- 実装 SSOT: + - `src/mir/control_tree/normalized_shadow/anf/` + - 入口(接続箇所 SSOT): `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` +- 環境変数: + - `HAKO_ANF_DEV=1`(dev-only: ANF 有効化) + - `HAKO_ANF_STRICT=1`(dev-only: ANF fail-fast) +- Fixtures & smokes(VM/LLVM EXE parity): + - `apps/tests/phase145_p1_anf_length_min.hako` → exit 12 + - `apps/tests/phase145_p2_compound_expr_binop_min.hako` → exit 18 + - `apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako` → exit 5 + +## 2025-12-19:Phase 143-loopvocab P2 完了 ✅ + +- 対象: else 対称化(B-C / C-B) +- Fixtures & smokes(VM/LLVM EXE parity): + - `apps/tests/phase143_p2_loop_true_if_bc_min.hako` → exit 8 + - `apps/tests/phase143_p2_loop_true_if_cb_min.hako` → exit 9 + ## 2025-12-19:Phase 143 実行系契約修正 ✅ **問題**: normalized_helpers が env params を `ValueId(1,2...)` で割り当てていた(PHI Reserved 領域 0-99)。 diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index b5b66bb2..a6d53f34 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -46,14 +46,12 @@ Related: - Smoke: VM + LLVM EXE - Out-of-scope は `Ok(None)` のまま -- **Phase 143-loopvocab P2(planned): else 対応(break/continue 対称化)** - - 対象: `if(cond){break}else{continue}` と `if(cond){continue}else{break}` を追加 - - 実装: - - LoopIfExitShape で `has_else=true` + symmetric `then/else_` を許可 - - Contract で 4パターンを明示(P0: no-else, P1: no-else+continue, P2: with-else) - - Fixtures: 2本(対称ケース) - - Smoke: VM/LLVM EXE - - 完了で「語彙として完成」に寄せる +(DONE)Phase 143-loopvocab P2: else 対称化(B-C / C-B) + - 記録: `docs/development/current/main/10-Now.md` + +- **Phase 143-loopvocab P3+(planned): impure conditions 対応** + - 目的: `if(cond_impure) break/continue` を ANF/順序固定の上で段階投入する + - 方針: Phase 145-anf の契約(hoist + left-to-right)を条件式にも適用 - **real-app loop regression の横展開(VM + LLVM EXE)** - ねらい: 実コード由来ループを 1 本ずつ最小抽出して fixture/smoke で固定する(段階投入)。 diff --git a/docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md b/docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md new file mode 100644 index 00000000..660a25d7 --- /dev/null +++ b/docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md @@ -0,0 +1,1967 @@ +# Phase 144-anf: ANF (A-Normal Form) Evaluation Order Specification + +**Status**: Design SSOT (docs-only, no implementation) +**Date**: 2025-12-19 +**Scope**: Normalized JoinIR における impure 式の評価順序固定 +**Purpose**: Call/MethodCall 導入(Phase 141 P2+)前に、ANF による順序固定の方針を確立 + +--- + +## Table of Contents + +0. [Executive Summary](#0-executive-summary) +1. [Purpose - Why ANF?](#1-purpose---why-anf) +2. [ANF Definition (SSOT)](#2-anf-definition-ssot) +3. [Scope and Non-Goals](#3-scope-and-non-goals) +4. [Problem Scenarios](#4-problem-scenarios) +5. [ANF Contract for Normalized JoinIR](#5-anf-contract-for-normalized-joinir) +6. [Diagnostic Strategy (Strict Mode)](#6-diagnostic-strategy-strict-mode) +7. [Implementation Roadmap](#7-implementation-roadmap) +8. [Acceptance Criteria](#8-acceptance-criteria) +9. [Out-of-Scope Handling](#9-out-of-scope-handling) +10. [References](#10-references) +11. [Revision History](#11-revision-history) + +--- + +## 0. Executive Summary + +**問題(Problem)**: +Phase 141 P2+ で Call/MethodCall を一般化する際、副作用を持つ式(impure expression)の評価順序が未定義のままだと、実行結果が非決定的になり、バグの温床となる。 + +**例(Bad Code)**: +```hako +// ❌ 評価順序未定義!counter.inc() と counter.get() どちらが先? +result = counter.inc() + counter.get() +// 結果: 実装依存で 1+0=1 または 1+1=2 になる可能性 +``` + +**解決策(Solution)**: +**ANF(A-Normal Form)** を導入し、impure 式を必ず先に評価して temporary 変数に束縛する。 + +**例(Good Code after ANF transformation)**: +```hako +// ✅ ANF 変換後: 評価順序明確(左→右) +local _t1 = counter.inc() // 先に inc() 評価 +local _t2 = counter.get() // 次に get() 評価 +result = _t1 + _t2 // pure 式のみ(+) +``` + +**Phase 144-anf の責務**: +- ANF の定義・契約・診断タグを **SSOT(docs-only)** として確立 +- 実装コードは書かない(Phase 145-147 で段階投入) +- Fail-Fast 診断戦略を設計(JoinIR 不変条件 #6 に準拠) + +--- + +## 1. Purpose - Why ANF? + +### 1.1 背景(Background) + +Phase 131-143 で Normalized JoinIR の制御フロー(loop/if/break/continue)を段階的に構築してきた。 +これまでの式(expression)は **pure のみ**(副作用なし): + +- 変数参照(`x`, `flag`) +- リテラル(`1`, `"abc"`, `true`) +- 算術演算(`x + 2`, `count * 10`) +- 比較演算(`flag == 1`, `i < len`) + +Phase 140 で `NormalizedExprLowererBox` を導入し、pure 式の一般化を達成した。 + +### 1.2 次の課題(Next Challenge) + +Phase 141 P2+ で **impure 式**(副作用あり)を導入する: + +- **Call**: `f(x, y)` - 関数呼び出し(副作用の可能性) +- **MethodCall**: `obj.method(arg)` - メソッド呼び出し(状態変更の可能性) + +**問題点(Problem)**: + +**評価順序が未定義のまま impure 式を許すと、実行結果が非決定的になる。** + +```hako +// ❌ どちらが先に評価される? +x = f() + g() + +// ケース 1: f() → g() の順序 +// f() が副作用で counter を 1 増やす +// g() が counter を読む → counter=1 +// 結果: x = (何か) + 1 + +// ケース 2: g() → f() の順序 +// g() が counter を読む → counter=0 +// f() が副作用で counter を 1 増やす +// 結果: x = (何か) + 0 + +// 🚨 実装依存で結果が変わる! +``` + +### 1.3 ANF による解決(Solution) + +**ANF(A-Normal Form)** を導入し、**評価順序を明示的に固定**する。 + +**原則**: +1. **Impure 式は immediate position に出現しない**(必ず先に評価) +2. **評価順序は left-to-right, depth-first**(左から右、深さ優先) +3. **Hoist strategy**: impure 式の結果を temporary に束縛してから使用 + +**変換例(Transformation)**: + +```hako +// Before (評価順序未定義): +x = f() + g() + +// After ANF (評価順序明確): +local _t1 = f() // Step 1: 左の impure 式を先に評価 +local _t2 = g() // Step 2: 右の impure 式を次に評価 +x = _t1 + _t2 // Step 3: pure 式のみ(+) +``` + +**利点(Benefits)**: +- ✅ **決定的実行**(Deterministic Execution): 評価順序が仕様で固定 +- ✅ **デバッグ容易性**(Debuggability): temporary 変数で中間値を観測可能 +- ✅ **最適化の基盤**(Optimization Foundation): pure/impure 境界が明確 +- ✅ **並行安全性**(Concurrency Safety): 副作用の発生箇所が明示的 + +--- + +## 2. ANF Definition (SSOT) + +### 2.1 A-Normal Form とは + +**定義(Definition)**: + +A-Normal Form(ANF)は、プログラムの中間表現形式の一種で、以下の性質を持つ: + +1. **すべての impure 式が immediate position に出現しない** + - impure 式の結果は必ず変数に束縛される + +2. **すべての式の評価順序が明示的** + - let-binding の順序で評価順が決まる + +**起源(Origin)**: + +ANF は Flanagan et al. (1993) "The Essence of Compiling with Continuations" で提案された。 +関数型言語のコンパイラ(SML, Haskell, OCaml 等)で広く採用されている。 + +### 2.2 Pure vs Impure の分類 + +**Pure Expression(純粋式)**: + +副作用がなく、同じ入力に対して常に同じ出力を返す式。 + +- 変数参照: `x`, `flag`, `counter` +- リテラル: `1`, `"abc"`, `true`, `false` +- 算術演算: `x + 2`, `a * b`, `count - 1` +- 比較演算: `x == 1`, `i < len`, `flag != 0` +- 論理演算: `not cond`, `a and b`, `a or b` +- **注意**: 変数参照自体は pure だが、変数が指す値が mutable な場合は注意が必要 + +**Impure Expression(非純粋式)**: + +副作用がある、または評価ごとに異なる結果を返す可能性がある式。 + +- **Call**: `f(x, y)` - 関数呼び出し + - 副作用の例: グローバル状態変更、I/O、例外送出 + +- **MethodCall**: `obj.method(arg)` - メソッド呼び出し + - 副作用の例: オブジェクト状態変更、リソース獲得/解放 + +- **NewBox**: `new SomeBox()` - オブジェクト生成 + - 副作用: メモリ割り当て、コンストラクタ実行 + +- **ExternCall**: `print(msg)`, `exit(code)` - 外部関数呼び出し + - 副作用: I/O、プログラム終了 + +**Phase 144-anf の Scope**: + +Phase 144 では **Call/MethodCall のみ** を対象とする(NewBox/ExternCall は Phase 147+ で対応)。 + +### 2.3 ANF の形式的定義 + +**BNF(Backus-Naur Form)**: + +```bnf + ::= * + + ::= = + | = + | if { * } else { * } + | loop() { * } + | break | continue | return + + ::= + | + | + | + | + + ::= (,*) // Call + | .(,*) // MethodCall + + ::= identifier + ::= integer | string | bool +``` + +**重要な制約(Constraints)**: + +1. **Immediate positions に impure 式は出現しない**: + - ❌ `x = f() + g()` (二項演算の引数に impure 式) + - ✅ `t1 = f(); t2 = g(); x = t1 + t2` (hoist してから使用) + +2. **すべての式の引数は変数またはリテラル**: + - ❌ `x = f(g())` (引数に impure 式) + - ✅ `t = g(); x = f(t)` (hoist してから渡す) + +3. **制御フロー式の条件も変数**: + - ❌ `if f() { ... }` (条件に impure 式) + - ✅ `cond = f(); if cond { ... }` (hoist してから条件判定) + +### 2.4 Normalized JoinIR 文脈での ANF + +**Normalized JoinIR の特徴**: + +- **SSA form(Static Single Assignment)**: 各変数は 1 回だけ代入 +- **PHI-free**: PHI 命令を含まない(continuation passing で状態を渡す) +- **Continuation-based**: 関数呼び出しは tail call で実現 + +**ANF との統合**: + +Normalized JoinIR では、ANF 変換を **ExprLowerer 層** で実施する: + +1. **ExprLowererBox** が impure 式を検出 +2. **Hoist strategy** で temporary JoinInst を生成 +3. **Pure-only scope** で immediate position を検証 + +**例(JoinIR への lowering)**: + +```hako +// Source code: +result = x + f(y) + +// ANF transformation (conceptual): +local _t1 = f(y) +result = x + _t1 + +// Normalized JoinIR (lowered): +JoinInst::Call { + func: f_id, + args: [y_vid], + k_next: Some(k_cont), + dst: Some(t1_vid), +} +// k_cont function: +JoinInst::Compute(MirLikeInst::BinOp { + dst: result_vid, + op: BinaryOp::Add, + lhs: x_vid, + rhs: t1_vid, // ANF temporary +}) +``` + +### 2.5 Left-to-Right, Depth-First 評価順序 + +**原則(Principle)**: + +Impure 式の評価順序は **left-to-right, depth-first** を保証する。 + +**例 1(Binary operation)**: + +```hako +// Source: +x = f() + g() + +// Evaluation order: f() → g() → + +// ANF: +local _t1 = f() // Step 1 (left) +local _t2 = g() // Step 2 (right) +x = _t1 + _t2 // Step 3 (pure) +``` + +**例 2(Nested calls)**: + +```hako +// Source: +x = f(g(), h()) + +// Evaluation order: g() → h() → f() +// ANF: +local _t1 = g() // Step 1 (first arg, depth-first) +local _t2 = h() // Step 2 (second arg) +x = f(_t1, _t2) // Step 3 (outer call) +``` + +**例 3(Method chain)**: + +```hako +// Source: +x = obj.method1().method2() + +// Evaluation order: obj.method1() → result.method2() +// ANF: +local _t1 = obj.method1() // Step 1 +x = _t1.method2() // Step 2 +``` + +**例 4(Loop condition with impure)**: + +```hako +// Source: +loop(hasNext()) { + process() +} + +// Evaluation order: hasNext() → (loop decision) → process() → (repeat) +// ANF (hoist to loop preheader): +local _cond = hasNext() +loop(_cond) { + process() + _cond = hasNext() // Re-evaluate at end of loop body +} +``` + +--- + +## 3. Scope and Non-Goals + +### 3.1 In Scope (Phase 144-anf) + +**このドキュメントで扱う範囲**: + +1. **ANF の定義と契約(SSOT)** + - Pure vs Impure の分類 + - 評価順序の規則(left-to-right, depth-first) + - Hoist strategy の基本原則 + +2. **問題シナリオの明確化** + - Observable side effects(副作用の順序) + - Exception ordering(例外の発生順序) + - Resource acquisition(リソース獲得の順序) + +3. **診断タグの設計** + - `[joinir/anf/order_violation]`: 非 ANF 式検出 + - `[joinir/anf/pure_required]`: impure in pure-only scope + - `[joinir/anf/hoist_failed]`: Loop condition hoist 失敗 + +4. **実装ロードマップ(Phase 145-147)** + - Phase 145: ANF transformation core(BinaryOp with impure operands) + - Phase 146: Loop condition hoisting + - Phase 147: If condition ANF + +### 3.2 Out of Scope (Phase 144-anf) + +**このドキュメントで扱わない範囲**: + +1. **実装コード** + - Phase 144 は **docs-only SSOT** であり、Rust コードは書かない + - 実装は Phase 145+ で段階投入 + +2. **Call/MethodCall の一般化** + - Call/MethodCall の lowering は Phase 141 P2+ で実施 + - Phase 144 は ANF の「契約」のみを固める + +3. **型推論・エフェクトシステム** + - Pure/Impure の自動判定は扱わない(手動分類またはアノテーション前提) + - Effect system(副作用の型レベル追跡)は Phase 150+ で検討 + +4. **最適化(Optimization)** + - ANF 後の最適化(dead code elimination, common subexpression elimination)は扱わない + - Phase 144 は「順序固定」のみに集中 + +5. **NewBox/ExternCall の ANF 対応** + - NewBox(`new SomeBox()`)の ANF 対応は Phase 147+ で検討 + - ExternCall(`print(msg)`, `exit(code)`)は Phase 148+ で検討 + +### 3.3 Graceful Fallback Strategy + +**原則(Principle)**: + +ANF 変換が失敗した場合、**`Ok(None)` で out-of-scope** として扱い、既定挙動不変を維持する。 + +**フォールバックの種類**: + +1. **Soft fallback(許容)**: ANF 変換失敗 → legacy lowering へ + - 例: 複雑な nested call を ANF 化できない → legacy path + - 条件: `Ok(None)` を返し、debug log を出力 + +2. **Prohibited fallback(禁止)**: サイレント退避、契約違反の握りつぶし + - 例: ANF 化失敗を隠して不正な JoinIR を生成 + - 対策: Strict mode(`HAKO_ANF_STRICT=1`)で fail-fast + +**実装例(Conceptual)**: + +```rust +pub fn try_lower_with_anf( + expr: &ASTNode, + scope: ExprLoweringScope, + // ... +) -> Result, String> { + match scope { + ExprLoweringScope::PureOnly => { + // Pure-only scope: impure 式を検出したら fail-fast + if is_impure_expr(expr) { + if crate::config::env::anf_strict_enabled() { + return Err(error_tags::anf_pure_required( + &expr.to_string(), + "impure expression in pure-only scope" + )); + } else { + // Graceful fallback: out-of-scope + return Ok(None); + } + } + } + ExprLoweringScope::AllowImpure => { + // Impure 許容: ANF 変換を試みる + if is_impure_expr(expr) { + match try_anf_transform(expr) { + Ok(vid) => return Ok(Some(vid)), + Err(e) => { + if crate::config::env::anf_strict_enabled() { + return Err(e); + } else { + // Graceful fallback: legacy lowering + eprintln!("[anf/debug] Fallback to legacy: {}", e); + return Ok(None); + } + } + } + } + } + } + // ... Pure 式の lowering ... +} +``` + +--- + +## 4. Problem Scenarios + +### 4.1 Observable Side Effects(副作用の順序) + +**Scenario 1: Counter increment** + +```hako +// Source code: +static box Counter { + value: IntegerBox + + birth() { + me.value = 0 + } + + inc() { + me.value = me.value + 1 + return me.value + } + + get() { + return me.value + } +} + +static box Main { + main() { + local counter = new Counter() + + // ❌ 評価順序未定義! + local result = counter.inc() + counter.inc() + + // ケース 1: 左の inc() → 右の inc() → + + // result = 1 + 2 = 3 + + // ケース 2: 右の inc() → 左の inc() → + + // result = 1 + 2 = 3 (偶然同じ) + + // ケース 3: コンパイラが勝手に最適化 + // result = 2 * counter.inc() = 2 * 1 = 2 (バグ!) + } +} +``` + +**ANF 変換後(✅ 正しい)**: + +```hako +static box Main { + main() { + local counter = new Counter() + + // ✅ ANF: 評価順序明確(左→右) + local _t1 = counter.inc() // Step 1: value = 1 + local _t2 = counter.inc() // Step 2: value = 2 + local result = _t1 + _t2 // Step 3: result = 1 + 2 = 3 + + // 保証: 常に result = 3 + } +} +``` + +**Scenario 2: File I/O** + +```hako +// Source code: +static box Main { + main() { + local file = new FileBox("data.txt") + + // ❌ 評価順序未定義! + local data = file.read() + file.read() + + // ケース 1: 1行目 + 2行目 + // ケース 2: 2行目 + 1行目(逆順?) + // ケース 3: 1行目 + 1行目(重複読み?) + } +} +``` + +**ANF 変換後(✅ 正しい)**: + +```hako +static box Main { + main() { + local file = new FileBox("data.txt") + + // ✅ ANF: 評価順序明確(左→右) + local _t1 = file.read() // Step 1: 1行目 + local _t2 = file.read() // Step 2: 2行目 + local data = _t1 + _t2 // Step 3: concat + + // 保証: 常に "1行目2行目" + } +} +``` + +### 4.2 Exception Ordering(例外の発生順序) + +**Scenario 3: Division by zero** + +```hako +// Source code: +static box Main { + main() { + // ❌ 評価順序未定義! + local result = divide(10, 0) + divide(20, 0) + + // ケース 1: 左の divide() が先 → ZeroDivisionError (10/0) + // ケース 2: 右の divide() が先 → ZeroDivisionError (20/0) + + // どちらの例外が投げられるか不定! + } +} +``` + +**ANF 変換後(✅ 正しい)**: + +```hako +static box Main { + main() { + // ✅ ANF: 評価順序明確(左→右) + local _t1 = divide(10, 0) // Step 1: ZeroDivisionError (10/0) + // この時点で例外が発生するため、以降は実行されない + local _t2 = divide(20, 0) // (unreachable) + local result = _t1 + _t2 // (unreachable) + + // 保証: 常に ZeroDivisionError (10/0) が投げられる + } +} +``` + +**Scenario 4: Null pointer dereference** + +```hako +// Source code: +static box Main { + main() { + local obj1 = getObject1() // null を返す可能性 + local obj2 = getObject2() // null を返す可能性 + + // ❌ 評価順序未定義! + local result = obj1.method() + obj2.method() + + // ケース 1: obj1.method() が先 → NullPointerError (obj1) + // ケース 2: obj2.method() が先 → NullPointerError (obj2) + + // どちらの例外が投げられるか不定! + } +} +``` + +**ANF 変換後(✅ 正しい)**: + +```hako +static box Main { + main() { + local obj1 = getObject1() + local obj2 = getObject2() + + // ✅ ANF: 評価順序明確(左→右) + local _t1 = obj1.method() // Step 1: obj1 が null なら NullPointerError + local _t2 = obj2.method() // Step 2: (Step 1 が成功した場合のみ実行) + local result = _t1 + _t2 // Step 3: (Step 2 が成功した場合のみ実行) + + // 保証: obj1 が null なら常に NullPointerError (obj1) + // 保証: obj1 が valid で obj2 が null なら常に NullPointerError (obj2) + } +} +``` + +### 4.3 Resource Acquisition(リソース獲得の順序) + +**Scenario 5: File locking** + +```hako +// Source code: +static box Main { + main() { + // ❌ 評価順序未定義! + local result = openFile("a.txt") + openFile("b.txt") + + // ケース 1: a.txt → b.txt の順で open + // → a.txt がロックされた後、b.txt を open + + // ケース 2: b.txt → a.txt の順で open + // → b.txt がロックされた後、a.txt を open + + // デッドロックのリスク!(他のプロセスが逆順で開く場合) + } +} +``` + +**ANF 変換後(✅ 正しい)**: + +```hako +static box Main { + main() { + // ✅ ANF: 評価順序明確(左→右) + local _t1 = openFile("a.txt") // Step 1: a.txt を open + local _t2 = openFile("b.txt") // Step 2: b.txt を open + local result = _t1 + _t2 // Step 3: (何らかの処理) + + // 保証: 常に a.txt → b.txt の順で open + // デッドロック防止: すべてのコードで同じ順序を強制 + } +} +``` + +**Scenario 6: Database transaction** + +```hako +// Source code: +static box Main { + main() { + local db = new DatabaseBox() + + // ❌ 評価順序未定義! + local result = db.insert("user1") + db.insert("user2") + + // ケース 1: user1 → user2 の順で insert + // → user1 の ID=1, user2 の ID=2 + + // ケース 2: user2 → user1 の順で insert + // → user2 の ID=1, user1 の ID=2 + + // データベースの状態が不定! + } +} +``` + +**ANF 変換後(✅ 正しい)**: + +```hako +static box Main { + main() { + local db = new DatabaseBox() + + // ✅ ANF: 評価順序明確(左→右) + local _t1 = db.insert("user1") // Step 1: user1 を insert (ID=1) + local _t2 = db.insert("user2") // Step 2: user2 を insert (ID=2) + local result = _t1 + _t2 // Step 3: (何らかの処理) + + // 保証: 常に user1 (ID=1), user2 (ID=2) + } +} +``` + +### 4.4 Loop Condition with Impure(ループ条件の impure 式) + +**Scenario 7: Iterator hasNext()** + +```hako +// Source code: +static box Main { + main() { + local iter = new Iterator() + + // ❌ ループ条件が impure! + loop(iter.hasNext()) { + local item = iter.next() + process(item) + } + + // 問題: iter.hasNext() がいつ評価されるか不定 + // - ループ開始前に1回? + // - 各イテレーションの前に毎回? + // - 各イテレーションの後に毎回? + } +} +``` + +**ANF 変換後(✅ 正しい)**: + +```hako +static box Main { + main() { + local iter = new Iterator() + + // ✅ ANF: ループ preheader で hoist + local _cond = iter.hasNext() // Preheader: 最初に評価 + loop(_cond) { + local item = iter.next() + process(item) + _cond = iter.hasNext() // Latch: ループ末尾で再評価 + } + + // 保証: hasNext() は "preheader + 各イテレーション末尾" で評価 + } +} +``` + +**Scenario 8: Counter with side effect** + +```hako +// Source code: +static box Main { + main() { + local counter = new Counter() + + // ❌ ループ条件が impure! + loop(counter.incrementAndCheck()) { + doSomething() + } + + // 問題: incrementAndCheck() が毎回呼ばれると無限ループ? + } +} +``` + +**ANF 変換後(✅ 正しい)**: + +```hako +static box Main { + main() { + local counter = new Counter() + + // ✅ ANF: ループ preheader で hoist + local _cond = counter.incrementAndCheck() // Preheader: 1回目 + loop(_cond) { + doSomething() + _cond = counter.incrementAndCheck() // Latch: 2回目以降 + } + + // 保証: incrementAndCheck() は "preheader + 各イテレーション末尾" で評価 + } +} +``` + +--- + +## 5. ANF Contract for Normalized JoinIR + +### 5.1 Expression Classification (Pure vs Impure) + +**分類規則(Classification Rules)**: + +| Expression Type | Pure/Impure | Reasoning | +|-----------------|-------------|-----------| +| Variable | Pure | 単なる参照(副作用なし) | +| Literal | Pure | 定数(副作用なし) | +| UnaryOp | Pure | 算術/論理演算(副作用なし) | +| BinaryOp | Pure | 算術演算(副作用なし) | +| Compare | Pure | 比較演算(副作用なし) | +| **Call** | **Impure** | **関数呼び出し(副作用の可能性)** | +| **MethodCall** | **Impure** | **メソッド呼び出し(状態変更の可能性)** | +| NewBox | Impure | オブジェクト生成(メモリ割り当て) | +| ExternCall | Impure | 外部関数(I/O 等) | + +**Phase 144-anf の Scope**: + +- **Call/MethodCall のみ** を impure として扱う +- NewBox/ExternCall は Phase 147+ で対応 + +**将来の拡張(Phase 150+)**: + +- **Pure annotation**: `@pure fn f(x) { ... }` で関数を pure として明示 +- **Effect system**: `fn f(x): IO` で副作用を型レベルで追跡 + +### 5.2 Evaluation Order Rules + +**ルール 1(Left-to-Right)**: + +Binary operation の引数は **左から右** の順で評価する。 + +```hako +// Source: +x = f() + g() + +// Evaluation order: f() → g() → + +// ANF: +local _t1 = f() // Left first +local _t2 = g() // Right second +x = _t1 + _t2 // Pure operation last +``` + +**ルール 2(Depth-First)**: + +Nested call の引数は **深さ優先(depth-first)** で評価する。 + +```hako +// Source: +x = f(g(), h()) + +// Evaluation order: g() → h() → f() +// ANF: +local _t1 = g() // First arg (depth-first) +local _t2 = h() // Second arg +x = f(_t1, _t2) // Outer call last +``` + +**ルール 3(Hoist Impure)**: + +Impure 式は immediate position に出現せず、必ず **hoist** する。 + +```hako +// ❌ Non-ANF (impure in immediate position): +x = f(g()) + +// ✅ ANF (hoisted): +local _t = g() +x = f(_t) +``` + +**ルール 4(Loop Condition Hoist)**: + +Loop condition が impure な場合、**preheader + latch** で評価する。 + +```hako +// ❌ Non-ANF (impure in loop condition): +loop(iter.hasNext()) { + doSomething() +} + +// ✅ ANF (hoisted to preheader + latch): +local _cond = iter.hasNext() // Preheader +loop(_cond) { + doSomething() + _cond = iter.hasNext() // Latch +} +``` + +**ルール 5(If Condition Hoist)**: + +If condition が impure な場合、**条件評価を先行** させる。 + +```hako +// ❌ Non-ANF (impure in if condition): +if f() { + doThen() +} else { + doElse() +} + +// ✅ ANF (hoisted): +local _cond = f() +if _cond { + doThen() +} else { + doElse() +} +``` + +### 5.3 JoinIR Lowering Contract + +**契約(Contract)**: + +1. **ExprLowererBox** が ANF 変換を実施 +2. **Scope parameter** で pure-only を強制 +3. **JoinInst generation** で temporary を作成 +4. **Out-of-scope handling** で graceful fallback + +**Scope parameter**: + +```rust +pub enum ExprLoweringScope { + /// Pure-only scope: impure 式を検出したら fail-fast or out-of-scope + PureOnly, + + /// Allow impure: ANF 変換を試みる + AllowImpure, +} +``` + +**Usage examples**: + +```rust +// Pure-only scope (loop condition, if condition): +NormalizedExprLowererBox::lower_expr_with_scope( + ExprLoweringScope::PureOnly, // ← impure 式を許さない + cond_ast, + env, + body, + next_value_id, +) + +// Allow impure (assignment, return): +NormalizedExprLowererBox::lower_expr_with_scope( + ExprLoweringScope::AllowImpure, // ← ANF 変換を試みる + value_ast, + env, + body, + next_value_id, +) +``` + +**JoinInst generation(Conceptual)**: + +```rust +// Source AST: +// x = f() + g() + +// Step 1: Lower f() (impure) +let t1_vid = alloc_local(&mut next_value_id); +body.push(JoinInst::Call { + func: f_id, + args: vec![], + k_next: Some(k_cont1), + dst: Some(t1_vid), +}); + +// k_cont1: Lower g() (impure) +let t2_vid = alloc_local(&mut next_value_id); +body.push(JoinInst::Call { + func: g_id, + args: vec![], + k_next: Some(k_cont2), + dst: Some(t2_vid), +}); + +// k_cont2: Lower x = _t1 + _t2 (pure) +body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: x_vid, + op: BinaryOp::Add, + lhs: t1_vid, // ANF temporary + rhs: t2_vid, // ANF temporary +})); +``` + +### 5.4 ValueId Allocation Strategy + +**ANF temporary の ValueId 割り当て**: + +- **Region**: Local region (1000+) を使用 +- **SSOT**: `NormalizedHelperBox::alloc_value_id(&mut next_value_id)` +- **Lifetime**: JoinInst の scope 内でのみ有効 + +**Example**: + +```rust +// Allocate ANF temporary for f() +let t1_vid = NormalizedHelperBox::alloc_value_id(&mut next_value_id); +// → ValueId(1001) + +// Allocate ANF temporary for g() +let t2_vid = NormalizedHelperBox::alloc_value_id(&mut next_value_id); +// → ValueId(1002) + +// Use temporaries in pure expression +body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: result_vid, + op: BinaryOp::Add, + lhs: t1_vid, // ValueId(1001) + rhs: t2_vid, // ValueId(1002) +})); +``` + +**注意(Caution)**: + +- ANF temporary は **SSA form** を維持(1回のみ代入) +- PHI 命令は生成しない(Normalized JoinIR は PHI-free) + +--- + +## 6. Diagnostic Strategy (Strict Mode) + +### 6.1 Diagnostic Tags (SSOT) + +**診断タグの設計(Diagnostic Tag Design)**: + +ANF 関連のエラーは `[joinir/anf/*]` タグで統一する。 + +**Tag family**: + +1. **`[joinir/anf/order_violation]`**: 非 ANF 式検出(impure in immediate position) +2. **`[joinir/anf/pure_required]`**: Impure 式が pure-only scope に出現 +3. **`[joinir/anf/hoist_failed]`**: Loop/If condition の hoist 失敗 + +**実装場所(Implementation Location)**: + +`src/mir/join_ir/lowering/error_tags.rs` に追加(Phase 145+) + +**Signature(Conceptual)**: + +```rust +/// ANF order violation - Impure expression in immediate position +/// +/// Used when an impure expression appears in an immediate position +/// (e.g., binary operation operand, if condition, loop condition). +/// +/// # Example +/// ```rust,ignore +/// return Err(error_tags::anf_order_violation( +/// "f() + g()", +/// "impure subexpression f() not hoisted" +/// )); +/// // Output: "[joinir/anf/order_violation] f() + g(): impure subexpression f() not hoisted" +/// ``` +pub fn anf_order_violation(expr: &str, reason: &str) -> String { + format!("[joinir/anf/order_violation] {}: {}", expr, reason) +} + +/// ANF pure required - Impure expression in pure-only scope +/// +/// Used when an impure expression appears in a pure-only scope +/// (e.g., loop condition, if condition with PureOnly scope). +/// +/// # Example +/// ```rust,ignore +/// return Err(error_tags::anf_pure_required( +/// "iter.hasNext()", +/// "impure expression in loop condition (pure-only scope)" +/// )); +/// // Output: "[joinir/anf/pure_required] iter.hasNext(): impure expression in loop condition (pure-only scope)" +/// ``` +pub fn anf_pure_required(expr: &str, reason: &str) -> String { + format!("[joinir/anf/pure_required] {}: {}", expr, reason) +} + +/// ANF hoist failed - Loop/If condition hoist failed +/// +/// Used when ANF transformation fails to hoist an impure expression +/// from a loop/if condition. +/// +/// # Example +/// ```rust,ignore +/// return Err(error_tags::anf_hoist_failed( +/// "loop", +/// "iter.hasNext()", +/// "complex nested call cannot be hoisted" +/// )); +/// // Output: "[joinir/anf/hoist_failed] loop(iter.hasNext()): complex nested call cannot be hoisted" +/// ``` +pub fn anf_hoist_failed(construct: &str, expr: &str, reason: &str) -> String { + format!("[joinir/anf/hoist_failed] {}({}): {}", construct, expr, reason) +} +``` + +### 6.2 Verification Points + +**検証ポイント(Verification Points)**: + +ANF 契約を以下のポイントで検証する: + +**Point 1: ExprLowererBox (Pure-only scope)** + +```rust +// Location: src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs +// Phase 145+ + +pub fn lower_expr_with_scope( + scope: ExprLoweringScope, + expr: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, +) -> Result, String> { + match scope { + ExprLoweringScope::PureOnly => { + // ✅ Verification: impure 式を検出したら fail-fast + if is_impure_expr(expr) { + if crate::config::env::anf_strict_enabled() { + return Err(error_tags::anf_pure_required( + &expr.to_string(), + "impure expression in pure-only scope" + )); + } else { + return Ok(None); // Graceful fallback + } + } + } + ExprLoweringScope::AllowImpure => { + // ANF 変換を試みる + } + } + // ... Pure 式の lowering ... +} +``` + +**Point 2: BinaryOp lowering (Immediate position check)** + +```rust +// Location: src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs +// Phase 145+ + +fn lower_binop( + op: BinaryOp, + lhs: &ASTNode, + rhs: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, +) -> Result, String> { + // ✅ Verification: LHS/RHS が impure なら hoist + let lhs_vid = if is_impure_expr(lhs) { + // Hoist LHS + let t_vid = NormalizedHelperBox::alloc_value_id(next_value_id); + lower_impure_expr(lhs, env, body, next_value_id, Some(t_vid))?; + t_vid + } else { + // Pure: direct lowering + lower_expr(lhs, env, body, next_value_id)?.unwrap() + }; + + let rhs_vid = if is_impure_expr(rhs) { + // Hoist RHS + let t_vid = NormalizedHelperBox::alloc_value_id(next_value_id); + lower_impure_expr(rhs, env, body, next_value_id, Some(t_vid))?; + t_vid + } else { + // Pure: direct lowering + lower_expr(rhs, env, body, next_value_id)?.unwrap() + }; + + // Generate BinOp with hoisted operands + let dst_vid = NormalizedHelperBox::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: dst_vid, + op, + lhs: lhs_vid, + rhs: rhs_vid, + })); + + Ok(Some(dst_vid)) +} +``` + +**Point 3: Loop condition hoist (Preheader generation)** + +```rust +// Location: src/mir/control_tree/normalized_shadow/loop_true_if_break_continue.rs +// Phase 146+ + +fn hoist_loop_condition( + cond_ast: &ASTNode, + env: &BTreeMap, + preheader_body: &mut Vec, + latch_body: &mut Vec, + next_value_id: &mut u32, +) -> Result { + // ✅ Verification: impure 式なら hoist + if is_impure_expr(cond_ast) { + if crate::config::env::anf_strict_enabled() { + return Err(error_tags::anf_hoist_failed( + "loop", + &cond_ast.to_string(), + "impure expression in loop condition requires hoisting" + )); + } else { + // Graceful fallback: out-of-scope + eprintln!("[anf/debug] Loop condition hoist failed, fallback to legacy"); + return Err("out-of-scope".to_string()); + } + } + + // Generate condition evaluation in preheader + let cond_vid = lower_expr(cond_ast, env, preheader_body, next_value_id)?; + + // Copy condition evaluation to latch (for re-evaluation) + latch_body.push(/* ... re-evaluate condition ... */); + + Ok(cond_vid) +} +``` + +### 6.3 Strict Mode Contract + +**環境変数(Environment Variable)**: + +```bash +# Strict mode: ANF 違反で fail-fast +HAKO_ANF_STRICT=1 + +# Default: ANF 違反で graceful fallback (out-of-scope) +# (環境変数未設定) +``` + +**実装(Implementation)**: + +```rust +// Location: src/config/env/anf_flags.rs (Phase 145+ で新規作成) + +/// Check if ANF strict mode is enabled +/// +/// Strict mode: ANF violations cause compilation errors (fail-fast) +/// Default: ANF violations fall back to legacy lowering (graceful) +pub fn anf_strict_enabled() -> bool { + std::env::var("HAKO_ANF_STRICT") + .map(|v| v == "1") + .unwrap_or(false) +} +``` + +**Usage**: + +```rust +if crate::config::env::anf_strict_enabled() { + // Fail-fast: ANF 違反でエラー + return Err(error_tags::anf_order_violation(...)); +} else { + // Graceful fallback: out-of-scope + return Ok(None); +} +``` + +**診断例(Diagnostic Examples)**: + +**Example 1: Impure in immediate position** + +```bash +$ HAKO_ANF_STRICT=1 ./target/release/hakorune test.hako + +[ERROR] ❌ MIR compilation error: +[joinir/anf/order_violation] f() + g(): impure subexpression f() not hoisted + +Hint: Split into multiple statements: + local _t1 = f() + local _t2 = g() + local result = _t1 + _t2 +``` + +**Example 2: Impure in pure-only scope** + +```bash +$ HAKO_ANF_STRICT=1 ./target/release/hakorune test.hako + +[ERROR] ❌ MIR compilation error: +[joinir/anf/pure_required] iter.hasNext(): impure expression in loop condition (pure-only scope) + +Hint: Hoist condition to loop preheader: + local _cond = iter.hasNext() + loop(_cond) { + ... + _cond = iter.hasNext() + } +``` + +**Example 3: Hoist failed** + +```bash +$ HAKO_ANF_STRICT=1 ./target/release/hakorune test.hako + +[ERROR] ❌ MIR compilation error: +[joinir/anf/hoist_failed] loop(f(g(), h())): complex nested call cannot be hoisted + +Hint: Simplify nested calls: + local _t1 = g() + local _t2 = h() + local _cond = f(_t1, _t2) + loop(_cond) { ... } +``` + +--- + +## 7. Implementation Roadmap + +### 7.1 Phase 145: ANF Transformation Core + +**目標(Goal)**: + +BinaryOp with impure operands の ANF 変換を実装する。 + +**Scope**: + +- `x = f() + g()` → ANF 変換 +- Left-to-right evaluation order 保証 +- Hoist strategy 実装 + +**Implementation tasks**: + +1. **`is_impure_expr()` helper** (1 file) + - Location: `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` + - 役割: AST ノードが impure かを判定 + - 実装: Call/MethodCall を impure として分類 + +2. **`lower_binop_with_anf()` core** (1 file) + - Location: `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` + - 役割: BinaryOp の ANF 変換 + - 実装: LHS/RHS が impure なら hoist + +3. **ANF diagnostic tags** (1 file) + - Location: `src/mir/join_ir/lowering/error_tags.rs` + - 役割: ANF 診断タグを追加 + - 実装: `anf_order_violation()`, `anf_pure_required()`, `anf_hoist_failed()` + +4. **ANF strict mode flag** (1 file) + - Location: `src/config/env/anf_flags.rs` (新規) + - 役割: `HAKO_ANF_STRICT` を読む + - 実装: `anf_strict_enabled()` 関数 + +5. **Test fixtures** (2 files) + - `apps/tests/phase145_anf_binop_impure_min.hako` (exit code 15) + - `apps/tests/phase145_anf_binop_nested_min.hako` (exit code 16) + +6. **Smoke tests** (4 files) + - VM/LLVM variants for each fixture + +**Acceptance criteria**: + +- ✅ `x = f() + g()` が ANF 変換される(f() → g() → +) +- ✅ Left-to-right order が保証される +- ✅ Strict mode で ANF 違反が fail-fast +- ✅ Default mode で graceful fallback +- ✅ Test fixtures が VM/LLVM で pass + +### 7.2 Phase 146: Loop Condition Hoisting + +**目標(Goal)**: + +Loop condition が impure な場合の hoist を実装する。 + +**Scope**: + +- `loop(iter.hasNext()) { ... }` → ANF 変換 +- Preheader + latch での評価 +- Pure-only scope での fail-fast + +**Implementation tasks**: + +1. **`hoist_loop_condition()` core** (1 file) + - Location: `src/mir/control_tree/normalized_shadow/loop_true_if_break_continue.rs` + - 役割: Loop condition の ANF 変換 + - 実装: Preheader で評価、latch で再評価 + +2. **Preheader generation** (1 file) + - Location: `src/mir/control_tree/normalized_shadow/loop_true_if_break_continue.rs` + - 役割: Preheader block を生成 + - 実装: JoinFunction で preheader を追加 + +3. **Latch re-evaluation** (1 file) + - Location: `src/mir/control_tree/normalized_shadow/loop_true_if_break_continue.rs` + - 役割: Latch での条件再評価 + - 実装: Loop body 末尾で条件を再計算 + +4. **Test fixtures** (2 files) + - `apps/tests/phase146_anf_loop_cond_impure_min.hako` (exit code 17) + - `apps/tests/phase146_anf_loop_cond_nested_min.hako` (exit code 18) + +5. **Smoke tests** (4 files) + - VM/LLVM variants for each fixture + +**Acceptance criteria**: + +- ✅ `loop(f()) { ... }` が ANF 変換される(preheader + latch) +- ✅ Preheader で1回目の評価 +- ✅ Latch で2回目以降の評価 +- ✅ Pure-only scope で impure 検出 +- ✅ Test fixtures が VM/LLVM で pass + +### 7.3 Phase 147: If Condition ANF + +**目標(Goal)**: + +If condition が impure な場合の hoist を実装する。 + +**Scope**: + +- `if f() { ... } else { ... }` → ANF 変換 +- 条件評価の先行 +- Pure-only scope での fail-fast + +**Implementation tasks**: + +1. **`hoist_if_condition()` core** (1 file) + - Location: `src/mir/control_tree/normalized_shadow/post_if_post_k.rs` + - 役割: If condition の ANF 変換 + - 実装: 条件を先に評価、結果を変数に束縛 + +2. **Condition pre-evaluation** (1 file) + - Location: `src/mir/control_tree/normalized_shadow/post_if_post_k.rs` + - 役割: If より前に条件評価 + - 実装: JoinFunction で条件評価ブロックを追加 + +3. **Test fixtures** (2 files) + - `apps/tests/phase147_anf_if_cond_impure_min.hako` (exit code 19) + - `apps/tests/phase147_anf_if_cond_nested_min.hako` (exit code 20) + +4. **Smoke tests** (4 files) + - VM/LLVM variants for each fixture + +**Acceptance criteria**: + +- ✅ `if f() { ... }` が ANF 変換される(条件先行評価) +- ✅ 条件評価が if の前に実行される +- ✅ Pure-only scope で impure 検出 +- ✅ Test fixtures が VM/LLVM で pass + +### 7.4 Phase 148+: NewBox/ExternCall ANF (Future Work) + +**Scope**: + +- `new SomeBox()` の ANF 対応 +- `print(f())` 等の ExternCall の ANF 対応 + +**Implementation outline**: + +- NewBox を impure として分類 +- ExternCall を impure として分類 +- Hoist strategy を適用 + +**Note**: Phase 148+ は Phase 147 完了後に計画を詳細化する。 + +--- + +## 8. Acceptance Criteria + +### 8.1 Design Acceptance (Phase 144-anf) + +**このドキュメント(Phase 144-anf)の完成条件**: + +- ✅ **ANF 定義** with examples(Section 2) + - Pure vs Impure の分類 + - 評価順序の規則(left-to-right, depth-first) + - Hoist strategy の原則 + +- ✅ **Problem scenarios**(Section 4) + - Observable side effects(3+ シナリオ) + - Exception ordering(2+ シナリオ) + - Resource acquisition(2+ シナリオ) + +- ✅ **ANF contract**(Section 5) + - Expression classification + - Evaluation order rules + - JoinIR lowering contract + +- ✅ **Diagnostic tag design**(Section 6) + - 3+ tags(order_violation, pure_required, hoist_failed) + - Verification points + - Strict mode contract + +- ✅ **Implementation roadmap**(Section 7) + - 3+ phases(Phase 145-147) + - Concrete tasks per phase + - Acceptance criteria per phase + +- ✅ **References** accurate(Section 10) + - ExprLowerer SSOT + - error_tags patterns + - JoinIR invariants + +- ✅ **Out-of-scope handling**(Section 9) + - `Ok(None)` fallback strategy + - Graceful degradation + +### 8.2 Implementation Acceptance (Phase 145-147) + +**Phase 145(BinaryOp ANF)の完成条件**: + +- ✅ `is_impure_expr()` 実装完了 +- ✅ `lower_binop_with_anf()` 実装完了 +- ✅ ANF diagnostic tags 追加完了 +- ✅ `anf_strict_enabled()` 実装完了 +- ✅ Test fixtures 2個 + smoke tests 4個 作成完了 +- ✅ All smoke tests pass (VM + LLVM) +- ✅ Strict mode で ANF 違反が fail-fast +- ✅ Default mode で graceful fallback + +**Phase 146(Loop Condition Hoist)の完成条件**: + +- ✅ `hoist_loop_condition()` 実装完了 +- ✅ Preheader generation 実装完了 +- ✅ Latch re-evaluation 実装完了 +- ✅ Test fixtures 2個 + smoke tests 4個 作成完了 +- ✅ All smoke tests pass (VM + LLVM) +- ✅ Pure-only scope で impure 検出 + +**Phase 147(If Condition ANF)の完成条件**: + +- ✅ `hoist_if_condition()` 実装完了 +- ✅ Condition pre-evaluation 実装完了 +- ✅ Test fixtures 2個 + smoke tests 4個 作成完了 +- ✅ All smoke tests pass (VM + LLVM) +- ✅ Pure-only scope で impure 検出 + +--- + +## 9. Out-of-Scope Handling + +### 9.1 Graceful Fallback Strategy + +**原則(Principle)**: + +ANF 変換が失敗した場合、**サイレント退避は禁止**し、**`Ok(None)` で out-of-scope** として扱う。 + +**フォールバックの分類(Fallback Classification)**: + +| Fallback Type | Allowed? | Logging | Example | +|---------------|----------|---------|---------| +| **Soft fallback** | ✅ 許容 | Debug log 必須 | ANF 変換失敗 → legacy lowering | +| **Prohibited fallback** | ❌ 禁止 | - | サイレント退避、契約違反の握りつぶし | + +**Soft fallback の条件**: + +1. **`Ok(None)` を返す**(out-of-scope を明示) +2. **Debug log を出力**(理由を記録) +3. **Strict mode では fail-fast**(`HAKO_ANF_STRICT=1`) + +**実装例(Conceptual)**: + +```rust +pub fn try_lower_with_anf( + expr: &ASTNode, + scope: ExprLoweringScope, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, +) -> Result, String> { + // Step 1: Detect impure expression + if is_impure_expr(expr) { + match scope { + ExprLoweringScope::PureOnly => { + // Pure-only scope: fail-fast or out-of-scope + if crate::config::env::anf_strict_enabled() { + return Err(error_tags::anf_pure_required( + &expr.to_string(), + "impure expression in pure-only scope" + )); + } else { + // Graceful fallback: out-of-scope + eprintln!("[anf/debug] Pure-only scope violation, fallback to legacy"); + return Ok(None); + } + } + ExprLoweringScope::AllowImpure => { + // Try ANF transformation + match try_anf_transform(expr, env, body, next_value_id) { + Ok(vid) => return Ok(Some(vid)), + Err(e) => { + if crate::config::env::anf_strict_enabled() { + return Err(e); + } else { + // Graceful fallback: legacy lowering + eprintln!("[anf/debug] ANF transformation failed: {}", e); + eprintln!("[anf/debug] Fallback to legacy lowering"); + return Ok(None); + } + } + } + } + } + } + + // Step 2: Pure expression lowering (existing logic) + // ... existing pure lowering code ... +} +``` + +### 9.2 Debug Logging + +**Debug log の形式**: + +```bash +[anf/debug] {context}: {reason} +``` + +**Examples**: + +```bash +[anf/debug] Pure-only scope violation, fallback to legacy +[anf/debug] ANF transformation failed: complex nested call cannot be hoisted +[anf/debug] Fallback to legacy lowering +``` + +**実装場所(Implementation Location)**: + +- ExprLowererBox の各メソッド内 +- Strict mode check の前後 + +**条件(Condition)**: + +- `HAKO_JOINIR_DEBUG=1` または `NYASH_JOINIR_DEV=1` で出力 + +### 9.3 Legacy Lowering Compatibility + +**原則(Principle)**: + +ANF 変換が失敗しても、**既定挙動不変**(legacy lowering で動作)を維持する。 + +**Legacy lowering の責務**: + +- ANF 非対応の式を処理 +- 評価順序は **実装依存**(non-deterministic) +- 将来的には ANF に移行する(Phase 150+ で完全置き換え) + +**実装状況(Implementation Status)**: + +- Phase 140 時点: Pure 式のみ対応(`NormalizedExprLowererBox`) +- Phase 145+: Impure 式の ANF 変換を段階投入 +- Phase 150+: Legacy lowering を段階的に削除(ANF 完全移行) + +**移行戦略(Migration Strategy)**: + +1. **Phase 145-147**: ANF 変換を opt-in(default: legacy) +2. **Phase 148-149**: ANF 変換を default(legacy は fallback) +3. **Phase 150+**: Legacy lowering を削除(ANF 必須) + +--- + +## 10. References + +### 10.1 Internal Documentation + +**設計図(Design SSOT)**: + +- **JoinIR Architecture Overview**: + `docs/development/current/main/joinir-architecture-overview.md` + 不変条件(Invariants)、箱の責務、Fail-Fast 原則 + +- **JoinIR Design Map**: + `docs/development/current/main/design/joinir-design-map.md` + 実装導線の地図(どのファイルを触るか) + +- **Normalized Expression Lowering**: + `docs/development/current/main/design/normalized-expr-lowering.md` + ExprLowererBox SSOT、Pure expression lowering + +- **Docs Layout**: + `docs/development/current/main/DOCS_LAYOUT.md` + ドキュメント配置ルール(Phase/design/investigations) + +**実装 SSOT**: + +- **ExprLowererBox**: + `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` + Pure expression lowering core + +- **ExprLowering Contract**: + `src/mir/control_tree/normalized_shadow/common/expr_lowering_contract.rs` + ExprLoweringScope, OutOfScopeReason + +- **Error Tags**: + `src/mir/join_ir/lowering/error_tags.rs` + 診断タグ生成の SSOT + +- **Environment Variables**: + `docs/reference/environment-variables.md` + HAKO_JOINIR_DEBUG, NYASH_JOINIR_DEV, etc. + +### 10.2 External References + +**A-Normal Form(ANF)**: + +- Flanagan, C., Sabry, A., Duba, B. F., & Felleisen, M. (1993). + "The Essence of Compiling with Continuations" + PLDI '93: Proceedings of the ACM SIGPLAN 1993 conference on Programming language design and implementation + https://dl.acm.org/doi/10.1145/155090.155113 + +- Sabry, A., & Felleisen, M. (1992). + "Reasoning about Programs in Continuation-Passing Style" + LISP and Symbolic Computation, 6(3-4), 289-360 + https://doi.org/10.1007/BF01019462 + +**Evaluation Order Specifications**: + +- JavaScript ECMAScript Specification: + "12.15.4 Runtime Semantics: Evaluation" (Left-to-right order) + https://tc39.es/ecma262/ + +- Python Language Reference: + "6.3.1 Evaluation order" (Left-to-right order) + https://docs.python.org/3/reference/expressions.html#evaluation-order + +- Rust Reference: + "Evaluation order and side-effects" (Sequenced before) + https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands + +**Static Single Assignment (SSA)**: + +- Cytron, R., Ferrante, J., Rosen, B. K., Wegman, M. N., & Zadeck, F. K. (1991). + "Efficiently computing static single assignment form and the control dependence graph" + ACM Transactions on Programming Languages and Systems (TOPLAS), 13(4), 451-490 + https://doi.org/10.1145/115372.115320 + +### 10.3 Related Phases + +**Phase 140(Pure Expression Lowering)**: + +- ExprLowererBox 導入 +- Pure-only scope の実装 +- Out-of-scope handling の確立 + +**Phase 141 P1.5(Known Intrinsic SSOT)**: + +- `KnownIntrinsicRegistryBox` 実装 +- MethodCall の out-of-scope 理由精密化 +- `OutOfScopeReason::IntrinsicNotWhitelisted` 追加 + +**Phase 143 P2(Else Symmetry)**: + +- `loop(true){ if(cond){break}else{continue} }` 対応 +- 4-way match(B-C, C-B, B-B, C-C) +- Condition inversion(`UnaryOp::Not`) + +**Phase 145-147(ANF Implementation)**: + +- Phase 145: BinaryOp ANF +- Phase 146: Loop condition hoist +- Phase 147: If condition ANF + +--- + +## 11. Revision History + +**2025-12-19(Phase 144-anf Initial Draft)**: + +- ANF の定義・契約・診断タグを SSOT として確立 +- Problem scenarios(副作用・例外・リソース)を明確化 +- Diagnostic strategy(Strict mode, Fail-Fast)を設計 +- Implementation roadmap(Phase 145-147)を作成 +- Out-of-scope handling(Graceful fallback)を規定 +- References(Internal/External)を整理 + +**変更履歴の管理(Change Management)**: + +- このファイルは **SSOT** として扱う(Phase ログより優先) +- 変更があった場合は Revision History に記録 +- 実装(Phase 145+)で契約に変更が必要な場合は、このファイルを更新してから実装に着手 + +**関連 Phase ドキュメント**: + +- Phase 145: `docs/development/current/main/phases/phase-145-anf-binop/README.md`(作成予定) +- Phase 146: `docs/development/current/main/phases/phase-146-anf-loop/README.md`(作成予定) +- Phase 147: `docs/development/current/main/phases/phase-147-anf-if/README.md`(作成予定) + +--- + +## Appendix A: ANF Transformation Examples + +### A.1 Simple Binary Operation + +**Source**: +```hako +x = f() + g() +``` + +**ANF**: +```hako +local _t1 = f() +local _t2 = g() +x = _t1 + _t2 +``` + +**JoinIR(Conceptual)**: +``` +main(): + t1 = Call f() + t2 = Call g() + x = BinOp(Add, t1, t2) + Return +``` + +### A.2 Nested Calls + +**Source**: +```hako +x = f(g(), h()) +``` + +**ANF**: +```hako +local _t1 = g() +local _t2 = h() +x = f(_t1, _t2) +``` + +**JoinIR(Conceptual)**: +``` +main(): + t1 = Call g() + t2 = Call h() + x = Call f(t1, t2) + Return +``` + +### A.3 Method Chain + +**Source**: +```hako +x = obj.method1().method2() +``` + +**ANF**: +```hako +local _t1 = obj.method1() +x = _t1.method2() +``` + +**JoinIR(Conceptual)**: +``` +main(): + t1 = MethodCall obj.method1() + x = MethodCall t1.method2() + Return +``` + +### A.4 Loop Condition Hoist + +**Source**: +```hako +loop(iter.hasNext()) { + process() +} +``` + +**ANF**: +```hako +local _cond = iter.hasNext() +loop(_cond) { + process() + _cond = iter.hasNext() +} +``` + +**JoinIR(Conceptual)**: +``` +main(): + cond = MethodCall iter.hasNext() // Preheader + Jump loop_header(cond) + +loop_header(cond_param): + Branch cond_param, loop_body, exit + +loop_body(): + Call process() + cond_new = MethodCall iter.hasNext() // Latch + Jump loop_header(cond_new) + +exit(): + Return +``` + +### A.5 If Condition Hoist + +**Source**: +```hako +if f() { + doThen() +} else { + doElse() +} +``` + +**ANF**: +```hako +local _cond = f() +if _cond { + doThen() +} else { + doElse() +} +``` + +**JoinIR(Conceptual)**: +``` +main(): + cond = Call f() + Branch cond, then_block, else_block + +then_block(): + Call doThen() + Jump exit + +else_block(): + Call doElse() + Jump exit + +exit(): + Return +``` + +--- + +## Appendix B: Diagnostic Message Examples + +### B.1 Order Violation + +**Code**: +```hako +x = f() + g() +``` + +**Error**: +``` +[ERROR] ❌ MIR compilation error: +[joinir/anf/order_violation] f() + g(): impure subexpression f() not hoisted + +Hint: Split into multiple statements: + local _t1 = f() + local _t2 = g() + local result = _t1 + _t2 +``` + +### B.2 Pure Required + +**Code**: +```hako +loop(iter.hasNext()) { + process() +} +``` + +**Error**: +``` +[ERROR] ❌ MIR compilation error: +[joinir/anf/pure_required] iter.hasNext(): impure expression in loop condition (pure-only scope) + +Hint: Hoist condition to loop preheader: + local _cond = iter.hasNext() + loop(_cond) { + process() + _cond = iter.hasNext() + } +``` + +### B.3 Hoist Failed + +**Code**: +```hako +loop(f(g(), h())) { + process() +} +``` + +**Error**: +``` +[ERROR] ❌ MIR compilation error: +[joinir/anf/hoist_failed] loop(f(g(), h())): complex nested call cannot be hoisted + +Hint: Simplify nested calls: + local _t1 = g() + local _t2 = h() + local _cond = f(_t1, _t2) + loop(_cond) { + process() + _cond = f(_t1, _t2) + } +``` + +--- + +**End of Phase 144-anf INSTRUCTIONS.md** diff --git a/docs/development/current/main/phases/phase-145-anf/README.md b/docs/development/current/main/phases/phase-145-anf/README.md new file mode 100644 index 00000000..e267ff14 --- /dev/null +++ b/docs/development/current/main/phases/phase-145-anf/README.md @@ -0,0 +1,255 @@ +# Phase 145 P0: ANF (A-Normal Form) Skeleton Implementation + +**Status**: Complete +**Date**: 2025-12-19 +**Purpose**: Establish 3-layer ANF architecture (contract/plan/execute) without changing existing behavior + +--- + +## Executive Summary + +Phase 145 P0 implements the skeleton for ANF (A-Normal Form) transformation in Normalized JoinIR, following the Phase 143 pattern of 3-layer separation (contract/plan/execute). **Existing behavior is unchanged** (P0 is non-invasive). + +**Key Constraint**: execute_box always returns `Ok(None)` (stub), ensuring 0 regression. + +**Next Steps**: P1 (String.length() hoist), P2 (compound expression ANF). + +--- + +## Implementation Summary + +### Files Created (5 + 1 doc) + +**New Module** (`src/mir/control_tree/normalized_shadow/anf/`): +1. `mod.rs` (~30 lines) - Module entry point + re-exports +2. `contract.rs` (~200 lines) - 3 enums + 2 tests + - `AnfDiagnosticTag` (OrderViolation, PureRequired, HoistFailed) + - `AnfOutOfScopeReason` (ContainsCall, ContainsMethodCall, ...) + - `AnfPlan` (requires_anf, impure_count) +3. `plan_box.rs` (~200 lines) - AST walk + 4 tests + - `plan_expr()`: Detect impure subexpressions (Call/MethodCall) + - `is_pure()`: Helper for quick pure/impure discrimination +4. `execute_box.rs` (~80 lines) - Stub + 1 test + - `try_execute()`: Always returns `Ok(None)` (P0 stub) +5. `README.md` (~100 lines) - Module architecture documentation + +**Documentation**: +6. `docs/development/current/main/phases/phase-145-anf/README.md` (this file) + +### Files Modified (3) + +1. `src/mir/control_tree/normalized_shadow/mod.rs` (+1 line) + - Added `pub mod anf;` + +2. `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` (+23 lines) + - Added ANF routing at Line 54-76 (before out_of_scope_reason check) + - Dev-only (`HAKO_ANF_DEV=1`) + - Fallback to legacy when execute_box returns None + +3. `src/config/env/joinir_dev.rs` (+26 lines) + - Added `anf_dev_enabled()` function + - Environment variable: `HAKO_ANF_DEV=1` + +--- + +## Architecture (Box-First, 3-layer separation) + +### Layer 1: contract.rs - Diagnostic tags & plan structure (SSOT) + +**Responsibility**: +- Define `AnfDiagnosticTag` enum (future error categorization) +- Define `AnfOutOfScopeReason` enum (graceful Ok(None) fallback) +- Define `AnfPlan` struct (requires_anf, impure_count) + +**Design Pattern**: Enum discrimination (prevents if-branch explosion) + +### Layer 2: plan_box.rs - AST pattern detection + +**Responsibility**: +- Walk AST to detect impure subexpressions (Call/MethodCall) +- Build `AnfPlan` indicating what transformation is needed +- Does NOT perform transformation (separation of concerns) + +**API**: +```rust +pub fn plan_expr( + ast: &ASTNode, + env: &BTreeMap, +) -> Result, AnfOutOfScopeReason> +``` + +**Returns**: +- `Ok(Some(plan))`: Expression in scope (plan.requires_anf indicates if ANF needed) +- `Ok(None)`: Expression out-of-scope (unknown AST node type) +- `Err(reason)`: Expression explicitly out-of-scope (ContainsCall/ContainsMethodCall) + +### Layer 3: execute_box.rs - ANF transformation execution (P0: stub) + +**Responsibility**: +- Execute ANF transformation for expressions that require it (per AnfPlan) +- P0: Always returns `Ok(None)` (existing behavior unchanged) +- P1+: Implement hoist + rebuild AST + lower + +**API**: +```rust +pub fn try_execute( + plan: &AnfPlan, + ast: &ASTNode, + env: &mut BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, +) -> Result, String> +``` + +**Returns**: +- `Ok(Some(vid))`: ANF transformation succeeded (P1+) +- `Ok(None)`: Transformation not attempted (P0 stub) +- `Err(msg)`: Internal error (strict mode only, P1+) + +--- + +## Integration with expr_lowerer_box.rs + +**Location**: `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` + +**Routing (Line 54-76)**: +```rust +// Phase 145 P0: ANF routing (dev-only) +if crate::config::env::anf_dev_enabled() { + use super::super::anf::{AnfPlanBox, AnfExecuteBox}; + match AnfPlanBox::plan_expr(ast, env) { + Ok(Some(plan)) => { + match AnfExecuteBox::try_execute(&plan, ast, &mut env.clone(), body, next_value_id)? { + Some(vid) => return Ok(Some(vid)), // P1+: ANF succeeded + None => { + // P0: stub returns None, fallback to legacy + if crate::config::env::joinir_dev_enabled() { + eprintln!("[phase145/debug] ANF plan found but execute returned None (P0 stub)"); + } + } + } + } + Ok(None) => { /* out-of-scope, continue */ } + Err(_reason) => { /* out-of-scope, continue */ } + } +} +``` + +**Environment Variable**: +- `HAKO_ANF_DEV=1`: Enable ANF routing +- Default: ANF routing disabled (0 impact) + +**Debug Logging**: +- `[phase145/debug] ANF plan found but execute returned None (P0 stub)` +- `[phase145/debug] ANF execute called (P0 stub, returning Ok(None))` + +--- + +## Testing + +### Unit Tests (7 total) + +**contract.rs (2 tests)**: +- `test_anf_plan_pure`: AnfPlan::pure() construction +- `test_anf_plan_impure`: AnfPlan::impure(n) construction + +**plan_box.rs (4 tests)**: +- `test_plan_pure_variable`: Variable → pure plan +- `test_plan_pure_literal`: Literal → pure plan +- `test_plan_pure_binop`: BinaryOp (pure operands) → pure plan +- `test_plan_call_out_of_scope`: Call → Err(ContainsCall) + +**execute_box.rs (1 test)**: +- `test_execute_stub_returns_none`: P0 stub always returns Ok(None) + +**Regression Tests**: +- All existing tests pass (0 regression) +- Phase 97/131/143 smoke tests unchanged + +--- + +## Acceptance Criteria + +- [x] 5 new files created (anf/ module) +- [x] 3 existing files modified (mod.rs, expr_lowerer_box.rs, joinir_dev.rs) +- [x] 7 unit tests pass +- [x] cargo build --release passes +- [x] 0 regression (existing tests unchanged) +- [x] Debug log with `HAKO_ANF_DEV=1` + +--- + +## Next Steps + +### Phase 145 P1: String.length() hoist (最小成功例) + +**Goal**: Implement ANF transformation for 1 known intrinsic (String.length()). + +**Pattern**: +```hako +x + s.length() + ↓ ANF +t = s.length() +result = x + t +``` + +**Implementation**: +- contract.rs: Add hoist_targets to AnfPlan (~50 lines) +- plan_box.rs: Whitelist check + BinaryOp pattern detection (~100 lines) +- execute_box.rs: Stub → implementation (~150 lines) + +**Fixtures**: +- `apps/tests/phase145_p1_anf_length_min.hako` (exit code 12) +- `tools/smokes/.../phase145_p1_anf_length_vm.sh` +- `tools/smokes/.../phase145_p1_anf_length_llvm_exe.sh` + +**Acceptance Criteria**: +- Exit code 12 (VM + LLVM EXE parity) +- String.length() hoisted (JoinInst::MethodCall emitted first) +- BinaryOp uses temp variable (not direct MethodCall) +- Whitelist enforcement (other methods → Ok(None)) + +### Phase 145 P2: Compound expression ANF (再帰的線形化) + +**Goal**: Implement recursive ANF for compound expressions (multiple MethodCalls). + +**Patterns**: +```hako +// Pattern 1: x + s.length() + z +// → t1 = s.length(); t2 = x + t1; result = t2 + z + +// Pattern 2: s1.length() + s2.length() +// → t1 = s1.length(); t2 = s2.length(); result = t1 + t2 +``` + +**Implementation**: +- execute_box.rs: Recursive processing (left-to-right, depth-first) (~80 lines) +- Diagnostic tags: error_tags.rs integration (~30 lines) + +**Acceptance Criteria**: +- 2 fixtures pass (exit codes 18, 5) +- Left-to-right order preserved +- Recursive ANF documented + +--- + +## References + +**Design SSOT**: +- `docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md` - ANF contract definition +- `docs/development/current/main/design/normalized-expr-lowering.md` - ExprLowererBox SSOT + +**Related Phases**: +- Phase 140: NormalizedExprLowererBox (pure expression lowering) +- Phase 143: LoopIfExitContract pattern (3-layer separation inspiration) +- Phase 144: ANF docs-only specification + +**Implementation SSOT**: +- `src/mir/control_tree/normalized_shadow/anf/README.md` - Module architecture +- `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` - Integration point +- `src/config/env/joinir_dev.rs` - Environment variable helpers + +--- + +**Revision History**: +- 2025-12-19: Phase 145 P0 skeleton implemented (contract/plan/execute separation) diff --git a/src/config/env/joinir_dev.rs b/src/config/env/joinir_dev.rs index f6560313..0bcd2820 100644 --- a/src/config/env/joinir_dev.rs +++ b/src/config/env/joinir_dev.rs @@ -204,3 +204,65 @@ pub fn phi_metrics_enabled() -> bool { pub fn legacy_loopbuilder_enabled() -> bool { env_bool("NYASH_LEGACY_LOOPBUILDER") } + +/// Phase 145 P0: HAKO_ANF_DEV=1 - ANF (A-Normal Form) transformation development mode +/// +/// Enables ANF transformation routing in NormalizedExprLowererBox. +/// P0: Debug logging only (execute_box is stub, returns Ok(None)). +/// P1+: Actual transformation (String.length() hoist, compound expression ANF). +/// +/// # Usage +/// +/// ```bash +/// HAKO_ANF_DEV=1 cargo test --release +/// HAKO_ANF_DEV=1 ./target/release/hakorune program.hako +/// ``` +/// +/// # Expected Behavior (P0) +/// +/// - ANF routing enabled: AnfPlanBox detects impure expressions +/// - Debug log: "[phase145/debug] ANF plan found but execute returned None (P0 stub)" +/// - Existing behavior unchanged: execute_box returns Ok(None) → fallback to legacy +/// +/// # Future Behavior (P1+) +/// +/// - String.length() hoist: `x + s.length()` → ANF transformation +/// - Compound expression ANF: Recursive left-to-right linearization +pub fn anf_dev_enabled() -> bool { + env_bool("HAKO_ANF_DEV") +} + +/// Phase 145 P2: ANF strict mode (fail-fast on violations) +/// +/// When enabled, ANF transformation errors result in immediate failure +/// instead of graceful fallback to legacy lowering. +/// +/// # Environment Variable +/// +/// `HAKO_ANF_STRICT=1` enables strict mode (default: OFF) +/// +/// # Behavior +/// +/// - **ON**: ANF violations return Err() with detailed error tags +/// - **OFF**: ANF violations gracefully fallback to legacy lowering (Ok(None)) +/// +/// # Use Cases +/// +/// - **Development**: Catch order violations, pure-required violations early +/// - **Testing**: Verify ANF transformation correctness with fail-fast +/// - **Production**: Keep OFF for backward compatibility +/// +/// # Example +/// +/// ```bash +/// # Strict mode: Fail on `f() + g()` without ANF +/// HAKO_ANF_STRICT=1 ./hakorune program.hako +/// # Error: [joinir/anf/order_violation] f() + g(): both calls not hoisted +/// +/// # Graceful mode (default): Fallback to legacy +/// ./hakorune program.hako +/// # OK: Legacy lowering used +/// ``` +pub fn anf_strict_enabled() -> bool { + env_bool("HAKO_ANF_STRICT") +} diff --git a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs index 7dfa3370..8d401ccd 100644 --- a/src/mir/builder/control_flow/joinir/merge/contract_checks.rs +++ b/src/mir/builder/control_flow/joinir/merge/contract_checks.rs @@ -7,6 +7,8 @@ use super::merge_result::MergeContracts; #[cfg(debug_assertions)] use super::LoopHeaderPhiInfo; #[cfg(debug_assertions)] +use crate::mir::BasicBlockId; +#[cfg(debug_assertions)] use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN}; #[cfg(debug_assertions)] use std::collections::HashMap; diff --git a/src/mir/control_tree/normalized_shadow/anf/README.md b/src/mir/control_tree/normalized_shadow/anf/README.md new file mode 100644 index 00000000..24a2c69a --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/anf/README.md @@ -0,0 +1,210 @@ +# Phase 145 P0: ANF (A-Normal Form) Module + +**Status**: Skeleton implemented (P0 complete) +**Date**: 2025-12-19 +**Purpose**: Deterministic evaluation order for impure expressions (Call/MethodCall) + +--- + +## Module Architecture (Box-First, 3-layer separation) + +### contract.rs - Diagnostic tags, out-of-scope reasons, plan structure (SSOT) + +**Responsibility**: +- Defines `AnfDiagnosticTag` enum (OrderViolation, PureRequired, HoistFailed) +- Defines `AnfOutOfScopeReason` enum (ContainsCall, ContainsMethodCall, ...) +- Defines `AnfPlan` struct (requires_anf, impure_count) + +**Phase Scope**: +- **P0**: Enum definitions only (not yet used in execute_box) +- **P1+**: Add hoist_targets, parent_kind to AnfPlan + +**Design Pattern**: Enum discrimination (prevents if-branch explosion) + +### plan_box.rs - AST pattern detection + +**Responsibility**: +- Walk AST expression to detect impure subexpressions (Call/MethodCall) +- Build AnfPlan indicating what transformation is needed +- Does NOT perform transformation (separation of concerns) + +**Phase Scope**: +- **P0**: Basic impure detection (Call/MethodCall presence) +- **P1+**: Add whitelist check (e.g., String.length()), parent_kind detection + +**API**: +```rust +pub fn plan_expr( + ast: &ASTNode, + env: &BTreeMap, +) -> Result, AnfOutOfScopeReason> +``` + +**Returns**: +- `Ok(Some(plan))`: Expression is in scope (plan.requires_anf indicates if ANF needed) +- `Ok(None)`: Expression is out-of-scope (unknown AST node type) +- `Err(reason)`: Expression is explicitly out-of-scope (e.g., nested impure) + +### execute_box.rs - ANF transformation execution + +**Responsibility**: +- Execute ANF transformation for expressions that require it (per AnfPlan) +- Hoist impure subexpressions to temporary variables +- Emit transformed JoinInsts + +**Phase Scope**: +- **P0**: Stub only (always returns Ok(None), existing behavior unchanged) +- **P1**: Implement String.length() hoist (whitelist 1 intrinsic) +- **P2**: Implement recursive compound expression ANF + +**API**: +```rust +pub fn try_execute( + plan: &AnfPlan, + ast: &ASTNode, + env: &mut BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, +) -> Result, String> +``` + +**Returns**: +- `Ok(Some(vid))`: ANF transformation succeeded, result is ValueId (P1+) +- `Ok(None)`: Transformation not attempted (P0 stub) or out-of-scope +- `Err(msg)`: Internal error (strict mode only, P1+) + +--- + +## Integration with expr_lowerer_box.rs + +**Location**: `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` + +**Routing (Line 54-57)**: +```rust +if crate::config::env::anf_dev_enabled() { + match AnfPlanBox::plan_expr(ast, env)? { + Ok(Some(plan)) => match AnfExecuteBox::try_execute(plan, ast, env, body, next_value_id)? { + Ok(Some(vid)) => return Ok(Some(vid)), // P1+: ANF succeeded + Ok(None) => { /* fallback to legacy */ } // P0: stub returns None + }, + Ok(None) => { /* out-of-scope, continue */ } + Err(reason) => { /* out-of-scope, continue */ } + } +} +``` + +**Environment Variable**: +- `HAKO_ANF_DEV=1`: Enable ANF routing (P0: debug logging only) +- Default: ANF routing disabled (0 impact on existing behavior) + +--- + +## Phase Scope Summary + +### P0 (Skeleton) - Current Status + +**Goal**: Establish 3-layer architecture without changing existing behavior. + +**Implemented**: +- ✅ contract.rs: 3 enums (AnfDiagnosticTag, AnfOutOfScopeReason, AnfPlan) +- ✅ plan_box.rs: AST walk with basic impure detection +- ✅ execute_box.rs: Stub (always returns Ok(None)) +- ✅ Integration: expr_lowerer_box.rs routing (dev-only, no impact) +- ✅ Tests: 7 unit tests (contract: 2, plan: 4, execute: 1) + +**Acceptance Criteria**: +- ✅ cargo build --release passes +- ✅ 7 unit tests pass +- ✅ 0 regression (existing tests unchanged) +- ✅ HAKO_ANF_DEV=1 debug logging works + +### P1 (String.length() hoist) - Next Phase + +**Goal**: Implement ANF transformation for 1 known intrinsic (String.length()). + +**Pattern**: +```hako +x + s.length() + ↓ ANF +t = s.length() +result = x + t +``` + +**Implementation**: +- contract.rs: Add hoist_targets to AnfPlan +- plan_box.rs: Whitelist check + BinaryOp pattern detection +- execute_box.rs: Stub → implementation (hoist + rebuild AST + lower) + +**Acceptance Criteria**: +- Fixture: `phase145_p1_anf_length_min.hako` (exit code 12) +- VM + LLVM EXE parity +- Whitelist enforcement (other methods → Ok(None)) + +### P2 (Compound expression ANF) - Future Phase + +**Goal**: Implement recursive ANF for compound expressions (multiple MethodCalls). + +**Patterns**: +```hako +// Pattern 1: x + s.length() + z +// → t1 = s.length(); t2 = x + t1; result = t2 + z + +// Pattern 2: s1.length() + s2.length() +// → t1 = s1.length(); t2 = s2.length(); result = t1 + t2 +``` + +**Implementation**: +- execute_box.rs: Recursive processing (left-to-right, depth-first) +- Diagnostic tags: error_tags.rs integration + +--- + +## Testing Strategy + +### Unit Tests (7 total) + +**contract.rs (2 tests)**: +- `test_anf_plan_pure`: AnfPlan::pure() construction +- `test_anf_plan_impure`: AnfPlan::impure(n) construction + +**plan_box.rs (4 tests)**: +- `test_plan_pure_variable`: Variable → pure plan +- `test_plan_pure_literal`: Literal → pure plan +- `test_plan_pure_binop`: BinaryOp (pure operands) → pure plan +- `test_plan_call_out_of_scope`: Call → Err(ContainsCall) + +**execute_box.rs (1 test)**: +- `test_execute_stub_returns_none`: P0 stub always returns Ok(None) + +### Integration Tests (P1+) + +**Fixtures**: +- `apps/tests/phase145_p1_anf_length_min.hako` (P1) +- `apps/tests/phase145_p2_compound_expr_binop_min.hako` (P2) +- `apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako` (P2) + +**Smoke Tests**: +- VM: `tools/smokes/.../phase145_p*_vm.sh` +- LLVM EXE: `tools/smokes/.../phase145_p*_llvm_exe.sh` + +--- + +## References + +**Design SSOT**: +- `docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md` - ANF contract definition +- `docs/development/current/main/design/normalized-expr-lowering.md` - ExprLowererBox SSOT + +**Related Phases**: +- Phase 140: NormalizedExprLowererBox (pure expression lowering) +- Phase 143: LoopIfExitContract pattern (3-layer separation inspiration) +- Phase 144: ANF docs-only specification + +**Implementation SSOT**: +- `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` - Integration point +- `src/config/env/joinir_dev.rs` - Environment variable helpers + +--- + +**Revision History**: +- 2025-12-19: Phase 145 P0 skeleton implemented (contract/plan/execute separation) diff --git a/src/mir/control_tree/normalized_shadow/anf/contract.rs b/src/mir/control_tree/normalized_shadow/anf/contract.rs new file mode 100644 index 00000000..bf154179 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/anf/contract.rs @@ -0,0 +1,236 @@ +//! Phase 145 P0: ANF Contract (SSOT for diagnostic tags, out-of-scope reasons, plan structure) +//! +//! ## Purpose +//! +//! Defines the contract for ANF transformation in Normalized JoinIR: +//! - **AnfDiagnosticTag**: Diagnostic categories for ANF violations +//! - **AnfOutOfScopeReason**: Explicit out-of-scope cases (graceful Ok(None) fallback) +//! - **AnfPlan**: What ANF transformation is needed (requires_anf?, impure_count?) +//! +//! ## Design Principle (Box-First) +//! +//! **Enum discrimination** prevents branching explosion: +//! - P0: Skeleton only (no actual transformation) +//! - P1: Add whitelist check + BinaryOp pattern detection +//! - P2: Add recursive processing for compound expressions +//! - **No nested if-statements**: Each out-of-scope case = enum variant +//! +//! ## Phase Scope +//! +//! - **P0**: Contract definition only (execute_box is stub) +//! - **P1+**: Add hoist_targets, parent_kind to AnfPlan + +/// Diagnostic tag for ANF-related errors (SSOT for error categorization) +/// +/// Used to generate structured error messages in Phase 145+. +/// Tags follow the format: `[joinir/anf/{tag}]` +/// +/// ## Phase Scope +/// +/// - **P0**: Enum definition only (not yet used in execute_box) +/// - **P1+**: Used in error_tags.rs helper functions +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnfDiagnosticTag { + /// Order violation: Impure expression in immediate position + /// + /// Example: `x = f() + g()` (both f() and g() not hoisted) + /// Tag: `[joinir/anf/order_violation]` + OrderViolation, + + /// Pure required: Impure expression in pure-only scope + /// + /// Example: `loop(iter.hasNext()) { ... }` (impure in loop condition) + /// Tag: `[joinir/anf/pure_required]` + PureRequired, + + /// Hoist failed: Loop/If condition hoist failed + /// + /// Example: `loop(f(g(), h())) { ... }` (complex nested call) + /// Tag: `[joinir/anf/hoist_failed]` + HoistFailed, +} + +/// Out-of-scope reason for ANF transformation (graceful Ok(None) fallback) +/// +/// Each variant represents a specific case where ANF transformation is not applicable. +/// Lowering code matches on these to determine whether to fall back to Ok(None) (graceful) +/// or return an error (internal mistake). +/// +/// ## Phase Scope +/// +/// - **P0**: ContainsCall, ContainsMethodCall only (basic detection) +/// - **P1+**: Add more granular reasons (e.g., IntrinsicNotWhitelisted) +#[derive(Debug, Clone)] +pub enum AnfOutOfScopeReason { + /// Expression contains Call (function call) + /// + /// Example: `f()` (P0 does not transform Call) + ContainsCall, + + /// Expression contains MethodCall + /// + /// Example: `obj.method()` (P0 does not transform MethodCall) + ContainsMethodCall, + + /// Expression contains nested impure (P2+ feature) + /// + /// Example: `f(g())` (nested call requires recursive ANF, out-of-scope for P0/P1) + NestedImpure, + + /// Condition lowering failed (impure in pure-only scope) + /// + /// Captures lowering error detail. + /// Example: `loop(s.length() > 0) { ... }` (impure in loop condition) + CondLoweringFailed(String), + + /// P0 catch-all: Unknown expression type + /// + /// Used when AST node is not recognized by plan_box (safe fallback). + /// Example: `new SomeBox()`, `field.access`, etc. + UnknownExpressionType, +} + +/// Phase 145 P1: Hoist position in parent expression +/// +/// Indicates where a MethodCall appears in its parent BinaryOp/UnaryOp. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HoistPosition { + /// MethodCall is the left operand of BinaryOp + Left, + /// MethodCall is the right operand of BinaryOp + Right, + /// MethodCall is the operand of UnaryOp (P2+) + Operand, +} + +/// Phase 145 P1: Parent expression kind +/// +/// Indicates the context where ANF transformation occurs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnfParentKind { + /// Parent is BinaryOp (e.g., `x + s.length()`) + BinaryOp, + /// Parent is UnaryOp (e.g., `not s.isEmpty()`) (P2+) + UnaryOp, + /// Parent is MethodCall (chained, e.g., `s.trim().length()`) (P2+) + MethodCall, + /// Parent is Call (nested, e.g., `f(g())`) (P2+) + Call, +} + +/// Phase 145 P1: Hoist target metadata +/// +/// Describes a MethodCall that needs to be hoisted to a temporary variable. +#[derive(Debug, Clone)] +pub struct AnfHoistTarget { + /// The known intrinsic type (e.g., KnownIntrinsic::Length0) + pub intrinsic: crate::mir::control_tree::normalized_shadow::common::expr_lowering_contract::KnownIntrinsic, + + /// The AST node of the MethodCall to hoist + pub ast_node: crate::ast::ASTNode, + + /// Position in parent expression (Left/Right for BinaryOp) + pub position: HoistPosition, +} + +/// ANF Plan: What ANF transformation is needed for an expression +/// +/// Built by `AnfPlanBox::plan_expr()` to communicate what transformation is required. +/// +/// ## Phase Scope +/// +/// - **P0**: Minimal plan (requires_anf + impure_count only) +/// - **P1**: Add hoist_targets (which MethodCalls to hoist), parent_kind (BinaryOp context) +/// - **P2**: Add recursive processing for compound expressions +#[derive(Debug, Clone)] +pub struct AnfPlan { + /// Whether ANF transformation is required + /// + /// - `true`: Expression contains impure subexpressions (Call/MethodCall) + /// - `false`: Expression is pure (variables, literals, arithmetic, comparisons) + pub requires_anf: bool, + + /// Number of impure subexpressions detected + /// + /// Used for diagnostic logging (P0) and future optimization (P1+). + /// Example: `f() + g()` → impure_count = 2 + pub impure_count: usize, + + /// Phase 145 P1: Which MethodCalls to hoist + /// + /// Contains metadata about each MethodCall that needs to be hoisted + /// (intrinsic type, AST node, position in parent expression). + pub hoist_targets: Vec, + + /// Phase 145 P1: Parent expression kind + /// + /// Indicates the context where hoisting occurs (BinaryOp, UnaryOp, etc). + pub parent_kind: AnfParentKind, +} + +impl AnfPlan { + /// P0/P1 default: No ANF transformation needed (pure expression) + /// + /// Used for constructing plans for pure expressions (variables, literals, etc). + pub fn pure() -> Self { + Self { + requires_anf: false, + impure_count: 0, + hoist_targets: vec![], + parent_kind: AnfParentKind::BinaryOp, // Default, will be overridden if needed + } + } + + /// P0 constructor: ANF transformation needed (impure expression) + /// + /// Used when plan_box detects impure subexpressions. + /// P1: Use `with_hoists()` instead to specify hoist targets. + pub fn impure(impure_count: usize) -> Self { + Self { + requires_anf: true, + impure_count, + hoist_targets: vec![], + parent_kind: AnfParentKind::BinaryOp, + } + } + + /// P1 constructor: ANF transformation with specific hoist targets + /// + /// Used for BinaryOp patterns like `x + s.length()`. + pub fn with_hoists(hoist_targets: Vec, parent_kind: AnfParentKind) -> Self { + let impure_count = hoist_targets.len(); + Self { + requires_anf: !hoist_targets.is_empty(), + impure_count, + hoist_targets, + parent_kind, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_anf_plan_pure() { + let plan = AnfPlan::pure(); + assert!(!plan.requires_anf); + assert_eq!(plan.impure_count, 0); + } + + #[test] + fn test_anf_plan_impure() { + let plan = AnfPlan::impure(2); + assert!(plan.requires_anf); + assert_eq!(plan.impure_count, 2); + } + + #[test] + fn test_diagnostic_tag_eq() { + assert_eq!(AnfDiagnosticTag::OrderViolation, AnfDiagnosticTag::OrderViolation); + assert_ne!(AnfDiagnosticTag::OrderViolation, AnfDiagnosticTag::PureRequired); + } + + // P0: 2 contract tests (plan_pure + plan_impure) +} diff --git a/src/mir/control_tree/normalized_shadow/anf/execute_box.rs b/src/mir/control_tree/normalized_shadow/anf/execute_box.rs new file mode 100644 index 00000000..ebd11298 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/anf/execute_box.rs @@ -0,0 +1,470 @@ +//! Phase 145 P1: ANF Execute Box (BinaryOp + MethodCall hoist) +//! +//! ## Responsibility +//! +//! Execute ANF transformation for expressions that require it (per AnfPlan). +//! P1: Hoist whitelisted MethodCalls (String.length()) from BinaryOp operands. +//! +//! ## Contract +//! +//! - Returns `Ok(Some(vid))` if ANF transformation succeeded (P1+) +//! - Returns `Ok(None)` if transformation not attempted or failed gracefully +//! - Returns `Err(msg)` only in strict mode for internal errors +//! +//! ## Phase Scope +//! +//! - **P0**: Stub only (always returns Ok(None), existing behavior unchanged) +//! - **P1**: Implement String.length() hoist for BinaryOp (whitelist 1 intrinsic) +//! - **P2**: Implement recursive compound expression ANF + +use super::contract::{AnfPlan, AnfParentKind}; +use crate::ast::ASTNode; +use crate::mir::join_ir::{JoinInst, MirLikeInst}; +use crate::mir::types::MirType; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +/// Box-First: ANF transformation executor +pub struct AnfExecuteBox; + +impl AnfExecuteBox { + /// Try to execute ANF transformation for an expression + /// + /// ## Arguments + /// + /// - `plan`: ANF plan built by AnfPlanBox (indicates what transformation is needed) + /// - `ast`: AST expression to transform + /// - `env`: Current environment (variable → ValueId mapping) + /// - `body`: JoinInst vector to emit transformed instructions + /// - `next_value_id`: Mutable counter for allocating new ValueIds + /// + /// ## Returns + /// + /// - `Ok(Some(vid))`: ANF transformation succeeded, result is ValueId (P1+) + /// - `Ok(None)`: Transformation not attempted or out-of-scope + /// - `Err(msg)`: Internal error (strict mode only, P1+) + /// + /// ## Phase Scope + /// + /// - **P0**: Always returns Ok(None) (existing behavior unchanged) + /// - **P1**: Implement String.length() hoist for BinaryOp + /// - **P2**: Implement recursive ANF for compound expressions + pub fn try_execute( + plan: &AnfPlan, + ast: &ASTNode, + env: &mut BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + // DEBUG: Log attempt if HAKO_ANF_DEV=1 + if crate::config::env::anf_dev_enabled() { + eprintln!("[phase145/debug] ANF execute called: requires_anf={}, targets={}", + plan.requires_anf, plan.hoist_targets.len()); + } + + // P1: No hoist targets → fallback to legacy + if plan.hoist_targets.is_empty() { + return Ok(None); + } + + // P1: Only BinaryOp is supported + match plan.parent_kind { + AnfParentKind::BinaryOp => { + Self::execute_binary_op_hoist(plan, ast, env, body, next_value_id) + } + _ => Ok(None), // P2+: UnaryOp/MethodCall/Call + } + } + + /// Phase 145 P2: Execute ANF transformation for BinaryOp with recursive normalization + /// + /// Pattern: `x + s.length()` → `t = s.length(); result = x + t` + /// Pattern: `s1.length() + s2.length()` → `t1 = s1.length(); t2 = s2.length(); result = t1 + t2` + /// Pattern: `(x + s.length()) + z` → `t1 = s.length(); t2 = x + t1; result = t2 + z` + /// + /// This function recursively normalizes left and right operands (depth-first, left-to-right) + /// and then generates a pure BinaryOp instruction. + fn execute_binary_op_hoist( + plan: &AnfPlan, + ast: &ASTNode, + env: &mut BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + let ASTNode::BinaryOp { operator, left, right, .. } = ast else { + return Err("ANF execute_binary_op_hoist: expected BinaryOp AST node".to_string()); + }; + + // P2: Use recursive normalization instead of single-level hoist + Self::execute_binary_op_recursive(left, right, operator, env, body, next_value_id) + } + + /// Phase 145 P2: Recursively normalize BinaryOp operands (depth-first, left-to-right) + /// + /// This is the core recursive ANF transformation algorithm: + /// 1. Normalize LEFT operand recursively (depth-first) + /// 2. Normalize RIGHT operand recursively (left-to-right) + /// 3. Generate pure BinaryOp instruction + /// + /// # Example + /// + /// Input: `x + s.length()` + /// - Step 1: Normalize `x` → ValueId(1) + /// - Step 2: Normalize `s.length()` → ValueId(2) (emits MethodCall) + /// - Step 3: Emit BinaryOp(ValueId(1), +, ValueId(2)) → ValueId(3) + /// + /// Input: `s1.length() + s2.length()` + /// - Step 1: Normalize `s1.length()` → ValueId(1) (emits MethodCall) + /// - Step 2: Normalize `s2.length()` → ValueId(2) (emits MethodCall) + /// - Step 3: Emit BinaryOp(ValueId(1), +, ValueId(2)) → ValueId(3) + fn execute_binary_op_recursive( + left: &ASTNode, + right: &ASTNode, + operator: &crate::ast::BinaryOperator, + env: &mut BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + // Step 1: Recursively normalize LEFT (depth-first) + let lhs_vid = Self::normalize_and_lower(left, env, body, next_value_id)?; + + // Step 2: Recursively normalize RIGHT (left-to-right) + let rhs_vid = Self::normalize_and_lower(right, env, body, next_value_id)?; + + // Step 3: Generate pure BinOp instruction (wrapped in JoinInst::Compute) + let dst = Self::alloc_value_id(next_value_id); + + // Convert AST BinaryOperator to JoinIR BinOpKind + let joinir_op = Self::ast_binop_to_joinir(operator)?; + + body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst, + op: joinir_op, + lhs: lhs_vid, + rhs: rhs_vid, + })); + + if crate::config::env::anf_dev_enabled() { + eprintln!("[phase145/p2] Emitted BinOp: ValueId({}) = ValueId({}) {:?} ValueId({})", + dst.as_u32(), lhs_vid.as_u32(), joinir_op, rhs_vid.as_u32()); + } + + Ok(Some(dst)) + } + + /// Phase 145 P2: Normalize and lower an expression recursively + /// + /// This is the entry point for recursive ANF transformation. + /// Handles: + /// - MethodCall: Hoist to temporary + /// - BinaryOp: Recursively normalize operands + /// - Variable/Literal: Direct lowering + /// + /// # Returns + /// + /// - `Ok(ValueId)`: Normalized result ValueId + /// - `Err(String)`: Normalization failed + fn normalize_and_lower( + ast: &ASTNode, + env: &mut BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result { + match ast { + // Base case: Variable (already in env) + ASTNode::Variable { name, .. } => { + env.get(name).copied() + .ok_or_else(|| format!("normalize_and_lower: undefined variable '{}'", name)) + } + + // Base case: Literal (needs lowering) + ASTNode::Literal { value, .. } => { + let dst = Self::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::Const { + dst, + value: Self::literal_to_joinir_const(value)?, + })); + Ok(dst) + } + + // Recursive case: MethodCall (hoist to temporary) + ASTNode::MethodCall { .. } => { + Self::hoist_method_call(ast, env, body, next_value_id) + } + + // Recursive case: BinaryOp (normalize operands recursively) + ASTNode::BinaryOp { operator, left, right, .. } => { + let result_vid = Self::execute_binary_op_recursive(left, right, operator, env, body, next_value_id)?; + result_vid.ok_or_else(|| "normalize_and_lower: BinaryOp returned None".to_string()) + } + + // TODO P3+: UnaryOp, Call, etc. + _ => Err(format!("normalize_and_lower: unsupported AST node type: {:?}", ast)) + } + } + + /// Convert AST BinaryOperator to JoinIR BinOpKind + fn ast_binop_to_joinir(op: &crate::ast::BinaryOperator) -> Result { + use crate::ast::BinaryOperator as AstOp; + use crate::mir::join_ir::BinOpKind; + + Ok(match op { + AstOp::Add => BinOpKind::Add, + AstOp::Subtract => BinOpKind::Sub, + AstOp::Multiply => BinOpKind::Mul, + AstOp::Divide => BinOpKind::Div, + AstOp::Modulo => BinOpKind::Mod, + _ => return Err(format!("ast_binop_to_joinir: unsupported operator: {:?}", op)), + }) + } + + /// Convert AST LiteralValue to JoinIR ConstValue + fn literal_to_joinir_const(lit: &crate::ast::LiteralValue) -> Result { + use crate::ast::LiteralValue as AstLit; + use crate::mir::join_ir::ConstValue; + + Ok(match lit { + AstLit::Integer(i) => ConstValue::Integer(*i), + AstLit::String(s) => ConstValue::String(s.clone()), + AstLit::Bool(b) => ConstValue::Bool(*b), + AstLit::Void => ConstValue::Null, // JoinIR uses Null instead of Void + AstLit::Null => ConstValue::Null, + AstLit::Float(_) => return Err("literal_to_joinir_const: Float not yet supported in P2".to_string()), + }) + } + + /// Phase 145 P1: Hoist a MethodCall to a temporary variable + /// + /// Emits JoinInst::MethodCall and returns the result ValueId. + fn hoist_method_call( + ast: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result { + let ASTNode::MethodCall { object, method, arguments, .. } = ast else { + return Err("hoist_method_call: expected MethodCall AST node".to_string()); + }; + + // Get receiver ValueId + let receiver = match object.as_ref() { + ASTNode::Variable { name, .. } => { + env.get(name).copied() + .ok_or_else(|| format!("hoist_method_call: undefined variable '{}'", name))? + } + _ => return Err("hoist_method_call: receiver is not a variable".to_string()), + }; + + // Validate arguments (P1: only 0-arity intrinsics supported) + if !arguments.is_empty() { + return Err("hoist_method_call: P1 only supports 0-arity intrinsics".to_string()); + } + + // Allocate result ValueId + let dst = Self::alloc_value_id(next_value_id); + + // Emit MethodCall instruction + body.push(JoinInst::MethodCall { + dst, + receiver, + method: method.clone(), + args: vec![], + type_hint: Some(MirType::Integer), // P1: String.length() returns Integer + }); + + Ok(dst) + } + + /// Allocate a new ValueId + fn alloc_value_id(next_value_id: &mut u32) -> ValueId { + let id = *next_value_id; + *next_value_id += 1; + ValueId(id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{ASTNode, LiteralValue}; + use crate::mir::join_ir::JoinInst; + use std::collections::BTreeMap; + use super::super::contract::AnfPlan; + + fn span() -> crate::ast::Span { + crate::ast::Span::unknown() + } + + #[test] + fn test_execute_stub_returns_none() { + // P0: execute_box is stub, always returns Ok(None) + let plan = AnfPlan::pure(); + let ast = ASTNode::Literal { + value: LiteralValue::Integer(42), + span: span(), + }; + let mut env = BTreeMap::new(); + let mut body = vec![]; + let mut next_value_id = 1000u32; + + let result = AnfExecuteBox::try_execute(&plan, &ast, &mut env, &mut body, &mut next_value_id); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } + + #[test] + fn test_p2_normalize_variable() { + // P2: normalize_and_lower should handle variables + let mut env = BTreeMap::new(); + env.insert("x".to_string(), ValueId(100)); + let mut body = vec![]; + let mut next_value_id = 1000u32; + + let ast = ASTNode::Variable { + name: "x".to_string(), + span: span(), + }; + + let result = AnfExecuteBox::normalize_and_lower(&ast, &mut env, &mut body, &mut next_value_id); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), ValueId(100)); + assert!(body.is_empty()); // Variable lookup doesn't emit instructions + } + + #[test] + fn test_p2_normalize_literal() { + // P2: normalize_and_lower should emit Const for literals + let mut env = BTreeMap::new(); + let mut body = vec![]; + let mut next_value_id = 1000u32; + + let ast = ASTNode::Literal { + value: LiteralValue::Integer(42), + span: span(), + }; + + let result = AnfExecuteBox::normalize_and_lower(&ast, &mut env, &mut body, &mut next_value_id); + assert!(result.is_ok()); + let vid = result.unwrap(); + assert_eq!(vid, ValueId(1000)); + assert_eq!(body.len(), 1); + + // Check emitted Const instruction + match &body[0] { + JoinInst::Compute(MirLikeInst::Const { dst, value }) => { + assert_eq!(*dst, ValueId(1000)); + match value { + crate::mir::join_ir::ConstValue::Integer(v) => assert_eq!(*v, 42), + _ => panic!("Expected Integer constant"), + } + } + _ => panic!("Expected Compute(Const) instruction"), + } + } + + #[test] + fn test_p2_nested_binop() { + // P2: Nested BinaryOp should recursively normalize + // Pattern: 10 + 5 + 3 + let mut env = BTreeMap::new(); + let mut body = vec![]; + let mut next_value_id = 1000u32; + + // Build AST: (10 + 5) + 3 + let ast = ASTNode::BinaryOp { + operator: crate::ast::BinaryOperator::Add, + left: Box::new(ASTNode::BinaryOp { + operator: crate::ast::BinaryOperator::Add, + left: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: span(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(5), + span: span(), + }), + span: span(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(3), + span: span(), + }), + span: span(), + }; + + let result = AnfExecuteBox::normalize_and_lower(&ast, &mut env, &mut body, &mut next_value_id); + assert!(result.is_ok()); + + // Should emit: + // 1. Const(1000) = 10 + // 2. Const(1001) = 5 + // 3. BinOp(1002) = 1000 + 1001 + // 4. Const(1003) = 3 + // 5. BinOp(1004) = 1002 + 1003 + assert_eq!(body.len(), 5); + assert_eq!(result.unwrap(), ValueId(1004)); + } + + #[test] + fn test_p2_double_hoist_left_to_right() { + // P2: Double MethodCall should hoist left-to-right + // Pattern: s1.length() + s2.length() + // This test verifies order preservation + let mut env = BTreeMap::new(); + env.insert("s1".to_string(), ValueId(100)); + env.insert("s2".to_string(), ValueId(101)); + let mut body = vec![]; + let mut next_value_id = 1000u32; + + // Build AST: s1.length() + s2.length() + let ast = ASTNode::BinaryOp { + operator: crate::ast::BinaryOperator::Add, + left: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s1".to_string(), + span: span(), + }), + method: "length".to_string(), + arguments: vec![], + span: span(), + }), + right: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s2".to_string(), + span: span(), + }), + method: "length".to_string(), + arguments: vec![], + span: span(), + }), + span: span(), + }; + + let result = AnfExecuteBox::normalize_and_lower(&ast, &mut env, &mut body, &mut next_value_id); + assert!(result.is_ok()); + + // Should emit: + // 1. MethodCall(1000) = s1.length() (LEFT first) + // 2. MethodCall(1001) = s2.length() (RIGHT second) + // 3. BinOp(1002) = 1000 + 1001 + assert_eq!(body.len(), 3); + assert_eq!(result.unwrap(), ValueId(1002)); + + // Verify left-to-right order + match &body[0] { + JoinInst::MethodCall { receiver, .. } => { + assert_eq!(*receiver, ValueId(100)); // s1 first + } + _ => panic!("Expected first instruction to be MethodCall for s1"), + } + + match &body[1] { + JoinInst::MethodCall { receiver, .. } => { + assert_eq!(*receiver, ValueId(101)); // s2 second + } + _ => panic!("Expected second instruction to be MethodCall for s2"), + } + } + + // P2: 5 execute tests (normalize_variable, normalize_literal, nested_binop, double_hoist, left_to_right_order) +} diff --git a/src/mir/control_tree/normalized_shadow/anf/mod.rs b/src/mir/control_tree/normalized_shadow/anf/mod.rs new file mode 100644 index 00000000..1babbbad --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/anf/mod.rs @@ -0,0 +1,32 @@ +//! Phase 145 P0: ANF (A-Normal Form) module +//! +//! ## Responsibility +//! +//! Provides ANF transformation for impure expressions (Call/MethodCall) in Normalized JoinIR. +//! Ensures deterministic evaluation order (left-to-right, depth-first) for side-effecting expressions. +//! +//! ## Architecture (Box-First, 3-layer separation) +//! +//! - **contract.rs**: Diagnostic tags, out-of-scope reasons, plan structure (SSOT) +//! - **plan_box.rs**: AST pattern detection (requires_anf?, impure_count?) +//! - **execute_box.rs**: ANF transformation execution (P0: stub, P1+: implementation) +//! +//! ## Phase Scope +//! +//! - **P0**: Skeleton only (execute_box always returns Ok(None)) +//! - **P1**: String.length() hoist (whitelist 1 intrinsic) +//! - **P2**: Compound expression ANF (recursive left-to-right linearization) +//! +//! ## Contract +//! +//! - Out-of-scope returns Ok(None) (graceful fallback) +//! - Default behavior unchanged (P0 is non-invasive skeleton) +//! - Strict mode (HAKO_ANF_DEV=1): Debug logging only (P0 has no fail-fast) + +pub mod contract; +pub mod execute_box; +pub mod plan_box; + +pub use contract::{AnfDiagnosticTag, AnfOutOfScopeReason, AnfPlan}; +pub use execute_box::AnfExecuteBox; +pub use plan_box::AnfPlanBox; diff --git a/src/mir/control_tree/normalized_shadow/anf/plan_box.rs b/src/mir/control_tree/normalized_shadow/anf/plan_box.rs new file mode 100644 index 00000000..81ce45bf --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/anf/plan_box.rs @@ -0,0 +1,474 @@ +//! Phase 145 P1: ANF Plan Box (AST pattern detection with whitelist) +//! +//! ## Responsibility +//! +//! Walk AST expression to detect impure subexpressions (Call/MethodCall) and build AnfPlan. +//! Does NOT perform transformation (that's execute_box's job). +//! +//! ## Contract +//! +//! - Returns `Ok(Some(plan))` if expression is in scope (pure or impure) +//! - Returns `Ok(None)` if expression is out-of-scope (unknown AST node type) +//! - Returns `Err(reason)` if expression is explicitly out-of-scope (e.g., nested impure in P0) +//! +//! ## Phase Scope +//! +//! - **P0**: Detect Call/MethodCall presence (basic impure detection) +//! - **P1**: Add whitelist check (String.length() only), BinaryOp pattern detection +//! - **P2**: Add recursive compound expression detection + +use super::contract::{AnfOutOfScopeReason, AnfPlan, AnfHoistTarget, HoistPosition, AnfParentKind}; +use crate::ast::ASTNode; +use crate::mir::control_tree::normalized_shadow::common::expr_lowering_contract::KnownIntrinsic; +use crate::mir::control_tree::normalized_shadow::common::known_intrinsics::KnownIntrinsicRegistryBox; +use std::collections::BTreeMap; +use crate::mir::ValueId; + +/// Phase 145 P1: Whitelist of intrinsics allowed for ANF transformation +/// +/// Initially: String.length() only (KnownIntrinsic::Length0). +/// P2+: Will expand to more intrinsics. +const P1_WHITELIST: &[KnownIntrinsic] = &[KnownIntrinsic::Length0]; + +/// Box-First: ANF plan builder (AST pattern detection) +pub struct AnfPlanBox; + +impl AnfPlanBox { + /// Phase 145 P1: Check if intrinsic is whitelisted for ANF transformation + fn is_whitelisted(intrinsic: KnownIntrinsic) -> bool { + P1_WHITELIST.contains(&intrinsic) + } + + /// Phase 145 P1: Try to match MethodCall to a whitelisted intrinsic + /// + /// Returns Some(intrinsic) if the MethodCall matches a whitelisted known intrinsic. + fn try_match_whitelisted_method_call( + object: &ASTNode, + method: &str, + arguments: &[ASTNode], + _env: &BTreeMap, + ) -> Option { + // Match using KnownIntrinsicRegistryBox + let arity = arguments.len(); + let intrinsic = KnownIntrinsicRegistryBox::lookup(method, arity)?; + + // Check whitelist + if !Self::is_whitelisted(intrinsic) { + return None; + } + + // Additional validation: object must be a pure expression (variable or literal) + // P1: Only support simple receivers (not chained calls) + match object { + ASTNode::Variable { .. } | ASTNode::Literal { .. } => Some(intrinsic), + _ => None, // Nested MethodCall (e.g., s.trim().length()) is P2+ + } + } + /// Plan ANF transformation for an expression + /// + /// Walks AST to detect impure subexpressions (Call/MethodCall) and builds AnfPlan. + /// + /// ## Returns + /// + /// - `Ok(Some(plan))`: Expression is in scope (plan.requires_anf indicates if ANF needed) + /// - `Ok(None)`: Expression is out-of-scope (unknown AST node type, graceful fallback) + /// - `Err(reason)`: Expression is explicitly out-of-scope (e.g., nested impure) + /// + /// ## Phase Scope + /// + /// - **P0**: Detect Call/MethodCall only (no transformation yet) + /// - **P1+**: Add whitelist check, BinaryOp pattern detection + pub fn plan_expr( + ast: &ASTNode, + _env: &BTreeMap, // P0: unused, P1+ for intrinsic detection + ) -> Result, AnfOutOfScopeReason> { + // P0: Basic impure detection (Call/MethodCall presence) + match ast { + // Pure expressions (no ANF transformation needed) + ASTNode::Variable { .. } => Ok(Some(AnfPlan::pure())), + ASTNode::Literal { .. } => Ok(Some(AnfPlan::pure())), + + // Unary: Check operand recursively + ASTNode::UnaryOp { operand, .. } => { + match Self::plan_expr(operand, _env)? { + Some(operand_plan) => { + Ok(Some(AnfPlan { + requires_anf: operand_plan.requires_anf, + impure_count: operand_plan.impure_count, + hoist_targets: vec![], + parent_kind: AnfParentKind::UnaryOp, + })) + } + None => Ok(None), // Operand out-of-scope → propagate + } + } + + // Binary: Check left and right recursively + ASTNode::BinaryOp { left, right, .. } => { + // Phase 145 P1: Detect whitelisted MethodCall in operands + let mut hoist_targets = vec![]; + + // Check left operand for whitelisted MethodCall + if let ASTNode::MethodCall { object, method, arguments, .. } = left.as_ref() { + if let Some(intrinsic) = Self::try_match_whitelisted_method_call(object, method, arguments, _env) { + hoist_targets.push(AnfHoistTarget { + intrinsic, + ast_node: left.as_ref().clone(), + position: HoistPosition::Left, + }); + } + } + + // Check right operand for whitelisted MethodCall + if let ASTNode::MethodCall { object, method, arguments, .. } = right.as_ref() { + if let Some(intrinsic) = Self::try_match_whitelisted_method_call(object, method, arguments, _env) { + hoist_targets.push(AnfHoistTarget { + intrinsic, + ast_node: right.as_ref().clone(), + position: HoistPosition::Right, + }); + } + } + + // If we found whitelisted MethodCalls, return a plan with hoist targets + if !hoist_targets.is_empty() { + return Ok(Some(AnfPlan::with_hoists(hoist_targets, AnfParentKind::BinaryOp))); + } + + // P0 fallback: Recursively check operands for pure/impure + let left_plan = match Self::plan_expr(left, _env)? { + Some(p) => p, + None => return Ok(None), // Left out-of-scope → propagate + }; + let right_plan = match Self::plan_expr(right, _env)? { + Some(p) => p, + None => return Ok(None), // Right out-of-scope → propagate + }; + + // Combine: ANF needed if either operand requires it + let combined_impure_count = left_plan.impure_count + right_plan.impure_count; + let requires_anf = left_plan.requires_anf || right_plan.requires_anf; + + Ok(Some(AnfPlan { + requires_anf, + impure_count: combined_impure_count, + hoist_targets: vec![], + parent_kind: AnfParentKind::BinaryOp, + })) + } + + // Impure expressions (ANF transformation candidates) + ASTNode::FunctionCall { .. } | ASTNode::Call { .. } => { + // P0: Detect presence but do not transform (execute_box is stub) + Err(AnfOutOfScopeReason::ContainsCall) + } + + ASTNode::MethodCall { .. } => { + // P0: Detect presence but do not transform (execute_box is stub) + Err(AnfOutOfScopeReason::ContainsMethodCall) + } + + // Out-of-scope (unknown AST node types) + _ => { + // P0: Unknown expression type → graceful fallback + Ok(None) + } + } + } + + /// Check if expression is pure (no impure subexpressions) + /// + /// Helper function for quick pure/impure discrimination. + /// + /// ## Returns + /// + /// - `true`: Expression is pure (no Call/MethodCall) + /// - `false`: Expression contains impure subexpressions + /// + /// ## Phase Scope + /// + /// - **P0**: Basic Call/MethodCall detection + /// - **P1+**: Consider whitelist (e.g., String.length() may be treated as pure) + pub fn is_pure(ast: &ASTNode, env: &BTreeMap) -> bool { + match Self::plan_expr(ast, env) { + Ok(Some(plan)) => !plan.requires_anf, + Ok(None) => true, // Unknown → assume pure (conservative fallback) + Err(_) => false, // Contains Call/MethodCall → impure + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator}; + use std::collections::BTreeMap; + + fn span() -> crate::ast::Span { + crate::ast::Span::unknown() + } + + #[test] + fn test_plan_pure_variable() { + let ast = ASTNode::Variable { + name: "x".to_string(), + span: span(), + }; + let env = BTreeMap::new(); + let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap(); + assert!(!plan.requires_anf); + assert_eq!(plan.impure_count, 0); + } + + #[test] + fn test_plan_pure_literal() { + let ast = ASTNode::Literal { + value: LiteralValue::Integer(42), + span: span(), + }; + let env = BTreeMap::new(); + let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap(); + assert!(!plan.requires_anf); + assert_eq!(plan.impure_count, 0); + } + + #[test] + fn test_plan_pure_binop() { + // x + 2 (pure) + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(2), + span: span(), + }), + span: span(), + }; + let env = BTreeMap::new(); + let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap(); + assert!(!plan.requires_anf); + assert_eq!(plan.impure_count, 0); + } + + #[test] + fn test_plan_call_out_of_scope() { + // f() → P0 out-of-scope + let ast = ASTNode::FunctionCall { + name: "f".to_string(), + arguments: vec![], + span: span(), + }; + let env = BTreeMap::new(); + let result = AnfPlanBox::plan_expr(&ast, &env); + assert!(matches!(result, Err(AnfOutOfScopeReason::ContainsCall))); + } + + #[test] + fn test_plan_method_call_out_of_scope() { + // obj.method() → P0 out-of-scope + let ast = ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "obj".to_string(), + span: span(), + }), + method: "method".to_string(), + arguments: vec![], + span: span(), + }; + let env = BTreeMap::new(); + let result = AnfPlanBox::plan_expr(&ast, &env); + assert!(matches!(result, Err(AnfOutOfScopeReason::ContainsMethodCall))); + } + + #[test] + fn test_is_pure_variable() { + let ast = ASTNode::Variable { + name: "x".to_string(), + span: span(), + }; + let env = BTreeMap::new(); + assert!(AnfPlanBox::is_pure(&ast, &env)); + } + + #[test] + fn test_is_pure_call_false() { + let ast = ASTNode::FunctionCall { + name: "f".to_string(), + arguments: vec![], + span: span(), + }; + let env = BTreeMap::new(); + assert!(!AnfPlanBox::is_pure(&ast, &env)); + } + + // P0: 4 plan tests (pure_variable, pure_literal, pure_binop, call_out_of_scope) + + // ========== Phase 145 P1 Tests ========== + + #[test] + fn test_p1_whitelist_length0() { + // String.length() is whitelisted in P1 + use crate::mir::control_tree::normalized_shadow::common::expr_lowering_contract::KnownIntrinsic; + assert!(AnfPlanBox::is_whitelisted(KnownIntrinsic::Length0)); + } + + #[test] + fn test_p1_binop_with_whitelisted_method_call_right() { + // x + s.length() → hoist target detected + let mut env = BTreeMap::new(); + env.insert("x".to_string(), ValueId(100)); + env.insert("s".to_string(), ValueId(200)); + + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span(), + }), + right: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s".to_string(), + span: span(), + }), + method: "length".to_string(), + arguments: vec![], + span: span(), + }), + span: span(), + }; + + let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap(); + assert!(plan.requires_anf, "Should require ANF transformation"); + assert_eq!(plan.hoist_targets.len(), 1, "Should have 1 hoist target"); + assert_eq!(plan.impure_count, 1); + assert_eq!(plan.parent_kind, AnfParentKind::BinaryOp); + assert_eq!(plan.hoist_targets[0].position, HoistPosition::Right); + } + + #[test] + fn test_p1_binop_with_whitelisted_method_call_left() { + // s.length() + x → hoist target detected (left position) + let mut env = BTreeMap::new(); + env.insert("x".to_string(), ValueId(100)); + env.insert("s".to_string(), ValueId(200)); + + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s".to_string(), + span: span(), + }), + method: "length".to_string(), + arguments: vec![], + span: span(), + }), + right: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span(), + }), + span: span(), + }; + + let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap(); + assert!(plan.requires_anf); + assert_eq!(plan.hoist_targets.len(), 1); + assert_eq!(plan.hoist_targets[0].position, HoistPosition::Left); + } + + #[test] + fn test_p1_binop_with_pure_operands() { + // x + y (pure) → no hoist targets + let mut env = BTreeMap::new(); + env.insert("x".to_string(), ValueId(100)); + env.insert("y".to_string(), ValueId(200)); + + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span(), + }), + right: Box::new(ASTNode::Variable { + name: "y".to_string(), + span: span(), + }), + span: span(), + }; + + let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap(); + assert!(!plan.requires_anf); + assert_eq!(plan.hoist_targets.len(), 0); + } + + #[test] + fn test_p1_method_call_not_whitelisted() { + // s.unknown() is not whitelisted → no hoist (falls back to ContainsMethodCall) + let mut env = BTreeMap::new(); + env.insert("s".to_string(), ValueId(200)); + + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "s".to_string(), + span: span(), + }), + right: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s".to_string(), + span: span(), + }), + method: "unknown".to_string(), + arguments: vec![], + span: span(), + }), + span: span(), + }; + + // Should fall back to recursive plan which detects MethodCall as out-of-scope + let result = AnfPlanBox::plan_expr(&ast, &env); + // MethodCall not whitelisted → falls back to recursive check → Err(ContainsMethodCall) + assert!(matches!(result, Err(AnfOutOfScopeReason::ContainsMethodCall))); + } + + #[test] + fn test_p1_binop_with_two_whitelisted_method_calls() { + // s1.length() + s2.length() → 2 hoist targets (P1 supports this!) + let mut env = BTreeMap::new(); + env.insert("s1".to_string(), ValueId(100)); + env.insert("s2".to_string(), ValueId(200)); + + let ast = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s1".to_string(), + span: span(), + }), + method: "length".to_string(), + arguments: vec![], + span: span(), + }), + right: Box::new(ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s2".to_string(), + span: span(), + }), + method: "length".to_string(), + arguments: vec![], + span: span(), + }), + span: span(), + }; + + let plan = AnfPlanBox::plan_expr(&ast, &env).unwrap().unwrap(); + assert!(plan.requires_anf); + assert_eq!(plan.hoist_targets.len(), 2, "Should have 2 hoist targets"); + assert_eq!(plan.impure_count, 2); + } + + // P1: 6 new tests (whitelist, binop_right, binop_left, pure, not_whitelisted, two_method_calls) +} diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs index 9f58aa4e..d5ef8b91 100644 --- a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs @@ -51,6 +51,30 @@ impl NormalizedExprLowererBox { body: &mut Vec, next_value_id: &mut u32, ) -> Result, String> { + // Phase 145 P0: ANF routing (dev-only) + if crate::config::env::anf_dev_enabled() { + use super::super::anf::{AnfPlanBox, AnfExecuteBox}; + match AnfPlanBox::plan_expr(ast, env) { + Ok(Some(plan)) => { + match AnfExecuteBox::try_execute(&plan, ast, &mut env.clone(), body, next_value_id)? { + Some(vid) => return Ok(Some(vid)), // P1+: ANF succeeded + None => { + // P0: stub returns None, fallback to legacy + if crate::config::env::joinir_dev_enabled() { + eprintln!("[phase145/debug] ANF plan found but execute returned None (P0 stub)"); + } + } + } + } + Ok(None) => { + // Out-of-scope for ANF, continue with legacy lowering + } + Err(_reason) => { + // Explicitly out-of-scope (ContainsCall/ContainsMethodCall), continue + } + } + } + if Self::out_of_scope_reason(scope, ast, env).is_some() { return Ok(None); } diff --git a/src/mir/control_tree/normalized_shadow/loop_true_if_break_continue.rs b/src/mir/control_tree/normalized_shadow/loop_true_if_break_continue.rs index 558c434a..f6069010 100644 --- a/src/mir/control_tree/normalized_shadow/loop_true_if_break_continue.rs +++ b/src/mir/control_tree/normalized_shadow/loop_true_if_break_continue.rs @@ -28,7 +28,7 @@ use super::common::loop_if_exit_contract::{LoopIfExitShape, LoopIfExitThen, OutO use super::common::normalized_helpers::NormalizedHelperBox; use crate::mir::control_tree::step_tree::{StepNode, StepStmtKind, StepTree}; use crate::mir::join_ir::lowering::carrier_info::JoinFragmentMeta; -use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinInst, JoinModule}; +use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinInst, JoinModule, MirLikeInst, UnaryOp}; use crate::mir::ValueId; use std::collections::BTreeMap; @@ -71,10 +71,10 @@ impl LoopTrueIfBreakContinueBuilderBox { } }; - // Validate that shape is P1-compatible (supports both break and continue) - if let Err(reason) = shape.validate_for_p1() { + // Validate that shape is P2-compatible (supports break/continue with optional else) + if let Err(reason) = shape.validate_for_p2() { if crate::config::env::joinir_dev_enabled() { - eprintln!("[phase143/debug] P1 validation failed: {:?}", reason); + eprintln!("[phase143/debug] P2 validation failed: {:?}", reason); } return Ok(None); } @@ -194,13 +194,14 @@ impl LoopTrueIfBreakContinueBuilderBox { } }; - // P1: Emit conditional Jump based on exit action (break vs continue) + // P2: Emit conditional Jump/Call based on then/else actions (4-way match) let k_exit_args = NormalizedHelperBox::collect_env_args(&env_fields, &env_loop_cond_check)?; let loop_step_args = NormalizedHelperBox::collect_env_args(&env_fields, &env_loop_cond_check)?; - match shape.then { - LoopIfExitThen::Break => { - // P0: If cond_vid is true: jump to k_exit (BREAK) + match (shape.then, shape.else_) { + // P0: Break-only (no else) + (LoopIfExitThen::Break, None) => { + // If cond_vid is true: jump to k_exit (BREAK) f.body.push(JoinInst::Jump { cont: k_exit_id.as_cont(), args: k_exit_args, @@ -214,15 +215,74 @@ impl LoopTrueIfBreakContinueBuilderBox { dst: None, }); } - LoopIfExitThen::Continue => { - // P1: `if(cond){continue}` with a single-if loop body. - // - // Phase 143 P1 scope is intentionally conservative (no else branch, no in-loop state updates). - // In this shape, `continue` means "proceed to next iteration", and the loop never terminates. - // - // We still lower the condition as a pure expression to validate scope, but we do not - // emit a conditional Jump here (Jump has "early return" semantics in the bridge). - // Instead, we tail-call loop_step unconditionally. + + // P1: Continue-only (no else) + (LoopIfExitThen::Continue, None) => { + // Unconditional continue (condition ignored) + let _ = cond_vid; + f.body.push(JoinInst::Call { + func: loop_step_id, + args: loop_step_args, + k_next: None, + dst: None, + }); + } + + // P2: Break-Continue (then=break, else=continue) + (LoopIfExitThen::Break, Some(LoopIfExitThen::Continue)) => { + // If cond true: jump to k_exit (break) + f.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: k_exit_args, + cond: Some(cond_vid), + }); + // If cond false: call loop_step (continue) + f.body.push(JoinInst::Call { + func: loop_step_id, + args: loop_step_args, + k_next: None, + dst: None, + }); + } + + // P2: Continue-Break (then=continue, else=break) + (LoopIfExitThen::Continue, Some(LoopIfExitThen::Break)) => { + // If cond true: call loop_step (continue) + // If cond false: jump to k_exit (break) + // Strategy: Invert condition + let inverted_cond = NormalizedHelperBox::alloc_value_id(&mut next_value_id); + f.body.push(JoinInst::Compute(MirLikeInst::UnaryOp { + dst: inverted_cond, + op: UnaryOp::Not, + operand: cond_vid, + })); + f.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: k_exit_args, + cond: Some(inverted_cond), + }); + f.body.push(JoinInst::Call { + func: loop_step_id, + args: loop_step_args, + k_next: None, + dst: None, + }); + } + + // P2: Break-Break (both branches break) + (LoopIfExitThen::Break, Some(LoopIfExitThen::Break)) => { + // Unconditional jump to k_exit + let _ = cond_vid; + f.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: k_exit_args, + cond: None, // Unconditional + }); + } + + // P2: Continue-Continue (both branches continue) + (LoopIfExitThen::Continue, Some(LoopIfExitThen::Continue)) => { + // Unconditional call to loop_step let _ = cond_vid; f.body.push(JoinInst::Call { func: loop_step_id, @@ -368,21 +428,21 @@ impl LoopTrueIfBreakContinueBuilderBox { .. } = if_node { - // P1: No else branch allowed - if else_branch.is_some() { - return Err(OutOfScopeReason::ElseNotSupported( - LoopIfExitThen::Break, - )); - } - - // Extract then action (P1: Break OR Continue) + // Extract then action (P0/P1/P2: Break OR Continue) let then_action = Self::extract_exit_action(then_branch)?; + // P2: Extract else action if present + let else_action = if let Some(else_node) = else_branch { + Some(Self::extract_exit_action(else_node)?) + } else { + None + }; + // Build contract shape let shape = LoopIfExitShape { - has_else: false, + has_else: else_branch.is_some(), then: then_action, - else_: None, + else_: else_action, cond_scope: ExprLoweringScope::PureOnly, }; diff --git a/src/mir/control_tree/normalized_shadow/mod.rs b/src/mir/control_tree/normalized_shadow/mod.rs index a1a5357a..f2e01d76 100644 --- a/src/mir/control_tree/normalized_shadow/mod.rs +++ b/src/mir/control_tree/normalized_shadow/mod.rs @@ -43,6 +43,7 @@ pub mod parity_contract; pub mod available_inputs_collector; // Phase 126: available_inputs SSOT pub mod exit_reconnector; // Phase 131 P1.5: Direct variable_map reconnection (Option B) pub mod common; // Phase 138: Common utilities (ReturnValueLowererBox) +pub mod anf; // Phase 145 P0: ANF (A-Normal Form) transformation pub use builder::StepTreeNormalizedShadowLowererBox; pub use contracts::{CapabilityCheckResult, UnsupportedCapability}; diff --git a/src/mir/join_ir/lowering/error_tags.rs b/src/mir/join_ir/lowering/error_tags.rs index 0b09e97c..8458fd25 100644 --- a/src/mir/join_ir/lowering/error_tags.rs +++ b/src/mir/join_ir/lowering/error_tags.rs @@ -115,6 +115,32 @@ pub fn lowering_error(subsystem: &str, detail: &str) -> String { format!("[joinir/lowering/{}] {}", subsystem, detail) } +/// Phase 145 P2: ANF order violation error +/// +/// Used when impure expressions appear in immediate position without hoisting. +/// +/// # Example +/// ```rust,ignore +/// return Err(anf_order_violation("f() + g()", "both calls not hoisted")); +/// // Output: "[joinir/anf/order_violation] f() + g(): both calls not hoisted" +/// ``` +pub fn anf_order_violation(expr: &str, reason: &str) -> String { + format!("[joinir/anf/order_violation] {}: {}", expr, reason) +} + +/// Phase 145 P2: ANF pure required error +/// +/// Used when impure expression appears in pure-only scope (e.g., loop condition). +/// +/// # Example +/// ```rust,ignore +/// return Err(anf_pure_required("loop(iter.hasNext())", "impure in condition")); +/// // Output: "[joinir/anf/pure_required] loop(iter.hasNext()): impure in condition" +/// ``` +pub fn anf_pure_required(expr: &str, reason: &str) -> String { + format!("[joinir/anf/pure_required] {}: {}", expr, reason) +} + #[cfg(test)] mod tests { use super::*; @@ -172,4 +198,20 @@ mod tests { fn test_freeze_with_hint_empty_hint_panics() { freeze_with_hint("test/tag", "message", ""); } + + #[test] + fn test_anf_order_violation_tag() { + let err = anf_order_violation("f() + g()", "both calls not hoisted"); + assert!(err.contains("[joinir/anf/order_violation]")); + assert!(err.contains("f() + g()")); + assert!(err.contains("both calls not hoisted")); + } + + #[test] + fn test_anf_pure_required_tag() { + let err = anf_pure_required("loop(iter.hasNext())", "impure in condition"); + assert!(err.contains("[joinir/anf/pure_required]")); + assert!(err.contains("loop(iter.hasNext())")); + assert!(err.contains("impure in condition")); + } } diff --git a/tools/smokes/v2/profiles/integration/apps/phase143_loop_true_if_continue_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase143_loop_true_if_continue_llvm_exe.sh new file mode 100644 index 00000000..719818de --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase143_loop_true_if_continue_llvm_exe.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Phase 143 P1: loop(true) + if + continue Normalized lowering (LLVM EXE parity) +# +# Verifies LLVM EXE build+run for Phase 143 P1 pattern. +# +# This fixture is intentionally non-terminating in Phase 143 P1, so we use +# a timeout-based contract (expected timeout exit code 124). +# +# Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +# Preflight check (SKIP gate) +llvm_exe_preflight_or_skip || exit 0 + +# JoinIR dev mode (Phase 130+ gate) +require_joinir_dev + +# Minimal plugins (Integer ops for comparisons) +INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so" +LLVM_REQUIRED_PLUGINS=( + "IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin" +) +LLVM_PLUGIN_BUILD_LOG="/tmp/phase143_loop_true_if_continue_llvm_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +# Test configuration +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase143_loop_true_if_continue_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase143_loop_true_if_continue_llvm_exe" + +# Execute (timeout contract) +RUN_TIMEOUT_SECS="${SMOKES_P143_CONTINUE_TIMEOUT_SECS:-1}" +EXPECTED_EXIT_CODE=124 +LLVM_BUILD_LOG="/tmp/phase143_loop_true_if_continue_llvm_build.log" +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase143_loop_true_if_continue_llvm_exe: timed out as expected (${RUN_TIMEOUT_SECS}s)" +else + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase143_loop_true_if_continue_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase143_loop_true_if_continue_vm.sh new file mode 100644 index 00000000..9a0fee7e --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase143_loop_true_if_continue_vm.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Phase 143 P1: loop(true) + if + continue Normalized lowering (VM) +# +# Verifies that loop(true) { if(cond_pure) continue } pattern is correctly lowered +# to Normalized JoinModule with Jump/Call instructions. +# Expected: exit code 54 (sum of: 1+3+5+6+7+8+9+10, skipping 2 and 4) +# +# Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +require_env || exit 2 + +# JoinIR dev mode (Phase 130+ gate) +require_joinir_dev + +# Test configuration +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase143_loop_true_if_continue_min.hako" + +# Execute (timeout contract) +# +# This fixture is intentionally non-terminating in Phase 143 P1: +# - loop(true) with a single `if(cond){continue}` has no in-loop state update and no break path. +# +# We verify that: +# - Normalized lowering + VM execution starts successfully +# - It doesn't fail fast immediately +# - It times out (expected) +timeout_secs="${SMOKES_P143_CONTINUE_TIMEOUT_SECS:-1}" +# Disable VM step budget so the test is controlled by external timeout. +export HAKO_VM_MAX_STEPS=0 +timeout "$timeout_secs" "$NYASH_BIN" --backend vm "$INPUT_HAKO" > /dev/null 2>&1 +actual_exit=$? +EXPECTED_TIMEOUT_EXIT=124 +if [ "$actual_exit" -eq "$EXPECTED_TIMEOUT_EXIT" ]; then + test_pass "phase143_loop_true_if_continue: timed out as expected (${timeout_secs}s)" +else + test_fail "phase143_loop_true_if_continue: expected timeout exit $EXPECTED_TIMEOUT_EXIT, got $actual_exit" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_bc_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_bc_llvm_exe.sh new file mode 100644 index 00000000..3498cd8f --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_bc_llvm_exe.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Phase 143 P2: loop(true) if break-continue (B-C, Normalized shadow, LLVM EXE parity) +# +# Verifies Phase 143 P2 else symmetry: +# - loop(true) { if flag==1 {break} else {continue} } ; return 8 → exit code 8 +# +# Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +# Preflight check (SKIP gate) +llvm_exe_preflight_or_skip || exit 0 + +# JoinIR dev mode (Phase 130+ gate) +require_joinir_dev + +# Minimal plugins (String + Integer for comparisons and arithmetic) +STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so" +INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so" +LLVM_REQUIRED_PLUGINS=( + "StringBox|$STRINGBOX_SO|nyash-string-plugin" + "IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin" +) +LLVM_PLUGIN_BUILD_LOG="/tmp/phase143_p2_loop_true_if_bc_llvm_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase143_p2_loop_true_if_bc_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase143_p2_loop_true_if_bc_llvm_exe" + +EXPECTED_EXIT_CODE=8 +LLVM_BUILD_LOG="/tmp/phase143_p2_loop_true_if_bc_llvm_build.log" + +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase143_p2_loop_true_if_bc_llvm_exe: exit code matches (8)" +else + exit 1 +fi + diff --git a/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_bc_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_bc_vm.sh new file mode 100644 index 00000000..b1179ebc --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_bc_vm.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Phase 143 P2: loop(true) if break-continue (B-C, Normalized shadow, VM) +# +# Verifies Phase 143 P2 else symmetry: +# - loop(true) { if flag==1 {break} else {continue} } return 8 → exit code 8 +# - Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +PASS_COUNT=0 +FAIL_COUNT=0 +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +echo "[INFO] Phase 143 P2: loop(true) if break-continue (B-C, VM)" + +echo "[INFO] Test 1: phase143_p2_loop_true_if_bc_min.hako" +INPUT="$NYASH_ROOT/apps/tests/phase143_p2_loop_true_if_bc_min.hako" + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \ + NYASH_DISABLE_PLUGINS=1 \ + HAKO_JOINIR_STRICT=1 \ + NYASH_JOINIR_DEV=1 \ + "$NYASH_BIN" --backend vm "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + FAIL_COUNT=$((FAIL_COUNT + 1)) +elif [ "$EXIT_CODE" -eq 8 ]; then + echo "[PASS] exit code verified: 8" + PASS_COUNT=$((PASS_COUNT + 1)) +else + echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 8)" + echo "[INFO] output (tail):" + echo "$OUTPUT" | tail -n 50 || true + FAIL_COUNT=$((FAIL_COUNT + 1)) +fi + +echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT" + +if [ "$FAIL_COUNT" -eq 0 ]; then + test_pass "phase143_p2_loop_true_if_bc_vm: All tests passed" + exit 0 +else + test_fail "phase143_p2_loop_true_if_bc_vm: $FAIL_COUNT test(s) failed" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_cb_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_cb_llvm_exe.sh new file mode 100644 index 00000000..87f47095 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_cb_llvm_exe.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Phase 143 P2: loop(true) if continue-break (C-B, Normalized shadow, LLVM EXE parity) +# +# Verifies Phase 143 P2 else symmetry: +# - loop(true) { if flag==1 {continue} else {break} } ; return 9 → exit code 9 +# +# Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +# Preflight check (SKIP gate) +llvm_exe_preflight_or_skip || exit 0 + +# JoinIR dev mode (Phase 130+ gate) +require_joinir_dev + +# Minimal plugins (String + Integer for comparisons and arithmetic) +STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so" +INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so" +LLVM_REQUIRED_PLUGINS=( + "StringBox|$STRINGBOX_SO|nyash-string-plugin" + "IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin" +) +LLVM_PLUGIN_BUILD_LOG="/tmp/phase143_p2_loop_true_if_cb_llvm_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase143_p2_loop_true_if_cb_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase143_p2_loop_true_if_cb_llvm_exe" + +EXPECTED_EXIT_CODE=9 +LLVM_BUILD_LOG="/tmp/phase143_p2_loop_true_if_cb_llvm_build.log" + +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase143_p2_loop_true_if_cb_llvm_exe: exit code matches (9)" +else + exit 1 +fi + diff --git a/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_cb_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_cb_vm.sh new file mode 100644 index 00000000..80de6fa7 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase143_p2_loop_true_if_cb_vm.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Phase 143 P2: loop(true) if continue-break (C-B, Normalized shadow, VM) +# +# Verifies Phase 143 P2 else symmetry: +# - loop(true) { if flag==1 {continue} else {break} } return 9 → exit code 9 +# - Dev-only: NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +PASS_COUNT=0 +FAIL_COUNT=0 +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +echo "[INFO] Phase 143 P2: loop(true) if continue-break (C-B, VM)" + +echo "[INFO] Test 1: phase143_p2_loop_true_if_cb_min.hako" +INPUT="$NYASH_ROOT/apps/tests/phase143_p2_loop_true_if_cb_min.hako" + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \ + NYASH_DISABLE_PLUGINS=1 \ + HAKO_JOINIR_STRICT=1 \ + NYASH_JOINIR_DEV=1 \ + "$NYASH_BIN" --backend vm "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + FAIL_COUNT=$((FAIL_COUNT + 1)) +elif [ "$EXIT_CODE" -eq 9 ]; then + echo "[PASS] exit code verified: 9" + PASS_COUNT=$((PASS_COUNT + 1)) +else + echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 9)" + echo "[INFO] output (tail):" + echo "$OUTPUT" | tail -n 50 || true + FAIL_COUNT=$((FAIL_COUNT + 1)) +fi + +echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT" + +if [ "$FAIL_COUNT" -eq 0 ]; then + test_pass "phase143_p2_loop_true_if_cb_vm: All tests passed" + exit 0 +else + test_fail "phase143_p2_loop_true_if_cb_vm: $FAIL_COUNT test(s) failed" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase145_p1_anf_length_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase145_p1_anf_length_llvm_exe.sh new file mode 100644 index 00000000..90527f32 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase145_p1_anf_length_llvm_exe.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Phase 145 P1: ANF String.length() hoist (LLVM EXE parity) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +llvm_exe_preflight_or_skip || exit 0 + +# Minimal plugins (String + Integer for s.length() and arithmetic) +STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so" +INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so" +LLVM_REQUIRED_PLUGINS=( + "StringBox|$STRINGBOX_SO|nyash-string-plugin" + "IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin" +) +LLVM_PLUGIN_BUILD_LOG="/tmp/phase145_p1_anf_length_llvm_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase145_p1_anf_length_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase145_p1_anf_length_llvm_exe" + +# Expected exit code: 12 (5 + 3 + 4) +EXPECTED_EXIT_CODE=12 +LLVM_BUILD_LOG="/tmp/phase145_p1_anf_length_build.log" + +# ANF transformation enabled during compilation +export HAKO_ANF_DEV=1 + +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase145_p1_anf_length_llvm_exe: ANF transformation verified (exit 12)" +else + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase145_p1_anf_length_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase145_p1_anf_length_vm.sh new file mode 100644 index 00000000..0adbaf11 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase145_p1_anf_length_vm.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Phase 145 P1: ANF String.length() hoist (VM) +# +# Verifies ANF transformation: x + s.length() → t = s.length(); result = x + t +# Expected exit code: 12 (5 + 3 + 4) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +PASS_COUNT=0 +FAIL_COUNT=0 +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +INPUT="$NYASH_ROOT/apps/tests/phase145_p1_anf_length_min.hako" + +echo "[INFO] Phase 145 P1: ANF length() hoist (VM) - $INPUT" + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \ + HAKO_ANF_DEV=1 \ + NYASH_DISABLE_PLUGINS=1 \ + "$NYASH_BIN" --backend vm "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + FAIL_COUNT=$((FAIL_COUNT + 1)) +elif [ "$EXIT_CODE" -eq 12 ]; then + echo "[PASS] Exit code verified: 12 (5 + 3 + 4)" + PASS_COUNT=$((PASS_COUNT + 1)) +else + echo "[FAIL] Expected exit code 12, got $EXIT_CODE" + echo "[INFO] output (tail):" + echo "$OUTPUT" | tail -n 50 || true + FAIL_COUNT=$((FAIL_COUNT + 1)) +fi + +echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT" + +if [ "$FAIL_COUNT" -eq 0 ]; then + test_pass "phase145_p1_anf_length_vm: ANF transformation verified (exit 12)" + exit 0 +else + test_fail "phase145_p1_anf_length_vm: $FAIL_COUNT test(s) failed" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_binop_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_binop_llvm_exe.sh new file mode 100644 index 00000000..4fa37659 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_binop_llvm_exe.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Phase 145 P2: Recursive ANF with nested BinaryOp + MethodCall (LLVM EXE parity) +# +# Pattern: x + s.length() + z → exit code 18 (10 + 5 + 3) +# Dev-only: HAKO_ANF_DEV=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +# Preflight check (SKIP gate) +llvm_exe_preflight_or_skip || exit 0 + +# Minimal plugins (String + Integer for s.length() and arithmetic) +STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so" +INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so" +LLVM_REQUIRED_PLUGINS=( + "StringBox|$STRINGBOX_SO|nyash-string-plugin" + "IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin" +) +LLVM_PLUGIN_BUILD_LOG="/tmp/phase145_p2_compound_expr_binop_llvm_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase145_p2_compound_expr_binop_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase145_p2_compound_expr_binop_llvm_exe" + +EXPECTED_EXIT_CODE=18 +LLVM_BUILD_LOG="/tmp/phase145_p2_compound_expr_binop_llvm_build.log" + +export HAKO_ANF_DEV=1 + +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase145_p2_compound_expr_binop_llvm_exe: exit code matches (18)" +else + exit 1 +fi + diff --git a/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_binop_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_binop_vm.sh new file mode 100644 index 00000000..55b0c005 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_binop_vm.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Phase 145 P2: Recursive ANF with nested BinaryOp + MethodCall (VM) +# +# Pattern: x + s.length() + z → exit code 18 (10 + 5 + 3) +# Dev-only: HAKO_ANF_DEV=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +PASS_COUNT=0 +FAIL_COUNT=0 +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +echo "[INFO] Phase 145 P2: Recursive ANF - nested BinaryOp + MethodCall (VM)" + +echo "[INFO] Test 1: phase145_p2_compound_expr_binop_min.hako" +INPUT="$NYASH_ROOT/apps/tests/phase145_p2_compound_expr_binop_min.hako" + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \ + NYASH_DISABLE_PLUGINS=1 \ + HAKO_ANF_DEV=1 \ + "$NYASH_BIN" --backend vm "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + FAIL_COUNT=$((FAIL_COUNT + 1)) +elif [ "$EXIT_CODE" -eq 18 ]; then + echo "[PASS] exit code verified: 18" + PASS_COUNT=$((PASS_COUNT + 1)) +else + echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 18)" + echo "[INFO] output (tail):" + echo "$OUTPUT" | tail -n 50 || true + FAIL_COUNT=$((FAIL_COUNT + 1)) +fi + +echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT" + +if [ "$FAIL_COUNT" -eq 0 ]; then + test_pass "phase145_p2_compound_expr_binop_vm: All tests passed" + exit 0 +else + test_fail "phase145_p2_compound_expr_binop_vm: $FAIL_COUNT test(s) failed" + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_double_intrinsic_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_double_intrinsic_llvm_exe.sh new file mode 100644 index 00000000..010f80a9 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_double_intrinsic_llvm_exe.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Phase 145 P2: Recursive ANF with two MethodCall operands (LLVM EXE parity) +# +# Pattern: s1.length() + s2.length() → exit code 5 (2 + 3) +# Dev-only: HAKO_ANF_DEV=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +source "$(dirname "$0")/../../../lib/llvm_exe_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +# Preflight check (SKIP gate) +llvm_exe_preflight_or_skip || exit 0 + +# Minimal plugins (String + Integer for length() and addition) +STRINGBOX_SO="$NYASH_ROOT/plugins/nyash-string-plugin/libnyash_string_plugin.so" +INTEGERBOX_SO="$NYASH_ROOT/plugins/nyash-integer-plugin/libnyash_integer_plugin.so" +LLVM_REQUIRED_PLUGINS=( + "StringBox|$STRINGBOX_SO|nyash-string-plugin" + "IntegerBox|$INTEGERBOX_SO|nyash-integer-plugin" +) +LLVM_PLUGIN_BUILD_LOG="/tmp/phase145_p2_compound_expr_double_intrinsic_llvm_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase145_p2_compound_expr_double_intrinsic_llvm_exe" + +EXPECTED_EXIT_CODE=5 +LLVM_BUILD_LOG="/tmp/phase145_p2_compound_expr_double_intrinsic_llvm_build.log" + +export HAKO_ANF_DEV=1 + +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase145_p2_compound_expr_double_intrinsic_llvm_exe: exit code matches (5)" +else + exit 1 +fi + diff --git a/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_double_intrinsic_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_double_intrinsic_vm.sh new file mode 100644 index 00000000..17ed923d --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase145_p2_compound_expr_double_intrinsic_vm.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Phase 145 P2: Recursive ANF with double MethodCall (VM) +# +# Pattern: s1.length() + s2.length() → exit code 5 (2 + 3) +# Dev-only: HAKO_ANF_DEV=1 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +PASS_COUNT=0 +FAIL_COUNT=0 +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +echo "[INFO] Phase 145 P2: Recursive ANF - double MethodCall (VM)" + +echo "[INFO] Test 1: phase145_p2_compound_expr_double_intrinsic_min.hako" +INPUT="$NYASH_ROOT/apps/tests/phase145_p2_compound_expr_double_intrinsic_min.hako" + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env \ + NYASH_DISABLE_PLUGINS=1 \ + HAKO_ANF_DEV=1 \ + "$NYASH_BIN" --backend vm "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + echo "[FAIL] hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + FAIL_COUNT=$((FAIL_COUNT + 1)) +elif [ "$EXIT_CODE" -eq 5 ]; then + echo "[PASS] exit code verified: 5" + PASS_COUNT=$((PASS_COUNT + 1)) +else + echo "[FAIL] hakorune failed with exit code $EXIT_CODE (expected 5)" + echo "[INFO] output (tail):" + echo "$OUTPUT" | tail -n 50 || true + FAIL_COUNT=$((FAIL_COUNT + 1)) +fi + +echo "[INFO] PASS: $PASS_COUNT, FAIL: $FAIL_COUNT" + +if [ "$FAIL_COUNT" -eq 0 ]; then + test_pass "phase145_p2_compound_expr_double_intrinsic_vm: All tests passed" + exit 0 +else + test_fail "phase145_p2_compound_expr_double_intrinsic_vm: $FAIL_COUNT test(s) failed" + exit 1 +fi