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,74 @@
1
+ //! Super call handling: `super` and `super(args)`
2
+ //!
3
+ //! Ruby's `super` calls the same-named method on the parent class.
4
+ //! - `super(args)` → SuperNode: explicit arguments
5
+ //! - `super` (bare) → ForwardingSuperNode: implicit argument forwarding
6
+ //!
7
+ //! Note: ForwardingSuperNode (bare `super`) is treated as a zero-argument
8
+ //! call. In Ruby, bare `super` forwards all arguments from the enclosing
9
+ //! method, but replicating this requires parameter-vertex forwarding that
10
+ //! is not yet implemented. Return type inference is unaffected.
11
+
12
+ use ruby_prism::{ForwardingSuperNode, SuperNode};
13
+
14
+ use crate::env::{GlobalEnv, LocalEnv};
15
+ use crate::graph::{ChangeSet, VertexId};
16
+ use crate::source_map::SourceLocation as SL;
17
+ use crate::types::Type;
18
+
19
+ /// Process SuperNode: `super(args)` — explicit arguments
20
+ pub(crate) fn process_super_node(
21
+ genv: &mut GlobalEnv,
22
+ lenv: &mut LocalEnv,
23
+ changes: &mut ChangeSet,
24
+ source: &str,
25
+ super_node: &SuperNode,
26
+ ) -> Option<VertexId> {
27
+ let location = SL::from_prism_location_with_source(&super_node.location(), source);
28
+ process_super_call(genv, lenv, changes, source, super_node.arguments(), location)
29
+ }
30
+
31
+ /// Process ForwardingSuperNode: `super` — implicit argument forwarding
32
+ pub(crate) fn process_forwarding_super_node(
33
+ genv: &mut GlobalEnv,
34
+ lenv: &mut LocalEnv,
35
+ changes: &mut ChangeSet,
36
+ source: &str,
37
+ node: &ForwardingSuperNode,
38
+ ) -> Option<VertexId> {
39
+ let location = SL::from_prism_location_with_source(&node.location(), source);
40
+ process_super_call(genv, lenv, changes, source, None, location)
41
+ }
42
+
43
+ /// Resolve a super call by looking up the same-named method on the superclass.
44
+ ///
45
+ /// Returns `None` if there is no enclosing method scope (super outside a method)
46
+ /// or no explicit superclass declared on the enclosing class.
47
+ fn process_super_call(
48
+ genv: &mut GlobalEnv,
49
+ lenv: &mut LocalEnv,
50
+ changes: &mut ChangeSet,
51
+ source: &str,
52
+ arguments: Option<ruby_prism::ArgumentsNode>,
53
+ location: SL,
54
+ ) -> Option<VertexId> {
55
+ let method_name = genv.scope_manager.current_method_name()?;
56
+ let superclass_name = genv.scope_manager.current_superclass()?;
57
+ let recv_vtx = genv.new_source(Type::instance(&superclass_name));
58
+
59
+ let (arg_vtxs, kw) = if let Some(args) = arguments {
60
+ super::dispatch::collect_arguments(genv, lenv, changes, source, args.arguments().iter())
61
+ } else {
62
+ (vec![], None)
63
+ };
64
+
65
+ Some(super::calls::install_method_call(
66
+ genv,
67
+ recv_vtx,
68
+ method_name,
69
+ arg_vtxs,
70
+ kw,
71
+ Some(location),
72
+ false, // super calls cannot use safe navigation
73
+ ))
74
+ }
@@ -10,7 +10,7 @@ use crate::graph::{ChangeSet, VertexId};
10
10
  use crate::types::Type;
11
11
 
12
12
  /// Install local variable write: x = value
13
- pub fn install_local_var_write(
13
+ pub(crate) fn install_local_var_write(
14
14
  genv: &mut GlobalEnv,
15
15
  lenv: &mut LocalEnv,
16
16
  changes: &mut ChangeSet,
@@ -24,7 +24,7 @@ pub fn install_local_var_write(
24
24
  }
25
25
 
26
26
  /// Install local variable read: x
27
- pub fn install_local_var_read(lenv: &LocalEnv, var_name: &str) -> Option<VertexId> {
27
+ pub(crate) fn install_local_var_read(lenv: &LocalEnv, var_name: &str) -> Option<VertexId> {
28
28
  lenv.get_var(var_name)
29
29
  }
30
30
 
@@ -33,7 +33,7 @@ pub fn install_local_var_read(lenv: &LocalEnv, var_name: &str) -> Option<VertexI
33
33
  /// If @name already has a pre-allocated VertexId (e.g., from attr_reader),
34
34
  /// an edge is added from value_vtx to the existing vertex so types propagate.
35
35
  /// Otherwise, value_vtx is registered directly as the ivar's VertexId.
36
- pub fn install_ivar_write(
36
+ pub(crate) fn install_ivar_write(
37
37
  genv: &mut GlobalEnv,
38
38
  ivar_name: String,
39
39
  value_vtx: VertexId,
@@ -49,36 +49,16 @@ pub fn install_ivar_write(
49
49
  }
50
50
 
51
51
  /// Install instance variable read: @name
52
- pub fn install_ivar_read(genv: &GlobalEnv, ivar_name: &str) -> Option<VertexId> {
52
+ pub(crate) fn install_ivar_read(genv: &GlobalEnv, ivar_name: &str) -> Option<VertexId> {
53
53
  genv.scope_manager.lookup_instance_var(ivar_name)
54
54
  }
55
55
 
56
56
  /// Install self node
57
57
  /// Uses the fully qualified name if available (e.g., Api::V1::User instead of just User)
58
- pub fn install_self(genv: &mut GlobalEnv) -> VertexId {
58
+ pub(crate) fn install_self(genv: &mut GlobalEnv) -> VertexId {
59
59
  if let Some(qualified_name) = genv.scope_manager.current_qualified_name() {
60
60
  genv.new_source(Type::instance(&qualified_name))
61
61
  } else {
62
62
  genv.new_source(Type::instance("Object"))
63
63
  }
64
64
  }
65
-
66
- #[cfg(test)]
67
- mod tests {
68
- use super::*;
69
-
70
- #[test]
71
- fn test_install_self_at_top_level() {
72
- let mut genv = GlobalEnv::new();
73
-
74
- let vtx = install_self(&mut genv);
75
- assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Object");
76
- }
77
-
78
- #[test]
79
- fn test_local_var_read_not_found() {
80
- let lenv = LocalEnv::new();
81
-
82
- assert_eq!(install_local_var_read(&lenv, "unknown"), None);
83
- }
84
- }
@@ -134,16 +134,3 @@ fn collect_diagnostics(genv: &GlobalEnv, file_path: &Path) -> Vec<Diagnostic> {
134
134
 
135
135
  diagnostics
136
136
  }
137
-
138
- #[cfg(test)]
139
- mod tests {
140
- use super::*;
141
-
142
- #[test]
143
- fn test_file_checker_creation() {
144
- // This test will fail if RBS cache doesn't exist
145
- // That's expected - cache should be generated from Ruby side first
146
- let result = FileChecker::new();
147
- assert!(result.is_ok() || result.is_err()); // Just check it doesn't panic
148
- }
149
- }
@@ -79,44 +79,3 @@ impl Diagnostic {
79
79
  Self::warning(location, message)
80
80
  }
81
81
  }
82
-
83
- #[cfg(test)]
84
- mod tests {
85
- use super::*;
86
-
87
- #[test]
88
- fn test_diagnostic_creation() {
89
- let loc = Location {
90
- file: PathBuf::from("test.rb"),
91
- line: 10,
92
- column: 5,
93
- length: None,
94
- };
95
-
96
- let diag = Diagnostic::undefined_method(loc.clone(), "Integer", "upcase");
97
- assert_eq!(diag.level, DiagnosticLevel::Error);
98
- assert_eq!(diag.message, "undefined method `upcase` for Integer");
99
- }
100
-
101
- #[test]
102
- fn test_union_partial_error() {
103
- let loc = Location {
104
- file: PathBuf::from("test.rb"),
105
- line: 15,
106
- column: 3,
107
- length: None,
108
- };
109
-
110
- let diag = Diagnostic::union_partial_error(
111
- loc,
112
- vec!["String".to_string()],
113
- vec!["Integer".to_string()],
114
- "upcase",
115
- );
116
-
117
- assert_eq!(diag.level, DiagnosticLevel::Warning);
118
- assert!(diag
119
- .message
120
- .contains("method `upcase` is defined for String but not for Integer"));
121
- }
122
- }
@@ -78,41 +78,3 @@ pub fn format_diagnostics_with_file(diagnostics: &[Diagnostic], file_path: &Path
78
78
  Err(_) => format_diagnostics(diagnostics), // Fallback to simple format
79
79
  }
80
80
  }
81
-
82
- #[cfg(test)]
83
- mod tests {
84
- use super::*;
85
- use crate::diagnostics::diagnostic::{Diagnostic, Location};
86
- use std::path::PathBuf;
87
-
88
- #[test]
89
- fn test_format_diagnostics() {
90
- let diagnostics = vec![
91
- Diagnostic::undefined_method(
92
- Location {
93
- file: PathBuf::from("test.rb"),
94
- line: 10,
95
- column: 5,
96
- length: None,
97
- },
98
- "Integer",
99
- "upcase",
100
- ),
101
- Diagnostic::union_partial_error(
102
- Location {
103
- file: PathBuf::from("test.rb"),
104
- line: 15,
105
- column: 3,
106
- length: None,
107
- },
108
- vec!["String".to_string()],
109
- vec!["Integer".to_string()],
110
- "upcase",
111
- ),
112
- ];
113
-
114
- let output = format_diagnostics(&diagnostics);
115
- assert!(output.contains("test.rb:10:5: error:"));
116
- assert!(output.contains("test.rb:15:3: warning:"));
117
- }
118
- }
@@ -87,33 +87,3 @@ impl BoxManager {
87
87
  self.boxes.is_empty()
88
88
  }
89
89
  }
90
-
91
- #[cfg(test)]
92
- mod tests {
93
- use super::*;
94
-
95
- #[test]
96
- fn test_add_run_prevents_duplicates() {
97
- let mut manager = BoxManager::new();
98
-
99
- let id = BoxId(0);
100
- manager.add_run(id);
101
- manager.add_run(id); // Should be ignored
102
-
103
- assert_eq!(manager.run_queue.len(), 1);
104
- }
105
-
106
- #[test]
107
- fn test_pop_run() {
108
- let mut manager = BoxManager::new();
109
-
110
- let id1 = BoxId(0);
111
- let id2 = BoxId(1);
112
- manager.add_run(id1);
113
- manager.add_run(id2);
114
-
115
- assert_eq!(manager.pop_run(), Some(id1));
116
- assert_eq!(manager.pop_run(), Some(id2));
117
- assert_eq!(manager.pop_run(), None);
118
- }
119
- }
@@ -6,7 +6,7 @@
6
6
  use std::collections::HashMap;
7
7
 
8
8
  use crate::env::box_manager::BoxManager;
9
- use crate::env::method_registry::{MethodInfo, MethodRegistry};
9
+ use crate::env::method_registry::{MethodInfo, MethodRegistry, ResolutionContext};
10
10
  use crate::env::scope::{Scope, ScopeId, ScopeKind, ScopeManager};
11
11
  use crate::env::type_error::TypeError;
12
12
  use crate::env::vertex_manager::VertexManager;
@@ -37,6 +37,15 @@ pub struct GlobalEnv {
37
37
 
38
38
  /// Scope management
39
39
  pub scope_manager: ScopeManager,
40
+
41
+ /// Module inclusions: class_name → Vec<module_name> (in include order)
42
+ module_inclusions: HashMap<String, Vec<String>>,
43
+
44
+ /// Superclass map: child_class → parent_class
45
+ superclass_map: HashMap<String, String>,
46
+
47
+ /// Module extensions: class_name → Vec<module_name> (in extend order)
48
+ module_extensions: HashMap<String, Vec<String>>,
40
49
  }
41
50
 
42
51
  impl GlobalEnv {
@@ -47,6 +56,9 @@ impl GlobalEnv {
47
56
  method_registry: MethodRegistry::new(),
48
57
  type_errors: Vec::new(),
49
58
  scope_manager: ScopeManager::new(),
59
+ module_inclusions: HashMap::new(),
60
+ superclass_map: HashMap::new(),
61
+ module_extensions: HashMap::new(),
50
62
  }
51
63
  }
52
64
 
@@ -158,7 +170,33 @@ impl GlobalEnv {
158
170
 
159
171
  /// Resolve method
160
172
  pub fn resolve_method(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> {
161
- self.method_registry.resolve(recv_ty, method_name)
173
+ let ctx = ResolutionContext {
174
+ inclusions: &self.module_inclusions,
175
+ superclass_map: &self.superclass_map,
176
+ extensions: &self.module_extensions,
177
+ };
178
+ self.method_registry
179
+ .resolve(recv_ty, method_name, &ctx)
180
+ }
181
+
182
+ /// Record that a class includes a module
183
+ pub fn record_include(&mut self, class_name: &str, module_name: &str) {
184
+ Self::record_mixin(&mut self.module_inclusions, class_name, module_name);
185
+ }
186
+
187
+ /// Record that a class extends a module
188
+ pub fn record_extend(&mut self, class_name: &str, module_name: &str) {
189
+ Self::record_mixin(&mut self.module_extensions, class_name, module_name);
190
+ }
191
+
192
+ fn record_mixin(
193
+ map: &mut HashMap<String, Vec<String>>,
194
+ class_name: &str,
195
+ module_name: &str,
196
+ ) {
197
+ map.entry(class_name.to_string())
198
+ .or_default()
199
+ .push(module_name.to_string());
162
200
  }
163
201
 
164
202
  /// Register built-in method
@@ -225,14 +263,38 @@ impl GlobalEnv {
225
263
  }
226
264
  }
227
265
 
228
- /// Enter a class scope
229
- pub fn enter_class(&mut self, name: String) -> ScopeId {
266
+ /// Enter a class scope with optional superclass
267
+ pub fn enter_class(&mut self, name: String, superclass: Option<&str>) -> ScopeId {
230
268
  let scope_id = self.scope_manager.new_scope(ScopeKind::Class {
231
269
  name: name.clone(),
232
- superclass: None,
270
+ superclass: superclass.map(|s| s.to_string()),
233
271
  });
234
272
  self.scope_manager.enter_scope(scope_id);
235
273
  self.register_constant_in_parent(scope_id, &name);
274
+
275
+ // Record superclass relationship in superclass_map
276
+ if let Some(parent) = superclass {
277
+ let child_name = self.scope_manager.current_qualified_name()
278
+ .unwrap_or_else(|| name.clone());
279
+ // NOTE: lookup_constant may fail for cross-namespace inheritance
280
+ // (e.g., `class Dog < Animal` inside `module Service` where Animal is `Api::Animal`).
281
+ // In that case, the raw name is used. This is a known limitation (see design doc Q2).
282
+ let parent_name = self.scope_manager.lookup_constant(parent)
283
+ .unwrap_or_else(|| parent.to_string());
284
+
285
+ // Detect superclass mismatch (Ruby raises TypeError for this at runtime)
286
+ if let Some(existing) = self.superclass_map.get(&child_name) {
287
+ if *existing != parent_name {
288
+ eprintln!(
289
+ "[methodray] warning: superclass mismatch for {}: previously {}, now {}",
290
+ child_name, existing, parent_name
291
+ );
292
+ }
293
+ }
294
+
295
+ self.superclass_map.insert(child_name, parent_name);
296
+ }
297
+
236
298
  scope_id
237
299
  }
238
300
 
@@ -279,78 +341,3 @@ impl Default for GlobalEnv {
279
341
  Self::new()
280
342
  }
281
343
  }
282
-
283
- #[cfg(test)]
284
- mod tests {
285
- use super::*;
286
-
287
- #[test]
288
- fn test_global_env_new_vertex() {
289
- let mut genv = GlobalEnv::new();
290
-
291
- let v1 = genv.new_vertex();
292
- let v2 = genv.new_vertex();
293
-
294
- assert_eq!(v1.0, 0);
295
- assert_eq!(v2.0, 1);
296
- }
297
-
298
- #[test]
299
- fn test_global_env_new_source() {
300
- let mut genv = GlobalEnv::new();
301
-
302
- let s1 = genv.new_source(Type::string());
303
- let s2 = genv.new_source(Type::integer());
304
-
305
- assert_eq!(genv.get_source(s1).unwrap().ty.show(), "String");
306
- assert_eq!(genv.get_source(s2).unwrap().ty.show(), "Integer");
307
- }
308
-
309
- #[test]
310
- fn test_global_env_edge_propagation() {
311
- let mut genv = GlobalEnv::new();
312
-
313
- // Source<String> -> Vertex
314
- let src = genv.new_source(Type::string());
315
- let vtx = genv.new_vertex();
316
-
317
- genv.add_edge(src, vtx);
318
-
319
- // Verify type propagated to Vertex
320
- assert_eq!(genv.get_vertex(vtx).unwrap().show(), "String");
321
- }
322
-
323
- #[test]
324
- fn test_global_env_chain_propagation() {
325
- let mut genv = GlobalEnv::new();
326
-
327
- // Source<String> -> Vertex1 -> Vertex2
328
- let src = genv.new_source(Type::string());
329
- let v1 = genv.new_vertex();
330
- let v2 = genv.new_vertex();
331
-
332
- genv.add_edge(src, v1);
333
- genv.add_edge(v1, v2);
334
-
335
- // Verify type propagated to v2
336
- assert_eq!(genv.get_vertex(v1).unwrap().show(), "String");
337
- assert_eq!(genv.get_vertex(v2).unwrap().show(), "String");
338
- }
339
-
340
- #[test]
341
- fn test_global_env_union_propagation() {
342
- let mut genv = GlobalEnv::new();
343
-
344
- // Source<String> -> Vertex
345
- // Source<Integer> -> Vertex
346
- let src1 = genv.new_source(Type::string());
347
- let src2 = genv.new_source(Type::integer());
348
- let vtx = genv.new_vertex();
349
-
350
- genv.add_edge(src1, vtx);
351
- genv.add_edge(src2, vtx);
352
-
353
- // Verify it became Union type
354
- assert_eq!(genv.get_vertex(vtx).unwrap().show(), "(Integer | String)");
355
- }
356
- }
@@ -0,0 +1,42 @@
1
+ use crate::graph::VertexId;
2
+ use std::collections::HashMap;
3
+
4
+ /// Local environment: mapping of local variable names to VertexIDs
5
+ pub struct LocalEnv {
6
+ locals: HashMap<String, VertexId>,
7
+ }
8
+
9
+ impl Default for LocalEnv {
10
+ fn default() -> Self {
11
+ Self::new()
12
+ }
13
+ }
14
+
15
+ impl LocalEnv {
16
+ pub fn new() -> Self {
17
+ Self {
18
+ locals: HashMap::new(),
19
+ }
20
+ }
21
+
22
+ /// Register variable
23
+ pub fn new_var(&mut self, name: String, vtx_id: VertexId) {
24
+ self.locals.insert(name, vtx_id);
25
+ }
26
+
27
+ /// Get variable
28
+ pub fn get_var(&self, name: &str) -> Option<VertexId> {
29
+ self.locals.get(name).copied()
30
+ }
31
+
32
+ /// Remove a variable from the local environment.
33
+ /// Used for scoped variables like rescue's `=> e` binding.
34
+ pub fn remove_var(&mut self, name: &str) {
35
+ self.locals.remove(name);
36
+ }
37
+
38
+ /// Get all variables
39
+ pub fn all_vars(&self) -> impl Iterator<Item = (&String, &VertexId)> {
40
+ self.locals.iter()
41
+ }
42
+ }
@@ -0,0 +1,173 @@
1
+ //! Method registration and resolution
2
+
3
+ use std::collections::{HashMap, HashSet};
4
+
5
+ use crate::graph::VertexId;
6
+ use crate::types::Type;
7
+ use smallvec::SmallVec;
8
+
9
+ const OBJECT_CLASS: &str = "Object";
10
+ const KERNEL_MODULE: &str = "Kernel";
11
+
12
+ /// Aggregated context for method resolution (inclusions, superclass chain, extensions)
13
+ pub struct ResolutionContext<'a> {
14
+ pub inclusions: &'a HashMap<String, Vec<String>>,
15
+ pub superclass_map: &'a HashMap<String, String>,
16
+ pub extensions: &'a HashMap<String, Vec<String>>,
17
+ }
18
+
19
+ /// Method information
20
+ #[derive(Debug, Clone)]
21
+ pub struct MethodInfo {
22
+ pub return_type: Type,
23
+ pub block_param_types: Option<Vec<Type>>,
24
+ pub return_vertex: Option<VertexId>,
25
+ pub param_vertices: Option<Vec<VertexId>>,
26
+ pub keyword_param_vertices: Option<HashMap<String, VertexId>>,
27
+ }
28
+
29
+ /// Registry for method definitions
30
+ #[derive(Debug, Default)]
31
+ pub struct MethodRegistry {
32
+ methods: HashMap<(Type, String), MethodInfo>,
33
+ }
34
+
35
+ impl MethodRegistry {
36
+ /// Create a new empty registry
37
+ pub fn new() -> Self {
38
+ Self {
39
+ methods: HashMap::new(),
40
+ }
41
+ }
42
+
43
+ /// Register a method for a receiver type
44
+ pub fn register(&mut self, recv_ty: Type, method_name: &str, ret_ty: Type) {
45
+ self.register_with_block(recv_ty, method_name, ret_ty, None);
46
+ }
47
+
48
+ /// Register a method with block parameter types
49
+ pub fn register_with_block(
50
+ &mut self,
51
+ recv_ty: Type,
52
+ method_name: &str,
53
+ ret_ty: Type,
54
+ block_param_types: Option<Vec<Type>>,
55
+ ) {
56
+ self.methods.insert(
57
+ (recv_ty, method_name.to_string()),
58
+ MethodInfo {
59
+ return_type: ret_ty,
60
+ block_param_types,
61
+ return_vertex: None,
62
+ param_vertices: None,
63
+ keyword_param_vertices: None,
64
+ },
65
+ );
66
+ }
67
+
68
+ /// Register a user-defined method (return type resolved via graph)
69
+ pub fn register_user_method(
70
+ &mut self,
71
+ recv_ty: Type,
72
+ method_name: &str,
73
+ return_vertex: VertexId,
74
+ param_vertices: Vec<VertexId>,
75
+ keyword_param_vertices: Option<HashMap<String, VertexId>>,
76
+ ) {
77
+ self.methods.insert(
78
+ (recv_ty, method_name.to_string()),
79
+ MethodInfo {
80
+ return_type: Type::Bot,
81
+ block_param_types: None,
82
+ return_vertex: Some(return_vertex),
83
+ param_vertices: Some(param_vertices),
84
+ keyword_param_vertices,
85
+ },
86
+ );
87
+ }
88
+
89
+ /// Add included modules for a given class name to the chain.
90
+ fn add_included_modules(
91
+ chain: &mut SmallVec<[Type; 8]>,
92
+ class_name: &str,
93
+ inclusions: &HashMap<String, Vec<String>>,
94
+ ) {
95
+ if let Some(modules) = inclusions.get(class_name) {
96
+ for module_name in modules.iter().rev() {
97
+ chain.push(Type::instance(module_name));
98
+ }
99
+ }
100
+ }
101
+
102
+ /// Build the method resolution order (MRO) fallback chain for a receiver type.
103
+ ///
104
+ /// Returns a list of types to search in order:
105
+ /// 1. Exact receiver type
106
+ /// 2. Generic → base class (e.g., Array[Integer] → Array)
107
+ /// 3. Included modules of self (last included first, matching Ruby MRO)
108
+ /// 4. Superclass chain: for each parent, add parent type + its included modules
109
+ /// 5. Object (for Instance/Generic types only)
110
+ /// 6. Kernel (for Instance/Generic types only)
111
+ /// 7. Extended modules (for Singleton types only, last extended has highest priority)
112
+ fn fallback_chain(
113
+ recv_ty: &Type,
114
+ ctx: &ResolutionContext,
115
+ ) -> SmallVec<[Type; 8]> {
116
+ let mut chain = SmallVec::new();
117
+ chain.push(recv_ty.clone());
118
+
119
+ if let Type::Generic { name, .. } = recv_ty {
120
+ chain.push(Type::Instance { name: name.clone() });
121
+ }
122
+
123
+ // MRO for Instance/Generic: included modules → superclass chain → Object → Kernel
124
+ if matches!(recv_ty, Type::Instance { .. } | Type::Generic { .. }) {
125
+ // Included modules of self (reverse order = last included has highest priority)
126
+ if let Some(class_name) = recv_ty.base_class_name() {
127
+ Self::add_included_modules(&mut chain, class_name, ctx.inclusions);
128
+
129
+ // Walk superclass chain
130
+ let mut visited = HashSet::new();
131
+ visited.insert(class_name.to_string());
132
+ let mut current = class_name.to_string();
133
+ while let Some(parent) = ctx.superclass_map.get(&current) {
134
+ if !visited.insert(parent.clone()) {
135
+ // Cycle detected, stop walking
136
+ break;
137
+ }
138
+ chain.push(Type::instance(parent));
139
+ Self::add_included_modules(&mut chain, parent, ctx.inclusions);
140
+ current = parent.clone();
141
+ }
142
+ }
143
+
144
+ chain.push(Type::instance(OBJECT_CLASS));
145
+ chain.push(Type::instance(KERNEL_MODULE));
146
+ }
147
+
148
+ // Singleton type: search extended modules (extend makes module methods available as class methods)
149
+ if let Type::Singleton { name } = recv_ty {
150
+ Self::add_included_modules(&mut chain, name.full_name(), ctx.extensions);
151
+ }
152
+
153
+ chain
154
+ }
155
+
156
+ /// Resolve a method for a receiver type
157
+ ///
158
+ /// Searches the MRO fallback chain: exact type → base class (for generics)
159
+ /// → included modules → superclass chain → Object → Kernel.
160
+ /// For Singleton types, also searches extended modules after exact match.
161
+ /// For other non-instance types (Nil, Union, Bot), only exact match is attempted.
162
+ pub fn resolve(
163
+ &self,
164
+ recv_ty: &Type,
165
+ method_name: &str,
166
+ ctx: &ResolutionContext,
167
+ ) -> Option<&MethodInfo> {
168
+ let method_key = method_name.to_string();
169
+ Self::fallback_chain(recv_ty, ctx)
170
+ .into_iter()
171
+ .find_map(|ty| self.methods.get(&(ty, method_key.clone())))
172
+ }
173
+ }