feat(joinir): Phase 188.3 - Pattern6 (NestedLoopMinimal) 選択ロジック実装
## Phase 188.3 進捗: Phase 2 完了 (6/13 tasks) ### 実装完了 ✅ **Phase 1: Fixture作成** - apps/tests/phase1883_nested_minimal.hako 追加 - Add/Compare のみ(乗算なし) - 期待 exit code: 9 (3×3 nested loops) - 既存 lowering で fallback 動作確認 **Phase 2: 選択ロジック (SSOT)** - LoopPatternContext に step_tree_max_loop_depth フィールド追加 - choose_pattern_kind() に Pattern6 選択ロジック実装: 1. Cheap check (has_inner_loop) 2. StepTree 構築 (max_loop_depth 取得) 3. AST validation (is_pattern6_lowerable) - pattern6_nested_minimal.rs モジュール作成 (stub) - LOOP_PATTERNS に Pattern6 entry 追加 - **検証**: Pattern6 が正しく選択される ✅ ### 設計原則 (確認済み) 1. **Fail-Fast**: Pattern6 選択後は Ok(None) で逃げない 2. **outer 変数 write-back 検出 → validation false** (Phase 188.4+) 3. **最小実装**: inner local だけ、Pattern1 モデル二重化 4. **cfg! 依存なし**: production で動作 ### 検証結果 ``` [choose_pattern_kind] has_inner_loop=true [choose_pattern_kind] max_loop_depth=2 [choose_pattern_kind] is_pattern6_lowerable=true ✅ Pattern6 SELECTED! ``` Stub からの期待エラー: ``` [ERROR] ❌ [Pattern6] Nested loop lowering not yet implemented ``` ### 次: Phase 3 (Lowering 実装 - 推定4時間) 残りタスク: - Phase 3-1: AST 抽出ヘルパー - Phase 3-2: Validation ヘルパー - Phase 3-3: Continuation 生成 (outer_step, inner_step, k_inner_exit) - Phase 3-4: fixture が exit=9 を返すことを検証 ### 変更ファイル **新規**: - apps/tests/phase1883_nested_minimal.hako - src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs - docs/development/current/main/phases/phase-188.{1,2,3}/README.md **変更**: - src/mir/builder/control_flow/joinir/routing.rs (Pattern6 選択) - src/mir/builder/control_flow/joinir/patterns/router.rs (Context 拡張) - src/mir/builder/control_flow/joinir/patterns/mod.rs (module 宣言) 🎯 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -34,11 +34,34 @@ pub fn check(tree: &StepTree, func_name: &str, strict: bool, dev: bool) -> Resul
|
||||
return Ok(()); // Default behavior: always pass
|
||||
}
|
||||
|
||||
// Phase 188.2: Check nesting depth BEFORE capability check
|
||||
// Reject max_loop_depth > 2 (only 1-level nesting supported)
|
||||
if tree.features.max_loop_depth > 2 {
|
||||
let tag = "control_tree/nested_loop/depth_exceeded";
|
||||
let msg = format!(
|
||||
"Nesting depth {} exceeds limit (max=2) in '{}' (step_tree_sig={})",
|
||||
tree.features.max_loop_depth,
|
||||
func_name,
|
||||
tree.signature.to_hex()
|
||||
);
|
||||
let hint = "Refactor to avoid 3+ level loop nesting, or run without HAKO_JOINIR_STRICT=1";
|
||||
|
||||
if dev {
|
||||
eprintln!(
|
||||
"[joinir/control_tree] depth exceeded: max_loop_depth={} in {}",
|
||||
tree.features.max_loop_depth, func_name
|
||||
);
|
||||
}
|
||||
|
||||
return Err(error_tags::freeze_with_hint(tag, &msg, hint));
|
||||
}
|
||||
|
||||
// Allowlist (supported capabilities)
|
||||
let allowed: BTreeSet<StepCapability> = [
|
||||
StepCapability::If,
|
||||
StepCapability::NestedIf,
|
||||
StepCapability::Loop,
|
||||
StepCapability::NestedLoop, // Phase 188.1: Pattern 6 minimal support
|
||||
StepCapability::Return,
|
||||
StepCapability::Break,
|
||||
StepCapability::Continue,
|
||||
@ -77,7 +100,8 @@ pub fn check(tree: &StepTree, func_name: &str, strict: bool, dev: bool) -> Resul
|
||||
fn get_hint_for_cap(cap: &StepCapability) -> String {
|
||||
match cap {
|
||||
StepCapability::NestedLoop => {
|
||||
"refactor to avoid nested loops (not supported yet) or run without HAKO_JOINIR_STRICT=1".to_string()
|
||||
// Phase 188.2: NestedLoop (1-level) is now supported
|
||||
"1-level nested loops supported; for 2+ levels use depth check error hint".to_string()
|
||||
}
|
||||
StepCapability::TryCatch => {
|
||||
"try/catch not supported in JoinIR yet, use HAKO_JOINIR_STRICT=0".to_string()
|
||||
@ -134,7 +158,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_loop_strict_rejects() {
|
||||
fn test_nested_loop_1level_strict_passes() {
|
||||
// Phase 188.2: 1-level nested loop (depth=2) should PASS
|
||||
// AST: loop(i < 3) { loop(j < 2) { ... } }
|
||||
let nested_loop_ast = vec![ASTNode::Loop {
|
||||
condition: Box::new(bin_lt(var("i"), int_lit(3))),
|
||||
@ -148,12 +173,37 @@ mod tests {
|
||||
|
||||
let tree = StepTreeBuilderBox::build_from_block(&nested_loop_ast);
|
||||
|
||||
// strict=true should reject NestedLoop
|
||||
// strict=true should PASS (NestedLoop is in allowlist, depth=2 is OK)
|
||||
let result = check(&tree, "test_func", true, false);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_loop_2level_strict_rejects() {
|
||||
// Phase 188.2: 2+ level nested loop (depth=3) should FAIL
|
||||
// AST: loop { loop { loop { ... } } }
|
||||
let deeply_nested_ast = vec![ASTNode::Loop {
|
||||
condition: Box::new(bin_lt(var("i"), int_lit(3))),
|
||||
body: vec![ASTNode::Loop {
|
||||
condition: Box::new(bin_lt(var("j"), int_lit(2))),
|
||||
body: vec![ASTNode::Loop {
|
||||
condition: Box::new(bin_lt(var("k"), int_lit(1))),
|
||||
body: vec![],
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let tree = StepTreeBuilderBox::build_from_block(&deeply_nested_ast);
|
||||
|
||||
// strict=true should reject depth > 2
|
||||
let result = check(&tree, "test_func", true, false);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.contains("[joinir/control_tree/cap_missing/NestedLoop]"));
|
||||
assert!(err.contains("Hint:"));
|
||||
assert!(err.contains("[joinir/control_tree/nested_loop/depth_exceeded]"));
|
||||
assert!(err.contains("max=2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -188,6 +188,7 @@ mod tests {
|
||||
continue_count: 0,
|
||||
is_infinite_loop: false,
|
||||
update_summary: None,
|
||||
..Default::default() // Phase 188.1: Use default for new fields
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -127,8 +127,7 @@ pub(crate) fn extract_features(
|
||||
break_count: if has_break { 1 } else { 0 },
|
||||
continue_count: if has_continue { 1 } else { 0 },
|
||||
is_infinite_loop,
|
||||
// Phase 170-C-2b: AST-based extraction doesn't have carrier names yet
|
||||
update_summary: None,
|
||||
..Default::default() // Phase 188.1: Use Default for nesting fields
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -79,6 +79,7 @@ pub(in crate::mir::builder) mod pattern2_with_break;
|
||||
pub(in crate::mir::builder) mod pattern3_with_if_phi;
|
||||
pub(in crate::mir::builder) mod pattern4_carrier_analyzer;
|
||||
pub(in crate::mir::builder) mod pattern4_with_continue;
|
||||
pub(in crate::mir::builder) mod pattern6_nested_minimal; // Phase 188.3: 1-level nested loop (Pattern1 outer + Pattern1 inner)
|
||||
pub(in crate::mir::builder) mod pattern6_scan_with_init; // Phase 254 P0: index_of/find/contains pattern
|
||||
pub(in crate::mir::builder) mod pattern7_split_scan; // Phase 256 P0: split/tokenization with variable step
|
||||
pub(in crate::mir::builder) mod pattern8_scan_bool_predicate; // Phase 259 P0: boolean predicate scan (is_integer/is_valid)
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
//! Phase 188.3: Pattern6 NestedLoopMinimal - 1-level nested loop lowering
|
||||
//!
|
||||
//! Handles loops of the form:
|
||||
//! ```nyash
|
||||
//! loop(outer_cond) {
|
||||
//! loop(inner_cond) {
|
||||
//! // inner body
|
||||
//! }
|
||||
//! // outer body (after inner loop)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Requirements (Pattern1 for both):
|
||||
//! - Outer loop: no break, no continue (Pattern1)
|
||||
//! - Inner loop: no break, no continue (Pattern1)
|
||||
//! - Exactly 1 inner loop
|
||||
//! - max_loop_depth == 2
|
||||
//!
|
||||
//! Strategy:
|
||||
//! - Generate outer_step continuation (contains inner loop call)
|
||||
//! - Generate inner_step continuation (tail recursion)
|
||||
//! - Generate k_inner_exit (bridges to outer continuation)
|
||||
//! - Wire continuations
|
||||
|
||||
use crate::mir::builder::control_flow::joinir::patterns::LoopPatternContext;
|
||||
use crate::mir::builder::MirBuilder;
|
||||
use crate::mir::loop_pattern_detection::LoopPatternKind;
|
||||
use crate::mir::ValueId;
|
||||
|
||||
/// Detect if this context can be lowered as Pattern6 (NestedLoopMinimal)
|
||||
///
|
||||
/// Pattern selection happens in choose_pattern_kind() (SSOT).
|
||||
/// This function just verifies ctx.pattern_kind matches.
|
||||
pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &LoopPatternContext) -> bool {
|
||||
ctx.pattern_kind == LoopPatternKind::Pattern6NestedLoopMinimal
|
||||
}
|
||||
|
||||
/// Lower Pattern6 (NestedLoopMinimal) to MIR
|
||||
///
|
||||
/// Phase 188.3: Full implementation with continuation generation
|
||||
pub(crate) fn lower(
|
||||
_builder: &mut MirBuilder,
|
||||
_ctx: &LoopPatternContext,
|
||||
) -> Result<Option<ValueId>, String> {
|
||||
// Phase 188.3 stub - full implementation in Phase 3-3
|
||||
// TODO: Implement continuation generation (outer_step, inner_step, k_inner_exit)
|
||||
Err("[Pattern6] Nested loop lowering not yet implemented (Phase 188.3 stub)".to_string())
|
||||
}
|
||||
@ -74,6 +74,11 @@ pub(crate) struct LoopPatternContext<'a> {
|
||||
/// SSOT Principle: Avoid re-detecting ConditionalStep in lowering phase.
|
||||
#[allow(dead_code)]
|
||||
pub skeleton: Option<&'a LoopSkeleton>,
|
||||
|
||||
/// Phase 188.3: Cached StepTree max_loop_depth for Pattern6
|
||||
/// None if not computed, Some(depth) if Pattern6 candidate
|
||||
/// Avoids re-building StepTree in lowering phase
|
||||
pub step_tree_max_loop_depth: Option<u32>,
|
||||
}
|
||||
|
||||
impl<'a> LoopPatternContext<'a> {
|
||||
@ -111,6 +116,7 @@ impl<'a> LoopPatternContext<'a> {
|
||||
pattern_kind,
|
||||
fn_body: None, // Phase 200-C: Default to None
|
||||
skeleton: None, // Phase 92 P0-2: Default to None
|
||||
step_tree_max_loop_depth: None, // Phase 188.3: Default to None
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,6 +392,11 @@ pub(crate) struct LoopPatternEntry {
|
||||
/// Note: func_name is now only used for debug logging, not pattern detection
|
||||
/// Phase 286: Pattern5 removed (migrated to Plan-based routing)
|
||||
pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[
|
||||
LoopPatternEntry {
|
||||
name: "Pattern6_NestedLoopMinimal", // Phase 188.3: 1-level nested loop
|
||||
detect: super::pattern6_nested_minimal::can_lower,
|
||||
lower: super::pattern6_nested_minimal::lower,
|
||||
},
|
||||
LoopPatternEntry {
|
||||
name: "Pattern4_WithContinue",
|
||||
detect: super::pattern4_with_continue::can_lower,
|
||||
|
||||
@ -43,6 +43,43 @@ pub(in crate::mir::builder) fn choose_pattern_kind(
|
||||
let has_break = ast_features::detect_break_in_body(body);
|
||||
let has_return = ast_features::detect_return_in_body(body);
|
||||
|
||||
// Phase 188.3: Pattern6 selection for 1-level nested loops
|
||||
// SSOT: All Pattern6 detection happens here (no dev dependency)
|
||||
//
|
||||
// Strategy: Cheap check → StepTree → Full AST validation
|
||||
// Only select Pattern6 if lowering is guaranteed to work
|
||||
|
||||
// Step 1: Cheap check - does body contain any Loop node?
|
||||
let has_inner_loop = body.iter().any(|stmt| matches!(stmt, ASTNode::Loop { .. }));
|
||||
|
||||
if has_inner_loop {
|
||||
// Step 2: Build StepTree to get nesting depth (cost: acceptable for nested loops only)
|
||||
use crate::ast::Span;
|
||||
use crate::mir::control_tree::StepTreeBuilderBox;
|
||||
|
||||
let loop_ast = ASTNode::Loop {
|
||||
condition: Box::new(condition.clone()),
|
||||
body: body.to_vec(),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let tree = StepTreeBuilderBox::build_from_ast(&loop_ast);
|
||||
|
||||
// Step 3: Check if exactly 1-level nesting (depth == 2)
|
||||
if tree.features.max_loop_depth == 2 {
|
||||
// Step 4: Full AST validation (Pattern1-compatible requirements)
|
||||
if is_pattern6_lowerable(&tree, body) {
|
||||
// Pattern6 selected - lowering MUST succeed
|
||||
trace::trace().dev(
|
||||
"choose_pattern_kind",
|
||||
"[routing] Pattern6 selected: 1-level nested loop validated"
|
||||
);
|
||||
return loop_pattern_detection::LoopPatternKind::Pattern6NestedLoopMinimal;
|
||||
}
|
||||
// Validation failed - not Pattern6, fall through to router_choice
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 110: StepTree parity check (structure-only SSOT).
|
||||
//
|
||||
// This is dev-only; strict mode turns mismatch into a fail-fast.
|
||||
@ -145,6 +182,56 @@ pub(in crate::mir::builder) fn choose_pattern_kind(
|
||||
router_choice
|
||||
}
|
||||
|
||||
/// Phase 188.3: Validate nested loop meets ALL Pattern6 requirements
|
||||
///
|
||||
/// Returns true ONLY if Pattern6 lowering is guaranteed to succeed.
|
||||
/// False → fall through to other patterns (NOT an error)
|
||||
fn is_pattern6_lowerable(tree: &crate::mir::control_tree::StepTree, body: &[crate::ast::ASTNode]) -> bool {
|
||||
use crate::ast::ASTNode;
|
||||
|
||||
// Requirement 1: Outer loop has no break (Pattern1 requirement)
|
||||
if tree.features.has_break {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Requirement 2: Outer loop has no continue (Pattern1 requirement)
|
||||
if tree.features.has_continue {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Requirement 3: Extract inner loop(s) - must have exactly 1
|
||||
let mut inner_loop: Option<&ASTNode> = None;
|
||||
for stmt in body.iter() {
|
||||
if matches!(stmt, ASTNode::Loop { .. }) {
|
||||
if inner_loop.is_some() {
|
||||
// Multiple inner loops - not supported
|
||||
return false;
|
||||
}
|
||||
inner_loop = Some(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
let inner_loop = match inner_loop {
|
||||
Some(l) => l,
|
||||
None => return false, // No inner loop found (shouldn't happen, but defensive)
|
||||
};
|
||||
|
||||
// Requirement 4: Inner loop has no break (Pattern1 requirement)
|
||||
use crate::mir::control_tree::StepTreeBuilderBox;
|
||||
let inner_tree = StepTreeBuilderBox::build_from_ast(inner_loop);
|
||||
if inner_tree.features.has_break {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Requirement 5: Inner loop has no continue (Pattern1 requirement)
|
||||
if inner_tree.features.has_continue {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All requirements met - Pattern6 lowering will succeed
|
||||
true
|
||||
}
|
||||
|
||||
impl MirBuilder {
|
||||
/// Phase 49: Try JoinIR Frontend for mainline integration
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user