method-ray 0.1.8 → 0.1.10

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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +9 -11
  4. data/{rust → core}/Cargo.toml +1 -1
  5. data/core/src/analyzer/assignments.rs +219 -0
  6. data/{rust → core}/src/analyzer/blocks.rs +0 -50
  7. data/{rust → core}/src/analyzer/calls.rs +3 -32
  8. data/core/src/analyzer/conditionals.rs +190 -0
  9. data/core/src/analyzer/definitions.rs +205 -0
  10. data/core/src/analyzer/dispatch.rs +455 -0
  11. data/core/src/analyzer/exceptions.rs +168 -0
  12. data/{rust → core}/src/analyzer/install.rs +16 -1
  13. data/{rust → core}/src/analyzer/literals.rs +3 -71
  14. data/core/src/analyzer/loops.rs +94 -0
  15. data/{rust → core}/src/analyzer/mod.rs +1 -15
  16. data/core/src/analyzer/operators.rs +79 -0
  17. data/{rust → core}/src/analyzer/parameters.rs +4 -67
  18. data/core/src/analyzer/parentheses.rs +25 -0
  19. data/core/src/analyzer/returns.rs +39 -0
  20. data/core/src/analyzer/super_calls.rs +74 -0
  21. data/{rust → core}/src/analyzer/variables.rs +5 -25
  22. data/{rust → core}/src/checker.rs +0 -13
  23. data/{rust → core}/src/diagnostics/diagnostic.rs +0 -41
  24. data/{rust → core}/src/diagnostics/formatter.rs +0 -38
  25. data/{rust → core}/src/env/box_manager.rs +0 -30
  26. data/{rust → core}/src/env/global_env.rs +67 -80
  27. data/core/src/env/local_env.rs +42 -0
  28. data/core/src/env/method_registry.rs +173 -0
  29. data/core/src/env/scope.rs +299 -0
  30. data/{rust → core}/src/env/vertex_manager.rs +0 -73
  31. data/core/src/graph/box.rs +347 -0
  32. data/{rust → core}/src/graph/change_set.rs +0 -65
  33. data/{rust → core}/src/graph/vertex.rs +0 -69
  34. data/{rust → core}/src/parser.rs +0 -77
  35. data/{rust → core}/src/types.rs +11 -0
  36. data/ext/Cargo.toml +2 -2
  37. data/lib/methodray/binary_locator.rb +2 -2
  38. data/lib/methodray/commands.rb +1 -1
  39. data/lib/methodray/version.rb +1 -1
  40. metadata +58 -56
  41. data/rust/src/analyzer/assignments.rs +0 -152
  42. data/rust/src/analyzer/conditionals.rs +0 -538
  43. data/rust/src/analyzer/definitions.rs +0 -719
  44. data/rust/src/analyzer/dispatch.rs +0 -1137
  45. data/rust/src/analyzer/exceptions.rs +0 -521
  46. data/rust/src/analyzer/loops.rs +0 -176
  47. data/rust/src/analyzer/operators.rs +0 -284
  48. data/rust/src/analyzer/parentheses.rs +0 -113
  49. data/rust/src/analyzer/returns.rs +0 -191
  50. data/rust/src/env/local_env.rs +0 -92
  51. data/rust/src/env/method_registry.rs +0 -268
  52. data/rust/src/env/scope.rs +0 -596
  53. data/rust/src/graph/box.rs +0 -766
  54. /data/{rust → core}/src/analyzer/attributes.rs +0 -0
  55. /data/{rust → core}/src/cache/mod.rs +0 -0
  56. /data/{rust → core}/src/cache/rbs_cache.rs +0 -0
  57. /data/{rust → core}/src/cli/args.rs +0 -0
  58. /data/{rust → core}/src/cli/commands.rs +0 -0
  59. /data/{rust → core}/src/cli/mod.rs +0 -0
  60. /data/{rust → core}/src/diagnostics/mod.rs +0 -0
  61. /data/{rust → core}/src/env/mod.rs +0 -0
  62. /data/{rust → core}/src/env/type_error.rs +0 -0
  63. /data/{rust → core}/src/graph/mod.rs +0 -0
  64. /data/{rust → core}/src/lib.rs +0 -0
  65. /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
  66. /data/{rust → core}/src/lsp/main.rs +0 -0
  67. /data/{rust → core}/src/lsp/mod.rs +0 -0
  68. /data/{rust → core}/src/lsp/server.rs +0 -0
  69. /data/{rust → core}/src/main.rs +0 -0
  70. /data/{rust → core}/src/rbs/converter.rs +0 -0
  71. /data/{rust → core}/src/rbs/error.rs +0 -0
  72. /data/{rust → core}/src/rbs/loader.rs +0 -0
  73. /data/{rust → core}/src/rbs/mod.rs +0 -0
  74. /data/{rust → core}/src/source_map.rs +0 -0
@@ -0,0 +1,168 @@
1
+ //! Exceptions - begin/rescue/ensure type inference
2
+ //!
3
+ //! Collects types from each branch and merges them into a Union
4
+ //! via edges into a single result Vertex.
5
+ //! Applies the same MergeVertex pattern as conditionals.rs.
6
+
7
+ use crate::env::{GlobalEnv, LocalEnv};
8
+ use crate::graph::{ChangeSet, VertexId};
9
+ use crate::types::Type;
10
+ use ruby_prism::{BeginNode, RescueModifierNode, RescueNode};
11
+
12
+ use super::bytes_to_name;
13
+ use super::install::{install_node, install_statements};
14
+
15
+ /// Process BeginNode: begin/rescue/else/ensure
16
+ ///
17
+ /// Type aggregation rules:
18
+ /// - No rescue clause: return begin body type directly
19
+ /// - With else clause: else type + all rescue types → Union (begin body excluded)
20
+ /// - Without else clause: begin body type + all rescue types → Union
21
+ /// - Ensure clause: processed for side effects only, does not affect return type
22
+ pub(crate) fn process_begin_node(
23
+ genv: &mut GlobalEnv,
24
+ lenv: &mut LocalEnv,
25
+ changes: &mut ChangeSet,
26
+ source: &str,
27
+ begin_node: &BeginNode,
28
+ ) -> Option<VertexId> {
29
+ let begin_vtx = begin_node
30
+ .statements()
31
+ .and_then(|s| install_statements(genv, lenv, changes, source, &s));
32
+
33
+ let result = if let Some(rescue_node) = begin_node.rescue_clause() {
34
+ let result_vtx = genv.new_vertex();
35
+
36
+ process_rescue_chain(genv, lenv, changes, source, &rescue_node, result_vtx);
37
+
38
+ if let Some(else_node) = begin_node.else_clause() {
39
+ // With else: else type replaces begin body type (Ruby spec)
40
+ let else_vtx = else_node
41
+ .statements()
42
+ .and_then(|s| install_statements(genv, lenv, changes, source, &s));
43
+ if let Some(vtx) = else_vtx {
44
+ genv.add_edge(vtx, result_vtx);
45
+ }
46
+ } else if let Some(vtx) = begin_vtx {
47
+ genv.add_edge(vtx, result_vtx);
48
+ }
49
+
50
+ Some(result_vtx)
51
+ } else {
52
+ begin_vtx
53
+ };
54
+
55
+ // Ensure: side effects only, does not affect return type
56
+ if let Some(ensure_node) = begin_node.ensure_clause() {
57
+ if let Some(stmts) = ensure_node.statements() {
58
+ let _ = install_statements(genv, lenv, changes, source, &stmts);
59
+ }
60
+ }
61
+
62
+ result
63
+ }
64
+
65
+ /// Process RescueNode chain recursively.
66
+ /// Empty rescue body evaluates to nil.
67
+ fn process_rescue_chain(
68
+ genv: &mut GlobalEnv,
69
+ lenv: &mut LocalEnv,
70
+ changes: &mut ChangeSet,
71
+ source: &str,
72
+ rescue_node: &RescueNode,
73
+ result_vtx: VertexId,
74
+ ) {
75
+ let body_vtx = process_rescue_body(genv, lenv, changes, source, rescue_node);
76
+ let vtx = body_vtx.unwrap_or_else(|| genv.new_source(Type::Nil));
77
+ genv.add_edge(vtx, result_vtx);
78
+
79
+ if let Some(next) = rescue_node.subsequent() {
80
+ process_rescue_chain(genv, lenv, changes, source, &next, result_vtx);
81
+ }
82
+ }
83
+
84
+ /// Extract the exception type from rescue node's exception class list.
85
+ /// Falls back to StandardError when no exceptions are specified or none can be resolved.
86
+ // TODO: Non-constant exception expressions (method calls, splats, variables) are silently skipped.
87
+ fn extract_exception_type(rescue_node: &RescueNode) -> Type {
88
+ let types: Vec<Type> = rescue_node
89
+ .exceptions()
90
+ .iter()
91
+ .filter_map(|exc| super::definitions::extract_constant_path(&exc))
92
+ .map(|name| Type::instance(&name))
93
+ .collect();
94
+
95
+ if types.is_empty() {
96
+ Type::instance("StandardError")
97
+ } else {
98
+ Type::union_of(types)
99
+ }
100
+ }
101
+
102
+ /// Process a single RescueNode body.
103
+ /// Registers the rescue variable (=> e), processes the body,
104
+ /// then removes the variable from scope.
105
+ fn process_rescue_body(
106
+ genv: &mut GlobalEnv,
107
+ lenv: &mut LocalEnv,
108
+ changes: &mut ChangeSet,
109
+ source: &str,
110
+ rescue_node: &RescueNode,
111
+ ) -> Option<VertexId> {
112
+ for exc in &rescue_node.exceptions() {
113
+ install_node(genv, lenv, changes, source, &exc);
114
+ }
115
+
116
+ // Save/restore rescue variable binding (=> e)
117
+ // TODO: Only LocalVariableTargetNode is handled; instance/global/class vars are not yet supported.
118
+ let var_binding = if let Some(ref_node) = rescue_node.reference() {
119
+ ref_node.as_local_variable_target_node().map(|target| {
120
+ let name = bytes_to_name(target.name().as_slice());
121
+ let saved = lenv.get_var(&name);
122
+ let exception_vtx = genv.new_vertex();
123
+ let exception_type = extract_exception_type(rescue_node);
124
+ let exception_src = genv.new_source(exception_type);
125
+ genv.add_edge(exception_src, exception_vtx);
126
+ lenv.new_var(name.clone(), exception_vtx);
127
+ (name, saved)
128
+ })
129
+ } else {
130
+ None
131
+ };
132
+
133
+ let body_vtx = rescue_node
134
+ .statements()
135
+ .and_then(|s| install_statements(genv, lenv, changes, source, &s));
136
+
137
+ if let Some((name, saved)) = var_binding {
138
+ match saved {
139
+ Some(prev_vtx) => lenv.new_var(name, prev_vtx),
140
+ None => lenv.remove_var(&name),
141
+ }
142
+ }
143
+
144
+ body_vtx
145
+ }
146
+
147
+ /// Process RescueModifierNode: `expression rescue rescue_expression`
148
+ pub(crate) fn process_rescue_modifier_node(
149
+ genv: &mut GlobalEnv,
150
+ lenv: &mut LocalEnv,
151
+ changes: &mut ChangeSet,
152
+ source: &str,
153
+ node: &RescueModifierNode,
154
+ ) -> Option<VertexId> {
155
+ let result_vtx = genv.new_vertex();
156
+
157
+ let expr_vtx = install_node(genv, lenv, changes, source, &node.expression());
158
+ if let Some(vtx) = expr_vtx {
159
+ genv.add_edge(vtx, result_vtx);
160
+ }
161
+
162
+ let rescue_vtx = install_node(genv, lenv, changes, source, &node.rescue_expression());
163
+ if let Some(vtx) = rescue_vtx {
164
+ genv.add_edge(vtx, result_vtx);
165
+ }
166
+
167
+ Some(result_vtx)
168
+ }
@@ -15,10 +15,11 @@ use super::definitions::{process_class_node, process_def_node, process_module_no
15
15
  use super::exceptions::{process_begin_node, process_rescue_modifier_node};
16
16
  use super::dispatch::{dispatch_needs_child, dispatch_simple, process_needs_child, DispatchResult};
17
17
  use super::literals::install_literal_node;
18
- use super::loops::{process_until_node, process_while_node};
18
+ use super::loops::{process_for_node, process_until_node, process_while_node};
19
19
  use super::operators::{process_and_node, process_or_node};
20
20
  use super::parentheses::process_parentheses_node;
21
21
  use super::returns::process_return_node;
22
+ use super::super_calls;
22
23
 
23
24
  /// Build graph from AST (public API wrapper)
24
25
  pub struct AstInstaller<'a> {
@@ -89,12 +90,26 @@ pub(crate) fn install_node(
89
90
  return process_rescue_modifier_node(genv, lenv, changes, source, &rescue_modifier);
90
91
  }
91
92
 
93
+ // SuperNode: super(args) — explicit arguments
94
+ if let Some(super_node) = node.as_super_node() {
95
+ return super_calls::process_super_node(genv, lenv, changes, source, &super_node);
96
+ }
97
+ // ForwardingSuperNode: super — implicit argument forwarding
98
+ if let Some(fwd_super_node) = node.as_forwarding_super_node() {
99
+ return super_calls::process_forwarding_super_node(
100
+ genv, lenv, changes, source, &fwd_super_node,
101
+ );
102
+ }
103
+
92
104
  if let Some(while_node) = node.as_while_node() {
93
105
  return process_while_node(genv, lenv, changes, source, &while_node);
94
106
  }
95
107
  if let Some(until_node) = node.as_until_node() {
96
108
  return process_until_node(genv, lenv, changes, source, &until_node);
97
109
  }
110
+ if let Some(for_node) = node.as_for_node() {
111
+ return process_for_node(genv, lenv, changes, source, &for_node);
112
+ }
98
113
 
99
114
  if let Some(paren_node) = node.as_parentheses_node() {
100
115
  return process_parentheses_node(genv, lenv, changes, source, &paren_node);
@@ -118,13 +118,9 @@ fn install_array_literal_elements(
118
118
 
119
119
  let array_type = if element_types.is_empty() {
120
120
  Type::array()
121
- } else if element_types.len() == 1 {
122
- let elem_type = element_types.into_iter().next().unwrap();
123
- Type::array_of(elem_type)
124
121
  } else {
125
122
  let types_vec: Vec<Type> = element_types.into_iter().collect();
126
- let union_type = Type::Union(types_vec);
127
- Type::array_of(union_type)
123
+ Type::array_of(Type::union_of(types_vec))
128
124
  };
129
125
 
130
126
  Some(genv.new_source(array_type))
@@ -176,18 +172,8 @@ fn install_hash_literal_elements(
176
172
  let hash_type = if key_types.is_empty() || value_types.is_empty() {
177
173
  Type::hash()
178
174
  } else {
179
- let key_type = if key_types.len() == 1 {
180
- key_types.into_iter().next().unwrap()
181
- } else {
182
- let types_vec: Vec<Type> = key_types.into_iter().collect();
183
- Type::Union(types_vec)
184
- };
185
- let value_type = if value_types.len() == 1 {
186
- value_types.into_iter().next().unwrap()
187
- } else {
188
- let types_vec: Vec<Type> = value_types.into_iter().collect();
189
- Type::Union(types_vec)
190
- };
175
+ let key_type = Type::union_of(key_types.into_iter().collect());
176
+ let value_type = Type::union_of(value_types.into_iter().collect());
191
177
  Type::hash_of(key_type, value_type)
192
178
  };
193
179
 
@@ -238,57 +224,3 @@ fn infer_range_element_type(
238
224
  }
239
225
  None
240
226
  }
241
-
242
- #[cfg(test)]
243
- mod tests {
244
- use super::*;
245
-
246
- #[test]
247
- fn test_install_string_literal() {
248
- let mut genv = GlobalEnv::new();
249
- let vtx = genv.new_source(Type::string());
250
- assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "String");
251
- }
252
-
253
- #[test]
254
- fn test_install_integer_literal() {
255
- let mut genv = GlobalEnv::new();
256
- let vtx = genv.new_source(Type::integer());
257
- assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Integer");
258
- }
259
-
260
- #[test]
261
- fn test_install_float_literal() {
262
- let mut genv = GlobalEnv::new();
263
- let vtx = genv.new_source(Type::float());
264
- assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Float");
265
- }
266
-
267
- #[test]
268
- fn test_install_regexp_literal() {
269
- let mut genv = GlobalEnv::new();
270
- let vtx = genv.new_source(Type::regexp());
271
- assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Regexp");
272
- }
273
-
274
- #[test]
275
- fn test_install_interpolated_string_literal() {
276
- let mut genv = GlobalEnv::new();
277
- let vtx = genv.new_source(Type::string());
278
- assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "String");
279
- }
280
-
281
- #[test]
282
- fn test_install_interpolated_symbol_literal() {
283
- let mut genv = GlobalEnv::new();
284
- let vtx = genv.new_source(Type::symbol());
285
- assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Symbol");
286
- }
287
-
288
- #[test]
289
- fn test_install_interpolated_regexp_literal() {
290
- let mut genv = GlobalEnv::new();
291
- let vtx = genv.new_source(Type::regexp());
292
- assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Regexp");
293
- }
294
- }
@@ -0,0 +1,94 @@
1
+ //! Loops - while/until loop type inference
2
+ //!
3
+ //! Ruby loop expressions evaluate to nil (except when break passes a value).
4
+ //! Break value propagation is not yet supported.
5
+
6
+ use crate::env::{GlobalEnv, LocalEnv};
7
+ use crate::graph::{ChangeSet, VertexId};
8
+ use crate::types::Type;
9
+ use ruby_prism::{ForNode, UntilNode, WhileNode};
10
+
11
+ use super::bytes_to_name;
12
+ use super::install::{install_node, install_statements};
13
+
14
+ /// Process WhileNode: `while predicate; statements; end`
15
+ ///
16
+ /// Returns nil. Traverses predicate and body to register method calls
17
+ /// and variable assignments in the type graph.
18
+ pub(crate) fn process_while_node(
19
+ genv: &mut GlobalEnv,
20
+ lenv: &mut LocalEnv,
21
+ changes: &mut ChangeSet,
22
+ source: &str,
23
+ while_node: &WhileNode,
24
+ ) -> Option<VertexId> {
25
+ install_node(genv, lenv, changes, source, &while_node.predicate());
26
+
27
+ if let Some(stmts) = while_node.statements() {
28
+ install_statements(genv, lenv, changes, source, &stmts);
29
+ }
30
+
31
+ Some(genv.new_source(Type::Nil))
32
+ }
33
+
34
+ /// Process UntilNode: `until predicate; statements; end`
35
+ ///
36
+ /// Returns nil. Traverses predicate and body to register method calls
37
+ /// and variable assignments in the type graph.
38
+ pub(crate) fn process_until_node(
39
+ genv: &mut GlobalEnv,
40
+ lenv: &mut LocalEnv,
41
+ changes: &mut ChangeSet,
42
+ source: &str,
43
+ until_node: &UntilNode,
44
+ ) -> Option<VertexId> {
45
+ install_node(genv, lenv, changes, source, &until_node.predicate());
46
+
47
+ if let Some(stmts) = until_node.statements() {
48
+ install_statements(genv, lenv, changes, source, &stmts);
49
+ }
50
+
51
+ Some(genv.new_source(Type::Nil))
52
+ }
53
+
54
+ /// Process ForNode: `for index in collection; statements; end`
55
+ ///
56
+ /// Ruby's `for` does NOT create a new scope — the loop variable persists
57
+ /// after the loop. This differs from `collection.each { |x| }` which
58
+ /// creates a block scope.
59
+ ///
60
+ /// Returns nil (consistent with while/until; Ruby's for returns the
61
+ /// collection, but the return value is rarely used in practice).
62
+ pub(crate) fn process_for_node(
63
+ genv: &mut GlobalEnv,
64
+ lenv: &mut LocalEnv,
65
+ changes: &mut ChangeSet,
66
+ source: &str,
67
+ for_node: &ForNode,
68
+ ) -> Option<VertexId> {
69
+ let collection_vtx = install_node(genv, lenv, changes, source, &for_node.collection());
70
+
71
+ // TODO: MultiTargetNode (e.g., `for a, b in [[1, "x"]]`) is not yet supported
72
+ if let Some(target) = for_node.index().as_local_variable_target_node() {
73
+ let name = bytes_to_name(target.name().as_slice());
74
+ let var_vtx = genv.new_vertex();
75
+
76
+ // Array[T] or Range[T] → loop var gets T
77
+ let elem_type = collection_vtx
78
+ .and_then(|vtx| genv.get_source(vtx))
79
+ .and_then(|src| src.ty.type_args())
80
+ .and_then(|args| args.first().cloned());
81
+ if let Some(ty) = elem_type {
82
+ let elem_src = genv.new_source(ty);
83
+ genv.add_edge(elem_src, var_vtx);
84
+ }
85
+
86
+ lenv.new_var(name, var_vtx);
87
+ }
88
+
89
+ if let Some(stmts) = for_node.statements() {
90
+ install_statements(genv, lenv, changes, source, &stmts);
91
+ }
92
+
93
+ Some(genv.new_source(Type::Nil))
94
+ }
@@ -13,6 +13,7 @@ mod operators;
13
13
  mod parameters;
14
14
  mod parentheses;
15
15
  mod returns;
16
+ mod super_calls;
16
17
  mod variables;
17
18
 
18
19
  pub use install::AstInstaller;
@@ -30,18 +31,3 @@ pub use install::AstInstaller;
30
31
  pub(crate) fn bytes_to_name(bytes: &[u8]) -> String {
31
32
  String::from_utf8_lossy(bytes).to_string()
32
33
  }
33
-
34
- #[cfg(test)]
35
- mod tests {
36
- use super::bytes_to_name;
37
-
38
- #[test]
39
- fn test_bytes_to_name_valid_utf8() {
40
- assert_eq!(bytes_to_name(b"hello"), "hello");
41
- }
42
-
43
- #[test]
44
- fn test_bytes_to_name_invalid_utf8_replaced() {
45
- assert_eq!(bytes_to_name(b"hello\xff"), "hello\u{FFFD}");
46
- }
47
- }
@@ -0,0 +1,79 @@
1
+ //! Operators - logical operator type inference (&&, ||, !)
2
+
3
+ use crate::env::{GlobalEnv, LocalEnv};
4
+ use crate::graph::{ChangeSet, VertexId};
5
+ use crate::types::Type;
6
+ use ruby_prism::{AndNode, Node, OrNode};
7
+
8
+ use super::install::install_node;
9
+
10
+ /// Merge two branch nodes into a union type vertex.
11
+ fn process_binary_logical_op<'a>(
12
+ genv: &mut GlobalEnv,
13
+ lenv: &mut LocalEnv,
14
+ changes: &mut ChangeSet,
15
+ source: &str,
16
+ left: Node<'a>,
17
+ right: Node<'a>,
18
+ ) -> Option<VertexId> {
19
+ let result_vtx = genv.new_vertex();
20
+
21
+ if let Some(vtx) = install_node(genv, lenv, changes, source, &left) {
22
+ genv.add_edge(vtx, result_vtx);
23
+ }
24
+
25
+ if let Some(vtx) = install_node(genv, lenv, changes, source, &right) {
26
+ genv.add_edge(vtx, result_vtx);
27
+ }
28
+
29
+ Some(result_vtx)
30
+ }
31
+
32
+ /// Process AndNode (a && b): Union(type(a), type(b))
33
+ ///
34
+ /// Runtime: if `a` is falsy, returns `a`; otherwise returns `b`.
35
+ /// Static: conservatively produce Union(type(a), type(b)).
36
+ pub(crate) fn process_and_node(
37
+ genv: &mut GlobalEnv,
38
+ lenv: &mut LocalEnv,
39
+ changes: &mut ChangeSet,
40
+ source: &str,
41
+ and_node: &AndNode,
42
+ ) -> Option<VertexId> {
43
+ process_binary_logical_op(genv, lenv, changes, source, and_node.left(), and_node.right())
44
+ }
45
+
46
+ /// Process OrNode (a || b): Union(type(a), type(b))
47
+ ///
48
+ /// Runtime: if `a` is truthy, returns `a`; otherwise returns `b`.
49
+ /// Static: conservatively produce Union(type(a), type(b)).
50
+ pub(crate) fn process_or_node(
51
+ genv: &mut GlobalEnv,
52
+ lenv: &mut LocalEnv,
53
+ changes: &mut ChangeSet,
54
+ source: &str,
55
+ or_node: &OrNode,
56
+ ) -> Option<VertexId> {
57
+ process_binary_logical_op(genv, lenv, changes, source, or_node.left(), or_node.right())
58
+ }
59
+
60
+ /// Process not operator (!expr): TrueClass | FalseClass
61
+ ///
62
+ /// In ruby-prism, `!expr` is represented as a CallNode with method name "!".
63
+ /// Static approximation: we cannot determine the receiver's truthiness at
64
+ /// compile time, so conservatively return TrueClass | FalseClass for any `!` call.
65
+ /// In practice, `!nil` and `!false` are always true, but we do not track that here.
66
+ ///
67
+ /// Receiver side effects are already analyzed by the caller (process_needs_child).
68
+ ///
69
+ /// TODO: Ruby allows overriding `BasicObject#!`. Currently we always return
70
+ /// TrueClass | FalseClass, ignoring user-defined `!` methods. If needed, look up
71
+ /// the receiver's RBS definition and use its return type instead.
72
+ pub(crate) fn process_not_operator(genv: &mut GlobalEnv) -> VertexId {
73
+ let result_vtx = genv.new_vertex();
74
+ let true_vtx = genv.new_source(Type::instance("TrueClass"));
75
+ let false_vtx = genv.new_source(Type::instance("FalseClass"));
76
+ genv.add_edge(true_vtx, result_vtx);
77
+ genv.add_edge(false_vtx, result_vtx);
78
+ result_vtx
79
+ }
@@ -24,7 +24,7 @@ use super::bytes_to_name;
24
24
  /// name.upcase
25
25
  /// end
26
26
  /// ```
27
- pub fn install_required_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
27
+ pub(crate) fn install_required_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
28
28
  // Create a vertex for the parameter (starts as Bot/untyped)
29
29
  let param_vtx = genv.new_vertex();
30
30
 
@@ -44,7 +44,7 @@ pub fn install_required_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, nam
44
44
  /// name.upcase
45
45
  /// end
46
46
  /// ```
47
- pub fn install_optional_parameter(
47
+ pub(crate) fn install_optional_parameter(
48
48
  genv: &mut GlobalEnv,
49
49
  lenv: &mut LocalEnv,
50
50
  _changes: &mut ChangeSet,
@@ -75,7 +75,7 @@ pub fn install_optional_parameter(
75
75
  /// items.first
76
76
  /// end
77
77
  /// ```
78
- pub fn install_rest_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
78
+ pub(crate) fn install_rest_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
79
79
  // Create a vertex for the parameter
80
80
  let param_vtx = genv.new_vertex();
81
81
 
@@ -99,7 +99,7 @@ pub fn install_rest_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: S
99
99
  /// options[:debug]
100
100
  /// end
101
101
  /// ```
102
- pub fn install_keyword_rest_parameter(
102
+ pub(crate) fn install_keyword_rest_parameter(
103
103
  genv: &mut GlobalEnv,
104
104
  lenv: &mut LocalEnv,
105
105
  name: String,
@@ -204,66 +204,3 @@ pub(crate) fn install_parameters(
204
204
 
205
205
  (param_vtxs, keyword_param_vtxs)
206
206
  }
207
-
208
- #[cfg(test)]
209
- mod tests {
210
- use super::*;
211
-
212
- #[test]
213
- fn test_install_required_parameter() {
214
- let mut genv = GlobalEnv::new();
215
- let mut lenv = LocalEnv::new();
216
-
217
- let vtx = install_required_parameter(&mut genv, &mut lenv, "name".to_string());
218
-
219
- // Parameter should be registered in LocalEnv
220
- assert_eq!(lenv.get_var("name"), Some(vtx));
221
-
222
- // Vertex should exist in GlobalEnv (as untyped)
223
- let vertex = genv.get_vertex(vtx);
224
- assert!(vertex.is_some());
225
- }
226
-
227
- #[test]
228
- fn test_install_multiple_parameters() {
229
- let mut genv = GlobalEnv::new();
230
- let mut lenv = LocalEnv::new();
231
-
232
- let vtx_a = install_required_parameter(&mut genv, &mut lenv, "a".to_string());
233
- let vtx_b = install_required_parameter(&mut genv, &mut lenv, "b".to_string());
234
- let vtx_c = install_required_parameter(&mut genv, &mut lenv, "c".to_string());
235
-
236
- // All parameters should be registered
237
- assert_eq!(lenv.get_var("a"), Some(vtx_a));
238
- assert_eq!(lenv.get_var("b"), Some(vtx_b));
239
- assert_eq!(lenv.get_var("c"), Some(vtx_c));
240
-
241
- // All vertices should be different
242
- assert_ne!(vtx_a, vtx_b);
243
- assert_ne!(vtx_b, vtx_c);
244
- assert_ne!(vtx_a, vtx_c);
245
- }
246
-
247
- #[test]
248
- fn test_install_optional_parameter_inherits_default_type() {
249
- let mut genv = GlobalEnv::new();
250
- let mut lenv = LocalEnv::new();
251
- let mut changes = ChangeSet::new();
252
-
253
- // Default value: 0 (Integer)
254
- let default_vtx = genv.new_source(Type::integer());
255
- let vtx = install_optional_parameter(
256
- &mut genv,
257
- &mut lenv,
258
- &mut changes,
259
- "age".to_string(),
260
- default_vtx,
261
- );
262
-
263
- assert_eq!(lenv.get_var("age"), Some(vtx));
264
-
265
- // Type should propagate from default value
266
- let vertex = genv.get_vertex(vtx).unwrap();
267
- assert_eq!(vertex.show(), "Integer");
268
- }
269
- }
@@ -0,0 +1,25 @@
1
+ //! Parentheses - pass-through type propagation for parenthesized expressions
2
+
3
+ use crate::env::{GlobalEnv, LocalEnv};
4
+ use crate::graph::{ChangeSet, VertexId};
5
+
6
+ use super::install::{install_node, install_statements};
7
+
8
+ /// Process ParenthesesNode: propagate inner expression's type
9
+ pub(crate) fn process_parentheses_node(
10
+ genv: &mut GlobalEnv,
11
+ lenv: &mut LocalEnv,
12
+ changes: &mut ChangeSet,
13
+ source: &str,
14
+ paren_node: &ruby_prism::ParenthesesNode,
15
+ ) -> Option<VertexId> {
16
+ let body = paren_node.body()?;
17
+
18
+ if let Some(stmts) = body.as_statements_node() {
19
+ // (expr1; expr2) → process all, return last expression's type
20
+ install_statements(genv, lenv, changes, source, &stmts)
21
+ } else {
22
+ // (expr) → propagate inner expression's type directly
23
+ install_node(genv, lenv, changes, source, &body)
24
+ }
25
+ }
@@ -0,0 +1,39 @@
1
+ //! Return statement handling
2
+ //!
3
+ //! Processes `return expr` by connecting the expression's vertex
4
+ //! to the enclosing method's merge vertex.
5
+
6
+ use crate::env::{GlobalEnv, LocalEnv};
7
+ use crate::graph::{ChangeSet, VertexId};
8
+
9
+ use super::install::install_node;
10
+
11
+ /// Process ReturnNode: connect return value to method's merge vertex
12
+ pub(crate) fn process_return_node(
13
+ genv: &mut GlobalEnv,
14
+ lenv: &mut LocalEnv,
15
+ changes: &mut ChangeSet,
16
+ source: &str,
17
+ return_node: &ruby_prism::ReturnNode,
18
+ ) -> Option<VertexId> {
19
+ // Process return value (first argument only; multi-value return not yet supported)
20
+ let value_vtx = if let Some(arguments) = return_node.arguments() {
21
+ arguments
22
+ .arguments()
23
+ .iter()
24
+ .next()
25
+ .and_then(|arg| install_node(genv, lenv, changes, source, &arg))
26
+ } else {
27
+ // `return` without value → nil
28
+ Some(genv.new_source(crate::types::Type::Nil))
29
+ };
30
+
31
+ // Connect return value to method's merge vertex
32
+ if let Some(vtx) = value_vtx {
33
+ if let Some(merge_vtx) = genv.scope_manager.current_method_return_vertex() {
34
+ genv.add_edge(vtx, merge_vtx);
35
+ }
36
+ }
37
+
38
+ None
39
+ }