diff --git a/src/mir/builder/control_flow/joinir/loop_context.rs b/src/mir/builder/control_flow/joinir/loop_context.rs new file mode 100644 index 00000000..459bd205 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/loop_context.rs @@ -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, + + /// Routing decision from canonicalizer (None = canonicalizer not run yet) + pub decision: Option, + + // ======================================================================== + // 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()); + } +} diff --git a/src/mir/builder/control_flow/joinir/mod.rs b/src/mir/builder/control_flow/joinir/mod.rs index cc3f68c0..9fbe6252 100644 --- a/src/mir/builder/control_flow/joinir/mod.rs +++ b/src/mir/builder/control_flow/joinir/mod.rs @@ -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;