diff graal/com.oracle.truffle.tools.debug.shell/src/com/oracle/truffle/tools/debug/shell/client/SimpleREPLClient.java @ 21568:3b8bbf51d320

Truffle/Debugging: add the Truffle DebugEngine and supporting code, as well as add a crude command-line debugging tool used mainly to test the DebugEngine. Migrate the small tols out of project com.oracle.truffle.api into the new project com.oracle.truffle.tools.
author Michael Van De Vanter <michael.van.de.vanter@oracle.com>
date Tue, 26 May 2015 16:38:13 -0700
parents
children 894f82515e38
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.tools.debug.shell/src/com/oracle/truffle/tools/debug/shell/client/SimpleREPLClient.java	Tue May 26 16:38:13 2015 -0700
@@ -0,0 +1,1399 @@
+/*
+ * 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.client;
+
+import java.io.*;
+import java.util.*;
+
+import jline.console.*;
+
+import com.oracle.truffle.api.*;
+import com.oracle.truffle.api.source.*;
+import com.oracle.truffle.tools.debug.shell.*;
+
+/**
+ * A very simple line-oriented, language-agnostic debugging client shell: the first step toward a
+ * general, extensible debugging framework designed to be adapted for remote debugging.
+ * <p>
+ * The architecture of this debugging framework is modeled loosely on <a
+ * href="https://github.com/clojure/tools.nrepl">nREPL</a>, a network REPL developed by the Clojure
+ * community with a focus on generality:
+ * <ul>
+ * <li>Client and (possibly remote) server communicate via <em>messages</em> carried over some
+ * <em>transport</em>;</li>
+ * <li>A message is a <em>map</em> of key/value pairs;</li>
+ * <li>Keys and values are <em>strings</em>;</li>
+ * <li>The client sends messages as <em>requests</em> to a server;</li>
+ * <li>A server dispatches each incoming request to an appropriate <em>handler</em> that takes
+ * appropriate action and responds to the client with one or more messages; and</li>
+ * <li>Many implementations of the <em>transport</em> are possible.</li>
+ * </ul>
+ * <p>
+ * <strong>Compromises:</strong>
+ * <p>
+ * In order to get
+ * <ol>
+ * <li>A debugging session should start from this shell, but there is no machinery in place for
+ * doing that; instead, an entry into the language implementation creates both the server and this
+ * shell;</li>
+ * <li>The current startup sequence is based on method calls, not messages;</li>
+ * <li>Only a very few request types and keys are implemented, omitting for example request and
+ * session ids;</li>
+ * <li>Message passing is synchronous and "transported" via method calls;</li>
+ * <li>Asynchrony is emulated by having each call to the server pass only a message, and by having
+ * the server return only a list of messages.</li>
+ * </ol>
+ *
+ * @see REPLServer
+ * @see REPLMessage
+ */
+public class SimpleREPLClient implements REPLClient {
+
+    private static final String REPLY_PREFIX = "==> ";
+    private static final String FAIL_PREFIX = "**> ";
+    private static final String WARNING_PREFIX = "!!> ";
+    private static final String TRACE_PREFIX = ">>> ";
+    private static final String[] NULL_ARGS = new String[0];
+
+    static final String INFO_LINE_FORMAT = "    %s\n";
+    static final String CODE_LINE_FORMAT = "    %3d  %s\n";
+    static final String CODE_LINE_BREAK_FORMAT = "--> %3d  %s\n";
+
+    private static final String STACK_FRAME_FORMAT = "    %3d: at %s in %s    line =\"%s\"\n";
+    private static final String STACK_FRAME_SELECTED_FORMAT = "==> %3d: at %s in %s    line =\"%s\"\n";
+
+    private final ExecutionContext executionContext;  // Language context
+
+    // Top level commands
+    private final Map<String, REPLCommand> commandMap = new HashMap<>();
+    private final Collection<String> commandNames = new TreeSet<>();
+
+    // Local options
+    private final Map<String, LocalOption> localOptions = new HashMap<>();
+    private final Collection<String> optionNames = new TreeSet<>();
+
+    // Current local context
+    ClientContextImpl clientContext;
+
+    // Cheating for the prototype; prototype startup now happens from the language server.
+    // So this isn't used.
+    public static void main(String[] args) {
+        final SimpleREPLClient repl = new SimpleREPLClient(null, null);
+        repl.start();
+    }
+
+    private final ConsoleReader reader;
+
+    private final PrintStream writer;
+
+    private final REPLServer replServer;
+
+    private final LocalOption astDepthOption = new IntegerOption(9, "astdepth", "default depth for AST display");
+
+    private final LocalOption autoWhereOption = new BooleanOption(true, "autowhere", "run the \"where\" command after each navigation");
+
+    private final LocalOption autoNodeOption = new BooleanOption(false, "autonode", "run the \"truffle node\" command after each navigation");
+
+    private final LocalOption autoSubtreeOption = new BooleanOption(false, "autosubtree", "run the \"truffle subtree\" command after each navigation");
+
+    private final LocalOption autoASTOption = new BooleanOption(false, "autoast", "run the \"truffle ast\" command after each navigation");
+
+    private final LocalOption listSizeOption = new IntegerOption(25, "listsize", "default number of lines to list");
+
+    private final LocalOption traceMessagesOption = new BooleanOption(false, "tracemessages", "trace REPL messages between client and server");
+
+    private final LocalOption verboseBreakpointInfoOption = new BooleanOption(true, "verbosebreakpointinfo", "\"info breakpoint\" displays more info");
+
+    private void addOption(LocalOption localOption) {
+        final String optionName = localOption.getName();
+        localOptions.put(optionName, localOption);
+        optionNames.add(optionName);
+    }
+
+    /**
+     * Non-null when the user has named a file other than where halted, providing context for
+     * commands such as "break"; if no explicit selection, then defaults to where halted. This is
+     * session state, so it persists across halting contexts.
+     */
+    private Source selectedSource = null;
+
+    public SimpleREPLClient(ExecutionContext context, REPLServer replServer) {
+        this.executionContext = context;
+        this.replServer = replServer;
+        this.writer = System.out;
+        try {
+            this.reader = new ConsoleReader();
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create console " + e);
+        }
+
+        addCommand(backtraceCommand);
+        addCommand(REPLRemoteCommand.BREAK_AT_LINE_CMD);
+        addCommand(REPLRemoteCommand.BREAK_AT_LINE_ONCE_CMD);
+        addCommand(REPLRemoteCommand.BREAK_AT_THROW_CMD);
+        addCommand(REPLRemoteCommand.BREAK_AT_THROW_ONCE_CMD);
+        addCommand(REPLRemoteCommand.CLEAR_BREAK_CMD);
+        addCommand(REPLRemoteCommand.CONDITION_BREAK_CMD);
+        addCommand(REPLRemoteCommand.CONTINUE_CMD);
+        addCommand(REPLRemoteCommand.DELETE_CMD);
+        addCommand(REPLRemoteCommand.DISABLE_CMD);
+        addCommand(REPLRemoteCommand.DOWN_CMD);
+        addCommand(REPLRemoteCommand.ENABLE_CMD);
+        addCommand(evalCommand);
+        addCommand(fileCommand);
+        addCommand(REPLRemoteCommand.FRAME_CMD);
+        addCommand(helpCommand);
+        addCommand(infoCommand);
+        addCommand(REPLRemoteCommand.KILL_CMD);
+        addCommand(listCommand);
+        addCommand(REPLRemoteCommand.LOAD_RUN_CMD);
+        addCommand(REPLRemoteCommand.LOAD_STEP_CMD);
+        addCommand(quitCommand);
+        addCommand(setCommand);
+        addCommand(REPLRemoteCommand.STEP_INTO_CMD);
+        addCommand(REPLRemoteCommand.STEP_OUT_CMD);
+        addCommand(REPLRemoteCommand.STEP_OVER_CMD);
+        addCommand(truffleCommand);
+        addCommand(REPLRemoteCommand.UP_CMD);
+        addCommand(whereCommand);
+
+        infoCommand.addCommand(infoBreakCommand);
+        infoCommand.addCommand(infoLanguageCommand);
+        infoCommand.addCommand(infoSetCommand);
+
+        truffleCommand.addCommand(truffleASTCommand);
+        truffleCommand.addCommand(truffleNodeCommand);
+        truffleCommand.addCommand(truffleSubtreeCommand);
+
+        addOption(astDepthOption);
+        addOption(autoASTOption);
+        addOption(autoNodeOption);
+        addOption(autoSubtreeOption);
+        addOption(autoWhereOption);
+        addOption(listSizeOption);
+        addOption(traceMessagesOption);
+        addOption(verboseBreakpointInfoOption);
+    }
+
+    public void start() {
+
+        REPLMessage startReply = replServer.start();
+
+        if (startReply.get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+            clientContext.displayFailReply(startReply.get(REPLMessage.DISPLAY_MSG));
+            throw new RuntimeException("Can't start REPL server");
+        }
+
+        this.clientContext = new ClientContextImpl(null, null);
+
+        try {
+            clientContext.startSession();
+        } finally {
+            clientContext.displayReply("Goodbye from " + executionContext.getLanguageShortName() + "/REPL");
+        }
+
+    }
+
+    public void addCommand(REPLCommand replCommand) {
+        final String commandName = replCommand.getCommand();
+        final String abbreviation = replCommand.getAbbreviation();
+
+        commandNames.add(commandName);
+        commandMap.put(commandName, replCommand);
+        if (abbreviation != null) {
+            commandMap.put(abbreviation, replCommand);
+        }
+    }
+
+    private class ClientContextImpl implements REPLClientContext {
+
+        private final ClientContextImpl predecessor;
+        private final int level;
+
+        // Information about where the execution is halted
+        /** The source where execution, if any, is halted; null if none. */
+        private Source haltedSource = null;
+        /** The line number where execution, if any, is halted; 0 if none. */
+        private int haltedLineNumber = 0;
+        /** The stack where execution, if any, is halted; null if none. Evaluated lazily. */
+        private List<REPLFrame> frames = null;
+
+        /** The frame number currently selected by user. */
+        private int selectedFrameNumber = 0;
+
+        private String currentPrompt;
+
+        /**
+         * Create a new context on the occasion of an execution halting.
+         */
+        public ClientContextImpl(ClientContextImpl predecessor, REPLMessage message) {
+            this.predecessor = predecessor;
+            this.level = predecessor == null ? 0 : predecessor.level + 1;
+
+            if (message != null) {
+                try {
+                    this.haltedSource = Source.fromFileName(message.get(REPLMessage.SOURCE_NAME));
+                    selectedSource = this.haltedSource;
+                    try {
+                        haltedLineNumber = Integer.parseInt(message.get(REPLMessage.LINE_NUMBER));
+                    } catch (NumberFormatException e) {
+                        haltedLineNumber = 0;
+                    }
+                } catch (IOException e1) {
+                    this.haltedSource = null;
+                    this.haltedLineNumber = 0;
+                }
+            }
+            updatePrompt();
+        }
+
+        private void selectSource(String fileName) {
+            try {
+                selectedSource = Source.fromFileName(fileName);
+            } catch (IOException e1) {
+                selectedSource = null;
+            }
+            updatePrompt();
+        }
+
+        private void updatePrompt() {
+            if (level == 0) {
+                // 0-level context; no executions halted.
+                if (selectedSource == null) {
+                    final String languageName = executionContext.getLanguageShortName();
+                    currentPrompt = languageName == null ? "() " : "(" + languageName + ") ";
+                } else {
+                    currentPrompt = "(" + selectedSource.getShortName() + ") ";
+                }
+            } else if (selectedSource != null && selectedSource != haltedSource) {
+                // User is focusing somewhere else than the current locn; show no line number.
+                final StringBuilder sb = new StringBuilder();
+                sb.append("(<" + Integer.toString(level) + "> ");
+                sb.append(selectedSource.getShortName());
+                sb.append(") ");
+                currentPrompt = sb.toString();
+            } else {
+                // Prompt reveals where currently halted.
+                final StringBuilder sb = new StringBuilder();
+                sb.append("(<" + Integer.toString(level) + "> ");
+                sb.append(haltedSource == null ? "??" : haltedSource.getShortName());
+                if (haltedLineNumber > 0) {
+                    sb.append(":" + Integer.toString(haltedLineNumber));
+                }
+                sb.append(") ");
+                currentPrompt = sb.toString();
+            }
+
+        }
+
+        public Source source() {
+            return haltedSource;
+        }
+
+        public int lineNumber() {
+            return haltedLineNumber;
+        }
+
+        public List<REPLFrame> frames() {
+            if (frames == null) {
+                final REPLMessage request = new REPLMessage(REPLMessage.OP, REPLMessage.BACKTRACE);
+                final REPLMessage[] replies = sendToServer(request);
+                if (replies[0].get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+                    return null;
+                }
+                frames = new ArrayList<>();
+                for (REPLMessage reply : replies) {
+                    final int index = reply.getIntValue(REPLMessage.FRAME_NUMBER);
+                    final String locationFilePath = reply.get(REPLMessage.FILE_PATH);
+                    final Integer locationLineNumber = reply.getIntValue(REPLMessage.LINE_NUMBER);
+                    final String locationDescription = reply.get(REPLMessage.SOURCE_LOCATION);
+                    final String name = reply.get(REPLMessage.METHOD_NAME);
+                    final String sourceLineText = reply.get(REPLMessage.SOURCE_LINE_TEXT);
+                    frames.add(new REPLFrameImpl(index, locationFilePath, locationLineNumber, locationDescription, name, sourceLineText));
+                }
+                frames = Collections.unmodifiableList(frames);
+            }
+            return frames;
+        }
+
+        public int level() {
+            return this.level;
+        }
+
+        public Source getSelectedSource() {
+            return selectedSource == null ? haltedSource : selectedSource;
+        }
+
+        public int getSelectedFrameNumber() {
+            return selectedFrameNumber;
+        }
+
+        public String stringQuery(String op) {
+            assert op != null;
+            REPLMessage request = null;
+            switch (op) {
+                case REPLMessage.TRUFFLE_AST:
+                    request = truffleASTCommand.createRequest(clientContext, NULL_ARGS);
+                    break;
+                case REPLMessage.TRUFFLE_SUBTREE:
+                    request = truffleSubtreeCommand.createRequest(clientContext, NULL_ARGS);
+                    break;
+                default:
+                    request = new REPLMessage();
+                    request.put(REPLMessage.OP, op);
+            }
+            if (request == null) {
+                return null;
+            }
+            final REPLMessage[] replies = sendToServer(request);
+            if (replies[0].get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+                return null;
+            }
+            return replies[0].get(REPLMessage.DISPLAY_MSG);
+        }
+
+        public void selectFrameNumber(int frameNumber) {
+            this.selectedFrameNumber = frameNumber;
+        }
+
+        void displayWhere() {
+            if (level == 0) {
+                displayFailReply("no active execution");
+                return;
+            }
+
+            Source whereSource = null;
+            int whereLineNumber = 0;
+
+            if (selectedFrameNumber == 0) {
+                whereSource = haltedSource;
+                whereLineNumber = haltedLineNumber;
+            } else {
+                final REPLFrame frame = frames().get(selectedFrameNumber);
+                final String locationFileName = frame.locationFilePath();
+                if (locationFileName != null) {
+                    try {
+                        whereSource = Source.fromFileName(locationFileName);
+                    } catch (IOException e) {
+                    }
+                }
+                whereLineNumber = frame.locationLineNumber();
+            }
+            if (whereSource == null) {
+                displayFailReply("Frame " + selectedFrameNumber + ": source unavailable");
+                return;
+            }
+            final int listSize = listSizeOption.getInt();
+
+            final int fileLineCount = whereSource.getLineCount();
+            final String code = whereSource.getCode();
+
+            writer.println("Frame " + selectedFrameNumber + ": " + whereSource.getShortName() + "\n");
+            final int halfListSize = listSize / 2;
+            final int startLineNumber = Math.max(1, whereLineNumber - halfListSize);
+            final int lastLineNumber = Math.min(startLineNumber + listSize - 1, fileLineCount);
+            for (int line = startLineNumber; line <= lastLineNumber; line++) {
+                final int offset = whereSource.getLineStartOffset(line);
+                final String lineText = code.substring(offset, offset + whereSource.getLineLength(line));
+                if (line == whereLineNumber) {
+                    writer.format(CODE_LINE_BREAK_FORMAT, line, lineText);
+                } else {
+                    writer.format(CODE_LINE_FORMAT, line, lineText);
+                }
+            }
+        }
+
+        public void displayStack() {
+            final List<REPLFrame> frameList = frames();
+            if (frameList == null) {
+                writer.println("<empty stack>");
+            } else {
+                for (REPLFrame frame : frameList) {
+                    String sourceLineText = frame.sourceLineText();
+                    if (sourceLineText == null) {
+                        sourceLineText = "<??>";
+                    }
+                    if (frame.index() == selectedFrameNumber) {
+                        writer.format(STACK_FRAME_SELECTED_FORMAT, frame.index(), frame.locationDescription(), frame.name(), sourceLineText);
+                    } else {
+                        writer.format(STACK_FRAME_FORMAT, frame.index(), frame.locationDescription(), frame.name(), sourceLineText);
+                    }
+                }
+            }
+        }
+
+        public void displayInfo(String message) {
+            writer.format(INFO_LINE_FORMAT, message);
+        }
+
+        public void displayReply(String message) {
+            writer.println(REPLY_PREFIX + message);
+        }
+
+        public void displayFailReply(String message) {
+            writer.println(FAIL_PREFIX + message);
+        }
+
+        public void displayWarnings(String warnings) {
+            for (String warning : warnings.split("\\n")) {
+                writer.println(WARNING_PREFIX + warning);
+            }
+        }
+
+        public void traceMessage(String message) {
+            writer.println(TRACE_PREFIX + message);
+        }
+
+        public void startSession() {
+
+            while (true) {
+                try {
+                    String[] args;
+                    String line = reader.readLine(currentPrompt).trim();
+                    if (line.startsWith("eval ")) {
+                        args = new String[]{"eval", line.substring(5)};
+                    } else {
+                        args = line.split("[ \t]+");
+                    }
+                    if (args.length == 0) {
+                        break;
+                    }
+                    final String cmd = args[0];
+
+                    if (cmd.isEmpty()) {
+                        continue;
+                    }
+
+                    REPLCommand command = commandMap.get(cmd);
+                    while (command instanceof REPLIndirectCommand) {
+                        if (traceMessagesOption.getBool()) {
+                            traceMessage("Executing indirect: " + command.getCommand());
+                        }
+                        command = ((REPLIndirectCommand) command).getCommand(args);
+                    }
+                    if (command == null) {
+                        clientContext.displayFailReply("Unrecognized command \"" + cmd + "\"");
+                        continue;
+                    }
+                    if (command instanceof REPLLocalCommand) {
+                        if (traceMessagesOption.getBool()) {
+                            traceMessage("Executing local: " + command.getCommand());
+                        }
+                        ((REPLLocalCommand) command).execute(args);
+
+                    } else if (command instanceof REPLRemoteCommand) {
+                        final REPLRemoteCommand remoteCommand = (REPLRemoteCommand) command;
+
+                        final REPLMessage request = remoteCommand.createRequest(clientContext, args);
+                        if (request == null) {
+                            continue;
+                        }
+
+                        REPLMessage[] replies = sendToServer(request);
+
+                        remoteCommand.processReply(clientContext, replies);
+                    } else {
+                        assert false; // Should not happen.
+                    }
+
+                } catch (REPLContinueException ex) {
+                    break;
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+
+        }
+
+        private REPLMessage[] sendToServer(REPLMessage request) {
+            if (traceMessagesOption.getBool()) {
+                clientContext.traceMessage("Sever request:");
+                request.print(writer, "  ");
+            }
+
+            REPLMessage[] replies = replServer.receive(request);
+
+            assert replies != null && replies.length > 0;
+            if (traceMessagesOption.getBool()) {
+                if (replies.length > 1) {
+                    clientContext.traceMessage("Received " + replies.length + " server replies");
+                    int replyCount = 0;
+                    for (REPLMessage reply : replies) {
+                        clientContext.traceMessage("Server Reply " + replyCount++ + ":");
+                        reply.print(writer, "  ");
+                    }
+                } else {
+                    clientContext.traceMessage("Received reply:");
+                    replies[0].print(writer, "  ");
+                }
+            }
+            return replies;
+        }
+
+        private final class REPLFrameImpl implements REPLFrame {
+
+            private final int index;
+            private final String locationFilePath;
+            private final Integer locationLineNumber;
+            private final String locationDescription;
+            private final String name;
+            private final String sourceLineText;
+
+            REPLFrameImpl(int index, String locationFilePath, Integer locationLineNumber, String locationDescription, String name, String sourceLineText) {
+                this.index = index;
+                this.locationFilePath = locationFilePath;
+                this.locationLineNumber = locationLineNumber;
+                this.locationDescription = locationDescription;
+                this.name = name;
+                this.sourceLineText = sourceLineText;
+            }
+
+            public int index() {
+                return index;
+            }
+
+            public String locationFilePath() {
+                return locationFilePath;
+            }
+
+            public Integer locationLineNumber() {
+                return locationLineNumber;
+            }
+
+            public String locationDescription() {
+                return locationDescription;
+            }
+
+            public String name() {
+                return name;
+            }
+
+            public String sourceLineText() {
+                return sourceLineText;
+            }
+
+        }
+
+    }
+
+    // Cheating with synchrony: asynchronous replies should arrive here, but don't.
+    @Override
+    public REPLMessage receive(REPLMessage request) {
+        final String result = request.get("result");
+        clientContext.displayReply(result != null ? result : request.toString());
+        return null;
+    }
+
+    /**
+     * Cheating with synchrony: take a direct call from the server that execution has halted and
+     * we've entered a nested debugging context.
+     */
+    public void halted(REPLMessage message) {
+
+        // Push a new context for where we've stopped.
+        clientContext = new ClientContextImpl(clientContext, message);
+        final String warnings = message.get(REPLMessage.WARNINGS);
+        if (warnings != null) {
+            clientContext.displayWarnings(warnings);
+        }
+        if (autoWhereOption.getBool()) {
+            clientContext.displayWhere();
+        }
+        if (autoNodeOption.getBool()) {
+            final String result = clientContext.stringQuery(REPLMessage.TRUFFLE_NODE);
+            if (result != null) {
+                displayTruffleNode(result);
+            }
+        }
+        if (autoASTOption.getBool()) {
+            final String result = clientContext.stringQuery(REPLMessage.TRUFFLE_AST);
+            if (result != null) {
+                displayTruffleAST(result);
+            }
+        }
+        if (autoSubtreeOption.getBool()) {
+            final String result = clientContext.stringQuery(REPLMessage.TRUFFLE_SUBTREE);
+            if (result != null) {
+                displayTruffleSubtree(result);
+            }
+        }
+
+        try {
+            clientContext.startSession();
+        } finally {
+
+            // To continue execution, pop the context and return
+            this.clientContext = clientContext.predecessor;
+        }
+    }
+
+    /**
+     * A command that can be executed without (direct) communication with the server; it may rely on
+     * some other method that goes to the server for information.
+     */
+    private abstract class REPLLocalCommand extends REPLCommand {
+
+        public REPLLocalCommand(String command, String abbreviation, String description) {
+            super(command, abbreviation, description);
+        }
+
+        abstract void execute(String[] args);
+    }
+
+    /**
+     * A command that redirects to other commands, based on arguments.
+     */
+    private abstract class REPLIndirectCommand extends REPLCommand {
+
+        public REPLIndirectCommand(String command, String abbreviation, String description) {
+            super(command, abbreviation, description);
+        }
+
+        abstract void addCommand(REPLCommand command);
+
+        abstract REPLCommand getCommand(String[] args);
+    }
+
+    private final REPLCommand backtraceCommand = new REPLLocalCommand("backtrace", "bt", "Display current stack") {
+
+        @Override
+        void execute(String[] args) {
+            if (clientContext.level == 0) {
+                clientContext.displayFailReply("no active execution");
+            } else {
+                clientContext.displayStack();
+            }
+        }
+    };
+
+    private final REPLCommand evalCommand = new REPLRemoteCommand("eval", null, "Evaluate a string, in context of the current frame if any") {
+
+        private int evalCounter = 0;
+
+        @Override
+        public REPLMessage createRequest(REPLClientContext context, String[] args) {
+            if (args.length > 1) {
+                final String code = args[1];
+                if (!code.isEmpty()) {
+                    // Create a fake entry in the file maps and cache, based on this unique name
+                    final String fakeFileName = "<eval" + ++evalCounter + ">";
+                    Source.fromNamedText(fakeFileName, code);
+                    final REPLMessage request = new REPLMessage();
+                    request.put(REPLMessage.OP, REPLMessage.EVAL);
+                    request.put(REPLMessage.CODE, code);
+                    request.put(REPLMessage.SOURCE_NAME, fakeFileName);
+                    if (clientContext.level > 0) {
+                        // Specify a requested execution context, if one exists; otherwise top level
+                        request.put(REPLMessage.FRAME_NUMBER, Integer.toString(context.getSelectedFrameNumber()));
+                    }
+                    return request;
+                }
+            }
+            return null;
+        }
+    };
+
+    private final REPLCommand fileCommand = new REPLRemoteCommand("file", null, "Set/display current file for viewing") {
+
+        final String[] help = {"file:  display current file path", "file <filename>: Set file to be current file for viewing"};
+
+        @Override
+        public String[] getHelp() {
+            return help;
+        }
+
+        @Override
+        public REPLMessage createRequest(REPLClientContext context, String[] args) {
+            if (args.length == 1) {
+                final Source source = clientContext.getSelectedSource();
+                if (source == null) {
+                    clientContext.displayFailReply("no file currently selected");
+                } else {
+                    clientContext.displayReply(source.getPath());
+                }
+                return null;
+            }
+            final REPLMessage request = new REPLMessage();
+            request.put(REPLMessage.OP, REPLMessage.FILE);
+            request.put(REPLMessage.SOURCE_NAME, args[1]);
+            return request;
+        }
+
+        @Override
+        void processReply(REPLClientContext context, REPLMessage[] replies) {
+            REPLMessage firstReply = replies[0];
+
+            if (firstReply.get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+                final String result = firstReply.get(REPLMessage.DISPLAY_MSG);
+                clientContext.displayFailReply(result != null ? result : firstReply.toString());
+                return;
+            }
+            final String fileName = firstReply.get(REPLMessage.SOURCE_NAME);
+            final String path = firstReply.get(REPLMessage.FILE_PATH);
+            clientContext.selectSource(path == null ? fileName : path);
+            clientContext.displayReply(clientContext.getSelectedSource().getPath());
+
+            for (int i = 1; i < replies.length; i++) {
+                REPLMessage reply = replies[i];
+                final String result = reply.get(REPLMessage.DISPLAY_MSG);
+                clientContext.displayInfo(result != null ? result : reply.toString());
+            }
+        }
+
+    };
+
+    private final REPLCommand helpCommand = new REPLLocalCommand("help", null, "Describe commands") {
+
+        final String[] help = {"help:  list available commands", "help <command>: additional information about <command>"};
+
+        @Override
+        public String[] getHelp() {
+            return help;
+        }
+
+        @Override
+        public void execute(String[] args) {
+
+            if (args.length == 1) {
+                clientContext.displayReply("Available commands:");
+                for (String commandName : commandNames) {
+                    final REPLCommand command = commandMap.get(commandName);
+                    if (command == null) {
+                        clientContext.displayInfo(commandName + ": Error, no implementation for command");
+                    } else {
+                        final String abbrev = command.getAbbreviation();
+                        if (abbrev == null) {
+                            clientContext.displayInfo(commandName + ": " + command.getDescription());
+                        } else {
+                            clientContext.displayInfo(commandName + "(" + abbrev + "): " + command.getDescription());
+                        }
+                    }
+                }
+            } else {
+                final String cmdName = args[1];
+                final REPLCommand cmd = commandMap.get(cmdName);
+                if (cmd == null) {
+                    clientContext.displayReply("command \"" + cmdName + "\" not recognized");
+                } else {
+                    final String[] helpLines = cmd.getHelp();
+                    if (helpLines == null) {
+                        clientContext.displayReply("\"" + cmdName + "\":");
+                    } else if (helpLines.length == 1) {
+                        clientContext.displayInfo(helpLines[0]);
+                    } else {
+                        clientContext.displayReply("\"" + cmdName + "\":");
+                        for (String line : helpLines) {
+                            clientContext.displayInfo(line);
+                        }
+                    }
+                }
+            }
+        }
+    };
+
+    private final REPLIndirectCommand infoCommand = new REPLIndirectCommand(REPLMessage.INFO, null, "Additional information on topics") {
+
+        // "Info" commands
+        private final Map<String, REPLCommand> infoCommandMap = new HashMap<>();
+        private final Collection<String> infoCommandNames = new TreeSet<>();
+
+        @Override
+        public String[] getHelp() {
+            final ArrayList<String> lines = new ArrayList<>();
+            for (String infoCommandName : infoCommandNames) {
+                final REPLCommand cmd = infoCommandMap.get(infoCommandName);
+                if (cmd == null) {
+                    lines.add("\"" + REPLMessage.INFO + " " + infoCommandName + "\" not implemented");
+                } else {
+                    lines.add("\"" + REPLMessage.INFO + " " + infoCommandName + "\": " + cmd.getDescription());
+                }
+            }
+            return lines.toArray(new String[0]);
+        }
+
+        @Override
+        void addCommand(REPLCommand replCommand) {
+            final String commandName = replCommand.getCommand();
+            final String abbreviation = replCommand.getAbbreviation();
+
+            infoCommandNames.add(commandName);
+            infoCommandMap.put(commandName, replCommand);
+            if (abbreviation != null) {
+                infoCommandMap.put(abbreviation, replCommand);
+            }
+        }
+
+        @Override
+        REPLCommand getCommand(String[] args) {
+            if (args.length == 1) {
+                clientContext.displayFailReply("info topic not specified; try \"help info\"");
+                return null;
+            }
+            final String topic = args[1];
+            REPLCommand command = infoCommandMap.get(topic);
+            if (command == null) {
+                clientContext.displayFailReply("topic \"" + topic + "\" not recognized");
+                return null;
+            }
+
+            return command;
+        }
+
+    };
+
+    private final REPLCommand infoBreakCommand = new REPLRemoteCommand("breakpoint", "break", "info about breakpoints") {
+
+        @Override
+        public REPLMessage createRequest(REPLClientContext context, String[] args) {
+            final REPLMessage request = new REPLMessage();
+            request.put(REPLMessage.OP, REPLMessage.BREAKPOINT_INFO);
+            return request;
+        }
+
+        @Override
+        void processReply(REPLClientContext context, REPLMessage[] replies) {
+            if (replies[0].get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+                clientContext.displayFailReply(replies[0].get(REPLMessage.DISPLAY_MSG));
+            } else {
+                Arrays.sort(replies, new Comparator<REPLMessage>() {
+
+                    public int compare(REPLMessage o1, REPLMessage o2) {
+                        try {
+                            final int n1 = Integer.parseInt(o1.get(REPLMessage.BREAKPOINT_ID));
+                            final int n2 = Integer.parseInt(o2.get(REPLMessage.BREAKPOINT_ID));
+                            return Integer.compare(n1, n2);
+                        } catch (Exception ex) {
+                        }
+                        return 0;
+                    }
+
+                });
+                clientContext.displayReply("Breakpoints set:");
+                for (REPLMessage message : replies) {
+                    final StringBuilder sb = new StringBuilder();
+
+                    sb.append(Integer.parseInt(message.get(REPLMessage.BREAKPOINT_ID)) + ": ");
+                    sb.append("@" + message.get(REPLMessage.INFO_VALUE));
+                    sb.append(" (state=" + message.get(REPLMessage.BREAKPOINT_STATE));
+                    if (verboseBreakpointInfoOption.getBool()) {
+                        sb.append(", group=" + Integer.parseInt(message.get(REPLMessage.BREAKPOINT_GROUP_ID)));
+                        sb.append(", hits=" + Integer.parseInt(message.get(REPLMessage.BREAKPOINT_HIT_COUNT)));
+                        sb.append(", ignore=" + Integer.parseInt(message.get(REPLMessage.BREAKPOINT_IGNORE_COUNT)));
+                    }
+                    final String condition = message.get(REPLMessage.BREAKPOINT_CONDITION);
+                    if (condition != null) {
+                        sb.append(", condition=\"" + condition + "\"");
+                    }
+                    sb.append(")");
+                    clientContext.displayInfo(sb.toString());
+                }
+            }
+        }
+    };
+
+    private final REPLCommand infoLanguageCommand = new REPLRemoteCommand("language", "lang", "language and implementation details") {
+
+        final String[] help = {"info language:  list details about the language implementation"};
+
+        @Override
+        public String[] getHelp() {
+            return help;
+        }
+
+        @Override
+        public REPLMessage createRequest(REPLClientContext context, String[] args) {
+            final REPLMessage request = new REPLMessage();
+            request.put(REPLMessage.OP, REPLMessage.INFO);
+            request.put(REPLMessage.TOPIC, REPLMessage.LANGUAGE);
+            return request;
+        }
+
+        @Override
+        void processReply(REPLClientContext context, REPLMessage[] replies) {
+            if (replies[0].get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+                clientContext.displayFailReply(replies[0].get(REPLMessage.DISPLAY_MSG));
+            } else {
+                clientContext.displayReply("Language info:");
+                for (REPLMessage message : replies) {
+                    final StringBuilder sb = new StringBuilder();
+                    sb.append(message.get(REPLMessage.INFO_KEY));
+                    sb.append(": ");
+                    sb.append(message.get(REPLMessage.INFO_VALUE));
+                    clientContext.displayInfo(sb.toString());
+                }
+            }
+        }
+    };
+
+    private final REPLCommand infoSetCommand = new REPLLocalCommand("set", null, "info about settings") {
+
+        final String[] help = {"info sets:  list local options that can be set"};
+
+        @Override
+        public String[] getHelp() {
+            return help;
+        }
+
+        @Override
+        public void execute(String[] args) {
+
+            clientContext.displayReply("Settable options:");
+
+            for (String optionName : optionNames) {
+                final LocalOption localOption = localOptions.get(optionName);
+                if (localOption == null) {
+                    clientContext.displayInfo(localOption + ": Error, no implementation for option");
+                } else {
+                    clientContext.displayInfo(optionName + "=" + localOption.getValue() + ": " + localOption.getDescription());
+                }
+            }
+        }
+    };
+
+    private final REPLCommand listCommand = new REPLLocalCommand("list", null, "Display selected source file") {
+
+        final String[] help = {"list:  list <listsize> lines of selected file (see option \"listsize\")", "list all: list all lines", "list <n>: list <listsize> lines centered around line <n>"};
+
+        private Source lastListedSource = null;
+
+        private int nextLineToList = 1;
+
+        @Override
+        public String[] getHelp() {
+            return help;
+        }
+
+        @Override
+        public void execute(String[] args) {
+            final Source source = clientContext.getSelectedSource();
+            if (source == null) {
+                clientContext.displayFailReply("No selected file");
+                reset();
+                return;
+            }
+            final int listSize = listSizeOption.getInt();
+
+            if (args.length == 1) {
+                if (!source.equals(lastListedSource)) {
+                    reset();
+                } else if (nextLineToList > source.getLineCount()) {
+                    reset();
+                }
+                final int lastListedLine = printLines(source, nextLineToList, listSize);
+                lastListedSource = source;
+                nextLineToList = lastListedLine > source.getLineCount() ? 1 : lastListedLine + 1;
+            } else if (args.length == 2) {
+                reset();
+                if (args[1].equals("all")) {
+                    printLines(source, 1, source.getLineCount());
+                } else {
+                    try {
+                        final int line = Integer.parseInt(args[1]);
+                        final int halfListSize = listSize / 2;
+                        final int start = Math.max(1, line - halfListSize);
+                        final int count = Math.min(source.getLineCount() + 1 - start, listSize);
+                        printLines(source, start, count);
+                    } catch (NumberFormatException e) {
+                        clientContext.displayFailReply("\"" + args[1] + "\" not recognized");
+                    }
+
+                }
+            }
+        }
+
+        private int printLines(Source printSource, int start, int listSize) {
+
+            clientContext.displayReply(printSource.getShortName() + ":");
+            final int lastLineNumber = Math.min(start + listSize - 1, printSource.getLineCount());
+            for (int line = start; line <= lastLineNumber; line++) {
+                writer.format(CODE_LINE_FORMAT, line, printSource.getCode(line));
+            }
+            return lastLineNumber;
+        }
+
+        /**
+         * Forget where we were in a sequence of list commands with no arguments
+         */
+        private void reset() {
+            lastListedSource = clientContext.getSelectedSource();
+            nextLineToList = 1;
+        }
+    };
+
+    private final REPLCommand quitCommand = new REPLRemoteCommand("quit", "q", "Quit execution and REPL") {
+
+        @Override
+        public REPLMessage createRequest(REPLClientContext context, String[] args) {
+            final REPLMessage request = new REPLMessage();
+            request.put(REPLMessage.OP, REPLMessage.QUIT);
+            return request;
+        }
+
+    };
+
+    private final REPLCommand setCommand = new REPLLocalCommand("set", null, "set <option>=<value>") {
+
+        @Override
+        public String[] getHelp() {
+            return new String[]{"Sets an option \"set <option-name>=<value>\";  see also \"info set\""};
+        }
+
+        @Override
+        public void execute(String[] args) {
+            REPLMessage request = null;
+            if (args.length == 1) {
+                clientContext.displayFailReply("No option specified, try \"help set\"");
+            } else if (args.length == 2) {
+                String[] split = new String[0];
+                try {
+                    split = args[1].split("=");
+                } catch (Exception ex) {
+                }
+                if (split.length == 0) {
+                    clientContext.displayFailReply("Arguments not understood, try \"help set\"");
+                } else if (split.length == 1) {
+                    clientContext.displayFailReply("No option value specified, try \"help set\"");
+                } else if (split.length > 2) {
+                    clientContext.displayFailReply("Arguments not understood, try \"help set\"");
+                } else {
+                    final String optionName = split[0];
+                    final String newValue = split[1];
+                    final LocalOption localOption = localOptions.get(optionName);
+                    if (localOption != null) {
+                        if (!localOption.setValue(newValue)) {
+                            clientContext.displayFailReply("Invalid option value \"" + newValue + "\"");
+                        }
+                        clientContext.displayInfo(localOption.name + " = " + localOption.getValue());
+                    } else {
+                        request = new REPLMessage();
+                        request.put(REPLMessage.OP, REPLMessage.SET);
+                        request.put(REPLMessage.OPTION, optionName);
+                        request.put(REPLMessage.VALUE, newValue);
+                    }
+                }
+            } else {
+                clientContext.displayFailReply("Arguments not understood, try \"help set\"");
+            }
+        }
+    };
+
+    private final REPLIndirectCommand truffleCommand = new REPLIndirectCommand(REPLMessage.TRUFFLE, "t", "Access to Truffle internals") {
+
+        // "Truffle" commands
+        private final Map<String, REPLCommand> truffleCommandMap = new HashMap<>();
+        private final Collection<String> truffleCommandNames = new TreeSet<>();
+
+        @Override
+        public String[] getHelp() {
+            final ArrayList<String> lines = new ArrayList<>();
+            for (String truffleCommandName : truffleCommandNames) {
+                final REPLCommand cmd = truffleCommandMap.get(truffleCommandName);
+                if (cmd == null) {
+                    lines.add("\"" + REPLMessage.TRUFFLE + " " + truffleCommandName + "\" not implemented");
+                } else {
+                    for (String line : cmd.getHelp()) {
+                        lines.add(line);
+                    }
+                }
+            }
+            return lines.toArray(new String[0]);
+        }
+
+        @Override
+        void addCommand(REPLCommand replCommand) {
+            final String commandName = replCommand.getCommand();
+            final String abbreviation = replCommand.getAbbreviation();
+
+            truffleCommandNames.add(commandName);
+            truffleCommandMap.put(commandName, replCommand);
+            if (abbreviation != null) {
+                truffleCommandMap.put(abbreviation, replCommand);
+            }
+        }
+
+        @Override
+        REPLCommand getCommand(String[] args) {
+            if (args.length == 1) {
+                clientContext.displayFailReply("truffle request not specified; try \"help truffle\"");
+                return null;
+            }
+            final String topic = args[1];
+            REPLCommand command = truffleCommandMap.get(topic);
+            if (command == null) {
+                clientContext.displayFailReply("truffle request \"" + topic + "\" not recognized");
+                return null;
+            }
+            return command;
+        }
+    };
+
+    private final REPLRemoteCommand truffleASTCommand = new REPLRemoteCommand("ast", null, "print the AST that contains the current node") {
+
+        final String[] help = {"truffle ast:  print the AST subtree that contains current node (see \"set treedepth\")",
+                        "truffle ast <n>:  print the AST subtree that contains current node to a maximum depth of <n>"};
+
+        @Override
+        public String[] getHelp() {
+            return help;
+        }
+
+        @Override
+        public REPLMessage createRequest(REPLClientContext context, String[] args) {
+            if (clientContext.level() == 0) {
+                context.displayFailReply("no active execution");
+                return null;
+            }
+
+            final REPLMessage request = new REPLMessage();
+            request.put(REPLMessage.OP, REPLMessage.TRUFFLE);
+            request.put(REPLMessage.TOPIC, REPLMessage.AST);
+
+            int astDepth = astDepthOption.getInt();
+            if (args.length > 2) {
+                final String depthText = args[2];
+                try {
+                    astDepth = Integer.parseInt(depthText);
+                } catch (NumberFormatException e) {
+                }
+            }
+            request.put(REPLMessage.AST_DEPTH, Integer.toString(astDepth));
+            return request;
+        }
+
+        @Override
+        void processReply(REPLClientContext context, REPLMessage[] replies) {
+            if (replies[0].get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+                clientContext.displayFailReply(replies[0].get(REPLMessage.DISPLAY_MSG));
+            } else {
+                clientContext.displayReply("AST containing the Current Node:");
+                for (REPLMessage message : replies) {
+                    for (String line : message.get(REPLMessage.DISPLAY_MSG).split("\n")) {
+                        clientContext.displayInfo(line);
+                    }
+                }
+            }
+        }
+    };
+
+    private void displayTruffleAST(String text) {
+        clientContext.displayReply("AST containing Current Node:");
+        for (String line : text.split("\n")) {
+            clientContext.displayInfo(line);
+        }
+    }
+
+    private final REPLRemoteCommand truffleNodeCommand = new REPLRemoteCommand("node", null, "describe current AST node") {
+
+        final String[] help = {"truffle node:  describe the AST node at the current execution context"};
+
+        @Override
+        public String[] getHelp() {
+            return help;
+        }
+
+        @Override
+        public REPLMessage createRequest(REPLClientContext context, String[] args) {
+            if (clientContext.level() == 0) {
+                context.displayFailReply("no active execution");
+                return null;
+            }
+            final REPLMessage request = new REPLMessage();
+            request.put(REPLMessage.OP, REPLMessage.TRUFFLE_NODE);
+            return request;
+        }
+
+        @Override
+        void processReply(REPLClientContext context, REPLMessage[] replies) {
+            if (replies[0].get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+                clientContext.displayFailReply(replies[0].get(REPLMessage.DISPLAY_MSG));
+            } else {
+                displayTruffleNode(replies[0].get(REPLMessage.DISPLAY_MSG));
+            }
+        }
+    };
+
+    private void displayTruffleNode(String nodeString) {
+        clientContext.displayReply("Current Node: " + nodeString);
+    }
+
+    private final REPLRemoteCommand truffleSubtreeCommand = new REPLRemoteCommand("subtree", "sub", "print the AST subtree rooted at the current node") {
+
+        final String[] help = {"truffle sub:  print the AST subtree at the current node (see \"set treedepth\")", "truffle sub <n>:  print the AST subtree at the current node to maximum depth <n>",
+                        "truffle subtree:   print the AST subtree at the current node (see \"set treedepth\")", "truffle sub <n>:  print the AST subtree at the current node to maximum depth <n>"};
+
+        @Override
+        public String[] getHelp() {
+            return help;
+        }
+
+        @Override
+        public REPLMessage createRequest(REPLClientContext context, String[] args) {
+            if (clientContext.level() == 0) {
+                context.displayFailReply("no active execution");
+                return null;
+            }
+
+            final REPLMessage request = new REPLMessage();
+            request.put(REPLMessage.OP, REPLMessage.TRUFFLE);
+            request.put(REPLMessage.TOPIC, REPLMessage.SUBTREE);
+
+            int astDepth = astDepthOption.getInt();
+            if (args.length > 2) {
+                final String depthText = args[2];
+                try {
+                    astDepth = Integer.parseInt(depthText);
+                } catch (NumberFormatException e) {
+                }
+            }
+            request.put(REPLMessage.AST_DEPTH, Integer.toString(astDepth));
+            return request;
+        }
+
+        @Override
+        void processReply(REPLClientContext context, REPLMessage[] replies) {
+            if (replies[0].get(REPLMessage.STATUS).equals(REPLMessage.FAILED)) {
+                clientContext.displayFailReply(replies[0].get(REPLMessage.DISPLAY_MSG));
+            } else {
+                clientContext.displayReply("AST subtree at Current Node:");
+                for (REPLMessage message : replies) {
+                    for (String line : message.get(REPLMessage.DISPLAY_MSG).split("\n")) {
+                        clientContext.displayInfo(line);
+                    }
+                }
+            }
+        }
+    };
+
+    private void displayTruffleSubtree(String text) {
+        clientContext.displayReply("AST subtree at Current Node:");
+        for (String line : text.split("\n")) {
+            clientContext.displayInfo(line);
+        }
+    }
+
+    private final REPLCommand whereCommand = new REPLLocalCommand("where", null, "Show code around current break location") {
+
+        @Override
+        public void execute(String[] args) {
+            clientContext.displayWhere();
+        }
+    };
+
+    private abstract static class LocalOption {
+        private final String name;
+        private final String description;
+
+        protected LocalOption(String name, String description) {
+            this.name = name;
+            this.description = description;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public abstract boolean setValue(String newValue);
+
+        public boolean getBool() {
+            assert false;
+            return false;
+        }
+
+        public int getInt() {
+            assert false;
+            return 0;
+        }
+
+        public abstract String getValue();
+    }
+
+    private static final class BooleanOption extends LocalOption {
+
+        private Boolean value;
+
+        public BooleanOption(boolean value, String name, String description) {
+            super(name, description);
+            this.value = value;
+        }
+
+        @Override
+        public boolean setValue(String newValue) {
+            final Boolean valueOf = Boolean.valueOf(newValue);
+            if (valueOf == null) {
+                return false;
+            }
+            value = valueOf;
+            return true;
+        }
+
+        @Override
+        public boolean getBool() {
+            return value;
+        }
+
+        @Override
+        public String getValue() {
+            return value.toString();
+        }
+    }
+
+    private static final class IntegerOption extends LocalOption {
+
+        private Integer value;
+
+        public IntegerOption(int value, String name, String description) {
+            super(name, description);
+            this.value = value;
+        }
+
+        @Override
+        public boolean setValue(String newValue) {
+            Integer valueOf;
+            try {
+                valueOf = Integer.valueOf(newValue);
+            } catch (NumberFormatException e) {
+                return false;
+            }
+            value = valueOf;
+            return true;
+        }
+
+        @Override
+        public int getInt() {
+            return value;
+        }
+
+        @Override
+        public String getValue() {
+            return value.toString();
+        }
+
+    }
+
+}