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:
@ -74,14 +74,27 @@ Verifies:
|
||||
|
||||
---
|
||||
|
||||
## Phase 70 Series
|
||||
|
||||
| Phase | Status | Description |
|
||||
|-------|--------|-------------|
|
||||
| Phase 70-A | ✅ Complete | Runtime guard with standard tag `[ownership/relay:runtime_unsupported]` |
|
||||
| Phase 70-B | ✅ Complete | Simple passthrough multihop support (contiguous path, no self-updates) |
|
||||
| Phase 70-C | ✅ Complete | Merge relay detection (multiple inner loops → same owner) |
|
||||
| Phase 70-D+ | 🚧 Future | Full runtime execution support (exit PHI merge, carrier propagation) |
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Phase 65: Multihop Design](phase65-ownership-relay-multihop-design.md)
|
||||
- [Phase 66: Multihop Implementation](phase65-ownership-relay-multihop-design.md#phase-66-implementation-status)
|
||||
- [Phase 70-C: Merge Relay](phase70c-merge-relay.md)
|
||||
- [Phase 56: Ownership-Relay Architecture](phase56-ownership-relay-design.md)
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
- **2025-12-13**: Phase 70-C completed - Merge relay detection and validation
|
||||
- **2025-12-13**: Phase 70-A created - Fail-Fast tag standardization
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -1872,3 +1872,309 @@ fn test_phase70a_multihop_relay_runtime_unsupported_tag() {
|
||||
// will be hit when P3 lowering encounters this plan at runtime.
|
||||
eprintln!("[phase70a/test] Runtime would fail with [ownership/relay:runtime_unsupported] tag");
|
||||
}
|
||||
|
||||
/// Phase 70-B: Test that simple passthrough multihop relay is accepted.
|
||||
///
|
||||
/// This test verifies the structural detection logic in OwnershipPlanValidator
|
||||
/// correctly identifies supported multihop patterns (pure passthrough, no self-updates).
|
||||
#[test]
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
fn test_phase70b_multihop_relay_simple_passthrough_succeeds() {
|
||||
use nyash_rust::mir::join_ir::ownership::{
|
||||
OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId, ScopeOwnedVar,
|
||||
};
|
||||
|
||||
// Build a plan for innermost loop (L3) with multihop relay
|
||||
// L3 writes to 'counter' owned by L1, relayed through L2
|
||||
let mut plan_l3 = OwnershipPlan::new(ScopeId(3)); // Inner loop scope
|
||||
plan_l3.owned_vars.push(ScopeOwnedVar {
|
||||
name: "i".to_string(), // loop variable
|
||||
is_written: true,
|
||||
is_condition_only: true,
|
||||
});
|
||||
plan_l3.relay_writes.push(RelayVar {
|
||||
name: "counter".to_string(),
|
||||
owner_scope: ScopeId(1), // L1 owns counter
|
||||
relay_path: vec![ScopeId(3), ScopeId(2)], // 2 hops: L3 → L2 → L1
|
||||
});
|
||||
// No owned_vars for 'counter' - pure passthrough from L3's perspective
|
||||
|
||||
// Phase 70-B: This should be accepted (passthrough pattern)
|
||||
let result = OwnershipPlanValidator::validate_relay_support(&plan_l3);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Phase 70-B: Simple passthrough multihop should be accepted, got: {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
eprintln!("[phase70b/test] Simple passthrough multihop relay accepted (3-layer loop)");
|
||||
}
|
||||
|
||||
/// Phase 70-B: Test that unsupported multihop patterns are still rejected.
|
||||
///
|
||||
/// This test verifies that complex patterns (e.g., self-conflict where a scope
|
||||
/// both owns and relays the same variable) are still rejected with the standard tag.
|
||||
#[test]
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
fn test_phase70b_multihop_relay_self_conflict_rejected() {
|
||||
use nyash_rust::mir::join_ir::ownership::{
|
||||
OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId, ScopeOwnedVar,
|
||||
};
|
||||
|
||||
// Build a plan where L3 both owns and relays 'counter' (conflict)
|
||||
// L3 → L2 → L1 (multihop with self-conflict at L3)
|
||||
let mut plan_l3 = OwnershipPlan::new(ScopeId(3)); // Inner loop scope
|
||||
plan_l3.owned_vars.push(ScopeOwnedVar {
|
||||
name: "counter".to_string(), // L3 owns counter
|
||||
is_written: true,
|
||||
is_condition_only: false,
|
||||
});
|
||||
plan_l3.relay_writes.push(RelayVar {
|
||||
name: "counter".to_string(), // L3 also relays counter (conflict)
|
||||
owner_scope: ScopeId(1),
|
||||
relay_path: vec![ScopeId(3), ScopeId(2)], // L3 → L2 → L1 (multihop)
|
||||
});
|
||||
|
||||
// Phase 70-B: This should be rejected (self-conflict)
|
||||
let result = OwnershipPlanValidator::validate_relay_support(&plan_l3);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Phase 70-B: Self-conflict multihop should be rejected"
|
||||
);
|
||||
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.contains("[ownership/relay:runtime_unsupported]"),
|
||||
"Error should contain standard tag: {}",
|
||||
err
|
||||
);
|
||||
|
||||
eprintln!("[phase70b/test] Self-conflict multihop relay correctly rejected");
|
||||
}
|
||||
|
||||
/// Phase 70-C: Merge Relay (multiple inner loops → same owner)
|
||||
///
|
||||
/// This test verifies that OwnershipAnalyzer correctly detects the "merge relay"
|
||||
/// pattern where multiple inner loops update the same owner variable.
|
||||
///
|
||||
/// Structure:
|
||||
/// ```text
|
||||
/// loop L1 {
|
||||
/// local total = 0 // owned by L1
|
||||
/// loop L2_A {
|
||||
/// total++ // L2_A → L1 relay
|
||||
/// }
|
||||
/// loop L2_B {
|
||||
/// total += 10 // L2_B → L1 relay
|
||||
/// }
|
||||
/// }
|
||||
/// // L1 exit: merge both L2_A and L2_B's updates to 'total'
|
||||
/// ```
|
||||
#[test]
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
fn test_phase70c_merge_relay_multiple_inner_loops_detected() {
|
||||
use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
||||
use nyash_rust::mir::join_ir::ownership::AstOwnershipAnalyzer;
|
||||
|
||||
// Build AST: loop L1 { local total=0; loop L2_A { total++ } loop L2_B { total+=10 } }
|
||||
fn var(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
fn lit_i(i: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Integer(i),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
fn lit_true() -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Bool(true),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
let ast = ASTNode::FunctionDeclaration {
|
||||
name: "test_merge_relay".to_string(),
|
||||
params: vec![],
|
||||
body: vec![
|
||||
// L1: outer loop
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(lit_true()),
|
||||
body: vec![
|
||||
// local total = 0 (L1 owns)
|
||||
ASTNode::Local {
|
||||
variables: vec!["total".to_string()],
|
||||
initial_values: vec![Some(Box::new(lit_i(0)))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
// L2_A: first inner loop (total++)
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(lit_true()),
|
||||
body: vec![
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(var("total")),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(var("total")),
|
||||
right: Box::new(lit_i(1)),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Break {
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
// L2_B: second inner loop (total += 10)
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(lit_true()),
|
||||
body: vec![
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(var("total")),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(var("total")),
|
||||
right: Box::new(lit_i(10)),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Break {
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Break {
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
is_static: false,
|
||||
is_override: false,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut analyzer = AstOwnershipAnalyzer::new();
|
||||
let plans = analyzer.analyze_ast(&ast).expect("analysis should succeed");
|
||||
|
||||
// Find L1 (owner of 'total')
|
||||
let l1_plan = plans
|
||||
.iter()
|
||||
.find(|p| p.owned_vars.iter().any(|v| v.name == "total"))
|
||||
.expect("expected L1 plan with owned total");
|
||||
let l1_scope_id = l1_plan.scope_id;
|
||||
|
||||
// Find L2_A and L2_B (both relay 'total' to L1)
|
||||
let relay_plans: Vec<_> = plans
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.relay_writes.iter().any(|r| r.name == "total")
|
||||
&& p.scope_id != l1_scope_id
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Verify: two inner loops relay to the same owner
|
||||
assert_eq!(
|
||||
relay_plans.len(),
|
||||
2,
|
||||
"Phase 70-C: Expected 2 inner loops relaying 'total' to L1, got {}",
|
||||
relay_plans.len()
|
||||
);
|
||||
|
||||
for (idx, plan) in relay_plans.iter().enumerate() {
|
||||
let relay = plan
|
||||
.relay_writes
|
||||
.iter()
|
||||
.find(|r| r.name == "total")
|
||||
.expect("expected 'total' in relay_writes");
|
||||
|
||||
// Verify: both relay to the same owner (L1)
|
||||
assert_eq!(
|
||||
relay.owner_scope, l1_scope_id,
|
||||
"Phase 70-C: relay {} should have owner_scope = L1",
|
||||
idx
|
||||
);
|
||||
|
||||
// Verify: single-hop relay (L2 → L1)
|
||||
assert_eq!(
|
||||
relay.relay_path.len(),
|
||||
1,
|
||||
"Phase 70-C: relay {} should have single-hop path",
|
||||
idx
|
||||
);
|
||||
|
||||
// Verify: relay_path[0] is this scope (L2_A or L2_B)
|
||||
assert_eq!(
|
||||
relay.relay_path[0], plan.scope_id,
|
||||
"Phase 70-C: relay {} relay_path[0] must be this scope",
|
||||
idx
|
||||
);
|
||||
}
|
||||
|
||||
// Verify: L1 owned_vars contains 'total' and is_written=true
|
||||
let total_var = l1_plan
|
||||
.owned_vars
|
||||
.iter()
|
||||
.find(|v| v.name == "total")
|
||||
.expect("expected 'total' in L1 owned_vars");
|
||||
assert!(
|
||||
total_var.is_written,
|
||||
"Phase 70-C: L1's 'total' should be is_written=true"
|
||||
);
|
||||
|
||||
eprintln!("[phase70c/test] Merge relay pattern detected: 2 inner loops → same owner variable");
|
||||
}
|
||||
|
||||
/// Phase 70-C: Merge Relay validation acceptance
|
||||
///
|
||||
/// This test verifies that OwnershipPlanValidator ACCEPTS merge relay patterns
|
||||
/// (multiple inner loops → same owner) because they are PERMITTED with owner merge.
|
||||
#[test]
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
fn test_phase70c_merge_relay_same_owner_accepted() {
|
||||
use nyash_rust::mir::join_ir::ownership::{
|
||||
OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId,
|
||||
};
|
||||
|
||||
// Build two plans for L2_A and L2_B, both relaying to L1
|
||||
// L2_A plan: relay 'total' to L1
|
||||
let mut plan_l2a = OwnershipPlan::new(ScopeId(2));
|
||||
plan_l2a.relay_writes.push(RelayVar {
|
||||
name: "total".to_string(),
|
||||
owner_scope: ScopeId(1), // L1 owns total
|
||||
relay_path: vec![ScopeId(2)], // Single hop: L2_A → L1
|
||||
});
|
||||
|
||||
// L2_B plan: relay 'total' to L1
|
||||
let mut plan_l2b = OwnershipPlan::new(ScopeId(3));
|
||||
plan_l2b.relay_writes.push(RelayVar {
|
||||
name: "total".to_string(),
|
||||
owner_scope: ScopeId(1), // L1 owns total (same owner)
|
||||
relay_path: vec![ScopeId(3)], // Single hop: L2_B → L1
|
||||
});
|
||||
|
||||
// Phase 70-C: Both should be accepted (single-hop relay to same owner)
|
||||
let result_a = OwnershipPlanValidator::validate_relay_support(&plan_l2a);
|
||||
assert!(
|
||||
result_a.is_ok(),
|
||||
"Phase 70-C: L2_A relay to L1 should be accepted, got: {:?}",
|
||||
result_a
|
||||
);
|
||||
|
||||
let result_b = OwnershipPlanValidator::validate_relay_support(&plan_l2b);
|
||||
assert!(
|
||||
result_b.is_ok(),
|
||||
"Phase 70-C: L2_B relay to L1 should be accepted, got: {:?}",
|
||||
result_b
|
||||
);
|
||||
|
||||
eprintln!("[phase70c/test] Merge relay validation accepted: multiple inner loops → same owner");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user