feat(joinir): Phase 89 P0 - Continue + Early Return pattern detector

## Pattern4 Detector 締め
- is_pattern4_continue_minimal() を厳しく
- Select 必須 + conditional Jump exactly 1
- loop 内 return を P4 と誤認しない

## 新パターン箱
- LoopPattern::ContinueReturn enum 追加
- has_return_in_loop_body() helper 追加
- Fail-Fast: UnimplementedPattern error

## Normalized-dev 統合
- NormalizedDevShape::PatternContinueReturnMinimal
- detector: Select + conditional Jumps >= 2
- canonical には入れない(dev-only)

## Documentation
- 10-Now.md, CURRENT_TASK.md 更新

Impact:
- 987 lib tests PASS
- 60 normalized_dev tests PASS
- Pattern4 誤爆防止

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-14 00:59:58 +09:00
parent b578eb7a65
commit 4e3fc4ad49
9 changed files with 351 additions and 16 deletions

View File

@ -11,12 +11,13 @@
---
## 今の状態Phase 7487 まで到達)
## 今の状態Phase 7489 まで到達)
- Scope/BindingId の段階移行dev-onlyは Pattern2/3/4 まで配線済みdual-path 維持)。
- Pattern2 の promoted carriersDigitPos/Trimについて ExitLine 契約ConditionOnly を exit PHI から除外)を E2E で固定済み。
- debug flag SSOT / DebugOutputBox 移行 / error tags 集約 / carrier init builder まで整備済み。
- **LLVM exe line SSOT 確立**: `tools/build_llvm.sh` を使用した .hako → executable パイプライン標準化完了。
- **Phase 89 P0 進行中**: Continue + Early Return パターンの dev-only 固定Step 0 完了、detector 締め済み)。
- `cargo test --release --lib` は PASS を維持(退行なし)。
参照:
@ -31,7 +32,26 @@
## 次の指示書(優先順位)
### P0: JoinIR / Selfhost depth-2 の前進Phase 88 候補)
### P0 (進行中): Phase 89 - Continue + Early Return Pattern
**現在地**: Step 0 完了detector 締め済み、Step 1 進行中docs 更新)
**残りステップ**:
1. ✅ Step 0: detector 締め + test完了
2. 🚧 Step 1: ドキュメント10-Now.md, CURRENT_TASK.md← 現在ここ
3. Step 2: 最小 fixture 作成continue + early return、決定的出力
4. Step 3: Frontend 新パターン箱追加LoopPattern::ContinueReturn
5. Step 4: normalized-dev 統合shape + normalize 関数)
6. Step 5: 受け入れテスト(構造 vs vm-bridge 一致 + 期待値)
**受け入れ基準**:
- `NYASH_JOINIR_NORMALIZED_DEV_RUN=1 cargo test --features normalized_dev --test normalized_joinir_min` PASS
- `cargo test --release --lib` PASS退行なし
- dev-only のみcanonical には入れない)
**参照**: ユーザー提供の Phase 89 指示書
### P1: JoinIR / Selfhost depth-2 の前進Phase 90 候補)
目的:
- JsonParserBox の残り複合ループを JoinIR 対応する。

View File

@ -55,10 +55,37 @@
- Prerequisites documented: llvm-config-18, llvmlite, LLVM features
- Integration test: PASS (or SKIP if no LLVM)
**Next**: Phase 88+ (TBD - await user direction)
**Reference**: phase87-selfhost-llvm-exe-line.md
### Phase 89 P0: Continue + Early Return Pattern (2025-12-14) 🚧
**Status**: IN PROGRESS - Dev-only fixture isolation
**Goal**:
- Establish new control axis: Continue + Early Return (loop-internal return)
- Prevent Pattern4 detector false positives (canonical mismatch)
- Maintain default behavior (normalized_dev scope only)
**Step 0 COMPLETE** (2025-12-14):
- ✅ Pattern4 detector tightened (require Select + exactly 1 conditional Jump)
- ✅ Test added: `test_pattern4_detector_rejects_loop_with_return`
- ✅ All existing Pattern4 tests pass
**Next Steps**:
- P0-1: Documentation (10-Now.md, CURRENT_TASK.md)
- P0-2: Minimal fixture (continue + early return, deterministic output)
- P0-3: Frontend pattern (LoopPattern::ContinueReturn enum + lowering box)
- P0-4: Normalized-dev integration (shape + normalize function)
- P0-5: Acceptance tests (structured vs vm-bridge parity + expected output)
**Constraints**:
- ❌ No default behavior changes
- ❌ No by-name branching
- ❌ No fallback processing (Fail-Fast)
- ❌ Not added to canonical shapes initially (dev-only first)
**Reference**: (TBD - phase89-continue-return-pattern.md)
### Scope / BindingIddev-only の段階移行ライン)
- MIR builder 側で lexical scope / shadowing を実在化し、言語仕様の “local はブロック境界で分離” を SSOT に揃えた。

View File

@ -412,4 +412,38 @@ impl AstToJoinIrLowerer {
}
})
}
/// Phase 89: Loop body に Return があるかチェック
///
/// ループパターン検出loop_frontend_bindingで使用される。
/// If文内のReturnステートメントを検出するloop-internal early return
///
/// # Arguments
/// * `loop_body` - ループ本体のステートメント配列
///
/// # Returns
/// ループ内にReturnがあればtrue
pub(crate) fn has_return_in_loop_body(loop_body: &[serde_json::Value]) -> bool {
loop_body.iter().any(|stmt| {
if stmt["type"].as_str() == Some("If") {
let then_has = stmt["then"]
.as_array()
.map(|body| {
body.iter()
.any(|s| s["type"].as_str() == Some("Return"))
})
.unwrap_or(false);
let else_has = stmt["else"]
.as_array()
.map(|body| {
body.iter()
.any(|s| s["type"].as_str() == Some("Return"))
})
.unwrap_or(false);
then_has || else_has
} else {
false
}
})
}
}

View File

@ -54,12 +54,19 @@ pub fn detect_loop_pattern(
"reduce" | "fold" => LoopPattern::Reduce,
// デフォルト: Simple パターン
// ただし Break/Continue があれば別パターン
// ただし Break/Continue/Return があれば別パターン
_ => {
if let Some(body) = loop_body {
if AstToJoinIrLowerer::has_break_in_loop_body(body) {
let has_break = AstToJoinIrLowerer::has_break_in_loop_body(body);
let has_continue = AstToJoinIrLowerer::has_continue_in_loop_body(body);
let has_return = AstToJoinIrLowerer::has_return_in_loop_body(body);
// Phase 89: Continue + Return の複合パターン
if has_continue && has_return {
LoopPattern::ContinueReturn
} else if has_break {
LoopPattern::Break
} else if AstToJoinIrLowerer::has_continue_in_loop_body(body) {
} else if has_continue {
LoopPattern::Continue
} else {
LoopPattern::Simple

View File

@ -57,6 +57,12 @@ pub enum LoopPattern {
/// Continue パターンPhase P4
/// 責務: if continue 条件で処理をスキップするループを Select に落とす
Continue,
/// ContinueReturn パターンPhase 89
/// 責務: continue + early return 両方を持つループを複合的に処理
/// - continue: Select で carrier 切り替え
/// - early return: 条件付き Jump で k_exit へ早期脱出
ContinueReturn,
}
/// ループパターン lowering エラー
@ -121,5 +127,9 @@ pub fn lower_loop_with_pattern(
LoopPattern::Simple => simple::lower(lowerer, program_json),
LoopPattern::Break => break_pattern::lower(lowerer, program_json),
LoopPattern::Continue => continue_pattern::lower(lowerer, program_json),
LoopPattern::ContinueReturn => Err(LoweringError::UnimplementedPattern {
pattern: LoopPattern::ContinueReturn,
reason: "ContinueReturn pattern Phase 89 implementation in progress (Step 3)".to_string(),
}),
}
}

View File

@ -1172,6 +1172,11 @@ pub(crate) fn normalized_dev_roundtrip_structured(
normalized_pattern2_to_structured(&norm)
}))
}
// Phase 89: Continue + Early Return pattern (dev-only, delegates to P2 for now)
NormalizedDevShape::PatternContinueReturnMinimal => catch_unwind(AssertUnwindSafe(|| {
let norm = normalize_pattern2_minimal(module);
normalized_pattern2_to_structured(&norm)
})),
};
match attempt {

View File

@ -84,6 +84,8 @@ pub enum NormalizedDevShape {
// Phase 54: selfhost P2/P3 shape growth (structural axis expansion)
SelfhostVerifySchemaP2,
SelfhostDetectFormatP3,
// Phase 89: Continue + Early Return pattern (dev-only)
PatternContinueReturnMinimal,
}
type Detector = fn(&JoinModule) -> bool;
@ -171,6 +173,11 @@ const SHAPE_DETECTORS: &[(NormalizedDevShape, Detector)] = &[
NormalizedDevShape::SelfhostDetectFormatP3,
detectors::is_selfhost_detect_format_p3,
),
// Phase 89: Continue + Early Return pattern
(
NormalizedDevShape::PatternContinueReturnMinimal,
detectors::is_pattern_continue_return_minimal,
),
];
/// direct ブリッジで扱う shapedev 限定)。
@ -213,6 +220,8 @@ pub fn capability_for_shape(shape: &NormalizedDevShape) -> ShapeCapability {
// Phase 54: selfhost P2/P3 shape growth
SelfhostVerifySchemaP2 => SelfhostP2Core,
SelfhostDetectFormatP3 => SelfhostP3IfSum,
// Phase 89: Continue + Early Return pattern (dev-only, maps to P4 family)
PatternContinueReturnMinimal => P4ContinueSkipWs,
};
ShapeCapability::new(kind)
@ -832,6 +841,10 @@ mod detectors {
}
/// Phase 48-A: Check if module matches Pattern4 continue minimal shape
///
/// Phase 89: Tightened to prevent continue + early return misdetection:
/// - Requires at least one Select instruction (continue's core)
/// - Requires exactly one conditional Jump to k_exit (loop break, not early return)
pub(crate) fn is_pattern4_continue_minimal(module: &JoinModule) -> bool {
// Structure-based detection (avoid name-based heuristics)
@ -848,11 +861,11 @@ mod detectors {
// P4 characteristics:
// - Has Compare instruction (loop condition or continue check)
// - Has conditional Jump (for continue/break semantics)
// - Has Select instruction (continue's core - carrier switching)
// - Has tail call (loop back)
// - Has exactly one conditional Jump to k_exit (loop break only)
//
// Note: Simplified detector - relies on Continue being present in original AST
// which gets lowered to conditional tail call structure.
// Phase 89: Tightened to exclude loop-internal return patterns
let has_compare = loop_step.body.iter().any(|inst| {
matches!(
@ -861,16 +874,36 @@ mod detectors {
)
});
// Has conditional jump or call (for continue/break check)
let has_conditional_flow = loop_step.body.iter().any(|inst| {
matches!(inst, JoinInst::Jump { cond: Some(_), .. })
|| matches!(inst, JoinInst::Call { k_next: None, .. })
// Phase 89: Require Select (continue's core)
let has_select = loop_step.body.iter().any(|inst| match inst {
JoinInst::Select { .. } => true,
JoinInst::Compute(mir_inst) => matches!(
mir_inst,
crate::mir::join_ir::MirLikeInst::Select { .. }
),
_ => false,
});
// Phase 89: Count conditional Jumps to k_exit
// Continue pattern should have exactly 1 (loop break), not multiple (early returns)
let k_exit_jumps_count = loop_step.body.iter().filter(|inst| {
matches!(inst, JoinInst::Jump { cond: Some(_), .. })
}).count();
let has_tail_call = loop_step
.body
.iter()
.any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. }));
// P4 minimal has 2-4 params (i, acc, possibly n)
let reasonable_param_count = (2..=4).contains(&loop_step.params.len());
has_compare && has_conditional_flow && reasonable_param_count
// Phase 89: Tightened conditions
has_compare
&& has_select
&& has_tail_call
&& reasonable_param_count
&& k_exit_jumps_count == 1 // Exactly one loop break (not early return)
}
pub(crate) fn is_jsonparser_parse_array_continue_skip_ws(module: &JoinModule) -> bool {
@ -889,6 +922,70 @@ mod detectors {
.any(|f| f.name == "jsonparser_parse_object_continue_skip_ws")
}
/// Phase 89: Check if module matches Continue + Early Return pattern
///
/// Structural characteristics:
/// - 3 functions (main, loop_step, k_exit)
/// - Has Select instruction (continue's core)
/// - Has TWO or more conditional Jumps to k_exit (loop break + early return)
/// - Has Compare instruction
/// - Has tail call (loop back)
pub(crate) fn is_pattern_continue_return_minimal(module: &JoinModule) -> bool {
// Must have exactly 3 functions
if !module.is_structured() || module.functions.len() != 3 {
return false;
}
// Find loop_step function
let loop_step = match find_loop_step(module) {
Some(f) => f,
None => return false,
};
// Continue + Return characteristics:
// - Has Select instruction (continue's core)
// - Has TWO or more conditional Jumps (loop break + early return)
// - Has Compare instruction
// - Has tail call (loop back)
let has_compare = loop_step.body.iter().any(|inst| {
matches!(
inst,
JoinInst::Compute(crate::mir::join_ir::MirLikeInst::Compare { .. })
)
});
let has_select = loop_step.body.iter().any(|inst| match inst {
JoinInst::Select { .. } => true,
JoinInst::Compute(mir_inst) => matches!(
mir_inst,
crate::mir::join_ir::MirLikeInst::Select { .. }
),
_ => false,
});
// Continue + Return pattern requires TWO or more conditional Jumps
// (at least one for loop break, one for early return)
let k_exit_jumps_count = loop_step.body.iter().filter(|inst| {
matches!(inst, JoinInst::Jump { cond: Some(_), .. })
}).count();
let has_tail_call = loop_step
.body
.iter()
.any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. }));
// Reasonable param count (i, acc, possibly n)
let reasonable_param_count = (2..=4).contains(&loop_step.params.len());
// Phase 89: Continue + Return pattern requires >= 2 conditional Jumps
has_compare
&& has_select
&& has_tail_call
&& reasonable_param_count
&& k_exit_jumps_count >= 2 // At least 2: loop break + early return
}
pub(super) fn find_loop_step(module: &JoinModule) -> Option<&JoinFunction> {
module
.functions
@ -1137,4 +1234,135 @@ mod tests {
object_shapes
);
}
#[cfg(feature = "normalized_dev")]
#[test]
fn test_pattern4_detector_rejects_loop_with_return() {
// Phase 89: Verify that Pattern4 detector does NOT match
// modules with loop-internal return (continue + early return pattern)
use crate::mir::join_ir::{JoinFuncId, JoinModule};
use crate::mir::ValueId;
use std::collections::BTreeMap;
// Minimal module with loop + continue + return
// (this would be the ContinueReturn pattern, NOT Pattern4)
let mut functions = BTreeMap::new();
// Entry function
let entry_func = JoinFunction {
id: JoinFuncId::new(0),
name: "loop_with_return_test".to_string(),
params: vec![ValueId(0)],
body: vec![
JoinInst::Compute(crate::mir::join_ir::MirLikeInst::Const {
dst: ValueId(1),
value: crate::mir::join_ir::ConstValue::Integer(0),
}),
JoinInst::Call {
func: JoinFuncId::new(1),
args: vec![ValueId(1), ValueId(1), ValueId(0)],
k_next: None,
dst: Some(ValueId(2)),
},
JoinInst::Ret { value: Some(ValueId(2)) },
],
exit_cont: None,
};
// loop_step function with TWO conditional Jumps (break + early return)
let loop_step_func = JoinFunction {
id: JoinFuncId::new(1),
name: "loop_step".to_string(),
params: vec![ValueId(0), ValueId(1), ValueId(2)],
body: vec![
// Compare for loop condition
JoinInst::Compute(crate::mir::join_ir::MirLikeInst::Compare {
dst: ValueId(10),
op: crate::mir::join_ir::CompareOp::Lt,
lhs: ValueId(0),
rhs: ValueId(2),
}),
JoinInst::Compute(crate::mir::join_ir::MirLikeInst::Const {
dst: ValueId(11),
value: crate::mir::join_ir::ConstValue::Bool(false),
}),
JoinInst::Compute(crate::mir::join_ir::MirLikeInst::Compare {
dst: ValueId(12),
op: crate::mir::join_ir::CompareOp::Eq,
lhs: ValueId(10),
rhs: ValueId(11),
}),
// First Jump: loop break
JoinInst::Jump {
cont: JoinFuncId::new(2).as_cont(),
args: vec![ValueId(1)],
cond: Some(ValueId(12)),
},
// Compare for early return condition
JoinInst::Compute(crate::mir::join_ir::MirLikeInst::Compare {
dst: ValueId(20),
op: crate::mir::join_ir::CompareOp::Eq,
lhs: ValueId(0),
rhs: ValueId(2),
}),
// Second Jump: early return (THIS MAKES IT NOT PATTERN4)
JoinInst::Jump {
cont: JoinFuncId::new(2).as_cont(),
args: vec![ValueId(1)],
cond: Some(ValueId(20)),
},
// Select (continue's core)
JoinInst::Select {
dst: ValueId(30),
cond: ValueId(20),
then_val: ValueId(1),
else_val: ValueId(1),
type_hint: None,
},
// Tail call (loop back)
JoinInst::Call {
func: JoinFuncId::new(1),
args: vec![ValueId(0), ValueId(30), ValueId(2)],
k_next: None,
dst: Some(ValueId(40)),
},
JoinInst::Ret { value: Some(ValueId(40)) },
],
exit_cont: None,
};
// k_exit function
let k_exit_func = JoinFunction {
id: JoinFuncId::new(2),
name: "k_exit".to_string(),
params: vec![ValueId(0)],
body: vec![JoinInst::Ret { value: Some(ValueId(0)) }],
exit_cont: None,
};
functions.insert(JoinFuncId::new(0), entry_func);
functions.insert(JoinFuncId::new(1), loop_step_func);
functions.insert(JoinFuncId::new(2), k_exit_func);
let module = JoinModule {
functions,
entry: Some(JoinFuncId::new(0)),
phase: crate::mir::join_ir::JoinIrPhase::Structured,
};
// Phase 89: This should NOT be detected as Pattern4ContinueMinimal
// because it has TWO conditional Jumps (loop break + early return)
assert!(
!detectors::is_pattern4_continue_minimal(&module),
"Module with loop-internal return should NOT match Pattern4ContinueMinimal"
);
let shapes = detect_shapes(&module);
assert!(
!shapes.contains(&NormalizedDevShape::Pattern4ContinueMinimal),
"Pattern4ContinueMinimal should not be detected for loop with return, got: {:?}",
shapes
);
}
}

View File

@ -140,6 +140,10 @@ fn normalize_for_shape(
.expect("P4 object normalization failed")
},
)),
// Phase 89: Continue + Early Return pattern (dev-only, delegates to P2 for now)
NormalizedDevShape::PatternContinueReturnMinimal => {
catch_unwind(AssertUnwindSafe(|| normalize_pattern2_minimal(module)))
}
};
match result {