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:
@ -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+)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user