Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/analyzer/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub(crate) fn process_block_node_with_params(
}

/// Install block parameters and return their vertex IDs
fn install_block_parameters_with_vtxs(
pub(crate) fn install_block_parameters_with_vtxs(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
changes: &mut ChangeSet,
Expand Down
25 changes: 25 additions & 0 deletions core/src/analyzer/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ pub(crate) enum NeedsChildKind<'a> {
/// Global variable write: need to process value, then call install_global_var_write
GlobalVarWrite { gvar_name: String, value: Node<'a> },
ConstantWrite { const_name: String, value: Node<'a> },
/// Proc/lambda literals
ProcLiteral { block: Node<'a> },
/// Method call: need to process receiver, then call finish_method_call
MethodCall {
receiver: Node<'a>,
Expand Down Expand Up @@ -289,6 +291,16 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
.unwrap_or_default();

if let Some(receiver) = call_node.receiver() {
if method_name == "new" {
if let Some(const_read) = receiver.as_constant_read_node() {
if const_read.name().as_slice() == b"Proc" {
if let Some(block_node) = block {
return Some(NeedsChildKind::ProcLiteral { block: block_node });
}
}
}
}

// Explicit receiver: x.upcase, x.each { |i| ... }
let prism_location = call_node
.call_operator_loc()
Expand All @@ -307,6 +319,12 @@ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<
} else {
// No receiver: implicit self method call (e.g., `name`, `puts "hello"`)

if matches!(method_name.as_str(), "lambda" | "proc") {
if let Some(block_node) = block {
return Some(NeedsChildKind::ProcLiteral { block: block_node });
}
}

if let Some(kind) = match method_name.as_str() {
"attr_reader" => Some(AttrKind::Reader),
"attr_writer" => Some(AttrKind::Writer),
Expand Down Expand Up @@ -385,6 +403,13 @@ pub(crate) fn process_needs_child(
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
Some(install_local_var_write(genv, lenv, changes, var_name, value_vtx))
}
NeedsChildKind::ProcLiteral { block } => {
if let Some(block_node) = block.as_block_node() {
super::lambdas::process_block_as_proc(genv, lenv, changes, source, &block_node)
} else {
None
}
}
NeedsChildKind::MethodCall {
receiver,
method_name,
Expand Down
5 changes: 5 additions & 0 deletions core/src/analyzer/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use super::blocks::process_block_node;
use super::conditionals::{process_case_match_node, process_case_node, process_if_node, process_unless_node};
use super::definitions::{process_class_node, process_def_node, process_module_node};
use super::exceptions::{process_begin_node, process_rescue_modifier_node};
use super::lambdas::process_lambda_node;
use super::dispatch::{dispatch_needs_child, dispatch_simple, process_needs_child, DispatchResult};
use super::literals::install_literal_node;
use super::loops::{process_for_node, process_until_node, process_while_node};
Expand Down Expand Up @@ -73,6 +74,10 @@ pub(crate) fn install_node(
return process_block_node(genv, lenv, changes, source, &block_node);
}

if let Some(lambda_node) = node.as_lambda_node() {
return process_lambda_node(genv, lenv, changes, source, &lambda_node);
}

if let Some(if_node) = node.as_if_node() {
return process_if_node(genv, lenv, changes, source, &if_node);
}
Expand Down
153 changes: 153 additions & 0 deletions core/src/analyzer/lambdas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Lambdas/Procs - type inference for lambda and proc literals
//!
//! Handles `-> { }`, `lambda { }`, `proc { }`, and `Proc.new { }`.

use ruby_prism::{LambdaNode, Node};

use crate::env::{GlobalEnv, LocalEnv};
use crate::graph::{ChangeSet, VertexId};
use crate::types::Type;

pub(crate) fn process_lambda_node(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
changes: &mut ChangeSet,
source: &str,
lambda_node: &LambdaNode,
) -> Option<VertexId> {
process_proc_body(genv, lenv, changes, source, lambda_node.parameters(), lambda_node.body())
}

pub(crate) fn process_block_as_proc(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
changes: &mut ChangeSet,
source: &str,
block_node: &ruby_prism::BlockNode,
) -> Option<VertexId> {
process_proc_body(genv, lenv, changes, source, block_node.parameters(), block_node.body())
}

fn process_proc_body<'a>(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
changes: &mut ChangeSet,
source: &str,
parameters: Option<Node<'a>>,
body: Option<Node<'a>>,
) -> Option<VertexId> {
let (_scope_id, merge_vtx) = genv.enter_lambda();

let param_vtxs = parameters
.and_then(|p| p.as_block_parameters_node())
.map(|bp| {
super::blocks::install_block_parameters_with_vtxs(genv, lenv, changes, source, &bp)
})
.unwrap_or_default();

let body_vtx = body.and_then(|b| {
if let Some(stmts) = b.as_statements_node() {
super::install::install_statements(genv, lenv, changes, source, &stmts)
} else {
super::install::install_node(genv, lenv, changes, source, &b)
}
});

if let Some(vtx) = body_vtx {
genv.add_edge(vtx, merge_vtx);
}

genv.exit_scope();

let proc_ty = Type::proc_type_with_vertex(merge_vtx, param_vtxs);
let proc_vtx = genv.new_source(proc_ty);

Some(proc_vtx)
}

#[cfg(test)]
mod tests {
use crate::analyzer::AstInstaller;
use crate::env::{GlobalEnv, LocalEnv};
use crate::parser::ParseSession;
use crate::types::Type;

fn parse_and_install(source: &str) -> GlobalEnv {
parse_and_install_with(source, |_| {})
}

fn parse_and_install_with_builtin(source: &str) -> GlobalEnv {
parse_and_install_with(source, |genv| {
genv.register_builtin_method(Type::string(), "upcase", Type::string());
})
}

fn parse_and_install_with(source: &str, setup: impl FnOnce(&mut GlobalEnv)) -> GlobalEnv {
let session = ParseSession::new();
let result = session.parse_source(source, "<test>").unwrap();
let mut genv = GlobalEnv::new();
setup(&mut genv);
let mut lenv = LocalEnv::new();
let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);

let root = result.node();
if let Some(program_node) = root.as_program_node() {
let statements = program_node.statements();
for stmt in &statements.body() {
installer.install_node(&stmt);
}
}
installer.finish();
genv
}

#[test]
fn test_lambda_basic_no_crash() {
let genv = parse_and_install("f = -> { 42 }");
assert!(genv.type_errors.is_empty());
}

#[test]
fn test_lambda_with_params_no_crash() {
let genv = parse_and_install("f = -> (x) { x }");
assert!(genv.type_errors.is_empty());
}

#[test]
fn test_lambda_body_type_error_detected() {
let genv = parse_and_install_with_builtin("f = -> { 42.upcase }");
assert!(
!genv.type_errors.is_empty(),
"Expected type error for 42.upcase inside lambda"
);
}

#[test]
fn test_lambda_method_basic() {
let genv = parse_and_install("f = lambda { 42 }");
assert!(genv.type_errors.is_empty());
}

#[test]
fn test_proc_method_basic() {
let genv = parse_and_install("f = proc { 42 }");
assert!(genv.type_errors.is_empty());
}

#[test]
fn test_proc_new_basic() {
let genv = parse_and_install("f = Proc.new { 42 }");
assert!(genv.type_errors.is_empty());
}

#[test]
fn test_lambda_call_arg_type_propagation() {
let genv = parse_and_install_with_builtin(
"f = ->(x) { x.upcase }\nf.call(42)",
);
assert!(
!genv.type_errors.is_empty(),
"Expected type error for 42.upcase via lambda arg propagation"
);
}
}
1 change: 1 addition & 0 deletions core/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod definitions;
mod exceptions;
mod dispatch;
mod install;
mod lambdas;
mod literals;
mod loops;
mod operators;
Expand Down
10 changes: 10 additions & 0 deletions core/src/env/global_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,16 @@ impl GlobalEnv {
scope_id
}

/// Enter a lambda scope
pub fn enter_lambda(&mut self) -> (ScopeId, VertexId) {
let merge_vtx = self.new_vertex();
let scope_id = self.scope_manager.new_scope(ScopeKind::Lambda {
return_vertex: Some(merge_vtx),
});
self.scope_manager.enter_scope(scope_id);
(scope_id, merge_vtx)
}

/// Exit current scope
pub fn exit_scope(&mut self) {
self.scope_manager.exit_scope();
Expand Down
15 changes: 8 additions & 7 deletions core/src/env/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub enum ScopeKind {
return_vertex: Option<VertexId>, // Merge vertex for return statements
},
Block,
Lambda {
return_vertex: Option<VertexId>,
},
}

/// Scope information
Expand Down Expand Up @@ -277,14 +280,12 @@ impl ScopeManager {
})
}

/// Get return_vertex from the nearest enclosing method scope
/// Get return_vertex from the nearest enclosing method or lambda scope
pub fn current_method_return_vertex(&self) -> Option<VertexId> {
self.walk_scopes().find_map(|scope| {
if let ScopeKind::Method { return_vertex, .. } = &scope.kind {
*return_vertex
} else {
None
}
self.walk_scopes().find_map(|scope| match &scope.kind {
ScopeKind::Method { return_vertex, .. } => *return_vertex,
ScopeKind::Lambda { return_vertex } => *return_vertex,
_ => None,
})
}

Expand Down
11 changes: 11 additions & 0 deletions core/src/graph/box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ impl MethodCallBox {
return;
}

if let Type::Proc { return_vertex, param_vertices, .. } = recv_ty {
if self.method_name == "call" {
if let Some(merge_vtx) = return_vertex {
changes.add_edge(*merge_vtx, self.ret);
}
propagate_arguments(&self.arg_vtxs, Some(param_vertices), changes);
}
// TODO: Proc#arity, Proc#curry etc. not yet resolved via RBS
return;
}

if let Some(method_info) = genv.resolve_method(recv_ty, &self.method_name) {
if let Some(return_vtx) = method_info.return_vertex {
// User-defined method: connect body's return vertex to call site
Expand Down
34 changes: 34 additions & 0 deletions core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ pub enum Type {
Union(Vec<Type>),
/// Bottom type: no type information
Bot,
Proc {
return_vertex: Option<crate::graph::VertexId>,
param_vertices: Vec<crate::graph::VertexId>,
},
}

impl Type {
Expand All @@ -159,6 +163,10 @@ impl Type {
names.join(" | ")
}
Type::Bot => "untyped".to_string(),
Type::Proc { param_vertices, .. } => {
let params = vec!["untyped"; param_vertices.len()];
format!("Proc[({})->untyped]", params.join(", "))
}
}
}

Expand Down Expand Up @@ -277,6 +285,16 @@ impl Type {
}
}

pub fn proc_type_with_vertex(
return_vertex: crate::graph::VertexId,
param_vertices: Vec<crate::graph::VertexId>,
) -> Self {
Type::Proc {
return_vertex: Some(return_vertex),
param_vertices,
}
}

/// Collapse a Vec<Type> into a single Type or Union.
/// Returns the single element if len==1, or Union if len>1.
/// Panics if the vec is empty.
Expand Down Expand Up @@ -417,4 +435,20 @@ mod tests {
assert_eq!(singleton.show(), "singleton(Api::User)");
assert_eq!(singleton.base_class_name(), Some("Api::User"));
}

#[test]
fn test_proc_type_show() {
let proc_ty = Type::Proc {
return_vertex: None,
param_vertices: vec![crate::graph::VertexId(99)],
};
assert_eq!(proc_ty.show(), "Proc[(untyped)->untyped]");

let proc_no_params = Type::Proc {
return_vertex: None,
param_vertices: vec![],
};
assert_eq!(proc_no_params.show(), "Proc[()->untyped]");
}

}
Loading