feat(joinir): Phase 64 - Ownership P3 production integration (dev-only)

P3(if-sum) 本番ルートに OwnershipPlan 解析を接続(analysis-only)。

Key changes:
- ast_analyzer.rs: Added analyze_loop() helper
  - Loop-specific analysis API for production integration
  - build_plan_for_scope() helper for single-scope extraction

- pattern3_with_if_phi.rs: P3 production integration
  - OwnershipPlan analysis after ConditionEnv building
  - check_ownership_plan_consistency() for validation
  - Feature-gated #[cfg(feature = "normalized_dev")]

Consistency checks:
- Fail-Fast: Multi-hop relay (relay_path.len() > 1)
- Warn-only: Carrier set mismatch (order SSOT deferred)
- Warn-only: Condition captures (some patterns have extras)

Tests: 49/49 PASS (2 new Phase 64 tests)
- test_phase64_p3_ownership_prod_integration
- test_phase64_p3_multihop_relay_detection
- Zero regressions

Design: Analysis-only, no behavior change
- Integration point: After ConditionEnv, before JoinIR lowering
- Dev-only validation for future SSOT migration

Next: Phase 65+ - Carrier order SSOT, owner-based init

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-12 23:02:40 +09:00
parent 0ff96612cf
commit ba6e420f31
7 changed files with 929 additions and 9 deletions

View File

@ -613,6 +613,139 @@ impl Default for AstOwnershipAnalyzer {
}
}
/// Phase 64: Analyze a single loop (condition + body) with parent context.
///
/// This helper is designed for P3 production integration. It creates a temporary
/// function scope for parent-defined variables, then analyzes the loop scope.
///
/// # Arguments
///
/// * `condition` - Loop condition AST node
/// * `body` - Loop body statements
/// * `parent_defined` - Variables defined in parent scope (function params/locals)
///
/// # Returns
///
/// OwnershipPlan for the loop scope only (not the temporary function scope).
///
/// # Example
///
/// ```ignore
/// // loop(i < 10) { local sum=0; sum=sum+1; i=i+1; }
/// let condition = /* i < 10 */;
/// let body = vec![/* local sum=0; sum=sum+1; i=i+1; */];
/// let parent_defined = vec![];
/// let plan = analyze_loop(&condition, &body, &parent_defined)?;
/// // plan.owned_vars contains: sum (is_written=true), i (is_written=true)
/// ```
#[cfg(feature = "normalized_dev")]
pub fn analyze_loop(
condition: &ASTNode,
body: &[ASTNode],
parent_defined: &[String],
) -> Result<OwnershipPlan, String> {
let mut analyzer = AstOwnershipAnalyzer::new();
// Create temporary function scope for parent context
let parent_scope = analyzer.alloc_scope(ScopeKind::Function, None);
for var in parent_defined {
analyzer
.scopes
.get_mut(&parent_scope)
.unwrap()
.defined
.insert(var.clone());
}
// Create loop scope
let loop_scope = analyzer.alloc_scope(ScopeKind::Loop, Some(parent_scope));
// Analyze condition (with is_condition=true flag)
analyzer.analyze_node(condition, loop_scope, true)?;
// Analyze body statements
for stmt in body {
analyzer.analyze_node(stmt, loop_scope, false)?;
}
// Propagate to parent
analyzer.propagate_to_parent(loop_scope);
// Build plan for loop only
analyzer.build_plan_for_scope(loop_scope)
}
impl AstOwnershipAnalyzer {
/// Phase 64: Build OwnershipPlan for a specific scope (helper for analyze_loop).
///
/// This is a private helper used by `analyze_loop()` to extract a single
/// scope's OwnershipPlan without building plans for all scopes.
#[cfg(feature = "normalized_dev")]
fn build_plan_for_scope(&self, scope_id: ScopeId) -> Result<OwnershipPlan, String> {
let scope = self
.scopes
.get(&scope_id)
.ok_or_else(|| format!("Scope {:?} not found", scope_id))?;
let mut plan = OwnershipPlan::new(scope_id);
// Collect owned vars
for name in &scope.defined {
let is_written = scope.writes.contains(name);
let is_condition_only = is_written && scope.condition_reads.contains(name);
plan.owned_vars.push(ScopeOwnedVar {
name: name.clone(),
is_written,
is_condition_only,
});
}
// Collect relay writes
for name in &scope.writes {
if scope.defined.contains(name) {
continue;
}
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!(
"AstOwnershipAnalyzer: relay write '{}' in scope {:?} has no owner",
name, scope_id
));
}
}
// Collect captures
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,
});
}
}
// Collect condition captures
for cap in &plan.captures {
if scope.condition_reads.contains(&cap.name) {
plan.condition_captures.push(cap.clone());
}
}
#[cfg(debug_assertions)]
plan.verify_invariants()?;
Ok(plan)
}
}
#[cfg(test)]
mod tests {
use super::*;