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
140 changes: 140 additions & 0 deletions core/src/analyzer/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
);
}
}
160 changes: 160 additions & 0 deletions core/src/analyzer/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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());
}
}