feat(joinir): Phase 286 P2.4 - Pattern8 BoolPredicateScan Plan化 PoC
## 概要 Pattern8 (BoolPredicateScan) を Plan extraction routing に追加。 static box 除外(Phase 269 決定)を尊重し、非 static box fixture で PoC。 ## 実装内容 - Pattern8BoolPredicateScanPlan struct + DomainPlan variant - extract_pattern8_plan(): 条件・predicate check・increment 抽出 - normalize_pattern8_bool_predicate_scan(): PoC stub(CoreExitPlan::Return 未統合) - PLAN_EXTRACTORS テーブルに Pattern8 追加(3rd priority) - エラーフォールバック: Plan normalization 失敗時 → legacy Pattern8 へ ## 動作フロー Plan extraction MATCHED → normalization failed (PoC stub) → legacy Pattern8 MATCHED ## 検証結果 - Integration: phase286_pattern8_plan_poc_vm PASS (exit 7) - Regression: quick 154 PASS, 0 FAILED 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
37
apps/tests/phase286_pattern8_plan_poc.hako
Normal file
37
apps/tests/phase286_pattern8_plan_poc.hako
Normal file
@ -0,0 +1,37 @@
|
||||
box StringUtils {
|
||||
is_digit(ch) {
|
||||
return ch == "0" or ch == "1" or ch == "2" or ch == "3" or ch == "4"
|
||||
or ch == "5" or ch == "6" or ch == "7" or ch == "8" or ch == "9"
|
||||
}
|
||||
|
||||
is_integer(s) {
|
||||
if s.length() == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
local start = 0
|
||||
if s.substring(0, 1) == "-" {
|
||||
if s.length() == 1 {
|
||||
return false
|
||||
}
|
||||
start = 1
|
||||
}
|
||||
|
||||
local i = start
|
||||
loop(i < s.length()) {
|
||||
if not me.is_digit(s.substring(i, i + 1)) {
|
||||
return false
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
static box Main {
|
||||
main() {
|
||||
local u
|
||||
u = new StringUtils()
|
||||
return u.is_integer("123") ? 7 : 1
|
||||
}
|
||||
}
|
||||
@ -222,7 +222,34 @@ Phase 286 では JoinIR line を “第2の lowerer” として放置せず、*
|
||||
- 核ロジックは main loop に密結合しているため、完全な分離にはさらなるリファクタリングが必要
|
||||
- スモークテスト: 既存FAILなし(1件のemit失敗は本変更と無関係)
|
||||
|
||||
### P2.4 (Pattern8 BoolPredicateScan Plan化 PoC) 🚧 IN PROGRESS (2025-12-26)
|
||||
|
||||
**背景**:
|
||||
- Pattern8 (BoolPredicateScan) は Phase 269 P1.2 で `static box` コンテキストを明示的にスキップする設計決定あり
|
||||
- 既存 fixture (`phase269_p0_pattern8_frag_min.hako`) は static box のため Pattern8 がマッチせず Pattern1 にフォールバック
|
||||
- PoC のためには Pattern8 が実際にマッチする**非 static box の fixture** が必要
|
||||
|
||||
**実装方針**:
|
||||
- **非 static box fixture**: `box StringUtils` に変更し、`Main.main()` から `new StringUtils()` でインスタンス生成
|
||||
- **Plan line 抽出**: `extract_pattern8_plan()` で parts 抽出(既存 pattern8 の構造を参考)
|
||||
- **Normalizer**: `normalize_pattern8_bool_predicate_scan()` で Scan 系の骨格を最小で再利用
|
||||
- **Router integration**: PLAN_EXTRACTORS テーブルに Pattern8 追加、`Ok(None)` なら legacy Pattern8 へフォールバック
|
||||
|
||||
**成果物** (予定):
|
||||
- `apps/tests/phase286_pattern8_plan_poc.hako` (新規: 非 static box fixture)
|
||||
- `tools/smokes/v2/profiles/integration/apps/phase286_pattern8_plan_poc_vm.sh` (新規: integration smoke)
|
||||
- `src/mir/builder/control_flow/plan/mod.rs` (変更: Pattern8BoolPredicateScanPlan + DomainPlan variant)
|
||||
- `src/mir/builder/control_flow/joinir/patterns/extractors/pattern8.rs` (新規: extract_pattern8_plan)
|
||||
- `src/mir/builder/control_flow/joinir/patterns/extractors/mod.rs` (変更: pattern8 モジュール追加)
|
||||
- `src/mir/builder/control_flow/plan/normalizer.rs` (変更: normalize_pattern8_bool_predicate_scan)
|
||||
- `src/mir/builder/control_flow/joinir/patterns/router.rs` (変更: PLAN_EXTRACTORS に Pattern8 追加)
|
||||
|
||||
**成功基準**:
|
||||
- Integration test: `phase286_pattern8_plan_poc_vm` PASS (exit 7)
|
||||
- Regression test: quick smoke 0 failed
|
||||
- Debug log: `route=plan strategy=extract pattern=Pattern8_BoolPredicateScan` 確認
|
||||
|
||||
## Acceptance(P0)
|
||||
|
||||
- 2本の lowering が “設計として” どこで 1 本に収束するかが明文化されている
|
||||
- 2本の lowering が "設計として" どこで 1 本に収束するかが明文化されている
|
||||
- Phase 284(Return)/ Phase 285(GC)と矛盾しない
|
||||
|
||||
@ -25,6 +25,7 @@ pub(crate) mod pattern2; // Phase 282 P4: Pattern2 extraction
|
||||
pub(crate) mod pattern3; // Phase 282 P5: Pattern3 extraction
|
||||
pub(crate) mod pattern4; // Phase 282 P6: Pattern4 extraction
|
||||
pub(crate) mod pattern5; // Phase 282 P7: Pattern5 extraction
|
||||
pub(crate) mod pattern8; // Phase 286 P2.4: Pattern8 Plan extraction
|
||||
pub(crate) mod pattern9; // Phase 286 P2.3: Pattern9 Plan extraction
|
||||
|
||||
// Phase 282 P9a: Common extraction helpers
|
||||
|
||||
@ -0,0 +1,450 @@
|
||||
//! Phase 286 P2.4: Pattern8 (BoolPredicateScan) Extraction
|
||||
//!
|
||||
//! Minimal subset extractor for Pattern8 Plan line.
|
||||
//!
|
||||
//! # Supported subset (PoC safety)
|
||||
//!
|
||||
//! - Loop condition: `i < s.length()`
|
||||
//! - Body: if statement with predicate check + loop increment
|
||||
//! - If condition: `not me.is_digit(s.substring(i, i + 1))`
|
||||
//! - Then branch: `return false`
|
||||
//! - Loop increment: `i = i + 1`
|
||||
//! - Post-loop: `return true` (enforced by caller)
|
||||
//! - Step literal: 1 (forward scan only)
|
||||
//!
|
||||
//! Returns Ok(None) for unsupported patterns → legacy fallback
|
||||
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator};
|
||||
use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern8BoolPredicateScanPlan};
|
||||
|
||||
/// Phase 286 P2.4: Minimal subset extractor for Pattern8 Plan line
|
||||
///
|
||||
/// # Detection Criteria
|
||||
///
|
||||
/// 1. **Condition**: `i < s.length()` (forward scan)
|
||||
/// 2. **Body**: if statement with predicate check
|
||||
/// - Condition: `not me.is_digit(s.substring(i, i + 1))`
|
||||
/// - Then branch: `return false`
|
||||
/// 3. **Body**: loop increment `i = i + 1`
|
||||
/// 4. **Step literal**: 1 (P0: forward scan only)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(Some(plan))`: Pattern8 match confirmed
|
||||
/// - `Ok(None)`: Not Pattern8 (構造不一致 or unsupported)
|
||||
/// - `Err(msg)`: Logic bug (malformed AST)
|
||||
pub(crate) fn extract_pattern8_plan(
|
||||
condition: &ASTNode,
|
||||
body: &[ASTNode],
|
||||
) -> Result<Option<DomainPlan>, String> {
|
||||
// Step 1: Validate loop condition: i < s.length()
|
||||
let (loop_var, haystack) = match validate_loop_condition_plan(condition) {
|
||||
Some((var, hay)) => (var, hay),
|
||||
None => return Ok(None), // Unsupported condition format
|
||||
};
|
||||
|
||||
// Step 2: Extract predicate check (if not predicate() { return false })
|
||||
let (predicate_receiver, predicate_method) = match extract_predicate_check(body, &loop_var, &haystack) {
|
||||
Some((receiver, method)) => (receiver, method),
|
||||
None => return Ok(None), // No predicate pattern found
|
||||
};
|
||||
|
||||
// Step 3: Extract loop increment (i = i + 1)
|
||||
let step_lit = match extract_loop_increment(body, &loop_var) {
|
||||
Some(lit) => lit,
|
||||
None => return Ok(None), // No increment found
|
||||
};
|
||||
|
||||
// P0: Step must be 1 (forward scan only)
|
||||
if step_lit != 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(DomainPlan::Pattern8BoolPredicateScan(
|
||||
Pattern8BoolPredicateScanPlan {
|
||||
loop_var,
|
||||
haystack,
|
||||
predicate_receiver,
|
||||
predicate_method,
|
||||
condition: condition.clone(),
|
||||
step_lit,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
/// Validate loop condition: supports `i < s.length()` only
|
||||
fn validate_loop_condition_plan(cond: &ASTNode) -> Option<(String, String)> {
|
||||
if let ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left,
|
||||
right,
|
||||
..
|
||||
} = cond
|
||||
{
|
||||
// Left must be a variable (loop_var)
|
||||
let loop_var = if let ASTNode::Variable { name, .. } = left.as_ref() {
|
||||
name.clone()
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Right must be s.length()
|
||||
let haystack = if let ASTNode::MethodCall {
|
||||
object,
|
||||
method,
|
||||
..
|
||||
} = right.as_ref()
|
||||
{
|
||||
if method == "length" {
|
||||
if let ASTNode::Variable { name, .. } = object.as_ref() {
|
||||
name.clone()
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((loop_var, haystack))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract predicate check from body
|
||||
///
|
||||
/// Looks for: `if not receiver.method(s.substring(i, i + 1)) { return false }`
|
||||
///
|
||||
/// Returns: Some((receiver, method)) or None
|
||||
fn extract_predicate_check(
|
||||
body: &[ASTNode],
|
||||
loop_var: &str,
|
||||
haystack: &str,
|
||||
) -> Option<(String, String)> {
|
||||
for stmt in body.iter() {
|
||||
if let ASTNode::If {
|
||||
condition: if_cond,
|
||||
then_body,
|
||||
..
|
||||
} = stmt
|
||||
{
|
||||
// Check if condition is: not receiver.method(...)
|
||||
if let ASTNode::UnaryOp {
|
||||
operator: UnaryOperator::Not,
|
||||
operand,
|
||||
..
|
||||
} = if_cond.as_ref()
|
||||
{
|
||||
// Operand must be MethodCall
|
||||
if let ASTNode::MethodCall {
|
||||
object,
|
||||
method,
|
||||
arguments,
|
||||
..
|
||||
} = operand.as_ref()
|
||||
{
|
||||
// Extract receiver (e.g., "me")
|
||||
let receiver = match object.as_ref() {
|
||||
ASTNode::Variable { name, .. } => name.clone(),
|
||||
ASTNode::Me { .. } => "me".to_string(),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// P0: Expect 1 argument: s.substring(i, i + 1)
|
||||
if arguments.len() != 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate argument is substring call
|
||||
if !validate_substring_call(&arguments[0], haystack, loop_var) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check then_body contains: return false
|
||||
if then_body.len() == 1 {
|
||||
if let ASTNode::Return { value, .. } = &then_body[0] {
|
||||
if let Some(ret_val) = value {
|
||||
if matches!(
|
||||
ret_val.as_ref(),
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Bool(false),
|
||||
..
|
||||
}
|
||||
) {
|
||||
return Some((receiver, method.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Validate substring call: s.substring(i, i + 1)
|
||||
fn validate_substring_call(arg: &ASTNode, haystack: &str, loop_var: &str) -> bool {
|
||||
if let ASTNode::MethodCall {
|
||||
object: substr_obj,
|
||||
method: substr_method,
|
||||
arguments: substr_args,
|
||||
..
|
||||
} = arg
|
||||
{
|
||||
if substr_method != "substring" {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Object must be haystack
|
||||
if let ASTNode::Variable { name, .. } = substr_obj.as_ref() {
|
||||
if name != haystack {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Args: (i, i + 1)
|
||||
if substr_args.len() != 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Arg 0: loop_var
|
||||
if !matches!(
|
||||
&substr_args[0],
|
||||
ASTNode::Variable { name, .. } if name == loop_var
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Arg 1: loop_var + 1
|
||||
if let ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left,
|
||||
right,
|
||||
..
|
||||
} = &substr_args[1]
|
||||
{
|
||||
// Left: loop_var
|
||||
if !matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == loop_var) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Right: Literal(1)
|
||||
if !matches!(
|
||||
right.as_ref(),
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
..
|
||||
}
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract loop increment from body
|
||||
///
|
||||
/// Looks for: `i = i + 1`
|
||||
///
|
||||
/// Returns: Some(step_lit) or None
|
||||
fn extract_loop_increment(body: &[ASTNode], loop_var: &str) -> Option<i64> {
|
||||
for stmt in body {
|
||||
if let ASTNode::Assignment { target, value, .. } = stmt {
|
||||
if let ASTNode::Variable {
|
||||
name: target_name, ..
|
||||
} = target.as_ref()
|
||||
{
|
||||
if target_name == loop_var {
|
||||
if let ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left,
|
||||
right,
|
||||
..
|
||||
} = value.as_ref()
|
||||
{
|
||||
if let ASTNode::Variable { name: left_name, .. } = left.as_ref() {
|
||||
if left_name == loop_var {
|
||||
if let ASTNode::Literal {
|
||||
value: LiteralValue::Integer(lit),
|
||||
..
|
||||
} = right.as_ref()
|
||||
{
|
||||
return Some(*lit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::Span;
|
||||
|
||||
fn make_condition(var: &str, haystack: &str) -> ASTNode {
|
||||
ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: haystack.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_predicate_if(receiver: &str, method: &str, haystack: &str, loop_var: &str) -> ASTNode {
|
||||
ASTNode::If {
|
||||
condition: Box::new(ASTNode::UnaryOp {
|
||||
operator: UnaryOperator::Not,
|
||||
operand: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(if receiver == "me" {
|
||||
ASTNode::Me {
|
||||
span: Span::unknown(),
|
||||
}
|
||||
} else {
|
||||
ASTNode::Variable {
|
||||
name: receiver.to_string(),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}),
|
||||
method: method.to_string(),
|
||||
arguments: vec![ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: haystack.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "substring".to_string(),
|
||||
arguments: vec![
|
||||
ASTNode::Variable {
|
||||
name: loop_var.to_string(),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: loop_var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
then_body: vec![ASTNode::Return {
|
||||
value: Some(Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Bool(false),
|
||||
span: Span::unknown(),
|
||||
})),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
else_body: None,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_loop_increment(var: &str, step: i64) -> ASTNode {
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: var.to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(step),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pattern8_success() {
|
||||
// loop(i < s.length()) { if not me.is_digit(s.substring(i, i + 1)) { return false } i = i + 1 }
|
||||
let condition = make_condition("i", "s");
|
||||
let body = vec![
|
||||
make_predicate_if("me", "is_digit", "s", "i"),
|
||||
make_loop_increment("i", 1),
|
||||
];
|
||||
|
||||
let result = extract_pattern8_plan(&condition, &body);
|
||||
assert!(result.is_ok());
|
||||
let plan = result.unwrap();
|
||||
assert!(plan.is_some());
|
||||
|
||||
if let Some(DomainPlan::Pattern8BoolPredicateScan(p)) = plan {
|
||||
assert_eq!(p.loop_var, "i");
|
||||
assert_eq!(p.haystack, "s");
|
||||
assert_eq!(p.predicate_receiver, "me");
|
||||
assert_eq!(p.predicate_method, "is_digit");
|
||||
assert_eq!(p.step_lit, 1);
|
||||
} else {
|
||||
panic!("Expected Pattern8BoolPredicateScan");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pattern8_wrong_step_returns_none() {
|
||||
// loop(i < s.length()) { ... i = i + 2 } <- wrong step
|
||||
let condition = make_condition("i", "s");
|
||||
let body = vec![
|
||||
make_predicate_if("me", "is_digit", "s", "i"),
|
||||
make_loop_increment("i", 2),
|
||||
];
|
||||
|
||||
let result = extract_pattern8_plan(&condition, &body);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none()); // Wrong step → None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pattern8_no_predicate_returns_none() {
|
||||
// loop(i < s.length()) { i = i + 1 } <- no predicate check
|
||||
let condition = make_condition("i", "s");
|
||||
let body = vec![make_loop_increment("i", 1)];
|
||||
|
||||
let result = extract_pattern8_plan(&condition, &body);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none()); // No predicate → None
|
||||
}
|
||||
}
|
||||
@ -185,6 +185,7 @@ enum PlanExtractorVariant {
|
||||
///
|
||||
/// Order is important: more specific patterns first
|
||||
/// - Pattern6/7: Need fn_body for capture analysis
|
||||
/// - Pattern8: Bool predicate scan (early-exit return, more specific)
|
||||
/// - Pattern4: Continue loop (more specific than Pattern1)
|
||||
/// - Pattern9: Accum const loop (2 carriers, more specific than Pattern1)
|
||||
/// - Pattern1: Simple while (fallback)
|
||||
@ -197,6 +198,10 @@ static PLAN_EXTRACTORS: &[PlanExtractorEntry] = &[
|
||||
name: "Pattern7_SplitScan (Phase 273)",
|
||||
extractor: PlanExtractorVariant::WithPostLoop(super::pattern7_split_scan::extract_split_scan_plan),
|
||||
},
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern8_BoolPredicateScan (Phase 286 P2.4)",
|
||||
extractor: PlanExtractorVariant::Simple(super::extractors::pattern8::extract_pattern8_plan),
|
||||
},
|
||||
PlanExtractorEntry {
|
||||
name: "Pattern4_Continue (Phase 286 P2)",
|
||||
extractor: PlanExtractorVariant::Simple(super::extractors::pattern4::extract_pattern4_plan),
|
||||
@ -238,17 +243,31 @@ fn try_plan_extractors(
|
||||
}
|
||||
};
|
||||
|
||||
// If extraction succeeded, lower and return
|
||||
// If extraction succeeded, try lowering
|
||||
if let Some(domain_plan) = plan_opt {
|
||||
let log_msg = format!("route=plan strategy=extract pattern={}", entry.name);
|
||||
trace::trace().pattern("route", &log_msg, true);
|
||||
return lower_via_plan(builder, domain_plan, ctx);
|
||||
}
|
||||
|
||||
// Extraction returned None - try next extractor
|
||||
if ctx.debug {
|
||||
let debug_msg = format!("{} extraction returned None, trying next pattern", entry.name);
|
||||
trace::trace().debug("route", &debug_msg);
|
||||
// Phase 286 P2.4: Catch normalization errors for PoC fallback
|
||||
// Pattern8 PoC returns error from normalizer to trigger legacy fallback
|
||||
match lower_via_plan(builder, domain_plan, ctx) {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) if e.contains("[normalizer/pattern8]") => {
|
||||
// Pattern8 PoC: normalization not yet supported, continue to legacy Pattern8
|
||||
if ctx.debug {
|
||||
trace::trace().debug("route", &format!("Pattern8 Plan PoC: normalization failed ({}), continuing to legacy Pattern8", e));
|
||||
}
|
||||
// Continue to next extractor (or legacy patterns)
|
||||
continue; // Explicit continue to skip "extraction returned None" log
|
||||
}
|
||||
Err(e) => return Err(e), // Real errors propagate
|
||||
}
|
||||
} else {
|
||||
// Extraction returned None - try next extractor
|
||||
if ctx.debug {
|
||||
let debug_msg = format!("{} extraction returned None, trying next pattern", entry.name);
|
||||
trace::trace().debug("route", &debug_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -51,6 +51,8 @@ pub(in crate::mir::builder) enum DomainPlan {
|
||||
Pattern1SimpleWhile(Pattern1SimpleWhilePlan),
|
||||
/// Pattern9: Accumulator Const Loop (Phase 286 P2.3)
|
||||
Pattern9AccumConstLoop(Pattern9AccumConstLoopPlan),
|
||||
/// Pattern8: Boolean Predicate Scan (Phase 286 P2.4)
|
||||
Pattern8BoolPredicateScan(Pattern8BoolPredicateScanPlan),
|
||||
}
|
||||
|
||||
/// Phase 273 P0: Scan direction for forward/reverse scan
|
||||
@ -154,6 +156,26 @@ pub(in crate::mir::builder) struct Pattern9AccumConstLoopPlan {
|
||||
pub loop_increment: ASTNode,
|
||||
}
|
||||
|
||||
/// Phase 286 P2.4: Extracted structure for Pattern8 (BoolPredicateScan)
|
||||
///
|
||||
/// This structure contains all the information needed to lower a boolean predicate scan loop.
|
||||
/// Pattern8 scans a string with a predicate check (e.g., is_digit) and returns false on first failure.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(in crate::mir::builder) struct Pattern8BoolPredicateScanPlan {
|
||||
/// Loop variable name (e.g., "i")
|
||||
pub loop_var: String,
|
||||
/// Haystack variable name (e.g., "s")
|
||||
pub haystack: String,
|
||||
/// Predicate receiver name (e.g., "me")
|
||||
pub predicate_receiver: String,
|
||||
/// Predicate method name (e.g., "is_digit")
|
||||
pub predicate_method: String,
|
||||
/// Loop condition AST (e.g., `i < s.length()`)
|
||||
pub condition: ASTNode,
|
||||
/// Loop increment literal (P0: must be 1)
|
||||
pub step_lit: i64,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CorePlan (固定語彙 - 構造ノードのみ)
|
||||
// ============================================================================
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
use super::{
|
||||
CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan,
|
||||
ScanWithInitPlan, SplitScanPlan, Pattern1SimpleWhilePlan, Pattern9AccumConstLoopPlan,
|
||||
Pattern8BoolPredicateScanPlan,
|
||||
};
|
||||
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
|
||||
use crate::mir::builder::MirBuilder;
|
||||
@ -42,6 +43,7 @@ impl PlanNormalizer {
|
||||
DomainPlan::Pattern4Continue(parts) => Self::normalize_pattern4_continue(builder, parts, ctx),
|
||||
DomainPlan::Pattern1SimpleWhile(parts) => Self::normalize_pattern1_simple_while(builder, parts, ctx),
|
||||
DomainPlan::Pattern9AccumConstLoop(parts) => Self::normalize_pattern9_accum_const_loop(builder, parts, ctx),
|
||||
DomainPlan::Pattern8BoolPredicateScan(parts) => Self::normalize_pattern8_bool_predicate_scan(builder, parts, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1685,4 +1687,20 @@ impl PlanNormalizer {
|
||||
|
||||
Ok(CorePlan::Loop(loop_plan))
|
||||
}
|
||||
|
||||
/// Pattern8BoolPredicateScan → CorePlan 変換 (Phase 286 P2.4 PoC)
|
||||
///
|
||||
/// Phase 286 P2.4 PoC scope: Pattern8 requires early-exit handling (return false).
|
||||
/// For PoC, we return an error to trigger legacy fallback.
|
||||
/// Full implementation deferred to Phase 286 P2.4.1+ (requires CoreExitPlan::Return support).
|
||||
fn normalize_pattern8_bool_predicate_scan(
|
||||
_builder: &mut MirBuilder,
|
||||
_parts: Pattern8BoolPredicateScanPlan,
|
||||
_ctx: &LoopPatternContext,
|
||||
) -> Result<CorePlan, String> {
|
||||
// Phase 286 P2.4 PoC: Pattern8 requires return false support
|
||||
// CoreExitPlan::Return exists but not integrated into lowerer yet
|
||||
// Return error to trigger legacy Pattern8 fallback
|
||||
Err("[normalizer/pattern8] P2.4 PoC: early-exit return not yet supported in Plan line (use legacy Pattern8)".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 286 P2.4: Pattern8 BoolPredicateScan Plan化 PoC (non-static box)
|
||||
# Expected: exit 7 (is_integer("123") == true)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../../../.." && pwd)"
|
||||
HAKORUNE="$REPO_ROOT/target/release/hakorune"
|
||||
|
||||
# Test: Pattern8 Plan line routing
|
||||
# Note: set -e disabled because hakorune exits with 7 (success return value from .hako)
|
||||
set +e
|
||||
"$HAKORUNE" --backend vm "$REPO_ROOT/apps/tests/phase286_pattern8_plan_poc.hako" 2>/dev/null
|
||||
exit_code=$?
|
||||
set -e
|
||||
|
||||
if [ $exit_code -eq 7 ]; then
|
||||
exit 0
|
||||
else
|
||||
echo "ERROR: Expected exit 7, got $exit_code" >&2
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user