changeset 22449:5df298d7c949

Truffle/Debugging: add change missing from previous fix concerning "pending" breakpoints
author Michael Van De Vanter <michael.van.de.vanter@oracle.com>
date Wed, 04 Nov 2015 16:35:38 -0800
parents 1c3deda60a9e
children fa1061fb21fa
files truffle/com.oracle.truffle.tools.debug.shell/src/com/oracle/truffle/tools/debug/shell/server/REPLServer.java
diffstat 1 files changed, 533 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/truffle/com.oracle.truffle.tools.debug.shell/src/com/oracle/truffle/tools/debug/shell/server/REPLServer.java	Wed Nov 04 16:35:38 2015 -0800
@@ -0,0 +1,533 @@
+/*
+ * Copyright (c) 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.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.tools.debug.shell.server;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import com.oracle.truffle.api.debug.Breakpoint;
+import com.oracle.truffle.api.debug.Debugger;
+import com.oracle.truffle.api.debug.ExecutionEvent;
+import com.oracle.truffle.api.debug.LineBreakpoint;
+import com.oracle.truffle.api.debug.SuspendedEvent;
+import com.oracle.truffle.api.debug.TagBreakpoint;
+import com.oracle.truffle.api.frame.FrameInstance;
+import com.oracle.truffle.api.frame.MaterializedFrame;
+import com.oracle.truffle.api.instrument.StandardSyntaxTag;
+import com.oracle.truffle.api.instrument.SyntaxTag;
+import com.oracle.truffle.api.instrument.Visualizer;
+import com.oracle.truffle.api.instrument.impl.DefaultVisualizer;
+import com.oracle.truffle.api.nodes.Node;
+import com.oracle.truffle.api.source.LineLocation;
+import com.oracle.truffle.api.source.Source;
+import com.oracle.truffle.api.source.SourceSection;
+import com.oracle.truffle.api.vm.EventConsumer;
+import com.oracle.truffle.api.vm.PolyglotEngine;
+import com.oracle.truffle.api.vm.PolyglotEngine.Language;
+import com.oracle.truffle.api.vm.PolyglotEngine.Value;
+import com.oracle.truffle.tools.debug.shell.REPLMessage;
+import com.oracle.truffle.tools.debug.shell.client.SimpleREPLClient;
+
+/**
+ * The server side of a simple message-based protocol for a possibly remote language
+ * Read-Eval-Print-Loop.
+ */
+public final class REPLServer {
+
+    // Language-agnostic
+    private final PolyglotEngine engine;
+    private Debugger db;
+    private Context currentServerContext;
+    private SimpleREPLClient replClient = null;
+    private String statusPrefix;
+    private final Map<String, REPLHandler> handlerMap = new HashMap<>();
+
+    // TODO (mlvdv) Language-specific
+    private final PolyglotEngine.Language language;
+    private final Visualizer visualizer;
+
+    private int nextBreakpointUID = 0;
+
+    /**
+     * Map: breakpoints => breakpoint UID.
+     * <ul>
+     * <li>Contains only pending breakpoints when the Debugger not yet available.</li>
+     * <li>When the Debugger becomes available, each pending breakpoint is replaced with a Debugger
+     * breakpoint, keeping same UID.</li>
+     * <li>Contains no disposed breakpoints.</li>
+     * <li>UIDs for disposed breakpoints are never reused.</li>
+     * </ul>
+     */
+    private Map<Breakpoint, Integer> breakpoints = new WeakHashMap<>();
+
+    /**
+     * Create a single-language server.
+     */
+    public REPLServer(String mimeType, Visualizer visualizer) {
+        this.visualizer = visualizer == null ? new DefaultVisualizer() : visualizer;
+        EventConsumer<SuspendedEvent> onHalted = new EventConsumer<SuspendedEvent>(SuspendedEvent.class) {
+            @Override
+            protected void on(SuspendedEvent ev) {
+                REPLServer.this.haltedAt(ev);
+            }
+        };
+        EventConsumer<ExecutionEvent> onExec = new EventConsumer<ExecutionEvent>(ExecutionEvent.class) {
+            @Override
+            protected void on(ExecutionEvent event) {
+                if (db == null) {
+                    db = event.getDebugger();
+                    if (!breakpoints.isEmpty()) {
+                        ArrayList<? extends Breakpoint> pendingBreakpoints = new ArrayList<>(breakpoints.keySet());
+                        try {
+                            for (Breakpoint pending : pendingBreakpoints) {
+
+                                Integer uid = breakpoints.get(pending);
+                                pending.dispose();
+                                Breakpoint replacement = null;
+                                if (pending instanceof PendingLineBreakpoint) {
+                                    final PendingLineBreakpoint lineBreak = (PendingLineBreakpoint) pending;
+                                    replacement = db.setLineBreakpoint(lineBreak.getIgnoreCount(), lineBreak.getLineLocation(), lineBreak.isOneShot());
+
+                                } else if (pending instanceof PendingTagBreakpoint) {
+                                    final PendingTagBreakpoint tagBreak = (PendingTagBreakpoint) pending;
+                                    replacement = db.setTagBreakpoint(tagBreak.getIgnoreCount(), tagBreak.getTag(), tagBreak.isOneShot());
+                                }
+                                breakpoints.put(replacement, uid);
+                            }
+                        } catch (IOException e) {
+                            throw new IllegalStateException("pending breakpoints should all be valid");
+                        }
+                    }
+                }
+                event.prepareStepInto();
+            }
+        };
+        engine = PolyglotEngine.buildNew().onEvent(onHalted).onEvent(onExec).build();
+        this.language = engine.getLanguages().get(mimeType);
+        if (language == null) {
+            throw new RuntimeException("Implementation not found for \"" + mimeType + "\"");
+        }
+        statusPrefix = languageName(language);
+    }
+
+    public void add(REPLHandler handler) {
+        handlerMap.put(handler.getOp(), handler);
+    }
+
+    /**
+     * Starts up a server; status returned in a message.
+     */
+    public void start() {
+
+        addHandlers();
+        this.replClient = new SimpleREPLClient(this);
+        this.currentServerContext = new Context(null, null);
+        replClient.start();
+    }
+
+    protected void addHandlers() {
+        add(REPLHandler.BACKTRACE_HANDLER);
+        add(REPLHandler.BREAK_AT_LINE_HANDLER);
+        add(REPLHandler.BREAK_AT_LINE_ONCE_HANDLER);
+        add(REPLHandler.BREAK_AT_THROW_HANDLER);
+        add(REPLHandler.BREAK_AT_THROW_ONCE_HANDLER);
+        add(REPLHandler.BREAKPOINT_INFO_HANDLER);
+        add(REPLHandler.CALL_HANDLER);
+        add(REPLHandler.CLEAR_BREAK_HANDLER);
+        add(REPLHandler.CONTINUE_HANDLER);
+        add(REPLHandler.DELETE_HANDLER);
+        add(REPLHandler.DISABLE_BREAK_HANDLER);
+        add(REPLHandler.ENABLE_BREAK_HANDLER);
+        add(REPLHandler.EVAL_HANDLER);
+        add(REPLHandler.FILE_HANDLER);
+        add(REPLHandler.FRAME_HANDLER);
+        add(REPLHandler.INFO_HANDLER);
+        add(REPLHandler.KILL_HANDLER);
+        add(REPLHandler.LOAD_HANDLER);
+        add(REPLHandler.QUIT_HANDLER);
+        add(REPLHandler.SET_BREAK_CONDITION_HANDLER);
+        add(REPLHandler.STEP_INTO_HANDLER);
+        add(REPLHandler.STEP_OUT_HANDLER);
+        add(REPLHandler.STEP_OVER_HANDLER);
+        add(REPLHandler.TRUFFLE_HANDLER);
+        add(REPLHandler.TRUFFLE_NODE_HANDLER);
+        add(REPLHandler.UNSET_BREAK_CONDITION_HANDLER);
+    }
+
+    void haltedAt(SuspendedEvent event) {
+        // Create and push a new debug context where execution is halted
+        currentServerContext = new Context(currentServerContext, event);
+
+        // Message the client that execution is halted and is in a new debugging context
+        final REPLMessage message = new REPLMessage();
+        message.put(REPLMessage.OP, REPLMessage.STOPPED);
+        final SourceSection src = event.getNode().getSourceSection();
+        final Source source = src.getSource();
+        message.put(REPLMessage.SOURCE_NAME, source.getName());
+        message.put(REPLMessage.FILE_PATH, source.getPath());
+        message.put(REPLMessage.LINE_NUMBER, Integer.toString(src.getStartLine()));
+        message.put(REPLMessage.STATUS, REPLMessage.SUCCEEDED);
+        message.put(REPLMessage.DEBUG_LEVEL, Integer.toString(currentServerContext.getLevel()));
+        List<String> warnings = event.getRecentWarnings();
+        if (!warnings.isEmpty()) {
+            final StringBuilder sb = new StringBuilder();
+            for (String warning : warnings) {
+                sb.append(warning + "\n");
+            }
+            message.put(REPLMessage.WARNINGS, sb.toString());
+        }
+        try {
+            // Cheat with synchrony: call client directly about entering a nested debugging
+            // context.
+            replClient.halted(message);
+        } finally {
+            // Returns when "continue" or "kill" is called in the new debugging context
+
+            // Pop the debug context, and return so that the old context will continue
+            currentServerContext = currentServerContext.predecessor;
+        }
+    }
+
+    /**
+     * Execution context of a halted program, possibly nested.
+     */
+    public final class Context {
+
+        private final Context predecessor;
+        private final int level;
+        private final SuspendedEvent event;
+
+        Context(Context predecessor, SuspendedEvent event) {
+            this.level = predecessor == null ? 0 : predecessor.getLevel() + 1;
+            this.predecessor = predecessor;
+            this.event = event;
+        }
+
+        /**
+         * The nesting depth of this context in the current session.
+         */
+        int getLevel() {
+            return level;
+        }
+
+        /**
+         * The AST node where execution is halted in this context.
+         */
+        Node getNodeAtHalt() {
+            return event.getNode();
+        }
+
+        /**
+         * Evaluates given code snippet in the context of currently suspended execution.
+         *
+         * @param code the snippet to evaluate
+         * @param frame <code>null</code> in case the evaluation should happen in top most frame,
+         *            non-null value
+         * @return result of the evaluation
+         * @throws IOException if something goes wrong
+         */
+        Object eval(String code, FrameInstance frame) throws IOException {
+            if (event == null) {
+                throw new IOException("top level \"eval\" not yet supported");
+            }
+            return event.eval(code, frame);
+        }
+
+        /**
+         * The frame where execution is halted in this context.
+         */
+        MaterializedFrame getFrameAtHalt() {
+            return event.getFrame();
+        }
+
+        /**
+         * Dispatches a REPL request to the appropriate handler.
+         */
+        REPLMessage[] receive(REPLMessage request) {
+            final String command = request.get(REPLMessage.OP);
+            final REPLHandler handler = handlerMap.get(command);
+
+            if (handler == null) {
+                final REPLMessage message = new REPLMessage();
+                message.put(REPLMessage.OP, command);
+                message.put(REPLMessage.STATUS, REPLMessage.FAILED);
+                message.put(REPLMessage.DISPLAY_MSG, statusPrefix + " op \"" + command + "\" not supported");
+                final REPLMessage[] reply = new REPLMessage[]{message};
+                return reply;
+            }
+            return handler.receive(request, REPLServer.this);
+        }
+
+        /**
+         * Provides access to the execution stack.
+         *
+         * @return immutable list of stack elements
+         */
+        List<FrameDebugDescription> getStack() {
+            List<FrameDebugDescription> frames = new ArrayList<>();
+            int frameCount = 1;
+            for (FrameInstance frameInstance : event.getStack()) {
+                if (frameCount == 1) {
+                    frames.add(new FrameDebugDescription(frameCount, event.getNode(), frameInstance));
+                } else {
+                    frames.add(new FrameDebugDescription(frameCount, frameInstance.getCallNode(), frameInstance));
+                }
+                frameCount++;
+            }
+            return Collections.unmodifiableList(frames);
+        }
+
+        void prepareStepOut() {
+            event.prepareStepOut();
+        }
+
+        void prepareStepInto(int repeat) {
+            event.prepareStepInto(repeat);
+        }
+
+        void prepareStepOver(int repeat) {
+            event.prepareStepOver(repeat);
+        }
+
+        void prepareContinue() {
+            event.prepareContinue();
+        }
+    }
+
+    /**
+     * Ask the server to handle a request. Return a non-empty array of messages to simulate remote
+     * operation where the protocol has possibly multiple messages being returned asynchronously in
+     * response to each request.
+     */
+    public REPLMessage[] receive(REPLMessage request) {
+        if (currentServerContext == null) {
+            final REPLMessage message = new REPLMessage();
+            message.put(REPLMessage.STATUS, REPLMessage.FAILED);
+            message.put(REPLMessage.DISPLAY_MSG, "server not started");
+            final REPLMessage[] reply = new REPLMessage[]{message};
+            return reply;
+        }
+        return currentServerContext.receive(request);
+    }
+
+    Context getCurrentContext() {
+        return currentServerContext;
+    }
+
+    Visualizer getVisualizer() {
+        return visualizer;
+    }
+
+    // TODO (mlvdv) language-specific
+    Language getLanguage() {
+        return language;
+    }
+
+    // TODO (mlvdv) language-specific
+    public String getLanguageName() {
+        return languageName(this.language);
+    }
+
+    private static String languageName(Language lang) {
+        return lang.getName() + "(" + lang.getVersion() + ")";
+    }
+
+    void eval(Source source) throws IOException {
+        engine.eval(source);
+    }
+
+    Breakpoint setLineBreakpoint(int ignoreCount, LineLocation lineLocation, boolean oneShot) throws IOException {
+        Breakpoint breakpoint;
+        if (db == null) {
+            breakpoint = new PendingLineBreakpoint(ignoreCount, lineLocation, oneShot);
+        } else {
+            breakpoint = db.setLineBreakpoint(ignoreCount, lineLocation, oneShot);
+        }
+        registerNewBreakpoint(breakpoint);
+        return breakpoint;
+    }
+
+    Breakpoint setTagBreakpoint(int ignoreCount, StandardSyntaxTag tag, boolean oneShot) throws IOException {
+        Breakpoint breakpoint;
+        if (db == null) {
+            breakpoint = new PendingTagBreakpoint(ignoreCount, tag, oneShot);
+        } else {
+            breakpoint = db.setTagBreakpoint(ignoreCount, tag, oneShot);
+        }
+        registerNewBreakpoint(breakpoint);
+        return breakpoint;
+    }
+
+    private synchronized void registerNewBreakpoint(Breakpoint breakpoint) {
+        breakpoints.put(breakpoint, nextBreakpointUID++);
+    }
+
+    synchronized Breakpoint findBreakpoint(int id) {
+        for (Map.Entry<Breakpoint, Integer> entrySet : breakpoints.entrySet()) {
+            if (id == entrySet.getValue()) {
+                return entrySet.getKey();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets a list of the currently existing breakpoints.
+     */
+    Collection<Breakpoint> getBreakpoints() {
+        return new ArrayList<>(breakpoints.keySet());
+    }
+
+    synchronized int getBreakpointID(Breakpoint breakpoint) {
+        final Integer id = breakpoints.get(breakpoint);
+        return id == null ? -1 : id;
+    }
+
+    void call(String name) throws IOException {
+        Value symbol = engine.findGlobalSymbol(name);
+        if (symbol == null) {
+            throw new IOException("symbol \"" + name + "\" not found");
+        }
+        symbol.invoke(null);
+    }
+
+    /**
+     * The intention to create a line breakpoint.
+     */
+    private final class PendingLineBreakpoint extends LineBreakpoint {
+
+        private Source conditionSource;
+
+        PendingLineBreakpoint(int ignoreCount, LineLocation lineLocation, boolean oneShot) {
+            super(Breakpoint.State.ENABLED_UNRESOLVED, lineLocation, ignoreCount, oneShot);
+        }
+
+        @Override
+        public void setEnabled(boolean enabled) {
+            switch (getState()) {
+                case ENABLED_UNRESOLVED:
+                    if (!enabled) {
+                        setState(State.DISABLED_UNRESOLVED);
+                    }
+                    break;
+                case DISABLED_UNRESOLVED:
+                    if (enabled) {
+                        setState(State.ENABLED_UNRESOLVED);
+                    }
+                    break;
+                case DISPOSED:
+                    throw new IllegalStateException("Disposed breakpoints must stay disposed");
+                default:
+                    throw new IllegalStateException("Unexpected breakpoint state");
+            }
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return getState() == State.ENABLED_UNRESOLVED;
+        }
+
+        @Override
+        public void setCondition(String expr) throws IOException {
+            this.conditionSource = Source.fromText(expr, "breakpoint condition from text: " + expr);
+        }
+
+        @Override
+        public Source getCondition() {
+            return conditionSource;
+        }
+
+        @Override
+        public void dispose() {
+            if (getState() == State.DISPOSED) {
+                throw new IllegalStateException("Breakpoint already disposed");
+            }
+            setState(State.DISPOSED);
+            breakpoints.remove(this);
+        }
+    }
+
+    /**
+     * The intention to create a line breakpoint.
+     */
+    private final class PendingTagBreakpoint extends TagBreakpoint {
+
+        private Source conditionSource;
+
+        PendingTagBreakpoint(int ignoreCount, SyntaxTag tag, boolean oneShot) {
+            super(Breakpoint.State.ENABLED_UNRESOLVED, tag, ignoreCount, oneShot);
+        }
+
+        @Override
+        public void setEnabled(boolean enabled) {
+            switch (getState()) {
+                case ENABLED_UNRESOLVED:
+                    if (!enabled) {
+                        setState(State.DISABLED_UNRESOLVED);
+                    }
+                    break;
+                case DISABLED_UNRESOLVED:
+                    if (enabled) {
+                        setState(State.ENABLED_UNRESOLVED);
+                    }
+                    break;
+                case DISPOSED:
+                    throw new IllegalStateException("Disposed breakpoints must stay disposed");
+                default:
+                    throw new IllegalStateException("Unexpected breakpoint state");
+            }
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return getState() == State.ENABLED_UNRESOLVED;
+        }
+
+        @Override
+        public void setCondition(String expr) throws IOException {
+            this.conditionSource = Source.fromText(expr, "breakpoint condition from text: " + expr);
+        }
+
+        @Override
+        public Source getCondition() {
+            return conditionSource;
+        }
+
+        @Override
+        public void dispose() {
+            if (getState() == State.DISPOSED) {
+                throw new IllegalStateException("Breakpoint already disposed");
+            }
+            setState(State.DISPOSED);
+            breakpoints.remove(this);
+        }
+    }
+}