Files
hakorune/src/mir/loop_pattern_detection/digitpos_detector.rs
nyash-codex e404746612 refactor(mir): Phase 139-P3-B - RoutingDecision を enum 対応 + レガシー削除
- RoutingDecision の missing_caps を Vec<CapabilityTag> に変更(型安全化)
- error_tags は to_tag() メソッドで自動生成
- 全 callsite を enum variant に修正
- capability_tags モジュール(文字列定数群)を完全削除
- 全テスト PASS(型安全性向上を確認)
- フォーマット適用
2025-12-16 07:02:14 +09:00

513 lines
17 KiB
Rust

//! 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<DigitPosDetectionResult> {
// 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_<var> (boolean): for break condition
// - <prefix>_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<String> {
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 {
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");
}
}