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

@ -0,0 +1,203 @@
# Normalization Entry Point Consolidation (Phase 134 P0)
**Date**: 2025-12-18
**Status**: In Progress
**Scope**: Unified entry point for Normalized shadow detection and execution
---
## Purpose
Consolidate the two separate entry points for Normalized shadow processing into a single, well-defined system:
1. **Before**: Dual entry points with scattered responsibility
- `try_normalized_shadow()` in routing.rs (loop-only)
- `suffix_router_box` in patterns/policies/ (loop + post statements)
- Decision logic ("what to lower") is duplicated and inconsistent
2. **After**: Single decision point using Box-First architecture
- **NormalizationPlanBox**: Detects pattern and plans consumption (SSOT for "what")
- **NormalizationExecuteBox**: Executes the plan (SSOT for "how")
- Both entry points use the same PlanBox for consistent decisions
---
## Architecture
### Entry Points
**Two callers, one SSOT decision**:
- `routing.rs::try_normalized_shadow()`: Loop-only patterns (Phase 131)
- `suffix_router_box::try_lower_loop_suffix()`: Loop + post patterns (Phase 132-133)
- Both call `NormalizationPlanBox::plan_block_suffix()` for detection
### Box Responsibilities
1. **NormalizationPlanBox** (`plan_box.rs`)
- Responsibility: Pattern detection and planning
- API: `plan_block_suffix(builder, remaining, func_name, debug) -> Result<Option<NormalizationPlan>>`
- Returns: Plan with consumed count and kind, or None if not applicable
2. **NormalizationExecuteBox** (`execute_box.rs`)
- Responsibility: Execute the plan (StepTree build, lowering, merge)
- API: `execute(builder, plan, remaining, func_name, debug) -> Result<()>`
- Uses: StepTreeBuilderBox, NormalizedShadowLowererBox, merge logic
### Data Structures
**NormalizationPlan** (`plan.rs`):
```rust
pub struct NormalizationPlan {
pub consumed: usize, // Number of statements to consume from remaining
pub kind: PlanKind, // What to lower
pub requires_return: bool, // Whether pattern includes return (unreachable detection)
}
pub enum PlanKind {
LoopOnly, // Phase 131: loop(true) { ... break } alone
LoopWithPost { // Phase 132-133: loop + post assigns + return
post_assign_count: usize,
},
}
```
---
## Design Principles
### Box-First
- **Plan Box**: Single responsibility for detection (no execution)
- **Execute Box**: Single responsibility for execution (no detection)
- Clear separation enables independent testing and evolution
### SSOT (Single Source of Truth)
- **Entry**: NormalizationPlanBox is the only place that decides normalization applicability
- **Contract**: Documented in this README (normative SSOT)
- **No duplication**: Suffix router and routing.rs both delegate to the same PlanBox
### Fail-Fast
- Invalid patterns return `Err` (not silent fallback)
- STRICT mode (JOINIR_DEV_STRICT) treats contract violations as panics
- Clear error messages with hints for debugging
### Legacy Preservation
- Existing behavior unchanged (dev-only guard)
- Non-normalized patterns return `Ok(None)` → legacy fallback
- No breaking changes to existing smokes
---
## Pattern Detection (Phase 131-133)
### Phase 131: Loop-Only
- Pattern: `loop(true) { ... break }`
- Consumed: 1 statement
- Kind: `PlanKind::LoopOnly`
- Example: `loop(true) { x = 1; break }; return x`
### Phase 132: Loop + Single Post
- Pattern: `loop(true) { ... break }; <assign>; return <expr>`
- Consumed: 3 statements (loop, assign, return)
- Kind: `PlanKind::LoopWithPost { post_assign_count: 1 }`
- Example: `loop(true) { x = 1; break }; x = x + 2; return x`
### Phase 133: Loop + Multiple Post
- Pattern: `loop(true) { ... break }; <assign>+; return <expr>`
- Consumed: 2 + N statements (loop, N assigns, return)
- Kind: `PlanKind::LoopWithPost { post_assign_count: N }`
- Example: `loop(true) { x = 1; break }; x = x + 2; x = x + 3; return x`
---
## Contract
### NormalizationPlanBox::plan_block_suffix()
**Inputs**:
- `builder`: Current MirBuilder state (for variable_map access)
- `remaining`: Block suffix to analyze (AST statements)
- `func_name`: Function name (for tracing)
- `debug`: Enable debug logging
**Returns**:
- `Ok(Some(plan))`: Pattern detected, plan specifies what to do
- `Ok(None)`: Not a normalized pattern, use legacy fallback
- `Err(msg)`: Internal error (should not happen in well-formed AST)
**Invariants**:
- `consumed <= remaining.len()` (never consume more than available)
- If `requires_return` is true, `remaining[consumed-1]` must be Return
- Post-assign patterns require `consumed >= 3` (loop + assign + return minimum)
### NormalizationExecuteBox::execute()
**Inputs**:
- `builder`: Current MirBuilder state (mutable, will be modified)
- `plan`: Normalization plan from PlanBox
- `remaining`: Same AST slice used for planning
- `func_name`: Function name (for tracing)
- `debug`: Enable debug logging
**Returns**:
- `Ok(())`: Successfully executed and merged
- `Err(msg)`: Lowering or merge failed
**Side Effects**:
- Modifies `builder` state (adds blocks, instructions, PHI)
- May emit return statement (for suffix patterns)
- Updates variable_map with exit values (DirectValue mode)
---
## Integration Points
### routing.rs
- `try_normalized_shadow()`: Call PlanBox, if LoopOnly → ExecuteBox, return ValueId
- Legacy path: If PlanBox returns None, continue with existing fallback
### suffix_router_box.rs
- `try_lower_loop_suffix()`: Call PlanBox, if LoopWithPost → ExecuteBox, return consumed
- Legacy path: If PlanBox returns None, return None (existing behavior)
### build_block() (stmts.rs)
- Existing while loop unchanged
- After suffix_router returns consumed, advance idx by that amount
- No change to default behavior
---
## Testing Strategy
### Unit Tests (plan_box.rs)
- Phase 131 pattern detection (loop-only)
- Phase 132 pattern detection (loop + single post)
- Phase 133 pattern detection (loop + multiple post)
- Return boundary detection (consumed stops at return)
- Non-matching patterns (returns None)
### Regression Smokes
- Phase 133: `phase133_loop_true_break_once_post_multi_add_{vm,llvm_exe}.sh`
- Phase 132: `phase132_loop_true_break_once_post_add_llvm_exe.sh`
- Phase 131: `phase131_loop_true_break_once_{vm,llvm_exe}.sh`
- Phase 97: `phase97_next_non_ws_llvm_exe.sh`
---
## Acceptance Criteria
- Entry SSOT: This README documents the normative contract
- By-name avoidance: Uses boundary contract SSOT (no variable name guessing)
- cargo test --lib: All unit tests PASS
- Phase 133 smokes: 2/2 PASS (VM + LLVM EXE)
- Phase 131/132 regression: PASS
- Phase 97 regression: PASS
- Default behavior unchanged (dev-only guard)
---
## References
- **Normalized shadow**: `src/mir/control_tree/normalized_shadow/`
- **Boundary contract**: `src/mir/join_ir/lowering/inline_boundary.rs`
- **StepTree**: `src/mir/control_tree/step_tree/`
- **Merge logic**: `src/mir/builder/control_flow/joinir/merge/`

View File

@ -0,0 +1,313 @@
//! NormalizationExecuteBox: Execute normalization plan (Phase 134 P0)
//!
//! ## Responsibility
//!
//! - Execute a NormalizationPlan from PlanBox
//! - Build StepTree, lower to JoinIR, merge into MIR
//! - SSOT for "how to execute" normalization
//!
//! ## Contract
//!
//! - Modifies builder state (adds blocks, instructions, updates variable_map)
//! - Uses DirectValue mode (no PHI generation)
//! - Returns Ok(()) on success, Err(_) on failure
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
use super::plan::{NormalizationPlan, PlanKind};
/// Box-First: Execute normalization plan
pub struct NormalizationExecuteBox;
impl NormalizationExecuteBox {
/// Execute a normalization plan
///
/// Returns:
/// - Ok(value_id): Successfully executed, returns result value
/// - Err(_): Lowering or merge failed
pub fn execute(
builder: &mut MirBuilder,
plan: &NormalizationPlan,
remaining: &[ASTNode],
func_name: &str,
debug: bool,
) -> Result<ValueId, String> {
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
if debug {
trace.routing(
"normalization/execute",
func_name,
&format!(
"Executing plan: kind={:?}, consumed={}",
plan.kind, plan.consumed
),
);
}
// Validate consumed vs remaining
if plan.consumed > remaining.len() {
return Err(format!(
"[normalization/execute] Plan wants to consume {} statements but only {} available",
plan.consumed,
remaining.len()
));
}
match &plan.kind {
PlanKind::LoopOnly => {
Self::execute_loop_only(builder, remaining, func_name, debug)
}
PlanKind::LoopWithPost { .. } => {
Self::execute_loop_with_post(builder, plan, remaining, func_name, debug)
}
}
}
/// Execute Phase 131: Loop-only pattern
fn execute_loop_only(
builder: &mut MirBuilder,
remaining: &[ASTNode],
func_name: &str,
debug: bool,
) -> Result<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;
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
// Build StepTree from loop AST
let loop_ast = if let ASTNode::Loop { condition, body, .. } = &remaining[0] {
ASTNode::Loop {
condition: condition.clone(),
body: body.clone(),
span: Span::unknown(),
}
} else {
return Err("[normalization/execute] First statement is not a loop".to_string());
};
let tree = StepTreeBuilderBox::build_from_ast(&loop_ast);
// Collect available inputs
let available_inputs = AvailableInputsCollectorBox::collect(builder, None);
if debug {
trace.routing(
"normalization/execute/loop_only",
func_name,
&format!("Available inputs: {}", available_inputs.len()),
);
}
// Try Normalized lowering
let (join_module, join_meta) =
match StepTreeNormalizedShadowLowererBox::try_lower_if_only(&tree, &available_inputs) {
Ok(Some(result)) => result,
Ok(None) => {
return Err(
"[normalization/execute] StepTree lowering returned None (out of scope)"
.to_string(),
);
}
Err(e) => {
if crate::config::env::joinir_dev::strict_enabled() {
use crate::mir::join_ir::lowering::error_tags;
return Err(error_tags::freeze_with_hint(
"phase134/normalization/loop_only",
&e,
"Loop should be supported by Normalized but lowering failed",
));
}
return Err(format!("[normalization/execute] Lowering failed: {}", e));
}
};
// Merge JoinIR into MIR
Self::merge_normalized_joinir(builder, join_module, join_meta, func_name, debug)?;
// Return void constant (loop doesn't produce a value)
use crate::mir::{ConstValue, MirInstruction};
let void_id = builder.next_value_id();
builder.emit_instruction(MirInstruction::Const {
dst: void_id,
value: ConstValue::Void,
})?;
Ok(void_id)
}
/// Execute Phase 132-133: Loop + post assignments + return
fn execute_loop_with_post(
builder: &mut MirBuilder,
plan: &NormalizationPlan,
remaining: &[ASTNode],
func_name: &str,
debug: bool,
) -> Result<ValueId, String> {
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;
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
// Build StepTree from the suffix (loop + assigns + return)
let suffix = &remaining[..plan.consumed];
let step_tree = StepTreeBuilderBox::build_from_block(suffix);
// Collect available inputs
let available_inputs = AvailableInputsCollectorBox::collect(builder, None);
if debug {
trace.routing(
"normalization/execute/loop_with_post",
func_name,
&format!(
"Suffix: {} statements, available inputs: {}",
suffix.len(),
available_inputs.len()
),
);
}
// Try Normalized lowering
let (join_module, join_meta) =
match StepTreeNormalizedShadowLowererBox::try_lower_if_only(&step_tree, &available_inputs) {
Ok(Some(result)) => result,
Ok(None) => {
return Err(
"[normalization/execute] StepTree lowering returned None (out of scope)"
.to_string(),
);
}
Err(e) => {
if crate::config::env::joinir_dev::strict_enabled() {
use crate::mir::join_ir::lowering::error_tags;
return Err(error_tags::freeze_with_hint(
"phase134/normalization/loop_with_post",
&e,
"Loop+post should be supported by Normalized but lowering failed",
));
}
return Err(format!("[normalization/execute] Lowering failed: {}", e));
}
};
// Merge JoinIR into MIR
Self::merge_normalized_joinir(builder, join_module, join_meta, func_name, debug)?;
// For suffix patterns, emit the return statement
// (JoinIR merge converts fragment Returns to Jump, so we need host-level return)
if plan.requires_return {
if let Some(ASTNode::Return { value, .. }) = suffix.last() {
let _ = builder.build_return_statement(value.clone())?;
}
}
// Return void constant
use crate::mir::{ConstValue, MirInstruction};
let void_id = builder.next_value_id();
builder.emit_instruction(MirInstruction::Const {
dst: void_id,
value: ConstValue::Void,
})?;
Ok(void_id)
}
/// Merge Normalized JoinModule into MIR builder
///
/// Extracted from routing.rs and suffix_router_box.rs
fn merge_normalized_joinir(
builder: &mut MirBuilder,
join_module: crate::mir::join_ir::JoinModule,
join_meta: crate::mir::join_ir::lowering::carrier_info::JoinFragmentMeta,
func_name: &str,
debug: bool,
) -> Result<(), String> {
use crate::mir::builder::control_flow::joinir::merge;
use crate::mir::join_ir::frontend::JoinFuncMetaMap;
use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, ExitReconnectMode};
use crate::mir::join_ir::lowering::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
use crate::mir::join_ir_vm_bridge::bridge_joinir_to_mir_with_meta;
use std::collections::BTreeMap;
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
// 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 = builder
.variable_ctx
.variable_map
.get(carrier_name)
.copied()
.unwrap_or_else(|| {
panic!(
"[Phase 134 P0] Carrier '{}' not in variable_map (available: {:?})",
carrier_name,
builder.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; // No PHI
boundary.continuation_func_ids = join_meta.continuation_funcs.clone();
if debug {
trace.routing(
"normalization/execute/merge",
func_name,
&format!(
"Merging JoinModule: {} functions, {} exit bindings",
join_module.functions.len(),
boundary.exit_bindings.len()
),
);
}
// Bridge JoinIR to MIR
let empty_meta: JoinFuncMetaMap = BTreeMap::new();
let mir_module = bridge_joinir_to_mir_with_meta(&join_module, &empty_meta)
.map_err(|e| format!("[normalization/execute] MIR conversion failed: {:?}", e))?;
// Merge with boundary
let _exit_phi_result = merge::merge_joinir_mir_blocks(
builder,
&mir_module,
Some(&boundary),
debug,
)?;
if debug {
trace.routing(
"normalization/execute/merge",
func_name,
"Merge + reconnection completed",
);
}
Ok(())
}
}

View File

@ -0,0 +1,25 @@
//! Normalization entry point consolidation (Phase 134 P0)
//!
//! ## Purpose
//!
//! Consolidate the dual entry points for Normalized shadow processing:
//! - `routing.rs::try_normalized_shadow()` (loop-only)
//! - `suffix_router_box::try_lower_loop_suffix()` (loop + post)
//!
//! Both now use the same NormalizationPlanBox for pattern detection.
//!
//! ## Architecture
//!
//! - **NormalizationPlanBox**: SSOT for "what to normalize" decision
//! - **NormalizationExecuteBox**: SSOT for "how to execute" normalization
//! - **NormalizationPlan**: Data structure for plan details
//!
//! See README.md for full design and contract documentation.
mod plan;
mod plan_box;
mod execute_box;
pub use plan::{NormalizationPlan, PlanKind};
pub use plan_box::NormalizationPlanBox;
pub use execute_box::NormalizationExecuteBox;

View File

@ -0,0 +1,87 @@
//! Normalization plan data structures (Phase 134 P0)
//!
//! Defines what to normalize and how many statements to consume.
/// Plan for normalizing a block suffix
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizationPlan {
/// Number of statements to consume from remaining block
pub consumed: usize,
/// What kind of normalization to perform
pub kind: PlanKind,
/// Whether the pattern includes an explicit return (for unreachable detection)
pub requires_return: bool,
}
/// Kind of normalization pattern detected
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanKind {
/// Phase 131: loop(true) { ... break } alone
///
/// Pattern: Single loop statement
/// Example: `loop(true) { x = 1; break }`
LoopOnly,
/// Phase 132-133: loop(true) + post assignments + return
///
/// Pattern: loop + N assignments + return
/// Example: `loop(true) { x = 1; break }; x = x + 2; return x`
LoopWithPost {
/// Number of post-loop assignment statements
post_assign_count: usize,
},
}
impl NormalizationPlan {
/// Create a Phase 131 plan (loop-only)
pub fn loop_only() -> Self {
Self {
consumed: 1,
kind: PlanKind::LoopOnly,
requires_return: false,
}
}
/// Create a Phase 132-133 plan (loop + post assignments + return)
pub fn loop_with_post(post_assign_count: usize) -> Self {
// consumed = 1 (loop) + N (assigns) + 1 (return)
let consumed = 1 + post_assign_count + 1;
Self {
consumed,
kind: PlanKind::LoopWithPost { post_assign_count },
requires_return: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_loop_only_plan() {
let plan = NormalizationPlan::loop_only();
assert_eq!(plan.consumed, 1);
assert_eq!(plan.kind, PlanKind::LoopOnly);
assert!(!plan.requires_return);
}
#[test]
fn test_loop_with_post_single() {
let plan = NormalizationPlan::loop_with_post(1);
assert_eq!(plan.consumed, 3); // loop + 1 assign + return
assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 1 });
assert!(plan.requires_return);
}
#[test]
fn test_loop_with_post_multiple() {
let plan = NormalizationPlan::loop_with_post(2);
assert_eq!(plan.consumed, 4); // loop + 2 assigns + return
assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 2 });
assert!(plan.requires_return);
}
}

View File

@ -0,0 +1,354 @@
//! NormalizationPlanBox: Pattern detection for Normalized shadow (Phase 134 P0)
//!
//! ## Responsibility
//!
//! - Detect block suffix patterns that can be normalized
//! - Return plan specifying what to consume and how to lower
//! - SSOT for "what to normalize" decision
//!
//! ## Contract
//!
//! - Returns Ok(Some(plan)): Pattern detected, proceed with normalization
//! - Returns Ok(None): Not a normalized pattern, use legacy fallback
//! - Returns Err(_): Internal error (malformed AST)
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use super::plan::{NormalizationPlan, PlanKind};
/// Box-First: Pattern detection for Normalized shadow
pub struct NormalizationPlanBox;
impl NormalizationPlanBox {
/// Detect Normalized pattern in block suffix
///
/// Returns:
/// - Ok(Some(plan)): Normalized pattern detected
/// - Ok(None): Not a normalized pattern (use legacy)
/// - Err(_): Internal error
pub fn plan_block_suffix(
_builder: &MirBuilder,
remaining: &[ASTNode],
func_name: &str,
debug: bool,
) -> Result<Option<NormalizationPlan>, String> {
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
if debug {
trace.routing(
"normalization/plan",
func_name,
&format!("Checking {} remaining statements", remaining.len()),
);
}
// Empty suffix - not normalized
if remaining.is_empty() {
return Ok(None);
}
// First statement must be a loop
let is_loop = matches!(&remaining[0], ASTNode::Loop { .. });
if !is_loop {
return Ok(None);
}
// Phase 131: Loop-only pattern (single statement)
if remaining.len() == 1 {
if debug {
trace.routing(
"normalization/plan",
func_name,
"Detected Phase 131 pattern: loop-only",
);
}
return Ok(Some(NormalizationPlan::loop_only()));
}
// Check if second statement is an assignment (Phase 132-133 specific)
let has_post_assignment = matches!(&remaining[1], ASTNode::Assignment { .. });
if !has_post_assignment {
// Not a post-assignment pattern, treat as loop-only if it's just a loop
// (if there are other statements after the loop that aren't assignments,
// this is not our pattern - return None)
if debug {
trace.routing(
"normalization/plan",
func_name,
"No post-assignment after loop, treating as loop-only",
);
}
return Ok(Some(NormalizationPlan::loop_only()));
}
// Phase 132-133: Loop + post assignments + return
// Minimum: loop + 1 assign + return (3 statements)
if remaining.len() < 3 {
// Has assignment after loop but no return - not a valid pattern
if debug {
trace.routing(
"normalization/plan",
func_name,
&format!(
"Loop + assignment but no return ({} statements), not a valid pattern",
remaining.len()
),
);
}
return Ok(None);
}
// Count consecutive assignments after the loop
let mut post_assign_count = 0;
for i in 1..remaining.len() {
if matches!(&remaining[i], ASTNode::Assignment { .. }) {
post_assign_count += 1;
} else {
break;
}
}
// After assignments, we need a return statement
let return_index = 1 + post_assign_count;
if return_index >= remaining.len() {
// No return statement - cannot use post pattern
if debug {
trace.routing(
"normalization/plan",
func_name,
&format!(
"No return after {} assignments, not a valid post pattern",
post_assign_count
),
);
}
return Ok(None);
}
let has_return = matches!(&remaining[return_index], ASTNode::Return { .. });
if !has_return {
// Statement after assignments is not return - not a post pattern
if debug {
trace.routing(
"normalization/plan",
func_name,
&format!(
"Statement after {} assignments is not return, not a valid post pattern",
post_assign_count
),
);
}
return Ok(None);
}
// Valid Phase 132-133 pattern: loop + N assignments + return
if debug {
trace.routing(
"normalization/plan",
func_name,
&format!(
"Detected Phase 132-133 pattern: loop + {} post assigns + return",
post_assign_count
),
);
}
Ok(Some(NormalizationPlan::loop_with_post(post_assign_count)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
fn make_span() -> Span {
Span::unknown()
}
fn make_loop() -> ASTNode {
ASTNode::Loop {
condition: Box::new(ASTNode::Literal {
value: LiteralValue::Bool(true),
span: make_span(),
}),
body: vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: make_span(),
}),
value: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: make_span(),
}),
span: make_span(),
},
ASTNode::Break { span: make_span() },
],
span: make_span(),
}
}
fn make_assignment(var: &str, value: i64) -> ASTNode {
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: var.to_string(),
span: make_span(),
}),
value: Box::new(ASTNode::BinaryOp {
left: Box::new(ASTNode::Variable {
name: var.to_string(),
span: make_span(),
}),
operator: BinaryOperator::Add,
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(value),
span: make_span(),
}),
span: make_span(),
}),
span: make_span(),
}
}
fn make_return(var: &str) -> ASTNode {
ASTNode::Return {
value: Some(Box::new(ASTNode::Variable {
name: var.to_string(),
span: make_span(),
})),
span: make_span(),
}
}
#[test]
fn test_plan_block_suffix_phase131_loop_only() {
use crate::mir::builder::MirBuilder;
let remaining = vec![make_loop()];
let builder = MirBuilder::new();
let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false)
.expect("Should not error");
assert!(plan.is_some());
let plan = plan.unwrap();
assert_eq!(plan.consumed, 1);
assert_eq!(plan.kind, PlanKind::LoopOnly);
assert!(!plan.requires_return);
}
#[test]
fn test_plan_block_suffix_phase132_loop_post_single() {
use crate::mir::builder::MirBuilder;
let remaining = vec![
make_loop(),
make_assignment("x", 2),
make_return("x"),
];
let builder = MirBuilder::new();
let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false)
.expect("Should not error");
assert!(plan.is_some());
let plan = plan.unwrap();
assert_eq!(plan.consumed, 3); // loop + 1 assign + return
assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 1 });
assert!(plan.requires_return);
}
#[test]
fn test_plan_block_suffix_phase133_loop_post_multi() {
use crate::mir::builder::MirBuilder;
let remaining = vec![
make_loop(),
make_assignment("x", 2),
make_assignment("x", 3),
make_return("x"),
];
let builder = MirBuilder::new();
let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false)
.expect("Should not error");
assert!(plan.is_some());
let plan = plan.unwrap();
assert_eq!(plan.consumed, 4); // loop + 2 assigns + return
assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 2 });
assert!(plan.requires_return);
}
#[test]
fn test_plan_block_suffix_return_boundary() {
use crate::mir::builder::MirBuilder;
// Pattern with unreachable statement after return
let remaining = vec![
make_loop(),
make_assignment("x", 2),
make_return("x"),
make_assignment("y", 999), // Unreachable
];
let builder = MirBuilder::new();
let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false)
.expect("Should not error");
assert!(plan.is_some());
let plan = plan.unwrap();
// Should consume only up to return (not the unreachable statement)
assert_eq!(plan.consumed, 3); // loop + 1 assign + return
assert_eq!(plan.kind, PlanKind::LoopWithPost { post_assign_count: 1 });
}
#[test]
fn test_plan_block_suffix_no_match_empty() {
use crate::mir::builder::MirBuilder;
let remaining: Vec<ASTNode> = vec![];
let builder = MirBuilder::new();
let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false)
.expect("Should not error");
assert!(plan.is_none());
}
#[test]
fn test_plan_block_suffix_no_match_not_loop() {
use crate::mir::builder::MirBuilder;
let remaining = vec![
make_assignment("x", 1),
make_return("x"),
];
let builder = MirBuilder::new();
let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false)
.expect("Should not error");
assert!(plan.is_none());
}
#[test]
fn test_plan_block_suffix_no_match_no_return() {
use crate::mir::builder::MirBuilder;
let remaining = vec![
make_loop(),
make_assignment("x", 2),
// Missing return
];
let builder = MirBuilder::new();
let plan = NormalizationPlanBox::plan_block_suffix(&builder, &remaining, "test", false)
.expect("Should not error");
// No return means not a valid post pattern
assert!(plan.is_none());
}
}