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:
nyash-codex
2025-12-10 16:21:01 +09:00
parent b5661c1915
commit 00d1ec7cc5
8 changed files with 1704 additions and 33 deletions

View File

@ -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,
}
}

View 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"),
}
}
}

View File

@ -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;