Import polyglot java inner classes (#6818)

Adds the ability to import nested inner classes in polyglot java imports.
This commit is contained in:
Pavel Marek 2023-05-31 09:38:59 +02:00 committed by GitHub
parent 8bc3ebd70a
commit e5c21713e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 317 additions and 46 deletions

View File

@ -19,7 +19,7 @@ public abstract class LookupClassNode extends Node {
@Specialization
Object doExecute(Object name, @Cached("build()") ExpectStringNode expectStringNode) {
return EnsoContext.get(this).getEnvironment().lookupHostSymbol(expectStringNode.execute(name));
return EnsoContext.get(this).lookupJavaClass(expectStringNode.execute(name));
}
abstract Object execute(Object name);

View File

@ -1,10 +1,19 @@
package org.enso.interpreter.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.UnknownIdentifierException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@ -345,6 +354,36 @@ public class EnsoContext {
.findFirst();
}
/**
* Tries to lookup a Java class (host symbol in Truffle terminology) by its fully qualified name.
* This method also tries to lookup inner classes. More specifically, if the provided name
* resolves to an inner class, then the import of the outer class is resolved, and the inner class
* is looked up by iterating the members of the outer class via Truffle's interop protocol.
*
* @param className Fully qualified class name, can also be nested static inner class.
* @return If the java class is found, return it, otherwise return null.
*/
@TruffleBoundary
public Object lookupJavaClass(String className) {
List<String> items = Arrays.asList(className.split("\\."));
for (int i = items.size() - 1; i >= 0; i--) {
String pkgName = String.join(".", items.subList(0, i));
String curClassName = items.get(i);
List<String> nestedClassPart =
i < items.size() - 1 ? items.subList(i + 1, items.size()) : List.of();
try {
Object hostSymbol = environment.lookupHostSymbol(pkgName + "." + curClassName);
if (nestedClassPart.isEmpty()) {
return hostSymbol;
} else {
return getNestedClass(hostSymbol, nestedClassPart);
}
} catch (RuntimeException ignored) {
}
}
return null;
}
/**
* Finds the package the provided module belongs to.
*
@ -542,6 +581,30 @@ public class EnsoContext {
return notificationHandler;
}
private Object getNestedClass(Object hostClass, List<String> nestedClassName) {
Object nestedClass = hostClass;
var interop = InteropLibrary.getUncached();
for (String name : nestedClassName) {
if (interop.isMemberReadable(nestedClass, name)) {
Object member;
try {
member = interop.readMember(nestedClass, name);
} catch (UnsupportedMessageException | UnknownIdentifierException e) {
throw new IllegalStateException(e);
}
assert member != null;
if (interop.isMetaObject(member)) {
nestedClass = member;
} else {
return null;
}
} else {
return null;
}
}
return nestedClass;
}
private <T> T getOption(OptionKey<T> key) {
var options = getEnvironment().getOptions();
var safely = false;

View File

@ -68,9 +68,9 @@ import org.enso.interpreter.runtime.callable.function.{
Function => RuntimeFunction
}
import org.enso.interpreter.runtime.callable.{
Annotation => RuntimeAnnotation,
UnresolvedConversion,
UnresolvedSymbol
UnresolvedSymbol,
Annotation => RuntimeAnnotation
}
import org.enso.interpreter.runtime.data.Type
import org.enso.interpreter.runtime.data.text.Text
@ -82,12 +82,11 @@ import org.enso.interpreter.runtime.scope.{
import org.enso.interpreter.{Constants, EnsoLanguage}
import java.math.BigInteger
import scala.annotation.tailrec
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.jdk.OptionConverters._
import scala.jdk.CollectionConverters._
import scala.jdk.OptionConverters._
/** This is an implementation of a codegeneration pass that lowers the Enso
* [[IR]] into the truffle structures that are actually executed.
@ -191,10 +190,17 @@ class IrToTruffle(
// Register the imports in scope
imports.foreach {
case poly @ Import.Polyglot(i: Import.Polyglot.Java, _, _, _, _) =>
this.moduleScope.registerPolyglotSymbol(
poly.getVisibleName,
context.getEnvironment.lookupHostSymbol(i.getJavaName)
)
val hostSymbol = context.lookupJavaClass(i.getJavaName)
if (hostSymbol != null) {
this.moduleScope.registerPolyglotSymbol(
poly.getVisibleName,
hostSymbol
)
} else {
throw new CompilerError(
s"Incorrect polyglot import: Cannot find host symbol (Java class) '${i.getJavaName}'"
)
}
case _: Import.Module =>
case _: Error =>
}

View File

@ -61,4 +61,35 @@ public class TestClass {
default -> 2;
};
}
public static String enumToString(InnerEnum e) {
return e.toString();
}
public static class StaticInnerClass {
private final String data;
public StaticInnerClass(String data) {
this.data = data;
}
public String getData() {
return data;
}
public long add(long a, long b) {
return a + b;
}
public static class StaticInnerInnerClass {
public long mul(long a, long b) {
return a * b;
}
}
}
public enum InnerEnum {
ENUM_VALUE_1,
ENUM_VALUE_2
}
}

View File

@ -0,0 +1,174 @@
package org.enso.interpreter.test;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class JavaInteropTest extends TestBase {
private static Context ctx;
private static final ByteArrayOutputStream out = new ByteArrayOutputStream();
@BeforeClass
public static void prepareCtx() {
ctx = createDefaultContext(out);
}
@AfterClass
public static void disposeCtx() {
ctx.close();
}
@Before
public void resetOutput() {
out.reset();
}
private void checkPrint(String code, List<String> expected) {
Value result = evalModule(ctx, code);
assertTrue("should return Nothing", result.isNull());
String[] logLines = out
.toString(StandardCharsets.UTF_8)
.trim()
.split(System.lineSeparator());
assertArrayEquals(expected.toArray(), logLines);
}
@Test
public void testClassImport() {
var code = """
polyglot java import org.enso.example.TestClass
main = TestClass.add 1 2
""";
var result = evalModule(ctx, code);
assertEquals(3, result.asInt());
}
@Test
public void testClassImportAndMethodCall() {
var code = """
polyglot java import org.enso.example.TestClass
main =
instance = TestClass.new (x -> x * 2)
instance.callFunctionAndIncrement 10
""";
var result = evalModule(ctx, code);
assertEquals(21, result.asInt());
}
@Test
public void testImportStaticInnerClass() {
var code = """
polyglot java import org.enso.example.TestClass.StaticInnerClass
main =
instance = StaticInnerClass.new "my_data"
instance.add 1 2
""";
var result = evalModule(ctx, code);
assertEquals(3, result.asInt());
}
@Test
public void testImportInnerEnum() {
var code = """
from Standard.Base import IO
polyglot java import org.enso.example.TestClass
polyglot java import org.enso.example.TestClass.InnerEnum
main =
IO.println <| TestClass.enumToString InnerEnum.ENUM_VALUE_1
IO.println <| TestClass.enumToString TestClass.InnerEnum.ENUM_VALUE_2
""";
checkPrint(code, List.of("ENUM_VALUE_1", "ENUM_VALUE_2"));
}
@Test
public void testImportOuterClassAndReferenceInner() {
var code = """
polyglot java import org.enso.example.TestClass
main =
instance = TestClass.StaticInnerClass.new "my_data"
instance.getData
""";
var result = evalModule(ctx, code);
assertEquals("my_data", result.asString());
}
@Test
public void testImportBothInnerAndOuterClass() {
var code = """
from Standard.Base import IO
polyglot java import org.enso.example.TestClass
polyglot java import org.enso.example.TestClass.StaticInnerClass
main =
inner_value = TestClass.StaticInnerClass.new "my_data"
other_inner_value = StaticInnerClass.new "my_data"
IO.println <| inner_value.getData
IO.println <| other_inner_value.getData
""";
checkPrint(code, List.of("my_data", "my_data"));
}
@Test
public void testImportNestedInnerClass() {
var code = """
polyglot java import org.enso.example.TestClass.StaticInnerClass.StaticInnerInnerClass
main =
inner_inner_value = StaticInnerInnerClass.new
inner_inner_value.mul 3 5
""";
var res = evalModule(ctx, code);
assertEquals(15, res.asInt());
}
@Test
public void testImportNonExistingInnerClass() {
var code = """
polyglot java import org.enso.example.TestClass.StaticInnerClass.Non_Existing_Class
""";
try {
evalModule(ctx, code);
fail("Should throw exception");
} catch (Exception ignored) {}
}
@Test
public void testImportNonExistingInnerNestedClass() {
var code = """
polyglot java import org.enso.example.TestClass.Non_Existing_Class.Another_Non_ExistingClass
""";
try {
evalModule(ctx, code);
fail("Should throw exception");
} catch (Exception ignored) {}
}
@Test
public void testImportOuterClassAndAccessNestedInnerClass() {
var code = """
polyglot java import org.enso.example.TestClass
main =
instance = TestClass.StaticInnerClass.StaticInnerInnerClass.new
instance.mul 3 5
""";
var res = evalModule(ctx, code);
assertEquals(15, res.asInt());
}
}

View File

@ -1,36 +0,0 @@
package org.enso.interpreter.test.semantic
import org.enso.interpreter.test.{InterpreterContext, InterpreterTest}
class JavaInteropTest extends InterpreterTest {
override def subject: String = "Java Interop"
override def specify(implicit
interpreterContext: InterpreterContext
): Unit = {
"allow importing classes and calling methods on them" in {
val code =
"""
|polyglot java import org.enso.example.TestClass
|
|main = TestClass.add 1 2
|""".stripMargin
eval(code) shouldEqual 3
}
"allow instantiating objects and calling methods on them" in {
val code =
"""
|polyglot java import org.enso.example.TestClass
|
|main =
| instance = TestClass.new (x -> x * 2)
| instance.callFunctionAndIncrement 10
|""".stripMargin
eval(code) shouldEqual 21
}
}
}

View File

@ -16,6 +16,29 @@ object NativeImage {
*/
private val includeDebugInfo: Boolean = false
/** List of classes that should be initialized at build time by the native image.
* Note that we strive to initialize as much classes during the native image build
* time as possible, as this reduces the time needed to start the native image.
* One wildcard could theoretically be used instead of the list, but to make things
* more explicit, we use the list.
*/
private val defaultBuildTimeInitClasses = Seq(
"org",
"org.enso",
"scala",
"java",
"sun",
"cats",
"io",
"shapeless",
"com",
"izumi",
"zio",
"enumeratum",
"akka",
"nl"
)
/** Creates a task that builds a native image for the current project.
*
* This task must be setup in such a way that the assembly JAR is built
@ -42,6 +65,8 @@ object NativeImage {
* @param initializeAtRuntime a list of classes that should be initialized at
* run time - useful to set exceptions if build
* time initialization is set to default
* @param initializeAtBuildtime a list of classes that should be initialized at
* build time.
*/
def buildNativeImage(
artifactName: String,
@ -50,6 +75,7 @@ object NativeImage {
buildMemoryLimitMegabytes: Option[Int] = Some(15608),
runtimeThreadStackMegabytes: Option[Int] = Some(2),
initializeAtRuntime: Seq[String] = Seq.empty,
initializeAtBuildtime: Seq[String] = defaultBuildTimeInitClasses,
mainClass: Option[String] = None,
cp: Option[String] = None
): Def.Initialize[Task[Unit]] = Def
@ -110,6 +136,13 @@ object NativeImage {
val runtimeMemoryOptions =
runtimeThreadStackMegabytes.map(megs => s"-R:StackSize=${megs}M").toSeq
val initializeAtBuildtimeOptions =
if (initializeAtBuildtime.isEmpty) Seq()
else {
val classes = initializeAtBuildtime.mkString(",")
Seq(s"--initialize-at-build-time=$classes")
}
val initializeAtRuntimeOptions =
if (initializeAtRuntime.isEmpty) Seq()
else {
@ -122,7 +155,7 @@ object NativeImage {
quickBuildOption ++
debugParameters ++ staticParameters ++ configs ++
Seq("--no-fallback", "--no-server") ++
Seq("--initialize-at-build-time=") ++
initializeAtBuildtimeOptions ++
initializeAtRuntimeOptions ++
buildMemoryLimitOptions ++
runtimeMemoryOptions ++