diff --git a/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/TruffleRLanguage.java b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/TruffleRLanguage.java index 6c98e3dd2b977f0b9916370373d985a7c723cf7b..7b91b6f22401363b844fb8316586e10d63202c20 100644 --- a/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/TruffleRLanguage.java +++ b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/TruffleRLanguage.java @@ -28,11 +28,13 @@ import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerAsserts; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.TruffleLanguage; +import com.oracle.truffle.api.frame.Frame; import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.instrumentation.Instrumenter; import com.oracle.truffle.api.instrumentation.ProvidedTags; import com.oracle.truffle.api.instrumentation.StandardTags; +import com.oracle.truffle.api.metadata.ScopeProvider; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.source.SourceSection; @@ -54,6 +56,7 @@ import com.oracle.truffle.r.runtime.context.RContext.RCloseable; import com.oracle.truffle.r.runtime.data.RFunction; import com.oracle.truffle.r.runtime.data.RPromise; import com.oracle.truffle.r.runtime.data.RTypedValue; +import com.oracle.truffle.r.runtime.env.RScope; import com.oracle.truffle.r.runtime.ffi.RFFIFactory; import com.oracle.truffle.r.runtime.nodes.RBaseNode; @@ -63,7 +66,7 @@ import com.oracle.truffle.r.runtime.nodes.RBaseNode; */ @TruffleLanguage.Registration(name = "R", version = "0.1", mimeType = {RRuntime.R_APP_MIME, RRuntime.R_TEXT_MIME}, interactive = true) @ProvidedTags({StandardTags.CallTag.class, StandardTags.StatementTag.class, StandardTags.RootTag.class, RSyntaxTags.LoopTag.class}) -public final class TruffleRLanguage extends TruffleLanguage<RContext> { +public final class TruffleRLanguage extends TruffleLanguage<RContext> implements ScopeProvider<RContext> { /** * The choice of {@link RFFIFactory} is made statically so that it is bound into an AOT-compiled @@ -242,4 +245,9 @@ public final class TruffleRLanguage extends TruffleLanguage<RContext> { public RContext actuallyFindContext0(Node contextNode) { return findContext(contextNode); } + + @Override + public AbstractScope findScope(RContext langContext, Node node, Frame frame) { + return RScope.createScope(node, frame); + } } diff --git a/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/env/RScope.java b/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/env/RScope.java new file mode 100644 index 0000000000000000000000000000000000000000..ced4f106a5be00264f3977fb9608df5ea6e5b75e --- /dev/null +++ b/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/env/RScope.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2014, 2017, 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.r.runtime.env; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.Frame; +import com.oracle.truffle.api.interop.ForeignAccess; +import com.oracle.truffle.api.interop.Message; +import com.oracle.truffle.api.interop.MessageResolution; +import com.oracle.truffle.api.interop.Resolve; +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.metadata.ScopeProvider.AbstractScope; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.r.runtime.ArgumentsSignature; +import com.oracle.truffle.r.runtime.RArguments; +import com.oracle.truffle.r.runtime.RInternalError; +import com.oracle.truffle.r.runtime.data.RFunction; +import com.oracle.truffle.r.runtime.data.RStringVector; +import com.oracle.truffle.r.runtime.env.REnvironment.PutException; + +/** + * Represents a variable scope for external tools like a debugger.<br> + * This is basically a view on R environments. + */ +public final class RScope extends AbstractScope { + + private final Node current; + private REnvironment env; + + /** + * Intended to be used when creating a parent scope where we do not know any associated node. + */ + private RScope(REnvironment env) { + this.env = env; + this.current = null; + } + + private RScope(Node current, REnvironment env) { + this.current = current; + this.env = env; + } + + @Override + protected String getName() { + // TODO promises (= closure) + return "function"; + } + + @Override + protected Node getNode() { + return current; + } + + @Override + protected Object getVariables(Frame frame) { + return new VariablesMapObject(env, false); + } + + private static REnvironment getEnv(Frame frame) { + assert RArguments.isRFrame(frame); + return REnvironment.frameToEnvironment(frame.materialize()); + } + + @Override + protected Object getArguments(Frame frame) { + return new VariablesMapObject(env, true); + } + + @Override + protected AbstractScope findParent() { + if (this.env == REnvironment.emptyEnv()) { + return null; + } + + return new RScope(env.getParent()); + } + + private static String[] ls(REnvironment env) { + RStringVector ls = env.ls(true, null, false); + return ls.getDataWithoutCopying(); + } + + private static String[] collectArgs(REnvironment env) { + ArgumentsSignature signature = RArguments.getSignature(env.getFrame()); + return signature.getNames(); + } + + public static RScope createScope(Node node, Frame frame) { + return new RScope(node.getRootNode(), getEnv(frame)); + } + + private static Object getInteropValue(Object value) { + return value; + } + + static final class VariablesMapObject implements TruffleObject { + + private final REnvironment env; + private final boolean argumentsOnly; + + private VariablesMapObject(REnvironment env, boolean argumentsOnly) { + this.env = env; + this.argumentsOnly = argumentsOnly; + } + + @Override + public ForeignAccess getForeignAccess() { + return VariablesMapMessageResolutionForeign.ACCESS; + } + + public static boolean isInstance(TruffleObject obj) { + return obj instanceof VariablesMapObject; + } + + @MessageResolution(receiverType = VariablesMapObject.class) + static final class VariablesMapMessageResolution { + + @Resolve(message = "KEYS") + abstract static class VarsMapKeysNode extends Node { + + @TruffleBoundary + public Object access(VariablesMapObject varMap) { + if (varMap.argumentsOnly) { + return new ArgumentNamesObject(collectArgs(varMap.env)); + } else { + return new VariableNamesObject(varMap.env); + } + } + } + + @Resolve(message = "KEY_INFO") + public abstract static class VarMapsKeyInfoNode extends Node { + + private static final int READABLE = 1 << 1; + private static final int WRITABLE = 1 << 2; + private static final int INVOCABLE = 1 << 3; + + @SuppressWarnings("try") + protected Object access(VariablesMapObject receiver, String identifier) { + int info = READABLE; + + if (!receiver.env.bindingIsLocked(identifier)) { + info += WRITABLE; + } + if (receiver.env.get(identifier) instanceof RFunction) { + info += INVOCABLE; + } + return info; + } + } + + @Resolve(message = "READ") + abstract static class VarsMapReadNode extends Node { + + @TruffleBoundary + public Object access(VariablesMapObject varMap, String name) { + if (varMap.env == null) { + throw UnsupportedMessageException.raise(Message.READ); + } + Object value = varMap.env.get(name); + + // If Java-null is returned, the identifier does not exist ! + if (value == null) { + throw UnknownIdentifierException.raise(name); + } else { + return getInteropValue(value); + } + } + } + + @Resolve(message = "WRITE") + abstract static class VarsMapWriteNode extends Node { + + @TruffleBoundary + public Object access(VariablesMapObject varMap, String name, Object value) { + if (varMap.env == null) { + throw UnsupportedMessageException.raise(Message.WRITE); + } + try { + varMap.env.put(name, value); + return value; + } catch (PutException e) { + throw RInternalError.shouldNotReachHere(e); + } + } + } + + } + } + + static final class VariableNamesObject implements TruffleObject { + + private final REnvironment env; + + private VariableNamesObject(REnvironment env) { + this.env = env; + } + + @Override + public ForeignAccess getForeignAccess() { + return VariableNamesMessageResolutionForeign.ACCESS; + } + + public static boolean isInstance(TruffleObject obj) { + return obj instanceof VariableNamesObject; + } + + @MessageResolution(receiverType = VariableNamesObject.class) + static final class VariableNamesMessageResolution { + + @Resolve(message = "HAS_SIZE") + abstract static class VarNamesHasSizeNode extends Node { + + @SuppressWarnings("unused") + public Object access(VariableNamesObject varNames) { + return true; + } + } + + @Resolve(message = "GET_SIZE") + abstract static class VarNamesGetSizeNode extends Node { + + public Object access(VariableNamesObject varNames) { + return ls(varNames.env).length; + } + } + + @Resolve(message = "READ") + abstract static class VarNamesReadNode extends Node { + + @TruffleBoundary + public Object access(VariableNamesObject varNames, int index) { + String[] names = ls(varNames.env); + if (index >= 0 && index < names.length) { + return names[index]; + } else { + throw UnknownIdentifierException.raise(Integer.toString(index)); + } + } + } + + } + } + + static final class ArgumentNamesObject implements TruffleObject { + + private final String[] names; + + private ArgumentNamesObject(String[] names) { + this.names = names; + } + + @Override + public ForeignAccess getForeignAccess() { + return VariableNamesMessageResolutionForeign.ACCESS; + } + + public static boolean isInstance(TruffleObject obj) { + return obj instanceof ArgumentNamesObject; + } + + @MessageResolution(receiverType = ArgumentNamesObject.class) + static final class ArgumentNamesMessageResolution { + + @Resolve(message = "HAS_SIZE") + abstract static class ArgNamesHasSizeNode extends Node { + + @SuppressWarnings("unused") + public Object access(ArgumentNamesObject varNames) { + return true; + } + } + + @Resolve(message = "GET_SIZE") + abstract static class ArgNamesGetSizeNode extends Node { + + public Object access(ArgumentNamesObject varNames) { + return varNames.names.length; + } + } + + @Resolve(message = "READ") + abstract static class ArgNamesReadNode extends Node { + + @TruffleBoundary + public Object access(ArgumentNamesObject varNames, int index) { + if (index >= 0 && index < varNames.names.length) { + return varNames.names[index]; + } else { + throw UnknownIdentifierException.raise(Integer.toString(index)); + } + } + } + + } + } + +} diff --git a/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/tck/FastRDebugTest.java b/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/tck/FastRDebugTest.java index 5edb60b06faa3bfaa9edbc2bdcaaa386137adcee..cf4d6207f54c143c1a15b360595f851e546d86c9 100644 --- a/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/tck/FastRDebugTest.java +++ b/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/tck/FastRDebugTest.java @@ -29,7 +29,9 @@ import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.HashSet; import java.util.LinkedList; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; @@ -38,6 +40,7 @@ import org.junit.Before; import org.junit.Test; import com.oracle.truffle.api.debug.Breakpoint; +import com.oracle.truffle.api.debug.DebugScope; import com.oracle.truffle.api.debug.DebugStackFrame; import com.oracle.truffle.api.debug.DebugValue; import com.oracle.truffle.api.debug.Debugger; @@ -54,6 +57,7 @@ import com.oracle.truffle.r.runtime.context.ContextInfo; import com.oracle.truffle.r.runtime.context.DefaultConsoleHandler; import com.oracle.truffle.r.runtime.context.RContext.ContextKind; import com.oracle.truffle.r.runtime.data.RPromise.EagerPromise; +import com.oracle.truffle.r.runtime.env.REnvironment; import com.oracle.truffle.r.runtime.env.frame.RFrameSlot; public class FastRDebugTest { @@ -230,6 +234,136 @@ public class FastRDebugTest { assertExecutedOK(); } + @Test + public void testScopeFunction() throws Throwable { + final Source srcFunMain = RSource.fromTextInternal("function () {\n" + + " i = 3L\n" + + " n = 15L\n" + + " str = \"hello\"\n" + + " i <- i + 1L\n" + + " ab <<- i\n" + + " i\n" + + "}", RSource.Internal.DEBUGTEST_DEBUG); + final Source source = RSource.fromTextInternal("x <- 10L\n" + + "makeActiveBinding('ab', function(v) { if(missing(v)) x else x <<- v }, .GlobalEnv)\n" + + "main <- " + srcFunMain.getCode() + "\n", + RSource.Internal.DEBUGTEST_DEBUG); + engine.eval(source); + + // @formatter:on + run.addLast(() -> { + assertNull(suspendedEvent); + assertNotNull(debuggerSession); + debuggerSession.suspendNextExecution(); + }); + + assertLocation(1, "main()", "x", 10, "ab", 10, "main", srcFunMain.getCode()); + stepInto(1); + assertLocation(4, "i = 3L"); + stepOver(1); + assertLocation(5, "n = 15L", "i", 3); + stepOver(1); + assertLocation(6, "str = \"hello\"", "i", 3, "n", 15); + stepOver(1); + assertLocation(7, "i <- i + 1L", "i", 3, "n", 15, "str", "hello"); + stepOver(1); + assertLocation(8, "ab <<- i", "i", 4, "n", 15, "str", "hello"); + stepOver(1); + assertScope(9, "i", true, false, "ab", 4, "x", 4); + stepOut(); + assertLocation(1, "main()", "x", 4, "ab", 4, "main", srcFunMain.getCode()); + performWork(); + + final Source evalSource = RSource.fromTextInternal("main()\n", RSource.Internal.DEBUGTEST_EVAL); + engine.eval(evalSource); + + assertExecutedOK(); + } + + @Test + public void testScopePromise() throws Throwable { + final Source source = RSource.fromTextInternal("main <- function(e) {\n" + + " x <- 10L\n" + + " e()\n" + + " x\n" + + "}\n" + + "closure <- function() {\n" + + " x <<- 123L\n" + + " x\n" + + "}\n", + + RSource.Internal.DEBUGTEST_DEBUG); + engine.eval(source); + + // @formatter:on + run.addLast(() -> { + assertNull(suspendedEvent); + assertNotNull(debuggerSession); + debuggerSession.suspendNextExecution(); + }); + + stepOver(1); + stepInto(1); + stepOver(1); + assertScope(3, "e()", false, false, "x", 10); + stepInto(1); + assertLocation(7, "x <<- 123L"); + assertScope(7, "x <<- 123L", true, false, "x", 0); + stepOver(1); + assertScope(8, "x", true, false, "x", 123); + continueExecution(); + performWork(); + + final Source evalSource = RSource.fromTextInternal("x <- 0L\nmain(closure)\n", RSource.Internal.DEBUGTEST_EVAL); + engine.eval(evalSource); + + assertExecutedOK(); + } + + @Test + public void testChangedScopeChain() throws Throwable { + final Source source = RSource.fromTextInternal("main <- function(e) {\n" + + " x <- 10L\n" + + " environment(e) <- environment()\n" + + " e()\n" + + " x\n" + + "}\n" + + "closure <- function() {\n" + + " x <<- 123L\n" + + " x\n" + + "}\n", + RSource.Internal.DEBUGTEST_DEBUG); + engine.eval(source); + + // @formatter:on + run.addLast(() -> { + assertNull(suspendedEvent); + assertNotNull(debuggerSession); + debuggerSession.suspendNextExecution(); + }); + + stepOver(1); + stepInto(1); + stepOver(2); + assertScope(4, "e()", false, false, "x", 10); + stepInto(1); + assertLocation(8, "x <<- 123L"); + assertScope(8, "x <<- 123L", true, false, "x", 10); + stepOver(1); + stepOut(); + assertScope(9, "x", false, false, "x", 123); + assertIdentifiers(false, "x", "e"); + stepOut(); + assertScope(9, "x", false, false, "x", 0); + continueExecution(); + performWork(); + + final Source evalSource = RSource.fromTextInternal("x <- 0L\nmain(closure)\n", RSource.Internal.DEBUGTEST_EVAL); + engine.eval(evalSource); + + assertExecutedOK(); + } + private void performWork() { try { if (ex == null && !run.isEmpty()) { @@ -246,7 +380,7 @@ public class FastRDebugTest { } private void stepOut() { - run.addLast(() -> suspendedEvent.prepareStepOut()); + run.addLast(() -> suspendedEvent.prepareStepOut(1)); } private void continueExecution() { @@ -257,6 +391,30 @@ public class FastRDebugTest { run.addLast(() -> suspendedEvent.prepareStepInto(size)); } + private void assertIdentifiers(boolean includeAncestors, String... identifiers) { + run.addLast(() -> { + + final DebugStackFrame frame = suspendedEvent.getTopStackFrame(); + DebugScope scope = frame.getScope(); + + Set<String> actualIdentifiers = new HashSet<>(); + do { + scope.getDeclaredValues().forEach((x) -> actualIdentifiers.add(x.getName())); + } while (includeAncestors && scope != null && !REnvironment.baseEnv().getName().equals(scope.getName())); + + Set<String> expected = new HashSet<>(); + for (String s : identifiers) { + expected.add(s); + } + + assertEquals(expected, actualIdentifiers); + + if (!run.isEmpty()) { + run.removeFirst().run(); + } + }); + } + private void assertLocation(final int line, final String code, final Object... expectedFrame) { run.addLast(() -> { try { @@ -265,31 +423,33 @@ public class FastRDebugTest { assertEquals(line, currentLine); final String currentCode = suspendedEvent.getSourceSection().getCode().trim(); assertEquals(code, currentCode); - final DebugStackFrame frame = suspendedEvent.getTopStackFrame(); + compareScope(line, code, false, true, expectedFrame); + } catch (RuntimeException | Error e) { - final AtomicInteger numFrameVars = new AtomicInteger(0); + final DebugStackFrame frame = suspendedEvent.getTopStackFrame(); frame.forEach(var -> { - // skip synthetic slots - for (RFrameSlot slot : RFrameSlot.values()) { - if (slot.toString().equals(var.getName())) { - return; - } - } - numFrameVars.incrementAndGet(); + System.out.println(var); }); - assertEquals(line + ": " + code, expectedFrame.length / 2, numFrameVars.get()); - - for (int i = 0; i < expectedFrame.length; i = i + 2) { - String expectedIdentifier = (String) expectedFrame[i]; - Object expectedValue = expectedFrame[i + 1]; - String expectedValueStr = (expectedValue != null) ? expectedValue.toString() : null; - DebugValue value = frame.getValue(expectedIdentifier); - assertNotNull(value); - String valueStr = value.as(String.class); - assertEquals(expectedValueStr, valueStr); - } + throw e; + } + }); + } - run.removeFirst().run(); + /** + * Ensure that the scope at a certain program position contains an expected set of key-value + * pairs. + * + * @param line line number + * @param code the code snippet of the program location + * @param includeAncestors Include current scope's ancestors for the identifier lookup. + * @param completeMatch {@code true} if the defined key-value pairs should be the only pairs in + * the scope. + * @param expectedFrame the key-value pairs (e.g. {@code "id0", 1, "id1", "strValue"}) + */ + private void assertScope(final int line, final String code, boolean includeAncestors, boolean completeMatch, final Object... expectedFrame) { + run.addLast(() -> { + try { + compareScope(line, code, includeAncestors, completeMatch, expectedFrame); } catch (RuntimeException | Error e) { final DebugStackFrame frame = suspendedEvent.getTopStackFrame(); @@ -301,6 +461,43 @@ public class FastRDebugTest { }); } + private void compareScope(final int line, final String code, boolean includeAncestors, boolean completeMatch, final Object[] expectedFrame) { + final DebugStackFrame frame = suspendedEvent.getTopStackFrame(); + + final AtomicInteger numFrameVars = new AtomicInteger(0); + frame.forEach(var -> { + // skip synthetic slots + for (RFrameSlot slot : RFrameSlot.values()) { + if (slot.toString().equals(var.getName())) { + return; + } + } + numFrameVars.incrementAndGet(); + }); + if (completeMatch) { + assertEquals(line + ": " + code, expectedFrame.length / 2, numFrameVars.get()); + } + + for (int i = 0; i < expectedFrame.length; i = i + 2) { + String expectedIdentifier = (String) expectedFrame[i]; + Object expectedValue = expectedFrame[i + 1]; + String expectedValueStr = (expectedValue != null) ? expectedValue.toString() : null; + DebugScope scope = frame.getScope(); + DebugValue value; + do { + value = scope.getDeclaredValue(expectedIdentifier); + scope = scope.getParent(); + } while (includeAncestors && value == null && scope != null && !REnvironment.baseEnv().getName().equals(scope.getName())); + assertNotNull("identifier \"" + expectedIdentifier + "\" not found", value); + String valueStr = value.as(String.class); + assertEquals(expectedValueStr, valueStr); + } + + if (!run.isEmpty()) { + run.removeFirst().run(); + } + } + Object getRValue(Object value) { // This will only work in simple cases if (value instanceof EagerPromise) { diff --git a/mx.fastr/suite.py b/mx.fastr/suite.py index b915323c9f59225b929f763f466f1f47938b95e8..31410e15f94313f2528981b781c72c0cb33f5c0e 100644 --- a/mx.fastr/suite.py +++ b/mx.fastr/suite.py @@ -29,7 +29,7 @@ suite = { { "name" : "truffle", "subdir" : True, - "version" : "7d960d6682ef3636cc7455e28132c1e537ea81f0", + "version" : "acbe9ec935090e0824372e508563c122b0e46682", "urls" : [ {"url" : "https://github.com/graalvm/graal", "kind" : "git"}, {"url" : "https://curio.ssw.jku.at/nexus/content/repositories/snapshots", "kind" : "binary"},