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:
nyash-codex
2025-12-16 05:17:56 +09:00
parent ec1ff5b766
commit e8d93f107c
2 changed files with 255 additions and 0 deletions

View File

@ -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(&not_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"));
}
}