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

@ -23,6 +23,7 @@ impl MirBuilder {
///
/// Phase 195: Delegates to JoinLoopTrace for unified tracing.
/// Enable with NYASH_TRACE_VARMAP=1
#[allow(dead_code)]
pub(in crate::mir::builder) fn trace_varmap(&self, context: &str) {
super::joinir::trace::trace().varmap(context, &self.variable_map);
}

View File

@ -0,0 +1,159 @@
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use crate::mir::join_ir::lowering::join_value_space::{LOCAL_MAX, PARAM_MAX, PARAM_MIN};
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
use super::LoopHeaderPhiInfo;
#[cfg(debug_assertions)]
pub(super) fn verify_loop_header_phis(
func: &MirFunction,
header_block: BasicBlockId,
loop_info: &LoopHeaderPhiInfo,
boundary: &JoinInlineBoundary,
) {
if let Some(ref loop_var_name) = boundary.loop_var_name {
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
panic!(
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
header_block,
func.blocks.len()
)
});
let has_loop_var_phi = header_block_data
.instructions
.iter()
.any(|instr| matches!(instr, MirInstruction::Phi { .. }));
if !has_loop_var_phi && !loop_info.carrier_phis.is_empty() {
panic!(
"[JoinIRVerifier] Loop variable '{}' in boundary but no PHI in header block {} (has {} carrier PHIs)",
loop_var_name, header_block.0, loop_info.carrier_phis.len()
);
}
}
if !loop_info.carrier_phis.is_empty() {
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
panic!(
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
header_block,
func.blocks.len()
)
});
let phi_count = header_block_data
.instructions
.iter()
.filter(|instr| matches!(instr, MirInstruction::Phi { .. }))
.count();
if phi_count == 0 {
panic!(
"[JoinIRVerifier] LoopHeaderPhiInfo has {} PHIs but header block {} has none",
loop_info.carrier_phis.len(),
header_block.0
);
}
for (carrier_name, entry) in &loop_info.carrier_phis {
let phi_exists = header_block_data.instructions.iter().any(|instr| {
if let MirInstruction::Phi { dst, .. } = instr {
*dst == entry.phi_dst
} else {
false
}
});
if !phi_exists {
panic!(
"[JoinIRVerifier] Carrier '{}' has PHI dst {:?} but PHI not found in header block {}",
carrier_name, entry.phi_dst, header_block.0
);
}
}
}
}
#[cfg(debug_assertions)]
pub(super) fn verify_exit_line(
func: &MirFunction,
exit_block: BasicBlockId,
boundary: &JoinInlineBoundary,
) {
if !func.blocks.contains_key(&exit_block) {
panic!(
"[JoinIRVerifier] Exit block {} out of range (func has {} blocks)",
exit_block.0,
func.blocks.len()
);
}
if !boundary.exit_bindings.is_empty() {
for binding in &boundary.exit_bindings {
if binding.host_slot.0 >= 1_000_000 {
panic!(
"[JoinIRVerifier] Exit binding '{}' has suspiciously large host_slot {:?}",
binding.carrier_name, binding.host_slot
);
}
}
}
}
#[cfg(debug_assertions)]
pub(super) fn verify_valueid_regions(
loop_info: &LoopHeaderPhiInfo,
boundary: &JoinInlineBoundary,
) {
fn region_name(id: ValueId) -> &'static str {
if id.0 < PARAM_MIN {
"PHI Reserved"
} else if id.0 <= PARAM_MAX {
"Param"
} else if id.0 <= LOCAL_MAX {
"Local"
} else {
"Invalid (> LOCAL_MAX)"
}
}
for join_id in &boundary.join_inputs {
if !(PARAM_MIN..=PARAM_MAX).contains(&join_id.0) {
panic!(
"[JoinIRVerifier] join_input {:?} not in Param region ({})",
join_id,
region_name(*join_id)
);
}
}
for (_, entry) in &loop_info.carrier_phis {
if entry.phi_dst.0 > LOCAL_MAX {
panic!(
"[JoinIRVerifier] Carrier PHI dst {:?} outside Local region ({})",
entry.phi_dst,
region_name(entry.phi_dst)
);
}
}
for binding in &boundary.condition_bindings {
if !(PARAM_MIN..=PARAM_MAX).contains(&binding.join_value.0) {
panic!(
"[JoinIRVerifier] Condition binding '{}' join_value {:?} not in Param region ({})",
binding.name,
binding.join_value,
region_name(binding.join_value)
);
}
}
for binding in &boundary.exit_bindings {
if !(PARAM_MIN..=PARAM_MAX).contains(&binding.join_exit_value.0) {
panic!(
"[JoinIRVerifier] Exit binding '{}' join_exit_value {:?} not in Param region ({})",
binding.carrier_name,
binding.join_exit_value,
region_name(binding.join_exit_value)
);
}
}
}

View File

@ -109,7 +109,7 @@ impl LoopHeaderPhiBuilder {
crate::mir::join_ir::lowering::carrier_info::CarrierInit::BoolConst(val) => {
// Phase 228: Generate explicit bool constant for ConditionOnly carriers
let const_id = builder.next_value_id();
builder.emit_instruction(MirInstruction::Const {
let _ = builder.emit_instruction(MirInstruction::Const {
dst: const_id,
value: crate::mir::types::ConstValue::Bool(*val),
});

View File

@ -58,6 +58,7 @@ pub struct CarrierPhiEntry {
pub latch_incoming: Option<(BasicBlockId, ValueId)>,
/// Phase 227: Role of this carrier (LoopState or ConditionOnly)
#[allow(dead_code)]
pub role: CarrierRole,
}

View File

@ -22,6 +22,8 @@ mod loop_header_phi_builder;
mod tail_call_classifier;
mod merge_result;
mod expr_result_resolver;
#[cfg(debug_assertions)]
mod contract_checks;
// Phase 33-17: Re-export for use by other modules
pub use loop_header_phi_info::LoopHeaderPhiInfo;
@ -691,204 +693,6 @@ fn remap_values(
Ok(())
}
// ============================================================================
// Phase 200-3: JoinIR Contract Verification
// ============================================================================
/// Verify loop header PHI consistency
///
/// # Checks
///
/// 1. If loop_var_name is Some, header block must have corresponding PHI
/// 2. All carriers in LoopHeaderPhiInfo should have PHIs in the header block
///
/// # Panics
///
/// Panics in debug mode if contract violations are detected.
#[cfg(debug_assertions)]
fn verify_loop_header_phis(
func: &crate::mir::MirFunction,
header_block: crate::mir::BasicBlockId,
loop_info: &LoopHeaderPhiInfo,
boundary: &JoinInlineBoundary,
) {
// Check 1: Loop variable PHI existence
if let Some(ref loop_var_name) = boundary.loop_var_name {
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
panic!(
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
header_block,
func.blocks.len()
)
});
let has_loop_var_phi = header_block_data
.instructions
.iter()
.any(|instr| matches!(instr, crate::mir::MirInstruction::Phi { .. }));
if !has_loop_var_phi && !loop_info.carrier_phis.is_empty() {
panic!(
"[JoinIRVerifier] Loop variable '{}' in boundary but no PHI in header block {} (has {} carrier PHIs)",
loop_var_name, header_block.0, loop_info.carrier_phis.len()
);
}
}
// Check 2: Carrier PHI existence
if !loop_info.carrier_phis.is_empty() {
let header_block_data = func.blocks.get(&header_block).unwrap_or_else(|| {
panic!(
"[JoinIRVerifier] Header block {} not found ({} blocks in func)",
header_block,
func.blocks.len()
)
});
let phi_count = header_block_data
.instructions
.iter()
.filter(|instr| matches!(instr, crate::mir::MirInstruction::Phi { .. }))
.count();
if phi_count == 0 {
panic!(
"[JoinIRVerifier] LoopHeaderPhiInfo has {} PHIs but header block {} has none",
loop_info.carrier_phis.len(),
header_block.0
);
}
// Verify each carrier has a corresponding PHI
for (carrier_name, entry) in &loop_info.carrier_phis {
let phi_exists = header_block_data.instructions.iter().any(|instr| {
if let crate::mir::MirInstruction::Phi { dst, .. } = instr {
*dst == entry.phi_dst
} else {
false
}
});
if !phi_exists {
panic!(
"[JoinIRVerifier] Carrier '{}' has PHI dst {:?} but PHI not found in header block {}",
carrier_name, entry.phi_dst, header_block.0
);
}
}
}
}
/// Verify exit line consistency
///
/// # Checks
///
/// 1. All exit_bindings in boundary should have corresponding values
/// 2. Exit block should exist and be in range
///
/// # Panics
///
/// Panics in debug mode if contract violations are detected.
#[cfg(debug_assertions)]
fn verify_exit_line(
func: &crate::mir::MirFunction,
exit_block: crate::mir::BasicBlockId,
boundary: &JoinInlineBoundary,
) {
// Check 1: Exit block exists
if !func.blocks.contains_key(&exit_block) {
panic!(
"[JoinIRVerifier] Exit block {} out of range (func has {} blocks)",
exit_block.0,
func.blocks.len()
);
}
// Check 2: Exit bindings reference valid values
if !boundary.exit_bindings.is_empty() {
for binding in &boundary.exit_bindings {
// Verify host_slot is reasonable (basic sanity check)
// We can't verify the exact value since it's from the host's value space,
// but we can check it's not obviously invalid
if binding.host_slot.0 >= 1000000 {
// Arbitrary large number check
panic!(
"[JoinIRVerifier] Exit binding '{}' has suspiciously large host_slot {:?}",
binding.carrier_name, binding.host_slot
);
}
}
}
}
/// Phase 205-4: Verify ValueId regions follow JoinValueSpace contracts
///
/// # Checks
///
/// 1. All `boundary.join_inputs` are in Param region (100-999)
/// 2. All `carrier_phis[].phi_dst` are within valid range (<= LOCAL_MAX)
/// 3. All `condition_bindings[].join_value` are in Param region
///
/// # Rationale
///
/// JoinValueSpace enforces disjoint regions (Param: 100-999, Local: 1000+)
/// to prevent ValueId collisions. This verifier ensures that the boundary
/// contracts are respected after JoinIR generation.
///
/// # Panics
///
/// Panics in debug mode if any ValueId is in an unexpected region.
#[cfg(debug_assertions)]
fn verify_valueid_regions(
loop_info: &LoopHeaderPhiInfo,
boundary: &JoinInlineBoundary,
) {
use crate::mir::join_ir::lowering::join_value_space::{PARAM_MIN, PARAM_MAX, LOCAL_MAX};
// Helper to classify region
fn region_name(id: ValueId) -> &'static str {
if id.0 < PARAM_MIN {
"PHI Reserved"
} else if id.0 <= PARAM_MAX {
"Param"
} else if id.0 <= LOCAL_MAX {
"Local"
} else {
"Invalid (> LOCAL_MAX)"
}
}
// Check 1: Boundary join_inputs must be in Param region
for join_id in &boundary.join_inputs {
if join_id.0 < PARAM_MIN || join_id.0 > PARAM_MAX {
panic!(
"[RegionVerifier] Boundary input {:?} is in {} region, expected Param (100-999)",
join_id, region_name(*join_id)
);
}
}
// Check 2: Condition bindings must be in Param region
for binding in &boundary.condition_bindings {
let join_value = binding.join_value;
if join_value.0 < PARAM_MIN || join_value.0 > PARAM_MAX {
panic!(
"[RegionVerifier] Condition binding '{}' join_value {:?} is in {} region, expected Param (100-999)",
binding.name, join_value, region_name(join_value)
);
}
}
// Check 3: PHI dst must be within valid range
for (carrier_name, entry) in &loop_info.carrier_phis {
let phi_dst = entry.phi_dst;
if phi_dst.0 > LOCAL_MAX {
panic!(
"[RegionVerifier] Carrier '{}' PHI dst {:?} exceeds LOCAL_MAX ({})",
carrier_name, phi_dst, LOCAL_MAX
);
}
}
}
/// Verify that PHI dst values are not overwritten by later instructions (Phase 204-2)
///
/// # Checks
@ -1059,9 +863,9 @@ fn verify_joinir_contracts(
loop_info: &LoopHeaderPhiInfo,
boundary: &JoinInlineBoundary,
) {
verify_loop_header_phis(func, header_block, loop_info, boundary);
contract_checks::verify_loop_header_phis(func, header_block, loop_info, boundary);
verify_no_phi_dst_overwrite(func, header_block, loop_info); // Phase 204-2
verify_phi_inputs_defined(func, header_block); // Phase 204-3
verify_exit_line(func, exit_block, boundary);
verify_valueid_regions(loop_info, boundary); // Phase 205-4
contract_checks::verify_exit_line(func, exit_block, boundary);
contract_checks::verify_valueid_regions(loop_info, boundary); // Phase 205-4
}

View File

@ -57,6 +57,17 @@ pub fn can_lower(builder: &MirBuilder, ctx: &super::router::LoopPatternContext)
return false;
}
// Debug: show routing decision when requested
if ctx.debug {
trace::trace().debug(
"pattern2/can_lower",
&format!(
"pattern_kind={:?}, break_count={}, continue_count={}",
ctx.pattern_kind, ctx.features.break_count, ctx.features.continue_count
),
);
}
// Phase 188/Refactor: Use common carrier update validation
// Extracts loop variable for dummy carrier creation (not used but required by API)
let loop_var_name = match builder.extract_loop_variable_from_condition(ctx.condition) {
@ -533,7 +544,7 @@ impl MirBuilder {
// This is a VALIDATION-ONLY step. We check if ExprLowerer can handle the condition,
// but still use the existing proven lowering path. Future phases will replace actual lowering.
{
use crate::mir::join_ir::lowering::scope_manager::{Pattern2ScopeManager, ScopeManager};
use crate::mir::join_ir::lowering::scope_manager::Pattern2ScopeManager;
use crate::mir::join_ir::lowering::expr_lowerer::{ExprLowerer, ExprContext, ExprLoweringError};
let scope_manager = Pattern2ScopeManager {
@ -651,3 +662,81 @@ impl MirBuilder {
Ok(Some(void_val))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BinaryOperator, LiteralValue, Span};
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::loop_pattern_detection::LoopPatternKind;
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(),
}
}
#[test]
fn parse_number_like_loop_is_routed_to_pattern2() {
let condition = bin(BinaryOperator::Less, var("p"), var("len"));
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("p")),
value: Box::new(bin(
BinaryOperator::Add,
var("p"),
lit_i(1),
)),
span: span(),
},
// num_str = num_str + ch (string append is allowed by CommonPatternInitializer)
ASTNode::Assignment {
target: Box::new(var("num_str")),
value: Box::new(bin(
BinaryOperator::Add,
var("num_str"),
var("ch"),
)),
span: span(),
},
];
let ctx = LoopPatternContext::new(&condition, &body, "parse_number_like", true);
let builder = MirBuilder::new();
assert_eq!(ctx.pattern_kind, LoopPatternKind::Pattern2Break);
assert!(
can_lower(&builder, &ctx),
"Pattern2 lowerer should accept JsonParser-like break loop"
);
}
}

View File

@ -127,9 +127,15 @@ impl MirBuilder {
&mut join_value_space,
)?;
eprintln!("[pattern3/if-sum] Phase 220-D: ConditionEnv has {} bindings", condition_bindings.len());
trace::trace().debug(
"pattern3/if-sum",
&format!("ConditionEnv bindings = {}", condition_bindings.len()),
);
for binding in &condition_bindings {
eprintln!(" '{}': HOST {:?} → JoinIR {:?}", binding.name, binding.host_value, binding.join_value);
trace::trace().debug(
"pattern3/if-sum",
&format!(" '{}': HOST {:?} → JoinIR {:?}", binding.name, binding.host_value, binding.join_value),
);
}
// Call AST-based if-sum lowerer with ConditionEnv

View File

@ -360,6 +360,7 @@ mod tests {
use crate::ast::{BinaryOperator, LiteralValue, Span};
// Helper: Create a simple condition (i < 10)
#[allow(dead_code)]
fn test_condition(var_name: &str) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Less,

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

View File

@ -33,6 +33,7 @@
use crate::mir::ValueId;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::ast::ASTNode;
use std::collections::BTreeSet;
/// A variable captured from function scope for use in loop conditions/body.
///
@ -281,22 +282,23 @@ pub(crate) fn analyze_captured_vars_v2(
}
// Step 1: Find loop position in fn_body by structural matching
let loop_index = match find_loop_index_by_structure(fn_body, loop_condition, loop_body) {
Some(idx) => idx,
None => {
if debug {
eprintln!("[capture/debug] Loop not found in function body by structure, returning empty CapturedEnv");
}
return CapturedEnv::new();
}
};
let loop_index = find_loop_index_by_structure(fn_body, loop_condition, loop_body);
if debug {
eprintln!("[capture/debug] Loop found at index {} by structure", loop_index);
match loop_index {
Some(idx) => eprintln!("[capture/debug] Loop found at index {} by structure", idx),
None => eprintln!("[capture/debug] Loop not found in function body by structure (may be unit test or synthetic case)"),
}
}
// Step 2: Collect local declarations BEFORE the loop
let pre_loop_locals = collect_local_declarations(&fn_body[..loop_index]);
let pre_loop_locals = if let Some(idx) = loop_index {
collect_local_declarations(&fn_body[..idx])
} else {
// No loop found in fn_body - might be a unit test or synthetic case
// Still collect all locals from fn_body
collect_local_declarations(fn_body)
};
if debug {
eprintln!("[capture/debug] Found {} pre-loop local declarations", pre_loop_locals.len());
@ -305,13 +307,13 @@ pub(crate) fn analyze_captured_vars_v2(
let mut env = CapturedEnv::new();
// Step 3: For each pre-loop local, check capture criteria
for (name, init_expr) in pre_loop_locals {
for (name, init_expr) in &pre_loop_locals {
if debug {
eprintln!("[capture/check] Checking variable '{}'", name);
}
// 3a: Is init expression a safe constant?
if !is_safe_const_init(&init_expr) {
if !is_safe_const_init(init_expr) {
if debug {
eprintln!("[capture/reject] '{}': init is not a safe constant", name);
}
@ -319,7 +321,7 @@ pub(crate) fn analyze_captured_vars_v2(
}
// 3b: Is this variable reassigned anywhere in fn_body?
if is_reassigned_in_fn(fn_body, &name) {
if is_reassigned_in_fn(fn_body, name) {
if debug {
eprintln!("[capture/reject] '{}': reassigned in function", name);
}
@ -327,7 +329,7 @@ pub(crate) fn analyze_captured_vars_v2(
}
// 3c: Is this variable used in loop (condition or body)?
if !is_used_in_loop_parts(loop_condition, loop_body, &name) {
if !is_used_in_loop_parts(loop_condition, loop_body, name) {
if debug {
eprintln!("[capture/reject] '{}': not used in loop", name);
}
@ -335,21 +337,21 @@ pub(crate) fn analyze_captured_vars_v2(
}
// 3d: Skip if already in pinned, carriers, or body_locals
if scope.pinned.contains(&name) {
if scope.pinned.contains(name) {
if debug {
eprintln!("[capture/reject] '{}': is a pinned variable", name);
}
continue;
}
if scope.carriers.contains(&name) {
if scope.carriers.contains(name) {
if debug {
eprintln!("[capture/reject] '{}': is a carrier variable", name);
}
continue;
}
if scope.body_locals.contains(&name) {
if scope.body_locals.contains(name) {
if debug {
eprintln!("[capture/reject] '{}': is a body-local variable", name);
}
@ -368,6 +370,48 @@ pub(crate) fn analyze_captured_vars_v2(
});
}
// Phase 245C: Capture function parameters used in loop
let names_in_loop = collect_names_in_loop_parts(loop_condition, loop_body);
// pre-loop local names (already processed above)
let pre_loop_local_names: BTreeSet<String> =
pre_loop_locals.iter().map(|(name, _)| name.clone()).collect();
// Check each variable used in loop
for name in names_in_loop {
// Skip if already processed as pre-loop local
if pre_loop_local_names.contains(&name) {
continue;
}
// Skip if already in pinned, carriers, or body_locals
if scope.pinned.contains(&name)
|| scope.carriers.contains(&name)
|| scope.body_locals.contains(&name)
{
continue;
}
// Skip if reassigned in function (function parameters should not be reassigned)
if is_reassigned_in_fn(fn_body, &name) {
if debug {
eprintln!("[capture/param/reject] '{}': reassigned in function", name);
}
continue;
}
// This is a function parameter-like variable - add to CapturedEnv
if debug {
eprintln!("[capture/param/accept] '{}': function parameter used in loop", name);
}
env.add_var(CapturedVar {
name: name.clone(),
host_id: ValueId(0), // Placeholder, will be resolved in ConditionEnvBuilder
is_immutable: true,
});
}
if debug {
eprintln!("[capture/result] Captured {} variables: {:?}",
env.vars.len(),
@ -664,6 +708,84 @@ fn is_used_in_loop_parts(condition: &ASTNode, body: &[ASTNode], name: &str) -> b
check_usage(condition, name) || body.iter().any(|n| check_usage(n, name))
}
/// Phase 245C: Collect all variable names used in loop condition and body
///
/// Helper for function parameter capture. Returns a set of all variable names
/// that appear in the loop's condition or body.
fn collect_names_in_loop_parts(condition: &ASTNode, body: &[ASTNode]) -> BTreeSet<String> {
fn collect(node: &ASTNode, acc: &mut BTreeSet<String>) {
match node {
ASTNode::Variable { name, .. } => {
acc.insert(name.clone());
}
ASTNode::If { condition, then_body, else_body, .. } => {
collect(condition, acc);
for stmt in then_body {
collect(stmt, acc);
}
if let Some(else_stmts) = else_body {
for stmt in else_stmts {
collect(stmt, acc);
}
}
}
ASTNode::Assignment { target, value, .. } => {
collect(target, acc);
collect(value, acc);
}
ASTNode::UnaryOp { operand, .. } => {
collect(operand, acc);
}
ASTNode::Return { value: Some(operand), .. } => {
collect(operand, acc);
}
ASTNode::BinaryOp { left, right, .. } => {
collect(left, acc);
collect(right, acc);
}
ASTNode::MethodCall { object, arguments, .. } => {
collect(object, acc);
for arg in arguments {
collect(arg, acc);
}
}
ASTNode::FunctionCall { arguments, .. } => {
for arg in arguments {
collect(arg, acc);
}
}
ASTNode::Local { initial_values, .. } => {
for init_opt in initial_values {
if let Some(val) = init_opt {
collect(val, acc);
}
}
}
ASTNode::FieldAccess { object, .. } => {
collect(object, acc);
}
ASTNode::Index { target, index, .. } => {
collect(target, acc);
collect(index, acc);
}
ASTNode::Loop { condition, body, .. } => {
collect(condition, acc);
for stmt in body {
collect(stmt, acc);
}
}
_ => {}
}
}
let mut acc = BTreeSet::new();
collect(condition, &mut acc);
for stmt in body {
collect(stmt, &mut acc);
}
acc
}
#[cfg(test)]
mod tests {
use super::*;
@ -1078,4 +1200,338 @@ mod tests {
// Should reject because digits is not used in loop
assert_eq!(env.vars.len(), 0);
}
// Phase 245C: Function parameter capture tests
#[test]
fn test_capture_function_param_used_in_condition() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
// Simulate: fn parse_number(s, p, len) { loop(p < len) { ... } }
// Expected: 'len' should be captured (used in condition, not reassigned)
let condition = Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "len".to_string(), // function parameter
span: Span::unknown(),
}),
span: Span::unknown(),
});
let body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
carriers: BTreeSet::new(),
body_locals: BTreeSet::new(),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
// Use analyze_captured_vars_v2 with structural matching
let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);
// Should capture 'len' (function parameter used in condition)
assert_eq!(env.vars.len(), 1);
assert!(env.get("len").is_some());
let var = env.get("len").unwrap();
assert_eq!(var.name, "len");
assert!(var.is_immutable);
}
#[test]
fn test_capture_function_param_used_in_method_call() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
// Simulate: fn parse_number(s, p) { loop(p < s.length()) { ch = s.charAt(p) } }
// Expected: 's' should be captured (used in condition and body, not reassigned)
let condition = Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "s".to_string(), // function parameter
span: Span::unknown(),
}),
method: "length".to_string(),
arguments: vec![],
span: Span::unknown(),
}),
span: Span::unknown(),
});
let body = vec![
ASTNode::Local {
variables: vec!["ch".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "s".to_string(), // function parameter
span: Span::unknown(),
}),
method: "charAt".to_string(),
arguments: vec![ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
}))],
span: Span::unknown(),
},
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
carriers: BTreeSet::new(),
body_locals: BTreeSet::from(["ch".to_string()]),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
// Use analyze_captured_vars_v2 with structural matching
let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);
// Should capture 's' (function parameter used in condition and body)
assert_eq!(env.vars.len(), 1);
assert!(env.get("s").is_some());
let var = env.get("s").unwrap();
assert_eq!(var.name, "s");
assert!(var.is_immutable);
}
#[test]
fn test_capture_function_param_reassigned_rejected() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
// Simulate: fn bad_func(x) { x = 5; loop(x < 10) { x = x + 1 } }
// Expected: 'x' should NOT be captured (reassigned in function)
let condition = Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
});
let body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
// fn_body includes reassignment before loop
let fn_body = vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::from(["x".to_string()]), // x is loop param
carriers: BTreeSet::new(),
body_locals: BTreeSet::new(),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
// Use analyze_captured_vars_v2 with structural matching
let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);
// Should NOT capture 'x' (reassigned in fn_body)
assert_eq!(env.vars.len(), 0);
}
#[test]
fn test_capture_mixed_locals_and_params() {
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
// Simulate: fn parse(s, len) { local digits = "0123"; loop(p < len) { ch = digits.indexOf(...); s.charAt(...) } }
// Expected: 'len', 's', and 'digits' should all be captured
let condition = Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "len".to_string(), // function parameter
span: Span::unknown(),
}),
span: Span::unknown(),
});
let body = vec![
ASTNode::Local {
variables: vec!["ch".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "s".to_string(), // function parameter
span: Span::unknown(),
}),
method: "charAt".to_string(),
arguments: vec![ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
}))],
span: Span::unknown(),
},
ASTNode::Local {
variables: vec!["digit".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "digits".to_string(), // pre-loop local
span: Span::unknown(),
}),
method: "indexOf".to_string(),
arguments: vec![ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
}))],
span: Span::unknown(),
},
];
// fn_body includes local declaration before loop
let fn_body = vec![
ASTNode::Local {
variables: vec!["digits".to_string()],
initial_values: vec![Some(Box::new(ASTNode::Literal {
value: LiteralValue::String("0123".to_string()),
span: Span::unknown(),
}))],
span: Span::unknown(),
},
];
use std::collections::{BTreeSet, BTreeMap};
use crate::mir::BasicBlockId;
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
carriers: BTreeSet::new(),
body_locals: BTreeSet::from(["ch".to_string(), "digit".to_string()]),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
// Use analyze_captured_vars_v2 with structural matching
let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);
// Should capture all three: 'len' (param), 's' (param), 'digits' (pre-loop local)
assert_eq!(env.vars.len(), 3);
assert!(env.get("len").is_some());
assert!(env.get("s").is_some());
assert!(env.get("digits").is_some());
}
}

View File

@ -50,7 +50,7 @@ pub struct DigitPosPromotionRequest<'a> {
/// Loop structure metadata (for future use)
#[allow(dead_code)]
pub scope_shape: Option<&'a LoopScopeShape>,
pub(crate) scope_shape: Option<&'a LoopScopeShape>,
/// Break condition AST (Pattern2: Some, Pattern4: None)
pub break_cond: Option<&'a ASTNode>,
@ -148,7 +148,7 @@ impl DigitPosPromoter {
);
// Step 3: Find indexOf() definition for the comparison variable
let definition = Self::find_indexOf_definition(req.loop_body, &var_in_cond);
let definition = Self::find_index_of_definition(req.loop_body, &var_in_cond);
if let Some(def_node) = definition {
eprintln!(
@ -157,7 +157,7 @@ impl DigitPosPromoter {
);
// Step 4: Verify it's an indexOf() method call
if Self::is_indexOf_method_call(def_node) {
if Self::is_index_of_method_call(def_node) {
eprintln!("[digitpos_promoter] Confirmed indexOf() method call");
// Step 5: Verify cascading dependency
@ -241,7 +241,7 @@ impl DigitPosPromoter {
/// Find indexOf() definition in loop body
///
/// Searches for assignment: `local var = ...indexOf(...)` or `var = ...indexOf(...)`
fn find_indexOf_definition<'a>(body: &'a [ASTNode], var_name: &str) -> Option<&'a ASTNode> {
fn find_index_of_definition<'a>(body: &'a [ASTNode], var_name: &str) -> Option<&'a ASTNode> {
let mut worklist: Vec<&'a ASTNode> = body.iter().collect();
while let Some(node) = worklist.pop() {
@ -302,7 +302,7 @@ impl DigitPosPromoter {
}
/// Check if node is an indexOf() method call
fn is_indexOf_method_call(node: &ASTNode) -> bool {
fn is_index_of_method_call(node: &ASTNode) -> bool {
matches!(
node,
ASTNode::MethodCall { method, .. } if method == "indexOf"
@ -349,9 +349,9 @@ impl DigitPosPromoter {
/// Example: `digits.indexOf(ch)` → returns "ch" if it's a LoopBodyLocal
fn find_first_loopbodylocal_dependency<'a>(
body: &'a [ASTNode],
indexOf_call: &'a ASTNode,
index_of_call: &'a ASTNode,
) -> Option<&'a str> {
if let ASTNode::MethodCall { arguments, .. } = indexOf_call {
if let ASTNode::MethodCall { arguments, .. } = index_of_call {
// Check first argument (e.g., "ch" in indexOf(ch))
if let Some(arg) = arguments.first() {
if let ASTNode::Variable { name, .. } = arg {
@ -558,7 +558,7 @@ mod tests {
}
#[test]
fn test_digitpos_non_indexOf_method() {
fn test_digitpos_non_index_of_method() {
// ch = s.substring(...) → pos = s.length() → if pos < 0
// Should fail: not indexOf()

View File

@ -663,181 +663,7 @@ fn has_simple_condition(_loop_form: &LoopForm) -> bool {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
use crate::mir::loop_pattern_detection::LoopFeatures;
fn span() -> Span {
Span::unknown()
}
fn var(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: span(),
}
}
fn lit_i(n: i64) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Integer(n),
span: span(),
}
}
fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: op,
left: Box::new(left),
right: Box::new(right),
span: span(),
}
}
fn assignment(target: ASTNode, value: ASTNode) -> ASTNode {
ASTNode::Assignment {
target: Box::new(target),
value: Box::new(value),
span: span(),
}
}
fn has_continue(node: &ASTNode) -> bool {
match node {
ASTNode::Continue { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_continue) || else_body.as_ref().map_or(false, |b| b.iter().any(has_continue))
}
ASTNode::Loop { body, .. } => body.iter().any(has_continue),
_ => false,
}
}
fn has_break(node: &ASTNode) -> bool {
match node {
ASTNode::Break { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_break) || else_body.as_ref().map_or(false, |b| b.iter().any(has_break))
}
ASTNode::Loop { body, .. } => body.iter().any(has_break),
_ => false,
}
}
fn has_if(body: &[ASTNode]) -> bool {
body.iter().any(|n| matches!(n, ASTNode::If { .. }))
}
fn carrier_count(body: &[ASTNode]) -> usize {
fn count(nodes: &[ASTNode]) -> usize {
let mut c = 0;
for n in nodes {
match n {
ASTNode::Assignment { .. } => c += 1,
ASTNode::If { then_body, else_body, .. } => {
c += count(then_body);
if let Some(else_body) = else_body {
c += count(else_body);
}
}
_ => {}
}
}
c
}
if count(body) > 0 { 1 } else { 0 }
}
fn classify_body(body: &[ASTNode]) -> LoopPatternKind {
let has_continue_flag = body.iter().any(has_continue);
let has_break_flag = body.iter().any(has_break);
let features = LoopFeatures {
has_break: has_break_flag,
has_continue: has_continue_flag,
has_if: has_if(body),
has_if_else_phi: false,
carrier_count: carrier_count(body),
break_count: if has_break_flag { 1 } else { 0 },
continue_count: if has_continue_flag { 1 } else { 0 },
update_summary: None,
};
classify(&features)
}
#[test]
fn pattern1_simple_while_is_detected() {
// loop(i < len) { i = i + 1 }
let body = vec![assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1)))];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern1SimpleWhile);
}
#[test]
fn pattern2_break_loop_is_detected() {
// loop(i < len) { if i > 0 { break } i = i + 1 }
let cond = bin(BinaryOperator::Greater, var("i"), lit_i(0));
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break);
}
#[test]
fn pattern3_if_sum_shape_is_detected() {
// loop(i < len) { if i % 2 == 1 { sum = sum + 1 } i = i + 1 }
let cond = bin(
BinaryOperator::Equal,
bin(BinaryOperator::Modulo, var("i"), lit_i(2)),
lit_i(1),
);
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![assignment(
var("sum"),
bin(BinaryOperator::Add, var("sum"), lit_i(1)),
)],
else_body: None,
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern3IfPhi);
}
#[test]
fn pattern4_continue_loop_is_detected() {
// loop(i < len) { if (i % 2 == 0) { continue } sum = sum + i; i = i + 1 }
let cond = bin(
BinaryOperator::Equal,
bin(BinaryOperator::Modulo, var("i"), lit_i(2)),
lit_i(0),
);
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![ASTNode::Continue { span: span() }],
else_body: Some(vec![assignment(
var("sum"),
bin(BinaryOperator::Add, var("sum"), var("i")),
)]),
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern4Continue);
}
}
mod tests;
// Phase 170-D: Loop Condition Scope Analysis Boxes
pub mod loop_condition_scope;

View File

@ -0,0 +1,194 @@
use super::*;
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
use crate::mir::loop_pattern_detection::LoopFeatures;
fn span() -> Span {
Span::unknown()
}
fn var(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: span(),
}
}
fn lit_i(n: i64) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Integer(n),
span: span(),
}
}
fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: op,
left: Box::new(left),
right: Box::new(right),
span: span(),
}
}
fn assignment(target: ASTNode, value: ASTNode) -> ASTNode {
ASTNode::Assignment {
target: Box::new(target),
value: Box::new(value),
span: span(),
}
}
fn has_continue(node: &ASTNode) -> bool {
match node {
ASTNode::Continue { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_continue) || else_body.as_ref().map_or(false, |b| b.iter().any(has_continue))
}
ASTNode::Loop { body, .. } => body.iter().any(has_continue),
_ => false,
}
}
fn has_break(node: &ASTNode) -> bool {
match node {
ASTNode::Break { .. } => true,
ASTNode::If { then_body, else_body, .. } => {
then_body.iter().any(has_break) || else_body.as_ref().map_or(false, |b| b.iter().any(has_break))
}
ASTNode::Loop { body, .. } => body.iter().any(has_break),
_ => false,
}
}
fn has_if(body: &[ASTNode]) -> bool {
body.iter().any(|n| matches!(n, ASTNode::If { .. }))
}
fn carrier_count(body: &[ASTNode]) -> usize {
fn count(nodes: &[ASTNode]) -> usize {
let mut c = 0;
for n in nodes {
match n {
ASTNode::Assignment { .. } => c += 1,
ASTNode::If { then_body, else_body, .. } => {
c += count(then_body);
if let Some(else_body) = else_body {
c += count(else_body);
}
}
_ => {}
}
}
c
}
if count(body) > 0 { 1 } else { 0 }
}
fn classify_body(body: &[ASTNode]) -> LoopPatternKind {
let has_continue_flag = body.iter().any(has_continue);
let has_break_flag = body.iter().any(has_break);
let features = LoopFeatures {
has_break: has_break_flag,
has_continue: has_continue_flag,
has_if: has_if(body),
has_if_else_phi: false,
carrier_count: carrier_count(body),
break_count: if has_break_flag { 1 } else { 0 },
continue_count: if has_continue_flag { 1 } else { 0 },
update_summary: None,
};
classify(&features)
}
#[test]
fn pattern1_simple_while_is_detected() {
// loop(i < len) { i = i + 1 }
let body = vec![assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1)))];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern1SimpleWhile);
}
#[test]
fn pattern2_break_loop_is_detected() {
// loop(i < len) { if i > 0 { break } i = i + 1 }
let cond = bin(BinaryOperator::Greater, var("i"), lit_i(0));
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break);
}
#[test]
fn parse_number_like_loop_is_classified_as_pattern2() {
// loop(p < len) {
// if digit_pos < 0 { break }
// p = p + 1
// }
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
assignment(var("p"), bin(BinaryOperator::Add, var("p"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern2Break);
}
#[test]
fn pattern3_if_sum_shape_is_detected() {
// loop(i < len) { if i % 2 == 1 { sum = sum + 1 } i = i + 1 }
let cond = bin(
BinaryOperator::Equal,
bin(BinaryOperator::Modulo, var("i"), lit_i(2)),
lit_i(1),
);
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![assignment(
var("sum"),
bin(BinaryOperator::Add, var("sum"), lit_i(1)),
)],
else_body: None,
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern3IfPhi);
}
#[test]
fn pattern4_continue_loop_is_detected() {
// loop(i < len) { if (i % 2 == 0) { continue } sum = sum + i; i = i + 1 }
let cond = bin(
BinaryOperator::Equal,
bin(BinaryOperator::Modulo, var("i"), lit_i(2)),
lit_i(0),
);
let body = vec![
ASTNode::If {
condition: Box::new(cond),
then_body: vec![ASTNode::Continue { span: span() }],
else_body: Some(vec![assignment(
var("sum"),
bin(BinaryOperator::Add, var("sum"), var("i")),
)]),
span: span(),
},
assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))),
];
let kind = classify_body(&body);
assert_eq!(kind, LoopPatternKind::Pattern4Continue);
}

View File

@ -5,6 +5,7 @@
//! Note: JoinIR Core は常時 ON。`NYASH_JOINIR_CORE` は deprecated なので、セットは互換目的だけ。
/// Core ON (joinir_core_enabled = true) にする。
#[allow(dead_code)]
pub fn set_core_on() {
std::env::set_var("NYASH_JOINIR_CORE", "1");
}

View File

@ -27,6 +27,7 @@ impl JoinIrFrontendTestRunner {
}
/// デバッグモードを有効化JoinIR Module をダンプ)
#[allow(dead_code)]
pub fn with_debug(mut self) -> Self {
self.debug_enabled = true;
self

View File

@ -1,9 +1,12 @@
#[cfg(all(test, not(feature = "jit-direct-only")))]
#[cfg(all(test, feature = "cranelift-jit", not(feature = "jit-direct-only")))]
mod tests {
#[allow(unused_imports)]
use crate::mir::{BasicBlockId, BinaryOp, ConstValue, EffectMask, MirInstruction, MirType};
#[allow(unused_imports)]
use crate::mir::{FunctionSignature, MirFunction, MirModule};
#[cfg(feature = "cranelift-jit")]
fn make_add_main(a: i64, b: i64) -> MirModule {
let sig = FunctionSignature {
name: "main".to_string(),

View File

@ -1,12 +1,14 @@
#[cfg(all(test, not(feature = "jit-direct-only")))]
#[cfg(all(test, feature = "cranelift-jit", not(feature = "jit-direct-only")))]
mod tests {
#[allow(unused_imports)]
use crate::mir::{
BasicBlockId, ConstValue, EffectMask, FunctionSignature, MirFunction, MirInstruction,
MirModule, MirType,
};
// Build a MIR that exercises Array.get/set/len, Map.set/size/has/get, and String.len
#[cfg(feature = "cranelift-jit")]
fn make_module() -> MirModule {
let mut module = MirModule::new("identical_collections".to_string());
let sig = FunctionSignature {

View File

@ -1,4 +1,4 @@
#[cfg(all(test, not(feature = "jit-direct-only")))]
#[cfg(all(test, feature = "cranelift-jit", not(feature = "jit-direct-only")))]
mod tests {
use std::collections::HashMap;
@ -12,6 +12,7 @@ mod tests {
};
// Minimal Person factory: creates InstanceBox with fields [name, age]
#[allow(dead_code)]
struct PersonFactory;
impl crate::box_factory::BoxFactory for PersonFactory {
fn create_box(
@ -41,6 +42,7 @@ mod tests {
}
}
#[allow(dead_code)]
fn build_person_module() -> MirModule {
let mut module = MirModule::new("identical_person".to_string());
let sig = FunctionSignature {

View File

@ -1,11 +1,13 @@
#[cfg(all(test, not(feature = "jit-direct-only")))]
#[cfg(all(test, feature = "cranelift-jit", not(feature = "jit-direct-only")))]
mod tests {
#[allow(unused_imports)]
use crate::mir::{
BasicBlockId, ConstValue, EffectMask, FunctionSignature, MirFunction, MirInstruction,
MirModule, MirType,
};
#[cfg(feature = "cranelift-jit")]
fn make_string_len() -> MirModule {
let mut module = MirModule::new("identical_string".to_string());
let sig = FunctionSignature {