refactor: split large modules into submodules

This commit is contained in:
2025-12-27 21:43:37 +09:00
parent f654dd316d
commit 7ab042ca91
42 changed files with 6067 additions and 6145 deletions

View File

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

View File

@ -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());
}

View File

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

View File

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