diff --git a/src/mir/builder/control_flow/joinir/routing.rs b/src/mir/builder/control_flow/joinir/routing.rs index d0917631..96d98bc8 100644 --- a/src/mir/builder/control_flow/joinir/routing.rs +++ b/src/mir/builder/control_flow/joinir/routing.rs @@ -122,6 +122,38 @@ impl MirBuilder { func_name: &str, debug: bool, ) -> Result, String> { + // Phase 137-2: Dev-only observation via Loop Canonicalizer + if crate::config::env::joinir_dev_enabled() { + use crate::ast::Span; + use crate::mir::loop_canonicalizer::canonicalize_loop_expr; + + // Reconstruct loop AST for canonicalizer + let loop_ast = ASTNode::Loop { + condition: Box::new(condition.clone()), + body: body.to_vec(), + span: Span::unknown(), + }; + + match canonicalize_loop_expr(&loop_ast) { + Ok((skeleton, decision)) => { + eprintln!("[loop_canonicalizer] Function: {}", func_name); + eprintln!("[loop_canonicalizer] Skeleton steps: {}", skeleton.steps.len()); + eprintln!("[loop_canonicalizer] Carriers: {}", skeleton.carriers.len()); + eprintln!("[loop_canonicalizer] Has exits: {}", skeleton.exits.has_any_exit()); + eprintln!("[loop_canonicalizer] Decision: {}", + if decision.is_success() { "SUCCESS" } else { "FAIL_FAST" }); + if decision.is_fail_fast() { + eprintln!("[loop_canonicalizer] Reason: {}", decision.notes.join("; ")); + eprintln!("[loop_canonicalizer] Missing caps: {:?}", decision.missing_caps); + } + } + Err(e) => { + eprintln!("[loop_canonicalizer] Function: {}", func_name); + eprintln!("[loop_canonicalizer] Error: {}", e); + } + } + } + // Phase 194: Use table-driven router instead of if/else chain use super::patterns::{route_loop_pattern, LoopPatternContext}; diff --git a/src/mir/loop_canonicalizer/mod.rs b/src/mir/loop_canonicalizer/mod.rs index 804d5c08..b0f65440 100644 --- a/src/mir/loop_canonicalizer/mod.rs +++ b/src/mir/loop_canonicalizer/mod.rs @@ -325,6 +325,108 @@ impl std::fmt::Display for CarrierRole { } } +// ============================================================================ +// Phase 2: Canonicalization Entry Point +// ============================================================================ + +/// Canonicalize a loop AST into LoopSkeleton (Phase 2: Minimal Implementation) +/// +/// Currently supports only the skip_whitespace pattern: +/// ``` +/// loop(cond) { +/// if check_cond { +/// carrier = carrier + step +/// } else { +/// break +/// } +/// } +/// ``` +/// +/// All other patterns return Fail-Fast with detailed reasoning. +/// +/// # Arguments +/// - `loop_expr`: The loop AST node (must be `ASTNode::Loop`) +/// +/// # Returns +/// - `Ok((skeleton, decision))`: Successfully extracted skeleton and routing decision +/// - `Err(String)`: Malformed AST or internal error +pub fn canonicalize_loop_expr( + loop_expr: &ASTNode, +) -> Result<(LoopSkeleton, RoutingDecision), String> { + // Extract loop components + let (condition, body, span) = match loop_expr { + ASTNode::Loop { + condition, + body, + span, + } => (condition.as_ref(), body, span.clone()), + _ => return Err(format!("Expected Loop node, got: {:?}", loop_expr)), + }; + + // Phase 2: Minimal implementation - detect skip_whitespace pattern only + // Pattern: loop(cond) { if check { update } else { break } } + + // Check for minimal pattern: single if-else with break + if body.len() != 1 { + return Ok(( + LoopSkeleton::new(span), + RoutingDecision::fail_fast( + vec![capability_tags::CAP_MISSING_SINGLE_BREAK], + format!("Phase 2: Only single-statement loops supported (got {} statements)", body.len()), + ), + )); + } + + // Check if it's an if-else statement + let _if_stmt = match &body[0] { + ASTNode::If { + condition: _if_cond, + then_body: _then_body, + else_body, + .. + } => { + // Must have else branch + if else_body.is_none() { + return Ok(( + LoopSkeleton::new(span), + RoutingDecision::fail_fast( + vec![capability_tags::CAP_MISSING_SINGLE_BREAK], + "Phase 2: If statement must have else branch".to_string(), + ), + )); + } + // Phase 2: Just validate structure, don't extract components yet + () + } + _ => { + return Ok(( + LoopSkeleton::new(span), + RoutingDecision::fail_fast( + vec![capability_tags::CAP_MISSING_SINGLE_BREAK], + "Phase 2: Loop body must be single if-else statement".to_string(), + ), + )); + } + }; + + // Build minimal skeleton + let mut skeleton = LoopSkeleton::new(span); + + // Add header condition + skeleton.steps.push(SkeletonStep::HeaderCond { + expr: Box::new(condition.clone()), + }); + + // For now, just mark as unsupported - full pattern detection will come in Phase 3 + Ok(( + skeleton, + RoutingDecision::fail_fast( + vec![capability_tags::CAP_MISSING_CONST_STEP], + "Phase 2: Pattern detection not yet implemented".to_string(), + ), + )) +} + #[cfg(test)] mod tests { use super::*; @@ -413,4 +515,125 @@ mod tests { let names = skeleton.carrier_names(); assert_eq!(names, vec!["i", "sum"]); } + + // ============================================================================ + // Phase 2: Canonicalize Tests + // ============================================================================ + + #[test] + fn test_canonicalize_rejects_non_loop() { + use crate::ast::LiteralValue; + + let not_loop = ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }; + + let result = canonicalize_loop_expr(¬_loop); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Expected Loop node")); + } + + #[test] + fn test_canonicalize_minimal_loop_structure() { + use crate::ast::LiteralValue; + + // Build minimal loop: loop(true) { if true { } else { break } } + let loop_node = ASTNode::Loop { + condition: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }), + body: vec![ASTNode::If { + condition: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }), + then_body: vec![], + else_body: Some(vec![ASTNode::Break { + span: Span::unknown(), + }]), + span: Span::unknown(), + }], + span: Span::unknown(), + }; + + let result = canonicalize_loop_expr(&loop_node); + assert!(result.is_ok()); + + let (skeleton, decision) = result.unwrap(); + // Should have header condition step + assert_eq!(skeleton.steps.len(), 1); + assert!(matches!( + skeleton.steps[0], + SkeletonStep::HeaderCond { .. } + )); + + // Phase 2: Should fail-fast (pattern detection not implemented) + assert!(decision.is_fail_fast()); + assert!(decision.notes[0].contains("Pattern detection not yet implemented")); + } + + #[test] + fn test_canonicalize_rejects_multi_statement_loop() { + use crate::ast::LiteralValue; + + // Build loop with 2 statements + let loop_node = ASTNode::Loop { + condition: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }), + body: vec![ + ASTNode::Print { + expression: Box::new(ASTNode::Literal { + value: LiteralValue::String("test".to_string()), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ASTNode::Break { + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }; + + let result = canonicalize_loop_expr(&loop_node); + assert!(result.is_ok()); + + let (_, decision) = result.unwrap(); + assert!(decision.is_fail_fast()); + assert!(decision.notes[0].contains("Only single-statement loops supported")); + } + + #[test] + fn test_canonicalize_rejects_if_without_else() { + use crate::ast::LiteralValue; + + // Build loop with if (no else) + let loop_node = ASTNode::Loop { + condition: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }), + body: vec![ASTNode::If { + condition: Box::new(ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }), + then_body: vec![], + else_body: None, // No else branch + span: Span::unknown(), + }], + span: Span::unknown(), + }; + + let result = canonicalize_loop_expr(&loop_node); + assert!(result.is_ok()); + + let (_, decision) = result.unwrap(); + assert!(decision.is_fail_fast()); + assert!(decision.notes[0].contains("must have else branch")); + } }