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:
@ -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 ブリッジで扱う shape(dev 限定)。
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user