diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index fa6705be..efb4026e 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -17,6 +17,19 @@ Scope: Repo root の旧リンク互換。現行の入口は `docs/development/cu ### 直近の道筋(JoinIR / Normalized) +### 設計方針メモ(SSOT候補) + +- ExprLowererBox(式SSOT) + - 役割: `AST(expr)` → `(prelude: Vec, value: ValueId)`(ANF含む) + - pure/impure/whitelist/strict の契約を集約(入口SSOT) +- ConditionLowererBox(条件→分岐SSOT) + - 役割: `AST(cond)` → `BranchPlan`(短絡なら分岐語彙で組む) + - 評価順は ExprLowererBox に委譲(ANFで順序固定) + - `&&/||` は制御として扱い、式で無理しない +- ControlLowererBox(制御SSOT) + - 役割: `StepNode/ControlTree` → JoinIR(継続 + env) + - `if/loop` はここ、条件の中身は ConditionLowererBox に委譲 + - Phase 139: if-only `post_k` の return lowering を `ReturnValueLowererBox` に統一(DONE) - `docs/development/current/main/phases/phase-139/README.md` - Phase 140: `NormalizedExprLowererBox` 初版(pure expression のみ)(DONE) @@ -40,6 +53,8 @@ Scope: Repo root の旧リンク互換。現行の入口は `docs/development/cu - `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` +- Phase 146(in progress): Loop/If 条件式へ ANF を横展開(順序固定と診断) + - `docs/development/current/main/phases/phase-146/README.md` ## Resolved (historical) diff --git a/apps/tests/phase146_p0_if_cond_unified_min.hako b/apps/tests/phase146_p0_if_cond_unified_min.hako new file mode 100644 index 00000000..63825eca --- /dev/null +++ b/apps/tests/phase146_p0_if_cond_unified_min.hako @@ -0,0 +1,14 @@ +// Phase 146 P0: If condition unified lowering (SSOT test) +static box Main { + main() { + local x + x = 5 + + // Pure condition (Phase 129 baseline via lower_expr_with_scope) + if x == 5 { + return 7 + } + + return 9 + } +} diff --git a/apps/tests/phase146_p1_if_cond_intrinsic_min.hako b/apps/tests/phase146_p1_if_cond_intrinsic_min.hako new file mode 100644 index 00000000..6312e1f6 --- /dev/null +++ b/apps/tests/phase146_p1_if_cond_intrinsic_min.hako @@ -0,0 +1,15 @@ +// Phase 146 P1: If condition with whitelisted intrinsic (String.length()) +static box Main { + main() { + local s + s = "abc" + + // Compound condition: s.length() == 3 + // ANF: t = s.length(); if (t == 3) + if s.length() == 3 { + return 7 + } else { + return 9 + } + } +} diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index d197dd85..a53a102f 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -7,6 +7,16 @@ - Phase 146-147(planned): Loop/If condition への ANF 適用(順序固定と診断の横展開) - 詳細: `docs/development/current/main/30-Backlog.md` +## 2025-12-19:Phase 146(着手)✅ + +- Phase 146 README: `docs/development/current/main/phases/phase-146/README.md` +- Fixtures: + - `apps/tests/phase146_p0_if_cond_unified_min.hako`(P0: pure cond, expected exit 7) + - `apps/tests/phase146_p1_if_cond_intrinsic_min.hako`(P1 planned: `s.length() == 3`) +- Smokes: + - `tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_vm.sh` + - `tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_llvm_exe.sh` + ## 2025-12-19:Phase 145-anf P0/P1/P2 完了 ✅ - SSOT docs: diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index a6d53f34..35e5bb3a 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -8,6 +8,11 @@ Related: ## 直近(JoinIR/selfhost) +- **収束方針(SSOT案): Expr/Condition/Control の 3 箱分割** + - ExprLowererBox(式SSOT): `AST(expr)` → `(prelude, value)`(ANF含む)。pure/impure/whitelist/strict を集約(入口SSOT)。 + - ConditionLowererBox(条件→分岐SSOT): `AST(cond)` → `BranchPlan`。評価順は ExprLowererBox に委譲し、`&&/||` は制御語彙で扱う。 + - ControlLowererBox(制御SSOT): `StepNode/ControlTree` → JoinIR(継続 + env)。`if/loop` を担当し、条件は ConditionLowererBox に委譲。 + - **Phase 141 P2+(planned): Call/MethodCall(effects + typing を分離して段階投入)** - ねらい: pure/impure 境界を壊さずに、impure lowering を段階投入する。 - 前提(DONE): diff --git a/docs/development/current/main/phases/phase-146/README.md b/docs/development/current/main/phases/phase-146/README.md new file mode 100644 index 00000000..bd70d034 --- /dev/null +++ b/docs/development/current/main/phases/phase-146/README.md @@ -0,0 +1,96 @@ +# Phase 146: Loop/If Condition ANF Implementation + +**Status**: P0 in progress / P1 planned +**Date**: 2025-12-19 +**Context**: Phase 145 P0/P1/P2 complete (ANF infrastructure). Phase 146/147 adds condition expression support. + +## Overview + +Phase 146 enables ANF (A-Normal Form) transformation for loop and if conditions, extending Phase 145's compound expression support to control flow conditions. + +## Phase 146 P0: ANF Routing SSOT 統一 + +**Goal**: Add scope check to ANF routing, unify SSOT, remove legacy inline lowering. + +### Implementation + +1. **expr_lowerer_box.rs**: Added scope check to ANF routing (L54-79) + - `PureOnly` scope: Skip ANF (P1 will enable) + - `WithImpure` scope: Try ANF (Phase 145 behavior) + +2. **post_if_post_k.rs**: Replaced legacy inline lowering with SSOT (L271-285) + - Use `NormalizedExprLowererBox::lower_expr_with_scope()` first + - Fallback to `lower_condition_legacy()` helper if needed + - Added helper function `lower_condition_legacy()` (L379-413) + +3. **contract.rs**: Already has `CondLoweringFailed` variant (L84) + +### Files Modified (3 files) + +- `src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs` (+10 lines) +- `src/mir/control_tree/normalized_shadow/post_if_post_k.rs` (+44 lines, -25 lines legacy) +- `src/mir/control_tree/normalized_shadow/anf/contract.rs` (no change, already has variant) + +### Files Created (4 files) + +- `apps/tests/phase146_p0_if_cond_unified_min.hako` (exit code 7) +- `tools/smokes/.../phase146_p0_if_cond_unified_vm.sh` +- `tools/smokes/.../phase146_p0_if_cond_unified_llvm_exe.sh` +- `docs/development/current/main/phases/phase-146/README.md` (this file) + +### Acceptance Criteria (P0) + +- [x] Scope check added to ANF routing +- [x] Legacy inline lowering removed from post_if_post_k.rs +- [x] SSOT unified (lower_expr_with_scope is only entry point) +- [ ] Build passes (cargo build --release) +- [ ] Tests pass (cargo test --release --lib) +- [ ] Phase 145 regression: 0 failures +- [ ] Fixture exit code: 7 (VM + LLVM EXE) + +## Phase 146 P1: 条件式 ANF 有効化(planned) + +**Goal**: Enable ANF in conditions for `PureOnly` scope behind a dev flag, starting with whitelisted intrinsic (`String.length()`). + +### Planned Implementation + +1. **expr_lowerer_box.rs**: Allow ANF for PureOnly with env flag +2. **anf/execute_box.rs**: Add Compare operator support +3. **config/env/joinir_dev.rs**: Add `anf_allow_pure_enabled()` function + +## Phase 147 P0: 複合条件の順序固定(planned) + +**Goal**: Extend recursive ANF to Compare operators for compound conditions. + +**Status**: Not yet implemented + +### Planned Implementation + +1. **anf/plan_box.rs**: Add Compare case to plan_expr() + +## Testing + +### P0 Smoke Tests + +```bash +# VM +./tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_vm.sh +# Expected: exit 7 + +# LLVM EXE +./tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_llvm_exe.sh +# Expected: exit 7 +``` + +### Regression Tests + +Phase 145 smokes must still pass: +- `phase145_p1_anf_length_min` → exit 12 +- `phase145_p2_compound_expr_binop_min` → exit 18 +- `phase145_p2_compound_expr_double_intrinsic_min` → exit 5 + +## References + +- **Plan File**: `/home/tomoaki/.claude/plans/buzzing-strolling-volcano.md` +- **Phase 145**: `docs/development/current/main/phases/phase-145-anf/README.md` +- **ANF Contract**: `docs/development/current/main/phases/phase-144-anf/INSTRUCTIONS.md` diff --git a/src/config/env/joinir_dev.rs b/src/config/env/joinir_dev.rs index 0bcd2820..cfd3e952 100644 --- a/src/config/env/joinir_dev.rs +++ b/src/config/env/joinir_dev.rs @@ -266,3 +266,32 @@ pub fn anf_dev_enabled() -> bool { pub fn anf_strict_enabled() -> bool { env_bool("HAKO_ANF_STRICT") } + +/// Phase 146 P1: HAKO_ANF_ALLOW_PURE=1 - Allow ANF in PureOnly scope (dev-only) +/// +/// Enables ANF transformation for PureOnly expression scopes (e.g., loop/if conditions). +/// Requires HAKO_ANF_DEV=1 to be set as well. +/// +/// # Environment Variable +/// +/// `HAKO_ANF_ALLOW_PURE=1` enables PureOnly ANF routing (default: OFF) +/// +/// # Behavior +/// +/// - **OFF (P0)**: ANF only for WithImpure scope (compound assignments) +/// - **ON (P1)**: ANF also for PureOnly scope (loop/if conditions with Compare) +/// +/// # Use Cases +/// +/// - **P1**: Enable `if (s.length() == 3)` ANF transformation +/// - **P147**: Enable `if (s1.length() < s2.length())` compound condition ANF +/// +/// # Example +/// +/// ```bash +/// # Enable condition ANF +/// HAKO_ANF_DEV=1 HAKO_ANF_ALLOW_PURE=1 ./hakorune program.hako +/// ``` +pub fn anf_allow_pure_enabled() -> bool { + env_bool("HAKO_ANF_ALLOW_PURE") +} diff --git a/src/mir/control_tree/normalized_shadow/anf/contract.rs b/src/mir/control_tree/normalized_shadow/anf/contract.rs index bf154179..ea8ea929 100644 --- a/src/mir/control_tree/normalized_shadow/anf/contract.rs +++ b/src/mir/control_tree/normalized_shadow/anf/contract.rs @@ -110,6 +110,8 @@ pub enum HoistPosition { pub enum AnfParentKind { /// Parent is BinaryOp (e.g., `x + s.length()`) BinaryOp, + /// Phase 146 P1: Parent is Compare (e.g., `s.length() == 3`) + Compare, /// Parent is UnaryOp (e.g., `not s.isEmpty()`) (P2+) UnaryOp, /// Parent is MethodCall (chained, e.g., `s.trim().length()`) (P2+) diff --git a/src/mir/control_tree/normalized_shadow/anf/execute_box.rs b/src/mir/control_tree/normalized_shadow/anf/execute_box.rs index ebd11298..4f9cc4b3 100644 --- a/src/mir/control_tree/normalized_shadow/anf/execute_box.rs +++ b/src/mir/control_tree/normalized_shadow/anf/execute_box.rs @@ -19,7 +19,7 @@ use super::contract::{AnfPlan, AnfParentKind}; use crate::ast::ASTNode; -use crate::mir::join_ir::{JoinInst, MirLikeInst}; +use crate::mir::join_ir::{CompareOp, JoinInst, MirLikeInst}; use crate::mir::types::MirType; use crate::mir::ValueId; use std::collections::BTreeMap; @@ -67,11 +67,15 @@ impl AnfExecuteBox { return Ok(None); } - // P1: Only BinaryOp is supported + // Phase 146 P1: BinaryOp and Compare supported match plan.parent_kind { AnfParentKind::BinaryOp => { Self::execute_binary_op_hoist(plan, ast, env, body, next_value_id) } + AnfParentKind::Compare => { + // Phase 146 P1: Route Compare to execute_compare_hoist + Self::execute_compare_hoist(plan, ast, env, body, next_value_id) + } _ => Ok(None), // P2+: UnaryOp/MethodCall/Call } } @@ -193,9 +197,17 @@ impl AnfExecuteBox { } // Recursive case: BinaryOp (normalize operands recursively) + // Phase 146 P1: Handle both arithmetic and comparison operators 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()) + if Self::is_compare_operator(operator) { + // Phase 146 P1: Comparison operator → emit Compare instruction + let result_vid = Self::execute_compare_recursive(left, right, operator, env, body, next_value_id)?; + result_vid.ok_or_else(|| "normalize_and_lower: Compare returned None".to_string()) + } else { + // Arithmetic operator → emit BinOp instruction + 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. @@ -275,6 +287,113 @@ impl AnfExecuteBox { Ok(dst) } + /// Phase 146 P1: Execute ANF transformation for Compare with recursive normalization + /// + /// Pattern: `s.length() == 3` → `t = s.length(); result = t == 3` + /// Pattern: `s1.length() < s2.length()` → `t1 = s1.length(); t2 = s2.length(); result = t1 < t2` + /// + /// This function recursively normalizes left and right operands (depth-first, left-to-right) + /// and then generates a pure Compare instruction. + fn execute_compare_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_compare_hoist: expected BinaryOp AST node".to_string()); + }; + + // Verify it's a comparison operator + if !Self::is_compare_operator(operator) { + return Err(format!("ANF execute_compare_hoist: expected comparison operator, got {:?}", operator)); + } + + // Use recursive normalization (same pattern as BinaryOp) + Self::execute_compare_recursive(left, right, operator, env, body, next_value_id) + } + + /// Phase 146 P1: Recursively normalize Compare operands (depth-first, left-to-right) + /// + /// This is the core recursive ANF transformation for Compare: + /// 1. Normalize LEFT operand recursively (depth-first) + /// 2. Normalize RIGHT operand recursively (left-to-right) + /// 3. Generate pure Compare instruction + /// + /// # Example + /// + /// Input: `s.length() == 3` + /// - Step 1: Normalize `s.length()` → ValueId(1) (emits MethodCall) + /// - Step 2: Normalize `3` → ValueId(2) (emits Const) + /// - Step 3: Emit Compare(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 Compare(ValueId(1), <, ValueId(2)) → ValueId(3) + fn execute_compare_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 Compare instruction + let dst = Self::alloc_value_id(next_value_id); + + // Convert AST BinaryOperator (comparison) to JoinIR CompareOp + let joinir_op = Self::ast_compare_to_joinir(operator)?; + + body.push(JoinInst::Compute(MirLikeInst::Compare { + dst, + op: joinir_op, + lhs: lhs_vid, + rhs: rhs_vid, + })); + + if crate::config::env::anf_dev_enabled() { + eprintln!("[phase146/p1] Emitted Compare: ValueId({}) = ValueId({}) {:?} ValueId({})", + dst.as_u32(), lhs_vid.as_u32(), joinir_op, rhs_vid.as_u32()); + } + + Ok(Some(dst)) + } + + /// Phase 146 P1: Check if BinaryOperator is a comparison operator + fn is_compare_operator(op: &crate::ast::BinaryOperator) -> bool { + use crate::ast::BinaryOperator; + matches!(op, + BinaryOperator::Equal | + BinaryOperator::NotEqual | + BinaryOperator::Less | + BinaryOperator::Greater | + BinaryOperator::LessEqual | + BinaryOperator::GreaterEqual + ) + } + + /// Phase 146 P1: Convert AST BinaryOperator (comparison) to JoinIR CompareOp + fn ast_compare_to_joinir(op: &crate::ast::BinaryOperator) -> Result { + use crate::ast::BinaryOperator; + Ok(match op { + BinaryOperator::Equal => CompareOp::Eq, + BinaryOperator::NotEqual => CompareOp::Ne, + BinaryOperator::Less => CompareOp::Lt, + BinaryOperator::LessEqual => CompareOp::Le, + BinaryOperator::Greater => CompareOp::Gt, + BinaryOperator::GreaterEqual => CompareOp::Ge, + _ => return Err(format!("ast_compare_to_joinir: not a comparison operator: {:?}", op)), + }) + } + /// Allocate a new ValueId fn alloc_value_id(next_value_id: &mut u32) -> ValueId { let id = *next_value_id; diff --git a/src/mir/control_tree/normalized_shadow/anf/plan_box.rs b/src/mir/control_tree/normalized_shadow/anf/plan_box.rs index 81ce45bf..e36e0df9 100644 --- a/src/mir/control_tree/normalized_shadow/anf/plan_box.rs +++ b/src/mir/control_tree/normalized_shadow/anf/plan_box.rs @@ -64,6 +64,20 @@ impl AnfPlanBox { _ => None, // Nested MethodCall (e.g., s.trim().length()) is P2+ } } + + /// Phase 146 P1: Check if BinaryOperator is a comparison operator + fn is_compare_operator(op: &crate::ast::BinaryOperator) -> bool { + use crate::ast::BinaryOperator; + matches!(op, + BinaryOperator::Equal | + BinaryOperator::NotEqual | + BinaryOperator::Less | + BinaryOperator::Greater | + BinaryOperator::LessEqual | + BinaryOperator::GreaterEqual + ) + } + /// Plan ANF transformation for an expression /// /// Walks AST to detect impure subexpressions (Call/MethodCall) and builds AnfPlan. @@ -104,7 +118,8 @@ impl AnfPlanBox { } // Binary: Check left and right recursively - ASTNode::BinaryOp { left, right, .. } => { + // Phase 146 P1: Handle both arithmetic and comparison operators + ASTNode::BinaryOp { operator, left, right, .. } => { // Phase 145 P1: Detect whitelisted MethodCall in operands let mut hoist_targets = vec![]; @@ -130,9 +145,16 @@ impl AnfPlanBox { } } + // Phase 146 P1: Determine parent kind (Compare vs BinaryOp) + let parent_kind = if Self::is_compare_operator(operator) { + AnfParentKind::Compare + } else { + AnfParentKind::BinaryOp + }; + // 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))); + return Ok(Some(AnfPlan::with_hoists(hoist_targets, parent_kind))); } // P0 fallback: Recursively check operands for pure/impure @@ -153,7 +175,7 @@ impl AnfPlanBox { requires_anf, impure_count: combined_impure_count, hoist_targets: vec![], - parent_kind: AnfParentKind::BinaryOp, + parent_kind, })) } 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 d5ef8b91..8b49751f 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,26 +51,34 @@ impl NormalizedExprLowererBox { body: &mut Vec, next_value_id: &mut u32, ) -> Result, String> { - // Phase 145 P0: ANF routing (dev-only) + // Phase 146 P1: ANF routing (dev-only, scope-aware) 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)"); + // P1: Allow ANF for PureOnly if HAKO_ANF_ALLOW_PURE=1 + let should_try_anf = match scope { + ExprLoweringScope::WithImpure(_) => true, + ExprLoweringScope::PureOnly => crate::config::env::anf_allow_pure_enabled(), + }; + + if should_try_anf { + 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 + Ok(None) => { + // Out-of-scope for ANF, continue with legacy lowering + } + Err(_reason) => { + // Explicitly out-of-scope (ContainsCall/ContainsMethodCall), continue + } } } } diff --git a/src/mir/control_tree/normalized_shadow/post_if_post_k.rs b/src/mir/control_tree/normalized_shadow/post_if_post_k.rs index 32349cdb..96898cc5 100644 --- a/src/mir/control_tree/normalized_shadow/post_if_post_k.rs +++ b/src/mir/control_tree/normalized_shadow/post_if_post_k.rs @@ -28,13 +28,18 @@ //! - Out of scope → Ok(None) (fallback to legacy) //! - In scope but conversion failed → Err (with freeze_with_hint in strict mode) +use std::collections::BTreeMap; + use super::env_layout::EnvLayout; use super::legacy::LegacyLowerer; use super::common::return_value_lowerer_box::ReturnValueLowererBox; use super::common::normalized_helpers::NormalizedHelperBox; +use super::common::expr_lowerer_box::NormalizedExprLowererBox; +use super::common::expr_lowering_contract::ExprLoweringScope; use crate::mir::control_tree::step_tree::{StepNode, StepStmtKind, StepTree}; use crate::mir::join_ir::lowering::carrier_info::JoinFragmentMeta; use crate::mir::join_ir::lowering::error_tags; +use crate::mir::ValueId; use crate::mir::join_ir::{ConstValue, JoinFunction, JoinFuncId, JoinInst, JoinModule, MirLikeInst}; /// Box-First: Post-if continuation lowering with post_k @@ -267,29 +272,21 @@ impl PostIfPostKBuilderBox { post_k_func.body.push(JoinInst::Ret { value: None }); } - // main: cond compare + conditional jump to k_then/k_else - let (lhs_var, op, rhs_literal) = LegacyLowerer::parse_minimal_compare(&cond_ast.0)?; - let lhs_vid = env_main.get(&lhs_var).copied().ok_or_else(|| { - error_tags::freeze_with_hint( - "phase129/post_k/cond_lhs_missing", - &format!("condition lhs var '{lhs_var}' not found in env"), - "ensure the if condition uses a variable from writes or captured inputs", - ) - })?; - - let rhs_vid = NormalizedHelperBox::alloc_value_id(&mut next_value_id); - main_func.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: rhs_vid, - value: ConstValue::Integer(rhs_literal), - })); - - let cond_vid = NormalizedHelperBox::alloc_value_id(&mut next_value_id); - main_func.body.push(JoinInst::Compute(MirLikeInst::Compare { - dst: cond_vid, - op, - lhs: lhs_vid, - rhs: rhs_vid, - })); + // Phase 146 P0: Use lower_expr_with_scope() SSOT (legacy fallback) + let cond_vid = match NormalizedExprLowererBox::lower_expr_with_scope( + ExprLoweringScope::PureOnly, + &cond_ast.0, + &env_main, + &mut main_func.body, + &mut next_value_id, + ) { + Ok(Some(vid)) => vid, + Ok(None) => { + // Fallback to legacy minimal compare (Phase 129 baseline) + Self::lower_condition_legacy(&cond_ast.0, &env_main, &mut main_func.body, &mut next_value_id)? + } + Err(e) => return Err(format!("phase146/p0/cond_lowering: {}", e)), + }; let main_args = NormalizedHelperBox::collect_env_args(&env_fields, &env_main) .map_err(|e| error_tags::freeze_with_hint( @@ -379,4 +376,40 @@ impl PostIfPostKBuilderBox { _ => Ok(Vec::new()), // Unsupported } } + + /// Phase 146 P0: Legacy condition lowering (fallback for out-of-scope cases) + /// + /// When ANF routing is unavailable (e.g., PureOnly scope, HAKO_ANF_DEV=0), + /// fall back to Phase 129 baseline minimal compare lowering. + fn lower_condition_legacy( + cond_ast: &crate::ast::ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result { + let (lhs_var, op, rhs_literal) = LegacyLowerer::parse_minimal_compare(cond_ast)?; + let lhs_vid = env.get(&lhs_var).copied().ok_or_else(|| { + error_tags::freeze_with_hint( + "phase146/p0/cond_lhs_missing", + &format!("condition lhs '{lhs_var}' not in env"), + "ensure variable exists in writes or inputs", + ) + })?; + + let rhs_vid = NormalizedHelperBox::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::Const { + dst: rhs_vid, + value: ConstValue::Integer(rhs_literal), + })); + + let cond_vid = NormalizedHelperBox::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cond_vid, + op, + lhs: lhs_vid, + rhs: rhs_vid, + })); + + Ok(cond_vid) + } } diff --git a/tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_llvm_exe.sh new file mode 100644 index 00000000..d825468b --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_llvm_exe.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Phase 146 P0: If condition unified lowering (LLVM EXE parity) +# +# Expected: exit code 7 + +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 (Integer 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/phase146_p0_if_cond_unified_llvm_plugin_build.log" +llvm_exe_ensure_plugins_or_fail || exit 1 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase146_p0_if_cond_unified_min.hako" +OUTPUT_EXE="$NYASH_ROOT/tmp/phase146_p0_if_cond_unified_llvm_exe" + +EXPECTED_EXIT_CODE=7 +LLVM_BUILD_LOG="/tmp/phase146_p0_if_cond_unified_llvm_build.log" + +if llvm_exe_build_and_run_expect_exit_code; then + test_pass "phase146_p0_if_cond_unified_llvm_exe: exit code matches (7)" +else + exit 1 +fi + diff --git a/tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_vm.sh new file mode 100644 index 00000000..d9de0b29 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase146_p0_if_cond_unified_vm.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Phase 146 P0: If condition unified lowering (VM) +# +# Expected: exit code 7 + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +INPUT_HAKO="$NYASH_ROOT/apps/tests/phase146_p0_if_cond_unified_min.hako" + +EXPECTED_EXIT_CODE=7 +"$NYASH_BIN" --backend vm "$INPUT_HAKO" >/dev/null 2>&1 +actual_exit=$? + +if [ "$actual_exit" -eq "$EXPECTED_EXIT_CODE" ]; then + test_pass "phase146_p0_if_cond_unified_vm: exit code matches (7)" +else + test_fail "phase146_p0_if_cond_unified_vm: expected 7, got $actual_exit" + exit 1 +fi + diff --git a/tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_llvm_exe.sh new file mode 100644 index 00000000..43c6fbdd --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_llvm_exe.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Phase 146 P1: If condition with whitelisted intrinsic (LLVM EXE) +set -euo pipefail + +HAKORUNE="${HAKORUNE:-./target/release/hakorune}" +TEST_FILE="apps/tests/phase146_p1_if_cond_intrinsic_min.hako" + +HAKO_ANF_DEV=1 HAKO_ANF_ALLOW_PURE=1 NYASH_LLVM_USE_HARNESS=1 "$HAKORUNE" --backend llvm "$TEST_FILE" diff --git a/tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_vm.sh new file mode 100644 index 00000000..d1a9c6a6 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase146_p1_if_cond_intrinsic_vm.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Phase 146 P1: If condition with whitelisted intrinsic (VM) +set -euo pipefail + +HAKORUNE="${HAKORUNE:-./target/release/hakorune}" +TEST_FILE="apps/tests/phase146_p1_if_cond_intrinsic_min.hako" + +HAKO_ANF_DEV=1 HAKO_ANF_ALLOW_PURE=1 "$HAKORUNE" --backend vm "$TEST_FILE"