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
3 changes: 2 additions & 1 deletion rust/src/analyzer/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(crate) fn process_attr_declaration(

// Register getter (attr_reader / attr_accessor)
if matches!(kind, AttrKind::Reader | AttrKind::Accessor) {
genv.register_user_method(recv_ty.clone(), &attr_name, ivar_vtx, vec![]);
genv.register_user_method(recv_ty.clone(), &attr_name, ivar_vtx, vec![], None);
}

// Register setter (attr_writer / attr_accessor)
Expand All @@ -51,6 +51,7 @@ pub(crate) fn process_attr_declaration(
&format!("{}=", attr_name),
ivar_vtx,
vec![param_vtx],
None,
);
}
}
Expand Down
10 changes: 7 additions & 3 deletions rust/src/analyzer/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
//! - Managing return value vertices
//! - Attaching source location for error reporting

use std::collections::HashMap;

use crate::env::GlobalEnv;
use crate::graph::{MethodCallBox, VertexId};
use crate::source_map::SourceLocation;
Expand All @@ -15,14 +17,16 @@ pub fn install_method_call(
recv_vtx: VertexId,
method_name: String,
arg_vtxs: Vec<VertexId>,
kwarg_vtxs: Option<HashMap<String, VertexId>>,
location: Option<SourceLocation>,
) -> VertexId {
// Create Vertex for return value
let ret_vtx = genv.new_vertex();

// Create MethodCallBox with location and argument vertices
let box_id = genv.alloc_box_id();
let call_box = MethodCallBox::new(box_id, recv_vtx, method_name, ret_vtx, arg_vtxs, location);
let call_box =
MethodCallBox::new(box_id, recv_vtx, method_name, ret_vtx, arg_vtxs, kwarg_vtxs, location);
genv.register_box(box_id, Box::new(call_box));

ret_vtx
Expand All @@ -39,7 +43,7 @@ mod tests {

let recv_vtx = genv.new_source(Type::string());
let ret_vtx =
install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None);
install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None, None);

// Return vertex should exist
assert!(genv.get_vertex(ret_vtx).is_some());
Expand All @@ -51,7 +55,7 @@ mod tests {

let recv_vtx = genv.new_source(Type::string());
let _ret_vtx =
install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None);
install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None, None);

// Box should be added
assert_eq!(genv.box_count(), 1);
Expand Down
8 changes: 6 additions & 2 deletions rust/src/analyzer/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//! - Method definition scope management (def baz ... end)
//! - Extracting class/module names from AST nodes (including qualified names like Api::User)

use std::collections::HashMap;

use crate::env::{GlobalEnv, LocalEnv};
use crate::graph::{ChangeSet, VertexId};
use crate::types::Type;
Expand Down Expand Up @@ -78,10 +80,10 @@ pub(crate) fn process_def_node(
let merge_vtx = genv.scope_manager.current_method_return_vertex();

// Process parameters BEFORE processing body
let param_vtxs = if let Some(params_node) = def_node.parameters() {
let (param_vtxs, keyword_param_vtxs) = if let Some(params_node) = def_node.parameters() {
install_parameters(genv, lenv, changes, source, &params_node)
} else {
vec![]
(vec![], HashMap::new())
};

let mut last_vtx = None;
Expand All @@ -108,11 +110,13 @@ pub(crate) fn process_def_node(
} else {
Type::instance(&name)
};
let kw_params = (!keyword_param_vtxs.is_empty()).then_some(keyword_param_vtxs);
genv.register_user_method(
recv_type,
&method_name,
ret_vtx,
param_vtxs,
kw_params,
);
}
}
Expand Down
148 changes: 142 additions & 6 deletions rust/src/analyzer/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! This module handles the pattern matching of Ruby AST nodes
//! and dispatches them to specialized handlers.

use std::collections::HashMap;

use crate::env::{GlobalEnv, LocalEnv};
use crate::graph::{BlockParameterTypeBox, ChangeSet, VertexId};
use crate::source_map::SourceLocation;
Expand Down Expand Up @@ -294,10 +296,31 @@ fn process_method_call_common<'a>(
return Some(super::operators::process_not_operator(genv));
}

let arg_vtxs: Vec<VertexId> = arguments
.iter()
.filter_map(|arg| super::install::install_node(genv, lenv, changes, source, arg))
.collect();
// Separate positional arguments and keyword arguments
let mut positional_arg_vtxs: Vec<VertexId> = Vec::new();
let mut keyword_arg_vtxs: HashMap<String, VertexId> = HashMap::new();

for arg in &arguments {
if let Some(kw_hash) = arg.as_keyword_hash_node() {
for element in kw_hash.elements().iter() {
let assoc = match element.as_assoc_node() {
Some(a) => a,
None => continue,
};
let name = match assoc.key().as_symbol_node() {
Some(sym) => bytes_to_name(sym.unescaped()),
None => continue,
};
if let Some(vtx) =
super::install::install_node(genv, lenv, changes, source, &assoc.value())
{
keyword_arg_vtxs.insert(name, vtx);
}
}
} else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, arg) {
positional_arg_vtxs.push(vtx);
}
}

if let Some(block_node) = block {
if let Some(block) = block_node.as_block_node() {
Expand All @@ -318,8 +341,19 @@ fn process_method_call_common<'a>(
}
}

let kwarg_vtxs = if keyword_arg_vtxs.is_empty() {
None
} else {
Some(keyword_arg_vtxs)
};

Some(finish_method_call(
genv, recv_vtx, method_name, arg_vtxs, location,
genv,
recv_vtx,
method_name,
positional_arg_vtxs,
kwarg_vtxs,
location,
))
}

Expand All @@ -329,9 +363,10 @@ fn finish_method_call(
recv_vtx: VertexId,
method_name: String,
arg_vtxs: Vec<VertexId>,
kwarg_vtxs: Option<HashMap<String, VertexId>>,
location: SourceLocation,
) -> VertexId {
install_method_call(genv, recv_vtx, method_name, arg_vtxs, Some(location))
install_method_call(genv, recv_vtx, method_name, arg_vtxs, kwarg_vtxs, Some(location))
}

#[cfg(test)]
Expand Down Expand Up @@ -984,4 +1019,105 @@ end
genv.type_errors
);
}

// === Keyword argument tests ===

// Test 26: Required keyword argument type propagation
#[test]
fn test_keyword_arg_required_propagation() {
let source = r#"
class Greeter
def greet(name:)
name
end
end

Greeter.new.greet(name: "Alice")
"#;
let genv = analyze(source);

let info = genv
.resolve_method(&Type::instance("Greeter"), "greet")
.expect("Greeter#greet should be registered");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "String");
}

// Test 27: Optional keyword argument with default type
#[test]
fn test_keyword_arg_optional_default_type() {
let source = r#"
class Counter
def count(step: 1)
step
end
end
"#;
let genv = analyze(source);

let info = genv
.resolve_method(&Type::instance("Counter"), "count")
.expect("Counter#count should be registered");
let ret_vtx = info.return_vertex.unwrap();
// step has Integer type from default value
assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
}

// Test 28: Positional and keyword arguments mixed
#[test]
fn test_positional_and_keyword_mixed() {
let source = r#"
class User
def initialize(id, name:)
@id = id
@name = name
end
end

User.new(1, name: "Alice")
"#;
let genv = analyze(source);
assert!(genv.type_errors.is_empty());
}

// Test 29: Keyword argument via .new propagation to initialize
#[test]
fn test_keyword_arg_via_new_to_initialize() {
let source = r#"
class Config
def initialize(debug:)
@debug = debug
end

def debug?
@debug
end
end

Config.new(debug: true)
"#;
let genv = analyze(source);
assert!(genv.type_errors.is_empty());
}

// Test 30: Multiple keyword arguments
#[test]
fn test_multiple_keyword_args() {
let source = r#"
class User
def profile(name:, age:)
name
end
end

User.new.profile(name: "Alice", age: 30)
"#;
let genv = analyze(source);

let info = genv
.resolve_method(&Type::instance("User"), "profile")
.expect("User#profile should be registered");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "String");
}
}
59 changes: 54 additions & 5 deletions rust/src/analyzer/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
//! - Creating vertices for parameters
//! - Registering parameters as local variables in method scope

use std::collections::HashMap;

use crate::env::{GlobalEnv, LocalEnv};
use crate::graph::{ChangeSet, VertexId};
use crate::types::Type;
Expand Down Expand Up @@ -117,16 +119,18 @@ pub fn install_keyword_rest_parameter(

/// Install method parameters as local variables
///
/// Returns a Vec of VertexId for required and optional parameters (positional),
/// which can be used for argument-to-parameter type propagation.
/// Returns a tuple of:
/// - Vec<VertexId>: positional parameter vertices (required and optional)
/// - HashMap<String, VertexId>: keyword parameter vertices (name → vertex)
pub(crate) fn install_parameters(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
changes: &mut ChangeSet,
source: &str,
params_node: &ruby_prism::ParametersNode,
) -> Vec<VertexId> {
) -> (Vec<VertexId>, HashMap<String, VertexId>) {
let mut param_vtxs = Vec::new();
let mut keyword_param_vtxs: HashMap<String, VertexId> = HashMap::new();

// Required parameters: def foo(a, b)
for node in params_node.requireds().iter() {
Expand Down Expand Up @@ -165,8 +169,30 @@ pub(crate) fn install_parameters(
}
}

// Keyword parameters: def foo(name:, age: 0)
// Reuses install_required_parameter / install_optional_parameter
// since the logic is identical for positional and keyword parameters.
for node in params_node.keywords().iter() {
if let Some(req_kw) = node.as_required_keyword_parameter_node() {
let name = bytes_to_name(req_kw.name().as_slice());
let vtx = install_required_parameter(genv, lenv, name.clone());
keyword_param_vtxs.insert(name, vtx);
} else if let Some(opt_kw) = node.as_optional_keyword_parameter_node() {
let name = bytes_to_name(opt_kw.name().as_slice());
let default_value = opt_kw.value();
let vtx = if let Some(default_vtx) =
super::install::install_node(genv, lenv, changes, source, &default_value)
{
install_optional_parameter(genv, lenv, changes, name.clone(), default_vtx)
} else {
install_required_parameter(genv, lenv, name.clone())
};
keyword_param_vtxs.insert(name, vtx);
}
}

// Keyword rest parameter: def foo(**kwargs)
// Not included in param_vtxs (keyword args need special handling)
// Not included in keyword_param_vtxs (collects all remaining keywords)
if let Some(kwrest_node) = params_node.keyword_rest() {
if let Some(kwrest_param) = kwrest_node.as_keyword_rest_parameter_node() {
if let Some(name_id) = kwrest_param.name() {
Expand All @@ -176,7 +202,7 @@ pub(crate) fn install_parameters(
}
}

param_vtxs
(param_vtxs, keyword_param_vtxs)
}

#[cfg(test)]
Expand Down Expand Up @@ -217,4 +243,27 @@ mod tests {
assert_ne!(vtx_b, vtx_c);
assert_ne!(vtx_a, vtx_c);
}

#[test]
fn test_install_optional_parameter_inherits_default_type() {
let mut genv = GlobalEnv::new();
let mut lenv = LocalEnv::new();
let mut changes = ChangeSet::new();

// Default value: 0 (Integer)
let default_vtx = genv.new_source(Type::integer());
let vtx = install_optional_parameter(
&mut genv,
&mut lenv,
&mut changes,
"age".to_string(),
default_vtx,
);

assert_eq!(lenv.get_var("age"), Some(vtx));

// Type should propagate from default value
let vertex = genv.get_vertex(vtx).unwrap();
assert_eq!(vertex.show(), "Integer");
}
}
Loading