From fe45da98d7db8e6aefe633c9c113f7eee9720db3 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 24 Oct 2024 13:56:28 +0200 Subject: [PATCH] Use `enso.dev.insight` property to turn Insight on (#11385) --- .../java/org/enso/common/ContextFactory.java | 24 +++-- .../org/enso/common/ContextInsightSetup.java | 102 ++++++++++++++++++ .../enso/languageserver/boot/MainModule.scala | 3 + .../common/test/ContextInsightSetupTest.java | 57 ++++++++++ .../enso/interpreter/epb/ForeignEvalNode.java | 20 ++-- .../enso/interpreter/epb/JsForeignNode.java | 6 +- .../interpreter/epb/ForeignEvalNodeTest.java | 5 +- .../src/Semantic/Js_Interop_Spec.enso | 13 ++- 8 files changed, 208 insertions(+), 22 deletions(-) create mode 100644 engine/common/src/main/java/org/enso/common/ContextInsightSetup.java create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/common/test/ContextInsightSetupTest.java diff --git a/engine/common/src/main/java/org/enso/common/ContextFactory.java b/engine/common/src/main/java/org/enso/common/ContextFactory.java index 2b42526bfdc3..79c1e5a9ee47 100644 --- a/engine/common/src/main/java/org/enso/common/ContextFactory.java +++ b/engine/common/src/main/java/org/enso/common/ContextFactory.java @@ -39,7 +39,7 @@ public final class ContextFactory { private OutputStream out = System.out; private OutputStream err = System.err; private MessageTransport messageTransport; - private Level logLevel; + private Level logLevel = Level.INFO; private boolean logMasking; private boolean enableIrCaches; private boolean disablePrivateCheck; @@ -166,7 +166,6 @@ public Context build() { .allowExperimentalOptions(true) .allowAllAccess(true) .allowHostAccess(allWithTypeMapping()) - .option(RuntimeOptions.PROJECT_ROOT, projectRoot) .option(RuntimeOptions.STRICT_ERRORS, Boolean.toString(strictErrors)) .option(RuntimeOptions.DISABLE_LINTING, Boolean.toString(disableLinting)) .option(RuntimeOptions.WAIT_FOR_PENDING_SERIALIZATION_JOBS, "true") @@ -183,6 +182,7 @@ public Context build() { .out(out) .err(err) .in(in); + if (checkForWarnings != null) { builder.option(DebugServerInfo.METHOD_BREAKPOINT_OPTION, checkForWarnings); } @@ -206,12 +206,15 @@ public Context build() { } builder.logHandler(logHandler); - var graalpy = - new File( - new File(new File(new File(new File(projectRoot), "polyglot"), "python"), "bin"), - "graalpy"); - if (graalpy.exists()) { - builder.option("python.Executable", graalpy.getAbsolutePath()); + if (projectRoot != null) { + builder.option(RuntimeOptions.PROJECT_ROOT, projectRoot); + var graalpy = + new File( + new File(new File(new File(new File(projectRoot), "polyglot"), "python"), "bin"), + "graalpy"); + if (graalpy.exists()) { + builder.option("python.Executable", graalpy.getAbsolutePath()); + } } if (ENGINE_HAS_JAVA) { var javaHome = System.getProperty("java.home"); @@ -224,7 +227,10 @@ public Context build() { .option("java.UseBindingsLoader", "true") .allowCreateThread(true); } - return builder.build(); + + var ctx = builder.build(); + ContextInsightSetup.configureContext(ctx); + return ctx; } /** diff --git a/engine/common/src/main/java/org/enso/common/ContextInsightSetup.java b/engine/common/src/main/java/org/enso/common/ContextInsightSetup.java new file mode 100644 index 000000000000..375240b878d1 --- /dev/null +++ b/engine/common/src/main/java/org/enso/common/ContextInsightSetup.java @@ -0,0 +1,102 @@ +package org.enso.common; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Function; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Source; + +/** + * Development support for running GraalVM Insight scripts in the + * Enso execution enviroments. Works both - in the CLI as well as in IDE. To use specify JVM + * property {@link #INSIGHT_PROP} when executing the CLI: + * + *
+ * enso --vm.D=enso.dev.insight=insightScript.js
+ * 
+ * + * or when launching the {@code project-manager}: + * + *
+ * ENSO_JVM_OPTS=-Denso.dev.insight=`pwd`/insightScript.js project-manager
+ * 
+ * + * The sample {@code insightScript.js} can look for example like: + * + *
+ * print("Initializing Insight: " + insight);
+ * insight.on("enter", function(ctx) {
+ *   print("Calling " + ctx.name);
+ * }, {
+ *   roots: true
+ * });
+ * 
+ * + * More information about Insight scripts can be found in the Insight manual and programatic + * documentation. + */ +final class ContextInsightSetup { + private static final String INSIGHT_PROP = "enso.dev.insight"; + private static ContextInsightSetup ACTIVE; + + private final Context ctx; + private final Path insightFile; + private AutoCloseable insightHandle; + + private ContextInsightSetup(Context ctx, Path file) { + this.ctx = ctx; + this.insightFile = file; + } + + /** + * Configures the context if {@link #INSIGHT_PROP} property is specified. This support is + * development only. It can be (and will be) removed in the future. + * + * @param ctx context to configure + * @throws AssertionError throws assertion error if the property is specified, but something goes + * wrong + */ + @SuppressWarnings("CallToPrintStackTrace") + static void configureContext(Context ctx) throws AssertionError { + var insightProp = System.getProperty(INSIGHT_PROP); + if (insightProp != null) { + var insightFile = new File(insightProp); + try { + insightFile = insightFile.getCanonicalFile(); + assert insightFile.isFile() + : "Cannot find " + insightFile + " specified via " + INSIGHT_PROP + " property"; + ACTIVE = new ContextInsightSetup(ctx, insightFile.toPath()); + ACTIVE.initialize(); + } catch (Error | Exception ex) { + var ae = + new AssertionError( + "Cannot initialize " + insightFile + " specified via " + INSIGHT_PROP + " property", + ex); + ae.printStackTrace(); + throw ae; + } + } + } + + private void initialize() throws IOException { + var insightCode = Files.readString(insightFile); + var language = "js"; + if (insightFile.getFileName().toString().endsWith(".py")) { + language = "python"; + } + var insightSrc = + Source.newBuilder("epb", insightFile.toFile()) + .content(language + ":0#" + insightCode) + .build(); + + var instrument = ctx.getEngine().getInstruments().get("insight"); + @SuppressWarnings("unchecked") + var insight = (Function) instrument.lookup(Function.class); + insightHandle = insight.apply(insightSrc); + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index bd2bbbb31b43..cea1efbc101e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -339,6 +339,9 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: Level) { connection } else null }) + if (System.getProperty("enso.dev.insight") != null) { + stdOut.attach(arr => System.out.write(arr)) + } system.eventStream.setLogLevel(AkkaConverter.toAkka(logLevel)) log.trace("Set akka log level to [{}]", logLevel) diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/common/test/ContextInsightSetupTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/common/test/ContextInsightSetupTest.java new file mode 100644 index 000000000000..80c0c67b85aa --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/common/test/ContextInsightSetupTest.java @@ -0,0 +1,57 @@ +package org.enso.common.test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; +import org.enso.common.ContextFactory; +import org.enso.test.utils.ContextUtils; +import org.hamcrest.core.AllOf; +import org.junit.AfterClass; +import org.junit.Test; + +/** + * Demonstrates usage of {@code -Denso.dev.insight=insightScript.js} property. This is a + * developement only support for playing with GraalVM Insight scripts inside of the IDE as well in + * CLI. + */ +public class ContextInsightSetupTest { + + public ContextInsightSetupTest() {} + + @AfterClass + public static void cleanupInsightProperty() { + System.getProperties().remove("enso.dev.insight"); + } + + @Test + public void initializeInsightViaProperty() throws Exception { + var insight = File.createTempFile("insight", ".js"); + try (java.io.FileWriter w = new FileWriter(insight)) { + w.write( + """ + print("Insight started. Properties: " + Object.getOwnPropertyNames(insight).sort()); + """); + } + + System.setProperty("enso.dev.insight", insight.getPath()); + + var out = new ByteArrayOutputStream(); + try (var ctx = ContextFactory.create().out(out).build()) { + + var fourtyTwo = ContextUtils.evalModule(ctx, """ + main = 42 + """); + + assertEquals("42", fourtyTwo.toString()); + + assertThat( + out.toString(), + AllOf.allOf( + containsString("Insight started."), containsString("Properties: id,version"))); + } + } +} 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 29321855ced1..6a3e995b4cb7 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 @@ -64,7 +64,8 @@ private int splitAt(CharSequence seq, char ch) { } at++; } - throw new ForeignParsingException("No " + ch + " found", this); + throw new ForeignParsingException( + "No `" + ch + "` found. Expecting `lang:lineno#code` format.", this); } @Override @@ -106,12 +107,17 @@ yield switch (id) { private ForeignFunctionCallNode parseJs() { var context = EpbContext.get(this); var inner = context.getInnerContext(); - var code = foreignSource(langAndCode); - var args = Arrays.stream(argNames).skip(1).collect(Collectors.joining(",")); - var wrappedSrc = "var poly_enso_eval=function(" + args + "){" + code + "\n};poly_enso_eval"; - Source source = newSource("js", wrappedSrc); - var fn = inner.evalPublic(this, source); - return JsForeignNode.build(fn); + if (inner != null) { + var code = foreignSource(langAndCode); + var args = Arrays.stream(argNames).collect(Collectors.joining(",")); + var wrappedSrc = "var poly_enso_eval=function(" + args + "){" + code + "\n};poly_enso_eval"; + Source source = newSource("js", wrappedSrc); + var fn = inner.evalPublic(this, source); + return JsForeignNode.build(fn); + } else { + return new GenericForeignNode( + RootNode.createConstantNode("Cannot evaluate script in inner context!").getCallTarget()); + } } private ForeignFunctionCallNode parseGeneric( diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/JsForeignNode.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/JsForeignNode.java index 5634e3060873..e4dc526d8680 100644 --- a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/JsForeignNode.java +++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/JsForeignNode.java @@ -1,5 +1,6 @@ package org.enso.interpreter.epb; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.NodeField; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.interop.InteropException; @@ -34,15 +35,15 @@ static JsForeignNode build(Object jsFunction) { @Specialization Object doExecute(Object[] arguments, @CachedLibrary("foreignFunction") InteropLibrary iop) throws InteropException { - var args = new ArgumentsArray(arguments); var self = arguments[0]; + var args = new ArgumentsArray(arguments); var raw = iop.invokeMember(getForeignFunction(), "apply", self, args); return coercePrimitiveNode.execute(raw); } @ExportLibrary(InteropLibrary.class) static final class ArgumentsArray implements TruffleObject { - private static final int OFFSET = 1; + private static final int OFFSET = 0; private final Object[] items; public ArgumentsArray(Object... items) { @@ -88,6 +89,7 @@ boolean isArrayElementInsertable(long index) { } @Override + @TruffleBoundary public String toString() { return Arrays.toString(items); } diff --git a/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/ForeignEvalNodeTest.java b/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/ForeignEvalNodeTest.java index 4a8f74e8f65d..28f0d1ec7d00 100644 --- a/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/ForeignEvalNodeTest.java +++ b/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/ForeignEvalNodeTest.java @@ -1,6 +1,7 @@ package org.enso.interpreter.epb; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import com.oracle.truffle.api.source.Source; import java.util.Collections; @@ -21,7 +22,7 @@ public void sourceWithoutHash() throws Exception { var res = node.execute(null); fail("Unexpected result: " + res); } catch (ForeignParsingException e) { - assertEquals("No # found", e.getMessage()); + assertEquals("No `#` found. Expecting `lang:lineno#code` format.", e.getMessage()); } } } diff --git a/test/Base_Tests/src/Semantic/Js_Interop_Spec.enso b/test/Base_Tests/src/Semantic/Js_Interop_Spec.enso index e854fe34d97b..339e75d86ab4 100644 --- a/test/Base_Tests/src/Semantic/Js_Interop_Spec.enso +++ b/test/Base_Tests/src/Semantic/Js_Interop_Spec.enso @@ -12,10 +12,13 @@ foreign js debug = """ type My_Type Value a b - foreign js my_method self = """ + foreign js my_method_self self = """ + return self.a + self.b; + + foreign js my_method_this self = """ return this.a + this.b; - my_method_2 self x = self.my_method * x + my_method_2 self x = self.my_method_this * x foreign js my_method_3 self y = """ var r = this.my_method_2(y) @@ -91,6 +94,12 @@ add_specs suite_builder = suite_builder.group "Polyglot JS" pending=pending_js_m group_builder.specify "should allow mutual calling of instance-level methods" <| My_Type.Value 3 4 . my_method_3 5 . should_equal 36 + group_builder.specify "should allows to access this in instance methods" <| + My_Type.Value 3 5 . my_method_this . should_equal 8 + + group_builder.specify "should allows to access self in instance methods" <| + My_Type.Value 5 4 . my_method_self . should_equal 9 + group_builder.specify "should expose methods and fields of JS objects" <| obj = make_object obj.x . should_equal 10