method-ray 0.1.10 → 0.2.0

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.
@@ -13,9 +13,11 @@ use ruby_prism::Node;
13
13
 
14
14
  use super::bytes_to_name;
15
15
  use super::calls::install_method_call;
16
+ use super::compound_assignments::{CompoundOp, CompoundVarKind};
16
17
  use super::variables::{
17
- install_ivar_read, install_ivar_write, install_local_var_read, install_local_var_write,
18
- install_self,
18
+ install_class_var_read, install_class_var_write, install_constant_read, install_constant_write,
19
+ install_global_var_read, install_global_var_write, install_ivar_read, install_ivar_write,
20
+ install_local_var_read, install_local_var_write, install_self,
19
21
  };
20
22
 
21
23
  /// Collect positional and keyword arguments from AST argument nodes.
@@ -75,10 +77,22 @@ pub(crate) enum DispatchResult {
75
77
 
76
78
  /// Kind of child processing needed
77
79
  pub(crate) enum NeedsChildKind<'a> {
78
- /// Instance variable write: need to process value, then call finish_ivar_write
80
+ /// Instance variable write: need to process value, then call install_ivar_write
79
81
  IvarWrite { ivar_name: String, value: Node<'a> },
80
- /// Local variable write: need to process value, then call finish_local_var_write
82
+ /// Class variable write: need to process value, then call install_class_var_write
83
+ ClassVarWrite { cvar_name: String, value: Node<'a> },
84
+ /// Local variable write: need to process value, then call install_local_var_write
81
85
  LocalVarWrite { var_name: String, value: Node<'a> },
86
+ /// Global variable write: need to process value, then call install_global_var_write
87
+ GlobalVarWrite { gvar_name: String, value: Node<'a> },
88
+ ConstantWrite { const_name: String, value: Node<'a> },
89
+ CompoundWrite {
90
+ var_kind: CompoundVarKind,
91
+ op: CompoundOp,
92
+ value: Node<'a>,
93
+ },
94
+ /// Proc/lambda literals
95
+ ProcLiteral { block: Node<'a> },
82
96
  /// Method call: need to process receiver, then call finish_method_call
83
97
  MethodCall {
84
98
  receiver: Node<'a>,
@@ -131,6 +145,33 @@ pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &
131
145
  };
132
146
  }
133
147
 
148
+ // Class variable read: @@name
149
+ // Ruby: uninitialized class variables raise NameError at runtime.
150
+ // We fall back to nil so downstream method calls are still type-checked
151
+ // rather than silently skipped. This may produce nil-union types when
152
+ // reads precede writes in source order.
153
+ if let Some(cvar_read) = node.as_class_variable_read_node() {
154
+ let cvar_name = bytes_to_name(cvar_read.name().as_slice());
155
+ let vtx = install_class_var_read(genv, &cvar_name).unwrap_or_else(|| {
156
+ let nil_vtx = genv.new_source(Type::Nil);
157
+ install_class_var_write(genv, cvar_name, nil_vtx)
158
+ });
159
+ return DispatchResult::Vertex(vtx);
160
+ }
161
+
162
+ // Global variable read: $name
163
+ // Ruby: uninitialized global variables are nil.
164
+ // Register the nil vertex so repeated reads of the same uninitialized
165
+ // variable reuse one vertex instead of allocating a new Source each time.
166
+ if let Some(gvar_read) = node.as_global_variable_read_node() {
167
+ let gvar_name = bytes_to_name(gvar_read.name().as_slice());
168
+ let vtx = install_global_var_read(genv, &gvar_name).unwrap_or_else(|| {
169
+ let nil_vtx = genv.new_source(Type::Nil);
170
+ install_global_var_write(genv, gvar_name, nil_vtx)
171
+ });
172
+ return DispatchResult::Vertex(vtx);
173
+ }
174
+
134
175
  // self
135
176
  if node.as_self_node().is_some() {
136
177
  return DispatchResult::Vertex(install_self(genv));
@@ -148,6 +189,11 @@ pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &
148
189
  // ConstantReadNode: User → Type::Singleton("User") or Type::Singleton("Api::User")
149
190
  if let Some(const_read) = node.as_constant_read_node() {
150
191
  let name = bytes_to_name(const_read.name().as_slice());
192
+
193
+ if let Some(vtx) = install_constant_read(genv, &name) {
194
+ return DispatchResult::Vertex(vtx);
195
+ }
196
+
151
197
  let resolved_name = genv.scope_manager.lookup_constant(&name)
152
198
  .unwrap_or(name);
153
199
  let vtx = genv.new_source(Type::singleton(&resolved_name));
@@ -162,6 +208,16 @@ pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &
162
208
  }
163
209
  }
164
210
 
211
+ // defined?(expr) → String | nil (child expression is NOT evaluated)
212
+ if node.as_defined_node().is_some() {
213
+ let result_vtx = genv.new_vertex();
214
+ let str_vtx = genv.new_source(Type::string());
215
+ let nil_vtx = genv.new_source(Type::Nil);
216
+ genv.add_edge(str_vtx, result_vtx);
217
+ genv.add_edge(nil_vtx, result_vtx);
218
+ return DispatchResult::Vertex(result_vtx);
219
+ }
220
+
165
221
  DispatchResult::NotHandled
166
222
  }
167
223
 
@@ -206,6 +262,32 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
206
262
  });
207
263
  }
208
264
 
265
+ // Class variable write: @@name = value
266
+ if let Some(cvar_write) = node.as_class_variable_write_node() {
267
+ let cvar_name = bytes_to_name(cvar_write.name().as_slice());
268
+ return Some(NeedsChildKind::ClassVarWrite {
269
+ cvar_name,
270
+ value: cvar_write.value(),
271
+ });
272
+ }
273
+
274
+ // Global variable write: $name = value
275
+ if let Some(gvar_write) = node.as_global_variable_write_node() {
276
+ let gvar_name = bytes_to_name(gvar_write.name().as_slice());
277
+ return Some(NeedsChildKind::GlobalVarWrite {
278
+ gvar_name,
279
+ value: gvar_write.value(),
280
+ });
281
+ }
282
+
283
+ if let Some(const_write) = node.as_constant_write_node() {
284
+ let const_name = bytes_to_name(const_write.name().as_slice());
285
+ return Some(NeedsChildKind::ConstantWrite {
286
+ const_name,
287
+ value: const_write.value(),
288
+ });
289
+ }
290
+
209
291
  // Local variable write: x = value
210
292
  if let Some(write_node) = node.as_local_variable_write_node() {
211
293
  let var_name = bytes_to_name(write_node.name().as_slice());
@@ -215,6 +297,46 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
215
297
  });
216
298
  }
217
299
 
300
+ macro_rules! dispatch_compound {
301
+ ($node:expr, $as_method:ident, $var_kind:ident, operator) => {
302
+ if let Some(n) = $node.$as_method() {
303
+ return Some(NeedsChildKind::CompoundWrite {
304
+ var_kind: CompoundVarKind::$var_kind(bytes_to_name(n.name().as_slice())),
305
+ op: CompoundOp::Operator(bytes_to_name(n.binary_operator().as_slice())),
306
+ value: n.value(),
307
+ });
308
+ }
309
+ };
310
+ ($node:expr, $as_method:ident, $var_kind:ident, $op:ident) => {
311
+ if let Some(n) = $node.$as_method() {
312
+ return Some(NeedsChildKind::CompoundWrite {
313
+ var_kind: CompoundVarKind::$var_kind(bytes_to_name(n.name().as_slice())),
314
+ op: CompoundOp::$op,
315
+ value: n.value(),
316
+ });
317
+ }
318
+ };
319
+ }
320
+
321
+ macro_rules! dispatch_compound_all {
322
+ ($node:expr, $op_method:ident, $or_method:ident, $and_method:ident, $var_kind:ident) => {
323
+ dispatch_compound!($node, $op_method, $var_kind, operator);
324
+ dispatch_compound!($node, $or_method, $var_kind, Logical);
325
+ dispatch_compound!($node, $and_method, $var_kind, Logical);
326
+ };
327
+ }
328
+
329
+ dispatch_compound_all!(node,
330
+ as_local_variable_operator_write_node, as_local_variable_or_write_node, as_local_variable_and_write_node, Local);
331
+ dispatch_compound_all!(node,
332
+ as_instance_variable_operator_write_node, as_instance_variable_or_write_node, as_instance_variable_and_write_node, Ivar);
333
+ dispatch_compound_all!(node,
334
+ as_class_variable_operator_write_node, as_class_variable_or_write_node, as_class_variable_and_write_node, ClassVar);
335
+ dispatch_compound_all!(node,
336
+ as_global_variable_operator_write_node, as_global_variable_or_write_node, as_global_variable_and_write_node, GlobalVar);
337
+ dispatch_compound_all!(node,
338
+ as_constant_operator_write_node, as_constant_or_write_node, as_constant_and_write_node, Constant);
339
+
218
340
  // Method call: x.upcase, x.each { |i| ... }, or name (implicit self)
219
341
  if let Some(call_node) = node.as_call_node() {
220
342
  let method_name = bytes_to_name(call_node.name().as_slice());
@@ -225,6 +347,16 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
225
347
  .unwrap_or_default();
226
348
 
227
349
  if let Some(receiver) = call_node.receiver() {
350
+ if method_name == "new" {
351
+ if let Some(const_read) = receiver.as_constant_read_node() {
352
+ if const_read.name().as_slice() == b"Proc" {
353
+ if let Some(block_node) = block {
354
+ return Some(NeedsChildKind::ProcLiteral { block: block_node });
355
+ }
356
+ }
357
+ }
358
+ }
359
+
228
360
  // Explicit receiver: x.upcase, x.each { |i| ... }
229
361
  let prism_location = call_node
230
362
  .call_operator_loc()
@@ -243,6 +375,12 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
243
375
  } else {
244
376
  // No receiver: implicit self method call (e.g., `name`, `puts "hello"`)
245
377
 
378
+ if matches!(method_name.as_str(), "lambda" | "proc") {
379
+ if let Some(block_node) = block {
380
+ return Some(NeedsChildKind::ProcLiteral { block: block_node });
381
+ }
382
+ }
383
+
246
384
  if let Some(kind) = match method_name.as_str() {
247
385
  "attr_reader" => Some(AttrKind::Reader),
248
386
  "attr_writer" => Some(AttrKind::Writer),
@@ -303,11 +441,36 @@ pub(crate) fn process_needs_child(
303
441
  match kind {
304
442
  NeedsChildKind::IvarWrite { ivar_name, value } => {
305
443
  let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
306
- Some(finish_ivar_write(genv, ivar_name, value_vtx))
444
+ Some(install_ivar_write(genv, ivar_name, value_vtx))
445
+ }
446
+ NeedsChildKind::ClassVarWrite { cvar_name, value } => {
447
+ let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
448
+ Some(install_class_var_write(genv, cvar_name, value_vtx))
449
+ }
450
+ NeedsChildKind::GlobalVarWrite { gvar_name, value } => {
451
+ let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
452
+ Some(install_global_var_write(genv, gvar_name, value_vtx))
453
+ }
454
+ NeedsChildKind::ConstantWrite { const_name, value } => {
455
+ let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
456
+ Some(install_constant_write(genv, const_name, value_vtx))
307
457
  }
308
458
  NeedsChildKind::LocalVarWrite { var_name, value } => {
309
459
  let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
310
- Some(finish_local_var_write(genv, lenv, changes, var_name, value_vtx))
460
+ Some(install_local_var_write(genv, lenv, changes, var_name, value_vtx))
461
+ }
462
+ NeedsChildKind::CompoundWrite { var_kind, op, value } => {
463
+ let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
464
+ Some(super::compound_assignments::process_compound_write(
465
+ genv, lenv, changes, var_kind, op, value_vtx,
466
+ ))
467
+ }
468
+ NeedsChildKind::ProcLiteral { block } => {
469
+ if let Some(block_node) = block.as_block_node() {
470
+ super::lambdas::process_block_as_proc(genv, lenv, changes, source, &block_node)
471
+ } else {
472
+ None
473
+ }
311
474
  }
312
475
  NeedsChildKind::MethodCall {
313
476
  receiver,
@@ -361,22 +524,6 @@ pub(crate) fn process_needs_child(
361
524
  }
362
525
  }
363
526
 
364
- /// Finish instance variable write after child is processed
365
- fn finish_ivar_write(genv: &mut GlobalEnv, ivar_name: String, value_vtx: VertexId) -> VertexId {
366
- install_ivar_write(genv, ivar_name, value_vtx)
367
- }
368
-
369
- /// Finish local variable write after child is processed
370
- fn finish_local_var_write(
371
- genv: &mut GlobalEnv,
372
- lenv: &mut LocalEnv,
373
- changes: &mut ChangeSet,
374
- var_name: String,
375
- value_vtx: VertexId,
376
- ) -> VertexId {
377
- install_local_var_write(genv, lenv, changes, var_name, value_vtx)
378
- }
379
-
380
527
  /// Bundled parameters for method call processing
381
528
  struct MethodCallContext<'a> {
382
529
  recv_vtx: VertexId,
@@ -453,3 +600,68 @@ fn finish_method_call(
453
600
  ) -> VertexId {
454
601
  install_method_call(genv, recv_vtx, method_name, arg_vtxs, kwarg_vtxs, Some(location), safe_navigation)
455
602
  }
603
+
604
+ #[cfg(test)]
605
+ mod tests {
606
+ use crate::analyzer::AstInstaller;
607
+ use crate::env::{GlobalEnv, LocalEnv};
608
+ use crate::parser::ParseSession;
609
+ use crate::types::Type;
610
+
611
+ fn parse_and_install(source: &str) -> GlobalEnv {
612
+ parse_and_install_with(source, |_| {})
613
+ }
614
+
615
+ fn parse_and_install_with_builtin(source: &str) -> GlobalEnv {
616
+ parse_and_install_with(source, |genv| {
617
+ genv.register_builtin_method(Type::string(), "upcase", Type::string());
618
+ })
619
+ }
620
+
621
+ fn parse_and_install_with(source: &str, setup: impl FnOnce(&mut GlobalEnv)) -> GlobalEnv {
622
+ let session = ParseSession::new();
623
+ let result = session.parse_source(source, "<test>").unwrap();
624
+ let mut genv = GlobalEnv::new();
625
+ setup(&mut genv);
626
+ let mut lenv = LocalEnv::new();
627
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
628
+
629
+ let root = result.node();
630
+ if let Some(program_node) = root.as_program_node() {
631
+ let statements = program_node.statements();
632
+ for stmt in &statements.body() {
633
+ installer.install_node(&stmt);
634
+ }
635
+ }
636
+ installer.finish();
637
+ genv
638
+ }
639
+
640
+ #[test]
641
+ fn test_defined_no_error() {
642
+ let genv = parse_and_install("result = defined?(foo)");
643
+ assert!(genv.type_errors.is_empty());
644
+ }
645
+
646
+ #[test]
647
+ fn test_defined_result_is_string_or_nil() {
648
+ // Calling upcase on String | nil produces an error for the nil branch
649
+ let genv = parse_and_install_with_builtin(
650
+ "result = defined?(foo)\nresult.upcase",
651
+ );
652
+ assert!(
653
+ !genv.type_errors.is_empty(),
654
+ "defined? returns String | nil, so upcase should error on nil branch"
655
+ );
656
+ }
657
+
658
+ #[test]
659
+ fn test_defined_child_not_evaluated() {
660
+ // If child expression were evaluated, 42.upcase would produce a type error
661
+ let genv = parse_and_install_with_builtin("defined?(42.upcase)");
662
+ assert!(
663
+ genv.type_errors.is_empty(),
664
+ "Child expression of defined? should not be evaluated"
665
+ );
666
+ }
667
+ }
@@ -10,9 +10,10 @@ use ruby_prism::Node;
10
10
 
11
11
  use super::assignments::process_multi_write_node;
12
12
  use super::blocks::process_block_node;
13
- use super::conditionals::{process_case_node, process_if_node, process_unless_node};
13
+ use super::conditionals::{process_case_match_node, process_case_node, process_if_node, process_unless_node};
14
14
  use super::definitions::{process_class_node, process_def_node, process_module_node};
15
15
  use super::exceptions::{process_begin_node, process_rescue_modifier_node};
16
+ use super::lambdas::process_lambda_node;
16
17
  use super::dispatch::{dispatch_needs_child, dispatch_simple, process_needs_child, DispatchResult};
17
18
  use super::literals::install_literal_node;
18
19
  use super::loops::{process_for_node, process_until_node, process_while_node};
@@ -20,6 +21,7 @@ use super::operators::{process_and_node, process_or_node};
20
21
  use super::parentheses::process_parentheses_node;
21
22
  use super::returns::process_return_node;
22
23
  use super::super_calls;
24
+ use super::yields::process_yield_node;
23
25
 
24
26
  /// Build graph from AST (public API wrapper)
25
27
  pub struct AstInstaller<'a> {
@@ -73,6 +75,10 @@ pub(crate) fn install_node(
73
75
  return process_block_node(genv, lenv, changes, source, &block_node);
74
76
  }
75
77
 
78
+ if let Some(lambda_node) = node.as_lambda_node() {
79
+ return process_lambda_node(genv, lenv, changes, source, &lambda_node);
80
+ }
81
+
76
82
  if let Some(if_node) = node.as_if_node() {
77
83
  return process_if_node(genv, lenv, changes, source, &if_node);
78
84
  }
@@ -82,6 +88,9 @@ pub(crate) fn install_node(
82
88
  if let Some(case_node) = node.as_case_node() {
83
89
  return process_case_node(genv, lenv, changes, source, &case_node);
84
90
  }
91
+ if let Some(case_match_node) = node.as_case_match_node() {
92
+ return process_case_match_node(genv, lenv, changes, source, &case_match_node);
93
+ }
85
94
 
86
95
  if let Some(begin_node) = node.as_begin_node() {
87
96
  return process_begin_node(genv, lenv, changes, source, &begin_node);
@@ -119,6 +128,32 @@ pub(crate) fn install_node(
119
128
  return process_return_node(genv, lenv, changes, source, &return_node);
120
129
  }
121
130
 
131
+ if let Some(yield_node) = node.as_yield_node() {
132
+ return process_yield_node(genv, lenv, changes, source, &yield_node);
133
+ }
134
+
135
+ if let Some(next_node) = node.as_next_node() {
136
+ if let Some(args) = next_node.arguments() {
137
+ for arg in args.arguments().iter() {
138
+ install_node(genv, lenv, changes, source, &arg);
139
+ }
140
+ }
141
+ return None;
142
+ }
143
+
144
+ if let Some(break_node) = node.as_break_node() {
145
+ if let Some(args) = break_node.arguments() {
146
+ for arg in args.arguments().iter() {
147
+ install_node(genv, lenv, changes, source, &arg);
148
+ }
149
+ }
150
+ return None;
151
+ }
152
+
153
+ if node.as_redo_node().is_some() || node.as_retry_node().is_some() {
154
+ return None;
155
+ }
156
+
122
157
  if let Some(and_node) = node.as_and_node() {
123
158
  return process_and_node(genv, lenv, changes, source, &and_node);
124
159
  }
@@ -0,0 +1,153 @@
1
+ //! Lambdas/Procs - type inference for lambda and proc literals
2
+ //!
3
+ //! Handles `-> { }`, `lambda { }`, `proc { }`, and `Proc.new { }`.
4
+
5
+ use ruby_prism::{LambdaNode, Node};
6
+
7
+ use crate::env::{GlobalEnv, LocalEnv};
8
+ use crate::graph::{ChangeSet, VertexId};
9
+ use crate::types::Type;
10
+
11
+ pub(crate) fn process_lambda_node(
12
+ genv: &mut GlobalEnv,
13
+ lenv: &mut LocalEnv,
14
+ changes: &mut ChangeSet,
15
+ source: &str,
16
+ lambda_node: &LambdaNode,
17
+ ) -> Option<VertexId> {
18
+ process_proc_body(genv, lenv, changes, source, lambda_node.parameters(), lambda_node.body())
19
+ }
20
+
21
+ pub(crate) fn process_block_as_proc(
22
+ genv: &mut GlobalEnv,
23
+ lenv: &mut LocalEnv,
24
+ changes: &mut ChangeSet,
25
+ source: &str,
26
+ block_node: &ruby_prism::BlockNode,
27
+ ) -> Option<VertexId> {
28
+ process_proc_body(genv, lenv, changes, source, block_node.parameters(), block_node.body())
29
+ }
30
+
31
+ fn process_proc_body<'a>(
32
+ genv: &mut GlobalEnv,
33
+ lenv: &mut LocalEnv,
34
+ changes: &mut ChangeSet,
35
+ source: &str,
36
+ parameters: Option<Node<'a>>,
37
+ body: Option<Node<'a>>,
38
+ ) -> Option<VertexId> {
39
+ let (_scope_id, merge_vtx) = genv.enter_lambda();
40
+
41
+ let param_vtxs = parameters
42
+ .and_then(|p| p.as_block_parameters_node())
43
+ .map(|bp| {
44
+ super::blocks::install_block_parameters_with_vtxs(genv, lenv, changes, source, &bp)
45
+ })
46
+ .unwrap_or_default();
47
+
48
+ let body_vtx = body.and_then(|b| {
49
+ if let Some(stmts) = b.as_statements_node() {
50
+ super::install::install_statements(genv, lenv, changes, source, &stmts)
51
+ } else {
52
+ super::install::install_node(genv, lenv, changes, source, &b)
53
+ }
54
+ });
55
+
56
+ if let Some(vtx) = body_vtx {
57
+ genv.add_edge(vtx, merge_vtx);
58
+ }
59
+
60
+ genv.exit_scope();
61
+
62
+ let proc_ty = Type::proc_type_with_vertex(merge_vtx, param_vtxs);
63
+ let proc_vtx = genv.new_source(proc_ty);
64
+
65
+ Some(proc_vtx)
66
+ }
67
+
68
+ #[cfg(test)]
69
+ mod tests {
70
+ use crate::analyzer::AstInstaller;
71
+ use crate::env::{GlobalEnv, LocalEnv};
72
+ use crate::parser::ParseSession;
73
+ use crate::types::Type;
74
+
75
+ fn parse_and_install(source: &str) -> GlobalEnv {
76
+ parse_and_install_with(source, |_| {})
77
+ }
78
+
79
+ fn parse_and_install_with_builtin(source: &str) -> GlobalEnv {
80
+ parse_and_install_with(source, |genv| {
81
+ genv.register_builtin_method(Type::string(), "upcase", Type::string());
82
+ })
83
+ }
84
+
85
+ fn parse_and_install_with(source: &str, setup: impl FnOnce(&mut GlobalEnv)) -> GlobalEnv {
86
+ let session = ParseSession::new();
87
+ let result = session.parse_source(source, "<test>").unwrap();
88
+ let mut genv = GlobalEnv::new();
89
+ setup(&mut genv);
90
+ let mut lenv = LocalEnv::new();
91
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
92
+
93
+ let root = result.node();
94
+ if let Some(program_node) = root.as_program_node() {
95
+ let statements = program_node.statements();
96
+ for stmt in &statements.body() {
97
+ installer.install_node(&stmt);
98
+ }
99
+ }
100
+ installer.finish();
101
+ genv
102
+ }
103
+
104
+ #[test]
105
+ fn test_lambda_basic_no_crash() {
106
+ let genv = parse_and_install("f = -> { 42 }");
107
+ assert!(genv.type_errors.is_empty());
108
+ }
109
+
110
+ #[test]
111
+ fn test_lambda_with_params_no_crash() {
112
+ let genv = parse_and_install("f = -> (x) { x }");
113
+ assert!(genv.type_errors.is_empty());
114
+ }
115
+
116
+ #[test]
117
+ fn test_lambda_body_type_error_detected() {
118
+ let genv = parse_and_install_with_builtin("f = -> { 42.upcase }");
119
+ assert!(
120
+ !genv.type_errors.is_empty(),
121
+ "Expected type error for 42.upcase inside lambda"
122
+ );
123
+ }
124
+
125
+ #[test]
126
+ fn test_lambda_method_basic() {
127
+ let genv = parse_and_install("f = lambda { 42 }");
128
+ assert!(genv.type_errors.is_empty());
129
+ }
130
+
131
+ #[test]
132
+ fn test_proc_method_basic() {
133
+ let genv = parse_and_install("f = proc { 42 }");
134
+ assert!(genv.type_errors.is_empty());
135
+ }
136
+
137
+ #[test]
138
+ fn test_proc_new_basic() {
139
+ let genv = parse_and_install("f = Proc.new { 42 }");
140
+ assert!(genv.type_errors.is_empty());
141
+ }
142
+
143
+ #[test]
144
+ fn test_lambda_call_arg_type_propagation() {
145
+ let genv = parse_and_install_with_builtin(
146
+ "f = ->(x) { x.upcase }\nf.call(42)",
147
+ );
148
+ assert!(
149
+ !genv.type_errors.is_empty(),
150
+ "Expected type error for 42.upcase via lambda arg propagation"
151
+ );
152
+ }
153
+ }
@@ -58,6 +58,14 @@ pub(crate) fn install_literal_node(
58
58
  return Some(genv.new_source(Type::regexp()));
59
59
  }
60
60
 
61
+ // InterpolatedXStringNode: `command #{expr}` → String
62
+ if let Some(interp) = node.as_interpolated_x_string_node() {
63
+ for part in &interp.parts() {
64
+ super::install::install_node(genv, lenv, changes, source, &part);
65
+ }
66
+ return Some(genv.new_source(Type::string()));
67
+ }
68
+
61
69
  install_simple_literal(genv, node)
62
70
  }
63
71
 
@@ -87,6 +95,9 @@ fn install_simple_literal(genv: &mut GlobalEnv, node: &Node) -> Option<VertexId>
87
95
  if node.as_regular_expression_node().is_some() {
88
96
  return Some(genv.new_source(Type::regexp()));
89
97
  }
98
+ if node.as_x_string_node().is_some() {
99
+ return Some(genv.new_source(Type::string()));
100
+ }
90
101
  None
91
102
  }
92
103
 
@@ -2,11 +2,13 @@ mod assignments;
2
2
  mod attributes;
3
3
  mod blocks;
4
4
  mod calls;
5
+ mod compound_assignments;
5
6
  mod conditionals;
6
7
  mod definitions;
7
8
  mod exceptions;
8
9
  mod dispatch;
9
10
  mod install;
11
+ mod lambdas;
10
12
  mod literals;
11
13
  mod loops;
12
14
  mod operators;
@@ -15,6 +17,7 @@ mod parentheses;
15
17
  mod returns;
16
18
  mod super_calls;
17
19
  mod variables;
20
+ mod yields;
18
21
 
19
22
  pub use install::AstInstaller;
20
23