Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions core/src/analyzer/compound_assignments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//! Compound assignment handlers (`x += 1`, `x ||= val`, `x &&= val`).
//!
//! `||=` and `&&=` use the same Union(existing, val) approximation — a sound
//! over-approximation without condition narrowing.

use crate::env::{GlobalEnv, LocalEnv};
use crate::graph::{ChangeSet, VertexId};
use crate::types::Type;

use super::calls::install_method_call;
use super::variables::{
install_class_var_read, install_class_var_write, install_constant_read, install_constant_write,
install_global_var_read, install_global_var_write, install_ivar_read, install_ivar_write,
install_local_var_read, install_local_var_write,
};

pub(crate) enum CompoundVarKind {
Local(String),
Ivar(String),
ClassVar(String),
GlobalVar(String),
Constant(String),
}

impl CompoundVarKind {
fn read(&self, genv: &GlobalEnv, lenv: &LocalEnv) -> Option<VertexId> {
match self {
Self::Local(name) => install_local_var_read(lenv, name),
Self::Ivar(name) => install_ivar_read(genv, name),
Self::ClassVar(name) => install_class_var_read(genv, name),
Self::GlobalVar(name) => install_global_var_read(genv, name),
Self::Constant(name) => install_constant_read(genv, name),
}
}

fn write(
self,
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
changes: &mut ChangeSet,
value_vtx: VertexId,
) -> VertexId {
match self {
Self::Local(name) => install_local_var_write(genv, lenv, changes, name, value_vtx),
Self::Ivar(name) => install_ivar_write(genv, name, value_vtx),
Self::ClassVar(name) => install_class_var_write(genv, name, value_vtx),
Self::GlobalVar(name) => install_global_var_write(genv, name, value_vtx),
Self::Constant(name) => install_constant_write(genv, name, value_vtx),
}
}
}

pub(crate) enum CompoundOp {
Operator(String),
Logical,
}

pub(crate) fn process_compound_write(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
changes: &mut ChangeSet,
var_kind: CompoundVarKind,
op: CompoundOp,
value_vtx: VertexId,
) -> VertexId {
match op {
CompoundOp::Operator(operator) => {
let current_vtx = var_kind.read(genv, lenv)
.unwrap_or_else(|| genv.new_source(Type::Nil));
let result_vtx = install_method_call(
genv, current_vtx, operator, vec![value_vtx], None, None, false,
);
var_kind.write(genv, lenv, changes, result_vtx)
}
CompoundOp::Logical => {
let merge_vtx = genv.new_vertex();
if let Some(current_vtx) = var_kind.read(genv, lenv) {
genv.add_edge(current_vtx, merge_vtx);
}
genv.add_edge(value_vtx, merge_vtx);
var_kind.write(genv, lenv, changes, merge_vtx)
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_operator_write() {
let mut genv = GlobalEnv::new();
let mut lenv = LocalEnv::new();
let mut changes = ChangeSet::new();

let int_vtx = genv.new_source(Type::integer());
install_local_var_write(&mut genv, &mut lenv, &mut changes, "x".to_string(), int_vtx);

let rhs_vtx = genv.new_source(Type::integer());
let result = process_compound_write(
&mut genv, &mut lenv, &mut changes,
CompoundVarKind::Local("x".to_string()),
CompoundOp::Operator("+".to_string()),
rhs_vtx,
);

assert_ne!(result, int_vtx);
assert_eq!(install_local_var_read(&lenv, "x"), Some(result));
}

#[test]
fn test_operator_write_uninitialized() {
let mut genv = GlobalEnv::new();
let mut lenv = LocalEnv::new();
let mut changes = ChangeSet::new();

let rhs_vtx = genv.new_source(Type::integer());
let result = process_compound_write(
&mut genv, &mut lenv, &mut changes,
CompoundVarKind::Local("x".to_string()),
CompoundOp::Operator("+".to_string()),
rhs_vtx,
);

assert_eq!(install_local_var_read(&lenv, "x"), Some(result));
}

#[test]
fn test_logical_write_produces_union() {
let mut genv = GlobalEnv::new();
let mut lenv = LocalEnv::new();
let mut changes = ChangeSet::new();

let str_vtx = genv.new_source(Type::string());
install_local_var_write(&mut genv, &mut lenv, &mut changes, "x".to_string(), str_vtx);

let int_vtx = genv.new_source(Type::integer());
let result = process_compound_write(
&mut genv, &mut lenv, &mut changes,
CompoundVarKind::Local("x".to_string()),
CompoundOp::Logical,
int_vtx,
);

assert_eq!(install_local_var_read(&lenv, "x"), Some(result));
genv.apply_changes(changes);
genv.run_all();
let types = genv.get_receiver_types(result).unwrap();
assert!(types.contains(&Type::string()));
assert!(types.contains(&Type::integer()));
}

#[test]
fn test_logical_write_uninitialized() {
let mut genv = GlobalEnv::new();
let mut lenv = LocalEnv::new();
let mut changes = ChangeSet::new();

let str_vtx = genv.new_source(Type::string());
let result = process_compound_write(
&mut genv, &mut lenv, &mut changes,
CompoundVarKind::Local("x".to_string()),
CompoundOp::Logical,
str_vtx,
);

genv.apply_changes(changes);
genv.run_all();
let types = genv.get_receiver_types(result).unwrap();
assert!(types.contains(&Type::string()));
}

#[test]
fn test_ivar_compound_write() {
let mut genv = GlobalEnv::new();
let mut lenv = LocalEnv::new();
let mut changes = ChangeSet::new();
genv.enter_class("TestClass".to_string(), None);
genv.enter_method("test_method".to_string());

let int_vtx = genv.new_source(Type::integer());
install_ivar_write(&mut genv, "@count".to_string(), int_vtx);

let rhs_vtx = genv.new_source(Type::integer());
let result = process_compound_write(
&mut genv, &mut lenv, &mut changes,
CompoundVarKind::Ivar("@count".to_string()),
CompoundOp::Operator("+".to_string()),
rhs_vtx,
);

assert_eq!(result, int_vtx);
}
}
52 changes: 52 additions & 0 deletions core/src/analyzer/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use ruby_prism::Node;

use super::bytes_to_name;
use super::calls::install_method_call;
use super::compound_assignments::{CompoundOp, CompoundVarKind};
use super::variables::{
install_class_var_read, install_class_var_write, install_constant_read, install_constant_write,
install_global_var_read, install_global_var_write, install_ivar_read, install_ivar_write,
Expand Down Expand Up @@ -85,6 +86,11 @@ pub(crate) enum NeedsChildKind<'a> {
/// Global variable write: need to process value, then call install_global_var_write
GlobalVarWrite { gvar_name: String, value: Node<'a> },
ConstantWrite { const_name: String, value: Node<'a> },
CompoundWrite {
var_kind: CompoundVarKind,
op: CompoundOp,
value: Node<'a>,
},
/// Proc/lambda literals
ProcLiteral { block: Node<'a> },
/// Method call: need to process receiver, then call finish_method_call
Expand Down Expand Up @@ -281,6 +287,46 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
});
}

macro_rules! dispatch_compound {
($node:expr, $as_method:ident, $var_kind:ident, operator) => {
if let Some(n) = $node.$as_method() {
return Some(NeedsChildKind::CompoundWrite {
var_kind: CompoundVarKind::$var_kind(bytes_to_name(n.name().as_slice())),
op: CompoundOp::Operator(bytes_to_name(n.binary_operator().as_slice())),
value: n.value(),
});
}
};
($node:expr, $as_method:ident, $var_kind:ident, $op:ident) => {
if let Some(n) = $node.$as_method() {
return Some(NeedsChildKind::CompoundWrite {
var_kind: CompoundVarKind::$var_kind(bytes_to_name(n.name().as_slice())),
op: CompoundOp::$op,
value: n.value(),
});
}
};
}

macro_rules! dispatch_compound_all {
($node:expr, $op_method:ident, $or_method:ident, $and_method:ident, $var_kind:ident) => {
dispatch_compound!($node, $op_method, $var_kind, operator);
dispatch_compound!($node, $or_method, $var_kind, Logical);
dispatch_compound!($node, $and_method, $var_kind, Logical);
};
}

dispatch_compound_all!(node,
as_local_variable_operator_write_node, as_local_variable_or_write_node, as_local_variable_and_write_node, Local);
dispatch_compound_all!(node,
as_instance_variable_operator_write_node, as_instance_variable_or_write_node, as_instance_variable_and_write_node, Ivar);
dispatch_compound_all!(node,
as_class_variable_operator_write_node, as_class_variable_or_write_node, as_class_variable_and_write_node, ClassVar);
dispatch_compound_all!(node,
as_global_variable_operator_write_node, as_global_variable_or_write_node, as_global_variable_and_write_node, GlobalVar);
dispatch_compound_all!(node,
as_constant_operator_write_node, as_constant_or_write_node, as_constant_and_write_node, Constant);

// Method call: x.upcase, x.each { |i| ... }, or name (implicit self)
if let Some(call_node) = node.as_call_node() {
let method_name = bytes_to_name(call_node.name().as_slice());
Expand Down Expand Up @@ -403,6 +449,12 @@ pub(crate) fn process_needs_child(
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
Some(install_local_var_write(genv, lenv, changes, var_name, value_vtx))
}
NeedsChildKind::CompoundWrite { var_kind, op, value } => {
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
Some(super::compound_assignments::process_compound_write(
genv, lenv, changes, var_kind, op, value_vtx,
))
}
NeedsChildKind::ProcLiteral { block } => {
if let Some(block_node) = block.as_block_node() {
super::lambdas::process_block_as_proc(genv, lenv, changes, source, &block_node)
Expand Down
1 change: 1 addition & 0 deletions core/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod assignments;
mod attributes;
mod blocks;
mod calls;
mod compound_assignments;
mod conditionals;
mod definitions;
mod exceptions;
Expand Down
Loading