Skip to content

Commit b42e628

Browse files
authored
Merge pull request #29 from dak2/return-statement-type-inference
Support return statement type inference with merge vertex pattern
2 parents 46873aa + 0ec147f commit b42e628

7 files changed

Lines changed: 364 additions & 3 deletions

File tree

rust/src/analyzer/definitions.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ pub(crate) fn process_def_node(
6767
let method_name = String::from_utf8_lossy(def_node.name().as_slice()).to_string();
6868
install_method(genv, method_name.clone());
6969

70+
let merge_vtx = genv.scope_manager.current_method_return_vertex();
71+
7072
// Process parameters BEFORE processing body
7173
let param_vtxs = if let Some(params_node) = def_node.parameters() {
7274
install_parameters(genv, lenv, changes, source, &params_node)
@@ -81,15 +83,22 @@ pub(crate) fn process_def_node(
8183
}
8284
}
8385

84-
// Register user-defined method with return vertex and param vertices (before exiting scope)
85-
if let Some(return_vtx) = last_vtx {
86+
// Connect last expression to merge vertex so that implicit return
87+
// (Ruby's last-expression-is-return-value) is included in the union type
88+
if let (Some(last), Some(merge)) = (last_vtx, merge_vtx) {
89+
genv.add_edge(last, merge);
90+
}
91+
92+
// Register user-defined method with merge vertex as return vertex
93+
let return_vtx = merge_vtx.or(last_vtx);
94+
if let Some(ret_vtx) = return_vtx {
8695
let recv_type_name = genv.scope_manager.current_qualified_name();
8796

8897
if let Some(name) = recv_type_name {
8998
genv.register_user_method(
9099
Type::instance(&name),
91100
&method_name,
92-
return_vtx,
101+
ret_vtx,
93102
param_vtxs,
94103
);
95104
}

rust/src/analyzer/install.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use super::definitions::{process_class_node, process_def_node, process_module_no
1414
use super::dispatch::{dispatch_needs_child, dispatch_simple, process_needs_child, DispatchResult};
1515
use super::literals::install_literal_node;
1616
use super::parentheses::process_parentheses_node;
17+
use super::returns::process_return_node;
1718

1819
/// Build graph from AST (public API wrapper)
1920
pub struct AstInstaller<'a> {
@@ -81,6 +82,10 @@ pub(crate) fn install_node(
8182
return process_parentheses_node(genv, lenv, changes, source, &paren_node);
8283
}
8384

85+
if let Some(return_node) = node.as_return_node() {
86+
return process_return_node(genv, lenv, changes, source, &return_node);
87+
}
88+
8489
match dispatch_simple(genv, lenv, node) {
8590
DispatchResult::Vertex(vtx) => return Some(vtx),
8691
DispatchResult::NotHandled => {}

rust/src/analyzer/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod install;
88
mod literals;
99
mod parameters;
1010
mod parentheses;
11+
mod returns;
1112
mod variables;
1213

1314
pub use install::AstInstaller;

rust/src/analyzer/returns.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
//! Return statement handling
2+
//!
3+
//! Processes `return expr` by connecting the expression's vertex
4+
//! to the enclosing method's merge vertex.
5+
6+
use crate::env::{GlobalEnv, LocalEnv};
7+
use crate::graph::{ChangeSet, VertexId};
8+
9+
use super::install::install_node;
10+
11+
/// Process ReturnNode: connect return value to method's merge vertex
12+
pub(crate) fn process_return_node(
13+
genv: &mut GlobalEnv,
14+
lenv: &mut LocalEnv,
15+
changes: &mut ChangeSet,
16+
source: &str,
17+
return_node: &ruby_prism::ReturnNode,
18+
) -> Option<VertexId> {
19+
// Process return value (first argument only; multi-value return not yet supported)
20+
let value_vtx = if let Some(arguments) = return_node.arguments() {
21+
arguments
22+
.arguments()
23+
.iter()
24+
.next()
25+
.and_then(|arg| install_node(genv, lenv, changes, source, &arg))
26+
} else {
27+
// `return` without value → nil
28+
Some(genv.new_source(crate::types::Type::Nil))
29+
};
30+
31+
// Connect return value to method's merge vertex
32+
if let Some(vtx) = value_vtx {
33+
if let Some(merge_vtx) = genv.scope_manager.current_method_return_vertex() {
34+
genv.add_edge(vtx, merge_vtx);
35+
}
36+
}
37+
38+
None
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use crate::env::{GlobalEnv, LocalEnv};
44+
use crate::graph::ChangeSet;
45+
use crate::parser::ParseSession;
46+
use crate::types::Type;
47+
48+
fn setup_and_infer(source: &str) -> GlobalEnv {
49+
let session = ParseSession::new();
50+
let parse_result = session.parse_source(source, "test.rb").unwrap();
51+
let root = parse_result.node();
52+
let program = root.as_program_node().unwrap();
53+
54+
let mut genv = GlobalEnv::new();
55+
let mut lenv = LocalEnv::new();
56+
let mut changes = ChangeSet::new();
57+
58+
for stmt in &program.statements().body() {
59+
crate::analyzer::install::install_node(
60+
&mut genv, &mut lenv, &mut changes, source, &stmt,
61+
);
62+
}
63+
64+
genv.apply_changes(changes);
65+
genv.run_all();
66+
genv
67+
}
68+
69+
fn get_return_type(genv: &GlobalEnv, class_name: &str, method_name: &str) -> String {
70+
let info = genv
71+
.resolve_method(&Type::instance(class_name), method_name)
72+
.unwrap_or_else(|| panic!("{}#{} should be registered", class_name, method_name));
73+
let vtx = info
74+
.return_vertex
75+
.expect("return_vertex should be Some");
76+
77+
if let Some(source) = genv.get_source(vtx) {
78+
source.ty.show()
79+
} else if let Some(vertex) = genv.get_vertex(vtx) {
80+
vertex.show()
81+
} else {
82+
panic!("return_vertex not found");
83+
}
84+
}
85+
86+
#[test]
87+
fn test_simple_return() {
88+
let source = r#"
89+
class Foo
90+
def bar
91+
return "hello"
92+
end
93+
end
94+
"#;
95+
let genv = setup_and_infer(source);
96+
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
97+
}
98+
99+
#[test]
100+
fn test_return_with_implicit_return_union() {
101+
let source = r#"
102+
class Foo
103+
def bar
104+
return "hello" if true
105+
42
106+
end
107+
end
108+
"#;
109+
let genv = setup_and_infer(source);
110+
let ty = get_return_type(&genv, "Foo", "bar");
111+
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
112+
assert!(ty.contains("String"), "should contain String, got: {}", ty);
113+
}
114+
115+
#[test]
116+
fn test_multiple_returns() {
117+
let source = r#"
118+
class Foo
119+
def bar
120+
return "a" if true
121+
return :b if false
122+
42
123+
end
124+
end
125+
"#;
126+
let genv = setup_and_infer(source);
127+
let ty = get_return_type(&genv, "Foo", "bar");
128+
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
129+
assert!(ty.contains("String"), "should contain String, got: {}", ty);
130+
assert!(ty.contains("Symbol"), "should contain Symbol, got: {}", ty);
131+
}
132+
133+
#[test]
134+
fn test_return_without_value() {
135+
let source = r#"
136+
class Foo
137+
def bar
138+
return if true
139+
42
140+
end
141+
end
142+
"#;
143+
let genv = setup_and_infer(source);
144+
let ty = get_return_type(&genv, "Foo", "bar");
145+
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
146+
assert!(ty.contains("nil"), "should contain nil, got: {}", ty);
147+
}
148+
149+
#[test]
150+
fn test_no_return_backward_compat() {
151+
let source = r#"
152+
class Foo
153+
def bar
154+
"hello"
155+
end
156+
end
157+
"#;
158+
let genv = setup_and_infer(source);
159+
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
160+
}
161+
162+
#[test]
163+
fn test_return_only_method() {
164+
let source = r#"
165+
class Foo
166+
def bar
167+
return "hello"
168+
end
169+
end
170+
"#;
171+
let genv = setup_and_infer(source);
172+
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
173+
}
174+
175+
#[test]
176+
fn test_return_dead_code_over_approximation() {
177+
let source = r#"
178+
class Foo
179+
def bar
180+
return "hello"
181+
42
182+
end
183+
end
184+
"#;
185+
let genv = setup_and_infer(source);
186+
let ty = get_return_type(&genv, "Foo", "bar");
187+
// Dead code after return is still processed (over-approximation)
188+
assert!(ty.contains("Integer"), "should contain Integer (dead code), got: {}", ty);
189+
assert!(ty.contains("String"), "should contain String, got: {}", ty);
190+
}
191+
}

rust/src/env/global_env.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,11 @@ impl GlobalEnv {
210210
pub fn enter_method(&mut self, name: String) -> ScopeId {
211211
// Look for class or module context
212212
let receiver_type = self.scope_manager.current_qualified_name();
213+
let return_vertex = Some(self.new_vertex());
213214
let scope_id = self.scope_manager.new_scope(ScopeKind::Method {
214215
name,
215216
receiver_type,
217+
return_vertex,
216218
});
217219
self.scope_manager.enter_scope(scope_id);
218220
scope_id

rust/src/env/scope.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub enum ScopeKind {
2020
Method {
2121
name: String,
2222
receiver_type: Option<String>, // Receiver class/module name
23+
return_vertex: Option<VertexId>, // Merge vertex for return statements
2324
},
2425
Block,
2526
}
@@ -309,6 +310,22 @@ impl ScopeManager {
309310
Some(result)
310311
}
311312

313+
/// Get return_vertex from the nearest enclosing method scope
314+
pub fn current_method_return_vertex(&self) -> Option<VertexId> {
315+
let mut current = Some(self.current_scope);
316+
while let Some(scope_id) = current {
317+
if let Some(scope) = self.scopes.get(&scope_id) {
318+
if let ScopeKind::Method { return_vertex, .. } = &scope.kind {
319+
return *return_vertex;
320+
}
321+
current = scope.parent;
322+
} else {
323+
break;
324+
}
325+
}
326+
None
327+
}
328+
312329
/// Lookup instance variable in enclosing module scope
313330
pub fn lookup_instance_var_in_module(&self, name: &str) -> Option<VertexId> {
314331
let mut current = Some(self.current_scope);
@@ -448,6 +465,7 @@ mod tests {
448465
let method_id = sm.new_scope(ScopeKind::Method {
449466
name: "test".to_string(),
450467
receiver_type: None,
468+
return_vertex: None,
451469
});
452470
sm.enter_scope(method_id);
453471

@@ -472,6 +490,7 @@ mod tests {
472490
let method_id = sm.new_scope(ScopeKind::Method {
473491
name: "helper".to_string(),
474492
receiver_type: Some("Utils".to_string()),
493+
return_vertex: None,
475494
});
476495
sm.enter_scope(method_id);
477496

@@ -500,6 +519,7 @@ mod tests {
500519
let method_id = sm.new_scope(ScopeKind::Method {
501520
name: "get_setting".to_string(),
502521
receiver_type: Some("Config".to_string()),
522+
return_vertex: None,
503523
});
504524
sm.enter_scope(method_id);
505525

@@ -559,6 +579,7 @@ mod tests {
559579
let method_id = sm.new_scope(ScopeKind::Method {
560580
name: "greet".to_string(),
561581
receiver_type: None,
582+
return_vertex: None,
562583
});
563584
sm.enter_scope(method_id);
564585

0 commit comments

Comments
 (0)