method-ray 0.1.6 → 0.1.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee779223feab7110e29256fdc4860ba480d13d676d18e85284fd0765eef2cc4b
4
- data.tar.gz: 249087d8f6b7772aa7428a4828ffc35eacb1b0aa8b27d5cd909b803176d6a683
3
+ metadata.gz: 3b5b60b4408b5e547cb40911fb0eb68d4dce182bc32fdf1ce3e6c8cd25f24975
4
+ data.tar.gz: 44f0b585ad8f2c83568d0df520711b2c1d09affd6fded8e35fbc3f27b082f073
5
5
  SHA512:
6
- metadata.gz: 8d6534a955eaf4937c5ebc4c525e07f5f7cd8599861f128cbb10161e00a6a932f1b47760e1283e7e2f48940ea0a89bb5c038ac83cdd8ce8a629f8f91b05e534a
7
- data.tar.gz: cbe0d247ffcbd0ce4965f9e20a2293336aca3d7a6eeb96878665cb7692e11ea4366d3c1f2a87ea32711dbcd00a404d4a41da3e84d34829d40568ec0bb37d74b4
6
+ metadata.gz: 1a7988edfb76b4fe4d1e03b8558104cc378299ebf8651b79dfc743e0e48b9cd4a04d235be1ca054f3ad172f9b7a8b0689e4bf99250935c02ab5cedb0737f1340
7
+ data.tar.gz: b6d94d5ce791e5b8317a880e986d60618a017434bc5823412dba3c8a04dcf3fb25089dff2a86e3662ecb7c2cdd13790cd32d5b18bc73960cf488ad8e8849a35b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.7] - 2026-03-07
9
+
10
+ ### Added
11
+
12
+ - Kernel/Object methods loaded from RBS to reduce false positives ([#39](https://github.com/dak2/method-ray/pull/39))
13
+ - Object/Kernel fallback chain for method resolution ([#40](https://github.com/dak2/method-ray/pull/40))
14
+ - Constant namespace resolution for ConstantReadNode in nested scopes ([#41](https://github.com/dak2/method-ray/pull/41))
15
+ - Cargo test added to CI workflow ([#38](https://github.com/dak2/method-ray/pull/38))
16
+
17
+ ### Changed
18
+
19
+ - Extract `bytes_to_name` helper to consolidate 17 UTF-8 conversion sites ([#42](https://github.com/dak2/method-ray/pull/42))
20
+ - Refactor MethodCallBox by extracting helper methods ([#43](https://github.com/dak2/method-ray/pull/43))
21
+
8
22
  ## [0.1.6] - 2026-02-23
9
23
 
10
24
  ### Fixed
@@ -95,6 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
95
109
  - Initial release
96
110
  - `methodray check` - Static type checking for Ruby files
97
111
 
112
+ [0.1.7]: https://github.com/dak2/method-ray/releases/tag/v0.1.7
98
113
  [0.1.6]: https://github.com/dak2/method-ray/releases/tag/v0.1.6
99
114
  [0.1.5]: https://github.com/dak2/method-ray/releases/tag/v0.1.5
100
115
  [0.1.4]: https://github.com/dak2/method-ray/releases/tag/v0.1.4
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MethodRay
4
- VERSION = '0.1.6'
4
+ VERSION = '0.1.7'
5
5
  end
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray-core"
3
- version = "0.1.6"
3
+ version = "0.1.7"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -8,6 +8,7 @@
8
8
  use crate::env::{GlobalEnv, LocalEnv, ScopeKind};
9
9
  use crate::graph::{ChangeSet, VertexId};
10
10
 
11
+ use super::bytes_to_name;
11
12
  use super::parameters::{install_optional_parameter, install_required_parameter, install_rest_parameter};
12
13
 
13
14
  /// Process block node
@@ -68,7 +69,7 @@ fn install_block_parameters_with_vtxs(
68
69
  // Required parameters (most common in blocks)
69
70
  for node in params.requireds().iter() {
70
71
  if let Some(req_param) = node.as_required_parameter_node() {
71
- let name = String::from_utf8_lossy(req_param.name().as_slice()).to_string();
72
+ let name = bytes_to_name(req_param.name().as_slice());
72
73
  let vtx = install_block_parameter(genv, lenv, name);
73
74
  vtxs.push(vtx);
74
75
  }
@@ -77,7 +78,7 @@ fn install_block_parameters_with_vtxs(
77
78
  // Optional parameters: { |x = 1| ... }
78
79
  for node in params.optionals().iter() {
79
80
  if let Some(opt_param) = node.as_optional_parameter_node() {
80
- let name = String::from_utf8_lossy(opt_param.name().as_slice()).to_string();
81
+ let name = bytes_to_name(opt_param.name().as_slice());
81
82
  let default_value = opt_param.value();
82
83
 
83
84
  if let Some(default_vtx) =
@@ -97,7 +98,7 @@ fn install_block_parameters_with_vtxs(
97
98
  if let Some(rest_node) = params.rest() {
98
99
  if let Some(rest_param) = rest_node.as_rest_parameter_node() {
99
100
  if let Some(name_id) = rest_param.name() {
100
- let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
101
+ let name = bytes_to_name(name_id.as_slice());
101
102
  let vtx = install_rest_parameter(genv, lenv, name);
102
103
  vtxs.push(vtx);
103
104
  }
@@ -11,6 +11,7 @@ use crate::graph::{ChangeSet, VertexId};
11
11
  use crate::types::Type;
12
12
  use ruby_prism::Node;
13
13
 
14
+ use super::bytes_to_name;
14
15
  use super::install::install_statements;
15
16
  use super::parameters::install_parameters;
16
17
 
@@ -64,7 +65,7 @@ pub(crate) fn process_def_node(
64
65
  source: &str,
65
66
  def_node: &ruby_prism::DefNode,
66
67
  ) -> Option<VertexId> {
67
- let method_name = String::from_utf8_lossy(def_node.name().as_slice()).to_string();
68
+ let method_name = bytes_to_name(def_node.name().as_slice());
68
69
 
69
70
  // Check if this is a class method (def self.foo)
70
71
  let is_class_method = def_node
@@ -163,7 +164,7 @@ fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
163
164
  pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
164
165
  // Simple constant read: `User`
165
166
  if let Some(constant_read) = node.as_constant_read_node() {
166
- return Some(String::from_utf8_lossy(constant_read.name().as_slice()).to_string());
167
+ return Some(bytes_to_name(constant_read.name().as_slice()));
167
168
  }
168
169
 
169
170
  // Constant path: `Api::User` or `Api::V1::User`
@@ -171,7 +172,7 @@ pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
171
172
  // name() returns Option<ConstantId>, use as_slice() to get &[u8]
172
173
  let name = constant_path
173
174
  .name()
174
- .map(|id| String::from_utf8_lossy(id.as_slice()).to_string())?;
175
+ .map(|id| bytes_to_name(id.as_slice()))?;
175
176
 
176
177
  // Get parent path if exists
177
178
  if let Some(parent_node) = constant_path.parent() {
@@ -9,6 +9,7 @@ use crate::source_map::SourceLocation;
9
9
  use crate::types::Type;
10
10
  use ruby_prism::Node;
11
11
 
12
+ use super::bytes_to_name;
12
13
  use super::calls::install_method_call;
13
14
  use super::variables::{
14
15
  install_ivar_read, install_ivar_write, install_local_var_read, install_local_var_write,
@@ -68,7 +69,7 @@ pub enum NeedsChildKind<'a> {
68
69
  pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -> DispatchResult {
69
70
  // Instance variable read: @name
70
71
  if let Some(ivar_read) = node.as_instance_variable_read_node() {
71
- let ivar_name = String::from_utf8_lossy(ivar_read.name().as_slice()).to_string();
72
+ let ivar_name = bytes_to_name(ivar_read.name().as_slice());
72
73
  return match install_ivar_read(genv, &ivar_name) {
73
74
  Some(vtx) => DispatchResult::Vertex(vtx),
74
75
  None => DispatchResult::NotHandled,
@@ -82,17 +83,19 @@ pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -
82
83
 
83
84
  // Local variable read: x
84
85
  if let Some(read_node) = node.as_local_variable_read_node() {
85
- let var_name = String::from_utf8_lossy(read_node.name().as_slice()).to_string();
86
+ let var_name = bytes_to_name(read_node.name().as_slice());
86
87
  return match install_local_var_read(lenv, &var_name) {
87
88
  Some(vtx) => DispatchResult::Vertex(vtx),
88
89
  None => DispatchResult::NotHandled,
89
90
  };
90
91
  }
91
92
 
92
- // ConstantReadNode: User → Type::Singleton("User")
93
+ // ConstantReadNode: User → Type::Singleton("User") or Type::Singleton("Api::User")
93
94
  if let Some(const_read) = node.as_constant_read_node() {
94
- let name = String::from_utf8_lossy(const_read.name().as_slice()).to_string();
95
- let vtx = genv.new_source(Type::singleton(&name));
95
+ let name = bytes_to_name(const_read.name().as_slice());
96
+ let resolved_name = genv.scope_manager.lookup_constant(&name)
97
+ .unwrap_or(name);
98
+ let vtx = genv.new_source(Type::singleton(&resolved_name));
96
99
  return DispatchResult::Vertex(vtx);
97
100
  }
98
101
 
@@ -116,7 +119,7 @@ fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
116
119
  .iter()
117
120
  .filter_map(|arg| {
118
121
  arg.as_symbol_node().map(|sym| {
119
- String::from_utf8_lossy(&sym.unescaped()).to_string()
122
+ bytes_to_name(sym.unescaped())
120
123
  })
121
124
  })
122
125
  .collect()
@@ -128,7 +131,7 @@ fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
128
131
  pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsChildKind<'a>> {
129
132
  // Instance variable write: @name = value
130
133
  if let Some(ivar_write) = node.as_instance_variable_write_node() {
131
- let ivar_name = String::from_utf8_lossy(ivar_write.name().as_slice()).to_string();
134
+ let ivar_name = bytes_to_name(ivar_write.name().as_slice());
132
135
  return Some(NeedsChildKind::IvarWrite {
133
136
  ivar_name,
134
137
  value: ivar_write.value(),
@@ -137,7 +140,7 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
137
140
 
138
141
  // Local variable write: x = value
139
142
  if let Some(write_node) = node.as_local_variable_write_node() {
140
- let var_name = String::from_utf8_lossy(write_node.name().as_slice()).to_string();
143
+ let var_name = bytes_to_name(write_node.name().as_slice());
141
144
  return Some(NeedsChildKind::LocalVarWrite {
142
145
  var_name,
143
146
  value: write_node.value(),
@@ -146,7 +149,7 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
146
149
 
147
150
  // Method call: x.upcase, x.each { |i| ... }, or name (implicit self)
148
151
  if let Some(call_node) = node.as_call_node() {
149
- let method_name = String::from_utf8_lossy(call_node.name().as_slice()).to_string();
152
+ let method_name = bytes_to_name(call_node.name().as_slice());
150
153
  let block = call_node.block();
151
154
  let arguments: Vec<Node<'a>> = call_node
152
155
  .arguments()
@@ -463,8 +466,8 @@ end
463
466
 
464
467
  // Utils.run should be registered
465
468
  let info = genv
466
- .resolve_method(&Type::instance("Utils"), "run")
467
- .expect("Utils#run should be registered");
469
+ .resolve_method(&Type::singleton("Utils"), "run")
470
+ .expect("Utils.run should be registered");
468
471
  assert!(info.return_vertex.is_some());
469
472
  }
470
473
 
@@ -893,4 +896,88 @@ Api::User.new.name
893
896
  genv.type_errors
894
897
  );
895
898
  }
899
+
900
+ // Test 23: ConstantReadNode inside module resolves to qualified name
901
+ #[test]
902
+ fn test_constant_read_inside_module_resolves_qualified() {
903
+ let source = r#"
904
+ module Api
905
+ class User
906
+ def name
907
+ "Alice"
908
+ end
909
+ end
910
+
911
+ class Service
912
+ def run
913
+ User.new.name
914
+ end
915
+ end
916
+ end
917
+ "#;
918
+ let genv = analyze(source);
919
+ assert!(
920
+ genv.type_errors.is_empty(),
921
+ "User.new inside module Api should resolve to Api::User: {:?}",
922
+ genv.type_errors
923
+ );
924
+ }
925
+
926
+ // Test 24: ConstantReadNode in deeply nested modules
927
+ #[test]
928
+ fn test_constant_read_deeply_nested() {
929
+ let source = r#"
930
+ module Api
931
+ module V1
932
+ class User
933
+ def name
934
+ "Alice"
935
+ end
936
+ end
937
+
938
+ class Service
939
+ def run
940
+ User.new.name
941
+ end
942
+ end
943
+ end
944
+ end
945
+ "#;
946
+ let genv = analyze(source);
947
+ assert!(
948
+ genv.type_errors.is_empty(),
949
+ "User.new inside Api::V1 should resolve to Api::V1::User: {:?}",
950
+ genv.type_errors
951
+ );
952
+ }
953
+
954
+ // Test 25: Same constant name in different modules
955
+ #[test]
956
+ fn test_constant_read_same_name_different_modules() {
957
+ let source = r#"
958
+ module Api
959
+ class User
960
+ def name; "Api User"; end
961
+ end
962
+ end
963
+
964
+ module Admin
965
+ class User
966
+ def name; "Admin User"; end
967
+ end
968
+
969
+ class Service
970
+ def run
971
+ User.new.name
972
+ end
973
+ end
974
+ end
975
+ "#;
976
+ let genv = analyze(source);
977
+ assert!(
978
+ genv.type_errors.is_empty(),
979
+ "User.new inside Admin should resolve to Admin::User: {:?}",
980
+ genv.type_errors
981
+ );
982
+ }
896
983
  }
@@ -13,3 +13,32 @@ mod returns;
13
13
  mod variables;
14
14
 
15
15
  pub use install::AstInstaller;
16
+
17
+ /// Convert ruby-prism identifier bytes to a String (lossy).
18
+ ///
19
+ /// ruby-prism returns identifiers (method names, variable names, constant names,
20
+ /// parameter names) as `&[u8]`. This helper provides a single conversion point
21
+ /// used throughout the analyzer.
22
+ ///
23
+ /// Note: Uses `from_utf8_lossy` — invalid UTF-8 bytes are replaced with U+FFFD.
24
+ /// ruby-prism identifiers are expected to be valid UTF-8, so this should not
25
+ /// occur in practice. Do NOT use this function for arbitrary byte data such as
26
+ /// string literal contents.
27
+ pub(crate) fn bytes_to_name(bytes: &[u8]) -> String {
28
+ String::from_utf8_lossy(bytes).to_string()
29
+ }
30
+
31
+ #[cfg(test)]
32
+ mod tests {
33
+ use super::bytes_to_name;
34
+
35
+ #[test]
36
+ fn test_bytes_to_name_valid_utf8() {
37
+ assert_eq!(bytes_to_name(b"hello"), "hello");
38
+ }
39
+
40
+ #[test]
41
+ fn test_bytes_to_name_invalid_utf8_replaced() {
42
+ assert_eq!(bytes_to_name(b"hello\xff"), "hello\u{FFFD}");
43
+ }
44
+ }
@@ -9,6 +9,8 @@ use crate::env::{GlobalEnv, LocalEnv};
9
9
  use crate::graph::{ChangeSet, VertexId};
10
10
  use crate::types::Type;
11
11
 
12
+ use super::bytes_to_name;
13
+
12
14
  /// Install a required parameter as a local variable
13
15
  ///
14
16
  /// Required parameters start with Bot (untyped) type since we don't know
@@ -129,7 +131,7 @@ pub(crate) fn install_parameters(
129
131
  // Required parameters: def foo(a, b)
130
132
  for node in params_node.requireds().iter() {
131
133
  if let Some(req_param) = node.as_required_parameter_node() {
132
- let name = String::from_utf8_lossy(req_param.name().as_slice()).to_string();
134
+ let name = bytes_to_name(req_param.name().as_slice());
133
135
  let vtx = install_required_parameter(genv, lenv, name);
134
136
  param_vtxs.push(vtx);
135
137
  }
@@ -138,7 +140,7 @@ pub(crate) fn install_parameters(
138
140
  // Optional parameters: def foo(a = 1, b = "hello")
139
141
  for node in params_node.optionals().iter() {
140
142
  if let Some(opt_param) = node.as_optional_parameter_node() {
141
- let name = String::from_utf8_lossy(opt_param.name().as_slice()).to_string();
143
+ let name = bytes_to_name(opt_param.name().as_slice());
142
144
  let default_value = opt_param.value();
143
145
 
144
146
  let vtx = if let Some(default_vtx) =
@@ -157,7 +159,7 @@ pub(crate) fn install_parameters(
157
159
  if let Some(rest_node) = params_node.rest() {
158
160
  if let Some(rest_param) = rest_node.as_rest_parameter_node() {
159
161
  if let Some(name_id) = rest_param.name() {
160
- let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
162
+ let name = bytes_to_name(name_id.as_slice());
161
163
  install_rest_parameter(genv, lenv, name);
162
164
  }
163
165
  }
@@ -168,7 +170,7 @@ pub(crate) fn install_parameters(
168
170
  if let Some(kwrest_node) = params_node.keyword_rest() {
169
171
  if let Some(kwrest_param) = kwrest_node.as_keyword_rest_parameter_node() {
170
172
  if let Some(name_id) = kwrest_param.name() {
171
- let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
173
+ let name = bytes_to_name(name_id.as_slice());
172
174
  install_keyword_rest_parameter(genv, lenv, name);
173
175
  }
174
176
  }
@@ -71,6 +71,21 @@ impl GlobalEnv {
71
71
  self.vertex_manager.get_source(id)
72
72
  }
73
73
 
74
+ /// Get the types associated with a vertex ID (handles both Vertex and Source)
75
+ ///
76
+ /// Returns `None` if neither a Vertex nor Source exists for this ID.
77
+ /// Returns `Some(vec![])` if a Vertex exists but has no types yet (e.g., unresolved block parameters).
78
+ /// Returns `Some(vec![ty])` if a Source exists (always exactly one type).
79
+ pub fn get_receiver_types(&self, id: VertexId) -> Option<Vec<Type>> {
80
+ if let Some(vertex) = self.get_vertex(id) {
81
+ Some(vertex.types.keys().cloned().collect())
82
+ } else if let Some(source) = self.get_source(id) {
83
+ Some(vec![source.ty.clone()])
84
+ } else {
85
+ None
86
+ }
87
+ }
88
+
74
89
  /// Add edge (immediate type propagation)
75
90
  pub fn add_edge(&mut self, src: VertexId, dst: VertexId) {
76
91
  self.vertex_manager.add_edge(src, dst);
@@ -189,20 +204,36 @@ impl GlobalEnv {
189
204
 
190
205
  // ===== Scope Management =====
191
206
 
207
+ /// Register a constant (simple name → qualified name) in the parent scope
208
+ fn register_constant_in_parent(&mut self, scope_id: ScopeId, name: &str) {
209
+ if name.contains("::") { return; }
210
+ let qualified = self.scope_manager.current_qualified_name()
211
+ .unwrap_or_else(|| name.to_string());
212
+ if let Some(parent_id) = self.scope_manager.get_scope(scope_id)
213
+ .and_then(|s| s.parent)
214
+ {
215
+ if let Some(parent_scope) = self.scope_manager.get_scope_mut(parent_id) {
216
+ parent_scope.constants.insert(name.to_string(), qualified);
217
+ }
218
+ }
219
+ }
220
+
192
221
  /// Enter a class scope
193
222
  pub fn enter_class(&mut self, name: String) -> ScopeId {
194
223
  let scope_id = self.scope_manager.new_scope(ScopeKind::Class {
195
- name,
224
+ name: name.clone(),
196
225
  superclass: None,
197
226
  });
198
227
  self.scope_manager.enter_scope(scope_id);
228
+ self.register_constant_in_parent(scope_id, &name);
199
229
  scope_id
200
230
  }
201
231
 
202
232
  /// Enter a module scope
203
233
  pub fn enter_module(&mut self, name: String) -> ScopeId {
204
- let scope_id = self.scope_manager.new_scope(ScopeKind::Module { name });
234
+ let scope_id = self.scope_manager.new_scope(ScopeKind::Module { name: name.clone() });
205
235
  self.scope_manager.enter_scope(scope_id);
236
+ self.register_constant_in_parent(scope_id, &name);
206
237
  scope_id
207
238
  }
208
239
 
@@ -2,8 +2,12 @@
2
2
 
3
3
  use crate::graph::VertexId;
4
4
  use crate::types::Type;
5
+ use smallvec::SmallVec;
5
6
  use std::collections::HashMap;
6
7
 
8
+ const OBJECT_CLASS: &str = "Object";
9
+ const KERNEL_MODULE: &str = "Kernel";
10
+
7
11
  /// Method information
8
12
  #[derive(Debug, Clone)]
9
13
  pub struct MethodInfo {
@@ -70,26 +74,40 @@ impl MethodRegistry {
70
74
  );
71
75
  }
72
76
 
73
- /// Resolve a method for a receiver type
77
+ /// Build the method resolution order (MRO) fallback chain for a receiver type.
74
78
  ///
75
- /// For generic types like `Array[Integer]`, first tries exact match,
76
- /// then falls back to base class match (`Array`).
77
- pub fn resolve(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> {
78
- // First, try exact match
79
- if let Some(info) = self
80
- .methods
81
- .get(&(recv_ty.clone(), method_name.to_string()))
82
- {
83
- return Some(info);
84
- }
79
+ /// Returns a list of types to search in order:
80
+ /// 1. Exact receiver type
81
+ /// 2. Generic base class (e.g., Array[Integer] Array)
82
+ /// 3. Object (for Instance/Generic types only)
83
+ /// 4. Kernel (for Instance/Generic types only)
84
+ fn fallback_chain(recv_ty: &Type) -> SmallVec<[Type; 4]> {
85
+ let mut chain = SmallVec::new();
86
+ chain.push(recv_ty.clone());
85
87
 
86
- // For generic types, fall back to base class
87
88
  if let Type::Generic { name, .. } = recv_ty {
88
- let base_type = Type::Instance { name: name.clone() };
89
- return self.methods.get(&(base_type, method_name.to_string()));
89
+ chain.push(Type::Instance { name: name.clone() });
90
90
  }
91
91
 
92
- None
92
+ // NOTE: Kernel is a module, not a class. Represented as Type::Instance
93
+ // due to lack of Type::Module variant.
94
+ if matches!(recv_ty, Type::Instance { .. } | Type::Generic { .. }) {
95
+ chain.push(Type::instance(OBJECT_CLASS));
96
+ chain.push(Type::instance(KERNEL_MODULE));
97
+ }
98
+
99
+ chain
100
+ }
101
+
102
+ /// Resolve a method for a receiver type
103
+ ///
104
+ /// Searches the MRO fallback chain: exact type → base class (for generics) → Object → Kernel.
105
+ /// For non-instance types (Singleton, Nil, Union, Bot), only exact match is attempted.
106
+ pub fn resolve(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> {
107
+ let method_key = method_name.to_string();
108
+ Self::fallback_chain(recv_ty)
109
+ .into_iter()
110
+ .find_map(|ty| self.methods.get(&(ty, method_key.clone())))
93
111
  }
94
112
  }
95
113
 
@@ -142,4 +160,103 @@ mod tests {
142
160
  assert_eq!(pvs[0], VertexId(20));
143
161
  assert_eq!(pvs[1], VertexId(21));
144
162
  }
163
+
164
+ // --- Object/Kernel fallback ---
165
+
166
+ #[test]
167
+ fn test_resolve_falls_back_to_object() {
168
+ let mut registry = MethodRegistry::new();
169
+ registry.register(Type::instance("Object"), "nil?", Type::instance("TrueClass"));
170
+ let info = registry.resolve(&Type::instance("CustomClass"), "nil?").unwrap();
171
+ assert_eq!(info.return_type.base_class_name(), Some("TrueClass"));
172
+ }
173
+
174
+ #[test]
175
+ fn test_resolve_falls_back_to_kernel() {
176
+ let mut registry = MethodRegistry::new();
177
+ registry.register(Type::instance("Kernel"), "puts", Type::Nil);
178
+ let info = registry.resolve(&Type::instance("MyApp"), "puts").unwrap();
179
+ assert_eq!(info.return_type, Type::Nil);
180
+ }
181
+
182
+ #[test]
183
+ fn test_resolve_object_before_kernel() {
184
+ let mut registry = MethodRegistry::new();
185
+ registry.register(Type::instance("Object"), "to_s", Type::string());
186
+ registry.register(Type::instance("Kernel"), "to_s", Type::integer());
187
+ let info = registry.resolve(&Type::instance("Anything"), "to_s").unwrap();
188
+ assert_eq!(info.return_type.base_class_name(), Some("String"));
189
+ }
190
+
191
+ #[test]
192
+ fn test_resolve_exact_match_over_fallback() {
193
+ let mut registry = MethodRegistry::new();
194
+ registry.register(Type::string(), "length", Type::integer());
195
+ registry.register(Type::instance("Object"), "length", Type::string());
196
+ let info = registry.resolve(&Type::string(), "length").unwrap();
197
+ assert_eq!(info.return_type.base_class_name(), Some("Integer"));
198
+ }
199
+
200
+ // --- Types that skip fallback ---
201
+
202
+ #[test]
203
+ fn test_singleton_type_skips_fallback() {
204
+ let mut registry = MethodRegistry::new();
205
+ registry.register(Type::instance("Kernel"), "puts", Type::Nil);
206
+ assert!(registry.resolve(&Type::singleton("User"), "puts").is_none());
207
+ }
208
+
209
+ #[test]
210
+ fn test_nil_type_skips_fallback() {
211
+ let mut registry = MethodRegistry::new();
212
+ registry.register(Type::instance("Kernel"), "puts", Type::Nil);
213
+ assert!(registry.resolve(&Type::Nil, "puts").is_none());
214
+ }
215
+
216
+ #[test]
217
+ fn test_union_type_skips_fallback() {
218
+ let mut registry = MethodRegistry::new();
219
+ registry.register(Type::instance("Kernel"), "puts", Type::Nil);
220
+ let union_ty = Type::Union(vec![Type::string(), Type::integer()]);
221
+ assert!(registry.resolve(&union_ty, "puts").is_none());
222
+ }
223
+
224
+ #[test]
225
+ fn test_bot_type_skips_fallback() {
226
+ let mut registry = MethodRegistry::new();
227
+ registry.register(Type::instance("Kernel"), "puts", Type::Nil);
228
+ assert!(registry.resolve(&Type::Bot, "puts").is_none());
229
+ }
230
+
231
+ // --- Generic type fallback chain ---
232
+
233
+ #[test]
234
+ fn test_resolve_generic_falls_back_to_kernel() {
235
+ let mut registry = MethodRegistry::new();
236
+ registry.register(Type::instance("Kernel"), "puts", Type::Nil);
237
+ let generic_type = Type::array_of(Type::integer());
238
+ let info = registry.resolve(&generic_type, "puts").unwrap();
239
+ assert_eq!(info.return_type, Type::Nil);
240
+ }
241
+
242
+ #[test]
243
+ fn test_resolve_generic_full_chain() {
244
+ // Verify the 4-step fallback: Generic[T] → Base → Object → Kernel
245
+ let mut registry = MethodRegistry::new();
246
+ registry.register(Type::instance("Kernel"), "object_id", Type::integer());
247
+ let generic_type = Type::array_of(Type::string());
248
+ // Array[String] → Array (none) → Object (none) → Kernel (exists)
249
+ let info = registry.resolve(&generic_type, "object_id").unwrap();
250
+ assert_eq!(info.return_type.base_class_name(), Some("Integer"));
251
+ }
252
+
253
+ // --- Namespaced class fallback ---
254
+
255
+ #[test]
256
+ fn test_resolve_namespaced_class_falls_back_to_object() {
257
+ let mut registry = MethodRegistry::new();
258
+ registry.register(Type::instance("Object"), "class", Type::string());
259
+ let info = registry.resolve(&Type::instance("Api::V1::User"), "class").unwrap();
260
+ assert_eq!(info.return_type.base_class_name(), Some("String"));
261
+ }
145
262
  }
@@ -41,6 +41,9 @@ pub struct Scope {
41
41
 
42
42
  /// Class variables (class scope only)
43
43
  pub class_vars: HashMap<String, VertexId>,
44
+
45
+ /// Constants (simple name → qualified name)
46
+ pub constants: HashMap<String, String>,
44
47
  }
45
48
 
46
49
  #[allow(dead_code)]
@@ -53,6 +56,7 @@ impl Scope {
53
56
  local_vars: HashMap::new(),
54
57
  instance_vars: HashMap::new(),
55
58
  class_vars: HashMap::new(),
59
+ constants: HashMap::new(),
56
60
  }
57
61
  }
58
62
 
@@ -135,6 +139,18 @@ impl ScopeManager {
135
139
  self.scopes.get_mut(&self.current_scope).unwrap()
136
140
  }
137
141
 
142
+ /// Walk scopes from current scope up to the top-level, yielding each scope
143
+ fn walk_scopes(&self) -> impl Iterator<Item = &Scope> + '_ {
144
+ let scopes = &self.scopes;
145
+ let mut current = Some(self.current_scope);
146
+ std::iter::from_fn(move || {
147
+ let scope_id = current?;
148
+ let scope = scopes.get(&scope_id)?;
149
+ current = scope.parent;
150
+ Some(scope)
151
+ })
152
+ }
153
+
138
154
  /// Get scope by ID
139
155
  pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> {
140
156
  self.scopes.get(&id)
@@ -147,103 +163,54 @@ impl ScopeManager {
147
163
 
148
164
  /// Lookup variable in current scope or parent scopes
149
165
  pub fn lookup_var(&self, name: &str) -> Option<VertexId> {
150
- let mut current = Some(self.current_scope);
151
-
152
- while let Some(scope_id) = current {
153
- if let Some(scope) = self.scopes.get(&scope_id) {
154
- if let Some(vtx) = scope.get_local_var(name) {
155
- return Some(vtx);
156
- }
157
- current = scope.parent;
158
- } else {
159
- break;
160
- }
161
- }
166
+ self.walk_scopes().find_map(|scope| scope.get_local_var(name))
167
+ }
162
168
 
163
- None
169
+ /// Lookup constant in current scope or parent scopes (simple name → qualified name)
170
+ pub fn lookup_constant(&self, simple_name: &str) -> Option<String> {
171
+ self.walk_scopes()
172
+ .find_map(|scope| scope.constants.get(simple_name).cloned())
164
173
  }
165
174
 
166
175
  /// Lookup instance variable in enclosing class scope
167
176
  pub fn lookup_instance_var(&self, name: &str) -> Option<VertexId> {
168
- let mut current = Some(self.current_scope);
169
-
170
- while let Some(scope_id) = current {
171
- if let Some(scope) = self.scopes.get(&scope_id) {
172
- // Walk up to class scope
173
- match &scope.kind {
174
- ScopeKind::Class { .. } => {
175
- return scope.get_instance_var(name);
176
- }
177
- _ => {
178
- current = scope.parent;
179
- }
180
- }
181
- } else {
182
- break;
183
- }
184
- }
185
-
186
- None
177
+ self.walk_scopes()
178
+ .find(|scope| matches!(&scope.kind, ScopeKind::Class { .. }))
179
+ .and_then(|scope| scope.get_instance_var(name))
187
180
  }
188
181
 
189
182
  /// Set instance variable in enclosing class scope
190
183
  pub fn set_instance_var_in_class(&mut self, name: String, vtx: VertexId) {
191
- let mut current = Some(self.current_scope);
192
-
193
- while let Some(scope_id) = current {
194
- if let Some(scope) = self.scopes.get(&scope_id) {
195
- // Find class scope and set variable
196
- match &scope.kind {
197
- ScopeKind::Class { .. } => {
198
- if let Some(class_scope) = self.scopes.get_mut(&scope_id) {
199
- class_scope.set_instance_var(name, vtx);
200
- }
201
- return;
202
- }
203
- _ => {
204
- current = scope.parent;
205
- }
206
- }
207
- } else {
208
- break;
184
+ let class_scope_id = self.walk_scopes()
185
+ .find(|scope| matches!(&scope.kind, ScopeKind::Class { .. }))
186
+ .map(|scope| scope.id);
187
+ if let Some(scope_id) = class_scope_id {
188
+ if let Some(scope) = self.scopes.get_mut(&scope_id) {
189
+ scope.set_instance_var(name, vtx);
209
190
  }
210
191
  }
211
192
  }
212
193
 
213
194
  /// Get current class name (simple name, not qualified)
214
195
  pub fn current_class_name(&self) -> Option<String> {
215
- let mut current = Some(self.current_scope);
216
-
217
- while let Some(scope_id) = current {
218
- if let Some(scope) = self.scopes.get(&scope_id) {
219
- if let ScopeKind::Class { name, .. } = &scope.kind {
220
- return Some(name.clone());
221
- }
222
- current = scope.parent;
196
+ self.walk_scopes().find_map(|scope| {
197
+ if let ScopeKind::Class { name, .. } = &scope.kind {
198
+ Some(name.clone())
223
199
  } else {
224
- break;
200
+ None
225
201
  }
226
- }
227
-
228
- None
202
+ })
229
203
  }
230
204
 
231
205
  /// Get current module name (simple name, not qualified)
232
206
  pub fn current_module_name(&self) -> Option<String> {
233
- let mut current = Some(self.current_scope);
234
-
235
- while let Some(scope_id) = current {
236
- if let Some(scope) = self.scopes.get(&scope_id) {
237
- if let ScopeKind::Module { name } = &scope.kind {
238
- return Some(name.clone());
239
- }
240
- current = scope.parent;
207
+ self.walk_scopes().find_map(|scope| {
208
+ if let ScopeKind::Module { name } = &scope.kind {
209
+ Some(name.clone())
241
210
  } else {
242
- break;
211
+ None
243
212
  }
244
- }
245
-
246
- None
213
+ })
247
214
  }
248
215
 
249
216
  /// Get current fully qualified name by traversing all parent class/module scopes
@@ -260,36 +227,12 @@ impl ScopeManager {
260
227
  /// ```
261
228
  /// When inside `greet`, this returns `Some("Api::V1::User")`
262
229
  pub fn current_qualified_name(&self) -> Option<String> {
263
- let mut path_segments: Vec<String> = Vec::new();
264
- let mut current = Some(self.current_scope);
265
-
266
- // Traverse from current scope up to top-level, collecting class/module names
267
- while let Some(scope_id) = current {
268
- if let Some(scope) = self.scopes.get(&scope_id) {
269
- match &scope.kind {
270
- ScopeKind::Class { name, .. } => {
271
- // If the name already contains ::, it's a qualified name from AST
272
- // (e.g., `class Api::User` defined at top level)
273
- if name.contains("::") {
274
- path_segments.push(name.clone());
275
- } else {
276
- path_segments.push(name.clone());
277
- }
278
- }
279
- ScopeKind::Module { name } => {
280
- if name.contains("::") {
281
- path_segments.push(name.clone());
282
- } else {
283
- path_segments.push(name.clone());
284
- }
285
- }
286
- _ => {}
287
- }
288
- current = scope.parent;
289
- } else {
290
- break;
291
- }
292
- }
230
+ let mut path_segments: Vec<&str> = self.walk_scopes()
231
+ .filter_map(|scope| match &scope.kind {
232
+ ScopeKind::Class { name, .. } | ScopeKind::Module { name } => Some(name.as_str()),
233
+ _ => None,
234
+ })
235
+ .collect();
293
236
 
294
237
  if path_segments.is_empty() {
295
238
  return None;
@@ -297,76 +240,35 @@ impl ScopeManager {
297
240
 
298
241
  // Reverse to get from outermost to innermost
299
242
  path_segments.reverse();
300
-
301
- // Join all segments, handling cases where segments may already contain ::
302
- let mut result = String::new();
303
- for segment in path_segments {
304
- if !result.is_empty() {
305
- result.push_str("::");
306
- }
307
- result.push_str(&segment);
308
- }
309
-
310
- Some(result)
243
+ Some(path_segments.join("::"))
311
244
  }
312
245
 
313
246
  /// Get return_vertex from the nearest enclosing method scope
314
247
  pub fn current_method_return_vertex(&self) -> Option<VertexId> {
315
- let mut current = Some(self.current_scope);
316
- while let Some(scope_id) = current {
317
- if let Some(scope) = self.scopes.get(&scope_id) {
318
- if let ScopeKind::Method { return_vertex, .. } = &scope.kind {
319
- return *return_vertex;
320
- }
321
- current = scope.parent;
248
+ self.walk_scopes().find_map(|scope| {
249
+ if let ScopeKind::Method { return_vertex, .. } = &scope.kind {
250
+ *return_vertex
322
251
  } else {
323
- break;
252
+ None
324
253
  }
325
- }
326
- None
254
+ })
327
255
  }
328
256
 
329
257
  /// Lookup instance variable in enclosing module scope
330
258
  pub fn lookup_instance_var_in_module(&self, name: &str) -> Option<VertexId> {
331
- let mut current = Some(self.current_scope);
332
-
333
- while let Some(scope_id) = current {
334
- if let Some(scope) = self.scopes.get(&scope_id) {
335
- match &scope.kind {
336
- ScopeKind::Module { .. } => {
337
- return scope.get_instance_var(name);
338
- }
339
- _ => {
340
- current = scope.parent;
341
- }
342
- }
343
- } else {
344
- break;
345
- }
346
- }
347
-
348
- None
259
+ self.walk_scopes()
260
+ .find(|scope| matches!(&scope.kind, ScopeKind::Module { .. }))
261
+ .and_then(|scope| scope.get_instance_var(name))
349
262
  }
350
263
 
351
264
  /// Set instance variable in enclosing module scope
352
265
  pub fn set_instance_var_in_module(&mut self, name: String, vtx: VertexId) {
353
- let mut current = Some(self.current_scope);
354
-
355
- while let Some(scope_id) = current {
356
- if let Some(scope) = self.scopes.get(&scope_id) {
357
- match &scope.kind {
358
- ScopeKind::Module { .. } => {
359
- if let Some(module_scope) = self.scopes.get_mut(&scope_id) {
360
- module_scope.set_instance_var(name, vtx);
361
- }
362
- return;
363
- }
364
- _ => {
365
- current = scope.parent;
366
- }
367
- }
368
- } else {
369
- break;
266
+ let module_scope_id = self.walk_scopes()
267
+ .find(|scope| matches!(&scope.kind, ScopeKind::Module { .. }))
268
+ .map(|scope| scope.id);
269
+ if let Some(scope_id) = module_scope_id {
270
+ if let Some(scope) = self.scopes.get_mut(&scope_id) {
271
+ scope.set_instance_var(name, vtx);
370
272
  }
371
273
  }
372
274
  }
@@ -614,4 +516,72 @@ mod tests {
614
516
  // At top level, no class/module
615
517
  assert_eq!(sm.current_qualified_name(), None);
616
518
  }
519
+
520
+ #[test]
521
+ fn test_constant_registration_and_lookup() {
522
+ let mut sm = ScopeManager::new();
523
+
524
+ // module Api
525
+ sm.current_scope_mut().constants.insert("Api".to_string(), "Api".to_string());
526
+ let api_id = sm.new_scope(ScopeKind::Module { name: "Api".to_string() });
527
+ sm.enter_scope(api_id);
528
+
529
+ // class User (inside Api) — register in parent scope (Api)
530
+ sm.current_scope_mut().constants.insert("User".to_string(), "Api::User".to_string());
531
+ let user_id = sm.new_scope(ScopeKind::Class {
532
+ name: "User".to_string(),
533
+ superclass: None,
534
+ });
535
+ sm.enter_scope(user_id);
536
+
537
+ assert_eq!(sm.lookup_constant("User"), Some("Api::User".to_string()));
538
+ assert_eq!(sm.lookup_constant("Api"), Some("Api".to_string()));
539
+ assert_eq!(sm.lookup_constant("Unknown"), None);
540
+ }
541
+
542
+ #[test]
543
+ fn test_constant_lookup_from_method_scope() {
544
+ let mut sm = ScopeManager::new();
545
+
546
+ sm.current_scope_mut().constants.insert("Api".to_string(), "Api".to_string());
547
+ let api_id = sm.new_scope(ScopeKind::Module { name: "Api".to_string() });
548
+ sm.enter_scope(api_id);
549
+
550
+ sm.current_scope_mut().constants.insert("User".to_string(), "Api::User".to_string());
551
+ let user_id = sm.new_scope(ScopeKind::Class {
552
+ name: "User".to_string(),
553
+ superclass: None,
554
+ });
555
+ sm.enter_scope(user_id);
556
+
557
+ let method_id = sm.new_scope(ScopeKind::Method {
558
+ name: "greet".to_string(),
559
+ receiver_type: None,
560
+ return_vertex: None,
561
+ });
562
+ sm.enter_scope(method_id);
563
+
564
+ // Should find constant by traversing parent scopes from method scope
565
+ assert_eq!(sm.lookup_constant("User"), Some("Api::User".to_string()));
566
+ }
567
+
568
+ #[test]
569
+ fn test_constant_same_name_different_namespaces() {
570
+ let mut sm = ScopeManager::new();
571
+
572
+ // module Api
573
+ let api_id = sm.new_scope(ScopeKind::Module { name: "Api".to_string() });
574
+ sm.enter_scope(api_id);
575
+ sm.current_scope_mut().constants.insert("User".to_string(), "Api::User".to_string());
576
+
577
+ sm.exit_scope();
578
+
579
+ // module Admin
580
+ let admin_id = sm.new_scope(ScopeKind::Module { name: "Admin".to_string() });
581
+ sm.enter_scope(admin_id);
582
+ sm.current_scope_mut().constants.insert("User".to_string(), "Admin::User".to_string());
583
+
584
+ // Inside Admin scope, User should resolve to Admin::User
585
+ assert_eq!(sm.lookup_constant("User"), Some("Admin::User".to_string()));
586
+ }
617
587
  }
@@ -16,6 +16,18 @@ pub trait BoxTrait: Send + Sync {
16
16
  fn ret(&self) -> VertexId;
17
17
  }
18
18
 
19
+ /// Propagate argument types to parameter vertices by adding edges
20
+ /// from each argument vertex to the corresponding parameter vertex.
21
+ fn propagate_arguments(
22
+ arg_vtxs: &[VertexId],
23
+ param_vtxs: Option<&[VertexId]>,
24
+ changes: &mut ChangeSet,
25
+ ) {
26
+ for (arg_vtx, param_vtx) in arg_vtxs.iter().zip(param_vtxs.unwrap_or_default()) {
27
+ changes.add_edge(*arg_vtx, *param_vtx);
28
+ }
29
+ }
30
+
19
31
  /// Box representing a method call
20
32
  #[allow(dead_code)]
21
33
  pub struct MethodCallBox {
@@ -51,6 +63,77 @@ impl MethodCallBox {
51
63
  reschedule_count: 0,
52
64
  }
53
65
  }
66
+
67
+ /// Reschedule this box for re-execution if the limit hasn't been reached.
68
+ /// Handles cases where the receiver has no types yet (e.g., block parameters
69
+ /// that get typed by a later box). If max reschedules are reached, the box
70
+ /// is silently dropped (receiver type remains unknown).
71
+ fn try_reschedule(&mut self, changes: &mut ChangeSet) {
72
+ if self.reschedule_count < MAX_RESCHEDULE_COUNT {
73
+ self.reschedule_count += 1;
74
+ changes.reschedule(self.id);
75
+ }
76
+ }
77
+
78
+ fn process_recv_type(
79
+ &self,
80
+ recv_ty: &Type,
81
+ genv: &mut GlobalEnv,
82
+ changes: &mut ChangeSet,
83
+ ) {
84
+ if let Some(method_info) = genv.resolve_method(recv_ty, &self.method_name) {
85
+ if let Some(return_vtx) = method_info.return_vertex {
86
+ // User-defined method: connect body's return vertex to call site
87
+ changes.add_edge(return_vtx, self.ret);
88
+ propagate_arguments(
89
+ &self.arg_vtxs,
90
+ method_info.param_vertices.as_deref(),
91
+ changes,
92
+ );
93
+ } else {
94
+ // Builtin/RBS method: create source with fixed return type
95
+ let ret_src_id = genv.new_source(method_info.return_type.clone());
96
+ changes.add_edge(ret_src_id, self.ret);
97
+ }
98
+ } else if self.method_name == "new" {
99
+ self.handle_new_call(recv_ty, genv, changes);
100
+ } else if !matches!(recv_ty, Type::Singleton { .. }) {
101
+ // Singleton types with unresolved methods are silently skipped;
102
+ // these are typically RBS class methods not yet supported.
103
+ self.report_type_error(recv_ty, genv);
104
+ }
105
+ }
106
+
107
+ /// Handle `.new` calls: singleton(Foo)#new produces instance(Foo),
108
+ /// and propagates arguments to the `initialize` method's parameters.
109
+ fn handle_new_call(
110
+ &self,
111
+ recv_ty: &Type,
112
+ genv: &mut GlobalEnv,
113
+ changes: &mut ChangeSet,
114
+ ) {
115
+ if let Type::Singleton { name } = recv_ty {
116
+ let instance_type = Type::instance(name.full_name());
117
+
118
+ let ret_src = genv.new_source(instance_type.clone());
119
+ changes.add_edge(ret_src, self.ret);
120
+
121
+ let init_params = genv
122
+ .resolve_method(&instance_type, "initialize")
123
+ .and_then(|info| info.param_vertices.as_deref());
124
+ propagate_arguments(&self.arg_vtxs, init_params, changes);
125
+ } else {
126
+ self.report_type_error(recv_ty, genv);
127
+ }
128
+ }
129
+
130
+ fn report_type_error(&self, recv_ty: &Type, genv: &mut GlobalEnv) {
131
+ genv.record_type_error(
132
+ recv_ty.clone(),
133
+ self.method_name.clone(),
134
+ self.location.clone(),
135
+ );
136
+ }
54
137
  }
55
138
 
56
139
  impl BoxTrait for MethodCallBox {
@@ -63,88 +146,17 @@ impl BoxTrait for MethodCallBox {
63
146
  }
64
147
 
65
148
  fn run(&mut self, genv: &mut GlobalEnv, changes: &mut ChangeSet) {
66
- // Get receiver type (handles both Vertex and Source)
67
- let recv_types: Vec<Type> = if let Some(recv_vertex) = genv.get_vertex(self.recv) {
68
- // Vertex case: may have multiple types
69
- recv_vertex.types.keys().cloned().collect()
70
- } else if let Some(recv_source) = genv.get_source(self.recv) {
71
- // Source case: has one fixed type (e.g., literals)
72
- vec![recv_source.ty.clone()]
73
- } else {
74
- // Receiver not found
149
+ let Some(recv_types) = genv.get_receiver_types(self.recv) else {
75
150
  return;
76
151
  };
77
152
 
78
- // If receiver has no types yet, reschedule this box for later
79
- // This handles cases like block parameters that are typed later
80
153
  if recv_types.is_empty() {
81
- if self.reschedule_count < MAX_RESCHEDULE_COUNT {
82
- self.reschedule_count += 1;
83
- changes.reschedule(self.id);
84
- }
85
- // If max reschedules reached, just skip (receiver type is unknown)
154
+ self.try_reschedule(changes);
86
155
  return;
87
156
  }
88
157
 
89
158
  for recv_ty in recv_types {
90
- // Resolve method
91
- if let Some(method_info) = genv.resolve_method(&recv_ty, &self.method_name) {
92
- if let Some(return_vtx) = method_info.return_vertex {
93
- // User-defined: edge from body's last expr → call site return
94
- changes.add_edge(return_vtx, self.ret);
95
-
96
- // Propagate argument types to parameter vertices
97
- if let Some(param_vtxs) = &method_info.param_vertices {
98
- for (i, param_vtx) in param_vtxs.iter().enumerate() {
99
- if let Some(arg_vtx) = self.arg_vtxs.get(i) {
100
- changes.add_edge(*arg_vtx, *param_vtx);
101
- }
102
- }
103
- }
104
- } else {
105
- // RBS/builtin: create Source with fixed return type
106
- let ret_src_id = genv.new_source(method_info.return_type.clone());
107
- changes.add_edge(ret_src_id, self.ret);
108
- }
109
- } else if self.method_name == "new" {
110
- if let Type::Singleton { name } = &recv_ty {
111
- // singleton(User)#new → instance(User)
112
- let instance_type = Type::instance(name.full_name());
113
- let ret_src = genv.new_source(instance_type.clone());
114
- changes.add_edge(ret_src, self.ret);
115
-
116
- // Propagate arguments to initialize parameters
117
- if let Some(init_info) = genv.resolve_method(&instance_type, "initialize") {
118
- if let Some(param_vtxs) = &init_info.param_vertices {
119
- for (i, param_vtx) in param_vtxs.iter().enumerate() {
120
- if let Some(arg_vtx) = self.arg_vtxs.get(i) {
121
- changes.add_edge(*arg_vtx, *param_vtx);
122
- }
123
- }
124
- }
125
- }
126
- continue;
127
- }
128
- // Non-singleton .new: record error
129
- genv.record_type_error(
130
- recv_ty.clone(),
131
- self.method_name.clone(),
132
- self.location.clone(),
133
- );
134
- } else if matches!(&recv_ty, Type::Singleton { .. }) {
135
- // Skip error for unknown class methods on Singleton types.
136
- // User-defined class methods (def self.foo) are resolved by
137
- // resolve_method above. Only unresolved methods reach here
138
- // (e.g., RBS class methods not yet supported).
139
- continue;
140
- } else {
141
- // Record type error for diagnostic reporting
142
- genv.record_type_error(
143
- recv_ty.clone(),
144
- self.method_name.clone(),
145
- self.location.clone(),
146
- );
147
- }
159
+ self.process_recv_type(&recv_ty, genv, changes);
148
160
  }
149
161
  }
150
162
  }
@@ -241,12 +253,7 @@ impl BoxTrait for BlockParameterTypeBox {
241
253
  }
242
254
 
243
255
  fn run(&mut self, genv: &mut GlobalEnv, changes: &mut ChangeSet) {
244
- // Get receiver types
245
- let recv_types: Vec<Type> = if let Some(recv_vertex) = genv.get_vertex(self.recv_vtx) {
246
- recv_vertex.types.keys().cloned().collect()
247
- } else if let Some(recv_source) = genv.get_source(self.recv_vtx) {
248
- vec![recv_source.ty.clone()]
249
- } else {
256
+ let Some(recv_types) = genv.get_receiver_types(self.recv_vtx) else {
250
257
  return;
251
258
  };
252
259
 
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.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - dak2