docs(joinir): Phase 56 - Ownership-Relay Design + interface skeleton

「読むのは自由、管理は直下 owned だけ」アーキテクチャの設計文書と型定義。

Key changes:
- Design doc: phase56-ownership-relay-design.md
  - Core definitions: owned/carriers/captures/relay
  - Invariants: Ownership Uniqueness, Carrier Locality, Relay Propagation
  - Shadowing rules, multi-writer merge semantics
  - JoinIR mapping from current system to new system
  - Implementation phases roadmap (56-61)

- New module: src/mir/join_ir/ownership/
  - types.rs: ScopeId, ScopeOwnedVar, RelayVar, CapturedVar, OwnershipPlan
  - mod.rs: Module documentation with responsibility boundaries
  - README.md: Usage guide and examples

- API methods:
  - OwnershipPlan::carriers() - owned AND written variables
  - OwnershipPlan::condition_only_carriers() - condition-only carriers
  - OwnershipPlan::verify_invariants() - invariant checking

Tests: 942/942 PASS (+3 unit tests)
Zero behavioral change - analysis module skeleton only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-12 17:39:59 +09:00
parent 80e952b83a
commit 2d10c5ce3f
6 changed files with 517 additions and 4 deletions

View File

@ -41,6 +41,9 @@ pub mod normalized;
// Phase 34-1: Frontend (AST→JoinIR) — skeleton only
pub mod frontend;
// Phase 56: Ownership analysis (reads/writes → owned/relay/capture)
pub mod ownership;
// Re-export lowering functions for backward compatibility
pub use lowering::{
lower_funcscanner_trim_to_joinir, lower_min_loop_to_joinir, lower_skip_ws_to_joinir,

View File

@ -0,0 +1,79 @@
# Ownership Analysis Module
## Responsibility Boundary
This module is responsible for **analysis only**:
- ✅ Collecting reads/writes from AST/ProgramJSON
- ✅ Determining variable ownership (owned/relay/capture)
- ✅ Producing OwnershipPlan for downstream lowering
This module does NOT:
- ❌ Generate MIR instructions
- ❌ Modify JoinIR structures
- ❌ Perform lowering transformations
## Core Types
| Type | Purpose |
|------|---------|
| `ScopeId` | Unique scope identifier |
| `ScopeOwnedVar` | Variable defined in this scope |
| `RelayVar` | Write to ancestor-owned variable |
| `CapturedVar` | Read-only reference to ancestor |
| `OwnershipPlan` | Complete analysis result |
## Invariants
1. `carriers = owned_vars.filter(is_written)`
2. No variable in both owned and relay
3. No variable in both owned and captures
4. Relay implies ancestor ownership exists
## Design Philosophy
**「読むのは自由、管理は直下 owned だけ」**
- **Owned**: Variable defined in this scope (unique owner)
- **Carrier**: Owned AND written (managed as loop_step argument)
- **Capture**: Read-only reference to ancestor (via CapturedEnv)
- **Relay**: Write to ancestor → relay up to owner (exit PHI at owner)
## Phase Status
- Phase 56: ✅ Interface skeleton
- Phase 57: ⏳ OwnershipAnalyzer implementation
- Phase 58: ⏳ P2 plumbing
- Phase 59: ⏳ P3 plumbing
- Phase 60: ⏳ Cleanup dev heuristics
- Phase 61: ⏳ Canonical promotion decision
## Usage (Future)
```rust
let plan = OwnershipAnalyzer::analyze(&ast_node, parent_scope);
plan.verify_invariants()?;
let carriers: Vec<_> = plan.carriers().collect();
```
## Example
```nyash
local limit = 100 // owned by outer
loop {
local sum = 0 // owned by loop
if sum < limit { // limit = capture (read-only)
sum++ // sum = carrier (owned + written)
}
}
```
**OwnershipPlan (loop scope)**:
- `owned_vars`: [`sum` (written), `limit` (read-only)]
- `relay_writes`: []
- `captures`: [`limit`]
- `condition_captures`: [`limit`]
## References
- Design Doc: [phase56-ownership-relay-design.md](../../../../docs/development/current/main/phase56-ownership-relay-design.md)
- JoinIR Architecture: [joinir-architecture-overview.md](../../../../docs/development/current/main/joinir-architecture-overview.md)

View File

@ -0,0 +1,28 @@
//! Ownership Analysis for JoinIR
//!
//! # Responsibility Boundary
//!
//! This module is responsible for **analysis only**:
//! - Collecting reads/writes from AST/ProgramJSON
//! - Determining variable ownership (owned/relay/capture)
//! - Producing OwnershipPlan for downstream lowering
//!
//! This module does NOT:
//! - Generate MIR instructions
//! - Modify JoinIR structures
//! - Perform lowering transformations
//!
//! # Core Invariants
//!
//! 1. **Ownership Uniqueness**: Each variable has exactly one owner scope
//! 2. **Carrier Locality**: carriers = writes ∩ owned
//! 3. **Relay Propagation**: writes to ancestor-owned → relay up
//! 4. **Capture Read-Only**: captures have no PHI at this scope
//!
//! # Phase 56 Status
//!
//! Interface skeleton only. Implementation in Phase 57+.
mod types;
pub use types::*;

View File

@ -0,0 +1,168 @@
//! Core types for ownership analysis.
//!
//! Phase 56: Interface definitions only (not yet used).
#[cfg(any(debug_assertions, test))]
use std::collections::BTreeSet;
/// Unique identifier for a scope (loop, function, block).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ScopeId(pub u32);
/// A variable owned by the current scope.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScopeOwnedVar {
/// Variable name
pub name: String,
/// Whether this variable is written within the scope
pub is_written: bool,
/// Whether this variable is used in loop conditions
pub is_condition_only: bool,
}
/// A variable whose updates should be relayed to an ancestor owner.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelayVar {
/// Variable name
pub name: String,
/// Scope that owns this variable
pub owner_scope: ScopeId,
/// Intermediate scopes that need to forward this update
pub relay_path: Vec<ScopeId>,
}
/// A variable captured (read-only) from an ancestor scope.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapturedVar {
/// Variable name
pub name: String,
/// Scope that owns this variable
pub owner_scope: ScopeId,
}
/// Complete ownership analysis result for a scope.
#[derive(Debug, Clone, Default)]
pub struct OwnershipPlan {
/// ID of this scope
pub scope_id: ScopeId,
/// Variables owned by this scope (defined here)
/// Invariant: carriers = owned_vars where is_written = true
pub owned_vars: Vec<ScopeOwnedVar>,
/// Variables written but owned by ancestor (need relay)
pub relay_writes: Vec<RelayVar>,
/// Variables read but not owned (read-only capture)
pub captures: Vec<CapturedVar>,
/// Subset of captures used in conditions
pub condition_captures: Vec<CapturedVar>,
}
impl Default for ScopeId {
fn default() -> Self {
ScopeId(0)
}
}
impl OwnershipPlan {
/// Create empty plan for a scope.
pub fn new(scope_id: ScopeId) -> Self {
Self {
scope_id,
..Default::default()
}
}
/// Get carriers (owned AND written).
pub fn carriers(&self) -> impl Iterator<Item = &ScopeOwnedVar> {
self.owned_vars.iter().filter(|v| v.is_written)
}
/// Get condition-only carriers (owned, written, condition-only).
pub fn condition_only_carriers(&self) -> impl Iterator<Item = &ScopeOwnedVar> {
self.owned_vars.iter().filter(|v| v.is_written && v.is_condition_only)
}
/// Check invariant: no variable appears in multiple categories.
#[cfg(any(debug_assertions, test))]
pub fn verify_invariants(&self) -> Result<(), String> {
let mut all_names: BTreeSet<&str> = BTreeSet::new();
for v in &self.owned_vars {
if !all_names.insert(&v.name) {
return Err(format!("Duplicate owned var: {}", v.name));
}
}
for v in &self.relay_writes {
if self.owned_vars.iter().any(|o| o.name == v.name) {
return Err(format!("Relay var '{}' conflicts with owned", v.name));
}
}
for v in &self.captures {
if self.owned_vars.iter().any(|o| o.name == v.name) {
return Err(format!("Captured var '{}' conflicts with owned", v.name));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_plan() {
let plan = OwnershipPlan::new(ScopeId(1));
assert_eq!(plan.scope_id.0, 1);
assert!(plan.owned_vars.is_empty());
assert_eq!(plan.carriers().count(), 0);
}
#[test]
fn test_carriers_filter() {
let mut plan = OwnershipPlan::new(ScopeId(1));
plan.owned_vars.push(ScopeOwnedVar {
name: "sum".to_string(),
is_written: true,
is_condition_only: false,
});
plan.owned_vars.push(ScopeOwnedVar {
name: "limit".to_string(),
is_written: false, // read-only owned
is_condition_only: false,
});
let carriers: Vec<_> = plan.carriers().collect();
assert_eq!(carriers.len(), 1);
assert_eq!(carriers[0].name, "sum");
}
#[test]
fn test_invariant_verification() {
let mut plan = OwnershipPlan::new(ScopeId(1));
plan.owned_vars.push(ScopeOwnedVar {
name: "x".to_string(),
is_written: true,
is_condition_only: false,
});
// Valid plan
assert!(plan.verify_invariants().is_ok());
// Add conflicting relay
plan.relay_writes.push(RelayVar {
name: "x".to_string(),
owner_scope: ScopeId(0),
relay_path: vec![],
});
// Now invalid
assert!(plan.verify_invariants().is_err());
}
}