Files
hakorune/src/mir/loop_pattern_detection/trim_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

459 lines
14 KiB
Rust

//! TrimDetector - Pure detection logic for trim pattern
//!
//! Extracted from LoopBodyCarrierPromoter 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-3 Trim (Substring + Equality OR Chain)
//!
//! ```nyash
//! loop(start < end) {
//! local ch = s.substring(start, start+1)
//! if ch == " " || ch == "\t" || ch == "\n" {
//! start = start + 1
//! } else {
//! break
//! }
//! }
//! ```
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
/// Detection result for trim pattern.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrimDetectionResult {
/// Variable name (e.g., "ch")
pub match_var: String,
/// Carrier name (e.g., "is_ch_match")
pub carrier_name: String,
/// Comparison literals (e.g., [" ", "\t", "\n", "\r"])
pub comparison_literals: Vec<String>,
}
/// Pure detection logic for A-3 Trim pattern
pub struct TrimDetector;
impl TrimDetector {
/// Detect trim pattern in condition and body.
///
/// Returns None if pattern not found, Some(result) if detected.
///
/// # Algorithm
///
/// 1. Extract equality literals from condition (e.g., [" ", "\t"])
/// 2. Find substring() definition in loop body
/// 3. Generate carrier name (e.g., "is_ch_match")
///
/// # Arguments
///
/// * `condition` - Break or continue condition AST node
/// * `body` - Loop body statements
/// * `var_name` - Variable name to check (from LoopBodyLocal analysis)
///
/// # Returns
///
/// * `Some(TrimDetectionResult)` if pattern detected
/// * `None` if pattern not found
pub fn detect(
condition: &ASTNode,
body: &[ASTNode],
var_name: &str,
) -> Option<TrimDetectionResult> {
// Step 1: Find substring() definition for the variable
let definition = Self::find_definition_in_body(body, var_name)?;
// Step 2: Verify it's a substring() method call
if !Self::is_substring_method_call(definition) {
return None;
}
// Step 3: Extract equality literals from condition
let literals = Self::extract_equality_literals(condition, var_name);
if literals.is_empty() {
return None;
}
// Step 4: Generate carrier name
let carrier_name = format!("is_{}_match", var_name);
Some(TrimDetectionResult {
match_var: var_name.to_string(),
carrier_name,
comparison_literals: literals,
})
}
/// Find definition in loop body
///
/// Searches for assignment: `local var = ...` or `var = ...`
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 {
// 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 a substring() method call
fn is_substring_method_call(node: &ASTNode) -> bool {
matches!(
node,
ASTNode::MethodCall { method, .. } if method == "substring"
)
}
/// Extract equality literals from condition
///
/// Handles: `ch == " " || ch == "\t" || ch == "\n"`
///
/// Returns: `[" ", "\t", "\n"]`
fn extract_equality_literals(cond: &ASTNode, var_name: &str) -> Vec<String> {
let mut result = Vec::new();
let mut worklist = vec![cond];
while let Some(node) = worklist.pop() {
match node {
// BinaryOp: Or splits, Equal compares
ASTNode::BinaryOp {
operator,
left,
right,
..
} => match operator {
BinaryOperator::Or => {
// OR chain: traverse both sides
worklist.push(left.as_ref());
worklist.push(right.as_ref());
}
BinaryOperator::Equal => {
// Equality comparison: extract literal
if let Some(literal) = Self::extract_literal_from_equality(
left.as_ref(),
right.as_ref(),
var_name,
) {
result.push(literal);
}
}
_ => {}
},
// UnaryOp: not (...)
ASTNode::UnaryOp { operand, .. } => {
worklist.push(operand.as_ref());
}
_ => {}
}
}
result
}
/// Extract string literal from equality comparison
///
/// Handles: `ch == " "` or `" " == ch`
fn extract_literal_from_equality(
left: &ASTNode,
right: &ASTNode,
var_name: &str,
) -> Option<String> {
// Pattern 1: var == literal
if let ASTNode::Variable { name, .. } = left {
if name == var_name {
if let ASTNode::Literal {
value: LiteralValue::String(s),
..
} = right
{
return Some(s.clone());
}
}
}
// Pattern 2: literal == var
if let ASTNode::Literal {
value: LiteralValue::String(s),
..
} = left
{
if let ASTNode::Variable { name, .. } = right {
if name == var_name {
return Some(s.clone());
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Span;
fn var_node(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: Span::unknown(),
}
}
fn string_literal(value: &str) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::String(value.to_string()),
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 equality(var: &str, literal: &str) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(var_node(var)),
right: Box::new(string_literal(literal)),
span: Span::unknown(),
}
}
fn or_op(left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Or,
left: Box::new(left),
right: Box::new(right),
span: Span::unknown(),
}
}
#[test]
fn test_detect_basic_trim_pattern() {
// local ch = s.substring(...)
// if ch == " " || ch == "\t" { ... }
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
let condition = or_op(equality("ch", " "), equality("ch", "\t"));
let result = TrimDetector::detect(&condition, &loop_body, "ch");
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.match_var, "ch");
assert_eq!(result.carrier_name, "is_ch_match");
assert_eq!(result.comparison_literals.len(), 2);
assert!(result.comparison_literals.contains(&" ".to_string()));
assert!(result.comparison_literals.contains(&"\t".to_string()));
}
#[test]
fn test_detect_no_match_not_substring() {
// ch = s.length() // Not substring!
// if ch == " " { ... }
let loop_body = vec![assignment("ch", method_call("s", "length", vec![]))];
let condition = equality("ch", " ");
let result = TrimDetector::detect(&condition, &loop_body, "ch");
assert!(result.is_none());
}
#[test]
fn test_detect_no_match_no_equality() {
// local ch = s.substring(...)
// if ch < "x" { ... } // Not equality!
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(var_node("ch")),
right: Box::new(string_literal("x")),
span: Span::unknown(),
};
let result = TrimDetector::detect(&condition, &loop_body, "ch");
assert!(result.is_none());
}
#[test]
fn test_detect_single_equality() {
// local ch = s.substring(...)
// if ch == " " { ... }
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
let condition = equality("ch", " ");
let result = TrimDetector::detect(&condition, &loop_body, "ch");
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.comparison_literals.len(), 1);
assert_eq!(result.comparison_literals[0], " ");
}
#[test]
fn test_detect_multiple_whitespace() {
// local ch = s.substring(...)
// if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { ... }
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
let condition = or_op(
or_op(equality("ch", " "), equality("ch", "\t")),
or_op(equality("ch", "\n"), equality("ch", "\r")),
);
let result = TrimDetector::detect(&condition, &loop_body, "ch");
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.comparison_literals.len(), 4);
assert!(result.comparison_literals.contains(&" ".to_string()));
assert!(result.comparison_literals.contains(&"\t".to_string()));
assert!(result.comparison_literals.contains(&"\n".to_string()));
assert!(result.comparison_literals.contains(&"\r".to_string()));
}
#[test]
fn test_detect_carrier_name_generation() {
// Test carrier name generation: "ch" → "is_ch_match"
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
let condition = equality("ch", " ");
let result = TrimDetector::detect(&condition, &loop_body, "ch").unwrap();
assert_eq!(result.carrier_name, "is_ch_match");
}
#[test]
fn test_detect_different_variable_names() {
// Test with different variable names
let test_cases = vec!["ch", "c", "char", "character"];
for var_name in test_cases {
let loop_body = vec![assignment(var_name, method_call("s", "substring", vec![]))];
let condition = equality(var_name, " ");
let result = TrimDetector::detect(&condition, &loop_body, var_name);
assert!(
result.is_some(),
"Expected detection for var '{}'",
var_name
);
let result = result.unwrap();
assert_eq!(result.match_var, var_name);
assert_eq!(result.carrier_name, format!("is_{}_match", var_name));
}
}
#[test]
fn test_extract_literal_reversed_equality() {
// Test: " " == ch (literal on left side)
let loop_body = vec![assignment("ch", method_call("s", "substring", vec![]))];
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(string_literal(" ")),
right: Box::new(var_node("ch")),
span: Span::unknown(),
};
let result = TrimDetector::detect(&condition, &loop_body, "ch");
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.comparison_literals.len(), 1);
assert_eq!(result.comparison_literals[0], " ");
}
}