feat(joinir): Phase 33-14 JoinFragmentMeta for expr/carrier separation
Introduces JoinFragmentMeta to distinguish between loop expression results and carrier variable updates, fixing SSA correctness issues. ## Changes ### New: JoinFragmentMeta struct (carrier_info.rs) - `expr_result: Option<ValueId>` - Loop as expression (return loop(...)) - `exit_meta: ExitMeta` - Carrier updates for variable_map - Helper methods: with_expr_result(), carrier_only(), empty() ### Pattern 2 Lowerer Updates - loop_with_break_minimal.rs: Returns (JoinModule, JoinFragmentMeta) - pattern2_with_break.rs: Sets boundary.expr_result from fragment_meta ### instruction_rewriter.rs - Phase 33-14: Only add to exit_phi_inputs when boundary.expr_result is Some - Phase 33-13: MergeResult struct with carrier_inputs map ### JoinInlineBoundary (inline_boundary.rs) - New field: expr_result: Option<ValueId> - All constructors updated with expr_result: None default ## Design Philosophy Previously, exit_phi_inputs mixed expr results with carrier updates, causing: - PHI inputs referencing undefined remapped values - SSA-undef errors in VM execution With JoinFragmentMeta: - expr_result → exit_phi_inputs (generates PHI for expr value) - exit_meta → carrier_inputs (updates variable_map via carrier PHIs) ## Test Results - Pattern 1 (carrier-only): Works correctly (no exit_phi_inputs) - Pattern 2 (expr result): Design complete, SSA-undef fix deferred to Phase 33-15 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -206,6 +206,115 @@ pub struct ExitMeta {
|
||||
pub exit_values: Vec<(String, ValueId)>,
|
||||
}
|
||||
|
||||
/// Phase 33-14: JoinFragmentMeta - Distinguishes expr result from carrier updates
|
||||
///
|
||||
/// ## Purpose
|
||||
///
|
||||
/// Separates two distinct use cases for JoinIR loops:
|
||||
///
|
||||
/// 1. **Expr Result Pattern** (joinir_min_loop.hako):
|
||||
/// ```nyash
|
||||
/// local result = loop(...) { ... } // Loop used as expression
|
||||
/// return result
|
||||
/// ```
|
||||
/// Here, the k_exit return value is the "expr result" that should go to exit_phi_inputs.
|
||||
///
|
||||
/// 2. **Carrier Update Pattern** (trim pattern):
|
||||
/// ```nyash
|
||||
/// loop(...) { start = start + 1 } // Loop used for side effects
|
||||
/// print(start) // Use carrier after loop
|
||||
/// ```
|
||||
/// Here, there's no "expr result" - only carrier variable updates.
|
||||
///
|
||||
/// ## SSA Correctness
|
||||
///
|
||||
/// Previously, exit_phi_inputs mixed expr results with carrier updates, causing:
|
||||
/// - PHI inputs that referenced undefined remapped values
|
||||
/// - SSA-undef errors in VM execution
|
||||
///
|
||||
/// With JoinFragmentMeta:
|
||||
/// - `expr_result`: Only goes to exit_phi_inputs (generates PHI for expr value)
|
||||
/// - `exit_meta`: Only goes to carrier_inputs (updates variable_map via carrier PHIs)
|
||||
///
|
||||
/// ## Example: Pattern 2 (joinir_min_loop.hako)
|
||||
///
|
||||
/// ```rust
|
||||
/// JoinFragmentMeta {
|
||||
/// expr_result: Some(i_exit), // k_exit returns i as expr value
|
||||
/// exit_meta: ExitMeta::single("i".to_string(), i_exit), // Also a carrier
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Example: Pattern 3 (trim pattern)
|
||||
///
|
||||
/// ```rust
|
||||
/// JoinFragmentMeta {
|
||||
/// expr_result: None, // Loop doesn't return a value
|
||||
/// exit_meta: ExitMeta::multiple(vec![
|
||||
/// ("start".to_string(), start_exit),
|
||||
/// ("end".to_string(), end_exit),
|
||||
/// ]),
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JoinFragmentMeta {
|
||||
/// Expression result ValueId from k_exit (JoinIR-local)
|
||||
///
|
||||
/// - `Some(vid)`: Loop is used as expression, k_exit's return value → exit_phi_inputs
|
||||
/// - `None`: Loop is used for side effects only, no PHI for expr value
|
||||
pub expr_result: Option<ValueId>,
|
||||
|
||||
/// Carrier variable exit bindings (existing ExitMeta)
|
||||
///
|
||||
/// Maps carrier names to their JoinIR-local exit values.
|
||||
/// These go to carrier_inputs for carrier PHI generation.
|
||||
pub exit_meta: ExitMeta,
|
||||
}
|
||||
|
||||
impl JoinFragmentMeta {
|
||||
/// Create JoinFragmentMeta for expression result pattern
|
||||
///
|
||||
/// Use when the loop returns a value (like `return loop(...)`).
|
||||
pub fn with_expr_result(expr_result: ValueId, exit_meta: ExitMeta) -> Self {
|
||||
Self {
|
||||
expr_result: Some(expr_result),
|
||||
exit_meta,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create JoinFragmentMeta for carrier-only pattern
|
||||
///
|
||||
/// Use when the loop only updates carriers (like trim pattern).
|
||||
pub fn carrier_only(exit_meta: ExitMeta) -> Self {
|
||||
Self {
|
||||
expr_result: None,
|
||||
exit_meta,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create empty JoinFragmentMeta (no expr result, no carriers)
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
expr_result: None,
|
||||
exit_meta: ExitMeta::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this fragment has an expression result
|
||||
pub fn has_expr_result(&self) -> bool {
|
||||
self.expr_result.is_some()
|
||||
}
|
||||
|
||||
/// Phase 33-14: Backward compatibility - convert to ExitMeta
|
||||
///
|
||||
/// During migration, some code may still expect ExitMeta.
|
||||
/// This extracts just the carrier bindings.
|
||||
#[deprecated(since = "33-14", note = "Use exit_meta directly for carrier access")]
|
||||
pub fn to_exit_meta(&self) -> ExitMeta {
|
||||
self.exit_meta.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExitMeta {
|
||||
/// Create new ExitMeta with no exit values
|
||||
pub fn empty() -> Self {
|
||||
|
||||
@ -205,6 +205,33 @@ pub struct JoinInlineBoundary {
|
||||
///
|
||||
/// This replaces `condition_inputs` to ensure proper ValueId separation.
|
||||
pub condition_bindings: Vec<super::condition_to_joinir::ConditionBinding>,
|
||||
|
||||
/// Phase 33-14: Expression result ValueId (JoinIR-local)
|
||||
///
|
||||
/// If the loop is used as an expression (like `return loop(...)`), this field
|
||||
/// contains the JoinIR-local ValueId of k_exit's return value.
|
||||
///
|
||||
/// - `Some(ValueId)`: Loop returns a value → k_exit return goes to exit_phi_inputs
|
||||
/// - `None`: Loop only updates carriers → no exit_phi_inputs generation
|
||||
///
|
||||
/// # Example: joinir_min_loop.hako (expr result pattern)
|
||||
///
|
||||
/// ```nyash
|
||||
/// loop(i < 3) { if (i >= 2) { break } i = i + 1 }
|
||||
/// return i
|
||||
/// ```
|
||||
///
|
||||
/// Here, `expr_result = Some(i_exit)` because the loop's result is used.
|
||||
///
|
||||
/// # Example: trim pattern (carrier-only)
|
||||
///
|
||||
/// ```nyash
|
||||
/// loop(start < end) { start = start + 1 }
|
||||
/// print(start) // Uses carrier after loop
|
||||
/// ```
|
||||
///
|
||||
/// Here, `expr_result = None` because the loop doesn't return a value.
|
||||
pub expr_result: Option<crate::mir::ValueId>,
|
||||
}
|
||||
|
||||
impl JoinInlineBoundary {
|
||||
@ -229,6 +256,7 @@ impl JoinInlineBoundary {
|
||||
#[allow(deprecated)]
|
||||
condition_inputs: vec![], // Phase 171: Default to empty (deprecated)
|
||||
condition_bindings: vec![], // Phase 171-fix: Default to empty
|
||||
expr_result: None, // Phase 33-14: Default to carrier-only pattern
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,6 +296,7 @@ impl JoinInlineBoundary {
|
||||
#[allow(deprecated)]
|
||||
condition_inputs: vec![], // Phase 171: Default to empty (deprecated)
|
||||
condition_bindings: vec![], // Phase 171-fix: Default to empty
|
||||
expr_result: None, // Phase 33-14
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,6 +332,7 @@ impl JoinInlineBoundary {
|
||||
#[allow(deprecated)]
|
||||
condition_inputs: vec![], // Phase 171: Default to empty (deprecated)
|
||||
condition_bindings: vec![], // Phase 171-fix: Default to empty
|
||||
expr_result: None, // Phase 33-14
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,6 +390,7 @@ impl JoinInlineBoundary {
|
||||
#[allow(deprecated)]
|
||||
condition_inputs: vec![], // Phase 171: Default to empty (deprecated)
|
||||
condition_bindings: vec![], // Phase 171-fix: Default to empty
|
||||
expr_result: None, // Phase 33-14
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,6 +434,7 @@ impl JoinInlineBoundary {
|
||||
#[allow(deprecated)]
|
||||
condition_inputs,
|
||||
condition_bindings: vec![], // Phase 171-fix: Will be populated by new constructor
|
||||
expr_result: None, // Phase 33-14
|
||||
}
|
||||
}
|
||||
|
||||
@ -450,6 +482,7 @@ impl JoinInlineBoundary {
|
||||
#[allow(deprecated)]
|
||||
condition_inputs,
|
||||
condition_bindings: vec![], // Phase 171-fix: Will be populated by new constructor
|
||||
expr_result: None, // Phase 33-14
|
||||
}
|
||||
}
|
||||
|
||||
@ -504,6 +537,7 @@ impl JoinInlineBoundary {
|
||||
#[allow(deprecated)]
|
||||
condition_inputs: vec![], // Deprecated, use condition_bindings instead
|
||||
condition_bindings,
|
||||
expr_result: None, // Phase 33-14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
//! Following the "80/20 rule" from CLAUDE.md - get it working first, generalize later.
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::carrier_info::ExitMeta;
|
||||
use crate::mir::join_ir::lowering::carrier_info::{ExitMeta, JoinFragmentMeta};
|
||||
use crate::mir::join_ir::lowering::condition_to_joinir::{lower_condition_to_joinir, ConditionEnv};
|
||||
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
|
||||
use crate::mir::join_ir::{
|
||||
@ -109,9 +109,11 @@ use crate::mir::ValueId;
|
||||
/// This ensures JoinIR never accesses HOST ValueIds directly.
|
||||
///
|
||||
/// # Phase 172-3: ExitMeta Return
|
||||
/// # Phase 33-14: JoinFragmentMeta Return
|
||||
///
|
||||
/// Returns `(JoinModule, ExitMeta)` where ExitMeta contains the JoinIR-local ValueId
|
||||
/// of the k_exit parameter. This allows the caller to build proper exit_bindings.
|
||||
/// Returns `(JoinModule, JoinFragmentMeta)` where:
|
||||
/// - `expr_result`: k_exit's return value (i_exit) - this is what `return i` uses
|
||||
/// - `exit_meta`: carrier bindings for variable_map updates
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@ -121,7 +123,7 @@ pub fn lower_loop_with_break_minimal(
|
||||
condition: &ASTNode,
|
||||
env: &ConditionEnv,
|
||||
loop_var_name: &str,
|
||||
) -> Result<(JoinModule, ExitMeta), String> {
|
||||
) -> Result<(JoinModule, JoinFragmentMeta), String> {
|
||||
// Phase 188-Impl-2: Use local ValueId allocator (sequential from 0)
|
||||
// JoinIR has NO knowledge of host ValueIds - boundary handled separately
|
||||
let mut value_counter = 0u32;
|
||||
@ -305,9 +307,11 @@ pub fn lower_loop_with_break_minimal(
|
||||
eprintln!("[joinir/pattern2] Condition from AST (not hardcoded)");
|
||||
eprintln!("[joinir/pattern2] Exit PHI: k_exit receives i from both natural exit and break");
|
||||
|
||||
// Phase 172-3: Build ExitMeta with k_exit parameter's ValueId
|
||||
// Phase 172-3 → Phase 33-14: Build JoinFragmentMeta with expr_result
|
||||
// Pattern 2: Loop is used as expression → `return i` means k_exit's return is expr_result
|
||||
let exit_meta = ExitMeta::single(loop_var_name.to_string(), i_exit);
|
||||
eprintln!("[joinir/pattern2] Phase 172-3: ExitMeta {{ {} → {:?} }}", loop_var_name, i_exit);
|
||||
let fragment_meta = JoinFragmentMeta::with_expr_result(i_exit, exit_meta);
|
||||
eprintln!("[joinir/pattern2] Phase 33-14: JoinFragmentMeta {{ expr_result: {:?}, carrier: {} }}", i_exit, loop_var_name);
|
||||
|
||||
Ok((join_module, exit_meta))
|
||||
Ok((join_module, fragment_meta))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user