refactor: split large modules into submodules
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@
|
||||
//! Core analysis functions for function scope capture
|
||||
|
||||
mod v1;
|
||||
mod v2;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use v1::analyze_captured_vars;
|
||||
pub(crate) use v2::analyze_captured_vars_v2;
|
||||
@ -0,0 +1,653 @@
|
||||
use super::*;
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
||||
use crate::mir::BasicBlockId;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
// Phase 200-B: Capture analysis tests
|
||||
|
||||
#[test]
|
||||
fn test_capture_simple_digits() {
|
||||
// Build AST for:
|
||||
// local digits = "0123456789"
|
||||
// loop(i < 10) {
|
||||
// local pos = digits.indexOf(ch)
|
||||
// }
|
||||
|
||||
let digits_decl = ASTNode::Local {
|
||||
variables: vec!["digits".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::String("0123456789".to_string()),
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let loop_body = vec![ASTNode::Local {
|
||||
variables: vec!["pos".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "digits".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "indexOf".to_string(),
|
||||
arguments: vec![ASTNode::Variable {
|
||||
name: "ch".to_string(),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let loop_node = ASTNode::Loop {
|
||||
condition: Box::new(ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "i".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(10),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
body: loop_body,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let fn_body = vec![digits_decl, loop_node.clone()];
|
||||
|
||||
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(["i".to_string()]),
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::from(["pos".to_string()]),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// IMPORTANT: Pass a reference to the same loop_node instance that's in fn_body
|
||||
// find_stmt_index uses pointer comparison, so we must use &fn_body[1] instead of &loop_node
|
||||
let env = analyze_captured_vars(&fn_body, &fn_body[1], &scope);
|
||||
|
||||
assert_eq!(env.vars.len(), 1);
|
||||
assert!(env.get("digits").is_some());
|
||||
let var = env.get("digits").unwrap();
|
||||
assert_eq!(var.name, "digits");
|
||||
assert!(var.is_immutable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_reassigned_rejected() {
|
||||
// Build AST for:
|
||||
// local digits = "0123456789"
|
||||
// digits = "abc" // reassignment
|
||||
// loop(i < 10) {
|
||||
// local pos = digits.indexOf(ch)
|
||||
// }
|
||||
|
||||
let digits_decl = ASTNode::Local {
|
||||
variables: vec!["digits".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::String("0123456789".to_string()),
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let reassignment = ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "digits".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::String("abc".to_string()),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let loop_body = vec![ASTNode::Local {
|
||||
variables: vec!["pos".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "digits".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "indexOf".to_string(),
|
||||
arguments: vec![],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let loop_node = ASTNode::Loop {
|
||||
condition: Box::new(ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "i".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(10),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
body: loop_body,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let fn_body = vec![digits_decl, reassignment, loop_node.clone()];
|
||||
|
||||
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(["i".to_string()]),
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::from(["pos".to_string()]),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let env = analyze_captured_vars(&fn_body, &loop_node, &scope);
|
||||
|
||||
// Should reject because digits is reassigned
|
||||
assert_eq!(env.vars.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_after_loop_rejected() {
|
||||
// Build AST for:
|
||||
// loop(i < 10) { }
|
||||
// local digits = "0123456789" // defined AFTER loop
|
||||
|
||||
let loop_node = ASTNode::Loop {
|
||||
condition: Box::new(ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "i".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(10),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
body: vec![],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let digits_decl = ASTNode::Local {
|
||||
variables: vec!["digits".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::String("0123456789".to_string()),
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let fn_body = vec![loop_node.clone(), digits_decl];
|
||||
|
||||
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(["i".to_string()]),
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::new(),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let env = analyze_captured_vars(&fn_body, &loop_node, &scope);
|
||||
|
||||
// Should reject because digits is defined after the loop
|
||||
assert_eq!(env.vars.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_method_call_init_rejected() {
|
||||
// Build AST for:
|
||||
// local result = someBox.getValue() // MethodCall init
|
||||
// loop(i < 10) {
|
||||
// local x = result.something()
|
||||
// }
|
||||
|
||||
let result_decl = ASTNode::Local {
|
||||
variables: vec!["result".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "someBox".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "getValue".to_string(),
|
||||
arguments: vec![],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let loop_body = vec![ASTNode::Local {
|
||||
variables: vec!["x".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "result".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "something".to_string(),
|
||||
arguments: vec![],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let loop_node = ASTNode::Loop {
|
||||
condition: Box::new(ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "i".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(10),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
body: loop_body,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let fn_body = vec![result_decl, loop_node.clone()];
|
||||
|
||||
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(["i".to_string()]),
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::from(["x".to_string()]),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let env = analyze_captured_vars(&fn_body, &loop_node, &scope);
|
||||
|
||||
// Should reject because result is initialized with MethodCall (not safe constant)
|
||||
assert_eq!(env.vars.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_unused_in_loop_rejected() {
|
||||
// Build AST for:
|
||||
// local digits = "0123456789" // not used in loop
|
||||
// loop(i < 10) {
|
||||
// print(i) // doesn't use digits
|
||||
// }
|
||||
|
||||
let digits_decl = ASTNode::Local {
|
||||
variables: vec!["digits".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::String("0123456789".to_string()),
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let loop_node = ASTNode::Loop {
|
||||
condition: Box::new(ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "i".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(10),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
body: vec![], // empty body, no usage of digits
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let fn_body = vec![digits_decl, loop_node.clone()];
|
||||
|
||||
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(["i".to_string()]),
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::new(),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let env = analyze_captured_vars(&fn_body, &loop_node, &scope);
|
||||
|
||||
// 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() {
|
||||
// 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(),
|
||||
}];
|
||||
|
||||
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() {
|
||||
// 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(),
|
||||
},
|
||||
];
|
||||
|
||||
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() {
|
||||
// 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(),
|
||||
}];
|
||||
|
||||
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() {
|
||||
// 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(),
|
||||
}];
|
||||
|
||||
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());
|
||||
}
|
||||
@ -0,0 +1,172 @@
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
|
||||
use crate::mir::ValueId;
|
||||
|
||||
use super::super::helpers::*;
|
||||
use super::super::types::{CapturedEnv, CapturedKind, CapturedVar};
|
||||
|
||||
/// Analyzes function-scoped variables that can be safely captured for loop conditions/body.
|
||||
///
|
||||
/// # Phase 200-B Implementation
|
||||
///
|
||||
/// Detects function-scoped variables that are effectively immutable constants
|
||||
/// within a loop context (e.g., `digits` in JsonParser._atoi()).
|
||||
///
|
||||
/// # Detection Criteria
|
||||
///
|
||||
/// A variable is captured if ALL of the following conditions are met:
|
||||
///
|
||||
/// 1. **Declared before the loop**: Variable must be declared in function scope before the loop
|
||||
/// 2. **Safe constant init**: Initialized with string/integer literal only
|
||||
/// 3. **Never reassigned**: Variable is never reassigned within the function (is_immutable = true)
|
||||
/// 4. **Referenced in loop**: Variable is referenced in loop condition or body
|
||||
/// 5. **Not a loop parameter**: Variable is not in scope.loop_params
|
||||
/// 6. **Not a body-local**: Variable is not in scope.body_locals
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```nyash
|
||||
/// method _atoi(s, pos, len) {
|
||||
/// local digits = "0123456789" // ✅ Captured (declared before loop, never reassigned)
|
||||
/// local value = 0 // ❌ Not captured (reassigned in loop body)
|
||||
/// loop(pos < len) {
|
||||
/// local ch = s.charAt(pos) // ❌ Not captured (body-local)
|
||||
/// local digit = digits.indexOf(ch)
|
||||
/// value = value * 10 + digit
|
||||
/// pos = pos + 1
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fn_body` - AST nodes of the function body (for analysis)
|
||||
/// * `loop_ast` - AST node of the loop statement
|
||||
/// * `scope` - LoopScopeShape (for excluding loop params and body-locals)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `CapturedEnv` containing all captured variables
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn analyze_captured_vars(
|
||||
fn_body: &[ASTNode],
|
||||
loop_ast: &ASTNode,
|
||||
scope: &LoopScopeShape,
|
||||
) -> CapturedEnv {
|
||||
use std::env;
|
||||
|
||||
let debug = env::var("NYASH_CAPTURE_DEBUG").is_ok();
|
||||
|
||||
if debug {
|
||||
eprintln!("[capture/debug] Starting capture analysis");
|
||||
}
|
||||
|
||||
// Step 1: Find loop position in fn_body
|
||||
let loop_index = match find_stmt_index(fn_body, loop_ast) {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[capture/debug] Loop not found in function body, returning empty CapturedEnv"
|
||||
);
|
||||
}
|
||||
return CapturedEnv::new();
|
||||
}
|
||||
};
|
||||
|
||||
if debug {
|
||||
eprintln!("[capture/debug] Loop found at index {}", loop_index);
|
||||
}
|
||||
|
||||
// Step 2: Collect local declarations BEFORE the loop
|
||||
let pre_loop_locals = collect_local_declarations(&fn_body[..loop_index]);
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[capture/debug] Found {} pre-loop local declarations",
|
||||
pre_loop_locals.len()
|
||||
);
|
||||
}
|
||||
|
||||
let mut env = CapturedEnv::new();
|
||||
|
||||
// Step 3: For each pre-loop local, check capture criteria
|
||||
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 debug {
|
||||
eprintln!("[capture/reject] '{}': init is not a safe constant", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3b: Is this variable reassigned anywhere in fn_body?
|
||||
if is_reassigned_in_fn(fn_body, &name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': reassigned in function", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3c: Is this variable used in loop (condition or body)?
|
||||
if !is_used_in_loop(loop_ast, &name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': not used in loop", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3d: Skip if already in pinned, carriers, or body_locals
|
||||
if scope.pinned.contains(&name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': is a pinned variable", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if scope.carriers.contains(&name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': is a carrier variable", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if scope.body_locals.contains(&name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': is a body-local variable", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// All checks passed: add to CapturedEnv
|
||||
// Note: We don't have access to variable_map here, so we use a placeholder ValueId
|
||||
// The actual host_id will be resolved in ConditionEnvBuilder
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[capture/accept] '{}': ALL CHECKS PASSED, adding to CapturedEnv",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
env.add_var(CapturedVar {
|
||||
name: name.clone(),
|
||||
host_id: ValueId(0), // Placeholder, will be resolved in ConditionEnvBuilder
|
||||
is_immutable: true,
|
||||
kind: CapturedKind::Explicit,
|
||||
});
|
||||
}
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[capture/result] Captured {} variables: {:?}",
|
||||
env.vars.len(),
|
||||
env.vars.iter().map(|v| &v.name).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
env
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
|
||||
use crate::mir::ValueId;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::super::helpers::*;
|
||||
use super::super::types::{CapturedEnv, CapturedKind, CapturedVar};
|
||||
|
||||
/// Phase 200-C: Analyze captured vars with condition/body instead of loop_ast
|
||||
///
|
||||
/// This variant solves the pointer comparison problem when the loop AST is constructed
|
||||
/// dynamically (e.g., in Pattern 2). Instead of passing a loop_ast reference,
|
||||
/// we pass the condition and body directly and perform structural matching.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fn_body` - AST nodes of the function body (for analysis)
|
||||
/// * `loop_condition` - Condition expression of the loop
|
||||
/// * `loop_body` - Body statements of the loop
|
||||
/// * `scope` - LoopScopeShape (for excluding loop params and body-locals)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `CapturedEnv` containing all captured variables
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn analyze_captured_vars_v2(
|
||||
fn_body: &[ASTNode],
|
||||
loop_condition: &ASTNode,
|
||||
loop_body: &[ASTNode],
|
||||
scope: &LoopScopeShape,
|
||||
) -> CapturedEnv {
|
||||
use std::env;
|
||||
|
||||
let debug = env::var("NYASH_CAPTURE_DEBUG").is_ok();
|
||||
|
||||
if debug {
|
||||
eprintln!("[capture/debug] Starting capture analysis v2 (structural matching)");
|
||||
}
|
||||
|
||||
// Step 1: Find loop position in fn_body by structural matching
|
||||
let loop_index = find_loop_index_by_structure(fn_body, loop_condition, loop_body);
|
||||
|
||||
if debug {
|
||||
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 = 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()
|
||||
);
|
||||
}
|
||||
|
||||
let mut env = CapturedEnv::new();
|
||||
|
||||
// Step 3: For each pre-loop local, check capture criteria
|
||||
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 debug {
|
||||
eprintln!("[capture/reject] '{}': init is not a safe constant", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3b: Is this variable reassigned anywhere in fn_body?
|
||||
if is_reassigned_in_fn(fn_body, name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': reassigned in function", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3c: Is this variable used in loop (condition or body)?
|
||||
if !is_used_in_loop_parts(loop_condition, loop_body, name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': not used in loop", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3d: Skip if already in pinned, carriers, or body_locals
|
||||
if scope.pinned.contains(name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': is a pinned variable", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if scope.carriers.contains(name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': is a carrier variable", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if scope.body_locals.contains(name) {
|
||||
if debug {
|
||||
eprintln!("[capture/reject] '{}': is a body-local variable", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// All checks passed: add to CapturedEnv
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[capture/accept] '{}': ALL CHECKS PASSED, adding to CapturedEnv",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
env.add_var(CapturedVar {
|
||||
name: name.clone(),
|
||||
host_id: ValueId(0), // Placeholder, will be resolved in ConditionEnvBuilder
|
||||
is_immutable: true,
|
||||
kind: CapturedKind::Explicit,
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
kind: CapturedKind::Explicit,
|
||||
});
|
||||
}
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[capture/result] Captured {} variables: {:?}",
|
||||
env.vars.len(),
|
||||
env.vars.iter().map(|v| &v.name).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
env
|
||||
}
|
||||
Reference in New Issue
Block a user