method-ray 0.1.2 → 0.1.4

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.
@@ -23,15 +23,22 @@ pub struct MethodCallBox {
23
23
  recv: VertexId,
24
24
  method_name: String,
25
25
  ret: VertexId,
26
+ arg_vtxs: Vec<VertexId>,
26
27
  location: Option<SourceLocation>, // Source code location
28
+ /// Number of times this box has been rescheduled
29
+ reschedule_count: u8,
27
30
  }
28
31
 
32
+ /// Maximum number of reschedules before giving up
33
+ const MAX_RESCHEDULE_COUNT: u8 = 3;
34
+
29
35
  impl MethodCallBox {
30
36
  pub fn new(
31
37
  id: BoxId,
32
38
  recv: VertexId,
33
39
  method_name: String,
34
40
  ret: VertexId,
41
+ arg_vtxs: Vec<VertexId>,
35
42
  location: Option<SourceLocation>,
36
43
  ) -> Self {
37
44
  Self {
@@ -39,7 +46,9 @@ impl MethodCallBox {
39
46
  recv,
40
47
  method_name,
41
48
  ret,
49
+ arg_vtxs,
42
50
  location,
51
+ reschedule_count: 0,
43
52
  }
44
53
  }
45
54
  }
@@ -66,14 +75,66 @@ impl BoxTrait for MethodCallBox {
66
75
  return;
67
76
  };
68
77
 
78
+ // If receiver has no types yet, reschedule this box for later
79
+ // This handles cases like block parameters that are typed later
80
+ 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)
86
+ return;
87
+ }
88
+
69
89
  for recv_ty in recv_types {
70
90
  // Resolve method
71
91
  if let Some(method_info) = genv.resolve_method(&recv_ty, &self.method_name) {
72
- // Create return type as Source
73
- let ret_src_id = genv.new_source(method_info.return_type.clone());
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);
74
95
 
75
- // Add edge to return value
76
- changes.add_edge(ret_src_id, self.ret);
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
+ // (class method RBS registration is not yet supported)
137
+ continue;
77
138
  } else {
78
139
  // Record type error for diagnostic reporting
79
140
  genv.record_type_error(
@@ -86,6 +147,148 @@ impl BoxTrait for MethodCallBox {
86
147
  }
87
148
  }
88
149
 
150
+ /// Box for resolving block parameter types from method call receiver
151
+ ///
152
+ /// When a method with a block is called (e.g., `str.each_char { |c| ... }`),
153
+ /// this box resolves the block parameter types from the method's RBS definition
154
+ /// and propagates them to the block parameter vertices.
155
+ #[allow(dead_code)]
156
+ pub struct BlockParameterTypeBox {
157
+ id: BoxId,
158
+ /// Receiver vertex of the method call
159
+ recv_vtx: VertexId,
160
+ /// Method name being called
161
+ method_name: String,
162
+ /// Block parameter vertices (in order)
163
+ block_param_vtxs: Vec<VertexId>,
164
+ }
165
+
166
+ impl BlockParameterTypeBox {
167
+ pub fn new(
168
+ id: BoxId,
169
+ recv_vtx: VertexId,
170
+ method_name: String,
171
+ block_param_vtxs: Vec<VertexId>,
172
+ ) -> Self {
173
+ Self {
174
+ id,
175
+ recv_vtx,
176
+ method_name,
177
+ block_param_vtxs,
178
+ }
179
+ }
180
+
181
+ /// Check if a type is a type variable name (e.g., Elem, K, V)
182
+ fn is_type_variable_name(name: &str) -> bool {
183
+ matches!(
184
+ name,
185
+ "Elem" | "K" | "V" | "T" | "U" | "A" | "B" | "Element" | "Key" | "Value" | "Out" | "In"
186
+ )
187
+ }
188
+
189
+ /// Try to resolve a type variable from receiver's type arguments.
190
+ ///
191
+ /// For `Array[Integer]#each { |x| }`, the block param type is `Elem`.
192
+ /// This resolves `Elem` → `Integer` using Array's type argument.
193
+ ///
194
+ /// Type variable mapping for common generic classes:
195
+ /// - Array[Elem]: Elem → type_args[0]
196
+ /// - Hash[K, V]: K → type_args[0], V → type_args[1]
197
+ fn resolve_type_variable(ty: &Type, recv_ty: &Type) -> Option<Type> {
198
+ let type_var_name = match ty {
199
+ Type::Instance { name } if Self::is_type_variable_name(name.full_name()) => {
200
+ name.full_name()
201
+ }
202
+ _ => return None, // Not a type variable
203
+ };
204
+
205
+ // Get type arguments from receiver
206
+ let type_args = recv_ty.type_args()?;
207
+ let class_name = recv_ty.base_class_name()?;
208
+
209
+ // Map type variable to type argument index based on class
210
+ let index = match (class_name, type_var_name) {
211
+ // Array[Elem]
212
+ ("Array", "Elem") => 0,
213
+ ("Array", "T") => 0,
214
+ ("Array", "Element") => 0,
215
+ // Hash[K, V]
216
+ ("Hash", "K") | ("Hash", "Key") => 0,
217
+ ("Hash", "V") | ("Hash", "Value") => 1,
218
+ // Generic fallback: first type arg for common names
219
+ (_, "Elem") | (_, "T") | (_, "Element") => 0,
220
+ _ => return None,
221
+ };
222
+
223
+ type_args.get(index).cloned()
224
+ }
225
+ }
226
+
227
+ impl BoxTrait for BlockParameterTypeBox {
228
+ fn id(&self) -> BoxId {
229
+ self.id
230
+ }
231
+
232
+ fn ret(&self) -> VertexId {
233
+ // This box doesn't have a single return value
234
+ // Return first param vtx as a placeholder
235
+ self.block_param_vtxs
236
+ .first()
237
+ .copied()
238
+ .unwrap_or(VertexId(0))
239
+ }
240
+
241
+ fn run(&mut self, genv: &mut GlobalEnv, changes: &mut ChangeSet) {
242
+ // Get receiver types
243
+ let recv_types: Vec<Type> = if let Some(recv_vertex) = genv.get_vertex(self.recv_vtx) {
244
+ recv_vertex.types.keys().cloned().collect()
245
+ } else if let Some(recv_source) = genv.get_source(self.recv_vtx) {
246
+ vec![recv_source.ty.clone()]
247
+ } else {
248
+ return;
249
+ };
250
+
251
+ for recv_ty in recv_types {
252
+ // Resolve method to get block parameter types
253
+ // Clone the block_param_types to avoid borrow issues
254
+ let block_param_types = genv
255
+ .resolve_method(&recv_ty, &self.method_name)
256
+ .and_then(|info| info.block_param_types.clone());
257
+
258
+ if let Some(param_types) = block_param_types {
259
+ // Map block parameter types to vertices
260
+ for (i, param_type) in param_types.iter().enumerate() {
261
+ if i < self.block_param_vtxs.len() {
262
+ let param_vtx = self.block_param_vtxs[i];
263
+
264
+ // Try to resolve type variable from receiver's type arguments
265
+ let resolved_type =
266
+ if let Some(resolved) = Self::resolve_type_variable(param_type, &recv_ty) {
267
+ // Type variable resolved (e.g., Elem → Integer)
268
+ resolved
269
+ } else if let Type::Instance { name } = &param_type {
270
+ if Self::is_type_variable_name(name.full_name()) {
271
+ // Type variable couldn't be resolved, skip
272
+ continue;
273
+ } else {
274
+ // Regular type, use as-is
275
+ param_type.clone()
276
+ }
277
+ } else {
278
+ // Other type (Union, Generic, etc.), use as-is
279
+ param_type.clone()
280
+ };
281
+
282
+ // Create source with the resolved type
283
+ let src_id = genv.new_source(resolved_type);
284
+ changes.add_edge(src_id, param_vtx);
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+
89
292
  #[cfg(test)]
90
293
  mod tests {
91
294
  use super::*;
@@ -112,6 +315,7 @@ mod tests {
112
315
  x_vtx,
113
316
  "upcase".to_string(),
114
317
  ret_vtx,
318
+ vec![],
115
319
  None, // No location in test
116
320
  );
117
321
 
@@ -143,6 +347,7 @@ mod tests {
143
347
  x_vtx,
144
348
  "unknown_method".to_string(),
145
349
  ret_vtx,
350
+ vec![],
146
351
  None, // No location in test
147
352
  );
148
353
 
@@ -154,4 +359,273 @@ mod tests {
154
359
  let ret_vertex = genv.get_vertex(ret_vtx).unwrap();
155
360
  assert_eq!(ret_vertex.show(), "untyped");
156
361
  }
362
+
363
+ #[test]
364
+ fn test_block_param_type_box_simple() {
365
+ let mut genv = GlobalEnv::new();
366
+
367
+ // Register String#each_char with block param type String
368
+ genv.register_builtin_method_with_block(
369
+ Type::string(),
370
+ "each_char",
371
+ Type::string(),
372
+ Some(vec![Type::string()]),
373
+ );
374
+
375
+ // Create receiver vertex with String type
376
+ let recv_vtx = genv.new_vertex();
377
+ let str_src = genv.new_source(Type::string());
378
+ genv.add_edge(str_src, recv_vtx);
379
+
380
+ // Create block parameter vertex
381
+ let param_vtx = genv.new_vertex();
382
+
383
+ // Create and run BlockParameterTypeBox
384
+ let box_id = genv.alloc_box_id();
385
+ let block_box = BlockParameterTypeBox::new(
386
+ box_id,
387
+ recv_vtx,
388
+ "each_char".to_string(),
389
+ vec![param_vtx],
390
+ );
391
+ genv.register_box(box_id, Box::new(block_box));
392
+
393
+ // Run all boxes
394
+ genv.run_all();
395
+
396
+ // Block parameter should now have String type
397
+ assert_eq!(genv.get_vertex(param_vtx).unwrap().show(), "String");
398
+ }
399
+
400
+ #[test]
401
+ fn test_block_param_type_variable_skipped() {
402
+ let mut genv = GlobalEnv::new();
403
+
404
+ // Register Array#each with block param type Elem (type variable)
405
+ genv.register_builtin_method_with_block(
406
+ Type::array(),
407
+ "each",
408
+ Type::array(),
409
+ Some(vec![Type::instance("Elem")]),
410
+ );
411
+
412
+ let recv_vtx = genv.new_vertex();
413
+ let arr_src = genv.new_source(Type::array());
414
+ genv.add_edge(arr_src, recv_vtx);
415
+
416
+ let param_vtx = genv.new_vertex();
417
+
418
+ let box_id = genv.alloc_box_id();
419
+ let block_box = BlockParameterTypeBox::new(
420
+ box_id,
421
+ recv_vtx,
422
+ "each".to_string(),
423
+ vec![param_vtx],
424
+ );
425
+ genv.register_box(box_id, Box::new(block_box));
426
+
427
+ genv.run_all();
428
+
429
+ // Block parameter should remain untyped (type variable skipped)
430
+ assert_eq!(genv.get_vertex(param_vtx).unwrap().show(), "untyped");
431
+ }
432
+
433
+ #[test]
434
+ fn test_block_param_multiple_params() {
435
+ let mut genv = GlobalEnv::new();
436
+
437
+ // Register a method with multiple block params
438
+ genv.register_builtin_method_with_block(
439
+ Type::string(),
440
+ "each_with_index",
441
+ Type::string(),
442
+ Some(vec![Type::string(), Type::integer()]),
443
+ );
444
+
445
+ let recv_vtx = genv.new_vertex();
446
+ let str_src = genv.new_source(Type::string());
447
+ genv.add_edge(str_src, recv_vtx);
448
+
449
+ let param1_vtx = genv.new_vertex();
450
+ let param2_vtx = genv.new_vertex();
451
+
452
+ let box_id = genv.alloc_box_id();
453
+ let block_box = BlockParameterTypeBox::new(
454
+ box_id,
455
+ recv_vtx,
456
+ "each_with_index".to_string(),
457
+ vec![param1_vtx, param2_vtx],
458
+ );
459
+ genv.register_box(box_id, Box::new(block_box));
460
+
461
+ genv.run_all();
462
+
463
+ // Both params should have their types
464
+ assert_eq!(genv.get_vertex(param1_vtx).unwrap().show(), "String");
465
+ assert_eq!(genv.get_vertex(param2_vtx).unwrap().show(), "Integer");
466
+ }
467
+
468
+ #[test]
469
+ fn test_block_param_type_variable_resolved() {
470
+ let mut genv = GlobalEnv::new();
471
+
472
+ // Register Array#each with block param type Elem (type variable)
473
+ genv.register_builtin_method_with_block(
474
+ Type::array(),
475
+ "each",
476
+ Type::array(),
477
+ Some(vec![Type::instance("Elem")]),
478
+ );
479
+
480
+ // Create receiver vertex with Array[Integer] type
481
+ let recv_vtx = genv.new_vertex();
482
+ let arr_src = genv.new_source(Type::array_of(Type::integer()));
483
+ genv.add_edge(arr_src, recv_vtx);
484
+
485
+ let param_vtx = genv.new_vertex();
486
+
487
+ let box_id = genv.alloc_box_id();
488
+ let block_box = BlockParameterTypeBox::new(
489
+ box_id,
490
+ recv_vtx,
491
+ "each".to_string(),
492
+ vec![param_vtx],
493
+ );
494
+ genv.register_box(box_id, Box::new(block_box));
495
+
496
+ genv.run_all();
497
+
498
+ // Block parameter should be Integer (resolved from Array[Integer])
499
+ assert_eq!(genv.get_vertex(param_vtx).unwrap().show(), "Integer");
500
+ }
501
+
502
+ #[test]
503
+ fn test_hash_type_variable_resolved() {
504
+ let mut genv = GlobalEnv::new();
505
+
506
+ // Register Hash#each with block param types K, V
507
+ genv.register_builtin_method_with_block(
508
+ Type::hash(),
509
+ "each",
510
+ Type::hash(),
511
+ Some(vec![Type::instance("K"), Type::instance("V")]),
512
+ );
513
+
514
+ // Create receiver vertex with Hash[String, Integer] type
515
+ let recv_vtx = genv.new_vertex();
516
+ let hash_src = genv.new_source(Type::hash_of(Type::string(), Type::integer()));
517
+ genv.add_edge(hash_src, recv_vtx);
518
+
519
+ let key_vtx = genv.new_vertex();
520
+ let value_vtx = genv.new_vertex();
521
+
522
+ let box_id = genv.alloc_box_id();
523
+ let block_box = BlockParameterTypeBox::new(
524
+ box_id,
525
+ recv_vtx,
526
+ "each".to_string(),
527
+ vec![key_vtx, value_vtx],
528
+ );
529
+ genv.register_box(box_id, Box::new(block_box));
530
+
531
+ genv.run_all();
532
+
533
+ // Block parameters should be resolved from Hash[String, Integer]
534
+ assert_eq!(genv.get_vertex(key_vtx).unwrap().show(), "String");
535
+ assert_eq!(genv.get_vertex(value_vtx).unwrap().show(), "Integer");
536
+ }
537
+
538
+ #[test]
539
+ fn test_method_call_box_user_defined_method() {
540
+ let mut genv = GlobalEnv::new();
541
+
542
+ // Simulate: def name; "Alice"; end
543
+ let body_src = genv.new_source(Type::string());
544
+
545
+ // Register user-defined method User#name with return_vertex
546
+ genv.register_user_method(Type::instance("User"), "name", body_src, vec![]);
547
+
548
+ // Simulate: user.name (receiver has type User)
549
+ let recv_vtx = genv.new_vertex();
550
+ let recv_src = genv.new_source(Type::instance("User"));
551
+ genv.add_edge(recv_src, recv_vtx);
552
+
553
+ let ret_vtx = genv.new_vertex();
554
+ let box_id = genv.alloc_box_id();
555
+ let call_box = MethodCallBox::new(
556
+ box_id,
557
+ recv_vtx,
558
+ "name".to_string(),
559
+ ret_vtx,
560
+ vec![],
561
+ None,
562
+ );
563
+ genv.register_box(box_id, Box::new(call_box));
564
+
565
+ genv.run_all();
566
+
567
+ // Return type should be String (propagated from body's last expression)
568
+ assert_eq!(genv.get_vertex(ret_vtx).unwrap().show(), "String");
569
+ }
570
+
571
+ #[test]
572
+ fn test_method_call_box_param_type_propagation() {
573
+ let mut genv = GlobalEnv::new();
574
+
575
+ // Simulate: def format(value); value.to_s; end
576
+ // 1. Create parameter vertex for 'value'
577
+ let param_vtx = genv.new_vertex();
578
+
579
+ // 2. Register Integer#to_s -> String (builtin)
580
+ genv.register_builtin_method(Type::integer(), "to_s", Type::string());
581
+
582
+ // 3. Create MethodCallBox for value.to_s (inside method body)
583
+ let inner_ret_vtx = genv.new_vertex();
584
+ let inner_box_id = genv.alloc_box_id();
585
+ let inner_call = MethodCallBox::new(
586
+ inner_box_id,
587
+ param_vtx,
588
+ "to_s".to_string(),
589
+ inner_ret_vtx,
590
+ vec![],
591
+ None,
592
+ );
593
+ genv.register_box(inner_box_id, Box::new(inner_call));
594
+
595
+ // 4. Register user-defined method Formatter#format with return_vertex and param_vertices
596
+ genv.register_user_method(
597
+ Type::instance("Formatter"),
598
+ "format",
599
+ inner_ret_vtx,
600
+ vec![param_vtx],
601
+ );
602
+
603
+ // 5. Simulate call: Formatter.new.format(42)
604
+ let recv_vtx = genv.new_vertex();
605
+ let recv_src = genv.new_source(Type::instance("Formatter"));
606
+ genv.add_edge(recv_src, recv_vtx);
607
+
608
+ let arg_vtx = genv.new_source(Type::integer()); // argument: 42
609
+
610
+ let call_ret_vtx = genv.new_vertex();
611
+ let call_box_id = genv.alloc_box_id();
612
+ let call_box = MethodCallBox::new(
613
+ call_box_id,
614
+ recv_vtx,
615
+ "format".to_string(),
616
+ call_ret_vtx,
617
+ vec![arg_vtx],
618
+ None,
619
+ );
620
+ genv.register_box(call_box_id, Box::new(call_box));
621
+
622
+ // Run all boxes
623
+ genv.run_all();
624
+
625
+ // param_vtx should have Integer type (propagated from argument)
626
+ assert_eq!(genv.get_vertex(param_vtx).unwrap().show(), "Integer");
627
+
628
+ // Return type should be String (Integer#to_s -> String)
629
+ assert_eq!(genv.get_vertex(call_ret_vtx).unwrap().show(), "String");
630
+ }
157
631
  }
@@ -1,3 +1,4 @@
1
+ use super::r#box::BoxId;
1
2
  use super::VertexId;
2
3
 
3
4
  /// Manages edge changes for type propagation
@@ -5,6 +6,8 @@ use super::VertexId;
5
6
  pub struct ChangeSet {
6
7
  new_edges: Vec<(VertexId, VertexId)>,
7
8
  edges: Vec<(VertexId, VertexId)>,
9
+ /// Boxes to reschedule for later execution
10
+ reschedule_boxes: Vec<BoxId>,
8
11
  }
9
12
 
10
13
  impl ChangeSet {
@@ -12,6 +15,7 @@ impl ChangeSet {
12
15
  Self {
13
16
  new_edges: Vec::new(),
14
17
  edges: Vec::new(),
18
+ reschedule_boxes: Vec::new(),
15
19
  }
16
20
  }
17
21
 
@@ -20,6 +24,16 @@ impl ChangeSet {
20
24
  self.new_edges.push((src, dst));
21
25
  }
22
26
 
27
+ /// Request to reschedule a Box for later execution
28
+ pub fn reschedule(&mut self, box_id: BoxId) {
29
+ self.reschedule_boxes.push(box_id);
30
+ }
31
+
32
+ /// Get and clear boxes that need to be rescheduled
33
+ pub fn take_reschedule_boxes(&mut self) -> Vec<BoxId> {
34
+ std::mem::take(&mut self.reschedule_boxes)
35
+ }
36
+
23
37
  /// Commit changes and return list of added/removed edges
24
38
  pub fn reinstall(&mut self) -> Vec<EdgeUpdate> {
25
39
  // Remove duplicates
@@ -3,5 +3,5 @@ pub mod change_set;
3
3
  pub mod vertex;
4
4
 
5
5
  pub use change_set::{ChangeSet, EdgeUpdate};
6
- pub use r#box::{BoxId, BoxTrait, MethodCallBox};
6
+ pub use r#box::{BlockParameterTypeBox, BoxId, BoxTrait, MethodCallBox};
7
7
  pub use vertex::{Source, Vertex, VertexId};
data/rust/src/lib.rs CHANGED
@@ -11,7 +11,8 @@ pub mod parser;
11
11
  pub mod source_map;
12
12
  pub mod types;
13
13
 
14
- #[cfg(feature = "ruby-ffi")]
14
+ // rbs module is always available (converter has no Ruby FFI dependency)
15
+ // but loader and error require ruby-ffi feature
15
16
  pub mod rbs;
16
17
 
17
18
  #[cfg(any(feature = "cli", feature = "lsp"))]