method-ray 0.1.7 → 0.1.8

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.
@@ -0,0 +1,176 @@
1
+ //! Loops - while/until loop type inference
2
+ //!
3
+ //! Ruby loop expressions evaluate to nil (except when break passes a value).
4
+ //! Break value propagation is not yet supported.
5
+
6
+ use crate::env::{GlobalEnv, LocalEnv};
7
+ use crate::graph::{ChangeSet, VertexId};
8
+ use crate::types::Type;
9
+ use ruby_prism::{UntilNode, WhileNode};
10
+
11
+ use super::install::{install_node, install_statements};
12
+
13
+ /// Process WhileNode: `while predicate; statements; end`
14
+ ///
15
+ /// Returns nil. Traverses predicate and body to register method calls
16
+ /// and variable assignments in the type graph.
17
+ pub(crate) fn process_while_node(
18
+ genv: &mut GlobalEnv,
19
+ lenv: &mut LocalEnv,
20
+ changes: &mut ChangeSet,
21
+ source: &str,
22
+ while_node: &WhileNode,
23
+ ) -> Option<VertexId> {
24
+ install_node(genv, lenv, changes, source, &while_node.predicate());
25
+
26
+ if let Some(stmts) = while_node.statements() {
27
+ install_statements(genv, lenv, changes, source, &stmts);
28
+ }
29
+
30
+ Some(genv.new_source(Type::Nil))
31
+ }
32
+
33
+ /// Process UntilNode: `until predicate; statements; end`
34
+ ///
35
+ /// Returns nil. Traverses predicate and body to register method calls
36
+ /// and variable assignments in the type graph.
37
+ pub(crate) fn process_until_node(
38
+ genv: &mut GlobalEnv,
39
+ lenv: &mut LocalEnv,
40
+ changes: &mut ChangeSet,
41
+ source: &str,
42
+ until_node: &UntilNode,
43
+ ) -> Option<VertexId> {
44
+ install_node(genv, lenv, changes, source, &until_node.predicate());
45
+
46
+ if let Some(stmts) = until_node.statements() {
47
+ install_statements(genv, lenv, changes, source, &stmts);
48
+ }
49
+
50
+ Some(genv.new_source(Type::Nil))
51
+ }
52
+
53
+ #[cfg(test)]
54
+ mod tests {
55
+ use crate::analyzer::install::AstInstaller;
56
+ use crate::env::{GlobalEnv, LocalEnv};
57
+ use crate::graph::VertexId;
58
+ use crate::parser::ParseSession;
59
+ use crate::types::Type;
60
+
61
+ /// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
62
+ fn analyze(source: &str) -> GlobalEnv {
63
+ let session = ParseSession::new();
64
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
65
+ let root = parse_result.node();
66
+ let program = root.as_program_node().unwrap();
67
+
68
+ let mut genv = GlobalEnv::new();
69
+ let mut lenv = LocalEnv::new();
70
+
71
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
72
+ for stmt in &program.statements().body() {
73
+ installer.install_node(&stmt);
74
+ }
75
+ installer.finish();
76
+
77
+ genv
78
+ }
79
+
80
+ /// Helper: get the type string for a vertex ID (checks both Vertex and Source)
81
+ fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
82
+ if let Some(vertex) = genv.get_vertex(vtx) {
83
+ vertex.show()
84
+ } else if let Some(source) = genv.get_source(vtx) {
85
+ source.ty.show()
86
+ } else {
87
+ panic!("vertex {:?} not found as either Vertex or Source", vtx);
88
+ }
89
+ }
90
+
91
+ #[test]
92
+ fn test_while_returns_nil() {
93
+ let source = r#"
94
+ class Foo
95
+ def bar
96
+ while true
97
+ "hello"
98
+ end
99
+ end
100
+ end
101
+ "#;
102
+ let genv = analyze(source);
103
+ let info = genv
104
+ .resolve_method(&Type::instance("Foo"), "bar")
105
+ .expect("Foo#bar should be registered");
106
+ let ret_vtx = info.return_vertex.unwrap();
107
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
108
+ }
109
+
110
+ #[test]
111
+ fn test_until_returns_nil() {
112
+ let source = r#"
113
+ class Foo
114
+ def bar
115
+ until false
116
+ "hello"
117
+ end
118
+ end
119
+ end
120
+ "#;
121
+ let genv = analyze(source);
122
+ let info = genv
123
+ .resolve_method(&Type::instance("Foo"), "bar")
124
+ .expect("Foo#bar should be registered");
125
+ let ret_vtx = info.return_vertex.unwrap();
126
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
127
+ }
128
+
129
+ #[test]
130
+ fn test_while_variable_assignment_in_body() {
131
+ // Should not panic — variable assignment inside loop is processed
132
+ let source = r#"
133
+ x = "initial"
134
+ while true
135
+ x = "hello"
136
+ end
137
+ "#;
138
+ analyze(source);
139
+ }
140
+
141
+ #[test]
142
+ fn test_while_modifier_form() {
143
+ let source = r#"
144
+ class Foo
145
+ def bar
146
+ x = "hello" while false
147
+ end
148
+ end
149
+ "#;
150
+ let genv = analyze(source);
151
+ let info = genv
152
+ .resolve_method(&Type::instance("Foo"), "bar")
153
+ .expect("Foo#bar should be registered");
154
+ let ret_vtx = info.return_vertex.unwrap();
155
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
156
+ }
157
+
158
+ #[test]
159
+ fn test_begin_end_while() {
160
+ let source = r#"
161
+ class Foo
162
+ def bar
163
+ begin
164
+ "hello"
165
+ end while false
166
+ end
167
+ end
168
+ "#;
169
+ let genv = analyze(source);
170
+ let info = genv
171
+ .resolve_method(&Type::instance("Foo"), "bar")
172
+ .expect("Foo#bar should be registered");
173
+ let ret_vtx = info.return_vertex.unwrap();
174
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
175
+ }
176
+ }
@@ -1,11 +1,14 @@
1
+ mod assignments;
1
2
  mod attributes;
2
3
  mod blocks;
3
4
  mod calls;
4
5
  mod conditionals;
5
6
  mod definitions;
7
+ mod exceptions;
6
8
  mod dispatch;
7
9
  mod install;
8
10
  mod literals;
11
+ mod loops;
9
12
  mod operators;
10
13
  mod parameters;
11
14
  mod parentheses;
@@ -1,42 +1,52 @@
1
- //! Operators - logical operator type inference (&&, ||)
1
+ //! Operators - logical operator type inference (&&, ||, !)
2
2
 
3
3
  use crate::env::{GlobalEnv, LocalEnv};
4
4
  use crate::graph::{ChangeSet, VertexId};
5
- use ruby_prism::{AndNode, OrNode};
5
+ use crate::types::Type;
6
+ use ruby_prism::{AndNode, Node, OrNode};
6
7
 
7
8
  use super::install::install_node;
8
9
 
9
- /// Process AndNode (a && b): Union(type(a), type(b))
10
- ///
11
- /// Short-circuit semantics: if `a` is falsy, returns `a`; otherwise returns `b`.
12
- /// Static approximation: we cannot determine truthiness at compile time,
13
- /// so we conservatively produce Union(type(a), type(b)).
14
- pub(crate) fn process_and_node(
10
+ /// Merge two branch nodes into a union type vertex.
11
+ fn process_binary_logical_op<'a>(
15
12
  genv: &mut GlobalEnv,
16
13
  lenv: &mut LocalEnv,
17
14
  changes: &mut ChangeSet,
18
15
  source: &str,
19
- and_node: &AndNode,
16
+ left: Node<'a>,
17
+ right: Node<'a>,
20
18
  ) -> Option<VertexId> {
21
19
  let result_vtx = genv.new_vertex();
22
20
 
23
- let left_vtx = install_node(genv, lenv, changes, source, &and_node.left());
24
- if let Some(vtx) = left_vtx {
21
+ if let Some(vtx) = install_node(genv, lenv, changes, source, &left) {
25
22
  genv.add_edge(vtx, result_vtx);
26
23
  }
27
24
 
28
- let right_vtx = install_node(genv, lenv, changes, source, &and_node.right());
29
- if let Some(vtx) = right_vtx {
25
+ if let Some(vtx) = install_node(genv, lenv, changes, source, &right) {
30
26
  genv.add_edge(vtx, result_vtx);
31
27
  }
32
28
 
33
29
  Some(result_vtx)
34
30
  }
35
31
 
32
+ /// Process AndNode (a && b): Union(type(a), type(b))
33
+ ///
34
+ /// Runtime: if `a` is falsy, returns `a`; otherwise returns `b`.
35
+ /// Static: conservatively produce Union(type(a), type(b)).
36
+ pub(crate) fn process_and_node(
37
+ genv: &mut GlobalEnv,
38
+ lenv: &mut LocalEnv,
39
+ changes: &mut ChangeSet,
40
+ source: &str,
41
+ and_node: &AndNode,
42
+ ) -> Option<VertexId> {
43
+ process_binary_logical_op(genv, lenv, changes, source, and_node.left(), and_node.right())
44
+ }
45
+
36
46
  /// Process OrNode (a || b): Union(type(a), type(b))
37
47
  ///
38
- /// Short-circuit semantics: if `a` is truthy, returns `a`; otherwise returns `b`.
39
- /// Static approximation: identical to AndNode — Union of both sides.
48
+ /// Runtime: if `a` is truthy, returns `a`; otherwise returns `b`.
49
+ /// Static: conservatively produce Union(type(a), type(b)).
40
50
  pub(crate) fn process_or_node(
41
51
  genv: &mut GlobalEnv,
42
52
  lenv: &mut LocalEnv,
@@ -44,19 +54,28 @@ pub(crate) fn process_or_node(
44
54
  source: &str,
45
55
  or_node: &OrNode,
46
56
  ) -> Option<VertexId> {
47
- let result_vtx = genv.new_vertex();
48
-
49
- let left_vtx = install_node(genv, lenv, changes, source, &or_node.left());
50
- if let Some(vtx) = left_vtx {
51
- genv.add_edge(vtx, result_vtx);
52
- }
53
-
54
- let right_vtx = install_node(genv, lenv, changes, source, &or_node.right());
55
- if let Some(vtx) = right_vtx {
56
- genv.add_edge(vtx, result_vtx);
57
- }
57
+ process_binary_logical_op(genv, lenv, changes, source, or_node.left(), or_node.right())
58
+ }
58
59
 
59
- Some(result_vtx)
60
+ /// Process not operator (!expr): TrueClass | FalseClass
61
+ ///
62
+ /// In ruby-prism, `!expr` is represented as a CallNode with method name "!".
63
+ /// Static approximation: we cannot determine the receiver's truthiness at
64
+ /// compile time, so conservatively return TrueClass | FalseClass for any `!` call.
65
+ /// In practice, `!nil` and `!false` are always true, but we do not track that here.
66
+ ///
67
+ /// Receiver side effects are already analyzed by the caller (process_needs_child).
68
+ ///
69
+ /// TODO: Ruby allows overriding `BasicObject#!`. Currently we always return
70
+ /// TrueClass | FalseClass, ignoring user-defined `!` methods. If needed, look up
71
+ /// the receiver's RBS definition and use its return type instead.
72
+ pub(crate) fn process_not_operator(genv: &mut GlobalEnv) -> VertexId {
73
+ let result_vtx = genv.new_vertex();
74
+ let true_vtx = genv.new_source(Type::instance("TrueClass"));
75
+ let false_vtx = genv.new_source(Type::instance("FalseClass"));
76
+ genv.add_edge(true_vtx, result_vtx);
77
+ genv.add_edge(false_vtx, result_vtx);
78
+ result_vtx
60
79
  }
61
80
 
62
81
  #[cfg(test)]
@@ -189,4 +208,77 @@ end
189
208
  assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
190
209
  }
191
210
 
211
+ // ============================================
212
+ // Not operator (!) tests
213
+ // ============================================
214
+
215
+ #[test]
216
+ fn test_not_operator_returns_boolean_union() {
217
+ let source = r#"
218
+ class Foo
219
+ def bar
220
+ !true
221
+ end
222
+ end
223
+ "#;
224
+ let genv = analyze(source);
225
+ let info = genv
226
+ .resolve_method(&Type::instance("Foo"), "bar")
227
+ .expect("bar should be registered");
228
+ let ty = get_type_show(&genv, info.return_vertex.unwrap());
229
+ assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
230
+ assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
231
+ }
232
+
233
+ #[test]
234
+ fn test_not_operator_receiver_side_effects_analyzed() {
235
+ let source = r#"
236
+ class Foo
237
+ def bar
238
+ !(1.upcase)
239
+ end
240
+ end
241
+ "#;
242
+ let genv = analyze(source);
243
+ assert!(
244
+ !genv.type_errors.is_empty(),
245
+ "expected type error for Integer#upcase"
246
+ );
247
+ }
248
+
249
+ #[test]
250
+ fn test_double_not_operator_union() {
251
+ let source = r#"
252
+ class Foo
253
+ def bar
254
+ !!true
255
+ end
256
+ end
257
+ "#;
258
+ let genv = analyze(source);
259
+ let info = genv
260
+ .resolve_method(&Type::instance("Foo"), "bar")
261
+ .expect("bar should be registered");
262
+ let ty = get_type_show(&genv, info.return_vertex.unwrap());
263
+ assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
264
+ assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
265
+ }
266
+
267
+ #[test]
268
+ fn test_not_nil_returns_boolean() {
269
+ let source = r#"
270
+ class Foo
271
+ def bar
272
+ !nil
273
+ end
274
+ end
275
+ "#;
276
+ let genv = analyze(source);
277
+ let info = genv
278
+ .resolve_method(&Type::instance("Foo"), "bar")
279
+ .expect("bar should be registered");
280
+ let ty = get_type_show(&genv, info.return_vertex.unwrap());
281
+ assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
282
+ assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
283
+ }
192
284
  }
@@ -5,6 +5,8 @@
5
5
  //! - Creating vertices for parameters
6
6
  //! - Registering parameters as local variables in method scope
7
7
 
8
+ use std::collections::HashMap;
9
+
8
10
  use crate::env::{GlobalEnv, LocalEnv};
9
11
  use crate::graph::{ChangeSet, VertexId};
10
12
  use crate::types::Type;
@@ -117,16 +119,18 @@ pub fn install_keyword_rest_parameter(
117
119
 
118
120
  /// Install method parameters as local variables
119
121
  ///
120
- /// Returns a Vec of VertexId for required and optional parameters (positional),
121
- /// which can be used for argument-to-parameter type propagation.
122
+ /// Returns a tuple of:
123
+ /// - Vec<VertexId>: positional parameter vertices (required and optional)
124
+ /// - HashMap<String, VertexId>: keyword parameter vertices (name → vertex)
122
125
  pub(crate) fn install_parameters(
123
126
  genv: &mut GlobalEnv,
124
127
  lenv: &mut LocalEnv,
125
128
  changes: &mut ChangeSet,
126
129
  source: &str,
127
130
  params_node: &ruby_prism::ParametersNode,
128
- ) -> Vec<VertexId> {
131
+ ) -> (Vec<VertexId>, HashMap<String, VertexId>) {
129
132
  let mut param_vtxs = Vec::new();
133
+ let mut keyword_param_vtxs: HashMap<String, VertexId> = HashMap::new();
130
134
 
131
135
  // Required parameters: def foo(a, b)
132
136
  for node in params_node.requireds().iter() {
@@ -165,8 +169,30 @@ pub(crate) fn install_parameters(
165
169
  }
166
170
  }
167
171
 
172
+ // Keyword parameters: def foo(name:, age: 0)
173
+ // Reuses install_required_parameter / install_optional_parameter
174
+ // since the logic is identical for positional and keyword parameters.
175
+ for node in params_node.keywords().iter() {
176
+ if let Some(req_kw) = node.as_required_keyword_parameter_node() {
177
+ let name = bytes_to_name(req_kw.name().as_slice());
178
+ let vtx = install_required_parameter(genv, lenv, name.clone());
179
+ keyword_param_vtxs.insert(name, vtx);
180
+ } else if let Some(opt_kw) = node.as_optional_keyword_parameter_node() {
181
+ let name = bytes_to_name(opt_kw.name().as_slice());
182
+ let default_value = opt_kw.value();
183
+ let vtx = if let Some(default_vtx) =
184
+ super::install::install_node(genv, lenv, changes, source, &default_value)
185
+ {
186
+ install_optional_parameter(genv, lenv, changes, name.clone(), default_vtx)
187
+ } else {
188
+ install_required_parameter(genv, lenv, name.clone())
189
+ };
190
+ keyword_param_vtxs.insert(name, vtx);
191
+ }
192
+ }
193
+
168
194
  // Keyword rest parameter: def foo(**kwargs)
169
- // Not included in param_vtxs (keyword args need special handling)
195
+ // Not included in keyword_param_vtxs (collects all remaining keywords)
170
196
  if let Some(kwrest_node) = params_node.keyword_rest() {
171
197
  if let Some(kwrest_param) = kwrest_node.as_keyword_rest_parameter_node() {
172
198
  if let Some(name_id) = kwrest_param.name() {
@@ -176,7 +202,7 @@ pub(crate) fn install_parameters(
176
202
  }
177
203
  }
178
204
 
179
- param_vtxs
205
+ (param_vtxs, keyword_param_vtxs)
180
206
  }
181
207
 
182
208
  #[cfg(test)]
@@ -217,4 +243,27 @@ mod tests {
217
243
  assert_ne!(vtx_b, vtx_c);
218
244
  assert_ne!(vtx_a, vtx_c);
219
245
  }
246
+
247
+ #[test]
248
+ fn test_install_optional_parameter_inherits_default_type() {
249
+ let mut genv = GlobalEnv::new();
250
+ let mut lenv = LocalEnv::new();
251
+ let mut changes = ChangeSet::new();
252
+
253
+ // Default value: 0 (Integer)
254
+ let default_vtx = genv.new_source(Type::integer());
255
+ let vtx = install_optional_parameter(
256
+ &mut genv,
257
+ &mut lenv,
258
+ &mut changes,
259
+ "age".to_string(),
260
+ default_vtx,
261
+ );
262
+
263
+ assert_eq!(lenv.get_var("age"), Some(vtx));
264
+
265
+ // Type should propagate from default value
266
+ let vertex = genv.get_vertex(vtx).unwrap();
267
+ assert_eq!(vertex.show(), "Integer");
268
+ }
220
269
  }
@@ -37,7 +37,6 @@ impl SerializableMethodInfo {
37
37
  }
38
38
  }
39
39
 
40
- #[allow(dead_code)]
41
40
  impl RbsCache {
42
41
  /// Get user cache file path (in ~/.cache/methodray/)
43
42
  pub fn cache_path() -> Result<PathBuf> {
@@ -1,7 +1,7 @@
1
1
  //! CLI command implementations
2
2
 
3
3
  use anyhow::Result;
4
- use std::path::PathBuf;
4
+ use std::path::Path;
5
5
 
6
6
  use crate::cache::RbsCache;
7
7
  use crate::checker::FileChecker;
@@ -9,7 +9,7 @@ use crate::diagnostics;
9
9
 
10
10
  /// Check a single Ruby file for type errors
11
11
  /// Returns Ok(true) if no errors, Ok(false) if errors found
12
- pub fn check_single_file(file_path: &PathBuf, verbose: bool) -> Result<bool> {
12
+ pub fn check_single_file(file_path: &Path, verbose: bool) -> Result<bool> {
13
13
  let checker = FileChecker::new()?;
14
14
  let diagnostics = checker.check_file(file_path)?;
15
15
 
@@ -38,7 +38,7 @@ pub fn check_project(_verbose: bool) -> Result<()> {
38
38
  }
39
39
 
40
40
  /// Watch a file for changes and re-check on modifications
41
- pub fn watch_file(file_path: &PathBuf) -> Result<()> {
41
+ pub fn watch_file(file_path: &Path) -> Result<()> {
42
42
  use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
43
43
  use std::sync::mpsc::channel;
44
44
  use std::time::Duration;
@@ -2,7 +2,6 @@ use std::path::PathBuf;
2
2
 
3
3
  /// Diagnostic severity level (LSP compatible)
4
4
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
- #[allow(dead_code)]
6
5
  pub enum DiagnosticLevel {
7
6
  Error,
8
7
  Warning,
@@ -28,7 +27,6 @@ pub struct Location {
28
27
 
29
28
  /// Type checking diagnostic
30
29
  #[derive(Debug, Clone)]
31
- #[allow(dead_code)]
32
30
  pub struct Diagnostic {
33
31
  pub location: Location,
34
32
  pub level: DiagnosticLevel,
@@ -36,7 +34,6 @@ pub struct Diagnostic {
36
34
  pub code: Option<String>, // e.g., "E001"
37
35
  }
38
36
 
39
- #[allow(dead_code)]
40
37
  impl Diagnostic {
41
38
  /// Create an error diagnostic
42
39
  pub fn error(location: Location, message: String) -> Self {
@@ -8,7 +8,6 @@ use std::path::Path;
8
8
  /// ```text
9
9
  /// app/models/user.rb:10:5: error: undefined method `upcase` for Integer
10
10
  /// ```
11
- #[allow(dead_code)]
12
11
  pub fn format_diagnostics(diagnostics: &[Diagnostic]) -> String {
13
12
  diagnostics
14
13
  .iter()
@@ -6,7 +6,6 @@ use crate::graph::{BoxId, BoxTrait};
6
6
  use std::collections::{HashMap, HashSet, VecDeque};
7
7
 
8
8
  /// Manages boxes and their execution queue
9
- #[allow(dead_code)]
10
9
  pub struct BoxManager {
11
10
  /// All registered boxes
12
11
  pub boxes: HashMap<BoxId, Box<dyn BoxTrait>>,
@@ -24,7 +23,6 @@ impl Default for BoxManager {
24
23
  }
25
24
  }
26
25
 
27
- #[allow(dead_code)]
28
26
  impl BoxManager {
29
27
  /// Create a new empty box manager
30
28
  pub fn new() -> Self {
@@ -37,8 +35,8 @@ impl BoxManager {
37
35
  }
38
36
 
39
37
  /// Get a box by ID
40
- pub fn get(&self, id: BoxId) -> Option<&Box<dyn BoxTrait>> {
41
- self.boxes.get(&id)
38
+ pub fn get(&self, id: BoxId) -> Option<&dyn BoxTrait> {
39
+ self.boxes.get(&id).map(|b| b.as_ref())
42
40
  }
43
41
 
44
42
  /// Remove a box and return it (for temporary mutation)
@@ -3,6 +3,8 @@
3
3
  //! This module provides a unified interface for managing vertices, boxes,
4
4
  //! methods, type errors, and scopes during type inference.
5
5
 
6
+ use std::collections::HashMap;
7
+
6
8
  use crate::env::box_manager::BoxManager;
7
9
  use crate::env::method_registry::{MethodInfo, MethodRegistry};
8
10
  use crate::env::scope::{Scope, ScopeId, ScopeKind, ScopeManager};
@@ -37,7 +39,6 @@ pub struct GlobalEnv {
37
39
  pub scope_manager: ScopeManager,
38
40
  }
39
41
 
40
- #[allow(dead_code)]
41
42
  impl GlobalEnv {
42
43
  pub fn new() -> Self {
43
44
  Self {
@@ -184,9 +185,15 @@ impl GlobalEnv {
184
185
  method_name: &str,
185
186
  return_vertex: VertexId,
186
187
  param_vertices: Vec<VertexId>,
188
+ keyword_param_vertices: Option<HashMap<String, VertexId>>,
187
189
  ) {
188
- self.method_registry
189
- .register_user_method(recv_ty, method_name, return_vertex, param_vertices);
190
+ self.method_registry.register_user_method(
191
+ recv_ty,
192
+ method_name,
193
+ return_vertex,
194
+ param_vertices,
195
+ keyword_param_vertices,
196
+ );
190
197
  }
191
198
 
192
199
  // ===== Type Errors =====
@@ -6,7 +6,12 @@ pub struct LocalEnv {
6
6
  locals: HashMap<String, VertexId>,
7
7
  }
8
8
 
9
- #[allow(dead_code)]
9
+ impl Default for LocalEnv {
10
+ fn default() -> Self {
11
+ Self::new()
12
+ }
13
+ }
14
+
10
15
  impl LocalEnv {
11
16
  pub fn new() -> Self {
12
17
  Self {
@@ -24,6 +29,12 @@ impl LocalEnv {
24
29
  self.locals.get(name).copied()
25
30
  }
26
31
 
32
+ /// Remove a variable from the local environment.
33
+ /// Used for scoped variables like rescue's `=> e` binding.
34
+ pub fn remove_var(&mut self, name: &str) {
35
+ self.locals.remove(name);
36
+ }
37
+
27
38
  /// Get all variables
28
39
  pub fn all_vars(&self) -> impl Iterator<Item = (&String, &VertexId)> {
29
40
  self.locals.iter()
@@ -34,6 +45,12 @@ impl LocalEnv {
34
45
  mod tests {
35
46
  use super::*;
36
47
 
48
+ #[test]
49
+ fn test_local_env_default() {
50
+ let lenv = LocalEnv::default();
51
+ assert_eq!(lenv.get_var("x"), None);
52
+ }
53
+
37
54
  #[test]
38
55
  fn test_local_env() {
39
56
  let mut lenv = LocalEnv::new();
@@ -46,6 +63,23 @@ mod tests {
46
63
  assert_eq!(lenv.get_var("z"), None);
47
64
  }
48
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
+
49
83
  #[test]
50
84
  fn test_local_env_override() {
51
85
  let mut lenv = LocalEnv::new();