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
115 changes: 113 additions & 2 deletions rust/src/analyzer/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

use super::blocks::{enter_block_scope, exit_block_scope, install_block_parameter};
Expand Down Expand Up @@ -84,15 +85,26 @@ impl<'a> AstInstaller<'a> {

/// Install literal node
///
/// Handles all literals including Array with element type inference
/// Handles all literals including Array and Hash with element type inference
fn install_literal_node(&mut self, node: &Node) -> Option<VertexId> {
// Array literals need special handling for element type inference
if node.as_array_node().is_some() {
let elements: Vec<Node> = node.as_array_node().unwrap().elements().iter().collect();
return self.install_array_literal_elements(elements);
}

// Other literals (String, Integer, Hash, nil, true, false, Symbol)
// Hash literals need special handling for key/value type inference
if node.as_hash_node().is_some() {
let elements: Vec<Node> = node.as_hash_node().unwrap().elements().iter().collect();
return self.install_hash_literal_elements(elements);
}

// Range literals need special handling for element type inference
if let Some(range_node) = node.as_range_node() {
return self.install_range_literal(&range_node);
}

// Other literals (String, Integer, nil, true, false, Symbol)
install_literal(self.genv, node)
}

Expand Down Expand Up @@ -133,6 +145,105 @@ impl<'a> AstInstaller<'a> {
Some(self.genv.new_source(array_type))
}

/// Install hash literal with element type inference
fn install_hash_literal_elements(&mut self, elements: Vec<Node>) -> Option<VertexId> {
use crate::types::Type;
use std::collections::HashSet;

if elements.is_empty() {
return Some(self.genv.new_source(Type::hash()));
}

let mut key_types: HashSet<Type> = HashSet::new();
let mut value_types: HashSet<Type> = HashSet::new();

for element in &elements {
if let Some(assoc_node) = element.as_assoc_node() {
// Infer key type
if let Some(key_vtx) = self.install_node(&assoc_node.key()) {
if let Some(source) = self.genv.get_source(key_vtx) {
key_types.insert(source.ty.clone());
} else if let Some(vertex) = self.genv.get_vertex(key_vtx) {
for ty in vertex.types.keys() {
key_types.insert(ty.clone());
}
}
}

// Infer value type
if let Some(value_vtx) = self.install_node(&assoc_node.value()) {
if let Some(source) = self.genv.get_source(value_vtx) {
value_types.insert(source.ty.clone());
} else if let Some(vertex) = self.genv.get_vertex(value_vtx) {
for ty in vertex.types.keys() {
value_types.insert(ty.clone());
}
}
}
}
}

let hash_type = if key_types.is_empty() || value_types.is_empty() {
Type::hash()
} else {
// Build key type (single type or union)
let key_type = if key_types.len() == 1 {
key_types.into_iter().next().unwrap()
} else {
let types_vec: Vec<Type> = key_types.into_iter().collect();
Type::Union(types_vec)
};

// Build value type (single type or union)
let value_type = if value_types.len() == 1 {
value_types.into_iter().next().unwrap()
} else {
let types_vec: Vec<Type> = value_types.into_iter().collect();
Type::Union(types_vec)
};

Type::hash_of(key_type, value_type)
};

Some(self.genv.new_source(hash_type))
}

/// Install range literal with element type inference
fn install_range_literal(&mut self, range_node: &ruby_prism::RangeNode) -> Option<VertexId> {
// Try to infer element type from left or right endpoint
let element_type = if let Some(left) = range_node.left() {
self.infer_range_element_type(&left)
} else if let Some(right) = range_node.right() {
self.infer_range_element_type(&right)
} else {
None
};

let range_type = match element_type {
Some(ty) => Type::range_of(ty),
None => Type::range(),
};

Some(self.genv.new_source(range_type))
}

/// Infer element type from a range endpoint node
fn infer_range_element_type(&mut self, node: &Node) -> Option<Type> {
// Install the node and get its type
if let Some(vtx) = self.install_node(node) {
if let Some(source) = self.genv.get_source(vtx) {
return Some(source.ty.clone());
}
if let Some(vertex) = self.genv.get_vertex(vtx) {
// Get first type from vertex (simplified)
if let Some(ty) = vertex.types.keys().next() {
return Some(ty.clone());
}
}
}
None
}

/// Process nodes that need child evaluation first
fn process_needs_child(&mut self, kind: NeedsChildKind) -> Option<VertexId> {
match kind {
Expand Down
24 changes: 3 additions & 21 deletions rust/src/analyzer/literals.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! Literal Handlers - Processing Ruby literal values
//!
//! This module is responsible for:
//! - String, Integer, Float, Hash, Regexp, Range literals
//! - String, Integer, Float, Regexp literals
//! - nil, true, false, Symbol literals
//! - Creating Source vertices with fixed types
//!
//! Note: Array literals are handled in install.rs for element type inference
//! Note: Array, Hash, and Range literals are handled in install.rs for element type inference

use crate::env::GlobalEnv;
use crate::graph::VertexId;
Expand All @@ -14,7 +14,7 @@ use ruby_prism::Node;

/// Install literal nodes and return their VertexId
///
/// Note: Array literals are NOT handled here because they require
/// Note: Array and Hash literals are NOT handled here because they require
/// child processing for element type inference. See install.rs.
pub fn install_literal(genv: &mut GlobalEnv, node: &Node) -> Option<VertexId> {
// "hello"
Expand All @@ -32,11 +32,6 @@ pub fn install_literal(genv: &mut GlobalEnv, node: &Node) -> Option<VertexId> {
return Some(genv.new_source(Type::float()));
}

// {a: 1}
if node.as_hash_node().is_some() {
return Some(genv.new_source(Type::hash()));
}

// nil
if node.as_nil_node().is_some() {
return Some(genv.new_source(Type::Nil));
Expand All @@ -62,11 +57,6 @@ pub fn install_literal(genv: &mut GlobalEnv, node: &Node) -> Option<VertexId> {
return Some(genv.new_source(Type::regexp()));
}

// 1..5, "a".."z" (Range literal)
if node.as_range_node().is_some() {
return Some(genv.new_source(Type::range()));
}

None
}

Expand Down Expand Up @@ -107,12 +97,4 @@ mod tests {
let vtx = genv.new_source(Type::regexp());
assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Regexp");
}

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

let vtx = genv.new_source(Type::range());
assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Range");
}
}
133 changes: 130 additions & 3 deletions rust/src/analyzer/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ fn test_range_literal_basic() {
let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range");
assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Integer]");
}

#[test]
Expand All @@ -556,7 +556,7 @@ fn test_range_literal_exclusive() {
let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range");
assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Integer]");
}

#[test]
Expand All @@ -566,7 +566,64 @@ fn test_range_literal_string() {
let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range");
assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[String]");
}

#[test]
fn test_range_generic_float() {
let source = r#"x = 1.0..5.0"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Range[Float]");
}

// ============================================
// Nested Generic Array Tests
// ============================================

#[test]
fn test_nested_array_integer() {
let source = r#"x = [[1, 2], [3]]"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(
genv.get_vertex(x_vtx).unwrap().show(),
"Array[Array[Integer]]"
);
}

#[test]
fn test_deeply_nested_array() {
let source = r#"x = [[[1]]]"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(
genv.get_vertex(x_vtx).unwrap().show(),
"Array[Array[Array[Integer]]]"
);
}

#[test]
fn test_nested_array_mixed() {
let source = r#"x = [[1], ["a"]]"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
// Union type order may vary, so check for either order
let result = genv.get_vertex(x_vtx).unwrap().show();
assert!(
result == "Array[Array[Integer] | Array[String]]"
|| result == "Array[Array[String] | Array[Integer]]",
"Expected nested array with union, got: {}",
result
);
}

#[test]
Expand Down Expand Up @@ -604,3 +661,73 @@ b = x.size
let b_vtx = lenv.get_var("b").unwrap();
assert_eq!(genv.get_vertex(b_vtx).unwrap().show(), "Integer");
}

// ============================================
// Hash Generic Type Tests
// ============================================

#[test]
fn test_hash_symbol_integer() {
let source = r#"x = { a: 1, b: 2 }"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(
genv.get_vertex(x_vtx).unwrap().show(),
"Hash[Symbol, Integer]"
);
}

#[test]
fn test_hash_string_string() {
let source = r#"x = { "k" => "v" }"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(
genv.get_vertex(x_vtx).unwrap().show(),
"Hash[String, String]"
);
}

#[test]
fn test_hash_mixed_values() {
let source = r#"x = { a: 1, b: "x" }"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
let result = genv.get_vertex(x_vtx).unwrap().show();
// Union type order may vary
assert!(
result == "Hash[Symbol, Integer | String]"
|| result == "Hash[Symbol, String | Integer]",
"Expected Hash with union value type, got: {}",
result
);
}

#[test]
fn test_hash_empty() {
let source = r#"x = {}"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(genv.get_vertex(x_vtx).unwrap().show(), "Hash");
}

#[test]
fn test_hash_nested() {
let source = r#"x = { a: [1] }"#;

let (genv, lenv) = analyze(source);

let x_vtx = lenv.get_var("x").unwrap();
assert_eq!(
genv.get_vertex(x_vtx).unwrap().show(),
"Hash[Symbol, Array[Integer]]"
);
}
8 changes: 8 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@ impl Type {
type_args: vec![key_type, value_type],
}
}

/// Create a generic Range type: Range[element_type]
pub fn range_of(element_type: Type) -> Self {
Type::Generic {
name: QualifiedName::simple("Range"),
type_args: vec![element_type],
}
}
}

#[cfg(test)]
Expand Down