Skip to content

Commit 419dc17

Browse files
authored
Merge pull request #100 from dak2/fix/global-variable-review-improvements
Add global variable ($var) type tracking
2 parents abc79d9 + 9f4f611 commit 419dc17

4 files changed

Lines changed: 233 additions & 2 deletions

File tree

core/src/analyzer/dispatch.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ use ruby_prism::Node;
1414
use super::bytes_to_name;
1515
use super::calls::install_method_call;
1616
use super::variables::{
17-
install_class_var_read, install_class_var_write, install_ivar_read, install_ivar_write,
18-
install_local_var_read, install_local_var_write, install_self,
17+
install_class_var_read, install_class_var_write, install_global_var_read,
18+
install_global_var_write, install_ivar_read, install_ivar_write, install_local_var_read,
19+
install_local_var_write, install_self,
1920
};
2021

2122
/// Collect positional and keyword arguments from AST argument nodes.
@@ -81,6 +82,8 @@ pub(crate) enum NeedsChildKind<'a> {
8182
ClassVarWrite { cvar_name: String, value: Node<'a> },
8283
/// Local variable write: need to process value, then call finish_local_var_write
8384
LocalVarWrite { var_name: String, value: Node<'a> },
85+
/// Global variable write: need to process value, then call install_global_var_write
86+
GlobalVarWrite { gvar_name: String, value: Node<'a> },
8487
/// Method call: need to process receiver, then call finish_method_call
8588
MethodCall {
8689
receiver: Node<'a>,
@@ -142,6 +145,19 @@ pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &
142145
};
143146
}
144147

148+
// Global variable read: $name
149+
// Ruby: uninitialized global variables are nil.
150+
// Register the nil vertex so repeated reads of the same uninitialized
151+
// variable reuse one vertex instead of allocating a new Source each time.
152+
if let Some(gvar_read) = node.as_global_variable_read_node() {
153+
let gvar_name = bytes_to_name(gvar_read.name().as_slice());
154+
let vtx = install_global_var_read(genv, &gvar_name).unwrap_or_else(|| {
155+
let nil_vtx = genv.new_source(Type::Nil);
156+
install_global_var_write(genv, gvar_name, nil_vtx)
157+
});
158+
return DispatchResult::Vertex(vtx);
159+
}
160+
145161
// self
146162
if node.as_self_node().is_some() {
147163
return DispatchResult::Vertex(install_self(genv));
@@ -226,6 +242,15 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
226242
});
227243
}
228244

245+
// Global variable write: $name = value
246+
if let Some(gvar_write) = node.as_global_variable_write_node() {
247+
let gvar_name = bytes_to_name(gvar_write.name().as_slice());
248+
return Some(NeedsChildKind::GlobalVarWrite {
249+
gvar_name,
250+
value: gvar_write.value(),
251+
});
252+
}
253+
229254
// Local variable write: x = value
230255
if let Some(write_node) = node.as_local_variable_write_node() {
231256
let var_name = bytes_to_name(write_node.name().as_slice());
@@ -329,6 +354,10 @@ pub(crate) fn process_needs_child(
329354
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
330355
Some(install_class_var_write(genv, cvar_name, value_vtx))
331356
}
357+
NeedsChildKind::GlobalVarWrite { gvar_name, value } => {
358+
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
359+
Some(install_global_var_write(genv, gvar_name, value_vtx))
360+
}
332361
NeedsChildKind::LocalVarWrite { var_name, value } => {
333362
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
334363
Some(finish_local_var_write(genv, lenv, changes, var_name, value_vtx))

core/src/analyzer/variables.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! - Local variable read/write (x, x = value)
55
//! - Instance variable read/write (@name, @name = value)
66
//! - Class variable read/write (@@name, @@name = value)
7+
//! - Global variable read/write ($var, $var = value)
78
//! - self node handling
89
910
use crate::env::{GlobalEnv, LocalEnv};
@@ -88,6 +89,22 @@ pub(crate) fn install_class_var_read(genv: &GlobalEnv, cvar_name: &str) -> Optio
8889
genv.scope_manager.lookup_class_var(cvar_name)
8990
}
9091

92+
/// Install global variable write: $var = value
93+
///
94+
/// Delegates to [`GlobalEnv::set_global_var`] for edge behavior details.
95+
pub(crate) fn install_global_var_write(
96+
genv: &mut GlobalEnv,
97+
gvar_name: String,
98+
value_vtx: VertexId,
99+
) -> VertexId {
100+
genv.set_global_var(gvar_name, value_vtx)
101+
}
102+
103+
/// Install global variable read: $var
104+
pub(crate) fn install_global_var_read(genv: &GlobalEnv, gvar_name: &str) -> Option<VertexId> {
105+
genv.get_global_var(gvar_name)
106+
}
107+
91108
#[cfg(test)]
92109
mod tests {
93110
use super::*;
@@ -141,4 +158,52 @@ mod tests {
141158
// Second write returns the same VertexId (edge added, not overwritten)
142159
assert_eq!(vtx1, vtx2);
143160
}
161+
162+
#[test]
163+
fn test_global_var_write_and_read() {
164+
let mut genv = GlobalEnv::new();
165+
let value_vtx = genv.new_source(Type::instance("String"));
166+
let result_vtx = install_global_var_write(&mut genv, "$config".to_string(), value_vtx);
167+
assert_eq!(result_vtx, value_vtx);
168+
169+
let read_vtx = install_global_var_read(&genv, "$config");
170+
assert_eq!(read_vtx, Some(value_vtx));
171+
}
172+
173+
#[test]
174+
fn test_global_var_read_unregistered() {
175+
let genv = GlobalEnv::new();
176+
let read_vtx = install_global_var_read(&genv, "$unknown");
177+
assert_eq!(read_vtx, None);
178+
}
179+
180+
#[test]
181+
fn test_global_var_write_twice_returns_same_vertex() {
182+
let mut genv = GlobalEnv::new();
183+
let vtx1 = genv.new_source(Type::instance("String"));
184+
let first = install_global_var_write(&mut genv, "$data".to_string(), vtx1);
185+
186+
let vtx2 = genv.new_source(Type::instance("Integer"));
187+
let second = install_global_var_write(&mut genv, "$data".to_string(), vtx2);
188+
189+
// Both writes should return the same VertexId (the first one registered)
190+
assert_eq!(first, second);
191+
// Read should also return the first vertex
192+
assert_eq!(install_global_var_read(&genv, "$data"), Some(first));
193+
}
194+
195+
#[test]
196+
fn test_global_var_write_twice_propagates_via_vertex() {
197+
let mut genv = GlobalEnv::new();
198+
// Use a Vertex (not Source) as the initial value so type propagation works
199+
let var_vtx = genv.new_vertex();
200+
install_global_var_write(&mut genv, "$data".to_string(), var_vtx);
201+
202+
// Second write with a Source should propagate types into the Vertex
203+
let str_src = genv.new_source(Type::instance("String"));
204+
install_global_var_write(&mut genv, "$data".to_string(), str_src);
205+
206+
let types = genv.get_receiver_types(var_vtx).unwrap();
207+
assert!(types.contains(&Type::instance("String")));
208+
}
144209
}

core/src/env/global_env.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub struct GlobalEnv {
4646

4747
/// Module extensions: class_name → Vec<module_name> (in extend order)
4848
module_extensions: HashMap<String, Vec<String>>,
49+
50+
/// Global variables: $var_name → VertexId
51+
global_variables: HashMap<String, VertexId>,
4952
}
5053

5154
impl GlobalEnv {
@@ -59,6 +62,7 @@ impl GlobalEnv {
5962
module_inclusions: HashMap::new(),
6063
superclass_map: HashMap::new(),
6164
module_extensions: HashMap::new(),
65+
global_variables: HashMap::new(),
6266
}
6367
}
6468

@@ -234,6 +238,25 @@ impl GlobalEnv {
234238
);
235239
}
236240

241+
// ===== Global Variables =====
242+
243+
/// Set a global variable. If it already exists, add an edge from value_vtx
244+
/// to the existing vertex so types propagate. Otherwise, register value_vtx directly.
245+
pub fn set_global_var(&mut self, name: String, value_vtx: VertexId) -> VertexId {
246+
if let Some(&existing_vtx) = self.global_variables.get(&name) {
247+
self.add_edge(value_vtx, existing_vtx);
248+
existing_vtx
249+
} else {
250+
self.global_variables.insert(name, value_vtx);
251+
value_vtx
252+
}
253+
}
254+
255+
/// Get the vertex for a global variable, if it has been registered.
256+
pub fn get_global_var(&self, name: &str) -> Option<VertexId> {
257+
self.global_variables.get(name).copied()
258+
}
259+
237260
// ===== Type Errors =====
238261

239262
/// Record a type error (undefined method)

test/global_variable_test.rb

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
5+
class GlobalVariableTest < Minitest::Test
6+
include CLITestHelper
7+
8+
# ============================================
9+
# No Error
10+
# ============================================
11+
12+
def test_global_variable_basic
13+
source = <<~RUBY
14+
$config = "production"
15+
$config.upcase
16+
RUBY
17+
assert_no_check_errors(source)
18+
end
19+
20+
def test_global_variable_type_error
21+
source = <<~RUBY
22+
$count = 42
23+
$count.upcase
24+
RUBY
25+
assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer')
26+
end
27+
28+
def test_global_variable_in_method
29+
source = <<~RUBY
30+
class App
31+
def setup
32+
$logger = "Logger instance"
33+
end
34+
35+
def run
36+
$logger.upcase
37+
end
38+
end
39+
RUBY
40+
assert_no_check_errors(source)
41+
end
42+
43+
# ============================================
44+
# Error Detection
45+
# ============================================
46+
47+
def test_global_variable_across_methods_type_error
48+
source = <<~RUBY
49+
class App
50+
def setup
51+
$counter = 0
52+
end
53+
54+
def process
55+
$counter.upcase
56+
end
57+
end
58+
RUBY
59+
assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer')
60+
end
61+
62+
def test_global_variable_top_level
63+
source = <<~RUBY
64+
$name = "Alice"
65+
$age = 30
66+
67+
def greet
68+
$name.upcase
69+
end
70+
71+
def show_age
72+
$age.upcase
73+
end
74+
RUBY
75+
assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer')
76+
end
77+
78+
def test_global_variable_type_error_in_same_method
79+
source = <<~RUBY
80+
def run
81+
$value = 100
82+
$value.upcase
83+
end
84+
RUBY
85+
assert_check_error(source, method_name: 'upcase', receiver_type: 'Integer')
86+
end
87+
88+
def test_global_variable_across_classes
89+
source = <<~RUBY
90+
class Writer
91+
def write
92+
$shared = "data"
93+
end
94+
end
95+
96+
class Reader
97+
def read
98+
$shared.upcase
99+
end
100+
end
101+
RUBY
102+
assert_no_check_errors(source)
103+
end
104+
105+
def test_multiple_global_variables_independent
106+
source = <<~RUBY
107+
$name = "Alice"
108+
$count = 42
109+
$name.upcase
110+
$count.to_s
111+
RUBY
112+
assert_no_check_errors(source)
113+
end
114+
end

0 commit comments

Comments
 (0)