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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 42 additions & 6 deletions jvm/src/jvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "<init>" {
// 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)
Comment thread
dlunch marked this conversation as resolved.
};

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::<Vec<_>>();
Expand All @@ -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?
Expand Down Expand Up @@ -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<dyn Method>)> = 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<Option<Box<dyn Field>>> {
Expand Down
Binary file added test-data/SpecialResolutionErrors$AbstractBase.class
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added test-data/SpecialResolutionErrors$StaticBase.class
Binary file not shown.
Binary file added test-data/SpecialResolutionErrors$StaticChild.class
Binary file not shown.
Binary file added test-data/SpecialResolutionErrors$StaticParent.class
Binary file not shown.
Binary file added test-data/SpecialResolutionErrors.class
Binary file not shown.
3 changes: 3 additions & 0 deletions test-data/SpecialResolutionErrors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
java.lang.NoSuchMethodError
java.lang.IncompatibleClassChangeError
java.lang.AbstractMethodError
Binary file added test-data/SuperClass$BaseDefault.class
Binary file not shown.
Binary file added test-data/SuperClass$DefaultChild.class
Binary file not shown.
Binary file added test-data/SuperClass$DefaultParent.class
Binary file not shown.
Binary file modified test-data/SuperClass$InnerClass.class
Binary file not shown.
Binary file modified test-data/SuperClass$InnerDerivedClass.class
Binary file not shown.
Binary file added test-data/SuperClass$InnerLeafClass.class
Binary file not shown.
Binary file added test-data/SuperClass$PrivateDefault.class
Binary file not shown.
Binary file added test-data/SuperClass$ReverseDefaultChild.class
Binary file not shown.
Binary file added test-data/SuperClass$ReverseDefaultParent.class
Binary file not shown.
Binary file added test-data/SuperClass$SpecificDefault.class
Binary file not shown.
Binary file modified test-data/SuperClass.class
Binary file not shown.
4 changes: 4 additions & 0 deletions test-data/SuperClass.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
2
test
test
leaf
test
2
2
42 changes: 42 additions & 0 deletions test-data/src/SuperClass.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions test-data/src/special-resolution/RuntimeTargets.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
56 changes: 56 additions & 0 deletions test-data/src/special-resolution/SpecialResolutionErrors.java
Original file line number Diff line number Diff line change
@@ -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(); }
}
}
Loading