phase29ao(p33): planner-derive pattern2 loopbodylocal smokes

This commit is contained in:
2025-12-30 15:58:19 +09:00
parent 363549b152
commit 59a29a86d3
12 changed files with 407 additions and 25 deletions

View File

@ -3,8 +3,7 @@
## Current Focus
- Phase: `docs/development/current/main/phases/phase-29ao/README.md`
- Next: Phase 29ao P33Pattern2 LoopBodyLocal planner-derive + tag gate
- 指示書: `docs/development/current/main/phases/phase-29ao/P33-PLANNER-DERIVE-PATTERN2-LOOPBODYLOCAL-SMOKES-INSTRUCTIONS.md`
- Next: Phase 29ao P34TBD
## Gate (SSOT)

View File

@ -5,8 +5,7 @@ Scope: 「次にやる候補」を短く列挙するメモ。入口は `docs/dev
## Active
- CorePlan migration: `docs/development/current/main/phases/phase-29ao/README.md`Next: P33
- 指示書: `docs/development/current/main/phases/phase-29ao/P33-PLANNER-DERIVE-PATTERN2-LOOPBODYLOCAL-SMOKES-INSTRUCTIONS.md`
- CorePlan migration: `docs/development/current/main/phases/phase-29ao/README.md`Next: P34 TBD
## Near-Term Candidates

View File

@ -34,8 +34,8 @@ Related:
## 1.1 Current (active)
- Active phase: `docs/development/current/main/phases/phase-29ao/README.md`
- Next step: `docs/development/current/main/phases/phase-29ao/P33-PLANNER-DERIVE-PATTERN2-LOOPBODYLOCAL-SMOKES-INSTRUCTIONS.md`
- After P33: TBD
- Next step: Phase 29ao P34 (TBD)
- After P34: TBD
## 2. すでに固めた SSOT再発防止の土台

View File

@ -2,12 +2,17 @@
## Current Focus: Phase 29aoCorePlan composition
Next: Phase 29ao P33TBD
Next: Phase 29ao P34TBD
指示書: TBD
運用ルール: integration filter で phase143_* は回さないJoinIR 回帰は phase29ae pack のみ)
運用ルール: phase286_pattern9_* は legacy pack (SKIP) を使う
移行道筋 SSOT: `docs/development/current/main/design/coreplan-migration-roadmap-ssot.md`
**2025-12-30: Phase 29ao P33 完了**
- 目的: Pattern2 LoopBodyLocal を planner 由来 Pattern2Break に引き上げ、strict/dev の shadow adopt タグを回帰で固定(仕様不変)
- 変更: `src/mir/builder/control_flow/plan/facts/pattern2_break_facts.rs` / `src/mir/builder/control_flow/plan/normalizer/pattern2_break.rs` / `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_loopbodylocal_min_vm.sh` / `tools/smokes/v2/profiles/integration/apps/phase29ab_pattern2_loopbodylocal_seg_min_vm.sh` / `docs/development/current/main/phases/phase-29ao/README.md` / `docs/development/current/main/10-Now.md` / `docs/development/current/main/30-Backlog.md` / `docs/development/current/main/design/coreplan-migration-roadmap-ssot.md`
- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh`
**2025-12-30: Phase 29ao P32 完了**
- 目的: Pattern2 real-world を planner subset に引き上げ、strict/dev で Facts→CorePlan shadow adopt を踏ませる(仕様不変)
- 変更: `src/mir/builder/control_flow/plan/facts/pattern2_break_facts.rs` / `tools/smokes/v2/profiles/integration/apps/phase263_pattern2_seg_realworld_min_vm.sh` / `docs/development/current/main/phases/phase-29ao/README.md` / `docs/development/current/main/10-Now.md` / `docs/development/current/main/30-Backlog.md` / `CURRENT_TASK.md` / `docs/development/current/main/design/coreplan-migration-roadmap-ssot.md`

View File

@ -15,7 +15,7 @@ Related:
- **Phase 29aoactive: CorePlan composition from Skeleton/Feature**
- 入口: `docs/development/current/main/phases/phase-29ao/README.md`
- 状況: P0P32 ✅ 完了 / Next: P33TBD
- 状況: P0P33 ✅ 完了 / Next: P34TBD
- Next 指示書: TBD
- **Phase 29af✅ COMPLETE: Boundary hygiene / regression entrypoint / carrier layout SSOT**

View File

@ -18,10 +18,13 @@ Scope: Repo root の旧リンク互換。現行の入口は `docs/development/cu
**CorePlan migration 道筋 SSOT**
`docs/development/current/main/design/coreplan-migration-roadmap-ssot.md` が移行タスクの Done 判定の入口。
**Next implementation (Phase 29ao P33)**
**Next implementation (Phase 29ao P34)**
- 目的: TBD
- 指示書: TBD
- After P33: TBD
- After P34: TBD
**2025-12-30: Phase 29ao P33 COMPLETE (Pattern2 LoopBodyLocal planner-derive + tag gate)**
Pattern2 LoopBodyLocal を planner 由来 Pattern2Break に引き上げ、strict/dev の shadow adopt タグを回帰で固定した(仕様不変)。
**2025-12-30: Phase 29ao P32 COMPLETE (Pattern2 real-world strict/dev shadow adopt)**
Pattern2 real-world を planner subset に引き上げ、strict/dev で Facts→CorePlan shadow adopt を踏ませた(仕様不変)。

View File

@ -186,8 +186,12 @@ GateSSOT:
- 指示書: `docs/development/current/main/phases/phase-29ao/P32-STRICT-ADOPT-PATTERN2-REALWORLD-FROM-FACTS-INSTRUCTIONS.md`
- ねらい: `phase263_pattern2_*` が strict/dev で Facts→CorePlan shadow adopt を踏むことを “タグ必須” で固定し、CorePlan 完全移行の回帰穴を塞ぐ(仕様不変)
## P33: Pattern2 LoopBodyLocal planner-derive + tag gate ✅
- 指示書: `docs/development/current/main/phases/phase-29ao/P33-PLANNER-DERIVE-PATTERN2-LOOPBODYLOCAL-SMOKES-INSTRUCTIONS.md`
- ねらい: `phase29ab_pattern2_loopbodylocal_{min,seg_min}` を planner 由来 Pattern2Break に引き上げ、shadow adopt タグを strict/dev 回帰で必須化(仕様不変)
## Nextplanned
- Next: P33Pattern2 LoopBodyLocal planner-derive + tag gate
- 指示書: `docs/development/current/main/phases/phase-29ao/P33-PLANNER-DERIVE-PATTERN2-LOOPBODYLOCAL-SMOKES-INSTRUCTIONS.md`
- After P33: TBD
- Next: P34TBD
- After P34: TBD

View File

@ -7,6 +7,9 @@ use crate::mir::builder::control_flow::plan::extractors::common_helpers::{
has_return_statement as common_has_return, ControlFlowDetector, extract_loop_increment_plan,
is_true_literal,
};
use crate::mir::builder::control_flow::plan::facts::pattern2_loopbodylocal_facts::{
try_extract_pattern2_loopbodylocal_facts, LoopBodyLocalShape,
};
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct Pattern2BreakFacts {
@ -27,6 +30,10 @@ pub(in crate::mir::builder) fn try_extract_pattern2_break_facts(
return Ok(Some(realworld));
}
if let Some(loopbodylocal) = try_extract_pattern2_break_loopbodylocal_subset(condition, body)? {
return Ok(Some(loopbodylocal));
}
let Some(loop_var) = extract_loop_var_for_plan_subset(condition) else {
return Ok(None);
};
@ -152,6 +159,207 @@ fn try_extract_pattern2_break_realworld_subset(
})
}
fn try_extract_pattern2_break_loopbodylocal_subset(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<Pattern2BreakFacts>, Freeze> {
let Some(loop_var) = extract_loop_var_for_len_condition(condition) else {
return Ok(None);
};
let counts = count_control_flow(body, ControlFlowDetector::default());
if counts.break_count != 1 || counts.continue_count > 0 || counts.return_count > 0 {
return Ok(None);
}
if body.len() != 3 && body.len() != 4 {
return Ok(None);
}
let loopbodylocal = match try_extract_pattern2_loopbodylocal_facts(condition, body)? {
Some(facts) => facts,
None => return Ok(None),
};
if loopbodylocal.loop_var != loop_var {
return Ok(None);
}
let (break_idx, _, carrier_update_in_break) = match find_break_if_parts(body) {
Some(parts) => parts,
None => return Ok(None),
};
if carrier_update_in_break.is_some() {
return Ok(None);
}
if has_assignment_after(body, break_idx, &loopbodylocal.loopbodylocal_var) {
return Ok(None);
}
let break_condition = match loopbodylocal.shape {
LoopBodyLocalShape::TrimSeg { s_var, i_var } => {
if i_var != loop_var {
return Ok(None);
}
let seg_expr = substring_call(
&s_var,
var(&loop_var),
add(var(&loop_var), lit_int(1)),
);
let is_space = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(seg_expr.clone()),
right: Box::new(lit_str(" ")),
span: Span::unknown(),
};
let is_tab = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(seg_expr),
right: Box::new(lit_str("\t")),
span: Span::unknown(),
};
ASTNode::BinaryOp {
operator: BinaryOperator::Or,
left: Box::new(is_space),
right: Box::new(is_tab),
span: Span::unknown(),
}
}
LoopBodyLocalShape::DigitPos { digits_var, ch_var } => {
let ch_expr = match find_local_init_expr(body, &ch_var) {
Some(expr) => expr,
None => return Ok(None),
};
let index_expr = index_of_call_expr(&digits_var, ch_expr);
ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(index_expr),
right: Box::new(lit_int(0)),
span: Span::unknown(),
}
}
};
let loop_increment = match extract_loop_increment_at_end(body, &loop_var) {
Some(inc) => inc,
None => return Ok(None),
};
Ok(Some(Pattern2BreakFacts {
loop_var: loop_var.clone(),
carrier_var: loop_var,
loop_condition: condition.clone(),
break_condition,
carrier_update_in_break: None,
carrier_update_in_body: loop_increment.clone(),
loop_increment,
}))
}
fn extract_loop_var_for_len_condition(condition: &ASTNode) -> Option<String> {
let ASTNode::BinaryOp {
operator: BinaryOperator::Less | BinaryOperator::LessEqual,
left,
right,
..
} = condition
else {
return None;
};
let ASTNode::Variable { name, .. } = left.as_ref() else {
return None;
};
if !matches!(
right.as_ref(),
ASTNode::MethodCall { object, method, arguments, .. }
if method == "length"
&& arguments.is_empty()
&& matches!(object.as_ref(), ASTNode::Variable { .. })
) {
return None;
}
Some(name.clone())
}
fn find_break_if_parts(body: &[ASTNode]) -> Option<(usize, ASTNode, Option<ASTNode>)> {
for (idx, stmt) in body.iter().enumerate() {
if let Some(parts) = extract_break_if_parts(stmt) {
return Some((idx, parts.0, parts.1));
}
}
None
}
fn has_assignment_after(body: &[ASTNode], start_idx: usize, var_name: &str) -> bool {
for stmt in body.iter().skip(start_idx + 1) {
let ASTNode::Assignment { target, .. } = stmt else {
continue;
};
if matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == var_name) {
return true;
}
}
false
}
fn find_local_init_expr(body: &[ASTNode], name: &str) -> Option<ASTNode> {
for stmt in body {
let ASTNode::Local {
variables,
initial_values,
..
} = stmt
else {
continue;
};
if variables.len() != 1 || initial_values.len() != 1 {
continue;
}
if variables[0] != name {
continue;
}
let Some(expr) = initial_values[0].as_ref() else {
return None;
};
return Some((*expr.clone()).clone());
}
None
}
fn extract_loop_increment_at_end(body: &[ASTNode], loop_var: &str) -> Option<ASTNode> {
let last = body.last()?;
let ASTNode::Assignment { target, value, .. } = last else {
return None;
};
let ASTNode::Variable { name, .. } = target.as_ref() else {
return None;
};
if name != loop_var {
return None;
}
let ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} = value.as_ref()
else {
return None;
};
if !matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == loop_var) {
return None;
}
if !matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(_),
..
}
) {
return None;
}
Some(value.as_ref().clone())
}
fn extract_loop_var_for_plan_subset(condition: &ASTNode) -> Option<String> {
let ASTNode::BinaryOp {
operator: BinaryOperator::Less,
@ -505,6 +713,15 @@ fn var(name: &str) -> ASTNode {
}
}
fn add(left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(left),
right: Box::new(right),
span: Span::unknown(),
}
}
fn lit_int(value: i64) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Integer(value),
@ -537,6 +754,15 @@ fn index_of_call(haystack: &str, sep: &str, loop_var: &str) -> ASTNode {
}
}
fn index_of_call_expr(haystack: &str, needle: ASTNode) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(var(haystack)),
method: "indexOf".to_string(),
arguments: vec![needle],
span: Span::unknown(),
}
}
fn substring_call(haystack: &str, start: ASTNode, end: ASTNode) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(var(haystack)),
@ -700,4 +926,76 @@ mod tests {
other => panic!("unexpected loop_increment: {:?}", other),
}
}
#[test]
fn extract_pattern2_break_loopbodylocal_trim_seg_subset() {
let condition = binop(
BinaryOperator::Less,
v("i"),
method_call("s", "length", vec![]),
);
let body = vec![
local(
"seg",
method_call(
"s",
"substring",
vec![v("i"), binop(BinaryOperator::Add, v("i"), lit_int(1))],
),
),
ASTNode::If {
condition: Box::new(binop(
BinaryOperator::Or,
binop(BinaryOperator::Equal, v("seg"), lit_str(" ")),
binop(BinaryOperator::Equal, v("seg"), lit_str("\t")),
)),
then_body: vec![ASTNode::Break { span: Span::unknown() }],
else_body: None,
span: Span::unknown(),
},
assign("i", binop(BinaryOperator::Add, v("i"), lit_int(1))),
];
let facts = try_extract_pattern2_break_facts(&condition, &body)
.expect("Ok")
.expect("Some facts");
assert_eq!(facts.loop_var, "i");
assert_eq!(facts.carrier_var, "i");
}
#[test]
fn extract_pattern2_break_loopbodylocal_digit_pos_subset() {
let condition = binop(
BinaryOperator::Less,
v("p"),
method_call("s", "length", vec![]),
);
let body = vec![
local(
"ch",
method_call(
"s",
"substring",
vec![v("p"), binop(BinaryOperator::Add, v("p"), lit_int(1))],
),
),
local(
"digit_pos",
method_call("digits", "indexOf", vec![v("ch")]),
),
ASTNode::If {
condition: Box::new(binop(BinaryOperator::Less, v("digit_pos"), lit_int(0))),
then_body: vec![ASTNode::Break { span: Span::unknown() }],
else_body: None,
span: Span::unknown(),
},
assign("p", binop(BinaryOperator::Add, v("p"), lit_int(1))),
];
let facts = try_extract_pattern2_break_facts(&condition, &body)
.expect("Ok")
.expect("Some facts");
assert_eq!(facts.loop_var, "p");
assert_eq!(facts.carrier_var, "p");
}
}

View File

@ -257,6 +257,18 @@ impl super::PlanNormalizer {
Ok((result_id, arg_effects))
}
ASTNode::BinaryOp { .. } => {
let (lhs, op, rhs, mut consts) =
Self::lower_binop_ast(ast, builder, phi_bindings)?;
let result_id = builder.alloc_typed(MirType::Integer);
consts.push(CoreEffectPlan::BinOp {
dst: result_id,
lhs,
op,
rhs,
});
Ok((result_id, consts))
}
_ => Err(format!("[normalizer] Unsupported value AST: {:?}", ast)),
}
}

View File

@ -7,7 +7,7 @@ use crate::mir::builder::control_flow::edgecfg::api::{
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::builder::MirBuilder;
use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout;
use crate::mir::{BinaryOp, ConstValue, MirType};
use crate::mir::{BinaryOp, ConstValue, MirType, ValueId};
use std::collections::BTreeMap;
impl super::PlanNormalizer {
@ -115,9 +115,6 @@ impl super::PlanNormalizer {
let (loop_cond_lhs, loop_cond_op, loop_cond_rhs, loop_cond_consts) =
Self::lower_compare_ast(&parts.loop_condition, builder, &phi_bindings)?;
let (break_cond_lhs, break_cond_op, break_cond_rhs, break_cond_consts) =
Self::lower_compare_ast(&parts.break_condition, builder, &phi_bindings)?;
let break_then_effects = if let Some(ref break_update_ast) = parts.carrier_update_in_break {
let (lhs, op, rhs, consts) =
Self::lower_binop_ast(break_update_ast, builder, &phi_bindings)?;
@ -162,15 +159,15 @@ impl super::PlanNormalizer {
// Step 7: Build body plans (break condition check)
let mut body_plans: Vec<CorePlan> = Vec::new();
for const_effect in break_cond_consts {
body_plans.push(CorePlan::Effect(const_effect));
let break_cond_effects = Self::lower_break_condition_effects(
&parts.break_condition,
builder,
&phi_bindings,
cond_break,
)?;
for effect in break_cond_effects {
body_plans.push(CorePlan::Effect(effect));
}
body_plans.push(CorePlan::Effect(CoreEffectPlan::Compare {
dst: cond_break,
lhs: break_cond_lhs,
op: break_cond_op,
rhs: break_cond_rhs,
}));
// Step 8: Build step_effects
let mut step_effects = carrier_consts;
@ -310,4 +307,59 @@ impl super::PlanNormalizer {
Ok(CorePlan::Loop(loop_plan))
}
fn lower_break_condition_effects(
ast: &crate::ast::ASTNode,
builder: &mut MirBuilder,
phi_bindings: &BTreeMap<String, ValueId>,
dst: ValueId,
) -> Result<Vec<CoreEffectPlan>, String> {
use crate::ast::{ASTNode, BinaryOperator};
match ast {
ASTNode::BinaryOp {
operator: BinaryOperator::Or,
left,
right,
..
} => {
let (lhs_l, op_l, rhs_l, consts_l) =
Self::lower_compare_ast(left, builder, phi_bindings)?;
let (lhs_r, op_r, rhs_r, consts_r) =
Self::lower_compare_ast(right, builder, phi_bindings)?;
let left_dst = builder.alloc_typed(MirType::Bool);
let right_dst = builder.alloc_typed(MirType::Bool);
let mut effects = Vec::new();
effects.extend(consts_l);
effects.push(CoreEffectPlan::Compare {
dst: left_dst,
lhs: lhs_l,
op: op_l,
rhs: rhs_l,
});
effects.extend(consts_r);
effects.push(CoreEffectPlan::Compare {
dst: right_dst,
lhs: lhs_r,
op: op_r,
rhs: rhs_r,
});
effects.push(CoreEffectPlan::BinOp {
dst,
lhs: left_dst,
op: BinaryOp::Or,
rhs: right_dst,
});
Ok(effects)
}
_ => {
let (lhs, op, rhs, consts) = Self::lower_compare_ast(ast, builder, phi_bindings)?;
let mut effects = consts;
effects.push(CoreEffectPlan::Compare { dst, lhs, op, rhs });
Ok(effects)
}
}
}
}

View File

@ -22,6 +22,11 @@ fi
OUTPUT_CLEAN=$(echo "$OUTPUT" | filter_noise)
EXPECTED_TAG="[plan/pattern2/promotion_hint:DigitPos]"
if ! echo "$OUTPUT" | grep -qF "[coreplan/shadow_adopt:pattern2_break_subset]"; then
test_fail "phase29ab_pattern2_loopbodylocal_min_vm: missing shadow adopt tag"
exit 1
fi
if echo "$OUTPUT_CLEAN" | grep -q "^2$" || echo "$OUTPUT" | grep -q "^RC: 2$"; then
if echo "$OUTPUT_CLEAN" | grep -qF "$EXPECTED_TAG"; then
test_pass "phase29ab_pattern2_loopbodylocal_min_vm: Pattern2 LoopBodyLocal promotion succeeded (output: 2)"

View File

@ -22,6 +22,11 @@ fi
OUTPUT_CLEAN=$(echo "$OUTPUT" | filter_noise)
EXPECTED_TAG="[plan/pattern2/promotion_hint:TrimSeg]"
if ! echo "$OUTPUT" | grep -qF "[coreplan/shadow_adopt:pattern2_break_subset]"; then
test_fail "phase29ab_pattern2_loopbodylocal_seg_min_vm: missing shadow adopt tag"
exit 1
fi
if echo "$OUTPUT_CLEAN" | grep -q "^2$" || echo "$OUTPUT" | grep -q "^RC: 2$"; then
if echo "$OUTPUT_CLEAN" | grep -qF "$EXPECTED_TAG"; then
test_pass "phase29ab_pattern2_loopbodylocal_seg_min_vm: Pattern2 Trim promotion succeeded (output: 2)"