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

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

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

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

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

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-11 13:13:08 +09:00
parent 00ecddbbc9
commit d4597dacfa
40 changed files with 2386 additions and 1046 deletions

View File

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