Apply Phase 256.8 SSOT fix to Pattern4/6/7: - Use join_module.entry.params.clone() instead of hardcoded ValueIds - Add fail-fast validation for params count mismatch - Remove ValueId(0), ValueId(PARAM_MIN + k) patterns - Clean up unused PARAM_MIN imports This prevents entry_param_mismatch errors structurally and maintains consistency with Pattern2/3. Changes: - pattern4_with_continue.rs: Lines 442-476 (SSOT extraction + validation) - pattern6_scan_with_init.rs: Lines 447-471 (SSOT extraction + validation) - pattern7_split_scan.rs: Lines 495-526 (SSOT extraction + validation) All patterns now use the same SSOT principle: 1. Extract entry function (priority: join_module.entry → fallback "main") 2. Use params as SSOT: join_inputs = entry_func.params.clone() 3. Build host_inputs in expected order (pattern-specific) 4. Fail-fast validation: join_inputs.len() == host_inputs.len() Verification: - cargo build --release: ✅ PASS (no PARAM_MIN warnings) - Quick profile: ✅ First FAIL still json_lint_vm (baseline maintained) - Pattern6 smoke: ✅ PASS (index_of test) - Pattern7 smoke: Pre-existing phi pred mismatch (not introduced by SSOT) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
982 lines
34 KiB
Rust
982 lines
34 KiB
Rust
//! Condition Expression Lowerer
|
|
//!
|
|
//! This module provides the core logic for lowering AST condition expressions
|
|
//! to JoinIR instructions. It handles comparisons, logical operators, and
|
|
//! arithmetic expressions.
|
|
//!
|
|
//! ## Design Philosophy
|
|
//!
|
|
//! **Single Responsibility**: This module ONLY performs AST → JoinIR lowering.
|
|
//! It does NOT:
|
|
//! - Manage variable environments (that's condition_env.rs)
|
|
//! - Extract variables from AST (that's condition_var_extractor.rs)
|
|
//! - Manage HOST ↔ JoinIR bindings (that's inline_boundary.rs)
|
|
|
|
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator};
|
|
use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, JoinInst, MirLikeInst, UnaryOp};
|
|
use crate::mir::ValueId;
|
|
|
|
use super::condition_env::ConditionEnv;
|
|
use super::loop_body_local_env::LoopBodyLocalEnv; // Phase 92 P2-2: Body-local support
|
|
use super::method_call_lowerer::MethodCallLowerer;
|
|
|
|
/// Lower an AST condition to JoinIR instructions
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `cond_ast` - AST node representing the boolean condition
|
|
/// * `alloc_value` - ValueId allocator function
|
|
/// * `env` - ConditionEnv for variable resolution (JoinIR-local ValueIds)
|
|
/// * `body_local_env` - Phase 92 P2-2: Optional body-local variable environment
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `Ok((ValueId, Vec<JoinInst>))` - Condition result ValueId and evaluation instructions
|
|
/// * `Err(String)` - Lowering error message
|
|
///
|
|
/// # Supported Patterns
|
|
///
|
|
/// - Comparisons: `i < n`, `x == y`, `a != b`, `x <= y`, `x >= y`, `x > y`
|
|
/// - Logical: `a && b`, `a || b`, `!cond`
|
|
/// - Variables and literals
|
|
///
|
|
/// # Phase 92 P2-2: Body-Local Variable Support
|
|
///
|
|
/// When lowering conditions that reference body-local variables (e.g., `ch == '\\'`
|
|
/// in escape patterns), the `body_local_env` parameter provides name → ValueId
|
|
/// mappings for variables defined in the loop body.
|
|
///
|
|
/// Variable resolution priority:
|
|
/// 1. ConditionEnv (loop parameters, captured variables)
|
|
/// 2. LoopBodyLocalEnv (body-local variables like `ch`)
|
|
///
|
|
/// # Phase 252: This-Method Support
|
|
///
|
|
/// When lowering conditions in static box methods (e.g., `StringUtils.trim_end/1`),
|
|
/// the `current_static_box_name` parameter enables `this.method(...)` calls to be
|
|
/// resolved to the appropriate static box method.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// let mut env = ConditionEnv::new();
|
|
/// env.insert("i".to_string(), ValueId(0));
|
|
/// env.insert("end".to_string(), ValueId(1));
|
|
///
|
|
/// let mut body_env = LoopBodyLocalEnv::new();
|
|
/// body_env.insert("ch".to_string(), ValueId(5)); // Phase 92 P2-2
|
|
///
|
|
/// let mut value_counter = 2u32;
|
|
/// let mut alloc_value = || {
|
|
/// let id = ValueId(value_counter);
|
|
/// value_counter += 1;
|
|
/// id
|
|
/// };
|
|
///
|
|
/// // Lower condition: ch == '\\'
|
|
/// let (cond_value, cond_insts) = lower_condition_to_joinir(
|
|
/// condition_ast,
|
|
/// &mut alloc_value,
|
|
/// &env,
|
|
/// Some(&body_env), // Phase 92 P2-2: Body-local support
|
|
/// Some("StringUtils"), // Phase 252: Static box name for this.method
|
|
/// )?;
|
|
/// ```
|
|
pub fn lower_condition_to_joinir(
|
|
cond_ast: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
|
|
current_static_box_name: Option<&str>, // Phase 252
|
|
) -> Result<(ValueId, Vec<JoinInst>), String> {
|
|
let mut instructions = Vec::new();
|
|
let result_value = lower_condition_recursive(
|
|
cond_ast,
|
|
alloc_value,
|
|
env,
|
|
body_local_env,
|
|
current_static_box_name,
|
|
&mut instructions,
|
|
)?;
|
|
Ok((result_value, instructions))
|
|
}
|
|
|
|
/// Convenience wrapper: lower a condition without body-local or static box support.
|
|
pub fn lower_condition_to_joinir_no_body_locals(
|
|
cond_ast: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
) -> Result<(ValueId, Vec<JoinInst>), String> {
|
|
lower_condition_to_joinir(cond_ast, alloc_value, env, None, None)
|
|
}
|
|
|
|
/// Recursive helper for condition lowering
|
|
///
|
|
/// Handles all supported AST node types and emits appropriate JoinIR instructions.
|
|
///
|
|
/// # Phase 92 P2-2
|
|
///
|
|
/// Added `body_local_env` parameter to support body-local variable resolution.
|
|
///
|
|
/// # Phase 252
|
|
///
|
|
/// Added `current_static_box_name` parameter to support `this.method(...)` calls
|
|
/// in static box method conditions.
|
|
fn lower_condition_recursive(
|
|
cond_ast: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
|
|
current_static_box_name: Option<&str>, // Phase 252
|
|
instructions: &mut Vec<JoinInst>,
|
|
) -> Result<ValueId, String> {
|
|
match cond_ast {
|
|
// Comparison operations: <, ==, !=, <=, >=, >
|
|
ASTNode::BinaryOp {
|
|
operator,
|
|
left,
|
|
right,
|
|
..
|
|
} => match operator {
|
|
BinaryOperator::Less
|
|
| BinaryOperator::Equal
|
|
| BinaryOperator::NotEqual
|
|
| BinaryOperator::LessEqual
|
|
| BinaryOperator::GreaterEqual
|
|
| BinaryOperator::Greater => {
|
|
lower_comparison(operator, left, right, alloc_value, env, body_local_env, current_static_box_name, instructions)
|
|
}
|
|
BinaryOperator::And => lower_logical_and(left, right, alloc_value, env, body_local_env, current_static_box_name, instructions),
|
|
BinaryOperator::Or => lower_logical_or(left, right, alloc_value, env, body_local_env, current_static_box_name, instructions),
|
|
_ => Err(format!(
|
|
"Unsupported binary operator in condition: {:?}",
|
|
operator
|
|
)),
|
|
},
|
|
|
|
// Unary NOT operator
|
|
ASTNode::UnaryOp {
|
|
operator: UnaryOperator::Not,
|
|
operand,
|
|
..
|
|
} => lower_not_operator(operand, alloc_value, env, body_local_env, current_static_box_name, instructions),
|
|
|
|
// Phase 92 P2-2: Variables - resolve from ConditionEnv or LoopBodyLocalEnv
|
|
ASTNode::Variable { name, .. } => {
|
|
// Priority 1: ConditionEnv (loop parameters, captured variables)
|
|
if let Some(value_id) = env.get(name) {
|
|
return Ok(value_id);
|
|
}
|
|
// Priority 2: LoopBodyLocalEnv (body-local variables like `ch`)
|
|
if let Some(body_env) = body_local_env {
|
|
if let Some(value_id) = body_env.get(name) {
|
|
return Ok(value_id);
|
|
}
|
|
}
|
|
Err(format!("Variable '{}' not found in ConditionEnv or LoopBodyLocalEnv", name))
|
|
}
|
|
|
|
// Literals - emit as constants
|
|
ASTNode::Literal { value, .. } => lower_literal(value, alloc_value, instructions),
|
|
|
|
// Phase 252: MethodCall support (this.method or builtin methods)
|
|
ASTNode::MethodCall {
|
|
object,
|
|
method,
|
|
arguments,
|
|
..
|
|
} => {
|
|
// Check if this is a me/this.method(...) call
|
|
match object.as_ref() {
|
|
ASTNode::Me { .. } | ASTNode::This { .. } => {
|
|
// me/this.method(...) - requires current_static_box_name
|
|
let box_name = current_static_box_name.ok_or_else(|| {
|
|
format!(
|
|
"this.{}(...) requires current_static_box_name (not in static box context)",
|
|
method
|
|
)
|
|
})?;
|
|
|
|
// Check if method is allowed in condition context via UserMethodPolicy
|
|
if !super::user_method_policy::UserMethodPolicy::allowed_in_condition(box_name, method) {
|
|
return Err(format!(
|
|
"User-defined method not allowed in loop condition: {}.{}() (not whitelisted)",
|
|
box_name, method
|
|
));
|
|
}
|
|
|
|
// Lower arguments using lower_for_init whitelist
|
|
// (Arguments are value expressions, not conditions, so we use init whitelist)
|
|
let mut arg_vals = Vec::new();
|
|
for arg_ast in arguments {
|
|
let arg_val = lower_value_expression(
|
|
arg_ast,
|
|
alloc_value,
|
|
env,
|
|
body_local_env,
|
|
current_static_box_name,
|
|
instructions,
|
|
)?;
|
|
arg_vals.push(arg_val);
|
|
}
|
|
|
|
// Emit BoxCall instruction
|
|
let dst = alloc_value();
|
|
instructions.push(JoinInst::Compute(MirLikeInst::BoxCall {
|
|
dst: Some(dst),
|
|
box_name: box_name.to_string(),
|
|
method: method.clone(),
|
|
args: arg_vals,
|
|
}));
|
|
|
|
Ok(dst)
|
|
}
|
|
_ => {
|
|
// Not this.method - treat as value expression (builtin methods via CoreMethodId)
|
|
lower_value_expression(object, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
Err(format!(
|
|
"MethodCall on non-this object not yet supported in condition: {:?}",
|
|
cond_ast
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
_ => Err(format!("Unsupported AST node in condition: {:?}", cond_ast)),
|
|
}
|
|
}
|
|
|
|
/// Lower a comparison operation (e.g., `i < end`)
|
|
fn lower_comparison(
|
|
operator: &BinaryOperator,
|
|
left: &ASTNode,
|
|
right: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
|
|
current_static_box_name: Option<&str>, // Phase 252
|
|
instructions: &mut Vec<JoinInst>,
|
|
) -> Result<ValueId, String> {
|
|
// Lower left and right sides
|
|
let lhs = lower_value_expression(left, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let rhs = lower_value_expression(right, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let dst = alloc_value();
|
|
|
|
let cmp_op = match operator {
|
|
BinaryOperator::Less => CompareOp::Lt,
|
|
BinaryOperator::Equal => CompareOp::Eq,
|
|
BinaryOperator::NotEqual => CompareOp::Ne,
|
|
BinaryOperator::LessEqual => CompareOp::Le,
|
|
BinaryOperator::GreaterEqual => CompareOp::Ge,
|
|
BinaryOperator::Greater => CompareOp::Gt,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
// Emit Compare instruction
|
|
instructions.push(JoinInst::Compute(MirLikeInst::Compare {
|
|
dst,
|
|
op: cmp_op,
|
|
lhs,
|
|
rhs,
|
|
}));
|
|
|
|
Ok(dst)
|
|
}
|
|
|
|
/// Lower logical AND operation (e.g., `a && b`)
|
|
fn lower_logical_and(
|
|
left: &ASTNode,
|
|
right: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
|
|
current_static_box_name: Option<&str>, // Phase 252
|
|
instructions: &mut Vec<JoinInst>,
|
|
) -> Result<ValueId, String> {
|
|
// Logical AND: evaluate both sides and combine
|
|
let lhs = lower_condition_recursive(left, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let rhs = lower_condition_recursive(right, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let dst = alloc_value();
|
|
|
|
// Emit BinOp And instruction
|
|
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
|
|
dst,
|
|
op: BinOpKind::And,
|
|
lhs,
|
|
rhs,
|
|
}));
|
|
|
|
Ok(dst)
|
|
}
|
|
|
|
/// Lower logical OR operation (e.g., `a || b`)
|
|
fn lower_logical_or(
|
|
left: &ASTNode,
|
|
right: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
|
|
current_static_box_name: Option<&str>, // Phase 252
|
|
instructions: &mut Vec<JoinInst>,
|
|
) -> Result<ValueId, String> {
|
|
// Logical OR: evaluate both sides and combine
|
|
let lhs = lower_condition_recursive(left, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let rhs = lower_condition_recursive(right, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let dst = alloc_value();
|
|
|
|
// Emit BinOp Or instruction
|
|
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
|
|
dst,
|
|
op: BinOpKind::Or,
|
|
lhs,
|
|
rhs,
|
|
}));
|
|
|
|
Ok(dst)
|
|
}
|
|
|
|
/// Lower NOT operator (e.g., `!cond`)
|
|
fn lower_not_operator(
|
|
operand: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
|
|
current_static_box_name: Option<&str>, // Phase 252
|
|
instructions: &mut Vec<JoinInst>,
|
|
) -> Result<ValueId, String> {
|
|
let operand_val = lower_condition_recursive(operand, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let dst = alloc_value();
|
|
|
|
// Emit UnaryOp Not instruction
|
|
instructions.push(JoinInst::Compute(MirLikeInst::UnaryOp {
|
|
dst,
|
|
op: UnaryOp::Not,
|
|
operand: operand_val,
|
|
}));
|
|
|
|
Ok(dst)
|
|
}
|
|
|
|
/// Lower a literal value (e.g., `10`, `true`, `"text"`)
|
|
fn lower_literal(
|
|
value: &LiteralValue,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
instructions: &mut Vec<JoinInst>,
|
|
) -> Result<ValueId, String> {
|
|
let dst = alloc_value();
|
|
let const_value = match value {
|
|
LiteralValue::Integer(n) => ConstValue::Integer(*n),
|
|
LiteralValue::String(s) => ConstValue::String(s.clone()),
|
|
LiteralValue::Bool(b) => ConstValue::Bool(*b),
|
|
LiteralValue::Float(_) => {
|
|
return Err("Float literals not supported in JoinIR conditions yet".to_string());
|
|
}
|
|
_ => {
|
|
return Err(format!(
|
|
"Unsupported literal type in condition: {:?}",
|
|
value
|
|
));
|
|
}
|
|
};
|
|
|
|
instructions.push(JoinInst::Compute(MirLikeInst::Const {
|
|
dst,
|
|
value: const_value,
|
|
}));
|
|
|
|
Ok(dst)
|
|
}
|
|
|
|
/// Lower a value expression (for comparison operands, etc.)
|
|
///
|
|
/// This handles the common case where we need to evaluate a simple value
|
|
/// (variable or literal) as part of a comparison.
|
|
///
|
|
/// # Phase 92 P2-2
|
|
///
|
|
/// Added `body_local_env` parameter to support body-local variable resolution
|
|
/// (e.g., `ch` in `ch == '\\'`).
|
|
///
|
|
/// # Phase 252
|
|
///
|
|
/// Added `current_static_box_name` parameter to support `this.method(...)` calls
|
|
/// in argument expressions.
|
|
pub fn lower_value_expression(
|
|
expr: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
|
|
current_static_box_name: Option<&str>, // Phase 252
|
|
instructions: &mut Vec<JoinInst>,
|
|
) -> Result<ValueId, String> {
|
|
match expr {
|
|
// Phase 92 P2-2: Variables - resolve from ConditionEnv or LoopBodyLocalEnv
|
|
ASTNode::Variable { name, .. } => {
|
|
// Priority 1: ConditionEnv (loop parameters, captured variables)
|
|
if let Some(value_id) = env.get(name) {
|
|
return Ok(value_id);
|
|
}
|
|
// Priority 2: LoopBodyLocalEnv (body-local variables like `ch`)
|
|
if let Some(body_env) = body_local_env {
|
|
if let Some(value_id) = body_env.get(name) {
|
|
return Ok(value_id);
|
|
}
|
|
}
|
|
Err(format!("Variable '{}' not found in ConditionEnv or LoopBodyLocalEnv", name))
|
|
}
|
|
|
|
// Literals - emit as constants
|
|
ASTNode::Literal { value, .. } => lower_literal(value, alloc_value, instructions),
|
|
|
|
// Binary operations (for arithmetic in conditions like i + 1 < n)
|
|
ASTNode::BinaryOp {
|
|
operator,
|
|
left,
|
|
right,
|
|
..
|
|
} => lower_arithmetic_binop(operator, left, right, alloc_value, env, body_local_env, current_static_box_name, instructions),
|
|
|
|
// Phase 224-C: MethodCall support with arguments (e.g., s.length(), s.indexOf(ch))
|
|
ASTNode::MethodCall {
|
|
object,
|
|
method,
|
|
arguments,
|
|
..
|
|
} => {
|
|
// 1. Lower receiver (object) to ValueId
|
|
let recv_val = lower_value_expression(object, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
|
|
// 2. Lower method call using MethodCallLowerer
|
|
// Phase 256.7: Use lower_for_init (more permissive whitelist) for value expressions
|
|
// Value expressions like s.substring(i, i+1) should be allowed even in condition arguments
|
|
let empty_body_local = super::loop_body_local_env::LoopBodyLocalEnv::new();
|
|
let body_env = body_local_env.unwrap_or(&empty_body_local);
|
|
MethodCallLowerer::lower_for_init(
|
|
recv_val,
|
|
method,
|
|
arguments,
|
|
alloc_value,
|
|
env,
|
|
body_env,
|
|
instructions,
|
|
)
|
|
}
|
|
|
|
_ => Err(format!(
|
|
"Unsupported expression in value context: {:?}",
|
|
expr
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Lower an arithmetic binary operation (e.g., `i + 1`)
|
|
fn lower_arithmetic_binop(
|
|
operator: &BinaryOperator,
|
|
left: &ASTNode,
|
|
right: &ASTNode,
|
|
alloc_value: &mut dyn FnMut() -> ValueId,
|
|
env: &ConditionEnv,
|
|
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
|
|
current_static_box_name: Option<&str>, // Phase 252
|
|
instructions: &mut Vec<JoinInst>,
|
|
) -> Result<ValueId, String> {
|
|
let lhs = lower_value_expression(left, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let rhs = lower_value_expression(right, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
|
|
let dst = alloc_value();
|
|
|
|
let bin_op = match operator {
|
|
BinaryOperator::Add => BinOpKind::Add,
|
|
BinaryOperator::Subtract => BinOpKind::Sub,
|
|
BinaryOperator::Multiply => BinOpKind::Mul,
|
|
BinaryOperator::Divide => BinOpKind::Div,
|
|
BinaryOperator::Modulo => BinOpKind::Mod,
|
|
_ => {
|
|
return Err(format!(
|
|
"Unsupported binary operator in expression: {:?}",
|
|
operator
|
|
));
|
|
}
|
|
};
|
|
|
|
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
|
|
dst,
|
|
op: bin_op,
|
|
lhs,
|
|
rhs,
|
|
}));
|
|
|
|
Ok(dst)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
|
|
|
/// Helper to create a test ConditionEnv with variables
|
|
fn create_test_env() -> ConditionEnv {
|
|
let mut env = ConditionEnv::new();
|
|
// Register test variables (using JoinIR-local ValueIds)
|
|
env.insert("i".to_string(), ValueId(0));
|
|
env.insert("end".to_string(), ValueId(1));
|
|
env
|
|
}
|
|
|
|
#[test]
|
|
fn test_simple_comparison() {
|
|
let env = create_test_env();
|
|
let mut value_counter = 2u32; // Start after i=0, end=1
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: i < end
|
|
let ast = ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Less,
|
|
left: Box::new(ASTNode::Variable {
|
|
name: "i".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::Variable {
|
|
name: "end".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
|
assert!(result.is_ok(), "Simple comparison should succeed");
|
|
|
|
let (_cond_value, instructions) = result.unwrap();
|
|
assert_eq!(
|
|
instructions.len(),
|
|
1,
|
|
"Should generate 1 Compare instruction"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_comparison_with_literal() {
|
|
let env = create_test_env();
|
|
let mut value_counter = 2u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: i < 10
|
|
let ast = ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Less,
|
|
left: Box::new(ASTNode::Variable {
|
|
name: "i".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::Literal {
|
|
value: LiteralValue::Integer(10),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
|
assert!(result.is_ok(), "Comparison with literal should succeed");
|
|
|
|
let (_cond_value, instructions) = result.unwrap();
|
|
// Should have: Const(10), Compare
|
|
assert_eq!(instructions.len(), 2, "Should generate Const + Compare");
|
|
}
|
|
|
|
#[test]
|
|
fn test_logical_or() {
|
|
let mut env = ConditionEnv::new();
|
|
env.insert("a".to_string(), ValueId(2));
|
|
env.insert("b".to_string(), ValueId(3));
|
|
|
|
let mut value_counter = 4u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: a < 5 || b < 5
|
|
let ast = ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Or,
|
|
left: Box::new(ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Less,
|
|
left: Box::new(ASTNode::Variable {
|
|
name: "a".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::Literal {
|
|
value: LiteralValue::Integer(5),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Less,
|
|
left: Box::new(ASTNode::Variable {
|
|
name: "b".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::Literal {
|
|
value: LiteralValue::Integer(5),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
|
assert!(result.is_ok(), "OR expression should succeed");
|
|
|
|
let (_cond_value, instructions) = result.unwrap();
|
|
// Should have: Const(5), Compare(a<5), Const(5), Compare(b<5), BinOp(Or)
|
|
assert_eq!(instructions.len(), 5, "Should generate proper OR chain");
|
|
}
|
|
|
|
#[test]
|
|
fn test_not_operator() {
|
|
let env = create_test_env();
|
|
let mut value_counter = 2u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: !(i < end)
|
|
let ast = ASTNode::UnaryOp {
|
|
operator: UnaryOperator::Not,
|
|
operand: Box::new(ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Less,
|
|
left: Box::new(ASTNode::Variable {
|
|
name: "i".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::Variable {
|
|
name: "end".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
|
|
assert!(result.is_ok(), "NOT operator should succeed");
|
|
|
|
let (_cond_value, instructions) = result.unwrap();
|
|
// Should have: Compare, UnaryOp(Not)
|
|
assert_eq!(instructions.len(), 2, "Should generate Compare + Not");
|
|
}
|
|
|
|
/// Phase 92 P4 Level 2: Test body-local variable resolution
|
|
///
|
|
/// This test verifies that conditions can reference body-local variables
|
|
/// (e.g., `ch == '\\'` in escape sequence patterns).
|
|
///
|
|
/// Variable resolution priority:
|
|
/// 1. ConditionEnv (loop parameters, captured variables)
|
|
/// 2. LoopBodyLocalEnv (body-local variables like `ch`)
|
|
#[test]
|
|
fn test_body_local_variable_resolution() {
|
|
// Setup ConditionEnv with loop variable
|
|
let mut env = ConditionEnv::new();
|
|
env.insert("i".to_string(), ValueId(100));
|
|
|
|
// Setup LoopBodyLocalEnv with body-local variable
|
|
let mut body_local_env = LoopBodyLocalEnv::new();
|
|
body_local_env.insert("ch".to_string(), ValueId(200));
|
|
|
|
let mut value_counter = 300u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: ch == "\\"
|
|
let ast = ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Equal,
|
|
left: Box::new(ASTNode::Variable {
|
|
name: "ch".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::Literal {
|
|
value: LiteralValue::String("\\".to_string()),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
// Phase 92 P2-2: Use lower_condition_to_joinir with body_local_env
|
|
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env), None);
|
|
assert!(
|
|
result.is_ok(),
|
|
"Body-local variable resolution should succeed"
|
|
);
|
|
|
|
let (cond_value, instructions) = result.unwrap();
|
|
// Should have: Const("\\"), Compare(ch == "\\")
|
|
assert_eq!(
|
|
instructions.len(),
|
|
2,
|
|
"Should generate Const + Compare for body-local variable"
|
|
);
|
|
|
|
// Verify the comparison uses the body-local variable's ValueId(200)
|
|
if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) {
|
|
assert_eq!(
|
|
*lhs,
|
|
ValueId(200),
|
|
"Compare should use body-local variable ValueId(200)"
|
|
);
|
|
} else {
|
|
panic!("Expected Compare instruction at position 1");
|
|
}
|
|
|
|
assert!(cond_value.0 >= 300, "Result should use newly allocated ValueId");
|
|
}
|
|
|
|
/// Phase 92 P4 Level 2: Test variable resolution priority (ConditionEnv takes precedence)
|
|
///
|
|
/// When a variable exists in both ConditionEnv and LoopBodyLocalEnv,
|
|
/// ConditionEnv should take priority.
|
|
#[test]
|
|
fn test_variable_resolution_priority() {
|
|
// Setup both environments with overlapping variable "x"
|
|
let mut env = ConditionEnv::new();
|
|
env.insert("x".to_string(), ValueId(100)); // ConditionEnv priority
|
|
|
|
let mut body_local_env = LoopBodyLocalEnv::new();
|
|
body_local_env.insert("x".to_string(), ValueId(200)); // Should be shadowed
|
|
|
|
let mut value_counter = 300u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: x == 42
|
|
let ast = ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Equal,
|
|
left: Box::new(ASTNode::Variable {
|
|
name: "x".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::Literal {
|
|
value: LiteralValue::Integer(42),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env), None);
|
|
assert!(result.is_ok(), "Variable resolution should succeed");
|
|
|
|
let (_cond_value, instructions) = result.unwrap();
|
|
|
|
// Verify the comparison uses ConditionEnv's ValueId(100), not LoopBodyLocalEnv's ValueId(200)
|
|
if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) {
|
|
assert_eq!(
|
|
*lhs,
|
|
ValueId(100),
|
|
"ConditionEnv should take priority over LoopBodyLocalEnv"
|
|
);
|
|
} else {
|
|
panic!("Expected Compare instruction at position 1");
|
|
}
|
|
}
|
|
|
|
/// Phase 92 P4 Level 2: Test error handling for undefined variables
|
|
///
|
|
/// Variables not found in either environment should produce clear error messages.
|
|
#[test]
|
|
fn test_undefined_variable_error() {
|
|
let env = ConditionEnv::new();
|
|
let body_local_env = LoopBodyLocalEnv::new();
|
|
|
|
let mut value_counter = 300u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: undefined_var == 42
|
|
let ast = ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Equal,
|
|
left: Box::new(ASTNode::Variable {
|
|
name: "undefined_var".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
right: Box::new(ASTNode::Literal {
|
|
value: LiteralValue::Integer(42),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env), None);
|
|
assert!(result.is_err(), "Undefined variable should fail");
|
|
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
err.contains("undefined_var"),
|
|
"Error message should mention the undefined variable name"
|
|
);
|
|
assert!(
|
|
err.contains("not found"),
|
|
"Error message should indicate variable was not found"
|
|
);
|
|
}
|
|
|
|
/// Phase 252 P1: Test this.methodcall(...) in conditions
|
|
///
|
|
/// Verifies that user-defined static box method calls work in conditions
|
|
#[test]
|
|
fn test_this_methodcall_in_condition() {
|
|
let env = create_test_env();
|
|
let mut value_counter = 2u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: not this.is_whitespace(ch)
|
|
// Simulates StringUtils.trim_end break condition
|
|
let method_call = ASTNode::MethodCall {
|
|
object: Box::new(ASTNode::Me {
|
|
span: Span::unknown(),
|
|
}),
|
|
method: "is_whitespace".to_string(),
|
|
arguments: vec![ASTNode::Variable {
|
|
name: "ch".to_string(),
|
|
span: Span::unknown(),
|
|
}],
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let ast = ASTNode::UnaryOp {
|
|
operator: crate::ast::UnaryOperator::Not,
|
|
operand: Box::new(method_call),
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
// Register 'ch' variable for the test
|
|
let mut env = env;
|
|
env.insert("ch".to_string(), ValueId(100));
|
|
|
|
let result = lower_condition_to_joinir(
|
|
&ast,
|
|
&mut alloc_value,
|
|
&env,
|
|
None,
|
|
Some("StringUtils"), // Phase 252: static box context
|
|
);
|
|
|
|
assert!(result.is_ok(), "this.methodcall should succeed: {:?}", result);
|
|
|
|
let (_cond_value, instructions) = result.unwrap();
|
|
|
|
// Should have: BoxCall for is_whitespace, UnaryOp(Not)
|
|
assert!(
|
|
instructions.len() >= 2,
|
|
"Should generate BoxCall + Not instructions"
|
|
);
|
|
|
|
// Verify BoxCall instruction exists
|
|
let has_box_call = instructions.iter().any(|inst| matches!(
|
|
inst,
|
|
JoinInst::Compute(MirLikeInst::BoxCall { method, .. }) if method == "is_whitespace"
|
|
));
|
|
assert!(
|
|
has_box_call,
|
|
"Should generate BoxCall for is_whitespace"
|
|
);
|
|
}
|
|
|
|
/// Phase 252 P1: Test this.methodcall fails without static box context
|
|
#[test]
|
|
fn test_this_methodcall_requires_context() {
|
|
let env = create_test_env();
|
|
let mut value_counter = 2u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: this.is_whitespace(ch)
|
|
let ast = ASTNode::MethodCall {
|
|
object: Box::new(ASTNode::Me {
|
|
span: Span::unknown(),
|
|
}),
|
|
method: "is_whitespace".to_string(),
|
|
arguments: vec![],
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = lower_condition_to_joinir(
|
|
&ast,
|
|
&mut alloc_value,
|
|
&env,
|
|
None,
|
|
None, // No static box context
|
|
);
|
|
|
|
assert!(result.is_err(), "this.methodcall should fail without context");
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
err.contains("current_static_box_name"),
|
|
"Error should mention missing static box context"
|
|
);
|
|
}
|
|
|
|
/// Phase 252 P1: Test disallowed method fails
|
|
#[test]
|
|
fn test_this_methodcall_disallowed_method() {
|
|
let env = create_test_env();
|
|
let mut value_counter = 2u32;
|
|
let mut alloc_value = || {
|
|
let id = ValueId(value_counter);
|
|
value_counter += 1;
|
|
id
|
|
};
|
|
|
|
// AST: this.trim("test") - trim is NOT allowed in conditions
|
|
let ast = ASTNode::MethodCall {
|
|
object: Box::new(ASTNode::Me {
|
|
span: Span::unknown(),
|
|
}),
|
|
method: "trim".to_string(),
|
|
arguments: vec![],
|
|
span: Span::unknown(),
|
|
};
|
|
|
|
let result = lower_condition_to_joinir(
|
|
&ast,
|
|
&mut alloc_value,
|
|
&env,
|
|
None,
|
|
Some("StringUtils"),
|
|
);
|
|
|
|
assert!(result.is_err(), "Disallowed method should fail");
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
err.contains("not allowed") || err.contains("not whitelisted"),
|
|
"Error should indicate method is not allowed: {}",
|
|
err
|
|
);
|
|
}
|
|
}
|