From ca91be349da50592b82b4742ccc3d48b731fe982 Mon Sep 17 00:00:00 2001 From: tomoaki Date: Sun, 28 Dec 2025 03:52:52 +0900 Subject: [PATCH] Refactor JoinIR lowerers and boundary --- .../control_flow/joinir/merge/coordinator.rs | 5 +- .../joinir/merge/exit_line/reconnector.rs | 9 - .../joinir/patterns/exit_binding.rs | 7 +- .../patterns/exit_binding_applicator.rs | 18 +- .../common/expr_lowerer_box.rs | 922 ------------------ .../common/expr_lowerer_box/binary.rs | 23 + .../common/expr_lowerer_box/intrinsics.rs | 59 ++ .../common/expr_lowerer_box/lowering.rs | 173 ++++ .../common/expr_lowerer_box/mod.rs | 137 +++ .../common/expr_lowerer_box/scope.rs | 301 ++++++ .../common/expr_lowerer_box/tests.rs | 412 ++++++++ src/mir/join_ir/lowering/inline_boundary.rs | 796 --------------- .../lowering/inline_boundary/constructors.rs | 135 +++ .../join_ir/lowering/inline_boundary/mod.rs | 28 + .../join_ir/lowering/inline_boundary/tests.rs | 50 + .../join_ir/lowering/inline_boundary/types.rs | 176 ++++ .../lowering/inline_boundary_builder.rs | 19 - .../lowering/loop_with_break_minimal.rs | 345 +------ .../body_local_init.rs | 83 ++ .../loop_with_break_minimal/carrier_update.rs | 198 ++++ .../header_break_lowering.rs | 73 +- .../loop_with_break_minimal/tail_builder.rs | 52 + .../carrier_updates.rs | 174 ++++ .../condition_lowering.rs | 86 ++ .../mod.rs} | 298 +----- .../loop_with_continue_minimal/validation.rs | 24 + 26 files changed, 2228 insertions(+), 2375 deletions(-) delete mode 100644 src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs create mode 100644 src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/binary.rs create mode 100644 src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/intrinsics.rs create mode 100644 src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/lowering.rs create mode 100644 src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/mod.rs create mode 100644 src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/scope.rs create mode 100644 src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/tests.rs delete mode 100644 src/mir/join_ir/lowering/inline_boundary.rs create mode 100644 src/mir/join_ir/lowering/inline_boundary/constructors.rs create mode 100644 src/mir/join_ir/lowering/inline_boundary/mod.rs create mode 100644 src/mir/join_ir/lowering/inline_boundary/tests.rs create mode 100644 src/mir/join_ir/lowering/inline_boundary/types.rs create mode 100644 src/mir/join_ir/lowering/loop_with_break_minimal/body_local_init.rs create mode 100644 src/mir/join_ir/lowering/loop_with_break_minimal/carrier_update.rs create mode 100644 src/mir/join_ir/lowering/loop_with_break_minimal/tail_builder.rs create mode 100644 src/mir/join_ir/lowering/loop_with_continue_minimal/carrier_updates.rs create mode 100644 src/mir/join_ir/lowering/loop_with_continue_minimal/condition_lowering.rs rename src/mir/join_ir/lowering/{loop_with_continue_minimal.rs => loop_with_continue_minimal/mod.rs} (61%) create mode 100644 src/mir/join_ir/lowering/loop_with_continue_minimal/validation.rs diff --git a/src/mir/builder/control_flow/joinir/merge/coordinator.rs b/src/mir/builder/control_flow/joinir/merge/coordinator.rs index b6b275ad..c7667722 100644 --- a/src/mir/builder/control_flow/joinir/merge/coordinator.rs +++ b/src/mir/builder/control_flow/joinir/merge/coordinator.rs @@ -49,9 +49,8 @@ use super::{ /// # Returns /// /// Returns `Ok(Some(exit_phi_id))` if the merged JoinIR functions have return values -/// that were collected into an exit block PHI. さらに、`boundary` に -/// host_outputs が指定されている場合は、exit PHI の結果をホスト側の -/// SSA スロットへ再接続する(variable_map 内の ValueId を更新する)。 +/// that were collected into an exit block PHI. Exit reconnection is driven by +/// explicit exit_bindings on the boundary (no legacy host_outputs). pub(in crate::mir::builder) fn merge_joinir_mir_blocks( builder: &mut crate::mir::builder::MirBuilder, mir_module: &MirModule, diff --git a/src/mir/builder/control_flow/joinir/merge/exit_line/reconnector.rs b/src/mir/builder/control_flow/joinir/merge/exit_line/reconnector.rs index a0164883..9b630758 100644 --- a/src/mir/builder/control_flow/joinir/merge/exit_line/reconnector.rs +++ b/src/mir/builder/control_flow/joinir/merge/exit_line/reconnector.rs @@ -234,15 +234,6 @@ impl ExitLineReconnector { } } - // Backward compatibility warning for deprecated host_outputs - #[allow(deprecated)] - if !boundary.host_outputs.is_empty() && debug { - trace.stderr_if( - "[joinir/exit-line] WARNING: Using deprecated host_outputs. Migrate to exit_bindings.", - true, - ); - } - // Phase 190-impl-D-3: Contract verification (debug build only) // Ensures all exit_bindings have corresponding entries in carrier_phis and variable_ctx.variable_map #[cfg(debug_assertions)] diff --git a/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs b/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs index cfdb597f..0801cf14 100644 --- a/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs +++ b/src/mir/builder/control_flow/joinir/patterns/exit_binding.rs @@ -95,7 +95,7 @@ impl<'a> ExitBindingBuilder<'a> { /// /// Phase 222.5-C: Delegates to applicator module. /// - /// Sets exit_bindings (and join_outputs for legacy) based on loop_var + carriers. + /// Sets exit_bindings based on loop_var + carriers. /// Must be called after build_loop_exit_bindings(). /// /// # Arguments @@ -374,11 +374,6 @@ mod tests { host_inputs: vec![], join_inputs: vec![], exit_bindings: vec![], // Phase 171: Add missing field - #[allow(deprecated)] - host_outputs: vec![], // legacy, unused in new assertions - join_outputs: vec![], - #[allow(deprecated)] - condition_inputs: vec![], // Phase 171: Add missing field condition_bindings: vec![], // Phase 171-fix: Add missing field expr_result: None, // Phase 33-14: Add missing field jump_args_layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly, diff --git a/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs b/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs index 0947ba5c..55a311b3 100644 --- a/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs +++ b/src/mir/builder/control_flow/joinir/patterns/exit_binding_applicator.rs @@ -10,7 +10,7 @@ use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for deter /// Apply bindings to JoinInlineBoundary /// -/// Sets exit_bindings (and join_outputs for legacy) based on loop_var + carriers. +/// Sets exit_bindings based on loop_var + carriers. /// Must be called after build_loop_exit_bindings(). /// /// Phase 222.5-C: Extracted from ExitBindingBuilder to separate application concerns. @@ -35,8 +35,6 @@ pub(crate) fn apply_exit_bindings_to_boundary( let mut bindings = Vec::new(); bindings.push(create_loop_var_exit_binding(carrier_info)); - let mut join_outputs = vec![carrier_info.loop_var_id]; // legacy field for compatibility - for carrier in &carrier_info.carriers { let post_loop_id = variable_map .get(&carrier.name) @@ -53,18 +51,9 @@ pub(crate) fn apply_exit_bindings_to_boundary( join_exit_value: join_exit_id, role: carrier.role, // Phase 227: Propagate role from CarrierInfo }); - - join_outputs.push(join_exit_id); } boundary.exit_bindings = bindings; - // Deprecated fields kept in sync for legacy consumers - let join_outputs_clone = join_outputs.clone(); - boundary.join_outputs = join_outputs; - #[allow(deprecated)] - { - boundary.host_outputs = join_outputs_clone; - } Ok(()) } @@ -128,11 +117,6 @@ mod tests { host_inputs: vec![], join_inputs: vec![], exit_bindings: vec![], // Phase 171: Add missing field - #[allow(deprecated)] - host_outputs: vec![], // legacy, unused in new assertions - join_outputs: vec![], - #[allow(deprecated)] - condition_inputs: vec![], // Phase 171: Add missing field condition_bindings: vec![], // Phase 171-fix: Add missing field expr_result: None, // Phase 33-14: Add missing field jump_args_layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly, 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 deleted file mode 100644 index 8e9153e6..00000000 --- a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box.rs +++ /dev/null @@ -1,922 +0,0 @@ -//! NormalizedExprLowererBox: Pure expression lowering SSOT (Phase 140 P0) -//! -//! ## Responsibility -//! -//! Lower a *pure* AST expression into JoinIR `ValueId` + emitted `JoinInst`s. -//! -//! ## Scope (Phase 140 P0) -//! -//! Supported (pure only): -//! - Variable: env lookup → ValueId -//! - Literals: Integer / Bool -//! - Unary: `-` (Neg), `not` (Not) -//! - Binary arith (int-only): `+ - * /` -//! - Compare (int-only): `== != < <= > >=` -//! -//! Out of scope (returns `Ok(None)`): -//! - Call/MethodCall/FromCall/Array/Map/FieldAccess/Index/New/This/Me/… -//! - Literals other than Integer/Bool -//! - Operators outside the scope above -//! -//! ## Contract -//! -//! - Out-of-scope returns `Ok(None)` (fallback to legacy routing). -//! - `Err(_)` is reserved for internal invariants only (should be rare). - -use super::expr_lowering_contract::{ExprLoweringScope, ImpurePolicy, KnownIntrinsic, OutOfScopeReason}; -use super::known_intrinsics::KnownIntrinsicRegistryBox; // Phase 141 P1.5 -use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator}; -use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, JoinInst, MirLikeInst, UnaryOp}; -use crate::mir::types::MirType; -use crate::mir::ValueId; -use std::collections::BTreeMap; - -/// Box-First: Pure expression lowering for Normalized shadow paths -pub struct NormalizedExprLowererBox; - -impl NormalizedExprLowererBox { - pub fn lower_expr( - ast: &ASTNode, - env: &BTreeMap, - body: &mut Vec, - next_value_id: &mut u32, - ) -> Result, String> { - Self::lower_expr_with_scope(ExprLoweringScope::PureOnly, ast, env, body, next_value_id) - } - - pub fn lower_expr_with_scope( - scope: ExprLoweringScope, - ast: &ASTNode, - env: &BTreeMap, - body: &mut Vec, - next_value_id: &mut u32, - ) -> Result, String> { - // Phase 146 P1: ANF routing (dev-only, scope-aware) - if crate::config::env::anf_dev_enabled() { - // 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 - } - } - } - } - - if Self::out_of_scope_reason(scope, ast, env).is_some() { - return Ok(None); - } - - match ast { - ASTNode::Variable { name, .. } => Ok(env.get(name).copied()), - ASTNode::Literal { value, .. } => Self::lower_literal(value, body, next_value_id), - ASTNode::UnaryOp { operator, operand, .. } => { - Self::lower_unary(operator, operand, env, body, next_value_id) - } - ASTNode::BinaryOp { operator, left, right, .. } => { - Self::lower_binary(operator, left, right, env, body, next_value_id) - } - ASTNode::MethodCall { object, method, arguments, .. } => match scope { - ExprLoweringScope::PureOnly => Ok(None), - ExprLoweringScope::WithImpure(policy) => { - let Some(intrinsic) = - Self::match_known_intrinsic_method_call(policy, object, method, arguments, env) - else { - return Ok(None); - }; - Self::lower_known_intrinsic_method_call(intrinsic, object, body, next_value_id, env) - } - }, - _ => Ok(None), - } - } - - /// Classify out-of-scope reasons for diagnostics/tests without changing the core API. - pub fn out_of_scope_reason( - scope: ExprLoweringScope, - ast: &ASTNode, - env: &BTreeMap, - ) -> Option { - match scope { - ExprLoweringScope::PureOnly => Self::out_of_scope_reason_pure(ast, env), - ExprLoweringScope::WithImpure(policy) => Self::out_of_scope_reason_with_impure(policy, ast, env), - } - } - - fn out_of_scope_reason_pure(ast: &ASTNode, env: &BTreeMap) -> Option { - match ast { - ASTNode::FunctionCall { .. } | ASTNode::Call { .. } => Some(OutOfScopeReason::Call), - ASTNode::MethodCall { .. } => Some(OutOfScopeReason::MethodCall), - - ASTNode::Variable { name, .. } => { - if env.contains_key(name) { - None - } else { - Some(OutOfScopeReason::MissingEnvVar) - } - } - - ASTNode::Literal { value, .. } => match value { - LiteralValue::Integer(_) | LiteralValue::Bool(_) => None, - _ => Some(OutOfScopeReason::UnsupportedLiteral), - }, - - ASTNode::UnaryOp { operator, operand, .. } => match operator { - UnaryOperator::Minus => { - // Only int operand is supported. - if Self::is_supported_int_expr(operand, env) { - None - } else { - Some(OutOfScopeReason::UnsupportedOperator) - } - } - UnaryOperator::Not => { - // Only bool operand is supported. - if Self::is_supported_bool_expr(operand, env) { - None - } else { - Some(OutOfScopeReason::UnsupportedOperator) - } - } - UnaryOperator::BitNot => Some(OutOfScopeReason::UnsupportedOperator), - UnaryOperator::Weak => Some(OutOfScopeReason::UnsupportedOperator), // Phase 285W-Syntax-0 - }, - - ASTNode::BinaryOp { operator, left, right, .. } => { - if Self::binary_kind(operator).is_some() { - // Phase 140 policy: binary ops require int operands (arith/compare). - if Self::is_supported_int_expr(left, env) && Self::is_supported_int_expr(right, env) { - None - } else { - Some(OutOfScopeReason::UnsupportedOperator) - } - } else { - Some(OutOfScopeReason::UnsupportedOperator) - } - } - - _ => Some(OutOfScopeReason::ImpureExpression), - } - } - - fn out_of_scope_reason_with_impure( - policy: ImpurePolicy, - ast: &ASTNode, - env: &BTreeMap, - ) -> Option { - match ast { - ASTNode::FunctionCall { .. } | ASTNode::Call { .. } => Some(OutOfScopeReason::Call), - ASTNode::MethodCall { object, method, arguments, .. } => { - // Phase 141 P1.5: Use registry for better diagnostics - if Self::match_known_intrinsic_method_call(policy, object, method, arguments, env).is_some() - { - None // In scope (whitelisted) - } else { - // Check if it's a known intrinsic but not whitelisted - let receiver_ok = matches!(object.as_ref(), ASTNode::Variable { name, .. } if env.contains_key(name)); - if receiver_ok && KnownIntrinsicRegistryBox::lookup(method, arguments.len()).is_some() { - // Known intrinsic signature but not in current policy allowlist - Some(OutOfScopeReason::IntrinsicNotWhitelisted) - } else { - // Generic method call not supported - Some(OutOfScopeReason::MethodCall) - } - } - } - - ASTNode::Variable { name, .. } => { - if env.contains_key(name) { - None - } else { - Some(OutOfScopeReason::MissingEnvVar) - } - } - - ASTNode::Literal { value, .. } => match value { - LiteralValue::Integer(_) | LiteralValue::Bool(_) => None, - _ => Some(OutOfScopeReason::UnsupportedLiteral), - }, - - ASTNode::UnaryOp { operator, operand, .. } => match operator { - UnaryOperator::Minus => { - if Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), operand, env) { - None - } else { - Some(OutOfScopeReason::UnsupportedOperator) - } - } - UnaryOperator::Not => { - if Self::is_supported_bool_expr_with_scope(ExprLoweringScope::WithImpure(policy), operand, env) { - None - } else { - Some(OutOfScopeReason::UnsupportedOperator) - } - } - UnaryOperator::BitNot => Some(OutOfScopeReason::UnsupportedOperator), - UnaryOperator::Weak => Some(OutOfScopeReason::UnsupportedOperator), // Phase 285W-Syntax-0 - }, - - ASTNode::BinaryOp { operator, left, right, .. } => { - if Self::binary_kind(operator).is_some() { - if Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), left, env) - && Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), right, env) - { - None - } else { - Some(OutOfScopeReason::UnsupportedOperator) - } - } else { - Some(OutOfScopeReason::UnsupportedOperator) - } - } - - _ => Some(OutOfScopeReason::ImpureExpression), - } - } - - fn is_supported_int_expr(ast: &ASTNode, env: &BTreeMap) -> bool { - match ast { - ASTNode::Variable { name, .. } => env.contains_key(name), - ASTNode::Literal { value: LiteralValue::Integer(_), .. } => true, - ASTNode::UnaryOp { operator: UnaryOperator::Minus, operand, .. } => { - Self::is_supported_int_expr(operand, env) - } - ASTNode::BinaryOp { operator, left, right, .. } => { - matches!(Self::binary_kind(operator), Some(BinaryKind::Arith(_))) - && Self::is_supported_int_expr(left, env) - && Self::is_supported_int_expr(right, env) - } - _ => false, - } - } - - fn is_supported_bool_expr(ast: &ASTNode, env: &BTreeMap) -> bool { - match ast { - ASTNode::Variable { name, .. } => env.contains_key(name), - ASTNode::Literal { value: LiteralValue::Bool(_), .. } => true, - ASTNode::UnaryOp { operator: UnaryOperator::Not, operand, .. } => { - Self::is_supported_bool_expr(operand, env) - } - _ => false, - } - } - - fn is_supported_int_expr_with_scope(scope: ExprLoweringScope, ast: &ASTNode, env: &BTreeMap) -> bool { - match scope { - ExprLoweringScope::PureOnly => Self::is_supported_int_expr(ast, env), - ExprLoweringScope::WithImpure(policy) => match ast { - ASTNode::MethodCall { object, method, arguments, .. } => { - Self::match_known_intrinsic_method_call(policy, object, method, arguments, env).is_some() - } - ASTNode::Variable { name, .. } => env.contains_key(name), - ASTNode::Literal { value: LiteralValue::Integer(_), .. } => true, - ASTNode::UnaryOp { operator: UnaryOperator::Minus, operand, .. } => { - Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), operand, env) - } - ASTNode::BinaryOp { operator, left, right, .. } => { - matches!(Self::binary_kind(operator), Some(BinaryKind::Arith(_)) | Some(BinaryKind::Compare(_))) - && Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), left, env) - && Self::is_supported_int_expr_with_scope(ExprLoweringScope::WithImpure(policy), right, env) - } - _ => false, - }, - } - } - - fn is_supported_bool_expr_with_scope(scope: ExprLoweringScope, ast: &ASTNode, env: &BTreeMap) -> bool { - match scope { - ExprLoweringScope::PureOnly => Self::is_supported_bool_expr(ast, env), - ExprLoweringScope::WithImpure(policy) => match ast { - ASTNode::Variable { name, .. } => env.contains_key(name), - ASTNode::Literal { value: LiteralValue::Bool(_), .. } => true, - ASTNode::UnaryOp { operator: UnaryOperator::Not, operand, .. } => { - Self::is_supported_bool_expr_with_scope(ExprLoweringScope::WithImpure(policy), operand, env) - } - _ => { - let _ = policy; - false - } - }, - } - } - - fn alloc_value_id(next_value_id: &mut u32) -> ValueId { - let vid = ValueId(*next_value_id); - *next_value_id += 1; - vid - } - - fn lower_literal( - value: &LiteralValue, - body: &mut Vec, - next_value_id: &mut u32, - ) -> Result, String> { - match value { - LiteralValue::Integer(i) => { - let dst = Self::alloc_value_id(next_value_id); - body.push(JoinInst::Compute(MirLikeInst::Const { - dst, - value: ConstValue::Integer(*i), - })); - Ok(Some(dst)) - } - LiteralValue::Bool(b) => { - let dst = Self::alloc_value_id(next_value_id); - body.push(JoinInst::Compute(MirLikeInst::Const { - dst, - value: ConstValue::Bool(*b), - })); - Ok(Some(dst)) - } - _ => Ok(None), - } - } - - fn lower_unary( - operator: &UnaryOperator, - operand: &ASTNode, - env: &BTreeMap, - body: &mut Vec, - next_value_id: &mut u32, - ) -> Result, String> { - match operator { - UnaryOperator::Minus => { - let operand_vid = - match Self::lower_int_expr(operand, env, body, next_value_id)? { - Some(v) => v, - None => return Ok(None), - }; - let dst = Self::alloc_value_id(next_value_id); - body.push(JoinInst::Compute(MirLikeInst::UnaryOp { - dst, - op: UnaryOp::Neg, - operand: operand_vid, - })); - Ok(Some(dst)) - } - UnaryOperator::Not => { - let operand_vid = - match Self::lower_bool_expr(operand, env, body, next_value_id)? { - Some(v) => v, - None => return Ok(None), - }; - let dst = Self::alloc_value_id(next_value_id); - body.push(JoinInst::Compute(MirLikeInst::UnaryOp { - dst, - op: UnaryOp::Not, - operand: operand_vid, - })); - Ok(Some(dst)) - } - UnaryOperator::BitNot => Ok(None), - UnaryOperator::Weak => Ok(None), // Phase 285W-Syntax-0: Not supported in normalized lowering - } - } - - /// Phase 141 P1.5: Refactored to use KnownIntrinsicRegistryBox - fn match_known_intrinsic_method_call( - policy: ImpurePolicy, - object: &ASTNode, - method: &str, - arguments: &[ASTNode], - env: &BTreeMap, - ) -> Option { - match policy { - ImpurePolicy::KnownIntrinsicOnly => { - let receiver_ok = matches!(object, ASTNode::Variable { name, .. } if env.contains_key(name)); - if !receiver_ok { - return None; - } - // SSOT: Use registry for lookup (Phase 141 P1.5) - KnownIntrinsicRegistryBox::lookup(method, arguments.len()) - } - } - } - - /// Phase 141 P1.5: Refactored to use KnownIntrinsicRegistryBox - fn lower_known_intrinsic_method_call( - intrinsic: KnownIntrinsic, - object: &ASTNode, - body: &mut Vec, - next_value_id: &mut u32, - env: &BTreeMap, - ) -> Result, String> { - let receiver = match object { - ASTNode::Variable { name, .. } => match env.get(name).copied() { - Some(v) => v, - None => return Ok(None), - }, - _ => return Ok(None), - }; - - // SSOT: Get spec from registry (Phase 141 P1.5) - let spec = KnownIntrinsicRegistryBox::get_spec(intrinsic); - - let dst = Self::alloc_value_id(next_value_id); - body.push(JoinInst::MethodCall { - dst, - receiver, - method: spec.method_name.to_string(), - args: vec![], - // Use type hint from registry (currently all intrinsics return Integer) - type_hint: Some(MirType::Integer), - }); - Ok(Some(dst)) - } - - fn lower_binary( - operator: &BinaryOperator, - left: &ASTNode, - right: &ASTNode, - env: &BTreeMap, - body: &mut Vec, - next_value_id: &mut u32, - ) -> Result, String> { - let Some(kind) = Self::binary_kind(operator) else { - return Ok(None); - }; - - match kind { - BinaryKind::Arith(op) => { - let lhs = match Self::lower_int_expr(left, env, body, next_value_id)? { - Some(v) => v, - None => return Ok(None), - }; - let rhs = match Self::lower_int_expr(right, env, body, next_value_id)? { - Some(v) => v, - None => return Ok(None), - }; - - let dst = Self::alloc_value_id(next_value_id); - body.push(JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs })); - Ok(Some(dst)) - } - BinaryKind::Compare(op) => { - let lhs = match Self::lower_int_expr(left, env, body, next_value_id)? { - Some(v) => v, - None => return Ok(None), - }; - let rhs = match Self::lower_int_expr(right, env, body, next_value_id)? { - Some(v) => v, - None => return Ok(None), - }; - - let dst = Self::alloc_value_id(next_value_id); - body.push(JoinInst::Compute(MirLikeInst::Compare { dst, op, lhs, rhs })); - Ok(Some(dst)) - } - } - } - - fn lower_int_expr( - ast: &ASTNode, - env: &BTreeMap, - body: &mut Vec, - next_value_id: &mut u32, - ) -> Result, String> { - match ast { - ASTNode::Variable { name, .. } => Ok(env.get(name).copied()), - ASTNode::Literal { value, .. } => match value { - LiteralValue::Integer(_) => Self::lower_literal(value, body, next_value_id), - _ => Ok(None), - }, - ASTNode::UnaryOp { operator: UnaryOperator::Minus, operand, .. } => { - Self::lower_unary(&UnaryOperator::Minus, operand, env, body, next_value_id) - } - ASTNode::BinaryOp { operator, left, right, .. } => { - let Some(BinaryKind::Arith(_)) = Self::binary_kind(operator) else { - return Ok(None); - }; - Self::lower_binary(operator, left, right, env, body, next_value_id) - } - _ => Ok(None), - } - } - - fn lower_bool_expr( - ast: &ASTNode, - env: &BTreeMap, - body: &mut Vec, - next_value_id: &mut u32, - ) -> Result, String> { - match ast { - ASTNode::Variable { name, .. } => Ok(env.get(name).copied()), - ASTNode::Literal { value, .. } => match value { - LiteralValue::Bool(_) => Self::lower_literal(value, body, next_value_id), - _ => Ok(None), - }, - ASTNode::UnaryOp { operator: UnaryOperator::Not, operand, .. } => { - Self::lower_unary(&UnaryOperator::Not, operand, env, body, next_value_id) - } - _ => Ok(None), - } - } - - fn binary_kind(op: &BinaryOperator) -> Option { - match op { - BinaryOperator::Add => Some(BinaryKind::Arith(BinOpKind::Add)), - BinaryOperator::Subtract => Some(BinaryKind::Arith(BinOpKind::Sub)), - BinaryOperator::Multiply => Some(BinaryKind::Arith(BinOpKind::Mul)), - BinaryOperator::Divide => Some(BinaryKind::Arith(BinOpKind::Div)), - BinaryOperator::Equal => Some(BinaryKind::Compare(CompareOp::Eq)), - BinaryOperator::NotEqual => Some(BinaryKind::Compare(CompareOp::Ne)), - BinaryOperator::Less => Some(BinaryKind::Compare(CompareOp::Lt)), - BinaryOperator::LessEqual => Some(BinaryKind::Compare(CompareOp::Le)), - BinaryOperator::Greater => Some(BinaryKind::Compare(CompareOp::Gt)), - BinaryOperator::GreaterEqual => Some(BinaryKind::Compare(CompareOp::Ge)), - _ => None, - } - } -} - -enum BinaryKind { - Arith(BinOpKind), - Compare(CompareOp), -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ast::Span; - - fn span() -> Span { - Span::unknown() - } - - #[test] - fn lower_var_from_env() { - let mut env = BTreeMap::new(); - env.insert("x".to_string(), ValueId(42)); - let mut body = vec![]; - let mut next = 100; - - let ast = ASTNode::Variable { - name: "x".to_string(), - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); - assert_eq!(got, Some(ValueId(42))); - assert!(body.is_empty()); - assert_eq!(next, 100); - } - - #[test] - fn out_of_scope_var_missing() { - let env = BTreeMap::new(); - let mut body = vec![]; - let mut next = 100; - - let ast = ASTNode::Variable { - name: "missing".to_string(), - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); - assert_eq!(got, None); - assert!(body.is_empty()); - assert_eq!(next, 100); - } - - #[test] - fn lower_int_literal_const_emits() { - let env = BTreeMap::new(); - let mut body = vec![]; - let mut next = 100; - - let ast = ASTNode::Literal { - value: LiteralValue::Integer(7), - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); - assert_eq!(got, Some(ValueId(100))); - assert_eq!(next, 101); - assert!(matches!( - body.as_slice(), - [JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(100), - value: ConstValue::Integer(7) - })] - )); - } - - #[test] - fn lower_bool_literal_const_emits() { - let env = BTreeMap::new(); - let mut body = vec![]; - let mut next = 100; - - let ast = ASTNode::Literal { - value: LiteralValue::Bool(true), - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); - assert_eq!(got, Some(ValueId(100))); - assert_eq!(next, 101); - assert!(matches!( - body.as_slice(), - [JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(100), - value: ConstValue::Bool(true) - })] - )); - } - - #[test] - fn lower_unary_minus_int() { - let env = BTreeMap::new(); - let mut body = vec![]; - let mut next = 10; - - let ast = ASTNode::UnaryOp { - operator: UnaryOperator::Minus, - operand: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(3), - span: span(), - }), - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); - assert_eq!(got, Some(ValueId(11))); - assert_eq!(next, 12); - assert!(matches!( - body.as_slice(), - [ - JoinInst::Compute(MirLikeInst::Const { .. }), - JoinInst::Compute(MirLikeInst::UnaryOp { dst: ValueId(11), op: UnaryOp::Neg, .. }) - ] - )); - } - - #[test] - fn lower_unary_not_bool() { - let env = BTreeMap::new(); - let mut body = vec![]; - let mut next = 10; - - let ast = ASTNode::UnaryOp { - operator: UnaryOperator::Not, - operand: Box::new(ASTNode::Literal { - value: LiteralValue::Bool(false), - span: span(), - }), - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); - assert_eq!(got, Some(ValueId(11))); - assert_eq!(next, 12); - assert!(matches!( - body.as_slice(), - [ - JoinInst::Compute(MirLikeInst::Const { .. }), - JoinInst::Compute(MirLikeInst::UnaryOp { dst: ValueId(11), op: UnaryOp::Not, .. }) - ] - )); - } - - #[test] - fn lower_add_sub_mul_div_ints() { - let mut env = BTreeMap::new(); - env.insert("x".to_string(), ValueId(1)); - let mut body = vec![]; - let mut next = 100; - - let add = 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 got = NormalizedExprLowererBox::lower_expr(&add, &env, &mut body, &mut next).unwrap(); - assert_eq!(got, Some(ValueId(101))); - - let sub = ASTNode::BinaryOp { - operator: BinaryOperator::Subtract, - left: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(5), - span: span(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(3), - span: span(), - }), - span: span(), - }; - let _ = NormalizedExprLowererBox::lower_expr(&sub, &env, &mut body, &mut next).unwrap(); - - let mul = ASTNode::BinaryOp { - operator: BinaryOperator::Multiply, - left: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(6), - span: span(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(7), - span: span(), - }), - span: span(), - }; - let _ = NormalizedExprLowererBox::lower_expr(&mul, &env, &mut body, &mut next).unwrap(); - - let div = ASTNode::BinaryOp { - operator: BinaryOperator::Divide, - left: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(8), - span: span(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(2), - span: span(), - }), - span: span(), - }; - let _ = NormalizedExprLowererBox::lower_expr(&div, &env, &mut body, &mut next).unwrap(); - - assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Add, .. })))); - assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Sub, .. })))); - assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Mul, .. })))); - assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Div, .. })))); - } - - #[test] - fn lower_compare_eq_lt_ints() { - let mut env = BTreeMap::new(); - env.insert("x".to_string(), ValueId(1)); - let mut body = vec![]; - let mut next = 100; - - let eq = ASTNode::BinaryOp { - operator: BinaryOperator::Equal, - left: Box::new(ASTNode::Variable { - name: "x".to_string(), - span: span(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(1), - span: span(), - }), - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(&eq, &env, &mut body, &mut next).unwrap(); - assert!(got.is_some()); - - let lt = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(0), - span: span(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(1), - span: span(), - }), - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(<, &env, &mut body, &mut next).unwrap(); - assert!(got.is_some()); - - assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::Compare { op: CompareOp::Eq, .. })))); - assert!(body.iter().any(|i| matches!(i, JoinInst::Compute(MirLikeInst::Compare { op: CompareOp::Lt, .. })))); - } - - #[test] - fn out_of_scope_call() { - let env = BTreeMap::new(); - let mut body = vec![]; - let mut next = 1; - - let ast = ASTNode::FunctionCall { - name: "f".to_string(), - arguments: vec![], - span: span(), - }; - let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); - assert_eq!(got, None); - assert!(body.is_empty()); - assert_eq!(next, 1); - } - - #[test] - fn call_is_out_of_scope_in_pure_only() { - let env = BTreeMap::new(); - let ast = ASTNode::Call { - callee: Box::new(ASTNode::Variable { - name: "f".to_string(), - span: span(), - }), - arguments: vec![], - span: span(), - }; - assert_eq!( - NormalizedExprLowererBox::out_of_scope_reason(ExprLoweringScope::PureOnly, &ast, &env), - Some(OutOfScopeReason::Call) - ); - } - - #[test] - fn methodcall_is_out_of_scope_in_pure_only() { - let env = BTreeMap::new(); - let ast = ASTNode::MethodCall { - object: Box::new(ASTNode::Variable { - name: "x".to_string(), - span: span(), - }), - method: "m".to_string(), - arguments: vec![], - span: span(), - }; - assert_eq!( - NormalizedExprLowererBox::out_of_scope_reason(ExprLoweringScope::PureOnly, &ast, &env), - Some(OutOfScopeReason::MethodCall) - ); - } - - #[test] - fn methodcall_length0_is_in_scope_with_known_intrinsic_only() { - let mut env = BTreeMap::new(); - env.insert("s".to_string(), ValueId(1)); - - let ast = ASTNode::MethodCall { - object: Box::new(ASTNode::Variable { - name: "s".to_string(), - span: span(), - }), - method: "length".to_string(), - arguments: vec![], - span: span(), - }; - - assert_eq!( - NormalizedExprLowererBox::out_of_scope_reason( - ExprLoweringScope::WithImpure(ImpurePolicy::KnownIntrinsicOnly), - &ast, - &env - ), - None - ); - } - - #[test] - fn lower_methodcall_length0_emits_method_call_inst() { - let mut env = BTreeMap::new(); - env.insert("s".to_string(), ValueId(7)); - let mut body = vec![]; - let mut next = 100; - - let ast = ASTNode::MethodCall { - object: Box::new(ASTNode::Variable { - name: "s".to_string(), - span: span(), - }), - method: "length".to_string(), - arguments: vec![], - span: span(), - }; - - let got = NormalizedExprLowererBox::lower_expr_with_scope( - ExprLoweringScope::WithImpure(ImpurePolicy::KnownIntrinsicOnly), - &ast, - &env, - &mut body, - &mut next, - ) - .unwrap(); - - assert_eq!(got, Some(ValueId(100))); - assert_eq!(next, 101); - assert!(matches!( - body.as_slice(), - [JoinInst::MethodCall { - dst: ValueId(100), - receiver: ValueId(7), - method, - args, - type_hint: Some(MirType::Integer), - }] if method == "length" && args.is_empty() - )); - } -} diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/binary.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/binary.rs new file mode 100644 index 00000000..c1253d8f --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/binary.rs @@ -0,0 +1,23 @@ +use crate::ast::BinaryOperator; +use crate::mir::join_ir::{BinOpKind, CompareOp}; + +pub(super) enum BinaryKind { + Arith(BinOpKind), + Compare(CompareOp), +} + +pub(super) fn binary_kind(op: &BinaryOperator) -> Option { + match op { + BinaryOperator::Add => Some(BinaryKind::Arith(BinOpKind::Add)), + BinaryOperator::Subtract => Some(BinaryKind::Arith(BinOpKind::Sub)), + BinaryOperator::Multiply => Some(BinaryKind::Arith(BinOpKind::Mul)), + BinaryOperator::Divide => Some(BinaryKind::Arith(BinOpKind::Div)), + BinaryOperator::Equal => Some(BinaryKind::Compare(CompareOp::Eq)), + BinaryOperator::NotEqual => Some(BinaryKind::Compare(CompareOp::Ne)), + BinaryOperator::Less => Some(BinaryKind::Compare(CompareOp::Lt)), + BinaryOperator::LessEqual => Some(BinaryKind::Compare(CompareOp::Le)), + BinaryOperator::Greater => Some(BinaryKind::Compare(CompareOp::Gt)), + BinaryOperator::GreaterEqual => Some(BinaryKind::Compare(CompareOp::Ge)), + _ => None, + } +} diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/intrinsics.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/intrinsics.rs new file mode 100644 index 00000000..d05d3ebc --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/intrinsics.rs @@ -0,0 +1,59 @@ +use super::NormalizedExprLowererBox; +use super::super::expr_lowering_contract::{ImpurePolicy, KnownIntrinsic}; +use super::super::known_intrinsics::KnownIntrinsicRegistryBox; +use crate::ast::ASTNode; +use crate::mir::join_ir::JoinInst; +use crate::mir::types::MirType; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +impl NormalizedExprLowererBox { + /// Phase 141 P1.5: Refactored to use KnownIntrinsicRegistryBox + pub(super) fn match_known_intrinsic_method_call( + policy: ImpurePolicy, + object: &ASTNode, + method: &str, + arguments: &[ASTNode], + env: &BTreeMap, + ) -> Option { + match policy { + ImpurePolicy::KnownIntrinsicOnly => { + let receiver_ok = + matches!(object, ASTNode::Variable { name, .. } if env.contains_key(name)); + if !receiver_ok { + return None; + } + KnownIntrinsicRegistryBox::lookup(method, arguments.len()) + } + } + } + + /// Phase 141 P1.5: Refactored to use KnownIntrinsicRegistryBox + pub(super) fn lower_known_intrinsic_method_call( + intrinsic: KnownIntrinsic, + object: &ASTNode, + body: &mut Vec, + next_value_id: &mut u32, + env: &BTreeMap, + ) -> Result, String> { + let receiver = match object { + ASTNode::Variable { name, .. } => match env.get(name).copied() { + Some(v) => v, + None => return Ok(None), + }, + _ => return Ok(None), + }; + + let spec = KnownIntrinsicRegistryBox::get_spec(intrinsic); + + let dst = Self::alloc_value_id(next_value_id); + body.push(JoinInst::MethodCall { + dst, + receiver, + method: spec.method_name.to_string(), + args: vec![], + type_hint: Some(MirType::Integer), + }); + Ok(Some(dst)) + } +} diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/lowering.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/lowering.rs new file mode 100644 index 00000000..2ed65c14 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/lowering.rs @@ -0,0 +1,173 @@ +use super::binary::{binary_kind, BinaryKind}; +use super::NormalizedExprLowererBox; +use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator}; +use crate::mir::join_ir::{ConstValue, JoinInst, MirLikeInst, UnaryOp}; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +impl NormalizedExprLowererBox { + pub(super) fn alloc_value_id(next_value_id: &mut u32) -> ValueId { + let vid = ValueId(*next_value_id); + *next_value_id += 1; + vid + } + + pub(super) fn lower_literal( + value: &LiteralValue, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + match value { + LiteralValue::Integer(i) => { + let dst = Self::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::Const { + dst, + value: ConstValue::Integer(*i), + })); + Ok(Some(dst)) + } + LiteralValue::Bool(b) => { + let dst = Self::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::Const { + dst, + value: ConstValue::Bool(*b), + })); + Ok(Some(dst)) + } + _ => Ok(None), + } + } + + pub(super) fn lower_unary( + operator: &UnaryOperator, + operand: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + match operator { + UnaryOperator::Minus => { + let operand_vid = match Self::lower_int_expr(operand, env, body, next_value_id)? { + Some(v) => v, + None => return Ok(None), + }; + let dst = Self::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::UnaryOp { + dst, + op: UnaryOp::Neg, + operand: operand_vid, + })); + Ok(Some(dst)) + } + UnaryOperator::Not => { + let operand_vid = match Self::lower_bool_expr(operand, env, body, next_value_id)? { + Some(v) => v, + None => return Ok(None), + }; + let dst = Self::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::UnaryOp { + dst, + op: UnaryOp::Not, + operand: operand_vid, + })); + Ok(Some(dst)) + } + UnaryOperator::BitNot => Ok(None), + UnaryOperator::Weak => Ok(None), // Phase 285W-Syntax-0: Not supported in normalized lowering + } + } + + pub(super) fn lower_binary( + operator: &BinaryOperator, + left: &ASTNode, + right: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + let Some(kind) = binary_kind(operator) else { + return Ok(None); + }; + + match kind { + BinaryKind::Arith(op) => { + let lhs = match Self::lower_int_expr(left, env, body, next_value_id)? { + Some(v) => v, + None => return Ok(None), + }; + let rhs = match Self::lower_int_expr(right, env, body, next_value_id)? { + Some(v) => v, + None => return Ok(None), + }; + + let dst = Self::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs })); + Ok(Some(dst)) + } + BinaryKind::Compare(op) => { + let lhs = match Self::lower_int_expr(left, env, body, next_value_id)? { + Some(v) => v, + None => return Ok(None), + }; + let rhs = match Self::lower_int_expr(right, env, body, next_value_id)? { + Some(v) => v, + None => return Ok(None), + }; + + let dst = Self::alloc_value_id(next_value_id); + body.push(JoinInst::Compute(MirLikeInst::Compare { dst, op, lhs, rhs })); + Ok(Some(dst)) + } + } + } + + pub(super) fn lower_int_expr( + ast: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + match ast { + ASTNode::Variable { name, .. } => Ok(env.get(name).copied()), + ASTNode::Literal { value, .. } => match value { + LiteralValue::Integer(_) => Self::lower_literal(value, body, next_value_id), + _ => Ok(None), + }, + ASTNode::UnaryOp { + operator: UnaryOperator::Minus, + operand, + .. + } => Self::lower_unary(&UnaryOperator::Minus, operand, env, body, next_value_id), + ASTNode::BinaryOp { + operator, left, right, .. + } => { + let Some(BinaryKind::Arith(_)) = binary_kind(operator) else { + return Ok(None); + }; + Self::lower_binary(operator, left, right, env, body, next_value_id) + } + _ => Ok(None), + } + } + + pub(super) fn lower_bool_expr( + ast: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + match ast { + ASTNode::Variable { name, .. } => Ok(env.get(name).copied()), + ASTNode::Literal { value, .. } => match value { + LiteralValue::Bool(_) => Self::lower_literal(value, body, next_value_id), + _ => Ok(None), + }, + ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand, + .. + } => Self::lower_unary(&UnaryOperator::Not, operand, env, body, next_value_id), + _ => Ok(None), + } + } +} diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/mod.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/mod.rs new file mode 100644 index 00000000..344e153d --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/mod.rs @@ -0,0 +1,137 @@ +//! NormalizedExprLowererBox: Pure expression lowering SSOT (Phase 140 P0) +//! +//! ## Responsibility +//! +//! Lower a *pure* AST expression into JoinIR `ValueId` + emitted `JoinInst`s. +//! +//! ## Scope (Phase 140 P0) +//! +//! Supported (pure only): +//! - Variable: env lookup → ValueId +//! - Literals: Integer / Bool +//! - Unary: `-` (Neg), `not` (Not) +//! - Binary arith (int-only): `+ - * /` +//! - Compare (int-only): `== != < <= > >=` +//! +//! Out of scope (returns `Ok(None)`): +//! - Call/MethodCall/FromCall/Array/Map/FieldAccess/Index/New/This/Me/… +//! - Literals other than Integer/Bool +//! - Operators outside the scope above +//! +//! ## Contract +//! +//! - Out-of-scope returns `Ok(None)` (caller routes to non-normalized lowering). +//! - `Err(_)` is reserved for internal invariants only (should be rare). + +mod binary; +mod intrinsics; +mod lowering; +mod scope; + +#[cfg(test)] +mod tests; + +use super::expr_lowering_contract::ExprLoweringScope; +use crate::ast::ASTNode; +use crate::mir::join_ir::JoinInst; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +/// Box-First: Pure expression lowering for Normalized shadow paths +pub struct NormalizedExprLowererBox; + +impl NormalizedExprLowererBox { + pub fn lower_expr( + ast: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + Self::lower_expr_with_scope(ExprLoweringScope::PureOnly, ast, env, body, next_value_id) + } + + pub fn lower_expr_with_scope( + scope: ExprLoweringScope, + ast: &ASTNode, + env: &BTreeMap, + body: &mut Vec, + next_value_id: &mut u32, + ) -> Result, String> { + // Phase 146 P1: ANF routing (dev-only, scope-aware) + if crate::config::env::anf_dev_enabled() { + // 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::{AnfExecuteBox, AnfPlanBox}; + 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)), + None => { + 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 normalized lowering + } + Err(_reason) => { + // Explicitly out-of-scope (ContainsCall/ContainsMethodCall), continue + } + } + } + } + + if Self::out_of_scope_reason(scope, ast, env).is_some() { + return Ok(None); + } + + match ast { + ASTNode::Variable { name, .. } => Ok(env.get(name).copied()), + ASTNode::Literal { value, .. } => Self::lower_literal(value, body, next_value_id), + ASTNode::UnaryOp { + operator, operand, .. + } => Self::lower_unary(operator, operand, env, body, next_value_id), + ASTNode::BinaryOp { + operator, left, right, .. + } => Self::lower_binary(operator, left, right, env, body, next_value_id), + ASTNode::MethodCall { + object, + method, + arguments, + .. + } => match scope { + ExprLoweringScope::PureOnly => Ok(None), + ExprLoweringScope::WithImpure(policy) => { + let Some(intrinsic) = + Self::match_known_intrinsic_method_call(policy, object, method, arguments, env) + else { + return Ok(None); + }; + Self::lower_known_intrinsic_method_call( + intrinsic, + object, + body, + next_value_id, + env, + ) + } + }, + _ => Ok(None), + } + } +} diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/scope.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/scope.rs new file mode 100644 index 00000000..70532364 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/scope.rs @@ -0,0 +1,301 @@ +use super::binary::{binary_kind, BinaryKind}; +use super::NormalizedExprLowererBox; +use super::super::expr_lowering_contract::{ + ExprLoweringScope, ImpurePolicy, OutOfScopeReason, +}; +use super::super::known_intrinsics::KnownIntrinsicRegistryBox; +use crate::ast::{ASTNode, LiteralValue, UnaryOperator}; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +impl NormalizedExprLowererBox { + /// Classify out-of-scope reasons for diagnostics/tests without changing the core API. + pub fn out_of_scope_reason( + scope: ExprLoweringScope, + ast: &ASTNode, + env: &BTreeMap, + ) -> Option { + match scope { + ExprLoweringScope::PureOnly => Self::out_of_scope_reason_pure(ast, env), + ExprLoweringScope::WithImpure(policy) => { + Self::out_of_scope_reason_with_impure(policy, ast, env) + } + } + } + + fn out_of_scope_reason_pure( + ast: &ASTNode, + env: &BTreeMap, + ) -> Option { + match ast { + ASTNode::FunctionCall { .. } | ASTNode::Call { .. } => Some(OutOfScopeReason::Call), + ASTNode::MethodCall { .. } => Some(OutOfScopeReason::MethodCall), + + ASTNode::Variable { name, .. } => { + if env.contains_key(name) { + None + } else { + Some(OutOfScopeReason::MissingEnvVar) + } + } + + ASTNode::Literal { value, .. } => match value { + LiteralValue::Integer(_) | LiteralValue::Bool(_) => None, + _ => Some(OutOfScopeReason::UnsupportedLiteral), + }, + + ASTNode::UnaryOp { + operator, operand, .. + } => match operator { + UnaryOperator::Minus => { + if Self::is_supported_int_expr(operand, env) { + None + } else { + Some(OutOfScopeReason::UnsupportedOperator) + } + } + UnaryOperator::Not => { + if Self::is_supported_bool_expr(operand, env) { + None + } else { + Some(OutOfScopeReason::UnsupportedOperator) + } + } + UnaryOperator::BitNot => Some(OutOfScopeReason::UnsupportedOperator), + UnaryOperator::Weak => Some(OutOfScopeReason::UnsupportedOperator), + }, + + ASTNode::BinaryOp { + operator, left, right, .. + } => { + if binary_kind(operator).is_some() { + if Self::is_supported_int_expr(left, env) && Self::is_supported_int_expr(right, env) { + None + } else { + Some(OutOfScopeReason::UnsupportedOperator) + } + } else { + Some(OutOfScopeReason::UnsupportedOperator) + } + } + + _ => Some(OutOfScopeReason::ImpureExpression), + } + } + + fn out_of_scope_reason_with_impure( + policy: ImpurePolicy, + ast: &ASTNode, + env: &BTreeMap, + ) -> Option { + match ast { + ASTNode::FunctionCall { .. } | ASTNode::Call { .. } => Some(OutOfScopeReason::Call), + ASTNode::MethodCall { + object, + method, + arguments, + .. + } => { + if Self::match_known_intrinsic_method_call(policy, object, method, arguments, env) + .is_some() + { + None + } else { + let receiver_ok = + matches!(object.as_ref(), ASTNode::Variable { name, .. } if env.contains_key(name)); + if receiver_ok && KnownIntrinsicRegistryBox::lookup(method, arguments.len()).is_some() + { + Some(OutOfScopeReason::IntrinsicNotWhitelisted) + } else { + Some(OutOfScopeReason::MethodCall) + } + } + } + + ASTNode::Variable { name, .. } => { + if env.contains_key(name) { + None + } else { + Some(OutOfScopeReason::MissingEnvVar) + } + } + + ASTNode::Literal { value, .. } => match value { + LiteralValue::Integer(_) | LiteralValue::Bool(_) => None, + _ => Some(OutOfScopeReason::UnsupportedLiteral), + }, + + ASTNode::UnaryOp { + operator, operand, .. + } => match operator { + UnaryOperator::Minus => { + if Self::is_supported_int_expr_with_scope( + ExprLoweringScope::WithImpure(policy), + operand, + env, + ) { + None + } else { + Some(OutOfScopeReason::UnsupportedOperator) + } + } + UnaryOperator::Not => { + if Self::is_supported_bool_expr_with_scope( + ExprLoweringScope::WithImpure(policy), + operand, + env, + ) { + None + } else { + Some(OutOfScopeReason::UnsupportedOperator) + } + } + UnaryOperator::BitNot => Some(OutOfScopeReason::UnsupportedOperator), + UnaryOperator::Weak => Some(OutOfScopeReason::UnsupportedOperator), + }, + + ASTNode::BinaryOp { + operator, left, right, .. + } => { + if binary_kind(operator).is_some() { + if Self::is_supported_int_expr_with_scope( + ExprLoweringScope::WithImpure(policy), + left, + env, + ) && Self::is_supported_int_expr_with_scope( + ExprLoweringScope::WithImpure(policy), + right, + env, + ) { + None + } else { + Some(OutOfScopeReason::UnsupportedOperator) + } + } else { + Some(OutOfScopeReason::UnsupportedOperator) + } + } + + _ => Some(OutOfScopeReason::ImpureExpression), + } + } + + fn is_supported_int_expr(ast: &ASTNode, env: &BTreeMap) -> bool { + match ast { + ASTNode::Variable { name, .. } => env.contains_key(name), + ASTNode::Literal { + value: LiteralValue::Integer(_), + .. + } => true, + ASTNode::UnaryOp { + operator: UnaryOperator::Minus, + operand, + .. + } => Self::is_supported_int_expr(operand, env), + ASTNode::BinaryOp { + operator, left, right, .. + } => { + matches!(binary_kind(operator), Some(BinaryKind::Arith(_))) + && Self::is_supported_int_expr(left, env) + && Self::is_supported_int_expr(right, env) + } + _ => false, + } + } + + fn is_supported_bool_expr(ast: &ASTNode, env: &BTreeMap) -> bool { + match ast { + ASTNode::Variable { name, .. } => env.contains_key(name), + ASTNode::Literal { + value: LiteralValue::Bool(_), + .. + } => true, + ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand, + .. + } => Self::is_supported_bool_expr(operand, env), + _ => false, + } + } + + fn is_supported_int_expr_with_scope( + scope: ExprLoweringScope, + ast: &ASTNode, + env: &BTreeMap, + ) -> bool { + match scope { + ExprLoweringScope::PureOnly => Self::is_supported_int_expr(ast, env), + ExprLoweringScope::WithImpure(policy) => match ast { + ASTNode::MethodCall { + object, + method, + arguments, + .. + } => Self::match_known_intrinsic_method_call(policy, object, method, arguments, env) + .is_some(), + ASTNode::Variable { name, .. } => env.contains_key(name), + ASTNode::Literal { + value: LiteralValue::Integer(_), + .. + } => true, + ASTNode::UnaryOp { + operator: UnaryOperator::Minus, + operand, + .. + } => Self::is_supported_int_expr_with_scope( + ExprLoweringScope::WithImpure(policy), + operand, + env, + ), + ASTNode::BinaryOp { + operator, left, right, .. + } => { + matches!( + binary_kind(operator), + Some(BinaryKind::Arith(_)) | Some(BinaryKind::Compare(_)) + ) && Self::is_supported_int_expr_with_scope( + ExprLoweringScope::WithImpure(policy), + left, + env, + ) && Self::is_supported_int_expr_with_scope( + ExprLoweringScope::WithImpure(policy), + right, + env, + ) + } + _ => false, + }, + } + } + + fn is_supported_bool_expr_with_scope( + scope: ExprLoweringScope, + ast: &ASTNode, + env: &BTreeMap, + ) -> bool { + match scope { + ExprLoweringScope::PureOnly => Self::is_supported_bool_expr(ast, env), + ExprLoweringScope::WithImpure(policy) => match ast { + ASTNode::Variable { name, .. } => env.contains_key(name), + ASTNode::Literal { + value: LiteralValue::Bool(_), + .. + } => true, + ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand, + .. + } => Self::is_supported_bool_expr_with_scope( + ExprLoweringScope::WithImpure(policy), + operand, + env, + ), + _ => { + let _ = policy; + false + } + }, + } + } +} diff --git a/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/tests.rs b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/tests.rs new file mode 100644 index 00000000..d0b0deab --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/common/expr_lowerer_box/tests.rs @@ -0,0 +1,412 @@ +use super::super::expr_lowering_contract::{ExprLoweringScope, ImpurePolicy, OutOfScopeReason}; +use super::*; +use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator}; +use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, JoinInst, MirLikeInst, UnaryOp}; +use crate::mir::types::MirType; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +fn span() -> Span { + Span::unknown() +} + +#[test] +fn lower_var_from_env() { + let mut env = BTreeMap::new(); + env.insert("x".to_string(), ValueId(42)); + let mut body = vec![]; + let mut next = 100; + + let ast = ASTNode::Variable { + name: "x".to_string(), + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); + assert_eq!(got, Some(ValueId(42))); + assert!(body.is_empty()); + assert_eq!(next, 100); +} + +#[test] +fn out_of_scope_var_missing() { + let env = BTreeMap::new(); + let mut body = vec![]; + let mut next = 100; + + let ast = ASTNode::Variable { + name: "missing".to_string(), + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); + assert_eq!(got, None); + assert!(body.is_empty()); + assert_eq!(next, 100); +} + +#[test] +fn lower_int_literal_const_emits() { + let env = BTreeMap::new(); + let mut body = vec![]; + let mut next = 100; + + let ast = ASTNode::Literal { + value: LiteralValue::Integer(7), + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); + assert_eq!(got, Some(ValueId(100))); + assert_eq!(next, 101); + assert!(matches!( + body.as_slice(), + [JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(100), + value: ConstValue::Integer(7) + })] + )); +} + +#[test] +fn lower_bool_literal_const_emits() { + let env = BTreeMap::new(); + let mut body = vec![]; + let mut next = 100; + + let ast = ASTNode::Literal { + value: LiteralValue::Bool(true), + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); + assert_eq!(got, Some(ValueId(100))); + assert_eq!(next, 101); + assert!(matches!( + body.as_slice(), + [JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(100), + value: ConstValue::Bool(true) + })] + )); +} + +#[test] +fn lower_unary_minus_int() { + let env = BTreeMap::new(); + let mut body = vec![]; + let mut next = 10; + + let ast = ASTNode::UnaryOp { + operator: UnaryOperator::Minus, + operand: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(3), + span: span(), + }), + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); + assert_eq!(got, Some(ValueId(11))); + assert_eq!(next, 12); + assert!(matches!( + body.as_slice(), + [ + JoinInst::Compute(MirLikeInst::Const { .. }), + JoinInst::Compute(MirLikeInst::UnaryOp { + dst: ValueId(11), + op: UnaryOp::Neg, + .. + }) + ] + )); +} + +#[test] +fn lower_unary_not_bool() { + let env = BTreeMap::new(); + let mut body = vec![]; + let mut next = 10; + + let ast = ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(false), + span: span(), + }), + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); + assert_eq!(got, Some(ValueId(11))); + assert_eq!(next, 12); + assert!(matches!( + body.as_slice(), + [ + JoinInst::Compute(MirLikeInst::Const { .. }), + JoinInst::Compute(MirLikeInst::UnaryOp { + dst: ValueId(11), + op: UnaryOp::Not, + .. + }) + ] + )); +} + +#[test] +fn lower_add_sub_mul_div_ints() { + let mut env = BTreeMap::new(); + env.insert("x".to_string(), ValueId(1)); + let mut body = vec![]; + let mut next = 100; + + let add = 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 got = NormalizedExprLowererBox::lower_expr(&add, &env, &mut body, &mut next).unwrap(); + assert_eq!(got, Some(ValueId(101))); + + let sub = ASTNode::BinaryOp { + operator: BinaryOperator::Subtract, + left: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(5), + span: span(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(3), + span: span(), + }), + span: span(), + }; + let _ = NormalizedExprLowererBox::lower_expr(&sub, &env, &mut body, &mut next).unwrap(); + + let mul = ASTNode::BinaryOp { + operator: BinaryOperator::Multiply, + left: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(6), + span: span(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(7), + span: span(), + }), + span: span(), + }; + let _ = NormalizedExprLowererBox::lower_expr(&mul, &env, &mut body, &mut next).unwrap(); + + let div = ASTNode::BinaryOp { + operator: BinaryOperator::Divide, + left: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(8), + span: span(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(2), + span: span(), + }), + span: span(), + }; + let _ = NormalizedExprLowererBox::lower_expr(&div, &env, &mut body, &mut next).unwrap(); + + assert!(body.iter().any(|i| matches!( + i, + JoinInst::Compute(MirLikeInst::BinOp { + op: BinOpKind::Add, + .. + }) + ))); + assert!(body.iter().any(|i| matches!( + i, + JoinInst::Compute(MirLikeInst::BinOp { + op: BinOpKind::Sub, + .. + }) + ))); + assert!(body.iter().any(|i| matches!( + i, + JoinInst::Compute(MirLikeInst::BinOp { + op: BinOpKind::Mul, + .. + }) + ))); + assert!(body.iter().any(|i| matches!( + i, + JoinInst::Compute(MirLikeInst::BinOp { + op: BinOpKind::Div, + .. + }) + ))); +} + +#[test] +fn lower_compare_eq_lt_ints() { + let mut env = BTreeMap::new(); + env.insert("x".to_string(), ValueId(1)); + let mut body = vec![]; + let mut next = 100; + + let eq = ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: span(), + }), + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(&eq, &env, &mut body, &mut next).unwrap(); + assert!(got.is_some()); + + let lt = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(0), + span: span(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: span(), + }), + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(<, &env, &mut body, &mut next).unwrap(); + assert!(got.is_some()); + + assert!(body.iter().any(|i| matches!( + i, + JoinInst::Compute(MirLikeInst::Compare { + op: CompareOp::Eq, + .. + }) + ))); + assert!(body.iter().any(|i| matches!( + i, + JoinInst::Compute(MirLikeInst::Compare { + op: CompareOp::Lt, + .. + }) + ))); +} + +#[test] +fn out_of_scope_call() { + let env = BTreeMap::new(); + let mut body = vec![]; + let mut next = 1; + + let ast = ASTNode::FunctionCall { + name: "f".to_string(), + arguments: vec![], + span: span(), + }; + let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap(); + assert_eq!(got, None); + assert!(body.is_empty()); + assert_eq!(next, 1); +} + +#[test] +fn call_is_out_of_scope_in_pure_only() { + let env = BTreeMap::new(); + let ast = ASTNode::Call { + callee: Box::new(ASTNode::Variable { + name: "f".to_string(), + span: span(), + }), + arguments: vec![], + span: span(), + }; + assert_eq!( + NormalizedExprLowererBox::out_of_scope_reason(ExprLoweringScope::PureOnly, &ast, &env), + Some(OutOfScopeReason::Call) + ); +} + +#[test] +fn methodcall_is_out_of_scope_in_pure_only() { + let env = BTreeMap::new(); + let ast = ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span(), + }), + method: "m".to_string(), + arguments: vec![], + span: span(), + }; + assert_eq!( + NormalizedExprLowererBox::out_of_scope_reason(ExprLoweringScope::PureOnly, &ast, &env), + Some(OutOfScopeReason::MethodCall) + ); +} + +#[test] +fn methodcall_length0_is_in_scope_with_known_intrinsic_only() { + let mut env = BTreeMap::new(); + env.insert("s".to_string(), ValueId(1)); + + let ast = ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s".to_string(), + span: span(), + }), + method: "length".to_string(), + arguments: vec![], + span: span(), + }; + + assert_eq!( + NormalizedExprLowererBox::out_of_scope_reason( + ExprLoweringScope::WithImpure(ImpurePolicy::KnownIntrinsicOnly), + &ast, + &env + ), + None + ); +} + +#[test] +fn lower_methodcall_length0_emits_method_call_inst() { + let mut env = BTreeMap::new(); + env.insert("s".to_string(), ValueId(7)); + let mut body = vec![]; + let mut next = 100; + + let ast = ASTNode::MethodCall { + object: Box::new(ASTNode::Variable { + name: "s".to_string(), + span: span(), + }), + method: "length".to_string(), + arguments: vec![], + span: span(), + }; + + let got = NormalizedExprLowererBox::lower_expr_with_scope( + ExprLoweringScope::WithImpure(ImpurePolicy::KnownIntrinsicOnly), + &ast, + &env, + &mut body, + &mut next, + ) + .unwrap(); + + assert_eq!(got, Some(ValueId(100))); + assert_eq!(next, 101); + assert!(matches!( + body.as_slice(), + [JoinInst::MethodCall { + dst: ValueId(100), + receiver: ValueId(7), + method, + args, + type_hint: Some(MirType::Integer), + }] if method == "length" && args.is_empty() + )); +} diff --git a/src/mir/join_ir/lowering/inline_boundary.rs b/src/mir/join_ir/lowering/inline_boundary.rs deleted file mode 100644 index 85515420..00000000 --- a/src/mir/join_ir/lowering/inline_boundary.rs +++ /dev/null @@ -1,796 +0,0 @@ -//! Phase 188-Impl-3: JoinInlineBoundary - Boundary information for JoinIR inlining -//! -//! This module defines the boundary between JoinIR fragments and the host MIR function. -//! It enables clean separation of concerns: -//! -//! - **Box A**: JoinIR Frontend (doesn't know about host ValueIds) -//! - **Box B**: Join→MIR Bridge (converts to MIR using local ValueIds) -//! - **Box C**: JoinInlineBoundary (stores boundary info - THIS FILE) -//! - **Box D**: JoinMirInlineMerger (injects Copy instructions at boundary) -//! -//! ## Design Philosophy -//! -//! The JoinIR lowerer should work with **JoinIR-side ValueIds** allocated via -//! `JoinValueSpace` (Param: 100-999, Local: 1000+) without knowing anything about -//! the host function's ValueId space. This ensures: -//! -//! 1. **Modularity**: JoinIR lowerers are pure transformers -//! 2. **Reusability**: Same lowerer can be used in different contexts -//! 3. **Testability**: JoinIR can be tested independently -//! 4. **Correctness**: SSA properties are maintained via explicit Copy instructions -//! -//! ## Example -//! -//! For `loop(i < 3) { print(i); i = i + 1 }`: -//! -//! ```text -//! Host Function: -//! ValueId(4) = Const 0 // i = 0 in host -//! -//! JoinIR Fragment: -//! ValueId(100) = param // i_param (JoinIR Param region) -//! ValueId(1000) = Const 3 -//! ValueId(1001) = Compare ... -//! -//! Boundary: -//! join_inputs: [ValueId(100)] // JoinIR's param slot (Param region) -//! host_inputs: [ValueId(4)] // Host's `i` variable -//! -//! Merged MIR (with Copy injection): -//! entry: -//! ValueId(100) = Copy ValueId(4) // Connect host→JoinIR -//! ValueId(101) = Const 3 -//! ... -//! ``` - -use super::carrier_info::{CarrierRole, ExitReconnectMode}; -use crate::mir::ValueId; -use std::collections::BTreeSet; - -/// Explicit binding between JoinIR exit value and host variable -/// -/// This structure formalizes the connection between a JoinIR exit PHI value -/// and the host variable it should update. This eliminates implicit assumptions -/// about which variable a ValueId represents. -/// -/// # Pattern 3 Example -/// -/// For `loop(i < 3) { sum = sum + i; i = i + 1 }`: -/// -/// ```text -/// LoopExitBinding { -/// carrier_name: "sum", -/// join_exit_value: ValueId(18), // k_exit's return value (JoinIR-local) -/// host_slot: ValueId(5), // variable_map["sum"] in host -/// } -/// ``` -/// -/// # Multi-Carrier Support (Pattern 4+) -/// -/// Multiple carriers can be represented as a vector: -/// -/// ```text -/// vec![ -/// LoopExitBinding { carrier_name: "sum", join_exit_value: ValueId(18), host_slot: ValueId(5), role: LoopState }, -/// LoopExitBinding { carrier_name: "count", join_exit_value: ValueId(19), host_slot: ValueId(6), role: LoopState }, -/// ] -/// ``` -#[derive(Debug, Clone)] -pub struct LoopExitBinding { - /// Carrier variable name (e.g., "sum", "count", "is_digit_pos") - /// - /// This is the variable name in the host's variable_map that should - /// receive the exit value. - pub carrier_name: String, - - /// JoinIR-side ValueId from k_exit (or exit parameter) - /// - /// This is the **JoinIR-local** ValueId that represents the exit value. - /// It will be remapped when merged into the host function. - pub join_exit_value: ValueId, - - /// Host-side variable_map slot to reconnect - /// - /// This is the host function's ValueId for the variable that should be - /// updated with the exit PHI result. - pub host_slot: ValueId, - - /// Phase 227: Role of this carrier (LoopState or ConditionOnly) - /// - /// Determines whether this carrier should participate in exit PHI: - /// - LoopState: Needs exit PHI (value used after loop) - /// - ConditionOnly: No exit PHI (only used in loop condition) - pub role: CarrierRole, -} - -/// Layout policy for JoinIR jump_args (SSOT) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum JumpArgsLayout { - /// jump_args = [carriers...] - CarriersOnly, - /// jump_args = [expr_result, carriers...] - ExprResultPlusCarriers, -} - -/// Boundary information for inlining a JoinIR fragment into a host function -/// -/// This structure captures the "interface" between a JoinIR fragment and the -/// host function, allowing the merger to inject necessary Copy instructions -/// to connect the two SSA value spaces. -/// -/// # Design Note -/// -/// This is a **pure data structure** with no logic. All transformation logic -/// lives in the merger (merge_joinir_mir_blocks). -#[derive(Debug, Clone)] -pub struct JoinInlineBoundary { - /// JoinIR-local ValueIds that act as "input slots" - /// - /// These are the ValueIds used **inside** the JoinIR fragment to refer - /// to values that come from the host. They should be in the JoinValueSpace - /// Param region (100-999). (They are typically allocated sequentially.) - /// - /// Example: For a loop variable `i`, JoinIR uses ValueId(100) as the parameter. - pub join_inputs: Vec, - - /// Host-function ValueIds that provide the input values - /// - /// These are the ValueIds from the **host function** that correspond to - /// the join_inputs. The merger will inject Copy instructions to connect - /// host_inputs[i] → join_inputs[i]. - /// - /// Example: If host has `i` as ValueId(4), then host_inputs = [ValueId(4)]. - pub host_inputs: Vec, - - /// JoinIR-local ValueIds that represent outputs (if any) - /// - /// For loops that produce values (e.g., loop result), these are the - /// JoinIR-local ValueIds that should be visible to the host after inlining. - /// - /// Phase 188/189 ではまだ利用していないが、将来的な Multi-carrier パターン - /// (複数の変数を一度に返すループ) のために予約している。 - pub join_outputs: Vec, - - /// Host-function ValueIds that receive the outputs (DEPRECATED) - /// - /// **DEPRECATED**: Use `exit_bindings` instead for explicit carrier naming. - /// - /// These are the destination ValueIds in the host function that should - /// receive the values from join_outputs, or (Pattern 3 のような単一 - /// キャリアのケースでは) ループ exit PHI の結果を受け取るホスト側の - /// SSA スロットを表す。 - /// - /// Phase 188-Impl-3 までは未使用だったが、Phase 189 で - /// loop_if_phi.hako の sum のような「ループの出口で更新されるキャリア」の - /// 再接続に利用する。 - #[deprecated(since = "Phase 190", note = "Use exit_bindings instead")] - pub host_outputs: Vec, - - /// Phase 255 P2: Loop invariant variables - /// - /// Variables that are referenced inside the loop body but do not change - /// across iterations. These variables need header PHI nodes (with the same - /// value from all incoming edges) but do NOT need exit PHI nodes. - /// - /// # Example: index_of(s, ch) - /// - /// ```nyash - /// index_of(s, ch) { - /// local i = 0 - /// loop(i < s.length()) { - /// if s.substring(i, i + 1) == ch { return i } - /// i = i + 1 - /// } - /// return -1 - /// } - /// ``` - /// - /// Here: - /// - `s` (haystack): loop invariant - used in loop body but never modified - /// - `ch` (needle): loop invariant - used in loop body but never modified - /// - `i` (loop index): loop state - modified each iteration (goes in exit_bindings) - /// - /// # Format - /// - /// Each entry is `(variable_name, host_value_id)`: - /// ``` - /// loop_invariants: vec![ - /// ("s".to_string(), ValueId(10)), // HOST ID for "s" - /// ("ch".to_string(), ValueId(11)), // HOST ID for "ch" - /// ] - /// ``` - /// - /// # Header PHI Generation - /// - /// For each loop invariant, LoopHeaderPhiBuilder generates: - /// ```mir - /// %phi_dst = phi [%host_id, entry], [%phi_dst, latch] - /// ``` - /// - /// The latch incoming is the PHI destination itself (same value preserved). - pub loop_invariants: Vec<(String, ValueId)>, - - /// Explicit exit bindings for loop carriers (Phase 190+) - /// - /// Each binding explicitly names which variable is being updated and - /// where the value comes from. This eliminates ambiguity and prepares - /// for multi-carrier support. - /// - /// For Pattern 3 (single carrier "sum"): - /// ``` - /// exit_bindings: vec![ - /// LoopExitBinding { - /// carrier_name: "sum", - /// join_exit_value: ValueId(18), // k_exit return value - /// host_slot: ValueId(5), // variable_map["sum"] - /// } - /// ] - /// ``` - pub exit_bindings: Vec, - - /// Condition-only input variables (Phase 171+ / Phase 171-fix) - /// - /// **DEPRECATED**: Use `condition_bindings` instead (Phase 171-fix). - /// - /// These are variables used ONLY in the loop condition, NOT as loop parameters. - /// They need to be available in JoinIR scope but are not modified by the loop. - /// - /// # Example - /// - /// For `loop(start < end) { i = i + 1 }`: - /// - Loop parameter: `i` → goes in `join_inputs`/`host_inputs` - /// - Condition-only: `start`, `end` → go in `condition_inputs` - /// - /// # Format - /// - /// Each entry is `(variable_name, host_value_id)`: - /// ``` - /// condition_inputs: vec![ - /// ("start".to_string(), ValueId(33)), // HOST ID for "start" - /// ("end".to_string(), ValueId(34)), // HOST ID for "end" - /// ] - /// ``` - /// - /// The merger will: - /// 1. Extract unique variable names from condition AST - /// 2. Look up HOST ValueIds from `builder.variable_map` - /// 3. Inject Copy instructions for each condition input - /// 4. Remap JoinIR references to use the copied values - #[deprecated(since = "Phase 171-fix", note = "Use condition_bindings instead")] - pub condition_inputs: Vec<(String, ValueId)>, - - /// Phase 171-fix: Condition bindings with explicit JoinIR ValueIds - /// - /// Each binding explicitly specifies: - /// - Variable name - /// - HOST ValueId (source) - /// - JoinIR ValueId (destination) - /// - /// This replaces `condition_inputs` to ensure proper ValueId separation. - pub condition_bindings: Vec, - - /// Phase 33-14: Expression result ValueId (JoinIR-local) - /// - /// If the loop is used as an expression (like `return loop(...)`), this field - /// contains the JoinIR-local ValueId of k_exit's return value. - /// - /// - `Some(ValueId)`: Loop returns a value → k_exit return goes to exit_phi_inputs - /// - `None`: Loop only updates carriers → no exit_phi_inputs generation - /// - /// # Example: joinir_min_loop.hako (expr result pattern) - /// - /// ```nyash - /// loop(i < 3) { if (i >= 2) { break } i = i + 1 } - /// return i - /// ``` - /// - /// Here, `expr_result = Some(i_exit)` because the loop's result is used. - /// - /// # Example: trim pattern (carrier-only) - /// - /// ```nyash - /// loop(start < end) { start = start + 1 } - /// print(start) // Uses carrier after loop - /// ``` - /// - /// Here, `expr_result = None` because the loop doesn't return a value. - pub expr_result: Option, - - /// Phase 256 P1.12: jump_args layout (SSOT) - /// - /// This prevents merge from guessing whether jump_args contains a leading - /// expr_result slot. - pub jump_args_layout: JumpArgsLayout, - - /// Phase 33-16: Loop variable name (for LoopHeaderPhiBuilder) - /// - /// The name of the loop control variable (e.g., "i" in `loop(i < 3)`). - /// Used to track which PHI corresponds to the loop variable. - pub loop_var_name: Option, - - /// Phase 287 P2: Loop header function name (SSOT) - /// - /// Merge must not guess the loop header function from "first non-main non-continuation". - /// For loop patterns, set this explicitly (typically `"loop_step"`). - /// - /// - `Some(name)`: Merge uses this as the loop header function. - /// - `None`: Legacy heuristic remains (for backwards compatibility). - pub loop_header_func_name: Option, - - /// Phase 228: Carrier metadata (for header PHI generation) - /// - /// Contains full carrier information including initialization policies. - /// This allows header PHI generation to handle ConditionOnly carriers - /// with explicit bool initialization. - /// - /// - `Some(CarrierInfo)`: Full carrier metadata available - /// - `None`: Legacy path (derive carriers from exit_bindings) - pub carrier_info: Option, - - /// Phase 132 P1: Continuation contract (SSOT) - /// Phase 256 P1.7: Changed from BTreeSet to BTreeSet - /// - /// JoinIR merge must not infer/guess continuation functions. The router/lowerer - /// must declare continuation function names here. - /// - /// Merge may still choose to *skip* a continuation function if it is a pure - /// exit stub (structural check), but it must never skip based on name alone. - /// - /// **Why Strings instead of JoinFuncIds**: The MirModule after bridge conversion - /// uses JoinFunction.name (e.g., "k_exit") as the key, not "join_func_{id}". - /// The merge code looks up functions by name in MirModule.functions, so we must - /// use the actual function names here. - pub continuation_func_ids: BTreeSet, - - /// Phase 131 P1.5: Exit reconnection mode - /// - /// Controls whether exit values are reconnected via PHI generation (Phi) - /// or direct variable_map update (DirectValue). - /// - /// - `Phi` (default): Existing loop patterns use exit PHI generation - /// - `DirectValue`: Normalized shadow uses direct value wiring - pub exit_reconnect_mode: ExitReconnectMode, -} - -impl JoinInlineBoundary { - /// Decide jump_args layout from boundary inputs (SSOT) - pub fn decide_jump_args_layout( - expr_result: Option, - exit_bindings: &[LoopExitBinding], - ) -> JumpArgsLayout { - if let Some(expr_result_id) = expr_result { - let expr_is_carrier = exit_bindings.iter().any(|binding| { - binding.role != CarrierRole::ConditionOnly - && binding.join_exit_value == expr_result_id - }); - if expr_is_carrier { - JumpArgsLayout::CarriersOnly - } else { - JumpArgsLayout::ExprResultPlusCarriers - } - } else { - JumpArgsLayout::CarriersOnly - } - } - - /// Validate jump_args layout against boundary contract (Fail-Fast) - pub fn validate_jump_args_layout(&self) -> Result<(), String> { - let expected = - Self::decide_jump_args_layout(self.expr_result, self.exit_bindings.as_slice()); - if self.jump_args_layout != expected { - return Err(format!( - "joinir/jump_args_layout_mismatch: expr_result={:?} layout={:?} expected={:?}", - self.expr_result, self.jump_args_layout, expected - )); - } - Ok(()) - } - - /// Phase 132-R0 Task 1: SSOT for default continuation function names - /// Phase 256 P1.7: Changed from JoinFuncIds to function names (Strings) - /// - /// Returns the default set of continuation functions (k_exit). - /// This is the single source of truth for continuation function identification. - /// - /// # Rationale - /// - /// - Router/lowerer must declare continuation functions explicitly - /// - Merge must NOT infer continuations by name alone - /// - This method centralizes the default continuation contract - /// - /// # Why Strings instead of JoinFuncIds - /// - /// The bridge uses JoinFunction.name as the MirModule function key (e.g., "k_exit"), - /// not "join_func_{id}". The merge code looks up functions by name in MirModule.functions, - /// so we must use actual function names here. - /// - /// # Usage - /// - /// Use this method when constructing JoinInlineBoundary objects: - /// - /// ```ignore - /// JoinInlineBoundary { - /// // ... - /// continuation_func_ids: JoinInlineBoundary::default_continuations(), - /// // ... - /// } - /// ``` - pub fn default_continuations() -> BTreeSet { - BTreeSet::from([crate::mir::join_ir::lowering::canonical_names::K_EXIT.to_string()]) - } - - /// Create a new boundary with input mappings only - /// - /// This is the common case for loops like Pattern 1 where: - /// - Inputs: loop variables (e.g., `i` in `loop(i < 3)`) - /// - Outputs: none (loop returns void/0) - pub fn new_inputs_only(join_inputs: Vec, host_inputs: Vec) -> Self { - assert_eq!( - join_inputs.len(), - host_inputs.len(), - "join_inputs and host_inputs must have same length" - ); - Self { - join_inputs, - host_inputs, - join_outputs: vec![], - #[allow(deprecated)] - host_outputs: vec![], - loop_invariants: vec![], // Phase 255 P2: Default to empty - exit_bindings: vec![], - #[allow(deprecated)] - condition_inputs: vec![], // Phase 171: Default to empty (deprecated) - condition_bindings: vec![], // Phase 171-fix: Default to empty - expr_result: None, // Phase 33-14: Default to carrier-only pattern - jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12 - loop_var_name: None, // Phase 33-16 - loop_header_func_name: None, // Phase 287 P2 - carrier_info: None, // Phase 228: Default to None - continuation_func_ids: Self::default_continuations(), - exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi - } - } - - /// Create a new boundary with both inputs and outputs (DEPRECATED) - /// - /// **DEPRECATED**: Use `new_with_exit_bindings` instead. - /// - - /// Create a new boundary with inputs and **host outputs only** (DEPRECATED) - /// - /// **DEPRECATED**: Use `new_with_exit_bindings` instead for explicit carrier naming. - /// - /// JoinIR 側の exit 値 (k_exit の引数など) を 1 つの PHI にまとめ、 - /// その PHI 結果をホスト側の変数スロットへ再接続したい場合に使う。 - /// - /// 典型例: Pattern 3 (loop_if_phi.hako) - /// - join_inputs : [i_init, sum_init] - /// - host_inputs : [host_i, host_sum] - /// - host_outputs : [host_sum] // ループ exit 時に上書きしたい変数 - #[deprecated(since = "Phase 190", note = "Use new_with_exit_bindings instead")] - pub fn new_with_input_and_host_outputs( - join_inputs: Vec, - host_inputs: Vec, - host_outputs: Vec, - ) -> Self { - assert_eq!( - join_inputs.len(), - host_inputs.len(), - "join_inputs and host_inputs must have same length" - ); - Self { - join_inputs, - host_inputs, - join_outputs: vec![], - #[allow(deprecated)] - host_outputs, - loop_invariants: vec![], // Phase 255 P2: Default to empty - exit_bindings: vec![], - #[allow(deprecated)] - condition_inputs: vec![], // Phase 171: Default to empty (deprecated) - condition_bindings: vec![], // Phase 171-fix: Default to empty - expr_result: None, // Phase 33-14 - jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12 - loop_var_name: None, // Phase 33-16 - loop_header_func_name: None, // Phase 287 P2 - carrier_info: None, // Phase 228 - continuation_func_ids: Self::default_continuations(), - exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi - } - } - - /// Create a new boundary with explicit exit bindings (Phase 190+) - /// - /// This is the recommended constructor for loops with exit carriers. - /// Each exit binding explicitly names the carrier variable and its - /// source/destination values. - /// - /// # Example: Pattern 3 (single carrier) - /// - /// ```ignore - /// let boundary = JoinInlineBoundary::new_with_exit_bindings( - /// vec![ValueId(0), ValueId(1)], // join_inputs (i, sum init) - /// vec![loop_var_id, sum_var_id], // host_inputs - /// vec![ - /// LoopExitBinding { - /// carrier_name: "sum".to_string(), - /// join_exit_value: ValueId(18), // k_exit return value - /// host_slot: sum_var_id, // variable_map["sum"] - /// } - /// ], - /// ); - /// ``` - /// - /// # Example: Pattern 4+ (multiple carriers) - /// - /// ```ignore - /// let boundary = JoinInlineBoundary::new_with_exit_bindings( - /// vec![ValueId(0), ValueId(1), ValueId(2)], // join_inputs - /// vec![i_id, sum_id, count_id], // host_inputs - /// vec![ - /// LoopExitBinding { carrier_name: "sum".to_string(), ... }, - /// LoopExitBinding { carrier_name: "count".to_string(), ... }, - /// ], - /// ); - /// ``` - pub fn new_with_exit_bindings( - join_inputs: Vec, - host_inputs: Vec, - exit_bindings: Vec, - ) -> Self { - assert_eq!( - join_inputs.len(), - host_inputs.len(), - "join_inputs and host_inputs must have same length" - ); - Self { - join_inputs, - host_inputs, - join_outputs: vec![], - #[allow(deprecated)] - host_outputs: vec![], - loop_invariants: vec![], // Phase 255 P2: Default to empty - exit_bindings, - #[allow(deprecated)] - condition_inputs: vec![], // Phase 171: Default to empty (deprecated) - condition_bindings: vec![], // Phase 171-fix: Default to empty - expr_result: None, // Phase 33-14 - jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12 - loop_var_name: None, // Phase 33-16 - loop_header_func_name: None, // Phase 287 P2 - carrier_info: None, // Phase 228 - continuation_func_ids: Self::default_continuations(), - exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi - } - } - - /// Create a new boundary with condition inputs (Phase 171+) - /// - /// # Arguments - /// - /// * `join_inputs` - JoinIR-local ValueIds for loop parameters - /// * `host_inputs` - HOST ValueIds for loop parameters - /// * `condition_inputs` - Condition-only variables [(name, host_value_id)] - /// - /// # Example - /// - /// ```ignore - /// let boundary = JoinInlineBoundary::new_with_condition_inputs( - /// vec![ValueId(0)], // join_inputs (i) - /// vec![ValueId(5)], // host_inputs (i) - /// vec![ - /// ("start".to_string(), ValueId(33)), - /// ("end".to_string(), ValueId(34)), - /// ], - /// ); - /// ``` - pub fn new_with_condition_inputs( - join_inputs: Vec, - host_inputs: Vec, - condition_inputs: Vec<(String, ValueId)>, - ) -> Self { - assert_eq!( - join_inputs.len(), - host_inputs.len(), - "join_inputs and host_inputs must have same length" - ); - Self { - join_inputs, - host_inputs, - join_outputs: vec![], - #[allow(deprecated)] - host_outputs: vec![], - loop_invariants: vec![], // Phase 255 P2: Default to empty - exit_bindings: vec![], - #[allow(deprecated)] - condition_inputs, - condition_bindings: vec![], // Phase 171-fix: Will be populated by new constructor - expr_result: None, // Phase 33-14 - jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12 - loop_var_name: None, // Phase 33-16 - loop_header_func_name: None, // Phase 287 P2 - carrier_info: None, // Phase 228 - continuation_func_ids: Self::default_continuations(), - exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi - } - } - - /// Create boundary with inputs, exit bindings, AND condition inputs (Phase 171+) - /// - /// This is the most complete constructor for loops with carriers and condition variables. - /// - /// # Example: Pattern 3 with condition variables - /// - /// ```ignore - /// let boundary = JoinInlineBoundary::new_with_exit_and_condition_inputs( - /// vec![ValueId(0), ValueId(1)], // join_inputs (i, sum) - /// vec![ValueId(5), ValueId(10)], // host_inputs - /// vec![ - /// LoopExitBinding { - /// carrier_name: "sum".to_string(), - /// join_exit_value: ValueId(18), - /// host_slot: ValueId(10), - /// } - /// ], - /// vec![ - /// ("start".to_string(), ValueId(33)), - /// ("end".to_string(), ValueId(34)), - /// ], - /// ); - /// ``` - pub fn new_with_exit_and_condition_inputs( - join_inputs: Vec, - host_inputs: Vec, - exit_bindings: Vec, - condition_inputs: Vec<(String, ValueId)>, - ) -> Self { - assert_eq!( - join_inputs.len(), - host_inputs.len(), - "join_inputs and host_inputs must have same length" - ); - Self { - join_inputs, - host_inputs, - join_outputs: vec![], - #[allow(deprecated)] - host_outputs: vec![], - loop_invariants: vec![], // Phase 255 P2: Default to empty - exit_bindings, - #[allow(deprecated)] - condition_inputs, - condition_bindings: vec![], // Phase 171-fix: Will be populated by new constructor - expr_result: None, // Phase 33-14 - jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12 - loop_var_name: None, // Phase 33-16 - loop_header_func_name: None, // Phase 287 P2 - carrier_info: None, // Phase 228 - continuation_func_ids: Self::default_continuations(), - exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi - } - } - - /// Phase 171-fix: Create boundary with ConditionBindings (NEW constructor) - /// - /// This is the recommended constructor that uses ConditionBindings instead of - /// the deprecated condition_inputs. - /// - /// # Arguments - /// - /// * `join_inputs` - JoinIR-local ValueIds for loop parameters - /// * `host_inputs` - HOST ValueIds for loop parameters - /// * `condition_bindings` - Explicit HOST ↔ JoinIR mappings for condition variables - /// - /// # Example - /// - /// ```ignore - /// let boundary = JoinInlineBoundary::new_with_condition_bindings( - /// vec![ValueId(0)], // join_inputs (loop param i) - /// vec![ValueId(5)], // host_inputs (loop param i) - /// vec![ - /// ConditionBinding { - /// name: "start".to_string(), - /// host_value: ValueId(33), // HOST - /// join_value: ValueId(1), // JoinIR - /// }, - /// ConditionBinding { - /// name: "end".to_string(), - /// host_value: ValueId(34), // HOST - /// join_value: ValueId(2), // JoinIR - /// }, - /// ], - /// ); - /// ``` - pub fn new_with_condition_bindings( - join_inputs: Vec, - host_inputs: Vec, - condition_bindings: Vec, - ) -> Self { - assert_eq!( - join_inputs.len(), - host_inputs.len(), - "join_inputs and host_inputs must have same length" - ); - Self { - join_inputs, - host_inputs, - join_outputs: vec![], - #[allow(deprecated)] - host_outputs: vec![], - loop_invariants: vec![], // Phase 255 P2: Default to empty - exit_bindings: vec![], - #[allow(deprecated)] - condition_inputs: vec![], // Deprecated, use condition_bindings instead - condition_bindings, - expr_result: None, // Phase 33-14 - jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12 - loop_var_name: None, // Phase 33-16 - loop_header_func_name: None, // Phase 287 P2 - carrier_info: None, // Phase 228 - continuation_func_ids: Self::default_continuations(), - exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_boundary_inputs_only() { - let boundary = JoinInlineBoundary::new_inputs_only( - vec![ValueId(0)], // JoinIR uses ValueId(0) for loop var - vec![ValueId(4)], // Host has loop var at ValueId(4) - ); - - assert_eq!(boundary.join_inputs.len(), 1); - assert_eq!(boundary.host_inputs.len(), 1); - assert_eq!(boundary.join_outputs.len(), 0); - #[allow(deprecated)] - { - assert_eq!(boundary.host_outputs.len(), 0); - assert_eq!(boundary.condition_inputs.len(), 0); // Phase 171: Deprecated field - } - assert_eq!(boundary.condition_bindings.len(), 0); // Phase 171-fix: New field - } - - #[test] - #[should_panic(expected = "join_inputs and host_inputs must have same length")] - fn test_boundary_mismatched_inputs() { - JoinInlineBoundary::new_inputs_only(vec![ValueId(0), ValueId(1)], vec![ValueId(4)]); - } - - #[test] - fn test_jump_args_layout_rejects_expr_result_carrier_mismatch() { - let boundary = JoinInlineBoundary { - join_inputs: vec![], - host_inputs: vec![], - join_outputs: vec![], - #[allow(deprecated)] - host_outputs: vec![], - loop_invariants: vec![], - exit_bindings: vec![LoopExitBinding { - carrier_name: "result".to_string(), - join_exit_value: ValueId(10), - host_slot: ValueId(1), - role: CarrierRole::LoopState, - }], - #[allow(deprecated)] - condition_inputs: vec![], - condition_bindings: vec![], - expr_result: Some(ValueId(10)), - jump_args_layout: JumpArgsLayout::ExprResultPlusCarriers, - loop_var_name: None, - loop_header_func_name: None, - carrier_info: None, - continuation_func_ids: JoinInlineBoundary::default_continuations(), - exit_reconnect_mode: ExitReconnectMode::default(), - }; - - let err = boundary - .validate_jump_args_layout() - .expect_err("layout mismatch must fail-fast"); - assert!(err.contains("jump_args_layout_mismatch")); - } -} diff --git a/src/mir/join_ir/lowering/inline_boundary/constructors.rs b/src/mir/join_ir/lowering/inline_boundary/constructors.rs new file mode 100644 index 00000000..12a4cd75 --- /dev/null +++ b/src/mir/join_ir/lowering/inline_boundary/constructors.rs @@ -0,0 +1,135 @@ +use super::types::{JoinInlineBoundary, JumpArgsLayout, LoopExitBinding}; +use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, ExitReconnectMode}; +use crate::mir::join_ir::lowering::condition_to_joinir::ConditionBinding; +use crate::mir::ValueId; +use std::collections::BTreeSet; + +impl JoinInlineBoundary { + /// Decide jump_args layout from boundary inputs (SSOT) + pub fn decide_jump_args_layout( + expr_result: Option, + exit_bindings: &[LoopExitBinding], + ) -> JumpArgsLayout { + if let Some(expr_result_id) = expr_result { + let expr_is_carrier = exit_bindings.iter().any(|binding| { + binding.role != CarrierRole::ConditionOnly + && binding.join_exit_value == expr_result_id + }); + if expr_is_carrier { + JumpArgsLayout::CarriersOnly + } else { + JumpArgsLayout::ExprResultPlusCarriers + } + } else { + JumpArgsLayout::CarriersOnly + } + } + + /// Validate jump_args layout against boundary contract (Fail-Fast) + pub fn validate_jump_args_layout(&self) -> Result<(), String> { + let expected = + Self::decide_jump_args_layout(self.expr_result, self.exit_bindings.as_slice()); + if self.jump_args_layout != expected { + return Err(format!( + "joinir/jump_args_layout_mismatch: expr_result={:?} layout={:?} expected={:?}", + self.expr_result, self.jump_args_layout, expected + )); + } + Ok(()) + } + + /// Phase 132-R0 Task 1: SSOT for default continuation function names + /// Phase 256 P1.7: Changed from JoinFuncIds to function names (Strings) + /// + /// Returns the default set of continuation functions (k_exit). + pub fn default_continuations() -> BTreeSet { + BTreeSet::from([crate::mir::join_ir::lowering::canonical_names::K_EXIT.to_string()]) + } + + /// Create a new boundary with input mappings only + /// + /// This is the common case for loops like Pattern 1 where: + /// - Inputs: loop variables (e.g., `i` in `loop(i < 3)`) + /// - Outputs: none (loop returns void/0) + pub fn new_inputs_only(join_inputs: Vec, host_inputs: Vec) -> Self { + assert_eq!( + join_inputs.len(), + host_inputs.len(), + "join_inputs and host_inputs must have same length" + ); + Self { + join_inputs, + host_inputs, + loop_invariants: vec![], + exit_bindings: vec![], + condition_bindings: vec![], + expr_result: None, + jump_args_layout: JumpArgsLayout::CarriersOnly, + loop_var_name: None, + loop_header_func_name: None, + carrier_info: None, + continuation_func_ids: Self::default_continuations(), + exit_reconnect_mode: ExitReconnectMode::default(), + } + } + + /// Create a new boundary with explicit exit bindings (Phase 190+) + /// + /// This is the recommended constructor for loops with exit carriers. + /// Each exit binding explicitly names the carrier variable and its + /// source/destination values. + pub fn new_with_exit_bindings( + join_inputs: Vec, + host_inputs: Vec, + exit_bindings: Vec, + ) -> Self { + assert_eq!( + join_inputs.len(), + host_inputs.len(), + "join_inputs and host_inputs must have same length" + ); + Self { + join_inputs, + host_inputs, + loop_invariants: vec![], + exit_bindings, + condition_bindings: vec![], + expr_result: None, + jump_args_layout: JumpArgsLayout::CarriersOnly, + loop_var_name: None, + loop_header_func_name: None, + carrier_info: None, + continuation_func_ids: Self::default_continuations(), + exit_reconnect_mode: ExitReconnectMode::default(), + } + } + + /// Phase 171-fix: Create boundary with ConditionBindings (recommended) + /// + /// This uses explicit ConditionBindings instead of legacy condition-only inputs. + pub fn new_with_condition_bindings( + join_inputs: Vec, + host_inputs: Vec, + condition_bindings: Vec, + ) -> Self { + assert_eq!( + join_inputs.len(), + host_inputs.len(), + "join_inputs and host_inputs must have same length" + ); + Self { + join_inputs, + host_inputs, + loop_invariants: vec![], + exit_bindings: vec![], + condition_bindings, + expr_result: None, + jump_args_layout: JumpArgsLayout::CarriersOnly, + loop_var_name: None, + loop_header_func_name: None, + carrier_info: None, + continuation_func_ids: Self::default_continuations(), + exit_reconnect_mode: ExitReconnectMode::default(), + } + } +} diff --git a/src/mir/join_ir/lowering/inline_boundary/mod.rs b/src/mir/join_ir/lowering/inline_boundary/mod.rs new file mode 100644 index 00000000..2bcd908c --- /dev/null +++ b/src/mir/join_ir/lowering/inline_boundary/mod.rs @@ -0,0 +1,28 @@ +//! Phase 188-Impl-3: JoinInlineBoundary - Boundary information for JoinIR inlining +//! +//! This module defines the boundary between JoinIR fragments and the host MIR function. +//! It enables clean separation of concerns: +//! +//! - **Box A**: JoinIR Frontend (doesn't know about host ValueIds) +//! - **Box B**: Join→MIR Bridge (converts to MIR using local ValueIds) +//! - **Box C**: JoinInlineBoundary (stores boundary info) +//! - **Box D**: JoinMirInlineMerger (injects Copy instructions at boundary) +//! +//! ## Design Philosophy +//! +//! The JoinIR lowerer should work with **JoinIR-side ValueIds** allocated via +//! `JoinValueSpace` (Param: 100-999, Local: 1000+) without knowing anything about +//! the host function's ValueId space. This ensures: +//! +//! 1. **Modularity**: JoinIR lowerers are pure transformers +//! 2. **Reusability**: Same lowerer can be used in different contexts +//! 3. **Testability**: JoinIR can be tested independently +//! 4. **Correctness**: SSA properties are maintained via explicit Copy instructions + +mod constructors; +mod types; + +#[cfg(test)] +mod tests; + +pub use types::{JoinInlineBoundary, JumpArgsLayout, LoopExitBinding}; diff --git a/src/mir/join_ir/lowering/inline_boundary/tests.rs b/src/mir/join_ir/lowering/inline_boundary/tests.rs new file mode 100644 index 00000000..315b412f --- /dev/null +++ b/src/mir/join_ir/lowering/inline_boundary/tests.rs @@ -0,0 +1,50 @@ +use super::*; +use crate::mir::ValueId; + +#[test] +fn test_boundary_inputs_only() { + let boundary = JoinInlineBoundary::new_inputs_only( + vec![ValueId(0)], // JoinIR uses ValueId(0) for loop var + vec![ValueId(4)], // Host has loop var at ValueId(4) + ); + + assert_eq!(boundary.join_inputs.len(), 1); + assert_eq!(boundary.host_inputs.len(), 1); + assert_eq!(boundary.loop_invariants.len(), 0); + assert_eq!(boundary.exit_bindings.len(), 0); + assert_eq!(boundary.condition_bindings.len(), 0); +} + +#[test] +#[should_panic(expected = "join_inputs and host_inputs must have same length")] +fn test_boundary_mismatched_inputs() { + JoinInlineBoundary::new_inputs_only(vec![ValueId(0), ValueId(1)], vec![ValueId(4)]); +} + +#[test] +fn test_jump_args_layout_rejects_expr_result_carrier_mismatch() { + let boundary = JoinInlineBoundary { + join_inputs: vec![], + host_inputs: vec![], + loop_invariants: vec![], + exit_bindings: vec![LoopExitBinding { + carrier_name: "result".to_string(), + join_exit_value: ValueId(10), + host_slot: ValueId(1), + role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState, + }], + condition_bindings: vec![], + expr_result: Some(ValueId(10)), + jump_args_layout: JumpArgsLayout::ExprResultPlusCarriers, + loop_var_name: None, + loop_header_func_name: None, + carrier_info: None, + continuation_func_ids: JoinInlineBoundary::default_continuations(), + exit_reconnect_mode: crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode::default(), + }; + + let err = boundary + .validate_jump_args_layout() + .expect_err("layout mismatch must fail-fast"); + assert!(err.contains("jump_args_layout_mismatch")); +} diff --git a/src/mir/join_ir/lowering/inline_boundary/types.rs b/src/mir/join_ir/lowering/inline_boundary/types.rs new file mode 100644 index 00000000..d189c8a1 --- /dev/null +++ b/src/mir/join_ir/lowering/inline_boundary/types.rs @@ -0,0 +1,176 @@ +use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierRole, ExitReconnectMode}; +use crate::mir::join_ir::lowering::condition_to_joinir::ConditionBinding; +use crate::mir::ValueId; +use std::collections::BTreeSet; + +/// Explicit binding between JoinIR exit value and host variable +/// +/// This structure formalizes the connection between a JoinIR exit PHI value +/// and the host variable it should update. This eliminates implicit assumptions +/// about which variable a ValueId represents. +/// +/// # Pattern 3 Example +/// +/// For `loop(i < 3) { sum = sum + i; i = i + 1 }`: +/// +/// ```text +/// LoopExitBinding { +/// carrier_name: "sum", +/// join_exit_value: ValueId(18), // k_exit's return value (JoinIR-local) +/// host_slot: ValueId(5), // variable_map["sum"] in host +/// } +/// ``` +/// +/// # Multi-Carrier Support (Pattern 4+) +/// +/// Multiple carriers can be represented as a vector: +/// +/// ```text +/// vec![ +/// LoopExitBinding { carrier_name: "sum", join_exit_value: ValueId(18), host_slot: ValueId(5), role: LoopState }, +/// LoopExitBinding { carrier_name: "count", join_exit_value: ValueId(19), host_slot: ValueId(6), role: LoopState }, +/// ] +/// ``` +#[derive(Debug, Clone)] +pub struct LoopExitBinding { + /// Carrier variable name (e.g., "sum", "count", "is_digit_pos") + /// + /// This is the variable name in the host's variable_map that should + /// receive the exit value. + pub carrier_name: String, + + /// JoinIR-side ValueId from k_exit (or exit parameter) + /// + /// This is the **JoinIR-local** ValueId that represents the exit value. + /// It will be remapped when merged into the host function. + pub join_exit_value: ValueId, + + /// Host-side variable_map slot to reconnect + /// + /// This is the host function's ValueId for the variable that should be + /// updated with the exit PHI result. + pub host_slot: ValueId, + + /// Phase 227: Role of this carrier (LoopState or ConditionOnly) + /// + /// Determines whether this carrier should participate in exit PHI: + /// - LoopState: Needs exit PHI (value used after loop) + /// - ConditionOnly: No exit PHI (only used in loop condition) + pub role: CarrierRole, +} + +/// Layout policy for JoinIR jump_args (SSOT) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JumpArgsLayout { + /// jump_args = [carriers...] + CarriersOnly, + /// jump_args = [expr_result, carriers...] + ExprResultPlusCarriers, +} + +/// Boundary information for inlining a JoinIR fragment into a host function +/// +/// This structure captures the "interface" between a JoinIR fragment and the +/// host function, allowing the merger to inject necessary Copy instructions +/// to connect the two SSA value spaces. +/// +/// # Design Note +/// +/// This is a **pure data structure** with no logic. All transformation logic +/// lives in the merger (merge_joinir_mir_blocks). +#[derive(Debug, Clone)] +pub struct JoinInlineBoundary { + /// JoinIR-local ValueIds that act as "input slots" + /// + /// These are the ValueIds used **inside** the JoinIR fragment to refer + /// to values that come from the host. They should be in the JoinValueSpace + /// Param region (100-999). (They are typically allocated sequentially.) + /// + /// Example: For a loop variable `i`, JoinIR uses ValueId(100) as the parameter. + pub join_inputs: Vec, + + /// Host-function ValueIds that provide the input values + /// + /// These are the ValueIds from the **host function** that correspond to + /// the join_inputs. The merger will inject Copy instructions to connect + /// host_inputs[i] → join_inputs[i]. + /// + /// Example: If host has `i` as ValueId(4), then host_inputs = [ValueId(4)]. + pub host_inputs: Vec, + + /// Phase 255 P2: Loop invariant variables + /// + /// Variables that are referenced inside the loop body but do not change + /// across iterations. These variables need header PHI nodes (with the same + /// value from all incoming edges) but do NOT need exit PHI nodes. + /// + /// # Format + /// + /// Each entry is `(variable_name, host_value_id)`. + pub loop_invariants: Vec<(String, ValueId)>, + + /// Explicit exit bindings for loop carriers (Phase 190+) + /// + /// Each binding explicitly names which variable is being updated and + /// where the value comes from. This eliminates ambiguity and prepares + /// for multi-carrier support. + pub exit_bindings: Vec, + + /// Phase 171-fix: Condition bindings with explicit JoinIR ValueIds + /// + /// Each binding explicitly specifies: + /// - Variable name + /// - HOST ValueId (source) + /// - JoinIR ValueId (destination) + /// + /// This replaces legacy condition-only input plumbing and ensures proper + /// ValueId separation. + pub condition_bindings: Vec, + + /// Phase 33-14: Expression result ValueId (JoinIR-local) + /// + /// If the loop is used as an expression (like `return loop(...)`), this field + /// contains the JoinIR-local ValueId of k_exit's return value. + /// + /// - `Some(ValueId)`: Loop returns a value → k_exit return goes to exit_phi_inputs + /// - `None`: Loop only updates carriers → no exit_phi_inputs generation + pub expr_result: Option, + + /// Phase 256 P1.12: jump_args layout (SSOT) + /// + /// This prevents merge from guessing whether jump_args contains a leading + /// expr_result slot. + pub jump_args_layout: JumpArgsLayout, + + /// Phase 33-16: Loop variable name (for LoopHeaderPhiBuilder) + /// + /// The name of the loop control variable (e.g., "i" in `loop(i < 3)`). + /// Used to track which PHI corresponds to the loop variable. + pub loop_var_name: Option, + + /// Phase 287 P2: Loop header function name (SSOT) + /// + /// Merge must not guess the loop header function from "first non-main non-continuation". + /// For loop patterns, set this explicitly (typically "loop_step"). + pub loop_header_func_name: Option, + + /// Phase 228: Carrier metadata (for header PHI generation) + /// + /// Contains full carrier information including initialization policies. + /// This allows header PHI generation to handle ConditionOnly carriers + /// with explicit bool initialization. + pub carrier_info: Option, + + /// Phase 132 P1: Continuation contract (SSOT) + /// Phase 256 P1.7: Changed from BTreeSet to BTreeSet + /// + /// JoinIR merge must not infer/guess continuation functions. The router/lowerer + /// must declare continuation function names here. + pub continuation_func_ids: BTreeSet, + + /// Phase 131 P1.5: Exit reconnection mode + /// + /// Controls whether exit values are reconnected via PHI generation (Phi) + /// or direct variable_map update (DirectValue). + pub exit_reconnect_mode: ExitReconnectMode, +} diff --git a/src/mir/join_ir/lowering/inline_boundary_builder.rs b/src/mir/join_ir/lowering/inline_boundary_builder.rs index dfbe207e..bf9835ac 100644 --- a/src/mir/join_ir/lowering/inline_boundary_builder.rs +++ b/src/mir/join_ir/lowering/inline_boundary_builder.rs @@ -87,13 +87,8 @@ impl JoinInlineBoundaryBuilder { boundary: JoinInlineBoundary { join_inputs: vec![], host_inputs: vec![], - join_outputs: vec![], - #[allow(deprecated)] - host_outputs: vec![], loop_invariants: vec![], // Phase 255 P2: Initialize as empty exit_bindings: vec![], - #[allow(deprecated)] - condition_inputs: vec![], condition_bindings: vec![], expr_result: None, jump_args_layout: JumpArgsLayout::CarriersOnly, @@ -127,19 +122,6 @@ impl JoinInlineBoundaryBuilder { self } - /// Set output mappings (JoinIR-local ↔ Host ValueIds) (DEPRECATED) - /// - /// **DEPRECATED**: Use `with_exit_bindings` instead for explicit carrier naming. - #[deprecated(since = "Phase 200-2", note = "Use with_exit_bindings instead")] - pub fn with_outputs(mut self, join_outputs: Vec, host_outputs: Vec) -> Self { - self.boundary.join_outputs = join_outputs; - #[allow(deprecated)] - { - self.boundary.host_outputs = host_outputs; - } - self - } - /// Set condition bindings (Phase 171-fix) /// /// Each binding explicitly maps: @@ -415,7 +397,6 @@ mod tests { assert_eq!(boundary.join_inputs, vec![ValueId(0)]); assert_eq!(boundary.host_inputs, vec![ValueId(4)]); - assert_eq!(boundary.join_outputs.len(), 0); assert_eq!(boundary.exit_bindings.len(), 0); assert_eq!(boundary.condition_bindings.len(), 0); assert_eq!(boundary.expr_result, None); diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal.rs b/src/mir/join_ir/lowering/loop_with_break_minimal.rs index 7ee3b824..30404e36 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal.rs @@ -56,21 +56,18 @@ //! Following the "80/20 rule" from CLAUDE.md - get it working first, generalize later. use crate::ast::ASTNode; +mod body_local_init; mod boundary_builder; +mod carrier_update; mod header_break_lowering; +mod tail_builder; #[cfg(test)] mod tests; use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, JoinFragmentMeta}; -use crate::mir::join_ir::lowering::carrier_update_emitter::{ - emit_carrier_update, emit_carrier_update_with_env, -}; -// Phase 92 P2-1: Import ConditionalStep emitter from dedicated module -use crate::mir::join_ir::lowering::common::conditional_step_emitter::emit_conditional_step_update; use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe; -use crate::mir::join_ir::lowering::common::balanced_depth_scan_emitter::{BalancedDepthScanEmitter, BalancedDepthScanRecipe}; +use crate::mir::join_ir::lowering::common::balanced_depth_scan_emitter::BalancedDepthScanRecipe; use crate::mir::join_ir::lowering::condition_to_joinir::ConditionEnv; -use crate::mir::loop_canonicalizer::UpdateKind; use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; @@ -79,12 +76,8 @@ use crate::mir::join_ir::lowering::step_schedule::{ build_pattern2_schedule_from_decision, decide_pattern2_schedule, Pattern2ScheduleFactsBox, Pattern2StepKind, }; -use crate::mir::join_ir::lowering::update_env::UpdateEnv; use crate::mir::loop_canonicalizer::LoopSkeleton; -use crate::mir::join_ir::{ - BinOpKind, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, - UnaryOp, -}; +use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, UnaryOp}; use crate::mir::loop_pattern_detection::error_messages::{ extract_body_local_names, }; @@ -92,8 +85,11 @@ use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScope use crate::mir::ValueId; use crate::mir::join_ir::lowering::error_tags; use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; +use body_local_init::emit_body_local_inits; use boundary_builder::build_fragment_meta; +use carrier_update::{emit_carrier_updates, CarrierUpdateResult}; use header_break_lowering::{lower_break_condition, lower_header_condition}; +use tail_builder::emit_tail_call; use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism pub(crate) struct LoopWithBreakLoweringInputs<'a> { @@ -416,7 +412,6 @@ pub(crate) fn lower_loop_with_break_minimal( let mut break_block: Vec = Vec::new(); let mut carrier_update_block: Vec = Vec::new(); let mut tail_block: Vec = Vec::new(); - let mut loop_var_next_override: Option = None; // Phase 94: conditional loop-var step (P5b) // ------------------------------------------------------------------ // Natural Exit Condition Check (Phase 169: from AST) @@ -458,72 +453,17 @@ pub(crate) fn lower_loop_with_break_minimal( // // Lower body-local variable initialization expressions to JoinIR // This must happen BEFORE break condition AND carrier updates since both may reference body-locals - if let Some(ref mut body_env) = body_local_env { - use crate::mir::join_ir::lowering::loop_body_local_init::LoopBodyLocalInitLowerer; - - // Create a mutable reference to the instruction buffer - // Phase 256.6: Pass current_static_box_name for me.method() resolution - let mut init_lowerer = - LoopBodyLocalInitLowerer::new( - env, - &mut body_init_block, - Box::new(&mut alloc_local_fn), - current_static_box_name.clone(), - ); - - init_lowerer.lower_inits_for_loop(body_ast, body_env)?; - - dev_log.log_if_enabled(|| { - format!( - "Phase 191/246-EX: Lowered {} body-local init expressions (scheduled block before break)", - body_env.len() - ) - }); - - // Phase 93 P0: Drop init_lowerer to release borrows before ConditionOnly emission - drop(init_lowerer); - - // Phase 93 P0: Emit ConditionOnly variable recalculation after body-local init - // Note: We emit instructions into body_init_block here, but we cannot update env - // since it's immutable. This is OK because the recalculated value will be available - // through body_init_block instructions, and the break condition will reference it - // through the body_local_env lookup mechanism (see dual_value_rewriter). - if let Some(recipe) = condition_only_recipe { - use crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyEmitter; - // Create a temporary env for the emitter to insert into - // The actual value lookup will happen through body_local_env in break condition lowering - let mut temp_env = env.clone(); - - let condition_value = ConditionOnlyEmitter::emit_condition_only_recalc( - recipe, - body_env, - &mut temp_env, - &mut alloc_local_fn, - &mut body_init_block, - )?; - - // Phase 93 P0: Register the derived value in body_local_env so break condition can find it - body_env.insert(recipe.name.clone(), condition_value); - - dev_log.log_if_enabled(|| { - format!( - "Phase 93 P0: Recalculated ConditionOnly variable '{}' → {:?} (registered in body_local_env)", - recipe.name, condition_value - ) - }); - } - - // Phase 107: Balanced depth-scan derived vars (depth_delta/depth_next) - if let Some(recipe) = balanced_depth_scan_recipe { - BalancedDepthScanEmitter::emit_derived( - recipe, - body_env, - env, - &mut alloc_local_fn, - &mut body_init_block, - )?; - } - } + emit_body_local_inits( + env, + body_ast, + body_local_env.as_deref_mut(), + condition_only_recipe, + balanced_depth_scan_recipe, + current_static_box_name.clone(), + &mut alloc_local_fn, + &mut body_init_block, + &dev_log, + )?; // ------------------------------------------------------------------ // Phase 170-B / Phase 244 / Phase 92 P2-2 / Phase 252: Lower break condition @@ -568,226 +508,31 @@ pub(crate) fn lower_loop_with_break_minimal( cond: Some(break_cond_value), // Phase 170-B: Use lowered condition }); - // ------------------------------------------------------------------ - // Phase 94: P5b escape derived body-local + conditional loop-var update - // ------------------------------------------------------------------ - if let (Some(recipe), Some(ref mut body_env)) = (body_local_derived_recipe, body_local_env.as_mut()) - { - use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedEmitter; - let emission = BodyLocalDerivedEmitter::emit( - recipe, - &mut alloc_local_fn, - env, - body_env, - &mut carrier_update_block, - )?; - loop_var_next_override = Some(emission.loop_counter_next); - dev_log.log_if_enabled(|| { - format!( - "[phase94/body_local_derived] enabled: name='{}', loop_counter='{}', loop_counter_next={:?}", - recipe.name, recipe.loop_counter_name, emission.loop_counter_next - ) - }); - } + let CarrierUpdateResult { + updated_carrier_values, + loop_var_next_override, + } = emit_carrier_updates( + env, + carrier_info, + carrier_updates, + body_local_env.as_deref_mut(), + body_local_derived_recipe, + skeleton, + &carrier_param_ids, + &mut alloc_local_fn, + &mut carrier_update_block, + &dev_log, + )?; - // ------------------------------------------------------------------ - // Loop Body: Compute updated values for all carriers - // ------------------------------------------------------------------ - // Phase 176-3: Multi-carrier support - emit updates for all carriers - let mut updated_carrier_values: Vec = Vec::new(); - - for (idx, carrier) in carrier_info.carriers.iter().enumerate() { - let carrier_name = &carrier.name; - - // Phase 247-EX: Loop-local derived carriers (e.g., digit_value) take the body-local - // computed value (digit_pos) as their update source each iteration. - if carrier.init == CarrierInit::LoopLocalZero { - use crate::mir::join_ir::lowering::common::dual_value_rewriter::try_derive_looplocal_from_bodylocal_pos; - if let Some(src_val) = try_derive_looplocal_from_bodylocal_pos( - carrier_name, - body_local_env.as_ref().map(|e| &**e), - ) { - updated_carrier_values.push(src_val); - continue; - } - } - - // Phase 227: ConditionOnly carriers don't have update expressions - // They just pass through their current value unchanged - // Phase 247-EX: FromHost carriers (e.g., digit_value when truly from host) also passthrough - // They're initialized from loop body and used in update expressions but not updated themselves - use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole}; - if carrier.role == CarrierRole::ConditionOnly { - // Phase 247-EX: If this is a promoted digit_pos boolean carrier, derive from body-local digit_pos - use crate::mir::join_ir::lowering::common::dual_value_rewriter::try_derive_conditiononly_is_from_bodylocal_pos; - if let Some(cmp) = try_derive_conditiononly_is_from_bodylocal_pos( - carrier_name, - body_local_env.as_ref().map(|e| &**e), - &mut alloc_local_fn, - &mut carrier_update_block, - ) { - updated_carrier_values.push(cmp); - continue; - } - - // ConditionOnly carrier fallback: just pass through the current value - // The carrier's ValueId from env is passed unchanged - let current_value = env.get(carrier_name).ok_or_else(|| { - format!("ConditionOnly carrier '{}' not found in env", carrier_name) - })?; - updated_carrier_values.push(current_value); - dev_log.log_if_enabled(|| { - format!( - "[carrier_update] Phase 227: ConditionOnly '{}' passthrough: {:?}", - carrier_name, current_value - ) - }); - continue; - } - - // Phase 247-EX: FromHost carriers passthrough (no update expressions) - // FromHost carriers (e.g., digit_value) are initialized from loop body (indexOf result) - // and used in update expressions, but not updated themselves. - // They're already in env (added by Phase 176-5), so pass through from there. - if carrier.init == CarrierInit::FromHost && !carrier_updates.contains_key(carrier_name) { - // FromHost carrier without update: pass through current value from env - let current_value = env - .get(carrier_name) - .ok_or_else(|| format!("FromHost carrier '{}' not found in env", carrier_name))?; - updated_carrier_values.push(current_value); - dev_log.log_if_enabled(|| { - format!( - "[carrier_update] Phase 247-EX: FromHost '{}' passthrough: {:?}", - carrier_name, current_value - ) - }); - continue; - } - - // Phase 92 P2-1: Check if skeleton has ConditionalStep for this carrier - if let Some(skel) = skeleton { - if let Some(carrier_slot) = skel.carriers.iter().find(|c| c.name == *carrier_name) { - if let UpdateKind::ConditionalStep { cond, then_delta, else_delta } = &carrier_slot.update_kind { - // Phase 92 P2-1: Use ConditionalStepEmitter (dedicated module) - dev_log.log_if_enabled(|| { - format!( - "Phase 92 P2-1: ConditionalStep detected for carrier '{}': then={}, else={}", - carrier_name, then_delta, else_delta - ) - }); - - // Phase 92 P2-1: Get carrier parameter ValueId (must be set by header PHI) - let carrier_param = carrier.join_id.ok_or_else(|| { - format!( - "[pattern2/conditional_step] Carrier '{}' join_id not set (header PHI not generated?)", - carrier_name - ) - })?; - - // Phase 92 P2-2: Pass body_local_env for condition lowering - let updated_value = emit_conditional_step_update( - carrier_name, - carrier_param, - &*cond, - *then_delta, - *else_delta, - &mut alloc_local_fn, - env, - body_local_env.as_ref().map(|e| &**e), // Phase 92 P2-2 - &mut carrier_update_block, - ).map_err(|e| format!("[pattern2/conditional_step] {}", e))?; - updated_carrier_values.push(updated_value); - dev_log.log_if_enabled(|| { - format!( - "Phase 92 P2-1: ConditionalStep carrier '{}' updated -> {:?}", - carrier_name, updated_value - ) - }); - continue; // Skip normal carrier update - } - } - } - - // Get the update expression for this carrier - let update_expr = carrier_updates.get(carrier_name).ok_or_else(|| { - format!( - "No update expression found for carrier '{}' in carrier_updates map", - carrier_name - ) - })?; - - // Phase 185-2: Emit carrier update with body-local support - // Phase 247-EX: Pass promoted_loopbodylocals for dual-value carrier resolution - let updated_value = if let Some(ref body_env) = body_local_env { - // Use UpdateEnv for body-local variable resolution - let update_env = UpdateEnv::new(env, body_env, &carrier_info.promoted_loopbodylocals); - emit_carrier_update_with_env( - carrier, - update_expr, - &mut alloc_local_fn, - &update_env, - &mut carrier_update_block, - )? - } else { - // Backward compatibility: use ConditionEnv directly - emit_carrier_update( - carrier, - update_expr, - &mut alloc_local_fn, - env, - &mut carrier_update_block, - )? - }; - - updated_carrier_values.push(updated_value); - - dev_log.log_if_enabled(|| { - format!( - "Phase 176-3: Carrier '{}' update: {:?} -> {:?}", - carrier_name, carrier_param_ids[idx], updated_value - ) - }); - } - - // Phase 176-3: Multi-carrier support - tail call includes all updated carriers - // Call(loop_step, [i_next, carrier1_next, carrier2_next, ...]) // tail recursion - // Phase 94: i_next may be overridden (escape skip: +2 vs +1). - let i_next = if let Some(i_next) = loop_var_next_override { - i_next - } else { - let const_1 = alloc_local_fn(); - tail_block.push(JoinInst::Compute(MirLikeInst::Const { - dst: const_1, - value: ConstValue::Integer(1), - })); - - let i_next = alloc_local_fn(); - tail_block.push(JoinInst::Compute(MirLikeInst::BinOp { - dst: i_next, - op: BinOpKind::Add, - lhs: i_param, - rhs: const_1, - })); - i_next - }; - - let mut tail_call_args = vec![i_next]; - tail_call_args.extend(updated_carrier_values.iter().copied()); - - dev_log.log_if_enabled(|| { - format!( - "tail call args count: {}, updated_carrier_values: {:?}", - tail_call_args.len(), - updated_carrier_values - ) - }); - - tail_block.push(JoinInst::Call { - func: loop_step_id, - args: tail_call_args, - k_next: None, // CRITICAL: None for tail call - dst: None, - }); + emit_tail_call( + loop_step_id, + i_param, + &updated_carrier_values, + loop_var_next_override, + &mut alloc_local_fn, + &mut tail_block, + &dev_log, + ); // Apply scheduled order to assemble the loop_step body. for step in schedule.iter() { @@ -854,8 +599,8 @@ pub(crate) fn lower_loop_with_break_minimal( dev_log.log_simple("Generated JoinIR for Loop with Break Pattern (Phase 170-B)"); dev_log.log_simple("Functions: main, loop_step, k_exit"); - dev_log.log_simple("Loop condition from AST (delegated to condition_to_joinir)"); - dev_log.log_simple("Break condition from AST (delegated to condition_to_joinir)"); + dev_log.log_simple("Loop condition from AST (ConditionLoweringBox)"); + dev_log.log_simple("Break condition from AST (ConditionLoweringBox)"); dev_log.log_simple("Exit PHI: k_exit receives i from both natural exit and break"); let fragment_meta = build_fragment_meta(carrier_info, loop_var_name, i_exit, &carrier_exit_ids); diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal/body_local_init.rs b/src/mir/join_ir/lowering/loop_with_break_minimal/body_local_init.rs new file mode 100644 index 00000000..1ff1c8ce --- /dev/null +++ b/src/mir/join_ir/lowering/loop_with_break_minimal/body_local_init.rs @@ -0,0 +1,83 @@ +use crate::ast::ASTNode; +use crate::mir::join_ir::lowering::common::balanced_depth_scan_emitter::{ + BalancedDepthScanEmitter, BalancedDepthScanRecipe, +}; +use crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe; +use crate::mir::join_ir::lowering::condition_env::ConditionEnv; +use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; +use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; +use crate::mir::join_ir::JoinInst; +use crate::mir::ValueId; + +pub(crate) fn emit_body_local_inits( + env: &ConditionEnv, + body_ast: &[ASTNode], + body_local_env: Option<&mut LoopBodyLocalEnv>, + condition_only_recipe: Option<&ConditionOnlyRecipe>, + balanced_depth_scan_recipe: Option<&BalancedDepthScanRecipe>, + current_static_box_name: Option, + alloc_local_fn: &mut F, + body_init_block: &mut Vec, + dev_log: &DebugOutputBox, +) -> Result<(), String> +where + F: FnMut() -> ValueId, +{ + let Some(body_env) = body_local_env else { + return Ok(()); + }; + + use crate::mir::join_ir::lowering::loop_body_local_init::LoopBodyLocalInitLowerer; + + let mut init_lowerer = LoopBodyLocalInitLowerer::new( + env, + body_init_block, + Box::new(&mut *alloc_local_fn), + current_static_box_name, + ); + + init_lowerer.lower_inits_for_loop(body_ast, body_env)?; + + dev_log.log_if_enabled(|| { + format!( + "Phase 191/246-EX: Lowered {} body-local init expressions (scheduled block before break)", + body_env.len() + ) + }); + + drop(init_lowerer); + + if let Some(recipe) = condition_only_recipe { + use crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyEmitter; + let mut temp_env = env.clone(); + + let condition_value = ConditionOnlyEmitter::emit_condition_only_recalc( + recipe, + body_env, + &mut temp_env, + alloc_local_fn, + body_init_block, + )?; + + body_env.insert(recipe.name.clone(), condition_value); + + dev_log.log_if_enabled(|| { + format!( + "Phase 93 P0: Recalculated ConditionOnly variable '{}' → {:?} (registered in body_local_env)", + recipe.name, condition_value + ) + }); + } + + if let Some(recipe) = balanced_depth_scan_recipe { + BalancedDepthScanEmitter::emit_derived( + recipe, + body_env, + env, + alloc_local_fn, + body_init_block, + )?; + } + + Ok(()) +} diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal/carrier_update.rs b/src/mir/join_ir/lowering/loop_with_break_minimal/carrier_update.rs new file mode 100644 index 00000000..a90cd8c1 --- /dev/null +++ b/src/mir/join_ir/lowering/loop_with_break_minimal/carrier_update.rs @@ -0,0 +1,198 @@ +use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit, CarrierRole}; +use crate::mir::join_ir::lowering::common::body_local_derived_emitter::{ + BodyLocalDerivedEmitter, BodyLocalDerivedRecipe, +}; +use crate::mir::join_ir::lowering::common::dual_value_rewriter::{ + try_derive_conditiononly_is_from_bodylocal_pos, try_derive_looplocal_from_bodylocal_pos, +}; +use crate::mir::join_ir::lowering::common::conditional_step_emitter::emit_conditional_step_update; +use crate::mir::join_ir::lowering::condition_to_joinir::ConditionEnv; +use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; +use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; +use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; +use crate::mir::join_ir::lowering::update_env::UpdateEnv; +use crate::mir::join_ir::lowering::{carrier_update_emitter}; +use crate::mir::join_ir::JoinInst; +use crate::mir::loop_canonicalizer::UpdateKind; +use crate::mir::loop_canonicalizer::LoopSkeleton; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +pub(crate) struct CarrierUpdateResult { + pub updated_carrier_values: Vec, + pub loop_var_next_override: Option, +} + +pub(crate) fn emit_carrier_updates( + env: &ConditionEnv, + carrier_info: &CarrierInfo, + carrier_updates: &BTreeMap, + mut body_local_env: Option<&mut LoopBodyLocalEnv>, + body_local_derived_recipe: Option<&BodyLocalDerivedRecipe>, + skeleton: Option<&LoopSkeleton>, + carrier_param_ids: &[ValueId], + alloc_local_fn: &mut dyn FnMut() -> ValueId, + carrier_update_block: &mut Vec, + dev_log: &DebugOutputBox, +) -> Result { + let mut loop_var_next_override: Option = None; + + if let (Some(recipe), Some(body_env)) = (body_local_derived_recipe, body_local_env.as_deref_mut()) { + let emission = BodyLocalDerivedEmitter::emit( + recipe, + alloc_local_fn, + env, + body_env, + carrier_update_block, + )?; + loop_var_next_override = Some(emission.loop_counter_next); + dev_log.log_if_enabled(|| { + format!( + "[phase94/body_local_derived] enabled: name='{}', loop_counter='{}', loop_counter_next={:?}", + recipe.name, recipe.loop_counter_name, emission.loop_counter_next + ) + }); + } + + let body_env_ref = body_local_env.as_deref(); + debug_assert_eq!(carrier_param_ids.len(), carrier_info.carriers.len()); + + let mut updated_carrier_values: Vec = Vec::new(); + + for (idx, carrier) in carrier_info.carriers.iter().enumerate() { + let carrier_name = &carrier.name; + + if carrier.init == CarrierInit::LoopLocalZero { + if let Some(src_val) = + try_derive_looplocal_from_bodylocal_pos(carrier_name, body_env_ref) + { + updated_carrier_values.push(src_val); + continue; + } + } + + if carrier.role == CarrierRole::ConditionOnly { + if let Some(cmp) = try_derive_conditiononly_is_from_bodylocal_pos( + carrier_name, + body_env_ref, + alloc_local_fn, + carrier_update_block, + ) { + updated_carrier_values.push(cmp); + continue; + } + + let current_value = env.get(carrier_name).ok_or_else(|| { + format!("ConditionOnly carrier '{}' not found in env", carrier_name) + })?; + updated_carrier_values.push(current_value); + dev_log.log_if_enabled(|| { + format!( + "[carrier_update] Phase 227: ConditionOnly '{}' passthrough: {:?}", + carrier_name, current_value + ) + }); + continue; + } + + if carrier.init == CarrierInit::FromHost && !carrier_updates.contains_key(carrier_name) { + let current_value = env + .get(carrier_name) + .ok_or_else(|| format!("FromHost carrier '{}' not found in env", carrier_name))?; + updated_carrier_values.push(current_value); + dev_log.log_if_enabled(|| { + format!( + "[carrier_update] Phase 247-EX: FromHost '{}' passthrough: {:?}", + carrier_name, current_value + ) + }); + continue; + } + + if let Some(skel) = skeleton { + if let Some(carrier_slot) = skel.carriers.iter().find(|c| c.name == *carrier_name) { + if let UpdateKind::ConditionalStep { + cond, + then_delta, + else_delta, + } = &carrier_slot.update_kind + { + dev_log.log_if_enabled(|| { + format!( + "Phase 92 P2-1: ConditionalStep detected for carrier '{}': then={}, else={}", + carrier_name, then_delta, else_delta + ) + }); + + let carrier_param = carrier.join_id.ok_or_else(|| { + format!( + "[pattern2/conditional_step] Carrier '{}' join_id not set (header PHI not generated?)", + carrier_name + ) + })?; + + let updated_value = emit_conditional_step_update( + carrier_name, + carrier_param, + &*cond, + *then_delta, + *else_delta, + alloc_local_fn, + env, + body_env_ref, + carrier_update_block, + ) + .map_err(|e| format!("[pattern2/conditional_step] {}", e))?; + updated_carrier_values.push(updated_value); + dev_log.log_if_enabled(|| { + format!( + "Phase 92 P2-1: ConditionalStep carrier '{}' updated -> {:?}", + carrier_name, updated_value + ) + }); + continue; + } + } + } + + let update_expr = carrier_updates.get(carrier_name).ok_or_else(|| { + format!( + "No update expression found for carrier '{}' in carrier_updates map", + carrier_name + ) + })?; + + let updated_value = if let Some(body_env) = body_env_ref { + let update_env = UpdateEnv::new(env, body_env, &carrier_info.promoted_loopbodylocals); + carrier_update_emitter::emit_carrier_update_with_env( + carrier, + update_expr, + alloc_local_fn, + &update_env, + carrier_update_block, + )? + } else { + carrier_update_emitter::emit_carrier_update( + carrier, + update_expr, + alloc_local_fn, + env, + carrier_update_block, + )? + }; + + updated_carrier_values.push(updated_value); + + dev_log.log_if_enabled(|| { + format!( + "Phase 176-3: Carrier '{}' update: {:?} -> {:?}", + carrier_name, carrier_param_ids[idx], updated_value + ) + }); + } + + Ok(CarrierUpdateResult { + updated_carrier_values, + loop_var_next_override, + }) +} diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal/header_break_lowering.rs b/src/mir/join_ir/lowering/loop_with_break_minimal/header_break_lowering.rs index db68ec29..f19b6fb1 100644 --- a/src/mir/join_ir/lowering/loop_with_break_minimal/header_break_lowering.rs +++ b/src/mir/join_ir/lowering/loop_with_break_minimal/header_break_lowering.rs @@ -3,8 +3,8 @@ use crate::mir::builder::MirBuilder; use crate::mir::join_ir::lowering::carrier_info::CarrierInfo; use crate::mir::join_ir::lowering::condition_env::ConditionEnv; use crate::mir::join_ir::lowering::condition_lowering_box::ConditionContext; -use crate::mir::join_ir::lowering::condition_to_joinir::lower_condition_to_joinir_no_body_locals; use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; +use crate::mir::join_ir::lowering::error_tags; use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer}; use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager; @@ -54,44 +54,41 @@ pub(crate) fn lower_header_condition( carrier_info, ); - if ExprLowerer::::is_supported_condition(condition) { - let mut dummy_builder = MirBuilder::new(); - let mut expr_lowerer = - ExprLowerer::new(&scope_manager, ExprContext::Condition, &mut dummy_builder); - - let mut context = ConditionContext { - loop_var_name: loop_var_name.to_string(), - loop_var_id, - scope: &scope_manager, - alloc_value, - current_static_box_name: current_static_box_name.map(|s| s.to_string()), // Phase 252 - }; - - match expr_lowerer.lower_condition(condition, &mut context) { - Ok(value_id) => { - let instructions = expr_lowerer.take_last_instructions(); - debug.log( - "phase244", - &format!( - "Header condition via ConditionLoweringBox: {} instructions", - instructions.len() - ), - ); - Ok((value_id, instructions)) - } - Err(e) => Err(format!( - "[joinir/pattern2/phase244] ConditionLoweringBox failed on supported condition: {:?}", - e - )), - } - } else { - debug.log( - "phase244", - "Header condition via legacy path (not yet supported by ConditionLoweringBox)", - ); - let mut shim = || alloc_value(); - lower_condition_to_joinir_no_body_locals(condition, &mut shim, env) // Phase 92 P2-2: No body-local for header + if !ExprLowerer::::is_supported_condition(condition) { + return Err(error_tags::lowering_error( + "pattern2/header_condition", + "ConditionLoweringBox does not support this condition (legacy path removed)", + )); } + + let mut dummy_builder = MirBuilder::new(); + let mut expr_lowerer = + ExprLowerer::new(&scope_manager, ExprContext::Condition, &mut dummy_builder); + + let mut context = ConditionContext { + loop_var_name: loop_var_name.to_string(), + loop_var_id, + scope: &scope_manager, + alloc_value, + current_static_box_name: current_static_box_name.map(|s| s.to_string()), // Phase 252 + }; + + let value_id = expr_lowerer.lower_condition(condition, &mut context).map_err(|e| { + format!( + "[joinir/pattern2/phase244] ConditionLoweringBox failed on supported condition: {:?}", + e + ) + })?; + + let instructions = expr_lowerer.take_last_instructions(); + debug.log( + "phase244", + &format!( + "Header condition via ConditionLoweringBox: {} instructions", + instructions.len() + ), + ); + Ok((value_id, instructions)) } /// Lower the break condition via ExprLowerer (no legacy fallback). diff --git a/src/mir/join_ir/lowering/loop_with_break_minimal/tail_builder.rs b/src/mir/join_ir/lowering/loop_with_break_minimal/tail_builder.rs new file mode 100644 index 00000000..44058ae3 --- /dev/null +++ b/src/mir/join_ir/lowering/loop_with_break_minimal/tail_builder.rs @@ -0,0 +1,52 @@ +use crate::mir::join_ir::{BinOpKind, ConstValue, JoinFuncId, JoinInst, MirLikeInst}; +use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; +use crate::mir::ValueId; + +pub(crate) fn emit_tail_call( + loop_step_id: JoinFuncId, + i_param: ValueId, + updated_carrier_values: &[ValueId], + loop_var_next_override: Option, + alloc_local_fn: &mut dyn FnMut() -> ValueId, + tail_block: &mut Vec, + dev_log: &DebugOutputBox, +) -> ValueId { + let i_next = if let Some(i_next) = loop_var_next_override { + i_next + } else { + let const_1 = alloc_local_fn(); + tail_block.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_1, + value: ConstValue::Integer(1), + })); + + let i_next = alloc_local_fn(); + tail_block.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: i_next, + op: BinOpKind::Add, + lhs: i_param, + rhs: const_1, + })); + i_next + }; + + let mut tail_call_args = vec![i_next]; + tail_call_args.extend(updated_carrier_values.iter().copied()); + + dev_log.log_if_enabled(|| { + format!( + "tail call args count: {}, updated_carrier_values: {:?}", + tail_call_args.len(), + updated_carrier_values + ) + }); + + tail_block.push(JoinInst::Call { + func: loop_step_id, + args: tail_call_args, + k_next: None, + dst: None, + }); + + i_next +} diff --git a/src/mir/join_ir/lowering/loop_with_continue_minimal/carrier_updates.rs b/src/mir/join_ir/lowering/loop_with_continue_minimal/carrier_updates.rs new file mode 100644 index 00000000..b117a274 --- /dev/null +++ b/src/mir/join_ir/lowering/loop_with_continue_minimal/carrier_updates.rs @@ -0,0 +1,174 @@ +use crate::mir::join_ir::lowering::carrier_info::CarrierInfo; +use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; +use crate::mir::join_ir::lowering::loop_update_analyzer::{UpdateExpr, UpdateRhs}; +use crate::mir::join_ir::{BinOpKind, JoinInst, MirLikeInst}; +use crate::mir::ValueId; +use std::collections::BTreeMap; + +pub(crate) fn emit_carrier_updates( + carrier_info: &CarrierInfo, + carrier_updates: &BTreeMap, + carrier_param_ids: &[ValueId], + carrier_next_ids: &[ValueId], + carrier_merged_ids: &[ValueId], + continue_cond: ValueId, + const_1: ValueId, + i_next: ValueId, + debug: &DebugOutputBox, + body: &mut Vec, +) -> Result<(), String> { + let carrier_count = carrier_info.carriers.len(); + debug_assert_eq!(carrier_param_ids.len(), carrier_count); + debug_assert_eq!(carrier_next_ids.len(), carrier_count); + debug_assert_eq!(carrier_merged_ids.len(), carrier_count); + + for idx in 0..carrier_count { + let carrier_param = carrier_param_ids[idx]; + let carrier_next = carrier_next_ids[idx]; + let carrier_merged = carrier_merged_ids[idx]; + let carrier_name = &carrier_info.carriers[idx].name; + + debug.log( + "carrier_update", + &format!("Processing carrier '{}' (idx={})", carrier_name, idx), + ); + let rhs = if let Some(update_expr) = carrier_updates.get(carrier_name) { + debug.log( + "carrier_update", + &format!("Found update expr: {:?}", update_expr), + ); + match update_expr { + UpdateExpr::BinOp { op, rhs, .. } => { + if *op != BinOpKind::Add { + debug.log( + "warn", + &format!( + "Carrier '{}' uses unsupported operator {:?}, defaulting to Add", + carrier_name, op + ), + ); + } + + match rhs { + UpdateRhs::Const(n) => { + if *n == 1 { + const_1 + } else { + debug.log( + "warn", + &format!( + "Carrier '{}' uses const {}, only const_1 is pre-allocated, using const_1", + carrier_name, n + ), + ); + const_1 + } + } + UpdateRhs::Variable(var_name) => { + if var_name == &carrier_info.loop_var_name { + debug.log( + "carrier_update", + &format!( + "Using i_next (ValueId({})) for variable '{}'", + i_next.0, var_name + ), + ); + i_next + } else { + debug.log( + "warn", + &format!( + "Carrier '{}' updates with unknown variable '{}', using const_1", + carrier_name, var_name + ), + ); + const_1 + } + } + UpdateRhs::NumberAccumulation { .. } => { + debug.log( + "phase190", + &format!( + "Carrier '{}' has number accumulation - not supported in Pattern 4, using Select passthrough", + carrier_name + ), + ); + body.push(JoinInst::Select { + dst: carrier_merged, + cond: continue_cond, + then_val: carrier_param, + else_val: carrier_param, + type_hint: None, + }); + continue; + } + UpdateRhs::StringLiteral(_) | UpdateRhs::Other => { + debug.log( + "phase178", + &format!( + "Carrier '{}' has string/complex update - skipping BinOp, using Select passthrough", + carrier_name + ), + ); + body.push(JoinInst::Select { + dst: carrier_merged, + cond: continue_cond, + then_val: carrier_param, + else_val: carrier_param, + type_hint: None, + }); + continue; + } + } + } + UpdateExpr::Const(n) => { + if *n == 1 { + const_1 + } else { + debug.log( + "warn", + &format!( + "Carrier '{}' uses const {}, only const_1 is pre-allocated, using const_1", + carrier_name, n + ), + ); + const_1 + } + } + } + } else { + debug.log( + "warn", + &format!( + "No update expression for carrier '{}', defaulting to +1", + carrier_name + ), + ); + const_1 + }; + + debug.log( + "carrier_update", + &format!( + "Generating: ValueId({}) = ValueId({}) + ValueId({})", + carrier_next.0, carrier_param.0, rhs.0 + ), + ); + body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: carrier_next, + op: BinOpKind::Add, + lhs: carrier_param, + rhs, + })); + + body.push(JoinInst::Select { + dst: carrier_merged, + cond: continue_cond, + then_val: carrier_param, + else_val: carrier_next, + type_hint: None, + }); + } + + Ok(()) +} diff --git a/src/mir/join_ir/lowering/loop_with_continue_minimal/condition_lowering.rs b/src/mir/join_ir/lowering/loop_with_continue_minimal/condition_lowering.rs new file mode 100644 index 00000000..23087da1 --- /dev/null +++ b/src/mir/join_ir/lowering/loop_with_continue_minimal/condition_lowering.rs @@ -0,0 +1,86 @@ +use crate::ast::ASTNode; +use crate::mir::builder::MirBuilder; +use crate::mir::join_ir::lowering::carrier_info::CarrierInfo; +use crate::mir::join_ir::lowering::condition_env::ConditionEnv; +use crate::mir::join_ir::lowering::condition_lowering_box::{ + ConditionContext, ConditionLoweringBox, +}; +use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; +use crate::mir::join_ir::lowering::error_tags; +use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer}; +use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; +use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager; +use crate::mir::join_ir::JoinInst; +use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv; +use crate::mir::ValueId; + +fn make_scope_manager<'a>( + condition_env: &'a ConditionEnv, + body_local_env: Option<&'a LoopBodyLocalEnv>, + captured_env: Option<&'a CapturedEnv>, + carrier_info: &'a CarrierInfo, +) -> Pattern2ScopeManager<'a> { + Pattern2ScopeManager { + condition_env, + loop_body_local_env: body_local_env, + captured_env, + carrier_info, + } +} + +pub(crate) fn lower_header_condition( + condition: &ASTNode, + env: &ConditionEnv, + carrier_info: &CarrierInfo, + loop_var_name: &str, + loop_var_id: ValueId, + alloc_value: &mut dyn FnMut() -> ValueId, + current_static_box_name: Option<&str>, + debug: &DebugOutputBox, +) -> Result<(ValueId, Vec), String> { + let empty_body_env = LoopBodyLocalEnv::new(); + let empty_captured_env = CapturedEnv::new(); + let scope_manager = make_scope_manager( + env, + Some(&empty_body_env), + Some(&empty_captured_env), + carrier_info, + ); + + if !ExprLowerer::::is_supported_condition(condition) { + return Err(error_tags::lowering_error( + "pattern4/condition", + "ConditionLoweringBox does not support this condition (legacy path removed)", + )); + } + + let mut dummy_builder = MirBuilder::new(); + let mut expr_lowerer = + ExprLowerer::new(&scope_manager, ExprContext::Condition, &mut dummy_builder); + + let mut context = ConditionContext { + loop_var_name: loop_var_name.to_string(), + loop_var_id, + scope: &scope_manager, + alloc_value, + current_static_box_name: current_static_box_name.map(|s| s.to_string()), + }; + + let value_id = expr_lowerer.lower_condition(condition, &mut context).map_err(|e| { + format!( + "[joinir/pattern4/phase244] ConditionLoweringBox failed on supported condition: {:?}", + e + ) + })?; + + let instructions = expr_lowerer.take_last_instructions(); + debug.log( + "phase244", + &format!( + "Header condition via ConditionLoweringBox: {} instructions", + instructions.len() + ), + ); + + Ok((value_id, instructions)) +} diff --git a/src/mir/join_ir/lowering/loop_with_continue_minimal.rs b/src/mir/join_ir/lowering/loop_with_continue_minimal/mod.rs similarity index 61% rename from src/mir/join_ir/lowering/loop_with_continue_minimal.rs rename to src/mir/join_ir/lowering/loop_with_continue_minimal/mod.rs index ed164f1c..f847d9bc 100644 --- a/src/mir/join_ir/lowering/loop_with_continue_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_continue_minimal/mod.rs @@ -57,31 +57,31 @@ //! //! Following the "80/20 rule" from CLAUDE.md - now generalizing after working. +mod carrier_updates; +mod condition_lowering; +mod validation; + use crate::ast::ASTNode; use crate::mir::builder::MirBuilder; use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, ExitMeta}; -use crate::mir::join_ir::lowering::condition_to_joinir::{ - lower_condition_to_joinir_no_body_locals, ConditionEnv, -}; +use crate::mir::join_ir::lowering::condition_env::ConditionEnv; use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox; use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; -use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; // Phase 244 use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; -use crate::mir::join_ir::lowering::loop_update_analyzer::{UpdateExpr, UpdateRhs}; +use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; use crate::mir::join_ir::lowering::return_collector::ReturnInfo; // Phase 284 P1 use crate::mir::join_ir::lowering::return_jump_emitter::emit_return_conditional_jump; // Phase 284 P1 use crate::mir::join_ir::{ BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, UnaryOp, }; -use crate::mir::loop_pattern_detection::error_messages::{ - extract_body_local_names, format_unsupported_condition_error, -}; -use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv; // Phase 244 -use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox; use crate::mir::ValueId; use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism +use carrier_updates::emit_carrier_updates; +use condition_lowering::lower_header_condition; +use validation::validate_condition_scope; + /// Lower Pattern 4 (Loop with Continue) to JoinIR /// /// # Phase 195-196: Pure JoinIR Fragment Generation @@ -146,16 +146,7 @@ pub(crate) fn lower_loop_with_continue_minimal( // Phase 170-D-impl-3: Validate that loop condition only uses supported variable scopes // LoopConditionScopeBox checks that loop conditions don't reference loop-body-local variables let loop_var_name = carrier_info.loop_var_name.clone(); - let loop_cond_scope = - LoopConditionScopeBox::analyze(&loop_var_name, &[condition], Some(&_scope)); - - if loop_cond_scope.has_loop_body_local() { - let body_local_names = extract_body_local_names(&loop_cond_scope.vars); - return Err(format_unsupported_condition_error( - "pattern4", - &body_local_names, - )); - } + let loop_cond_scope = validate_condition_scope(&_scope, condition, &loop_var_name)?; let debug = DebugOutputBox::new_dev("joinir/pattern4"); debug.log( @@ -276,69 +267,16 @@ pub(crate) fn lower_loop_with_continue_minimal( let mut alloc_value = || join_value_space.alloc_local(); // Phase 169 / Phase 171-fix / Phase 244: Lower condition using ConditionLoweringBox trait - let (cond_value, mut cond_instructions) = { - use crate::mir::builder::MirBuilder; - use crate::mir::join_ir::lowering::condition_lowering_box::{ - ConditionContext, ConditionLoweringBox, - }; - use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer}; - use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager; - - let debug = DebugOutputBox::new_dev("joinir/pattern4"); - - // Build minimal ScopeManager for header condition - let empty_body_env = LoopBodyLocalEnv::new(); - let empty_captured_env = CapturedEnv::new(); - - let scope_manager = Pattern2ScopeManager { - condition_env: &env, - loop_body_local_env: Some(&empty_body_env), - captured_env: Some(&empty_captured_env), - carrier_info: &carrier_info, - }; - - if ExprLowerer::::is_supported_condition(condition) { - // Phase 244: ExprLowerer via ConditionLoweringBox trait - let mut dummy_builder = MirBuilder::new(); - let mut expr_lowerer = - ExprLowerer::new(&scope_manager, ExprContext::Condition, &mut dummy_builder); - - let mut context = ConditionContext { - loop_var_name: loop_var_name.clone(), - loop_var_id: i_param, - scope: &scope_manager, - alloc_value: &mut alloc_value, - current_static_box_name: None, // Phase 252: TODO - plumb through Pattern 3 - }; - - match expr_lowerer.lower_condition(condition, &mut context) { - Ok(value_id) => { - let instructions = expr_lowerer.take_last_instructions(); - debug.log( - "phase244", - &format!( - "Header condition via ConditionLoweringBox: {} instructions", - instructions.len() - ), - ); - (value_id, instructions) - } - Err(e) => { - return Err(format!( - "[joinir/pattern4/phase244] ConditionLoweringBox failed on supported condition: {:?}", - e - )); - } - } - } else { - // Legacy path: condition_to_joinir (for complex conditions not yet supported) - debug.log( - "phase244", - "Header condition via legacy path (not yet supported by ConditionLoweringBox)", - ); - lower_condition_to_joinir_no_body_locals(condition, &mut alloc_value, &env)? // Phase 92 P2-2: No body-local for header - } - }; + let (cond_value, mut cond_instructions) = lower_header_condition( + condition, + &env, + carrier_info, + &loop_var_name, + i_param, + &mut alloc_value, + None, + &debug, + )?; // Loop control temporaries let exit_cond = alloc_value(); @@ -484,188 +422,18 @@ pub(crate) fn lower_loop_with_continue_minimal( rhs: const_0, })); - // ------------------------------------------------------------------ - // Phase 197: Generate Select for EACH carrier (using update expression metadata) - // ------------------------------------------------------------------ - // For each carrier: - // 1. carrier_next = carrier_param + rhs (where rhs is from update expression) - // 2. carrier_merged = Select(continue_cond, carrier_param, carrier_next) - // - // Phase 197: Use update expression metadata for semantic correctness - // For loop_continue_multi_carrier.hako: - // - sum = sum + i → use i_param (current iteration value) - // - count = count + 1 → use const_1 - // - // Previous Phase 196 used i_next for all non-count carriers, which was - // semantically incorrect (used next iteration's value instead of current). - - for idx in 0..carrier_count { - let carrier_param = carrier_param_ids[idx]; - let carrier_next = carrier_next_ids[idx]; - let carrier_merged = carrier_merged_ids[idx]; - let carrier_name = &carrier_info.carriers[idx].name; - - // Phase 197: Extract RHS from update expression metadata - debug.log( - "carrier_update", - &format!("Processing carrier '{}' (idx={})", carrier_name, idx), - ); - let rhs = if let Some(update_expr) = carrier_updates.get(carrier_name) { - debug.log( - "carrier_update", - &format!("Found update expr: {:?}", update_expr), - ); - match update_expr { - UpdateExpr::BinOp { op, rhs, .. } => { - // Verify operator is Add (only supported for now) - if *op != BinOpKind::Add { - debug.log( - "warn", - &format!( - "Carrier '{}' uses unsupported operator {:?}, defaulting to Add", - carrier_name, op - ), - ); - } - - // Generate RHS value based on update expression - match rhs { - UpdateRhs::Const(n) => { - if *n == 1 { - const_1 - } else { - // Need to allocate a new constant value - debug.log( - "warn", - &format!( - "Carrier '{}' uses const {}, only const_1 is pre-allocated, using const_1", - carrier_name, n - ), - ); - const_1 - } - } - UpdateRhs::Variable(var_name) => { - if var_name == &carrier_info.loop_var_name { - // sum = sum + i → use i_next (incremented value) - // Because in the source: i = i + 1 happens FIRST, then sum = sum + i uses the NEW value - debug.log( - "carrier_update", - &format!( - "Using i_next (ValueId({})) for variable '{}'", - i_next.0, var_name - ), - ); - i_next - } else { - debug.log( - "warn", - &format!( - "Carrier '{}' updates with unknown variable '{}', using const_1", - carrier_name, var_name - ), - ); - const_1 - } - } - // Phase 190: Number accumulation not supported in Pattern 4 yet - // Skip JoinIR update - use Select passthrough to keep carrier_merged defined - UpdateRhs::NumberAccumulation { .. } => { - debug.log( - "phase190", - &format!( - "Carrier '{}' has number accumulation - not supported in Pattern 4, using Select passthrough", - carrier_name - ), - ); - // Emit Select with same values: carrier_merged = Select(_, carrier_param, carrier_param) - // This is effectively a passthrough (no JoinIR update) - loop_step_func.body.push(JoinInst::Select { - dst: carrier_merged, - cond: continue_cond, // Condition doesn't matter when both values are same - then_val: carrier_param, - else_val: carrier_param, - type_hint: None, - }); - continue; // Skip the BinOp and normal Select below - } - // Phase 178: String updates detected but not lowered to JoinIR yet - // Skip JoinIR update - use Select passthrough to keep carrier_merged defined - UpdateRhs::StringLiteral(_) | UpdateRhs::Other => { - debug.log( - "phase178", - &format!( - "Carrier '{}' has string/complex update - skipping BinOp, using Select passthrough", - carrier_name - ), - ); - // Emit Select with same values: carrier_merged = Select(_, carrier_param, carrier_param) - // This is effectively a passthrough (no JoinIR update) - loop_step_func.body.push(JoinInst::Select { - dst: carrier_merged, - cond: continue_cond, // Condition doesn't matter when both values are same - then_val: carrier_param, - else_val: carrier_param, - type_hint: None, - }); - continue; // Skip the BinOp and normal Select below - } - } - } - UpdateExpr::Const(n) => { - // Direct constant (rare, but supported) - if *n == 1 { - const_1 - } else { - debug.log( - "warn", - &format!( - "Carrier '{}' uses const {}, only const_1 is pre-allocated, using const_1", - carrier_name, n - ), - ); - const_1 - } - } - } - } else { - // No update expression found - fallback to const_1 (safe default) - debug.log( - "warn", - &format!( - "No update expression for carrier '{}', defaulting to +1", - carrier_name - ), - ); - const_1 - }; - - // carrier_next = carrier_param + rhs - debug.log( - "carrier_update", - &format!( - "Generating: ValueId({}) = ValueId({}) + ValueId({})", - carrier_next.0, carrier_param.0, rhs.0 - ), - ); - loop_step_func - .body - .push(JoinInst::Compute(MirLikeInst::BinOp { - dst: carrier_next, - op: BinOpKind::Add, - lhs: carrier_param, - rhs, - })); - - // carrier_merged = Select(continue_cond, carrier_param, carrier_next) - loop_step_func.body.push(JoinInst::Select { - dst: carrier_merged, - cond: continue_cond, - then_val: carrier_param, // Continue: no update - else_val: carrier_next, // Normal: update - type_hint: None, - }); - } + emit_carrier_updates( + carrier_info, + carrier_updates, + &carrier_param_ids, + &carrier_next_ids, + &carrier_merged_ids, + continue_cond, + const_1, + i_next, + &debug, + &mut loop_step_func.body, + )?; // ------------------------------------------------------------------ // Tail call: loop_step(i_next, carrier1_merged, carrier2_merged, ...) diff --git a/src/mir/join_ir/lowering/loop_with_continue_minimal/validation.rs b/src/mir/join_ir/lowering/loop_with_continue_minimal/validation.rs new file mode 100644 index 00000000..483de9e0 --- /dev/null +++ b/src/mir/join_ir/lowering/loop_with_continue_minimal/validation.rs @@ -0,0 +1,24 @@ +use crate::ast::ASTNode; +use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; +use crate::mir::loop_pattern_detection::error_messages::{ + extract_body_local_names, format_unsupported_condition_error, +}; +use crate::mir::loop_pattern_detection::loop_condition_scope::{LoopConditionScope, LoopConditionScopeBox}; + +pub(crate) fn validate_condition_scope( + scope: &LoopScopeShape, + condition: &ASTNode, + loop_var_name: &str, +) -> Result { + let loop_cond_scope = LoopConditionScopeBox::analyze(loop_var_name, &[condition], Some(scope)); + + if loop_cond_scope.has_loop_body_local() { + let body_local_names = extract_body_local_names(&loop_cond_scope.vars); + return Err(format_unsupported_condition_error( + "pattern4", + &body_local_names, + )); + } + + Ok(loop_cond_scope) +}