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

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

View File

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

View 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 { .. })
}

View 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);
}
}
_ => {}
}
}

View 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(),
}
}

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"
);
}

View File

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

View File

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

View File

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