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 new file mode 100644 index 00000000..ae5af55e --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs @@ -0,0 +1,587 @@ +//! Phase 107: Balanced depth-scan policy (json_cur find_balanced_* family) +//! +//! Responsibility (analysis only): +//! - Recognize the `depth` scan loop shape with nested-if + `return i` +//! - Produce a Pattern2-compatible break condition + derived recipe inputs +//! - Fail-fast with tagged reasons when the shape is close but unsupported + +use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; +use crate::mir::join_ir::lowering::common::balanced_depth_scan_emitter::BalancedDepthScanRecipe; +use crate::mir::join_ir::lowering::error_tags; +use crate::mir::join_ir::lowering::loop_update_analyzer::{UpdateExpr, UpdateRhs}; +use crate::mir::join_ir::BinOpKind; + +use super::PolicyDecision; +use std::collections::BTreeMap; + +#[derive(Debug, Clone)] +pub(crate) struct PostLoopEarlyReturnPlan { + pub loop_counter_name: String, + pub bound_name: String, +} + +#[derive(Debug, Clone)] +pub(crate) struct BalancedDepthScanPolicyResult { + pub break_condition_node: ASTNode, + pub allowed_body_locals_for_conditions: Vec, + pub carrier_updates_override: BTreeMap, + pub derived_recipe: BalancedDepthScanRecipe, + pub post_loop_early_return: PostLoopEarlyReturnPlan, +} + +pub(crate) fn classify_balanced_depth_scan_array_end( + condition: &ASTNode, + body: &[ASTNode], +) -> PolicyDecision { + classify_balanced_depth_scan(condition, body, "[", "]") +} + +fn classify_balanced_depth_scan( + condition: &ASTNode, + body: &[ASTNode], + open: &str, + close: &str, +) -> PolicyDecision { + // bounded loop: loop(i < n) + let (loop_counter_name, bound_name) = match extract_bounded_loop_counter(condition) { + Some(v) => v, + None => return PolicyDecision::None, + }; + + let summary = match extract_depth_scan_shape(body, &loop_counter_name, open, close) { + Ok(v) => v, + Err(reason) => return PolicyDecision::Reject(reason), + }; + + let depth_delta_name = "depth_delta".to_string(); + let depth_next_name = "depth_next".to_string(); + if summary.declared_locals.contains(&depth_delta_name) || summary.declared_locals.contains(&depth_next_name) { + return PolicyDecision::Reject(error_tags::freeze( + "[phase107/balanced_depth_scan/contract/name_conflict] 'depth_delta' or 'depth_next' is already declared in the loop body", + )); + } + + let break_condition_node = ASTNode::BinaryOp { + operator: BinaryOperator::And, + left: Box::new(eq_str(var(&summary.ch_name), close)), + right: Box::new(eq_int(var(&depth_next_name), 0)), + span: Span::unknown(), + }; + + // Carrier update override (SSOT): depth = depth + depth_delta, i = i + 1 + let mut carrier_updates_override: BTreeMap = BTreeMap::new(); + carrier_updates_override.insert(loop_counter_name.clone(), UpdateExpr::Const(1)); + carrier_updates_override.insert( + summary.depth_name.clone(), + UpdateExpr::BinOp { + lhs: summary.depth_name.clone(), + op: BinOpKind::Add, + rhs: UpdateRhs::Variable(depth_delta_name.clone()), + }, + ); + + PolicyDecision::Use(BalancedDepthScanPolicyResult { + break_condition_node, + allowed_body_locals_for_conditions: vec![summary.ch_name.clone(), depth_next_name.clone()], + carrier_updates_override, + derived_recipe: BalancedDepthScanRecipe { + depth_var: summary.depth_name, + ch_var: summary.ch_name, + open: open.to_string(), + close: close.to_string(), + depth_delta_name, + depth_next_name, + }, + post_loop_early_return: PostLoopEarlyReturnPlan { + loop_counter_name, + bound_name, + }, + }) +} + +#[derive(Debug)] +struct DepthScanShapeSummary { + ch_name: String, + depth_name: String, + declared_locals: std::collections::BTreeSet, +} + +fn extract_bounded_loop_counter(condition: &ASTNode) -> Option<(String, String)> { + match condition { + ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left, + right, + .. + } => match (left.as_ref(), right.as_ref()) { + (ASTNode::Variable { name: i, .. }, ASTNode::Variable { name: n, .. }) => { + Some((i.clone(), n.clone())) + } + _ => None, + }, + _ => None, + } +} + +fn extract_depth_scan_shape( + body: &[ASTNode], + loop_counter_name: &str, + open: &str, + close: &str, +) -> Result { + if body.is_empty() { + return Err(error_tags::freeze( + "[phase107/balanced_depth_scan/contract/empty_body] empty loop body", + )); + } + + // Collect declared locals to protect derived slot names. + let mut declared_locals: std::collections::BTreeSet = std::collections::BTreeSet::new(); + for stmt in body { + if let ASTNode::Local { variables, .. } = stmt { + for v in variables { + declared_locals.insert(v.clone()); + } + } + } + + // Find `local ch = s.substring(i, i+1)` (name may vary, but must be a single local). + let ch_name = find_substring_body_local(body, loop_counter_name).ok_or_else(|| { + error_tags::freeze( + "[phase107/balanced_depth_scan/contract/missing_ch] missing body-local `ch = s.substring(i, i+1)`", + ) + })?; + + // Find open/close branches and extract `depth` name. + let (depth_from_open, depth_from_close, has_return_i) = + find_depth_branches(body, &ch_name, loop_counter_name, open, close)?; + + if depth_from_open != depth_from_close { + return Err(error_tags::freeze(&format!( + "[phase107/balanced_depth_scan/contract/depth_mismatch] depth variable differs: open='{}', close='{}'", + depth_from_open, depth_from_close + ))); + } + + if !has_return_i { + return Err(error_tags::freeze( + "[phase107/balanced_depth_scan/contract/missing_return_i] missing `if depth == 0 { return i }` inside close branch", + )); + } + + // Require a tail `i = i + 1` at top-level (keeps the family narrow). + let has_tail_inc = body.iter().any(|n| is_inc_assign(n, loop_counter_name, 1)); + if !has_tail_inc { + return Err(error_tags::freeze( + "[phase107/balanced_depth_scan/contract/missing_tail_inc] missing `i = i + 1` tail update", + )); + } + + // Reject other breaks/continues and non-matching returns (fail-fast). + let mut return_count = 0usize; + for stmt in body { + scan_control_flow(stmt, &mut return_count)?; + } + if return_count != 1 { + return Err(error_tags::freeze(&format!( + "[phase107/balanced_depth_scan/contract/return_count] expected exactly 1 return in loop body, got {}", + return_count + ))); + } + + Ok(DepthScanShapeSummary { + ch_name, + depth_name: depth_from_open, + declared_locals, + }) +} + +fn scan_control_flow(node: &ASTNode, return_count: &mut usize) -> Result<(), String> { + match node { + ASTNode::Break { .. } => Err(error_tags::freeze( + "[phase107/balanced_depth_scan/contract/unexpected_break] break is not allowed in this family (return-in-loop only)", + )), + ASTNode::Continue { .. } => Err(error_tags::freeze( + "[phase107/balanced_depth_scan/contract/unexpected_continue] continue is not allowed in this family", + )), + ASTNode::Return { .. } => { + *return_count += 1; + Ok(()) + } + ASTNode::If { + then_body, + else_body, + .. + } => { + for s in then_body { + scan_control_flow(s, return_count)?; + } + if let Some(else_body) = else_body { + for s in else_body { + scan_control_flow(s, return_count)?; + } + } + Ok(()) + } + _ => Ok(()), + } +} + +fn find_substring_body_local(body: &[ASTNode], loop_counter_name: &str) -> Option { + for stmt in body { + let (name, init) = match stmt { + ASTNode::Local { + variables, + initial_values, + .. + } if variables.len() == 1 && initial_values.len() == 1 => ( + variables[0].clone(), + initial_values[0].as_deref()?, + ), + _ => continue, + }; + + let (object, method, args) = match init { + ASTNode::MethodCall { + object, + method, + arguments, + .. + } => (object.as_ref(), method.as_str(), arguments.as_slice()), + _ => continue, + }; + if method != "substring" { + continue; + } + if !matches!(object, ASTNode::Variable { .. }) { + continue; + } + if args.len() != 2 { + continue; + } + if !matches!(&args[0], ASTNode::Variable { name, .. } if name == loop_counter_name) { + continue; + } + if !is_var_plus_int(&args[1], loop_counter_name, 1) { + continue; + } + return Some(name); + } + None +} + +fn find_depth_branches( + body: &[ASTNode], + ch_name: &str, + loop_counter_name: &str, + open: &str, + close: &str, +) -> Result<(String, String, bool), String> { + let mut open_depth: Option = None; + let mut close_depth: Option = None; + let mut has_return_i = false; + + for stmt in body { + let (cond, then_body) = match stmt { + ASTNode::If { + condition, + then_body, + else_body, + .. + } if else_body.is_none() => (condition.as_ref(), then_body.as_slice()), + _ => continue, + }; + + let lit = match cond { + ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left, + right, + .. + } if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == ch_name) => match right.as_ref() { + ASTNode::Literal { value: LiteralValue::String(s), .. } => s.as_str(), + _ => continue, + }, + _ => continue, + }; + + if lit == open { + let depth_name = find_depth_delta_assign(then_body, 1)?; + open_depth = Some(depth_name); + } else if lit == close { + let depth_name = find_depth_delta_assign(then_body, -1)?; + close_depth = Some(depth_name.clone()); + has_return_i = find_depth_zero_return(then_body, &depth_name, loop_counter_name); + } + } + + let open_depth = open_depth.ok_or_else(|| { + error_tags::freeze( + "[phase107/balanced_depth_scan/contract/missing_open_branch] missing `if ch == open { depth = depth + 1 }`", + ) + })?; + let close_depth = close_depth.ok_or_else(|| { + error_tags::freeze( + "[phase107/balanced_depth_scan/contract/missing_close_branch] missing `if ch == close { depth = depth - 1 ... }`", + ) + })?; + + Ok((open_depth, close_depth, has_return_i)) +} + +fn find_depth_delta_assign(stmts: &[ASTNode], delta: i64) -> Result { + for stmt in stmts { + if let ASTNode::Assignment { target, value, .. } = stmt { + let depth_name = match target.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => continue, + }; + if delta == 1 && is_add_assign(value.as_ref(), &depth_name, 1) { + return Ok(depth_name); + } + if delta == -1 && is_sub_assign(value.as_ref(), &depth_name, 1) { + return Ok(depth_name); + } + } + } + + Err(error_tags::freeze(&format!( + "[phase107/balanced_depth_scan/contract/missing_depth_update] missing `depth = depth {} 1` in branch", + if delta == 1 { "+" } else { "-" } + ))) +} + +fn find_depth_zero_return(stmts: &[ASTNode], depth_name: &str, loop_counter_name: &str) -> bool { + for stmt in stmts { + let (cond, then_body) = match stmt { + ASTNode::If { + condition, + then_body, + else_body, + .. + } if else_body.is_none() => (condition.as_ref(), then_body.as_slice()), + _ => continue, + }; + + let is_depth_zero = matches!( + cond, + ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left, + right, + .. + } + if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == depth_name) + && matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(0), .. }) + ); + if !is_depth_zero { + continue; + } + + if then_body.iter().any(|n| matches!( + n, + ASTNode::Return { value: Some(v), .. } + if matches!(v.as_ref(), ASTNode::Variable { name, .. } if name == loop_counter_name) + )) { + return true; + } + } + false +} + +fn is_inc_assign(node: &ASTNode, var_name: &str, step: i64) -> bool { + match node { + ASTNode::Assignment { target, value, .. } => match target.as_ref() { + ASTNode::Variable { name, .. } if name == var_name => is_add_assign(value.as_ref(), var_name, step), + _ => false, + }, + _ => false, + } +} + +fn is_add_assign(value: &ASTNode, var_name: &str, step: i64) -> bool { + matches!( + value, + ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } + if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == var_name) + && matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(n), .. } if *n == step) + ) +} + +fn is_sub_assign(value: &ASTNode, var_name: &str, step: i64) -> bool { + matches!( + value, + ASTNode::BinaryOp { + operator: BinaryOperator::Subtract, + left, + right, + .. + } + if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == var_name) + && matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(n), .. } if *n == step) + ) +} + +fn is_var_plus_int(node: &ASTNode, var_name: &str, n: i64) -> bool { + matches!( + node, + ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } + if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == var_name) + && matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(m), .. } if *m == n) + ) +} + +fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } +} + +fn eq_str(left: ASTNode, s: &str) -> ASTNode { + ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left: Box::new(left), + right: Box::new(ASTNode::Literal { + value: LiteralValue::String(s.to_string()), + span: Span::unknown(), + }), + span: Span::unknown(), + } +} + +fn eq_int(left: ASTNode, n: i64) -> ASTNode { + ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left: Box::new(left), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(n), + span: Span::unknown(), + }), + span: Span::unknown(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::Span; + + fn span() -> Span { + Span::unknown() + } + + fn var_node(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: span(), + } + } + + fn int_lit(n: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(n), + span: span(), + } + } + + fn str_lit(s: &str) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::String(s.to_string()), + span: span(), + } + } + + fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: op, + left: Box::new(left), + right: Box::new(right), + span: span(), + } + } + + fn if_then(cond: ASTNode, then_body: Vec) -> ASTNode { + ASTNode::If { + condition: Box::new(cond), + then_body, + else_body: None, + span: span(), + } + } + + #[test] + fn detects_balanced_array_end_min_shape() { + // 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 + // } + let condition = bin(BinaryOperator::Less, var_node("i"), var_node("n")); + let local_ch = ASTNode::Local { + variables: vec!["ch".to_string()], + initial_values: vec![Some(Box::new(ASTNode::MethodCall { + object: Box::new(var_node("s")), + method: "substring".to_string(), + arguments: vec![var_node("i"), bin(BinaryOperator::Add, var_node("i"), int_lit(1))], + span: span(), + }))], + span: span(), + }; + let open_branch = if_then( + bin(BinaryOperator::Equal, var_node("ch"), str_lit("[")), + vec![ASTNode::Assignment { + target: Box::new(var_node("depth")), + value: Box::new(bin(BinaryOperator::Add, var_node("depth"), int_lit(1))), + span: span(), + }], + ); + let close_branch = if_then( + bin(BinaryOperator::Equal, var_node("ch"), str_lit("]")), + vec![ + ASTNode::Assignment { + target: Box::new(var_node("depth")), + value: Box::new(bin(BinaryOperator::Subtract, var_node("depth"), int_lit(1))), + span: span(), + }, + if_then( + bin(BinaryOperator::Equal, var_node("depth"), int_lit(0)), + vec![ASTNode::Return { + value: Some(Box::new(var_node("i"))), + span: span(), + }], + ), + ], + ); + let tail_inc = ASTNode::Assignment { + target: Box::new(var_node("i")), + value: Box::new(bin(BinaryOperator::Add, var_node("i"), int_lit(1))), + span: span(), + }; + + let body = vec![local_ch, open_branch, close_branch, tail_inc]; + let decision = classify_balanced_depth_scan_array_end(&condition, &body); + let result = match decision { + PolicyDecision::Use(v) => v, + other => panic!("expected Use, got {:?}", other), + }; + + assert_eq!(result.post_loop_early_return.loop_counter_name, "i"); + assert_eq!(result.post_loop_early_return.bound_name, "n"); + assert!(result.allowed_body_locals_for_conditions.contains(&"ch".to_string())); + assert!(result.allowed_body_locals_for_conditions.contains(&"depth_next".to_string())); + assert!(result.carrier_updates_override.contains_key("i")); + assert!(result.carrier_updates_override.contains_key("depth")); + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs b/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs index 07b5565c..89674244 100644 --- a/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/policies/mod.rs @@ -33,3 +33,4 @@ pub enum PolicyDecision { pub(in crate::mir::builder) mod p5b_escape_derived_policy; pub(in crate::mir::builder) mod trim_policy; pub(in crate::mir::builder) mod loop_true_read_digits_policy; +pub(in crate::mir::builder) mod balanced_depth_scan_policy; diff --git a/src/mir/join_ir/lowering/common.rs b/src/mir/join_ir/lowering/common.rs index 505a1dab..a9a0ad1b 100644 --- a/src/mir/join_ir/lowering/common.rs +++ b/src/mir/join_ir/lowering/common.rs @@ -8,6 +8,7 @@ pub mod body_local_slot; // Phase 92 P3: Read-only body-local slot for condition pub mod dual_value_rewriter; // Phase 246-EX/247-EX: name-based dual-value rewrites pub mod condition_only_emitter; // Phase 93 P0: ConditionOnly (Derived Slot) recalculation pub mod body_local_derived_emitter; // Phase 94: Derived body-local (P5b escape "ch" reassignment) +pub mod balanced_depth_scan_emitter; // Phase 107: Balanced depth-scan (find_balanced_* recipe) pub mod string_accumulator_emitter; // Phase 100 P3-2: String accumulator (out = out + ch) use crate::mir::loop_form::LoopForm; diff --git a/src/mir/join_ir/lowering/common/balanced_depth_scan_emitter.rs b/src/mir/join_ir/lowering/common/balanced_depth_scan_emitter.rs new file mode 100644 index 00000000..61be5272 --- /dev/null +++ b/src/mir/join_ir/lowering/common/balanced_depth_scan_emitter.rs @@ -0,0 +1,17 @@ +//! Phase 107: Balanced depth-scan derived emission (recipe only) +//! +//! This recipe is produced by builder-side policy (`patterns/policies/*`) and +//! consumed by JoinIR lowering to derive: +//! - `depth_delta`: per-iteration delta (-1/0/+1) +//! - `depth_next`: depth + depth_delta (for break checks) + +#[derive(Debug, Clone)] +pub struct BalancedDepthScanRecipe { + pub depth_var: String, + pub ch_var: String, + pub open: String, + pub close: String, + pub depth_delta_name: String, + pub depth_next_name: String, +} +