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:
@ -122,6 +122,38 @@ impl MirBuilder {
|
|||||||
func_name: &str,
|
func_name: &str,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> Result<Option<ValueId>, String> {
|
) -> Result<Option<ValueId>, 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
|
// Phase 194: Use table-driven router instead of if/else chain
|
||||||
use super::patterns::{route_loop_pattern, LoopPatternContext};
|
use super::patterns::{route_loop_pattern, LoopPatternContext};
|
||||||
|
|
||||||
|
|||||||
@ -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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -413,4 +515,125 @@ mod tests {
|
|||||||
let names = skeleton.carrier_names();
|
let names = skeleton.carrier_names();
|
||||||
assert_eq!(names, vec!["i", "sum"]);
|
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