317 lines
9.9 KiB
Rust
317 lines
9.9 KiB
Rust
|
|
//! Phase 170-D-impl-2: Condition Variable Analyzer Box
|
||
|
|
//!
|
||
|
|
//! Pure functions for analyzing condition AST nodes and determining
|
||
|
|
//! variable scopes based on LoopScopeShape information.
|
||
|
|
//!
|
||
|
|
//! This Box extracts all variable references from condition expressions
|
||
|
|
//! and determines whether they are from the loop parameter, outer scope,
|
||
|
|
//! or loop body scope.
|
||
|
|
//!
|
||
|
|
//! # Design Philosophy
|
||
|
|
//!
|
||
|
|
//! - **Pure functions**: No side effects, only analysis
|
||
|
|
//! - **Composable**: Can be used independently or as part of LoopConditionScopeBox
|
||
|
|
//! - **Fail-Fast**: Defaults to conservative classification (LoopBodyLocal)
|
||
|
|
|
||
|
|
use crate::ast::ASTNode;
|
||
|
|
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
|
||
|
|
use crate::mir::BasicBlockId;
|
||
|
|
use std::collections::HashSet;
|
||
|
|
|
||
|
|
/// Extract all variable names from an AST expression
|
||
|
|
///
|
||
|
|
/// Recursively traverses the AST node and collects all Variable references.
|
||
|
|
/// Handles: Variables, UnaryOp, BinaryOp, MethodCall, FieldAccess, Index, If
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `node` - AST node to analyze
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// HashSet of all variable names found in the expression
|
||
|
|
///
|
||
|
|
/// # Example
|
||
|
|
///
|
||
|
|
/// For expression `(i < 10) && (ch != ' ')`, returns `{"i", "ch"}`
|
||
|
|
pub fn extract_all_variables(node: &ASTNode) -> HashSet<String> {
|
||
|
|
let mut vars = HashSet::new();
|
||
|
|
extract_vars_recursive(node, &mut vars);
|
||
|
|
vars
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Internal recursive helper for variable extraction
|
||
|
|
fn extract_vars_recursive(node: &ASTNode, vars: &mut HashSet<String>) {
|
||
|
|
match node {
|
||
|
|
ASTNode::Variable { name, .. } => {
|
||
|
|
vars.insert(name.clone());
|
||
|
|
}
|
||
|
|
ASTNode::UnaryOp { operand, .. } => {
|
||
|
|
extract_vars_recursive(operand, vars);
|
||
|
|
}
|
||
|
|
ASTNode::BinaryOp { left, right, .. } => {
|
||
|
|
extract_vars_recursive(left, vars);
|
||
|
|
extract_vars_recursive(right, vars);
|
||
|
|
}
|
||
|
|
ASTNode::MethodCall {
|
||
|
|
object, arguments, ..
|
||
|
|
} => {
|
||
|
|
extract_vars_recursive(object, vars);
|
||
|
|
for arg in arguments {
|
||
|
|
extract_vars_recursive(arg, vars);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
ASTNode::FieldAccess { object, .. } => {
|
||
|
|
extract_vars_recursive(object, vars);
|
||
|
|
}
|
||
|
|
ASTNode::Index { target, index, .. } => {
|
||
|
|
extract_vars_recursive(target, vars);
|
||
|
|
extract_vars_recursive(index, vars);
|
||
|
|
}
|
||
|
|
ASTNode::If {
|
||
|
|
condition,
|
||
|
|
then_body,
|
||
|
|
else_body,
|
||
|
|
..
|
||
|
|
} => {
|
||
|
|
extract_vars_recursive(condition, vars);
|
||
|
|
for stmt in then_body {
|
||
|
|
extract_vars_recursive(stmt, vars);
|
||
|
|
}
|
||
|
|
if let Some(else_body) = else_body {
|
||
|
|
for stmt in else_body {
|
||
|
|
extract_vars_recursive(stmt, vars);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => {} // Skip literals, constants, etc.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Determine if a variable is from the outer scope
|
||
|
|
///
|
||
|
|
/// # Phase 170-D-impl-2: Simple heuristic
|
||
|
|
///
|
||
|
|
/// A variable is "outer local" if:
|
||
|
|
/// 1. It appears in LoopScopeShape.pinned (loop parameters or outer variables)
|
||
|
|
/// 2. It appears in LoopScopeShape.variable_definitions as being defined
|
||
|
|
/// in the header block (NOT in body/latch/exit)
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `var_name` - Name of the variable to check
|
||
|
|
/// * `scope` - Optional LoopScopeShape with variable definition information
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// - `true` if variable is definitively from outer scope
|
||
|
|
/// - `false` if unknown, from body scope, or no scope info available
|
||
|
|
///
|
||
|
|
/// # Notes
|
||
|
|
///
|
||
|
|
/// This is a simplified implementation for Phase 170-D.
|
||
|
|
/// Future versions may include:
|
||
|
|
/// - Dominance tree analysis
|
||
|
|
/// - More sophisticated scope inference
|
||
|
|
pub fn is_outer_scope_variable(
|
||
|
|
var_name: &str,
|
||
|
|
scope: Option<&LoopScopeShape>,
|
||
|
|
) -> bool {
|
||
|
|
match scope {
|
||
|
|
None => false, // No scope info → assume body-local
|
||
|
|
Some(scope) => {
|
||
|
|
// Check 1: Is it a pinned variable (loop parameter or passed-in)?
|
||
|
|
if scope.pinned.contains(var_name) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check 2: Does it appear in variable_definitions and is it from header only?
|
||
|
|
if let Some(def_blocks) = scope.variable_definitions.get(var_name) {
|
||
|
|
// Simplified heuristic: if defined ONLY in header block → outer scope
|
||
|
|
// Phase 170-D: This is conservative - we only accept variables
|
||
|
|
// that are EXCLUSIVELY defined in the header (before loop enters)
|
||
|
|
if def_blocks.len() == 1 && def_blocks.contains(&scope.header) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
// Helper: Create a Variable node
|
||
|
|
fn var_node(name: &str) -> ASTNode {
|
||
|
|
ASTNode::Variable {
|
||
|
|
name: name.to_string(),
|
||
|
|
span: crate::ast::Span::unknown(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper: Create a BinaryOp node
|
||
|
|
fn binop_node(left: ASTNode, right: ASTNode) -> ASTNode {
|
||
|
|
ASTNode::BinaryOp {
|
||
|
|
operator: crate::ast::BinaryOperator::Add, // Placeholder operator
|
||
|
|
left: Box::new(left),
|
||
|
|
right: Box::new(right),
|
||
|
|
span: crate::ast::Span::unknown(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper: Create a UnaryOp node
|
||
|
|
fn unary_node(operand: ASTNode) -> ASTNode {
|
||
|
|
ASTNode::UnaryOp {
|
||
|
|
operator: crate::ast::UnaryOperator::Not,
|
||
|
|
operand: Box::new(operand),
|
||
|
|
span: crate::ast::Span::unknown(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_extract_single_variable() {
|
||
|
|
let node = var_node("x");
|
||
|
|
let vars = extract_all_variables(&node);
|
||
|
|
assert_eq!(vars.len(), 1);
|
||
|
|
assert!(vars.contains("x"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_extract_multiple_variables() {
|
||
|
|
let node = binop_node(var_node("x"), var_node("y"));
|
||
|
|
let vars = extract_all_variables(&node);
|
||
|
|
assert_eq!(vars.len(), 2);
|
||
|
|
assert!(vars.contains("x"));
|
||
|
|
assert!(vars.contains("y"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_extract_deduplicated_variables() {
|
||
|
|
let node = binop_node(var_node("x"), var_node("x"));
|
||
|
|
let vars = extract_all_variables(&node);
|
||
|
|
assert_eq!(vars.len(), 1); // HashSet deduplicates
|
||
|
|
assert!(vars.contains("x"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_extract_nested_variables() {
|
||
|
|
// Create (x + y) + z structure
|
||
|
|
let inner = binop_node(var_node("x"), var_node("y"));
|
||
|
|
let outer = binop_node(inner, var_node("z"));
|
||
|
|
let vars = extract_all_variables(&outer);
|
||
|
|
|
||
|
|
assert_eq!(vars.len(), 3);
|
||
|
|
assert!(vars.contains("x"));
|
||
|
|
assert!(vars.contains("y"));
|
||
|
|
assert!(vars.contains("z"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_extract_with_unary_op() {
|
||
|
|
// Create !(x) structure
|
||
|
|
let node = unary_node(var_node("x"));
|
||
|
|
let vars = extract_all_variables(&node);
|
||
|
|
|
||
|
|
assert_eq!(vars.len(), 1);
|
||
|
|
assert!(vars.contains("x"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_extract_no_variables_from_literal() {
|
||
|
|
let node = ASTNode::Literal {
|
||
|
|
value: crate::ast::LiteralValue::Integer(42),
|
||
|
|
span: crate::ast::Span::unknown(),
|
||
|
|
};
|
||
|
|
let vars = extract_all_variables(&node);
|
||
|
|
|
||
|
|
assert!(vars.is_empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_is_outer_scope_variable_with_no_scope() {
|
||
|
|
let result = is_outer_scope_variable("x", None);
|
||
|
|
assert!(!result); // No scope → assume body-local
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_is_outer_scope_variable_pinned() {
|
||
|
|
use std::collections::{BTreeMap, BTreeSet};
|
||
|
|
|
||
|
|
let mut pinned = BTreeSet::new();
|
||
|
|
pinned.insert("len".to_string());
|
||
|
|
|
||
|
|
let scope = LoopScopeShape {
|
||
|
|
header: BasicBlockId(0),
|
||
|
|
body: BasicBlockId(1),
|
||
|
|
latch: BasicBlockId(2),
|
||
|
|
exit: BasicBlockId(3),
|
||
|
|
pinned,
|
||
|
|
carriers: BTreeSet::new(),
|
||
|
|
body_locals: BTreeSet::new(),
|
||
|
|
exit_live: BTreeSet::new(),
|
||
|
|
progress_carrier: None,
|
||
|
|
variable_definitions: BTreeMap::new(),
|
||
|
|
};
|
||
|
|
|
||
|
|
assert!(is_outer_scope_variable("len", Some(&scope)));
|
||
|
|
assert!(!is_outer_scope_variable("unknown", Some(&scope)));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_is_outer_scope_variable_from_header_only() {
|
||
|
|
use std::collections::{BTreeMap, BTreeSet};
|
||
|
|
|
||
|
|
let mut variable_definitions = BTreeMap::new();
|
||
|
|
let mut header_only = BTreeSet::new();
|
||
|
|
header_only.insert(BasicBlockId(0)); // Only in header
|
||
|
|
|
||
|
|
variable_definitions.insert("start".to_string(), header_only);
|
||
|
|
|
||
|
|
let scope = LoopScopeShape {
|
||
|
|
header: BasicBlockId(0),
|
||
|
|
body: BasicBlockId(1),
|
||
|
|
latch: BasicBlockId(2),
|
||
|
|
exit: BasicBlockId(3),
|
||
|
|
pinned: BTreeSet::new(),
|
||
|
|
carriers: BTreeSet::new(),
|
||
|
|
body_locals: BTreeSet::new(),
|
||
|
|
exit_live: BTreeSet::new(),
|
||
|
|
progress_carrier: None,
|
||
|
|
variable_definitions,
|
||
|
|
};
|
||
|
|
|
||
|
|
assert!(is_outer_scope_variable("start", Some(&scope)));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_is_outer_scope_variable_from_body() {
|
||
|
|
use std::collections::{BTreeMap, BTreeSet};
|
||
|
|
|
||
|
|
let mut variable_definitions = BTreeMap::new();
|
||
|
|
let mut header_and_body = BTreeSet::new();
|
||
|
|
header_and_body.insert(BasicBlockId(0)); // header
|
||
|
|
header_and_body.insert(BasicBlockId(1)); // body
|
||
|
|
|
||
|
|
variable_definitions.insert("ch".to_string(), header_and_body);
|
||
|
|
|
||
|
|
let scope = LoopScopeShape {
|
||
|
|
header: BasicBlockId(0),
|
||
|
|
body: BasicBlockId(1),
|
||
|
|
latch: BasicBlockId(2),
|
||
|
|
exit: BasicBlockId(3),
|
||
|
|
pinned: BTreeSet::new(),
|
||
|
|
carriers: BTreeSet::new(),
|
||
|
|
body_locals: BTreeSet::new(),
|
||
|
|
exit_live: BTreeSet::new(),
|
||
|
|
progress_carrier: None,
|
||
|
|
variable_definitions,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Variable defined in body (in addition to header) → NOT outer-only
|
||
|
|
assert!(!is_outer_scope_variable("ch", Some(&scope)));
|
||
|
|
}
|
||
|
|
}
|