phase29ai(p11): planner-first pattern2 break subset

This commit is contained in:
2025-12-29 09:55:00 +09:00
parent 4d26133d6a
commit 0cf6749b4a
10 changed files with 243 additions and 6 deletions

View File

@ -0,0 +1,20 @@
// Phase 29ai P11: Pattern2 break plan subset OK minimal
// Expect: exit 15
static box Main {
main() {
local sum = 0
local i = 0
loop(i < 10) {
if i == 5 {
sum = sum + 10
break
}
sum = sum + 1
i = i + 1
}
return sum
}
}

View File

@ -2,7 +2,12 @@
## Current Focus: Phase 29aiPlan/Frag single-planner
Next: `docs/development/current/main/phases/phase-29ai/P11-PLANNER-PATTERN2-BREAK-SUBSET-WIRE-INSTRUCTIONS.md`
Next: Phase 29ai P12Pattern2 LoopBodyLocal promotion の Facts 仕様化)
**2025-12-29: Phase 29ai P11 完了**
- 目的: Pattern2 break subset を Facts→Planner に吸収し、single_planner で planner-first を開始(仕様不変)
- 実装: `src/mir/builder/control_flow/plan/facts/pattern2_break_facts.rs` / `src/mir/builder/control_flow/plan/planner/build.rs` / `src/mir/builder/control_flow/plan/single_planner/rules.rs`
- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` / `./tools/smokes/v2/run.sh --profile integration --filter "phase29ai_pattern2_break_plan_subset_ok_min"` PASS
**2025-12-29: Phase 29ai P10 完了**
- 目的: Pattern2 extractor を plan 層へ移設して依存方向を固定(仕様不変)

View File

@ -19,7 +19,7 @@ Related:
- **Phase 29aicandidate: Plan/Frag single-plannerFacts SSOT**
- 入口: `docs/development/current/main/phases/phase-29ai/README.md`
- Next: P11Planner: Pattern2 break subset
- Next: P12Pattern2 LoopBodyLocal promotion の Facts 仕様化
- **Phase 29ae P1✅ COMPLETE: JoinIR Regression Pack (SSOT固定)**
- 入口: `docs/development/current/main/phases/phase-29ae/README.md`

View File

@ -67,6 +67,8 @@ Goal: pattern 名による分岐を外部APIから消し、Facts事実
- 指示書: `docs/development/current/main/phases/phase-29ai/P11-PLANNER-PATTERN2-BREAK-SUBSET-WIRE-INSTRUCTIONS.md`
- ねらい: Pattern2breakの PoC subset を Facts→Planner に吸収し、single_planner で planner-first を開始(仕様不変で段階吸収)
- 完了: Pattern2BreakFacts/Planner 候補/Pattern2 planner-first を接続、subset fixture + smoke 追加
- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` / `./tools/smokes/v2/run.sh --profile integration --filter "phase29ai_pattern2_break_plan_subset_ok_min"`
## Verification (SSOT)

View File

@ -13,6 +13,7 @@ use super::scan_shapes::{ConditionShape, StepShape};
use crate::mir::builder::control_flow::plan::planner::Freeze;
use crate::ast::{BinaryOperator, LiteralValue};
use super::scan_shapes::LengthMethod;
use super::pattern2_break_facts::{Pattern2BreakFacts, try_extract_pattern2_break_facts};
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct LoopFacts {
@ -20,6 +21,7 @@ pub(in crate::mir::builder) struct LoopFacts {
pub step_shape: StepShape,
pub scan_with_init: Option<ScanWithInitFacts>,
pub split_scan: Option<SplitScanFacts>,
pub pattern2_break: Option<Pattern2BreakFacts>,
}
#[derive(Debug, Clone)]
@ -51,8 +53,9 @@ pub(in crate::mir::builder) fn try_build_loop_facts(
let step_shape = try_extract_step_shape(body)?.unwrap_or(StepShape::Unknown);
let scan_with_init = try_extract_scan_with_init_facts(body, &condition_shape, &step_shape)?;
let split_scan = try_extract_split_scan_facts(condition, body)?;
let pattern2_break = try_extract_pattern2_break_facts(condition, body)?;
if scan_with_init.is_none() && split_scan.is_none() {
if scan_with_init.is_none() && split_scan.is_none() && pattern2_break.is_none() {
return Ok(None);
}
@ -61,6 +64,7 @@ pub(in crate::mir::builder) fn try_build_loop_facts(
step_shape,
scan_with_init,
split_scan,
pattern2_break,
}))
}

View File

@ -7,6 +7,10 @@
#![allow(dead_code)]
pub(in crate::mir::builder) mod loop_facts;
pub(in crate::mir::builder) mod pattern2_break_facts;
pub(in crate::mir::builder) mod scan_shapes;
pub(in crate::mir::builder) use loop_facts::{try_build_loop_facts, LoopFacts};
pub(in crate::mir::builder) use loop_facts::{
try_build_loop_facts, LoopFacts, ScanWithInitFacts, SplitScanFacts,
};
pub(in crate::mir::builder) use pattern2_break_facts::Pattern2BreakFacts;

View File

@ -0,0 +1,145 @@
//! Phase 29ai P11: Pattern2BreakFacts (Facts SSOT)
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
use crate::mir::builder::control_flow::plan::planner::Freeze;
use crate::mir::builder::control_flow::plan::extractors::common_helpers::{
count_control_flow, has_continue_statement as common_has_continue,
has_return_statement as common_has_return, ControlFlowDetector, extract_loop_increment_plan,
};
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct Pattern2BreakFacts {
pub loop_var: String,
pub carrier_var: String,
pub loop_condition: ASTNode,
pub break_condition: ASTNode,
pub carrier_update_in_break: Option<ASTNode>,
pub carrier_update_in_body: ASTNode,
pub loop_increment: ASTNode,
}
pub(in crate::mir::builder) fn try_extract_pattern2_break_facts(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<Pattern2BreakFacts>, Freeze> {
let Some(loop_var) = extract_loop_var_for_plan_subset(condition) else {
return Ok(None);
};
let break_count = count_control_flow(body, ControlFlowDetector::default()).break_count;
if break_count != 1 {
return Ok(None);
}
if has_continue_statement(body) {
return Ok(None);
}
if has_return_statement(body) {
return Ok(None);
}
if body.len() != 3 {
return Ok(None);
}
let (break_condition, carrier_update_in_break) =
match extract_break_if_parts(&body[0]) {
Some(parts) => parts,
None => return Ok(None),
};
let (carrier_var, carrier_update_in_body) = match &body[1] {
ASTNode::Assignment { target, value, .. } => {
let carrier_name = match target.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return Ok(None),
};
(carrier_name, value.as_ref().clone())
}
_ => return Ok(None),
};
let loop_increment = match extract_loop_increment_plan(body, &loop_var) {
Ok(Some(inc)) => inc,
_ => return Ok(None),
};
Ok(Some(Pattern2BreakFacts {
loop_var,
carrier_var,
loop_condition: condition.clone(),
break_condition,
carrier_update_in_break,
carrier_update_in_body,
loop_increment,
}))
}
fn extract_loop_var_for_plan_subset(condition: &ASTNode) -> Option<String> {
let ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left,
right,
..
} = condition
else {
return None;
};
let ASTNode::Variable { name, .. } = left.as_ref() else {
return None;
};
if !matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(_),
..
}
) {
return None;
}
Some(name.clone())
}
fn extract_break_if_parts(stmt: &ASTNode) -> Option<(ASTNode, Option<ASTNode>)> {
let ASTNode::If {
condition,
then_body,
else_body,
..
} = stmt
else {
return None;
};
if else_body.is_some() {
return None;
}
let has_break_at_end = then_body
.last()
.map(|n| matches!(n, ASTNode::Break { .. }))
.unwrap_or(false);
if !has_break_at_end {
return None;
}
let carrier_update_in_break = if then_body.len() == 1 {
None
} else if then_body.len() == 2 {
match &then_body[0] {
ASTNode::Assignment { value, .. } => Some(value.as_ref().clone()),
_ => return None,
}
} else {
return None;
};
Some((condition.as_ref().clone(), carrier_update_in_break))
}
fn has_continue_statement(body: &[ASTNode]) -> bool {
common_has_continue(body)
}
fn has_return_statement(body: &[ASTNode]) -> bool {
common_has_return(body)
}

View File

@ -10,7 +10,7 @@ use crate::mir::builder::control_flow::plan::normalize::{canonicalize_loop_facts
use super::candidates::{CandidateSet, PlanCandidate};
use super::Freeze;
use crate::mir::builder::control_flow::plan::{
DomainPlan, ScanDirection, ScanWithInitPlan, SplitScanPlan,
DomainPlan, Pattern2BreakPlan, ScanDirection, ScanWithInitPlan, SplitScanPlan,
};
/// Phase 29ai P0: External-ish SSOT entrypoint (skeleton)
@ -70,6 +70,21 @@ pub(in crate::mir::builder) fn build_plan_from_facts(
});
}
if let Some(pattern2) = &facts.facts.pattern2_break {
candidates.push(PlanCandidate {
plan: DomainPlan::Pattern2Break(Pattern2BreakPlan {
loop_var: pattern2.loop_var.clone(),
carrier_var: pattern2.carrier_var.clone(),
loop_condition: pattern2.loop_condition.clone(),
break_condition: pattern2.break_condition.clone(),
carrier_update_in_break: pattern2.carrier_update_in_break.clone(),
carrier_update_in_body: pattern2.carrier_update_in_body.clone(),
loop_increment: pattern2.loop_increment.clone(),
}),
rule: "loop/pattern2_break",
});
}
candidates.finalize()
}
@ -97,6 +112,7 @@ mod tests {
i_var: "i".to_string(),
start_var: "start".to_string(),
}),
pattern2_break: None,
};
let canonical = canonicalize_loop_facts(facts);
let plan = build_plan_from_facts(canonical).expect("Ok");
@ -120,6 +136,7 @@ mod tests {
step_shape: StepShape::Unknown,
scan_with_init: None,
split_scan: None,
pattern2_break: None,
};
let canonical = canonicalize_loop_facts(facts);
let plan = build_plan_from_facts(canonical).expect("Ok");
@ -138,6 +155,7 @@ mod tests {
step_lit: 1,
}),
split_scan: None,
pattern2_break: None,
};
let canonical = canonicalize_loop_facts(facts);
let plan = build_plan_from_facts(canonical).expect("Ok");

View File

@ -46,7 +46,7 @@ pub(super) fn try_build_domain_plan(ctx: &LoopPatternContext) -> Result<Option<D
},
RuleEntry {
name: "Pattern2_Break (Phase 286 P3.1)",
kind: RuleKind::Simple(legacy_rules::pattern2::extract),
kind: RuleKind::Pattern2,
},
RuleEntry {
name: "Pattern1_SimpleWhile (Phase 286 P2.1)",
@ -81,6 +81,14 @@ pub(super) fn try_build_domain_plan(ctx: &LoopPatternContext) -> Result<Option<D
None => (legacy_rules::pattern7::extract(ctx)?, true),
}
}
RuleKind::Pattern2 => {
let from_planner = planner::build_plan(ctx.condition, ctx.body)
.map_err(|freeze| freeze.to_string())?;
match from_planner {
Some(DomainPlan::Pattern2Break(_)) => (from_planner, false),
_ => (legacy_rules::pattern2::extract(ctx)?, true),
}
}
};
if let Some(domain_plan) = plan_opt {
@ -123,4 +131,5 @@ enum RuleKind {
Simple(fn(&LoopPatternContext) -> Result<Option<DomainPlan>, String>),
Pattern6,
Pattern7,
Pattern2,
}

View File

@ -0,0 +1,30 @@
#!/bin/bash
# Phase 29ai P11: Pattern2 break plan subset OK minimal (VM backend)
# Tests: exit -> 15
source "$(dirname "$0")/../../../lib/test_runner.sh"
export SMOKES_USE_PYVM=0
require_env || exit 2
INPUT="$NYASH_ROOT/apps/tests/phase29ai_pattern2_break_plan_subset_ok_min.hako"
RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10}
set +e
OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env NYASH_DISABLE_PLUGINS=1 HAKO_JOINIR_STRICT=1 "$NYASH_BIN" "$INPUT" 2>&1)
EXIT_CODE=$?
set -e
if [ "$EXIT_CODE" -eq 124 ]; then
test_fail "phase29ai_pattern2_break_plan_subset_ok_min_vm: hakorune timed out (>${RUN_TIMEOUT_SECS}s)"
exit 1
fi
if [ "$EXIT_CODE" -eq 15 ]; then
test_pass "phase29ai_pattern2_break_plan_subset_ok_min_vm: RC=15 (expected)"
exit 0
fi
echo "[FAIL] Expected exit 15, got $EXIT_CODE"
echo "$OUTPUT" | tail -n 40 || true
test_fail "phase29ai_pattern2_break_plan_subset_ok_min_vm: Unexpected RC"
exit 1