//! DigitPosDetector - Pure detection logic for digit position pattern //! //! Extracted from DigitPosPromoter to enable: //! - Single responsibility (detection only) //! - Independent unit testing //! - Reusable pattern for future analyzers //! //! # Design Philosophy //! //! This detector follows the **Detector/Recorder separation** principle: //! - **Detector**: Pure detection logic (this module) //! - **Recorder**: PromotedBindingRecorder (records BindingId mappings) //! - **Promoter**: Orchestrates detection + recording + carrier building //! //! # Pattern: A-4 DigitPos (Cascading indexOf) //! //! ```nyash //! loop(p < s.length()) { //! local ch = s.substring(p, p+1) // First LoopBodyLocal //! local digit_pos = digits.indexOf(ch) // Second LoopBodyLocal (cascading) //! //! if digit_pos < 0 { // Comparison condition //! break //! } //! //! // Continue processing... //! p = p + 1 //! } //! ``` use crate::ast::{ASTNode, BinaryOperator}; /// Detection result for digit position pattern. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DigitPosDetectionResult { /// Variable name that was promoted (e.g., "digit_pos") pub var_name: String, /// Bool carrier name (e.g., "is_digit_pos") pub bool_carrier_name: String, /// Integer carrier name (e.g., "digit_value") pub int_carrier_name: String, } /// Pure detection logic for A-4 DigitPos pattern pub struct DigitPosDetector; impl DigitPosDetector { /// Detect digit position pattern in condition and body. /// /// Returns None if pattern not found, Some(result) if detected. /// /// # Algorithm /// /// 1. Extract comparison variable from condition (e.g., "digit_pos") /// 2. Find indexOf() definition in loop body /// 3. Verify cascading dependency (indexOf depends on another LoopBodyLocal via substring) /// 4. Generate carrier names (bool + int) /// /// # Arguments /// /// * `condition` - Break or continue condition AST node /// * `body` - Loop body statements /// * `loop_var` - Loop parameter name (currently unused, for future use) /// /// # Returns /// /// * `Some(DigitPosDetectionResult)` if pattern detected /// * `None` if pattern not found pub fn detect( condition: &ASTNode, body: &[ASTNode], _loop_var: &str, ) -> Option { // Step 1: Extract comparison variable from condition let var_in_cond = Self::extract_comparison_var(condition)?; // Step 2: Find indexOf() definition for the comparison variable let definition = Self::find_index_of_definition(body, &var_in_cond)?; // Step 3: Verify it's an indexOf() method call if !Self::is_index_of_method_call(definition) { return None; } // Step 4: Verify cascading dependency (indexOf depends on LoopBodyLocal) let _dependency = Self::find_first_loopbodylocal_dependency(body, definition)?; // Step 5: Generate carrier names // Phase 247-EX: DigitPos generates TWO carriers (dual-value model) // - is_ (boolean): for break condition // - _value (integer): for NumberAccumulation // Naming: "digit_pos" → "is_digit_pos" + "digit_value" (not "digit_pos_value") let bool_carrier_name = format!("is_{}", var_in_cond); // Extract the base name for integer carrier (e.g., "digit_pos" → "digit") let base_name = if var_in_cond.ends_with("_pos") { &var_in_cond[..var_in_cond.len() - 4] // Remove "_pos" suffix } else { var_in_cond.as_str() }; let int_carrier_name = format!("{}_value", base_name); Some(DigitPosDetectionResult { var_name: var_in_cond, bool_carrier_name, int_carrier_name, }) } /// Find indexOf() definition in loop body /// /// Searches for assignment: `local var = ...indexOf(...)` or `var = ...indexOf(...)` fn find_index_of_definition<'a>(body: &'a [ASTNode], var_name: &str) -> Option<&'a ASTNode> { let mut worklist: Vec<&'a ASTNode> = body.iter().collect(); while let Some(node) = worklist.pop() { match node { // Assignment: target = value ASTNode::Assignment { target, value, .. } => { if let ASTNode::Variable { name, .. } = target.as_ref() { if name == var_name { return Some(value.as_ref()); } } } // Local: local var = value ASTNode::Local { variables, initial_values, .. } if initial_values.len() == variables.len() => { for (i, var) in variables.iter().enumerate() { if var == var_name { if let Some(Some(init_expr)) = initial_values.get(i) { return Some(init_expr.as_ref()); } } } } // Nested structures ASTNode::If { then_body, else_body, .. } => { for stmt in then_body { worklist.push(stmt); } if let Some(else_stmts) = else_body { for stmt in else_stmts { worklist.push(stmt); } } } ASTNode::Loop { body: loop_body, .. } => { for stmt in loop_body { worklist.push(stmt); } } _ => {} } } None } /// Check if node is an indexOf() method call fn is_index_of_method_call(node: &ASTNode) -> bool { matches!( node, ASTNode::MethodCall { method, .. } if method == "indexOf" ) } /// Extract variable used in comparison condition /// /// Handles: `if digit_pos < 0`, `if digit_pos >= 0`, etc. fn extract_comparison_var(cond: &ASTNode) -> Option { match cond { ASTNode::BinaryOp { operator, left, .. } => { // Check if it's a comparison operator (not equality) match operator { BinaryOperator::Less | BinaryOperator::LessEqual | BinaryOperator::Greater | BinaryOperator::GreaterEqual | BinaryOperator::NotEqual => { // Extract variable from left side if let ASTNode::Variable { name, .. } = left.as_ref() { return Some(name.clone()); } } _ => {} } } // UnaryOp: not (...) ASTNode::UnaryOp { operand, .. } => { return Self::extract_comparison_var(operand.as_ref()); } _ => {} } None } /// Find first LoopBodyLocal dependency in indexOf() call /// /// Example: `digits.indexOf(ch)` → returns "ch" if it's a LoopBodyLocal fn find_first_loopbodylocal_dependency<'a>( body: &'a [ASTNode], index_of_call: &'a ASTNode, ) -> Option<&'a str> { if let ASTNode::MethodCall { arguments, .. } = index_of_call { // Check first argument (e.g., "ch" in indexOf(ch)) if let Some(arg) = arguments.first() { if let ASTNode::Variable { name, .. } = arg { // Verify it's defined by substring() in body let def = Self::find_definition_in_body(body, name); if let Some(def_node) = def { if Self::is_substring_method_call(def_node) { return Some(name.as_str()); } } } } } None } /// Find definition in loop body (helper) fn find_definition_in_body<'a>(body: &'a [ASTNode], var_name: &str) -> Option<&'a ASTNode> { let mut worklist: Vec<&'a ASTNode> = body.iter().collect(); while let Some(node) = worklist.pop() { match node { ASTNode::Assignment { target, value, .. } => { if let ASTNode::Variable { name, .. } = target.as_ref() { if name == var_name { return Some(value.as_ref()); } } } ASTNode::Local { variables, initial_values, .. } if initial_values.len() == variables.len() => { for (i, var) in variables.iter().enumerate() { if var == var_name { if let Some(Some(init_expr)) = initial_values.get(i) { return Some(init_expr.as_ref()); } } } } ASTNode::If { then_body, else_body, .. } => { for stmt in then_body { worklist.push(stmt); } if let Some(else_stmts) = else_body { for stmt in else_stmts { worklist.push(stmt); } } } ASTNode::Loop { body: loop_body, .. } => { for stmt in loop_body { worklist.push(stmt); } } _ => {} } } None } /// Check if node is a substring() method call fn is_substring_method_call(node: &ASTNode) -> bool { matches!( node, ASTNode::MethodCall { method, .. } if method == "substring" ) } } #[cfg(test)] mod tests { use super::*; use crate::ast::{LiteralValue, Span}; fn var_node(name: &str) -> ASTNode { ASTNode::Variable { name: name.to_string(), span: Span::unknown(), } } fn int_literal(value: i64) -> ASTNode { ASTNode::Literal { value: LiteralValue::Integer(value), span: Span::unknown(), } } fn method_call(object: &str, method: &str, args: Vec) -> ASTNode { ASTNode::MethodCall { object: Box::new(var_node(object)), method: method.to_string(), arguments: args, span: Span::unknown(), } } fn assignment(target: &str, value: ASTNode) -> ASTNode { ASTNode::Assignment { target: Box::new(var_node(target)), value: Box::new(value), span: Span::unknown(), } } fn comparison(var: &str, op: BinaryOperator, literal: i64) -> ASTNode { ASTNode::BinaryOp { operator: op, left: Box::new(var_node(var)), right: Box::new(int_literal(literal)), span: Span::unknown(), } } #[test] fn test_detect_basic_pattern() { // Full A-4 pattern: // local ch = s.substring(...) // local digit_pos = digits.indexOf(ch) // if digit_pos < 0 { break } let loop_body = vec![ assignment("ch", method_call("s", "substring", vec![])), assignment( "digit_pos", method_call("digits", "indexOf", vec![var_node("ch")]), ), ]; let condition = comparison("digit_pos", BinaryOperator::Less, 0); let result = DigitPosDetector::detect(&condition, &loop_body, "p"); assert!(result.is_some()); let result = result.unwrap(); assert_eq!(result.var_name, "digit_pos"); assert_eq!(result.bool_carrier_name, "is_digit_pos"); assert_eq!(result.int_carrier_name, "digit_value"); } #[test] fn test_detect_no_match_non_index_of() { // ch = s.substring(...) → pos = s.length() → if pos < 0 // Should fail: not indexOf() let loop_body = vec![ assignment("ch", method_call("s", "substring", vec![])), assignment("pos", method_call("s", "length", vec![])), // NOT indexOf ]; let condition = comparison("pos", BinaryOperator::Less, 0); let result = DigitPosDetector::detect(&condition, &loop_body, "p"); assert!(result.is_none()); } #[test] fn test_detect_no_match_no_loopbodylocal_dependency() { // digit_pos = fixed_string.indexOf("x") // No LoopBodyLocal dependency // Should fail: indexOf doesn't depend on substring LoopBodyLocal let loop_body = vec![assignment( "digit_pos", method_call( "fixed_string", "indexOf", vec![ASTNode::Literal { value: LiteralValue::String("x".to_string()), span: Span::unknown(), }], ), )]; let condition = comparison("digit_pos", BinaryOperator::Less, 0); let result = DigitPosDetector::detect(&condition, &loop_body, "p"); assert!(result.is_none()); } #[test] fn test_detect_comparison_operators() { // Test different comparison operators: <, >, <=, >=, != let operators = vec![ BinaryOperator::Less, BinaryOperator::Greater, BinaryOperator::LessEqual, BinaryOperator::GreaterEqual, BinaryOperator::NotEqual, ]; for op in operators { let loop_body = vec![ assignment("ch", method_call("s", "substring", vec![])), assignment( "digit_pos", method_call("digits", "indexOf", vec![var_node("ch")]), ), ]; let condition = comparison("digit_pos", op.clone(), 0); let result = DigitPosDetector::detect(&condition, &loop_body, "p"); assert!(result.is_some(), "Expected detection for operator {:?}", op); } } #[test] fn test_detect_equality_operator_fails() { // if digit_pos == -1 { break } // Should fail: Equality is A-3 Trim territory, not A-4 DigitPos let loop_body = vec![ assignment("ch", method_call("s", "substring", vec![])), assignment( "digit_pos", method_call("digits", "indexOf", vec![var_node("ch")]), ), ]; let condition = ASTNode::BinaryOp { operator: BinaryOperator::Equal, // Equality, not comparison left: Box::new(var_node("digit_pos")), right: Box::new(int_literal(-1)), span: Span::unknown(), }; let result = DigitPosDetector::detect(&condition, &loop_body, "p"); assert!(result.is_none()); } #[test] fn test_carrier_name_generation() { // Test carrier name generation: "digit_pos" → "is_digit_pos" + "digit_value" let loop_body = vec![ assignment("ch", method_call("s", "substring", vec![])), assignment( "digit_pos", method_call("digits", "indexOf", vec![var_node("ch")]), ), ]; let condition = comparison("digit_pos", BinaryOperator::Less, 0); let result = DigitPosDetector::detect(&condition, &loop_body, "p").unwrap(); assert_eq!(result.bool_carrier_name, "is_digit_pos"); assert_eq!(result.int_carrier_name, "digit_value"); // Not "digit_pos_value" } #[test] fn test_carrier_name_without_pos_suffix() { // Test carrier name for variable without "_pos" suffix let loop_body = vec![ assignment("ch", method_call("s", "substring", vec![])), assignment( "index", method_call("digits", "indexOf", vec![var_node("ch")]), ), ]; let condition = comparison("index", BinaryOperator::Less, 0); let result = DigitPosDetector::detect(&condition, &loop_body, "p").unwrap(); assert_eq!(result.bool_carrier_name, "is_index"); assert_eq!(result.int_carrier_name, "index_value"); } }