feat(joinir): Phase 70-C - Merge Relay Detection (dev-only)

Phase 70-C enables detection of merge relay patterns where multiple inner
loops update the same owner-owned variable. This allows compile-time
validation that relay sources can be merged at owner scope's exit PHI.

Changes:
- phase70c-merge-relay.md: New SSOT doc for merge relay semantics
- normalized_joinir_min.rs: +2 integration tests

Tests: normalized_dev 54/54 PASS, lib 950/950 PASS
Design: Validator accepts multiple relays to same owner (individually valid)

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-13 03:41:26 +09:00
parent c2df1cacf6
commit 24cc948f61

View File

@ -0,0 +1,208 @@
# Phase 70-C: Merge Relay (Multiple Inner Loops → Same Owner)
**Status**: ✅ Completed
**Date**: 2025-12-13
---
## Overview
Phase 70-C implements detection and validation support for "merge relay" patterns where **multiple inner loops update the same owner-owned variable**.
This builds upon Phase 70-B's simple passthrough multihop support by handling the case where a single owner scope must merge updates from multiple relay sources.
---
## Merge Relay Pattern
### Definition
**Merge Relay**: Multiple inner loops update the same ancestor-owned variable, requiring the owner to merge all relay updates at its exit PHI.
### Example
```nyash
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'
```
### OwnershipPlans
**L2_A Plan**:
```rust
OwnershipPlan {
scope_id: ScopeId(2),
relay_writes: [
RelayVar {
name: "total",
owner_scope: ScopeId(1), // L1
relay_path: [ScopeId(2)], // Single hop
}
],
owned_vars: [], // L2_A doesn't own total
}
```
**L2_B Plan**:
```rust
OwnershipPlan {
scope_id: ScopeId(3),
relay_writes: [
RelayVar {
name: "total",
owner_scope: ScopeId(1), // L1 (same owner)
relay_path: [ScopeId(3)], // Single hop
}
],
owned_vars: [], // L2_B doesn't own total
}
```
**L1 Plan**:
```rust
OwnershipPlan {
scope_id: ScopeId(1),
owned_vars: [
ScopeOwnedVar {
name: "total",
is_written: true, // Owner accepts relays
}
],
relay_writes: [], // No further relay (L1 is owner)
}
```
---
## Design Decisions
### 1. PERMIT with Owner Merge
**Decision**: Merge relay is **PERMITTED** (not rejected) because:
- Multiple inner loops updating a shared variable is a **legitimate pattern**
- Owner scope can merge all updates at its exit PHI
- Each inner loop has a valid single-hop relay to the same owner
### 2. Validation Strategy
**OwnershipPlanValidator accepts merge relay if**:
1. Each relay has valid `relay_path` (non-empty, first hop is current scope)
2. No self-conflict (scope doesn't both own and relay the same variable)
3. All relays point to the same owner scope (validated separately per relay)
**Note**: Validator validates **individual plans** - it doesn't check cross-plan consistency (e.g., "two different scopes relay to the same owner"). This is intentional - each relay is independently valid.
### 3. Runtime Support
**Phase 70-C Status**: Detection and validation only (dev-only)
**Runtime Implementation** (Phase 70-D+):
- Owner scope exit PHI must merge values from all relay sources
- Each relay source appears as a separate carrier in owner's loop_step
- PHI merges: `total_final = phi(total_init, total_from_L2_A, total_from_L2_B)`
---
## Implementation
### Files Modified
1. **tests/normalized_joinir_min.rs**
- `test_phase70c_merge_relay_multiple_inner_loops_detected()`: AST → OwnershipPlan detection
- `test_phase70c_merge_relay_same_owner_accepted()`: Validator accepts merge relay
### Test Coverage
#### Test 1: AST Detection
- **Input**: 3-level nested loop (L1 owns, L2_A and L2_B relay)
- **Verification**:
- 2 relay plans detected
- Both relay to the same owner (L1)
- Single-hop relay paths
- Owner plan marks variable as written
#### Test 2: Validator Acceptance
- **Input**: Two OwnershipPlans with single-hop relay to same owner
- **Verification**:
- Both plans pass `OwnershipPlanValidator::validate_relay_support()`
- No runtime_unsupported errors
---
## Test Results
```bash
$ NYASH_JOINIR_NORMALIZED_DEV_RUN=1 cargo test --features normalized_dev --test normalized_joinir_min test_phase70c
running 2 tests
test test_phase70c_merge_relay_same_owner_accepted ... ok
test test_phase70c_merge_relay_multiple_inner_loops_detected ... ok
test result: ok. 2 passed; 0 failed; 0 ignored
```
**Regression Check**:
- normalized_dev: 54/54 PASS ✅
- lib tests: 950/950 PASS ✅
- Zero regressions
---
## Acceptance Criteria
- [x] Merge relay pattern fixture created (2 inner loops → 1 owner)
- [x] AST analyzer correctly detects merge relay (2 relay plans)
- [x] OwnershipPlanValidator accepts merge relay (no runtime_unsupported error)
- [x] Tests pass with normalized_dev feature
- [x] No regressions in lib tests
---
## Future Work (Phase 70-D+)
### Runtime Execution Support
**Required for full merge relay execution**:
1. **Exit PHI Generation**:
- Owner scope must generate PHI with N+1 inputs (init + each relay source)
- Example: `phi(total_init, total_from_L2_A, total_from_L2_B)`
2. **Carrier Propagation**:
- Each relay source must appear in owner's loop_step signature
- Owner boundary must collect all relay values
3. **Integration Testing**:
- E2E test: 2 inner loops → owner → verify final merged value
- Combination: Multihop + Merge (3-layer nested with multiple relays)
4. **Edge Cases**:
- More than 2 relay sources
- If-branches with relay (conditional merge)
- Multihop + Merge (nested relay with multiple sources)
---
## Related Documents
- [Phase 65: Multihop Design](phase65-ownership-relay-multihop-design.md#merge-relay-の意味論)
- [Phase 70-A: Runtime Guard](phase70-relay-runtime-guard.md)
- [Phase 70-B: Simple Passthrough](phase70-relay-runtime-guard.md)
- [Phase 56: Ownership-Relay Architecture](phase56-ownership-relay-design.md)
---
## Changelog
- **2025-12-13**: Phase 70-C completed - Merge relay detection and validation
- 2 tests added (AST detection, validator acceptance)
- 54/54 normalized_dev tests pass
- 950/950 lib tests pass (zero regressions)