diff --git a/build.sbt b/build.sbt
index 980a8892c2d8..7b34b47b24bf 100644
--- a/build.sbt
+++ b/build.sbt
@@ -2130,6 +2130,8 @@ lazy val `language-server` = (project in file("engine/language-server"))
(`language-server-deps-wrapper` / Compile / exportedModule).value,
(`fansi-wrapper` / Compile / exportedModule).value,
(`text-buffer` / Compile / exportedModule).value,
+ (`jvm-channel` / Compile / exportedModule).value,
+ (`jvm-interop` / Compile / exportedModule).value,
(`runtime-suggestions` / Compile / exportedModule).value,
(`runtime-parser` / Compile / exportedModule).value,
(`runtime-compiler` / Compile / exportedModule).value,
@@ -2414,10 +2416,13 @@ lazy val `runtime-language-epb` =
"org.graalvm.sdk" % "nativeimage" % graalMavenPackagesVersion
),
Compile / internalModuleDependencies := Seq(
+ (`jvm-channel` / Compile / exportedModule).value,
+ (`jvm-interop` / Compile / exportedModule).value,
(`ydoc-polyfill` / Compile / exportedModule).value,
(`runtime-utils` / Compile / exportedModule).value
)
)
+ .dependsOn(`jvm-interop` % Test)
lazy val `runtime-language-arrow` =
(project in file("engine/runtime-language-arrow"))
@@ -2696,6 +2701,8 @@ lazy val `runtime-integration-tests` =
(`connected-lock-manager` / Compile / exportedModule).value,
(`library-manager` / Compile / exportedModule).value,
(`persistance` / Compile / exportedModule).value,
+ (`jvm-channel` / Compile / exportedModule).value,
+ (`jvm-interop` / Compile / exportedModule).value,
(`interpreter-dsl` / Compile / exportedModule).value,
(`engine-common` / Compile / exportedModule).value,
(`edition-updater` / Compile / exportedModule).value,
@@ -2882,6 +2889,8 @@ lazy val `runtime-benchmarks` =
(`library-manager` / Compile / exportedModule).value,
(`persistance` / Compile / exportedModule).value,
(`interpreter-dsl` / Compile / exportedModule).value,
+ (`jvm-channel` / Compile / exportedModule).value,
+ (`jvm-interop` / Compile / exportedModule).value,
(`engine-common` / Compile / exportedModule).value,
(`edition-updater` / Compile / exportedModule).value,
(`editions` / Compile / exportedModule).value,
@@ -3990,22 +3999,19 @@ lazy val `jvm-interop` =
(Test / fork) := true,
commands += WithDebugCommand.withDebug,
libraryDependencies ++= Seq(
- "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided",
- "org.graalvm.truffle" % "truffle-dsl-processor" % graalMavenPackagesVersion % "provided",
- "org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % "provided",
- "org.graalvm.sdk" % "graal-sdk" % graalMavenPackagesVersion % Test,
- "junit" % "junit" % junitVersion % Test,
- "com.github.sbt" % "junit-interface" % junitIfVersion % Test
+ "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided",
+ "org.graalvm.truffle" % "truffle-dsl-processor" % graalMavenPackagesVersion % "provided",
+ "org.graalvm.sdk" % "graal-sdk" % graalMavenPackagesVersion % Test,
+ "junit" % "junit" % junitVersion % Test,
+ "com.github.sbt" % "junit-interface" % junitIfVersion % Test
),
Compile / moduleDependencies ++= Seq(
- "org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion,
- "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion,
- "org.graalvm.sdk" % "nativeimage" % graalMavenPackagesVersion,
- "org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion,
- "org.graalvm.sdk" % "word" % graalMavenPackagesVersion
+ "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion,
+ "org.graalvm.sdk" % "nativeimage" % graalMavenPackagesVersion,
+ "org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion,
+ "org.graalvm.sdk" % "word" % graalMavenPackagesVersion
),
Compile / internalModuleDependencies ++= Seq(
- (`engine-common` / Compile / exportedModule).value,
(`jvm-channel` / Compile / exportedModule).value,
(`persistance` / Compile / exportedModule).value
)
diff --git a/docs/polyglot/README.md b/docs/polyglot/README.md
index eb73117571fe..abde59e9a9d3 100644
--- a/docs/polyglot/README.md
+++ b/docs/polyglot/README.md
@@ -8,11 +8,40 @@ order: 0
# Enso Polyglot Support
-Enso supports robust polyglot interoperation with other programming languages
-that are supported on its platform. This section of the design documentation
-deals with
+Enso supports robust interoperability with other programming languages - e.g.
+Enso is a _polyglot programming_ languge.
-It is broken down into the following sections:
+### interoperability with Java
+
+Unlike many other programming languages the _system language_ of Enso (e.g. the
+language that is used to do low-level operating system tasks) is **Java**. As
+such a lot of attention has been dedicated to make interoperability with
+**Java** as smooth as possible:
+
+- [**Java:**](./java.md) Detailed info about the Java polyglot bindings.
+
+Many `Standard` libraries are using these `polyglot java import` statements.
+Custom projects and libraries are encouraged to do the same. Interoperability
+with Java is a first class citizen in the Enso programming language.
+
+### Interoperability with Python, JavaScript & co.
+
+Enso greatly benefits from the
+[polyglot ecosystem of GraalVM](http://graalvm.org) and easily interops with any
+language from that ecosystem. Including **JavaScript**, **Python**, **R**, etc.
+
+- [**Python:**](./python.md) Specifics of the Python polyglot bindings.
+
+Interop with these _dynamic languages_ is primarily supported via
+[foreign function definitions](./polyglot-bindings.md#foreign-functions). When
+accessing Python, as well as other dynamic languages, the same pattern is used.
+Enough include the language support in the
+[distribution](../distribution/distribution.md) and the language gets
+automatically exposed via `foreign` directive to Enso programs.
+
+## Implementation
+
+Additional overview is provided in following documents:
- [**Polyglot Bindings:**](./polyglot-bindings.md) A document providing an
overview of the mechanisms provided to work with polyglot bindings in Enso.
@@ -20,9 +49,5 @@ It is broken down into the following sections:
of how we can provide a modicum of type safety for the polyglot bindings in
Enso.
-It also provides language-specific documentation for the various supported
-polyglot languages. These are as follows:
-
-- [**Java:**](./java.md) Information specific to the Java polyglot bindings.
-- [**Python:**](./python.md) Information specific to the Python polyglot
- bindings.
+Implementation details are described in
+[EpbLanguage javadoc](../../engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java).
diff --git a/docs/polyglot/polyglot-bindings.md b/docs/polyglot/polyglot-bindings.md
index f35896a364d7..400982350121 100644
--- a/docs/polyglot/polyglot-bindings.md
+++ b/docs/polyglot/polyglot-bindings.md
@@ -8,98 +8,19 @@ order: 1
# Polyglot Bindings
-This document deals with the specification and design for the polyglot interop
-system provided in the Enso runtime. This system allows users to connect Enso to
-other supported programming languages, to both provide access to a wealth of
-libraries, and to integrate Enso into existing systems.
-
-The polyglot support in Enso is best-in class, and it supports this through two
-main mechanisms:
-
-1. **Polyglot FFI:** The low-level polyglot support provides a fairly low-level
- syntax sugar system for working with values from foreign languages.
-2. **Embedded Syntax:** This system allows users to write code from other
- languages directly in their `.enso` files, and to seamlessly share values
- between Enso and that foreign code.
-
-
-
-- [Impedance Mismatch](#impedance-mismatch)
-- [The Polyglot FFI](#the-polyglot-ffi)
- - [Importing Polyglot Bindings](#importing-polyglot-bindings)
- - [Using Polyglot Bindings](#using-polyglot-bindings)
- - [Importing Polyglot Bindings \(Syntax\)](#importing-polyglot-bindings-syntax)
- - [Using Polyglot Bindings \(Syntax\)](#using-polyglot-bindings-syntax)
- - [Finding Polyglot Bindings](#finding-polyglot-bindings)
-- [Embedded Syntax](#embedded-syntax)
- - [Embedded Syntax Usage \(Syntax\)](#embedded-syntax-usage-syntax)
-
-
+Enso provides [robust interoperability](./README.md) with other programming
+languages. This document describes how users can connect Enso to other supported
+programming languages to gain access to a wealth of libraries, as well as to
+integrate Enso into existing systems.
-## Impedance Mismatch
-
-Polyglot interoperation in Enso has a significant impedance mismatch. In
-essence, this means that there is a mismatch between Enso's language semantics
-and the semantics of the foreign languages that are being worked with.
-
-While some of this mismatch can be worked around by manually wrapping the
-foreign constructs in Enso, there are still concepts that can't easily be
-represented by Enso.
-
-> The actionables for this section are:
->
-> - Expand on the impedance mismatch and how it leads to the defined semantics.
-
-## The Polyglot FFI
-
-The low-level polyglot FFI mechanism refers to a way to use polyglot objects
-directly in Enso code. This can be used to underlie a library implementaion in
-Enso, or to interoperate with code running in other languages.
-
-The mechanism provides users with the facilities to import bindings from other
-languages and call them via a generic mechanism.
-
-### Importing Polyglot Bindings
-
-When importing a polyglot binding into scope in an Enso file, this introduces a
-_polyglot object_ into scope. This object will have appropriate fields and/or
-methods defined on it, as described by the foreign language implementation.
-
-> The actionables for this section are:
->
-> - Expand greatly on the detail of this as the semantics of the imports become
-> clearer.
-
-### Using Polyglot Bindings
-
-With a polyglot object in scope, the user is free to call methods on it
-directly. These polyglot objects are inherently dynamically typed, meaning that
-any operation may _fail_ at runtime.
-
-Enso implements a generic variadic syntax for calling polyglot functions using
-vectors of arguments. In essence, this is necessary due to the significant
-impedance mismatch between Enso's runtime semantics (let alone the type system)
-and the runtime semantics of many of the polyglot languages.
-
-We went the way of the variadic call for multiple reasons:
-
-- It allows us to match up with a wide range of language semantics (such as
- subtyping and overloading).
-- It is flexible and easy to expand in the future.
-- We can easily build a more Enso-feeling interface on top of it.
-
-By way of illustrative example, Java supports method overloading and subtyping,
-two things which have no real equivalent in the Enso type system.
+## `polyglot import`
-> The actionables for this section are:
->
-> - Expand greatly on the runtime semantics of working with polyglot bindings.
-> - Determine how to make the inherent 'failability' of polyglot objects safer.
+Accessing existing objects of foreign languages can be done via
+`polyglot xyz import` statements. This primarily works for **Java** classes:
-### Importing Polyglot Bindings (Syntax)
+- [**Java:**](./java.md) Detailed info about the Java polyglot bindings.
-Polyglot bindings can be imported using a polyglot import directive. This is
-constructed as follows:
+The _polyglot import directive_ is constructed as follows:
- The `polyglot` keyword
- A language identifier (e.g. `java`).
@@ -113,39 +34,41 @@ For example:
```ruby
polyglot java import org.example.MyClass as MyClassJava
-polyglot c import struct NetworkPacket as NetworkPacketC
+polyglot c import struct NetworkPacket
```
-### Using Polyglot Bindings (Syntax)
-
-A polyglot binding is a polyglot object that has methods and/or fields defined
-on it. Due to an impedance mismatch between languages, Enso implements a
-variadic syntax for calling these polyglot bindings using vectors.
-
-In essence, we have a primitive function as follows:
+Once imported the `MyClassJava` as well as `NetworkPacket` objects behave as
+`Any` Enso objects. Such objects have methods and/or fields defined on them. The
+following is a valid usage of a polyglot binding:
```ruby
-Polyglot.method : Polyglot.Object -> [Any] -> Any
+main =
+ x = MyClassJava.foo [1, 2, 3] # a static method
+ inst = MyClassJava.new [a, b, c] # a constructor
+ bar = inst.method [x, y] # an instance method
```
-It works as follows:
+### Using Polyglot Bindings
-- It is a method called `method` defined on the `Polyglot` type. The name
- `method` is, however, a stand-in for the name of the method in question.
-- It takes an object instance of the polyglot object.
-- It takes a vector of arguments (and is hence variadic).
-- And it returns some value.
+With a polyglot object in scope, the user is free to call methods on it
+directly. These polyglot objects are inherently dynamically typed, meaning that
+they have `Any` type. As such there is no _static type checking_ when invoking
+methods on such types and potential errors are only detected during runtime and
+result in a runtime _failure_ (a typical behavior of Python or JavaScript
+programs).
-By way of example, the following is a valid usage of a polyglot binding:
+Enso implements a generic variadic syntax for calling polyglot functions using
+vectors of arguments. In essence, this is necessary due to the significant
+impedance mismatch between Enso's runtime semantics and the runtime semantics of
+many of the polyglot languages. Such a solution:
-```ruby
-polyglot java import com.example.MyClass as MyClassJava
+- allows Enso to match up with a wide range of language semantics
+ - for example Java's subtyping and overloading
+- it is flexible and easy to expand in the future.
+- allows building a more Enso-feeling interface on top of it.
-main =
- x = MyClassJava.foo [1, 2, 3] # a static method
- inst = MyClassJava.new [a, b, c] # a constructor
- bar = inst.metod [x, y] # a static method
-```
+Thanks to the generic variadic syntax, it is possible to smoothly invoke Java
+overloaded and overriden methods.
### Finding Polyglot Bindings
@@ -158,32 +81,49 @@ Inside each directory is an implementation-defined structure, with the polyglot
implementation for that particular language needing to specify it. Please see
the language-specific documentation for details.
-## Embedded Syntax
+## `foreign` functions
+
+It is possible to define new code snippets of foreign languages directly in
+`.enso` source files using _"Embedded Syntax"_. Such a handy support provides a
+truly smooth user experience:
-The term "Embedded Syntax" is our terminology for the ability to use foreign
-language syntaxes directly from inside `.enso` files. This system builds upon
-the more generic mechanisms used by the [polyglot FFI](#the-polyglot-ffi) to
-provide a truly seamless user experience.
+```ruby
+foreign python concat x y = """
+ def join(a, b):
+ return str(a) + str(b)
+ return join(x, y)
-### Embedded Syntax Usage (Syntax)
+main u="Hello" s=" " w="World!" =
+ concat (concat u s) w
+```
-A polyglot block is introduced as follows:
+The previous example defines an Enso function `concat` that takes two arguments
+`a` and `b`. The function is implemented in Python. The Python code defines a
+local function `join` and uses it to compute and return result of `concat`. Then
+the `concat` function is invoked from a `main` Enso function to concatenate
+typical _Hello World!_ message.
-- The `foreign` keyword starts a block.
-- This must be followed by a language identifier (e.g. `python`).
-- After the language identifier, the remaining syntax behaves like it is an Enso
- function definition until the `=`.
-- After the `=`, the user may write their foreign code as a string.
+- [**Python:**](./python.md) Details on Python polyglot bindings.
-```ruby
-foreign python concat a b = """
- def concat(a, b):
- str(a) + str(b)
-```
+Similar syntax can be used for `js` and other dynamic languages. Certain
+languages require/have special support, but in general this mechanism is reusing
+polyglot capabilities of GraalVM Truffle framework and works with any language
+that implements its `InteropLibrary` and _"parse in a context"_ protocols.
-In the above example, this defines a function `concat` that takes two arguments
-`a` and `b`, implemented in Python.
+## Impedance Mismatch
-> The actionables for this section are:
->
-> - Greatly flesh out the syntax for the high-level polyglot functionality.
+Enso is designed as a functional programming language and as such it assumes
+_mininal side effects_ when performing operation. Especially the _live
+programming_ environment provided by the Enso visual editor relies on operations
+being idempotent and having no side effects. Enso semantic enforces such _no
+side effects_ behavior for programs written in Enso.
+
+This is not a typical behavior of other programming languages and certainly it
+is not enforced in languages like JavaScript, Python or Java. Polyglot
+interoperation in Enso has a significant impedance mismatch. In essence, this
+means that there is a mismatch between Enso's language semantics and the
+semantics of the foreign languages that are being worked with.
+
+Some of thes mismatches can be worked around by manually wrapping the foreign
+constructs in Enso, however some just cannot. Care must be taken when dealing
+with other languages and especially their side-effects.
diff --git a/docs/polyglot/typing-polyglot-bindings.md b/docs/polyglot/typing-polyglot-bindings.md
index 38e1b1fe8642..a41163b4fdea 100644
--- a/docs/polyglot/typing-polyglot-bindings.md
+++ b/docs/polyglot/typing-polyglot-bindings.md
@@ -8,96 +8,43 @@ order: 2
# Typing the Polyglot Bindings
-The polyglot bindings inherently provide a problem for the Enso type system.
-When many of the languages with which we can interoperate are highly dynamic and
-flexible, or have significant mismatches between their type system and Enso's,
-we can only make a best effort attempt to maintain type safety across this
-boundary.
-
-
-
-- [Enso Values](#enso-values)
-- [Polyglot Values](#polyglot-values)
-- [Dynamic](#dynamic)
- - [The Enso Boundary](#the-enso-boundary)
-
-
+The polyglot bindings inherently provide a problem for the Enso static type
+system. Many of the languages with which we can interoperate are highly dynamic
+and flexible, or have significant mismatches between their type system and Enso.
+The Enso static analysis just gives up when dealing with such
+[polyglot bindings](./polyglot-bindings.md).
## Enso Values
-The underlying nature of our runtime allows us to pass Enso values across the
-polyglot boundary while ensuring that they aren't modified. This means that the
-typing information known about a value `v` _before_ it is passed to a polyglot
-call is valid after the polyglot call, as long as the following properties hold:
-
-- The polyglot call does not modify the entity passed in.
-- The polyglot call returns an entity of the same type.
-
-However, there are sometimes cases where we _want_ to let an Enso value be used
-freely by the polyglot language. To that end, we have to have some way of
-distinguishing _safe_ usages of Enso values from _unsafe_ ones. In the latter
-case, the value needs to be treated as `Dynamic` after its use.
-
-> The actionables for this section are:
->
-> - Think much more on this.
+Enso values are immutable which allows us to pass Enso values across the
+polyglot boundary while ensuring that they aren't modified by foreign languages.
+This means that the typing information known about a value `v` _before_ it is
+passed to a polyglot call is valid after the polyglot call,
## Polyglot Values
In the presence of a polyglot value, however, there is very little that we can
determine about a value with which we are working. This means that we need to
have a principled way to assert properties on a polyglot object that can then be
-reflected in the Enso type system. This mechanism needs to deal with:
-
-- Concurrent access to polyglot objects.
-- Mutation and modification of polyglot objects.
-- Potentially taking _ownership_ of polyglot objects.
-
-> The actionables for this section are:
->
-> - Reflect more on this problem and think about what principled approaches we
-> could take to it.
-
-## Dynamic
-
-As Enso can seamlessly interoperate with other programming languages, we need a
-principled way of handling dynamic types that we don't really know anything
-about. This mechanism needs:
+reflected in the Enso type system. An object coming from foreign language can
+mutate and change its state anytime.
-- A way to record what properties we _expect_ from the dynamic.
-- A way to turn a dynamic into a well-principled type-system member without
- having the dynamics pollute the whole type system. This may involve a 'trust
- me' function, and potentially dynamicness-polymorphic types.
-- A way to understand as much as possible about what a dynamic _does_ provide.
-- A way to try and refine information about dynamics where possible.
+### Array like Structures
-```ruby
-obj.model =
- { atom : Text
- , dict : Map Text Any
- , info :
- { doc : Text
- , name : Text
- , code : Text
- , loc : Location
- }
- , arg : # used only when calling like a function
- { doc : Text
- , default : Maybe Any
- }
- }
-```
+GraalVM `InteropLibrary` offers a protocol for recognizing certain types of
+objects. For example one can find out whether an object `hasArrayElements`. Such
+objects are then recognized as _array like structures_.
-> The actionables for this section are:
->
-> - Work out how to do dynamic properly, keeping in mind that in a dynamic value
-> could self-modify underneath us.
+Enso distinguishes between its own _array like structures_ and foreign ones.
+While Enso `Vector` is immutable, _array like foreign objects_ may potentially
+mutate. To address that Enso offers two types:
-### The Enso Boundary
+- `Vector` - guaranteed (by those who create it) to be immutable
+- `Array` - an _array like structure_ which may potentially change
+- both these types follow the same interface and offer the same methods
-Fortunately, we can at least avoid foreign languages modifying memory owned by
-the Enso interpreter. As part of the interop library, Graal lets us mark memory
-as read-only. This means that the majority of data passed out (from a functional
-language like Enso) is not at risk. However, if we _do_ allow data to be worked
-with mutably, when control is returned to Enso it needs to be treated as a
-dynamic as it may have been modified.
+There is no way in Enso to modify objects of type `Array` (neither `Vector`). A
+**JavaScript** or **Python** allocated `Array` can mutate during execution of
+the program. Should one need to guarantee immutability, one can convert `Array`
+to `Vector` with `array_like_foreign_object . to Vector` conversion. Such a
+conversion creates read-only snapshot of the original _array like object_.
diff --git a/engine/common/src/main/java/module-info.java b/engine/common/src/main/java/module-info.java
index 1948bd645f37..d8a8258e258b 100644
--- a/engine/common/src/main/java/module-info.java
+++ b/engine/common/src/main/java/module-info.java
@@ -1,5 +1,4 @@
import org.enso.common.ContextLoggingConfigurator;
-import org.enso.common.PolyglotSymbolResolver;
module org.enso.engine.common {
requires org.graalvm.nativeimage;
@@ -7,6 +6,5 @@
exports org.enso.common;
- uses PolyglotSymbolResolver;
uses ContextLoggingConfigurator;
}
diff --git a/engine/common/src/main/java/org/enso/common/PolyglotSymbolResolver.java b/engine/common/src/main/java/org/enso/common/PolyglotSymbolResolver.java
deleted file mode 100644
index a5430d51bfe5..000000000000
--- a/engine/common/src/main/java/org/enso/common/PolyglotSymbolResolver.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package org.enso.common;
-
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.ServiceLoader;
-
-/**
- * Generic support for loading Java polyglot symbols. The resolver provides two kinds of interfaces:
- *
- *
- *
the client API - represented by all the public static methods
- *
the SPI - e.g. service provider interface - those are the protected abstract methods
- *
- *
- * Those who tend to extend the capabilities of loading Java classes into Enso runtime shall
- * register their own implementation visible via {@link ServiceLoader}.
- *
- * @see RuntimeOptions#HOST_CLASS_LOADING
- */
-public abstract class PolyglotSymbolResolver {
- private static final Collection ALL;
-
- static {
- var arr = new ArrayList();
- for (var l : ServiceLoader.load(PolyglotSymbolResolver.class)) {
- arr.add(l);
- }
- ALL = Collections.unmodifiableList(arr);
- }
-
- /**
- * Search all providers for given name.
- *
- * @param name dot separated name to search for
- * @return non-null object representing the name
- * @throws java.lang.ClassNotFoundException if no name was found
- */
- public static Object loadClass(String name) throws ClassNotFoundException {
- ClassNotFoundException ex = null;
- for (var p : ALL) {
- try {
- var found = p.handleLoadClass(name);
- assert found != null;
- return found;
- } catch (ClassNotFoundException cnfe) {
- ex = cnfe;
- }
- }
- if (ex == null) {
- throw new ClassNotFoundException(name);
- } else {
- throw ex;
- }
- }
-
- public static void addToClassPath(URL url) {
- for (var p : ALL) {
- p.handleAddToClassPath(url);
- }
- }
-
- /**
- * Subclasses implement this method to search for class with the provided name.
- *
- * @param name dot separated name to search for
- * @return non-{@code null} object representing the name
- * @throws java.lang.ClassNotFoundException if no name was found
- */
- protected abstract Object handleLoadClass(String name) throws ClassNotFoundException;
-
- protected abstract void handleAddToClassPath(URL url);
-}
diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/example/TestException.java b/engine/runtime-integration-tests/src/test/java/org/enso/example/TestException.java
new file mode 100644
index 000000000000..e3b220e2e1af
--- /dev/null
+++ b/engine/runtime-integration-tests/src/test/java/org/enso/example/TestException.java
@@ -0,0 +1,15 @@
+package org.enso.example;
+
+public class TestException extends Exception {
+ public TestException() {}
+
+ public static void throwMe() throws Exception {
+ throw new TestException();
+ }
+
+ public static void throwSubtype() throws Exception {
+ throw new SubException();
+ }
+
+ private static final class SubException extends TestException {}
+}
diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/example/ToString.java b/engine/runtime-integration-tests/src/test/java/org/enso/example/ToString.java
index 7655e475ada4..eaa409d4d0b7 100644
--- a/engine/runtime-integration-tests/src/test/java/org/enso/example/ToString.java
+++ b/engine/runtime-integration-tests/src/test/java/org/enso/example/ToString.java
@@ -1,5 +1,7 @@
package org.enso.example;
+import org.graalvm.polyglot.PolyglotException;
+
public class ToString {
private ToString() {}
@@ -9,8 +11,12 @@ public static interface Fooable {
}
public static String callFoo(Fooable f) {
- long x = f.foo();
- return "Fooable.foo() = " + x;
+ try {
+ long x = f.foo();
+ return "Fooable.foo() = " + x;
+ } catch (Throwable t) {
+ throw t;
+ }
}
public static String showObject(Object obj) {
@@ -18,7 +24,12 @@ public static String showObject(Object obj) {
}
public static String callFooAndShow(Fooable f) {
- long x = f.foo();
- return "{" + f.toString() + "}.foo() = " + x;
+ var x = f.foo();
+ try {
+ var s = f.toString();
+ return "{" + s + "}.foo() = " + x;
+ } catch (PolyglotException ex) {
+ return "Ex: " + ex.getMessage() + ", but foo() = " + x;
+ }
}
}
diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/GuestJavaInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/GuestJavaInteropTest.java
new file mode 100644
index 000000000000..0150f9cc9faf
--- /dev/null
+++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/GuestJavaInteropTest.java
@@ -0,0 +1,17 @@
+package org.enso.interpreter.test.interop;
+
+import org.enso.test.utils.ContextUtils;
+import org.junit.ClassRule;
+
+public final class GuestJavaInteropTest extends JavaInteropTest {
+ @ClassRule
+ public static final ContextUtils ctxRule =
+ ContextUtils.newBuilder()
+ .withModifiedContext((b) -> b.option("enso.classLoading", "guest"))
+ .build();
+
+ @Override
+ protected final ContextUtils ctx() {
+ return ctxRule;
+ }
+}
diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/HostJavaInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/HostJavaInteropTest.java
new file mode 100644
index 000000000000..93f8cf734f27
--- /dev/null
+++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/HostJavaInteropTest.java
@@ -0,0 +1,13 @@
+package org.enso.interpreter.test.interop;
+
+import org.enso.test.utils.ContextUtils;
+import org.junit.ClassRule;
+
+public final class HostJavaInteropTest extends JavaInteropTest {
+ @ClassRule public static final ContextUtils ctxRule = ContextUtils.newBuilder().build();
+
+ @Override
+ protected final ContextUtils ctx() {
+ return ctxRule;
+ }
+}
diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java
index 95c3d4e8ed92..49ca84e219a6 100644
--- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java
+++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java
@@ -12,24 +12,46 @@
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Value;
import org.junit.After;
-import org.junit.ClassRule;
import org.junit.Test;
-public class JavaInteropTest {
-
- @ClassRule public static final ContextUtils ctxRule = ContextUtils.createDefault();
+/**
+ * Tests {@code polyglot java import} behavior in isolation. When there is a problem with
+ * interactions with the Java classes, it is best to expand this test. It is easier to debug the
+ * problem then having whole integration tests and moreover this suite executes the same test in
+ * various configurations automatically.
+ *
+ *
The test itself is abstract class and just defines the test cases. Then there are various
+ * implementations:
+ *
+ *
{@link GuestJavaInteropTest} - dual JVM mode used when running Enso in native
+ * image mode and loading classes in separate HotSpot JVM
+ *
+ *
+ * Those implementations setup the {@link #ctx()} and execute the test in that setup. This way we
+ * can guarantee consistency between various implementations of the {@code polyglot java import}
+ * statements.
+ *
+ *
+ */
+public abstract class JavaInteropTest {
@After
public void resetOutput() {
- ctxRule.resetOut();
+ ctx().resetOut();
}
private String[] getStdOutLines() {
- return ctxRule.getOut().trim().split(System.lineSeparator());
+ return ctx().getOut().trim().split(System.lineSeparator());
}
private void checkPrint(String code, List expected) {
- Value result = ctxRule.evalModule(code);
+ Value result = ctx().evalModule(code);
assertTrue("should return Nothing", result.isNull());
assertArrayEquals(expected.toArray(), getStdOutLines());
}
@@ -41,7 +63,7 @@ public void testClassImport() {
polyglot java import org.enso.example.TestClass
main = TestClass.add 1 2
""";
- var result = ctxRule.evalModule(code);
+ var result = ctx().evalModule(code);
assertEquals(3, result.asInt());
}
@@ -55,7 +77,7 @@ public void testClassImportAndMethodCall() {
instance = TestClass.new (x -> x * 2)
instance.callFunctionAndIncrement 10
""";
- var result = ctxRule.evalModule(code);
+ var result = ctx().evalModule(code);
assertEquals(21, result.asInt());
}
@@ -69,7 +91,7 @@ public void testImportStaticInnerClass() {
instance = StaticInnerClass.new "my_data"
instance.add 1 2
""";
- var result = ctxRule.evalModule(code);
+ var result = ctx().evalModule(code);
assertEquals(3, result.asInt());
}
@@ -122,7 +144,7 @@ public void testCaseOnFunctionalInterface() {
main = check
""";
- var check = ctxRule.evalModule(code);
+ var check = ctx().evalModule(code);
assertEquals("'no'", check.execute("Not FnIntrfc").toString());
@@ -141,7 +163,7 @@ public void testCaseOnFunctionalInterface() {
main = My_Type.Value 1
""";
- var atom = ctxRule.evalModule(atomCode);
+ var atom = ctx().evalModule(atomCode);
assertEquals(
"atom is not Java interface at all " + "and it shouldn't pass the call:FnIntrfc check",
"'no'",
@@ -215,7 +237,7 @@ public void testImportOuterClassAndReferenceInner() {
instance = TestClass.StaticInnerClass.new "my_data"
instance.getData
""";
- var result = ctxRule.evalModule(code);
+ var result = ctx().evalModule(code);
assertEquals("my_data", result.asString());
}
@@ -246,7 +268,7 @@ public void testImportNestedInnerClass() {
inner_inner_value = StaticInnerInnerClass.new
inner_inner_value.mul 3 5
""";
- var res = ctxRule.evalModule(code);
+ var res = ctx().evalModule(code);
assertEquals(15, res.asInt());
}
@@ -257,7 +279,7 @@ public void testImportNonExistingInnerClass() {
polyglot java import org.enso.example.TestClass.StaticInnerClass.Non_Existing_Class
""";
try {
- ctxRule.evalModule(code);
+ ctx().evalModule(code);
fail("Should throw exception");
} catch (Exception ignored) {
}
@@ -270,7 +292,7 @@ public void testImportNonExistingInnerNestedClass() {
polyglot java import org.enso.example.TestClass.Non_Existing_Class.Another_Non_ExistingClass
""";
try {
- ctxRule.evalModule(code);
+ ctx().evalModule(code);
fail("Should throw exception");
} catch (Exception ignored) {
}
@@ -286,7 +308,7 @@ public void testImportOuterClassAndAccessNestedInnerClass() {
instance = TestClass.StaticInnerClass.StaticInnerInnerClass.new
instance.mul 3 5
""";
- var res = ctxRule.evalModule(code);
+ var res = ctx().evalModule(code);
assertEquals(15, res.asInt());
}
@@ -314,7 +336,7 @@ public void testToStringBehavior() {
[a, b, c, d, e]
""";
- var res = ctxRule.evalModule(code);
+ var res = ctx().evalModule(code);
assertTrue("It is an array", res.hasArrayElements());
assertEquals("Array with five elements", 5, res.getArraySize());
assertEquals(123, res.getArrayElement(0).asInt());
@@ -324,6 +346,85 @@ public void testToStringBehavior() {
assertEquals("{(Instance 23)}.foo() = 123", res.getArrayElement(4).asString());
}
+ @Test
+ public void testToStringBehaviorSimple1() {
+ var code =
+ """
+ from Standard.Base import all
+
+ polyglot java import org.enso.example.ToString as Foo
+
+ type My_Fooable_Implementation
+ Instance x
+
+ foo : Integer
+ foo self = 100+self.x
+
+ main =
+ fooable = My_Fooable_Implementation.Instance 23
+ e = Foo.callFooAndShow fooable
+ e
+ """;
+
+ var res = ctx().evalModule(code);
+ assertEquals("{(Instance 23)}.foo() = 123", res.asString());
+ }
+
+ @Test
+ public void throwsParsingError() {
+ var code =
+ """
+ from Standard.Base import Panic
+ polyglot java import java.lang.Integer as Num
+ polyglot java import java.lang.NumberFormatException as Ex
+
+ main =
+ Panic.catch Ex (Num.parseInt "NotAnInt") .payload
+ """;
+
+ var res = ctx().evalModule(code);
+ assertTrue("Got an exception back", res.isException());
+ var typeEx = res.getMetaObject();
+ assertEquals("java.lang.NumberFormatException", typeEx.getMetaQualifiedName());
+ try {
+ throw res.throwException();
+ } catch (PolyglotException ex) {
+ assertEquals("For input string: \"NotAnInt\"", ex.getMessage());
+ }
+ }
+
+ @Test
+ public void throwsParsingErrorIndirect() {
+ var code =
+ """
+ from Standard.Base import Panic
+ polyglot java import java.lang.Integer as Num
+ polyglot java import java.lang.NumberFormatException as Ex
+ polyglot java import org.enso.example.TestClass
+
+ type En
+ Err msg
+
+ main =
+ e = TestClass.newDirectExecutor
+ e.execute
+ Panic.catch Ex (Num.parseInt "NotAnInt") ex->
+ Panic.throw (En.Err ex.payload.to_text)
+ """;
+
+ try {
+ var res = ctx().evalModule(code);
+ fail("Expecting an exception: " + res);
+ } catch (PolyglotException ex) {
+ var exObj = ex.getGuestObject();
+ var typeEx = exObj.getMetaObject();
+ assertEquals("Standard.Base.Panic.Panic", typeEx.getMetaQualifiedName());
+ assertEquals(
+ "java.lang.NumberFormatException: For input string: \"NotAnInt\"",
+ exObj.getMember("msg").asString());
+ }
+ }
+
@Test
public void testInterfaceProxyFailuresA() {
var payload = evalInterfaceProxyFailures("a");
@@ -373,6 +474,54 @@ private Value evalInterfaceProxyFailures(String methodToEval) {
b = Panic.catch No_Such_Method (Foo.callFoo Fooable_Unresolved.Value) (caught-> caught.payload.method_name)
""";
- return ctxRule.evalModule(code + "\nmain = " + methodToEval);
+ return ctx().evalModule(code + "\nmain = " + methodToEval);
}
+
+ @Test
+ public void catchCheckedExceptionValueIsReturned() {
+ var result = checkedException(0);
+ assertEquals(result.asInt(), 10);
+ }
+
+ @Test
+ public void catchCheckedExceptionThrownInEnso() {
+ var result = checkedException(1);
+ assertEquals(result.asInt(), -1);
+ }
+
+ @Test
+ public void catchCheckedExceptionThrownInJava() {
+ var result = checkedException(2);
+ assertEquals(result.asInt(), -1);
+ }
+
+ @Test
+ public void catchCheckedSubExceptionThrownInJava() {
+ var result = checkedException(3);
+ assertEquals(result.asInt(), -1);
+ }
+
+ private Value checkedException(int t) {
+ var code =
+ """
+ polyglot java import org.enso.example.TestException
+ from Standard.Base import Panic
+
+ handle_errors ~action =
+ Panic.catch TestException action caught_panic->
+ -1
+
+ run t = case t of
+ 0 -> handle_errors 10
+ 1 -> handle_errors (Panic.throw TestException.new)
+ 2 -> handle_errors (TestException.throwMe)
+ 3 -> handle_errors (TestException.throwSubtype)
+
+ main = run
+ """;
+ var result = ctx().evalModule(code);
+ return result.execute(t);
+ }
+
+ protected abstract ContextUtils ctx();
}
diff --git a/engine/runtime-language-epb/src/main/java/module-info.java b/engine/runtime-language-epb/src/main/java/module-info.java
index 17a4474d0827..e3fdea5cb594 100644
--- a/engine/runtime-language-epb/src/main/java/module-info.java
+++ b/engine/runtime-language-epb/src/main/java/module-info.java
@@ -4,6 +4,8 @@
requires org.graalvm.truffle;
requires org.enso.runtime.utils;
requires org.enso.ydoc.polyfill;
+ requires org.enso.jvm.channel;
+ requires org.enso.jvm.interop;
provides com.oracle.truffle.api.provider.TruffleLanguageProvider with
org.enso.interpreter.epb.EpbLanguageProvider;
diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java
index 608dc355e63a..72d09cd7c760 100644
--- a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java
+++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java
@@ -4,9 +4,63 @@
import com.oracle.truffle.api.TruffleLanguage;
import java.util.function.Consumer;
-/** An internal language that serves as a bridge between Enso and other supported languages. */
+/**
+ * Enso Polyglot Bindings language is an internal language that serves as a bridge between
+ * Enso and other supported languages. See polyglot docs
+ * for a high level overview of intended behavior. Technical details are provided in this Javadoc
+ * and of course in this package code.
+ *
+ *
Generic foreign Support
+ *
+ * Each compliant enough Truffle language implementation that supports {@link
+ * TruffleLanguage#parse(com.oracle.truffle.api.TruffleLanguage.ParsingRequest)} with arguments
+ * provided via {@link ParsingRequest#getArgumentNames()} can be integrated with Enso's {@code
+ * foreign} code snippet concept.
+ *
+ *
foreign js Support
+ *
+ * Graal.js is compliant enough, but comes with a single threaded restriction. Thus it
+ * needs special support that creates a secondary JavaScript only context in {@link
+ * ForeignEvalNode#parseJs()}.
+ *
+ *
Another special support provided to {@code foreign js} code is ability to refer to Enso {@code
+ * self} as JavaScript {@code this}. Such a support isn't achievable via standard Truffle means and
+ * requires {@link JsForeignNode#doExecute} to use {@code apply} JavaScript function directly.
+ *
+ *
foreign python Support
+ *
+ * Graal Python had a lot of limitations in the past and as such the {@link PyForeignNode} does a
+ * lot of conversions to make sure Python {@code None} and Enso {@code Nothing} represent the same
+ * null value, that dates are properly transferred, etc.
+ *
+ *
Ideally each such limitation is reported to GraalPython guys. List of known issues:
+ *
+ *
+ *
+ * Once an official solution is available, workarounds may be removed.
+ *
+ *
polyglot java Support
+ *
+ * {@code EpbLanguage} is responsible for handling loading of Java classes via {@link
+ * JavaPolyglotNode}. There are three modes to load classes:
+ *
+ *
+ *
{@code hosted} - regular GraalVM Java interop is used to load JVM classes
+ *
{@code guest} - support SubstrateVM/HotspotVM bridge via {@code JVM} and {@code Channel}
+ *
Espresso support - experimental controlled by {@code ENSO_JAVA} environment variable
+ *
+ *
+ * Each of these mechanisms is supposed to create a {@link TruffleObject} representing the JVM class
+ * that Enso can then operate with.
+ */
@TruffleLanguage.Registration(
- id = "epb",
+ id = EpbLanguage.ID,
name = "Enso Polyglot Bridge",
characterMimeTypes = {EpbLanguage.MIME},
internal = true,
@@ -14,6 +68,7 @@
contextPolicy = TruffleLanguage.ContextPolicy.SHARED,
services = Consumer.class)
public final class EpbLanguage extends TruffleLanguage {
+ public static final String ID = "epb";
public static final String MIME = "application/epb";
@Override
diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java
index 1b0b0f29887d..b2defad906a0 100644
--- a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java
+++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java
@@ -83,11 +83,14 @@ public Object execute(VirtualFrame frame) {
var installedLanguages = context.getEnv().getPublicLanguages();
var node =
switch (installedLanguages.containsKey(id) ? 1 : 0) {
- case 0 -> {
- var sortedLangs = new TreeSet<>(installedLanguages.keySet());
- var ex = new ForeignParsingException(id, sortedLangs, this);
- yield new ExceptionForeignNode(ex);
- }
+ case 0 -> switch (id) {
+ case "java" -> parseJava();
+ default -> {
+ var sortedLangs = new TreeSet<>(installedLanguages.keySet());
+ var ex = new ForeignParsingException(id, sortedLangs, this);
+ yield new ExceptionForeignNode(ex);
+ }
+ };
default -> {
context.log(
Level.FINE,
@@ -111,6 +114,16 @@ yield switch (id) {
}
}
+ private ForeignFunctionCallNode parseJava() {
+ var code = foreignSource(langAndCode);
+ var context = EpbContext.get(this);
+ if ("hosted".equals(code)) {
+ return JavaPolyglotNode.createHosted(context);
+ } else {
+ return JavaPolyglotNode.create(context);
+ }
+ }
+
private ForeignFunctionCallNode parseJs() {
var context = EpbContext.get(this);
var inner = context.getInnerContext();
diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java
new file mode 100644
index 000000000000..5a87b186f224
--- /dev/null
+++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java
@@ -0,0 +1,232 @@
+package org.enso.interpreter.epb;
+
+import com.oracle.truffle.api.CompilerDirectives;
+import com.oracle.truffle.api.interop.ArityException;
+import com.oracle.truffle.api.interop.InteropException;
+import com.oracle.truffle.api.interop.InteropLibrary;
+import com.oracle.truffle.api.interop.InvalidArrayIndexException;
+import com.oracle.truffle.api.interop.TruffleObject;
+import com.oracle.truffle.api.interop.UnknownIdentifierException;
+import com.oracle.truffle.api.interop.UnsupportedMessageException;
+import com.oracle.truffle.api.interop.UnsupportedTypeException;
+import com.oracle.truffle.api.library.ExportLibrary;
+import com.oracle.truffle.api.library.ExportMessage;
+import java.io.File;
+import java.lang.System.Logger;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.graalvm.polyglot.Context;
+
+/**
+ * Host class loader that serves as a replacement for {@link
+ * com.oracle.truffle.host.HostClassLoader}. Add URLs to Jar archives with {@link #add(URL)}. All
+ * the classes that are loaded via this class loader are first searched inside those archives. If
+ * not found, delegates to parent class loaders.
+ */
+@ExportLibrary(InteropLibrary.class)
+final class HostClassLoader extends URLClassLoader implements AutoCloseable, TruffleObject {
+
+ private final Map> loadedClasses = new ConcurrentHashMap<>();
+ private static final Logger logger = System.getLogger(HostClassLoader.class.getName());
+ // Classes from "org.graalvm" packages are loaded either by a class loader for the boot
+ // module layer, or by a specific class loader, depending on how enso is run. For example,
+ // if enso is run via `org.graalvm.polyglot.Context.eval` from `javac`, then the graalvm
+ // classes are loaded via a class loader somehow created by `javac` and not by the boot
+ // module layer's class loader.
+ private static final ClassLoader polyglotClassLoader = Context.class.getClassLoader();
+
+ // polyglotClassLoader will be used only iff `org.enso.runtime` module is not in the
+ // boot module layer.
+ private static final boolean isRuntimeModInBootLayer;
+ private Object findLibraries;
+
+ public HostClassLoader() {
+ super(new URL[0]);
+ }
+
+ static {
+ var bootModules = ModuleLayer.boot().modules();
+ var hasRuntimeMod =
+ bootModules.stream().anyMatch(module -> module.getName().equals("org.enso.runtime"));
+ isRuntimeModInBootLayer = hasRuntimeMod;
+ }
+
+ void add(URL u) {
+ logger.log(Logger.Level.DEBUG, "Adding URL '{0}' to class path", u);
+ addURL(u);
+ }
+
+ @Override
+ @CompilerDirectives.TruffleBoundary
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ return loadClass(name, false);
+ }
+
+ @Override
+ @CompilerDirectives.TruffleBoundary
+ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ logger.log(Logger.Level.TRACE, "Loading class {0}", name);
+ var l = loadedClasses.get(name);
+ if (l != null) {
+ logger.log(Logger.Level.TRACE, "Class {0} found in cache", name);
+ return l;
+ }
+ synchronized (this) {
+ l = loadedClasses.get(name);
+ if (l != null) {
+ logger.log(Logger.Level.TRACE, "Class {0} found in cache", name);
+ return l;
+ }
+ if (!isRuntimeModInBootLayer && name.startsWith("org.graalvm")) {
+ return polyglotClassLoader.loadClass(name);
+ }
+ if (name.startsWith("org.slf4j")) {
+ // Delegating to system class loader ensures that log classes are not loaded again
+ // and do not require special setup. In other words, it is using log configuration that
+ // has been setup by the runner that started the process. See #11641.
+ return polyglotClassLoader.loadClass(name);
+ }
+ try {
+ l = findClass(name);
+ if (resolve) {
+ l.getMethods();
+ }
+ logger.log(Logger.Level.TRACE, "Class {0} found, putting in cache", name);
+ loadedClasses.put(name, l);
+ return l;
+ } catch (ClassNotFoundException ex) {
+ logger.log(Logger.Level.TRACE, "Class {0} not found, delegating to super", name);
+ return super.loadClass(name, resolve);
+ } catch (Throwable e) {
+ logger.log(Logger.Level.TRACE, "Failure while loading a class: " + e.getMessage(), e);
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Find the library with the specified name inside the {@code polyglot/lib} directory of caller's
+ * project. The search inside the {@code polyglot/lib} directory hierarchy is specified by NetBeans
+ * JNI specification.
+ *
+ *