feat(joinir): Phase 224 - DigitPosPromoter for A-4 pattern (core complete)
- New DigitPosPromoter Box for cascading indexOf pattern detection - Two-tier promotion strategy: A-3 Trim → A-4 DigitPos fallback - Unit tests 6/6 PASS (comparison operators, cascading dependency) - Promotion verified: digit_pos → is_digit_pos carrier ⚠️ Lowerer integration gap: lower_loop_with_break_minimal doesn't recognize promoted variables yet (Phase 224-continuation) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -136,19 +136,23 @@ impl LoopBodyCondPromoter {
|
||||
/// - Condition: Simple equality chain (e.g., `ch == " " || ch == "\t"`)
|
||||
/// - Pattern: Identical to existing Trim pattern
|
||||
///
|
||||
/// ## Algorithm (Delegated to LoopBodyCarrierPromoter)
|
||||
/// ## Algorithm (Phase 224: Two-Tier Promotion Strategy)
|
||||
///
|
||||
/// 1. Extract LoopBodyLocal names from cond_scope
|
||||
/// 2. Build PromotionRequest for LoopBodyCarrierPromoter
|
||||
/// 3. Call LoopBodyCarrierPromoter::try_promote()
|
||||
/// 4. Convert PromotionResult to ConditionPromotionResult
|
||||
/// 5. Return result (Promoted or CannotPromote)
|
||||
/// 2. Try A-3 Trim promotion (LoopBodyCarrierPromoter)
|
||||
/// 3. If A-3 fails, try A-4 DigitPos promotion (DigitPosPromoter)
|
||||
/// 4. If both fail, return CannotPromote
|
||||
///
|
||||
/// ## Differences from TrimLoopLowerer
|
||||
///
|
||||
/// - TrimLoopLowerer: Full lowering pipeline (detection + code generation)
|
||||
/// - LoopBodyCondPromoter: Detection + metadata only (no code generation)
|
||||
pub fn try_promote_for_condition(req: ConditionPromotionRequest) -> ConditionPromotionResult {
|
||||
use crate::mir::loop_pattern_detection::loop_body_digitpos_promoter::{
|
||||
DigitPosPromotionRequest, DigitPosPromotionResult, DigitPosPromoter,
|
||||
};
|
||||
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;
|
||||
|
||||
// P0 constraint: Need LoopScopeShape for LoopBodyCarrierPromoter
|
||||
let scope_shape = match req.scope_shape {
|
||||
Some(s) => s,
|
||||
@ -165,7 +169,7 @@ impl LoopBodyCondPromoter {
|
||||
// Pattern4: continue_cond (use as break_cond for Trim pattern detection)
|
||||
let condition_for_promotion = req.break_cond.or(req.continue_cond);
|
||||
|
||||
// Build request for LoopBodyCarrierPromoter
|
||||
// Step 1: Try A-3 Trim promotion
|
||||
let promotion_request = PromotionRequest {
|
||||
scope: scope_shape,
|
||||
cond_scope: req.cond_scope,
|
||||
@ -173,31 +177,72 @@ impl LoopBodyCondPromoter {
|
||||
loop_body: req.loop_body,
|
||||
};
|
||||
|
||||
// Delegate to existing LoopBodyCarrierPromoter
|
||||
match LoopBodyCarrierPromoter::try_promote(&promotion_request) {
|
||||
PromotionResult::Promoted { trim_info } => {
|
||||
eprintln!(
|
||||
"[cond_promoter] LoopBodyLocal '{}' promoted to carrier '{}'",
|
||||
"[cond_promoter] A-3 Trim pattern promoted: '{}' → carrier '{}'",
|
||||
trim_info.var_name, trim_info.carrier_name
|
||||
);
|
||||
|
||||
// Convert TrimPatternInfo to CarrierInfo
|
||||
let carrier_info = trim_info.to_carrier_info();
|
||||
|
||||
ConditionPromotionResult::Promoted {
|
||||
return ConditionPromotionResult::Promoted {
|
||||
carrier_info,
|
||||
promoted_var: trim_info.var_name,
|
||||
carrier_name: trim_info.carrier_name,
|
||||
}
|
||||
};
|
||||
}
|
||||
PromotionResult::CannotPromote { reason, vars } => {
|
||||
PromotionResult::CannotPromote { reason, .. } => {
|
||||
eprintln!("[cond_promoter] A-3 Trim promotion failed: {}", reason);
|
||||
eprintln!("[cond_promoter] Trying A-4 DigitPos promotion...");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Try A-4 DigitPos promotion
|
||||
let digitpos_request = DigitPosPromotionRequest {
|
||||
loop_param_name: req.loop_param_name,
|
||||
cond_scope: req.cond_scope,
|
||||
scope_shape: req.scope_shape,
|
||||
break_cond: req.break_cond,
|
||||
continue_cond: req.continue_cond,
|
||||
loop_body: req.loop_body,
|
||||
};
|
||||
|
||||
match DigitPosPromoter::try_promote(digitpos_request) {
|
||||
DigitPosPromotionResult::Promoted {
|
||||
carrier_info,
|
||||
promoted_var,
|
||||
carrier_name,
|
||||
} => {
|
||||
eprintln!(
|
||||
"[cond_promoter] Cannot promote LoopBodyLocal variables {:?}: {}",
|
||||
vars, reason
|
||||
"[cond_promoter] A-4 DigitPos pattern promoted: '{}' → carrier '{}'",
|
||||
promoted_var, carrier_name
|
||||
);
|
||||
|
||||
ConditionPromotionResult::CannotPromote { reason, vars }
|
||||
return ConditionPromotionResult::Promoted {
|
||||
carrier_info,
|
||||
promoted_var,
|
||||
carrier_name,
|
||||
};
|
||||
}
|
||||
DigitPosPromotionResult::CannotPromote { reason, .. } => {
|
||||
eprintln!("[cond_promoter] A-4 DigitPos promotion failed: {}", reason);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Fail-Fast (no pattern matched)
|
||||
let body_local_names: Vec<String> = req
|
||||
.cond_scope
|
||||
.vars
|
||||
.iter()
|
||||
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
|
||||
.map(|v| v.name.clone())
|
||||
.collect();
|
||||
|
||||
ConditionPromotionResult::CannotPromote {
|
||||
reason: "No promotable pattern detected (tried A-3 Trim, A-4 DigitPos)".to_string(),
|
||||
vars: body_local_names,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
691
src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs
Normal file
691
src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs
Normal file
@ -0,0 +1,691 @@
|
||||
//! Phase 224: DigitPosPromoter Box
|
||||
//!
|
||||
//! Handles promotion of A-4 pattern: Cascading LoopBodyLocal with indexOf()
|
||||
//!
|
||||
//! ## Pattern Example
|
||||
//!
|
||||
//! ```nyash
|
||||
//! loop(p < s.length()) {
|
||||
//! local ch = s.substring(p, p+1) // First LoopBodyLocal
|
||||
//! local digit_pos = digits.indexOf(ch) // Second LoopBodyLocal (depends on ch)
|
||||
//!
|
||||
//! if digit_pos < 0 { // Comparison condition
|
||||
//! break
|
||||
//! }
|
||||
//!
|
||||
//! // Continue processing...
|
||||
//! p = p + 1
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Design
|
||||
//!
|
||||
//! - **Responsibility**: Detect and promote A-4 digit position pattern
|
||||
//! - **Input**: LoopConditionScope + break/continue condition + loop body
|
||||
//! - **Output**: CarrierInfo with bool carrier (e.g., "is_digit")
|
||||
//!
|
||||
//! ## Key Differences from A-3 Trim
|
||||
//!
|
||||
//! | Feature | A-3 Trim | A-4 DigitPos |
|
||||
//! |---------|----------|--------------|
|
||||
//! | Method | substring() | substring() → indexOf() |
|
||||
//! | Dependency | Single | Cascading (2 variables) |
|
||||
//! | Condition | Equality (==) | Comparison (<, >, !=) |
|
||||
//! | Structure | OR chain | Single comparison |
|
||||
|
||||
use crate::ast::{ASTNode, BinaryOperator};
|
||||
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
|
||||
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
|
||||
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScope;
|
||||
use crate::mir::ValueId;
|
||||
|
||||
/// Promotion request for A-4 digit position pattern
|
||||
pub struct DigitPosPromotionRequest<'a> {
|
||||
/// Loop parameter name (e.g., "p")
|
||||
#[allow(dead_code)]
|
||||
pub loop_param_name: &'a str,
|
||||
|
||||
/// Condition scope analysis result
|
||||
pub cond_scope: &'a LoopConditionScope,
|
||||
|
||||
/// Loop structure metadata (for future use)
|
||||
#[allow(dead_code)]
|
||||
pub scope_shape: Option<&'a LoopScopeShape>,
|
||||
|
||||
/// Break condition AST (Pattern2: Some, Pattern4: None)
|
||||
pub break_cond: Option<&'a ASTNode>,
|
||||
|
||||
/// Continue condition AST (Pattern4: Some, Pattern2: None)
|
||||
pub continue_cond: Option<&'a ASTNode>,
|
||||
|
||||
/// Loop body statements
|
||||
pub loop_body: &'a [ASTNode],
|
||||
}
|
||||
|
||||
/// Promotion result
|
||||
pub enum DigitPosPromotionResult {
|
||||
/// Promotion successful
|
||||
Promoted {
|
||||
/// Carrier metadata
|
||||
carrier_info: CarrierInfo,
|
||||
|
||||
/// Variable name that was promoted (e.g., "digit_pos")
|
||||
promoted_var: String,
|
||||
|
||||
/// Promoted carrier name (e.g., "is_digit")
|
||||
carrier_name: String,
|
||||
},
|
||||
|
||||
/// Cannot promote (Fail-Fast)
|
||||
CannotPromote {
|
||||
/// Human-readable reason
|
||||
reason: String,
|
||||
|
||||
/// List of problematic LoopBodyLocal variables
|
||||
vars: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Phase 224: DigitPosPromoter Box
|
||||
pub struct DigitPosPromoter;
|
||||
|
||||
impl DigitPosPromoter {
|
||||
/// Try to promote A-4 pattern (cascading indexOf)
|
||||
///
|
||||
/// ## Algorithm
|
||||
///
|
||||
/// 1. Extract LoopBodyLocal variables from cond_scope
|
||||
/// 2. Find indexOf() definition in loop body
|
||||
/// 3. Extract comparison variable from condition
|
||||
/// 4. Verify cascading dependency (indexOf depends on another LoopBodyLocal)
|
||||
/// 5. Build CarrierInfo with bool carrier
|
||||
pub fn try_promote(req: DigitPosPromotionRequest) -> DigitPosPromotionResult {
|
||||
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;
|
||||
|
||||
// Step 1: Extract LoopBodyLocal variables
|
||||
let body_locals: Vec<&String> = req
|
||||
.cond_scope
|
||||
.vars
|
||||
.iter()
|
||||
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
|
||||
.map(|v| &v.name)
|
||||
.collect();
|
||||
|
||||
if body_locals.is_empty() {
|
||||
return DigitPosPromotionResult::CannotPromote {
|
||||
reason: "No LoopBodyLocal variables to promote".to_string(),
|
||||
vars: vec![],
|
||||
};
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[digitpos_promoter] Phase 224: Found {} LoopBodyLocal variables: {:?}",
|
||||
body_locals.len(),
|
||||
body_locals
|
||||
);
|
||||
|
||||
// Step 2: Extract comparison variable from condition
|
||||
let condition = req.break_cond.or(req.continue_cond);
|
||||
if condition.is_none() {
|
||||
return DigitPosPromotionResult::CannotPromote {
|
||||
reason: "No break or continue condition provided".to_string(),
|
||||
vars: body_locals.iter().map(|s| s.to_string()).collect(),
|
||||
};
|
||||
}
|
||||
|
||||
let var_in_cond = Self::extract_comparison_var(condition.unwrap());
|
||||
if var_in_cond.is_none() {
|
||||
return DigitPosPromotionResult::CannotPromote {
|
||||
reason: "No comparison variable found in condition".to_string(),
|
||||
vars: body_locals.iter().map(|s| s.to_string()).collect(),
|
||||
};
|
||||
}
|
||||
|
||||
let var_in_cond = var_in_cond.unwrap();
|
||||
eprintln!(
|
||||
"[digitpos_promoter] Comparison variable in condition: '{}'",
|
||||
var_in_cond
|
||||
);
|
||||
|
||||
// Step 3: Find indexOf() definition for the comparison variable
|
||||
let definition = Self::find_indexOf_definition(req.loop_body, &var_in_cond);
|
||||
|
||||
if let Some(def_node) = definition {
|
||||
eprintln!(
|
||||
"[digitpos_promoter] Found indexOf() definition for '{}'",
|
||||
var_in_cond
|
||||
);
|
||||
|
||||
// Step 4: Verify it's an indexOf() method call
|
||||
if Self::is_indexOf_method_call(def_node) {
|
||||
eprintln!("[digitpos_promoter] Confirmed indexOf() method call");
|
||||
|
||||
// Step 5: Verify cascading dependency
|
||||
let dependency = Self::find_first_loopbodylocal_dependency(req.loop_body, def_node);
|
||||
|
||||
if dependency.is_none() {
|
||||
return DigitPosPromotionResult::CannotPromote {
|
||||
reason: format!(
|
||||
"indexOf() call for '{}' does not depend on LoopBodyLocal",
|
||||
var_in_cond
|
||||
),
|
||||
vars: vec![var_in_cond],
|
||||
};
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[digitpos_promoter] Cascading dependency confirmed: {} → indexOf({})",
|
||||
dependency.unwrap(),
|
||||
var_in_cond
|
||||
);
|
||||
|
||||
// Step 6: Build CarrierInfo
|
||||
let carrier_name = format!("is_{}", var_in_cond);
|
||||
let carrier_info = CarrierInfo::with_carriers(
|
||||
carrier_name.clone(),
|
||||
ValueId(0), // Placeholder (will be remapped)
|
||||
vec![],
|
||||
);
|
||||
|
||||
eprintln!(
|
||||
"[digitpos_promoter] A-4 DigitPos pattern promoted: {} → {}",
|
||||
var_in_cond, carrier_name
|
||||
);
|
||||
|
||||
return DigitPosPromotionResult::Promoted {
|
||||
carrier_info,
|
||||
promoted_var: var_in_cond,
|
||||
carrier_name,
|
||||
};
|
||||
} else {
|
||||
eprintln!(
|
||||
"[digitpos_promoter] Definition for '{}' is not indexOf() method",
|
||||
var_in_cond
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"[digitpos_promoter] No definition found for '{}' in loop body",
|
||||
var_in_cond
|
||||
);
|
||||
}
|
||||
|
||||
// No pattern matched
|
||||
DigitPosPromotionResult::CannotPromote {
|
||||
reason: "No A-4 DigitPos pattern detected (indexOf not found or not cascading)"
|
||||
.to_string(),
|
||||
vars: body_locals.iter().map(|s| s.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find indexOf() definition in loop body
|
||||
///
|
||||
/// Searches for assignment: `local var = ...indexOf(...)` or `var = ...indexOf(...)`
|
||||
fn find_indexOf_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_indexOf_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],
|
||||
indexOf_call: &'a ASTNode,
|
||||
) -> Option<&'a str> {
|
||||
if let ASTNode::MethodCall { arguments, .. } = indexOf_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 (similar to LoopBodyCarrierPromoter)
|
||||
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};
|
||||
use crate::mir::loop_pattern_detection::loop_condition_scope::{
|
||||
CondVarScope, LoopConditionScope,
|
||||
};
|
||||
|
||||
fn cond_scope_with_body_locals(vars: &[&str]) -> LoopConditionScope {
|
||||
let mut scope = LoopConditionScope::new();
|
||||
for var in vars {
|
||||
scope.add_var(var.to_string(), CondVarScope::LoopBodyLocal);
|
||||
}
|
||||
scope
|
||||
}
|
||||
|
||||
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_digitpos_no_body_locals() {
|
||||
let cond_scope = LoopConditionScope::new();
|
||||
|
||||
let req = DigitPosPromotionRequest {
|
||||
loop_param_name: "p",
|
||||
cond_scope: &cond_scope,
|
||||
scope_shape: None,
|
||||
break_cond: None,
|
||||
continue_cond: None,
|
||||
loop_body: &[],
|
||||
};
|
||||
|
||||
match DigitPosPromoter::try_promote(req) {
|
||||
DigitPosPromotionResult::CannotPromote { reason, vars } => {
|
||||
assert!(vars.is_empty());
|
||||
assert!(reason.contains("No LoopBodyLocal"));
|
||||
}
|
||||
_ => panic!("Expected CannotPromote when no LoopBodyLocal variables"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_digitpos_basic_pattern() {
|
||||
// Full A-4 pattern:
|
||||
// local ch = s.substring(...)
|
||||
// local digit_pos = digits.indexOf(ch)
|
||||
// if digit_pos < 0 { break }
|
||||
|
||||
let cond_scope = cond_scope_with_body_locals(&["ch", "digit_pos"]);
|
||||
|
||||
let loop_body = vec![
|
||||
assignment("ch", method_call("s", "substring", vec![])),
|
||||
assignment(
|
||||
"digit_pos",
|
||||
method_call("digits", "indexOf", vec![var_node("ch")]),
|
||||
),
|
||||
];
|
||||
|
||||
let break_cond = comparison("digit_pos", BinaryOperator::Less, 0);
|
||||
|
||||
let req = DigitPosPromotionRequest {
|
||||
loop_param_name: "p",
|
||||
cond_scope: &cond_scope,
|
||||
scope_shape: None,
|
||||
break_cond: Some(&break_cond),
|
||||
continue_cond: None,
|
||||
loop_body: &loop_body,
|
||||
};
|
||||
|
||||
match DigitPosPromoter::try_promote(req) {
|
||||
DigitPosPromotionResult::Promoted {
|
||||
promoted_var,
|
||||
carrier_name,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(promoted_var, "digit_pos");
|
||||
assert_eq!(carrier_name, "is_digit_pos");
|
||||
}
|
||||
DigitPosPromotionResult::CannotPromote { reason, .. } => {
|
||||
panic!("Expected Promoted, got CannotPromote: {}", reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_digitpos_non_indexOf_method() {
|
||||
// ch = s.substring(...) → pos = s.length() → if pos < 0
|
||||
// Should fail: not indexOf()
|
||||
|
||||
let cond_scope = cond_scope_with_body_locals(&["ch", "pos"]);
|
||||
|
||||
let loop_body = vec![
|
||||
assignment("ch", method_call("s", "substring", vec![])),
|
||||
assignment("pos", method_call("s", "length", vec![])), // NOT indexOf
|
||||
];
|
||||
|
||||
let break_cond = comparison("pos", BinaryOperator::Less, 0);
|
||||
|
||||
let req = DigitPosPromotionRequest {
|
||||
loop_param_name: "p",
|
||||
cond_scope: &cond_scope,
|
||||
scope_shape: None,
|
||||
break_cond: Some(&break_cond),
|
||||
continue_cond: None,
|
||||
loop_body: &loop_body,
|
||||
};
|
||||
|
||||
match DigitPosPromoter::try_promote(req) {
|
||||
DigitPosPromotionResult::CannotPromote { reason, .. } => {
|
||||
assert!(reason.contains("DigitPos pattern"));
|
||||
}
|
||||
_ => panic!("Expected CannotPromote for non-indexOf pattern"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_digitpos_no_loopbodylocal_dependency() {
|
||||
// digit_pos = fixed_string.indexOf("x") // No LoopBodyLocal dependency
|
||||
// Should fail: indexOf doesn't depend on LoopBodyLocal
|
||||
|
||||
let cond_scope = cond_scope_with_body_locals(&["digit_pos"]);
|
||||
|
||||
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 break_cond = comparison("digit_pos", BinaryOperator::Less, 0);
|
||||
|
||||
let req = DigitPosPromotionRequest {
|
||||
loop_param_name: "p",
|
||||
cond_scope: &cond_scope,
|
||||
scope_shape: None,
|
||||
break_cond: Some(&break_cond),
|
||||
continue_cond: None,
|
||||
loop_body: &loop_body,
|
||||
};
|
||||
|
||||
match DigitPosPromoter::try_promote(req) {
|
||||
DigitPosPromotionResult::CannotPromote { reason, .. } => {
|
||||
assert!(reason.contains("LoopBodyLocal"));
|
||||
}
|
||||
_ => panic!("Expected CannotPromote when no LoopBodyLocal dependency"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_digitpos_comparison_operators() {
|
||||
// Test different comparison operators: <, >, <=, >=, !=
|
||||
let operators = vec![
|
||||
BinaryOperator::Less,
|
||||
BinaryOperator::Greater,
|
||||
BinaryOperator::LessEqual,
|
||||
BinaryOperator::GreaterEqual,
|
||||
BinaryOperator::NotEqual,
|
||||
];
|
||||
|
||||
for op in operators {
|
||||
let cond_scope = cond_scope_with_body_locals(&["ch", "digit_pos"]);
|
||||
|
||||
let loop_body = vec![
|
||||
assignment("ch", method_call("s", "substring", vec![])),
|
||||
assignment(
|
||||
"digit_pos",
|
||||
method_call("digits", "indexOf", vec![var_node("ch")]),
|
||||
),
|
||||
];
|
||||
|
||||
let break_cond = comparison("digit_pos", op.clone(), 0);
|
||||
|
||||
let req = DigitPosPromotionRequest {
|
||||
loop_param_name: "p",
|
||||
cond_scope: &cond_scope,
|
||||
scope_shape: None,
|
||||
break_cond: Some(&break_cond),
|
||||
continue_cond: None,
|
||||
loop_body: &loop_body,
|
||||
};
|
||||
|
||||
match DigitPosPromoter::try_promote(req) {
|
||||
DigitPosPromotionResult::Promoted { .. } => {
|
||||
// Success
|
||||
}
|
||||
DigitPosPromotionResult::CannotPromote { reason, .. } => {
|
||||
panic!("Expected Promoted for operator {:?}, got: {}", op, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_digitpos_equality_operator() {
|
||||
// if digit_pos == -1 { break }
|
||||
// Should fail: Equality is A-3 Trim territory, not A-4 DigitPos
|
||||
|
||||
let cond_scope = cond_scope_with_body_locals(&["ch", "digit_pos"]);
|
||||
|
||||
let loop_body = vec![
|
||||
assignment("ch", method_call("s", "substring", vec![])),
|
||||
assignment(
|
||||
"digit_pos",
|
||||
method_call("digits", "indexOf", vec![var_node("ch")]),
|
||||
),
|
||||
];
|
||||
|
||||
let break_cond = 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 req = DigitPosPromotionRequest {
|
||||
loop_param_name: "p",
|
||||
cond_scope: &cond_scope,
|
||||
scope_shape: None,
|
||||
break_cond: Some(&break_cond),
|
||||
continue_cond: None,
|
||||
loop_body: &loop_body,
|
||||
};
|
||||
|
||||
match DigitPosPromoter::try_promote(req) {
|
||||
DigitPosPromotionResult::CannotPromote { reason, .. } => {
|
||||
assert!(reason.contains("comparison variable"));
|
||||
}
|
||||
_ => panic!("Expected CannotPromote for equality operator"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -781,6 +781,9 @@ pub mod loop_body_carrier_promoter;
|
||||
// Phase 223-3: LoopBodyLocal Condition Promotion (for Pattern4)
|
||||
pub mod loop_body_cond_promoter;
|
||||
|
||||
// Phase 224: A-4 DigitPos Pattern Promotion
|
||||
pub mod loop_body_digitpos_promoter;
|
||||
|
||||
// Phase 171-C-5: Trim Pattern Helper
|
||||
pub mod trim_loop_helper;
|
||||
pub use trim_loop_helper::TrimLoopHelper;
|
||||
|
||||
Reference in New Issue
Block a user