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:
nyash-codex
2025-12-16 21:37:07 +09:00
parent 568619df89
commit d2972c1437
43 changed files with 2615 additions and 980 deletions

View File

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

View File

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

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

View File

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

View 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)
}

View File

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

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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