Skip to content

Commit c5cf231

Browse files
authored
Support array covariance and ArrayStoreException (#168)
* Support array covariance and ArrayStoreException is_instance now follows JVMS 4.10.3 array subtyping via a new is_assignable helper: an array is a subtype of Object, Cloneable, and Serializable, and S[] is a subtype of T[] when S is a subtype of T (primitive component arrays only match exactly). This fixes instanceof/checkcast on covariant array types. aastore checks the stored reference against the array's component type and throws ArrayStoreException (new runtime class) on a mismatch; null is always allowed. * Address review: check aastore bounds before the store type check JVMS orders aastore checks as NullPointer, then ArrayIndexOutOfBounds, then ArrayStore. The component-type check now runs after an explicit upper-bound check, so an out-of-bounds store of an incompatible value throws ArrayIndexOutOfBoundsException rather than ArrayStoreException. * Express array subtyping with JavaType instead of string slicing is_assignable now parses names into JavaType and recurses structurally (Array/Class) rather than manipulating descriptor strings, which makes the array-covariance and Object/Cloneable/Serializable rules clearer. Behavior is unchanged. * Move array/class name parsing into JavaType::from_class_name The CONSTANT_Class_info name form (binary name for classes, descriptor for arrays; JVMS 4.4.1) to JavaType conversion now lives on JavaType as a named constructor, and is_instance calls is_type_assignable directly, dropping the str-based is_assignable helper.
1 parent da5a408 commit c5cf231

13 files changed

Lines changed: 228 additions & 12 deletions

File tree

java_runtime/src/classes/java/lang.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod abstract_method_error;
22
mod arithmetic_exception;
33
mod array_index_out_of_bounds_exception;
4+
mod array_store_exception;
45
mod class;
56
mod class_cast_exception;
67
mod class_loader;
@@ -39,14 +40,15 @@ mod unsupported_operation_exception;
3940

4041
pub use self::{
4142
abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException,
42-
array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, class::Class, class_cast_exception::ClassCastException,
43-
class_loader::ClassLoader, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, error::Error,
44-
exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, illegal_argument_exception::IllegalArgumentException,
45-
incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException,
46-
instantiation_error::InstantiationError, integer::Integer, interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math,
47-
negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError,
48-
no_such_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException,
49-
object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String,
50-
string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread,
51-
throwable::Throwable, unsupported_operation_exception::UnsupportedOperationException,
43+
array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, class::Class,
44+
class_cast_exception::ClassCastException, class_loader::ClassLoader, clone_not_supported_exception::CloneNotSupportedException,
45+
cloneable::Cloneable, comparable::Comparable, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError,
46+
illegal_argument_exception::IllegalArgumentException, incompatible_class_change_error::IncompatibleClassChangeError,
47+
index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, integer::Integer,
48+
interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math, negative_array_size_exception::NegativeArraySizeException,
49+
no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, no_such_method_error::NoSuchMethodError,
50+
null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException, object::Object, runnable::Runnable,
51+
runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String, string_buffer::StringBuffer,
52+
string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable,
53+
unsupported_operation_exception::UnsupportedOperationException,
5254
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use alloc::vec;
2+
3+
use java_class_proto::JavaMethodProto;
4+
use jvm::{ClassInstanceRef, Jvm, Result};
5+
6+
use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String};
7+
8+
// class java.lang.ArrayStoreException
9+
pub struct ArrayStoreException;
10+
11+
impl ArrayStoreException {
12+
pub fn as_proto() -> RuntimeClassProto {
13+
RuntimeClassProto {
14+
name: "java/lang/ArrayStoreException",
15+
parent_class: Some("java/lang/RuntimeException"),
16+
interfaces: vec![],
17+
methods: vec![
18+
JavaMethodProto::new("<init>", "()V", Self::init, Default::default()),
19+
JavaMethodProto::new("<init>", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()),
20+
],
21+
fields: vec![],
22+
access_flags: Default::default(),
23+
}
24+
}
25+
26+
async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>) -> Result<()> {
27+
tracing::debug!("java.lang.ArrayStoreException::<init>({:?})", &this);
28+
29+
let _: () = jvm.invoke_special(&this, "java/lang/RuntimeException", "<init>", "()V", ()).await?;
30+
31+
Ok(())
32+
}
33+
34+
async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, message: ClassInstanceRef<String>) -> Result<()> {
35+
tracing::debug!("java.lang.ArrayStoreException::<init>({:?}, {:?})", &this, &message);
36+
37+
let _: () = jvm
38+
.invoke_special(&this, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V", (message,))
39+
.await?;
40+
41+
Ok(())
42+
}
43+
}

java_runtime/src/loader.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option<RuntimeClassProto> {
3535
crate::classes::java::lang::AbstractMethodError::as_proto(),
3636
crate::classes::java::lang::ArithmeticException::as_proto(),
3737
crate::classes::java::lang::ArrayIndexOutOfBoundsException::as_proto(),
38+
crate::classes::java::lang::ArrayStoreException::as_proto(),
3839
crate::classes::java::lang::Class::as_proto(),
3940
crate::classes::java::lang::ClassCastException::as_proto(),
4041
crate::classes::java::lang::ClassLoader::as_proto(),

jvm/src/jvm.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,9 +621,43 @@ impl Jvm {
621621
}
622622

623623
pub fn is_instance(&self, instance: &dyn ClassInstance, class_name: &str) -> bool {
624-
let class = instance.class_definition();
624+
self.is_type_assignable(
625+
&JavaType::from_class_name(&instance.class_definition().name()),
626+
&JavaType::from_class_name(class_name),
627+
)
628+
}
629+
630+
// aastore: whether value may be stored into array (JVMS 6.5 aastore)
631+
pub fn array_store_allowed(&self, array: &dyn ClassInstance, value: &dyn ClassInstance) -> bool {
632+
let JavaType::Array(component) = JavaType::parse(&array.class_definition().name()) else {
633+
return true;
634+
};
635+
636+
self.is_type_assignable(&JavaType::from_class_name(&value.class_definition().name()), &component)
637+
}
625638

626-
self.is_inherited_from(&*class, class_name)
639+
// JVMS 4.10.3 subtyping, including array covariance
640+
fn is_type_assignable(&self, source: &JavaType, target: &JavaType) -> bool {
641+
if source == target {
642+
return true;
643+
}
644+
645+
match (source, target) {
646+
(JavaType::Array(source_component), JavaType::Array(target_component)) => self.is_type_assignable(source_component, target_component),
647+
// every array type is a subtype of Object, Cloneable, and java.io.Serializable (JLS 4.10.3)
648+
(JavaType::Array(_), JavaType::Class(name)) => {
649+
name == "java/lang/Object" || name == "java/lang/Cloneable" || name == "java/io/Serializable"
650+
}
651+
(JavaType::Class(source), JavaType::Class(target)) => self.is_class_assignable(source, target),
652+
_ => false,
653+
}
654+
}
655+
656+
fn is_class_assignable(&self, source: &str, target: &str) -> bool {
657+
match self.get_class(source) {
658+
Some(class) => self.is_inherited_from(&*class.definition, target),
659+
None => false,
660+
}
627661
}
628662

629663
pub fn is_inherited_from(&self, class: &dyn ClassDefinition, class_name: &str) -> bool {

jvm/src/type.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ impl JavaType {
5252
Self::parse_type(descriptor).unwrap().1
5353
}
5454

55+
// a CONSTANT_Class_info name (JVMS 4.4.1): a class binary name in internal form (java/lang/String)
56+
// or an array type descriptor ([Ljava/lang/String;, [I)
57+
pub fn from_class_name(name: &str) -> Self {
58+
if name.starts_with('[') {
59+
Self::parse(name)
60+
} else {
61+
Self::Class(name.to_string())
62+
}
63+
}
64+
5565
pub fn as_method(&self) -> (&[Self], &Self) {
5666
if let Self::Method(params, return_type) = self {
5767
(params, return_type)

jvm/tests/test_is_instance.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,31 @@ async fn test_is_instance_interface() -> JvmResult<()> {
3030

3131
Ok(())
3232
}
33+
34+
#[tokio::test]
35+
async fn test_is_instance_array_covariance() -> JvmResult<()> {
36+
let jvm = test_jvm().await?;
37+
38+
let string_array = jvm.instantiate_array("Ljava/lang/String;", 1).await?;
39+
assert!(jvm.is_instance(&*string_array, "[Ljava/lang/Object;"));
40+
assert!(jvm.is_instance(&*string_array, "java/lang/Object"));
41+
assert!(jvm.is_instance(&*string_array, "java/lang/Cloneable"));
42+
assert!(jvm.is_instance(&*string_array, "java/io/Serializable"));
43+
assert!(jvm.is_instance(&*string_array, "[Ljava/lang/String;"));
44+
assert!(!jvm.is_instance(&*string_array, "[Ljava/lang/Integer;"));
45+
46+
let int_array = jvm.instantiate_array("I", 1).await?;
47+
assert!(jvm.is_instance(&*int_array, "java/lang/Object"));
48+
assert!(!jvm.is_instance(&*int_array, "[Ljava/lang/Object;"));
49+
assert!(!jvm.is_instance(&*int_array, "[J"));
50+
51+
let nested = jvm.instantiate_array("[Ljava/lang/String;", 1).await?;
52+
assert!(jvm.is_instance(&*nested, "[Ljava/lang/Object;"));
53+
assert!(jvm.is_instance(&*nested, "[[Ljava/lang/Object;"));
54+
assert!(jvm.is_instance(&*nested, "[[Ljava/lang/String;"));
55+
56+
let object_array = jvm.instantiate_array("Ljava/lang/Object;", 1).await?;
57+
assert!(!jvm.is_instance(&*object_array, "[Ljava/lang/String;"));
58+
59+
Ok(())
60+
}

jvm_rust/src/interpreter.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ impl Interpreter {
106106
return Err(jvm.exception("java/lang/NullPointerException", "Array is null").await);
107107
}
108108

109+
// JVMS: aastore reports an out-of-bounds index before any ArrayStoreException type check
110+
let length = jvm.array_length(array.as_ref().unwrap()).await?;
111+
if index as usize >= length {
112+
return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", &format!("{}", index)).await);
113+
}
114+
109115
let element_type = jvm.array_element_type(array.as_ref().unwrap()).await?;
110116

111117
// operand stack has only integer, so convert it to the correct type
@@ -118,6 +124,15 @@ impl Interpreter {
118124
_ => value,
119125
};
120126

127+
if matches!(opcode, Opcode::Aastore) {
128+
let stored: &Option<Box<dyn ClassInstance>> = (&value).into();
129+
if let Some(stored) = stored
130+
&& !jvm.array_store_allowed(&**array.as_ref().unwrap(), &**stored)
131+
{
132+
return Err(jvm.exception("java/lang/ArrayStoreException", &stored.class_definition().name()).await);
133+
}
134+
}
135+
121136
jvm.store_array(array.as_mut().unwrap(), index as usize, [value]).await?;
122137
}
123138
Opcode::AconstNull => stack_frame.operand_stack.push(JavaValue::Object(None)),

test_data/ArrayCovariance.class

975 Bytes
Binary file not shown.

test_data/ArrayCovariance.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
true
2+
true
3+
true
4+
true
5+
true
6+
false
7+
true
8+
true
9+
true
10+
true
11+
cce

test_data/ArrayStore.class

1.15 KB
Binary file not shown.

0 commit comments

Comments
 (0)