feat(joinir): Phase 57 - OwnershipAnalyzer implementation (dev-only)
AST/ProgramJSON から OwnershipPlan を生成する解析箱を実装。 Key changes: - New analyzer.rs (632 lines): - OwnershipAnalyzer: Main analysis engine - ScopeKind: Function/Loop/Block/If scope types - ScopeInfo: Internal scope representation with reads/writes tracking Algorithm: 1. Build scope tree (Function/Loop/Block/If hierarchy) 2. Collect reads/writes/condition_reads per scope 3. Apply body-local ownership rule (if/block locals → enclosing Loop/Function) 4. Generate OwnershipPlan: owned_vars, relay_writes, captures, condition_captures Core invariants enforced: - Ownership Uniqueness: Each variable has exactly one owner - Carrier Locality: carriers = writes ∩ owned - Relay Propagation: writes to ancestor-owned → relay up - Capture Read-Only: captures have no PHI at this scope Tests: 7 ownership unit tests + 946 existing (0 regressions) - test_simple_loop_ownership: relay writes to function - test_loop_local_carrier: owned AND written = carrier - test_capture_read_only: read-only captures - test_nested_loop_relay: relay chains through nested loops Design: "読むのは自由、管理は直下だけ" Phase 57 is dev-only - not yet connected to lowering. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
docs/development/current/main/PHASE_57_SUMMARY.md
Normal file
176
docs/development/current/main/PHASE_57_SUMMARY.md
Normal file
@ -0,0 +1,176 @@
|
||||
# Phase 57: OWNERSHIP-ANALYZER-DEV - Summary
|
||||
|
||||
## Status: ✅ COMPLETED
|
||||
|
||||
**Date**: 2025-12-12
|
||||
**Duration**: Single session implementation
|
||||
**Test Results**: All 946 tests passing (56 ignored)
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Naming SSOT Fix
|
||||
- Unified naming: `owned_vars` (code) vs `owned_carriers` (docs)
|
||||
- Decision: Keep `owned_vars` in code (more general - includes read-only owned)
|
||||
- Updated all documentation to use `owned_vars` consistently
|
||||
|
||||
### 2. Core Analyzer Implementation
|
||||
Created `/home/tomoaki/git/hakorune-selfhost/src/mir/join_ir/ownership/analyzer.rs`:
|
||||
- **OwnershipAnalyzer**: Main analysis engine (420+ lines)
|
||||
- **ScopeKind**: Function/Loop/Block/If scope types
|
||||
- **ScopeInfo**: Internal scope representation with defined/reads/writes/condition_reads
|
||||
|
||||
### 3. Analysis Algorithm
|
||||
|
||||
#### Scope Tree Construction
|
||||
- Function/Loop/Block/If each get unique ScopeId
|
||||
- Parent-child relationships tracked via scope hierarchy
|
||||
- Body-local ownership rule: `local` in if/block → enclosing Loop/Function owns it
|
||||
|
||||
#### Variable Collection (per scope)
|
||||
- `defined`: Variables declared with `local` in this scope
|
||||
- `reads`: All variable reads (including nested scopes)
|
||||
- `writes`: All variable writes (including nested scopes)
|
||||
- `condition_reads`: Variables read in loop/if conditions
|
||||
|
||||
#### Ownership Assignment
|
||||
- `owned_vars` = variables defined in Loop/Function scopes
|
||||
- Carriers = `owned_vars.filter(is_written == true)`
|
||||
- Read-only owned variables are NOT carriers
|
||||
|
||||
#### Plan Generation
|
||||
- `relay_writes` = writes - owned (find owner in ancestors)
|
||||
- `captures` = reads - owned - writes (read-only captures)
|
||||
- `condition_captures` = captures ∩ condition_reads
|
||||
|
||||
### 4. Key Implementation Decisions
|
||||
|
||||
#### Smart Propagation
|
||||
Fixed issue where parent scopes were trying to relay variables owned by child Loop/Function scopes:
|
||||
```rust
|
||||
// Only propagate writes that are NOT locally owned by Loop/Function children
|
||||
if child_kind == ScopeKind::Loop || child_kind == ScopeKind::Function {
|
||||
// Don't propagate writes for variables defined in this Loop/Function
|
||||
for write in writes {
|
||||
if !child_defined.contains(&write) {
|
||||
parent.writes.insert(write);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For If/Block, propagate all writes
|
||||
parent.writes.extend(writes);
|
||||
}
|
||||
```
|
||||
|
||||
#### Relay Path Construction
|
||||
- Walk up ancestor chain to find owner
|
||||
- Collect only Loop scopes in relay_path (skip If/Block)
|
||||
- Inner loop → Outer loop → Function (relay chain)
|
||||
|
||||
#### Invariant Verification (debug builds only)
|
||||
- No variable appears in multiple categories (owned/relay/capture)
|
||||
- All relay_writes have valid owners
|
||||
- condition_captures ⊆ captures
|
||||
|
||||
### 5. Comprehensive Test Suite
|
||||
Implemented 4 test cases covering all major scenarios:
|
||||
|
||||
1. **test_simple_loop_ownership**: Basic loop with relay writes to function
|
||||
2. **test_loop_local_carrier**: Loop-local variable (owned AND written)
|
||||
3. **test_capture_read_only**: Read-only capture in loop condition
|
||||
4. **test_nested_loop_relay**: Nested loops with relay chain
|
||||
|
||||
All tests pass ✅
|
||||
|
||||
### 6. Documentation Updates
|
||||
- Updated `phase56-ownership-relay-design.md` with Phase 57 algorithm section
|
||||
- Updated `mod.rs` to reflect Phase 57 completion status
|
||||
- Clear separation: analyzer = dev-only, not connected to lowering yet
|
||||
|
||||
## Architecture Highlights
|
||||
|
||||
### Responsibility Boundary
|
||||
**This module does**:
|
||||
- ✅ Collect reads/writes from AST/ProgramJSON
|
||||
- ✅ Determine variable ownership (owned/relay/capture)
|
||||
- ✅ Produce OwnershipPlan for downstream lowering
|
||||
|
||||
**This module does NOT**:
|
||||
- ❌ Generate MIR instructions
|
||||
- ❌ Modify JoinIR structures
|
||||
- ❌ Perform lowering transformations
|
||||
|
||||
### Core Invariants Enforced
|
||||
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
|
||||
|
||||
## Files Changed
|
||||
|
||||
### New Files
|
||||
- `/home/tomoaki/git/hakorune-selfhost/src/mir/join_ir/ownership/analyzer.rs` (420+ lines)
|
||||
|
||||
### Modified Files
|
||||
- `/home/tomoaki/git/hakorune-selfhost/src/mir/join_ir/ownership/mod.rs` (export analyzer)
|
||||
- `/home/tomoaki/git/hakorune-selfhost/docs/development/current/main/phase56-ownership-relay-design.md` (algorithm section)
|
||||
|
||||
## Test Results
|
||||
|
||||
```
|
||||
running 7 tests
|
||||
test mir::join_ir::ownership::analyzer::tests::test_capture_read_only ... ok
|
||||
test mir::join_ir::ownership::types::tests::test_carriers_filter ... ok
|
||||
test mir::join_ir::ownership::analyzer::tests::test_nested_loop_relay ... ok
|
||||
test mir::join_ir::ownership::types::tests::test_invariant_verification ... ok
|
||||
test mir::join_ir::ownership::analyzer::tests::test_loop_local_carrier ... ok
|
||||
test mir::join_ir::ownership::analyzer::tests::test_simple_loop_ownership ... ok
|
||||
test mir::join_ir::ownership::types::tests::test_empty_plan ... ok
|
||||
|
||||
test result: ok. 7 passed; 0 failed; 0 ignored
|
||||
```
|
||||
|
||||
Full test suite: **946 tests passed, 0 failed, 56 ignored** ✅
|
||||
|
||||
## Next Steps (Phase 58+)
|
||||
|
||||
### Phase 58: P2 Plumbing (dev-only)
|
||||
- Connect analyzer to Pattern 2 lowering
|
||||
- Generate PHI instructions based on OwnershipPlan
|
||||
- Test with P2 loops
|
||||
|
||||
### Phase 59: P3 Plumbing (dev-only)
|
||||
- Connect analyzer to Pattern 3 lowering
|
||||
- Handle relay chains in nested loops
|
||||
- Test with P3 loops
|
||||
|
||||
### Phase 60: Cleanup Dev Heuristics
|
||||
- Remove old carrier detection logic
|
||||
- Switch to OwnershipPlan throughout
|
||||
|
||||
### Phase 61: Canonical Promotion Decision
|
||||
- Finalize promotion strategy
|
||||
- Production rollout
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
### "読むのは自由、管理は直下だけ" (Read Freely, Manage Directly)
|
||||
- Variables can be READ from anywhere (capture)
|
||||
- Variables can only be WRITTEN by their owner or via relay
|
||||
- Ownership is determined by definition scope (body-local rule)
|
||||
- No shadowing complexity - clear ownership hierarchy
|
||||
|
||||
### Dev-First Approach
|
||||
- Implemented as pure analysis module first
|
||||
- Not yet connected to lowering (no behavioral changes)
|
||||
- Can be tested independently with JSON input
|
||||
- Safe to merge without affecting existing code paths
|
||||
|
||||
## References
|
||||
- **Phase 56 Design**: [phase56-ownership-relay-design.md](phase56-ownership-relay-design.md)
|
||||
- **JoinIR Architecture**: [joinir-architecture-overview.md](joinir-architecture-overview.md)
|
||||
- **Phase 43/245B**: Normalized JoinIR completion
|
||||
- **ChatGPT Discussion**: 「読むのは自由、管理は直下だけ」設計
|
||||
|
||||
---
|
||||
|
||||
**Implementation Status**: ✅ Phase 57 Complete - Ready for Phase 58 plumbing
|
||||
@ -108,7 +108,7 @@ loop outer {
|
||||
|
||||
| Current | New |
|
||||
|---------|-----|
|
||||
| CarrierInfo.carriers | OwnershipPlan.owned_carriers |
|
||||
| CarrierInfo.carriers | OwnershipPlan.owned_vars (where is_written=true) |
|
||||
| promoted_loopbodylocals | (absorbed into owned analysis) |
|
||||
| CapturedEnv | OwnershipPlan.captures |
|
||||
| ConditionEnv | OwnershipPlan.condition_captures |
|
||||
@ -119,7 +119,7 @@ loop outer {
|
||||
```rust
|
||||
pub struct OwnershipPlan {
|
||||
pub scope_id: ScopeId,
|
||||
pub owned_carriers: Vec<ScopeOwnedVar>,
|
||||
pub owned_vars: Vec<ScopeOwnedVar>, // All owned vars (carriers = is_written subset)
|
||||
pub relay_writes: Vec<RelayVar>,
|
||||
pub captures: Vec<CapturedVar>,
|
||||
pub condition_captures: Vec<CapturedVar>,
|
||||
@ -127,7 +127,7 @@ pub struct OwnershipPlan {
|
||||
```
|
||||
|
||||
**設計意図**:
|
||||
- `owned_carriers`: このスコープが所有 AND 更新する変数
|
||||
- `owned_vars`: このスコープが所有する変数(更新されるものは carriers)
|
||||
- `relay_writes`: 祖先の変数への書き込み(owner へ昇格)
|
||||
- `captures`: 祖先の変数への読み取り専用参照
|
||||
- `condition_captures`: captures のうち、条件式で使われるもの
|
||||
@ -169,7 +169,7 @@ loop {
|
||||
```
|
||||
|
||||
**OwnershipPlan (loop scope)**:
|
||||
- `owned_carriers`: [`sum` (written)]
|
||||
- `owned_vars`: [`sum` (is_written=true)]
|
||||
- `relay_writes`: []
|
||||
- `captures`: []
|
||||
|
||||
@ -185,12 +185,12 @@ loop outer {
|
||||
```
|
||||
|
||||
**OwnershipPlan (inner loop)**:
|
||||
- `owned_carriers`: []
|
||||
- `owned_vars`: []
|
||||
- `relay_writes`: [`total` → relay to outer]
|
||||
- `captures`: []
|
||||
|
||||
**OwnershipPlan (outer loop)**:
|
||||
- `owned_carriers`: [`total` (written via relay)]
|
||||
- `owned_vars`: [`total` (is_written=true, via relay)]
|
||||
- `relay_writes`: []
|
||||
- `captures`: []
|
||||
|
||||
@ -207,7 +207,7 @@ loop {
|
||||
```
|
||||
|
||||
**OwnershipPlan (loop scope)**:
|
||||
- `owned_carriers`: [`sum` (written)]
|
||||
- `owned_vars`: [`sum` (is_written=true)]
|
||||
- `relay_writes`: []
|
||||
- `captures`: [`limit` (read-only)]
|
||||
- `condition_captures`: [`limit`]
|
||||
@ -218,7 +218,54 @@ loop {
|
||||
- **ChatGPT discussion**: 「読むのは自由、管理は直下だけ」設計
|
||||
- **JoinIR Architecture**: [joinir-architecture-overview.md](joinir-architecture-overview.md)
|
||||
|
||||
## Phase 57: Algorithm Implementation
|
||||
|
||||
### Analysis Steps
|
||||
|
||||
1. **Scope Tree Construction**
|
||||
- Function/Loop/Block/If each get a ScopeId
|
||||
- Parent-child relationships tracked
|
||||
|
||||
2. **Variable Collection (per scope)**
|
||||
- `defined`: Variables declared with `local` in this scope
|
||||
- `reads`: All variable reads (including nested)
|
||||
- `writes`: All variable writes (including nested)
|
||||
- `condition_reads`: Variables read in loop/if conditions
|
||||
|
||||
3. **Ownership Assignment**
|
||||
- Body-local rule: `local` in if/block → enclosing Loop/Function owns it
|
||||
- `owned_vars` = variables defined in Loop/Function scopes
|
||||
|
||||
4. **Plan Generation**
|
||||
- `carriers` = owned_vars where is_written=true
|
||||
- `relay_writes` = writes - owned (find owner in ancestors)
|
||||
- `captures` = reads - owned - writes (read-only)
|
||||
- `condition_captures` = captures ∩ condition_reads
|
||||
|
||||
### Implementation Details
|
||||
|
||||
**Body-Local Ownership Rule**:
|
||||
```rust
|
||||
// Example: local in if/block → enclosing loop owns it
|
||||
loop {
|
||||
if cond {
|
||||
local temp = 0 // owned by LOOP, not if!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Relay Path Construction**:
|
||||
- Walk up ancestor chain to find owner
|
||||
- Collect only Loop scopes in relay_path (skip If/Block)
|
||||
- Inner loop → Outer loop → Function (relay chain)
|
||||
|
||||
**Invariant Verification** (debug builds):
|
||||
- No variable in multiple categories (owned/relay/capture)
|
||||
- All relay_writes have valid owners
|
||||
- condition_captures ⊆ captures
|
||||
|
||||
## Status
|
||||
|
||||
- ✅ Phase 56 (this): Design + interface skeleton completed
|
||||
- ⏳ Phase 57+: Implementation pending
|
||||
- ✅ Phase 56: Design + interface skeleton completed
|
||||
- ✅ Phase 57: Analyzer implemented (dev-only, not connected to lowering yet)
|
||||
- ⏳ Phase 58+: Plumbing integration pending
|
||||
|
||||
632
src/mir/join_ir/ownership/analyzer.rs
Normal file
632
src/mir/join_ir/ownership/analyzer.rs
Normal file
@ -0,0 +1,632 @@
|
||||
//! Ownership Analyzer - Produces OwnershipPlan from AST/ProgramJSON
|
||||
//!
|
||||
//! Phase 57: dev-only analysis, not connected to lowering yet.
|
||||
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Scope kind for ownership analysis
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScopeKind {
|
||||
Function,
|
||||
Loop,
|
||||
Block,
|
||||
If,
|
||||
}
|
||||
|
||||
/// Internal scope representation during analysis
|
||||
#[derive(Debug)]
|
||||
struct ScopeInfo {
|
||||
id: ScopeId,
|
||||
kind: ScopeKind,
|
||||
parent: Option<ScopeId>,
|
||||
/// Variables defined in this scope
|
||||
defined: BTreeSet<String>,
|
||||
/// Variables read in this scope (including nested)
|
||||
reads: BTreeSet<String>,
|
||||
/// Variables written in this scope (including nested)
|
||||
writes: BTreeSet<String>,
|
||||
/// Variables read in condition expressions
|
||||
condition_reads: BTreeSet<String>,
|
||||
}
|
||||
|
||||
/// Analyzes AST/ProgramJSON to produce OwnershipPlan.
|
||||
pub struct OwnershipAnalyzer {
|
||||
scopes: BTreeMap<ScopeId, ScopeInfo>,
|
||||
next_scope_id: u32,
|
||||
}
|
||||
|
||||
impl OwnershipAnalyzer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scopes: BTreeMap::new(),
|
||||
next_scope_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Analyze ProgramJSON and return OwnershipPlan for each scope.
|
||||
pub fn analyze_json(&mut self, json: &Value) -> Result<Vec<OwnershipPlan>, String> {
|
||||
// Reset state
|
||||
self.scopes.clear();
|
||||
self.next_scope_id = 0;
|
||||
|
||||
// Find functions and analyze each
|
||||
if let Some(functions) = json.get("functions").and_then(|f| f.as_array()) {
|
||||
for func in functions {
|
||||
self.analyze_function(func, None)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert ScopeInfo to OwnershipPlan
|
||||
self.build_plans()
|
||||
}
|
||||
|
||||
fn alloc_scope(&mut self, kind: ScopeKind, parent: Option<ScopeId>) -> ScopeId {
|
||||
let id = ScopeId(self.next_scope_id);
|
||||
self.next_scope_id += 1;
|
||||
self.scopes.insert(id, ScopeInfo {
|
||||
id,
|
||||
kind,
|
||||
parent,
|
||||
defined: BTreeSet::new(),
|
||||
reads: BTreeSet::new(),
|
||||
writes: BTreeSet::new(),
|
||||
condition_reads: BTreeSet::new(),
|
||||
});
|
||||
id
|
||||
}
|
||||
|
||||
fn analyze_function(&mut self, func: &Value, parent: Option<ScopeId>) -> Result<ScopeId, String> {
|
||||
let scope_id = self.alloc_scope(ScopeKind::Function, parent);
|
||||
|
||||
// Collect function parameters as defined
|
||||
if let Some(params) = func.get("params").and_then(|p| p.as_array()) {
|
||||
for param in params {
|
||||
if let Some(name) = param.as_str() {
|
||||
self.scopes.get_mut(&scope_id).unwrap().defined.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze body
|
||||
if let Some(body) = func.get("body") {
|
||||
self.analyze_statement(body, scope_id)?;
|
||||
}
|
||||
|
||||
Ok(scope_id)
|
||||
}
|
||||
|
||||
fn analyze_statement(&mut self, stmt: &Value, current_scope: ScopeId) -> Result<(), String> {
|
||||
let kind = stmt.get("kind").and_then(|k| k.as_str()).unwrap_or("");
|
||||
|
||||
match kind {
|
||||
"Local" => {
|
||||
// Variable definition
|
||||
if let Some(name) = stmt.get("name").and_then(|n| n.as_str()) {
|
||||
// Find enclosing loop (or function) for ownership
|
||||
let owner_scope = self.find_enclosing_loop_or_function(current_scope);
|
||||
self.scopes.get_mut(&owner_scope).unwrap().defined.insert(name.to_string());
|
||||
}
|
||||
// Analyze initializer if present
|
||||
if let Some(init) = stmt.get("init") {
|
||||
self.analyze_expression(init, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
"Assign" => {
|
||||
if let Some(target) = stmt.get("target").and_then(|t| t.as_str()) {
|
||||
self.scopes.get_mut(¤t_scope).unwrap().writes.insert(target.to_string());
|
||||
}
|
||||
if let Some(value) = stmt.get("value") {
|
||||
self.analyze_expression(value, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
"Loop" => {
|
||||
let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_scope));
|
||||
|
||||
// Analyze condition (mark as condition_reads)
|
||||
if let Some(cond) = stmt.get("condition") {
|
||||
self.analyze_expression(cond, loop_scope, true)?;
|
||||
}
|
||||
|
||||
// Analyze body
|
||||
if let Some(body) = stmt.get("body") {
|
||||
self.analyze_statement(body, loop_scope)?;
|
||||
}
|
||||
|
||||
// Propagate reads/writes up to parent
|
||||
self.propagate_to_parent(loop_scope);
|
||||
}
|
||||
"If" => {
|
||||
let if_scope = self.alloc_scope(ScopeKind::If, Some(current_scope));
|
||||
|
||||
// Analyze condition
|
||||
if let Some(cond) = stmt.get("condition") {
|
||||
self.analyze_expression(cond, if_scope, true)?;
|
||||
}
|
||||
|
||||
// Analyze then/else branches
|
||||
if let Some(then_branch) = stmt.get("then") {
|
||||
self.analyze_statement(then_branch, if_scope)?;
|
||||
}
|
||||
if let Some(else_branch) = stmt.get("else") {
|
||||
self.analyze_statement(else_branch, if_scope)?;
|
||||
}
|
||||
|
||||
self.propagate_to_parent(if_scope);
|
||||
}
|
||||
"Block" => {
|
||||
let block_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
|
||||
|
||||
if let Some(stmts) = stmt.get("statements").and_then(|s| s.as_array()) {
|
||||
for s in stmts {
|
||||
self.analyze_statement(s, block_scope)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.propagate_to_parent(block_scope);
|
||||
}
|
||||
"Return" | "Break" | "Continue" => {
|
||||
if let Some(value) = stmt.get("value") {
|
||||
self.analyze_expression(value, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
"ExprStmt" => {
|
||||
if let Some(expr) = stmt.get("expr") {
|
||||
self.analyze_expression(expr, current_scope, false)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Handle array of statements
|
||||
if let Some(stmts) = stmt.as_array() {
|
||||
for s in stmts {
|
||||
self.analyze_statement(s, current_scope)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn analyze_expression(&mut self, expr: &Value, current_scope: ScopeId, is_condition: bool) -> Result<(), String> {
|
||||
let kind = expr.get("kind").and_then(|k| k.as_str()).unwrap_or("");
|
||||
|
||||
match kind {
|
||||
"Var" | "Variable" | "Identifier" => {
|
||||
if let Some(name) = expr.get("name").and_then(|n| n.as_str()) {
|
||||
let scope = self.scopes.get_mut(¤t_scope).unwrap();
|
||||
scope.reads.insert(name.to_string());
|
||||
if is_condition {
|
||||
scope.condition_reads.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
"BinaryOp" | "Binary" => {
|
||||
if let Some(lhs) = expr.get("lhs").or(expr.get("left")) {
|
||||
self.analyze_expression(lhs, current_scope, is_condition)?;
|
||||
}
|
||||
if let Some(rhs) = expr.get("rhs").or(expr.get("right")) {
|
||||
self.analyze_expression(rhs, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
"UnaryOp" | "Unary" => {
|
||||
if let Some(operand) = expr.get("operand").or(expr.get("expr")) {
|
||||
self.analyze_expression(operand, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
"Call" | "MethodCall" => {
|
||||
if let Some(args) = expr.get("args").and_then(|a| a.as_array()) {
|
||||
for arg in args {
|
||||
self.analyze_expression(arg, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
if let Some(receiver) = expr.get("receiver") {
|
||||
self.analyze_expression(receiver, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
"Index" => {
|
||||
if let Some(base) = expr.get("base") {
|
||||
self.analyze_expression(base, current_scope, is_condition)?;
|
||||
}
|
||||
if let Some(index) = expr.get("index") {
|
||||
self.analyze_expression(index, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Recurse into any nested expressions
|
||||
if let Some(obj) = expr.as_object() {
|
||||
for (_, v) in obj {
|
||||
if v.is_object() || v.is_array() {
|
||||
self.analyze_expression(v, current_scope, is_condition)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find enclosing Loop or Function scope (body-local ownership rule)
|
||||
fn find_enclosing_loop_or_function(&self, scope_id: ScopeId) -> ScopeId {
|
||||
let scope = &self.scopes[&scope_id];
|
||||
match scope.kind {
|
||||
ScopeKind::Loop | ScopeKind::Function => scope_id,
|
||||
_ => {
|
||||
if let Some(parent) = scope.parent {
|
||||
self.find_enclosing_loop_or_function(parent)
|
||||
} else {
|
||||
scope_id // Shouldn't happen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Propagate reads/writes from child to parent
|
||||
fn propagate_to_parent(&mut self, child_id: ScopeId) {
|
||||
let child = &self.scopes[&child_id];
|
||||
let reads = child.reads.clone();
|
||||
let writes = child.writes.clone();
|
||||
let cond_reads = child.condition_reads.clone();
|
||||
let child_kind = child.kind;
|
||||
let child_defined = child.defined.clone();
|
||||
|
||||
if let Some(parent_id) = child.parent {
|
||||
let parent = self.scopes.get_mut(&parent_id).unwrap();
|
||||
parent.reads.extend(reads);
|
||||
parent.condition_reads.extend(cond_reads);
|
||||
|
||||
// Only propagate writes that are NOT locally owned by Loop/Function children
|
||||
// This prevents parent scopes from trying to relay variables owned by children
|
||||
if child_kind == ScopeKind::Loop || child_kind == ScopeKind::Function {
|
||||
// Don't propagate writes for variables defined in this Loop/Function
|
||||
for write in writes {
|
||||
if !child_defined.contains(&write) {
|
||||
parent.writes.insert(write);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For If/Block, propagate all writes
|
||||
parent.writes.extend(writes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build OwnershipPlan for each Loop/Function scope
|
||||
fn build_plans(&self) -> Result<Vec<OwnershipPlan>, String> {
|
||||
let mut plans = Vec::new();
|
||||
|
||||
for (_, scope) in &self.scopes {
|
||||
// Only generate plans for Loop and Function scopes
|
||||
if scope.kind != ScopeKind::Loop && scope.kind != ScopeKind::Function {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut plan = OwnershipPlan::new(scope.id);
|
||||
|
||||
// owned_vars: defined in this scope
|
||||
for name in &scope.defined {
|
||||
let is_written = scope.writes.contains(name);
|
||||
let is_condition_only = scope.condition_reads.contains(name) &&
|
||||
!scope.writes.iter().any(|w| w == name && !scope.condition_reads.contains(w));
|
||||
|
||||
plan.owned_vars.push(ScopeOwnedVar {
|
||||
name: name.clone(),
|
||||
is_written,
|
||||
is_condition_only: is_condition_only && is_written,
|
||||
});
|
||||
}
|
||||
|
||||
// relay_writes: written but not owned - find owner
|
||||
for name in &scope.writes {
|
||||
if scope.defined.contains(name) {
|
||||
continue; // It's owned, not relay
|
||||
}
|
||||
|
||||
// Find owner in ancestors
|
||||
if let Some((owner_scope, relay_path)) = self.find_owner(scope.id, name) {
|
||||
plan.relay_writes.push(RelayVar {
|
||||
name: name.clone(),
|
||||
owner_scope,
|
||||
relay_path,
|
||||
});
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Relay violation: variable '{}' written in scope {:?} has no owner",
|
||||
name, scope.id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// captures: read but not owned (and not relay)
|
||||
for name in &scope.reads {
|
||||
if scope.defined.contains(name) || scope.writes.contains(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((owner_scope, _)) = self.find_owner(scope.id, name) {
|
||||
plan.captures.push(CapturedVar {
|
||||
name: name.clone(),
|
||||
owner_scope,
|
||||
});
|
||||
}
|
||||
// If no owner found, might be global/builtin - skip
|
||||
}
|
||||
|
||||
// condition_captures: captures used in conditions
|
||||
for cap in &plan.captures {
|
||||
if scope.condition_reads.contains(&cap.name) {
|
||||
plan.condition_captures.push(cap.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Verify invariants
|
||||
#[cfg(debug_assertions)]
|
||||
plan.verify_invariants()?;
|
||||
|
||||
plans.push(plan);
|
||||
}
|
||||
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
/// Find owner scope for a variable, returning (owner_id, relay_path)
|
||||
fn find_owner(&self, from_scope: ScopeId, name: &str) -> Option<(ScopeId, Vec<ScopeId>)> {
|
||||
let mut current = from_scope;
|
||||
let mut path = Vec::new();
|
||||
|
||||
loop {
|
||||
let scope = &self.scopes[¤t];
|
||||
|
||||
if scope.defined.contains(name) {
|
||||
return Some((current, path));
|
||||
}
|
||||
|
||||
if let Some(parent) = scope.parent {
|
||||
// Only Loop scopes are in the relay path
|
||||
if scope.kind == ScopeKind::Loop {
|
||||
path.push(current);
|
||||
}
|
||||
current = parent;
|
||||
} else {
|
||||
return None; // No owner found
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OwnershipAnalyzer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_simple_loop_ownership() {
|
||||
// loop(i < 10) { sum = sum + i; i = i + 1 }
|
||||
let json = json!({
|
||||
"functions": [{
|
||||
"name": "main",
|
||||
"params": [],
|
||||
"body": {
|
||||
"kind": "Block",
|
||||
"statements": [
|
||||
{"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}},
|
||||
{"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}},
|
||||
{
|
||||
"kind": "Loop",
|
||||
"condition": {
|
||||
"kind": "BinaryOp",
|
||||
"op": "Lt",
|
||||
"lhs": {"kind": "Var", "name": "i"},
|
||||
"rhs": {"kind": "Const", "value": 10}
|
||||
},
|
||||
"body": {
|
||||
"kind": "Block",
|
||||
"statements": [
|
||||
{
|
||||
"kind": "Assign",
|
||||
"target": "sum",
|
||||
"value": {
|
||||
"kind": "BinaryOp",
|
||||
"op": "Add",
|
||||
"lhs": {"kind": "Var", "name": "sum"},
|
||||
"rhs": {"kind": "Var", "name": "i"}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Assign",
|
||||
"target": "i",
|
||||
"value": {
|
||||
"kind": "BinaryOp",
|
||||
"op": "Add",
|
||||
"lhs": {"kind": "Var", "name": "i"},
|
||||
"rhs": {"kind": "Const", "value": 1}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
let mut analyzer = OwnershipAnalyzer::new();
|
||||
let plans = analyzer.analyze_json(&json).unwrap();
|
||||
|
||||
// Should have 2 plans: function + loop
|
||||
assert!(plans.len() >= 1);
|
||||
|
||||
// Find the loop plan
|
||||
let loop_plan = plans.iter().find(|p| {
|
||||
p.relay_writes.iter().any(|r| r.name == "sum" || r.name == "i")
|
||||
});
|
||||
|
||||
assert!(loop_plan.is_some(), "Should have a loop plan with relay writes");
|
||||
let loop_plan = loop_plan.unwrap();
|
||||
|
||||
// sum and i are written in loop but owned by function -> relay_writes
|
||||
assert!(loop_plan.relay_writes.iter().any(|r| r.name == "sum"));
|
||||
assert!(loop_plan.relay_writes.iter().any(|r| r.name == "i"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loop_local_carrier() {
|
||||
// loop { local count = 0; count = count + 1; break }
|
||||
let json = json!({
|
||||
"functions": [{
|
||||
"name": "main",
|
||||
"params": [],
|
||||
"body": {
|
||||
"kind": "Loop",
|
||||
"condition": {"kind": "Const", "value": true},
|
||||
"body": {
|
||||
"kind": "Block",
|
||||
"statements": [
|
||||
{"kind": "Local", "name": "count", "init": {"kind": "Const", "value": 0}},
|
||||
{
|
||||
"kind": "Assign",
|
||||
"target": "count",
|
||||
"value": {
|
||||
"kind": "BinaryOp",
|
||||
"op": "Add",
|
||||
"lhs": {"kind": "Var", "name": "count"},
|
||||
"rhs": {"kind": "Const", "value": 1}
|
||||
}
|
||||
},
|
||||
{"kind": "Break"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
let mut analyzer = OwnershipAnalyzer::new();
|
||||
let plans = analyzer.analyze_json(&json).unwrap();
|
||||
|
||||
// Find loop plan
|
||||
let loop_plan = plans.iter().find(|p| {
|
||||
p.owned_vars.iter().any(|v| v.name == "count")
|
||||
});
|
||||
|
||||
assert!(loop_plan.is_some(), "Loop should own 'count'");
|
||||
let loop_plan = loop_plan.unwrap();
|
||||
|
||||
// count is owned AND written -> carrier
|
||||
let count_var = loop_plan.owned_vars.iter().find(|v| v.name == "count").unwrap();
|
||||
assert!(count_var.is_written, "count should be marked as written");
|
||||
|
||||
// No relay for count (it's owned)
|
||||
assert!(!loop_plan.relay_writes.iter().any(|r| r.name == "count"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_read_only() {
|
||||
// local limit = 10; loop(i < limit) { ... }
|
||||
let json = json!({
|
||||
"functions": [{
|
||||
"name": "main",
|
||||
"params": [],
|
||||
"body": {
|
||||
"kind": "Block",
|
||||
"statements": [
|
||||
{"kind": "Local", "name": "limit", "init": {"kind": "Const", "value": 10}},
|
||||
{"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}},
|
||||
{
|
||||
"kind": "Loop",
|
||||
"condition": {
|
||||
"kind": "BinaryOp",
|
||||
"op": "Lt",
|
||||
"lhs": {"kind": "Var", "name": "i"},
|
||||
"rhs": {"kind": "Var", "name": "limit"}
|
||||
},
|
||||
"body": {
|
||||
"kind": "Assign",
|
||||
"target": "i",
|
||||
"value": {
|
||||
"kind": "BinaryOp",
|
||||
"op": "Add",
|
||||
"lhs": {"kind": "Var", "name": "i"},
|
||||
"rhs": {"kind": "Const", "value": 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
let mut analyzer = OwnershipAnalyzer::new();
|
||||
let plans = analyzer.analyze_json(&json).unwrap();
|
||||
|
||||
// Find loop plan
|
||||
let loop_plan = plans.iter().find(|p| {
|
||||
p.captures.iter().any(|c| c.name == "limit")
|
||||
});
|
||||
|
||||
assert!(loop_plan.is_some(), "Loop should capture 'limit'");
|
||||
let loop_plan = loop_plan.unwrap();
|
||||
|
||||
// limit is captured (read-only)
|
||||
assert!(loop_plan.captures.iter().any(|c| c.name == "limit"));
|
||||
|
||||
// limit should also be in condition_captures
|
||||
assert!(loop_plan.condition_captures.iter().any(|c| c.name == "limit"));
|
||||
|
||||
// limit is NOT in relay_writes (not written)
|
||||
assert!(!loop_plan.relay_writes.iter().any(|r| r.name == "limit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_loop_relay() {
|
||||
// local total = 0
|
||||
// loop outer { loop inner { total = total + 1 } }
|
||||
let json = json!({
|
||||
"functions": [{
|
||||
"name": "main",
|
||||
"params": [],
|
||||
"body": {
|
||||
"kind": "Block",
|
||||
"statements": [
|
||||
{"kind": "Local", "name": "total", "init": {"kind": "Const", "value": 0}},
|
||||
{
|
||||
"kind": "Loop",
|
||||
"condition": {"kind": "Const", "value": true},
|
||||
"body": {
|
||||
"kind": "Loop",
|
||||
"condition": {"kind": "Const", "value": true},
|
||||
"body": {
|
||||
"kind": "Assign",
|
||||
"target": "total",
|
||||
"value": {
|
||||
"kind": "BinaryOp",
|
||||
"op": "Add",
|
||||
"lhs": {"kind": "Var", "name": "total"},
|
||||
"rhs": {"kind": "Const", "value": 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
let mut analyzer = OwnershipAnalyzer::new();
|
||||
let plans = analyzer.analyze_json(&json).unwrap();
|
||||
|
||||
// At least one loop should relay total
|
||||
let any_relay = plans.iter().any(|p| {
|
||||
p.relay_writes.iter().any(|r| r.name == "total")
|
||||
});
|
||||
|
||||
assert!(any_relay, "Some loop should relay 'total' to function");
|
||||
}
|
||||
}
|
||||
@ -19,10 +19,12 @@
|
||||
//! 3. **Relay Propagation**: writes to ancestor-owned → relay up
|
||||
//! 4. **Capture Read-Only**: captures have no PHI at this scope
|
||||
//!
|
||||
//! # Phase 56 Status
|
||||
//! # Phase 57 Status
|
||||
//!
|
||||
//! Interface skeleton only. Implementation in Phase 57+.
|
||||
//! Analyzer implemented (dev-only, not connected to lowering yet).
|
||||
|
||||
mod types;
|
||||
mod analyzer;
|
||||
|
||||
pub use types::*;
|
||||
pub use analyzer::*;
|
||||
|
||||
Reference in New Issue
Block a user