refactor(pattern2): move promotion decision into pattern2/api SSOT

Phase 263 P0.2: pattern2/api/ フォルダ化 - 入口SSOTの物理固定

目的: PromoteDecision/try_promote の参照点を1箇所に閉じ込めて、迷子防止

Changes:
1. 新規フォルダ構造:
   - pattern2/api/mod.rs - 入口SSOT(try_promote と PromoteDecision を再export)
   - pattern2/api/promote_decision.rs - PromoteDecision/PromoteStepResult 型定義
   - pattern2/api/promote_runner.rs - try_promote(...) 実装(SSOT entry point)
   - pattern2/mod.rs - api モジュールを公開

2. 移動/抽出:
   - PromoteDecision enum を promote_step_box.rs → pattern2/api/promote_decision.rs へ
   - promote_and_prepare_carriers を try_promote として pattern2/api/promote_runner.rs へ抽出

3. promote_step_box.rs を薄いラッパに縮退(35行, -177行):
   - pattern2::api::try_promote を呼び出すだけの互換用ラッパ
   - 将来的に削除予定(deprecated)

4. orchestrator を新API に書き換え:
   - use super::pattern2::api::{try_promote, PromoteDecision};
   - try_promote(...) を直接呼び出し

5. 参照確認:
   - rg で確認: すべての参照が pattern2::api 経由に収束 

検証:
- cargo test --lib: 1368/1368 PASS 
- quick smoke: 45/46 PASS (悪化なし)
- 参照点が pattern2/api に一本化され、迷子防止を物理的に保証
This commit is contained in:
2025-12-21 11:07:36 +09:00
parent abdb860e7e
commit e17902a443
7 changed files with 249 additions and 193 deletions

View File

@ -60,6 +60,7 @@ pub(in crate::mir::builder) mod escape_pattern_recognizer; // Phase 91 P5b
pub(in crate::mir::builder) mod common_init;
pub(in crate::mir::builder) mod loop_true_counter_extractor; // Phase 104: loop(true) counter extraction for Pattern2
pub(in crate::mir::builder) mod read_digits_break_condition_box; // Phase 104: break cond normalization for read_digits(loop(true))
pub(in crate::mir::builder) mod pattern2; // Phase 263 P0.2: Pattern2 module (api/ SSOT entry point)
pub(in crate::mir::builder) mod pattern2_break_condition_policy_router; // Phase 105: policy router box for Pattern2 break condition
pub(in crate::mir::builder) mod pattern2_policy_router; // Phase 108: unified Pattern2 policy router (balanced/read_digits/default)
pub(in crate::mir::builder) mod pattern2_inputs_facts_box; // Phase 105: Pattern2 input facts (analysis only)

View File

@ -0,0 +1,23 @@
//! Phase 263 P0.2: Pattern2 promotion API (entry point SSOT)
//!
//! This module is the single entry point for Pattern2 promotion logic.
//! All callers should use this module's exports instead of accessing internals.
//!
//! # Usage
//!
//! ```ignore
//! use super::pattern2::api::{try_promote, PromoteDecision};
//!
//! match try_promote(builder, condition, body, inputs, debug, verbose)? {
//! PromoteDecision::Promoted(result) => { /* ... */ }
//! PromoteDecision::NotApplicable => { /* fallback to next path */ }
//! PromoteDecision::Freeze(reason) => { /* fail-fast */ }
//! }
//! ```
mod promote_decision;
mod promote_runner;
// Re-export the SSOT types and functions
pub(in crate::mir::builder) use promote_decision::{PromoteDecision, PromoteStepResult};
pub(in crate::mir::builder) use promote_runner::try_promote;

View File

@ -0,0 +1,26 @@
//! Phase 263 P0.2: Promotion decision types (SSOT)
//!
//! PromoteDecision enum eliminates Option<_> wrapping ambiguity by making
//! the decision explicit. All Pattern2 promotion logic flows through this type.
use super::super::super::pattern2_inputs_facts_box::Pattern2Inputs;
pub(crate) struct PromoteStepResult {
pub inputs: Pattern2Inputs,
}
/// Phase 263 P0.1: Promotion decision for Pattern2 LoopBodyLocal handling
///
/// Eliminates Option<_> wrapping ambiguity by making decision explicit.
pub(crate) enum PromoteDecision {
/// Promotion succeeded - Pattern2 can proceed
Promoted(PromoteStepResult),
/// Pattern2 not applicable (e.g., reassigned LoopBodyLocal, no promotable pattern)
/// → Router should try next path (legacy binding, etc.)
NotApplicable,
/// Pattern2 should handle this but implementation is missing
/// → Fail-Fast with error message
Freeze(String),
}

View File

@ -0,0 +1,180 @@
//! Phase 263 P0.2: Pattern2 promotion runner (SSOT entry point)
//!
//! Single entry point for all Pattern2 promotion logic.
//! All callers should use `try_promote()` instead of accessing internals directly.
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use super::super::super::body_local_policy::{classify_for_pattern2, BodyLocalRoute};
use super::super::super::pattern2_inputs_facts_box::Pattern2Inputs;
use super::super::super::policies::PolicyDecision;
use super::promote_decision::{PromoteDecision, PromoteStepResult};
/// Phase 263 P0.2: Try to promote LoopBodyLocal variables for Pattern2
///
/// This is the single entry point for Pattern2 promotion logic.
/// Returns PromoteDecision to indicate success, applicability, or freeze.
pub(in crate::mir::builder) fn try_promote(
builder: &mut MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
inputs: Pattern2Inputs,
debug: bool,
verbose: bool,
) -> Result<PromoteDecision, String> {
let mut inputs = inputs;
use crate::mir::join_ir::lowering::digitpos_condition_normalizer::DigitPosConditionNormalizer;
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox;
let cond_scope = LoopConditionScopeBox::analyze(
&inputs.loop_var_name,
&vec![condition, &inputs.break_condition_node],
Some(&inputs.scope),
);
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() {
// Policy-controlled: some families must not run promotion/slot heuristics here.
// Example: balanced depth-scan uses derived vars and doesn't have a break-guard node.
if matches!(
inputs.body_local_handling,
crate::mir::builder::control_flow::joinir::patterns::pattern2_inputs_facts_box::BodyLocalHandlingPolicy::SkipPromotion
) {
// no-op: lowerers will populate LoopBodyLocalEnv via init/derived emission.
} else if !inputs.is_loop_true_read_digits {
match classify_for_pattern2(
builder,
&inputs.loop_var_name,
&inputs.scope,
&inputs.break_condition_node,
&cond_scope,
body,
) {
PolicyDecision::Use(BodyLocalRoute::Promotion {
promoted_carrier,
promoted_var,
carrier_name,
}) => {
let is_trim_promotion = promoted_carrier.trim_helper().is_some();
if !is_trim_promotion {
promoted_pairs.push((promoted_var.clone(), carrier_name.clone()));
}
#[cfg(feature = "normalized_dev")]
{
use crate::mir::join_ir::lowering::carrier_binding_assigner::CarrierBindingAssigner;
let mut promoted_carrier = promoted_carrier;
CarrierBindingAssigner::assign_promoted_binding(
builder,
&mut promoted_carrier,
&promoted_var,
&carrier_name,
)
.map_err(|e| format!("[phase78/binding_assign] {:?}", e))?;
inputs.carrier_info.merge_from(&promoted_carrier);
}
#[cfg(not(feature = "normalized_dev"))]
{
inputs.carrier_info.merge_from(&promoted_carrier);
}
inputs
.carrier_info
.promoted_loopbodylocals
.push(promoted_var.clone());
if !is_trim_promotion {
inputs.break_condition_node = DigitPosConditionNormalizer::normalize(
&inputs.break_condition_node,
&promoted_var,
&carrier_name,
);
}
}
PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)) => {
inputs.allowed_body_locals_for_conditions = vec![slot.name.clone()];
inputs.read_only_body_local_slot = Some(slot);
}
PolicyDecision::Reject(reason) => {
// Phase 263 P0.1: Reject を PromoteDecision で二分化(型安全)
if reason.contains("not_readonly")
|| reason.contains("No promotable pattern detected")
{
// 対象外: Pattern2 で処理できない形 → NotApplicable で後続経路へ
#[cfg(debug_assertions)]
{
eprintln!(
"[pattern2/api/promote] Pattern2 対象外LoopBodyLocal {:?}: {}. 後続経路へfallback.",
cond_body_local_vars, reason
);
}
return Ok(PromoteDecision::NotApplicable);
} else {
// 対象だが未対応freeze級: 実装バグ or 将来実装予定 → Freeze で Fail-Fast
return Ok(PromoteDecision::Freeze(format!(
"[pattern2/api/promote] Pattern2 未対応エラーLoopBodyLocal {:?}: {}",
cond_body_local_vars, reason
)));
}
}
PolicyDecision::None => {}
}
}
}
// Allocate join_ids for carriers and register bindings.
for carrier in &mut inputs.carrier_info.carriers {
let carrier_join_id = inputs.join_value_space.alloc_param();
carrier.join_id = Some(carrier_join_id);
#[cfg(feature = "normalized_dev")]
if let Some(binding_id) = carrier.binding_id {
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
match carrier.role {
CarrierRole::ConditionOnly => inputs.env.register_condition_binding(binding_id, carrier_join_id),
CarrierRole::LoopState => inputs.env.register_carrier_binding(binding_id, carrier_join_id),
}
}
}
for (promoted_var, promoted_carrier_name) in promoted_pairs {
let join_id = inputs
.carrier_info
.find_carrier(&promoted_carrier_name)
.and_then(|c| c.join_id)
.ok_or_else(|| format!("[phase229] promoted carrier '{}' has no join_id", promoted_carrier_name))?;
inputs.env.insert(promoted_var, join_id);
}
// ExprLowerer validation (best-effort; unchanged behavior)
{
use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer, ExprLoweringError};
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
let scope_manager = Pattern2ScopeManager {
condition_env: &inputs.env,
loop_body_local_env: Some(&inputs.body_local_env),
captured_env: Some(&inputs.captured_env),
carrier_info: &inputs.carrier_info,
};
match ExprLowerer::new(&scope_manager, ExprContext::Condition, builder)
.with_debug(debug)
.lower(&inputs.break_condition_node)
{
Ok(_) => {}
Err(ExprLoweringError::UnsupportedNode(_)) => {}
Err(_) => {}
}
}
Ok(PromoteDecision::Promoted(PromoteStepResult { inputs }))
}

View File

@ -0,0 +1,6 @@
//! Phase 263 P0.2: Pattern2 module structure
//!
//! This module organizes Pattern2 logic with a clear SSOT structure:
//! - `api/` - Public entry point for promotion logic (SSOT)
pub(in crate::mir::builder) mod api;

View File

@ -62,9 +62,9 @@ impl Pattern2LoweringOrchestrator {
let facts = GatherFactsStepBox::gather(builder, condition, body, fn_body, &ctx, verbose)?;
let inputs = ApplyPolicyStepBox::apply(condition, body, facts)?;
// Phase 263 P0.1: PromoteDecision で分岐を1箇所に固定型安全
use super::pattern2_steps::promote_step_box::PromoteDecision;
let mut inputs = match PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)? {
// Phase 263 P0.2: pattern2::api 経由で try_promote を呼び出し入口SSOT
use super::pattern2::api::{try_promote, PromoteDecision};
let mut inputs = match try_promote(builder, condition, body, inputs, debug, verbose)? {
PromoteDecision::Promoted(result) => {
result.inputs
}

View File

@ -1,40 +1,24 @@
//! PromoteStepBox (Phase 106)
//!
//! Phase 263 P0.2: This is now a thin wrapper over pattern2::api::try_promote.
//! All promotion logic has been moved to pattern2::api for SSOT.
//!
//! Responsibility:
//! - promotion / read-only slot routing for body-local vars in conditions
//! - carrier preparation for JoinIR emission
//! - Backward compatibility wrapper for existing callers
//! - Delegates to pattern2::api::try_promote for actual implementation
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::loop_pattern_detection::error_messages;
use super::super::body_local_policy::{classify_for_pattern2, BodyLocalRoute};
use super::super::pattern2::api::{try_promote, PromoteDecision, PromoteStepResult};
use super::super::pattern2_inputs_facts_box::Pattern2Inputs;
use super::super::policies::PolicyDecision;
pub(crate) struct PromoteStepResult {
pub inputs: Pattern2Inputs,
}
/// Phase 263 P0.1: Promotion decision for Pattern2 LoopBodyLocal handling
///
/// Eliminates Option<_> wrapping ambiguity by making decision explicit.
pub(crate) enum PromoteDecision {
/// Promotion succeeded - Pattern2 can proceed
Promoted(PromoteStepResult),
/// Pattern2 not applicable (e.g., reassigned LoopBodyLocal, no promotable pattern)
/// → Router should try next path (legacy binding, etc.)
NotApplicable,
/// Pattern2 should handle this but implementation is missing
/// → Fail-Fast with error message
Freeze(String),
}
pub(crate) struct PromoteStepBox;
impl PromoteStepBox {
/// Phase 263 P0.2: Thin wrapper over pattern2::api::try_promote
///
/// This method is deprecated. Use pattern2::api::try_promote directly.
pub(crate) fn run(
builder: &mut MirBuilder,
condition: &ASTNode,
@ -43,170 +27,6 @@ impl PromoteStepBox {
debug: bool,
verbose: bool,
) -> Result<PromoteDecision, String> {
Self::promote_and_prepare_carriers(builder, condition, body, inputs, debug, verbose)
try_promote(builder, condition, body, inputs, debug, verbose)
}
pub(in crate::mir::builder) fn promote_and_prepare_carriers(
builder: &mut MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
inputs: Pattern2Inputs,
debug: bool,
verbose: bool,
) -> Result<PromoteDecision, String> {
let mut inputs = inputs;
use crate::mir::join_ir::lowering::digitpos_condition_normalizer::DigitPosConditionNormalizer;
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox;
let cond_scope = LoopConditionScopeBox::analyze(
&inputs.loop_var_name,
&vec![condition, &inputs.break_condition_node],
Some(&inputs.scope),
);
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() {
// Policy-controlled: some families must not run promotion/slot heuristics here.
// Example: balanced depth-scan uses derived vars and doesn't have a break-guard node.
if matches!(
inputs.body_local_handling,
crate::mir::builder::control_flow::joinir::patterns::pattern2_inputs_facts_box::BodyLocalHandlingPolicy::SkipPromotion
) {
// no-op: lowerers will populate LoopBodyLocalEnv via init/derived emission.
} else if !inputs.is_loop_true_read_digits {
match classify_for_pattern2(
builder,
&inputs.loop_var_name,
&inputs.scope,
&inputs.break_condition_node,
&cond_scope,
body,
) {
PolicyDecision::Use(BodyLocalRoute::Promotion {
promoted_carrier,
promoted_var,
carrier_name,
}) => {
let is_trim_promotion = promoted_carrier.trim_helper().is_some();
if !is_trim_promotion {
promoted_pairs.push((promoted_var.clone(), carrier_name.clone()));
}
#[cfg(feature = "normalized_dev")]
{
use crate::mir::join_ir::lowering::carrier_binding_assigner::CarrierBindingAssigner;
let mut promoted_carrier = promoted_carrier;
CarrierBindingAssigner::assign_promoted_binding(
builder,
&mut promoted_carrier,
&promoted_var,
&carrier_name,
)
.map_err(|e| format!("[phase78/binding_assign] {:?}", e))?;
inputs.carrier_info.merge_from(&promoted_carrier);
}
#[cfg(not(feature = "normalized_dev"))]
{
inputs.carrier_info.merge_from(&promoted_carrier);
}
inputs
.carrier_info
.promoted_loopbodylocals
.push(promoted_var.clone());
if !is_trim_promotion {
inputs.break_condition_node = DigitPosConditionNormalizer::normalize(
&inputs.break_condition_node,
&promoted_var,
&carrier_name,
);
}
}
PolicyDecision::Use(BodyLocalRoute::ReadOnlySlot(slot)) => {
inputs.allowed_body_locals_for_conditions = vec![slot.name.clone()];
inputs.read_only_body_local_slot = Some(slot);
}
PolicyDecision::Reject(reason) => {
// Phase 263 P0.1: Reject を PromoteDecision で二分化(型安全)
if reason.contains("not_readonly")
|| reason.contains("No promotable pattern detected")
{
// 対象外: Pattern2 で処理できない形 → NotApplicable で後続経路へ
#[cfg(debug_assertions)]
{
eprintln!(
"[pattern2/promote_step] Pattern2 対象外LoopBodyLocal {:?}: {}. 後続経路へfallback.",
cond_body_local_vars, reason
);
}
return Ok(PromoteDecision::NotApplicable);
} else {
// 対象だが未対応freeze級: 実装バグ or 将来実装予定 → Freeze で Fail-Fast
return Ok(PromoteDecision::Freeze(format!(
"[pattern2/promote_step] Pattern2 未対応エラーLoopBodyLocal {:?}: {}",
cond_body_local_vars, reason
)));
}
}
PolicyDecision::None => {}
}
}
}
// Allocate join_ids for carriers and register bindings.
for carrier in &mut inputs.carrier_info.carriers {
let carrier_join_id = inputs.join_value_space.alloc_param();
carrier.join_id = Some(carrier_join_id);
#[cfg(feature = "normalized_dev")]
if let Some(binding_id) = carrier.binding_id {
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
match carrier.role {
CarrierRole::ConditionOnly => inputs.env.register_condition_binding(binding_id, carrier_join_id),
CarrierRole::LoopState => inputs.env.register_carrier_binding(binding_id, carrier_join_id),
}
}
}
for (promoted_var, promoted_carrier_name) in promoted_pairs {
let join_id = inputs
.carrier_info
.find_carrier(&promoted_carrier_name)
.and_then(|c| c.join_id)
.ok_or_else(|| format!("[phase229] promoted carrier '{}' has no join_id", promoted_carrier_name))?;
inputs.env.insert(promoted_var, join_id);
}
// ExprLowerer validation (best-effort; unchanged behavior)
{
use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer, ExprLoweringError};
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
let scope_manager = Pattern2ScopeManager {
condition_env: &inputs.env,
loop_body_local_env: Some(&inputs.body_local_env),
captured_env: Some(&inputs.captured_env),
carrier_info: &inputs.carrier_info,
};
match ExprLowerer::new(&scope_manager, ExprContext::Condition, builder)
.with_debug(debug)
.lower(&inputs.break_condition_node)
{
Ok(_) => {}
Err(ExprLoweringError::UnsupportedNode(_)) => {}
Err(_) => {}
}
}
Ok(PromoteDecision::Promoted(PromoteStepResult { inputs }))
}
}