method-ray 0.1.8 → 0.1.9

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/{rust → core}/Cargo.toml +1 -1
  4. data/core/src/analyzer/assignments.rs +499 -0
  5. data/{rust → core}/src/analyzer/blocks.rs +140 -0
  6. data/{rust → core}/src/analyzer/definitions.rs +6 -5
  7. data/{rust → core}/src/analyzer/dispatch.rs +295 -31
  8. data/{rust → core}/src/analyzer/exceptions.rs +104 -3
  9. data/{rust → core}/src/analyzer/install.rs +16 -1
  10. data/{rust → core}/src/analyzer/literals.rs +3 -17
  11. data/{rust → core}/src/analyzer/loops.rs +126 -1
  12. data/{rust → core}/src/analyzer/mod.rs +1 -0
  13. data/{rust → core}/src/analyzer/parameters.rs +160 -0
  14. data/core/src/analyzer/super_calls.rs +285 -0
  15. data/{rust → core}/src/env/global_env.rs +18 -4
  16. data/{rust → core}/src/env/method_registry.rs +109 -23
  17. data/{rust → core}/src/env/scope.rs +78 -0
  18. data/{rust → core}/src/types.rs +11 -0
  19. data/ext/Cargo.toml +2 -2
  20. data/lib/methodray/binary_locator.rb +2 -2
  21. data/lib/methodray/commands.rb +1 -1
  22. data/lib/methodray/version.rb +1 -1
  23. metadata +54 -53
  24. data/rust/src/analyzer/assignments.rs +0 -152
  25. /data/{rust → core}/src/analyzer/attributes.rs +0 -0
  26. /data/{rust → core}/src/analyzer/calls.rs +0 -0
  27. /data/{rust → core}/src/analyzer/conditionals.rs +0 -0
  28. /data/{rust → core}/src/analyzer/operators.rs +0 -0
  29. /data/{rust → core}/src/analyzer/parentheses.rs +0 -0
  30. /data/{rust → core}/src/analyzer/returns.rs +0 -0
  31. /data/{rust → core}/src/analyzer/variables.rs +0 -0
  32. /data/{rust → core}/src/cache/mod.rs +0 -0
  33. /data/{rust → core}/src/cache/rbs_cache.rs +0 -0
  34. /data/{rust → core}/src/checker.rs +0 -0
  35. /data/{rust → core}/src/cli/args.rs +0 -0
  36. /data/{rust → core}/src/cli/commands.rs +0 -0
  37. /data/{rust → core}/src/cli/mod.rs +0 -0
  38. /data/{rust → core}/src/diagnostics/diagnostic.rs +0 -0
  39. /data/{rust → core}/src/diagnostics/formatter.rs +0 -0
  40. /data/{rust → core}/src/diagnostics/mod.rs +0 -0
  41. /data/{rust → core}/src/env/box_manager.rs +0 -0
  42. /data/{rust → core}/src/env/local_env.rs +0 -0
  43. /data/{rust → core}/src/env/mod.rs +0 -0
  44. /data/{rust → core}/src/env/type_error.rs +0 -0
  45. /data/{rust → core}/src/env/vertex_manager.rs +0 -0
  46. /data/{rust → core}/src/graph/box.rs +0 -0
  47. /data/{rust → core}/src/graph/change_set.rs +0 -0
  48. /data/{rust → core}/src/graph/mod.rs +0 -0
  49. /data/{rust → core}/src/graph/vertex.rs +0 -0
  50. /data/{rust → core}/src/lib.rs +0 -0
  51. /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
  52. /data/{rust → core}/src/lsp/main.rs +0 -0
  53. /data/{rust → core}/src/lsp/mod.rs +0 -0
  54. /data/{rust → core}/src/lsp/server.rs +0 -0
  55. /data/{rust → core}/src/main.rs +0 -0
  56. /data/{rust → core}/src/parser.rs +0 -0
  57. /data/{rust → core}/src/rbs/converter.rs +0 -0
  58. /data/{rust → core}/src/rbs/error.rs +0 -0
  59. /data/{rust → core}/src/rbs/loader.rs +0 -0
  60. /data/{rust → core}/src/rbs/mod.rs +0 -0
  61. /data/{rust → core}/src/source_map.rs +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f44709f8354439522722c8ea65da30463fae9de68d9be1aa07c8dac9627a03ce
4
- data.tar.gz: 2a09885854020892cf93a1c85e83735a23be60240fcbee09f77fa8c9fbf58801
3
+ metadata.gz: fc17c7a50f5d33ee2d219ff5f28c1447b162cdc8c9e832e2e3a4c4d89885dd98
4
+ data.tar.gz: 6a746447dcfe70be3c7da02052697bd5dec36306810d972b5c05e5800e00e566
5
5
  SHA512:
6
- metadata.gz: 4be6a23881adf54e88039c4e62255f09aa52005cf4085e6cbdc1910f642dae8e763ec7993445051bd3cff20693b5576e417f47b0864359c60db6becf1c21e204
7
- data.tar.gz: cc9b502af18acc8463e9cddf95cc16b8ed6095ffc34d826642fe8193b2f4cc16aa7490a96f930493fe30fc4af21412cf2ff515f8f45e4d04bf9ec704f256ea26
6
+ metadata.gz: 2c4ceefe7929f02f14d2574dda3b5f04e8f5e67d1fd7746939318d5e7f49b932ad07d78a16975f83c16014e70222d251912608eea8d8a2ee901ea1f13415a187
7
+ data.tar.gz: 57c9411ff5c60a8fa5e3c5283b401c3869ef3a644d4959e0b426b49570d15641141e2aa4da98deaeadc216656257e3eebff9a5ba9209872b53bd8f018e59bbb5
data/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ 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.9] - 2026-03-15
9
+
10
+ ### Added
11
+
12
+ - Add linter CI workflow for Clippy and RuboCop ([#61](https://github.com/dak2/method-ray/pull/61))
13
+ - Add unit tests for blocks and parameters analyzers ([#60](https://github.com/dak2/method-ray/pull/60))
14
+ - Add super call type inference support ([#59](https://github.com/dak2/method-ray/pull/59))
15
+ - Add for loop type inference support ([#58](https://github.com/dak2/method-ray/pull/58))
16
+ - Add module include support for mixin method resolution ([#57](https://github.com/dak2/method-ray/pull/57))
17
+ - Add complete multi-assignment type inference ([#56](https://github.com/dak2/method-ray/pull/56))
18
+ - Add auto-tagging workflow for release PR merges ([#53](https://github.com/dak2/method-ray/pull/53))
19
+
20
+ ### Changed
21
+
22
+ - Rename rust/ directory to core/ ([#55](https://github.com/dak2/method-ray/pull/55))
23
+ - Infer actual exception types in rescue clauses ([#54](https://github.com/dak2/method-ray/pull/54))
24
+
8
25
  ## [0.1.8] - 2026-03-09
9
26
 
10
27
  ### Added
@@ -123,6 +140,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
123
140
  - Initial release
124
141
  - `methodray check` - Static type checking for Ruby files
125
142
 
143
+ [0.1.9]: https://github.com/dak2/method-ray/releases/tag/v0.1.9
126
144
  [0.1.8]: https://github.com/dak2/method-ray/releases/tag/v0.1.8
127
145
  [0.1.7]: https://github.com/dak2/method-ray/releases/tag/v0.1.7
128
146
  [0.1.6]: https://github.com/dak2/method-ray/releases/tag/v0.1.6
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray-core"
3
- version = "0.1.8"
3
+ version = "0.1.9"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -0,0 +1,499 @@
1
+ //! Multiple Assignment Handlers - Processing Ruby multiple assignment
2
+ //!
3
+ //! Supports: ArrayNode RHS with 1:1 mapping, LHS > RHS nil fill,
4
+ //! splat targets (*rest) as Array type, and basic single-expression RHS decomposition.
5
+ //! TODO: Support RHS as method return value decomposition (requires graph lazy resolution)
6
+
7
+ use crate::env::{GlobalEnv, LocalEnv};
8
+ use crate::graph::{ChangeSet, VertexId};
9
+ use crate::types::Type;
10
+
11
+ use super::bytes_to_name;
12
+ use super::variables::install_local_var_write;
13
+
14
+ /// Install an RHS node and assign it to a named local variable.
15
+ /// Falls back to an untyped vertex when `install_node` returns `None`.
16
+ fn install_target(
17
+ genv: &mut GlobalEnv,
18
+ lenv: &mut LocalEnv,
19
+ changes: &mut ChangeSet,
20
+ source: &str,
21
+ var_name: String,
22
+ rhs_node: &ruby_prism::Node,
23
+ ) -> VertexId {
24
+ if let Some(rv) = super::install::install_node(genv, lenv, changes, source, rhs_node) {
25
+ install_local_var_write(genv, lenv, changes, var_name, rv)
26
+ } else {
27
+ let vtx = genv.new_vertex();
28
+ lenv.new_var(var_name, vtx);
29
+ vtx
30
+ }
31
+ }
32
+
33
+ /// Assign `Type::Nil` to a named local variable.
34
+ fn install_nil_target(
35
+ genv: &mut GlobalEnv,
36
+ lenv: &mut LocalEnv,
37
+ changes: &mut ChangeSet,
38
+ var_name: String,
39
+ ) -> VertexId {
40
+ let nil_src = genv.new_source(Type::Nil);
41
+ install_local_var_write(genv, lenv, changes, var_name, nil_src)
42
+ }
43
+
44
+ /// Extract the splat target variable name from a `node.rest()` result.
45
+ fn splat_var_name(rest_node: &ruby_prism::Node) -> Option<String> {
46
+ let splat = rest_node.as_splat_node()?;
47
+ let expr = splat.expression()?;
48
+ let target = expr.as_local_variable_target_node()?;
49
+ Some(bytes_to_name(target.name().as_slice()))
50
+ }
51
+
52
+ /// Collect unique types from a slice of elements, returning element type for Array[T].
53
+ /// Only nodes that resolve to a Source (fixed type) contribute; Vertex-type nodes
54
+ /// are excluded (known limitation — requires lazy resolution for method return values).
55
+ fn collect_element_type(
56
+ genv: &mut GlobalEnv,
57
+ lenv: &mut LocalEnv,
58
+ changes: &mut ChangeSet,
59
+ source: &str,
60
+ elements: &[ruby_prism::Node],
61
+ ) -> Type {
62
+ let mut types: Vec<Type> = Vec::new();
63
+ for elem in elements {
64
+ if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, elem) {
65
+ if let Some(src) = genv.get_source(vtx) {
66
+ if !types.contains(&src.ty) {
67
+ types.push(src.ty.clone());
68
+ }
69
+ }
70
+ }
71
+ }
72
+ if types.is_empty() {
73
+ Type::Bot
74
+ } else {
75
+ Type::union_of(types)
76
+ }
77
+ }
78
+
79
+ /// Process multiple assignment node (e.g., `a, b = 1, "hello"`)
80
+ pub(crate) fn process_multi_write_node(
81
+ genv: &mut GlobalEnv,
82
+ lenv: &mut LocalEnv,
83
+ changes: &mut ChangeSet,
84
+ source: &str,
85
+ node: &ruby_prism::MultiWriteNode,
86
+ ) -> Option<VertexId> {
87
+ let value = node.value();
88
+ let mut last_vtx = None;
89
+
90
+ if let Some(array_node) = value.as_array_node() {
91
+ let lefts = node.lefts();
92
+ let elements: Vec<_> = array_node.elements().iter().collect();
93
+ let total = elements.len();
94
+ let lefts_count = lefts.len();
95
+ let rights = node.rights();
96
+ let rights_count = rights.len();
97
+
98
+ // Phase 1: Left targets — assign from start, nil for missing RHS
99
+ for (i, target) in lefts.iter().enumerate() {
100
+ if let Some(target_node) = target.as_local_variable_target_node() {
101
+ let var_name = bytes_to_name(target_node.name().as_slice());
102
+ if i < total {
103
+ last_vtx = Some(install_target(
104
+ genv,
105
+ lenv,
106
+ changes,
107
+ source,
108
+ var_name,
109
+ &elements[i],
110
+ ));
111
+ } else {
112
+ last_vtx = Some(install_nil_target(genv, lenv, changes, var_name));
113
+ }
114
+ }
115
+ }
116
+
117
+ // Phase 2: Splat target (*rest) — collect middle elements (after lefts, before rights) into Array[T]
118
+ if let Some(rest_node) = node.rest() {
119
+ if let Some(var_name) = splat_var_name(&rest_node) {
120
+ let splat_start = lefts_count;
121
+ let splat_end = total.saturating_sub(rights_count);
122
+ let splat_elements = if splat_start < splat_end {
123
+ &elements[splat_start..splat_end]
124
+ } else {
125
+ &elements[0..0]
126
+ };
127
+ let element_type = collect_element_type(
128
+ genv,
129
+ lenv,
130
+ changes,
131
+ source,
132
+ splat_elements,
133
+ );
134
+ let array_src = genv.new_source(Type::array_of(element_type));
135
+ last_vtx = Some(install_local_var_write(
136
+ genv, lenv, changes, var_name, array_src,
137
+ ));
138
+ }
139
+ }
140
+
141
+ // Phase 3: Right targets — assigned from end of elements, nil if overlapping with lefts
142
+ for (i, target) in rights.iter().enumerate() {
143
+ if let Some(target_node) = target.as_local_variable_target_node() {
144
+ let var_name = bytes_to_name(target_node.name().as_slice());
145
+ let signed_idx = total as isize - rights_count as isize + i as isize;
146
+ if signed_idx >= lefts_count as isize && (signed_idx as usize) < total {
147
+ last_vtx = Some(install_target(
148
+ genv,
149
+ lenv,
150
+ changes,
151
+ source,
152
+ var_name,
153
+ &elements[signed_idx as usize],
154
+ ));
155
+ } else {
156
+ last_vtx = Some(install_nil_target(genv, lenv, changes, var_name));
157
+ }
158
+ }
159
+ }
160
+ } else {
161
+ // RHS is a single expression (not comma-separated)
162
+ let rhs_vtx = super::install::install_node(genv, lenv, changes, source, &value);
163
+
164
+ let rhs_type = rhs_vtx
165
+ .and_then(|vtx| genv.get_source(vtx))
166
+ .map(|src| src.ty.clone());
167
+
168
+ // If RHS is Array[T], each target gets T; otherwise first target gets RHS, rest get nil
169
+ let element_type = rhs_type
170
+ .as_ref()
171
+ .and_then(|ty| ty.type_args())
172
+ .and_then(|args| args.first().cloned());
173
+
174
+ for (i, target) in node.lefts().iter().enumerate() {
175
+ if let Some(target_node) = target.as_local_variable_target_node() {
176
+ let var_name = bytes_to_name(target_node.name().as_slice());
177
+ if let Some(ref elem_ty) = element_type {
178
+ let src = genv.new_source(elem_ty.clone());
179
+ last_vtx = Some(install_local_var_write(genv, lenv, changes, var_name, src));
180
+ } else if i == 0 {
181
+ if let Some(rv) = rhs_vtx {
182
+ last_vtx = Some(install_local_var_write(genv, lenv, changes, var_name, rv));
183
+ } else {
184
+ let vtx = genv.new_vertex();
185
+ lenv.new_var(var_name, vtx);
186
+ last_vtx = Some(vtx);
187
+ }
188
+ } else if rhs_type.is_some() {
189
+ last_vtx = Some(install_nil_target(genv, lenv, changes, var_name));
190
+ } else {
191
+ let vtx = genv.new_vertex();
192
+ lenv.new_var(var_name, vtx);
193
+ last_vtx = Some(vtx);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Splat in single-expression RHS
199
+ if let Some(rest_node) = node.rest() {
200
+ if let Some(var_name) = splat_var_name(&rest_node) {
201
+ let elem_ty = element_type.unwrap_or(Type::Bot);
202
+ let array_src = genv.new_source(Type::array_of(elem_ty));
203
+ last_vtx = Some(install_local_var_write(
204
+ genv, lenv, changes, var_name, array_src,
205
+ ));
206
+ }
207
+ }
208
+
209
+ // Right targets in single-expression RHS → nil
210
+ for target in node.rights().iter() {
211
+ if let Some(target_node) = target.as_local_variable_target_node() {
212
+ let var_name = bytes_to_name(target_node.name().as_slice());
213
+ last_vtx = Some(install_nil_target(genv, lenv, changes, var_name));
214
+ }
215
+ }
216
+ }
217
+
218
+ last_vtx
219
+ }
220
+
221
+ #[cfg(test)]
222
+ mod tests {
223
+ use crate::analyzer::install::AstInstaller;
224
+ use crate::env::{GlobalEnv, LocalEnv};
225
+ use crate::graph::VertexId;
226
+ use crate::parser::ParseSession;
227
+
228
+ fn analyze(source: &str) -> (GlobalEnv, LocalEnv) {
229
+ let session = ParseSession::new();
230
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
231
+ let root = parse_result.node();
232
+ let program = root.as_program_node().unwrap();
233
+
234
+ let mut genv = GlobalEnv::new();
235
+ let mut lenv = LocalEnv::new();
236
+
237
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
238
+ for stmt in &program.statements().body() {
239
+ installer.install_node(&stmt);
240
+ }
241
+ installer.finish();
242
+
243
+ (genv, lenv)
244
+ }
245
+
246
+ fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
247
+ if let Some(vertex) = genv.get_vertex(vtx) {
248
+ vertex.show()
249
+ } else if let Some(source) = genv.get_source(vtx) {
250
+ source.ty.show()
251
+ } else {
252
+ panic!("vertex {:?} not found as either Vertex or Source", vtx);
253
+ }
254
+ }
255
+
256
+ #[test]
257
+ fn test_multi_write_integer_and_string() {
258
+ let source = r#"a, b = 1, "hello""#;
259
+ let (genv, lenv) = analyze(source);
260
+
261
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
262
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
263
+
264
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
265
+ assert_eq!(get_type_show(&genv, b_vtx), "String");
266
+ }
267
+
268
+ #[test]
269
+ fn test_multi_write_all_integer() {
270
+ let source = "a, b, c = 1, 2, 3";
271
+ let (genv, lenv) = analyze(source);
272
+
273
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
274
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
275
+
276
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
277
+ assert_eq!(get_type_show(&genv, b_vtx), "Integer");
278
+
279
+ let c_vtx = lenv.get_var("c").expect("c should be registered");
280
+ assert_eq!(get_type_show(&genv, c_vtx), "Integer");
281
+ }
282
+
283
+ #[test]
284
+ fn test_multi_write_variable_reference_after_assignment() {
285
+ let source = r#"
286
+ a, b = 1, "hello"
287
+ x = a
288
+ "#;
289
+ let (genv, lenv) = analyze(source);
290
+
291
+ let x_vtx = lenv.get_var("x").expect("x should be registered");
292
+ assert_eq!(get_type_show(&genv, x_vtx), "Integer");
293
+ }
294
+
295
+ #[test]
296
+ fn test_multi_write_lhs_longer_than_rhs() {
297
+ let source = "a, b, c = 1, 2";
298
+ let (genv, lenv) = analyze(source);
299
+
300
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
301
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
302
+
303
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
304
+ assert_eq!(get_type_show(&genv, b_vtx), "Integer");
305
+
306
+ let c_vtx = lenv.get_var("c").expect("c should be registered with nil");
307
+ assert_eq!(get_type_show(&genv, c_vtx), "nil");
308
+ }
309
+
310
+ #[test]
311
+ fn test_multi_write_does_not_panic_on_non_array_rhs() {
312
+ let source = "a, b = some_expr";
313
+ let (_, lenv) = analyze(source);
314
+
315
+ // Variables should be registered (untyped) without panic
316
+ assert!(lenv.get_var("a").is_some(), "a should be registered");
317
+ assert!(lenv.get_var("b").is_some(), "b should be registered");
318
+ }
319
+
320
+ #[test]
321
+ fn test_multi_write_splat_basic() {
322
+ let source = "first, *rest = 1, 2, 3";
323
+ let (genv, lenv) = analyze(source);
324
+
325
+ let first_vtx = lenv.get_var("first").expect("first should be registered");
326
+ assert_eq!(get_type_show(&genv, first_vtx), "Integer");
327
+
328
+ let rest_vtx = lenv.get_var("rest").expect("rest should be registered");
329
+ assert_eq!(get_type_show(&genv, rest_vtx), "Array[Integer]");
330
+ }
331
+
332
+ #[test]
333
+ fn test_multi_write_splat_mixed_types() {
334
+ let source = r#"first, *rest = 1, "hello", :sym"#;
335
+ let (genv, lenv) = analyze(source);
336
+
337
+ let first_vtx = lenv.get_var("first").expect("first should be registered");
338
+ assert_eq!(get_type_show(&genv, first_vtx), "Integer");
339
+
340
+ let rest_vtx = lenv.get_var("rest").expect("rest should be registered");
341
+ let type_str = get_type_show(&genv, rest_vtx);
342
+ assert!(
343
+ type_str.contains("Array"),
344
+ "should be Array type: {}",
345
+ type_str
346
+ );
347
+ assert!(
348
+ type_str.contains("String"),
349
+ "should contain String: {}",
350
+ type_str
351
+ );
352
+ assert!(
353
+ type_str.contains("Symbol"),
354
+ "should contain Symbol: {}",
355
+ type_str
356
+ );
357
+ }
358
+
359
+ #[test]
360
+ fn test_multi_write_splat_empty() {
361
+ let source = "first, *rest = 1";
362
+ let (genv, lenv) = analyze(source);
363
+
364
+ let first_vtx = lenv.get_var("first").expect("first should be registered");
365
+ assert_eq!(get_type_show(&genv, first_vtx), "Integer");
366
+
367
+ let rest_vtx = lenv.get_var("rest").expect("rest should be registered");
368
+ assert_eq!(get_type_show(&genv, rest_vtx), "Array[untyped]");
369
+ }
370
+
371
+ #[test]
372
+ fn test_multi_write_splat_with_rights() {
373
+ let source = "first, *rest, last = 1, 2, 3, 4";
374
+ let (genv, lenv) = analyze(source);
375
+
376
+ let first_vtx = lenv.get_var("first").expect("first should be registered");
377
+ assert_eq!(get_type_show(&genv, first_vtx), "Integer");
378
+
379
+ let rest_vtx = lenv.get_var("rest").expect("rest should be registered");
380
+ assert_eq!(get_type_show(&genv, rest_vtx), "Array[Integer]");
381
+
382
+ let last_vtx = lenv.get_var("last").expect("last should be registered");
383
+ assert_eq!(get_type_show(&genv, last_vtx), "Integer");
384
+ }
385
+
386
+ #[test]
387
+ fn test_multi_write_splat_only() {
388
+ let source = "*all = 1, 2, 3";
389
+ let (genv, lenv) = analyze(source);
390
+
391
+ let all_vtx = lenv.get_var("all").expect("all should be registered");
392
+ assert_eq!(get_type_show(&genv, all_vtx), "Array[Integer]");
393
+ }
394
+
395
+ #[test]
396
+ fn test_multi_write_splat_rights_no_lefts() {
397
+ let source = "*rest, last = 1, 2, 3";
398
+ let (genv, lenv) = analyze(source);
399
+
400
+ let rest_vtx = lenv.get_var("rest").expect("rest should be registered");
401
+ assert_eq!(get_type_show(&genv, rest_vtx), "Array[Integer]");
402
+
403
+ let last_vtx = lenv.get_var("last").expect("last should be registered");
404
+ assert_eq!(get_type_show(&genv, last_vtx), "Integer");
405
+ }
406
+
407
+ #[test]
408
+ fn test_multi_write_array_literal_rhs() {
409
+ // Explicit array literal RHS is decomposed element-by-element (same as comma-separated form)
410
+ let source = r#"a, b = [1, "hi"]"#;
411
+ let (genv, lenv) = analyze(source);
412
+
413
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
414
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
415
+
416
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
417
+ assert_eq!(get_type_show(&genv, b_vtx), "String");
418
+ }
419
+
420
+ #[test]
421
+ fn test_multi_write_splat_lefts_exceed_rhs() {
422
+ // Edge case: more left targets than RHS elements with splat
423
+ let source = "a, b, c, *rest = 1, 2";
424
+ let (genv, lenv) = analyze(source);
425
+
426
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
427
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
428
+
429
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
430
+ assert_eq!(get_type_show(&genv, b_vtx), "Integer");
431
+
432
+ let c_vtx = lenv.get_var("c").expect("c should be registered");
433
+ assert_eq!(get_type_show(&genv, c_vtx), "nil");
434
+
435
+ let rest_vtx = lenv.get_var("rest").expect("rest should be registered");
436
+ assert_eq!(get_type_show(&genv, rest_vtx), "Array[untyped]");
437
+ }
438
+
439
+ #[test]
440
+ fn test_multi_write_splat_with_rights_insufficient_rhs() {
441
+ // Edge case: lefts + rights > total elements, splat between them
442
+ let source = "a, *rest, z = 1";
443
+ let (genv, lenv) = analyze(source);
444
+
445
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
446
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
447
+
448
+ let rest_vtx = lenv.get_var("rest").expect("rest should be registered");
449
+ assert_eq!(get_type_show(&genv, rest_vtx), "Array[untyped]");
450
+
451
+ let z_vtx = lenv.get_var("z").expect("z should be registered");
452
+ assert_eq!(get_type_show(&genv, z_vtx), "nil");
453
+ }
454
+
455
+ #[test]
456
+ fn test_multi_write_rights_exceed_rhs() {
457
+ // Edge case: more right targets than available elements
458
+ let source = r#"*rest, x, y, z = "a", 1"#;
459
+ let (genv, lenv) = analyze(source);
460
+
461
+ let rest_vtx = lenv.get_var("rest").expect("rest should be registered");
462
+ assert_eq!(get_type_show(&genv, rest_vtx), "Array[untyped]");
463
+
464
+ let x_vtx = lenv.get_var("x").expect("x should be registered");
465
+ assert_eq!(get_type_show(&genv, x_vtx), "nil");
466
+
467
+ let y_vtx = lenv.get_var("y").expect("y should be registered");
468
+ assert_eq!(get_type_show(&genv, y_vtx), "String");
469
+
470
+ let z_vtx = lenv.get_var("z").expect("z should be registered");
471
+ assert_eq!(get_type_show(&genv, z_vtx), "Integer");
472
+ }
473
+
474
+ #[test]
475
+ fn test_multi_write_scalar_rhs() {
476
+ // Single non-array expression: first target gets value, rest get nil
477
+ let source = "a, b = 42";
478
+ let (genv, lenv) = analyze(source);
479
+
480
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
481
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
482
+
483
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
484
+ assert_eq!(get_type_show(&genv, b_vtx), "nil");
485
+ }
486
+
487
+ #[test]
488
+ fn test_multi_write_rhs_longer_than_lhs() {
489
+ // Extra RHS elements are silently discarded
490
+ let source = "a, b = 1, 2, 3, 4";
491
+ let (genv, lenv) = analyze(source);
492
+
493
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
494
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
495
+
496
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
497
+ assert_eq!(get_type_show(&genv, b_vtx), "Integer");
498
+ }
499
+ }