refactor(joinir): unify policy decisions and trim routing

This commit is contained in:
nyash-codex
2025-12-17 01:20:35 +09:00
parent 8e17534829
commit ad67d50798
7 changed files with 110 additions and 94 deletions

View File

@ -5,6 +5,7 @@
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::builder::control_flow::joinir::patterns::policies::PolicyDecision;
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
use crate::mir::join_ir::lowering::common::body_local_slot::{
ReadOnlyBodyLocalSlot, ReadOnlyBodyLocalSlotBox,
@ -19,14 +20,13 @@ use crate::mir::loop_pattern_detection::loop_condition_scope::{CondVarScope, Loo
///
/// This is a "route" decision (not a fallback): we choose exactly one of the supported
/// strategies and reject otherwise.
pub enum BodyLocalPolicyDecision {
UsePromotion {
pub enum BodyLocalRoute {
Promotion {
promoted_carrier: CarrierInfo,
promoted_var: String,
carrier_name: String,
},
UseReadOnlySlot(ReadOnlyBodyLocalSlot),
Reject { reason: String, vars: Vec<String> },
ReadOnlySlot(ReadOnlyBodyLocalSlot),
}
pub fn classify_for_pattern2(
@ -36,7 +36,7 @@ pub fn classify_for_pattern2(
break_condition_node: &ASTNode,
cond_scope: &LoopConditionScope,
body: &[ASTNode],
) -> BodyLocalPolicyDecision {
) -> PolicyDecision<BodyLocalRoute> {
let vars: Vec<String> = cond_scope
.vars
.iter()
@ -60,19 +60,18 @@ pub fn classify_for_pattern2(
carrier_info: promoted_carrier,
promoted_var,
carrier_name,
} => BodyLocalPolicyDecision::UsePromotion {
} => PolicyDecision::Use(BodyLocalRoute::Promotion {
promoted_carrier,
promoted_var,
carrier_name,
},
}),
ConditionPromotionResult::CannotPromote { reason, .. } => {
match extract_body_local_inits_for_conditions(&vars, body) {
Ok(Some(slot)) => BodyLocalPolicyDecision::UseReadOnlySlot(slot),
Ok(None) => BodyLocalPolicyDecision::Reject { reason, vars },
Err(slot_err) => BodyLocalPolicyDecision::Reject {
reason: format!("{reason}; read-only-slot rejected: {slot_err}"),
vars,
},
Ok(Some(slot)) => PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)),
Ok(None) => PolicyDecision::Reject(reason),
Err(slot_err) => PolicyDecision::Reject(format!(
"{reason}; read-only-slot rejected: {slot_err}"
)),
}
}
}

View File

@ -5,7 +5,7 @@ use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
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 super::body_local_policy::{classify_for_pattern2, BodyLocalRoute};
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;
@ -20,6 +20,7 @@ use crate::mir::ValueId;
use super::policies::p5b_escape_derived_policy::{
classify_p5b_escape_derived, P5bEscapeDerivedDecision,
};
use super::policies::PolicyDecision;
use std::collections::BTreeMap;
struct Pattern2DebugLog {
@ -283,6 +284,12 @@ fn promote_and_prepare_carriers(
let log = Pattern2DebugLog::new(verbose);
let mut promoted_pairs: Vec<(String, String)> = Vec::new();
let cond_body_local_vars: Vec<String> = cond_scope
.vars
.iter()
.filter(|v| matches!(v.scope, crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope::LoopBodyLocal))
.map(|v| v.name.clone())
.collect();
if cond_scope.has_loop_body_local() {
match classify_for_pattern2(
@ -293,11 +300,11 @@ fn promote_and_prepare_carriers(
&cond_scope,
body,
) {
BodyLocalPolicyDecision::UsePromotion {
PolicyDecision::Use(BodyLocalRoute::Promotion {
promoted_carrier,
promoted_var,
carrier_name,
} => {
}) => {
// Phase 133 P1: Check if this is a Trim promotion (A-3 pattern)
// Trim promotions are handled by TrimLoopLowerer (apply_trim_and_normalize)
// which provides SSOT for env/join_id, so we defer to that path.
@ -399,7 +406,7 @@ fn promote_and_prepare_carriers(
);
}
}
BodyLocalPolicyDecision::UseReadOnlySlot(slot) => {
PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)) => {
log.log(
"body_local_slot",
format!(
@ -410,11 +417,12 @@ fn promote_and_prepare_carriers(
inputs.allowed_body_locals_for_conditions = vec![slot.name.clone()];
inputs.read_only_body_local_slot = Some(slot);
}
BodyLocalPolicyDecision::Reject { reason, vars } => {
PolicyDecision::Reject(reason) => {
return Err(error_messages::format_error_pattern2_promotion_failed(
&vars, &reason,
&cond_body_local_vars, &reason,
));
}
PolicyDecision::None => {}
}
}
@ -817,7 +825,7 @@ impl MirBuilder {
// 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) => {
P5bEscapeDerivedDecision::Use(recipe) => {
log.log(
"phase94",
format!(

View File

@ -85,8 +85,8 @@
- Reject理由は`error_tags::freeze()`でタグ付与
### Decision型の統一
- `Decision` enumNone / Use(...) / Reject(String))を統一パターンとして使用
- 例: `P5bEscapeDerivedDecision`, `TrimDecision`(将来)
- `PolicyDecision<T>`Use / Reject / NoneをSSOTにする
- 例: `P5bEscapeDerivedDecision = PolicyDecision<BodyLocalDerivedRecipe>`, `TrimPolicyResult`
## 使用パターン

View File

@ -8,6 +8,10 @@
//! - ルーティング決定: 適用可能なLoweringパターンを決定
//! - Recipe生成: Pattern固有の情報ConditionOnlyRecipe, BodyLocalDerivedRecipe, etc.)を生成
//!
//! ## 決定型のSSOT
//! - `PolicyDecision<T>` に統一Use / Reject / None
//! - BodyLocal, Trim, P5b escape などすべてここ経由で route することで Pattern2 側の分岐を簡潔に保つ
//!
//! ## 設計原則
//! - **単一判断の原則**: 各policy箱は1つのパターン判断のみ
//! - **非破壊的判断**: 入力を変更せず、Decision型で結果を返す
@ -17,11 +21,14 @@
//! policies/ は「認識とルーティング決定policy」を分離する受け皿です。
//! Phase 94P5b derivedから段階的に移設を開始しました。
//!
//! ### 段階的な移行計画
//! - Phase 1: ディレクトリ準備 ✅
//! - Phase 2: 既存policy箱の移動進行中
//! - Phase 3: インターフェース統一(将来)
//!
//! 詳細は [README.md](README.md) を参照してください。
#[derive(Debug, Clone)]
pub enum PolicyDecision<T> {
Use(T),
Reject(String),
None,
}
pub(in crate::mir::builder) mod p5b_escape_derived_policy;
pub(in crate::mir::builder) mod trim_policy;

View File

@ -13,13 +13,9 @@ 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;
use super::PolicyDecision;
#[derive(Debug)]
pub enum P5bEscapeDerivedDecision {
None,
UseDerived(BodyLocalDerivedRecipe),
Reject(String),
}
pub type P5bEscapeDerivedDecision = PolicyDecision<BodyLocalDerivedRecipe>;
/// Detect a P5b derived body-local (`ch`) recipe from a Pattern2 loop body.
///
@ -43,7 +39,7 @@ pub fn classify_p5b_escape_derived(
}
match build_recipe_from_info(body, &info) {
Ok(Some(recipe)) => P5bEscapeDerivedDecision::UseDerived(recipe),
Ok(Some(recipe)) => P5bEscapeDerivedDecision::Use(recipe),
Ok(None) => {
// Escape pattern exists but there is no body-local reassignment to cover.
P5bEscapeDerivedDecision::None
@ -257,7 +253,7 @@ mod tests {
];
match classify_p5b_escape_derived(&body, "i") {
P5bEscapeDerivedDecision::UseDerived(recipe) => {
P5bEscapeDerivedDecision::Use(recipe) => {
assert_eq!(recipe.name, "ch");
assert_eq!(recipe.loop_counter_name, "i");
assert_eq!(recipe.pre_delta, 1);

View File

@ -0,0 +1,51 @@
//! Trim pattern policy box (判定専用)
//!
//! 目的: Trim 形状かどうかを判定し、ConditionScope を返すだけに責務を絞る。
//! 生成loweringは従来通り TrimLoopLowerer 側が担当する。
use crate::ast::ASTNode;
use crate::mir::builder::control_flow::joinir::patterns::trim_loop_lowering::TrimLoopLowerer;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::loop_pattern_detection::loop_condition_scope::{
CondVarInfo, CondVarScope, LoopConditionScope, LoopConditionScopeBox,
};
use super::PolicyDecision;
/// 判定結果(生成に必要な最小情報だけ運ぶ)
#[derive(Debug, Clone)]
pub struct TrimPolicyResult {
pub cond_scope: LoopConditionScope,
pub condition_body_locals: Vec<CondVarInfo>,
}
pub fn classify_trim_like_loop(
scope: &LoopScopeShape,
loop_cond: &ASTNode,
break_cond: &ASTNode,
loop_var_name: &str,
) -> PolicyDecision<TrimPolicyResult> {
let cond_scope =
LoopConditionScopeBox::analyze(loop_var_name, &[loop_cond, break_cond], Some(scope));
if !cond_scope.has_loop_body_local() {
return PolicyDecision::None;
}
let condition_body_locals: Vec<_> = cond_scope
.vars
.iter()
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
.filter(|v| TrimLoopLowerer::is_var_used_in_condition(&v.name, break_cond))
.cloned()
.collect();
if condition_body_locals.is_empty() {
return PolicyDecision::None;
}
PolicyDecision::Use(TrimPolicyResult {
cond_scope,
condition_body_locals,
})
}

View File

@ -44,8 +44,9 @@ use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::loop_pattern_detection::loop_body_carrier_promoter::{
LoopBodyCarrierPromoter, PromotionRequest, PromotionResult,
};
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox;
use crate::mir::ValueId;
use super::policies::trim_policy::{classify_trim_like_loop, TrimPolicyResult};
use super::policies::PolicyDecision;
/// Trim pattern lowering orchestrator
///
@ -96,7 +97,10 @@ impl TrimLoopLowerer {
/// # Returns
///
/// `true` if `var_name` appears anywhere in `cond_node`, `false` otherwise
fn is_var_used_in_condition(var_name: &str, cond_node: &ASTNode) -> bool {
pub(in crate::mir::builder::control_flow::joinir::patterns) fn is_var_used_in_condition(
var_name: &str,
cond_node: &ASTNode,
) -> bool {
match cond_node {
ASTNode::Variable { name, .. } => name == var_name,
ASTNode::BinaryOp { left, right, .. } => {
@ -179,63 +183,14 @@ impl TrimLoopLowerer {
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
let verbose = crate::config::env::joinir_dev_enabled() || trace.is_joinir_enabled();
// Phase 180-2: Skeleton implementation
// TODO: Phase 180-3 will implement full logic from Pattern2
// Step 1: Check if condition references LoopBodyLocal variables
let cond_scope =
LoopConditionScopeBox::analyze(loop_var_name, &[loop_cond, break_cond], Some(scope));
trace.emit_if(
"trim",
"scope",
&format!(
"Analyzing condition scope: {} variables",
cond_scope.vars.len()
),
verbose,
);
if !cond_scope.has_loop_body_local() {
// Not a Trim pattern - normal loop
trace.emit_if(
"trim",
"scope",
"No LoopBodyLocal detected, skipping Trim lowering",
verbose,
);
return Ok(None);
}
trace.emit_if(
"trim",
"scope",
"LoopBodyLocal detected in condition scope",
verbose,
);
// Phase 183-2: Filter to only condition LoopBodyLocal (skip body-only)
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;
let condition_body_locals: Vec<_> = cond_scope
.vars
.iter()
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
.filter(|v| {
// Check if variable is actually used in break condition
Self::is_var_used_in_condition(&v.name, break_cond)
})
.collect();
if condition_body_locals.is_empty() {
// All LoopBodyLocal are body-only (not in conditions) → Not a Trim pattern
trace.emit_if(
"trim",
"phase183",
"All LoopBodyLocal are body-only (not in conditions), skipping Trim lowering",
verbose,
);
return Ok(None);
}
let TrimPolicyResult {
cond_scope,
condition_body_locals,
} = match classify_trim_like_loop(scope, loop_cond, break_cond, loop_var_name) {
PolicyDecision::Use(res) => res,
PolicyDecision::None => return Ok(None),
PolicyDecision::Reject(reason) => return Err(reason),
};
trace.emit_if(
"trim",