diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs b/src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs index 438a8e99..2ff7ff2c 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs @@ -311,63 +311,13 @@ impl MirBuilder { /// 3. **Condition captures consistency**: plan captures vs condition bindings (warn-only) /// /// Phase 70-A: Standardized error tag for runtime unsupported patterns. +/// Phase 71-Pre: Delegated to OwnershipPlanValidator box. #[cfg(feature = "normalized_dev")] fn check_ownership_plan_consistency( plan: &crate::mir::join_ir::ownership::OwnershipPlan, carrier_info: &crate::mir::join_ir::lowering::carrier_info::CarrierInfo, condition_bindings: &std::collections::BTreeSet, ) -> Result<(), String> { - use std::collections::BTreeSet; - - // Check 1: Multi-hop relay is rejected (Fail-Fast) - // Tag: [ownership/relay:runtime_unsupported] - standardized for Phase 70-A - for relay in &plan.relay_writes { - if relay.relay_path.len() > 1 { - return Err(format!( - "[ownership/relay:runtime_unsupported] Multihop relay not executable yet: var='{}', owner={:?}, relay_path={:?}", - relay.name, relay.owner_scope, relay.relay_path - )); - } - } - - // Check 2: Carrier set consistency (warn-only, order SSOT deferred) - let plan_carriers: BTreeSet = plan - .owned_vars - .iter() - .filter(|v| v.is_written) - .map(|v| v.name.clone()) - .collect(); - - let existing_carriers: BTreeSet = carrier_info - .carriers - .iter() - .map(|c| c.name.clone()) - .collect(); - - if plan_carriers != existing_carriers { - eprintln!("[phase64/ownership] Carrier set mismatch (warn-only, order SSOT deferred):"); - eprintln!(" OwnershipPlan carriers: {:?}", plan_carriers); - eprintln!(" Existing carriers: {:?}", existing_carriers); - // Don't fail - just warn (order SSOT not yet implemented) - } - - // Check 3: Condition captures consistency (warn-only) - let plan_cond_captures: BTreeSet = plan - .condition_captures - .iter() - .map(|c| c.name.clone()) - .collect(); - - if !plan_cond_captures.is_subset(condition_bindings) { - let extra: Vec<_> = plan_cond_captures - .difference(condition_bindings) - .collect(); - eprintln!( - "[phase64/ownership] Extra condition captures in plan (warn-only): {:?}", - extra - ); - // Warn only - this might be expected in some cases - } - - Ok(()) + use crate::mir::join_ir::ownership::OwnershipPlanValidator; + OwnershipPlanValidator::validate_all(plan, carrier_info, condition_bindings) } diff --git a/src/mir/join_ir/ownership/mod.rs b/src/mir/join_ir/ownership/mod.rs index 9e16ee0d..8f402d96 100644 --- a/src/mir/join_ir/ownership/mod.rs +++ b/src/mir/join_ir/ownership/mod.rs @@ -24,6 +24,7 @@ //! - Phase 57: Analyzer implemented (dev-only) //! - Phase 58: plan_to_lowering helper for P2 (analyzer-based testing only) //! - Phase 59: plan_to_lowering helper for P3 (if-sum patterns) +//! - Phase 71-Pre: plan_validator box (reusable validation) mod types; mod analyzer; @@ -31,6 +32,8 @@ mod analyzer; mod plan_to_lowering; #[cfg(feature = "normalized_dev")] mod ast_analyzer; +#[cfg(feature = "normalized_dev")] +mod plan_validator; pub use types::*; pub use analyzer::*; @@ -38,3 +41,5 @@ pub use analyzer::*; pub use plan_to_lowering::*; #[cfg(feature = "normalized_dev")] pub use ast_analyzer::*; +#[cfg(feature = "normalized_dev")] +pub use plan_validator::*; diff --git a/src/mir/join_ir/ownership/plan_validator.rs b/src/mir/join_ir/ownership/plan_validator.rs new file mode 100644 index 00000000..b6a202be --- /dev/null +++ b/src/mir/join_ir/ownership/plan_validator.rs @@ -0,0 +1,193 @@ +//! OwnershipPlan Validator Box +//! +//! Phase 71-Pre: Extracted from pattern3_with_if_phi.rs for reuse across patterns. +//! +//! # Responsibility +//! +//! Validates OwnershipPlan against CarrierInfo and condition bindings. +//! This is **analysis-only** - no MIR generation or lowering. +//! +//! # Checks +//! +//! 1. **Relay support**: Multi-hop relay → Err with `[ownership/relay:runtime_unsupported]` +//! 2. **Carrier consistency**: Plan carriers vs existing carriers (warn-only) +//! 3. **Condition captures**: Plan captures vs condition bindings (warn-only) + +use super::OwnershipPlan; +use crate::mir::join_ir::lowering::carrier_info::CarrierInfo; +use std::collections::BTreeSet; + +/// Ownership Plan Validator +/// +/// Provides reusable validation methods for OwnershipPlan consistency checks. +/// Used by Pattern 2, 3, 4 lowering to ensure plan integrity before execution. +pub struct OwnershipPlanValidator; + +impl OwnershipPlanValidator { + /// Validate relay support (Fail-Fast) + /// + /// Returns Err if any relay has `relay_path.len() > 1` (multi-hop). + /// Tag: `[ownership/relay:runtime_unsupported]` + /// + /// # Phase 70-A + /// + /// This is the standardized runtime guard. When Phase 70-B+ implements + /// multi-hop execution, this check will be relaxed. + pub fn validate_relay_support(plan: &OwnershipPlan) -> Result<(), String> { + for relay in &plan.relay_writes { + if relay.relay_path.len() > 1 { + return Err(format!( + "[ownership/relay:runtime_unsupported] Multihop relay not executable yet: var='{}', owner={:?}, relay_path={:?}", + relay.name, relay.owner_scope, relay.relay_path + )); + } + } + Ok(()) + } + + /// Validate carrier set consistency (warn-only) + /// + /// Compares plan's owned_vars (is_written=true) against existing CarrierInfo. + /// Warns on mismatch but does not fail (order SSOT not yet implemented). + pub fn validate_carrier_consistency( + plan: &OwnershipPlan, + carrier_info: &CarrierInfo, + ) -> Result<(), String> { + let plan_carriers: BTreeSet = plan + .owned_vars + .iter() + .filter(|v| v.is_written) + .map(|v| v.name.clone()) + .collect(); + + let existing_carriers: BTreeSet = carrier_info + .carriers + .iter() + .map(|c| c.name.clone()) + .collect(); + + if plan_carriers != existing_carriers { + eprintln!("[ownership/validator] Carrier set mismatch (warn-only):"); + eprintln!(" OwnershipPlan carriers: {:?}", plan_carriers); + eprintln!(" Existing carriers: {:?}", existing_carriers); + } + + Ok(()) + } + + /// Validate condition captures consistency (warn-only) + /// + /// Checks that plan's condition_captures are a subset of condition_bindings. + /// Warns on extra captures but does not fail. + pub fn validate_condition_captures( + plan: &OwnershipPlan, + condition_bindings: &BTreeSet, + ) -> Result<(), String> { + let plan_cond_captures: BTreeSet = plan + .condition_captures + .iter() + .map(|c| c.name.clone()) + .collect(); + + if !plan_cond_captures.is_subset(condition_bindings) { + let extra: Vec<_> = plan_cond_captures.difference(condition_bindings).collect(); + eprintln!( + "[ownership/validator] Extra condition captures in plan (warn-only): {:?}", + extra + ); + } + + Ok(()) + } + + /// Validate all checks (fail-fast on any Err) + /// + /// Runs all validation checks in order: + /// 1. validate_relay_support (Fail-Fast) + /// 2. validate_carrier_consistency (warn-only) + /// 3. validate_condition_captures (warn-only) + pub fn validate_all( + plan: &OwnershipPlan, + carrier_info: &CarrierInfo, + condition_bindings: &BTreeSet, + ) -> Result<(), String> { + Self::validate_relay_support(plan)?; + Self::validate_carrier_consistency(plan, carrier_info)?; + Self::validate_condition_captures(plan, condition_bindings)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole, CarrierVar}; + use crate::mir::join_ir::ownership::{CapturedVar, RelayVar, ScopeId, ScopeOwnedVar}; + use crate::mir::ValueId; + + #[test] + fn test_validate_relay_support_single_hop_ok() { + let mut plan = OwnershipPlan::new(ScopeId(1)); + plan.relay_writes.push(RelayVar { + name: "sum".to_string(), + owner_scope: ScopeId(0), + relay_path: vec![ScopeId(1)], // Single hop + }); + + let result = OwnershipPlanValidator::validate_relay_support(&plan); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_relay_support_multihop_rejected() { + let mut plan = OwnershipPlan::new(ScopeId(2)); + plan.relay_writes.push(RelayVar { + name: "sum".to_string(), + owner_scope: ScopeId(0), + relay_path: vec![ScopeId(2), ScopeId(1)], // Multi-hop + }); + + let result = OwnershipPlanValidator::validate_relay_support(&plan); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.contains("[ownership/relay:runtime_unsupported]"), + "Error should contain standard tag: {}", + err + ); + } + + #[test] + fn test_validate_all_with_consistent_data() { + let mut plan = OwnershipPlan::new(ScopeId(1)); + plan.owned_vars.push(ScopeOwnedVar { + name: "sum".to_string(), + is_written: true, + is_condition_only: false, + }); + plan.condition_captures.push(CapturedVar { + name: "limit".to_string(), + owner_scope: ScopeId(0), + }); + + let carrier_info = CarrierInfo { + loop_var_name: "i".to_string(), + loop_var_id: ValueId(0), + carriers: vec![CarrierVar::with_role_and_init( + "sum".to_string(), + ValueId(1), + CarrierRole::LoopState, + CarrierInit::FromHost, + )], + trim_helper: None, + promoted_loopbodylocals: vec![], + }; + + let condition_bindings: BTreeSet = + ["limit".to_string(), "i".to_string()].into_iter().collect(); + + let result = + OwnershipPlanValidator::validate_all(&plan, &carrier_info, &condition_bindings); + assert!(result.is_ok()); + } +}