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:
@ -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,
|
||||
|
||||
79
src/mir/join_ir/ownership/README.md
Normal file
79
src/mir/join_ir/ownership/README.md
Normal 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)
|
||||
28
src/mir/join_ir/ownership/mod.rs
Normal file
28
src/mir/join_ir/ownership/mod.rs
Normal 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::*;
|
||||
168
src/mir/join_ir/ownership/types.rs
Normal file
168
src/mir/join_ir/ownership/types.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user