method-ray 0.1.9 → 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.
@@ -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;
@@ -41,6 +41,11 @@ pub struct GlobalEnv {
41
41
  /// Module inclusions: class_name → Vec<module_name> (in include order)
42
42
  module_inclusions: HashMap<String, Vec<String>>,
43
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>>,
44
49
  }
45
50
 
46
51
  impl GlobalEnv {
@@ -52,6 +57,8 @@ impl GlobalEnv {
52
57
  type_errors: Vec::new(),
53
58
  scope_manager: ScopeManager::new(),
54
59
  module_inclusions: HashMap::new(),
60
+ superclass_map: HashMap::new(),
61
+ module_extensions: HashMap::new(),
55
62
  }
56
63
  }
57
64
 
@@ -163,14 +170,31 @@ impl GlobalEnv {
163
170
 
164
171
  /// Resolve method
165
172
  pub fn resolve_method(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> {
173
+ let ctx = ResolutionContext {
174
+ inclusions: &self.module_inclusions,
175
+ superclass_map: &self.superclass_map,
176
+ extensions: &self.module_extensions,
177
+ };
166
178
  self.method_registry
167
- .resolve(recv_ty, method_name, &self.module_inclusions)
179
+ .resolve(recv_ty, method_name, &ctx)
168
180
  }
169
181
 
170
182
  /// Record that a class includes a module
171
183
  pub fn record_include(&mut self, class_name: &str, module_name: &str) {
172
- self.module_inclusions
173
- .entry(class_name.to_string())
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())
174
198
  .or_default()
175
199
  .push(module_name.to_string());
176
200
  }
@@ -247,6 +271,30 @@ impl GlobalEnv {
247
271
  });
248
272
  self.scope_manager.enter_scope(scope_id);
249
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
+
250
298
  scope_id
251
299
  }
252
300
 
@@ -293,78 +341,3 @@ impl Default for GlobalEnv {
293
341
  Self::new()
294
342
  }
295
343
  }
296
-
297
- #[cfg(test)]
298
- mod tests {
299
- use super::*;
300
-
301
- #[test]
302
- fn test_global_env_new_vertex() {
303
- let mut genv = GlobalEnv::new();
304
-
305
- let v1 = genv.new_vertex();
306
- let v2 = genv.new_vertex();
307
-
308
- assert_eq!(v1.0, 0);
309
- assert_eq!(v2.0, 1);
310
- }
311
-
312
- #[test]
313
- fn test_global_env_new_source() {
314
- let mut genv = GlobalEnv::new();
315
-
316
- let s1 = genv.new_source(Type::string());
317
- let s2 = genv.new_source(Type::integer());
318
-
319
- assert_eq!(genv.get_source(s1).unwrap().ty.show(), "String");
320
- assert_eq!(genv.get_source(s2).unwrap().ty.show(), "Integer");
321
- }
322
-
323
- #[test]
324
- fn test_global_env_edge_propagation() {
325
- let mut genv = GlobalEnv::new();
326
-
327
- // Source<String> -> Vertex
328
- let src = genv.new_source(Type::string());
329
- let vtx = genv.new_vertex();
330
-
331
- genv.add_edge(src, vtx);
332
-
333
- // Verify type propagated to Vertex
334
- assert_eq!(genv.get_vertex(vtx).unwrap().show(), "String");
335
- }
336
-
337
- #[test]
338
- fn test_global_env_chain_propagation() {
339
- let mut genv = GlobalEnv::new();
340
-
341
- // Source<String> -> Vertex1 -> Vertex2
342
- let src = genv.new_source(Type::string());
343
- let v1 = genv.new_vertex();
344
- let v2 = genv.new_vertex();
345
-
346
- genv.add_edge(src, v1);
347
- genv.add_edge(v1, v2);
348
-
349
- // Verify type propagated to v2
350
- assert_eq!(genv.get_vertex(v1).unwrap().show(), "String");
351
- assert_eq!(genv.get_vertex(v2).unwrap().show(), "String");
352
- }
353
-
354
- #[test]
355
- fn test_global_env_union_propagation() {
356
- let mut genv = GlobalEnv::new();
357
-
358
- // Source<String> -> Vertex
359
- // Source<Integer> -> Vertex
360
- let src1 = genv.new_source(Type::string());
361
- let src2 = genv.new_source(Type::integer());
362
- let vtx = genv.new_vertex();
363
-
364
- genv.add_edge(src1, vtx);
365
- genv.add_edge(src2, vtx);
366
-
367
- // Verify it became Union type
368
- assert_eq!(genv.get_vertex(vtx).unwrap().show(), "(Integer | String)");
369
- }
370
- }
@@ -40,53 +40,3 @@ impl LocalEnv {
40
40
  self.locals.iter()
41
41
  }
42
42
  }
43
-
44
- #[cfg(test)]
45
- mod tests {
46
- use super::*;
47
-
48
- #[test]
49
- fn test_local_env_default() {
50
- let lenv = LocalEnv::default();
51
- assert_eq!(lenv.get_var("x"), None);
52
- }
53
-
54
- #[test]
55
- fn test_local_env() {
56
- let mut lenv = LocalEnv::new();
57
-
58
- lenv.new_var("x".to_string(), VertexId(1));
59
- lenv.new_var("y".to_string(), VertexId(2));
60
-
61
- assert_eq!(lenv.get_var("x"), Some(VertexId(1)));
62
- assert_eq!(lenv.get_var("y"), Some(VertexId(2)));
63
- assert_eq!(lenv.get_var("z"), None);
64
- }
65
-
66
- #[test]
67
- fn test_local_env_remove_var() {
68
- let mut lenv = LocalEnv::new();
69
-
70
- lenv.new_var("e".to_string(), VertexId(1));
71
- assert_eq!(lenv.get_var("e"), Some(VertexId(1)));
72
-
73
- lenv.remove_var("e");
74
- assert_eq!(lenv.get_var("e"), None);
75
- }
76
-
77
- #[test]
78
- fn test_local_env_remove_nonexistent() {
79
- let mut lenv = LocalEnv::new();
80
- lenv.remove_var("x"); // should not panic
81
- }
82
-
83
- #[test]
84
- fn test_local_env_override() {
85
- let mut lenv = LocalEnv::new();
86
-
87
- lenv.new_var("x".to_string(), VertexId(1));
88
- lenv.new_var("x".to_string(), VertexId(2)); // Override
89
-
90
- assert_eq!(lenv.get_var("x"), Some(VertexId(2)));
91
- }
92
- }
@@ -1,6 +1,6 @@
1
1
  //! Method registration and resolution
2
2
 
3
- use std::collections::HashMap;
3
+ use std::collections::{HashMap, HashSet};
4
4
 
5
5
  use crate::graph::VertexId;
6
6
  use crate::types::Type;
@@ -9,6 +9,13 @@ use smallvec::SmallVec;
9
9
  const OBJECT_CLASS: &str = "Object";
10
10
  const KERNEL_MODULE: &str = "Kernel";
11
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
+
12
19
  /// Method information
13
20
  #[derive(Debug, Clone)]
14
21
  pub struct MethodInfo {
@@ -79,17 +86,32 @@ impl MethodRegistry {
79
86
  );
80
87
  }
81
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
+
82
102
  /// Build the method resolution order (MRO) fallback chain for a receiver type.
83
103
  ///
84
104
  /// Returns a list of types to search in order:
85
105
  /// 1. Exact receiver type
86
106
  /// 2. Generic → base class (e.g., Array[Integer] → Array)
87
- /// 3. Included modules (last included first, matching Ruby MRO)
88
- /// 4. Object (for Instance/Generic types only)
89
- /// 5. Kernel (for Instance/Generic types only)
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)
90
112
  fn fallback_chain(
91
113
  recv_ty: &Type,
92
- inclusions: &HashMap<String, Vec<String>>,
114
+ ctx: &ResolutionContext,
93
115
  ) -> SmallVec<[Type; 8]> {
94
116
  let mut chain = SmallVec::new();
95
117
  chain.push(recv_ty.clone());
@@ -98,14 +120,24 @@ impl MethodRegistry {
98
120
  chain.push(Type::Instance { name: name.clone() });
99
121
  }
100
122
 
101
- // MRO for Instance/Generic: included modules → Object → Kernel
123
+ // MRO for Instance/Generic: included modules → superclass chain → Object → Kernel
102
124
  if matches!(recv_ty, Type::Instance { .. } | Type::Generic { .. }) {
103
- // Included modules (reverse order = last included has highest priority)
125
+ // Included modules of self (reverse order = last included has highest priority)
104
126
  if let Some(class_name) = recv_ty.base_class_name() {
105
- if let Some(modules) = inclusions.get(class_name) {
106
- for module_name in modules.iter().rev() {
107
- chain.push(Type::instance(module_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;
108
137
  }
138
+ chain.push(Type::instance(parent));
139
+ Self::add_included_modules(&mut chain, parent, ctx.inclusions);
140
+ current = parent.clone();
109
141
  }
110
142
  }
111
143
 
@@ -113,242 +145,29 @@ impl MethodRegistry {
113
145
  chain.push(Type::instance(KERNEL_MODULE));
114
146
  }
115
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
+
116
153
  chain
117
154
  }
118
155
 
119
156
  /// Resolve a method for a receiver type
120
157
  ///
121
158
  /// Searches the MRO fallback chain: exact type → base class (for generics)
122
- /// → included modules → Object → Kernel.
123
- /// For non-instance types (Singleton, Nil, Union, Bot), only exact match is attempted.
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.
124
162
  pub fn resolve(
125
163
  &self,
126
164
  recv_ty: &Type,
127
165
  method_name: &str,
128
- inclusions: &HashMap<String, Vec<String>>,
166
+ ctx: &ResolutionContext,
129
167
  ) -> Option<&MethodInfo> {
130
168
  let method_key = method_name.to_string();
131
- Self::fallback_chain(recv_ty, inclusions)
169
+ Self::fallback_chain(recv_ty, ctx)
132
170
  .into_iter()
133
171
  .find_map(|ty| self.methods.get(&(ty, method_key.clone())))
134
172
  }
135
173
  }
136
-
137
- #[cfg(test)]
138
- mod tests {
139
- use super::*;
140
-
141
- #[test]
142
- fn test_register_and_resolve() {
143
- let mut registry = MethodRegistry::new();
144
- registry.register(Type::string(), "length", Type::integer());
145
-
146
- let info = registry.resolve(&Type::string(), "length", &HashMap::new()).unwrap();
147
- assert_eq!(info.return_type.base_class_name(), Some("Integer"));
148
- }
149
-
150
- #[test]
151
- fn test_resolve_not_found() {
152
- let registry = MethodRegistry::new();
153
- assert!(registry.resolve(&Type::string(), "unknown", &HashMap::new()).is_none());
154
- }
155
-
156
- #[test]
157
- fn test_register_user_method_and_resolve() {
158
- let mut registry = MethodRegistry::new();
159
- let return_vtx = VertexId(42);
160
- registry.register_user_method(Type::instance("User"), "name", return_vtx, vec![], None);
161
-
162
- let info = registry.resolve(&Type::instance("User"), "name", &HashMap::new()).unwrap();
163
- assert_eq!(info.return_vertex, Some(VertexId(42)));
164
- assert_eq!(info.return_type, Type::Bot);
165
- }
166
-
167
- #[test]
168
- fn test_register_user_method_with_param_vertices() {
169
- let mut registry = MethodRegistry::new();
170
- let return_vtx = VertexId(10);
171
- let param_vtxs = vec![VertexId(20), VertexId(21)];
172
- registry.register_user_method(
173
- Type::instance("Calc"),
174
- "add",
175
- return_vtx,
176
- param_vtxs,
177
- None,
178
- );
179
-
180
- let info = registry.resolve(&Type::instance("Calc"), "add", &HashMap::new()).unwrap();
181
- assert_eq!(info.return_vertex, Some(VertexId(10)));
182
- let pvs = info.param_vertices.as_ref().unwrap();
183
- assert_eq!(pvs.len(), 2);
184
- assert_eq!(pvs[0], VertexId(20));
185
- assert_eq!(pvs[1], VertexId(21));
186
- }
187
-
188
- // --- Object/Kernel fallback ---
189
-
190
- #[test]
191
- fn test_resolve_falls_back_to_object() {
192
- let mut registry = MethodRegistry::new();
193
- registry.register(Type::instance("Object"), "nil?", Type::instance("TrueClass"));
194
- let info = registry.resolve(&Type::instance("CustomClass"), "nil?", &HashMap::new()).unwrap();
195
- assert_eq!(info.return_type.base_class_name(), Some("TrueClass"));
196
- }
197
-
198
- #[test]
199
- fn test_resolve_falls_back_to_kernel() {
200
- let mut registry = MethodRegistry::new();
201
- registry.register(Type::instance("Kernel"), "puts", Type::Nil);
202
- let info = registry.resolve(&Type::instance("MyApp"), "puts", &HashMap::new()).unwrap();
203
- assert_eq!(info.return_type, Type::Nil);
204
- }
205
-
206
- #[test]
207
- fn test_resolve_object_before_kernel() {
208
- let mut registry = MethodRegistry::new();
209
- registry.register(Type::instance("Object"), "to_s", Type::string());
210
- registry.register(Type::instance("Kernel"), "to_s", Type::integer());
211
- let info = registry.resolve(&Type::instance("Anything"), "to_s", &HashMap::new()).unwrap();
212
- assert_eq!(info.return_type.base_class_name(), Some("String"));
213
- }
214
-
215
- #[test]
216
- fn test_resolve_exact_match_over_fallback() {
217
- let mut registry = MethodRegistry::new();
218
- registry.register(Type::string(), "length", Type::integer());
219
- registry.register(Type::instance("Object"), "length", Type::string());
220
- let info = registry.resolve(&Type::string(), "length", &HashMap::new()).unwrap();
221
- assert_eq!(info.return_type.base_class_name(), Some("Integer"));
222
- }
223
-
224
- // --- Types that skip fallback ---
225
-
226
- #[test]
227
- fn test_singleton_type_skips_fallback() {
228
- let mut registry = MethodRegistry::new();
229
- registry.register(Type::instance("Kernel"), "puts", Type::Nil);
230
- assert!(registry.resolve(&Type::singleton("User"), "puts", &HashMap::new()).is_none());
231
- }
232
-
233
- #[test]
234
- fn test_nil_type_skips_fallback() {
235
- let mut registry = MethodRegistry::new();
236
- registry.register(Type::instance("Kernel"), "puts", Type::Nil);
237
- assert!(registry.resolve(&Type::Nil, "puts", &HashMap::new()).is_none());
238
- }
239
-
240
- #[test]
241
- fn test_union_type_skips_fallback() {
242
- let mut registry = MethodRegistry::new();
243
- registry.register(Type::instance("Kernel"), "puts", Type::Nil);
244
- let union_ty = Type::Union(vec![Type::string(), Type::integer()]);
245
- assert!(registry.resolve(&union_ty, "puts", &HashMap::new()).is_none());
246
- }
247
-
248
- #[test]
249
- fn test_bot_type_skips_fallback() {
250
- let mut registry = MethodRegistry::new();
251
- registry.register(Type::instance("Kernel"), "puts", Type::Nil);
252
- assert!(registry.resolve(&Type::Bot, "puts", &HashMap::new()).is_none());
253
- }
254
-
255
- // --- Generic type fallback chain ---
256
-
257
- #[test]
258
- fn test_resolve_generic_falls_back_to_kernel() {
259
- let mut registry = MethodRegistry::new();
260
- registry.register(Type::instance("Kernel"), "puts", Type::Nil);
261
- let generic_type = Type::array_of(Type::integer());
262
- let info = registry.resolve(&generic_type, "puts", &HashMap::new()).unwrap();
263
- assert_eq!(info.return_type, Type::Nil);
264
- }
265
-
266
- #[test]
267
- fn test_resolve_generic_full_chain() {
268
- // Verify the 4-step fallback: Generic[T] → Base → Object → Kernel
269
- let mut registry = MethodRegistry::new();
270
- registry.register(Type::instance("Kernel"), "object_id", Type::integer());
271
- let generic_type = Type::array_of(Type::string());
272
- // Array[String] → Array (none) → Object (none) → Kernel (exists)
273
- let info = registry.resolve(&generic_type, "object_id", &HashMap::new()).unwrap();
274
- assert_eq!(info.return_type.base_class_name(), Some("Integer"));
275
- }
276
-
277
- // --- Namespaced class fallback ---
278
-
279
- #[test]
280
- fn test_resolve_namespaced_class_falls_back_to_object() {
281
- let mut registry = MethodRegistry::new();
282
- registry.register(Type::instance("Object"), "class", Type::string());
283
- let info = registry.resolve(&Type::instance("Api::V1::User"), "class", &HashMap::new()).unwrap();
284
- assert_eq!(info.return_type.base_class_name(), Some("String"));
285
- }
286
-
287
- // --- Include (mixin) fallback ---
288
-
289
- #[test]
290
- fn test_resolve_with_include() {
291
- let mut registry = MethodRegistry::new();
292
- registry.register(Type::instance("Greetable"), "greet", Type::string());
293
-
294
- let mut inclusions = HashMap::new();
295
- inclusions.insert("User".to_string(), vec!["Greetable".to_string()]);
296
-
297
- let info = registry.resolve(&Type::instance("User"), "greet", &inclusions).unwrap();
298
- assert_eq!(info.return_type.base_class_name(), Some("String"));
299
- }
300
-
301
- #[test]
302
- fn test_resolve_include_order() {
303
- // include A; include B → B's method found first (MRO: last included wins)
304
- let mut registry = MethodRegistry::new();
305
- registry.register(Type::instance("A"), "foo", Type::string());
306
- registry.register(Type::instance("B"), "foo", Type::integer());
307
-
308
- let mut inclusions = HashMap::new();
309
- inclusions.insert("User".to_string(), vec!["A".to_string(), "B".to_string()]);
310
-
311
- let info = registry.resolve(&Type::instance("User"), "foo", &inclusions).unwrap();
312
- assert_eq!(info.return_type.base_class_name(), Some("Integer"));
313
- }
314
-
315
- #[test]
316
- fn test_resolve_class_method_over_include() {
317
- // Class's own method takes priority over included module
318
- let mut registry = MethodRegistry::new();
319
- registry.register(Type::instance("Greetable"), "greet", Type::string());
320
- registry.register(Type::instance("User"), "greet", Type::integer());
321
-
322
- let mut inclusions = HashMap::new();
323
- inclusions.insert("User".to_string(), vec!["Greetable".to_string()]);
324
-
325
- let info = registry.resolve(&Type::instance("User"), "greet", &inclusions).unwrap();
326
- assert_eq!(info.return_type.base_class_name(), Some("Integer"));
327
- }
328
-
329
- #[test]
330
- fn test_resolve_include_before_object() {
331
- // Included module is searched before Object in MRO
332
- let mut registry = MethodRegistry::new();
333
- registry.register(Type::instance("Object"), "foo", Type::string());
334
- registry.register(Type::instance("MyModule"), "foo", Type::integer());
335
-
336
- let mut inclusions = HashMap::new();
337
- inclusions.insert("User".to_string(), vec!["MyModule".to_string()]);
338
-
339
- let info = registry.resolve(&Type::instance("User"), "foo", &inclusions).unwrap();
340
- assert_eq!(info.return_type.base_class_name(), Some("Integer"));
341
- }
342
-
343
- #[test]
344
- fn test_singleton_type_skips_include_fallback() {
345
- // include adds instance methods only — Singleton (class-level) should NOT resolve
346
- let mut registry = MethodRegistry::new();
347
- registry.register(Type::instance("Greetable"), "greet", Type::string());
348
-
349
- let mut inclusions = HashMap::new();
350
- inclusions.insert("User".to_string(), vec!["Greetable".to_string()]);
351
-
352
- assert!(registry.resolve(&Type::singleton("User"), "greet", &inclusions).is_none());
353
- }
354
- }