diff --git a/AGENTS.md b/AGENTS.md index 7185a1b0..a4cffaa7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ ## Testing Boundaries - Keep `rustjava-runtime/tests/classes` limited to Java standard library class and API behavior. - Test JVM and interpreter semantics, including class initialization, bytecode execution, and monitor behavior, with compiled Java fixtures under `test-data/src` and expected output under `test-data`, executed by `tests/test_class.rs`. +- Add or extend fixtures consumed by the existing `test_class` runner rather than adding Rust tests that directly invoke JVM APIs. For linkage errors that Java source cannot express directly, compile the caller against one class version and replace the target class in the fixture; keep the source variants and reproducible build instructions alongside it. - Do not place JVM core behavior tests in the `rustjava-runtime` standard library test tree. ## Compatibility Sources diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index fb154d2b..b7bf4f30 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -435,9 +435,14 @@ impl Jvm { tracing::trace!("Invoke special {class_name}.{name}:{descriptor}({args:?})"); let class = self.resolve_class(class_name).await?; - let method = class.definition.method(name, descriptor, false); + let method = if name == "" { + // Constructors are not inherited (JVMS 6.5 invokespecial). + class.definition.method(name, descriptor, false).map(|method| (class, method)) + } else { + self.resolve_method(&class, name, descriptor, false) + }; - if let Some(method) = method { + if let Some((class, method)) = method { let args = iter::once(JavaValue::Object(Some(clone_box(&**instance)))) .chain(args.into_vec()) .collect::>(); @@ -448,6 +453,12 @@ impl Jvm { .await); } + if method.access_flags().contains(MethodAccessFlags::ABSTRACT) { + return Err(self + .exception("java/lang/AbstractMethodError", &format!("{class_name}.{name}:{descriptor}")) + .await); + } + Ok(self .execute_method(&class, Some(instance.clone()), &method, args.into_boxed_slice()) .await? @@ -1295,15 +1306,40 @@ impl Jvm { .and_then(|super_class| self.get_class(&super_class)); } + let mut visited = HashSet::new(); + let mut candidates: Vec<(Class, Box)> = Vec::new(); while let Some(interface_name) = interfaces.pop() { - let interface = self.get_class(&interface_name)?; - if let Some(method) = interface.definition.method(name, descriptor, false) { - return Some((interface, method)); + if !visited.insert(interface_name.clone()) { + continue; } + let interface = self.get_class(&interface_name)?; interfaces.extend(interface.definition.interface_names()); + if let Some(method) = interface.definition.method(name, descriptor, false) + && !method.access_flags().contains(MethodAccessFlags::PRIVATE) + { + if candidates + .iter() + .any(|(candidate, _)| self.is_inherited_from(&*candidate.definition, &interface_name)) + { + continue; + } + // Keep only declarations not overridden by a more specific interface. + candidates.retain(|(candidate, _)| !self.is_inherited_from(&*interface.definition, &candidate.definition.name())); + candidates.push((interface, method)); + } + } + + let mut concrete = candidates + .iter() + .enumerate() + .filter(|(_, (_, method))| !method.access_flags().contains(MethodAccessFlags::ABSTRACT)); + if let Some((index, _)) = concrete.next() + && concrete.next().is_none() + { + return Some(candidates.swap_remove(index)); } - None + candidates.into_iter().next() } pub(crate) fn find_field(&self, class: &dyn ClassDefinition, name: &str, descriptor: &str) -> Result>> { diff --git a/test-data/SpecialResolutionErrors$AbstractBase.class b/test-data/SpecialResolutionErrors$AbstractBase.class new file mode 100644 index 00000000..9989b212 Binary files /dev/null and b/test-data/SpecialResolutionErrors$AbstractBase.class differ diff --git a/test-data/SpecialResolutionErrors$AbstractChild.class b/test-data/SpecialResolutionErrors$AbstractChild.class new file mode 100644 index 00000000..d7f97ddd Binary files /dev/null and b/test-data/SpecialResolutionErrors$AbstractChild.class differ diff --git a/test-data/SpecialResolutionErrors$AbstractParent.class b/test-data/SpecialResolutionErrors$AbstractParent.class new file mode 100644 index 00000000..82ef91fd Binary files /dev/null and b/test-data/SpecialResolutionErrors$AbstractParent.class differ diff --git a/test-data/SpecialResolutionErrors$ConstructorBase.class b/test-data/SpecialResolutionErrors$ConstructorBase.class new file mode 100644 index 00000000..d481baf1 Binary files /dev/null and b/test-data/SpecialResolutionErrors$ConstructorBase.class differ diff --git a/test-data/SpecialResolutionErrors$ConstructorChild.class b/test-data/SpecialResolutionErrors$ConstructorChild.class new file mode 100644 index 00000000..46b288b6 Binary files /dev/null and b/test-data/SpecialResolutionErrors$ConstructorChild.class differ diff --git a/test-data/SpecialResolutionErrors$StaticBase.class b/test-data/SpecialResolutionErrors$StaticBase.class new file mode 100644 index 00000000..860ab64d Binary files /dev/null and b/test-data/SpecialResolutionErrors$StaticBase.class differ diff --git a/test-data/SpecialResolutionErrors$StaticChild.class b/test-data/SpecialResolutionErrors$StaticChild.class new file mode 100644 index 00000000..89a30309 Binary files /dev/null and b/test-data/SpecialResolutionErrors$StaticChild.class differ diff --git a/test-data/SpecialResolutionErrors$StaticParent.class b/test-data/SpecialResolutionErrors$StaticParent.class new file mode 100644 index 00000000..2fcc1508 Binary files /dev/null and b/test-data/SpecialResolutionErrors$StaticParent.class differ diff --git a/test-data/SpecialResolutionErrors.class b/test-data/SpecialResolutionErrors.class new file mode 100644 index 00000000..3390226b Binary files /dev/null and b/test-data/SpecialResolutionErrors.class differ diff --git a/test-data/SpecialResolutionErrors.txt b/test-data/SpecialResolutionErrors.txt new file mode 100644 index 00000000..25162a04 --- /dev/null +++ b/test-data/SpecialResolutionErrors.txt @@ -0,0 +1,3 @@ +java.lang.NoSuchMethodError +java.lang.IncompatibleClassChangeError +java.lang.AbstractMethodError diff --git a/test-data/SuperClass$BaseDefault.class b/test-data/SuperClass$BaseDefault.class new file mode 100644 index 00000000..d9ff20dd Binary files /dev/null and b/test-data/SuperClass$BaseDefault.class differ diff --git a/test-data/SuperClass$DefaultChild.class b/test-data/SuperClass$DefaultChild.class new file mode 100644 index 00000000..66aa9ee8 Binary files /dev/null and b/test-data/SuperClass$DefaultChild.class differ diff --git a/test-data/SuperClass$DefaultParent.class b/test-data/SuperClass$DefaultParent.class new file mode 100644 index 00000000..9044bdd9 Binary files /dev/null and b/test-data/SuperClass$DefaultParent.class differ diff --git a/test-data/SuperClass$InnerClass.class b/test-data/SuperClass$InnerClass.class index 981bd8f8..228ed3b2 100644 Binary files a/test-data/SuperClass$InnerClass.class and b/test-data/SuperClass$InnerClass.class differ diff --git a/test-data/SuperClass$InnerDerivedClass.class b/test-data/SuperClass$InnerDerivedClass.class index 756a8da8..e970b951 100644 Binary files a/test-data/SuperClass$InnerDerivedClass.class and b/test-data/SuperClass$InnerDerivedClass.class differ diff --git a/test-data/SuperClass$InnerLeafClass.class b/test-data/SuperClass$InnerLeafClass.class new file mode 100644 index 00000000..c0048aad Binary files /dev/null and b/test-data/SuperClass$InnerLeafClass.class differ diff --git a/test-data/SuperClass$PrivateDefault.class b/test-data/SuperClass$PrivateDefault.class new file mode 100644 index 00000000..99599bae Binary files /dev/null and b/test-data/SuperClass$PrivateDefault.class differ diff --git a/test-data/SuperClass$ReverseDefaultChild.class b/test-data/SuperClass$ReverseDefaultChild.class new file mode 100644 index 00000000..3121def0 Binary files /dev/null and b/test-data/SuperClass$ReverseDefaultChild.class differ diff --git a/test-data/SuperClass$ReverseDefaultParent.class b/test-data/SuperClass$ReverseDefaultParent.class new file mode 100644 index 00000000..5c0e5ef0 Binary files /dev/null and b/test-data/SuperClass$ReverseDefaultParent.class differ diff --git a/test-data/SuperClass$SpecificDefault.class b/test-data/SuperClass$SpecificDefault.class new file mode 100644 index 00000000..40174421 Binary files /dev/null and b/test-data/SuperClass$SpecificDefault.class differ diff --git a/test-data/SuperClass.class b/test-data/SuperClass.class index 3baeeae5..70b53973 100644 Binary files a/test-data/SuperClass.class and b/test-data/SuperClass.class differ diff --git a/test-data/SuperClass.txt b/test-data/SuperClass.txt index bd767ccc..e8b67ef4 100644 --- a/test-data/SuperClass.txt +++ b/test-data/SuperClass.txt @@ -3,3 +3,7 @@ 2 test test +leaf +test +2 +2 diff --git a/test-data/src/SuperClass.java b/test-data/src/SuperClass.java index 5c237bfe..9d295a0d 100644 --- a/test-data/src/SuperClass.java +++ b/test-data/src/SuperClass.java @@ -1,4 +1,28 @@ class SuperClass { + interface BaseDefault { + default int value() { return 1; } + } + + interface SpecificDefault extends BaseDefault { + default int value() { return 2; } + } + + interface PrivateDefault { + private int value() { return 3; } + } + + static class DefaultParent implements SpecificDefault, BaseDefault, PrivateDefault {} + + static class DefaultChild extends DefaultParent { + int call() { return super.value(); } + } + + static class ReverseDefaultParent implements BaseDefault, SpecificDefault, PrivateDefault {} + + static class ReverseDefaultChild extends ReverseDefaultParent { + int call() { return super.value(); } + } + class InnerClass { int field1; long field2; @@ -13,6 +37,17 @@ class InnerDerivedClass extends InnerClass { String field4; } + class InnerLeafClass extends InnerDerivedClass { + @Override + void test() { + System.out.println("leaf"); + } + + void testSuper() { + super.test(); + } + } + void run() { InnerDerivedClass derived = null; @@ -34,6 +69,13 @@ void run() { System.out.println(derived.field4); derived.test(); + + InnerLeafClass leaf = new InnerLeafClass(); + leaf.test(); + leaf.testSuper(); + + System.out.println(new DefaultChild().call()); + System.out.println(new ReverseDefaultChild().call()); } public static void main(String[] args) { diff --git a/test-data/src/special-resolution/RuntimeTargets.java b/test-data/src/special-resolution/RuntimeTargets.java new file mode 100644 index 00000000..a2cdeef1 --- /dev/null +++ b/test-data/src/special-resolution/RuntimeTargets.java @@ -0,0 +1,21 @@ +// Replace only these targets after compiling SpecialResolutionErrors.java. +// The stale callers must report linkage errors rather than use inherited +// constructors, invoke static methods, or execute abstract methods. +class SpecialResolutionErrors { + static class ConstructorBase { + ConstructorBase() {} + ConstructorBase(int value) {} + } + + static class ConstructorChild extends ConstructorBase { + ConstructorChild() {} + } + + static class StaticBase { + static int method() { return 1; } + } + + static abstract class AbstractBase { + abstract int method(); + } +} diff --git a/test-data/src/special-resolution/SpecialResolutionErrors.java b/test-data/src/special-resolution/SpecialResolutionErrors.java new file mode 100644 index 00000000..7eba2a14 --- /dev/null +++ b/test-data/src/special-resolution/SpecialResolutionErrors.java @@ -0,0 +1,56 @@ +// Rebuild from the repository root with JDK 22 or newer: +// classes=$(mktemp -d) +// javac --release 22 -d test-data test-data/src/special-resolution/SpecialResolutionErrors.java +// javac --release 22 -d "$classes" test-data/src/special-resolution/RuntimeTargets.java +// cp "$classes"/'SpecialResolutionErrors$ConstructorChild.class' test-data/ +// cp "$classes"/'SpecialResolutionErrors$StaticBase.class' test-data/ +// cp "$classes"/'SpecialResolutionErrors$AbstractBase.class' test-data/ +// rm -r "$classes" +public class SpecialResolutionErrors { + public static void main(String[] args) { + try { + new ConstructorChild(1); + } catch (NoSuchMethodError e) { + System.out.println(e.getClass().getName()); + } + try { + new StaticChild().call(); + } catch (IncompatibleClassChangeError e) { + System.out.println(e.getClass().getName()); + } + try { + new AbstractChild().call(); + } catch (AbstractMethodError e) { + System.out.println(e.getClass().getName()); + } + } + static class ConstructorBase { + ConstructorBase() {} + ConstructorBase(int value) {} + } + + static class ConstructorChild extends ConstructorBase { + ConstructorChild(int value) { super(value); } + } + + static class StaticBase { + int method() { return 1; } + } + + static class StaticParent extends StaticBase {} + + static class StaticChild extends StaticParent { + int call() { return super.method(); } + } + + static class AbstractBase { + int method() { return 1; } + } + + static class AbstractParent extends AbstractBase {} + + static class AbstractChild extends AbstractParent { + int method() { return 2; } + int call() { return super.method(); } + } +}