/* * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.oracle.truffle.api.test.vm; import static com.oracle.truffle.api.test.vm.ImplicitExplicitExportTest.L1; import static com.oracle.truffle.api.test.vm.ImplicitExplicitExportTest.L1_ALT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import org.junit.After; import org.junit.Test; import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.TruffleLanguage; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.ForeignAccess; import com.oracle.truffle.api.interop.Message; import com.oracle.truffle.api.interop.TruffleObject; import com.oracle.truffle.api.interop.UnsupportedTypeException; import com.oracle.truffle.api.interop.java.JavaInterop; import com.oracle.truffle.api.interop.java.MethodMessage; import com.oracle.truffle.api.nodes.LanguageInfo; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.source.Source; import com.oracle.truffle.api.test.vm.ImplicitExplicitExportTest.Ctx; import com.oracle.truffle.api.vm.PolyglotEngine; import com.oracle.truffle.api.vm.PolyglotEngine.Builder; import com.oracle.truffle.api.vm.PolyglotEngine.Value; import com.oracle.truffle.api.vm.PolyglotRuntime; public class EngineTest { private final PolyglotRuntime testRuntime = PolyglotRuntime.newBuilder().build(); private final Set<PolyglotEngine> toDispose = new HashSet<>(); protected PolyglotEngine.Builder createBuilder() { return PolyglotEngine.newBuilder(); } private PolyglotEngine.Builder createBuilderInternal() { PolyglotEngine.Builder builder = createBuilder(); builder.runtime(testRuntime); return builder; } private PolyglotEngine register(PolyglotEngine engine) { toDispose.add(engine); return engine; } @After public void dispose() { for (PolyglotEngine engine : toDispose) { engine.dispose(); } } @Test public void npeWhenCastingAs() throws Exception { PolyglotEngine tvm = createBuilder().build(); register(tvm); PolyglotEngine.Language language1 = tvm.getLanguages().get("application/x-test-import-export-1"); PolyglotEngine.Language language2 = tvm.getLanguages().get("application/x-test-import-export-2"); language2.eval(Source.newBuilder("explicit.value=42").name("define 42").mimeType("content/unknown").build()); PolyglotEngine.Value value = language1.eval(Source.newBuilder("return=value").name("42.value").mimeType("content/unknown").build()); String res = value.as(String.class); assertNotNull(res); } @Test public void testPassingThroughInteropException() throws Exception { PolyglotEngine tvm = createBuilder().build(); register(tvm); PolyglotEngine.Language language1 = tvm.getLanguages().get("application/x-test-import-export-1"); try { PolyglotEngine.Value value = language1.eval(Source.newBuilder("throwInteropException").name("interopTest").mimeType("content/unknown").build()); value.as(Object.class); } catch (Exception e) { while (e instanceof RuntimeException) { e = (Exception) e.getCause(); } assertEquals("Expecting UnsupportedTypeException", UnsupportedTypeException.class, e.getClass()); return; } fail("Expected UnsupportedTypeException, got none"); } @Test public void checkCachingOfNodes() { PolyglotEngine vm1 = createBuilder().build(); register(vm1); PolyglotEngine vm2 = createBuilder().executor(Executors.newSingleThreadExecutor()).build(); register(vm2); PolyglotEngine.Language language1 = vm1.getLanguages().get("application/x-test-hash"); PolyglotEngine.Language language2 = vm2.getLanguages().get("application/x-test-hash"); PolyglotEngine.Language alt1 = vm1.getLanguages().get("application/x-test-hash-alt"); PolyglotEngine.Language alt2 = vm2.getLanguages().get("application/x-test-hash-alt"); final Source sharedSource = Source.newBuilder("anything").name("something").mimeType("content/unknown").build(); Object hashIn1Round1 = language1.eval(sharedSource).get(); Object hashIn2Round1 = language2.eval(sharedSource).get(); Object hashIn1Round2 = language1.eval(sharedSource).get(); Object hashIn2Round2 = language2.eval(sharedSource).get(); Object altIn1Round1 = alt1.eval(sharedSource).get(); Object altIn2Round1 = alt2.eval(sharedSource).get(); Object altIn1Round2 = alt1.eval(sharedSource).get(); Object altIn2Round2 = alt2.eval(sharedSource).get(); assertEquals("Two executions in 1st engine share the nodes", hashIn1Round1, hashIn1Round2); assertEquals("Two executions in 2nd engine share the nodes", hashIn2Round1, hashIn2Round2); assertEquals("Two alternative executions in 1st engine share the nodes", altIn1Round1, altIn1Round2); assertEquals("Two alternative executions in 2nd engine share the nodes", altIn2Round1, altIn2Round2); assertNotEquals("Two executions in different languages don't share the nodes", hashIn1Round1, altIn1Round1); assertNotEquals("Two executions in different languages don't share the nodes", hashIn1Round1, altIn2Round1); assertNotEquals("Two executions in different languages don't share the nodes", hashIn2Round2, altIn1Round2); assertNotEquals("Two executions in different languages don't share the nodes", hashIn2Round2, altIn2Round2); assertNotEquals("Two executions in different engines don't share the nodes", hashIn1Round1, hashIn2Round1); assertNotEquals("Two executions in different engines don't share the nodes", hashIn2Round2, hashIn1Round2); } protected Thread forbiddenThread() { return null; } private interface AccessArray { AccessArray dupl(); List<? extends Number> get(int index); } @Test public void wrappedAsArray() throws Exception { Object[][] matrix = {{1, 2, 3}}; PolyglotEngine tvm = createBuilder().globalSymbol("arr", new ArrayTruffleObject(matrix, forbiddenThread())).build(); register(tvm); PolyglotEngine.Language language1 = tvm.getLanguages().get("application/x-test-import-export-1"); AccessArray access = language1.eval(Source.newBuilder("return=arr").name("get the array").mimeType("content/unknown").build()).as(AccessArray.class); assertNotNull("Array converted to list", access); access = access.dupl(); List<? extends Number> list = access.get(0); assertEquals("Size 3", 3, list.size()); assertEquals(1, list.get(0)); assertEquals(2, list.get(1)); assertEquals(3, list.get(2)); Integer[] arr = list.toArray(new Integer[0]); assertEquals("Three items in array", 3, arr.length); assertEquals(1, arr[0].intValue()); assertEquals(2, arr[1].intValue()); assertEquals(3, arr[2].intValue()); } @Test public void engineConfigBasicAccess() { Builder builder = createBuilderInternal(); builder.config("application/x-test-import-export-1", "cmd-line-args", new String[]{"1", "2"}); builder.config("application/x-test-import-export-2", "hello", "world"); PolyglotEngine vm = builder.build(); register(vm); PolyglotEngine.Language language1 = vm.getLanguages().get("application/x-test-import-export-1"); assertNotNull("Lang1 found", language1); Ctx ctx1 = language1.getGlobalObject().as(Ctx.class); String[] args = (String[]) ctx1.env.getConfig().get("cmd-line-args"); assertNotNull("Founds args", args); assertEquals("1", args[0]); assertEquals("2", args[1]); assertNull("Can't see settings for other language", ctx1.env.getConfig().get("hello")); PolyglotEngine.Language language2 = vm.getLanguages().get("application/x-test-import-export-2"); assertNotNull("Lang2 found", language2); Ctx ctx2 = language2.getGlobalObject().as(Ctx.class); assertEquals("world", ctx2.env.getConfig().get("hello")); assertNull("Cannot find args", ctx2.env.getConfig().get("cmd-line-args")); } @Test public void engineConfigShouldBeReadOnly() { Builder builder = createBuilderInternal(); builder.config("application/x-test-import-export-1", "cmd-line-args", new String[]{"1", "2"}); builder.config("application/x-test-import-export-2", "hello", "world"); PolyglotEngine vm = builder.build(); register(vm); PolyglotEngine.Language language1 = vm.getLanguages().get("application/x-test-import-export-1"); Ctx ctx1 = language1.getGlobalObject().as(Ctx.class); // make sure configuration is read-only try { ctx1.env.getConfig().put("hi", "there!"); fail("The map should be readonly"); } catch (UnsupportedOperationException ex) { // OK } } @Test public void secondValueWins() { Builder builder = createBuilderInternal(); builder.config("application/x-test-import-export-2", "hello", "truffle"); builder.config("application/x-test-import-export-2", "hello", "world"); PolyglotEngine vm = builder.build(); register(vm); PolyglotEngine.Language language2 = vm.getLanguages().get("application/x-test-import-export-2"); Ctx ctx2 = language2.getGlobalObject().as(Ctx.class); assertEquals("world", ctx2.env.getConfig().get("hello")); } @Test public void secondValueWins2() { Builder builder = createBuilderInternal(); builder.config("application/x-test-import-export-2", "hello", "world"); builder.config("application/x-test-import-export-2", "hello", "truffle"); PolyglotEngine vm = builder.build(); register(vm); PolyglotEngine.Language language2 = vm.getLanguages().get("application/x-test-import-export-2"); Ctx ctx2 = language2.getGlobalObject().as(Ctx.class); assertEquals("truffle", ctx2.env.getConfig().get("hello")); } @Test public void altValueWins() { Builder builder = createBuilderInternal(); builder.config(L1, "hello", "truffle"); builder.config(L1_ALT, "hello", "world"); PolyglotEngine vm = builder.build(); register(vm); PolyglotEngine.Language language1 = vm.getLanguages().get(L1); Ctx ctx2 = language1.getGlobalObject().as(Ctx.class); assertEquals("world", ctx2.env.getConfig().get("hello")); } @Test public void altValueWins2() { Builder builder = createBuilderInternal(); builder.config(L1_ALT, "hello", "truffle"); builder.config(L1, "hello", "world"); PolyglotEngine vm = builder.build(); register(vm); PolyglotEngine.Language language1 = vm.getLanguages().get(L1); Ctx ctx2 = language1.getGlobalObject().as(Ctx.class); assertEquals("world", ctx2.env.getConfig().get("hello")); } @Test public void configIsNeverNull() { Builder builder = createBuilderInternal(); PolyglotEngine vm = builder.build(); register(vm); PolyglotEngine.Language language1 = vm.getLanguages().get(L1); Ctx ctx2 = language1.getGlobalObject().as(Ctx.class); assertNull(ctx2.env.getConfig().get("hello")); } static class YourLang { public static final String MIME_TYPE = L1; } @Test public void exampleOfConfiguration() { // @formatter:off String[] args = {"--kernel", "Kernel.som", "--instrument", "dyn-metrics"}; Builder builder = PolyglotEngine.newBuilder(); builder.config(YourLang.MIME_TYPE, "CMD_ARGS", args); PolyglotEngine vm = builder.build(); // @formatter:on try { PolyglotEngine.Language language1 = vm.getLanguages().get(L1); Ctx ctx2 = language1.getGlobalObject().as(Ctx.class); String[] read = (String[]) ctx2.env.getConfig().get("CMD_ARGS"); assertSame("The same array as specified is returned", args, read); } finally { vm.dispose(); } } @Test public void testCaching() { CachingLanguageChannel channel = new CachingLanguageChannel(); PolyglotEngine vm = register(createBuilder().config(CachingLanguage.MIME_TYPE, "channel", channel).build()); final Source source1 = Source.newBuilder("unboxed").name("something").mimeType(CachingLanguage.MIME_TYPE).build(); final Source source2 = Source.newBuilder("unboxed").name("something").mimeType(CachingLanguage.MIME_TYPE).build(); int cachedTargetsSize = -1; int interopTargetsSize = -1; // from now on we should not create any new targets for (int i = 0; i < 10; i++) { Value value1 = vm.eval(source1); Value value2 = vm.eval(source2); value1 = value1.execute().execute().execute().execute(); value2 = value2.execute().execute().execute().execute(); value1.get(); value2.get(); assertNotNull(value1.as(CachingTruffleObject.class)); assertNotNull(value2.as(CachingTruffleObject.class)); if (i == 0) { cachedTargetsSize = channel.parseTargets.size(); interopTargetsSize = channel.interopTargets.size(); // its fair to assume some call targets need to get created assertNotEquals(0, cachedTargetsSize); assertNotEquals(0, interopTargetsSize); } else { // we need to have stable call targets after the first run. assertEquals(cachedTargetsSize, channel.parseTargets.size()); assertEquals(interopTargetsSize, channel.interopTargets.size()); } } } @FunctionalInterface interface TestInterface { void foobar(); } interface ArrayLike { @MethodMessage(message = "WRITE") void set(int index, Object value); @MethodMessage(message = "READ") Object get(int index); @MethodMessage(message = "GET_SIZE") int size(); @MethodMessage(message = "HAS_SIZE") boolean isArray(); } @Test public void testCachingFailing() { CachingLanguageChannel channel = new CachingLanguageChannel(); PolyglotEngine vm = register(createBuilder().config(CachingLanguage.MIME_TYPE, "channel", channel).build()); final Source source1 = Source.newBuilder("boxed").name("something").mimeType(CachingLanguage.MIME_TYPE).build(); final Source source2 = Source.newBuilder("boxed").name("something").mimeType(CachingLanguage.MIME_TYPE).build(); int cachedTargetsSize = -1; int interopTargetsSize = -1; for (int i = 0; i < 10; i++) { Value value1 = vm.eval(source1); Value value2 = vm.eval(source2); TestInterface testInterface1 = value1.as(TestInterface.class); testInterface1.foobar(); value1.as(Byte.class); value1.as(Short.class); value1.as(Integer.class); value1.as(Long.class); value1.as(Float.class); value1.as(Double.class); Map<?, ?> m1 = value1.as(Map.class); assertTrue(m1.isEmpty()); List<?> l1 = value1.as(List.class); assertEquals(0, l1.size()); ArrayLike a1 = value1.as(ArrayLike.class); assertEquals(0, a1.size()); assertTrue(a1.isArray()); TestInterface testInterface2 = value2.as(TestInterface.class); testInterface2.foobar(); value2.as(Byte.class); value2.as(Short.class); value2.as(Integer.class); value2.as(Long.class); value2.as(Float.class); value2.as(Double.class); value2.as(Map.class); Map<?, ?> m2 = value2.as(Map.class); assertTrue(m2.isEmpty()); List<?> l2 = value2.as(List.class); assertEquals(0, l2.size()); ArrayLike a2 = value1.as(ArrayLike.class); assertEquals(0, a2.size()); assertTrue(a2.isArray()); if (i == 0) { // warmup cachedTargetsSize = channel.parseTargets.size(); interopTargetsSize = channel.interopTargets.size(); assertNotEquals(0, cachedTargetsSize); assertNotEquals(0, interopTargetsSize); channel.frozen = true; } else { // we need to have stable call targets after the first run. assertEquals(cachedTargetsSize, channel.parseTargets.size()); assertEquals(interopTargetsSize, channel.interopTargets.size()); } } } private static class CachingLanguageChannel { final List<CallTarget> parseTargets = new ArrayList<>(); final List<CallTarget> interopTargets = new ArrayList<>(); boolean frozen; } private static class CachingTruffleObject implements TruffleObject { private final CachingLanguageChannel channel; private boolean boxed; CachingTruffleObject(CachingLanguageChannel channel, boolean boxed) { this.channel = channel; this.boxed = boxed; } public ForeignAccess getForeignAccess() { return ForeignAccess.create(new ForeignAccess.Factory() { @Override public boolean canHandle(TruffleObject obj) { return true; } public CallTarget accessMessage(final Message tree) { RootNode root = new RootNode(null) { @Override public Object execute(VirtualFrame frame) { if (tree == Message.IS_BOXED) { return boxed; } else if (tree == Message.IS_EXECUTABLE) { return true; } else if (tree == Message.IS_NULL) { return false; } else if (tree == Message.HAS_SIZE) { return true; } else if (tree == Message.GET_SIZE) { return 0; } else if (tree == Message.KEYS) { return JavaInterop.asTruffleObject(Collections.emptyList()); } else if (tree == Message.UNBOX) { return 42; } return new CachingTruffleObject(channel, boxed); } }; CallTarget target = Truffle.getRuntime().createCallTarget(root); channel.interopTargets.add(target); if (channel.frozen) { throw new IllegalStateException("No new calltargets for " + tree); } return target; } }); } } @TruffleLanguage.Registration(mimeType = CachingLanguage.MIME_TYPE, version = "", name = "") public static final class CachingLanguage extends TruffleLanguage<CachingLanguageChannel> { static final String MIME_TYPE = "application/x-test-caching"; public CachingLanguage() { } @Override protected CachingLanguageChannel createContext(com.oracle.truffle.api.TruffleLanguage.Env env) { return (CachingLanguageChannel) env.getConfig().get("channel"); } @Override protected CallTarget parse(com.oracle.truffle.api.TruffleLanguage.ParsingRequest request) throws Exception { final boolean boxed = request.getSource().getCode().equals("boxed"); final CachingLanguageChannel channel = getContextReference().get(); RootNode root = new RootNode(this) { @Override public Object execute(VirtualFrame frame) { return new CachingTruffleObject(channel, boxed); } }; CallTarget target = Truffle.getRuntime().createCallTarget(root); channel.parseTargets.add(target); if (channel.frozen) { throw new IllegalStateException("No new calltargets"); } return target; } @Override protected Object findExportedSymbol(CachingLanguageChannel context, String globalName, boolean onlyExplicit) { return null; } @Override protected Object getLanguageGlobal(CachingLanguageChannel context) { return null; } @Override protected boolean isObjectOfLanguage(Object object) { return false; } } @Test public void languageInstancesAreNotShared() { ForkingLanguage.constructorInvocationCount = 0; final Builder builder = createBuilderInternal(); ForkingLanguageChannel channel1 = new ForkingLanguageChannel(builder::build); PolyglotEngine vm1 = register(builder.config(ForkingLanguage.MIME_TYPE, "channel", channel1).build()); register(vm1); vm1.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject(); assertEquals(1, ForkingLanguage.constructorInvocationCount); ForkingLanguageChannel channel2 = new ForkingLanguageChannel(builder::build); PolyglotEngine vm2 = register(createBuilder().config(ForkingLanguage.MIME_TYPE, "channel", channel2).build()); register(vm2); vm2.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject(); assertEquals(2, ForkingLanguage.constructorInvocationCount); assertNotSame(channel1.language, channel2.language); } @Test public void basicForkTest() throws Exception { final Builder builder = createBuilderInternal(); ForkingLanguageChannel channel = new ForkingLanguageChannel(builder::build); builder.config(ForkingLanguage.MIME_TYPE, "channel", channel); PolyglotEngine vm = register(builder.build()); PolyglotEngine uninitializedFork = builder.build(); // language is not yet initialized -> no fork necessary assertEquals(0, channel.forks.size()); assertEquals(channel.globalObject, vm.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject().as(String.class)); assertEquals(1, channel.language.createContextCount); assertEquals(0, channel.language.forkContextCount); assertEquals(0, channel.language.disposeContextCount); // unsure that the uninitialized fork creates its own context uninitializedFork.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject(); assertEquals(2, channel.language.createContextCount); assertEquals(0, channel.language.forkContextCount); assertEquals(0, channel.language.disposeContextCount); List<PolyglotEngine> forks = new ArrayList<>(); for (int i = 0; i < 5; i++) { forks.add(channel.fork()); assertEquals(channel.forks.size(), forks.size()); assertEquals(2, channel.language.createContextCount); assertEquals(i + 1, channel.language.forkContextCount); assertEquals(0, channel.language.disposeContextCount); assertEquals(channel.forks.get(i).globalObject, forks.get(i).getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject().as(String.class)); } for (ForkingLanguageChannel forkChannel : channel.forks) { // the language instance is shared across all languages. assertSame(channel.language, forkChannel.language); } int forksLeft = forks.size(); for (PolyglotEngine fork : forks) { // test we can still safely access the global object fork.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject(); fork.dispose(); forksLeft--; assertEquals(channel.forks.size(), forksLeft); // test we can still safely access the global object of the origin vm vm.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject(); assertEquals(2, channel.language.createContextCount); assertEquals(5, channel.language.forkContextCount); assertEquals(forks.indexOf(fork) + 1, channel.language.disposeContextCount); } } @Test(expected = UnsupportedOperationException.class) public void forkUnsupportedFailsGracefully() throws Exception { final Builder builder = createBuilderInternal(); ForkingLanguageChannel channel = new ForkingLanguageChannel(builder::build); builder.config(ForkingLanguage.MIME_TYPE, "channel", channel); PolyglotEngine vm = register(builder.build()); // fork supported when not initialized try { assertNotNull(channel.fork()); } catch (Exception e) { fail(); } vm.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject(); channel.language.forkSupported = false; channel.fork(); } @Test public void forkedSymbolsNotSharedButCopied() throws Exception { final Builder builder = createBuilderInternal(); ForkingLanguageChannel channel = new ForkingLanguageChannel(builder::build); builder.config(ForkingLanguage.MIME_TYPE, "channel", channel); PolyglotEngine vm = register(builder.build()); channel.symbols.put("sym1", "symvalue1"); vm.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject(); // initialize language assertEquals("symvalue1", vm.findGlobalSymbol("sym1").as(String.class)); PolyglotEngine fork = channel.fork(); assertEquals("symvalue1", vm.findGlobalSymbol("sym1").as(String.class)); assertEquals("symvalue1", fork.findGlobalSymbol("sym1").as(String.class)); final ForkingLanguageChannel forkChannel = channel.forks.get(0); forkChannel.symbols.put("sym2", "symvalue2"); assertNull(vm.findGlobalSymbol("sym2")); assertEquals("symvalue2", fork.findGlobalSymbol("sym2").as(String.class)); channel.symbols.put("sym2", "symvalue3"); assertEquals("symvalue3", vm.findGlobalSymbol("sym2").as(String.class)); assertEquals("symvalue2", fork.findGlobalSymbol("sym2").as(String.class)); assertEquals("symvalue1", vm.findGlobalSymbol("sym1").as(String.class)); assertEquals("symvalue1", fork.findGlobalSymbol("sym1").as(String.class)); PolyglotEngine forkfork = forkChannel.fork(); assertEquals("symvalue1", forkfork.findGlobalSymbol("sym1").as(String.class)); assertEquals("symvalue2", forkfork.findGlobalSymbol("sym2").as(String.class)); } @Test public void forkInLanguageTest() { final Builder builder = createBuilderInternal(); ForkingLanguageChannel channel = new ForkingLanguageChannel(builder::build); PolyglotEngine vm = builder.config(ForkingLanguage.MIME_TYPE, "channel", channel).build(); vm.eval(Source.newBuilder("").name("").mimeType(ForkingLanguage.MIME_TYPE).build()).get(); assertEquals(1, channel.languageForks.size()); assertFalse(channel.languageForks.get(0).disposed); vm.dispose(); // make sure language forks are disposed with the engine that created it. assertTrue(channel.languageForks.get(0).disposed); } @Test public void testLanguageInfo() { final Builder builder = createBuilderInternal(); ForkingLanguageChannel channel = new ForkingLanguageChannel(builder::build); PolyglotEngine vm = builder.config(ForkingLanguage.MIME_TYPE, "channel", channel).build(); vm.eval(Source.newBuilder("").name("").mimeType(ForkingLanguage.MIME_TYPE).build()).get(); assertNotNull(channel.info); assertEquals(1, channel.info.getMimeTypes().size()); assertTrue(channel.info.getMimeTypes().contains(ForkingLanguage.MIME_TYPE)); assertEquals("forkinglanguage", channel.info.getName()); assertEquals("version", channel.info.getVersion()); } @Test public void testLanguageAccess() { final Builder builder = createBuilderInternal(); ForkingLanguageChannel channel = new ForkingLanguageChannel(builder::build); PolyglotEngine vm = builder.config(ForkingLanguage.MIME_TYPE, "channel", channel).build(); vm.eval(Source.newBuilder("").name("").mimeType(ForkingLanguage.MIME_TYPE).build()).get(); RootNode root = new RootNode(channel.language) { @Override public Object execute(VirtualFrame frame) { return null; } }; try { // no access using a TruffleLanguage hack root.getLanguage(TruffleLanguage.class); fail(); } catch (ClassCastException e) { } Class<?> oClass = Object.class; @SuppressWarnings({"rawtypes", "unchecked"}) Class<? extends TruffleLanguage> lang = (Class<? extends TruffleLanguage>) oClass; try { // no access using a TruffleLanguage class cast root.getLanguage(lang); fail(); } catch (ClassCastException e) { } oClass = SecretInterfaceType.class; @SuppressWarnings({"rawtypes", "unchecked"}) Class<? extends TruffleLanguage> secretInterface = (Class<? extends TruffleLanguage>) oClass; try { // no access using secret interface root.getLanguage(secretInterface); fail(); } catch (ClassCastException e) { } // this should work as expected assertNotNull(root.getLanguage(ForkingLanguage.class)); } interface SecretInterfaceType { } private static class ForkingLanguageChannel implements TruffleObject { ForkingLanguage language; private static int globalIndex = 0; final String globalObject = "global" + globalIndex++; final ForkingLanguageChannel parent; final Map<String, Object> symbols = new HashMap<>(); final List<ForkingLanguageChannel> forks = new ArrayList<>(); final List<ForkingLanguageChannel> languageForks = new ArrayList<>(); final List<PolyglotEngine> dispose = new ArrayList<>(); LanguageInfo info; boolean disposed; ForkingLanguageChannel toFork; Callable<PolyglotEngine> toCreate; ForkingLanguageChannel(Callable<PolyglotEngine> toCreate) { this((ForkingLanguageChannel) null); this.toCreate = toCreate; } ForkingLanguageChannel(ForkingLanguageChannel parent) { this.parent = parent; if (parent != null) { this.symbols.putAll(parent.symbols); } this.symbols.put("thisContext", this); } PolyglotEngine fork() { ForkingLanguageChannel channel = this; while (channel.parent != null) { channel = channel.parent; } channel.toFork = this; PolyglotEngine fork; try { fork = channel.toCreate.call(); } catch (Exception ex) { throw raise(RuntimeException.class, ex); } fork.getLanguages().get(ForkingLanguage.MIME_TYPE).getGlobalObject(); assertNull("The toFork channel was used", channel.toFork); dispose.add(fork); return fork; } @SuppressWarnings("unchecked") private static <E extends Exception> E raise(@SuppressWarnings("unused") Class<E> aClass, Exception ex) throws E { throw (E) ex; } @Override public ForeignAccess getForeignAccess() { return null; } } @TruffleLanguage.Registration(mimeType = ForkingLanguage.MIME_TYPE, version = "version", name = "forkinglanguage") public static final class ForkingLanguage extends TruffleLanguage<ForkingLanguageChannel> implements SecretInterfaceType { static final String MIME_TYPE = "application/x-test-forking"; static int constructorInvocationCount; int createContextCount = 0; int disposeContextCount = 0; int forkContextCount = 0; boolean forkSupported = true; public ForkingLanguage() { constructorInvocationCount++; } @Override protected ForkingLanguageChannel createContext(com.oracle.truffle.api.TruffleLanguage.Env env) { ForkingLanguageChannel channel = (ForkingLanguageChannel) env.getConfig().get("channel"); if (channel.toFork != null) { ForkingLanguageChannel forking = channel.toFork; channel.toFork = null; return forkContext(forking); } createContextCount++; channel.language = this; channel.info = new RootNode(this) { @Override public Object execute(VirtualFrame frame) { return null; } }.getLanguageInfo(); return channel; } protected ForkingLanguageChannel forkContext(ForkingLanguageChannel context) { forkContextCount++; if (!forkSupported) { throw new UnsupportedOperationException(); } ForkingLanguageChannel channel = new ForkingLanguageChannel(context); channel.language = this; context.forks.add(channel); return channel; } @Override protected void disposeContext(ForkingLanguageChannel context) { disposeContextCount++; context.disposed = true; if (context.parent != null) { context.parent.forks.remove(context); } for (PolyglotEngine eng : context.dispose) { try { eng.dispose(); } catch (IllegalStateException ex) { // ignore } } } @Override protected CallTarget parse(com.oracle.truffle.api.TruffleLanguage.ParsingRequest request) throws Exception { return Truffle.getRuntime().createCallTarget(new RootNode(this) { boolean initialized; @Override public Object execute(VirtualFrame frame) { if (!initialized) { initialized = true; int prevForkContextCount = forkContextCount; final ForkingLanguageChannel myContext = getContextReference().get(); PolyglotEngine eng = myContext.fork(); assertEquals(prevForkContextCount + 1, forkContextCount); ForkingLanguageChannel forkedContext = eng.findGlobalSymbol("thisContext").as(ForkingLanguageChannel.class); getContextReference().get().languageForks.add(forkedContext); assertEquals(getContextReference().get(), forkedContext.parent); } int prevForkContextCount = forkContextCount; final ForkingLanguageChannel myContext = getContextReference().get(); PolyglotEngine fork = myContext.fork(); assertEquals(prevForkContextCount + 1, forkContextCount); int prevDisposeCount = disposeContextCount; fork.dispose(); assertEquals(prevDisposeCount + 1, disposeContextCount); return null; } }); } @Override protected Object findExportedSymbol(ForkingLanguageChannel context, String globalName, boolean onlyExplicit) { return context.symbols.get(globalName); } @Override protected Object getLanguageGlobal(ForkingLanguageChannel context) { return context.globalObject; } @Override protected boolean isObjectOfLanguage(Object object) { return false; } } }