feat(joinir): Phase 245C - Function parameter capture + test fix

Extend CapturedEnv to include function parameters used in loop conditions,
enabling ExprLowerer to resolve variables like `s` in `loop(p < s.length())`.

Phase 245C changes:
- function_scope_capture.rs: Add collect_names_in_loop_parts() helper
- function_scope_capture.rs: Extend analyze_captured_vars_v2() with param capture logic
- function_scope_capture.rs: Add 4 new comprehensive tests

Test fix:
- expr_lowerer/ast_support.rs: Accept all MethodCall nodes for syntax support
  (validation happens during lowering in MethodCallLowerer)

Problem solved: "Variable not found: s" errors in loop conditions

Test results: 924/924 PASS (+13 from baseline 911)

🤖 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-11 13:13:08 +09:00
parent 00ecddbbc9
commit d4597dacfa
40 changed files with 2386 additions and 1046 deletions

View File

@ -33,6 +33,7 @@
use crate::mir::ValueId;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::ast::ASTNode;
use std::collections::BTreeSet;
/// A variable captured from function scope for use in loop conditions/body.
///
@ -281,22 +282,23 @@ pub(crate) fn analyze_captured_vars_v2(
}
// Step 1: Find loop position in fn_body by structural matching
let loop_index = match find_loop_index_by_structure(fn_body, loop_condition, loop_body) {
Some(idx) => idx,
None => {
if debug {
eprintln!("[capture/debug] Loop not found in function body by structure, returning empty CapturedEnv");
}
return CapturedEnv::new();
}
};
let loop_index = find_loop_index_by_structure(fn_body, loop_condition, loop_body);
if debug {
eprintln!("[capture/debug] Loop found at index {} by structure", loop_index);
match loop_index {
Some(idx) => eprintln!("[capture/debug] Loop found at index {} by structure", idx),
None => eprintln!("[capture/debug] Loop not found in function body by structure (may be unit test or synthetic case)"),
}
}
// Step 2: Collect local declarations BEFORE the loop
let pre_loop_locals = collect_local_declarations(&fn_body[..loop_index]);
let pre_loop_locals = if let Some(idx) = loop_index {
collect_local_declarations(&fn_body[..idx])
} else {
// No loop found in fn_body - might be a unit test or synthetic case
// Still collect all locals from fn_body
collect_local_declarations(fn_body)
};
if debug {
eprintln!("[capture/debug] Found {} pre-loop local declarations", pre_loop_locals.len());
@ -305,13 +307,13 @@ pub(crate) fn analyze_captured_vars_v2(
let mut env = CapturedEnv::new();
// Step 3: For each pre-loop local, check capture criteria
for (name, init_expr) in pre_loop_locals {
for (name, init_expr) in &pre_loop_locals {
if debug {
eprintln!("[capture/check] Checking variable '{}'", name);
}
// 3a: Is init expression a safe constant?
if !is_safe_const_init(&init_expr) {
if !is_safe_const_init(init_expr) {
if debug {
eprintln!("[capture/reject] '{}': init is not a safe constant", name);
}
@ -319,7 +321,7 @@ pub(crate) fn analyze_captured_vars_v2(
}
// 3b: Is this variable reassigned anywhere in fn_body?
if is_reassigned_in_fn(fn_body, &name) {
if is_reassigned_in_fn(fn_body, name) {
if debug {
eprintln!("[capture/reject] '{}': reassigned in function", name);
}
@ -327,7 +329,7 @@ pub(crate) fn analyze_captured_vars_v2(
}
// 3c: Is this variable used in loop (condition or body)?
if !is_used_in_loop_parts(loop_condition, loop_body, &name) {
if !is_used_in_loop_parts(loop_condition, loop_body, name) {
if debug {
eprintln!("[capture/reject] '{}': not used in loop", name);
}
@ -335,21 +337,21 @@ pub(crate) fn analyze_captured_vars_v2(
}
// 3d: Skip if already in pinned, carriers, or body_locals
if scope.pinned.contains(&name) {
if scope.pinned.contains(name) {
if debug {
eprintln!("[capture/reject] '{}': is a pinned variable", name);
}
continue;
}
if scope.carriers.contains(&name) {
if scope.carriers.contains(name) {
if debug {
eprintln!("[capture/reject] '{}': is a carrier variable", name);
}
continue;
}
if scope.body_locals.contains(&name) {
if scope.body_locals.contains(name) {
if debug {
eprintln!("[capture/reject] '{}': is a body-local variable", name);
}
@ -368,6 +370,48 @@ pub(crate) fn analyze_captured_vars_v2(
});
}
// Phase 245C: Capture function parameters used in loop
let names_in_loop = collect_names_in_loop_parts(loop_condition, loop_body);
// pre-loop local names (already processed above)
let pre_loop_local_names: BTreeSet<String> =
pre_loop_locals.iter().map(|(name, _)| name.clone()).collect();
// Check each variable used in loop
for name in names_in_loop {
// Skip if already processed as pre-loop local
if pre_loop_local_names.contains(&name) {
continue;
}
// Skip if already in pinned, carriers, or body_locals
if scope.pinned.contains(&name)
|| scope.carriers.contains(&name)
|| scope.body_locals.contains(&name)
{
continue;
}
// Skip if reassigned in function (function parameters should not be reassigned)
if is_reassigned_in_fn(fn_body, &name) {
if debug {
eprintln!("[capture/param/reject] '{}': reassigned in function", name);
}
continue;
}
// This is a function parameter-like variable - add to CapturedEnv
if debug {
eprintln!("[capture/param/accept] '{}': function parameter used in loop", name);
}
env.add_var(CapturedVar {
name: name.clone(),
host_id: ValueId(0), // Placeholder, will be resolved in ConditionEnvBuilder
is_immutable: true,
});
}
if debug {
eprintln!("[capture/result] Captured {} variables: {:?}",
env.vars.len(),
@ -664,6 +708,84 @@ fn is_used_in_loop_parts(condition: &ASTNode, body: &[ASTNode], name: &str) -> b
check_usage(condition, name) || body.iter().any(|n| check_usage(n, name))
}
/// Phase 245C: Collect all variable names used in loop condition and body
///
/// Helper for function parameter capture. Returns a set of all variable names
/// that appear in the loop's condition or body.
fn collect_names_in_loop_parts(condition: &ASTNode, body: &[ASTNode]) -> BTreeSet<String> {
fn collect(node: &ASTNode, acc: &mut BTreeSet<String>) {
match node {
ASTNode::Variable { name, .. } => {
acc.insert(name.clone());
}
ASTNode::If { condition, then_body, else_body, .. } => {
collect(condition, acc);
for stmt in then_body {
collect(stmt, acc);
}
if let Some(else_stmts) = else_body {
for stmt in else_stmts {
collect(stmt, acc);
}
}
}
ASTNode::Assignment { target, value, .. } => {
collect(target, acc);
collect(value, acc);
}
ASTNode::UnaryOp { operand, .. } => {
collect(operand, acc);
}
ASTNode::Return { value: Some(operand), .. } => {
collect(operand, acc);
}
ASTNode::BinaryOp { left, right, .. } => {
collect(left, acc);
collect(right, acc);
}
ASTNode::MethodCall { object, arguments, .. } => {
collect(object, acc);
for arg in arguments {
collect(arg, acc);
}
}
ASTNode::FunctionCall { arguments, .. } => {
for arg in arguments {
collect(arg, acc);
}
}
ASTNode::Local { initial_values, .. } => {
for init_opt in initial_values {
if let Some(val) = init_opt {
collect(val, acc);
}
}
}
ASTNode::FieldAccess { object, .. } => {
collect(object, acc);
}
ASTNode::Index { target, index, .. } => {
collect(target, acc);
collect(index, acc);
}
ASTNode::Loop { condition, body, .. } => {
collect(condition, acc);
for stmt in body {
collect(stmt, acc);
}
}
_ => {}
}
}
let mut acc = BTreeSet::new();
collect(condition, &mut acc);
for stmt in body {
collect(stmt, &mut acc);
}
acc
}
#[cfg(test)]
mod tests {
use super::*;
@ -1078,4 +1200,338 @@ mod tests {
// Should reject because digits is not used in loop
assert_eq!(env.vars.len(), 0);
}
// Phase 245C: Function parameter capture tests
#[test]
fn test_capture_function_param_used_in_condition() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
// Simulate: fn parse_number(s, p, len) { loop(p < len) { ... } }
// Expected: 'len' should be captured (used in condition, not reassigned)
let condition = Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "len".to_string(), // function parameter
span: Span::unknown(),
}),
span: Span::unknown(),
});
let body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
carriers: BTreeSet::new(),
body_locals: BTreeSet::new(),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
// Use analyze_captured_vars_v2 with structural matching
let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);
// Should capture 'len' (function parameter used in condition)
assert_eq!(env.vars.len(), 1);
assert!(env.get("len").is_some());
let var = env.get("len").unwrap();
assert_eq!(var.name, "len");
assert!(var.is_immutable);
}
#[test]
fn test_capture_function_param_used_in_method_call() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
// Simulate: fn parse_number(s, p) { loop(p < s.length()) { ch = s.charAt(p) } }
// Expected: 's' should be captured (used in condition and body, not reassigned)
let condition = Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "s".to_string(), // function parameter
span: Span::unknown(),
}),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
span: Span::unknown(),
});
let body = vec![
ASTNode::Local {
variables: vec!["ch".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "s".to_string(), // function parameter
span: Span::unknown(),
}),
method: "charAt".to_string(),
arguments: vec![ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
}))],
span: Span::unknown(),
},
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
carriers: BTreeSet::new(),
body_locals: BTreeSet::from(["ch".to_string()]),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
// Use analyze_captured_vars_v2 with structural matching
let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);
// Should capture 's' (function parameter used in condition and body)
assert_eq!(env.vars.len(), 1);
assert!(env.get("s").is_some());
let var = env.get("s").unwrap();
assert_eq!(var.name, "s");
assert!(var.is_immutable);
}
#[test]
fn test_capture_function_param_reassigned_rejected() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
// Simulate: fn bad_func(x) { x = 5; loop(x < 10) { x = x + 1 } }
// Expected: 'x' should NOT be captured (reassigned in function)
let condition = Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
});
let body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
// fn_body includes reassignment before loop
let fn_body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::from(["x".to_string()]), // x is loop param
carriers: BTreeSet::new(),
body_locals: BTreeSet::new(),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
// Use analyze_captured_vars_v2 with structural matching
let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);
// Should NOT capture 'x' (reassigned in fn_body)
assert_eq!(env.vars.len(), 0);
}
#[test]
fn test_capture_mixed_locals_and_params() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
// Simulate: fn parse(s, len) { local digits = "0123"; loop(p < len) { ch = digits.indexOf(...); s.charAt(...) } }
// Expected: 'len', 's', and 'digits' should all be captured
let condition = Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "len".to_string(), // function parameter
span: Span::unknown(),
}),
span: Span::unknown(),
});
let body = vec![
ASTNode::Local {
variables: vec!["ch".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "s".to_string(), // function parameter
span: Span::unknown(),
}),
method: "charAt".to_string(),
arguments: vec![ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
}))],
span: Span::unknown(),
},
ASTNode::Local {
variables: vec!["digit".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "digits".to_string(), // pre-loop local
span: Span::unknown(),
}),
method: "indexOf".to_string(),
arguments: vec![ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
}))],
span: Span::unknown(),
},
];
// fn_body includes local declaration before loop
let fn_body = vec![
ASTNode::Local {
variables: vec!["digits".to_string()],
initial_values: vec![Some(Box::new(ASTNode::Literal {
value: LiteralValue::String("0123".to_string()),
span: Span::unknown(),
}))],
span: Span::unknown(),
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
carriers: BTreeSet::new(),
body_locals: BTreeSet::from(["ch".to_string(), "digit".to_string()]),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
// Use analyze_captured_vars_v2 with structural matching
let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);
// Should capture all three: 'len' (param), 's' (param), 'digits' (pre-loop local)
assert_eq!(env.vars.len(), 3);
assert!(env.get("len").is_some());
assert!(env.get("s").is_some());
assert!(env.get("digits").is_some());
}
}

View File

@ -50,7 +50,7 @@ pub struct DigitPosPromotionRequest<'a> {
/// Loop structure metadata (for future use)
#[allow(dead_code)]
pub scope_shape: Option<&'a LoopScopeShape>,
pub(crate) scope_shape: Option<&'a LoopScopeShape>,
/// Break condition AST (Pattern2: Some, Pattern4: None)
pub break_cond: Option<&'a ASTNode>,
@ -148,7 +148,7 @@ impl DigitPosPromoter {
);
// Step 3: Find indexOf() definition for the comparison variable
let definition = Self::find_indexOf_definition(req.loop_body, &var_in_cond);
let definition = Self::find_index_of_definition(req.loop_body, &var_in_cond);
if let Some(def_node) = definition {
eprintln!(
@ -157,7 +157,7 @@ impl DigitPosPromoter {
);
// Step 4: Verify it's an indexOf() method call
if Self::is_indexOf_method_call(def_node) {
if Self::is_index_of_method_call(def_node) {
eprintln!("[digitpos_promoter] Confirmed indexOf() method call");
// Step 5: Verify cascading dependency
@ -241,7 +241,7 @@ impl DigitPosPromoter {
/// 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> {
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() {
@ -302,7 +302,7 @@ impl DigitPosPromoter {
}
/// Check if node is an indexOf() method call
fn is_indexOf_method_call(node: &ASTNode) -> bool {
fn is_index_of_method_call(node: &ASTNode) -> bool {
matches!(
node,
ASTNode::MethodCall { method, .. } if method == "indexOf"
@ -349,9 +349,9 @@ impl DigitPosPromoter {
/// Example: `digits.indexOf(ch)` → returns "ch" if it's a LoopBodyLocal
fn find_first_loopbodylocal_dependency<'a>(
body: &'a [ASTNode],
indexOf_call: &'a ASTNode,
index_of_call: &'a ASTNode,
) -> Option<&'a str> {
if let ASTNode::MethodCall { arguments, .. } = indexOf_call {
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 {
@ -558,7 +558,7 @@ mod tests {
}
#[test]
fn test_digitpos_non_indexOf_method() {
fn test_digitpos_non_index_of_method() {
// ch = s.substring(...) → pos = s.length() → if pos < 0
// Should fail: not indexOf()

View File

@ -663,181 +663,7 @@ fn has_simple_condition(_loop_form: &LoopForm) -> bool {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
use crate::mir::loop_pattern_detection::LoopFeatures;
fn span() -> Span {
Span::unknown()
}
fn var(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: span(),
}
}
fn lit_i(n: i64) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Integer(n),
span: span(),
}
}
fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: op,
left: Box::new(left),
right: Box::new(right),
span: span(),
}
}
fn assignment(target: ASTNode, value: ASTNode) -> ASTNode {
ASTNode::Assignment {
target: Box::new(target),
value: Box::new(value),
span: span(),
}
}
fn has_continue(node: &ASTNode) -> bool {
match node {
ASTNode::Continue { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_continue) || else_body.as_ref().map_or(false, |b| b.iter().any(has_continue))
}
ASTNode::Loop { body, .. } => body.iter().any(has_continue),
_ => false,
}
}
fn has_break(node: &ASTNode) -> bool {
match node {
ASTNode::Break { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_break) || else_body.as_ref().map_or(false, |b| b.iter().any(has_break))
}
ASTNode::Loop { body, .. } => body.iter().any(has_break),
_ => false,
}
}
fn has_if(body: &[ASTNode]) -> bool {
body.iter().any(|n| matches!(n, ASTNode::If { .. }))
}
fn carrier_count(body: &[ASTNode]) -> usize {
fn count(nodes: &[ASTNode]) -> usize {
let mut c = 0;
for n in nodes {
match n {
ASTNode::Assignment { .. } => c += 1,
ASTNode::If { then_body, else_body, .. } => {
c += count(then_body);
if let Some(else_body) = else_body {
c += count(else_body);
}
}
_ => {}
}
}
c
}
if count(body) > 0 { 1 } else { 0 }
}
fn classify_body(body: &[ASTNode]) -> LoopPatternKind {
let has_continue_flag = body.iter().any(has_continue);
let has_break_flag = body.iter().any(has_break);
let features = LoopFeatures {
has_break: has_break_flag,
has_continue: has_continue_flag,
has_if: has_if(body),
has_if_else_phi: false,
carrier_count: carrier_count(body),
break_count: if has_break_flag { 1 } else { 0 },
continue_count: if has_continue_flag { 1 } else { 0 },
update_summary: None,
};
classify(&features)
}
#[test]
fn pattern1_simple_while_is_detected() {
// loop(i < len) { i = i + 1 }
let body = vec![assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1)))];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern1SimpleWhile);
}
#[test]
fn pattern2_break_loop_is_detected() {
// loop(i < len) { if i > 0 { break } i = i + 1 }
let cond = bin(BinaryOperator::Greater, var("i"), lit_i(0));
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break);
}
#[test]
fn pattern3_if_sum_shape_is_detected() {
// loop(i < len) { if i % 2 == 1 { sum = sum + 1 } i = i + 1 }
let cond = bin(
BinaryOperator::Equal,
bin(BinaryOperator::Modulo, var("i"), lit_i(2)),
lit_i(1),
);
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![assignment(
var("sum"),
bin(BinaryOperator::Add, var("sum"), lit_i(1)),
)],
else_body: None,
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern3IfPhi);
}
#[test]
fn pattern4_continue_loop_is_detected() {
// loop(i < len) { if (i % 2 == 0) { continue } sum = sum + i; i = i + 1 }
let cond = bin(
BinaryOperator::Equal,
bin(BinaryOperator::Modulo, var("i"), lit_i(2)),
lit_i(0),
);
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![ASTNode::Continue { span: span() }],
else_body: Some(vec![assignment(
var("sum"),
bin(BinaryOperator::Add, var("sum"), var("i")),
)]),
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern4Continue);
}
}
mod tests;
// Phase 170-D: Loop Condition Scope Analysis Boxes
pub mod loop_condition_scope;

View File

@ -0,0 +1,194 @@
use super::*;
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
use crate::mir::loop_pattern_detection::LoopFeatures;
fn span() -> Span {
Span::unknown()
}
fn var(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: span(),
}
}
fn lit_i(n: i64) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Integer(n),
span: span(),
}
}
fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: op,
left: Box::new(left),
right: Box::new(right),
span: span(),
}
}
fn assignment(target: ASTNode, value: ASTNode) -> ASTNode {
ASTNode::Assignment {
target: Box::new(target),
value: Box::new(value),
span: span(),
}
}
fn has_continue(node: &ASTNode) -> bool {
match node {
ASTNode::Continue { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_continue) || else_body.as_ref().map_or(false, |b| b.iter().any(has_continue))
}
ASTNode::Loop { body, .. } => body.iter().any(has_continue),
_ => false,
}
}
fn has_break(node: &ASTNode) -> bool {
match node {
ASTNode::Break { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_break) || else_body.as_ref().map_or(false, |b| b.iter().any(has_break))
}
ASTNode::Loop { body, .. } => body.iter().any(has_break),
_ => false,
}
}
fn has_if(body: &[ASTNode]) -> bool {
body.iter().any(|n| matches!(n, ASTNode::If { .. }))
}
fn carrier_count(body: &[ASTNode]) -> usize {
fn count(nodes: &[ASTNode]) -> usize {
let mut c = 0;
for n in nodes {
match n {
ASTNode::Assignment { .. } => c += 1,
ASTNode::If { then_body, else_body, .. } => {
c += count(then_body);
if let Some(else_body) = else_body {
c += count(else_body);
}
}
_ => {}
}
}
c
}
if count(body) > 0 { 1 } else { 0 }
}
fn classify_body(body: &[ASTNode]) -> LoopPatternKind {
let has_continue_flag = body.iter().any(has_continue);
let has_break_flag = body.iter().any(has_break);
let features = LoopFeatures {
has_break: has_break_flag,
has_continue: has_continue_flag,
has_if: has_if(body),
has_if_else_phi: false,
carrier_count: carrier_count(body),
break_count: if has_break_flag { 1 } else { 0 },
continue_count: if has_continue_flag { 1 } else { 0 },
update_summary: None,
};
classify(&features)
}
#[test]
fn pattern1_simple_while_is_detected() {
// loop(i < len) { i = i + 1 }
let body = vec![assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1)))];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern1SimpleWhile);
}
#[test]
fn pattern2_break_loop_is_detected() {
// loop(i < len) { if i > 0 { break } i = i + 1 }
let cond = bin(BinaryOperator::Greater, var("i"), lit_i(0));
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break);
}
#[test]
fn parse_number_like_loop_is_classified_as_pattern2() {
// loop(p < len) {
// if digit_pos < 0 { break }
// p = p + 1
// }
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
assignment(var("p"), bin(BinaryOperator::Add, var("p"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break);
}
#[test]
fn pattern3_if_sum_shape_is_detected() {
// loop(i < len) { if i % 2 == 1 { sum = sum + 1 } i = i + 1 }
let cond = bin(
BinaryOperator::Equal,
bin(BinaryOperator::Modulo, var("i"), lit_i(2)),
lit_i(1),
);
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![assignment(
var("sum"),
bin(BinaryOperator::Add, var("sum"), lit_i(1)),
)],
else_body: None,
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern3IfPhi);
}
#[test]
fn pattern4_continue_loop_is_detected() {
// loop(i < len) { if (i % 2 == 0) { continue } sum = sum + i; i = i + 1 }
let cond = bin(
BinaryOperator::Equal,
bin(BinaryOperator::Modulo, var("i"), lit_i(2)),
lit_i(0),
);
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![ASTNode::Continue { span: span() }],
else_body: Some(vec![assignment(
var("sum"),
bin(BinaryOperator::Add, var("sum"), var("i")),
)]),
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern4Continue);
}