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,