Files
hakorune/src/mir/join_ir/ownership/plan_validator.rs

311 lines
11 KiB
Rust

//! 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, RelayVar};
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 with structural detection)
///
/// # Phase 70-B: Partial Multihop Support
///
/// This method now distinguishes between:
/// - **Supported multihop**: Simple passthrough patterns (relay_path is contiguous, no self-updates)
/// - **Unsupported multihop**: Complex patterns requiring full implementation
///
/// Returns Ok if:
/// - Single-hop relay (relay_path.len() == 1)
/// - Supported multihop pattern (relay_path.len() > 1, contiguous path, passthrough only)
///
/// Returns Err with `[ownership/relay:runtime_unsupported]` if:
/// - Unsupported multihop pattern detected
///
/// # Supported Multihop Pattern (Phase 70-B)
///
/// A multihop relay is supported if:
/// 1. `relay_path` is non-empty and contiguous (each scope is immediate child of next)
/// 2. Each intermediate scope is a pure passthrough (no self-updates to the relay variable)
/// 3. Owner scope will perform final merge at exit PHI
///
/// Example (3-layer loop, counter relay):
/// ```text
/// loop L1 {
/// local counter = 0 // owned by L1
/// loop L2 {
/// loop L3 {
/// counter++ // relay L3 → L2 → L1 (multihop)
/// }
/// }
/// }
/// ```
/// - L3 plan: relay_path = [L3, L2], owner = L1 (2 hops)
/// - L2 is pure passthrough (no self-update to counter)
/// - L1 performs exit PHI merge
///
/// Phase 70-A: Standardized runtime guard
/// Phase 70-B: Relaxed to accept simple passthrough patterns
pub fn validate_relay_support(plan: &OwnershipPlan) -> Result<(), String> {
for relay in &plan.relay_writes {
// Single-hop: always supported
if relay.relay_path.len() <= 1 {
continue;
}
// Multihop: check if it's a supported pattern
if !Self::is_supported_multihop_pattern(plan, relay) {
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(())
}
/// Check if a multihop relay matches supported pattern (Phase 70-B)
///
/// # Supported Pattern Criteria
///
/// 1. **Contiguous path**: relay_path must be non-empty
/// 2. **First hop is current scope**: relay_path[0] == plan.scope_id
/// 3. **No self-conflict**: relay variable not in owned_vars (passthrough only)
///
/// Note: Full contiguity check (parent-child relationship between scopes) would require
/// scope tree metadata. For Phase 70-B, we rely on analyzer correctness (relay_path
/// is already validated to be contiguous by analyzer).
fn is_supported_multihop_pattern(plan: &OwnershipPlan, relay: &RelayVar) -> bool {
// Check 1: relay_path must be non-empty (sanity check)
if relay.relay_path.is_empty() {
return false;
}
// Check 2: First hop must be current scope
if relay.relay_path[0] != plan.scope_id {
return false;
}
// Check 3: No self-conflict (passthrough only)
// If relay var appears in owned_vars, this scope is trying to own AND relay
// the same variable - not a pure passthrough
let is_passthrough = !plan.owned_vars.iter().any(|v| v.name == relay.name);
if !is_passthrough {
return false;
}
// All checks passed - this is a supported multihop pattern
true
}
/// 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<String> = plan
.owned_vars
.iter()
.filter(|v| v.is_written)
.map(|v| v.name.clone())
.collect();
let existing_carriers: BTreeSet<String> = 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<String>,
) -> Result<(), String> {
let plan_cond_captures: BTreeSet<String> = 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<String>,
) -> 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_passthrough_accepted() {
// Phase 70-B: Multihop relay with passthrough pattern should be accepted
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: L2 → L1 → L0
});
// No owned_vars for 'sum' - pure passthrough
let result = OwnershipPlanValidator::validate_relay_support(&plan);
assert!(
result.is_ok(),
"Phase 70-B: Passthrough multihop should be accepted, got: {:?}",
result
);
}
#[test]
fn test_validate_relay_support_multihop_self_conflict_rejected() {
// Phase 70-B: Multihop relay with self-conflict (owned + relay) should be rejected
let mut plan = OwnershipPlan::new(ScopeId(2));
plan.owned_vars.push(ScopeOwnedVar {
name: "sum".to_string(),
is_written: true,
is_condition_only: false,
});
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_relay_support_multihop_wrong_first_hop_rejected() {
// Phase 70-B: Multihop relay where first hop != plan.scope_id should be 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(3), ScopeId(1)], // Wrong first 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![],
#[cfg(feature = "normalized_dev")]
promoted_bindings: std::collections::BTreeMap::new(),
};
let condition_bindings: BTreeSet<String> =
["limit".to_string(), "i".to_string()].into_iter().collect();
let result =
OwnershipPlanValidator::validate_all(&plan, &carrier_info, &condition_bindings);
assert!(result.is_ok());
}
}