refactor(control_flow): Phase 134 P0 - Normalization entry point SSOT

Consolidate dual entry points into unified NormalizationPlan system:

Problem:
- Dual entry points: try_normalized_shadow() + suffix_router_box
- ~220 lines of duplicated pattern detection logic
- Maintenance burden: changes required in two places

Solution:
- New normalization module with Box-First architecture
- NormalizationPlanBox: Single source of truth for pattern detection
- NormalizationExecuteBox: Single source of truth for execution

Implementation:
- src/mir/builder/control_flow/normalization/ (new module)
  - README.md: Design contract (SSOT)
  - plan.rs: NormalizationPlan data structure
  - plan_box.rs: Pattern detection (7 unit tests)
  - execute_box.rs: Execution logic
  - mod.rs: Module integration

Refactored files:
- routing.rs::try_normalized_shadow(): 165 → 87 lines (-78 lines)
- suffix_router_box::try_lower_loop_suffix(): 258 → 116 lines (-142 lines)

Pattern support maintained:
- Phase 131: loop(true) only (consumed: 1)
- Phase 132: loop + single post (consumed: 3)
- Phase 133: loop + multiple post (consumed: 2+N)

Box-First principles:
- Plan Box: Detection responsibility only
- Execute Box: Execution responsibility only
- README.md: Contract documentation (SSOT)
- Clear separation enables independent testing

Test results:
- cargo test --lib: 1186 PASS (10 new tests added)
- Phase 133 VM/LLVM EXE: PASS (exit code 6)
- Phase 132 LLVM EXE: PASS (exit code 3)
- Phase 131 LLVM EXE: PASS (exit code 1)

Benefits:
- Code duplication eliminated (~220 lines)
- Single source of truth for normalization decisions
- Improved maintainability and testability
- Future-proof extensibility (easy to add new patterns)

Default behavior unchanged: Dev-only guard maintained

Related: Phase 134 normalization infrastructure improvement

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-18 22:29:29 +09:00
parent ef71ea955c
commit f4ab5ca5f4
9 changed files with 1373 additions and 337 deletions

View File

@ -405,6 +405,8 @@ impl MirBuilder {
/// - Ok(Some(value_id)): Successfully lowered and merged via Normalized
/// - Ok(None): Out of scope (not a Normalized pattern)
/// - Err(msg): In scope but failed (Fail-Fast in strict mode)
///
/// Phase 134 P0: Unified with NormalizationPlanBox/ExecuteBox
fn try_normalized_shadow(
&mut self,
condition: &ASTNode,
@ -413,154 +415,83 @@ impl MirBuilder {
debug: bool,
) -> Result<Option<ValueId>, String> {
use crate::ast::Span;
use crate::mir::control_tree::normalized_shadow::available_inputs_collector::AvailableInputsCollectorBox;
use crate::mir::control_tree::normalized_shadow::StepTreeNormalizedShadowLowererBox;
use crate::mir::control_tree::StepTreeBuilderBox;
use crate::mir::builder::control_flow::normalization::{NormalizationPlanBox, NormalizationExecuteBox, PlanKind};
// Build StepTree from loop AST
// Build loop AST for pattern detection
let loop_ast = ASTNode::Loop {
condition: Box::new(condition.clone()),
body: body.to_vec(),
span: Span::unknown(),
};
let tree = StepTreeBuilderBox::build_from_ast(&loop_ast);
// Collect available inputs from MirBuilder state
let available_inputs = AvailableInputsCollectorBox::collect(self, None);
// Phase 134 P0: Delegate pattern detection to NormalizationPlanBox (SSOT)
// Convert loop to remaining format (single-element array)
let remaining = vec![loop_ast];
trace::trace().routing(
"router/normalized",
func_name,
&format!(
"Trying Normalized shadow lowering (available_inputs: {})",
available_inputs.len()
),
);
// Try Normalized lowering (loop(true) break-once pattern)
match StepTreeNormalizedShadowLowererBox::try_lower_if_only(&tree, &available_inputs) {
Ok(Some((join_module, join_meta))) => {
trace::trace().routing(
"router/normalized",
func_name,
&format!(
"Normalized lowering succeeded ({} functions)",
join_module.functions.len()
),
);
// Phase 131 P1.5: Create boundary with DirectValue mode
//
// Strategy (SSOT: merge owns remapper):
// 1. Create boundary with exit_bindings from meta
// 2. Set exit_reconnect_mode = DirectValue (no PHI generation)
// 3. Merge populates MergeResult.remapped_exit_values (JoinIR → Host ValueIds)
// 4. Use remapped_exit_values for direct variable_map reconnection
use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, ExitReconnectMode};
use crate::mir::join_ir::lowering::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
// Build exit_bindings from meta
let exit_bindings: Vec<LoopExitBinding> = join_meta
.exit_meta
.exit_values
.iter()
.map(|(carrier_name, join_exit_value)| {
// Get host_slot from variable_map
let host_slot = self
.variable_ctx
.variable_map
.get(carrier_name)
.copied()
.unwrap_or_else(|| {
panic!(
"[Phase 131 P1.5] Carrier '{}' not in variable_map (available: {:?})",
carrier_name,
self.variable_ctx.variable_map.keys().collect::<Vec<_>>()
)
});
LoopExitBinding {
carrier_name: carrier_name.clone(),
join_exit_value: *join_exit_value,
host_slot,
role: CarrierRole::LoopState,
}
})
.collect();
// Create boundary with DirectValue mode
let mut boundary = JoinInlineBoundary::new_with_exit_bindings(
vec![], // No join_inputs for Normalized
vec![], // No host_inputs for Normalized
exit_bindings,
);
boundary.exit_reconnect_mode = ExitReconnectMode::DirectValue; // Phase 131 P1.5: No PHI
boundary.continuation_func_ids = join_meta.continuation_funcs.clone();
// Merge with boundary - this will populate MergeResult.remapped_exit_values
use crate::mir::builder::control_flow::joinir::merge;
use crate::mir::join_ir_vm_bridge::bridge_joinir_to_mir_with_meta;
use crate::mir::join_ir::frontend::JoinFuncMetaMap;
use std::collections::BTreeMap;
let empty_meta: JoinFuncMetaMap = BTreeMap::new();
let mir_module = bridge_joinir_to_mir_with_meta(&join_module, &empty_meta)
.map_err(|e| format!("[normalized/pipeline] MIR conversion failed: {:?}", e))?;
// Merge with boundary - this populates MergeResult.remapped_exit_values
// and calls ExitLineOrchestrator with DirectValue mode
let _exit_phi_result = merge::merge_joinir_mir_blocks(
self,
&mir_module,
Some(&boundary),
debug,
)?;
trace::trace().routing(
"router/normalized",
func_name,
&format!(
"Normalized merge + reconnection completed ({} exit bindings)",
boundary.exit_bindings.len()
),
);
// Phase 131 P1.5: Loop executed successfully, return void constant
use crate::mir::{ConstValue, MirInstruction};
let void_id = self.next_value_id();
self.emit_instruction(MirInstruction::Const {
dst: void_id,
value: ConstValue::Void,
})?;
Ok(Some(void_id))
let plan = match NormalizationPlanBox::plan_block_suffix(self, &remaining, func_name, debug)? {
Some(plan) => plan,
None => {
if debug {
trace::trace().routing(
"router/normalized",
func_name,
"NormalizationPlanBox returned None (not a normalized pattern)",
);
}
return Ok(None);
}
Ok(None) => {
// Out of scope (not a Normalized pattern)
trace::trace().routing(
"router/normalized",
func_name,
"Normalized lowering: out of scope",
);
Ok(None)
};
// Only handle loop-only patterns here
// (suffix patterns with post-statements go through suffix_router_box)
match &plan.kind {
PlanKind::LoopOnly => {
if debug {
trace::trace().routing(
"router/normalized",
func_name,
"Loop-only pattern detected, proceeding with normalization",
);
}
}
PlanKind::LoopWithPost { .. } => {
// This should not happen in try_normalized_shadow context
// (post patterns should be caught by suffix_router_box earlier)
if debug {
trace::trace().routing(
"router/normalized",
func_name,
"Loop+post pattern in try_normalized_shadow (unexpected, using legacy)",
);
}
return Ok(None);
}
}
// Phase 134 P0: Delegate execution to NormalizationExecuteBox (SSOT)
match NormalizationExecuteBox::execute(self, &plan, &remaining, func_name, debug) {
Ok(value_id) => {
if debug {
trace::trace().routing(
"router/normalized",
func_name,
"Normalization succeeded",
);
}
Ok(Some(value_id))
}
Err(e) => {
// In scope but failed - Fail-Fast in strict mode
let msg = format!(
"Phase 131/normalized: Failed to lower loop(true) break-once pattern in '{}': {}",
func_name, e
);
if crate::config::env::joinir_dev::strict_enabled() {
use crate::mir::join_ir::lowering::error_tags;
return Err(error_tags::freeze_with_hint(
"phase131/normalized_loop/internal",
"phase134/routing/normalized",
&e,
"Loop should be supported by Normalized but conversion failed. \
"Loop should be supported by Normalized but execution failed. \
Check that condition is Bool(true) and body ends with break.",
));
}
trace::trace().routing("router/normalized/error", func_name, &msg);
Ok(None) // Non-strict: fall back to existing patterns
trace::trace().routing("router/normalized/error", func_name, &e);
Ok(None) // Non-strict: fallback
}
}
}