method-ray 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,9 @@
3
3
  //! This module is responsible for:
4
4
  //! - Local variable read/write (x, x = value)
5
5
  //! - Instance variable read/write (@name, @name = value)
6
+ //! - Class variable read/write (@@name, @@name = value)
7
+ //! - Global variable read/write ($var, $var = value)
8
+ //! - Constant read/write (CONST = value, CONST)
6
9
  //! - self node handling
7
10
 
8
11
  use crate::env::{GlobalEnv, LocalEnv};
@@ -62,3 +65,224 @@ pub(crate) fn install_self(genv: &mut GlobalEnv) -> VertexId {
62
65
  genv.new_source(Type::instance("Object"))
63
66
  }
64
67
  }
68
+
69
+ /// Install class variable write: @@name = value
70
+ ///
71
+ /// If @@name already has a VertexId (e.g., from a previous assignment),
72
+ /// an edge is added from value_vtx to the existing vertex so types propagate.
73
+ /// Otherwise, value_vtx is registered directly as the cvar's VertexId.
74
+ pub(crate) fn install_class_var_write(
75
+ genv: &mut GlobalEnv,
76
+ cvar_name: String,
77
+ value_vtx: VertexId,
78
+ ) -> VertexId {
79
+ if let Some(existing_vtx) = genv.scope_manager.lookup_class_var(&cvar_name) {
80
+ genv.add_edge(value_vtx, existing_vtx);
81
+ existing_vtx
82
+ } else {
83
+ genv.scope_manager.set_class_var_in_class(cvar_name, value_vtx);
84
+ value_vtx
85
+ }
86
+ }
87
+
88
+ /// Install class variable read: @@name
89
+ pub(crate) fn install_class_var_read(genv: &GlobalEnv, cvar_name: &str) -> Option<VertexId> {
90
+ genv.scope_manager.lookup_class_var(cvar_name)
91
+ }
92
+
93
+ /// Install global variable write: $var = value
94
+ ///
95
+ /// Delegates to [`GlobalEnv::set_global_var`] for edge behavior details.
96
+ pub(crate) fn install_global_var_write(
97
+ genv: &mut GlobalEnv,
98
+ gvar_name: String,
99
+ value_vtx: VertexId,
100
+ ) -> VertexId {
101
+ genv.set_global_var(gvar_name, value_vtx)
102
+ }
103
+
104
+ /// Install global variable read: $var
105
+ pub(crate) fn install_global_var_read(genv: &GlobalEnv, gvar_name: &str) -> Option<VertexId> {
106
+ genv.get_global_var(gvar_name)
107
+ }
108
+
109
+ pub(crate) fn install_constant_write(
110
+ genv: &mut GlobalEnv,
111
+ const_name: String,
112
+ value_vtx: VertexId,
113
+ ) -> VertexId {
114
+ let key = match genv.scope_manager.current_qualified_name() {
115
+ Some(ns) => format!("{}::{}", ns, const_name),
116
+ None => const_name,
117
+ };
118
+ genv.set_constant(key, value_vtx)
119
+ }
120
+
121
+ pub(crate) fn install_constant_read(
122
+ genv: &GlobalEnv,
123
+ const_name: &str,
124
+ ) -> Option<VertexId> {
125
+ if let Some(ns) = genv.scope_manager.current_qualified_name() {
126
+ let mut current_ns = ns.as_str();
127
+ loop {
128
+ let key = format!("{}::{}", current_ns, const_name);
129
+ if let Some(vtx) = genv.get_constant(&key) {
130
+ return Some(vtx);
131
+ }
132
+ match current_ns.rfind("::") {
133
+ Some(pos) => current_ns = &current_ns[..pos],
134
+ None => break,
135
+ }
136
+ }
137
+ }
138
+ genv.get_constant(const_name)
139
+ }
140
+
141
+ #[cfg(test)]
142
+ mod tests {
143
+ use super::*;
144
+ use crate::types::Type;
145
+
146
+ fn setup_class_scope(genv: &mut GlobalEnv) {
147
+ genv.enter_class("TestClass".to_string(), None);
148
+ genv.enter_method("test_method".to_string());
149
+ }
150
+
151
+ #[test]
152
+ fn test_install_class_var_write_new() {
153
+ let mut genv = GlobalEnv::new();
154
+ setup_class_scope(&mut genv);
155
+
156
+ let value_vtx = genv.new_source(Type::integer());
157
+ let result_vtx = install_class_var_write(&mut genv, "@@count".to_string(), value_vtx);
158
+
159
+ assert_eq!(result_vtx, value_vtx);
160
+ }
161
+
162
+ #[test]
163
+ fn test_install_class_var_read_after_write() {
164
+ let mut genv = GlobalEnv::new();
165
+ setup_class_scope(&mut genv);
166
+
167
+ let value_vtx = genv.new_source(Type::integer());
168
+ install_class_var_write(&mut genv, "@@count".to_string(), value_vtx);
169
+
170
+ let read_vtx = install_class_var_read(&genv, "@@count");
171
+ assert_eq!(read_vtx, Some(value_vtx));
172
+ }
173
+
174
+ #[test]
175
+ fn test_install_class_var_read_undefined() {
176
+ let genv = GlobalEnv::new();
177
+ assert_eq!(install_class_var_read(&genv, "@@undefined"), None);
178
+ }
179
+
180
+ #[test]
181
+ fn test_install_class_var_write_twice_merges() {
182
+ let mut genv = GlobalEnv::new();
183
+ setup_class_scope(&mut genv);
184
+
185
+ let str_vtx = genv.new_source(Type::string());
186
+ let vtx1 = install_class_var_write(&mut genv, "@@var".to_string(), str_vtx);
187
+
188
+ let int_vtx = genv.new_source(Type::integer());
189
+ let vtx2 = install_class_var_write(&mut genv, "@@var".to_string(), int_vtx);
190
+
191
+ assert_eq!(vtx1, vtx2);
192
+ }
193
+
194
+ #[test]
195
+ fn test_global_var_write_and_read() {
196
+ let mut genv = GlobalEnv::new();
197
+ let value_vtx = genv.new_source(Type::instance("String"));
198
+ let result_vtx = install_global_var_write(&mut genv, "$config".to_string(), value_vtx);
199
+ assert_eq!(result_vtx, value_vtx);
200
+
201
+ let read_vtx = install_global_var_read(&genv, "$config");
202
+ assert_eq!(read_vtx, Some(value_vtx));
203
+ }
204
+
205
+ #[test]
206
+ fn test_global_var_read_unregistered() {
207
+ let genv = GlobalEnv::new();
208
+ let read_vtx = install_global_var_read(&genv, "$unknown");
209
+ assert_eq!(read_vtx, None);
210
+ }
211
+
212
+ #[test]
213
+ fn test_global_var_write_twice_returns_same_vertex() {
214
+ let mut genv = GlobalEnv::new();
215
+ let vtx1 = genv.new_source(Type::instance("String"));
216
+ let first = install_global_var_write(&mut genv, "$data".to_string(), vtx1);
217
+
218
+ let vtx2 = genv.new_source(Type::instance("Integer"));
219
+ let second = install_global_var_write(&mut genv, "$data".to_string(), vtx2);
220
+
221
+ assert_eq!(first, second);
222
+ assert_eq!(install_global_var_read(&genv, "$data"), Some(first));
223
+ }
224
+
225
+ #[test]
226
+ fn test_global_var_write_twice_propagates_via_vertex() {
227
+ let mut genv = GlobalEnv::new();
228
+ let var_vtx = genv.new_vertex();
229
+ install_global_var_write(&mut genv, "$data".to_string(), var_vtx);
230
+
231
+ let str_src = genv.new_source(Type::instance("String"));
232
+ install_global_var_write(&mut genv, "$data".to_string(), str_src);
233
+
234
+ let types = genv.get_receiver_types(var_vtx).unwrap();
235
+ assert!(types.contains(&Type::instance("String")));
236
+ }
237
+
238
+ #[test]
239
+ fn test_constant_write_twice_merges() {
240
+ let mut genv = GlobalEnv::new();
241
+ let str_vtx = genv.new_source(Type::string());
242
+ let vtx1 = install_constant_write(&mut genv, "VAL".to_string(), str_vtx);
243
+
244
+ let int_vtx = genv.new_source(Type::integer());
245
+ let vtx2 = install_constant_write(&mut genv, "VAL".to_string(), int_vtx);
246
+
247
+ assert_eq!(vtx1, vtx2);
248
+ }
249
+
250
+ #[test]
251
+ fn test_constant_read_undefined() {
252
+ let genv = GlobalEnv::new();
253
+ assert_eq!(install_constant_read(&genv, "UNDEFINED"), None);
254
+ }
255
+
256
+ #[test]
257
+ fn test_constant_read_nested_namespace_walk() {
258
+ let mut genv = GlobalEnv::new();
259
+
260
+ genv.enter_class("Api".to_string(), None);
261
+ let api_vtx = genv.new_source(Type::string());
262
+ install_constant_write(&mut genv, "VERSION".to_string(), api_vtx);
263
+
264
+ genv.enter_class("V1".to_string(), None);
265
+ genv.enter_class("Users".to_string(), None);
266
+ genv.enter_method("index".to_string());
267
+
268
+ let read = install_constant_read(&genv, "VERSION");
269
+ assert_eq!(read, Some(api_vtx));
270
+ }
271
+
272
+ #[test]
273
+ fn test_constant_read_prefers_inner_namespace() {
274
+ let mut genv = GlobalEnv::new();
275
+
276
+ let top_vtx = genv.new_source(Type::string());
277
+ install_constant_write(&mut genv, "NAME".to_string(), top_vtx);
278
+
279
+ genv.enter_class("Config".to_string(), None);
280
+ let class_vtx = genv.new_source(Type::integer());
281
+ install_constant_write(&mut genv, "NAME".to_string(), class_vtx);
282
+
283
+ genv.enter_method("get_name".to_string());
284
+
285
+ let read = install_constant_read(&genv, "NAME");
286
+ assert_eq!(read, Some(class_vtx));
287
+ }
288
+ }
@@ -0,0 +1,24 @@
1
+ //! Yield statement handling
2
+
3
+ use crate::env::{GlobalEnv, LocalEnv};
4
+ use crate::graph::{ChangeSet, VertexId};
5
+
6
+ use super::install::install_node;
7
+
8
+ /// Process YieldNode: evaluate arguments for type checking, return unresolved vertex
9
+ pub(crate) fn process_yield_node(
10
+ genv: &mut GlobalEnv,
11
+ lenv: &mut LocalEnv,
12
+ changes: &mut ChangeSet,
13
+ source: &str,
14
+ yield_node: &ruby_prism::YieldNode,
15
+ ) -> Option<VertexId> {
16
+ // TODO: Connect argument vertices to block parameter types
17
+ if let Some(args) = yield_node.arguments() {
18
+ for arg in args.arguments().iter() {
19
+ install_node(genv, lenv, changes, source, &arg);
20
+ }
21
+ }
22
+
23
+ Some(genv.new_vertex())
24
+ }
@@ -46,6 +46,11 @@ pub struct GlobalEnv {
46
46
 
47
47
  /// Module extensions: class_name → Vec<module_name> (in extend order)
48
48
  module_extensions: HashMap<String, Vec<String>>,
49
+
50
+ /// Global variables: $var_name → VertexId
51
+ global_variables: HashMap<String, VertexId>,
52
+
53
+ constants: HashMap<String, VertexId>,
49
54
  }
50
55
 
51
56
  impl GlobalEnv {
@@ -59,6 +64,8 @@ impl GlobalEnv {
59
64
  module_inclusions: HashMap::new(),
60
65
  superclass_map: HashMap::new(),
61
66
  module_extensions: HashMap::new(),
67
+ global_variables: HashMap::new(),
68
+ constants: HashMap::new(),
62
69
  }
63
70
  }
64
71
 
@@ -234,6 +241,25 @@ impl GlobalEnv {
234
241
  );
235
242
  }
236
243
 
244
+ // ===== Global Variables =====
245
+
246
+ /// Set a global variable. If it already exists, add an edge from value_vtx
247
+ /// to the existing vertex so types propagate. Otherwise, register value_vtx directly.
248
+ pub fn set_global_var(&mut self, name: String, value_vtx: VertexId) -> VertexId {
249
+ if let Some(&existing_vtx) = self.global_variables.get(&name) {
250
+ self.add_edge(value_vtx, existing_vtx);
251
+ existing_vtx
252
+ } else {
253
+ self.global_variables.insert(name, value_vtx);
254
+ value_vtx
255
+ }
256
+ }
257
+
258
+ /// Get the vertex for a global variable, if it has been registered.
259
+ pub fn get_global_var(&self, name: &str) -> Option<VertexId> {
260
+ self.global_variables.get(name).copied()
261
+ }
262
+
237
263
  // ===== Type Errors =====
238
264
 
239
265
  /// Record a type error (undefined method)
@@ -247,6 +273,20 @@ impl GlobalEnv {
247
273
  .push(TypeError::new(receiver_type, method_name, location));
248
274
  }
249
275
 
276
+ pub fn set_constant(&mut self, key: String, value_vtx: VertexId) -> VertexId {
277
+ if let Some(&existing_vtx) = self.constants.get(&key) {
278
+ self.add_edge(value_vtx, existing_vtx);
279
+ existing_vtx
280
+ } else {
281
+ self.constants.insert(key, value_vtx);
282
+ value_vtx
283
+ }
284
+ }
285
+
286
+ pub fn get_constant(&self, key: &str) -> Option<VertexId> {
287
+ self.constants.get(key).copied()
288
+ }
289
+
250
290
  // ===== Scope Management =====
251
291
 
252
292
  /// Register a constant (simple name → qualified name) in the parent scope
@@ -320,6 +360,16 @@ impl GlobalEnv {
320
360
  scope_id
321
361
  }
322
362
 
363
+ /// Enter a lambda scope
364
+ pub fn enter_lambda(&mut self) -> (ScopeId, VertexId) {
365
+ let merge_vtx = self.new_vertex();
366
+ let scope_id = self.scope_manager.new_scope(ScopeKind::Lambda {
367
+ return_vertex: Some(merge_vtx),
368
+ });
369
+ self.scope_manager.enter_scope(scope_id);
370
+ (scope_id, merge_vtx)
371
+ }
372
+
323
373
  /// Exit current scope
324
374
  pub fn exit_scope(&mut self) {
325
375
  self.scope_manager.exit_scope();
@@ -22,6 +22,9 @@ pub enum ScopeKind {
22
22
  return_vertex: Option<VertexId>, // Merge vertex for return statements
23
23
  },
24
24
  Block,
25
+ Lambda {
26
+ return_vertex: Option<VertexId>,
27
+ },
25
28
  }
26
29
 
27
30
  /// Scope information
@@ -76,6 +79,16 @@ impl Scope {
76
79
  pub fn get_instance_var(&self, name: &str) -> Option<VertexId> {
77
80
  self.instance_vars.get(name).copied()
78
81
  }
82
+
83
+ /// Add class variable
84
+ pub fn set_class_var(&mut self, name: String, vtx: VertexId) {
85
+ self.class_vars.insert(name, vtx);
86
+ }
87
+
88
+ /// Get class variable
89
+ pub fn get_class_var(&self, name: &str) -> Option<VertexId> {
90
+ self.class_vars.get(name).copied()
91
+ }
79
92
  }
80
93
 
81
94
  /// Scope manager
@@ -267,14 +280,12 @@ impl ScopeManager {
267
280
  })
268
281
  }
269
282
 
270
- /// Get return_vertex from the nearest enclosing method scope
283
+ /// Get return_vertex from the nearest enclosing method or lambda scope
271
284
  pub fn current_method_return_vertex(&self) -> Option<VertexId> {
272
- self.walk_scopes().find_map(|scope| {
273
- if let ScopeKind::Method { return_vertex, .. } = &scope.kind {
274
- *return_vertex
275
- } else {
276
- None
277
- }
285
+ self.walk_scopes().find_map(|scope| match &scope.kind {
286
+ ScopeKind::Method { return_vertex, .. } => *return_vertex,
287
+ ScopeKind::Lambda { return_vertex } => *return_vertex,
288
+ _ => None,
278
289
  })
279
290
  }
280
291
 
@@ -296,4 +307,77 @@ impl ScopeManager {
296
307
  }
297
308
  }
298
309
  }
310
+
311
+ /// Lookup class variable in enclosing class scope.
312
+ ///
313
+ /// Note: Only searches `ScopeKind::Class` scopes. Module-scoped @@var and
314
+ /// inheritance-chain traversal are not supported in v0.2.0.
315
+ pub fn lookup_class_var(&self, name: &str) -> Option<VertexId> {
316
+ self.walk_scopes()
317
+ .find(|scope| matches!(&scope.kind, ScopeKind::Class { .. }))
318
+ .and_then(|scope| scope.get_class_var(name))
319
+ }
320
+
321
+ /// Set class variable in enclosing class scope.
322
+ ///
323
+ /// No-op if there is no enclosing `ScopeKind::Class` scope (e.g., top-level or module scope).
324
+ /// Module-scoped @@var support is planned for a future version.
325
+ pub fn set_class_var_in_class(&mut self, name: String, vtx: VertexId) {
326
+ let class_scope_id = self.walk_scopes()
327
+ .find(|scope| matches!(&scope.kind, ScopeKind::Class { .. }))
328
+ .map(|scope| scope.id);
329
+ if let Some(scope_id) = class_scope_id {
330
+ if let Some(scope) = self.scopes.get_mut(&scope_id) {
331
+ scope.set_class_var(name, vtx);
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ #[cfg(test)]
338
+ mod tests {
339
+ use super::*;
340
+
341
+ #[test]
342
+ fn test_class_var_set_and_get() {
343
+ let mut scope = Scope::new(ScopeId(0), ScopeKind::TopLevel, None);
344
+ let vtx = VertexId(1);
345
+ scope.set_class_var("@@count".to_string(), vtx);
346
+ assert_eq!(scope.get_class_var("@@count"), Some(vtx));
347
+ assert_eq!(scope.get_class_var("@@missing"), None);
348
+ }
349
+
350
+ #[test]
351
+ fn test_lookup_class_var_from_method_scope() {
352
+ let mut manager = ScopeManager::new();
353
+
354
+ // Class scope
355
+ let class_id = manager.new_scope(ScopeKind::Class {
356
+ name: "Counter".to_string(),
357
+ superclass: None,
358
+ });
359
+ manager.enter_scope(class_id);
360
+
361
+ let vtx = VertexId(42);
362
+ manager.set_class_var_in_class("@@count".to_string(), vtx);
363
+
364
+ // Method scope (inside the class)
365
+ let method_id = manager.new_scope(ScopeKind::Method {
366
+ name: "increment".to_string(),
367
+ receiver_type: Some("Counter".to_string()),
368
+ return_vertex: None,
369
+ });
370
+ manager.enter_scope(method_id);
371
+
372
+ // Should find @@count through the class scope
373
+ assert_eq!(manager.lookup_class_var("@@count"), Some(vtx));
374
+ }
375
+
376
+ #[test]
377
+ fn test_set_class_var_noop_without_class_scope() {
378
+ let mut manager = ScopeManager::new();
379
+ // At top-level, no class scope exists
380
+ manager.set_class_var_in_class("@@var".to_string(), VertexId(1));
381
+ assert_eq!(manager.lookup_class_var("@@var"), None);
382
+ }
299
383
  }
@@ -112,6 +112,17 @@ impl MethodCallBox {
112
112
  return;
113
113
  }
114
114
 
115
+ if let Type::Proc { return_vertex, param_vertices, .. } = recv_ty {
116
+ if self.method_name == "call" {
117
+ if let Some(merge_vtx) = return_vertex {
118
+ changes.add_edge(*merge_vtx, self.ret);
119
+ }
120
+ propagate_arguments(&self.arg_vtxs, Some(param_vertices), changes);
121
+ }
122
+ // TODO: Proc#arity, Proc#curry etc. not yet resolved via RBS
123
+ return;
124
+ }
125
+
115
126
  if let Some(method_info) = genv.resolve_method(recv_ty, &self.method_name) {
116
127
  if let Some(return_vtx) = method_info.return_vertex {
117
128
  // User-defined method: connect body's return vertex to call site
data/core/src/types.rs CHANGED
@@ -141,6 +141,10 @@ pub enum Type {
141
141
  Union(Vec<Type>),
142
142
  /// Bottom type: no type information
143
143
  Bot,
144
+ Proc {
145
+ return_vertex: Option<crate::graph::VertexId>,
146
+ param_vertices: Vec<crate::graph::VertexId>,
147
+ },
144
148
  }
145
149
 
146
150
  impl Type {
@@ -159,6 +163,10 @@ impl Type {
159
163
  names.join(" | ")
160
164
  }
161
165
  Type::Bot => "untyped".to_string(),
166
+ Type::Proc { param_vertices, .. } => {
167
+ let params = vec!["untyped"; param_vertices.len()];
168
+ format!("Proc[({})->untyped]", params.join(", "))
169
+ }
162
170
  }
163
171
  }
164
172
 
@@ -277,6 +285,16 @@ impl Type {
277
285
  }
278
286
  }
279
287
 
288
+ pub fn proc_type_with_vertex(
289
+ return_vertex: crate::graph::VertexId,
290
+ param_vertices: Vec<crate::graph::VertexId>,
291
+ ) -> Self {
292
+ Type::Proc {
293
+ return_vertex: Some(return_vertex),
294
+ param_vertices,
295
+ }
296
+ }
297
+
280
298
  /// Collapse a Vec<Type> into a single Type or Union.
281
299
  /// Returns the single element if len==1, or Union if len>1.
282
300
  /// Panics if the vec is empty.
@@ -417,4 +435,20 @@ mod tests {
417
435
  assert_eq!(singleton.show(), "singleton(Api::User)");
418
436
  assert_eq!(singleton.base_class_name(), Some("Api::User"));
419
437
  }
438
+
439
+ #[test]
440
+ fn test_proc_type_show() {
441
+ let proc_ty = Type::Proc {
442
+ return_vertex: None,
443
+ param_vertices: vec![crate::graph::VertexId(99)],
444
+ };
445
+ assert_eq!(proc_ty.show(), "Proc[(untyped)->untyped]");
446
+
447
+ let proc_no_params = Type::Proc {
448
+ return_vertex: None,
449
+ param_vertices: vec![],
450
+ };
451
+ assert_eq!(proc_no_params.show(), "Proc[()->untyped]");
452
+ }
453
+
420
454
  }
data/ext/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray"
3
- version = "0.1.10"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MethodRay
4
- VERSION = '0.1.10'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: method-ray
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dak2
@@ -43,11 +43,13 @@ files:
43
43
  - core/src/analyzer/attributes.rs
44
44
  - core/src/analyzer/blocks.rs
45
45
  - core/src/analyzer/calls.rs
46
+ - core/src/analyzer/compound_assignments.rs
46
47
  - core/src/analyzer/conditionals.rs
47
48
  - core/src/analyzer/definitions.rs
48
49
  - core/src/analyzer/dispatch.rs
49
50
  - core/src/analyzer/exceptions.rs
50
51
  - core/src/analyzer/install.rs
52
+ - core/src/analyzer/lambdas.rs
51
53
  - core/src/analyzer/literals.rs
52
54
  - core/src/analyzer/loops.rs
53
55
  - core/src/analyzer/mod.rs
@@ -57,6 +59,7 @@ files:
57
59
  - core/src/analyzer/returns.rs
58
60
  - core/src/analyzer/super_calls.rs
59
61
  - core/src/analyzer/variables.rs
62
+ - core/src/analyzer/yields.rs
60
63
  - core/src/cache/mod.rs
61
64
  - core/src/cache/rbs_cache.rs
62
65
  - core/src/checker.rs