feat(joinir): Phase 94 - P5b escape full E2E (derived ch + +1/+2)

This commit is contained in:
nyash-codex
2025-12-17 00:59:33 +09:00
parent c213ecc3c0
commit 7ab459503b
25 changed files with 1498 additions and 238 deletions

View 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に一言追記して入口を明示。

View File

@ -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,
};

View File

@ -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;

View File

@ -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),
}
}
}

View File

@ -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,
})

View File

@ -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;

View File

@ -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),
}
}
}

View File

@ -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) {

View File

@ -0,0 +1,235 @@
# JoinIR Pattern Policies - ルーティング箱の責務
## 概要
このディレクトリには、Pattern認識とルーティングpolicy決定を行う「箱」が格納されています。
## Policy箱の責務
### ルーティング決定
- 入力: LoopSkeleton、break条件、carrier情報
- 出力: 適用可能なPatternPattern2, 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` enumNone / 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箱同士は独立・疎結合

View 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/)に配置されています