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:
@ -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);
|
||||
}
|
||||
|
||||
159
src/mir/builder/control_flow/joinir/merge/contract_checks.rs
Normal file
159
src/mir/builder/control_flow/joinir/merge/contract_checks.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
});
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user