refactor(joinir): split Pattern2 orchestrator into smaller steps
This commit is contained in:
@ -8,9 +8,12 @@ use crate::mir::builder::MirBuilder;
|
||||
use crate::mir::ValueId;
|
||||
|
||||
use super::pattern2_steps::apply_policy_step_box::ApplyPolicyStepBox;
|
||||
use super::pattern2_steps::body_local_derived_step_box::BodyLocalDerivedStepBox;
|
||||
use super::pattern2_steps::carrier_updates_step_box::CarrierUpdatesStepBox;
|
||||
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::normalize_body_step_box::NormalizeBodyStepBox;
|
||||
use super::pattern2_steps::post_loop_early_return_step_box::PostLoopEarlyReturnStepBox;
|
||||
use super::pattern2_steps::promote_step_box::PromoteStepBox;
|
||||
|
||||
@ -38,25 +41,31 @@ impl Pattern2LoweringOrchestrator {
|
||||
let facts = GatherFactsStepBox::gather(builder, condition, body, fn_body, &ctx, verbose)?;
|
||||
let inputs = ApplyPolicyStepBox::apply(condition, body, facts)?;
|
||||
|
||||
let mut promoted = PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)?;
|
||||
let normalized_body = promoted.normalized_body.take();
|
||||
let promoted = PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)?;
|
||||
let mut inputs = promoted.inputs;
|
||||
|
||||
let normalized = NormalizeBodyStepBox::run(builder, condition, body, &mut inputs, verbose)?;
|
||||
let normalized_body = normalized.normalized_body;
|
||||
let analysis_body = normalized_body.as_deref().unwrap_or(body);
|
||||
let effective_break_condition = promoted.effective_break_condition;
|
||||
let carrier_updates = promoted.carrier_updates;
|
||||
let effective_break_condition = normalized.effective_break_condition;
|
||||
|
||||
BodyLocalDerivedStepBox::apply(analysis_body, &mut inputs, verbose)?;
|
||||
let carrier_updates =
|
||||
CarrierUpdatesStepBox::analyze_and_filter(analysis_body, &mut inputs, verbose);
|
||||
let emitted = EmitJoinIRStepBox::emit(
|
||||
builder,
|
||||
condition,
|
||||
analysis_body,
|
||||
&effective_break_condition,
|
||||
&carrier_updates,
|
||||
&mut promoted.inputs,
|
||||
&mut inputs,
|
||||
debug,
|
||||
verbose,
|
||||
skeleton,
|
||||
)?;
|
||||
|
||||
let out = MergeStepBox::merge(builder, emitted.join_module, emitted.boundary, debug)?;
|
||||
PostLoopEarlyReturnStepBox::maybe_emit(builder, promoted.inputs.post_loop_early_return.as_ref())?;
|
||||
PostLoopEarlyReturnStepBox::maybe_emit(builder, inputs.post_loop_early_return.as_ref())?;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
//! BodyLocalDerivedStepBox (Phase 94, extracted in Phase 5)
|
||||
//!
|
||||
//! Responsibility:
|
||||
//! - Apply the Phase 94 P5b escape-derived routing to Pattern2 inputs.
|
||||
//! - No JoinIR generation. Purely sets `inputs.body_local_derived_recipe` or fails fast.
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
|
||||
use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs};
|
||||
use super::super::policies::p5b_escape_derived_policy::classify_p5b_escape_derived;
|
||||
use super::super::policies::PolicyDecision;
|
||||
|
||||
pub(crate) struct BodyLocalDerivedStepBox;
|
||||
|
||||
impl BodyLocalDerivedStepBox {
|
||||
pub(crate) fn apply(
|
||||
analysis_body: &[ASTNode],
|
||||
inputs: &mut Pattern2Inputs,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) {
|
||||
PolicyDecision::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);
|
||||
Ok(())
|
||||
}
|
||||
PolicyDecision::Reject(reason) => Err(format!("[cf_loop/pattern2] {}", reason)),
|
||||
PolicyDecision::None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
//! CarrierUpdatesStepBox (Phase 176+, extracted in Phase 5)
|
||||
//!
|
||||
//! Responsibility:
|
||||
//! - Analyze carrier updates for Pattern2 (or use policy override).
|
||||
//! - Filter carriers to only those required by updates / condition-only / loop-local-zero.
|
||||
//! - Ensure JoinValue env has join-ids for carriers referenced only from body updates.
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit, CarrierRole};
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionBinding;
|
||||
use crate::mir::join_ir::lowering::loop_update_analyzer::{LoopUpdateAnalyzer, UpdateExpr};
|
||||
|
||||
use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(crate) struct CarrierUpdatesStepBox;
|
||||
|
||||
impl CarrierUpdatesStepBox {
|
||||
pub(crate) fn analyze_and_filter(
|
||||
analysis_body: &[ASTNode],
|
||||
inputs: &mut Pattern2Inputs,
|
||||
verbose: bool,
|
||||
) -> BTreeMap<String, UpdateExpr> {
|
||||
let carrier_updates = if let Some(override_map) = inputs.carrier_updates_override.take() {
|
||||
override_map
|
||||
} else {
|
||||
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 {
|
||||
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
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
carrier_updates
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_carriers_for_updates(info: &mut CarrierInfo, updates: &BTreeMap<String, UpdateExpr>) {
|
||||
// Keep carriers that:
|
||||
// - have updates
|
||||
// - are condition-only (used for break condition)
|
||||
// - are loop-local-zero (policy-injected for derived computations)
|
||||
info.carriers.retain(|c| {
|
||||
updates.contains_key(&c.name)
|
||||
|| c.role == CarrierRole::ConditionOnly
|
||||
|| c.init == CarrierInit::LoopLocalZero
|
||||
});
|
||||
}
|
||||
@ -7,8 +7,11 @@
|
||||
//! at the step boundary.
|
||||
|
||||
pub(crate) mod apply_policy_step_box;
|
||||
pub(crate) mod body_local_derived_step_box;
|
||||
pub(crate) mod carrier_updates_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 normalize_body_step_box;
|
||||
pub(crate) mod post_loop_early_return_step_box;
|
||||
pub(crate) mod promote_step_box;
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
//! NormalizeBodyStepBox (Trim + break condition normalization)
|
||||
//!
|
||||
//! Responsibility:
|
||||
//! - Apply trim normalization when enabled (Phase 93+).
|
||||
//! - Return the effective break condition and an optional normalized body.
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::builder::MirBuilder;
|
||||
|
||||
use super::super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs};
|
||||
|
||||
pub(crate) struct NormalizedBodyResult {
|
||||
pub effective_break_condition: ASTNode,
|
||||
pub normalized_body: Option<Vec<ASTNode>>,
|
||||
}
|
||||
|
||||
pub(crate) struct NormalizeBodyStepBox;
|
||||
|
||||
impl NormalizeBodyStepBox {
|
||||
pub(crate) fn run(
|
||||
builder: &mut MirBuilder,
|
||||
condition: &ASTNode,
|
||||
body: &[ASTNode],
|
||||
inputs: &mut Pattern2Inputs,
|
||||
verbose: bool,
|
||||
) -> Result<NormalizedBodyResult, String> {
|
||||
let log = Pattern2DebugLog::new(verbose);
|
||||
let mut alloc_join_value = || inputs.join_value_space.alloc_param();
|
||||
|
||||
// Trim-like loops have their own canonicalizer path; keep existing behavior:
|
||||
// read_digits(loop(true)) disables trim handling here.
|
||||
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(NormalizedBodyResult {
|
||||
effective_break_condition,
|
||||
normalized_body: if has_normalization {
|
||||
Some(normalized_body)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2,34 +2,18 @@
|
||||
//!
|
||||
//! 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
|
||||
//! - carrier preparation for JoinIR emission
|
||||
|
||||
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::pattern2_inputs_facts_box::Pattern2Inputs;
|
||||
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<Vec<ASTNode>>,
|
||||
pub carrier_updates: BTreeMap<String, UpdateExpr>,
|
||||
}
|
||||
|
||||
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;
|
||||
@ -44,100 +28,7 @@ impl PromoteStepBox {
|
||||
verbose: bool,
|
||||
) -> Result<PromoteStepResult, String> {
|
||||
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 = if let Some(override_map) = inputs.carrier_updates_override.take() {
|
||||
override_map
|
||||
} else {
|
||||
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,
|
||||
})
|
||||
Ok(PromoteStepResult { inputs })
|
||||
}
|
||||
|
||||
pub(in crate::mir::builder) fn promote_and_prepare_carriers(
|
||||
@ -286,67 +177,4 @@ impl PromoteStepBox {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_trim_and_normalize(
|
||||
builder: &mut MirBuilder,
|
||||
condition: &ASTNode,
|
||||
body: &[ASTNode],
|
||||
inputs: &mut Pattern2Inputs,
|
||||
verbose: bool,
|
||||
) -> Result<(ASTNode, Option<Vec<ASTNode>>), 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<String, UpdateExpr>) {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
@ -29,12 +29,26 @@ pub fn classify_p5b_escape_derived(
|
||||
body: &[ASTNode],
|
||||
loop_var_name: &str,
|
||||
) -> P5bEscapeDerivedDecision {
|
||||
let strict = joinir_dev::strict_enabled();
|
||||
let has_ch_init = find_local_init_expr(body, "ch").is_some();
|
||||
let has_ch_reassign = has_assignment_to_var(body, "ch");
|
||||
|
||||
let Some(info) = super::super::ast_feature_extractor::detect_escape_skip_pattern(body) else {
|
||||
if strict && has_ch_init && has_ch_reassign {
|
||||
return P5bEscapeDerivedDecision::Reject(error_tags::freeze(
|
||||
"[phase94/body_local_derived/contract/unhandled_reassign] Body-local reassignment to 'ch' detected but escape shape is not recognized",
|
||||
));
|
||||
}
|
||||
return P5bEscapeDerivedDecision::None;
|
||||
};
|
||||
|
||||
if info.counter_name != loop_var_name {
|
||||
// Not the loop counter we lower as the JoinIR loop var; ignore to avoid misrouting.
|
||||
if strict && has_ch_init && has_ch_reassign {
|
||||
return P5bEscapeDerivedDecision::Reject(error_tags::freeze(
|
||||
"[phase94/body_local_derived/contract/loop_counter_mismatch] Body-local reassignment to 'ch' detected but loop counter does not match Pattern2 loop var",
|
||||
));
|
||||
}
|
||||
return P5bEscapeDerivedDecision::None;
|
||||
}
|
||||
|
||||
@ -55,6 +69,29 @@ pub fn classify_p5b_escape_derived(
|
||||
}
|
||||
}
|
||||
|
||||
fn has_assignment_to_var(body: &[ASTNode], name: &str) -> bool {
|
||||
fn node_has_assignment(node: &ASTNode, name: &str) -> bool {
|
||||
match node {
|
||||
ASTNode::Assignment { target, .. } => is_var_named(target.as_ref(), name),
|
||||
ASTNode::If {
|
||||
then_body,
|
||||
else_body,
|
||||
..
|
||||
} => {
|
||||
then_body.iter().any(|n| node_has_assignment(n, name))
|
||||
|| else_body
|
||||
.as_ref()
|
||||
.map_or(false, |e| e.iter().any(|n| node_has_assignment(n, name)))
|
||||
}
|
||||
ASTNode::Loop { body, .. } => body.iter().any(|n| node_has_assignment(n, name)),
|
||||
ASTNode::ScopeBox { body, .. } => body.iter().any(|n| node_has_assignment(n, name)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
body.iter().any(|n| node_has_assignment(n, name))
|
||||
}
|
||||
|
||||
fn build_recipe_from_info(
|
||||
body: &[ASTNode],
|
||||
info: &EscapeSkipPatternInfo,
|
||||
|
||||
Reference in New Issue
Block a user