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
63 changes: 63 additions & 0 deletions rust/src/analyzer/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ use super::dispatch::{
dispatch_needs_child, dispatch_simple, finish_ivar_write, finish_local_var_write,
finish_method_call, DispatchResult, NeedsChildKind,
};
use super::parameters::{
install_keyword_rest_parameter, install_optional_parameter, install_required_parameter,
install_rest_parameter,
};

/// Build graph from AST
pub struct AstInstaller<'a> {
Expand Down Expand Up @@ -111,6 +115,12 @@ impl<'a> AstInstaller<'a> {
let method_name = String::from_utf8_lossy(def_node.name().as_slice()).to_string();
install_method(self.genv, method_name);

// Process parameters BEFORE processing body
// This ensures parameters are available as local variables in the method body
if let Some(params_node) = def_node.parameters() {
self.install_parameters(&params_node);
}

if let Some(body) = def_node.body() {
if let Some(statements) = body.as_statements_node() {
self.install_statements(&statements);
Expand All @@ -121,6 +131,59 @@ impl<'a> AstInstaller<'a> {
None
}

/// Install method parameters as local variables
fn install_parameters(&mut self, params_node: &ruby_prism::ParametersNode) {
// Required parameters: def foo(a, b)
for node in params_node.requireds().iter() {
if let Some(req_param) = node.as_required_parameter_node() {
let name = String::from_utf8_lossy(req_param.name().as_slice()).to_string();
install_required_parameter(self.genv, self.lenv, name);
}
}

// Optional parameters: def foo(a = 1, b = "hello")
for node in params_node.optionals().iter() {
if let Some(opt_param) = node.as_optional_parameter_node() {
let name = String::from_utf8_lossy(opt_param.name().as_slice()).to_string();
let default_value = opt_param.value();

// Process default value to get its type
if let Some(default_vtx) = self.install_node(&default_value) {
install_optional_parameter(
self.genv,
self.lenv,
&mut self.changes,
name,
default_vtx,
);
} else {
// Fallback to untyped if default can't be processed
install_required_parameter(self.genv, self.lenv, name);
}
}
}

// Rest parameter: def foo(*args)
if let Some(rest_node) = params_node.rest() {
if let Some(rest_param) = rest_node.as_rest_parameter_node() {
if let Some(name_id) = rest_param.name() {
let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
install_rest_parameter(self.genv, self.lenv, name);
}
}
}

// Keyword rest parameter: def foo(**kwargs)
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() {
let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
install_keyword_rest_parameter(self.genv, self.lenv, name);
}
}
}
}

/// Process multiple statements
fn install_statements(&mut self, statements: &ruby_prism::StatementsNode) {
for stmt in &statements.body() {
Expand Down
1 change: 1 addition & 0 deletions rust/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod definitions;
mod dispatch;
mod install;
mod literals;
mod parameters;
mod variables;

#[cfg(test)]
Expand Down
154 changes: 154 additions & 0 deletions rust/src/analyzer/parameters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//! Parameter Handlers - Processing Ruby method/block parameters
//!
//! This module is responsible for:
//! - Extracting parameter names from DefNode
//! - Creating vertices for parameters
//! - Registering parameters as local variables in method scope

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

/// Install a required parameter as a local variable
///
/// Required parameters start with Bot (untyped) type since we don't know
/// what type will be passed at call sites.
///
/// # Example
/// ```ruby
/// def greet(name) # 'name' is a required parameter
/// name.upcase
/// end
/// ```
pub fn install_required_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
// Create a vertex for the parameter (starts as Bot/untyped)
let param_vtx = genv.new_vertex();

// Register in LocalEnv for variable lookup
lenv.new_var(name, param_vtx);

param_vtx
}

/// Install an optional parameter with a default value
///
/// The parameter's type is inferred from the default value expression.
///
/// # Example
/// ```ruby
/// def greet(name = "World") # 'name' has type String from default
/// name.upcase
/// end
/// ```
pub fn install_optional_parameter(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
_changes: &mut ChangeSet,
name: String,
default_value_vtx: VertexId,
) -> VertexId {
// Create a vertex for the parameter
let param_vtx = genv.new_vertex();

// Connect default value to parameter vertex for type inference
// Use genv.add_edge directly so the type is immediately propagated
// before the method body is processed
genv.add_edge(default_value_vtx, param_vtx);

// Register in LocalEnv for variable lookup
lenv.new_var(name, param_vtx);

param_vtx
}

/// Install a rest parameter (*args) as a local variable with Array type
///
/// Rest parameters collect all remaining arguments into an Array.
///
/// # Example
/// ```ruby
/// def collect(*items) # 'items' has type Array
/// items.first
/// end
/// ```
pub fn install_rest_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
// Create a vertex for the parameter
let param_vtx = genv.new_vertex();

// Rest parameters are always Arrays
let array_src = genv.new_source(Type::array());
genv.add_edge(array_src, param_vtx);

// Register in LocalEnv for variable lookup
lenv.new_var(name, param_vtx);

param_vtx
}

/// Install a keyword rest parameter (**kwargs) as a local variable with Hash type
///
/// Keyword rest parameters collect all remaining keyword arguments into a Hash.
///
/// # Example
/// ```ruby
/// def configure(**options) # 'options' has type Hash
/// options[:debug]
/// end
/// ```
pub fn install_keyword_rest_parameter(
genv: &mut GlobalEnv,
lenv: &mut LocalEnv,
name: String,
) -> VertexId {
// Create a vertex for the parameter
let param_vtx = genv.new_vertex();

// Keyword rest parameters are always Hashes
let hash_src = genv.new_source(Type::hash());
genv.add_edge(hash_src, param_vtx);

// Register in LocalEnv for variable lookup
lenv.new_var(name, param_vtx);

param_vtx
}

#[cfg(test)]
mod tests {
use super::*;

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

let vtx = install_required_parameter(&mut genv, &mut lenv, "name".to_string());

// Parameter should be registered in LocalEnv
assert_eq!(lenv.get_var("name"), Some(vtx));

// Vertex should exist in GlobalEnv (as untyped)
let vertex = genv.get_vertex(vtx);
assert!(vertex.is_some());
}

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

let vtx_a = install_required_parameter(&mut genv, &mut lenv, "a".to_string());
let vtx_b = install_required_parameter(&mut genv, &mut lenv, "b".to_string());
let vtx_c = install_required_parameter(&mut genv, &mut lenv, "c".to_string());

// All parameters should be registered
assert_eq!(lenv.get_var("a"), Some(vtx_a));
assert_eq!(lenv.get_var("b"), Some(vtx_b));
assert_eq!(lenv.get_var("c"), Some(vtx_c));

// All vertices should be different
assert_ne!(vtx_a, vtx_b);
assert_ne!(vtx_b, vtx_c);
assert_ne!(vtx_a, vtx_c);
}
}
Loading