feat(anf): Phase 146/147 - Loop/If Condition ANF with Compare support

## Phase 146 P0: ANF Routing SSOT Unified

**Goal**: Unify ANF routing in `lower_expr_with_scope()` L54-84, remove legacy lowering

**Changes**:
- expr_lowerer_box.rs: Added scope check (PureOnly → skip ANF, WithImpure → try ANF)
- post_if_post_k.rs: Removed legacy inline lowering (L271-285), added `lower_condition_legacy()` helper
- contract.rs: Already had `CondLoweringFailed` out-of-scope reason

**Test Results**:  Phase 146 P0 smoke (exit 7), 0 regressions

## Phase 146 P1: Compare Operator Support

**Goal**: Enable ANF for condition expressions with Compare operators

**Changes**:
- joinir_dev.rs: Added `anf_allow_pure_enabled()` (HAKO_ANF_ALLOW_PURE=1)
- expr_lowerer_box.rs: PureOnly scope ANF support (L56-66)
- execute_box.rs: Compare operator support (+122 lines)
  - `execute_compare_hoist()`, `execute_compare_recursive()`, `ast_compare_to_joinir()`
  - Extended `normalize_and_lower()` for Compare

**Test Results**:  Phase 146 P1 smoke (exit 7 with flags), 0 regressions

## Phase 147 P0: Recursive Comparison ANF

**Goal**: Extend recursive ANF to Compare operators

**Changes**:
- contract.rs: Added `AnfParentKind::Compare` variant
- plan_box.rs: Compare case in BinaryOp routing (L68-79, L134-139)
  - Distinguishes Compare vs arithmetic BinaryOp

**Benefits**: Enables recursive ANF for comparisons
- `s.length() == 3` → `t = s.length(); if (t == 3)` 
- `s1.length() < s2.length()` → `t1 = s1.length(); t2 = s2.length(); if (t1 < t2)` 

## Implementation Summary

**Files Modified** (9 files, +253 lines, -25 lines = +228 net):
1. src/config/env/joinir_dev.rs (+28 lines)
2. src/mir/control_tree/normalized_shadow/anf/contract.rs (+2 lines)
3. src/mir/control_tree/normalized_shadow/anf/execute_box.rs (+122 lines)
4. src/mir/control_tree/normalized_shadow/anf/plan_box.rs (+18 lines)
5. src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs (+18 lines, -0 lines)
6. src/mir/control_tree/normalized_shadow/post_if_post_k.rs (+44 lines, -25 lines)
7. CURRENT_TASK.md
8. docs/development/current/main/10-Now.md
9. docs/development/current/main/30-Backlog.md

**Files Created** (7 files):
- apps/tests/phase146_p0_if_cond_unified_min.hako
- apps/tests/phase146_p1_if_cond_intrinsic_min.hako
- tools/smokes/.../phase146_p0_if_cond_unified_vm.sh
- tools/smokes/.../phase146_p0_if_cond_unified_llvm_exe.sh
- tools/smokes/.../phase146_p1_if_cond_intrinsic_vm.sh
- tools/smokes/.../phase146_p1_if_cond_intrinsic_llvm_exe.sh
- docs/development/current/main/phases/phase-146/README.md

**Acceptance Criteria**:  All met
- cargo build --release: PASS (0 errors, 0 warnings)
- Phase 145 regressions: PASS (exit 12, 18, 5)
- Phase 146 P0: PASS (exit 7)
- Phase 146 P1: PASS (exit 7 with HAKO_ANF_ALLOW_PURE=1)

**Architecture**:
- SSOT: ANF routing only in `lower_expr_with_scope()` L54-84
- Box-First: Phase 145 `anf/` module extended
- Legacy removed: post_if_post_k.rs unified with SSOT

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-19 17:03:56 +09:00
parent 6a3b6deb20
commit 9336785680
16 changed files with 484 additions and 46 deletions

View File

@ -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+)

View File

@ -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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<CompareOp, String> {
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;

View File

@ -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,
}))
}

View File

@ -51,26 +51,34 @@ impl NormalizedExprLowererBox {
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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
}
}
}
}

View File

@ -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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<ValueId, String> {
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)
}
}