diff --git a/src/mir/builder/control_flow/joinir/patterns/mod.rs b/src/mir/builder/control_flow/joinir/patterns/mod.rs index 79d026fd..b1c1903d 100644 --- a/src/mir/builder/control_flow/joinir/patterns/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/mod.rs @@ -59,6 +59,7 @@ pub(in crate::mir::builder) mod read_digits_break_condition_box; // Phase 104: b 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_inputs_facts_box; // Phase 105: Pattern2 input facts (analysis only) pub(in crate::mir::builder) mod pattern2_lowering_orchestrator; // Phase 105: Pattern2 orchestration (wiring/emission) +pub(in crate::mir::builder) mod pattern2_steps; // Phase 106: Pattern2 step boxes (pipeline SSOT) pub(in crate::mir::builder) mod condition_env_builder; pub(in crate::mir::builder) mod conversion_pipeline; pub(in crate::mir::builder) mod exit_binding; diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs index df8d58d1..e1020002 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_inputs_facts_box.rs @@ -1,20 +1,17 @@ //! Pattern2 input analysis (facts collection only) //! -//! This box collects everything Pattern2 lowering needs *without* emitting JoinIR: +//! This box collects everything Pattern2 lowering needs *without* emitting JoinIR +//! and *without* applying policy routing: //! - capture/pinned local analysis //! - mutable accumulator promotion into carriers //! - condition env + JoinValueSpace initialization -//! - break-condition routing (policy SSOT) -//! - (optional) body-local allow-list + slot metadata (Phase 92 P3) //! -//! The goal is to keep orchestration/emission code out of this module. - -use super::pattern2_break_condition_policy_router::Pattern2BreakConditionPolicyRouterBox; +//! Policy routing (break condition normalization + allow-list) is a separate step +//! in Phase 106 (`pattern2_steps::ApplyPolicyStepBox`). 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::carrier_info::CarrierInfo; use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEnv}; -use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlot; 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; @@ -36,13 +33,25 @@ impl Pattern2DebugLog { } } - pub(crate) fn log(&self, tag: &str, message: impl AsRef) { +pub(crate) fn log(&self, tag: &str, message: impl AsRef) { if self.verbose { self.debug.log(tag, message.as_ref()); } } } +pub(in crate::mir::builder) struct Pattern2Facts { + pub loop_var_name: String, + pub loop_var_id: ValueId, + pub carrier_info: CarrierInfo, + pub scope: LoopScopeShape, + pub captured_env: CapturedEnv, + pub join_value_space: JoinValueSpace, + pub env: ConditionEnv, + pub condition_bindings: Vec, + pub body_local_env: LoopBodyLocalEnv, +} + pub(in crate::mir::builder) struct Pattern2Inputs { pub loop_var_name: String, pub loop_var_id: ValueId, @@ -57,12 +66,10 @@ pub(in crate::mir::builder) struct Pattern2Inputs { /// This must stay minimal (1 variable) and is validated by ReadOnlyBodyLocalSlotBox. pub allowed_body_locals_for_conditions: Vec, /// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable. - pub read_only_body_local_slot: Option, + pub read_only_body_local_slot: Option, + /// Policy-routed "break when true" condition node. pub break_condition_node: ASTNode, /// loop(true) + break-only digits(read_digits_from family) - /// - /// Policy-routed: multiple breaks are normalized into a single `break when true` - /// condition, and Pattern2 must schedule body-init before break check. pub is_loop_true_read_digits: bool, /// Phase 93 P0: ConditionOnly recipe for derived slot recalculation pub condition_only_recipe: Option< @@ -83,7 +90,7 @@ impl Pattern2InputsFactsBox { fn_body: Option<&[ASTNode]>, ctx: &super::pattern_pipeline::PatternPipelineContext, verbose: bool, - ) -> Result { + ) -> Result { let log = Pattern2DebugLog::new(verbose); use super::condition_env_builder::ConditionEnvBuilder; use crate::mir::loop_pattern_detection::function_scope_capture::{analyze_captured_vars_v2, CapturedEnv}; @@ -359,11 +366,7 @@ impl Pattern2InputsFactsBox { } let body_local_env = LoopBodyLocalEnv::new(); - - // Break condition extraction is policy-routed (SSOT). - let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?; - - let mut inputs = Pattern2Inputs { + Ok(Pattern2Facts { loop_var_name, loop_var_id, carrier_info, @@ -373,25 +376,6 @@ impl Pattern2InputsFactsBox { env, condition_bindings, body_local_env, - allowed_body_locals_for_conditions: Vec::new(), - read_only_body_local_slot: None, - break_condition_node: break_routing.break_condition_node, - is_loop_true_read_digits: break_routing.is_loop_true_read_digits, - condition_only_recipe: None, - body_local_derived_recipe: None, - }; - - if !break_routing.allowed_body_locals_for_conditions.is_empty() { - use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlotBox; - inputs.allowed_body_locals_for_conditions = - break_routing.allowed_body_locals_for_conditions.clone(); - inputs.read_only_body_local_slot = Some(ReadOnlyBodyLocalSlotBox::extract_single( - &inputs.allowed_body_locals_for_conditions, - body, - )?); - } - - Ok(inputs) + }) } } - diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs index 8ef583df..e41d0a19 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_lowering_orchestrator.rs @@ -1,29 +1,17 @@ //! Pattern2 lowering orchestration (wiring + emission) //! -//! This module is the "do the work" side of Pattern2: -//! - promotion/slot routing for body-local vars in conditions -//! - trim normalization (when enabled) -//! - derived-slot routing (Phase 94) -//! - carrier update analysis + filtering -//! - invoking JoinIR lowerer + boundary merge pipeline -//! -//! It intentionally depends on `Pattern2InputsFactsBox` for analysis-only inputs. +//! Phase 106: the orchestrator is intentionally thin. +//! Most "do the work" logic lives in explicit step boxes under `pattern2_steps/`. 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; -use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; -use crate::mir::loop_pattern_detection::error_messages; use crate::mir::ValueId; -use super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs, Pattern2InputsFactsBox}; - -use super::body_local_policy::{classify_for_pattern2, BodyLocalRoute}; -use super::policies::p5b_escape_derived_policy::{classify_p5b_escape_derived, P5bEscapeDerivedDecision}; -use super::policies::PolicyDecision; - -use std::collections::BTreeMap; +use super::pattern2_steps::apply_policy_step_box::ApplyPolicyStepBox; +use super::pattern2_steps::emit_joinir_step_box::EmitJoinIRStepBox; +use super::pattern2_steps::gather_facts_step_box::GatherFactsStepBox; +use super::pattern2_steps::merge_step_box::MergeStepBox; +use super::pattern2_steps::promote_step_box::PromoteStepBox; pub(crate) struct Pattern2LoweringOrchestrator; @@ -32,15 +20,12 @@ impl Pattern2LoweringOrchestrator { builder: &mut MirBuilder, condition: &ASTNode, body: &[ASTNode], - func_name: &str, + _func_name: &str, debug: bool, fn_body: Option<&[ASTNode]>, skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>, ) -> Result, String> { - use crate::mir::join_ir::lowering::loop_with_break_minimal::lower_loop_with_break_minimal; - let verbose = debug || crate::config::env::joinir_dev_enabled(); - let log = Pattern2DebugLog::new(verbose); super::super::trace::trace().debug("pattern2", "Calling Pattern 2 minimal lowerer"); @@ -49,355 +34,26 @@ impl Pattern2LoweringOrchestrator { super::super::trace::trace().varmap("pattern2_start", &builder.variable_ctx.variable_map); - let mut inputs = Pattern2InputsFactsBox::analyze(builder, condition, body, fn_body, &ctx, verbose)?; + let facts = GatherFactsStepBox::gather(builder, condition, body, fn_body, &ctx, verbose)?; + let inputs = ApplyPolicyStepBox::apply(condition, body, facts)?; - promote_and_prepare_carriers(builder, condition, body, &mut inputs, debug, verbose)?; - - let (effective_break_condition, normalized_body) = - apply_trim_and_normalize(builder, condition, body, &mut inputs, verbose)?; + let mut promoted = PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)?; + let normalized_body = promoted.normalized_body.take(); let analysis_body = normalized_body.as_deref().unwrap_or(body); - - // Phase 94: Detect P5b escape-derived (`ch` reassignment + escape counter). - match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) { - P5bEscapeDerivedDecision::Use(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 => { - 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, &inputs.carrier_info.carriers); - - log.log( - "updates", - format!("Phase 176-3: Analyzed {} carrier updates", carrier_updates.len()), - ); - - let original_carrier_count = inputs.carrier_info.carriers.len(); - filter_carriers_for_updates(&mut inputs.carrier_info, &carrier_updates); - - log.log( - "updates", - format!( - "Phase 176-4: Filtered carriers: {} → {} (kept only carriers with updates/condition-only/loop-local-zero)", - original_carrier_count, - inputs.carrier_info.carriers.len() - ), - ); - - // Ensure env has join-ids for carriers that are referenced only from body updates. - for carrier in &inputs.carrier_info.carriers { - if inputs.env.get(&carrier.name).is_none() { - let join_value = carrier - .join_id - .unwrap_or_else(|| inputs.join_value_space.alloc_param()); - - inputs.env.insert(carrier.name.clone(), join_value); - - if carrier.init != CarrierInit::LoopLocalZero { - inputs.condition_bindings.push(ConditionBinding { - name: carrier.name.clone(), - host_value: carrier.host_id, - join_value, - }); - } else { - log.log( - "updates", - format!( - "Phase 247-EX: Skipping host binding for loop-local carrier '{}' (init=LoopLocalZero)", - carrier.name - ), - ); - } - } - } - - let lowering_inputs = crate::mir::join_ir::lowering::loop_with_break_minimal::LoopWithBreakLoweringInputs { - scope: inputs.scope, - condition, - break_condition: &effective_break_condition, - env: &inputs.env, - carrier_info: &inputs.carrier_info, - carrier_updates: &carrier_updates, - body_ast: analysis_body, - body_local_env: Some(&mut inputs.body_local_env), - allowed_body_locals_for_conditions: if inputs.allowed_body_locals_for_conditions.is_empty() { - None - } else { - Some(inputs.allowed_body_locals_for_conditions.as_slice()) - }, - join_value_space: &mut inputs.join_value_space, - skeleton, - condition_only_recipe: inputs.condition_only_recipe.as_ref(), - body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(), - }; - - let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) { - Ok((module, meta)) => (module, meta), - Err(e) => { - super::super::trace::trace().debug("pattern2", &format!("Pattern 2 lowerer failed: {}", e)); - return Err(format!("[cf_loop/pattern2] Lowering failed: {}", e)); - } - }; - - let exit_meta = &fragment_meta.exit_meta; - use crate::mir::builder::control_flow::joinir::merge::exit_line::ExitMetaCollector; - let exit_bindings = ExitMetaCollector::collect(builder, exit_meta, Some(&inputs.carrier_info), debug); - - // JoinIR main() params: [ValueId(0), ValueId(1), ...] - let mut join_input_slots = vec![ValueId(0)]; - let mut host_input_values = vec![inputs.loop_var_id]; - for (idx, carrier) in inputs.carrier_info.carriers.iter().enumerate() { - join_input_slots.push(ValueId((idx + 1) as u32)); - host_input_values.push(carrier.host_id); - } - - use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; - let boundary = JoinInlineBoundaryBuilder::new() - .with_inputs(join_input_slots, host_input_values) - .with_condition_bindings(inputs.condition_bindings) - .with_exit_bindings(exit_bindings.clone()) - .with_expr_result(fragment_meta.expr_result) - .with_loop_var_name(Some(inputs.loop_var_name.clone())) - .with_carrier_info(inputs.carrier_info.clone()) - .build(); - - use super::conversion_pipeline::JoinIRConversionPipeline; - let _ = JoinIRConversionPipeline::execute(builder, join_module, Some(&boundary), "pattern2", debug)?; - - let void_val = crate::mir::builder::emission::constant::emit_void(builder); - super::super::trace::trace().debug("pattern2", &format!("Loop complete, returning Void {:?}", void_val)); - Ok(Some(void_val)) - } -} - -/// Updated carriers filtering helper (shared by orchestrator + tests). -fn filter_carriers_for_updates(carrier_info: &mut CarrierInfo, carrier_updates: &BTreeMap) { - use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole}; - carrier_info.carriers.retain(|carrier| { - carrier_updates.contains_key(&carrier.name) - || carrier.role == CarrierRole::ConditionOnly - || carrier.init == CarrierInit::LoopLocalZero - }); -} - -pub(in crate::mir::builder) fn promote_and_prepare_carriers( - builder: &mut MirBuilder, - condition: &ASTNode, - body: &[ASTNode], - inputs: &mut Pattern2Inputs, - debug: bool, - verbose: bool, -) -> Result<(), String> { - 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 log = Pattern2DebugLog::new(verbose); - let mut promoted_pairs: Vec<(String, String)> = Vec::new(); - let cond_body_local_vars: Vec = 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() { - 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) => { - return Err(error_messages::format_error_pattern2_promotion_failed( - &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(()) -} - -fn apply_trim_and_normalize( - builder: &mut MirBuilder, - condition: &ASTNode, - body: &[ASTNode], - inputs: &mut Pattern2Inputs, - verbose: bool, -) -> Result<(ASTNode, Option>), String> { - let log = Pattern2DebugLog::new(verbose); - let mut alloc_join_value = || inputs.join_value_space.alloc_param(); - - let disable_trim = inputs.is_loop_true_read_digits; - - let effective_break_condition = if !disable_trim { - if let Some(trim_result) = super::trim_loop_lowering::TrimLoopLowerer::try_lower_trim_like_loop( + let effective_break_condition = promoted.effective_break_condition; + let carrier_updates = promoted.carrier_updates; + let emitted = EmitJoinIRStepBox::emit( builder, - &inputs.scope, condition, - &inputs.break_condition_node, - body, - &inputs.loop_var_name, - &mut inputs.carrier_info, - &mut alloc_join_value, - )? { - log.log("trim", "TrimLoopLowerer processed Trim pattern successfully"); - inputs.carrier_info = trim_result.carrier_info; - inputs.condition_only_recipe = trim_result.condition_only_recipe; - trim_result.condition - } else { - inputs.break_condition_node.clone() - } - } else { - inputs.break_condition_node.clone() - }; + analysis_body, + &effective_break_condition, + &carrier_updates, + &mut promoted.inputs, + debug, + verbose, + skeleton, + )?; - use crate::mir::join_ir::lowering::complex_addend_normalizer::{ComplexAddendNormalizer, NormalizationResult}; - let mut normalized_body = Vec::new(); - let mut has_normalization = false; - - for node in body { - match ComplexAddendNormalizer::normalize_assign(node) { - NormalizationResult::Normalized { temp_def, new_assign, .. } => { - normalized_body.push(temp_def); - normalized_body.push(new_assign); - has_normalization = true; - } - NormalizationResult::Unchanged => normalized_body.push(node.clone()), - } + MergeStepBox::merge(builder, emitted.join_module, emitted.boundary, debug) } - - Ok(( - effective_break_condition, - if has_normalization { Some(normalized_body) } else { None }, - )) } - diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/apply_policy_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/apply_policy_step_box.rs new file mode 100644 index 00000000..d5bcf0a0 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/apply_policy_step_box.rs @@ -0,0 +1,46 @@ +//! ApplyPolicyStepBox (Phase 106) +//! +//! Responsibility: apply policy routing for Pattern2 break condition + allow-list. + +use crate::ast::ASTNode; + +use super::super::pattern2_break_condition_policy_router::Pattern2BreakConditionPolicyRouterBox; +use super::super::pattern2_inputs_facts_box::{Pattern2Facts, Pattern2Inputs}; + +pub(crate) struct ApplyPolicyStepBox; + +impl ApplyPolicyStepBox { + pub(crate) fn apply(condition: &ASTNode, body: &[ASTNode], facts: Pattern2Facts) -> Result { + use crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlotBox; + + let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?; + + let read_only_body_local_slot = if break_routing.allowed_body_locals_for_conditions.is_empty() { + None + } else { + Some(ReadOnlyBodyLocalSlotBox::extract_single( + &break_routing.allowed_body_locals_for_conditions, + body, + )?) + }; + + Ok(Pattern2Inputs { + loop_var_name: facts.loop_var_name, + loop_var_id: facts.loop_var_id, + carrier_info: facts.carrier_info, + scope: facts.scope, + captured_env: facts.captured_env, + join_value_space: facts.join_value_space, + env: facts.env, + condition_bindings: facts.condition_bindings, + body_local_env: facts.body_local_env, + allowed_body_locals_for_conditions: break_routing.allowed_body_locals_for_conditions, + read_only_body_local_slot, + break_condition_node: break_routing.break_condition_node, + is_loop_true_read_digits: break_routing.is_loop_true_read_digits, + condition_only_recipe: None, + body_local_derived_recipe: None, + }) + } +} + diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs new file mode 100644 index 00000000..dcd5616a --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/emit_joinir_step_box.rs @@ -0,0 +1,91 @@ +//! EmitJoinIRStepBox (Phase 106) +//! +//! Responsibility: call Pattern2 JoinIR lowerer and build inline boundary. + +use crate::ast::ASTNode; +use crate::mir::builder::MirBuilder; +use crate::mir::ValueId; + +use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs}; +use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; + +use std::collections::BTreeMap; + +pub(crate) struct EmitJoinIRStepOutput { + pub join_module: crate::mir::join_ir::JoinModule, + pub boundary: JoinInlineBoundary, +} + +pub(crate) struct EmitJoinIRStepBox; + +impl EmitJoinIRStepBox { + pub(crate) fn emit( + builder: &mut MirBuilder, + condition: &ASTNode, + body_ast: &[ASTNode], + effective_break_condition: &ASTNode, + carrier_updates: &BTreeMap, + inputs: &mut Pattern2Inputs, + debug: bool, + verbose: bool, + skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>, + ) -> Result { + use crate::mir::join_ir::lowering::loop_with_break_minimal::lower_loop_with_break_minimal; + + let log = Pattern2DebugLog::new(verbose); + + let lowering_inputs = crate::mir::join_ir::lowering::loop_with_break_minimal::LoopWithBreakLoweringInputs { + scope: inputs.scope.clone(), + condition, + break_condition: effective_break_condition, + env: &inputs.env, + carrier_info: &inputs.carrier_info, + carrier_updates, + body_ast, + body_local_env: Some(&mut inputs.body_local_env), + allowed_body_locals_for_conditions: if inputs.allowed_body_locals_for_conditions.is_empty() { + None + } else { + Some(inputs.allowed_body_locals_for_conditions.as_slice()) + }, + join_value_space: &mut inputs.join_value_space, + skeleton, + condition_only_recipe: inputs.condition_only_recipe.as_ref(), + body_local_derived_recipe: inputs.body_local_derived_recipe.as_ref(), + }; + + let (join_module, fragment_meta) = match lower_loop_with_break_minimal(lowering_inputs) { + Ok((module, meta)) => (module, meta), + Err(e) => { + super::super::super::trace::trace().debug("pattern2", &format!("Pattern 2 lowerer failed: {}", e)); + return Err(format!("[cf_loop/pattern2] Lowering failed: {}", e)); + } + }; + + let exit_meta = &fragment_meta.exit_meta; + use crate::mir::builder::control_flow::joinir::merge::exit_line::ExitMetaCollector; + let exit_bindings = ExitMetaCollector::collect(builder, exit_meta, Some(&inputs.carrier_info), debug); + + // JoinIR main() params: [ValueId(0), ValueId(1), ...] + let mut join_input_slots = vec![ValueId(0)]; + let mut host_input_values = vec![inputs.loop_var_id]; + for (idx, carrier) in inputs.carrier_info.carriers.iter().enumerate() { + join_input_slots.push(ValueId((idx + 1) as u32)); + host_input_values.push(carrier.host_id); + } + + use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; + let boundary = JoinInlineBoundaryBuilder::new() + .with_inputs(join_input_slots, host_input_values) + .with_condition_bindings(std::mem::take(&mut inputs.condition_bindings)) + .with_exit_bindings(exit_bindings.clone()) + .with_expr_result(fragment_meta.expr_result) + .with_loop_var_name(Some(inputs.loop_var_name.clone())) + .with_carrier_info(inputs.carrier_info.clone()) + .build(); + + log.log("emit", "JoinIR module + boundary constructed"); + + Ok(EmitJoinIRStepOutput { join_module, boundary }) + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/gather_facts_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/gather_facts_step_box.rs new file mode 100644 index 00000000..8d7470c1 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/gather_facts_step_box.rs @@ -0,0 +1,24 @@ +//! GatherFactsStepBox (Phase 106) +//! +//! Responsibility: gather Pattern2 analysis-only inputs. + +use crate::ast::ASTNode; +use crate::mir::builder::MirBuilder; + +use super::super::pattern2_inputs_facts_box::{Pattern2Facts, Pattern2InputsFactsBox}; + +pub(crate) struct GatherFactsStepBox; + +impl GatherFactsStepBox { + pub(crate) fn gather( + builder: &MirBuilder, + condition: &ASTNode, + body: &[ASTNode], + fn_body: Option<&[ASTNode]>, + ctx: &super::super::pattern_pipeline::PatternPipelineContext, + verbose: bool, + ) -> Result { + Pattern2InputsFactsBox::analyze(builder, condition, body, fn_body, ctx, verbose) + } +} + diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/merge_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/merge_step_box.rs new file mode 100644 index 00000000..c16edee5 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/merge_step_box.rs @@ -0,0 +1,27 @@ +//! MergeStepBox (Phase 106) +//! +//! Responsibility: run JoinIR conversion pipeline (JoinIR → MIR merge) and return void. + +use crate::mir::builder::MirBuilder; +use crate::mir::ValueId; +use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary; + +pub(crate) struct MergeStepBox; + +impl MergeStepBox { + pub(crate) fn merge( + builder: &mut MirBuilder, + join_module: crate::mir::join_ir::JoinModule, + boundary: JoinInlineBoundary, + debug: bool, + ) -> Result, String> { + use crate::mir::builder::emission::constant::emit_void; + use super::super::conversion_pipeline::JoinIRConversionPipeline; + + let _ = JoinIRConversionPipeline::execute(builder, join_module, Some(&boundary), "pattern2", debug)?; + + let void_val = emit_void(builder); + super::super::super::trace::trace().debug("pattern2", &format!("Loop complete, returning Void {:?}", void_val)); + Ok(Some(void_val)) + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/mod.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/mod.rs new file mode 100644 index 00000000..f91d95eb --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/mod.rs @@ -0,0 +1,14 @@ +//! Phase 106: Pattern2 Step Boxes (SSOT) +//! +//! Goal: keep `pattern2_with_break.rs` and the orchestrator thin by splitting +//! the Pattern2 pipeline into explicit steps with clear boundaries. +//! +//! Each step should have a single responsibility and fail-fast with a clear tag +//! at the step boundary. + +pub(crate) mod apply_policy_step_box; +pub(crate) mod emit_joinir_step_box; +pub(crate) mod gather_facts_step_box; +pub(crate) mod merge_step_box; +pub(crate) mod promote_step_box; + diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/promote_step_box.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/promote_step_box.rs new file mode 100644 index 00000000..3c4b52cb --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_steps/promote_step_box.rs @@ -0,0 +1,342 @@ +//! PromoteStepBox (Phase 106) +//! +//! Responsibility: +//! - promotion / read-only slot routing for body-local vars in conditions +//! - trim normalization (when enabled) +//! - derived-slot routing (Phase 94) +//! - carrier update analysis + filtering + +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::loop_update_analyzer::UpdateExpr; +use crate::mir::loop_pattern_detection::error_messages; + +use super::super::body_local_policy::{classify_for_pattern2, BodyLocalRoute}; +use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs}; +use super::super::policies::p5b_escape_derived_policy::{classify_p5b_escape_derived, P5bEscapeDerivedDecision}; +use super::super::policies::PolicyDecision; + +use std::collections::BTreeMap; + +pub(crate) struct PromoteStepResult { + pub inputs: Pattern2Inputs, + pub effective_break_condition: ASTNode, + pub normalized_body: Option>, + pub carrier_updates: BTreeMap, +} + +impl PromoteStepResult { + pub(crate) fn analysis_body<'a>(&'a self, original_body: &'a [ASTNode]) -> &'a [ASTNode] { + self.normalized_body.as_deref().unwrap_or(original_body) + } +} + +pub(crate) struct PromoteStepBox; + +impl PromoteStepBox { + pub(crate) fn run( + builder: &mut MirBuilder, + condition: &ASTNode, + body: &[ASTNode], + mut inputs: Pattern2Inputs, + debug: bool, + verbose: bool, + ) -> Result { + Self::promote_and_prepare_carriers(builder, condition, body, &mut inputs, debug, verbose)?; + + let (effective_break_condition, normalized_body) = + Self::apply_trim_and_normalize(builder, 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). + match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) { + P5bEscapeDerivedDecision::Use(recipe) => { + Pattern2DebugLog::new(verbose).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 => { + 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, &inputs.carrier_info.carriers); + Pattern2DebugLog::new(verbose).log( + "updates", + format!("Phase 176-3: Analyzed {} carrier updates", carrier_updates.len()), + ); + + let original_carrier_count = inputs.carrier_info.carriers.len(); + filter_carriers_for_updates(&mut inputs.carrier_info, &carrier_updates); + Pattern2DebugLog::new(verbose).log( + "updates", + format!( + "Phase 176-4: Filtered carriers: {} → {} (kept only carriers with updates/condition-only/loop-local-zero)", + original_carrier_count, + inputs.carrier_info.carriers.len() + ), + ); + + // Ensure env has join-ids for carriers that are referenced only from body updates. + for carrier in &inputs.carrier_info.carriers { + if inputs.env.get(&carrier.name).is_none() { + let join_value = carrier + .join_id + .unwrap_or_else(|| inputs.join_value_space.alloc_param()); + + inputs.env.insert(carrier.name.clone(), join_value); + + if carrier.init != CarrierInit::LoopLocalZero { + use crate::mir::join_ir::lowering::condition_env::ConditionBinding; + inputs.condition_bindings.push(ConditionBinding { + name: carrier.name.clone(), + host_value: carrier.host_id, + join_value, + }); + } else { + Pattern2DebugLog::new(verbose).log( + "updates", + format!( + "Phase 247-EX: Skipping host binding for loop-local carrier '{}' (init=LoopLocalZero)", + carrier.name + ), + ); + } + } + } + + Ok(PromoteStepResult { + inputs, + effective_break_condition, + normalized_body, + carrier_updates, + }) + } + + pub(in crate::mir::builder) fn promote_and_prepare_carriers( + builder: &mut MirBuilder, + condition: &ASTNode, + body: &[ASTNode], + inputs: &mut Pattern2Inputs, + debug: bool, + verbose: bool, + ) -> Result<(), String> { + 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 = 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() { + 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) => { + return Err(error_messages::format_error_pattern2_promotion_failed( + &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(()) + } + + fn apply_trim_and_normalize( + builder: &mut MirBuilder, + condition: &ASTNode, + body: &[ASTNode], + inputs: &mut Pattern2Inputs, + verbose: bool, + ) -> Result<(ASTNode, Option>), String> { + let log = Pattern2DebugLog::new(verbose); + let mut alloc_join_value = || inputs.join_value_space.alloc_param(); + + let disable_trim = inputs.is_loop_true_read_digits; + + let effective_break_condition = if !disable_trim { + if let Some(trim_result) = super::super::trim_loop_lowering::TrimLoopLowerer::try_lower_trim_like_loop( + builder, + &inputs.scope, + condition, + &inputs.break_condition_node, + body, + &inputs.loop_var_name, + &mut inputs.carrier_info, + &mut alloc_join_value, + )? { + log.log("trim", "TrimLoopLowerer processed Trim pattern successfully"); + inputs.carrier_info = trim_result.carrier_info; + inputs.condition_only_recipe = trim_result.condition_only_recipe; + trim_result.condition + } else { + inputs.break_condition_node.clone() + } + } else { + inputs.break_condition_node.clone() + }; + + use crate::mir::join_ir::lowering::complex_addend_normalizer::{ComplexAddendNormalizer, NormalizationResult}; + let mut normalized_body = Vec::new(); + let mut has_normalization = false; + + for node in body { + match ComplexAddendNormalizer::normalize_assign(node) { + NormalizationResult::Normalized { temp_def, new_assign, .. } => { + normalized_body.push(temp_def); + normalized_body.push(new_assign); + has_normalization = true; + } + NormalizationResult::Unchanged => normalized_body.push(node.clone()), + } + } + + Ok(( + effective_break_condition, + if has_normalization { Some(normalized_body) } else { None }, + )) + } +} + +fn filter_carriers_for_updates(carrier_info: &mut CarrierInfo, carrier_updates: &BTreeMap) { + use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole}; + carrier_info.carriers.retain(|carrier| { + carrier_updates.contains_key(&carrier.name) + || carrier.role == CarrierRole::ConditionOnly + || carrier.init == CarrierInit::LoopLocalZero + }); +} diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs index 2381b34b..0f1b8509 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs @@ -233,7 +233,8 @@ mod tests { use crate::ast::Span; use crate::mir::ValueId; use super::super::pattern2_inputs_facts_box::Pattern2InputsFactsBox; - use super::super::pattern2_lowering_orchestrator::promote_and_prepare_carriers; + use super::super::pattern2_steps::apply_policy_step_box::ApplyPolicyStepBox; + use super::super::pattern2_steps::promote_step_box::PromoteStepBox; let mut builder = MirBuilder::new(); builder @@ -311,10 +312,12 @@ mod tests { let ctx = build_pattern_context(&mut builder, &condition, &body, PatternVariant::Pattern2) .expect("build_pattern_context"); - let mut inputs = + let facts = Pattern2InputsFactsBox::analyze(&builder, &condition, &body, None, &ctx, false) .expect("Pattern2InputsFactsBox::analyze"); - promote_and_prepare_carriers(&mut builder, &condition, &body, &mut inputs, false, false) + let mut inputs = ApplyPolicyStepBox::apply(&condition, &body, facts) + .expect("ApplyPolicyStepBox::apply"); + PromoteStepBox::promote_and_prepare_carriers(&mut builder, &condition, &body, &mut inputs, false, false) .expect("promote_and_prepare_carriers"); assert!( diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/loop_true_read_digits_policy.rs b/src/mir/builder/control_flow/joinir/patterns/policies/loop_true_read_digits_policy.rs index 57f4d382..3e94edc2 100644 --- a/src/mir/builder/control_flow/joinir/patterns/policies/loop_true_read_digits_policy.rs +++ b/src/mir/builder/control_flow/joinir/patterns/policies/loop_true_read_digits_policy.rs @@ -12,6 +12,7 @@ use super::PolicyDecision; use crate::ast::{ASTNode, BinaryOperator, Span, UnaryOperator}; +#[derive(Debug, Clone)] pub(crate) struct LoopTrueReadDigitsPolicyResult { pub break_condition_node: ASTNode, pub allowed_ch_var: String, @@ -59,3 +60,137 @@ pub(crate) fn classify_loop_true_read_digits( }) } +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{BinaryOperator, LiteralValue, Span, UnaryOperator}; + + fn span() -> Span { + Span::unknown() + } + + fn bool_lit(v: bool) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Bool(v), + span: span(), + } + } + + fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: span(), + } + } + + fn str_lit(s: &str) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::String(s.to_string()), + span: span(), + } + } + + fn int_lit(n: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(n), + span: span(), + } + } + + fn eq(left: ASTNode, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left: Box::new(left), + right: Box::new(right), + span: span(), + } + } + + fn add(left: ASTNode, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(left), + right: Box::new(right), + span: span(), + } + } + + fn or(left: ASTNode, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: BinaryOperator::Or, + left: Box::new(left), + right: Box::new(right), + span: span(), + } + } + + fn not(node: ASTNode) -> ASTNode { + ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand: Box::new(node), + span: span(), + } + } + + fn digit_chain(var_name: &str, digits: &[&str]) -> ASTNode { + let mut it = digits.iter(); + let first = it + .next() + .expect("digits must be non-empty"); + let mut acc = eq(var(var_name), str_lit(first)); + for d in it { + acc = or(acc, eq(var(var_name), str_lit(d))); + } + acc + } + + #[test] + fn read_digits_loop_true_policy_returns_break_when_true_and_allowlist() { + // loop(true) { + // if ch == "" { break } + // if is_digit(ch) { out = out + ch; i = i + 1 } else { break } + // } + let condition = bool_lit(true); + + let eos_cond = eq(var("ch"), str_lit("")); + let digit_cond = digit_chain( + "ch", + &["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + ); + + let body = vec![ + ASTNode::If { + condition: Box::new(eos_cond.clone()), + then_body: vec![ASTNode::Break { span: span() }], + else_body: None, + span: span(), + }, + ASTNode::If { + condition: Box::new(digit_cond.clone()), + then_body: vec![ + ASTNode::Assignment { + target: Box::new(var("out")), + value: Box::new(add(var("out"), var("ch"))), + span: span(), + }, + ASTNode::Assignment { + target: Box::new(var("i")), + value: Box::new(add(var("i"), int_lit(1))), + span: span(), + }, + ], + else_body: Some(vec![ASTNode::Break { span: span() }]), + span: span(), + }, + ]; + + let decision = classify_loop_true_read_digits(&condition, &body); + let result = match decision { + PolicyDecision::Use(v) => v, + other => panic!("expected PolicyDecision::Use, got {:?}", other), + }; + + assert_eq!(result.allowed_ch_var, "ch"); + assert_eq!(result.break_condition_node, or(eos_cond, not(digit_cond))); + } +}