refactor(joinir): Phase 106 Pattern2 step boxes
This commit is contained in:
@ -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_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_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_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 condition_env_builder;
|
||||||
pub(in crate::mir::builder) mod conversion_pipeline;
|
pub(in crate::mir::builder) mod conversion_pipeline;
|
||||||
pub(in crate::mir::builder) mod exit_binding;
|
pub(in crate::mir::builder) mod exit_binding;
|
||||||
|
|||||||
@ -1,20 +1,17 @@
|
|||||||
//! Pattern2 input analysis (facts collection only)
|
//! 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
|
//! - capture/pinned local analysis
|
||||||
//! - mutable accumulator promotion into carriers
|
//! - mutable accumulator promotion into carriers
|
||||||
//! - condition env + JoinValueSpace initialization
|
//! - 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.
|
//! Policy routing (break condition normalization + allow-list) is a separate step
|
||||||
|
//! in Phase 106 (`pattern2_steps::ApplyPolicyStepBox`).
|
||||||
use super::pattern2_break_condition_policy_router::Pattern2BreakConditionPolicyRouterBox;
|
|
||||||
use crate::ast::ASTNode;
|
use crate::ast::ASTNode;
|
||||||
use crate::mir::builder::MirBuilder;
|
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::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::debug_output_box::DebugOutputBox;
|
||||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||||
@ -43,6 +40,18 @@ impl Pattern2DebugLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<ConditionBinding>,
|
||||||
|
pub body_local_env: LoopBodyLocalEnv,
|
||||||
|
}
|
||||||
|
|
||||||
pub(in crate::mir::builder) struct Pattern2Inputs {
|
pub(in crate::mir::builder) struct Pattern2Inputs {
|
||||||
pub loop_var_name: String,
|
pub loop_var_name: String,
|
||||||
pub loop_var_id: ValueId,
|
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.
|
/// This must stay minimal (1 variable) and is validated by ReadOnlyBodyLocalSlotBox.
|
||||||
pub allowed_body_locals_for_conditions: Vec<String>,
|
pub allowed_body_locals_for_conditions: Vec<String>,
|
||||||
/// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable.
|
/// Phase 92 P3: Diagnostics / debug metadata for the allow-listed variable.
|
||||||
pub read_only_body_local_slot: Option<ReadOnlyBodyLocalSlot>,
|
pub read_only_body_local_slot: Option<crate::mir::join_ir::lowering::common::body_local_slot::ReadOnlyBodyLocalSlot>,
|
||||||
|
/// Policy-routed "break when true" condition node.
|
||||||
pub break_condition_node: ASTNode,
|
pub break_condition_node: ASTNode,
|
||||||
/// loop(true) + break-only digits(read_digits_from family)
|
/// 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,
|
pub is_loop_true_read_digits: bool,
|
||||||
/// Phase 93 P0: ConditionOnly recipe for derived slot recalculation
|
/// Phase 93 P0: ConditionOnly recipe for derived slot recalculation
|
||||||
pub condition_only_recipe: Option<
|
pub condition_only_recipe: Option<
|
||||||
@ -83,7 +90,7 @@ impl Pattern2InputsFactsBox {
|
|||||||
fn_body: Option<&[ASTNode]>,
|
fn_body: Option<&[ASTNode]>,
|
||||||
ctx: &super::pattern_pipeline::PatternPipelineContext,
|
ctx: &super::pattern_pipeline::PatternPipelineContext,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
) -> Result<Pattern2Inputs, String> {
|
) -> Result<Pattern2Facts, String> {
|
||||||
let log = Pattern2DebugLog::new(verbose);
|
let log = Pattern2DebugLog::new(verbose);
|
||||||
use super::condition_env_builder::ConditionEnvBuilder;
|
use super::condition_env_builder::ConditionEnvBuilder;
|
||||||
use crate::mir::loop_pattern_detection::function_scope_capture::{analyze_captured_vars_v2, CapturedEnv};
|
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();
|
let body_local_env = LoopBodyLocalEnv::new();
|
||||||
|
Ok(Pattern2Facts {
|
||||||
// Break condition extraction is policy-routed (SSOT).
|
|
||||||
let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?;
|
|
||||||
|
|
||||||
let mut inputs = Pattern2Inputs {
|
|
||||||
loop_var_name,
|
loop_var_name,
|
||||||
loop_var_id,
|
loop_var_id,
|
||||||
carrier_info,
|
carrier_info,
|
||||||
@ -373,25 +376,6 @@ impl Pattern2InputsFactsBox {
|
|||||||
env,
|
env,
|
||||||
condition_bindings,
|
condition_bindings,
|
||||||
body_local_env,
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,17 @@
|
|||||||
//! Pattern2 lowering orchestration (wiring + emission)
|
//! Pattern2 lowering orchestration (wiring + emission)
|
||||||
//!
|
//!
|
||||||
//! This module is the "do the work" side of Pattern2:
|
//! Phase 106: the orchestrator is intentionally thin.
|
||||||
//! - promotion/slot routing for body-local vars in conditions
|
//! Most "do the work" logic lives in explicit step boxes under `pattern2_steps/`.
|
||||||
//! - 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.
|
|
||||||
|
|
||||||
use crate::ast::ASTNode;
|
use crate::ast::ASTNode;
|
||||||
use crate::mir::builder::MirBuilder;
|
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 crate::mir::ValueId;
|
||||||
|
|
||||||
use super::pattern2_inputs_facts_box::{Pattern2DebugLog, Pattern2Inputs, Pattern2InputsFactsBox};
|
use super::pattern2_steps::apply_policy_step_box::ApplyPolicyStepBox;
|
||||||
|
use super::pattern2_steps::emit_joinir_step_box::EmitJoinIRStepBox;
|
||||||
use super::body_local_policy::{classify_for_pattern2, BodyLocalRoute};
|
use super::pattern2_steps::gather_facts_step_box::GatherFactsStepBox;
|
||||||
use super::policies::p5b_escape_derived_policy::{classify_p5b_escape_derived, P5bEscapeDerivedDecision};
|
use super::pattern2_steps::merge_step_box::MergeStepBox;
|
||||||
use super::policies::PolicyDecision;
|
use super::pattern2_steps::promote_step_box::PromoteStepBox;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
pub(crate) struct Pattern2LoweringOrchestrator;
|
pub(crate) struct Pattern2LoweringOrchestrator;
|
||||||
|
|
||||||
@ -32,15 +20,12 @@ impl Pattern2LoweringOrchestrator {
|
|||||||
builder: &mut MirBuilder,
|
builder: &mut MirBuilder,
|
||||||
condition: &ASTNode,
|
condition: &ASTNode,
|
||||||
body: &[ASTNode],
|
body: &[ASTNode],
|
||||||
func_name: &str,
|
_func_name: &str,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
fn_body: Option<&[ASTNode]>,
|
fn_body: Option<&[ASTNode]>,
|
||||||
skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>,
|
skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>,
|
||||||
) -> Result<Option<ValueId>, String> {
|
) -> Result<Option<ValueId>, 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 verbose = debug || crate::config::env::joinir_dev_enabled();
|
||||||
let log = Pattern2DebugLog::new(verbose);
|
|
||||||
|
|
||||||
super::super::trace::trace().debug("pattern2", "Calling Pattern 2 minimal lowerer");
|
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);
|
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 mut promoted = PromoteStepBox::run(builder, condition, body, inputs, debug, verbose)?;
|
||||||
|
let normalized_body = promoted.normalized_body.take();
|
||||||
let (effective_break_condition, normalized_body) =
|
|
||||||
apply_trim_and_normalize(builder, condition, body, &mut inputs, verbose)?;
|
|
||||||
let analysis_body = normalized_body.as_deref().unwrap_or(body);
|
let analysis_body = normalized_body.as_deref().unwrap_or(body);
|
||||||
|
let effective_break_condition = promoted.effective_break_condition;
|
||||||
// Phase 94: Detect P5b escape-derived (`ch` reassignment + escape counter).
|
let carrier_updates = promoted.carrier_updates;
|
||||||
match classify_p5b_escape_derived(analysis_body, &inputs.loop_var_name) {
|
let emitted = EmitJoinIRStepBox::emit(
|
||||||
P5bEscapeDerivedDecision::Use(recipe) => {
|
builder,
|
||||||
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,
|
condition,
|
||||||
break_condition: &effective_break_condition,
|
analysis_body,
|
||||||
env: &inputs.env,
|
&effective_break_condition,
|
||||||
carrier_info: &inputs.carrier_info,
|
&carrier_updates,
|
||||||
carrier_updates: &carrier_updates,
|
&mut promoted.inputs,
|
||||||
body_ast: analysis_body,
|
debug,
|
||||||
body_local_env: Some(&mut inputs.body_local_env),
|
verbose,
|
||||||
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,
|
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) {
|
MergeStepBox::merge(builder, emitted.join_module, emitted.boundary, debug)
|
||||||
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<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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String> = cond_scope
|
|
||||||
.vars
|
|
||||||
.iter()
|
|
||||||
.filter(|v| matches!(v.scope, crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope::LoopBodyLocal))
|
|
||||||
.map(|v| v.name.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if cond_scope.has_loop_body_local() {
|
|
||||||
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<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::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 },
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -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<Pattern2Inputs, String> {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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<String, crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr>,
|
||||||
|
inputs: &mut Pattern2Inputs,
|
||||||
|
debug: bool,
|
||||||
|
verbose: bool,
|
||||||
|
skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>,
|
||||||
|
) -> Result<EmitJoinIRStepOutput, String> {
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Pattern2Facts, String> {
|
||||||
|
Pattern2InputsFactsBox::analyze(builder, condition, body, fn_body, ctx, verbose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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<Option<ValueId>, 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
@ -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<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;
|
||||||
|
|
||||||
|
impl PromoteStepBox {
|
||||||
|
pub(crate) fn run(
|
||||||
|
builder: &mut MirBuilder,
|
||||||
|
condition: &ASTNode,
|
||||||
|
body: &[ASTNode],
|
||||||
|
mut inputs: Pattern2Inputs,
|
||||||
|
debug: bool,
|
||||||
|
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 =
|
||||||
|
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<String> = cond_scope
|
||||||
|
.vars
|
||||||
|
.iter()
|
||||||
|
.filter(|v| matches!(v.scope, crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope::LoopBodyLocal))
|
||||||
|
.map(|v| v.name.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if cond_scope.has_loop_body_local() {
|
||||||
|
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<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
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -233,7 +233,8 @@ mod tests {
|
|||||||
use crate::ast::Span;
|
use crate::ast::Span;
|
||||||
use crate::mir::ValueId;
|
use crate::mir::ValueId;
|
||||||
use super::super::pattern2_inputs_facts_box::Pattern2InputsFactsBox;
|
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();
|
let mut builder = MirBuilder::new();
|
||||||
builder
|
builder
|
||||||
@ -311,10 +312,12 @@ mod tests {
|
|||||||
|
|
||||||
let ctx = build_pattern_context(&mut builder, &condition, &body, PatternVariant::Pattern2)
|
let ctx = build_pattern_context(&mut builder, &condition, &body, PatternVariant::Pattern2)
|
||||||
.expect("build_pattern_context");
|
.expect("build_pattern_context");
|
||||||
let mut inputs =
|
let facts =
|
||||||
Pattern2InputsFactsBox::analyze(&builder, &condition, &body, None, &ctx, false)
|
Pattern2InputsFactsBox::analyze(&builder, &condition, &body, None, &ctx, false)
|
||||||
.expect("Pattern2InputsFactsBox::analyze");
|
.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");
|
.expect("promote_and_prepare_carriers");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
use super::PolicyDecision;
|
use super::PolicyDecision;
|
||||||
use crate::ast::{ASTNode, BinaryOperator, Span, UnaryOperator};
|
use crate::ast::{ASTNode, BinaryOperator, Span, UnaryOperator};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct LoopTrueReadDigitsPolicyResult {
|
pub(crate) struct LoopTrueReadDigitsPolicyResult {
|
||||||
pub break_condition_node: ASTNode,
|
pub break_condition_node: ASTNode,
|
||||||
pub allowed_ch_var: String,
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user