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
48 changes: 25 additions & 23 deletions core/src/analyzer/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use ruby_prism::Node;
use super::bytes_to_name;
use super::calls::install_method_call;
use super::variables::{
install_class_var_read, install_class_var_write, install_global_var_read,
install_global_var_write, install_ivar_read, install_ivar_write, install_local_var_read,
install_local_var_write, install_self,
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, install_self,
};

/// Collect positional and keyword arguments from AST argument nodes.
Expand Down Expand Up @@ -76,14 +76,15 @@ pub(crate) enum DispatchResult {

/// Kind of child processing needed
pub(crate) enum NeedsChildKind<'a> {
/// Instance variable write: need to process value, then call finish_ivar_write
/// Instance variable write: need to process value, then call install_ivar_write
IvarWrite { ivar_name: String, value: Node<'a> },
/// Class variable write: need to process value, then call install_class_var_write
ClassVarWrite { cvar_name: String, value: Node<'a> },
/// Local variable write: need to process value, then call finish_local_var_write
/// Local variable write: need to process value, then call install_local_var_write
LocalVarWrite { var_name: String, value: Node<'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> },
/// Method call: need to process receiver, then call finish_method_call
MethodCall {
receiver: Node<'a>,
Expand Down Expand Up @@ -180,6 +181,11 @@ pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &
// ConstantReadNode: User → Type::Singleton("User") or Type::Singleton("Api::User")
if let Some(const_read) = node.as_constant_read_node() {
let name = bytes_to_name(const_read.name().as_slice());

if let Some(vtx) = install_constant_read(genv, &name) {
return DispatchResult::Vertex(vtx);
}

let resolved_name = genv.scope_manager.lookup_constant(&name)
.unwrap_or(name);
let vtx = genv.new_source(Type::singleton(&resolved_name));
Expand Down Expand Up @@ -256,6 +262,14 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
});
}

if let Some(const_write) = node.as_constant_write_node() {
let const_name = bytes_to_name(const_write.name().as_slice());
return Some(NeedsChildKind::ConstantWrite {
const_name,
value: const_write.value(),
});
}

// Local variable write: x = value
if let Some(write_node) = node.as_local_variable_write_node() {
let var_name = bytes_to_name(write_node.name().as_slice());
Expand Down Expand Up @@ -353,7 +367,7 @@ pub(crate) fn process_needs_child(
match kind {
NeedsChildKind::IvarWrite { ivar_name, value } => {
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
Some(finish_ivar_write(genv, ivar_name, value_vtx))
Some(install_ivar_write(genv, ivar_name, value_vtx))
}
NeedsChildKind::ClassVarWrite { cvar_name, value } => {
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
Expand All @@ -363,9 +377,13 @@ pub(crate) fn process_needs_child(
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
Some(install_global_var_write(genv, gvar_name, value_vtx))
}
NeedsChildKind::ConstantWrite { const_name, value } => {
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
Some(install_constant_write(genv, const_name, value_vtx))
}
NeedsChildKind::LocalVarWrite { var_name, value } => {
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
Some(finish_local_var_write(genv, lenv, changes, var_name, value_vtx))
Some(install_local_var_write(genv, lenv, changes, var_name, value_vtx))
}
NeedsChildKind::MethodCall {
receiver,
Expand Down Expand Up @@ -419,22 +437,6 @@ pub(crate) fn process_needs_child(
}
}

/// Finish instance variable write after child is processed
fn finish_ivar_write(genv: &mut GlobalEnv, ivar_name: String, value_vtx: VertexId) -> VertexId {
install_ivar_write(genv, ivar_name, value_vtx)
}

/// Finish local variable write after child is processed
fn finish_local_var_write(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
changes: &mut ChangeSet,
var_name: String,
value_vtx: VertexId,
) -> VertexId {
install_local_var_write(genv, lenv, changes, var_name, value_vtx)
}

/// Bundled parameters for method call processing
struct MethodCallContext<'a> {
recv_vtx: VertexId,
Expand Down
89 changes: 84 additions & 5 deletions core/src/analyzer/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! - Instance variable read/write (@name, @name = value)
//! - Class variable read/write (@@name, @@name = value)
//! - Global variable read/write ($var, $var = value)
//! - Constant read/write (CONST = value, CONST)
//! - self node handling

use crate::env::{GlobalEnv, LocalEnv};
Expand Down Expand Up @@ -105,6 +106,38 @@ pub(crate) fn install_global_var_read(genv: &GlobalEnv, gvar_name: &str) -> Opti
genv.get_global_var(gvar_name)
}

pub(crate) fn install_constant_write(
genv: &mut GlobalEnv,
const_name: String,
value_vtx: VertexId,
) -> VertexId {
let key = match genv.scope_manager.current_qualified_name() {
Some(ns) => format!("{}::{}", ns, const_name),
None => const_name,
};
genv.set_constant(key, value_vtx)
}

pub(crate) fn install_constant_read(
genv: &GlobalEnv,
const_name: &str,
) -> Option<VertexId> {
if let Some(ns) = genv.scope_manager.current_qualified_name() {
let mut current_ns = ns.as_str();
loop {
let key = format!("{}::{}", current_ns, const_name);
if let Some(vtx) = genv.get_constant(&key) {
return Some(vtx);
}
match current_ns.rfind("::") {
Some(pos) => current_ns = &current_ns[..pos],
None => break,
}
}
}
genv.get_constant(const_name)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -155,7 +188,6 @@ mod tests {
let int_vtx = genv.new_source(Type::integer());
let vtx2 = install_class_var_write(&mut genv, "@@var".to_string(), int_vtx);

// Second write returns the same VertexId (edge added, not overwritten)
assert_eq!(vtx1, vtx2);
}

Expand Down Expand Up @@ -186,24 +218,71 @@ mod tests {
let vtx2 = genv.new_source(Type::instance("Integer"));
let second = install_global_var_write(&mut genv, "$data".to_string(), vtx2);

// Both writes should return the same VertexId (the first one registered)
assert_eq!(first, second);
// Read should also return the first vertex
assert_eq!(install_global_var_read(&genv, "$data"), Some(first));
}

#[test]
fn test_global_var_write_twice_propagates_via_vertex() {
let mut genv = GlobalEnv::new();
// Use a Vertex (not Source) as the initial value so type propagation works
let var_vtx = genv.new_vertex();
install_global_var_write(&mut genv, "$data".to_string(), var_vtx);

// Second write with a Source should propagate types into the Vertex
let str_src = genv.new_source(Type::instance("String"));
install_global_var_write(&mut genv, "$data".to_string(), str_src);

let types = genv.get_receiver_types(var_vtx).unwrap();
assert!(types.contains(&Type::instance("String")));
}

#[test]
fn test_constant_write_twice_merges() {
let mut genv = GlobalEnv::new();
let str_vtx = genv.new_source(Type::string());
let vtx1 = install_constant_write(&mut genv, "VAL".to_string(), str_vtx);

let int_vtx = genv.new_source(Type::integer());
let vtx2 = install_constant_write(&mut genv, "VAL".to_string(), int_vtx);

assert_eq!(vtx1, vtx2);
}

#[test]
fn test_constant_read_undefined() {
let genv = GlobalEnv::new();
assert_eq!(install_constant_read(&genv, "UNDEFINED"), None);
}

#[test]
fn test_constant_read_nested_namespace_walk() {
let mut genv = GlobalEnv::new();

genv.enter_class("Api".to_string(), None);
let api_vtx = genv.new_source(Type::string());
install_constant_write(&mut genv, "VERSION".to_string(), api_vtx);

genv.enter_class("V1".to_string(), None);
genv.enter_class("Users".to_string(), None);
genv.enter_method("index".to_string());

let read = install_constant_read(&genv, "VERSION");
assert_eq!(read, Some(api_vtx));
}

#[test]
fn test_constant_read_prefers_inner_namespace() {
let mut genv = GlobalEnv::new();

let top_vtx = genv.new_source(Type::string());
install_constant_write(&mut genv, "NAME".to_string(), top_vtx);

genv.enter_class("Config".to_string(), None);
let class_vtx = genv.new_source(Type::integer());
install_constant_write(&mut genv, "NAME".to_string(), class_vtx);

genv.enter_method("get_name".to_string());

let read = install_constant_read(&genv, "NAME");
assert_eq!(read, Some(class_vtx));
}
}
17 changes: 17 additions & 0 deletions core/src/env/global_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub struct GlobalEnv {

/// Global variables: $var_name → VertexId
global_variables: HashMap<String, VertexId>,

constants: HashMap<String, VertexId>,
}

impl GlobalEnv {
Expand All @@ -63,6 +65,7 @@ impl GlobalEnv {
superclass_map: HashMap::new(),
module_extensions: HashMap::new(),
global_variables: HashMap::new(),
constants: HashMap::new(),
}
}

Expand Down Expand Up @@ -270,6 +273,20 @@ impl GlobalEnv {
.push(TypeError::new(receiver_type, method_name, location));
}

pub fn set_constant(&mut self, key: String, value_vtx: VertexId) -> VertexId {
if let Some(&existing_vtx) = self.constants.get(&key) {
self.add_edge(value_vtx, existing_vtx);
existing_vtx
} else {
self.constants.insert(key, value_vtx);
value_vtx
}
}

pub fn get_constant(&self, key: &str) -> Option<VertexId> {
self.constants.get(key).copied()
}

// ===== Scope Management =====

/// Register a constant (simple name → qualified name) in the parent scope
Expand Down
88 changes: 88 additions & 0 deletions test/constant_write_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

require 'test_helper'

class ConstantWriteTest < Minitest::Test
include CLITestHelper

# ============================================
# No Error
# ============================================

def test_constant_basic_string
source = <<~RUBY
MESSAGE = "hello"
MESSAGE.upcase
RUBY
assert_no_check_errors(source)
end

def test_constant_basic_integer
source = <<~RUBY
MAX_SIZE = 100
MAX_SIZE.even?
RUBY
assert_no_check_errors(source)
end

def test_constant_type_error
source = <<~RUBY
MAX_SIZE = 100
MAX_SIZE.upcase
RUBY
assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer')
end

def test_constant_in_class
source = <<~RUBY
class Config
MAX_RETRIES = 3

def retry_count
MAX_RETRIES.to_s
end
end
RUBY
assert_no_check_errors(source)
end

# ============================================
# Error Detection
# ============================================

def test_constant_type_error_in_class_method
source = <<~RUBY
class Config
MAX_RETRIES = 3

def check
MAX_RETRIES.upcase
end
end
RUBY
assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer')
end

def test_top_level_constant_from_class
source = <<~RUBY
MAX = 100

class Foo
def limit
MAX.to_s
end
end
RUBY
assert_no_check_errors(source)
end

def test_multiple_independent_constants
source = <<~RUBY
MAX = 100
NAME = "Alice"
MAX.even?
NAME.upcase
RUBY
assert_no_check_errors(source)
end
end