Refactor JoinIR lowerers and boundary

This commit is contained in:
2025-12-28 03:52:52 +09:00
parent 3e2086cc78
commit ca91be349d
26 changed files with 2228 additions and 2375 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<String, ValueId>,
) -> Option<OutOfScopeReason> {
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<String, ValueId>) -> Option<OutOfScopeReason> {
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<String, ValueId>,
) -> Option<OutOfScopeReason> {
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<String, ValueId>) -> 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<String, ValueId>) -> 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<String, ValueId>) -> 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<String, ValueId>) -> 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<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<String, ValueId>,
) -> Option<KnownIntrinsic> {
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<JoinInst>,
next_value_id: &mut u32,
env: &BTreeMap<String, ValueId>,
) -> Result<Option<ValueId>, 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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<String, ValueId>,
body: &mut Vec<JoinInst>,
next_value_id: &mut u32,
) -> Result<Option<ValueId>, 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<BinaryKind> {
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(&lt, &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()
));
}
}

View File

@ -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<BinaryKind> {
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,
}
}

View File

@ -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<String, ValueId>,
) -> Option<KnownIntrinsic> {
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<JoinInst>,
next_value_id: &mut u32,
env: &BTreeMap<String, ValueId>,
) -> Result<Option<ValueId>, 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))
}
}

View File

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

View File

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

View File

@ -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<String, ValueId>,
) -> Option<OutOfScopeReason> {
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<String, ValueId>,
) -> Option<OutOfScopeReason> {
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<String, ValueId>,
) -> Option<OutOfScopeReason> {
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<String, ValueId>) -> 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<String, ValueId>) -> 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<String, ValueId>,
) -> 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<String, ValueId>,
) -> 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
}
},
}
}
}

View File

@ -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(&lt, &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()
));
}

View File

@ -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<ValueId>,
/// 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<ValueId>,
/// 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<ValueId>,
/// 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<ValueId>,
/// 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<LoopExitBinding>,
/// 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<super::condition_to_joinir::ConditionBinding>,
/// 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<crate::mir::ValueId>,
/// 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<String>,
/// 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<String>,
/// 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<super::carrier_info::CarrierInfo>,
/// Phase 132 P1: Continuation contract (SSOT)
/// Phase 256 P1.7: Changed from BTreeSet<JoinFuncId> to BTreeSet<String>
///
/// 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<String>,
/// 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<ValueId>,
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<String> {
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<ValueId>, host_inputs: Vec<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: 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<ValueId>,
host_inputs: Vec<ValueId>,
host_outputs: Vec<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,
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<ValueId>,
host_inputs: Vec<ValueId>,
exit_bindings: Vec<LoopExitBinding>,
) -> 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<ValueId>,
host_inputs: Vec<ValueId>,
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<ValueId>,
host_inputs: Vec<ValueId>,
exit_bindings: Vec<LoopExitBinding>,
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<ValueId>,
host_inputs: Vec<ValueId>,
condition_bindings: Vec<super::condition_to_joinir::ConditionBinding>,
) -> 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"));
}
}

View File

@ -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<ValueId>,
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<String> {
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<ValueId>, host_inputs: Vec<ValueId>) -> 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<ValueId>,
host_inputs: Vec<ValueId>,
exit_bindings: Vec<LoopExitBinding>,
) -> 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<ValueId>,
host_inputs: Vec<ValueId>,
condition_bindings: Vec<ConditionBinding>,
) -> 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(),
}
}
}

View File

@ -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};

View File

@ -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"));
}

View File

@ -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<ValueId>,
/// 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<ValueId>,
/// 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<LoopExitBinding>,
/// 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<ConditionBinding>,
/// 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<ValueId>,
/// 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<String>,
/// 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<String>,
/// 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<CarrierInfo>,
/// Phase 132 P1: Continuation contract (SSOT)
/// Phase 256 P1.7: Changed from BTreeSet<JoinFuncId> to BTreeSet<String>
///
/// JoinIR merge must not infer/guess continuation functions. The router/lowerer
/// must declare continuation function names here.
pub continuation_func_ids: BTreeSet<String>,
/// 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,
}

View File

@ -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<ValueId>, host_outputs: Vec<ValueId>) -> 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);

View File

@ -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<JoinInst> = Vec::new();
let mut carrier_update_block: Vec<JoinInst> = Vec::new();
let mut tail_block: Vec<JoinInst> = Vec::new();
let mut loop_var_next_override: Option<ValueId> = 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<ValueId> = 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);

View File

@ -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<F>(
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<String>,
alloc_local_fn: &mut F,
body_init_block: &mut Vec<JoinInst>,
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(())
}

View File

@ -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<ValueId>,
pub loop_var_next_override: Option<ValueId>,
}
pub(crate) fn emit_carrier_updates(
env: &ConditionEnv,
carrier_info: &CarrierInfo,
carrier_updates: &BTreeMap<String, UpdateExpr>,
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<JoinInst>,
dev_log: &DebugOutputBox,
) -> Result<CarrierUpdateResult, String> {
let mut loop_var_next_override: Option<ValueId> = 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<ValueId> = 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,
})
}

View File

@ -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::<Pattern2ScopeManager>::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::<Pattern2ScopeManager>::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).

View File

@ -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<ValueId>,
alloc_local_fn: &mut dyn FnMut() -> ValueId,
tail_block: &mut Vec<JoinInst>,
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
}

View File

@ -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<String, UpdateExpr>,
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<JoinInst>,
) -> 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(())
}

View File

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

View File

@ -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::<Pattern2ScopeManager>::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, ...)

View File

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