From f5f26f99d58fabe6f9e1c8e0472d88a28cd56a76 Mon Sep 17 00:00:00 2001 From: dak2 Date: Thu, 22 Jan 2026 23:13:21 +0900 Subject: [PATCH] support method params --- rust/src/analyzer/install.rs | 63 ++++++++ rust/src/analyzer/mod.rs | 1 + rust/src/analyzer/parameters.rs | 154 +++++++++++++++++++ rust/src/analyzer/tests/integration_test.rs | 162 ++++++++++++++++++++ 4 files changed, 380 insertions(+) create mode 100644 rust/src/analyzer/parameters.rs diff --git a/rust/src/analyzer/install.rs b/rust/src/analyzer/install.rs index 72ac188..a83d2cd 100644 --- a/rust/src/analyzer/install.rs +++ b/rust/src/analyzer/install.rs @@ -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> { @@ -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(¶ms_node); + } + if let Some(body) = def_node.body() { if let Some(statements) = body.as_statements_node() { self.install_statements(&statements); @@ -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() { diff --git a/rust/src/analyzer/mod.rs b/rust/src/analyzer/mod.rs index b65e1ff..e62eb0b 100644 --- a/rust/src/analyzer/mod.rs +++ b/rust/src/analyzer/mod.rs @@ -3,6 +3,7 @@ mod definitions; mod dispatch; mod install; mod literals; +mod parameters; mod variables; #[cfg(test)] diff --git a/rust/src/analyzer/parameters.rs b/rust/src/analyzer/parameters.rs new file mode 100644 index 0000000..7bb749a --- /dev/null +++ b/rust/src/analyzer/parameters.rs @@ -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); + } +} diff --git a/rust/src/analyzer/tests/integration_test.rs b/rust/src/analyzer/tests/integration_test.rs index d4874a5..112e93a 100644 --- a/rust/src/analyzer/tests/integration_test.rs +++ b/rust/src/analyzer/tests/integration_test.rs @@ -134,3 +134,165 @@ y = x.upcase.downcase let y_vtx = lenv.get_var("y").unwrap(); assert_eq!(genv.get_vertex(y_vtx).unwrap().show(), "String"); } + +// ============================================ +// Method Parameter Tests +// ============================================ + +#[test] +fn test_method_parameter_available_as_local_var() { + let source = r#" +def greet(name) + x = name +end +"#; + + let (genv, _lenv) = analyze(source); + + // No type errors should occur - name parameter should be available + assert_eq!(genv.type_errors.len(), 0); +} + +#[test] +fn test_method_multiple_parameters() { + let source = r#" +def calculate(a, b, c) + x = a + y = b + z = c +end +"#; + + let (genv, _lenv) = analyze(source); + + // No type errors should occur - all parameters should be available + assert_eq!(genv.type_errors.len(), 0); +} + +#[test] +fn test_class_method_with_parameter() { + let source = r#" +class User + def initialize(name) + @name = name + end +end +"#; + + let (genv, _lenv) = analyze(source); + + // No type errors should occur + assert_eq!(genv.type_errors.len(), 0); +} + +#[test] +fn test_parameter_method_call() { + // Parameter has Bot (untyped) type, so method calls won't error + // because we can't verify if the method exists on an untyped value + let source = r#" +def greet(name) + name.upcase +end +"#; + + let (genv, _lenv) = analyze(source); + + // With Bot type, we don't know if upcase exists or not + // Current behavior: Bot type means no method resolution, so no error + // This is acceptable for Phase 3 - we can improve later with call-site inference + assert!( + genv.type_errors.is_empty(), + "Bot (untyped) parameters should not produce method errors" + ); +} + +#[test] +fn test_optional_parameter_type_from_default() { + // Optional parameter with String default should have String type + let source = r#" +def greet(name = "World") + name.upcase +end +"#; + + let (genv, _lenv) = analyze(source); + + // No type errors - name is String from default value, upcase exists on String + assert_eq!(genv.type_errors.len(), 0); +} + +#[test] +fn test_optional_parameter_type_error() { + // Optional parameter with Integer default should error on String method + let source = r#" +def greet(count = 42) + count.upcase +end +"#; + + let (genv, _lenv) = analyze(source); + + // Type error should be detected: count is Integer, upcase is not available + assert_eq!(genv.type_errors.len(), 1); + assert_eq!(genv.type_errors[0].method_name, "upcase"); +} + +#[test] +fn test_mixed_required_and_optional_parameters() { + let source = r#" +def greet(greeting, name = "World") + x = greeting + y = name.upcase +end +"#; + + let (genv, _lenv) = analyze(source); + + // No type errors - name has String type from default + assert_eq!(genv.type_errors.len(), 0); +} + +#[test] +fn test_rest_parameter_has_array_type() { + let source = r#" +def collect(*items) + x = items +end +"#; + + let (genv, _lenv) = analyze(source); + + // No type errors - items is Array + assert_eq!(genv.type_errors.len(), 0); +} + +#[test] +fn test_keyword_rest_parameter_has_hash_type() { + let source = r#" +def configure(**options) + x = options +end +"#; + + let (genv, _lenv) = analyze(source); + + // No type errors - options is Hash + assert_eq!(genv.type_errors.len(), 0); +} + +#[test] +fn test_all_parameter_types_combined() { + let source = r#" +def complex_method(required, optional = "default", *rest, **kwargs) + a = required + b = optional.upcase + c = rest + d = kwargs +end +"#; + + let (genv, _lenv) = analyze(source); + + // No type errors - optional.upcase should work (String has upcase) + assert_eq!(genv.type_errors.len(), 0); +}