Refactor JoinIR lowerers and boundary
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(<, &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()
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,412 @@
|
||||
use super::super::expr_lowering_contract::{ExprLoweringScope, ImpurePolicy, OutOfScopeReason};
|
||||
use super::*;
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator};
|
||||
use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, JoinInst, MirLikeInst, UnaryOp};
|
||||
use crate::mir::types::MirType;
|
||||
use crate::mir::ValueId;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn span() -> Span {
|
||||
Span::unknown()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_var_from_env() {
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("x".to_string(), ValueId(42));
|
||||
let mut body = vec![];
|
||||
let mut next = 100;
|
||||
|
||||
let ast = ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||
assert_eq!(got, Some(ValueId(42)));
|
||||
assert!(body.is_empty());
|
||||
assert_eq!(next, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_scope_var_missing() {
|
||||
let env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next = 100;
|
||||
|
||||
let ast = ASTNode::Variable {
|
||||
name: "missing".to_string(),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||
assert_eq!(got, None);
|
||||
assert!(body.is_empty());
|
||||
assert_eq!(next, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_int_literal_const_emits() {
|
||||
let env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next = 100;
|
||||
|
||||
let ast = ASTNode::Literal {
|
||||
value: LiteralValue::Integer(7),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||
assert_eq!(got, Some(ValueId(100)));
|
||||
assert_eq!(next, 101);
|
||||
assert!(matches!(
|
||||
body.as_slice(),
|
||||
[JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: ValueId(100),
|
||||
value: ConstValue::Integer(7)
|
||||
})]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_bool_literal_const_emits() {
|
||||
let env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next = 100;
|
||||
|
||||
let ast = ASTNode::Literal {
|
||||
value: LiteralValue::Bool(true),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||
assert_eq!(got, Some(ValueId(100)));
|
||||
assert_eq!(next, 101);
|
||||
assert!(matches!(
|
||||
body.as_slice(),
|
||||
[JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: ValueId(100),
|
||||
value: ConstValue::Bool(true)
|
||||
})]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_unary_minus_int() {
|
||||
let env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next = 10;
|
||||
|
||||
let ast = ASTNode::UnaryOp {
|
||||
operator: UnaryOperator::Minus,
|
||||
operand: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(3),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||
assert_eq!(got, Some(ValueId(11)));
|
||||
assert_eq!(next, 12);
|
||||
assert!(matches!(
|
||||
body.as_slice(),
|
||||
[
|
||||
JoinInst::Compute(MirLikeInst::Const { .. }),
|
||||
JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||
dst: ValueId(11),
|
||||
op: UnaryOp::Neg,
|
||||
..
|
||||
})
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_unary_not_bool() {
|
||||
let env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next = 10;
|
||||
|
||||
let ast = ASTNode::UnaryOp {
|
||||
operator: UnaryOperator::Not,
|
||||
operand: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Bool(false),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||
assert_eq!(got, Some(ValueId(11)));
|
||||
assert_eq!(next, 12);
|
||||
assert!(matches!(
|
||||
body.as_slice(),
|
||||
[
|
||||
JoinInst::Compute(MirLikeInst::Const { .. }),
|
||||
JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||
dst: ValueId(11),
|
||||
op: UnaryOp::Not,
|
||||
..
|
||||
})
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_add_sub_mul_div_ints() {
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("x".to_string(), ValueId(1));
|
||||
let mut body = vec![];
|
||||
let mut next = 100;
|
||||
|
||||
let add = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(2),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&add, &env, &mut body, &mut next).unwrap();
|
||||
assert_eq!(got, Some(ValueId(101)));
|
||||
|
||||
let sub = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Subtract,
|
||||
left: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(5),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(3),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let _ = NormalizedExprLowererBox::lower_expr(&sub, &env, &mut body, &mut next).unwrap();
|
||||
|
||||
let mul = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Multiply,
|
||||
left: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(6),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(7),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let _ = NormalizedExprLowererBox::lower_expr(&mul, &env, &mut body, &mut next).unwrap();
|
||||
|
||||
let div = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Divide,
|
||||
left: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(8),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(2),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let _ = NormalizedExprLowererBox::lower_expr(&div, &env, &mut body, &mut next).unwrap();
|
||||
|
||||
assert!(body.iter().any(|i| matches!(
|
||||
i,
|
||||
JoinInst::Compute(MirLikeInst::BinOp {
|
||||
op: BinOpKind::Add,
|
||||
..
|
||||
})
|
||||
)));
|
||||
assert!(body.iter().any(|i| matches!(
|
||||
i,
|
||||
JoinInst::Compute(MirLikeInst::BinOp {
|
||||
op: BinOpKind::Sub,
|
||||
..
|
||||
})
|
||||
)));
|
||||
assert!(body.iter().any(|i| matches!(
|
||||
i,
|
||||
JoinInst::Compute(MirLikeInst::BinOp {
|
||||
op: BinOpKind::Mul,
|
||||
..
|
||||
})
|
||||
)));
|
||||
assert!(body.iter().any(|i| matches!(
|
||||
i,
|
||||
JoinInst::Compute(MirLikeInst::BinOp {
|
||||
op: BinOpKind::Div,
|
||||
..
|
||||
})
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_compare_eq_lt_ints() {
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("x".to_string(), ValueId(1));
|
||||
let mut body = vec![];
|
||||
let mut next = 100;
|
||||
|
||||
let eq = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Equal,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&eq, &env, &mut body, &mut next).unwrap();
|
||||
assert!(got.is_some());
|
||||
|
||||
let lt = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(0),
|
||||
span: span(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: span(),
|
||||
}),
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(<, &env, &mut body, &mut next).unwrap();
|
||||
assert!(got.is_some());
|
||||
|
||||
assert!(body.iter().any(|i| matches!(
|
||||
i,
|
||||
JoinInst::Compute(MirLikeInst::Compare {
|
||||
op: CompareOp::Eq,
|
||||
..
|
||||
})
|
||||
)));
|
||||
assert!(body.iter().any(|i| matches!(
|
||||
i,
|
||||
JoinInst::Compute(MirLikeInst::Compare {
|
||||
op: CompareOp::Lt,
|
||||
..
|
||||
})
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_scope_call() {
|
||||
let env = BTreeMap::new();
|
||||
let mut body = vec![];
|
||||
let mut next = 1;
|
||||
|
||||
let ast = ASTNode::FunctionCall {
|
||||
name: "f".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
let got = NormalizedExprLowererBox::lower_expr(&ast, &env, &mut body, &mut next).unwrap();
|
||||
assert_eq!(got, None);
|
||||
assert!(body.is_empty());
|
||||
assert_eq!(next, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_is_out_of_scope_in_pure_only() {
|
||||
let env = BTreeMap::new();
|
||||
let ast = ASTNode::Call {
|
||||
callee: Box::new(ASTNode::Variable {
|
||||
name: "f".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
assert_eq!(
|
||||
NormalizedExprLowererBox::out_of_scope_reason(ExprLoweringScope::PureOnly, &ast, &env),
|
||||
Some(OutOfScopeReason::Call)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn methodcall_is_out_of_scope_in_pure_only() {
|
||||
let env = BTreeMap::new();
|
||||
let ast = ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "m".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
assert_eq!(
|
||||
NormalizedExprLowererBox::out_of_scope_reason(ExprLoweringScope::PureOnly, &ast, &env),
|
||||
Some(OutOfScopeReason::MethodCall)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn methodcall_length0_is_in_scope_with_known_intrinsic_only() {
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("s".to_string(), ValueId(1));
|
||||
|
||||
let ast = ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
NormalizedExprLowererBox::out_of_scope_reason(
|
||||
ExprLoweringScope::WithImpure(ImpurePolicy::KnownIntrinsicOnly),
|
||||
&ast,
|
||||
&env
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_methodcall_length0_emits_method_call_inst() {
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert("s".to_string(), ValueId(7));
|
||||
let mut body = vec![];
|
||||
let mut next = 100;
|
||||
|
||||
let ast = ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(),
|
||||
span: span(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
|
||||
let got = NormalizedExprLowererBox::lower_expr_with_scope(
|
||||
ExprLoweringScope::WithImpure(ImpurePolicy::KnownIntrinsicOnly),
|
||||
&ast,
|
||||
&env,
|
||||
&mut body,
|
||||
&mut next,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, Some(ValueId(100)));
|
||||
assert_eq!(next, 101);
|
||||
assert!(matches!(
|
||||
body.as_slice(),
|
||||
[JoinInst::MethodCall {
|
||||
dst: ValueId(100),
|
||||
receiver: ValueId(7),
|
||||
method,
|
||||
args,
|
||||
type_hint: Some(MirType::Integer),
|
||||
}] if method == "length" && args.is_empty()
|
||||
));
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
135
src/mir/join_ir/lowering/inline_boundary/constructors.rs
Normal file
135
src/mir/join_ir/lowering/inline_boundary/constructors.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/mir/join_ir/lowering/inline_boundary/mod.rs
Normal file
28
src/mir/join_ir/lowering/inline_boundary/mod.rs
Normal 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};
|
||||
50
src/mir/join_ir/lowering/inline_boundary/tests.rs
Normal file
50
src/mir/join_ir/lowering/inline_boundary/tests.rs
Normal 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"));
|
||||
}
|
||||
176
src/mir/join_ir/lowering/inline_boundary/types.rs
Normal file
176
src/mir/join_ir/lowering/inline_boundary/types.rs
Normal 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,
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -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, ...)
|
||||
@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user