feat(joinir): Phase 70-B - Multihop Relay Passthrough Support (dev-only)
Phase 70-B relaxes runtime guard from "always error" to "unsupported only". Adds structural detection (is_supported_multihop_pattern) to distinguish: - Simple passthrough (no self-updates): ACCEPT - Complex patterns (self-conflict, etc): REJECT with [ownership/relay:runtime_unsupported] Changes: - plan_validator.rs: +is_supported_multihop_pattern() method + unit tests - normalized_joinir_min.rs: +2 integration tests for passthrough/rejection - phase70-relay-runtime-guard.md: Phase 70 series status table added Tests: normalized_dev 52/52 PASS, lib 950/950 PASS Design: Structural detection only (no by-name branching) 🤖 Generated with Claude Code Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -13,7 +13,7 @@
|
||||
//! 2. **Carrier consistency**: Plan carriers vs existing carriers (warn-only)
|
||||
//! 3. **Condition captures**: Plan captures vs condition bindings (warn-only)
|
||||
|
||||
use super::OwnershipPlan;
|
||||
use super::{OwnershipPlan, RelayVar};
|
||||
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
@ -24,18 +24,54 @@ use std::collections::BTreeSet;
|
||||
pub struct OwnershipPlanValidator;
|
||||
|
||||
impl OwnershipPlanValidator {
|
||||
/// Validate relay support (Fail-Fast)
|
||||
/// Validate relay support (Fail-Fast with structural detection)
|
||||
///
|
||||
/// Returns Err if any relay has `relay_path.len() > 1` (multi-hop).
|
||||
/// Tag: `[ownership/relay:runtime_unsupported]`
|
||||
/// # Phase 70-B: Partial Multihop Support
|
||||
///
|
||||
/// # Phase 70-A
|
||||
/// This method now distinguishes between:
|
||||
/// - **Supported multihop**: Simple passthrough patterns (relay_path is contiguous, no self-updates)
|
||||
/// - **Unsupported multihop**: Complex patterns requiring full implementation
|
||||
///
|
||||
/// This is the standardized runtime guard. When Phase 70-B+ implements
|
||||
/// multi-hop execution, this check will be relaxed.
|
||||
/// 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 {
|
||||
if relay.relay_path.len() > 1 {
|
||||
// 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
|
||||
@ -45,6 +81,40 @@ impl OwnershipPlanValidator {
|
||||
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.
|
||||
@ -139,8 +209,33 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_relay_support_multihop_rejected() {
|
||||
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),
|
||||
@ -157,6 +252,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[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));
|
||||
|
||||
Reference in New Issue
Block a user