feat(joinir): Phase 227 - CarrierRole separation (LoopState vs ConditionOnly)

- Add CarrierRole enum to distinguish state carriers from condition-only carriers
- ConditionOnly carriers (is_digit_pos) skip exit PHI but keep header PHI
- Update all test struct literals with role field
- 877/884 tests PASS (7 pre-existing failures unrelated)

Remaining: ValueId(0) undefined - header PHI initialization for ConditionOnly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-10 20:07:30 +09:00
parent d28e54ba06
commit 478cc0012e
20 changed files with 163 additions and 6 deletions

View File

@ -18,6 +18,42 @@
use crate::mir::ValueId;
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
/// Phase 227: CarrierRole - Distinguishes loop state carriers from condition-only carriers
///
/// When LoopBodyLocal variables are promoted to carriers, we need to know whether
/// they carry loop state (need exit PHI) or are only used in conditions (no exit PHI).
///
/// # Example
///
/// ```ignore
/// // LoopState carrier: sum needs exit PHI (value persists after loop)
/// loop(i < n) {
/// sum = sum + i; // sum updated in loop body
/// }
/// print(sum); // sum used after loop
///
/// // ConditionOnly carrier: is_digit_pos does NOT need exit PHI
/// loop(p < s.length()) {
/// local digit_pos = digits.indexOf(s.substring(p, p+1));
/// if digit_pos < 0 { break; } // Only used in condition
/// num_str = num_str + ch;
/// p = p + 1;
/// }
/// // digit_pos not used after loop
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarrierRole {
/// Value needed after loop (sum, result, count, p, num_str)
/// - Participates in header PHI (loop iteration)
/// - Participates in exit PHI (final value after loop)
LoopState,
/// Only used for loop condition (is_digit_pos, is_whitespace)
/// - Participates in header PHI (loop iteration)
/// - Does NOT participate in exit PHI (not needed after loop)
ConditionOnly,
}
/// Phase 224-D: Alias for promoted LoopBodyLocal in condition expressions
///
/// When a LoopBodyLocal variable is promoted to a carrier, the original variable
@ -44,7 +80,7 @@ pub struct ConditionAlias {
/// Information about a single carrier variable
#[derive(Debug, Clone)]
pub struct CarrierVar {
/// Variable name (e.g., "sum", "printed")
/// Variable name (e.g., "sum", "printed", "is_digit_pos")
pub name: String,
/// Host ValueId for this variable (MIR側)
pub host_id: ValueId,
@ -56,6 +92,36 @@ pub struct CarrierVar {
/// - `Some(vid)`: Header PHI生成後にセットされる
/// - `None`: まだPHI生成前、または該当なし
pub join_id: Option<ValueId>,
/// Phase 227: Role of this carrier (LoopState or ConditionOnly)
///
/// - `LoopState`: Value needed after loop (participates in exit PHI)
/// - `ConditionOnly`: Only used for loop condition (no exit PHI)
pub role: CarrierRole,
}
impl CarrierVar {
/// Create a new CarrierVar with default LoopState role
///
/// This is the primary constructor for CarrierVar. Use this instead of
/// struct literal syntax to ensure role defaults to LoopState.
pub fn new(name: String, host_id: ValueId) -> Self {
Self {
name,
host_id,
join_id: None,
role: CarrierRole::LoopState,
}
}
/// Create a CarrierVar with explicit role
pub fn with_role(name: String, host_id: ValueId, role: CarrierRole) -> Self {
Self {
name,
host_id,
join_id: None,
role,
}
}
}
/// Complete carrier information for a loop
@ -130,6 +196,7 @@ impl CarrierInfo {
name: name.clone(),
host_id: id,
join_id: None, // Phase 177-STRUCT-1: Set by header PHI generation
role: CarrierRole::LoopState, // Phase 227: Default to LoopState
})
.collect();
@ -189,6 +256,7 @@ impl CarrierInfo {
name,
host_id,
join_id: None, // Phase 177-STRUCT-1: Set by header PHI generation
role: CarrierRole::LoopState, // Phase 227: Default to LoopState
});
}
@ -507,6 +575,7 @@ mod tests {
name: name.to_string(),
host_id: ValueId(id),
join_id: None, // Phase 177-STRUCT-1
role: CarrierRole::LoopState, // Phase 227: Default to LoopState
}
}

View File

@ -421,6 +421,7 @@ mod tests {
name: name.to_string(),
host_id: ValueId(host_id),
join_id: None, // Phase 177-STRUCT-1
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
}
}

View File

@ -43,6 +43,7 @@
//! ```
use crate::mir::ValueId;
use super::carrier_info::CarrierRole;
/// Explicit binding between JoinIR exit value and host variable
///
@ -68,13 +69,13 @@ use crate::mir::ValueId;
///
/// ```text
/// vec![
/// LoopExitBinding { carrier_name: "sum", join_exit_value: ValueId(18), host_slot: ValueId(5) },
/// LoopExitBinding { carrier_name: "count", join_exit_value: ValueId(19), host_slot: ValueId(6) },
/// LoopExitBinding { carrier_name: "sum", join_exit_value: ValueId(18), host_slot: ValueId(5), role: LoopState },
/// LoopExitBinding { carrier_name: "count", join_exit_value: ValueId(19), host_slot: ValueId(6), role: LoopState },
/// ]
/// ```
#[derive(Debug, Clone)]
pub struct LoopExitBinding {
/// Carrier variable name (e.g., "sum", "count")
/// Carrier variable name (e.g., "sum", "count", "is_digit_pos")
///
/// This is the variable name in the host's variable_map that should
/// receive the exit value.
@ -91,6 +92,13 @@ pub struct LoopExitBinding {
/// This is the host function's ValueId for the variable that should be
/// updated with the exit PHI result.
pub host_slot: ValueId,
/// Phase 227: Role of this carrier (LoopState or ConditionOnly)
///
/// Determines whether this carrier should participate in exit PHI:
/// - LoopState: Needs exit PHI (value used after loop)
/// - ConditionOnly: No exit PHI (only used in loop condition)
pub role: CarrierRole,
}
/// Boundary information for inlining a JoinIR fragment into a host function

View File

@ -27,6 +27,7 @@
use crate::mir::ValueId;
use super::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
use super::condition_to_joinir::ConditionBinding;
use super::carrier_info::CarrierRole;
/// Role of a parameter in JoinIR lowering (Phase 200-A)
///
@ -299,6 +300,7 @@ mod tests {
carrier_name: "sum".to_string(),
join_exit_value: ValueId(18),
host_slot: ValueId(5),
role: CarrierRole::LoopState,
};
let boundary = JoinInlineBoundaryBuilder::new()
@ -346,6 +348,7 @@ mod tests {
carrier_name: "sum".to_string(),
join_exit_value: ValueId(18),
host_slot: ValueId(101),
role: CarrierRole::LoopState,
}
])
.with_loop_var_name(Some("i".to_string()))
@ -372,11 +375,13 @@ mod tests {
carrier_name: "i".to_string(),
join_exit_value: ValueId(11),
host_slot: ValueId(100),
role: CarrierRole::LoopState,
},
LoopExitBinding {
carrier_name: "sum".to_string(),
join_exit_value: ValueId(20),
host_slot: ValueId(101),
role: CarrierRole::LoopState,
}
])
.with_loop_var_name(Some("i".to_string()))

View File

@ -298,6 +298,7 @@ mod tests {
name: "count".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None, // Phase 177-STRUCT-1
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
@ -355,6 +356,7 @@ mod tests {
name: "result".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
@ -413,6 +415,7 @@ mod tests {
name: "result".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);
@ -471,6 +474,7 @@ mod tests {
name: "result".to_string(),
host_id: crate::mir::ValueId(0),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
}];
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers);

View File

@ -377,6 +377,23 @@ pub(crate) fn lower_loop_with_break_minimal(
for (idx, carrier) in carrier_info.carriers.iter().enumerate() {
let carrier_name = &carrier.name;
// Phase 227: ConditionOnly carriers don't have update expressions
// They just pass through their current value unchanged
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
if carrier.role == CarrierRole::ConditionOnly {
// ConditionOnly carrier: just pass through the current value
// The carrier's ValueId from env is passed unchanged
let current_value = env.get(carrier_name).ok_or_else(|| {
format!("ConditionOnly carrier '{}' not found in env", carrier_name)
})?;
updated_carrier_values.push(current_value);
eprintln!(
"[loop/carrier_update] Phase 227: ConditionOnly carrier '{}' passthrough: {:?}",
carrier_name, current_value
);
continue;
}
// Get the update expression for this carrier
let update_expr = carrier_updates.get(carrier_name).ok_or_else(|| {
format!(