feat(mir): Loop Canonicalizer Phase 2 - dev-only observation
## Summary ループ入口で LoopSkeleton/RoutingDecision を観測できるようにした。 既定挙動は完全不変(dev-only の並行観測のみ)。 ## Changes - src/mir/loop_canonicalizer/mod.rs: canonicalize_loop_expr() 追加 - Phase 2 最小実装: loop 構造検証のみ - パターン検出は未実装(全て Fail-Fast) - LoopSkeleton の基本構造(HeaderCond step)を生成 - 詳細な Fail-Fast 理由を返す - src/mir/builder/control_flow/joinir/routing.rs: dev-only 観測ポイント - joinir_dev_enabled() ON 時のみ観測ログ出力 - LoopSkeleton/RoutingDecision の内容を可視化 - 既存の routing/lowering は完全無変更 - 最小実装: skip_whitespace 相当の構造検証のみ対応 - 追加テスト: - test_canonicalize_rejects_non_loop - test_canonicalize_minimal_loop_structure - test_canonicalize_rejects_multi_statement_loop - test_canonicalize_rejects_if_without_else ## Tests - cargo test --release --lib: 1043 passed (退行なし) - HAKO_JOINIR_DEBUG=1: 観測ログ出力確認 - デフォルト: 完全無変更(ログも挙動も) ## Acceptance Criteria ✅ joinir_dev_enabled() ON 時のみ観測ログが出る ✅ joinir_dev_enabled() OFF 時は完全に無変更(ログも挙動も) ✅ 既存の smoke / cargo test --release --lib が退行しない ✅ 最小パターン(if-else with break)で LoopSkeleton が生成できる ✅ 未対応パターンは Fail-Fast で詳細理由を返す Phase 137-2 complete
This commit is contained in:
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user