feat(joinir): Phase 92完了 - ConditionalStep + body-local変数サポート
## Phase 92全体の成果 **Phase 92 P0-P2**: ConditionalStep JoinIR生成とbody-local変数サポート - ConditionalStep(条件付きキャリア更新)のJoinIR生成実装 - Body-local変数(ch等)の条件式での参照サポート - 変数解決優先度: ConditionEnv → LoopBodyLocalEnv **Phase 92 P3**: BodyLocalPolicyBox + 安全ガード - BodyLocalPolicyDecision実装(Accept/Reject判定) - BodyLocalSlot + DualValueRewriter(JoinIR/MIR二重書き込み) - Fail-Fast契約(Cannot promote LoopBodyLocal検出) **Phase 92 P4**: E2E固定+回帰最小化 (本コミット) - Unit test 3本追加(body-local変数解決検証) - Integration smoke追加(phase92_pattern2_baseline.sh、2ケースPASS) - P4-E2E-PLAN.md、P4-COMPLETION.md作成 ## 主要な実装 ### ConditionalStep(条件付きキャリア更新) - `conditional_step_emitter.rs`: JoinIR Select命令生成 - `loop_with_break_minimal.rs`: ConditionalStep検出と統合 - `loop_with_continue_minimal.rs`: Pattern4対応 ### Body-local変数サポート - `condition_lowerer.rs`: body-local変数解決機能 - `lower_condition_to_joinir`: body_local_env パラメータ追加 - 変数解決優先度実装(ConditionEnv優先) - Unit test 3本追加: 変数解決/優先度/エラー - `header_break_lowering.rs`: break条件でbody-local変数参照 - 7ファイルで後方互換ラッパー(lower_condition_to_joinir_no_body_locals) ### Body-local Policy & Safety - `body_local_policy.rs`: BodyLocalPolicyDecision(Accept/Reject) - `body_local_slot.rs`: JoinIR/MIR二重書き込み - `dual_value_rewriter.rs`: ValueId書き換えヘルパー ## テスト体制 ### Unit Tests (+3) - `test_body_local_variable_resolution`: body-local変数解決 - `test_variable_resolution_priority`: 変数解決優先度(ConditionEnv優先) - `test_undefined_variable_error`: 未定義変数エラー - 全7テストPASS(cargo test --release condition_lowerer::tests) ### Integration Smoke (+1) - `phase92_pattern2_baseline.sh`: - Case A: loop_min_while.hako (Pattern2 baseline) - Case B: phase92_conditional_step_minimal.hako (条件付きインクリメント) - 両ケースPASS、integration profileで発見可能 ### 退行確認 - ✅ 既存Pattern2Breakテスト正常(退行なし) - ✅ Phase 135 smoke正常(MIR検証PASS) ## アーキテクチャ設計 ### 変数解決メカニズム ```rust // Priority 1: ConditionEnv (loop params, captured) if let Some(value_id) = env.get(name) { return Ok(value_id); } // Priority 2: LoopBodyLocalEnv (body-local like `ch`) if let Some(body_env) = body_local_env { if let Some(value_id) = body_env.get(name) { return Ok(value_id); } } ``` ### Fail-Fast契約 - Delta equality check (conditional_step_emitter.rs) - Variable resolution error messages (ConditionEnv) - Body-local promotion rejection (BodyLocalPolicyDecision::Reject) ## ドキュメント - `P4-E2E-PLAN.md`: 3レベルテスト戦略(Level 1-2完了、Level 3延期) - `P4-COMPLETION.md`: Phase 92完了報告 - `README.md`: Phase 92全体のまとめ ## 将来の拡張(Phase 92スコープ外) - Body-local promotionシステム拡張 - P5bパターン認識の汎化(flagベース条件サポート) - 完全なP5b E2Eテスト(body-local promotion実装後) 🎯 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -367,7 +367,7 @@ pub fn emit_carrier_update(
|
||||
// ============================================================================
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::condition_lowerer::lower_condition_to_joinir;
|
||||
use crate::mir::join_ir::lowering::condition_lowerer::lower_condition_to_joinir_no_body_locals;
|
||||
use crate::mir::join_ir::VarId;
|
||||
use crate::mir::MirType;
|
||||
|
||||
@ -409,7 +409,7 @@ pub fn emit_conditional_step_update(
|
||||
) -> Result<ValueId, String> {
|
||||
// Step 1: Lower the condition expression
|
||||
// Phase 92 P2-2: No body-local support in legacy emitter (use common/conditional_step_emitter instead)
|
||||
let (cond_id, cond_insts) = lower_condition_to_joinir(cond_ast, alloc_value, env, None)?;
|
||||
let (cond_id, cond_insts) = lower_condition_to_joinir_no_body_locals(cond_ast, alloc_value, env)?;
|
||||
instructions.extend(cond_insts);
|
||||
|
||||
// Step 2: Get carrier parameter ValueId from env
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
|
||||
pub mod case_a;
|
||||
pub mod conditional_step_emitter; // Phase 92 P1-1: ConditionalStep emission module
|
||||
pub mod body_local_slot; // Phase 92 P3: Read-only body-local slot for conditions
|
||||
pub mod dual_value_rewriter; // Phase 246-EX/247-EX: name-based dual-value rewrites
|
||||
|
||||
use crate::mir::loop_form::LoopForm;
|
||||
use crate::mir::query::{MirQuery, MirQueryBox};
|
||||
|
||||
314
src/mir/join_ir/lowering/common/body_local_slot.rs
Normal file
314
src/mir/join_ir/lowering/common/body_local_slot.rs
Normal file
@ -0,0 +1,314 @@
|
||||
//! Phase 92 P3: ReadOnlyBodyLocalSlot Box
|
||||
//!
|
||||
//! Purpose: support the minimal case where a loop condition/break condition
|
||||
//! references a loop-body-local variable (e.g., `ch`) that is recomputed every
|
||||
//! iteration and is read-only (no assignment).
|
||||
//!
|
||||
//! This box is intentionally narrow and fail-fast:
|
||||
//! - Supports exactly 1 body-local variable used in conditions.
|
||||
//! - Requires a top-level `local <name> = <init_expr>` before the break-guard `if`.
|
||||
//! - Forbids any assignment to that variable (including in nested blocks).
|
||||
//!
|
||||
//! NOTE: This box does NOT lower the init expression itself.
|
||||
//! Lowering is handled by `LoopBodyLocalInitLowerer` (Phase 186).
|
||||
//! This box only validates the contract and provides an allow-list for
|
||||
//! condition lowering checks.
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::error_tags;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReadOnlyBodyLocalSlot {
|
||||
pub name: String,
|
||||
pub init_expr: ASTNode,
|
||||
pub decl_stmt_index: usize,
|
||||
pub break_guard_stmt_index: usize,
|
||||
}
|
||||
|
||||
/// A tiny "box" API: analyze loop body and decide whether we can allow a single
|
||||
/// loop-body-local variable to be referenced from Pattern2 break conditions.
|
||||
pub struct ReadOnlyBodyLocalSlotBox;
|
||||
|
||||
impl ReadOnlyBodyLocalSlotBox {
|
||||
/// Extract and validate a single read-only body-local slot used in conditions.
|
||||
///
|
||||
/// # Contract (Fail-Fast)
|
||||
/// - `names_in_conditions` must contain exactly 1 name.
|
||||
/// - A top-level `local <name> = <expr>` must exist in `body`.
|
||||
/// - The declaration statement must appear before the first top-level `if` that contains `break`.
|
||||
/// - No assignment to `<name>` may exist anywhere in the loop body (including nested statements).
|
||||
pub fn extract_single(
|
||||
names_in_conditions: &[String],
|
||||
body: &[ASTNode],
|
||||
) -> Result<ReadOnlyBodyLocalSlot, String> {
|
||||
if names_in_conditions.is_empty() {
|
||||
return Err(error_tags::freeze(
|
||||
"[pattern2/body_local_slot/internal/empty_names] extract_single called with empty names_in_conditions",
|
||||
));
|
||||
}
|
||||
if names_in_conditions.len() != 1 {
|
||||
return Err(error_tags::freeze(&format!(
|
||||
"[pattern2/body_local_slot/contract/multiple_vars] Unsupported: multiple LoopBodyLocal variables in condition: {:?}",
|
||||
names_in_conditions
|
||||
)));
|
||||
}
|
||||
|
||||
let name = names_in_conditions[0].clone();
|
||||
|
||||
let break_guard_stmt_index = find_first_top_level_break_guard_if(body).ok_or_else(|| {
|
||||
error_tags::freeze(
|
||||
"[pattern2/body_local_slot/contract/missing_break_guard] Missing top-level `if (...) { break }` (Pattern2 break guard)",
|
||||
)
|
||||
})?;
|
||||
|
||||
let (decl_stmt_index, init_expr) =
|
||||
find_top_level_local_init(body, &name).ok_or_else(|| {
|
||||
error_tags::freeze(&format!(
|
||||
"[pattern2/body_local_slot/contract/missing_local_init] Missing top-level `local {} = <expr>` for LoopBodyLocal used in condition",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
|
||||
if decl_stmt_index >= break_guard_stmt_index {
|
||||
return Err(error_tags::freeze(&format!(
|
||||
"[pattern2/body_local_slot/contract/decl_after_break_guard] `local {}` must appear before the break guard if-statement (decl_index={}, break_if_index={})",
|
||||
name, decl_stmt_index, break_guard_stmt_index
|
||||
)));
|
||||
}
|
||||
|
||||
if contains_assignment_to_name(body, &name) {
|
||||
return Err(error_tags::freeze(&format!(
|
||||
"[pattern2/body_local_slot/contract/not_readonly] `{}` must be read-only (assignment detected in loop body)",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(ReadOnlyBodyLocalSlot {
|
||||
name,
|
||||
init_expr,
|
||||
decl_stmt_index,
|
||||
break_guard_stmt_index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn find_first_top_level_break_guard_if(body: &[ASTNode]) -> Option<usize> {
|
||||
for (idx, stmt) in body.iter().enumerate() {
|
||||
if let ASTNode::If {
|
||||
then_body,
|
||||
else_body,
|
||||
..
|
||||
} = stmt
|
||||
{
|
||||
if then_body.iter().any(|n| matches!(n, ASTNode::Break { .. })) {
|
||||
return Some(idx);
|
||||
}
|
||||
if let Some(else_body) = else_body {
|
||||
if else_body.iter().any(|n| matches!(n, ASTNode::Break { .. })) {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_top_level_local_init(body: &[ASTNode], name: &str) -> Option<(usize, ASTNode)> {
|
||||
for (idx, stmt) in body.iter().enumerate() {
|
||||
if let ASTNode::Local {
|
||||
variables,
|
||||
initial_values,
|
||||
..
|
||||
} = stmt
|
||||
{
|
||||
// Keep Phase 92 P3 minimal: the statement must be a 1-variable local.
|
||||
if variables.len() != 1 {
|
||||
continue;
|
||||
}
|
||||
if variables[0] != name {
|
||||
continue;
|
||||
}
|
||||
let init = initial_values
|
||||
.get(0)
|
||||
.and_then(|v| v.as_ref())
|
||||
.map(|b| (*b.clone()).clone())?;
|
||||
return Some((idx, init));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn contains_assignment_to_name(body: &[ASTNode], name: &str) -> bool {
|
||||
body.iter().any(|stmt| contains_assignment_to_name_in_node(stmt, name))
|
||||
}
|
||||
|
||||
fn contains_assignment_to_name_in_node(node: &ASTNode, name: &str) -> bool {
|
||||
match node {
|
||||
ASTNode::Assignment { target, value, .. } => {
|
||||
if matches!(&**target, ASTNode::Variable { name: n, .. } if n == name) {
|
||||
return true;
|
||||
}
|
||||
contains_assignment_to_name_in_node(target, name)
|
||||
|| contains_assignment_to_name_in_node(value, name)
|
||||
}
|
||||
ASTNode::Nowait { variable, .. } => variable == name,
|
||||
ASTNode::If {
|
||||
condition,
|
||||
then_body,
|
||||
else_body,
|
||||
..
|
||||
} => {
|
||||
contains_assignment_to_name_in_node(condition, name)
|
||||
|| then_body
|
||||
.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name))
|
||||
|| else_body.as_ref().is_some_and(|e| {
|
||||
e.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name))
|
||||
})
|
||||
}
|
||||
ASTNode::Loop { condition, body, .. } => {
|
||||
contains_assignment_to_name_in_node(condition, name)
|
||||
|| body
|
||||
.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name))
|
||||
}
|
||||
ASTNode::While { condition, body, .. } => {
|
||||
contains_assignment_to_name_in_node(condition, name)
|
||||
|| body
|
||||
.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name))
|
||||
}
|
||||
ASTNode::ForRange { body, .. } => body
|
||||
.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name)),
|
||||
ASTNode::TryCatch {
|
||||
try_body,
|
||||
catch_clauses,
|
||||
finally_body,
|
||||
..
|
||||
} => {
|
||||
try_body
|
||||
.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name))
|
||||
|| catch_clauses.iter().any(|c| {
|
||||
c.body
|
||||
.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name))
|
||||
})
|
||||
|| finally_body.as_ref().is_some_and(|b| {
|
||||
b.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name))
|
||||
})
|
||||
}
|
||||
ASTNode::ScopeBox { body, .. } => body
|
||||
.iter()
|
||||
.any(|n| contains_assignment_to_name_in_node(n, name)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{BinaryOperator, LiteralValue, Span};
|
||||
|
||||
fn span() -> Span {
|
||||
Span::unknown()
|
||||
}
|
||||
|
||||
fn var(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: span(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lit_i(value: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Integer(value),
|
||||
span: span(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp {
|
||||
operator: op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
span: span(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_single_ok() {
|
||||
// local ch = 0; if (ch < 1) { break }
|
||||
let body = vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["ch".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(0)))],
|
||||
span: span(),
|
||||
},
|
||||
ASTNode::If {
|
||||
condition: Box::new(bin(BinaryOperator::Less, var("ch"), lit_i(1))),
|
||||
then_body: vec![ASTNode::Break { span: span() }],
|
||||
else_body: None,
|
||||
span: span(),
|
||||
},
|
||||
];
|
||||
|
||||
let slot = ReadOnlyBodyLocalSlotBox::extract_single(&[String::from("ch")], &body).unwrap();
|
||||
assert_eq!(slot.name, "ch");
|
||||
assert_eq!(slot.decl_stmt_index, 0);
|
||||
assert_eq!(slot.break_guard_stmt_index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_single_reject_assignment() {
|
||||
let body = vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["ch".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(0)))],
|
||||
span: span(),
|
||||
},
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(var("ch")),
|
||||
value: Box::new(lit_i(1)),
|
||||
span: span(),
|
||||
},
|
||||
ASTNode::If {
|
||||
condition: Box::new(bin(BinaryOperator::Less, var("ch"), lit_i(1))),
|
||||
then_body: vec![ASTNode::Break { span: span() }],
|
||||
else_body: None,
|
||||
span: span(),
|
||||
},
|
||||
];
|
||||
|
||||
let err = ReadOnlyBodyLocalSlotBox::extract_single(&[String::from("ch")], &body)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("[joinir/freeze]"));
|
||||
assert!(err.contains("read-only"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_single_reject_decl_after_break_if() {
|
||||
let body = vec![
|
||||
ASTNode::If {
|
||||
condition: Box::new(bin(BinaryOperator::Less, var("ch"), lit_i(1))),
|
||||
then_body: vec![ASTNode::Break { span: span() }],
|
||||
else_body: None,
|
||||
span: span(),
|
||||
},
|
||||
ASTNode::Local {
|
||||
variables: vec!["ch".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(0)))],
|
||||
span: span(),
|
||||
},
|
||||
];
|
||||
|
||||
let err = ReadOnlyBodyLocalSlotBox::extract_single(&[String::from("ch")], &body)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("[joinir/freeze]"));
|
||||
assert!(err.contains("must appear before"));
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,6 @@
|
||||
//! 2. Condition must be pure expression (no side effects)
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::carrier_info::CarrierVar;
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use crate::mir::join_ir::lowering::condition_lowerer::lower_condition_to_joinir;
|
||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv; // Phase 92 P2-2
|
||||
@ -181,6 +180,7 @@ mod tests {
|
||||
1, // else_delta (normal: i + 1)
|
||||
&mut alloc_value,
|
||||
&env,
|
||||
None,
|
||||
&mut instructions,
|
||||
);
|
||||
|
||||
@ -223,6 +223,7 @@ mod tests {
|
||||
2, // else_delta (SAME! Should fail)
|
||||
&mut alloc_value,
|
||||
&env,
|
||||
None,
|
||||
&mut instructions,
|
||||
);
|
||||
|
||||
@ -256,6 +257,7 @@ mod tests {
|
||||
1, // else_delta
|
||||
&mut alloc_value,
|
||||
&env,
|
||||
None,
|
||||
&mut instructions,
|
||||
);
|
||||
|
||||
|
||||
118
src/mir/join_ir/lowering/common/dual_value_rewriter.rs
Normal file
118
src/mir/join_ir/lowering/common/dual_value_rewriter.rs
Normal file
@ -0,0 +1,118 @@
|
||||
//! Phase 246-EX / 247-EX: Dual-value rewrite helpers (Box)
|
||||
//!
|
||||
//! Purpose: isolate name-based rewrite rules for promoted condition carriers
|
||||
//! and loop-local derived carriers, so Pattern2 lowering remains structural.
|
||||
//!
|
||||
//! This module is intentionally narrow and fail-fast-ish:
|
||||
//! - It only performs rewrites when it can prove the required body-local source exists.
|
||||
//! - Otherwise it leaves the original instructions/behavior unchanged.
|
||||
|
||||
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
|
||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||
use crate::mir::join_ir::{CompareOp, ConstValue, JoinInst, MirLikeInst, UnaryOp};
|
||||
use crate::mir::ValueId;
|
||||
|
||||
/// Rewrite a lowered break-condition instruction stream to use fresh body-local values
|
||||
/// instead of stale promoted carrier parameters.
|
||||
///
|
||||
/// Current supported rewrite:
|
||||
/// - `!<is_*>` where operand matches a carrier Join ValueId, and body-local provides `<name>`
|
||||
/// derived from `carrier.name.strip_prefix("is_")`.
|
||||
pub fn rewrite_break_condition_insts(
|
||||
insts: Vec<JoinInst>,
|
||||
carrier_info: &CarrierInfo,
|
||||
body_local_env: Option<&LoopBodyLocalEnv>,
|
||||
alloc_value: &mut dyn FnMut() -> ValueId,
|
||||
) -> Vec<JoinInst> {
|
||||
let mut out = Vec::with_capacity(insts.len());
|
||||
|
||||
for inst in insts.into_iter() {
|
||||
match inst {
|
||||
JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand,
|
||||
dst,
|
||||
}) => {
|
||||
let mut operand_value = operand;
|
||||
|
||||
// Check if operand is a promoted carrier (e.g., is_digit)
|
||||
for carrier in &carrier_info.carriers {
|
||||
if carrier.join_id == Some(operand_value) {
|
||||
if let Some(stripped) = carrier.name.strip_prefix("is_") {
|
||||
let source_name = stripped.to_string();
|
||||
if let Some(src_val) = body_local_env.and_then(|env| env.get(&source_name)) {
|
||||
// Emit fresh comparison: is_* = (source >= 0)
|
||||
let zero = alloc_value();
|
||||
out.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: zero,
|
||||
value: ConstValue::Integer(0),
|
||||
}));
|
||||
|
||||
let fresh_bool = alloc_value();
|
||||
out.push(JoinInst::Compute(MirLikeInst::Compare {
|
||||
dst: fresh_bool,
|
||||
op: CompareOp::Ge,
|
||||
lhs: src_val,
|
||||
rhs: zero,
|
||||
}));
|
||||
|
||||
operand_value = fresh_bool;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.push(JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||
dst,
|
||||
op: UnaryOp::Not,
|
||||
operand: operand_value,
|
||||
}));
|
||||
}
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Try to derive an updated value for a loop-local derived carrier (e.g., `<x>_value`)
|
||||
/// from a body-local `<x>_pos` value.
|
||||
pub fn try_derive_looplocal_from_bodylocal_pos(
|
||||
carrier_name: &str,
|
||||
body_local_env: Option<&LoopBodyLocalEnv>,
|
||||
) -> Option<ValueId> {
|
||||
let stripped = carrier_name.strip_suffix("_value")?;
|
||||
let source_name = format!("{}_pos", stripped);
|
||||
body_local_env.and_then(|env| env.get(&source_name))
|
||||
}
|
||||
|
||||
/// Try to derive a condition-only boolean carrier (e.g., `is_<x>`) from a body-local `<x>_pos`.
|
||||
///
|
||||
/// Emits: `cmp = (<x>_pos >= 0)` and returns `cmp` ValueId.
|
||||
pub fn try_derive_conditiononly_is_from_bodylocal_pos(
|
||||
carrier_name: &str,
|
||||
body_local_env: Option<&LoopBodyLocalEnv>,
|
||||
alloc_value: &mut dyn FnMut() -> ValueId,
|
||||
out: &mut Vec<JoinInst>,
|
||||
) -> Option<ValueId> {
|
||||
let stripped = carrier_name.strip_prefix("is_")?;
|
||||
let source_name = format!("{}_pos", stripped);
|
||||
let src_val = body_local_env.and_then(|env| env.get(&source_name))?;
|
||||
|
||||
let zero = alloc_value();
|
||||
out.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: zero,
|
||||
value: ConstValue::Integer(0),
|
||||
}));
|
||||
|
||||
let cmp = alloc_value();
|
||||
out.push(JoinInst::Compute(MirLikeInst::Compare {
|
||||
dst: cmp,
|
||||
op: CompareOp::Ge,
|
||||
lhs: src_val,
|
||||
rhs: zero,
|
||||
}));
|
||||
|
||||
Some(cmp)
|
||||
}
|
||||
|
||||
@ -86,6 +86,15 @@ pub fn lower_condition_to_joinir(
|
||||
Ok((result_value, instructions))
|
||||
}
|
||||
|
||||
/// Convenience wrapper: lower a condition without body-local support.
|
||||
pub fn lower_condition_to_joinir_no_body_locals(
|
||||
cond_ast: &ASTNode,
|
||||
alloc_value: &mut dyn FnMut() -> ValueId,
|
||||
env: &ConditionEnv,
|
||||
) -> Result<(ValueId, Vec<JoinInst>), String> {
|
||||
lower_condition_to_joinir(cond_ast, alloc_value, env, None)
|
||||
}
|
||||
|
||||
/// Recursive helper for condition lowering
|
||||
///
|
||||
/// Handles all supported AST node types and emits appropriate JoinIR instructions.
|
||||
@ -437,7 +446,7 @@ mod tests {
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env);
|
||||
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
||||
assert!(result.is_ok(), "Simple comparison should succeed");
|
||||
|
||||
let (_cond_value, instructions) = result.unwrap();
|
||||
@ -472,7 +481,7 @@ mod tests {
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env);
|
||||
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
||||
assert!(result.is_ok(), "Comparison with literal should succeed");
|
||||
|
||||
let (_cond_value, instructions) = result.unwrap();
|
||||
@ -523,7 +532,7 @@ mod tests {
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env);
|
||||
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
||||
assert!(result.is_ok(), "OR expression should succeed");
|
||||
|
||||
let (_cond_value, instructions) = result.unwrap();
|
||||
@ -559,11 +568,173 @@ mod tests {
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env);
|
||||
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
||||
assert!(result.is_ok(), "NOT operator should succeed");
|
||||
|
||||
let (_cond_value, instructions) = result.unwrap();
|
||||
// Should have: Compare, UnaryOp(Not)
|
||||
assert_eq!(instructions.len(), 2, "Should generate Compare + Not");
|
||||
}
|
||||
|
||||
/// Phase 92 P4 Level 2: Test body-local variable resolution
|
||||
///
|
||||
/// This test verifies that conditions can reference body-local variables
|
||||
/// (e.g., `ch == '\\'` in escape sequence patterns).
|
||||
///
|
||||
/// Variable resolution priority:
|
||||
/// 1. ConditionEnv (loop parameters, captured variables)
|
||||
/// 2. LoopBodyLocalEnv (body-local variables like `ch`)
|
||||
#[test]
|
||||
fn test_body_local_variable_resolution() {
|
||||
// Setup ConditionEnv with loop variable
|
||||
let mut env = ConditionEnv::new();
|
||||
env.insert("i".to_string(), ValueId(100));
|
||||
|
||||
// Setup LoopBodyLocalEnv with body-local variable
|
||||
let mut body_local_env = LoopBodyLocalEnv::new();
|
||||
body_local_env.insert("ch".to_string(), ValueId(200));
|
||||
|
||||
let mut value_counter = 300u32;
|
||||
let mut alloc_value = || {
|
||||
let id = ValueId(value_counter);
|
||||
value_counter += 1;
|
||||
id
|
||||
};
|
||||
|
||||
// AST: ch == "\\"
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Equal,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "ch".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::String("\\".to_string()),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
// Phase 92 P2-2: Use lower_condition_to_joinir with body_local_env
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env));
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Body-local variable resolution should succeed"
|
||||
);
|
||||
|
||||
let (cond_value, instructions) = result.unwrap();
|
||||
// Should have: Const("\\"), Compare(ch == "\\")
|
||||
assert_eq!(
|
||||
instructions.len(),
|
||||
2,
|
||||
"Should generate Const + Compare for body-local variable"
|
||||
);
|
||||
|
||||
// Verify the comparison uses the body-local variable's ValueId(200)
|
||||
if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) {
|
||||
assert_eq!(
|
||||
*lhs,
|
||||
ValueId(200),
|
||||
"Compare should use body-local variable ValueId(200)"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected Compare instruction at position 1");
|
||||
}
|
||||
|
||||
assert!(cond_value.0 >= 300, "Result should use newly allocated ValueId");
|
||||
}
|
||||
|
||||
/// Phase 92 P4 Level 2: Test variable resolution priority (ConditionEnv takes precedence)
|
||||
///
|
||||
/// When a variable exists in both ConditionEnv and LoopBodyLocalEnv,
|
||||
/// ConditionEnv should take priority.
|
||||
#[test]
|
||||
fn test_variable_resolution_priority() {
|
||||
// Setup both environments with overlapping variable "x"
|
||||
let mut env = ConditionEnv::new();
|
||||
env.insert("x".to_string(), ValueId(100)); // ConditionEnv priority
|
||||
|
||||
let mut body_local_env = LoopBodyLocalEnv::new();
|
||||
body_local_env.insert("x".to_string(), ValueId(200)); // Should be shadowed
|
||||
|
||||
let mut value_counter = 300u32;
|
||||
let mut alloc_value = || {
|
||||
let id = ValueId(value_counter);
|
||||
value_counter += 1;
|
||||
id
|
||||
};
|
||||
|
||||
// AST: x == 42
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Equal,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(42),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env));
|
||||
assert!(result.is_ok(), "Variable resolution should succeed");
|
||||
|
||||
let (_cond_value, instructions) = result.unwrap();
|
||||
|
||||
// Verify the comparison uses ConditionEnv's ValueId(100), not LoopBodyLocalEnv's ValueId(200)
|
||||
if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) {
|
||||
assert_eq!(
|
||||
*lhs,
|
||||
ValueId(100),
|
||||
"ConditionEnv should take priority over LoopBodyLocalEnv"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected Compare instruction at position 1");
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 92 P4 Level 2: Test error handling for undefined variables
|
||||
///
|
||||
/// Variables not found in either environment should produce clear error messages.
|
||||
#[test]
|
||||
fn test_undefined_variable_error() {
|
||||
let env = ConditionEnv::new();
|
||||
let body_local_env = LoopBodyLocalEnv::new();
|
||||
|
||||
let mut value_counter = 300u32;
|
||||
let mut alloc_value = || {
|
||||
let id = ValueId(value_counter);
|
||||
value_counter += 1;
|
||||
id
|
||||
};
|
||||
|
||||
// AST: undefined_var == 42
|
||||
let ast = ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Equal,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "undefined_var".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(42),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env));
|
||||
assert!(result.is_err(), "Undefined variable should fail");
|
||||
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.contains("undefined_var"),
|
||||
"Error message should mention the undefined variable name"
|
||||
);
|
||||
assert!(
|
||||
err.contains("not found"),
|
||||
"Error message should indicate variable was not found"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,9 @@
|
||||
|
||||
// Re-export public API from specialized modules
|
||||
pub use super::condition_env::{ConditionBinding, ConditionEnv};
|
||||
pub use super::condition_lowerer::{lower_condition_to_joinir, lower_value_expression};
|
||||
pub use super::condition_lowerer::{
|
||||
lower_condition_to_joinir, lower_condition_to_joinir_no_body_locals, lower_value_expression,
|
||||
};
|
||||
pub use super::condition_var_extractor::extract_condition_variables;
|
||||
|
||||
// Re-export JoinIR types for convenience
|
||||
@ -86,7 +88,7 @@ mod api_tests {
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env);
|
||||
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
@ -143,7 +145,7 @@ mod api_tests {
|
||||
id
|
||||
};
|
||||
|
||||
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env);
|
||||
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let (_cond_value, instructions) = result.unwrap();
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
//! - Feature-gated (no-op in production)
|
||||
//! - Zero runtime cost when disabled
|
||||
|
||||
use crate::config::env::is_joinir_debug;
|
||||
use crate::config::env::{is_joinir_debug, joinir_dev_enabled};
|
||||
|
||||
/// DebugOutputBox: Centralized debug output for JoinIR lowering
|
||||
///
|
||||
@ -50,6 +50,28 @@ impl DebugOutputBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a DebugOutputBox with an explicit enabled flag.
|
||||
///
|
||||
/// Use this when the caller already has a higher-level gate (e.g. a `verbose` flag)
|
||||
/// and wants consistent formatting without re-checking env vars.
|
||||
pub fn new_with_enabled(context_tag: impl Into<String>, enabled: bool) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
context_tag: context_tag.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a DebugOutputBox enabled by JoinIR dev mode (NYASH_JOINIR_DEV=1).
|
||||
///
|
||||
/// This is useful for "developer convenience" logs that should not require
|
||||
/// explicitly setting HAKO_JOINIR_DEBUG, but still must stay opt-in.
|
||||
pub fn new_dev(context_tag: impl Into<String>) -> Self {
|
||||
Self {
|
||||
enabled: joinir_dev_enabled(),
|
||||
context_tag: context_tag.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a debug message with category
|
||||
///
|
||||
/// Output format: `[context_tag/category] message`
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
//! **Fail-Safe**: Unsupported AST nodes return explicit errors, allowing callers
|
||||
//! to fall back to legacy paths.
|
||||
|
||||
use super::condition_lowerer::lower_condition_to_joinir;
|
||||
use super::condition_lowerer::lower_condition_to_joinir_no_body_locals;
|
||||
use super::scope_manager::ScopeManager;
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::builder::MirBuilder;
|
||||
@ -211,7 +211,7 @@ impl<'env, 'builder, S: ScopeManager> ExprLowerer<'env, 'builder, S> {
|
||||
};
|
||||
|
||||
let (result_value, instructions) =
|
||||
lower_condition_to_joinir(ast, &mut alloc_value, &condition_env, None) // Phase 92 P2-2
|
||||
lower_condition_to_joinir_no_body_locals(ast, &mut alloc_value, &condition_env) // Phase 92 P2-2
|
||||
.map_err(|e| ExprLoweringError::LoweringError(e))?;
|
||||
|
||||
// Phase 235: 保存しておき、テストから観察できるようにする
|
||||
@ -297,7 +297,7 @@ impl<'env, 'builder, S: ScopeManager> ConditionLoweringBox<S> for ExprLowerer<'e
|
||||
|
||||
// Delegate to the well-tested lowerer, but use the caller-provided allocator (SSOT).
|
||||
let (result_value, instructions) =
|
||||
lower_condition_to_joinir(condition, &mut *context.alloc_value, &condition_env, None) // Phase 92 P2-2
|
||||
lower_condition_to_joinir_no_body_locals(condition, &mut *context.alloc_value, &condition_env) // Phase 92 P2-2
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
self.last_instructions = instructions;
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
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::lowering::method_call_lowerer::MethodCallLowerer;
|
||||
use crate::mir::join_ir::{BinOpKind, ConstValue, JoinInst, MirLikeInst};
|
||||
@ -131,6 +132,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
body_ast: &[ASTNode],
|
||||
env: &mut LoopBodyLocalEnv,
|
||||
) -> Result<(), String> {
|
||||
let debug = DebugOutputBox::new_dev("loop_body_local_init");
|
||||
for node in body_ast {
|
||||
if let ASTNode::Local {
|
||||
variables,
|
||||
@ -138,7 +140,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
..
|
||||
} = node
|
||||
{
|
||||
self.lower_single_init(variables, initial_values, env)?;
|
||||
self.lower_single_init(variables, initial_values, env, &debug)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -160,40 +162,29 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
variables: &[String],
|
||||
initial_values: &[Option<Box<ASTNode>>],
|
||||
env: &mut LoopBodyLocalEnv,
|
||||
debug: &DebugOutputBox,
|
||||
) -> Result<(), String> {
|
||||
// Handle each variable-value pair
|
||||
for (var_name, maybe_init_expr) in variables.iter().zip(initial_values.iter()) {
|
||||
// Skip if already has JoinIR ValueId (avoid duplicate lowering)
|
||||
if env.get(var_name).is_some() {
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Skipping '{}' (already has ValueId)",
|
||||
var_name
|
||||
);
|
||||
debug.log("skip", &format!("'{}' (already has ValueId)", var_name));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if no initialization expression (e.g., `local temp` without `= ...`)
|
||||
let Some(init_expr) = maybe_init_expr else {
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Skipping '{}' (no init expression)",
|
||||
var_name
|
||||
);
|
||||
debug.log("skip", &format!("'{}' (no init expression)", var_name));
|
||||
continue;
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Lowering init for '{}': {:?}",
|
||||
var_name, init_expr
|
||||
);
|
||||
debug.log_if_enabled(|| format!("lower '{}' = {:?}", var_name, init_expr));
|
||||
|
||||
// Lower init expression to JoinIR
|
||||
// Phase 226: Pass env for cascading LoopBodyLocal support
|
||||
let value_id = self.lower_init_expr(init_expr, env)?;
|
||||
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Stored '{}' → {:?}",
|
||||
var_name, value_id
|
||||
);
|
||||
debug.log("store", &format!("'{}' -> {:?}", var_name, value_id));
|
||||
|
||||
// Store in env
|
||||
env.insert(var_name.clone(), value_id);
|
||||
@ -229,6 +220,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
expr: &ASTNode,
|
||||
env: &LoopBodyLocalEnv,
|
||||
) -> Result<ValueId, String> {
|
||||
let debug = DebugOutputBox::new_dev("loop_body_local_init");
|
||||
match expr {
|
||||
// Constant literal: 42, 0, 1, "string" (use Literal with value)
|
||||
ASTNode::Literal { value, .. } => {
|
||||
@ -239,10 +231,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
dst: vid,
|
||||
value: ConstValue::Integer(*i),
|
||||
}));
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Const({}) → {:?}",
|
||||
i, vid
|
||||
);
|
||||
debug.log("const", &format!("Int({}) -> {:?}", i, vid));
|
||||
Ok(vid)
|
||||
}
|
||||
// Phase 193: String literal support (for method args like "0")
|
||||
@ -252,10 +241,7 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
dst: vid,
|
||||
value: ConstValue::String(s.clone()),
|
||||
}));
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Const(\"{}\") → {:?}",
|
||||
s, vid
|
||||
);
|
||||
debug.log("const", &format!("String({:?}) -> {:?}", s, vid));
|
||||
Ok(vid)
|
||||
}
|
||||
_ => Err(format!(
|
||||
@ -273,16 +259,13 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
name
|
||||
)
|
||||
})?;
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Variable({}) → {:?}",
|
||||
name, vid
|
||||
);
|
||||
debug.log("var", &format!("Variable({}) → {:?}", name, vid));
|
||||
Ok(vid)
|
||||
}
|
||||
|
||||
// Binary operation: pos - start, i * 2, etc.
|
||||
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||
eprintln!("[loop_body_local_init] BinaryOp({:?})", operator);
|
||||
debug.log("binop", &format!("BinaryOp({:?})", operator));
|
||||
|
||||
// Recursively lower operands
|
||||
// Phase 226: Pass env for cascading support
|
||||
@ -301,9 +284,9 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
rhs,
|
||||
}));
|
||||
|
||||
eprintln!(
|
||||
"[loop_body_local_init] BinOp({:?}, {:?}, {:?}) → {:?}",
|
||||
op_kind, lhs, rhs, result
|
||||
debug.log(
|
||||
"binop",
|
||||
&format!("BinOp({:?}, {:?}, {:?}) → {:?}", op_kind, lhs, rhs, result),
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
@ -420,14 +403,18 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
instructions: &mut Vec<JoinInst>,
|
||||
alloc: &mut dyn FnMut() -> ValueId,
|
||||
) -> Result<ValueId, String> {
|
||||
eprintln!(
|
||||
"[loop_body_local_init] MethodCall: {}.{}(...)",
|
||||
if let ASTNode::Variable { name, .. } = receiver {
|
||||
name
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
method
|
||||
let debug = DebugOutputBox::new_dev("loop_body_local_init");
|
||||
debug.log(
|
||||
"method_call",
|
||||
&format!(
|
||||
"MethodCall: {}.{}(...)",
|
||||
if let ASTNode::Variable { name, .. } = receiver {
|
||||
name
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
method
|
||||
),
|
||||
);
|
||||
|
||||
// 1. Resolve receiver (check LoopBodyLocalEnv first, then ConditionEnv)
|
||||
@ -436,15 +423,15 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
ASTNode::Variable { name, .. } => {
|
||||
// Try LoopBodyLocalEnv first (for cascading cases like `digit_pos = digits.indexOf(ch)` where `digits` might be body-local)
|
||||
if let Some(vid) = body_local_env.get(name) {
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Receiver '{}' found in LoopBodyLocalEnv → {:?}",
|
||||
name, vid
|
||||
debug.log(
|
||||
"method_call",
|
||||
&format!("Receiver '{}' found in LoopBodyLocalEnv → {:?}", name, vid),
|
||||
);
|
||||
vid
|
||||
} else if let Some(vid) = cond_env.get(name) {
|
||||
eprintln!(
|
||||
"[loop_body_local_init] Receiver '{}' found in ConditionEnv → {:?}",
|
||||
name, vid
|
||||
debug.log(
|
||||
"method_call",
|
||||
&format!("Receiver '{}' found in ConditionEnv → {:?}", name, vid),
|
||||
);
|
||||
vid
|
||||
} else {
|
||||
@ -485,9 +472,9 @@ impl<'a> LoopBodyLocalInitLowerer<'a> {
|
||||
instructions,
|
||||
)?;
|
||||
|
||||
eprintln!(
|
||||
"[loop_body_local_init] MethodCallLowerer completed → {:?}",
|
||||
result_id
|
||||
debug.log(
|
||||
"method_call",
|
||||
&format!("MethodCallLowerer completed → {:?}", result_id),
|
||||
);
|
||||
|
||||
Ok(result_id)
|
||||
|
||||
@ -77,19 +77,36 @@ use crate::mir::join_ir::lowering::step_schedule::{
|
||||
build_pattern2_schedule, Pattern2ScheduleContext, Pattern2StepKind,
|
||||
};
|
||||
use crate::mir::join_ir::lowering::update_env::UpdateEnv;
|
||||
use crate::mir::loop_canonicalizer::LoopSkeleton;
|
||||
use crate::mir::join_ir::{
|
||||
BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst,
|
||||
BinOpKind, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst,
|
||||
UnaryOp,
|
||||
};
|
||||
use crate::mir::loop_pattern_detection::error_messages::{
|
||||
extract_body_local_names, format_unsupported_condition_error,
|
||||
extract_body_local_names,
|
||||
};
|
||||
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox;
|
||||
use crate::mir::ValueId;
|
||||
use crate::mir::join_ir::lowering::error_tags;
|
||||
use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox;
|
||||
use boundary_builder::build_fragment_meta;
|
||||
use header_break_lowering::{lower_break_condition, lower_header_condition};
|
||||
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
|
||||
|
||||
pub(crate) struct LoopWithBreakLoweringInputs<'a> {
|
||||
pub scope: LoopScopeShape,
|
||||
pub condition: &'a ASTNode,
|
||||
pub break_condition: &'a ASTNode,
|
||||
pub env: &'a ConditionEnv,
|
||||
pub carrier_info: &'a CarrierInfo,
|
||||
pub carrier_updates: &'a BTreeMap<String, UpdateExpr>,
|
||||
pub body_ast: &'a [ASTNode],
|
||||
pub body_local_env: Option<&'a mut LoopBodyLocalEnv>,
|
||||
pub allowed_body_locals_for_conditions: Option<&'a [String]>,
|
||||
pub join_value_space: &'a mut JoinValueSpace,
|
||||
pub skeleton: Option<&'a LoopSkeleton>,
|
||||
}
|
||||
|
||||
/// Lower Pattern 2 (Loop with Conditional Break) to JoinIR
|
||||
///
|
||||
/// # Phase 188-Impl-2: Pure JoinIR Fragment Generation
|
||||
@ -150,17 +167,24 @@ use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for deter
|
||||
/// * `join_value_space` - Phase 201: Unified JoinIR ValueId allocator (Local region: 1000+)
|
||||
/// * `skeleton` - Phase 92 P0-3: Optional LoopSkeleton for ConditionalStep support
|
||||
pub(crate) fn lower_loop_with_break_minimal(
|
||||
_scope: LoopScopeShape,
|
||||
condition: &ASTNode,
|
||||
break_condition: &ASTNode,
|
||||
env: &ConditionEnv,
|
||||
carrier_info: &CarrierInfo,
|
||||
carrier_updates: &BTreeMap<String, UpdateExpr>, // Phase 222.5-D: HashMap → BTreeMap for determinism
|
||||
body_ast: &[ASTNode],
|
||||
mut body_local_env: Option<&mut LoopBodyLocalEnv>,
|
||||
join_value_space: &mut JoinValueSpace,
|
||||
skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>,
|
||||
inputs: LoopWithBreakLoweringInputs<'_>,
|
||||
) -> Result<(JoinModule, JoinFragmentMeta), String> {
|
||||
let LoopWithBreakLoweringInputs {
|
||||
scope: _scope,
|
||||
condition,
|
||||
break_condition,
|
||||
env,
|
||||
carrier_info,
|
||||
carrier_updates,
|
||||
body_ast,
|
||||
body_local_env,
|
||||
allowed_body_locals_for_conditions,
|
||||
join_value_space,
|
||||
skeleton,
|
||||
} = inputs;
|
||||
|
||||
let mut body_local_env = body_local_env;
|
||||
let dev_log = DebugOutputBox::new_dev("joinir/pattern2");
|
||||
// Phase 170-D-impl-3: Validate that conditions only use supported variable scopes
|
||||
// LoopConditionScopeBox checks that loop conditions don't reference loop-body-local variables
|
||||
let loop_var_name = &carrier_info.loop_var_name; // Phase 176-3: Extract from CarrierInfo
|
||||
@ -174,32 +198,38 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
let unpromoted_locals: Vec<&String> = body_local_names
|
||||
.iter()
|
||||
.filter(|name| !carrier_info.promoted_loopbodylocals.contains(*name))
|
||||
.filter(|name| {
|
||||
allowed_body_locals_for_conditions
|
||||
.map(|allow| !allow.iter().any(|s| s.as_str() == (*name).as_str()))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
if !unpromoted_locals.is_empty() {
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 224: {} body-local variables after promotion filter: {:?}",
|
||||
unpromoted_locals.len(),
|
||||
unpromoted_locals
|
||||
);
|
||||
return Err(format_unsupported_condition_error(
|
||||
"pattern2",
|
||||
&unpromoted_locals,
|
||||
));
|
||||
return Err(error_tags::freeze(&format!(
|
||||
"[pattern2/body_local_slot/contract/unhandled_vars] Unsupported LoopBodyLocal variables in condition: {:?} (promoted={:?}, allowed={:?})",
|
||||
unpromoted_locals,
|
||||
carrier_info.promoted_loopbodylocals,
|
||||
allowed_body_locals_for_conditions.unwrap_or(&[])
|
||||
)));
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 224: All {} body-local variables were promoted: {:?}",
|
||||
body_local_names.len(),
|
||||
body_local_names
|
||||
);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"Phase 224: All {} body-local variables were handled (promoted or allowed): {:?}",
|
||||
body_local_names.len(),
|
||||
body_local_names
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 170-D: Condition variables verified: {:?}",
|
||||
loop_cond_scope.var_names()
|
||||
);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"Phase 170-D: Condition variables verified: {:?}",
|
||||
loop_cond_scope.var_names()
|
||||
)
|
||||
});
|
||||
|
||||
// Phase 201: Use JoinValueSpace for unified ValueId allocation
|
||||
// - Local region (1000+) ensures no collision with Param region (100-999)
|
||||
@ -221,15 +251,17 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
// Phase 176-3: Multi-carrier support - allocate parameters for all carriers
|
||||
let carrier_count = carrier_info.carriers.len();
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 176-3: Generating JoinIR for {} carriers: {:?}",
|
||||
carrier_count,
|
||||
carrier_info
|
||||
.carriers
|
||||
.iter()
|
||||
.map(|c| &c.name)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"Phase 176-3: Generating JoinIR for {} carriers: {:?}",
|
||||
carrier_count,
|
||||
carrier_info
|
||||
.carriers
|
||||
.iter()
|
||||
.map(|c| &c.name)
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
});
|
||||
|
||||
// Phase 201: main() parameters use Local region (entry point slots)
|
||||
// These don't need to match ConditionEnv - they're just input slots for main
|
||||
@ -262,13 +294,15 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
carrier_param_ids.push(carrier_join_id);
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 201: loop_step params - i_param={:?}, carrier_params={:?}",
|
||||
i_param, carrier_param_ids
|
||||
);
|
||||
if crate::config::env::joinir_dev_enabled() || crate::config::env::joinir_test_debug_enabled() {
|
||||
eprintln!(
|
||||
"[joinir/pattern2/debug] loop_var='{}' env.get(loop_var)={:?}, carriers={:?}",
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"Phase 201: loop_step params - i_param={:?}, carrier_params={:?}",
|
||||
i_param, carrier_param_ids
|
||||
)
|
||||
});
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"loop_var='{}' env.get(loop_var)={:?}, carriers={:?}",
|
||||
loop_var_name,
|
||||
env.get(loop_var_name),
|
||||
carrier_info
|
||||
@ -276,8 +310,8 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
.iter()
|
||||
.map(|c| (c.name.clone(), c.join_id))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
// Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244: Lower condition
|
||||
let (cond_value, mut cond_instructions) = lower_header_condition(
|
||||
@ -398,10 +432,12 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
|
||||
init_lowerer.lower_inits_for_loop(body_ast, body_env)?;
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 191/246-EX: Lowered {} body-local init expressions (scheduled block before break)",
|
||||
body_env.len()
|
||||
);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"Phase 191/246-EX: Lowered {} body-local init expressions (scheduled block before break)",
|
||||
body_env.len()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@ -427,66 +463,14 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
// and lowered before body-local init. It references the carrier param which has stale values.
|
||||
//
|
||||
// Solution: Replace references to promoted carriers with fresh body-local computations.
|
||||
// For "!is_digit_pos", we replace "is_digit_pos" with a fresh comparison of "digit_pos >= 0".
|
||||
for inst in break_cond_instructions.into_iter() {
|
||||
if let JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand,
|
||||
dst,
|
||||
}) = inst
|
||||
{
|
||||
let mut operand_value = operand;
|
||||
// Check if operand is a promoted carrier (e.g., is_digit_pos)
|
||||
for carrier in &carrier_info.carriers {
|
||||
if carrier.join_id == Some(operand_value) {
|
||||
if let Some(stripped) = carrier.name.strip_prefix("is_") {
|
||||
// Phase 246-EX: "is_digit_pos" → "digit_pos" (no additional suffix needed)
|
||||
let source_name = stripped.to_string();
|
||||
if let Some(src_val) = body_local_env
|
||||
.as_ref()
|
||||
.and_then(|env| env.get(&source_name))
|
||||
{
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 246-EX: Rewriting break condition - replacing carrier '{}' ({:?}) with fresh body-local '{}' ({:?})",
|
||||
carrier.name, operand_value, source_name, src_val
|
||||
);
|
||||
|
||||
// Emit fresh comparison: is_digit_pos = (digit_pos >= 0)
|
||||
let zero = alloc_value();
|
||||
break_block.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: zero,
|
||||
value: ConstValue::Integer(0),
|
||||
}));
|
||||
|
||||
let fresh_bool = alloc_value();
|
||||
break_block.push(JoinInst::Compute(MirLikeInst::Compare {
|
||||
dst: fresh_bool,
|
||||
op: CompareOp::Ge,
|
||||
lhs: src_val,
|
||||
rhs: zero,
|
||||
}));
|
||||
|
||||
// Update the UnaryOp to use the fresh boolean
|
||||
operand_value = fresh_bool;
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 246-EX: Break condition now uses fresh value {:?} instead of stale carrier param {:?}",
|
||||
fresh_bool, carrier.join_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break_block.push(JoinInst::Compute(MirLikeInst::UnaryOp {
|
||||
dst,
|
||||
op: UnaryOp::Not,
|
||||
operand: operand_value,
|
||||
}));
|
||||
} else {
|
||||
break_block.push(inst);
|
||||
}
|
||||
}
|
||||
// (See common::dual_value_rewriter for the name-based rules.)
|
||||
use crate::mir::join_ir::lowering::common::dual_value_rewriter::rewrite_break_condition_insts;
|
||||
break_block.extend(rewrite_break_condition_insts(
|
||||
break_cond_instructions,
|
||||
carrier_info,
|
||||
body_local_env.as_ref().map(|e| &**e),
|
||||
&mut alloc_value,
|
||||
));
|
||||
|
||||
// Phase 176-3: Multi-carrier support - Jump includes all carrier values
|
||||
// Jump(k_exit, [i, carrier1, carrier2, ...], cond=break_cond) // Break exit path
|
||||
@ -510,24 +494,13 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
// 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 {
|
||||
if let Some(stripped) = carrier_name.strip_suffix("_value") {
|
||||
let source_name = format!("{}_pos", stripped);
|
||||
if let Some(src_val) = body_local_env
|
||||
.as_ref()
|
||||
.and_then(|env| env.get(&source_name))
|
||||
{
|
||||
updated_carrier_values.push(src_val);
|
||||
eprintln!(
|
||||
"[loop/carrier_update] Phase 247-EX: Loop-local carrier '{}' updated from body-local '{}' → {:?}",
|
||||
carrier_name, source_name, src_val
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
eprintln!(
|
||||
"[loop/carrier_update] Phase 247-EX WARNING: loop-local carrier '{}' could not find body-local source '{}'",
|
||||
carrier_name, source_name
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -538,33 +511,15 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
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
|
||||
if let Some(stripped) = carrier_name.strip_prefix("is_") {
|
||||
let source_name = format!("{}_pos", stripped);
|
||||
if let Some(src_val) = body_local_env
|
||||
.as_ref()
|
||||
.and_then(|env| env.get(&source_name))
|
||||
{
|
||||
let zero = alloc_value();
|
||||
carrier_update_block.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: zero,
|
||||
value: ConstValue::Integer(0),
|
||||
}));
|
||||
|
||||
let cmp = alloc_value();
|
||||
carrier_update_block.push(JoinInst::Compute(MirLikeInst::Compare {
|
||||
dst: cmp,
|
||||
op: CompareOp::Ge,
|
||||
lhs: src_val,
|
||||
rhs: zero,
|
||||
}));
|
||||
|
||||
updated_carrier_values.push(cmp);
|
||||
eprintln!(
|
||||
"[loop/carrier_update] Phase 247-EX: ConditionOnly carrier '{}' derived from body-local '{}' → {:?}",
|
||||
carrier_name, source_name, cmp
|
||||
);
|
||||
continue;
|
||||
}
|
||||
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_value,
|
||||
&mut carrier_update_block,
|
||||
) {
|
||||
updated_carrier_values.push(cmp);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ConditionOnly carrier fallback: just pass through the current value
|
||||
@ -573,10 +528,12 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
format!("ConditionOnly carrier '{}' not found in env", carrier_name)
|
||||
})?;
|
||||
updated_carrier_values.push(current_value);
|
||||
eprintln!(
|
||||
"[loop/carrier_update] Phase 227: ConditionOnly carrier '{}' passthrough: {:?}",
|
||||
carrier_name, current_value
|
||||
);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"[carrier_update] Phase 227: ConditionOnly '{}' passthrough: {:?}",
|
||||
carrier_name, current_value
|
||||
)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -590,10 +547,12 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
.get(carrier_name)
|
||||
.ok_or_else(|| format!("FromHost carrier '{}' not found in env", carrier_name))?;
|
||||
updated_carrier_values.push(current_value);
|
||||
eprintln!(
|
||||
"[loop/carrier_update] Phase 247-EX: FromHost carrier '{}' passthrough: {:?}",
|
||||
carrier_name, current_value
|
||||
);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"[carrier_update] Phase 247-EX: FromHost '{}' passthrough: {:?}",
|
||||
carrier_name, current_value
|
||||
)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -602,10 +561,12 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
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)
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 92 P2-1: ConditionalStep detected for carrier '{}': then={}, else={}",
|
||||
carrier_name, then_delta, else_delta
|
||||
);
|
||||
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(|| {
|
||||
@ -628,10 +589,12 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
&mut carrier_update_block,
|
||||
).map_err(|e| format!("[pattern2/conditional_step] {}", e))?;
|
||||
updated_carrier_values.push(updated_value);
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 92 P2-1: ConditionalStep carrier '{}' updated → {:?}",
|
||||
carrier_name, updated_value
|
||||
);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"Phase 92 P2-1: ConditionalStep carrier '{}' updated -> {:?}",
|
||||
carrier_name, updated_value
|
||||
)
|
||||
});
|
||||
continue; // Skip normal carrier update
|
||||
}
|
||||
}
|
||||
@ -670,10 +633,12 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
|
||||
updated_carrier_values.push(updated_value);
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 176-3: Carrier '{}' update: {:?} -> {:?}",
|
||||
carrier_name, carrier_param_ids[idx], 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
|
||||
@ -696,11 +661,13 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
let mut tail_call_args = vec![i_next];
|
||||
tail_call_args.extend(updated_carrier_values.iter().copied());
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2/debug] Tail call args count: {}, updated_carrier_values: {:?}",
|
||||
tail_call_args.len(),
|
||||
updated_carrier_values
|
||||
);
|
||||
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,
|
||||
@ -746,16 +713,16 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
|
||||
let debug_dump = crate::config::env::joinir_debug_level() > 0;
|
||||
if debug_dump {
|
||||
eprintln!(
|
||||
"[joinir/pattern2] k_exit param layout: i_exit={:?}, carrier_exit_ids={:?}",
|
||||
i_exit, carrier_exit_ids
|
||||
);
|
||||
let strict_debug = DebugOutputBox::new("joinir/pattern2");
|
||||
strict_debug.log_if_enabled(|| {
|
||||
format!(
|
||||
"k_exit param layout: i_exit={:?}, carrier_exit_ids={:?}",
|
||||
i_exit, carrier_exit_ids
|
||||
)
|
||||
});
|
||||
for (idx, carrier) in carrier_info.carriers.iter().enumerate() {
|
||||
let exit_id = carrier_exit_ids.get(idx).copied().unwrap_or(ValueId(0));
|
||||
eprintln!(
|
||||
"[joinir/pattern2] carrier '{}' exit → {:?}",
|
||||
carrier.name, exit_id
|
||||
);
|
||||
strict_debug.log("k_exit", &format!("carrier '{}' exit -> {:?}", carrier.name, exit_id));
|
||||
}
|
||||
}
|
||||
|
||||
@ -777,19 +744,21 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
// Set entry point
|
||||
join_module.entry = Some(main_id);
|
||||
|
||||
eprintln!("[joinir/pattern2] Generated JoinIR for Loop with Break Pattern (Phase 170-B)");
|
||||
eprintln!("[joinir/pattern2] Functions: main, loop_step, k_exit");
|
||||
eprintln!("[joinir/pattern2] Loop condition from AST (delegated to condition_to_joinir)");
|
||||
eprintln!("[joinir/pattern2] Break condition from AST (delegated to condition_to_joinir)");
|
||||
eprintln!("[joinir/pattern2] Exit PHI: k_exit receives i from both natural exit and break");
|
||||
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("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);
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 33-14/176-3: JoinFragmentMeta {{ expr_result: {:?}, carriers: {} }}",
|
||||
i_exit,
|
||||
carrier_info.carriers.len()
|
||||
);
|
||||
dev_log.log_if_enabled(|| {
|
||||
format!(
|
||||
"Phase 33-14/176-3: JoinFragmentMeta {{ expr_result: {:?}, carriers: {} }}",
|
||||
i_exit,
|
||||
carrier_info.carriers.len()
|
||||
)
|
||||
});
|
||||
|
||||
Ok((join_module, fragment_meta))
|
||||
}
|
||||
|
||||
@ -3,7 +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;
|
||||
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::expr_lowerer::{ExprContext, ExprLowerer};
|
||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
|
||||
@ -37,6 +38,8 @@ pub(crate) fn lower_header_condition(
|
||||
) -> Result<(ValueId, Vec<JoinInst>), String> {
|
||||
use crate::mir::join_ir::lowering::condition_lowering_box::ConditionLoweringBox;
|
||||
|
||||
let debug = DebugOutputBox::new_dev("joinir/pattern2");
|
||||
|
||||
let empty_body_env = LoopBodyLocalEnv::new();
|
||||
let empty_captured_env = CapturedEnv::new();
|
||||
let scope_manager = make_scope_manager(
|
||||
@ -61,9 +64,12 @@ pub(crate) fn lower_header_condition(
|
||||
match expr_lowerer.lower_condition(condition, &mut context) {
|
||||
Ok(value_id) => {
|
||||
let instructions = expr_lowerer.take_last_instructions();
|
||||
eprintln!(
|
||||
"[joinir/pattern2/phase244] Header condition via ConditionLoweringBox: {} instructions",
|
||||
instructions.len()
|
||||
debug.log(
|
||||
"phase244",
|
||||
&format!(
|
||||
"Header condition via ConditionLoweringBox: {} instructions",
|
||||
instructions.len()
|
||||
),
|
||||
);
|
||||
Ok((value_id, instructions))
|
||||
}
|
||||
@ -73,11 +79,12 @@ pub(crate) fn lower_header_condition(
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"[joinir/pattern2/phase244] Header condition via legacy path (not yet supported by ConditionLoweringBox)"
|
||||
debug.log(
|
||||
"phase244",
|
||||
"Header condition via legacy path (not yet supported by ConditionLoweringBox)",
|
||||
);
|
||||
let mut shim = || alloc_value();
|
||||
lower_condition_to_joinir(condition, &mut shim, env, None) // Phase 92 P2-2: No body-local for header
|
||||
lower_condition_to_joinir_no_body_locals(condition, &mut shim, env) // Phase 92 P2-2: No body-local for header
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -134,18 +134,19 @@ fn test_pattern2_header_condition_via_exprlowerer() {
|
||||
let carrier_updates = BTreeMap::new();
|
||||
let mut join_value_space = JoinValueSpace::new();
|
||||
|
||||
let result = lower_loop_with_break_minimal(
|
||||
let result = lower_loop_with_break_minimal(super::LoopWithBreakLoweringInputs {
|
||||
scope,
|
||||
&loop_cond,
|
||||
&break_cond,
|
||||
&condition_env,
|
||||
&carrier_info,
|
||||
&carrier_updates,
|
||||
&[],
|
||||
None,
|
||||
&mut join_value_space,
|
||||
None, // Phase 92 P0-3: skeleton=None for backward compatibility
|
||||
);
|
||||
condition: &loop_cond,
|
||||
break_condition: &break_cond,
|
||||
env: &condition_env,
|
||||
carrier_info: &carrier_info,
|
||||
carrier_updates: &carrier_updates,
|
||||
body_ast: &[],
|
||||
body_local_env: None,
|
||||
allowed_body_locals_for_conditions: None,
|
||||
join_value_space: &mut join_value_space,
|
||||
skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility
|
||||
});
|
||||
|
||||
assert!(result.is_ok(), "ExprLowerer header path should succeed");
|
||||
|
||||
|
||||
@ -60,7 +60,10 @@
|
||||
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, ConditionEnv};
|
||||
use crate::mir::join_ir::lowering::condition_to_joinir::{
|
||||
lower_condition_to_joinir_no_body_locals, 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;
|
||||
@ -144,22 +147,29 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
));
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern4] Phase 170-D: Condition variables verified: {:?}",
|
||||
loop_cond_scope.var_names()
|
||||
let debug = DebugOutputBox::new_dev("joinir/pattern4");
|
||||
debug.log(
|
||||
"phase170d",
|
||||
&format!(
|
||||
"Condition variables verified: {:?}",
|
||||
loop_cond_scope.var_names()
|
||||
),
|
||||
);
|
||||
|
||||
let mut join_module = JoinModule::new();
|
||||
let carrier_count = carrier_info.carriers.len();
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern4] Phase 202-C: Generating JoinIR for {} carriers: {:?}",
|
||||
carrier_count,
|
||||
carrier_info
|
||||
.carriers
|
||||
.iter()
|
||||
.map(|c| &c.name)
|
||||
.collect::<Vec<_>>()
|
||||
debug.log(
|
||||
"phase202c",
|
||||
&format!(
|
||||
"Generating JoinIR for {} carriers: {:?}",
|
||||
carrier_count,
|
||||
carrier_info
|
||||
.carriers
|
||||
.iter()
|
||||
.map(|c| &c.name)
|
||||
.collect::<Vec<_>>()
|
||||
),
|
||||
);
|
||||
|
||||
// ==================================================================
|
||||
@ -208,13 +218,18 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
// Phase 80-C (P2): Register BindingIds for condition variables (dev-only)
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
if let Some(binding_map) = binding_map {
|
||||
let debug = DebugOutputBox::new_dev("phase80/p4");
|
||||
|
||||
// Register loop variable BindingId
|
||||
if let Some(bid) = binding_map.get(&loop_var_name) {
|
||||
env.register_loop_var_binding(*bid, i_param);
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"[phase80/p4] Registered loop var '{}' BindingId({}) -> ValueId({})",
|
||||
loop_var_name, bid.0, i_param.0
|
||||
debug.log(
|
||||
"register",
|
||||
&format!(
|
||||
"Registered loop var '{}' BindingId({}) -> ValueId({})",
|
||||
loop_var_name, bid.0, i_param.0
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -223,9 +238,12 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
if let Some(bid) = binding_map.get(var_name) {
|
||||
env.register_condition_binding(*bid, *join_id);
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"[phase80/p4] Registered condition binding '{}' BindingId({}) -> ValueId({})",
|
||||
var_name, bid.0, join_id.0
|
||||
debug.log(
|
||||
"register",
|
||||
&format!(
|
||||
"Registered condition binding '{}' BindingId({}) -> ValueId({})",
|
||||
var_name, bid.0, join_id.0
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -244,6 +262,8 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
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();
|
||||
@ -271,7 +291,13 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
match expr_lowerer.lower_condition(condition, &mut context) {
|
||||
Ok(value_id) => {
|
||||
let instructions = expr_lowerer.take_last_instructions();
|
||||
eprintln!("[joinir/pattern4/phase244] Header condition via ConditionLoweringBox: {} instructions", instructions.len());
|
||||
debug.log(
|
||||
"phase244",
|
||||
&format!(
|
||||
"Header condition via ConditionLoweringBox: {} instructions",
|
||||
instructions.len()
|
||||
),
|
||||
);
|
||||
(value_id, instructions)
|
||||
}
|
||||
Err(e) => {
|
||||
@ -283,8 +309,11 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
}
|
||||
} else {
|
||||
// Legacy path: condition_to_joinir (for complex conditions not yet supported)
|
||||
eprintln!("[joinir/pattern4/phase244] Header condition via legacy path (not yet supported by ConditionLoweringBox)");
|
||||
lower_condition_to_joinir(condition, &mut alloc_value, &env, None)? // Phase 92 P2-2: No body-local for header
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
@ -443,20 +472,26 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
let carrier_name = &carrier_info.carriers[idx].name;
|
||||
|
||||
// Phase 197: Extract RHS from update expression metadata
|
||||
eprintln!(
|
||||
"[loop_with_continue_minimal] Processing carrier '{}' (idx={})",
|
||||
carrier_name, idx
|
||||
debug.log(
|
||||
"carrier_update",
|
||||
&format!("Processing carrier '{}' (idx={})", carrier_name, idx),
|
||||
);
|
||||
let rhs = if let Some(update_expr) = carrier_updates.get(carrier_name) {
|
||||
eprintln!(
|
||||
"[loop_with_continue_minimal] Found update expr: {:?}",
|
||||
update_expr
|
||||
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 {
|
||||
eprintln!("[loop_with_continue_minimal] Warning: carrier '{}' uses unsupported operator {:?}, defaulting to Add", carrier_name, op);
|
||||
debug.log(
|
||||
"warn",
|
||||
&format!(
|
||||
"Carrier '{}' uses unsupported operator {:?}, defaulting to Add",
|
||||
carrier_name, op
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Generate RHS value based on update expression
|
||||
@ -466,7 +501,13 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
const_1
|
||||
} else {
|
||||
// Need to allocate a new constant value
|
||||
eprintln!("[loop_with_continue_minimal] Warning: carrier '{}' uses const {}, only const_1 is pre-allocated, using const_1", carrier_name, n);
|
||||
debug.log(
|
||||
"warn",
|
||||
&format!(
|
||||
"Carrier '{}' uses const {}, only const_1 is pre-allocated, using const_1",
|
||||
carrier_name, n
|
||||
),
|
||||
);
|
||||
const_1
|
||||
}
|
||||
}
|
||||
@ -474,19 +515,34 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
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
|
||||
eprintln!("[loop_with_continue_minimal] Using i_next (ValueId({})) for variable '{}'", i_next.0, var_name);
|
||||
debug.log(
|
||||
"carrier_update",
|
||||
&format!(
|
||||
"Using i_next (ValueId({})) for variable '{}'",
|
||||
i_next.0, var_name
|
||||
),
|
||||
);
|
||||
i_next
|
||||
} else {
|
||||
eprintln!("[loop_with_continue_minimal] Warning: carrier '{}' updates with unknown variable '{}', using const_1", carrier_name, var_name);
|
||||
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 { .. } => {
|
||||
eprintln!(
|
||||
"[loop_with_continue_minimal] Phase 190: Carrier '{}' has number accumulation - not supported in Pattern 4, using Select passthrough",
|
||||
carrier_name
|
||||
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)
|
||||
@ -502,9 +558,12 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
// 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 => {
|
||||
eprintln!(
|
||||
"[loop_with_continue_minimal] Phase 178: Carrier '{}' has string/complex update - skipping BinOp, using Select passthrough",
|
||||
carrier_name
|
||||
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)
|
||||
@ -524,21 +583,36 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
if *n == 1 {
|
||||
const_1
|
||||
} else {
|
||||
eprintln!("[loop_with_continue_minimal] Warning: carrier '{}' uses const {}, only const_1 is pre-allocated, using const_1", carrier_name, n);
|
||||
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)
|
||||
eprintln!("[loop_with_continue_minimal] Warning: no update expression for carrier '{}', defaulting to +1", carrier_name);
|
||||
debug.log(
|
||||
"warn",
|
||||
&format!(
|
||||
"No update expression for carrier '{}', defaulting to +1",
|
||||
carrier_name
|
||||
),
|
||||
);
|
||||
const_1
|
||||
};
|
||||
|
||||
// carrier_next = carrier_param + rhs
|
||||
eprintln!(
|
||||
"[loop_with_continue_minimal] Generating: ValueId({}) = ValueId({}) + ValueId({})",
|
||||
carrier_next.0, carrier_param.0, rhs.0
|
||||
debug.log(
|
||||
"carrier_update",
|
||||
&format!(
|
||||
"Generating: ValueId({}) = ValueId({}) + ValueId({})",
|
||||
carrier_next.0, carrier_param.0, rhs.0
|
||||
),
|
||||
);
|
||||
loop_step_func
|
||||
.body
|
||||
@ -595,18 +669,21 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
// Set entry point
|
||||
join_module.entry = Some(main_id);
|
||||
|
||||
eprintln!("[joinir/pattern4] Phase 202-C: Generated JoinIR for Loop with Continue Pattern");
|
||||
eprintln!("[joinir/pattern4] Functions: main, loop_step, k_exit");
|
||||
eprintln!("[joinir/pattern4] Continue: Select-based skip");
|
||||
eprintln!("[joinir/pattern4] ValueId allocation: JoinValueSpace (Local region 1000+)");
|
||||
eprintln!(
|
||||
"[joinir/pattern4] Carriers: {} ({:?})",
|
||||
carrier_count,
|
||||
carrier_info
|
||||
.carriers
|
||||
.iter()
|
||||
.map(|c| c.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
debug.log_simple("Phase 202-C: Generated JoinIR for Loop with Continue Pattern");
|
||||
debug.log_simple("Functions: main, loop_step, k_exit");
|
||||
debug.log_simple("Continue: Select-based skip");
|
||||
debug.log_simple("ValueId allocation: JoinValueSpace (Local region 1000+)");
|
||||
debug.log(
|
||||
"summary",
|
||||
&format!(
|
||||
"Carriers: {} ({:?})",
|
||||
carrier_count,
|
||||
carrier_info
|
||||
.carriers
|
||||
.iter()
|
||||
.map(|c| c.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
),
|
||||
);
|
||||
|
||||
// ==================================================================
|
||||
@ -623,17 +700,23 @@ pub(crate) fn lower_loop_with_continue_minimal(
|
||||
// Phase 197-B: Use carrier_param_ids instead of carrier_exit_ids
|
||||
let exit_id = carrier_param_ids[idx];
|
||||
exit_values.push((carrier.name.clone(), exit_id));
|
||||
eprintln!(
|
||||
"[joinir/pattern4] ExitMeta: {} → ValueId({}) (carrier_param)",
|
||||
carrier.name, exit_id.0
|
||||
debug.log(
|
||||
"exit_meta",
|
||||
&format!(
|
||||
"ExitMeta: {} → ValueId({}) (carrier_param)",
|
||||
carrier.name, exit_id.0
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let exit_meta = ExitMeta::multiple(exit_values);
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern4] Phase 169: ExitMeta total: {} bindings (condition from AST)",
|
||||
exit_meta.exit_values.len()
|
||||
debug.log(
|
||||
"phase169",
|
||||
&format!(
|
||||
"ExitMeta total: {} bindings (condition from AST)",
|
||||
exit_meta.exit_values.len()
|
||||
),
|
||||
);
|
||||
|
||||
Ok((join_module, exit_meta))
|
||||
|
||||
@ -123,16 +123,19 @@ pub fn build_pattern2_minimal_structured() -> JoinModule {
|
||||
let mut join_value_space = JoinValueSpace::new();
|
||||
|
||||
let (module, _) = lower_loop_with_break_minimal(
|
||||
scope,
|
||||
&loop_cond,
|
||||
&break_cond,
|
||||
&condition_env,
|
||||
&carrier_info,
|
||||
&carrier_updates,
|
||||
&[],
|
||||
None,
|
||||
&mut join_value_space,
|
||||
None, // Phase 92 P0-3: skeleton=None for backward compatibility
|
||||
crate::mir::join_ir::lowering::loop_with_break_minimal::LoopWithBreakLoweringInputs {
|
||||
scope,
|
||||
condition: &loop_cond,
|
||||
break_condition: &break_cond,
|
||||
env: &condition_env,
|
||||
carrier_info: &carrier_info,
|
||||
carrier_updates: &carrier_updates,
|
||||
body_ast: &[],
|
||||
body_local_env: None,
|
||||
allowed_body_locals_for_conditions: None,
|
||||
join_value_space: &mut join_value_space,
|
||||
skeleton: None, // Phase 92 P0-3: skeleton=None for backward compatibility
|
||||
},
|
||||
)
|
||||
.expect("pattern2 minimal lowering should succeed");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user