refactor(mir): Phase 140-P5-A - LoopProcessingContext SSOT化

## 変更内容

### 新規ファイル
- `src/mir/builder/control_flow/joinir/loop_context.rs` (324行)
  - LoopProcessingContext 型定義
  - AST + Skeleton + Pattern の統合 Context
  - verify_parity() メソッド実装

### 修正ファイル
- `src/mir/builder/control_flow/joinir/mod.rs`
  - loop_context モジュール追加

## 実装内容

### LoopProcessingContext 構造
```rust
pub struct LoopProcessingContext<'a> {
    // AST 情報
    pub condition: &'a ASTNode,
    pub body: &'a [ASTNode],
    pub span: Span,

    // Canonicalizer 出力(Option: 未実行時は None)
    pub skeleton: Option<LoopSkeleton>,
    pub decision: Option<RoutingDecision>,

    // Router 情報(常に存在)
    pub pattern_kind: LoopPatternKind,
    pub features: LoopFeatures,
}
```

### 主要メソッド
- `new()`: 新規作成(Canonicalizer 未実行)
- `set_canonicalizer_result()`: Canonicalizer 出力を設定
- `to_loop_ast()`: AST 再構築(parity verification 用)
- `verify_parity()`: Canonicalizer と Router の一致検証

### テスト
- 5 つの単体テスト実装
- Context 作成、AST 再構築、parity 検証をカバー
- 全テスト PASS 

## 効果
- AST + Skeleton + Pattern の SSOT 化
- 重複 AST 再構築コードの削除準備完了
- 情報の一元管理による保守性向上

## 次のステップ
Phase 140-P5-B: parity_checker.rs を Context 化
This commit is contained in:
nyash-codex
2025-12-16 07:22:30 +09:00
parent d0d3512be4
commit 4f41293d86
2 changed files with 325 additions and 0 deletions

View File

@ -0,0 +1,323 @@
//! Loop Processing Context - SSOT for AST + Skeleton + Pattern
//!
//! Phase 140-P5: Unified context for loop processing that integrates:
//! - AST information (condition, body, span)
//! - Canonicalizer output (LoopSkeleton, RoutingDecision)
//! - Router information (LoopPatternKind, LoopFeatures)
//!
//! This eliminates duplicate AST reconstruction and centralizes loop processing state.
use crate::ast::{ASTNode, Span};
use crate::mir::loop_canonicalizer::{LoopSkeleton, RoutingDecision};
use crate::mir::loop_pattern_detection::{LoopFeatures, LoopPatternKind};
/// Loop processing context - SSOT for AST + Skeleton + Pattern
///
/// This context integrates all information needed for loop processing:
/// - AST: The source structure (condition, body, span)
/// - Canonicalizer: Normalized skeleton and routing decision (optional)
/// - Router: Pattern classification and features
///
/// # Lifecycle
///
/// 1. Create with `new()` - AST + Router info only
/// 2. Call `set_canonicalizer_result()` - Add Canonicalizer output
/// 3. Call `verify_parity()` - Check consistency (dev-only)
#[derive(Debug)]
pub struct LoopProcessingContext<'a> {
// ========================================================================
// AST Information
// ========================================================================
/// Loop condition AST node
pub condition: &'a ASTNode,
/// Loop body statements
pub body: &'a [ASTNode],
/// Source location for debugging
pub span: Span,
// ========================================================================
// Canonicalizer Output (Optional - set after canonicalization)
// ========================================================================
/// Normalized loop skeleton (None = canonicalizer not run yet)
pub skeleton: Option<LoopSkeleton>,
/// Routing decision from canonicalizer (None = canonicalizer not run yet)
pub decision: Option<RoutingDecision>,
// ========================================================================
// Router Information (Always Present)
// ========================================================================
/// Pattern kind determined by router
pub pattern_kind: LoopPatternKind,
/// Loop features extracted from AST
pub features: LoopFeatures,
}
impl<'a> LoopProcessingContext<'a> {
/// Create new context (canonicalizer not run yet)
///
/// # Parameters
///
/// - `condition`: Loop condition AST node
/// - `body`: Loop body statements
/// - `span`: Source location
/// - `pattern_kind`: Pattern classification from router
/// - `features`: Loop features extracted from AST
pub fn new(
condition: &'a ASTNode,
body: &'a [ASTNode],
span: Span,
pattern_kind: LoopPatternKind,
features: LoopFeatures,
) -> Self {
Self {
condition,
body,
span,
skeleton: None,
decision: None,
pattern_kind,
features,
}
}
/// Set canonicalizer result (skeleton + decision)
///
/// This should be called after running the canonicalizer.
/// After this call, `verify_parity()` can be used to check consistency.
pub fn set_canonicalizer_result(
&mut self,
skeleton: LoopSkeleton,
decision: RoutingDecision,
) {
self.skeleton = Some(skeleton);
self.decision = Some(decision);
}
/// Reconstruct loop AST for canonicalizer input
///
/// This is used for parity verification when we need to run the canonicalizer
/// on the same AST that the router processed.
pub fn to_loop_ast(&self) -> ASTNode {
ASTNode::Loop {
condition: Box::new(self.condition.clone()),
body: self.body.to_vec(),
span: self.span.clone(),
}
}
/// Verify parity between canonicalizer and router (dev-only)
///
/// Checks that the canonicalizer's pattern choice matches the router's
/// pattern_kind. On mismatch:
/// - Strict mode (HAKO_JOINIR_STRICT=1): Returns error
/// - Debug mode: Logs warning
///
/// Returns Ok(()) if:
/// - Canonicalizer not run yet (decision is None)
/// - Patterns match
/// - Non-strict mode (mismatch logged only)
pub fn verify_parity(&self) -> Result<(), String> {
use crate::config::env::joinir_dev;
// Canonicalizer not run yet - skip verification
let decision = match &self.decision {
Some(d) => d,
None => return Ok(()),
};
// Canonicalizer failed (Fail-Fast) - no pattern to compare
let canonical_pattern = match decision.chosen {
Some(p) => p,
None => return Ok(()), // Router might still handle it
};
// Compare patterns
let actual_pattern = self.pattern_kind;
if canonical_pattern != actual_pattern {
let msg = format!(
"[loop_canonicalizer/PARITY] MISMATCH: canonical={:?}, actual={:?}",
canonical_pattern, actual_pattern
);
if joinir_dev::strict_enabled() {
// Strict mode: fail fast
return Err(msg);
} else {
// Debug mode: log only
eprintln!("{}", msg);
}
} else {
// Patterns match - success!
eprintln!(
"[loop_canonicalizer/PARITY] OK: canonical and actual agree on {:?}",
canonical_pattern
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::LiteralValue;
/// Helper: Create a simple loop context for testing
fn make_simple_context<'a>(
condition: &'a ASTNode,
body: &'a [ASTNode],
) -> LoopProcessingContext<'a> {
LoopProcessingContext::new(
condition,
body,
Span::unknown(),
LoopPatternKind::Pattern1SimpleWhile,
LoopFeatures {
has_if: false,
has_break: false,
has_continue: false,
has_if_else_phi: false,
carrier_count: 0,
break_count: 0,
continue_count: 0,
is_infinite_loop: false,
update_summary: None,
},
)
}
#[test]
fn test_context_creation() {
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let ctx = make_simple_context(&condition, &body);
// Check AST fields
assert!(matches!(
ctx.condition,
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
));
assert_eq!(ctx.body.len(), 0);
assert_eq!(ctx.span, Span::unknown());
// Check canonicalizer fields (not set yet)
assert!(ctx.skeleton.is_none());
assert!(ctx.decision.is_none());
// Check router fields
assert_eq!(ctx.pattern_kind, LoopPatternKind::Pattern1SimpleWhile);
}
#[test]
fn test_to_loop_ast_reconstruction() {
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let ctx = make_simple_context(&condition, &body);
let loop_ast = ctx.to_loop_ast();
// Check reconstructed AST
match loop_ast {
ASTNode::Loop {
condition,
body,
span,
} => {
assert!(matches!(
*condition,
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
));
assert_eq!(body.len(), 0);
assert_eq!(span, Span::unknown());
}
_ => panic!("Expected Loop node"),
}
}
#[test]
fn test_verify_parity_without_canonicalizer() {
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let ctx = make_simple_context(&condition, &body);
// Should succeed (canonicalizer not run yet)
assert!(ctx.verify_parity().is_ok());
}
#[test]
fn test_verify_parity_with_matching_patterns() {
use crate::mir::loop_canonicalizer::{ExitContract, RoutingDecision};
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let mut ctx = make_simple_context(&condition, &body);
// Set canonicalizer result with matching pattern
let decision = RoutingDecision::success(LoopPatternKind::Pattern1SimpleWhile);
ctx.set_canonicalizer_result(
LoopSkeleton {
steps: vec![],
carriers: vec![],
exits: ExitContract::none(),
captured: None,
span: Span::unknown(),
},
decision,
);
// Should succeed (patterns match)
assert!(ctx.verify_parity().is_ok());
}
#[test]
fn test_verify_parity_with_fail_fast() {
use crate::mir::loop_canonicalizer::{CapabilityTag, ExitContract, RoutingDecision};
let condition = ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
};
let body = vec![];
let mut ctx = make_simple_context(&condition, &body);
// Set canonicalizer result with fail-fast decision
let decision =
RoutingDecision::fail_fast(vec![CapabilityTag::ConstStep], "Test fail-fast".to_string());
ctx.set_canonicalizer_result(
LoopSkeleton {
steps: vec![],
carriers: vec![],
exits: ExitContract::none(),
captured: None,
span: Span::unknown(),
},
decision,
);
// Should succeed (canonicalizer failed, no pattern to compare)
assert!(ctx.verify_parity().is_ok());
}
}

View File

@ -4,9 +4,11 @@
//! - Pattern lowerers (patterns/)
//! - Routing logic (routing.rs) ✅
//! - Parity verification (parity_checker.rs) ✅ Phase 138
//! - Loop processing context (loop_context.rs) ✅ Phase 140-P5
//! - MIR block merging (merge/) ✅ Phase 4
//! - Unified tracing (trace.rs) ✅ Phase 195
pub(in crate::mir::builder) mod loop_context;
pub(in crate::mir::builder) mod merge;
pub(in crate::mir::builder) mod parity_checker;
pub(in crate::mir::builder) mod patterns;