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:
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
194
src/mir/loop_pattern_detection/tests.rs
Normal file
194
src/mir/loop_pattern_detection/tests.rs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user