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:
@ -15,13 +15,17 @@
|
||||
//! **Fail-Safe**: Unsupported AST nodes return explicit errors, allowing callers
|
||||
//! to fall back to legacy paths.
|
||||
|
||||
use crate::ast::{ASTNode, BinaryOperator, UnaryOperator};
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::ValueId;
|
||||
use crate::mir::join_ir::{JoinInst, MirLikeInst, BinOpKind, UnaryOp as JoinUnaryOp};
|
||||
use crate::mir::join_ir::JoinInst;
|
||||
use crate::mir::builder::MirBuilder;
|
||||
use super::scope_manager::ScopeManager;
|
||||
use super::condition_lowerer::lower_condition_to_joinir;
|
||||
use super::condition_env::ConditionEnv;
|
||||
|
||||
mod ast_support;
|
||||
mod scope_resolution;
|
||||
#[cfg(test)]
|
||||
mod test_helpers;
|
||||
|
||||
/// Phase 231: Expression lowering context
|
||||
///
|
||||
@ -180,7 +184,7 @@ impl<'env, 'builder, S: ScopeManager> ExprLowerer<'env, 'builder, S> {
|
||||
/// * `Err(ExprLoweringError)` - Lowering failed
|
||||
fn lower_condition(&mut self, ast: &ASTNode) -> Result<ValueId, ExprLoweringError> {
|
||||
// 1. Check if AST is supported in condition context
|
||||
if !Self::is_supported_condition(ast) {
|
||||
if !ast_support::is_supported_condition(ast) {
|
||||
return Err(ExprLoweringError::UnsupportedNode(
|
||||
format!("Unsupported condition node: {:?}", ast)
|
||||
));
|
||||
@ -189,7 +193,7 @@ impl<'env, 'builder, S: ScopeManager> ExprLowerer<'env, 'builder, S> {
|
||||
// 2. Build ConditionEnv from ScopeManager
|
||||
// This is the key integration point: we translate ScopeManager's view
|
||||
// into the ConditionEnv format expected by condition_lowerer.
|
||||
let condition_env = self.build_condition_env_from_scope(ast)?;
|
||||
let condition_env = scope_resolution::build_condition_env_from_scope(self.scope, ast)?;
|
||||
|
||||
// 3. Delegate to existing condition_lowerer
|
||||
// Phase 231: We use the existing, well-tested lowering logic.
|
||||
@ -216,95 +220,9 @@ impl<'env, 'builder, S: ScopeManager> ExprLowerer<'env, 'builder, S> {
|
||||
Ok(result_value)
|
||||
}
|
||||
|
||||
/// Build ConditionEnv from ScopeManager
|
||||
///
|
||||
/// This method extracts all variables referenced in the AST and resolves
|
||||
/// them through ScopeManager, building a ConditionEnv for condition_lowerer.
|
||||
fn build_condition_env_from_scope(&self, ast: &ASTNode) -> Result<ConditionEnv, ExprLoweringError> {
|
||||
let mut env = ConditionEnv::new();
|
||||
|
||||
// Extract all variable names from the AST
|
||||
let var_names = Self::extract_variable_names(ast);
|
||||
|
||||
// Resolve each variable through ScopeManager
|
||||
for name in var_names {
|
||||
if let Some(value_id) = self.scope.lookup(&name) {
|
||||
env.insert(name.clone(), value_id);
|
||||
} else {
|
||||
return Err(ExprLoweringError::VariableNotFound(name));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
/// Extract all variable names from an AST node (recursively)
|
||||
fn extract_variable_names(ast: &ASTNode) -> Vec<String> {
|
||||
let mut names = Vec::new();
|
||||
Self::extract_variable_names_recursive(ast, &mut names);
|
||||
names.sort();
|
||||
names.dedup();
|
||||
names
|
||||
}
|
||||
|
||||
/// Recursive helper for variable name extraction
|
||||
fn extract_variable_names_recursive(ast: &ASTNode, names: &mut Vec<String>) {
|
||||
match ast {
|
||||
ASTNode::Variable { name, .. } => {
|
||||
names.push(name.clone());
|
||||
}
|
||||
ASTNode::BinaryOp { left, right, .. } => {
|
||||
Self::extract_variable_names_recursive(left, names);
|
||||
Self::extract_variable_names_recursive(right, names);
|
||||
}
|
||||
ASTNode::UnaryOp { operand, .. } => {
|
||||
Self::extract_variable_names_recursive(operand, names);
|
||||
}
|
||||
// Phase 231: Only support simple expressions
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an AST node is supported in condition context
|
||||
///
|
||||
/// Phase 231: Conservative whitelist. We only support patterns we know work.
|
||||
/// Phase 240-EX: Made public to allow callers to check before attempting lowering.
|
||||
/// Public helper used by Pattern2/3 callers to gate ExprLowerer usage.
|
||||
pub fn is_supported_condition(ast: &ASTNode) -> bool {
|
||||
match ast {
|
||||
// Literals: Integer, Bool
|
||||
ASTNode::Literal { .. } => true,
|
||||
|
||||
// Variables
|
||||
ASTNode::Variable { .. } => true,
|
||||
|
||||
// Comparison operators
|
||||
ASTNode::BinaryOp { operator, left, right, .. } => {
|
||||
let op_supported = matches!(
|
||||
operator,
|
||||
BinaryOperator::Less
|
||||
| BinaryOperator::Greater
|
||||
| BinaryOperator::Equal
|
||||
| BinaryOperator::NotEqual
|
||||
| BinaryOperator::LessEqual
|
||||
| BinaryOperator::GreaterEqual
|
||||
| BinaryOperator::And
|
||||
| BinaryOperator::Or
|
||||
);
|
||||
|
||||
op_supported
|
||||
&& Self::is_supported_condition(left)
|
||||
&& Self::is_supported_condition(right)
|
||||
}
|
||||
|
||||
// Unary operators (not)
|
||||
ASTNode::UnaryOp { operator, operand, .. } => {
|
||||
matches!(operator, UnaryOperator::Not)
|
||||
&& Self::is_supported_condition(operand)
|
||||
}
|
||||
|
||||
// Everything else is unsupported
|
||||
_ => false,
|
||||
}
|
||||
ast_support::is_supported_condition(ast)
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,50 +274,25 @@ impl<'env, 'builder, S: ScopeManager> ConditionLoweringBox<S> for ExprLowerer<'e
|
||||
/// This delegates to the existing `is_supported_condition()` static method,
|
||||
/// allowing callers to check support before attempting lowering.
|
||||
fn supports(&self, condition: &ASTNode) -> bool {
|
||||
Self::is_supported_condition(condition)
|
||||
ast_support::is_supported_condition(condition)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{Span, LiteralValue};
|
||||
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use crate::ast::{BinaryOperator, LiteralValue, Span, UnaryOperator};
|
||||
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
|
||||
use crate::mir::join_ir::{BinOpKind, MirLikeInst, UnaryOp as JoinUnaryOp};
|
||||
use super::test_helpers::{bin, lit_i, span, var};
|
||||
|
||||
// Helper to create a test MirBuilder (Phase 231: minimal stub)
|
||||
fn create_test_builder() -> MirBuilder {
|
||||
MirBuilder::new()
|
||||
}
|
||||
|
||||
fn span() -> Span {
|
||||
Span::unknown()
|
||||
}
|
||||
|
||||
fn var(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: span(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lit_i(value: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Integer(value),
|
||||
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 not(expr: ASTNode) -> ASTNode {
|
||||
ASTNode::UnaryOp {
|
||||
operator: UnaryOperator::Not,
|
||||
@ -512,16 +405,8 @@ mod tests {
|
||||
|
||||
let mut builder = create_test_builder();
|
||||
|
||||
// AST: MethodCall (unsupported in condition context)
|
||||
let ast = ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
// AST: Break (unsupported in condition context)
|
||||
let ast = ASTNode::Break { span: Span::unknown() };
|
||||
|
||||
let mut expr_lowerer = ExprLowerer::new(&scope, ExprContext::Condition, &mut builder);
|
||||
let result = expr_lowerer.lower(&ast);
|
||||
@ -546,7 +431,7 @@ mod tests {
|
||||
};
|
||||
assert!(ExprLowerer::<Pattern2ScopeManager>::is_supported_condition(&ast));
|
||||
|
||||
// Unsupported: MethodCall
|
||||
// Supported: MethodCall
|
||||
let ast = ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(),
|
||||
@ -556,6 +441,10 @@ mod tests {
|
||||
arguments: vec![],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
assert!(ExprLowerer::<Pattern2ScopeManager>::is_supported_condition(&ast));
|
||||
|
||||
// Unsupported: Break node
|
||||
let ast = ASTNode::Break { span: Span::unknown() };
|
||||
assert!(!ExprLowerer::<Pattern2ScopeManager>::is_supported_condition(&ast));
|
||||
}
|
||||
|
||||
@ -589,6 +478,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_scope_with_p_and_s() -> Pattern2ScopeManager<'static> {
|
||||
// Leak these tiny envs for test lifetime convenience only.
|
||||
let mut condition_env = ConditionEnv::new();
|
||||
condition_env.insert("p".to_string(), ValueId(1));
|
||||
condition_env.insert("s".to_string(), ValueId(2));
|
||||
|
||||
let boxed_env: Box<ConditionEnv> = Box::new(condition_env);
|
||||
let condition_env_ref: &'static ConditionEnv = Box::leak(boxed_env);
|
||||
|
||||
let carrier_info = CarrierInfo {
|
||||
loop_var_name: "p".to_string(),
|
||||
loop_var_id: ValueId(1),
|
||||
carriers: vec![],
|
||||
trim_helper: None,
|
||||
promoted_loopbodylocals: vec![],
|
||||
};
|
||||
let boxed_carrier: Box<CarrierInfo> = Box::new(carrier_info);
|
||||
let carrier_ref: &'static CarrierInfo = Box::leak(boxed_carrier);
|
||||
|
||||
Pattern2ScopeManager {
|
||||
condition_env: condition_env_ref,
|
||||
loop_body_local_env: None,
|
||||
captured_env: None,
|
||||
carrier_info: carrier_ref,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_has_compare(instructions: &[JoinInst]) {
|
||||
assert!(
|
||||
instructions.iter().any(|inst| matches!(
|
||||
@ -755,29 +671,29 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expr_lowerer_pattern2_break_methodcall_is_unsupported() {
|
||||
let scope = make_basic_scope();
|
||||
fn test_expr_lowerer_methodcall_unknown_method_is_rejected() {
|
||||
let scope = make_scope_with_p_and_s();
|
||||
let mut builder = create_test_builder();
|
||||
|
||||
// s.length() (MethodCall) is not supported for Pattern2 break condition
|
||||
// Unknown method name should fail through MethodCallLowerer
|
||||
let ast = ASTNode::MethodCall {
|
||||
object: Box::new(var("s")),
|
||||
method: "length".to_string(),
|
||||
method: "unknown_method".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
|
||||
assert!(
|
||||
!ExprLowerer::<Pattern2ScopeManager>::is_supported_condition(&ast),
|
||||
"MethodCall should be rejected for Pattern2 break condition"
|
||||
ExprLowerer::<Pattern2ScopeManager>::is_supported_condition(&ast),
|
||||
"MethodCall nodes should be routed to MethodCallLowerer for validation"
|
||||
);
|
||||
|
||||
let mut expr_lowerer = ExprLowerer::new(&scope, ExprContext::Condition, &mut builder);
|
||||
let result = expr_lowerer.lower(&ast);
|
||||
|
||||
assert!(
|
||||
matches!(result, Err(ExprLoweringError::UnsupportedNode(_))),
|
||||
"MethodCall should fail-fast with UnsupportedNode"
|
||||
matches!(result, Err(ExprLoweringError::LoweringError(msg)) if msg.contains("MethodCall")),
|
||||
"Unknown method should fail-fast via MethodCallLowerer"
|
||||
);
|
||||
}
|
||||
|
||||
@ -827,6 +743,40 @@ mod tests {
|
||||
assert_has_compare(&instructions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expr_lowerer_supports_header_condition_with_length_call() {
|
||||
// header pattern: p < s.length()
|
||||
let length_call = ASTNode::MethodCall {
|
||||
object: Box::new(var("s")),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: span(),
|
||||
};
|
||||
let ast = bin(BinaryOperator::Less, var("p"), length_call);
|
||||
|
||||
assert!(
|
||||
ExprLowerer::<Pattern2ScopeManager>::is_supported_condition(&ast),
|
||||
"p < s.length() should be supported for Pattern2 header condition"
|
||||
);
|
||||
|
||||
let scope = make_scope_with_p_and_s();
|
||||
let mut builder = create_test_builder();
|
||||
let mut lowerer = ExprLowerer::new(&scope, ExprContext::Condition, &mut builder);
|
||||
|
||||
let result = lowerer.lower(&ast);
|
||||
assert!(result.is_ok(), "p < s.length() should lower successfully");
|
||||
|
||||
let instructions = lowerer.take_last_instructions();
|
||||
assert_has_compare(&instructions);
|
||||
assert!(
|
||||
instructions.iter().any(|inst| matches!(
|
||||
inst,
|
||||
JoinInst::Compute(MirLikeInst::BoxCall { method, .. }) if method == "length"
|
||||
)),
|
||||
"Expected BoxCall for length receiver in lowered instructions"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expr_lowerer_header_condition_generates_expected_instructions() {
|
||||
// Test that header condition i < 10 generates proper Compare instruction
|
||||
|
||||
Reference in New Issue
Block a user