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:
@ -397,6 +397,15 @@ mod tests {
|
||||
assert!(is_simple_comparison(&cond));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern3_cond_i_mod_2_eq_1_is_recognized() {
|
||||
// Pattern3/4 で使う典型的なフィルタ条件: i % 2 == 1
|
||||
let lhs = binop(BinaryOperator::Modulo, var("i"), int_lit(2));
|
||||
let cond = binop(BinaryOperator::Equal, lhs, int_lit(1));
|
||||
assert_eq!(analyze_condition_pattern(&cond), ConditionPattern::SimpleComparison);
|
||||
assert!(is_simple_comparison(&cond));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_logical_and() {
|
||||
// a && b (logical And)
|
||||
@ -427,6 +436,19 @@ mod tests {
|
||||
assert!(!is_simple_comparison(&cond));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_nested_mod_condition_is_rejected() {
|
||||
// (i % 2 == 1) && (j > 0) → 複合条件として Complex 扱い
|
||||
let mod_eq = {
|
||||
let lhs = binop(BinaryOperator::Modulo, var("i"), int_lit(2));
|
||||
binop(BinaryOperator::Equal, lhs, int_lit(1))
|
||||
};
|
||||
let gt_zero = binop(BinaryOperator::Greater, var("j"), int_lit(0));
|
||||
let cond = binop(BinaryOperator::And, mod_eq, gt_zero);
|
||||
assert_eq!(analyze_condition_pattern(&cond), ConditionPattern::Complex);
|
||||
assert!(!is_simple_comparison(&cond));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_non_binary_op() {
|
||||
// Just a variable (not a comparison)
|
||||
|
||||
@ -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
|
||||
|
||||
32
src/mir/join_ir/lowering/expr_lowerer/ast_support.rs
Normal file
32
src/mir/join_ir/lowering/expr_lowerer/ast_support.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use crate::ast::{ASTNode, BinaryOperator, UnaryOperator};
|
||||
|
||||
pub(crate) fn is_supported_condition(ast: &ASTNode) -> bool {
|
||||
use ASTNode::*;
|
||||
match ast {
|
||||
Literal { .. } => true,
|
||||
Variable { .. } => true,
|
||||
BinaryOp { operator, left, right, .. } => {
|
||||
is_supported_binary_op(operator)
|
||||
&& is_supported_condition(left)
|
||||
&& is_supported_condition(right)
|
||||
}
|
||||
UnaryOp { operator, operand, .. } => {
|
||||
matches!(operator, UnaryOperator::Not) && is_supported_condition(operand)
|
||||
}
|
||||
MethodCall { .. } => is_supported_method_call(ast),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_supported_binary_op(op: &BinaryOperator) -> bool {
|
||||
use BinaryOperator::*;
|
||||
matches!(op, Less | LessEqual | Greater | GreaterEqual | Equal | NotEqual | And | Or)
|
||||
}
|
||||
|
||||
fn is_supported_method_call(ast: &ASTNode) -> bool {
|
||||
// Phase 224-C: Accept all MethodCall nodes for syntax support.
|
||||
// Validation of method names and signatures happens during lowering in MethodCallLowerer.
|
||||
// This allows is_supported_condition() to return true for any MethodCall,
|
||||
// and fail-fast validation occurs in ExprLowerer::lower() -> MethodCallLowerer.
|
||||
matches!(ast, ASTNode::MethodCall { .. })
|
||||
}
|
||||
41
src/mir/join_ir/lowering/expr_lowerer/scope_resolution.rs
Normal file
41
src/mir/join_ir/lowering/expr_lowerer/scope_resolution.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use super::{ExprLoweringError, ScopeManager};
|
||||
|
||||
pub(crate) fn build_condition_env_from_scope<S: ScopeManager>(
|
||||
scope: &S,
|
||||
ast: &ASTNode,
|
||||
) -> Result<ConditionEnv, ExprLoweringError> {
|
||||
let mut env = ConditionEnv::new();
|
||||
let mut vars = Vec::new();
|
||||
collect_vars(ast, &mut vars);
|
||||
|
||||
for var in vars {
|
||||
if let Some(id) = scope.lookup(&var) {
|
||||
env.insert(var.clone(), id);
|
||||
} else {
|
||||
return Err(ExprLoweringError::VariableNotFound(var));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
/// Collect variable names from AST (simple traversal for supported nodes)
|
||||
fn collect_vars(ast: &ASTNode, vars: &mut Vec<String>) {
|
||||
match ast {
|
||||
ASTNode::Variable { name, .. } => vars.push(name.clone()),
|
||||
ASTNode::BinaryOp { left, right, .. } => {
|
||||
collect_vars(left, vars);
|
||||
collect_vars(right, vars);
|
||||
}
|
||||
ASTNode::UnaryOp { operand, .. } => collect_vars(operand, vars),
|
||||
ASTNode::MethodCall { object, arguments, .. } => {
|
||||
collect_vars(object, vars);
|
||||
for arg in arguments {
|
||||
collect_vars(arg, vars);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
28
src/mir/join_ir/lowering/expr_lowerer/test_helpers.rs
Normal file
28
src/mir/join_ir/lowering/expr_lowerer/test_helpers.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
||||
|
||||
pub(crate) fn span() -> Span {
|
||||
Span::unknown()
|
||||
}
|
||||
|
||||
pub(crate) fn var(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: span(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lit_i(value: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Integer(value),
|
||||
span: span(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp {
|
||||
operator: op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
span: span(),
|
||||
}
|
||||
}
|
||||
@ -25,9 +25,8 @@
|
||||
//! ```
|
||||
|
||||
use crate::mir::ValueId;
|
||||
use super::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
|
||||
use super::condition_to_joinir::ConditionBinding;
|
||||
use super::carrier_info::CarrierRole; // Phase 228: Restored for test code
|
||||
use super::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
|
||||
|
||||
/// Role of a parameter in JoinIR lowering (Phase 200-A)
|
||||
///
|
||||
@ -296,6 +295,7 @@ impl Default for JoinInlineBoundaryBuilder {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
|
||||
|
||||
#[test]
|
||||
fn test_builder_basic() {
|
||||
|
||||
@ -299,7 +299,7 @@ impl CaseALoweringShape {
|
||||
// Note: carriers is BTreeSet<String>, so each item is already a String
|
||||
let carrier_names: Vec<String> = scope.carriers.iter().cloned().collect();
|
||||
let update_summary =
|
||||
crate::mir::join_ir::lowering::loop_update_summary::analyze_loop_updates(&carrier_names);
|
||||
crate::mir::join_ir::lowering::loop_update_summary::analyze_loop_updates_by_name(&carrier_names);
|
||||
|
||||
// Create stub features (Phase 170-B will use real LoopFeatures)
|
||||
let stub_features = crate::mir::loop_pattern_detection::LoopFeatures {
|
||||
|
||||
@ -76,7 +76,7 @@ impl LoopViewBuilder {
|
||||
|
||||
// Phase 170-A-2: Structure-based routing with CaseALoweringShape
|
||||
let carrier_names: Vec<String> = scope.carriers.iter().cloned().collect();
|
||||
let update_summary = loop_update_summary::analyze_loop_updates(&carrier_names);
|
||||
let update_summary = loop_update_summary::analyze_loop_updates_by_name(&carrier_names);
|
||||
|
||||
let stub_features = crate::mir::loop_pattern_detection::LoopFeatures {
|
||||
has_break: false,
|
||||
|
||||
@ -56,9 +56,14 @@
|
||||
//! Following the "80/20 rule" from CLAUDE.md - get it working first, generalize later.
|
||||
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, ExitMeta, JoinFragmentMeta};
|
||||
mod header_break_lowering;
|
||||
mod boundary_builder;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, JoinFragmentMeta};
|
||||
use crate::mir::join_ir::lowering::carrier_update_emitter::{emit_carrier_update, emit_carrier_update_with_env};
|
||||
use crate::mir::join_ir::lowering::condition_to_joinir::{lower_condition_to_joinir, ConditionEnv};
|
||||
use crate::mir::join_ir::lowering::condition_to_joinir::ConditionEnv;
|
||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||
use crate::mir::join_ir::lowering::update_env::UpdateEnv;
|
||||
@ -72,28 +77,10 @@ use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScope
|
||||
use crate::mir::loop_pattern_detection::error_messages::{
|
||||
format_unsupported_condition_error, extract_body_local_names,
|
||||
};
|
||||
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
|
||||
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
|
||||
use crate::mir::ValueId;
|
||||
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
|
||||
|
||||
/// Phase 240-EX: Helper to build Pattern2ScopeManager with minimal dependencies
|
||||
///
|
||||
/// This helper creates a Pattern2ScopeManager for use in ExprLowerer, providing
|
||||
/// a clean way to reuse scope management setup for both header and break conditions.
|
||||
fn make_pattern2_scope_manager<'a>(
|
||||
condition_env: &'a ConditionEnv,
|
||||
body_local_env: Option<&'a LoopBodyLocalEnv>,
|
||||
captured_env: Option<&'a CapturedEnv>,
|
||||
carrier_info: &'a CarrierInfo,
|
||||
) -> Pattern2ScopeManager<'a> {
|
||||
Pattern2ScopeManager {
|
||||
condition_env,
|
||||
loop_body_local_env: body_local_env,
|
||||
captured_env,
|
||||
carrier_info,
|
||||
}
|
||||
}
|
||||
use header_break_lowering::{lower_break_condition, lower_header_condition};
|
||||
use boundary_builder::build_fragment_meta;
|
||||
|
||||
/// Lower Pattern 2 (Loop with Conditional Break) to JoinIR
|
||||
///
|
||||
@ -261,107 +248,27 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
);
|
||||
|
||||
// Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244: Lower condition
|
||||
//
|
||||
// Phase 244: Use ConditionLoweringBox trait for unified condition lowering
|
||||
let (cond_value, mut cond_instructions) = {
|
||||
use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer};
|
||||
use crate::mir::join_ir::lowering::condition_lowering_box::{ConditionLoweringBox, ConditionContext};
|
||||
use crate::mir::builder::MirBuilder;
|
||||
|
||||
// Build minimal ScopeManager for header condition
|
||||
let empty_body_env = LoopBodyLocalEnv::new();
|
||||
let empty_captured_env = CapturedEnv::new();
|
||||
|
||||
let scope_manager = make_pattern2_scope_manager(
|
||||
env,
|
||||
Some(&empty_body_env),
|
||||
Some(&empty_captured_env),
|
||||
carrier_info,
|
||||
);
|
||||
|
||||
if ExprLowerer::<Pattern2ScopeManager>::is_supported_condition(condition) {
|
||||
// Phase 244: ExprLowerer via ConditionLoweringBox trait
|
||||
let mut dummy_builder = MirBuilder::new();
|
||||
let mut expr_lowerer = ExprLowerer::new(&scope_manager, ExprContext::Condition, &mut dummy_builder);
|
||||
|
||||
// Phase 244: Use trait method instead of direct call
|
||||
let context = ConditionContext {
|
||||
loop_var_name: loop_var_name.clone(),
|
||||
loop_var_id: i_param,
|
||||
scope: &scope_manager,
|
||||
alloc_value: &mut alloc_value,
|
||||
};
|
||||
|
||||
match expr_lowerer.lower_condition(condition, &context) {
|
||||
Ok(value_id) => {
|
||||
let instructions = expr_lowerer.take_last_instructions();
|
||||
eprintln!("[joinir/pattern2/phase244] Header condition via ConditionLoweringBox: {} instructions", instructions.len());
|
||||
(value_id, instructions)
|
||||
}
|
||||
Err(e) => {
|
||||
// Fail-Fast: If ConditionLoweringBox says it's supported but fails, this is a bug
|
||||
return Err(format!(
|
||||
"[joinir/pattern2/phase244] ConditionLoweringBox failed on supported condition: {:?}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy path: condition_to_joinir (for complex conditions not yet supported)
|
||||
eprintln!("[joinir/pattern2/phase244] Header condition via legacy path (not yet supported by ConditionLoweringBox)");
|
||||
lower_condition_to_joinir(condition, &mut alloc_value, env)?
|
||||
}
|
||||
};
|
||||
let (cond_value, mut cond_instructions) = lower_header_condition(
|
||||
condition,
|
||||
env,
|
||||
carrier_info,
|
||||
loop_var_name,
|
||||
i_param,
|
||||
&mut alloc_value,
|
||||
)?;
|
||||
|
||||
// After condition lowering, allocate remaining ValueIds
|
||||
let exit_cond = alloc_value(); // Exit condition (negated loop condition)
|
||||
|
||||
// Phase 170-B / Phase 236-EX / Phase 244: Lower break condition
|
||||
//
|
||||
// Phase 244: Use ConditionLoweringBox trait for unified break condition lowering
|
||||
let (break_cond_value, mut break_cond_instructions) = {
|
||||
use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer, ExprLoweringError};
|
||||
use crate::mir::join_ir::lowering::condition_lowering_box::{ConditionLoweringBox, ConditionContext};
|
||||
use crate::mir::builder::MirBuilder;
|
||||
|
||||
// Phase 244: ExprLowerer is MirBuilder-compatible for API consistency,
|
||||
// but Pattern2 uses direct JoinInst buffer and ValueId allocator.
|
||||
let mut dummy_builder = MirBuilder::new();
|
||||
|
||||
// Build minimal ScopeManager view for break condition lowering.
|
||||
let empty_body_env = LoopBodyLocalEnv::new();
|
||||
let empty_captured_env = CapturedEnv::new();
|
||||
|
||||
let scope_manager = make_pattern2_scope_manager(
|
||||
env,
|
||||
Some(&empty_body_env),
|
||||
Some(&empty_captured_env),
|
||||
carrier_info,
|
||||
);
|
||||
|
||||
let mut expr_lowerer = ExprLowerer::new(&scope_manager, ExprContext::Condition, &mut dummy_builder);
|
||||
|
||||
// Phase 244: Use ConditionLoweringBox trait method
|
||||
let context = ConditionContext {
|
||||
loop_var_name: loop_var_name.clone(),
|
||||
loop_var_id: i_param,
|
||||
scope: &scope_manager,
|
||||
alloc_value: &mut alloc_value,
|
||||
};
|
||||
|
||||
let value_id = match expr_lowerer.lower_condition(break_condition, &context) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"[joinir/pattern2/phase244] ConditionLoweringBox failed to lower break condition: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let instructions = expr_lowerer.take_last_instructions();
|
||||
(value_id, instructions)
|
||||
};
|
||||
let (break_cond_value, mut break_cond_instructions) = lower_break_condition(
|
||||
break_condition,
|
||||
env,
|
||||
carrier_info,
|
||||
loop_var_name,
|
||||
i_param,
|
||||
&mut alloc_value,
|
||||
)?;
|
||||
|
||||
let _const_1 = alloc_value(); // Increment constant
|
||||
let _i_next = alloc_value(); // i + 1
|
||||
@ -612,37 +519,8 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
eprintln!("[joinir/pattern2] Break condition from AST (delegated to condition_to_joinir)");
|
||||
eprintln!("[joinir/pattern2] Exit PHI: k_exit receives i from both natural exit and break");
|
||||
|
||||
// Phase 176-3: Multi-carrier support - ExitMeta includes all carrier bindings
|
||||
// Build exit_values vec with all carriers
|
||||
let mut exit_values = Vec::new();
|
||||
|
||||
// Add loop variable first
|
||||
exit_values.push((loop_var_name.to_string(), i_exit));
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2/exit_meta] Building exit_values: loop_var '{}' → {:?}",
|
||||
loop_var_name, i_exit
|
||||
);
|
||||
|
||||
// Add all additional carriers
|
||||
for (idx, carrier) in carrier_info.carriers.iter().enumerate() {
|
||||
eprintln!(
|
||||
"[joinir/pattern2/exit_meta] Adding carrier '{}' → {:?} (idx={})",
|
||||
carrier.name, carrier_exit_ids[idx], idx
|
||||
);
|
||||
exit_values.push((carrier.name.clone(), carrier_exit_ids[idx]));
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2/exit_meta] Total exit_values count: {}",
|
||||
exit_values.len()
|
||||
);
|
||||
|
||||
let exit_meta = ExitMeta::multiple(exit_values);
|
||||
|
||||
// Phase 172-3 → Phase 33-14: Build JoinFragmentMeta with expr_result
|
||||
// Pattern 2: Loop is used as expression → `return i` means k_exit's return is expr_result
|
||||
let fragment_meta = JoinFragmentMeta::with_expr_result(i_exit, exit_meta);
|
||||
let fragment_meta =
|
||||
build_fragment_meta(carrier_info, loop_var_name, i_exit, &carrier_exit_ids);
|
||||
|
||||
eprintln!(
|
||||
"[joinir/pattern2] Phase 33-14/176-3: JoinFragmentMeta {{ expr_result: {:?}, carriers: {} }}",
|
||||
@ -652,224 +530,3 @@ pub(crate) fn lower_loop_with_break_minimal(
|
||||
|
||||
Ok((join_module, fragment_meta))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::Span;
|
||||
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
|
||||
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
// Helper: Create a simple variable node
|
||||
fn var_node(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Create an integer literal node
|
||||
fn int_literal_node(value: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: crate::ast::LiteralValue::Integer(value),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Create a binary operation node (Less operator for comparisons)
|
||||
fn binop_node(left: ASTNode, right: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Less,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Create a minimal LoopScopeShape
|
||||
fn minimal_scope() -> LoopScopeShape {
|
||||
LoopScopeShape {
|
||||
header: crate::mir::BasicBlockId(0),
|
||||
body: crate::mir::BasicBlockId(1),
|
||||
latch: crate::mir::BasicBlockId(2),
|
||||
exit: crate::mir::BasicBlockId(3),
|
||||
pinned: BTreeSet::new(),
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::new(),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Create a scope with outer variable
|
||||
fn scope_with_outer_var(var_name: &str) -> LoopScopeShape {
|
||||
let mut scope = minimal_scope();
|
||||
let mut pinned = BTreeSet::new();
|
||||
pinned.insert(var_name.to_string());
|
||||
scope.pinned = pinned;
|
||||
scope
|
||||
}
|
||||
|
||||
// Helper: Create a scope with loop-body-local variable
|
||||
fn scope_with_body_local_var(var_name: &str) -> LoopScopeShape {
|
||||
let mut scope = minimal_scope();
|
||||
let mut body_locals = BTreeSet::new();
|
||||
body_locals.insert(var_name.to_string());
|
||||
scope.body_locals = body_locals;
|
||||
scope
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_accepts_loop_param_only() {
|
||||
// Simple case: loop(i < 10) { if i >= 5 { break } }
|
||||
let loop_cond = binop_node(var_node("i"), int_literal_node(10));
|
||||
let break_cond = binop_node(var_node("i"), int_literal_node(5));
|
||||
|
||||
let scope = scope_with_outer_var("i"); // i is loop parameter (pinned)
|
||||
let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope));
|
||||
|
||||
assert!(!cond_scope.has_loop_body_local());
|
||||
// Only "i" is a variable; numeric literals "10" and "5" are ignored
|
||||
assert_eq!(cond_scope.var_names().len(), 1);
|
||||
assert!(cond_scope.var_names().contains("i"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_accepts_outer_scope_variables() {
|
||||
// Case: loop(i < end) { if i >= threshold { break } }
|
||||
let loop_cond = binop_node(var_node("i"), var_node("end"));
|
||||
let break_cond = binop_node(var_node("i"), var_node("threshold"));
|
||||
|
||||
let mut scope = minimal_scope();
|
||||
let mut pinned = BTreeSet::new();
|
||||
pinned.insert("i".to_string());
|
||||
pinned.insert("end".to_string());
|
||||
pinned.insert("threshold".to_string());
|
||||
scope.pinned = pinned;
|
||||
|
||||
let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope));
|
||||
|
||||
assert!(!cond_scope.has_loop_body_local());
|
||||
assert_eq!(cond_scope.var_names().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_rejects_loop_body_local_variables() {
|
||||
// Case: loop(i < 10) { local ch = ... if ch == ' ' { break } }
|
||||
let loop_cond = binop_node(var_node("i"), var_node("10"));
|
||||
let break_cond = binop_node(var_node("ch"), var_node("' '"));
|
||||
|
||||
let scope = scope_with_body_local_var("ch"); // ch is defined in loop body
|
||||
let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope));
|
||||
|
||||
assert!(cond_scope.has_loop_body_local());
|
||||
let body_local_vars: Vec<&String> = cond_scope.vars.iter()
|
||||
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
|
||||
.map(|v| &v.name)
|
||||
.collect();
|
||||
assert_eq!(body_local_vars.len(), 1);
|
||||
assert_eq!(*body_local_vars[0], "ch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_detects_mixed_scope_variables() {
|
||||
// Case: loop(i < end) { local ch = ... if ch == ' ' && i >= start { break } }
|
||||
let loop_cond = binop_node(var_node("i"), var_node("end"));
|
||||
// More complex: (ch == ' ') && (i >= start) - using nested binops with Less operator
|
||||
let ch_eq = binop_node(var_node("ch"), var_node("' '"));
|
||||
let i_ge = binop_node(var_node("i"), var_node("start"));
|
||||
let break_cond = binop_node(ch_eq, i_ge);
|
||||
|
||||
let mut scope = minimal_scope();
|
||||
let mut pinned = BTreeSet::new();
|
||||
pinned.insert("i".to_string());
|
||||
pinned.insert("end".to_string());
|
||||
pinned.insert("start".to_string());
|
||||
scope.pinned = pinned;
|
||||
let mut body_locals = BTreeSet::new();
|
||||
body_locals.insert("ch".to_string());
|
||||
scope.body_locals = body_locals;
|
||||
|
||||
let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope));
|
||||
|
||||
assert!(cond_scope.has_loop_body_local());
|
||||
let vars = cond_scope.var_names();
|
||||
assert!(vars.contains("i"));
|
||||
assert!(vars.contains("end"));
|
||||
assert!(vars.contains("start"));
|
||||
assert!(vars.contains("ch")); // The problematic body-local variable
|
||||
}
|
||||
|
||||
// Phase 240-EX: Integration test for ExprLowerer header path
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_header_condition_via_exprlowerer() {
|
||||
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierVar, CarrierRole, CarrierInit};
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||
|
||||
// Simple header condition: i < 10
|
||||
let loop_cond = binop_node(var_node("i"), int_literal_node(10));
|
||||
// Simple break condition: i >= 5
|
||||
let break_cond = binop_node(var_node("i"), int_literal_node(5));
|
||||
|
||||
let scope = minimal_scope();
|
||||
|
||||
// Setup ConditionEnv
|
||||
let mut condition_env = ConditionEnv::new();
|
||||
condition_env.insert("i".to_string(), ValueId(100));
|
||||
|
||||
// Setup CarrierInfo
|
||||
let carrier_info = CarrierInfo {
|
||||
loop_var_name: "i".to_string(),
|
||||
loop_var_id: ValueId(1),
|
||||
carriers: vec![],
|
||||
trim_helper: None,
|
||||
promoted_loopbodylocals: vec![],
|
||||
};
|
||||
|
||||
// Setup carrier_updates (empty for simple loop)
|
||||
let carrier_updates = BTreeMap::new();
|
||||
|
||||
// Setup JoinValueSpace
|
||||
let mut join_value_space = JoinValueSpace::new();
|
||||
|
||||
// Call lower_loop_with_break_minimal
|
||||
let result = lower_loop_with_break_minimal(
|
||||
scope,
|
||||
&loop_cond,
|
||||
&break_cond,
|
||||
&condition_env,
|
||||
&carrier_info,
|
||||
&carrier_updates,
|
||||
&[], // Empty body AST
|
||||
None, // No body-local env
|
||||
&mut join_value_space,
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "Should lower successfully with ExprLowerer for header condition");
|
||||
|
||||
let (join_module, _fragment_meta) = result.unwrap();
|
||||
|
||||
// Verify JoinModule structure
|
||||
assert_eq!(join_module.functions.len(), 3, "Should have 3 functions: main, loop_step, k_exit");
|
||||
|
||||
// Verify that loop_step has Compare instructions for both header and break conditions
|
||||
let loop_step_func = join_module.functions.values()
|
||||
.find(|f| f.name == "loop_step")
|
||||
.expect("Should have loop_step function");
|
||||
|
||||
let compare_count = loop_step_func.body.iter()
|
||||
.filter(|inst| matches!(inst, JoinInst::Compute(MirLikeInst::Compare { .. })))
|
||||
.count();
|
||||
|
||||
assert!(
|
||||
compare_count >= 2,
|
||||
"Should have at least 2 Compare instructions (header + break), got {}",
|
||||
compare_count
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, ExitMeta, JoinFragmentMeta};
|
||||
use crate::mir::ValueId;
|
||||
|
||||
/// Build ExitMeta and JoinFragmentMeta for Pattern2.
|
||||
pub(crate) fn build_fragment_meta(
|
||||
carrier_info: &CarrierInfo,
|
||||
loop_var_name: &str,
|
||||
i_exit: ValueId,
|
||||
carrier_exit_ids: &[ValueId],
|
||||
) -> JoinFragmentMeta {
|
||||
let mut exit_values = Vec::new();
|
||||
exit_values.push((loop_var_name.to_string(), i_exit));
|
||||
|
||||
for (idx, carrier) in carrier_info.carriers.iter().enumerate() {
|
||||
exit_values.push((carrier.name.clone(), carrier_exit_ids[idx]));
|
||||
}
|
||||
|
||||
let exit_meta = ExitMeta::multiple(exit_values);
|
||||
JoinFragmentMeta::with_expr_result(i_exit, exit_meta)
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
use crate::ast::ASTNode;
|
||||
use crate::mir::join_ir::lowering::condition_lowering_box::ConditionContext;
|
||||
use crate::mir::join_ir::lowering::condition_to_joinir::lower_condition_to_joinir;
|
||||
use crate::mir::join_ir::lowering::expr_lowerer::{ExprContext, ExprLowerer};
|
||||
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
|
||||
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
|
||||
use crate::mir::join_ir::JoinInst;
|
||||
use crate::mir::ValueId;
|
||||
use crate::mir::builder::MirBuilder;
|
||||
|
||||
/// Build a Pattern2ScopeManager for ExprLowerer paths.
|
||||
fn make_scope_manager<'a>(
|
||||
condition_env: &'a ConditionEnv,
|
||||
body_local_env: Option<&'a LoopBodyLocalEnv>,
|
||||
captured_env: Option<&'a CapturedEnv>,
|
||||
carrier_info: &'a CarrierInfo,
|
||||
) -> Pattern2ScopeManager<'a> {
|
||||
Pattern2ScopeManager {
|
||||
condition_env,
|
||||
loop_body_local_env: body_local_env,
|
||||
captured_env,
|
||||
carrier_info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Lower the header condition.
|
||||
pub(crate) fn lower_header_condition(
|
||||
condition: &ASTNode,
|
||||
env: &ConditionEnv,
|
||||
carrier_info: &CarrierInfo,
|
||||
loop_var_name: &str,
|
||||
loop_var_id: ValueId,
|
||||
alloc_value: &mut dyn FnMut() -> ValueId,
|
||||
) -> Result<(ValueId, Vec<JoinInst>), String> {
|
||||
use crate::mir::join_ir::lowering::condition_lowering_box::ConditionLoweringBox;
|
||||
|
||||
let empty_body_env = LoopBodyLocalEnv::new();
|
||||
let empty_captured_env = CapturedEnv::new();
|
||||
let scope_manager = make_scope_manager(
|
||||
env,
|
||||
Some(&empty_body_env),
|
||||
Some(&empty_captured_env),
|
||||
carrier_info,
|
||||
);
|
||||
|
||||
if ExprLowerer::<Pattern2ScopeManager>::is_supported_condition(condition) {
|
||||
let mut dummy_builder = MirBuilder::new();
|
||||
let mut expr_lowerer =
|
||||
ExprLowerer::new(&scope_manager, ExprContext::Condition, &mut dummy_builder);
|
||||
|
||||
let context = ConditionContext {
|
||||
loop_var_name: loop_var_name.to_string(),
|
||||
loop_var_id,
|
||||
scope: &scope_manager,
|
||||
alloc_value,
|
||||
};
|
||||
|
||||
match expr_lowerer.lower_condition(condition, &context) {
|
||||
Ok(value_id) => {
|
||||
let instructions = expr_lowerer.take_last_instructions();
|
||||
eprintln!(
|
||||
"[joinir/pattern2/phase244] Header condition via ConditionLoweringBox: {} instructions",
|
||||
instructions.len()
|
||||
);
|
||||
Ok((value_id, instructions))
|
||||
}
|
||||
Err(e) => Err(format!(
|
||||
"[joinir/pattern2/phase244] ConditionLoweringBox failed on supported condition: {:?}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"[joinir/pattern2/phase244] Header condition via legacy path (not yet supported by ConditionLoweringBox)"
|
||||
);
|
||||
let mut shim = || alloc_value();
|
||||
lower_condition_to_joinir(condition, &mut shim, env)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lower the break condition via ExprLowerer (no legacy fallback).
|
||||
pub(crate) fn lower_break_condition(
|
||||
break_condition: &ASTNode,
|
||||
env: &ConditionEnv,
|
||||
carrier_info: &CarrierInfo,
|
||||
loop_var_name: &str,
|
||||
loop_var_id: ValueId,
|
||||
alloc_value: &mut dyn FnMut() -> ValueId,
|
||||
) -> Result<(ValueId, Vec<JoinInst>), String> {
|
||||
use crate::mir::join_ir::lowering::condition_lowering_box::ConditionLoweringBox;
|
||||
|
||||
let empty_body_env = LoopBodyLocalEnv::new();
|
||||
let empty_captured_env = CapturedEnv::new();
|
||||
let scope_manager = make_scope_manager(
|
||||
env,
|
||||
Some(&empty_body_env),
|
||||
Some(&empty_captured_env),
|
||||
carrier_info,
|
||||
);
|
||||
|
||||
let mut dummy_builder = MirBuilder::new();
|
||||
let mut expr_lowerer =
|
||||
ExprLowerer::new(&scope_manager, ExprContext::Condition, &mut dummy_builder);
|
||||
|
||||
let context = ConditionContext {
|
||||
loop_var_name: loop_var_name.to_string(),
|
||||
loop_var_id,
|
||||
scope: &scope_manager,
|
||||
alloc_value,
|
||||
};
|
||||
|
||||
let value_id = expr_lowerer
|
||||
.lower_condition(break_condition, &context)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"[joinir/pattern2/phase244] ConditionLoweringBox failed to lower break condition: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((value_id, expr_lowerer.take_last_instructions()))
|
||||
}
|
||||
168
src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs
Normal file
168
src/mir/join_ir/lowering/loop_with_break_minimal/tests.rs
Normal file
@ -0,0 +1,168 @@
|
||||
use super::*;
|
||||
use crate::ast::Span;
|
||||
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
|
||||
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
fn var_node(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn int_literal_node(value: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: crate::ast::LiteralValue::Integer(value),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn binop_node(left: ASTNode, right: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp {
|
||||
operator: crate::ast::BinaryOperator::Less,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn minimal_scope() -> LoopScopeShape {
|
||||
LoopScopeShape {
|
||||
header: crate::mir::BasicBlockId(0),
|
||||
body: crate::mir::BasicBlockId(1),
|
||||
latch: crate::mir::BasicBlockId(2),
|
||||
exit: crate::mir::BasicBlockId(3),
|
||||
pinned: BTreeSet::new(),
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::new(),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_with_outer_var(var_name: &str) -> LoopScopeShape {
|
||||
let mut scope = minimal_scope();
|
||||
let mut pinned = BTreeSet::new();
|
||||
pinned.insert(var_name.to_string());
|
||||
scope.pinned = pinned;
|
||||
scope
|
||||
}
|
||||
|
||||
fn scope_with_body_local_var(var_name: &str) -> LoopScopeShape {
|
||||
let mut scope = minimal_scope();
|
||||
let mut body_locals = BTreeSet::new();
|
||||
body_locals.insert(var_name.to_string());
|
||||
scope.body_locals = body_locals;
|
||||
scope
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_accepts_loop_param_only() {
|
||||
let loop_cond = binop_node(var_node("i"), int_literal_node(10));
|
||||
let break_cond = binop_node(var_node("i"), int_literal_node(5));
|
||||
|
||||
let scope = scope_with_outer_var("i");
|
||||
let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope));
|
||||
|
||||
assert!(!cond_scope.has_loop_body_local());
|
||||
assert_eq!(cond_scope.var_names().len(), 1);
|
||||
assert!(cond_scope.var_names().contains("i"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_accepts_outer_scope_variables() {
|
||||
let loop_cond = binop_node(var_node("i"), var_node("end"));
|
||||
let break_cond = binop_node(var_node("i"), var_node("threshold"));
|
||||
|
||||
let mut scope = minimal_scope();
|
||||
let mut pinned = BTreeSet::new();
|
||||
pinned.insert("i".to_string());
|
||||
pinned.insert("end".to_string());
|
||||
pinned.insert("threshold".to_string());
|
||||
scope.pinned = pinned;
|
||||
|
||||
let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope));
|
||||
|
||||
assert!(!cond_scope.has_loop_body_local());
|
||||
assert_eq!(cond_scope.var_names().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_rejects_loop_body_local_variables() {
|
||||
let loop_cond = binop_node(var_node("i"), var_node("10"));
|
||||
let break_cond = binop_node(var_node("ch"), var_node("' '"));
|
||||
|
||||
let scope = scope_with_body_local_var("ch");
|
||||
let cond_scope = LoopConditionScopeBox::analyze("i", &[&loop_cond, &break_cond], Some(&scope));
|
||||
|
||||
assert!(cond_scope.has_loop_body_local());
|
||||
let body_local_vars: Vec<&String> = cond_scope
|
||||
.vars
|
||||
.iter()
|
||||
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
|
||||
.map(|v| &v.name)
|
||||
.collect();
|
||||
assert_eq!(body_local_vars.len(), 1);
|
||||
assert_eq!(*body_local_vars[0], "ch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern2_header_condition_via_exprlowerer() {
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||
|
||||
let loop_cond = binop_node(var_node("i"), int_literal_node(10));
|
||||
let break_cond = binop_node(var_node("i"), int_literal_node(5));
|
||||
|
||||
let scope = minimal_scope();
|
||||
|
||||
let mut condition_env = ConditionEnv::new();
|
||||
condition_env.insert("i".to_string(), ValueId(100));
|
||||
|
||||
let carrier_info = CarrierInfo {
|
||||
loop_var_name: "i".to_string(),
|
||||
loop_var_id: ValueId(1),
|
||||
carriers: vec![],
|
||||
trim_helper: None,
|
||||
promoted_loopbodylocals: vec![],
|
||||
};
|
||||
|
||||
let carrier_updates = BTreeMap::new();
|
||||
let mut join_value_space = JoinValueSpace::new();
|
||||
|
||||
let result = lower_loop_with_break_minimal(
|
||||
scope,
|
||||
&loop_cond,
|
||||
&break_cond,
|
||||
&condition_env,
|
||||
&carrier_info,
|
||||
&carrier_updates,
|
||||
&[],
|
||||
None,
|
||||
&mut join_value_space,
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "ExprLowerer header path should succeed");
|
||||
|
||||
let (join_module, _fragment_meta) = result.unwrap();
|
||||
assert_eq!(join_module.functions.len(), 3);
|
||||
|
||||
let loop_step_func = join_module
|
||||
.functions
|
||||
.values()
|
||||
.find(|f| f.name == "loop_step")
|
||||
.expect("loop_step should exist");
|
||||
|
||||
let compare_count = loop_step_func
|
||||
.body
|
||||
.iter()
|
||||
.filter(|inst| matches!(inst, JoinInst::Compute(MirLikeInst::Compare { .. })))
|
||||
.count();
|
||||
|
||||
assert!(
|
||||
compare_count >= 2,
|
||||
"header + break should emit at least two Compare instructions"
|
||||
);
|
||||
}
|
||||
@ -33,6 +33,8 @@ use crate::mir::join_ir::lowering::carrier_info::{ExitMeta, JoinFragmentMeta};
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use crate::mir::join_ir::lowering::condition_lowerer::lower_value_expression;
|
||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::mir::join_ir::lowering::condition_pattern::{analyze_condition_pattern, ConditionPattern};
|
||||
use crate::mir::ValueId;
|
||||
use crate::mir::join_ir::{
|
||||
BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule,
|
||||
@ -65,6 +67,16 @@ pub fn lower_if_sum_pattern(
|
||||
// Allocator for extracting condition values
|
||||
let mut alloc_value = || join_value_space.alloc_local();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let ASTNode::If { condition, .. } = if_stmt {
|
||||
let pattern = analyze_condition_pattern(condition);
|
||||
debug_assert!(
|
||||
matches!(pattern, ConditionPattern::SimpleComparison),
|
||||
"[if-sum] Unexpected complex condition pattern passed to AST-based lowerer: {:?}",
|
||||
pattern
|
||||
);
|
||||
}
|
||||
|
||||
// Step 1: Extract loop condition info (e.g., i < len → var="i", op=Lt, limit=ValueId)
|
||||
// Phase 220-D: Now returns ValueId and instructions for limit
|
||||
// Phase 242-EX-A: Now supports complex LHS (e.g., `i % 2 == 1`)
|
||||
@ -533,3 +545,107 @@ fn extract_integer_literal(node: &ASTNode) -> Result<i64, String> {
|
||||
_ => Err(format!("[if-sum] Expected integer literal, got {:?}", node)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{BinaryOperator, LiteralValue, Span};
|
||||
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
|
||||
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||
use crate::mir::join_ir::{BinOpKind, JoinInst, MirLikeInst};
|
||||
|
||||
fn var(name: &str) -> ASTNode {
|
||||
ASTNode::Variable {
|
||||
name: name.to_string(),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn int_lit(value: i64) -> ASTNode {
|
||||
ASTNode::Literal {
|
||||
value: LiteralValue::Integer(value),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp {
|
||||
operator: op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
fn assignment(target: ASTNode, value: ASTNode) -> ASTNode {
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(target),
|
||||
value: Box::new(value),
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_sum_lowering_supports_i_mod_2_eq_1_filter() {
|
||||
// Pattern3/4 で使う複雑条件 (i % 2 == 1) が JoinIR に落ちることを確認
|
||||
let mut join_value_space = JoinValueSpace::new();
|
||||
let mut cond_env = ConditionEnv::new();
|
||||
let i_id = join_value_space.alloc_param();
|
||||
let len_id = join_value_space.alloc_param();
|
||||
cond_env.insert("i".to_string(), i_id);
|
||||
cond_env.insert("len".to_string(), len_id);
|
||||
|
||||
let loop_condition = bin(BinaryOperator::Less, var("i"), var("len"));
|
||||
let if_condition = bin(
|
||||
BinaryOperator::Equal,
|
||||
bin(BinaryOperator::Modulo, var("i"), int_lit(2)),
|
||||
int_lit(1),
|
||||
);
|
||||
|
||||
let sum_update = assignment(
|
||||
var("sum"),
|
||||
bin(BinaryOperator::Add, var("sum"), int_lit(1)),
|
||||
);
|
||||
let counter_update = assignment(
|
||||
var("i"),
|
||||
bin(BinaryOperator::Add, var("i"), int_lit(1)),
|
||||
);
|
||||
|
||||
let if_stmt = ASTNode::If {
|
||||
condition: Box::new(if_condition),
|
||||
then_body: vec![sum_update],
|
||||
else_body: None,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let body = vec![if_stmt.clone(), counter_update];
|
||||
|
||||
let (module, _meta) = lower_if_sum_pattern(
|
||||
&loop_condition,
|
||||
&if_stmt,
|
||||
&body,
|
||||
&cond_env,
|
||||
&mut join_value_space,
|
||||
)
|
||||
.expect("if-sum lowering should succeed");
|
||||
|
||||
let mut has_mod = false;
|
||||
let mut has_compare = false;
|
||||
|
||||
for func in module.functions.values() {
|
||||
for inst in &func.body {
|
||||
match inst {
|
||||
JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Mod, .. }) => {
|
||||
has_mod = true;
|
||||
}
|
||||
JoinInst::Compute(MirLikeInst::Compare { .. }) => {
|
||||
has_compare = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(has_mod, "expected modulo lowering in JoinIR output");
|
||||
assert!(has_compare, "expected compare lowering in JoinIR output");
|
||||
}
|
||||
}
|
||||
|
||||
@ -513,7 +513,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lower_indexOf_with_arg() {
|
||||
fn test_lower_index_of_with_arg() {
|
||||
// Phase 226 Test: s.indexOf(ch) with 1 argument (cascading support)
|
||||
let recv_val = ValueId(10);
|
||||
let ch_val = ValueId(11);
|
||||
|
||||
@ -24,7 +24,6 @@ use super::condition_env::ConditionEnv;
|
||||
use super::loop_body_local_env::LoopBodyLocalEnv;
|
||||
use super::carrier_info::CarrierInfo;
|
||||
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Phase 231: Scope kind for variables
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user