diff --git a/core/src/analyzer/blocks.rs b/core/src/analyzer/blocks.rs index ef83c5d..a157284 100644 --- a/core/src/analyzer/blocks.rs +++ b/core/src/analyzer/blocks.rs @@ -128,6 +128,55 @@ fn install_block_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: Stri #[cfg(test)] mod tests { use super::*; + use crate::analyzer::install::AstInstaller; + use crate::env::LocalEnv; + use crate::parser::ParseSession; + use crate::types::Type; + + fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String { + if let Some(vertex) = genv.get_vertex(vtx) { + vertex.show() + } else if let Some(source) = genv.get_source(vtx) { + source.ty.show() + } else { + panic!("vertex {:?} not found as either Vertex or Source", vtx); + } + } + + fn analyze_with_stdlib(source: &str) -> GlobalEnv { + let session = ParseSession::new(); + let parse_result = session.parse_source(source, "test.rb").unwrap(); + let root = parse_result.node(); + let program = root.as_program_node().unwrap(); + + let mut genv = GlobalEnv::new(); + + // Register stdlib methods needed for block tests + genv.register_builtin_method_with_block( + Type::array(), + "each", + Type::array(), + Some(vec![Type::instance("Elem")]), + ); + genv.register_builtin_method_with_block( + Type::string(), + "each_char", + Type::string(), + Some(vec![Type::string()]), + ); + genv.register_builtin_method(Type::integer(), "even?", Type::instance("TrueClass")); + genv.register_builtin_method(Type::string(), "upcase", Type::string()); + + let mut lenv = LocalEnv::new(); + + let mut installer = AstInstaller::new(&mut genv, &mut lenv, source); + for stmt in &program.statements().body() { + installer.install_node(&stmt); + } + installer.finish(); + + genv + } #[test] fn test_enter_exit_block_scope() { @@ -173,4 +222,95 @@ mod tests { exit_block_scope(&mut genv); } + + #[test] + fn test_block_parameter_type_from_array() { + let source = r#" +class Foo + def bar + [1, 2, 3].each { |x| x.even? } + end +end +"#; + let genv = analyze_with_stdlib(source); + assert!( + genv.type_errors.is_empty(), + "x.even? should not produce type errors: {:?}", + genv.type_errors + ); + // Verify bar returns Array (each returns its receiver) + let info = genv.resolve_method(&Type::instance("Foo"), "bar").unwrap(); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "Array"); + } + + #[test] + fn test_block_external_variable_access() { + let source = r#" +class Foo + def bar + y = "hello" + [1].each { y.upcase } + end +end +"#; + let genv = analyze_with_stdlib(source); + assert!( + genv.type_errors.is_empty(), + "y.upcase should not produce type errors: {:?}", + genv.type_errors + ); + } + + #[test] + fn test_block_parameter_from_each_char() { + let source = r#" +class Foo + def bar + "hello".each_char { |c| c.upcase } + end +end +"#; + let genv = analyze_with_stdlib(source); + assert!( + genv.type_errors.is_empty(), + "c.upcase should not produce type errors: {:?}", + genv.type_errors + ); + } + + #[test] + fn test_block_body_does_not_affect_method_return() { + let source = r#" +class Foo + def bar + [1, 2].each { |x| "string" } + end +end +"#; + let genv = analyze_with_stdlib(source); + // each returns its receiver (Array), not the block body result (String) + let info = genv.resolve_method(&Type::instance("Foo"), "bar").unwrap(); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "Array"); + } + + #[test] + fn test_nested_blocks() { + let source = r#" +class Foo + def bar + [1, 2].each { |x| + "hello".each_char { |c| c.upcase } + } + end +end +"#; + let genv = analyze_with_stdlib(source); + assert!( + genv.type_errors.is_empty(), + "nested block should not produce type errors: {:?}", + genv.type_errors + ); + } } diff --git a/core/src/analyzer/parameters.rs b/core/src/analyzer/parameters.rs index 30dd729..1de2da5 100644 --- a/core/src/analyzer/parameters.rs +++ b/core/src/analyzer/parameters.rs @@ -208,6 +208,36 @@ pub(crate) fn install_parameters( #[cfg(test)] mod tests { use super::*; + use crate::analyzer::install::AstInstaller; + use crate::parser::ParseSession; + + fn analyze(source: &str) -> GlobalEnv { + let session = ParseSession::new(); + let parse_result = session.parse_source(source, "test.rb").unwrap(); + let root = parse_result.node(); + let program = root.as_program_node().unwrap(); + + let mut genv = GlobalEnv::new(); + let mut lenv = LocalEnv::new(); + + let mut installer = AstInstaller::new(&mut genv, &mut lenv, source); + for stmt in &program.statements().body() { + installer.install_node(&stmt); + } + installer.finish(); + + genv + } + + fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String { + if let Some(vertex) = genv.get_vertex(vtx) { + vertex.show() + } else if let Some(source) = genv.get_source(vtx) { + source.ty.show() + } else { + panic!("vertex {:?} not found as either Vertex or Source", vtx); + } + } #[test] fn test_install_required_parameter() { @@ -266,4 +296,134 @@ mod tests { let vertex = genv.get_vertex(vtx).unwrap(); assert_eq!(vertex.show(), "Integer"); } + + #[test] + fn test_required_parameter_type_propagation() { + let source = r#" +class Foo + def greet(name) + name + end +end + +Foo.new.greet("Alice") +"#; + let genv = analyze(source); + let info = genv.resolve_method(&Type::instance("Foo"), "greet").unwrap(); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "String"); + } + + #[test] + fn test_optional_parameter_default_type() { + let source = r#" +class Foo + def greet(name = "World") + name + end +end +"#; + let genv = analyze(source); + let info = genv.resolve_method(&Type::instance("Foo"), "greet").unwrap(); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "String"); + } + + #[test] + fn test_multiple_parameters_from_call_site() { + let source = r#" +class Calc + def add(x, y) + x + end +end + +Calc.new.add(1, 2) +"#; + let genv = analyze(source); + let info = genv.resolve_method(&Type::instance("Calc"), "add").unwrap(); + let param_vtxs = info.param_vertices.as_ref().unwrap(); + assert_eq!(param_vtxs.len(), 2); + // Verify return type is Integer (method returns x, which receives 1) + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "Integer"); + } + + #[test] + fn test_keyword_parameter_propagation() { + let source = r#" +class Foo + def greet(name:) + name + end +end + +Foo.new.greet(name: "Alice") +"#; + let genv = analyze(source); + let info = genv.resolve_method(&Type::instance("Foo"), "greet").unwrap(); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "String"); + } + + #[test] + fn test_optional_keyword_parameter_default() { + 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").unwrap(); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "Integer"); + } + + #[test] + fn test_mixed_positional_and_keyword_params() { + 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] + fn test_rest_parameter() { + let source = r#" +class Foo + def bar(*args) + args + end +end +"#; + let genv = analyze(source); + let info = genv.resolve_method(&Type::instance("Foo"), "bar").unwrap(); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "Array"); + } + + #[test] + fn test_no_parameters() { + let source = r#" +class Foo + def bar + "hello" + end +end +"#; + let genv = analyze(source); + let info = genv.resolve_method(&Type::instance("Foo"), "bar").unwrap(); + let param_vtxs = info.param_vertices.as_ref().unwrap(); + assert!(param_vtxs.is_empty()); + } }