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 index 07d01185..9bf3a809 100644 --- 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 @@ -17,29 +17,44 @@ impl ApplyPolicyStepBox { // Phase 107: balanced depth-scan (return-in-loop) policy. // This route provides its own break-cond + derived recipe + post-loop early return plan. - if let PolicyDecision::Use(result) = - balanced_depth_scan_policy::classify_balanced_depth_scan_array_end(condition, body) - { - return 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: result.allowed_body_locals_for_conditions, - read_only_body_local_slot: None, - break_condition_node: result.break_condition_node, - is_loop_true_read_digits: false, - condition_only_recipe: None, - body_local_derived_recipe: None, - balanced_depth_scan_recipe: Some(result.derived_recipe), - carrier_updates_override: Some(result.carrier_updates_override), - post_loop_early_return: Some(result.post_loop_early_return), - }); + let decision = balanced_depth_scan_policy::classify_balanced_depth_scan_array_end(condition, body); + if crate::config::env::joinir_dev_enabled() { + use crate::mir::builder::control_flow::joinir::trace; + let summary = match &decision { + PolicyDecision::Use(_) => "Use".to_string(), + PolicyDecision::Reject(reason) => format!("Reject: {}", reason), + PolicyDecision::None => "None".to_string(), + }; + trace::trace().dev("phase107/balanced_depth_scan_policy", &summary); + } + + match decision { + PolicyDecision::Use(result) => { + return 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: result.allowed_body_locals_for_conditions, + read_only_body_local_slot: None, + break_condition_node: result.break_condition_node, + is_loop_true_read_digits: false, + condition_only_recipe: None, + body_local_derived_recipe: None, + balanced_depth_scan_recipe: Some(result.derived_recipe), + carrier_updates_override: Some(result.carrier_updates_override), + post_loop_early_return: Some(result.post_loop_early_return), + }); + } + PolicyDecision::Reject(reason) => { + return Err(reason); + } + PolicyDecision::None => {} } let break_routing = Pattern2BreakConditionPolicyRouterBox::route(condition, body)?; 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 index 7cf42c26..d5b79321 100644 --- 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 @@ -166,7 +166,11 @@ impl PromoteStepBox { .collect(); if cond_scope.has_loop_body_local() { - if !inputs.is_loop_true_read_digits { + // Phase 107: balanced depth-scan policy already provides an allow-list + derived recipe. + // Do not re-run Pattern2 body-local promotion/slot heuristics here (they assume break-guard shapes). + if inputs.balanced_depth_scan_recipe.is_some() { + // no-op: LoopBodyLocalInitLowerer + BalancedDepthScanEmitter will populate the env. + } else if !inputs.is_loop_true_read_digits { match classify_for_pattern2( builder, &inputs.loop_var_name, diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs b/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs index ae5af55e..be61355a 100644 --- a/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs +++ b/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs @@ -476,6 +476,7 @@ fn eq_int(left: ASTNode, n: i64) -> ASTNode { mod tests { use super::*; use crate::ast::Span; + use crate::parser::NyashParser; fn span() -> Span { Span::unknown() @@ -520,6 +521,46 @@ mod tests { } } + fn find_first_loop<'a>(node: &'a ASTNode) -> Option<(&'a ASTNode, &'a [ASTNode])> { + match node { + ASTNode::Loop { condition, body, .. } => Some((condition.as_ref(), body.as_slice())), + ASTNode::Program { statements, .. } => statements.iter().find_map(find_first_loop), + ASTNode::BoxDeclaration { + methods, + constructors, + static_init, + .. + } => { + for v in methods.values() { + if let Some(found) = find_first_loop(v) { + return Some(found); + } + } + for v in constructors.values() { + if let Some(found) = find_first_loop(v) { + return Some(found); + } + } + if let Some(init) = static_init { + if let Some(found) = init.iter().find_map(find_first_loop) { + return Some(found); + } + } + None + } + ASTNode::FunctionDeclaration { body, .. } => body.iter().find_map(find_first_loop), + ASTNode::If { + then_body, + else_body, + .. + } => then_body + .iter() + .find_map(find_first_loop) + .or_else(|| else_body.as_ref()?.iter().find_map(find_first_loop)), + _ => None, + } + } + #[test] fn detects_balanced_array_end_min_shape() { // loop(i < n) { @@ -584,4 +625,29 @@ mod tests { assert!(result.carrier_updates_override.contains_key("i")); assert!(result.carrier_updates_override.contains_key("depth")); } + + #[test] + fn detects_balanced_array_end_min_shape_from_parser_ast() { + let src = r#" +static box Main { + f(s, idx) { + local n = s.length() + if s.substring(idx, idx+1) != "[" { return -1 } + local depth = 0 + local i = idx + loop (i < n) { + local ch = s.substring(i, i+1) + if ch == "[" { depth = depth + 1 } + if ch == "]" { depth = depth - 1 if depth == 0 { return i } } + i = i + 1 + } + return -1 + } +} +"#; + let ast = NyashParser::parse_from_string(src).expect("parse ok"); + let (condition, body) = find_first_loop(&ast).expect("find loop"); + let decision = classify_balanced_depth_scan_array_end(condition, body); + assert!(matches!(decision, PolicyDecision::Use(_)), "got {:?}", decision); + } } diff --git a/src/mir/builder/control_flow/joinir/routing.rs b/src/mir/builder/control_flow/joinir/routing.rs index 271e36f3..741ac361 100644 --- a/src/mir/builder/control_flow/joinir/routing.rs +++ b/src/mir/builder/control_flow/joinir/routing.rs @@ -24,11 +24,18 @@ pub(in crate::mir::builder) fn choose_pattern_kind( // Phase 107: Route balanced depth-scan (return-in-loop) to Pattern2 via policy. // // This keeps Pattern routing structural: no by-name dispatch, no silent fallback. - if matches!( - classify_balanced_depth_scan_array_end(condition, body), - PolicyDecision::Use(_) - ) { - return loop_pattern_detection::LoopPatternKind::Pattern2Break; + match classify_balanced_depth_scan_array_end(condition, body) { + PolicyDecision::Use(_) => { + return loop_pattern_detection::LoopPatternKind::Pattern2Break; + } + PolicyDecision::Reject(_reason) => { + // In strict mode, treat "close-but-unsupported" as a fail-fast + // Pattern2 route so the policy can surface the precise contract violation. + if crate::config::env::joinir_dev::strict_enabled() { + return loop_pattern_detection::LoopPatternKind::Pattern2Break; + } + } + PolicyDecision::None => {} } // Phase 193: Use AST Feature Extractor Box for break/continue detection