mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Import polyglot java inner classes (#6818)
Adds the ability to import nested inner classes in polyglot java imports.
This commit is contained in:
parent
8bc3ebd70a
commit
e5c21713e7
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 =>
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 ++
|
||||
|
Loading…
Reference in New Issue
Block a user