From 2bb5fbc8eb3e9de1d7e007e3bb06e88b97ea9caf Mon Sep 17 00:00:00 2001 From: kindlich Date: Sun, 18 Oct 2026 12:15:46 +0000 Subject: [PATCH 1/2] Use the upper bound for out-wildcards in internal names and descriptors JavaTypeInternalNameVisitor mapped every `out X` wildcard to java/lang/Object, so values typed `out Parent` were checkcast to Object and calling Parent's methods on them failed bytecode verification. JavaTypeDescriptorVisitor recursed into itself instead of the bound. Co-Authored-By: Claude Opus 5.5 --- .../openzen/zenscript/javashared/JavaTypeDescriptorVisitor.java | 2 +- .../zenscript/javashared/JavaTypeInternalNameVisitor.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/JavaShared/src/main/java/org/openzen/zenscript/javashared/JavaTypeDescriptorVisitor.java b/JavaShared/src/main/java/org/openzen/zenscript/javashared/JavaTypeDescriptorVisitor.java index 2c274fdf..5102ae0c 100644 --- a/JavaShared/src/main/java/org/openzen/zenscript/javashared/JavaTypeDescriptorVisitor.java +++ b/JavaShared/src/main/java/org/openzen/zenscript/javashared/JavaTypeDescriptorVisitor.java @@ -159,7 +159,7 @@ public String visitWildcardIn(WildcardInTypeID type) { @Override public String visitWildcardOut(WildcardOutTypeID type) { - return type.accept(this); + return type.upperBound.accept(this); } @Override diff --git a/JavaShared/src/main/java/org/openzen/zenscript/javashared/JavaTypeInternalNameVisitor.java b/JavaShared/src/main/java/org/openzen/zenscript/javashared/JavaTypeInternalNameVisitor.java index bd1042db..dd0de5a9 100644 --- a/JavaShared/src/main/java/org/openzen/zenscript/javashared/JavaTypeInternalNameVisitor.java +++ b/JavaShared/src/main/java/org/openzen/zenscript/javashared/JavaTypeInternalNameVisitor.java @@ -143,7 +143,7 @@ public String visitWildcardIn(WildcardInTypeID type) { @Override public String visitWildcardOut(WildcardOutTypeID type) { - return "java/lang/Object"; + return type.upperBound.accept(this); } @Override From e303d2e0e580a073448ee1e67b9e2ab388576dfd Mon Sep 17 00:00:00 2001 From: kindlich Date: Sun, 18 Oct 2026 12:15:46 +0000 Subject: [PATCH 2/2] Support raw generic types in native headers A raw class reference (e.g. a `GenClass` parameter) was translated by looking up the class's own type variables in the current context, which only works when the class refers to itself. From anywhere else this threw "Could not find type variable T". Raw types are now erased the way Java does it: each type argument becomes `out `, so a raw `Something` exposes Parent's members. As in JavaRuntimeClass.translateTypeParameters, the type variables are put into a child TypeVariableContext before their bounds are resolved, so a bound leading back to the class being loaded (e.g. `T extends Node`) is detected and becomes Object. Fixes RawTypeInHeaderTest; adds RawBoundedTypeInHeaderTest and RawRecursiveBoundTypeInHeaderTest. Co-Authored-By: Claude Opus 5.5 --- .../JavaRuntimeTypeConverterImpl.java | 36 ++++++++ .../RawBoundedTypeInHeaderTest.java | 89 +++++++++++++++++++ .../RawRecursiveBoundTypeInHeaderTest.java | 52 +++++++++++ 3 files changed, 177 insertions(+) create mode 100644 ScriptingExample/src/test/java/org/openzen/zenscript/scriptingexample/tests/actual_test/java_native/RawBoundedTypeInHeaderTest.java create mode 100644 ScriptingExample/src/test/java/org/openzen/zenscript/scriptingexample/tests/actual_test/java_native/RawRecursiveBoundTypeInHeaderTest.java diff --git a/JavaIntegration/src/main/java/org/openzen/zencode/java/impl/conversion/JavaRuntimeTypeConverterImpl.java b/JavaIntegration/src/main/java/org/openzen/zencode/java/impl/conversion/JavaRuntimeTypeConverterImpl.java index 795fd2bf..2fb601ab 100644 --- a/JavaIntegration/src/main/java/org/openzen/zencode/java/impl/conversion/JavaRuntimeTypeConverterImpl.java +++ b/JavaIntegration/src/main/java/org/openzen/zencode/java/impl/conversion/JavaRuntimeTypeConverterImpl.java @@ -196,6 +196,12 @@ private TypeID loadClass(TypeVariableContext context, Class type) { // ((JavaRuntimeClass) definition).translateTypeParameters(context); //} + // Type variables not in context mean a raw type used from outside the class, + // e.g. a parameter declared as `Foo` instead of `Foo`. Inside Foo itself, T is in context. + if (!context.containsAll(type.getTypeParameters())) { + return loadRawClass(context, type, definition); + } + final List typeParameters = new ArrayList<>(); for (TypeVariable> typeParameter : type.getTypeParameters()) { typeParameters.add(new GenericTypeID(context.get(typeParameter))); @@ -204,6 +210,36 @@ private TypeID loadClass(TypeVariableContext context, Class type) { return DefinitionTypeID.create(definition, typeParameters.toArray(TypeID.NONE)); } + // Raw types are treated the way Java erases them: each type argument becomes "out ". + // Like JavaRuntimeClass.translateTypeParameters, the type variables are put into the context before their bounds + // are resolved, so a bound leading back to a class being loaded (e.g. T extends Node) is detected and becomes Object. + private TypeID loadRawClass(TypeVariableContext context, Class type, TypeSymbol definition) { + final TypeVariable>[] javaTypeParameters = type.getTypeParameters(); + final TypeVariableContext rawContext = context.createChildContext(); + for (TypeVariable> typeVariable : javaTypeParameters) { + rawContext.put(typeVariable, new TypeParameter(CodePosition.NATIVE, typeVariable.getName())); + } + + final TypeID[] typeArguments = new TypeID[javaTypeParameters.length]; + for (int i = 0; i < javaTypeParameters.length; i++) { + Class bound = erase(javaTypeParameters[i].getBounds()[0]); + boolean boundIsLoading = bound.getTypeParameters().length > 0 && rawContext.containsAll(bound.getTypeParameters()); + typeArguments[i] = new WildcardOutTypeID(loadClass(rawContext, boundIsLoading ? Object.class : bound)); + } + + return DefinitionTypeID.create(definition, typeArguments); + } + + private static Class erase(Type type) { + if (type instanceof Class) + return (Class) type; + if (type instanceof ParameterizedType) + return erase(((ParameterizedType) type).getRawType()); + if (type instanceof TypeVariable) + return erase(((TypeVariable) type).getBounds()[0]); + return Object.class; + } + private TypeID loadGenericArray(TypeVariableContext context, GenericArrayType type) { final JavaAnnotatedType componentType = JavaAnnotatedType.of(type.getGenericComponentType()); final TypeID baseType = this.loadType(context, componentType); diff --git a/ScriptingExample/src/test/java/org/openzen/zenscript/scriptingexample/tests/actual_test/java_native/RawBoundedTypeInHeaderTest.java b/ScriptingExample/src/test/java/org/openzen/zenscript/scriptingexample/tests/actual_test/java_native/RawBoundedTypeInHeaderTest.java new file mode 100644 index 00000000..f4e3b040 --- /dev/null +++ b/ScriptingExample/src/test/java/org/openzen/zenscript/scriptingexample/tests/actual_test/java_native/RawBoundedTypeInHeaderTest.java @@ -0,0 +1,89 @@ +package org.openzen.zenscript.scriptingexample.tests.actual_test.java_native; + +import org.junit.jupiter.api.Test; +import org.openzen.zencode.java.ZenCodeGlobals; +import org.openzen.zencode.java.ZenCodeType; +import org.openzen.zenscript.scriptingexample.tests.helpers.ZenCodeTest; + +import java.util.List; + +public class RawBoundedTypeInHeaderTest extends ZenCodeTest { + + @Override + public List> getRequiredClasses() { + final List> requiredClasses = super.getRequiredClasses(); + requiredClasses.add(Parent.class); + requiredClasses.add(Child.class); + requiredClasses.add(BoundedGenClass.class); + requiredClasses.add(TestGlobals.class); + return requiredClasses; + } + + @Test + void rawBoundedTypeAcceptsParameterizedValue() { + addScript("acceptRaw(new test_module.java_native.BoundedGenClass(new test_module.java_native.Child()));"); + + executeEngine(); + + logger.assertNoErrors(); + logger.assertNoWarnings(); + } + + @Test + void rawBoundedTypeExposesBoundMembers() { + addScript("println(makeRaw().unwrap().greet());"); + + executeEngine(); + + logger.assertNoErrors(); + logger.printlnOutputs().assertLinesInOrder("Hi from child"); + } + + @ZenCodeType.Name("test_module.java_native.RawBoundedTestGlobals") + public static final class TestGlobals { + @ZenCodeGlobals.Global + public static void acceptRaw(BoundedGenClass clazz) { + } + + @ZenCodeGlobals.Global + public static BoundedGenClass makeRaw() { + return new BoundedGenClass<>(new Child()); + } + } + + @ZenCodeType.Name("test_module.java_native.Parent") + public static class Parent { + @ZenCodeType.Method + public String greet() { + return "Hi from parent"; + } + } + + @ZenCodeType.Name("test_module.java_native.Child") + public static class Child extends Parent { + @ZenCodeType.Constructor + public Child() { + } + + @Override + @ZenCodeType.Method + public String greet() { + return "Hi from child"; + } + } + + @ZenCodeType.Name("test_module.java_native.BoundedGenClass") + public static final class BoundedGenClass { + private final T value; + + @ZenCodeType.Constructor + public BoundedGenClass(T value) { + this.value = value; + } + + @ZenCodeType.Method + public T unwrap() { + return value; + } + } +} diff --git a/ScriptingExample/src/test/java/org/openzen/zenscript/scriptingexample/tests/actual_test/java_native/RawRecursiveBoundTypeInHeaderTest.java b/ScriptingExample/src/test/java/org/openzen/zenscript/scriptingexample/tests/actual_test/java_native/RawRecursiveBoundTypeInHeaderTest.java new file mode 100644 index 00000000..cb42689b --- /dev/null +++ b/ScriptingExample/src/test/java/org/openzen/zenscript/scriptingexample/tests/actual_test/java_native/RawRecursiveBoundTypeInHeaderTest.java @@ -0,0 +1,52 @@ +package org.openzen.zenscript.scriptingexample.tests.actual_test.java_native; + +import org.junit.jupiter.api.Test; +import org.openzen.zencode.java.ZenCodeGlobals; +import org.openzen.zencode.java.ZenCodeType; +import org.openzen.zenscript.scriptingexample.tests.helpers.ZenCodeTest; + +import java.util.List; + +/** + * Raw types whose type parameter bound leads back to the type itself (T extends SelfBoundedGenClass). + * Without the guard in JavaRuntimeTypeConverterImpl.loadRawClass, the bound would either recurse or leak the + * guard's placeholder type parameter, so the round trip below would no longer type-check. + * No other test registers these types, so a failure here points at that guard. + */ +public class RawRecursiveBoundTypeInHeaderTest extends ZenCodeTest { + + @Override + public List> getRequiredClasses() { + final List> requiredClasses = super.getRequiredClasses(); + requiredClasses.add(SelfBoundedGenClass.class); + requiredClasses.add(TestGlobals.class); + return requiredClasses; + } + + @Test + void rawSelfBoundedTypeRoundTrips() { + addScript("println(acceptRawSelfBounded(makeRawSelfBounded()));"); + + executeEngine(); + + logger.assertNoErrors(); + logger.printlnOutputs().assertLinesInOrder("self bounded"); + } + + @ZenCodeType.Name("test_module.java_native.RawRecursiveBoundTestGlobals") + public static final class TestGlobals { + @ZenCodeGlobals.Global + public static SelfBoundedGenClass makeRawSelfBounded() { + return new SelfBoundedGenClass<>(); + } + + @ZenCodeGlobals.Global + public static String acceptRawSelfBounded(SelfBoundedGenClass clazz) { + return "self bounded"; + } + } + + @ZenCodeType.Name("test_module.java_native.SelfBoundedGenClass") + public static final class SelfBoundedGenClass> { + } +}