Initial implementation of Debugger Adapter Protocol
authorTom Tromey <tromey@adacore.com>
Thu, 23 Jun 2022 17:11:36 +0000 (11:11 -0600)
committerTom Tromey <tromey@adacore.com>
Mon, 2 Jan 2023 16:49:37 +0000 (09:49 -0700)
The Debugger Adapter Protocol is a JSON-RPC protocol that IDEs can use
to communicate with debuggers.  You can find more information here:

    https://microsoft.github.io/debug-adapter-protocol/

Frequently this is implemented as a shim, but it seemed to me that GDB
could implement it directly, via the Python API.  This patch is the
initial implementation.

DAP is implemented as a new "interp".  This is slightly weird, because
it doesn't act like an ordinary interpreter -- for example it doesn't
implement a command syntax, and doesn't use GDB's ordinary event loop.
However, this seemed like the best approach overall.

To run GDB in this mode, use:

    gdb -i=dap

The DAP code will accept JSON-RPC messages on stdin and print
responses to stdout.  GDB redirects the inferior's stdout to a new
pipe so that output can be encapsulated by the protocol.

The Python code uses multiple threads to do its work.  Separate
threads are used for reading JSON from the client and for writing JSON
to the client.  All GDB work is done in the main thread.  (The first
implementation used asyncio, but this had some limitations, and so I
rewrote it to use threads instead.)

This is not a complete implementation of the protocol, but it does
implement enough to demonstrate that the overall approach works.

There is a rudimentary test suite.  It uses a JSON parser written in
pure Tcl.  This parser is under the same license as Tcl itself, so I
felt it was acceptable to simply import it into the tree.

There is also a bit of documentation -- just documenting the new
interpreter name.

26 files changed:
gdb/Makefile.in
gdb/NEWS
gdb/data-directory/Makefile.in
gdb/doc/gdb.texinfo
gdb/python/lib/gdb/dap/__init__.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/breakpoint.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/bt.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/disassemble.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/evaluate.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/events.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/frames.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/io.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/launch.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/next.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/pause.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/scopes.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/server.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/startup.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/state.py [new file with mode: 0644]
gdb/python/lib/gdb/dap/threads.py [new file with mode: 0644]
gdb/python/py-dap.c [new file with mode: 0644]
gdb/testsuite/gdb.dap/basic-dap.c [new file with mode: 0644]
gdb/testsuite/gdb.dap/basic-dap.exp [new file with mode: 0644]
gdb/testsuite/lib/dap-support.exp [new file with mode: 0644]
gdb/testsuite/lib/mi-support.exp
gdb/testsuite/lib/ton.tcl [new file with mode: 0644]

index c5d66e480d1e0bfab283c67639996a3ea1484c39..b22a6c624a6080698a137ba8e888ddcf2a6ed83b 100644 (file)
@@ -396,6 +396,7 @@ SUBDIR_PYTHON_SRCS = \
        python/py-cmd.c \
        python/py-connection.c \
        python/py-continueevent.c \
+       python/py-dap.c \
        python/py-disasm.c \
        python/py-event.c \
        python/py-evtregistry.c \
index e61f06081de6125082df7037d9264b710351cecd..41d815567ce0dac10c8fe31c22a5647b72ec83de 100644 (file)
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -5,6 +5,10 @@
 
 * MI version 1 has been removed.
 
+* GDB has initial built-in support for the Debugger Adapter Protocol.
+  This support requires that GDB be built with Python scripting
+  enabled.
+
 *** Changes in GDB 13
 
 * MI version 1 is deprecated, and will be removed in GDB 14.
index 557a63b40d97e5bc9909e1b45dea25aa60bc91ce..f1139291eed7bf59bc6ba6d28cb0e81650fb4916 100644 (file)
@@ -87,6 +87,22 @@ PYTHON_FILE_LIST = \
        gdb/command/type_printers.py \
        gdb/command/unwinders.py \
        gdb/command/xmethods.py \
+       gdb/dap/breakpoint.py \
+       gdb/dap/bt.py \
+       gdb/dap/disassemble.py \
+       gdb/dap/evaluate.py \
+       gdb/dap/events.py \
+       gdb/dap/frames.py \
+       gdb/dap/__init__.py \
+       gdb/dap/io.py \
+       gdb/dap/launch.py \
+       gdb/dap/next.py \
+       gdb/dap/pause.py \
+       gdb/dap/scopes.py \
+       gdb/dap/server.py \
+       gdb/dap/startup.py \
+       gdb/dap/state.py \
+       gdb/dap/threads.py \
        gdb/function/__init__.py \
        gdb/function/as_string.py \
        gdb/function/caller_is.py \
index a72b2b9eb260cc92cecf8f3ac626708fd038f3b2..ea54f25b08e0b55c25407add042422fefe6f98d7 100644 (file)
@@ -29136,6 +29136,15 @@ The traditional console or command-line interpreter.  This is the most often
 used interpreter with @value{GDBN}. With no interpreter specified at runtime,
 @value{GDBN} will use this interpreter.
 
+@item dap
+@cindex DAP
+@cindex Debugger Adapter Protocol
+When @value{GDBN} has been built with Python support, it also supports
+the Debugger Adapter Protocol.  This protocol can be used by a
+debugger GUI or an IDE to communicate with @value{GDBN}.  This
+protocol is documented at
+@url{https://microsoft.github.io/debug-adapter-protocol/}.
+
 @item mi
 @cindex mi interpreter
 The newest @sc{gdb/mi} interface (currently @code{mi3}).  Used primarily
diff --git a/gdb/python/lib/gdb/dap/__init__.py b/gdb/python/lib/gdb/dap/__init__.py
new file mode 100644 (file)
index 0000000..0df9386
--- /dev/null
@@ -0,0 +1,69 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import gdb
+
+# This must come before other DAP imports.
+from . import startup
+
+# Load modules that define commands.
+from . import breakpoint
+from . import bt
+from . import disassemble
+from . import evaluate
+from . import launch
+from . import next
+from . import pause
+from . import scopes
+from . import threads
+
+from .server import Server
+
+
+def run():
+    """Main entry point for the DAP server.
+    This is called by the GDB DAP interpreter."""
+    startup.exec_and_log("set python print-stack full")
+    startup.exec_and_log("set pagination off")
+
+    # We want to control gdb stdin and stdout entirely, so we dup
+    # them to new file descriptors.
+    saved_out = os.dup(1)
+    saved_in = os.dup(0)
+    # Make sure these are not inheritable.  This is already the case
+    # for Unix, but not for Windows.
+    os.set_inheritable(saved_out, False)
+    os.set_inheritable(saved_in, False)
+
+    # The new gdb (and inferior) stdin will just be /dev/null.  For
+    # gdb, the "dap" interpreter also rewires the UI so that gdb
+    # doesn't try to read this (and thus see EOF and exit).
+    new_in = os.open(os.devnull, os.O_RDONLY)
+    os.dup2(new_in, 0, True)
+    os.close(new_in)
+
+    # Make the new stdout be a pipe.  This way the DAP code can easily
+    # read from the inferior and send OutputEvent to the client.
+    (rfd, wfd) = os.pipe()
+    os.set_inheritable(rfd, False)
+    os.dup2(wfd, 1, True)
+    # Also send stderr this way.
+    os.dup2(wfd, 2, True)
+    os.close(wfd)
+
+    # Note the inferior output is opened in text mode.
+    server = Server(open(saved_in, "rb"), open(saved_out, "wb"), open(rfd, "r"))
+    startup.start_dap(server.main_loop)
diff --git a/gdb/python/lib/gdb/dap/breakpoint.py b/gdb/python/lib/gdb/dap/breakpoint.py
new file mode 100644 (file)
index 0000000..502beb0
--- /dev/null
@@ -0,0 +1,143 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+import os
+
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# Map from the breakpoint "kind" (like "function") to a second map, of
+# breakpoints of that type.  The second map uses the breakpoint spec
+# as a key, and the gdb.Breakpoint itself as a value.  This is used to
+# implement the clearing behavior specified by the protocol, while
+# allowing for reuse when a breakpoint can be kept.
+breakpoint_map = {}
+
+
+@in_gdb_thread
+def breakpoint_descriptor(bp):
+    "Return the Breakpoint object descriptor given a gdb Breakpoint."
+    if bp.locations:
+        # Just choose the first location, because DAP doesn't allow
+        # multiple locations.  See
+        # https://github.com/microsoft/debug-adapter-protocol/issues/13
+        loc = bp.locations[0]
+        (basename, line) = loc.source
+        return {
+            "id": bp.number,
+            "verified": True,
+            "source": {
+                "name": os.path.basename(basename),
+                "path": loc.fullname,
+                # We probably don't need this but it doesn't hurt to
+                # be explicit.
+                "sourceReference": 0,
+            },
+            "line": line,
+            "instructionReference": hex(loc.address),
+        }
+    else:
+        return {
+            "id": bp.number,
+            "verified": False,
+        }
+
+
+# Helper function to set some breakpoints according to a list of
+# specifications.
+@in_gdb_thread
+def _set_breakpoints(kind, specs):
+    global breakpoint_map
+    # Try to reuse existing breakpoints if possible.
+    if kind in breakpoint_map:
+        saved_map = breakpoint_map[kind]
+    else:
+        saved_map = {}
+    breakpoint_map[kind] = {}
+    result = []
+    for spec in specs:
+        keyspec = frozenset(spec.items())
+        if keyspec in saved_map:
+            bp = saved_map.pop(keyspec)
+        else:
+            # FIXME handle exceptions here
+            bp = gdb.Breakpoint(**spec)
+        breakpoint_map[kind][keyspec] = bp
+        result.append(breakpoint_descriptor(bp))
+    # Delete any breakpoints that were not reused.
+    for entry in saved_map.values():
+        entry.delete()
+    return result
+
+
+@request("setBreakpoints")
+def set_breakpoint(source, *, breakpoints=[], **args):
+    if "path" not in source:
+        result = []
+    else:
+        specs = []
+        for obj in breakpoints:
+            specs.append(
+                {
+                    "source": source["path"],
+                    "line": obj["line"],
+                }
+            )
+        # Be sure to include the path in the key, so that we only
+        # clear out breakpoints coming from this same source.
+        key = "source:" + source["path"]
+        result = send_gdb_with_response(lambda: _set_breakpoints(key, specs))
+    return {
+        "breakpoints": result,
+    }
+
+
+@request("setFunctionBreakpoints")
+@capability("supportsFunctionBreakpoints")
+def set_fn_breakpoint(breakpoints, **args):
+    specs = []
+    for bp in breakpoints:
+        specs.append(
+            {
+                "function": bp["name"],
+            }
+        )
+    result = send_gdb_with_response(lambda: _set_breakpoints("function", specs))
+    return {
+        "breakpoints": result,
+    }
+
+
+@request("setInstructionBreakpoints")
+@capability("supportsInstructionBreakpoints")
+def set_insn_breakpoints(*, breakpoints, offset=None, **args):
+    specs = []
+    for bp in breakpoints:
+        # There's no way to set an explicit address breakpoint
+        # from Python, so we rely on "spec" instead.
+        val = "*" + bp["instructionReference"]
+        if offset is not None:
+            val = val + " + " + str(offset)
+        specs.append(
+            {
+                "spec": val,
+            }
+        )
+    result = send_gdb_with_response(lambda: _set_breakpoints("instruction", specs))
+    return {
+        "breakpoints": result,
+    }
diff --git a/gdb/python/lib/gdb/dap/bt.py b/gdb/python/lib/gdb/dap/bt.py
new file mode 100644 (file)
index 0000000..990ab13
--- /dev/null
@@ -0,0 +1,93 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+import os
+
+from .frames import frame_id
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+from .state import set_thread
+
+
+# Helper function to safely get the name of a frame as a string.
+@in_gdb_thread
+def _frame_name(frame):
+    name = frame.name()
+    if name is None:
+        name = "???"
+    return name
+
+
+# Helper function to get a frame's SAL without an error.
+@in_gdb_thread
+def _safe_sal(frame):
+    try:
+        return frame.find_sal()
+    except gdb.error:
+        return None
+
+
+# Helper function to compute a stack trace.
+@in_gdb_thread
+def _backtrace(thread_id, levels, startFrame):
+    set_thread(thread_id)
+    frames = []
+    current_number = 0
+    # FIXME could invoke frame filters here.
+    try:
+        current_frame = gdb.newest_frame()
+    except gdb.error:
+        current_frame = None
+    # Note that we always iterate over all frames, which is lame, but
+    # seemingly necessary to support the totalFrames response.
+    # FIXME maybe the mildly mysterious note about "monotonically
+    # increasing totalFrames values" would let us fix this.
+    while current_frame is not None:
+        # This condition handles the startFrame==0 case as well.
+        if current_number >= startFrame and (levels == 0 or len(frames) < levels):
+            newframe = {
+                "id": frame_id(current_frame),
+                "name": _frame_name(current_frame),
+                # This must always be supplied, but we will set it
+                # correctly later if that is possible.
+                "line": 0,
+                # GDB doesn't support columns.
+                "column": 0,
+                "instructionPointerReference": hex(current_frame.pc()),
+            }
+            sal = _safe_sal(current_frame)
+            if sal is not None:
+                newframe["source"] = {
+                    "name": os.path.basename(sal.symtab.filename),
+                    "path": sal.symtab.filename,
+                    # We probably don't need this but it doesn't hurt
+                    # to be explicit.
+                    "sourceReference": 0,
+                }
+                newframe["line"] = sal.line
+            frames.append(newframe)
+        current_number = current_number + 1
+        current_frame = current_frame.older()
+    return {
+        "stackFrames": frames,
+        "totalFrames": current_number,
+    }
+
+
+@request("stackTrace")
+@capability("supportsDelayedStackTraceLoading")
+def stacktrace(*, levels=0, startFrame=0, threadId, **extra):
+    return send_gdb_with_response(lambda: _backtrace(threadId, levels, startFrame))
diff --git a/gdb/python/lib/gdb/dap/disassemble.py b/gdb/python/lib/gdb/dap/disassemble.py
new file mode 100644 (file)
index 0000000..3d3b3a5
--- /dev/null
@@ -0,0 +1,51 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+@in_gdb_thread
+def _disassemble(pc, skip_insns, count):
+    try:
+        arch = gdb.selected_frame().architecture()
+    except gdb.error:
+        # Maybe there was no frame.
+        arch = gdb.selected_inferior().architecture()
+    result = []
+    total_count = skip_insns + count
+    for elt in arch.disassemble(pc, count=total_count)[skip_insns:]:
+        result.append(
+            {
+                "address": hex(elt["addr"]),
+                "instruction": elt["asm"],
+            }
+        )
+    return {
+        "instructions": result,
+    }
+
+
+@request("disassemble")
+@capability("supportsDisassembleRequest")
+def disassemble(
+    *, memoryReference, offset=0, instructionOffset=0, instructionCount, **extra
+):
+    pc = int(memoryReference, 0) + offset
+    return send_gdb_with_response(
+        lambda: _disassemble(pc, instructionOffset, instructionCount)
+    )
diff --git a/gdb/python/lib/gdb/dap/evaluate.py b/gdb/python/lib/gdb/dap/evaluate.py
new file mode 100644 (file)
index 0000000..c05e62d
--- /dev/null
@@ -0,0 +1,42 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .frames import frame_for_id
+from .server import request
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# Helper function to evaluate an expression in a certain frame.
+@in_gdb_thread
+def _evaluate(expr, frame_id):
+    if frame_id is not None:
+        frame = frame_for_id(frame_id)
+        frame.select()
+    return str(gdb.parse_and_eval(expr))
+
+
+# FIXME 'format' & hex
+# FIXME return a structured response using pretty-printers / varobj
+# FIXME supportsVariableType handling
+@request("evaluate")
+def eval_request(expression, *, frameId=None, **args):
+    result = send_gdb_with_response(lambda: _evaluate(expression, frameId))
+    return {
+        "result": result,
+        # FIXME
+        "variablesReference": -1,
+    }
diff --git a/gdb/python/lib/gdb/dap/events.py b/gdb/python/lib/gdb/dap/events.py
new file mode 100644 (file)
index 0000000..45e2a1e
--- /dev/null
@@ -0,0 +1,166 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import enum
+import gdb
+
+from .server import send_event
+from .startup import in_gdb_thread, Invoker, log
+from .breakpoint import breakpoint_descriptor
+
+
+@in_gdb_thread
+def _on_exit(event):
+    code = 0
+    if hasattr(event, "exit_code"):
+        code = event.exit_code
+    send_event(
+        "exited",
+        {
+            "exitCode": code,
+        },
+    )
+
+
+@in_gdb_thread
+def _bp_modified(event):
+    send_event(
+        "breakpoint",
+        {
+            "reason": "changed",
+            "breakpoint": breakpoint_descriptor(event),
+        },
+    )
+
+
+@in_gdb_thread
+def _bp_created(event):
+    send_event(
+        "breakpoint",
+        {
+            "reason": "new",
+            "breakpoint": breakpoint_descriptor(event),
+        },
+    )
+
+
+@in_gdb_thread
+def _bp_deleted(event):
+    send_event(
+        "breakpoint",
+        {
+            "reason": "removed",
+            "breakpoint": breakpoint_descriptor(event),
+        },
+    )
+
+
+@in_gdb_thread
+def _new_thread(event):
+    send_event(
+        "thread",
+        {
+            "reason": "started",
+            "threadId": event.inferior_thread.global_num,
+        },
+    )
+
+
+_suppress_cont = False
+
+
+@in_gdb_thread
+def _cont(event):
+    global _suppress_cont
+    if _suppress_cont:
+        log("_suppress_cont case")
+        _suppress_cont = False
+    else:
+        send_event(
+            "continued",
+            {
+                "threadId": gdb.selected_thread().global_num,
+                "allThreadsContinued": True,
+            },
+        )
+
+
+class StopKinds(enum.Enum):
+    # The values here are chosen to follow the DAP spec.
+    STEP = "step"
+    BREAKPOINT = "breakpoint"
+    PAUSE = "pause"
+    EXCEPTION = "exception"
+
+
+_expected_stop = None
+
+
+@in_gdb_thread
+def expect_stop(reason):
+    """Indicate that a stop is expected, for the reason given."""
+    global _expected_stop
+    _expected_stop = reason
+
+
+# A wrapper for Invoker that also sets the expected stop.
+class ExecutionInvoker(Invoker):
+    """A subclass of Invoker that sets the expected stop.
+    Note that this assumes that the command will restart the inferior,
+    so it will also cause ContinuedEvents to be suppressed."""
+
+    def __init__(self, cmd, expected):
+        super().__init__(cmd)
+        self.expected = expected
+
+    @in_gdb_thread
+    def __call__(self):
+        expect_stop(self.expected)
+        global _suppress_cont
+        _suppress_cont = True
+        # FIXME if the call fails should we clear _suppress_cont?
+        super().__call__()
+
+
+@in_gdb_thread
+def _on_stop(event):
+    log("entering _on_stop: " + repr(event))
+    global _expected_stop
+    obj = {
+        "threadId": gdb.selected_thread().global_num,
+        # FIXME we don't support non-stop for now.
+        "allThreadsStopped": True,
+    }
+    if isinstance(event, gdb.BreakpointEvent):
+        # Ignore the expected stop, we hit a breakpoint instead.
+        # FIXME differentiate between 'breakpoint', 'function breakpoint',
+        # 'data breakpoint' and 'instruction breakpoint' here.
+        _expected_stop = StopKinds.BREAKPOINT
+        obj["hitBreakpointIds"] = [x.number for x in event.breakpoints]
+    elif _expected_stop is None:
+        # FIXME what is even correct here
+        _expected_stop = StopKinds.EXCEPTION
+    obj["reason"] = _expected_stop.value
+    _expected_stop = None
+    send_event("stopped", obj)
+
+
+gdb.events.stop.connect(_on_stop)
+gdb.events.exited.connect(_on_exit)
+gdb.events.breakpoint_created.connect(_bp_created)
+gdb.events.breakpoint_modified.connect(_bp_modified)
+gdb.events.breakpoint_deleted.connect(_bp_deleted)
+gdb.events.new_thread.connect(_new_thread)
+gdb.events.cont.connect(_cont)
diff --git a/gdb/python/lib/gdb/dap/frames.py b/gdb/python/lib/gdb/dap/frames.py
new file mode 100644 (file)
index 0000000..a1c2689
--- /dev/null
@@ -0,0 +1,57 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .startup import in_gdb_thread
+
+
+# Map from frame (thread,level) pair to frame ID numbers.  Note we
+# can't use the frame itself here as it is not hashable.
+_frame_ids = {}
+
+# Map from frame ID number back to frames.
+_id_to_frame = {}
+
+
+# Clear all the frame IDs.
+@in_gdb_thread
+def _clear_frame_ids(evt):
+    global _frame_ids, _id_to_frame
+    _frame_ids = {}
+    _id_to_frame = {}
+
+
+# Clear the frame ID map whenever the inferior runs.
+gdb.events.cont.connect(_clear_frame_ids)
+
+
+@in_gdb_thread
+def frame_id(frame):
+    """Return the frame identifier for FRAME."""
+    global _frame_ids, _id_to_frame
+    pair = (gdb.selected_thread().global_num, frame.level)
+    if pair not in _frame_ids:
+        id = len(_frame_ids)
+        _frame_ids[pair] = id
+        _id_to_frame[id] = frame
+    return _frame_ids[pair]
+
+
+@in_gdb_thread
+def frame_for_id(id):
+    """Given a frame identifier ID, return the corresponding frame."""
+    global _id_to_frame
+    return _id_to_frame[id]
diff --git a/gdb/python/lib/gdb/dap/io.py b/gdb/python/lib/gdb/dap/io.py
new file mode 100644 (file)
index 0000000..656ac08
--- /dev/null
@@ -0,0 +1,67 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+from .startup import start_thread, send_gdb
+
+
+def read_json(stream):
+    """Read a JSON-RPC message from STREAM.
+    The decoded object is returned."""
+    # First read and parse the header.
+    content_length = None
+    while True:
+        line = stream.readline()
+        line = line.strip()
+        if line == b"":
+            break
+        if line.startswith(b"Content-Length:"):
+            line = line[15:].strip()
+            content_length = int(line)
+    data = bytes()
+    while len(data) < content_length:
+        new_data = stream.read(content_length - len(data))
+        data += new_data
+    result = json.loads(data)
+    return result
+
+
+def start_json_writer(stream, queue):
+    """Start the JSON writer thread.
+    It will read objects from QUEUE and write them to STREAM,
+    following the JSON-RPC protocol."""
+
+    def _json_writer():
+        seq = 1
+        while True:
+            obj = queue.get()
+            if obj is None:
+                # This is an exit request.  The stream is already
+                # flushed, so all that's left to do is request an
+                # exit.
+                send_gdb("quit")
+                break
+            obj["seq"] = seq
+            seq = seq + 1
+            encoded = json.dumps(obj)
+            body_bytes = encoded.encode("utf-8")
+            header = f"Content-Length: {len(body_bytes)}\r\n\r\n"
+            header_bytes = header.encode("ASCII")
+            stream.write(header_bytes)
+            stream.write(body_bytes)
+            stream.flush()
+
+    start_thread("JSON writer", _json_writer)
diff --git a/gdb/python/lib/gdb/dap/launch.py b/gdb/python/lib/gdb/dap/launch.py
new file mode 100644 (file)
index 0000000..7ac8177
--- /dev/null
@@ -0,0 +1,39 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .events import ExecutionInvoker
+from .server import request, capability
+from .startup import send_gdb
+
+
+_program = None
+
+
+@request("launch")
+def launch(*, program=None, **args):
+    if program is not None:
+        global _program
+        _program = program
+        send_gdb(f"file {_program}")
+
+
+@capability("supportsConfigurationDoneRequest")
+@request("configurationDone")
+def config_done(**args):
+    global _program
+    if _program is not None:
+        # Suppress the continue event, but don't set any particular
+        # expected stop.
+        send_gdb(ExecutionInvoker("run", None))
diff --git a/gdb/python/lib/gdb/dap/next.py b/gdb/python/lib/gdb/dap/next.py
new file mode 100644 (file)
index 0000000..726b659
--- /dev/null
@@ -0,0 +1,51 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .events import StopKinds, ExecutionInvoker
+from .server import capability, request
+from .startup import send_gdb
+from .state import set_thread
+
+
+# Helper function to set the current thread.
+def _handle_thread_step(threadId):
+    # Ensure we're going to step the correct thread.
+    send_gdb(lambda: set_thread(threadId))
+
+
+@request("next")
+def next(*, threadId, granularity="statement", **args):
+    _handle_thread_step(threadId)
+    cmd = "next"
+    if granularity == "instruction":
+        cmd += "i"
+    send_gdb(ExecutionInvoker(cmd, StopKinds.STEP))
+
+
+@capability("supportsSteppingGranularity")
+@request("stepIn")
+def stepIn(*, threadId, granularity="statement", **args):
+    _handle_thread_step(threadId)
+    cmd = "step"
+    if granularity == "instruction":
+        cmd += "i"
+    send_gdb(ExecutionInvoker(cmd, StopKinds.STEP))
+
+
+@request("continue")
+def continue_request(**args):
+    send_gdb(ExecutionInvoker("continue", None))
+    # FIXME Just ignore threadId for the time being, and assume all-stop.
+    return {"allThreadsContinued": True}
diff --git a/gdb/python/lib/gdb/dap/pause.py b/gdb/python/lib/gdb/dap/pause.py
new file mode 100644 (file)
index 0000000..74fdf48
--- /dev/null
@@ -0,0 +1,23 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .events import StopKinds, ExecutionInvoker
+from .server import request
+from .startup import send_gdb
+
+
+@request("pause")
+def pause(**args):
+    send_gdb(ExecutionInvoker("interrupt -a", StopKinds.PAUSE))
diff --git a/gdb/python/lib/gdb/dap/scopes.py b/gdb/python/lib/gdb/dap/scopes.py
new file mode 100644 (file)
index 0000000..0c887db
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .frames import frame_for_id
+from .startup import send_gdb_with_response, in_gdb_thread
+from .server import request
+
+
+# Helper function to return a frame's block without error.
+@in_gdb_thread
+def _safe_block(frame):
+    try:
+        return frame.block()
+    except gdb.error:
+        return None
+
+
+# Helper function to return a list of variables of block, up to the
+# enclosing function.
+@in_gdb_thread
+def _block_vars(block):
+    result = []
+    while True:
+        result += list(block)
+        if block.function is not None:
+            break
+        block = block.superblock
+    return result
+
+
+# Helper function to create a DAP scopes for a given frame ID.
+@in_gdb_thread
+def _get_scope(id):
+    frame = frame_for_id(id)
+    block = _safe_block(frame)
+    scopes = []
+    if block is not None:
+        new_scope = {
+            # FIXME
+            "name": "Locals",
+            "expensive": False,
+            "namedVariables": len(_block_vars(block)),
+        }
+        scopes.append(new_scope)
+    return scopes
+
+
+@request("scopes")
+def scopes(*, frameId, **extra):
+    scopes = send_gdb_with_response(lambda: _get_scope(frameId))
+    return {"scopes": scopes}
diff --git a/gdb/python/lib/gdb/dap/server.py b/gdb/python/lib/gdb/dap/server.py
new file mode 100644 (file)
index 0000000..d6fc0bd
--- /dev/null
@@ -0,0 +1,205 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import queue
+
+from .io import start_json_writer, read_json
+from .startup import (
+    in_dap_thread,
+    start_thread,
+    log,
+    log_stack,
+    send_gdb_with_response,
+)
+
+
+# Map capability names to values.
+_capabilities = {}
+
+# Map command names to callables.
+_commands = {}
+
+# The global server.
+_server = None
+
+
+class Server:
+    """The DAP server class."""
+
+    def __init__(self, in_stream, out_stream, child_stream):
+        self.in_stream = in_stream
+        self.out_stream = out_stream
+        self.child_stream = child_stream
+        self.delayed_events = []
+        # This queue accepts JSON objects that are then sent to the
+        # DAP client.  Writing is done in a separate thread to avoid
+        # blocking the read loop.
+        self.write_queue = queue.SimpleQueue()
+        self.done = False
+        global _server
+        _server = self
+
+    # Treat PARAMS as a JSON-RPC request and perform its action.
+    # PARAMS is just a dictionary from the JSON.
+    @in_dap_thread
+    def _handle_command(self, params):
+        # We don't handle 'cancel' for now.
+        result = {
+            "request_seq": params["seq"],
+            "type": "response",
+            "command": params["command"],
+        }
+        try:
+            if "arguments" in params:
+                args = params["arguments"]
+            else:
+                args = {}
+            global _commands
+            body = _commands[params["command"]](**args)
+            if body is not None:
+                result["body"] = body
+            result["success"] = True
+        except BaseException as e:
+            log_stack()
+            result["success"] = False
+            result["message"] = str(e)
+        return result
+
+    # Read inferior output and sends OutputEvents to the client.  It
+    # is run in its own thread.
+    def _read_inferior_output(self):
+        while True:
+            line = self.child_stream.readline()
+            self.send_event(
+                "output",
+                {
+                    "category": "stdout",
+                    "output": line,
+                },
+            )
+
+    # Send OBJ to the client, logging first if needed.
+    def _send_json(self, obj):
+        log("WROTE: <<<" + json.dumps(obj) + ">>>")
+        self.write_queue.put(obj)
+
+    # This must be run in the DAP thread, but we can't use
+    # @in_dap_thread here because the global isn't set until after
+    # this starts running.  FIXME.
+    def main_loop(self):
+        """The main loop of the DAP server."""
+        # Before looping, start the thread that writes JSON to the
+        # client, and the thread that reads output from the inferior.
+        start_thread("output reader", self._read_inferior_output)
+        start_json_writer(self.out_stream, self.write_queue)
+        while not self.done:
+            cmd = read_json(self.in_stream)
+            log("READ: <<<" + json.dumps(cmd) + ">>>")
+            result = self._handle_command(cmd)
+            self._send_json(result)
+            events = self.delayed_events
+            self.delayed_events = []
+            for (event, body) in events:
+                self.send_event(event, body)
+        # Got the terminate request.  This is handled by the
+        # JSON-writing thread, so that we can ensure that all
+        # responses are flushed to the client before exiting.
+        self.write_queue.put(None)
+
+    @in_dap_thread
+    def send_event_later(self, event, body=None):
+        """Send a DAP event back to the client, but only after the
+        current request has completed."""
+        self.delayed_events.append((event, body))
+
+    # Note that this does not need to be run in any particular thread,
+    # because it just creates an object and writes it to a thread-safe
+    # queue.
+    def send_event(self, event, body=None):
+        """Send an event to the DAP client.
+        EVENT is the name of the event, a string.
+        BODY is the body of the event, an arbitrary object."""
+        obj = {
+            "type": "event",
+            "event": event,
+        }
+        if body is not None:
+            obj["body"] = body
+        self._send_json(obj)
+
+    def shutdown(self):
+        """Request that the server shut down."""
+        # Just set a flag.  This operation is complicated because we
+        # want to write the result of the request before exiting.  See
+        # main_loop.
+        self.done = True
+
+
+def send_event(event, body):
+    """Send an event to the DAP client.
+    EVENT is the name of the event, a string.
+    BODY is the body of the event, an arbitrary object."""
+    global _server
+    _server.send_event(event, body)
+
+
+def request(name):
+    """A decorator that indicates that the wrapper function implements
+    the DAP request NAME."""
+
+    def wrap(func):
+        global _commands
+        _commands[name] = func
+        # All requests must run in the DAP thread.
+        return in_dap_thread(func)
+
+    return wrap
+
+
+def capability(name):
+    """A decorator that indicates that the wrapper function implements
+    the DAP capability NAME."""
+
+    def wrap(func):
+        global _capabilities
+        _capabilities[name] = True
+        return func
+
+    return wrap
+
+
+@request("initialize")
+def initialize(**args):
+    global _server, _capabilities
+    _server.config = args
+    _server.send_event_later("initialized")
+    return _capabilities.copy()
+
+
+@request("terminate")
+@capability("supportsTerminateRequest")
+def terminate(**args):
+    # We can ignore the result here, because we only really need to
+    # synchronize.
+    send_gdb_with_response("kill")
+
+
+@request("disconnect")
+@capability("supportTerminateDebuggee")
+def disconnect(*, terminateDebuggee=False, **args):
+    if terminateDebuggee:
+        terminate()
+    _server.shutdown()
diff --git a/gdb/python/lib/gdb/dap/startup.py b/gdb/python/lib/gdb/dap/startup.py
new file mode 100644 (file)
index 0000000..acfdcb4
--- /dev/null
@@ -0,0 +1,189 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Do not import other gdbdap modules here -- this module must come
+# first.
+import functools
+import gdb
+import queue
+import signal
+import threading
+import traceback
+from contextlib import contextmanager
+
+
+# The GDB thread, aka the main thread.
+_gdb_thread = threading.current_thread()
+
+
+# The DAP thread.
+_dap_thread = None
+
+
+@contextmanager
+def blocked_signals():
+    """A helper function that blocks and unblocks signals."""
+    if not hasattr(signal, "pthread_sigmask"):
+        yield
+        return
+
+    to_block = {signal.SIGCHLD, signal.SIGINT, signal.SIGALRM, signal.SIGWINCH}
+    signal.pthread_sigmask(signal.SIG_BLOCK, to_block)
+    try:
+        yield None
+    finally:
+        signal.pthread_sigmask(signal.SIG_UNBLOCK, to_block)
+
+
+def start_thread(name, target, args=()):
+    """Start a new thread, invoking TARGET with *ARGS there.
+    This is a helper function that ensures that any GDB signals are
+    correctly blocked."""
+    # GDB requires that these be delivered to the gdb thread.  We
+    # do this here to avoid any possible race with the creation of
+    # the new thread.  The thread mask is inherited by new
+    # threads.
+    with blocked_signals():
+        result = threading.Thread(target=target, args=args, daemon=True)
+        result.start()
+        return result
+
+
+def start_dap(target):
+    """Start the DAP thread and invoke TARGET there."""
+    global _dap_thread
+    exec_and_log("set breakpoint pending on")
+    _dap_thread = start_thread("DAP", target)
+
+
+def in_gdb_thread(func):
+    """A decorator that asserts that FUNC must be run in the GDB thread."""
+
+    @functools.wraps(func)
+    def ensure_gdb_thread(*args, **kwargs):
+        assert threading.current_thread() is _gdb_thread
+        return func(*args, **kwargs)
+
+    return ensure_gdb_thread
+
+
+def in_dap_thread(func):
+    """A decorator that asserts that FUNC must be run in the DAP thread."""
+
+    @functools.wraps(func)
+    def ensure_dap_thread(*args, **kwargs):
+        assert threading.current_thread() is _dap_thread
+        return func(*args, **kwargs)
+
+    return ensure_dap_thread
+
+
+class LoggingParam(gdb.Parameter):
+    """Whether DAP logging is enabled."""
+
+    set_doc = "Set the DAP logging status."
+    show_doc = "Show the DAP logging status."
+
+    log_file = None
+
+    def __init__(self):
+        super().__init__(
+            "debug dap-log-file", gdb.COMMAND_MAINTENANCE, gdb.PARAM_OPTIONAL_FILENAME
+        )
+        self.value = None
+
+    def get_set_string(self):
+        # Close any existing log file, no matter what.
+        if self.log_file is not None:
+            self.log_file.close()
+            self.log_file = None
+        if self.value is not None:
+            self.log_file = open(self.value, "w")
+        return ""
+
+
+dap_log = LoggingParam()
+
+
+def log(something):
+    """Log SOMETHING to the log file, if logging is enabled."""
+    if dap_log.log_file is not None:
+        print(something, file=dap_log.log_file)
+        dap_log.log_file.flush()
+
+
+def log_stack():
+    """Log a stack trace to the log file, if logging is enabled."""
+    if dap_log.log_file is not None:
+        traceback.print_exc(file=dap_log.log_file)
+
+
+@in_gdb_thread
+def exec_and_log(cmd):
+    """Execute the gdb command CMD.
+    If logging is enabled, log the command and its output."""
+    log("+++ " + cmd)
+    try:
+        output = gdb.execute(cmd, from_tty=True, to_string=True)
+        if output != "":
+            log(">>> " + output)
+    except gdb.error:
+        log_stack()
+
+
+class Invoker(object):
+    """A simple class that can invoke a gdb command."""
+
+    def __init__(self, cmd):
+        self.cmd = cmd
+
+    # This is invoked in the gdb thread to run the command.
+    @in_gdb_thread
+    def __call__(self):
+        exec_and_log(self.cmd)
+
+
+def send_gdb(cmd):
+    """Send CMD to the gdb thread.
+    CMD can be either a function or a string.
+    If it is a string, it is passed to gdb.execute."""
+    if isinstance(cmd, str):
+        cmd = Invoker(cmd)
+    gdb.post_event(cmd)
+
+
+def send_gdb_with_response(fn):
+    """Send FN to the gdb thread and return its result.
+    If FN is a string, it is passed to gdb.execute and None is
+    returned as the result.
+    If FN throws an exception, this function will throw the
+    same exception in the calling thread.
+    """
+    if isinstance(fn, str):
+        fn = Invoker(fn)
+    result_q = queue.SimpleQueue()
+
+    def message():
+        try:
+            val = fn()
+            result_q.put(val)
+        except Exception as e:
+            result_q.put(e)
+
+    send_gdb(message)
+    val = result_q.get()
+    if isinstance(val, Exception):
+        raise val
+    return val
diff --git a/gdb/python/lib/gdb/dap/state.py b/gdb/python/lib/gdb/dap/state.py
new file mode 100644 (file)
index 0000000..fb2e543
--- /dev/null
@@ -0,0 +1,25 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .startup import in_gdb_thread, exec_and_log, log
+
+
+@in_gdb_thread
+def set_thread(thread_id):
+    """Set the current thread to THREAD_ID."""
+    if thread_id == 0:
+        log("+++ Thread == 0 +++")
+    else:
+        exec_and_log(f"thread {thread_id}")
diff --git a/gdb/python/lib/gdb/dap/threads.py b/gdb/python/lib/gdb/dap/threads.py
new file mode 100644 (file)
index 0000000..b6a0ca0
--- /dev/null
@@ -0,0 +1,42 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .server import request
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# A helper function to construct the list of threads.
+@in_gdb_thread
+def _get_threads():
+    result = []
+    for thr in gdb.selected_inferior().threads():
+        one_result = {
+            "id": thr.global_num,
+        }
+        name = thr.name
+        if name is not None:
+            one_result["name"] = name
+        result.append(one_result)
+    return result
+
+
+@request("threads")
+def threads(**args):
+    result = send_gdb_with_response(_get_threads)
+    return {
+        "threads": result,
+    }
diff --git a/gdb/python/py-dap.c b/gdb/python/py-dap.c
new file mode 100644 (file)
index 0000000..8e977bc
--- /dev/null
@@ -0,0 +1,99 @@
+/* Python DAP interpreter
+
+   Copyright (C) 2022 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include "defs.h"
+#include "python-internal.h"
+#include "interps.h"
+#include "cli-out.h"
+#include "top.h"
+
+class dap_interp final : public interp
+{
+public:
+
+  explicit dap_interp (const char *name)
+    : interp (name),
+      m_ui_out (new cli_ui_out (gdb_stdout))
+  {
+  }
+
+  ~dap_interp () override = default;
+
+  void init (bool top_level) override;
+
+  void suspend () override
+  {
+  }
+
+  void resume () override
+  {
+  }
+
+  gdb_exception exec (const char *command) override
+  {
+    /* Just ignore it.  */
+    return {};
+  }
+
+  void set_logging (ui_file_up logfile, bool logging_redirect,
+                   bool debug_redirect) override
+  {
+    /* Just ignore it.  */
+  }
+
+  ui_out *interp_ui_out () override
+  {
+    return m_ui_out.get ();
+  }
+
+private:
+
+  std::unique_ptr<ui_out> m_ui_out;
+};
+
+void
+dap_interp::init (bool top_level)
+{
+  gdbpy_enter enter_py;
+
+  gdbpy_ref<> dap_module (PyImport_ImportModule ("gdb.dap"));
+  if (dap_module == nullptr)
+    gdbpy_handle_exception ();
+
+  gdbpy_ref<> func (PyObject_GetAttrString (dap_module.get (), "run"));
+  if (func == nullptr)
+    gdbpy_handle_exception ();
+
+  gdbpy_ref<> result_obj (PyObject_CallNoArgs (func.get ()));
+  if (result_obj == nullptr)
+    gdbpy_handle_exception ();
+
+  current_ui->input_fd = -1;
+  current_ui->m_input_interactive_p = false;
+}
+
+void _initialize_py_interp ();
+void
+_initialize_py_interp ()
+{
+  interp_factory_register ("dap", [] (const char *name) -> interp *
+    {
+      return new dap_interp (name);
+    });
+}
diff --git a/gdb/testsuite/gdb.dap/basic-dap.c b/gdb/testsuite/gdb.dap/basic-dap.c
new file mode 100644 (file)
index 0000000..eab1c99
--- /dev/null
@@ -0,0 +1,44 @@
+/* Copyright 2022 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+int global_variable = 23;
+
+void
+function_breakpoint_here ()
+{
+  ++global_variable;
+  ++global_variable;
+}
+
+void
+do_not_stop_here ()
+{
+  /* This exists to test that breakpoints are cleared.  */
+}
+
+void
+address_breakpoint_here ()
+{
+}
+
+int main ()
+{
+  do_not_stop_here ();
+  function_breakpoint_here ();
+  address_breakpoint_here ();
+  return 0;                    /* BREAK */
+}
diff --git a/gdb/testsuite/gdb.dap/basic-dap.exp b/gdb/testsuite/gdb.dap/basic-dap.exp
new file mode 100644 (file)
index 0000000..d3acf0c
--- /dev/null
@@ -0,0 +1,151 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Basic DAP test.
+
+load_lib dap-support.exp
+
+standard_testfile
+
+if {[build_executable ${testfile}.exp $testfile] == -1} {
+    return
+}
+
+if {[dap_launch $testfile] == ""} {
+    return
+}
+
+set obj [dap_check_request_and_response "set breakpoint on two functions" \
+            setFunctionBreakpoints \
+            {o breakpoints [a [o name [s function_breakpoint_here]] \
+                                [o name [s do_not_stop_here]]]}]
+set fn_bpno [dap_get_breakpoint_number $obj]
+
+# This also tests that the previous do_not_stop_here breakpoint is
+# cleared.
+set obj [dap_check_request_and_response "set breakpoint on function" \
+            setFunctionBreakpoints \
+            {o breakpoints [a [o name [s function_breakpoint_here]]]}]
+set fn_bpno [dap_get_breakpoint_number $obj]
+
+set obj [dap_check_request_and_response "set breakpoint with invalid filename" \
+            setBreakpoints \
+            [format {o source [o path [s nosuchfilename.c]] breakpoints [a [o line [i 29]]]}]]
+
+set line [gdb_get_line_number "BREAK"]
+set obj [dap_check_request_and_response "set breakpoint by line number" \
+            setBreakpoints \
+            [format {o source [o path [%s]] breakpoints [a [o line [i %d]]]} \
+                 [list s $srcfile] $line]]
+set line_bpno [dap_get_breakpoint_number $obj]
+
+# Check the new breakpoint event.
+set ok 0
+foreach event [lindex $obj 1] {
+    set d [namespace eval ton::2dict $event]
+    if {[dict get $d type] != "event"
+       || [dict get $d event] != "breakpoint"} {
+       continue
+    }
+    if {[dict get $d body reason] == "new"
+       && [dict get $d body breakpoint verified] == "true"} {
+       set ok 1
+       pass "check new breakpoint event"
+       break
+    }
+}
+if {!$ok} {
+    fail "check new breakpoint event"
+}
+
+set obj [dap_check_request_and_response "reset breakpoint by line number" \
+            setBreakpoints \
+            [format {o source [o path [%s]] breakpoints [a [o line [i %d]]]} \
+                 [list s $srcfile] $line]]
+set new_line_bpno [dap_get_breakpoint_number $obj]
+
+if {$new_line_bpno == $line_bpno} {
+    pass "re-setting kept same breakpoint number"
+} else {
+    fail "re-setting kept same breakpoint number"
+}
+
+# This uses "&address_breakpoint_here" as the address -- this is a
+# hack because we know how this is implemented under the hood.
+set obj [dap_check_request_and_response "set breakpoint by address" \
+            setInstructionBreakpoints \
+            {o breakpoints [a [o instructionReference [s &address_breakpoint_here]]]}]
+set insn_bpno [dap_get_breakpoint_number $obj]
+
+set d [namespace eval ton::2dict [lindex $obj 0]]
+set bplist [dict get $d body breakpoints]
+set insn_pc [dict get [lindex $bplist 0] instructionReference]
+
+dap_check_request_and_response "start inferior" configurationDone
+dap_read_event "inferior started" thread "body reason" started
+
+dap_read_event "stopped at function breakpoint" stopped \
+    "body reason" breakpoint \
+    "body hitBreakpointIds" $fn_bpno
+
+set obj [dap_check_request_and_response "evaluate global in function" \
+            evaluate {o expression [s global_variable]}]
+dap_match_values "global value in function" [lindex $obj 0] \
+    "body result" 23
+
+dap_check_request_and_response step stepIn {o threadId [i 1]}
+dap_read_event "stopped after step" stopped "body reason" step
+
+set obj [dap_check_request_and_response "evaluate global second time" \
+            evaluate {o expression [s global_variable]}]
+dap_match_values "global value after step" [lindex $obj 0] \
+    "body result" 24
+
+dap_check_request_and_response "continue to address" continue
+dap_read_event "stopped at address breakpoint" stopped \
+    "body reason" breakpoint \
+    "body hitBreakpointIds" $insn_bpno
+
+dap_check_request_and_response "continue to line" continue
+dap_read_event "stopped at line breakpoint" stopped \
+    "body reason" breakpoint \
+    "body hitBreakpointIds" $line_bpno
+
+set obj [dap_check_request_and_response "evaluate global in main" \
+            evaluate {o expression [s global_variable]}]
+dap_match_values "global value in main" [lindex $obj 0] \
+    "body result" 25
+
+set obj [dap_request_and_response "evaluate non-existing variable" \
+            evaluate {o expression [s nosuchvariable]}]
+set d [namespace eval ton::2dict [lindex $obj 0]]
+if {[dict get $d success] == "false"} {
+    pass "result of invalid request"
+} else {
+    fail "result of invalid request"
+}
+
+set obj [dap_check_request_and_response "disassemble one instruction" \
+            disassemble \
+            [format {o memoryReference [s %s] instructionCount [i 1]} \
+                 $insn_pc]]
+set d [namespace eval ton::2dict [lindex $obj 0]]
+if {[dict exists $d body instructions]} {
+    pass "instructions in disassemble output"
+} else {
+    fail "instructions in disassemble output"
+}
+
+dap_shutdown
diff --git a/gdb/testsuite/lib/dap-support.exp b/gdb/testsuite/lib/dap-support.exp
new file mode 100644 (file)
index 0000000..adf332c
--- /dev/null
@@ -0,0 +1,343 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# The JSON parser.
+load_lib ton.tcl
+
+# The sequence number for the next DAP request.  This is used by the
+# automatic sequence-counting code below.  It is reset each time GDB
+# is restarted.
+set dap_seq 1
+
+# Start gdb using the DAP interpreter.
+proc dap_gdb_start {} {
+    # Keep track of the number of times GDB has been launched.
+    global gdb_instances
+    incr gdb_instances
+
+    gdb_stdin_log_init
+
+    global GDBFLAGS stty_init
+    save_vars { GDBFLAGS stty_init } {
+       set stty_init "-echo raw"
+       set logfile [standard_output_file "dap.log.$gdb_instances"]
+       append GDBFLAGS " -iex \"set debug dap-log-file $logfile\" -q -i=dap"
+       set res [gdb_spawn]
+       if {$res != 0} {
+           return $res
+       }
+    }
+
+    # Reset the counter.
+    set ::dap_seq 1
+
+    return 0
+}
+
+# A helper for dap_to_ton that decides if the list L is a JSON object
+# or if it is an array.
+proc _dap_is_obj {l} {
+    if {[llength $l] % 2 != 0} {
+       return 0
+    }
+    foreach {key value} $l {
+       if {![string is alpha $key]} {
+           return 0
+       }
+    }
+    return 1
+}
+
+# The "TON" format is a bit of a pain to write by hand, so this proc
+# can be used to convert an ordinary Tcl list into TON by guessing at
+# the correct forms to use.  This can't be used in all cases, because
+# Tcl can't really differentiate between literal forms.  For example,
+# there's no way to decide if "true" should be a string or the literal
+# true.
+#
+# JSON objects must be passed in a particular form here -- as a list
+# with an even number of elements, alternating keys and values.  Each
+# key must consist only of letters, no digits or other non-letter
+# characters.  Note that this is compatible with the Tcl 'dict'
+# representation.
+proc dap_to_ton {obj} {
+    if {[string is list $obj] && [llength $obj] > 1} {
+       if {[_dap_is_obj $obj]} {
+           set result o
+           foreach {key value} $obj {
+               lappend result $key \[[dap_to_ton $value]\]
+           }
+       } else {
+           set result a
+           foreach val $obj {
+               lappend result \[[dap_to_ton $val]\]
+           }
+       }
+    } elseif {[string is entier $obj]} {
+       set result [list i $obj]
+    } elseif {[string is double $obj]} {
+       set result [list d $obj]
+    } elseif {$obj == "true" || $obj == "false" || $obj == "null"} {
+       set result [list l $obj]
+    } else {
+       set result [list s $obj]
+    }
+    return $result
+}
+
+# Format the object OBJ, in TON format, as JSON and send it to gdb.
+proc dap_send_ton {obj} {
+    set json [namespace eval ton::2json $obj]
+    # FIXME this is wrong for non-ASCII characters.
+    set len [string length $json]
+    verbose ">>> $json"
+    send_gdb "Content-Length: $len\r\n\r\n$json"
+}
+
+# Send a DAP request to gdb.  COMMAND is the request's "command"
+# field, and OBJ is the "arguments" field.  If OBJ is empty, it is
+# omitted.  The sequence number of the request is automatically added,
+# and this is also the return value.  OBJ is assumed to already be in
+# TON form.
+proc dap_send_request {command {obj {}}} {
+    # We can construct this directly as a TON object.
+    set result $::dap_seq
+    incr ::dap_seq
+    set req [format {o seq [i %d] type [s request] command [%s]} \
+                $result [list s $command]]
+    if {$obj != ""} {
+       append req " arguments \[$obj\]"
+    }
+    dap_send_ton $req
+    return $result
+}
+
+# Read a JSON response from gdb.  This will return a TON object on
+# success, or throw an exception on error.
+proc dap_read_json {} {
+    set length ""
+    gdb_expect {
+       -re "^Content-Length: (\[0-9\]+)\r\n" {
+           set length $expect_out(1,string)
+           exp_continue
+       }
+       -re "^(\[^\r\n\]+)\r\n" {
+           # Any other header field.
+           exp_continue
+       }
+       -re "^\r\n" {
+           # Done.
+       }
+       timeout {
+           error "timeout reading json header"
+       }
+       eof {
+           error "eof reading json header"
+       }
+    }
+
+    if {$length == ""} {
+       error "didn't find content-length"
+    }
+
+    set json ""
+    while {$length > 0} {
+       # Tcl only allows up to 255 characters in a {} expression in a
+       # regexp, so we may need to read in chunks.
+       set this_len [expr {min ($length, 255)}]
+       gdb_expect {
+           -re "^.{$this_len}" {
+               append json $expect_out(0,string)
+           }
+           timeout {
+               error "timeout reading json body"
+           }
+           eof {
+               error "eof reading json body"
+           }
+       }
+       incr length -$this_len
+    }
+
+    return [ton::json2ton $json]
+}
+
+# Read a sequence of JSON objects from gdb, until a response object is
+# seen.  If the response object has the request sequence number NUM,
+# and is for command CMD, return a list of two elements: the response
+# object and a list of any preceding events, in the order they were
+# emitted.  The objects are in TON format.  If a response object is
+# seen but has the wrong sequence number or command, throw an
+# exception
+proc dap_read_response {cmd num} {
+    set result {}
+    while 1 {
+       set obj [dap_read_json]
+       set d [namespace eval ton::2dict $obj]
+       if {[dict get $d type] == "response"} {
+           if {[dict get $d request_seq] != $num} {
+               error "saw wrong request_seq in $obj"
+           } elseif {[dict get $d command] != $cmd} {
+               error "saw wrong command in $obj"
+           } else {
+               return [list $obj $result]
+           }
+       } else {
+           lappend result $obj
+       }
+    }
+}
+
+# A wrapper for dap_send_request and dap_read_response.  This sends a
+# request to gdb and returns the result.  NAME is used to issue a pass
+# or fail; on failure, this always returns an empty string.
+proc dap_request_and_response {name command {obj {}}} {
+    set result {}
+    if {[catch {
+       set seq [dap_send_request $command $obj]
+       set result [dap_read_response $command $seq]
+    } text]} {
+       verbose "reason: $text"
+       fail $name
+    } else {
+       pass $name
+    }
+    return $result
+}
+
+# Like dap_request_and_response, but also checks that the response
+# indicates success.
+proc dap_check_request_and_response {name command {obj {}}} {
+    set result [dap_request_and_response $name $command $obj]
+    if {$result == ""} {
+       return ""
+    }
+    set d [namespace eval ton::2dict [lindex $result 0]]
+    if {[dict get $d success] != "true"} {
+       verbose "request failure: $result"
+       fail "$name success"
+       return ""
+    }
+    pass "$name success"
+    return $result
+}
+
+# Start gdb, send a DAP initialization request and return the
+# response.  This approach lets the caller check the feature list, if
+# desired.  Callers not caring about this should probably use
+# dap_launch.  Returns the empty string on failure.  NAME is used as
+# the test name.
+proc dap_initialize {name} {
+    if {[dap_gdb_start]} {
+       return ""
+    }
+    return [dap_check_request_and_response $name initialize]
+}
+
+# Start gdb, send a DAP initialize request, and then a launch request
+# specifying FILE as the program to use for the inferior.  Returns the
+# empty string on failure, or the response object from the launch
+# request.  After this is called, gdb will be ready to accept
+# breakpoint requests.  NAME is used as the test name.  It has a
+# reasonable default but can be overridden in case a test needs to
+# launch gdb more than once.
+proc dap_launch {file {name startup}} {
+    if {[dap_initialize "$name - initialize"] == ""} {
+       return ""
+    }
+    return [dap_check_request_and_response "$name - launch" launch \
+               [format {o program [%s]} \
+                    [list s [standard_output_file $file]]]]
+}
+
+# Cleanly shut down gdb.  NAME is used as the test name.
+proc dap_shutdown {{name shutdown}} {
+    dap_check_request_and_response $name disconnect
+}
+
+# Search the event list EVENTS for an output event matching the regexp
+# RX.  Pass the test NAME if found, fail if not.
+proc dap_search_output {name rx events} {
+    foreach event $events {
+       set d [namespace eval ton::2dict $event]
+       if {[dict get $d type] != "event"
+           || [dict get $d event] != "output"} {
+           continue
+       }
+       if {[regexp $rx [dict get $d body output]]} {
+           pass $name
+           return
+       }
+    }
+    fail $name
+}
+
+# Check that OBJ (a single TON object) has values that match the
+# key/value pairs given in ARGS.  NAME is used as the test name.
+proc dap_match_values {name obj args} {
+    set d [namespace eval ton::2dict $obj]
+    foreach {key value} $args {
+       if {[eval dict get [list $d] $key] != $value} {
+           fail "$name (checking $key)"
+           return ""
+       }
+    }
+    pass $name
+}
+
+# A helper for dap_read_event that reads events, looking for one
+# matching TYPE.
+proc _dap_read_event {type} {
+    while 1 {
+       # We don't do any extra error checking here for the time
+       # being; we'll just get a timeout thrown instead.
+       set obj [dap_read_json]
+       set d [namespace eval ton::2dict $obj]
+       if {[dict get $d type] == "event"
+           && [dict get $d event] == $type} {
+           return $obj
+       }
+    }
+}
+
+# Read JSON objects looking for an event whose "event" field is TYPE.
+# NAME is used as the test name; it defaults to TYPE.  Extra arguments
+# are used to check fields of the event; the arguments alternate
+# between a field name (in "dict get" form) and its expected value.
+# Returns the TON object for the chosen event, or empty string on
+# error.
+proc dap_read_event {name type args} {
+    if {$name == ""} {
+       set name $type
+    }
+
+    if {[catch {_dap_read_event $type} result]} {
+       fail $name
+       return ""
+    }
+
+    eval dap_match_values [list $name $result] $args
+
+    return $result
+}
+
+# A convenience function to extract the breakpoint number when a new
+# breakpoint is created.  OBJ is an object as returned by
+# dap_check_request_and_response.
+proc dap_get_breakpoint_number {obj} {
+    set d [namespace eval ton::2dict [lindex $obj 0]]
+    set bplist [dict get $d body breakpoints]
+    return [dict get [lindex $bplist 0] id]
+}
index e9226ada1584c739cf0b1df909ddbbd8270c4de0..1ee087d81270e9ee8c9db4cbea958d170e51760d 100644 (file)
@@ -238,8 +238,6 @@ proc default_mi_gdb_start { { flags {} } } {
        return [mi_gdb_start_separate_mi_tty $flags]
     }
 
-    set inferior_pty no-tty
-
     # Set the default value, it may be overriden later by specific testfile.
     set use_gdb_stub [target_info exists use_gdb_stub]
 
diff --git a/gdb/testsuite/lib/ton.tcl b/gdb/testsuite/lib/ton.tcl
new file mode 100644 (file)
index 0000000..a9013b9
--- /dev/null
@@ -0,0 +1,303 @@
+# This was imported into gdb from:
+# https://github.com/jorge-leon/ton
+
+# This software is copyrighted by Georg Lehner <jorge@at.anteris.net>.
+# The following terms apply to all files associated with the software
+# unless explicitly disclaimed in individual files.
+
+# The authors hereby grant permission to use, copy, modify, distribute,
+# and license this software and its documentation for any purpose,
+# provided that existing copyright notices are retained in all copies
+# and that this notice is included verbatim in any distributions. No
+# written agreement, license, or royalty fee is required for any of the
+# authorized uses.  Modifications to this software may be copyrighted by
+# their authors and need not follow the licensing terms described here,
+# provided that the new terms are clearly indicated on the first page of
+# each file where they apply.
+
+# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
+# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
+# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
+# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
+# NON-INFRINGEMENT.  THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND
+# THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE
+# MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+# GOVERNMENT USE: If you are acquiring this software on behalf of the
+# U.S. government, the Government shall have only "Restricted Rights" in
+# the software and related documentation as defined in the Federal
+# Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2).  If you
+# are acquiring the software on behalf of the Department of Defense, the
+# software shall be classified as "Commercial Computer Software" and the
+# Government shall have only "Restricted Rights" as defined in Clause
+# 252.227-7013 (c) (1) of DFARs.  Notwithstanding the foregoing, the
+# authors grant the U.S. Government and others acting in its behalf
+# permission to use and distribute the software in accordance with the
+# terms specified in this license.
+
+
+# leg20180331: ton / TON - Tcl Object Notation
+#
+# This package provides manipulation functionality for TON - a data
+# serialization format with a direct mapping to JSON.
+#
+# In its essence, a JSON parser is provided, which can convert a JSON
+# string into a Tcllib json style dictionary (dicts and arrays mixed),
+# into a jimhttp style dictionary (only dicts) or into a nested, typed
+# Tcl list.
+#
+# Finally, TON can be converted into (unformatted) JSON.
+
+namespace eval ton {
+    namespace export json2ton
+
+    variable version 0.4
+    
+}
+proc ton::json2ton json {
+    # Parse JSON string json
+    #
+    # return: TON
+    
+    set i [trr $json [string length $json]]
+    if {!$i} {return ""}
+    lassign [jscan $json $i] i ton
+    if {[set i [trr $json $i]]} {
+       error "json string invalid:[incr i -1]: left over characters."
+    }
+    return $ton
+}
+proc ton::trr {s i} {
+    # Trim righthand whitespace on the first i characters of s.
+    # return: number of remaining characters in s
+    
+    while {[set j $i] &&
+          ([string is space [set c [string index $s [incr i -1]]]]
+           || $c eq "\n")} {}
+    return $j
+}
+proc ton::jscan {json i {d :}} {
+    # Scan JSON in first i characters of string json.
+    # d is the default delimiter list for the next token.
+    #
+    # return list of
+    # - remaining characters in json
+    # - TON
+    #
+    # The string must already be whitespace trimmed from the right.
+
+    incr i -1
+
+    if {[set c [string index $json $i]] eq "\""} {
+       str $json [incr i -1]
+    } elseif {$c eq "\}"} {
+       obj $json $i
+    } elseif {$c eq "\]"} {
+       arr $json $i
+    } elseif {$c in {e l}} {
+       lit $json $i
+    } elseif {[string match {[0-9.]} $c]} {
+       num $json $i $c $d
+    } else {
+       error "json string end invalid:$i: ..[string range $json $i-10 $i].."
+    }
+}
+proc ton::num {json i c d} {
+    # Parse number from position i in string json to the left.
+    # c .. character at position i
+    # d .. delimiter on which to stop
+    #
+    # return list:
+    # - remaining string length
+    # - TON of number
+    
+    set float [expr {$c eq "."}]
+    for {set j $i} {$i} {incr i -1} {
+       if {[string match $d [set c [string index $json $i-1]]]} break
+       set float [expr {$float || [string match "\[eE.]" $c]}]
+    }
+    set num [string trimleft [string range $json $i $j]]
+    if {!$float && [string is entier $num]} {
+           list $i "i $num"
+    } elseif {$float && [string is double $num]} {
+       list $i "d $num"
+    } else {
+       error "number invalid:$i: $num."
+    }
+}
+proc ton::lit {json i} {
+    # Parse literal from position i in string json to the left
+    # return list:
+    # - remaining string length
+    # - TON of literal
+
+    if {[set c [string index $json $i-1]] eq "u"} {
+       list [incr i -3] "l true"
+    } elseif {$c eq "s"} {
+       list [incr i -4] "l false"
+    } elseif {$c eq "l"} {
+       list [incr i -3] "l null"
+    } else {
+       set e [string range $json $i-3 $i]
+       error "literal invalid:[incr i -1]: ..$e."
+    }
+}
+proc ton::str {json i} {
+    # Parse string from position i in string json to the left
+    # return list:
+    # - remaining string length
+    # - TON of string
+    
+    for {set j $i} {$i} {incr i -1} {
+       set i [string last \" $json $i]
+       if {[string index $json $i-1] ne "\\"} break
+    }
+    if {$i==-1} {
+       error "json string start invalid:$i: exhausted while parsing string."
+    }
+    list $i "s [list [string range $json $i+1 $j]]"
+}
+proc ton::arr {json i} {
+    # Parse array from i characters in string json
+    # return list:
+    # - remaining string length
+    # - TON of array
+    
+    set i [trr $json $i]
+    if {!$i} {
+       error "json string invalid:0: exhausted while parsing array."
+    }
+    if {[string index $json $i-1] eq "\["} {
+       return [list [incr i -1] a]
+    }
+    set r {}
+    while {$i} {
+       lassign [jscan $json $i "\[,\[]"] i v
+       lappend r \[$v\]
+       set i [trr $json $i]
+       incr i -1
+       if {[set c [string index $json $i]] eq ","} {
+           set i [trr $json $i]
+           continue
+       } elseif {$c eq "\["} break
+       error "json string invalid:$i: parsing array."
+    }
+    lappend r a
+    return [list $i [join [lreverse $r]]]
+}
+proc ton::obj {json i} {
+    # Parse array from i character in string json
+    # return list:
+    # - remaining string length
+    # - TON of object
+
+    set i [trr $json $i]
+    if {!$i} {
+       error "json string invalid:0: exhausted while parsing object."
+    }
+    if {[string index $json $i-1] eq "\{"} {
+       return [list [incr i -1] o]
+    }
+    set r {}
+    while {$i} {
+       lassign [jscan $json $i] i v
+       set i [trr $json $i]
+       incr i -1
+       if {[string index $json $i] ne ":"} {
+           error "json string invalid:$i: parsing key in object."
+       }
+       set i [trr $json $i]
+       lassign [jscan $json $i] i k
+       lassign $k type k
+       if {$type ne "s"} {
+           error "json string invalid:[incr i -1]: key not a string."
+       }
+       lappend r \[$v\] [list $k]
+       set i [trr $json $i]
+       incr i -1
+       if {[set c [string index $json $i]] eq ","} {
+           set i [trr $json $i]
+           continue
+       } elseif {$c eq "\{"} break
+       error "json string invalid:$i: parsing object." 
+    }
+    lappend r o
+    return [list $i [join [lreverse $r]]]
+}
+# TON decoders
+namespace eval ton::2list {
+    proc atom {type v} {list $type $v}
+    foreach type {i d s l} {
+       interp alias {} $type {} [namespace current]::atom $type
+    }
+    proc a args {
+       set r a
+       foreach v $args {lappend r $v}
+       return $r
+    }
+    proc o args {
+       set r o
+       foreach {k v} $args {lappend r $k $v}
+       return $r
+    }
+    # There is plenty of room for validation in get
+    # array index bounds
+    # object key existence
+    proc get {l args} {
+       foreach k $args {
+           switch [lindex $l 0] {
+               o {set l [dict get [lrange $l 1 end] $k]}
+               a {set l [lindex $l [incr k]]}
+               default {
+                   error "error: key $k to long, or wrong data: [lindex $l 0]"
+               }
+           }
+       }
+       return $l
+    }
+}
+namespace eval ton::2dict {
+    proc atom v {return $v}
+    foreach type {i d l s} {
+       interp alias {} $type {} [namespace current]::atom
+    }
+    proc a args {return $args}
+    proc o args {return $args}
+}
+namespace eval ton::a2dict {
+    proc atom v {return $v}
+    foreach type {i d l s} {
+       interp alias {} $type {} [namespace current]::atom
+    }
+    proc a args {
+       set i -1
+       set r {}
+       foreach v $args {
+           lappend r [incr i] $v
+       }
+       return $r
+    }
+    proc o args {return $args}
+}
+namespace eval ton::2json {
+    proc atom v {return $v}
+    foreach type {i d l} {
+       interp alias {} $type {} [namespace current]::atom
+    }
+    proc a args {
+       return "\[[join $args {, }]]"
+    }
+    proc o args {
+       set r {}
+       foreach {k v} $args {lappend r "\"$k\": $v"}
+       return "{[join $r {, }]}"
+    }
+    proc s s {return "\"$s\""}
+}
+
+package provide ton $ton::version