feat(joinir): Phase 94 - P5b escape full E2E (derived ch + +1/+2)
This commit is contained in:
20
src/mir/builder/control_flow/joinir/merge/README.md
Normal file
20
src/mir/builder/control_flow/joinir/merge/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# JoinIR Merge Coordinator(箱の地図)
|
||||
|
||||
責務: JoinIR 生成物をホスト MIR に統合するフェーズ(block/ValueId remap → rewrite → PHI/ExitLine 再接続)。
|
||||
|
||||
主な箱と入口:
|
||||
- `instruction_rewriter.rs` — JoinIR→MIR 命令を書き換えつつブロックを組み立てるメイン導線
|
||||
- `phi_block_remapper.rs` — PHI の block-id だけを再マップする専用箱(ValueId は remapper 済みを前提)
|
||||
- `loop_header_phi_builder.rs` / `exit_phi_builder.rs` — ヘッダ/出口 PHI の組み立て
|
||||
- `inline_boundary_injector.rs` — Host↔JoinIR の ValueId 接続(Boundary 注入)
|
||||
- `value_collector.rs` / `block_allocator.rs` — 再マップ前の収集と ID 割り当て
|
||||
- `tail_call_classifier.rs` — tail call 判定(loop_step の末尾呼び出し検出)
|
||||
|
||||
Fail-Fast の基本:
|
||||
- block/ValueId 衝突や PHI 契約違反は握りつぶさず `Err` で止める
|
||||
- remap は「ValueId remap」→「block-id remap」の順で一貫させる(PHI 二度 remap は禁止)
|
||||
|
||||
拡張時のチェックリスト:
|
||||
1. 新しい JoinInst → MIR 変換を追加する場合、`instruction_rewriter` に閉じて追加し、PHI/ExitLine 契約が壊れないか確認。
|
||||
2. PHI の入力ブロックを触るときは `phi_block_remapper` 経由に寄せて二重 remap を防ぐ。
|
||||
3. 増やした box/契約はここ(README)に一言追記して入口を明示。
|
||||
@ -348,25 +348,10 @@ pub(super) fn merge_and_rewrite(
|
||||
then_bb: local_block_map.get(&then_bb).copied().unwrap_or(then_bb),
|
||||
else_bb: local_block_map.get(&else_bb).copied().unwrap_or(else_bb),
|
||||
},
|
||||
MirInstruction::Phi {
|
||||
dst,
|
||||
inputs,
|
||||
type_hint: None,
|
||||
} => MirInstruction::Phi {
|
||||
dst,
|
||||
// Phase 196: Fix Select expansion PHI - ValueIds are ALREADY remapped by remap_instruction()
|
||||
// We only need to remap block IDs here (not ValueIds!)
|
||||
inputs: inputs
|
||||
.iter()
|
||||
.map(|(bb, val)| {
|
||||
let remapped_bb = local_block_map.get(bb).copied().unwrap_or(*bb);
|
||||
// Phase 196 FIX: Don't double-remap values!
|
||||
// remapper.remap_instruction() already remapped *val
|
||||
(remapped_bb, *val)
|
||||
})
|
||||
.collect(),
|
||||
type_hint: None,
|
||||
},
|
||||
MirInstruction::Phi { dst, inputs, type_hint } => {
|
||||
use super::phi_block_remapper::remap_phi_instruction;
|
||||
remap_phi_instruction(dst, &inputs, type_hint, &local_block_map)
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ mod instruction_rewriter;
|
||||
mod loop_header_phi_builder;
|
||||
mod loop_header_phi_info;
|
||||
mod merge_result;
|
||||
mod phi_block_remapper; // Phase 94: Phi block-id remap box
|
||||
mod tail_call_classifier;
|
||||
mod value_collector;
|
||||
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
//! Box: PHI block remapper (Phase 94 tidy-up)
|
||||
//!
|
||||
//! Responsibility: remap only the block ids of MIR `Phi` inputs using the
|
||||
//! caller-provided `local_block_map`, leaving ValueIds untouched (already
|
||||
//! remapped upstream). Handles both `type_hint = None` and `Some(...)`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::mir::{BasicBlockId, MirInstruction, MirType, ValueId};
|
||||
|
||||
/// Remap a single PHI instruction's block ids.
|
||||
pub(crate) fn remap_phi_instruction(
|
||||
dst: ValueId,
|
||||
inputs: &[(BasicBlockId, ValueId)],
|
||||
type_hint: Option<MirType>,
|
||||
local_block_map: &BTreeMap<BasicBlockId, BasicBlockId>,
|
||||
)-> MirInstruction {
|
||||
MirInstruction::Phi {
|
||||
dst,
|
||||
inputs: remap_phi_inputs(inputs, local_block_map),
|
||||
type_hint,
|
||||
}
|
||||
}
|
||||
|
||||
fn remap_phi_inputs(
|
||||
inputs: &[(BasicBlockId, ValueId)],
|
||||
local_block_map: &BTreeMap<BasicBlockId, BasicBlockId>,
|
||||
) -> Vec<(BasicBlockId, ValueId)> {
|
||||
inputs
|
||||
.iter()
|
||||
.map(|(bb, val)| {
|
||||
let remapped_bb = local_block_map.get(bb).copied().unwrap_or(*bb);
|
||||
(remapped_bb, *val)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn bb(id: u32) -> BasicBlockId {
|
||||
BasicBlockId(id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaps_blocks_preserves_values_and_type_none() {
|
||||
let inputs = vec![(bb(1), ValueId(10)), (bb(2), ValueId(11))];
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(bb(1), bb(10));
|
||||
map.insert(bb(99), bb(100)); // unused entry should not matter
|
||||
|
||||
let inst = remap_phi_instruction(ValueId(5), &inputs, None, &map);
|
||||
|
||||
match inst {
|
||||
MirInstruction::Phi { dst, inputs, type_hint } => {
|
||||
assert_eq!(dst, ValueId(5));
|
||||
assert_eq!(
|
||||
inputs,
|
||||
vec![(bb(10), ValueId(10)), (bb(2), ValueId(11))]
|
||||
);
|
||||
assert!(type_hint.is_none());
|
||||
}
|
||||
other => panic!("expected Phi, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaps_blocks_preserves_type_hint() {
|
||||
let inputs = vec![(bb(3), ValueId(20))];
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(bb(3), bb(30));
|
||||
|
||||
let inst = remap_phi_instruction(ValueId(7), &inputs, Some(MirType::Integer), &map);
|
||||
|
||||
match inst {
|
||||
MirInstruction::Phi { dst, inputs, type_hint } => {
|
||||
assert_eq!(dst, ValueId(7));
|
||||
assert_eq!(inputs, vec![(bb(30), ValueId(20))]);
|
||||
assert_eq!(type_hint, Some(MirType::Integer));
|
||||
}
|
||||
other => panic!("expected Phi, got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,10 @@ pub struct EscapeSkipPatternInfo {
|
||||
pub counter_name: String,
|
||||
pub normal_delta: i64,
|
||||
pub escape_delta: i64,
|
||||
/// Index of the break-guard `if ... { break }` within the loop body.
|
||||
pub break_idx: usize,
|
||||
/// Index of the escape `if` within the loop body.
|
||||
pub escape_idx: usize,
|
||||
/// Phase 92 P0-3: The condition expression for conditional increment
|
||||
/// e.g., `ch == '\\'` for escape sequence handling
|
||||
pub escape_cond: Box<ASTNode>,
|
||||
@ -74,6 +78,8 @@ pub fn detect_escape_skip_pattern(body: &[ASTNode]) -> Option<EscapeSkipPatternI
|
||||
counter_name,
|
||||
normal_delta,
|
||||
escape_delta,
|
||||
break_idx,
|
||||
escape_idx,
|
||||
escape_cond, // Phase 92 P0-3: Condition for JoinIR Select
|
||||
body_stmts,
|
||||
})
|
||||
|
||||
@ -44,10 +44,16 @@
|
||||
//! Phase 91 P5b: Escape Pattern Recognizer
|
||||
//! - escape_pattern_recognizer.rs: P5b (escape sequence handling) pattern detection
|
||||
//! - Extracted from ast_feature_extractor for improved modularity
|
||||
//!
|
||||
//! Phase 93/94: Pattern Policies
|
||||
//! - policies/: Pattern recognition and routing decision (future expansion)
|
||||
//! - Currently a placeholder directory for future policy box organization
|
||||
|
||||
pub(in crate::mir::builder) mod ast_feature_extractor;
|
||||
pub(in crate::mir::builder) mod policies; // Phase 93/94: Pattern routing policies (future expansion)
|
||||
pub(in crate::mir::builder) mod body_local_policy; // Phase 92 P3: promotion vs slot routing
|
||||
pub(in crate::mir::builder) mod escape_pattern_recognizer; // Phase 91 P5b
|
||||
pub(in crate::mir::builder) mod p5b_escape_derived_policy; // Phase 94: derived `ch` + conditional counter
|
||||
pub(in crate::mir::builder) mod common_init;
|
||||
pub(in crate::mir::builder) mod condition_env_builder;
|
||||
pub(in crate::mir::builder) mod conversion_pipeline;
|
||||
|
||||
@ -0,0 +1,323 @@
|
||||
//! Phase 94: P5b escape + body-local derived policy (Box)
|
||||
//!
|
||||
//! Purpose: detect the minimal "escape handling" shape that requires:
|
||||
//! - loop counter conditional step (skip escape char)
|
||||
//! - body-local reassignment (e.g. `ch = substring(...)`) represented as a derived Select
|
||||
//!
|
||||
//! This is a *route* decision (not a fallback). In strict mode, if we detect
|
||||
//! a body-local reassignment that matches the P5b entry shape but cannot be
|
||||
//! converted to a derived recipe, we fail-fast with a reason tag.
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
use crate::config::env::joinir_dev;
|
||||
use crate::mir::builder::control_flow::joinir::patterns::escape_pattern_recognizer::EscapeSkipPatternInfo;
|
||||
use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe;
|
||||
use crate::mir::join_ir::lowering::error_tags;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum P5bEscapeDerivedDecision {
|
||||
None,
|
||||
UseDerived(BodyLocalDerivedRecipe),
|
||||
Reject(String),
|
||||
}
|
||||
|
||||
/// Detect a P5b derived body-local (`ch`) recipe from a Pattern2 loop body.
|
||||
///
|
||||
/// Minimal supported shape (SSOT):
|
||||
/// - `local ch = <expr>` exists at top level
|
||||
/// - escape if exists (detected by EscapeSkipPatternInfo)
|
||||
/// - inside the escape if's then-body, after pre-increment:
|
||||
/// - optional bounds `if i < n { ch = <override_expr> }`
|
||||
/// - or direct `ch = <override_expr>`
|
||||
pub fn classify_p5b_escape_derived(
|
||||
body: &[ASTNode],
|
||||
loop_var_name: &str,
|
||||
) -> P5bEscapeDerivedDecision {
|
||||
let Some(info) = super::ast_feature_extractor::detect_escape_skip_pattern(body) else {
|
||||
return P5bEscapeDerivedDecision::None;
|
||||
};
|
||||
|
||||
if info.counter_name != loop_var_name {
|
||||
// Not the loop counter we lower as the JoinIR loop var; ignore to avoid misrouting.
|
||||
return P5bEscapeDerivedDecision::None;
|
||||
}
|
||||
|
||||
match build_recipe_from_info(body, &info) {
|
||||
Ok(Some(recipe)) => P5bEscapeDerivedDecision::UseDerived(recipe),
|
||||
Ok(None) => {
|
||||
// Escape pattern exists but there is no body-local reassignment to cover.
|
||||
P5bEscapeDerivedDecision::None
|
||||
}
|
||||
Err(e) => {
|
||||
if joinir_dev::strict_enabled() {
|
||||
P5bEscapeDerivedDecision::Reject(error_tags::freeze(&e))
|
||||
} else {
|
||||
// Non-strict mode: keep legacy behavior (no derived slot); still loggable via dev.
|
||||
P5bEscapeDerivedDecision::None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_recipe_from_info(
|
||||
body: &[ASTNode],
|
||||
info: &EscapeSkipPatternInfo,
|
||||
) -> Result<Option<BodyLocalDerivedRecipe>, String> {
|
||||
// 1) Find base init: `local ch = <expr>`
|
||||
let Some(base_init_expr) = find_local_init_expr(body, "ch") else {
|
||||
return Err("[phase94/body_local_derived/contract/missing_local_init] Missing `local ch = <expr>`".to_string());
|
||||
};
|
||||
|
||||
// 2) Locate escape if and find override assignment to ch
|
||||
let escape_if = body.get(info.escape_idx).ok_or_else(|| {
|
||||
format!(
|
||||
"[phase94/body_local_derived/contract/escape_idx_oob] escape_idx={} out of bounds (body.len={})",
|
||||
info.escape_idx,
|
||||
body.len()
|
||||
)
|
||||
})?;
|
||||
let (escape_cond, then_body) = match escape_if {
|
||||
ASTNode::If { condition, then_body, else_body: _, .. } => (condition.as_ref().clone(), then_body.as_slice()),
|
||||
other => {
|
||||
return Err(format!(
|
||||
"[phase94/body_local_derived/contract/escape_node_kind] escape_idx points to non-If: {:?}",
|
||||
other
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let override_assignment = find_ch_override_in_escape_then(then_body)?;
|
||||
let Some((bounds_check, override_expr)) = override_assignment else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// EscapeSkipPatternInfo uses "escape_delta" for the then-body increment, and "normal_delta" for the unconditional tail.
|
||||
// For the common P5b shape:
|
||||
// - escape iteration total delta = escape_delta + normal_delta
|
||||
// - normal iteration total delta = normal_delta
|
||||
let recipe = BodyLocalDerivedRecipe {
|
||||
name: "ch".to_string(),
|
||||
base_init_expr,
|
||||
escape_cond,
|
||||
loop_counter_name: info.counter_name.clone(),
|
||||
pre_delta: info.escape_delta,
|
||||
post_delta: info.normal_delta,
|
||||
bounds_check,
|
||||
override_expr,
|
||||
};
|
||||
Ok(Some(recipe))
|
||||
}
|
||||
|
||||
fn find_local_init_expr(body: &[ASTNode], name: &str) -> Option<ASTNode> {
|
||||
for node in body {
|
||||
if let ASTNode::Local { variables, initial_values, .. } = node {
|
||||
for (var_name, maybe_expr) in variables.iter().zip(initial_values.iter()) {
|
||||
if var_name == name {
|
||||
if let Some(expr) = maybe_expr.as_ref() {
|
||||
return Some((**expr).clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Find `ch = <expr>` either directly or under an inner bounds `if`.
|
||||
///
|
||||
/// Returns:
|
||||
/// - Ok(Some((bounds_opt, override_expr))) when an override assignment exists
|
||||
/// - Ok(None) when no override assignment exists (no derived slot needed)
|
||||
/// - Err when an override exists but violates minimal contract
|
||||
fn find_ch_override_in_escape_then(
|
||||
then_body: &[ASTNode],
|
||||
) -> Result<Option<(Option<ASTNode>, ASTNode)>, String> {
|
||||
// Direct assignment form: `ch = <expr>`
|
||||
for stmt in then_body {
|
||||
if let ASTNode::Assignment { target, value, .. } = stmt {
|
||||
if is_var_named(target.as_ref(), "ch") {
|
||||
return Ok(Some((None, value.as_ref().clone())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nested bounds form: `if <cond> { ch = <expr> }`
|
||||
for stmt in then_body {
|
||||
if let ASTNode::If { condition, then_body, else_body: None, .. } = stmt {
|
||||
if then_body.len() != 1 {
|
||||
continue;
|
||||
}
|
||||
if let ASTNode::Assignment { target, value, .. } = &then_body[0] {
|
||||
if is_var_named(target.as_ref(), "ch") {
|
||||
return Ok(Some((Some(condition.as_ref().clone()), value.as_ref().clone())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn is_var_named(node: &ASTNode, name: &str) -> bool {
|
||||
matches!(node, ASTNode::Variable { name: n, .. } if n == name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{BinaryOperator, Span};
|
||||
|
||||
fn var(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn str_lit(s: &str) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: crate::ast::LiteralValue::String(s.to_string()),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn int_lit(v: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: crate::ast::LiteralValue::Integer(v),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn binop(op: BinaryOperator, lhs: ASTNode, rhs: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp {
|
||||
operator: op,
|
||||
left: Box::new(lhs),
|
||||
right: Box::new(rhs),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn assignment(target: ASTNode, value: ASTNode) -> ASTNode {
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(target),
|
||||
value: Box::new(value),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn method_call(obj: &str, method: &str, args: Vec<ASTNode>) -> ASTNode {
|
||||
ASTNode::MethodCall {
|
||||
object: Box::new(var(obj)),
|
||||
method: method.to_string(),
|
||||
arguments: args,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_p5b_shape_and_builds_recipe() {
|
||||
// Body layout:
|
||||
// 0: local ch = s.substring(i, i+1)
|
||||
// 1: if ch == "\"" { break }
|
||||
// 2: if ch == "\\" { i = i + 1; ch = s.substring(i, i+1) }
|
||||
// 3: i = i + 1
|
||||
let body = vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["ch".to_string()],
|
||||
initial_values: vec![Some(Box::new(method_call(
|
||||
"s",
|
||||
"substring",
|
||||
vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))],
|
||||
)))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::If {
|
||||
condition: Box::new(binop(BinaryOperator::Equal, var("ch"), str_lit("\""))),
|
||||
then_body: vec![ASTNode::Break { span: Span::unknown() }],
|
||||
else_body: None,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::If {
|
||||
condition: Box::new(binop(BinaryOperator::Equal, var("ch"), str_lit("\\"))),
|
||||
then_body: vec![
|
||||
assignment(var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))),
|
||||
assignment(
|
||||
var("ch"),
|
||||
method_call(
|
||||
"s",
|
||||
"substring",
|
||||
vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))],
|
||||
),
|
||||
),
|
||||
],
|
||||
else_body: None,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
assignment(var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))),
|
||||
];
|
||||
|
||||
match classify_p5b_escape_derived(&body, "i") {
|
||||
P5bEscapeDerivedDecision::UseDerived(recipe) => {
|
||||
assert_eq!(recipe.name, "ch");
|
||||
assert_eq!(recipe.loop_counter_name, "i");
|
||||
assert_eq!(recipe.pre_delta, 1);
|
||||
assert_eq!(recipe.post_delta, 1);
|
||||
match recipe.override_expr {
|
||||
ASTNode::MethodCall { ref method, .. } => assert_eq!(method, "substring"),
|
||||
other => panic!("expected override MethodCall, got {:?}", other),
|
||||
}
|
||||
}
|
||||
other => panic!("expected UseDerived recipe, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_rejects_when_local_init_missing() {
|
||||
// escape pattern exists, but `local ch = ...` is absent -> strict should reject
|
||||
let body = vec![
|
||||
ASTNode::If {
|
||||
condition: Box::new(binop(BinaryOperator::Equal, var("ch"), str_lit("\""))),
|
||||
then_body: vec![ASTNode::Break { span: Span::unknown() }],
|
||||
else_body: None,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::If {
|
||||
condition: Box::new(binop(BinaryOperator::Equal, var("ch"), str_lit("\\"))),
|
||||
then_body: vec![
|
||||
assignment(var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))),
|
||||
assignment(
|
||||
var("ch"),
|
||||
method_call(
|
||||
"s",
|
||||
"substring",
|
||||
vec![var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))],
|
||||
),
|
||||
),
|
||||
],
|
||||
else_body: None,
|
||||
span: Span::unknown(),
|
||||
},
|
||||
assignment(var("i"), binop(BinaryOperator::Add, var("i"), int_lit(1))),
|
||||
];
|
||||
|
||||
let prev = std::env::var("HAKO_JOINIR_STRICT").ok();
|
||||
std::env::set_var("HAKO_JOINIR_STRICT", "1");
|
||||
let decision = classify_p5b_escape_derived(&body, "i");
|
||||
if let Some(v) = prev {
|
||||
std::env::set_var("HAKO_JOINIR_STRICT", v);
|
||||
} else {
|
||||
std::env::remove_var("HAKO_JOINIR_STRICT");
|
||||
}
|
||||
|
||||
match decision {
|
||||
P5bEscapeDerivedDecision::Reject(reason) => {
|
||||
assert!(
|
||||
reason.contains("missing_local_init"),
|
||||
"unexpected reason: {}",
|
||||
reason
|
||||
);
|
||||
}
|
||||
other => panic!("expected Reject, got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit};
|
||||
use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEnv};
|
||||
use super::body_local_policy::{classify_for_pattern2, BodyLocalPolicyDecision};
|
||||
use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlot;
|
||||
use crate::mir::join_ir::lowering::common::body_local_derived_emitter::BodyLocalDerivedRecipe;
|
||||
use crate::mir::join_ir::lowering::debug_output_box::DebugOutputBox;
|
||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||
@ -16,6 +17,7 @@ use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
|
||||
use crate::mir::loop_pattern_detection::error_messages;
|
||||
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
|
||||
use crate::mir::ValueId;
|
||||
use super::p5b_escape_derived_policy::{classify_p5b_escape_derived, P5bEscapeDerivedDecision};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
struct Pattern2DebugLog {
|
||||
@ -56,6 +58,8 @@ struct Pattern2Inputs {
|
||||
break_condition_node: ASTNode,
|
||||
/// Phase 93 P0: ConditionOnly recipe for derived slot recalculation
|
||||
condition_only_recipe: Option<crate::mir::join_ir::lowering::common::condition_only_emitter::ConditionOnlyRecipe>,
|
||||
/// Phase 94: BodyLocalDerived recipe for P5b "ch" reassignment + escape counter.
|
||||
body_local_derived_recipe: Option<BodyLocalDerivedRecipe>,
|
||||
}
|
||||
|
||||
fn prepare_pattern2_inputs(
|
||||
@ -254,6 +258,7 @@ fn prepare_pattern2_inputs(
|
||||
read_only_body_local_slot: None,
|
||||
break_condition_node,
|
||||
condition_only_recipe: None, // Phase 93 P0: Will be set by apply_trim_and_normalize
|
||||
body_local_derived_recipe: None, // Phase 94: Will be set after normalization
|
||||
})
|
||||
}
|
||||
|
||||
@ -807,6 +812,42 @@ impl MirBuilder {
|
||||
apply_trim_and_normalize(self, condition, _body, &mut inputs, verbose)?;
|
||||
let analysis_body = normalized_body.as_deref().unwrap_or(_body);
|
||||
|
||||
// Phase 94: Detect P5b escape-derived (`ch` reassignment + escape counter).
|
||||
// Route is explicit; in strict mode, partial matches that require derived support must fail-fast.
|
||||
match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) {
|
||||
P5bEscapeDerivedDecision::UseDerived(recipe) => {
|
||||
log.log(
|
||||
"phase94",
|
||||
format!(
|
||||
"Phase 94: Enabled BodyLocalDerived for '{}' (counter='{}', pre_delta={}, post_delta={})",
|
||||
recipe.name, recipe.loop_counter_name, recipe.pre_delta, recipe.post_delta
|
||||
),
|
||||
);
|
||||
inputs.body_local_derived_recipe = Some(recipe);
|
||||
}
|
||||
P5bEscapeDerivedDecision::Reject(reason) => {
|
||||
return Err(format!("[cf_loop/pattern2] {}", reason));
|
||||
}
|
||||
P5bEscapeDerivedDecision::None => {
|
||||
// Strict-only guard: if we see a body-local reassignment to `ch`, don't silently miscompile.
|
||||
let has_ch_reassign = analysis_body.iter().any(|n| match n {
|
||||
ASTNode::Assignment { target, .. } => matches!(
|
||||
target.as_ref(),
|
||||
ASTNode::Variable { name, .. } if name == "ch"
|
||||
),
|
||||
_ => false,
|
||||
});
|
||||
if crate::config::env::joinir_dev::strict_enabled() && has_ch_reassign {
|
||||
return Err(format!(
|
||||
"[cf_loop/pattern2] {}",
|
||||
crate::mir::join_ir::lowering::error_tags::freeze(
|
||||
"[phase94/body_local_derived/contract/unhandled_reassign] Body-local reassignment to 'ch' detected but not supported by Phase 94 recipe"
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::mir::join_ir::lowering::loop_update_analyzer::LoopUpdateAnalyzer;
|
||||
let carrier_updates = LoopUpdateAnalyzer::analyze_carrier_updates(
|
||||
analysis_body,
|
||||
@ -894,6 +935,7 @@ impl MirBuilder {
|
||||
join_value_space,
|
||||
skeleton,
|
||||
condition_only_recipe: inputs.condition_only_recipe.as_ref(), // Phase 93 P0
|
||||
body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(), // Phase 94
|
||||
};
|
||||
|
||||
let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) {
|
||||
|
||||
235
src/mir/builder/control_flow/joinir/patterns/policies/README.md
Normal file
235
src/mir/builder/control_flow/joinir/patterns/policies/README.md
Normal file
@ -0,0 +1,235 @@
|
||||
# JoinIR Pattern Policies - ルーティング箱の責務
|
||||
|
||||
## 概要
|
||||
このディレクトリには、Pattern認識とルーティング(policy決定)を行う「箱」が格納されています。
|
||||
|
||||
## Policy箱の責務
|
||||
|
||||
### ルーティング決定
|
||||
- 入力: LoopSkeleton、break条件、carrier情報
|
||||
- 出力: 適用可能なPattern(Pattern2, Pattern3, etc.)とLoweringResult
|
||||
- 判断基準: パターンマッチング条件(Trim, ConditionalStep, Escape, etc.)
|
||||
|
||||
### Lowering Result生成
|
||||
- Pattern固有の情報(ConditionOnlyRecipe, ConditionalStepInfo, etc.)を生成
|
||||
- CarrierInfo拡張(promoted variables, trim_helper, etc.)
|
||||
- ConditionBinding設定
|
||||
|
||||
## Policy箱の候補(将来の整理対象)
|
||||
|
||||
### trim_loop_lowering.rs (Phase 180/92/93)
|
||||
**現在の場所**: `patterns/trim_loop_lowering.rs`
|
||||
|
||||
**責務**: Trimパターン認識とConditionOnlyルーティング
|
||||
|
||||
**判断基準**:
|
||||
- LoopBodyLocal変数がTrim条件(whitespace check)
|
||||
- break条件がConditionOnly(毎イテレーション再計算)
|
||||
|
||||
**出力**:
|
||||
- `TrimLoweringResult` - condition, carrier_info, condition_only_recipe
|
||||
|
||||
**将来的な整理**:
|
||||
- Trimパターン判断ロジックをpolicies/へ移動検討
|
||||
- 現在はpatterns/直下に配置(Phase 180統合済み)
|
||||
|
||||
---
|
||||
|
||||
### p5b_escape_derived_policy.rs (Phase 94)
|
||||
**現在の場所**: `patterns/p5b_escape_derived_policy.rs`
|
||||
|
||||
**責務**: P5b escapeパターン認識とBodyLocalDerivedルーティング
|
||||
|
||||
**判断基準**:
|
||||
- body-local変数の再代入(例: `ch = s.substring(...)`)
|
||||
- escape skip条件(例: `if ch == "\\" { i = i + 2 }`)
|
||||
- loop変数の条件付き更新
|
||||
|
||||
**出力**:
|
||||
- `P5bEscapeDerivedDecision::UseDerived(BodyLocalDerivedRecipe)` - escape recipe
|
||||
- `P5bEscapeDerivedDecision::Reject(String)` - 検出失敗理由
|
||||
- `P5bEscapeDerivedDecision::None` - 該当なし
|
||||
|
||||
**将来的な整理**:
|
||||
- policies/へ移動してpolicy箱として統一
|
||||
- 現在はpatterns/直下に配置
|
||||
|
||||
---
|
||||
|
||||
### body_local_policy.rs
|
||||
**現在の場所**: `patterns/body_local_policy.rs`
|
||||
|
||||
**責務**: Body-local変数の昇格判断
|
||||
|
||||
**判断基準**:
|
||||
- body-local変数がloop carrier候補か
|
||||
- 昇格可能なパターンか(Trim, Escape, etc.)
|
||||
|
||||
**将来的な整理**:
|
||||
- policies/へ移動してpolicy箱として統一
|
||||
|
||||
---
|
||||
|
||||
## 設計原則
|
||||
|
||||
### 単一判断の原則
|
||||
- 各policy箱は1つのパターン判断のみ
|
||||
- 複数パターンの判断は別のpolicy箱に委譲
|
||||
|
||||
### 非破壊的判断
|
||||
- 入力を変更しない
|
||||
- 判断結果をResultで返す(`Ok(Some(result))` / `Ok(None)` / `Err(msg)`)
|
||||
|
||||
### Fail-Fast
|
||||
- パターンマッチング失敗は即座に`Ok(None)`を返す
|
||||
- エラーは明示的に`Err(msg)`
|
||||
- Reject理由は`error_tags::freeze()`でタグ付与
|
||||
|
||||
### Decision型の統一
|
||||
- `Decision` enum(None / Use(...) / Reject(String))を統一パターンとして使用
|
||||
- 例: `P5bEscapeDerivedDecision`, `TrimDecision`(将来)
|
||||
|
||||
## 使用パターン
|
||||
|
||||
### Pattern2ルーティング(現在のパターン)
|
||||
```rust
|
||||
// Step 1: P5b escapeパターン判断
|
||||
match classify_p5b_escape_derived(body, loop_var_name) {
|
||||
P5bEscapeDerivedDecision::UseDerived(recipe) => {
|
||||
// P5b escape適用
|
||||
return lower_with_p5b_escape(recipe, ...);
|
||||
}
|
||||
P5bEscapeDerivedDecision::Reject(reason) => {
|
||||
return Err(reason);
|
||||
}
|
||||
P5bEscapeDerivedDecision::None => {
|
||||
// 次のパターンへ
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Trimパターン判断
|
||||
if let Some(trim_result) = TrimLoopLowerer::try_lower_trim_pattern(...)? {
|
||||
// Trimパターン適用
|
||||
return Ok(trim_result);
|
||||
}
|
||||
|
||||
// Step 3: デフォルトパターン
|
||||
Ok(default_pattern2_lowering(...))
|
||||
```
|
||||
|
||||
### 将来の統一パターン(policies/移動後)
|
||||
```rust
|
||||
use patterns::policies::{P5bEscapePolicy, TrimPolicy, ConditionalStepPolicy};
|
||||
|
||||
// Policy boxの統一インターフェース
|
||||
trait PatternPolicy {
|
||||
type Decision;
|
||||
fn classify(&self, context: &PatternContext) -> Self::Decision;
|
||||
}
|
||||
|
||||
// ルーティングパイプライン
|
||||
let decision = P5bEscapePolicy.classify(&ctx)
|
||||
.or_else(|| TrimPolicy.classify(&ctx))
|
||||
.or_else(|| ConditionalStepPolicy.classify(&ctx))
|
||||
.unwrap_or(DefaultDecision);
|
||||
```
|
||||
|
||||
## デバッグ
|
||||
|
||||
### ログprefixの統一
|
||||
- `[policy/p5b-escape]` - P5b escape判断
|
||||
- `[policy/trim]` - Trim判断
|
||||
- `[policy/conditional-step]` - ConditionalStep判断
|
||||
|
||||
### 環境変数
|
||||
- `HAKO_JOINIR_DEBUG=1` - JoinIR全般のデバッグログ
|
||||
- `joinir_dev_enabled()` - 既存の制御機構
|
||||
|
||||
### デバッグ出力例
|
||||
```rust
|
||||
if joinir_dev_enabled() {
|
||||
eprintln!("[policy/p5b-escape] Detected escape pattern: {:?}", info);
|
||||
}
|
||||
```
|
||||
|
||||
## 命名規則
|
||||
|
||||
### ファイル命名
|
||||
- `<パターン名>_policy.rs` - パターン判断policy箱
|
||||
- 例: `trim_policy.rs`, `escape_policy.rs`, `conditional_step_policy.rs`
|
||||
|
||||
### 型命名
|
||||
- `<パターン名>Decision` - 判断結果型
|
||||
- `<パターン名>Policy` - policy箱の構造体(将来)
|
||||
|
||||
### 列挙型パターン
|
||||
```rust
|
||||
pub enum Decision {
|
||||
None, // 該当なし
|
||||
Use(Recipe), // 適用可能
|
||||
Reject(String), // 検出失敗
|
||||
}
|
||||
```
|
||||
|
||||
## 将来の拡張
|
||||
|
||||
### Phase 95以降の候補
|
||||
- `trim_policy.rs` - Trimパターン判断をpolicies/へ移動
|
||||
- `escape_policy.rs` - P5b escapeを統一インターフェースに
|
||||
- `array_loop_policy.rs` - 配列ループパターン判断
|
||||
- `map_loop_policy.rs` - Mapループパターン判断
|
||||
- `conditional_step_policy.rs` - ConditionalStep独立箱化
|
||||
|
||||
### 段階的な移行計画
|
||||
|
||||
#### Phase 1: ディレクトリ準備(今回)
|
||||
- policies/ディレクトリ作成 ✅
|
||||
- README.md作成 ✅
|
||||
- mod.rs作成 ✅
|
||||
|
||||
#### Phase 2: 既存policy箱の移動(将来)
|
||||
- `p5b_escape_derived_policy.rs` → `policies/escape_policy.rs`
|
||||
- `body_local_policy.rs` → `policies/body_local_policy.rs`
|
||||
- trim関連ロジック抽出 → `policies/trim_policy.rs`
|
||||
|
||||
#### Phase 3: インターフェース統一(将来)
|
||||
- `PatternPolicy` trait定義
|
||||
- Decision型の統一
|
||||
- ルーティングパイプラインの実装
|
||||
|
||||
### 拡張時の注意
|
||||
- README.mdにpolicy箱の責務を追加
|
||||
- Decision型を統一パターンに従う
|
||||
- 既存のpolicy箱との一貫性を保つ
|
||||
- Fail-Fastタグを明記
|
||||
|
||||
## 参考資料
|
||||
|
||||
### 関連ドキュメント
|
||||
- [JoinIR アーキテクチャ概要](../../../../development/current/main/joinir-architecture-overview.md)
|
||||
- [JoinIR 設計マップ](../../../../development/current/main/design/joinir-design-map.md)
|
||||
- [Pattern Pipeline](../pattern_pipeline.rs) - パターン判断の統合処理
|
||||
- [Escape Pattern Recognizer](../escape_pattern_recognizer.rs) - escape検出ロジック
|
||||
|
||||
### Phase Log
|
||||
- [Phase 92 Log](../../../../development/current/main/phases/phase-92/) - ConditionalStep
|
||||
- [Phase 93 Log](../../../../development/current/main/phases/phase-93/) - ConditionOnly
|
||||
- [Phase 94 Log](../../../../development/current/main/phases/phase-94/) - BodyLocalDerived
|
||||
|
||||
## 設計哲学(Box Theory)
|
||||
|
||||
### 箱理論の適用
|
||||
- **箱にする**: Policy判断を独立した箱に分離
|
||||
- **境界を作る**: Decisionを統一インターフェースに
|
||||
- **戻せる**: 段階的移行で既存コードを壊さない
|
||||
- **見える化**: ログprefixで判断過程を可視化
|
||||
|
||||
### Fail-Fast原則の徹底
|
||||
- フォールバック処理は原則禁止
|
||||
- Reject理由を明示的に返す
|
||||
- エラータグで根本原因を追跡可能に
|
||||
|
||||
### 単一責任の箱
|
||||
- 1つのpolicy箱 = 1つのパターン判断
|
||||
- 複数パターンの組み合わせは上位層で制御
|
||||
- policy箱同士は独立・疎結合
|
||||
29
src/mir/builder/control_flow/joinir/patterns/policies/mod.rs
Normal file
29
src/mir/builder/control_flow/joinir/patterns/policies/mod.rs
Normal file
@ -0,0 +1,29 @@
|
||||
//! JoinIR Pattern Policies - パターン認識とルーティング決定
|
||||
//!
|
||||
//! ## 概要
|
||||
//! このモジュールには、Pattern認識とルーティング(policy決定)を行う「箱」が格納されています。
|
||||
//!
|
||||
//! ## Policy箱の責務
|
||||
//! - パターン認識: LoopSkeletonから特定のパターン(Trim, Escape, etc.)を検出
|
||||
//! - ルーティング決定: 適用可能なLoweringパターンを決定
|
||||
//! - Recipe生成: Pattern固有の情報(ConditionOnlyRecipe, BodyLocalDerivedRecipe, etc.)を生成
|
||||
//!
|
||||
//! ## 設計原則
|
||||
//! - **単一判断の原則**: 各policy箱は1つのパターン判断のみ
|
||||
//! - **非破壊的判断**: 入力を変更せず、Decision型で結果を返す
|
||||
//! - **Fail-Fast**: パターンマッチング失敗は即座にReject/Noneを返す
|
||||
//!
|
||||
//! ## 将来の拡張
|
||||
//! 現在はpolicies/ディレクトリの準備段階です。
|
||||
//! 既存のpolicy関連ファイル(p5b_escape_derived_policy.rs, body_local_policy.rs等)は
|
||||
//! patterns/直下に配置されていますが、将来的にこのディレクトリへ移動する予定です。
|
||||
//!
|
||||
//! ### 段階的な移行計画
|
||||
//! - Phase 1: ディレクトリ準備(今回) ✅
|
||||
//! - Phase 2: 既存policy箱の移動(将来)
|
||||
//! - Phase 3: インターフェース統一(将来)
|
||||
//!
|
||||
//! 詳細は [README.md](README.md) を参照してください。
|
||||
|
||||
// 現在は空モジュール(将来の拡張用)
|
||||
// 既存のpolicy関連ファイルは親モジュール(patterns/)に配置されています
|
||||
Reference in New Issue
Block a user