[gjs: 2/5] debugger: GJS Debugger



commit ee89551eb8af59520252fc46e4cc6aac3b17af15
Author: Philip Chimento <philip chimento gmail com>
Date:   Thu Jul 12 16:48:51 2018 +0200

    debugger: GJS Debugger
    
    This adds a simple debugger, adapted from the "jorendb" program in the
    SpiderMonkey source. It has basic stepping, breaking, and printing
    commands, that work like GDB. Activate it by running the GJS console
    interpreter with the -d or --debugger flag _before_ the name of the JS
    program on the command line.
    
    To integrate it into programs that embed the GJS interpreter, call
    gjs_context_setup_debugger_console() before executing the JS program.
    
    It will print when Promises are launched and resolved, although it's not
    yet possible to break at those points.
    
    Closes: #110

 Makefile-insttest.am                               |  19 +-
 Makefile-test.am                                   |  31 +-
 configure.ac                                       |  10 +
 gjs-srcs.mk                                        |   1 +
 gjs/console.cpp                                    |  21 +
 gjs/context.cpp                                    |  24 +
 gjs/context.h                                      |   3 +
 gjs/debugger.cpp                                   | 130 ++++
 installed-tests/debugger-test.sh                   |  23 +
 installed-tests/debugger.test.in                   |   4 +
 installed-tests/debugger/.eslintrc.json            |   5 +
 installed-tests/debugger/backtrace.debugger        |   6 +
 installed-tests/debugger/backtrace.debugger.js     |   8 +
 installed-tests/debugger/backtrace.debugger.output |  15 +
 installed-tests/debugger/breakpoint.debugger       |   7 +
 installed-tests/debugger/breakpoint.debugger.js    |   7 +
 .../debugger/breakpoint.debugger.output            |  20 +
 installed-tests/debugger/continue.debugger         |   3 +
 installed-tests/debugger/continue.debugger.js      |   2 +
 installed-tests/debugger/continue.debugger.output  |   8 +
 installed-tests/debugger/delete.debugger           |  10 +
 installed-tests/debugger/delete.debugger.js        |   5 +
 installed-tests/debugger/delete.debugger.output    |  26 +
 installed-tests/debugger/detach.debugger           |   1 +
 installed-tests/debugger/detach.debugger.js        |   1 +
 installed-tests/debugger/detach.debugger.output    |   5 +
 installed-tests/debugger/down-up.debugger          |  12 +
 installed-tests/debugger/down-up.debugger.js       |  17 +
 installed-tests/debugger/down-up.debugger.output   |  26 +
 installed-tests/debugger/finish.debugger           |   5 +
 installed-tests/debugger/finish.debugger.js        |  16 +
 installed-tests/debugger/finish.debugger.output    |  22 +
 installed-tests/debugger/frame.debugger            |   4 +
 installed-tests/debugger/frame.debugger.js         |   9 +
 installed-tests/debugger/frame.debugger.output     |  10 +
 installed-tests/debugger/keys.debugger             |   4 +
 installed-tests/debugger/keys.debugger.js          |   7 +
 installed-tests/debugger/keys.debugger.output      |  20 +
 installed-tests/debugger/next.debugger             |   8 +
 installed-tests/debugger/next.debugger.js          |  11 +
 installed-tests/debugger/next.debugger.output      |  24 +
 installed-tests/debugger/print.debugger            |  21 +
 installed-tests/debugger/print.debugger.js         |  16 +
 installed-tests/debugger/print.debugger.output     |  86 +++
 installed-tests/debugger/quit.debugger             |   1 +
 installed-tests/debugger/quit.debugger.js          |   1 +
 installed-tests/debugger/quit.debugger.output      |   4 +
 installed-tests/debugger/return.debugger           |   8 +
 installed-tests/debugger/return.debugger.js        |  15 +
 installed-tests/debugger/return.debugger.output    |  19 +
 installed-tests/debugger/set.debugger              |  21 +
 installed-tests/debugger/set.debugger.js           |   3 +
 installed-tests/debugger/set.debugger.output       |  39 ++
 installed-tests/debugger/step.debugger             |  12 +
 installed-tests/debugger/step.debugger.js          |  10 +
 installed-tests/debugger/step.debugger.output      |  36 +
 installed-tests/debugger/throw.debugger            |   4 +
 installed-tests/debugger/throw.debugger.js         |  10 +
 installed-tests/debugger/throw.debugger.output     |  21 +
 installed-tests/debugger/until.debugger            |   4 +
 installed-tests/debugger/until.debugger.js         |   7 +
 installed-tests/debugger/until.debugger.output     |  18 +
 modules/_bootstrap/debugger.js                     | 749 +++++++++++++++++++++
 modules/modules.gresource.xml                      |   1 +
 64 files changed, 1692 insertions(+), 4 deletions(-)
---
diff --git a/Makefile-insttest.am b/Makefile-insttest.am
index 1b1b5c56..24da82ee 100644
--- a/Makefile-insttest.am
+++ b/Makefile-insttest.am
@@ -8,24 +8,30 @@ gjsinsttestdir = $(pkglibexecdir)/installed-tests
 installedtestmetadir = $(datadir)/installed-tests/gjs
 jstestsdir = $(gjsinsttestdir)/js
 jsscripttestsdir = $(gjsinsttestdir)/scripts
+debuggertestsdir = $(gjsinsttestdir)/debugger
 
 gjsinsttest_PROGRAMS = 
 gjsinsttest_DATA =
+gjsinsttest_SCRIPTS =
 installedtestmeta_DATA = 
 jstests_DATA =
 jsscripttests_DATA =
+debuggertests_DATA =
 pkglib_LTLIBRARIES =
 
 if BUILDOPT_INSTALL_TESTS
 
 gjsinsttest_PROGRAMS += minijasmine
+gjsinsttest_SCRIPTS += installed-tests/debugger-test.sh
 gjsinsttest_DATA += $(TEST_INTROSPECTION_TYPELIBS)
-installedtestmeta_DATA +=              \
-       $(jasmine_tests:.js=.test)      \
-       $(simple_tests:%=%.test)        \
+installedtestmeta_DATA +=                      \
+       $(jasmine_tests:.js=.test)              \
+       $(simple_tests:%=%.test)                \
+       $(debugger_tests:.debugger=.test)       \
        $(NULL)
 jstests_DATA += $(jasmine_tests)
 jsscripttests_DATA += $(simple_tests)
+debuggertests_DATA += $(debugger_tests)
 pkglib_LTLIBRARIES += libregress.la libwarnlib.la libgimarshallingtests.la
 
 %.test: %.js installed-tests/minijasmine.test.in Makefile
@@ -35,6 +41,13 @@ pkglib_LTLIBRARIES += libregress.la libwarnlib.la libgimarshallingtests.la
                < $(srcdir)/installed-tests/minijasmine.test.in > $@.tmp && \
        mv $@.tmp $@
 
+%.test: %.debugger installed-tests/debugger.test.in Makefile
+       $(AM_V_GEN)$(MKDIR_P) $(@D) && \
+       $(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir),g \
+               -e s,@name\@,$(notdir $<), \
+               < $(srcdir)/installed-tests/debugger.test.in > $@.tmp && \
+       mv $@.tmp $@
+
 %.test: % installed-tests/script.test.in Makefile
        $(AM_V_GEN)$(MKDIR_P) $(@D) && \
        $(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir), \
diff --git a/Makefile-test.am b/Makefile-test.am
index 16982515..4ae22eea 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -296,13 +296,39 @@ simple_tests =                                            \
        $(NULL)
 EXTRA_DIST += $(simple_tests)
 
+debugger_tests =                                       \
+       installed-tests/debugger/backtrace.debugger     \
+       installed-tests/debugger/breakpoint.debugger    \
+       installed-tests/debugger/continue.debugger      \
+       installed-tests/debugger/delete.debugger        \
+       installed-tests/debugger/detach.debugger        \
+       installed-tests/debugger/down-up.debugger       \
+       installed-tests/debugger/finish.debugger        \
+       installed-tests/debugger/frame.debugger         \
+       installed-tests/debugger/keys.debugger          \
+       installed-tests/debugger/next.debugger          \
+       installed-tests/debugger/print.debugger         \
+       installed-tests/debugger/quit.debugger          \
+       installed-tests/debugger/return.debugger        \
+       installed-tests/debugger/set.debugger           \
+       installed-tests/debugger/step.debugger          \
+       installed-tests/debugger/throw.debugger         \
+       installed-tests/debugger/until.debugger         \
+       $(NULL)
+EXTRA_DIST +=                          \
+       $(debugger_tests)               \
+       $(debugger_tests:%=%.js)        \
+       $(debugger_tests:%=%.output)    \
+       $(NULL)
+
 TESTS =                                \
        gjs-tests.gtester       \
        $(simple_tests)         \
        $(jasmine_tests)        \
+       $(debugger_tests)       \
        $(NULL)
 
-TEST_EXTENSIONS = .gtester .sh .js
+TEST_EXTENSIONS = .gtester .sh .js .debugger
 
 LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
 
@@ -314,6 +340,9 @@ SH_LOG_DRIVER = $(LOG_DRIVER)
 JS_LOG_DRIVER = $(LOG_DRIVER)
 JS_LOG_COMPILER = $$LOG_COMPILER $$LOG_FLAGS $(top_builddir)/minijasmine
 
+DEBUGGER_LOG_DRIVER = $(LOG_DRIVER)
+DEBUGGER_LOG_COMPILER = $(top_srcdir)/installed-tests/debugger-test.sh
+
 CODE_COVERAGE_IGNORE_PATTERN = */{include,mfbt,gjs/test}/*
 CODE_COVERAGE_GENHTML_OPTIONS =                        \
        $(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT)        \
diff --git a/configure.ac b/configure.ac
index 55cc8c98..cc72ba7d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -296,6 +296,16 @@ AX_APPEND_COMPILE_FLAGS([$lt_prog_compiler_pic], [CXXFLAGS])
 AX_APPEND_COMPILE_FLAGS([$lt_prog_compiler_pic], [CFLAGS])
 AX_APPEND_LINK_FLAGS([$lt_prog_compiler_pic])
 
+dnl If SpiderMonkey was compiled with this configure option, then GJS needs to
+dnl be compiled with it as well, because we inherit from a SpiderMonkey class in
+dnl context.cpp. See build/autoconf/compiler-opts.m4 in mozjs52.
+AC_ARG_ENABLE([cpp-rtti],
+  [AS_HELP_STRING([--enable-cpp-rtti],
+    [needs to match SpiderMonkey's config option @<:@default=off@:>@])])
+AS_IF([test "x$enable_cpp_rtti" != "xyes"],
+  [AX_APPEND_COMPILE_FLAGS([-fno-rtti])],
+  [AX_APPEND_COMPILE_FLAGS([-GR-])])
+
 AC_ARG_WITH([xvfb-tests],
   [AS_HELP_STRING([--with-xvfb-tests],
     [Run all tests under an XVFB server @<:@default=no@:>@])])
diff --git a/gjs-srcs.mk b/gjs-srcs.mk
index 00ea0bdd..dadfa0ed 100644
--- a/gjs-srcs.mk
+++ b/gjs-srcs.mk
@@ -58,6 +58,7 @@ gjs_srcs =                            \
        gjs/context.cpp                 \
        gjs/context-private.h           \
        gjs/coverage.cpp                \
+       gjs/debugger.cpp                \
        gjs/engine.cpp                  \
        gjs/engine.h                    \
        gjs/global.cpp                  \
diff --git a/gjs/console.cpp b/gjs/console.cpp
index 4c997227..1739d882 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -37,11 +37,13 @@ static char *profile_output_path = nullptr;
 static char *command = NULL;
 static gboolean print_version = false;
 static gboolean print_js_version = false;
+static gboolean debugging = false;
 static bool enable_profiler = false;
 
 static gboolean parse_profile_arg(const char *, const char *, void *, GError **);
 
 /* Keep in sync with entries in check_script_args_for_stray_gjs_args() */
+// clang-format off
 static GOptionEntry entries[] = {
     { "version", 0, 0, G_OPTION_ARG_NONE, &print_version, "Print GJS version and exit" },
     { "jsversion", 0, 0, G_OPTION_ARG_NONE, &print_js_version,
@@ -54,8 +56,10 @@ static GOptionEntry entries[] = {
         G_OPTION_ARG_CALLBACK, reinterpret_cast<void *>(&parse_profile_arg),
         "Enable the profiler and write output to FILE (default: gjs-$PID.syscap)",
         "FILE" },
+    { "debugger", 'd', 0, G_OPTION_ARG_NONE, &debugging, "Start in debug mode" },
     { NULL }
 };
+// clang-format on
 
 static char **
 strndupv(int           n,
@@ -248,6 +252,7 @@ main(int argc, char **argv)
     command = NULL;
     print_version = false;
     print_js_version = false;
+    debugging = false;
     g_option_context_set_ignore_unknown_options(context, false);
     g_option_context_set_help_enabled(context, true);
     if (!g_option_context_parse_strv(context, &gjs_argv, &error))
@@ -341,6 +346,19 @@ main(int argc, char **argv)
         goto out;
     }
 
+    /* If we're debugging, set up the debugger and prepend a debugger statement
+     * to the script we're going to evaluate. The debugger statement will break
+     * and cause a debugger prompt to appear.
+     * TODO: This is not great, as it messes up column offsets on the first
+     * line of all scripts. It would be better to hook into a debugger event
+     * and interrupt on the first frame. */
+    if (debugging) {
+        gjs_context_setup_debugger_console(js_context);
+        char* old_script = script;
+        script = g_strconcat("debugger;", old_script, nullptr);
+        g_free(old_script);
+    }
+
     /* evaluate the script */
     if (!gjs_context_eval(js_context, script, len,
                           filename, &code, &error)) {
@@ -364,5 +382,8 @@ main(int argc, char **argv)
         g_object_unref(coverage);
     g_object_unref(js_context);
     g_free(script);
+
+    if (debugging)
+        g_print("Program exited with code %d\n", code);
     exit(code);
 }
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 0dc3883d..5c4871d7 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -71,6 +71,26 @@ static void     gjs_context_set_property      (GObject               *object,
                                                   const GValue          *value,
                                                   GParamSpec            *pspec);
 
+/* Environment preparer needed for debugger, taken from SpiderMonkey's
+ * JS shell */
+struct GjsEnvironmentPreparer final : public js::ScriptEnvironmentPreparer {
+    JSContext* m_cx;
+
+    explicit GjsEnvironmentPreparer(JSContext* cx) : m_cx(cx) {
+        js::SetScriptEnvironmentPreparer(m_cx, this);
+    }
+
+    void invoke(JS::HandleObject scope, Closure& closure) override;
+};
+
+void GjsEnvironmentPreparer::invoke(JS::HandleObject scope, Closure& closure) {
+    g_assert(!JS_IsExceptionPending(m_cx));
+
+    JSAutoCompartment ac(m_cx, scope);
+    if (!closure(m_cx))
+        gjs_log_exception(m_cx);
+}
+
 using JobQueue = JS::GCVector<JSObject *, 0, js::SystemAllocPolicy>;
 
 struct _GjsContext {
@@ -104,6 +124,8 @@ struct _GjsContext {
     GjsProfiler *profiler;
     bool should_profile : 1;
     bool should_listen_sigusr2 : 1;
+
+    GjsEnvironmentPreparer environment_preparer;
 };
 
 /* Keep this consistent with GjsConstString */
@@ -432,6 +454,7 @@ gjs_context_finalize(GObject *object)
     js_context->global.~Heap();
     js_context->const_strings.~array();
     js_context->unhandled_rejection_stacks.~unordered_map();
+    js_context->environment_preparer.~GjsEnvironmentPreparer();
     G_OBJECT_CLASS(gjs_context_parent_class)->finalize(object);
 }
 
@@ -467,6 +490,7 @@ gjs_context_constructed(GObject *object)
 
     new (&js_context->unhandled_rejection_stacks) std::unordered_map<uint64_t, GjsAutoChar>;
     new (&js_context->const_strings) std::array<JS::PersistentRootedId*, GJS_STRING_LAST>;
+    new (&js_context->environment_preparer) GjsEnvironmentPreparer(cx);
     for (i = 0; i < GJS_STRING_LAST; i++) {
         js_context->const_strings[i] = new JS::PersistentRootedId(cx,
             gjs_intern_string_to_id(cx, const_strings[i]));
diff --git a/gjs/context.h b/gjs/context.h
index 5dfa3497..eecc0da8 100644
--- a/gjs/context.h
+++ b/gjs/context.h
@@ -105,6 +105,9 @@ void            gjs_dumpstack                     (void);
 GJS_EXPORT
 const char *gjs_get_js_version(void);
 
+GJS_EXPORT
+void gjs_context_setup_debugger_console(GjsContext* gjs);
+
 G_END_DECLS
 
 #endif  /* __GJS_CONTEXT_H__ */
diff --git a/gjs/debugger.cpp b/gjs/debugger.cpp
new file mode 100644
index 00000000..d54c5b70
--- /dev/null
+++ b/gjs/debugger.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2018 Philip Chimento  <philip chimento gmail com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Authored By: Philip Chimento <philip chimento gmail com>
+ */
+
+#include <unistd.h>
+
+#include <gio/gio.h>
+
+#include "gjs/context-private.h"
+#include "gjs/global.h"
+#include "gjs/jsapi-util-args.h"
+
+#ifdef HAVE_READLINE_READLINE_H
+#include <readline/history.h>
+#include <readline/readline.h>
+#include <stdio.h>
+#endif
+
+static bool quit(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JSAutoRequest ar(cx);
+    int32_t exitcode;
+    if (!gjs_parse_call_args(cx, "quit", args, "i", "exitcode", &exitcode))
+        return false;
+
+    auto* gjs = static_cast<GjsContext*>(JS_GetContextPrivate(cx));
+    _gjs_context_exit(gjs, exitcode);
+    return false;  // without gjs_throw() == "throw uncatchable exception"
+}
+
+static bool do_readline(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    JSAutoRequest ar(cx);
+
+    GjsAutoJSChar prompt;
+    if (!gjs_parse_call_args(cx, "readline", args, "|s", "prompt", &prompt))
+        return false;
+
+    GjsAutoChar line;
+    do {
+        const char* real_prompt = prompt ? prompt.get() : "db> ";
+#ifdef HAVE_READLINE_READLINE_H
+        if (isatty(STDIN_FILENO)) {
+            line = readline(real_prompt);
+        } else {
+#else
+        {
+#endif  // HAVE_READLINE_READLINE_H
+            char buf[256];
+            g_print("%s", real_prompt);
+            fflush(stdout);
+            if (!fgets(buf, sizeof buf, stdin))
+                buf[0] = '\0';
+            line.reset(g_strchomp(g_strdup(buf)));
+
+            if (!isatty(STDIN_FILENO)) {
+                if (feof(stdin)) {
+                    g_print("[quit due to end of input]\n");
+                    line.reset(g_strdup("quit"));
+                } else {
+                    g_print("%s\n", line.get());
+                }
+            }
+        }
+
+        /* EOF, return null */
+        if (!line) {
+            args.rval().setUndefined();
+            return true;
+        }
+    } while (line && line[0] == '\0');
+
+    /* Add line to history and convert it to a JSString so that we can pass it
+     * back as the return value */
+#ifdef HAVE_READLINE_READLINE_H
+    add_history(line);
+#endif
+    args.rval().setString(JS_NewStringCopyZ(cx, line));
+    return true;
+}
+
+// clang-format off
+static JSFunctionSpec debugger_funcs[] = {
+    JS_FS("quit", quit, 1, GJS_MODULE_PROP_FLAGS),
+    JS_FS("readline", do_readline, 1, GJS_MODULE_PROP_FLAGS),
+    JS_FS_END
+};
+// clang-format on
+
+void gjs_context_setup_debugger_console(GjsContext* gjs) {
+    auto cx = static_cast<JSContext*>(gjs_context_get_native_context(gjs));
+    JSAutoRequest ar(cx);
+
+    JS::RootedObject debuggee(cx, gjs_get_import_global(cx));
+    JS::RootedObject debugger_compartment(cx, gjs_create_global_object(cx));
+
+    /* Enter compartment of the debugger and initialize it with the debuggee */
+    JSAutoCompartment compartment(cx, debugger_compartment);
+    JS::RootedObject debuggee_wrapper(cx, debuggee);
+    if (!JS_WrapObject(cx, &debuggee_wrapper)) {
+        gjs_log_exception(cx);
+        return;
+    }
+
+    JS::RootedValue v_wrapper(cx, JS::ObjectValue(*debuggee_wrapper));
+    if (!JS_SetProperty(cx, debugger_compartment, "debuggee", v_wrapper) ||
+        !JS_DefineFunctions(cx, debugger_compartment, debugger_funcs) ||
+        !gjs_define_global_properties(cx, debugger_compartment, "debugger"))
+        gjs_log_exception(cx);
+}
diff --git a/installed-tests/debugger-test.sh b/installed-tests/debugger-test.sh
new file mode 100755
index 00000000..5052d8b8
--- /dev/null
+++ b/installed-tests/debugger-test.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+if test "$GJS_USE_UNINSTALLED_FILES" = "1"; then
+    gjs="$TOP_BUILDDIR/gjs-console"
+else
+    gjs=gjs-console
+fi
+
+echo 1..1
+
+DEBUGGER_SCRIPT="$1"
+JS_SCRIPT="$1.js"
+EXPECTED_OUTPUT="$1.output"
+THE_DIFF=$("$gjs" -d "$JS_SCRIPT" < "$DEBUGGER_SCRIPT" | sed \
+    -e "s#$1#$(basename $1)#g" \
+    -e "s/0x[0-9a-f]\{4,16\}/0xADDR/g" \
+    | diff -u "$EXPECTED_OUTPUT" -)
+if test -n "$THE_DIFF"; then
+    echo "not ok 1 - $1"
+    echo "$THE_DIFF" | while read line; do echo "#$line"; done
+else
+    echo "ok 1 - $1"
+fi
diff --git a/installed-tests/debugger.test.in b/installed-tests/debugger.test.in
new file mode 100644
index 00000000..67d4c3ad
--- /dev/null
+++ b/installed-tests/debugger.test.in
@@ -0,0 +1,4 @@
+[Test]
+Type=session
+Exec=@pkglibexecdir@/installed-tests/debugger-test.sh @pkglibexecdir@/installed-tests/debugger/@name@
+Output=TAP
diff --git a/installed-tests/debugger/.eslintrc.json b/installed-tests/debugger/.eslintrc.json
new file mode 100644
index 00000000..b96322ab
--- /dev/null
+++ b/installed-tests/debugger/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+    "rules": {
+        "no-debugger": "off"
+    }
+}
diff --git a/installed-tests/debugger/backtrace.debugger b/installed-tests/debugger/backtrace.debugger
new file mode 100644
index 00000000..7b86c07c
--- /dev/null
+++ b/installed-tests/debugger/backtrace.debugger
@@ -0,0 +1,6 @@
+backtrace
+c
+bt
+c
+where
+q
diff --git a/installed-tests/debugger/backtrace.debugger.js b/installed-tests/debugger/backtrace.debugger.js
new file mode 100644
index 00000000..b5e9bc71
--- /dev/null
+++ b/installed-tests/debugger/backtrace.debugger.js
@@ -0,0 +1,8 @@
+debugger;
+[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]].forEach(array => {
+    debugger;
+    array.forEach(num => {
+        debugger;
+        print(num);
+    });
+});
diff --git a/installed-tests/debugger/backtrace.debugger.output 
b/installed-tests/debugger/backtrace.debugger.output
new file mode 100644
index 00000000..753d4fe4
--- /dev/null
+++ b/installed-tests/debugger/backtrace.debugger.output
@@ -0,0 +1,15 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at backtrace.debugger.js:1:0
+db> backtrace
+#0    toplevel at backtrace.debugger.js:1:0
+db> c
+Debugger statement, toplevel at backtrace.debugger.js:1:9
+db> bt
+#0    toplevel at backtrace.debugger.js:1:9
+db> c
+Debugger statement, <anonymous>([object Array], 0, [object Array]) at backtrace.debugger.js:3:4
+db> where
+#0    <anonymous>([object Array], 0, [object Array]) at backtrace.debugger.js:3:4
+#1    toplevel at backtrace.debugger.js:2:0
+db> q
+Program exited with code 0
diff --git a/installed-tests/debugger/breakpoint.debugger b/installed-tests/debugger/breakpoint.debugger
new file mode 100644
index 00000000..7a14f474
--- /dev/null
+++ b/installed-tests/debugger/breakpoint.debugger
@@ -0,0 +1,7 @@
+breakpoint 2
+break 4
+b 6
+c
+c
+c
+c
diff --git a/installed-tests/debugger/breakpoint.debugger.js b/installed-tests/debugger/breakpoint.debugger.js
new file mode 100644
index 00000000..1ca40305
--- /dev/null
+++ b/installed-tests/debugger/breakpoint.debugger.js
@@ -0,0 +1,7 @@
+print('1');
+print('2');
+function foo() {
+    print('Function foo');
+}
+print('3');
+foo();
diff --git a/installed-tests/debugger/breakpoint.debugger.output 
b/installed-tests/debugger/breakpoint.debugger.output
new file mode 100644
index 00000000..ffd8710a
--- /dev/null
+++ b/installed-tests/debugger/breakpoint.debugger.output
@@ -0,0 +1,20 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at breakpoint.debugger.js:1:0
+db> breakpoint 2
+Breakpoint 1 at breakpoint.debugger.js:2:0
+db> break 4
+Breakpoint 2 at breakpoint.debugger.js:4:4
+db> b 6
+Breakpoint 3 at breakpoint.debugger.js:6:0
+db> c
+1
+Breakpoint 1, toplevel at breakpoint.debugger.js:2:0
+db> c
+2
+Breakpoint 3, toplevel at breakpoint.debugger.js:6:0
+db> c
+3
+Breakpoint 2, foo() at breakpoint.debugger.js:4:4
+db> c
+Function foo
+Program exited with code 0
diff --git a/installed-tests/debugger/continue.debugger b/installed-tests/debugger/continue.debugger
new file mode 100644
index 00000000..bdbcae46
--- /dev/null
+++ b/installed-tests/debugger/continue.debugger
@@ -0,0 +1,3 @@
+continue
+cont
+c
diff --git a/installed-tests/debugger/continue.debugger.js b/installed-tests/debugger/continue.debugger.js
new file mode 100644
index 00000000..2af2400d
--- /dev/null
+++ b/installed-tests/debugger/continue.debugger.js
@@ -0,0 +1,2 @@
+debugger;
+debugger;
diff --git a/installed-tests/debugger/continue.debugger.output 
b/installed-tests/debugger/continue.debugger.output
new file mode 100644
index 00000000..38447a45
--- /dev/null
+++ b/installed-tests/debugger/continue.debugger.output
@@ -0,0 +1,8 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at continue.debugger.js:1:0
+db> continue
+Debugger statement, toplevel at continue.debugger.js:1:9
+db> cont
+Debugger statement, toplevel at continue.debugger.js:2:0
+db> c
+Program exited with code 0
diff --git a/installed-tests/debugger/delete.debugger b/installed-tests/debugger/delete.debugger
new file mode 100644
index 00000000..9fc4560d
--- /dev/null
+++ b/installed-tests/debugger/delete.debugger
@@ -0,0 +1,10 @@
+b 2
+b 3
+b 4
+b 5
+# Check that breakpoint 4 still remains after deleting 1-3
+delete 1
+del 2
+d 3
+c
+c
diff --git a/installed-tests/debugger/delete.debugger.js b/installed-tests/debugger/delete.debugger.js
new file mode 100644
index 00000000..9f6b2cd5
--- /dev/null
+++ b/installed-tests/debugger/delete.debugger.js
@@ -0,0 +1,5 @@
+print('1');
+print('2');
+print('3');
+print('4');
+print('5');
diff --git a/installed-tests/debugger/delete.debugger.output b/installed-tests/debugger/delete.debugger.output
new file mode 100644
index 00000000..7047d3d7
--- /dev/null
+++ b/installed-tests/debugger/delete.debugger.output
@@ -0,0 +1,26 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at delete.debugger.js:1:0
+db> b 2
+Breakpoint 1 at delete.debugger.js:2:0
+db> b 3
+Breakpoint 2 at delete.debugger.js:3:0
+db> b 4
+Breakpoint 3 at delete.debugger.js:4:0
+db> b 5
+Breakpoint 4 at delete.debugger.js:5:0
+db> # Check that breakpoint 4 still remains after deleting 1-3
+db> delete 1
+Breakpoint 1 at delete.debugger.js:2:0 deleted
+db> del 2
+Breakpoint 2 at delete.debugger.js:3:0 deleted
+db> d 3
+Breakpoint 3 at delete.debugger.js:4:0 deleted
+db> c
+1
+2
+3
+4
+Breakpoint 4, toplevel at delete.debugger.js:5:0
+db> c
+5
+Program exited with code 0
diff --git a/installed-tests/debugger/detach.debugger b/installed-tests/debugger/detach.debugger
new file mode 100644
index 00000000..8ddea54f
--- /dev/null
+++ b/installed-tests/debugger/detach.debugger
@@ -0,0 +1 @@
+detach
diff --git a/installed-tests/debugger/detach.debugger.js b/installed-tests/debugger/detach.debugger.js
new file mode 100644
index 00000000..10ed45c0
--- /dev/null
+++ b/installed-tests/debugger/detach.debugger.js
@@ -0,0 +1 @@
+print('hi');
diff --git a/installed-tests/debugger/detach.debugger.output b/installed-tests/debugger/detach.debugger.output
new file mode 100644
index 00000000..5417796b
--- /dev/null
+++ b/installed-tests/debugger/detach.debugger.output
@@ -0,0 +1,5 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at detach.debugger.js:1:0
+db> detach
+hi
+Program exited with code 0
diff --git a/installed-tests/debugger/down-up.debugger b/installed-tests/debugger/down-up.debugger
new file mode 100644
index 00000000..0b5a0e0d
--- /dev/null
+++ b/installed-tests/debugger/down-up.debugger
@@ -0,0 +1,12 @@
+c
+down
+up
+up
+up
+up
+up
+down
+dn
+dn
+dn
+c
diff --git a/installed-tests/debugger/down-up.debugger.js b/installed-tests/debugger/down-up.debugger.js
new file mode 100644
index 00000000..534099df
--- /dev/null
+++ b/installed-tests/debugger/down-up.debugger.js
@@ -0,0 +1,17 @@
+function a() {
+    b();
+}
+
+function b() {
+    c();
+}
+
+function c() {
+    d();
+}
+
+function d() {
+    debugger;
+}
+
+a();
diff --git a/installed-tests/debugger/down-up.debugger.output 
b/installed-tests/debugger/down-up.debugger.output
new file mode 100644
index 00000000..3ae26908
--- /dev/null
+++ b/installed-tests/debugger/down-up.debugger.output
@@ -0,0 +1,26 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at down-up.debugger.js:1:0
+db> c
+Debugger statement, d() at down-up.debugger.js:14:4
+db> down
+Youngest frame selected; you cannot go down.
+db> up
+#1    c() at down-up.debugger.js:10:4
+db> up
+#2    b() at down-up.debugger.js:6:4
+db> up
+#3    a() at down-up.debugger.js:2:4
+db> up
+#4    toplevel at down-up.debugger.js:17:0
+db> up
+Initial frame selected; you cannot go up.
+db> down
+#3    a() at down-up.debugger.js:2:4
+db> dn
+#2    b() at down-up.debugger.js:6:4
+db> dn
+#1    c() at down-up.debugger.js:10:4
+db> dn
+#0    d() at down-up.debugger.js:14:4
+db> c
+Program exited with code 0
diff --git a/installed-tests/debugger/finish.debugger b/installed-tests/debugger/finish.debugger
new file mode 100644
index 00000000..344ebe51
--- /dev/null
+++ b/installed-tests/debugger/finish.debugger
@@ -0,0 +1,5 @@
+c
+finish
+c
+fin
+c
diff --git a/installed-tests/debugger/finish.debugger.js b/installed-tests/debugger/finish.debugger.js
new file mode 100644
index 00000000..f8f8d010
--- /dev/null
+++ b/installed-tests/debugger/finish.debugger.js
@@ -0,0 +1,16 @@
+function foo() {
+    print('Print me');
+    debugger;
+    print('Print me also');
+}
+
+function bar() {
+    print('Print me');
+    debugger;
+    print('Print me also');
+    return 5;
+}
+
+foo();
+bar();
+print('Print me at the end');
diff --git a/installed-tests/debugger/finish.debugger.output b/installed-tests/debugger/finish.debugger.output
new file mode 100644
index 00000000..ae967996
--- /dev/null
+++ b/installed-tests/debugger/finish.debugger.output
@@ -0,0 +1,22 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at finish.debugger.js:1:0
+db> c
+Print me
+Debugger statement, foo() at finish.debugger.js:3:4
+db> finish
+Run till exit from foo() at finish.debugger.js:3:4
+Print me also
+No value returned.
+toplevel at finish.debugger.js:14:0
+db> c
+Print me
+Debugger statement, bar() at finish.debugger.js:9:4
+db> fin
+Run till exit from bar() at finish.debugger.js:9:4
+Print me also
+Value returned is:
+$1 = 5
+toplevel at finish.debugger.js:15:0
+db> c
+Print me at the end
+Program exited with code 0
diff --git a/installed-tests/debugger/frame.debugger b/installed-tests/debugger/frame.debugger
new file mode 100644
index 00000000..8ef8ed3a
--- /dev/null
+++ b/installed-tests/debugger/frame.debugger
@@ -0,0 +1,4 @@
+c
+frame 2
+f 1
+c
diff --git a/installed-tests/debugger/frame.debugger.js b/installed-tests/debugger/frame.debugger.js
new file mode 100644
index 00000000..0b6509b8
--- /dev/null
+++ b/installed-tests/debugger/frame.debugger.js
@@ -0,0 +1,9 @@
+function a() {
+    b();
+}
+
+function b() {
+    debugger;
+}
+
+a();
diff --git a/installed-tests/debugger/frame.debugger.output b/installed-tests/debugger/frame.debugger.output
new file mode 100644
index 00000000..83a8ff5d
--- /dev/null
+++ b/installed-tests/debugger/frame.debugger.output
@@ -0,0 +1,10 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at frame.debugger.js:1:0
+db> c
+Debugger statement, b() at frame.debugger.js:6:4
+db> frame 2
+#2    toplevel at frame.debugger.js:9:0
+db> f 1
+#1    a() at frame.debugger.js:2:4
+db> c
+Program exited with code 0
diff --git a/installed-tests/debugger/keys.debugger b/installed-tests/debugger/keys.debugger
new file mode 100644
index 00000000..473b2122
--- /dev/null
+++ b/installed-tests/debugger/keys.debugger
@@ -0,0 +1,4 @@
+c
+keys a
+k a
+c
diff --git a/installed-tests/debugger/keys.debugger.js b/installed-tests/debugger/keys.debugger.js
new file mode 100644
index 00000000..09c1e46f
--- /dev/null
+++ b/installed-tests/debugger/keys.debugger.js
@@ -0,0 +1,7 @@
+const a = {
+    foo: 1,
+    bar: null,
+    tres: undefined,
+};
+debugger;
+void a;
\ No newline at end of file
diff --git a/installed-tests/debugger/keys.debugger.output b/installed-tests/debugger/keys.debugger.output
new file mode 100644
index 00000000..7eb99138
--- /dev/null
+++ b/installed-tests/debugger/keys.debugger.output
@@ -0,0 +1,20 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at keys.debugger.js:1:0
+db> c
+Debugger statement, toplevel at keys.debugger.js:6:0
+db> keys a
+$1 = [object Array]
+[
+    "foo",
+    "bar",
+    "tres"
+]
+db> k a
+$2 = [object Array]
+[
+    "foo",
+    "bar",
+    "tres"
+]
+db> c
+Program exited with code 0
diff --git a/installed-tests/debugger/next.debugger b/installed-tests/debugger/next.debugger
new file mode 100644
index 00000000..33fba962
--- /dev/null
+++ b/installed-tests/debugger/next.debugger
@@ -0,0 +1,8 @@
+c
+next
+n
+n
+n
+n
+n
+n
diff --git a/installed-tests/debugger/next.debugger.js b/installed-tests/debugger/next.debugger.js
new file mode 100644
index 00000000..4162f2b9
--- /dev/null
+++ b/installed-tests/debugger/next.debugger.js
@@ -0,0 +1,11 @@
+function a() {
+    debugger;
+    b();
+    print('A line in a');
+}
+
+function b() {
+    print('A line in b');
+}
+
+a();
diff --git a/installed-tests/debugger/next.debugger.output b/installed-tests/debugger/next.debugger.output
new file mode 100644
index 00000000..e9a39427
--- /dev/null
+++ b/installed-tests/debugger/next.debugger.output
@@ -0,0 +1,24 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at next.debugger.js:1:0
+db> c
+Debugger statement, a() at next.debugger.js:2:4
+db> next
+a() at next.debugger.js:2:4
+db> n
+a() at next.debugger.js:3:4
+A line in b
+db> n
+a() at next.debugger.js:4:4
+A line in a
+db> n
+a() at next.debugger.js:5:0
+No value returned.
+db> n
+a() at next.debugger.js:5:0
+toplevel at next.debugger.js:11:0
+db> n
+toplevel at next.debugger.js:11:0
+No value returned.
+db> n
+toplevel at next.debugger.js:11:4
+Program exited with code 0
diff --git a/installed-tests/debugger/print.debugger b/installed-tests/debugger/print.debugger
new file mode 100644
index 00000000..2dd0387c
--- /dev/null
+++ b/installed-tests/debugger/print.debugger
@@ -0,0 +1,21 @@
+c
+# Simple types
+print a
+p b
+p c
+p d
+p e
+p f
+p g
+# Objects
+print h
+print/b h
+print/p h
+p i
+p/b i
+p j
+p k
+p/b k
+p l
+p m
+c
diff --git a/installed-tests/debugger/print.debugger.js b/installed-tests/debugger/print.debugger.js
new file mode 100644
index 00000000..05ee0d47
--- /dev/null
+++ b/installed-tests/debugger/print.debugger.js
@@ -0,0 +1,16 @@
+const {GObject} = imports.gi;
+const a = undefined;
+const b = null;
+const c = 42;
+const d = 'some string';
+const e = false;
+const f = true;
+const g = Symbol('foobar');
+const h = [1, 'money', 2, 'show', {three: 'to', 'get ready': 'go cat go'}];
+const i = {some: 'plain object', that: 'has keys'};
+const j = new Set([5, 6, 7]);
+const k = class J {};
+const l = new GObject.Object();
+const m = new Error('message');
+debugger;
+void (a, b, c, d, e, f, g, h, i, j, k, l, m);
diff --git a/installed-tests/debugger/print.debugger.output b/installed-tests/debugger/print.debugger.output
new file mode 100644
index 00000000..f9e4c063
--- /dev/null
+++ b/installed-tests/debugger/print.debugger.output
@@ -0,0 +1,86 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at print.debugger.js:1:0
+db> c
+Debugger statement, toplevel at print.debugger.js:15:0
+db> # Simple types
+db> print a
+$1 = undefined
+db> p b
+$2 = null
+db> p c
+$3 = 42
+db> p d
+$4 = "some string"
+db> p e
+$5 = false
+db> p f
+$6 = true
+db> p g
+$7 = Symbol("foobar")
+db> # Objects
+db> print h
+$8 = [object Array]
+[
+    1,
+    "money",
+    2,
+    "show",
+    {
+        "three": "to",
+        "get ready": "go cat go"
+    }
+]
+db> print/b h
+$9 = [object Array]
+{
+    "0": 1,
+    "1": "money",
+    "2": 2,
+    "3": "show",
+    "4": "(...)",
+    "length": 5
+}
+db> print/p h
+$10 = [object Array]
+[
+    1,
+    "money",
+    2,
+    "show",
+    {
+        "three": "to",
+        "get ready": "go cat go"
+    }
+]
+db> p i
+$11 = [object Object]
+{
+    "some": "plain object",
+    "that": "has keys"
+}
+db> p/b i
+$12 = [object Object]
+{
+    "some": "plain object",
+    "that": "has keys"
+}
+db> p j
+$13 = [object Set]
+{}
+db> p k
+$14 = [object Function]
+db> p/b k
+$15 = [object Function]
+{
+    "prototype": "(...)",
+    "length": 0,
+    "name": "J"
+}
+db> p l
+$16 = [object GObject_Object]
+[object instance proxy GIName:GObject.Object jsobj@0xADDR native@0xADDR]
+db> p m
+$17 = [object Error]
+Error: message
+db> c
+Program exited with code 0
diff --git a/installed-tests/debugger/quit.debugger b/installed-tests/debugger/quit.debugger
new file mode 100644
index 00000000..bca70f35
--- /dev/null
+++ b/installed-tests/debugger/quit.debugger
@@ -0,0 +1 @@
+q
diff --git a/installed-tests/debugger/quit.debugger.js b/installed-tests/debugger/quit.debugger.js
new file mode 100644
index 00000000..10ed45c0
--- /dev/null
+++ b/installed-tests/debugger/quit.debugger.js
@@ -0,0 +1 @@
+print('hi');
diff --git a/installed-tests/debugger/quit.debugger.output b/installed-tests/debugger/quit.debugger.output
new file mode 100644
index 00000000..3ed984b7
--- /dev/null
+++ b/installed-tests/debugger/quit.debugger.output
@@ -0,0 +1,4 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at quit.debugger.js:1:0
+db> q
+Program exited with code 0
diff --git a/installed-tests/debugger/return.debugger b/installed-tests/debugger/return.debugger
new file mode 100644
index 00000000..4613817d
--- /dev/null
+++ b/installed-tests/debugger/return.debugger
@@ -0,0 +1,8 @@
+b 2
+b 6
+b 10
+c
+return
+ret 5
+ret `${4 * 10 + 2} is the answer`
+c
diff --git a/installed-tests/debugger/return.debugger.js b/installed-tests/debugger/return.debugger.js
new file mode 100644
index 00000000..4133a185
--- /dev/null
+++ b/installed-tests/debugger/return.debugger.js
@@ -0,0 +1,15 @@
+function func1() {
+    return 1;
+}
+
+function func2() {
+    return 2;
+}
+
+function func3() {
+    return 3;
+}
+
+print(func1());
+print(func2());
+print(func3());
diff --git a/installed-tests/debugger/return.debugger.output b/installed-tests/debugger/return.debugger.output
new file mode 100644
index 00000000..f063d669
--- /dev/null
+++ b/installed-tests/debugger/return.debugger.output
@@ -0,0 +1,19 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at return.debugger.js:1:0
+db> b 2
+Breakpoint 1 at return.debugger.js:2:4
+db> b 6
+Breakpoint 2 at return.debugger.js:6:4
+db> b 10
+Breakpoint 3 at return.debugger.js:10:4
+db> c
+Breakpoint 1, func1() at return.debugger.js:2:4
+db> return
+undefined
+Breakpoint 2, func2() at return.debugger.js:6:4
+db> ret 5
+5
+Breakpoint 3, func3() at return.debugger.js:10:4
+db> ret `${4 * 10 + 2} is the answer`
+42 is the answer
+Program exited with code 0
diff --git a/installed-tests/debugger/set.debugger b/installed-tests/debugger/set.debugger
new file mode 100644
index 00000000..a8331d73
--- /dev/null
+++ b/installed-tests/debugger/set.debugger
@@ -0,0 +1,21 @@
+# Currently the only option is "pretty" for pretty-printing. Set doesn't yet
+# allow setting variables in the program.
+c
+p a
+set pretty 0
+p a
+set pretty 1
+p a
+set pretty off
+p a
+set pretty on
+p a
+set pretty false
+p a
+set pretty true
+p a
+set pretty no
+p a
+set pretty yes
+p a
+q
diff --git a/installed-tests/debugger/set.debugger.js b/installed-tests/debugger/set.debugger.js
new file mode 100644
index 00000000..e2a5f5ae
--- /dev/null
+++ b/installed-tests/debugger/set.debugger.js
@@ -0,0 +1,3 @@
+const a = {};
+debugger;
+void a;
diff --git a/installed-tests/debugger/set.debugger.output b/installed-tests/debugger/set.debugger.output
new file mode 100644
index 00000000..5b89910d
--- /dev/null
+++ b/installed-tests/debugger/set.debugger.output
@@ -0,0 +1,39 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at set.debugger.js:1:0
+db> # Currently the only option is "pretty" for pretty-printing. Set doesn't yet
+db> # allow setting variables in the program.
+db> c
+Debugger statement, toplevel at set.debugger.js:2:0
+db> p a
+$1 = [object Object]
+{}
+db> set pretty 0
+db> p a
+$2 = [object Object]
+db> set pretty 1
+db> p a
+$3 = [object Object]
+{}
+db> set pretty off
+db> p a
+$4 = [object Object]
+db> set pretty on
+db> p a
+$5 = [object Object]
+{}
+db> set pretty false
+db> p a
+$6 = [object Object]
+db> set pretty true
+db> p a
+$7 = [object Object]
+{}
+db> set pretty no
+db> p a
+$8 = [object Object]
+db> set pretty yes
+db> p a
+$9 = [object Object]
+{}
+db> q
+Program exited with code 0
diff --git a/installed-tests/debugger/step.debugger b/installed-tests/debugger/step.debugger
new file mode 100644
index 00000000..df197150
--- /dev/null
+++ b/installed-tests/debugger/step.debugger
@@ -0,0 +1,12 @@
+s
+s
+s
+s
+s
+s
+s
+s
+s
+s
+s
+s
diff --git a/installed-tests/debugger/step.debugger.js b/installed-tests/debugger/step.debugger.js
new file mode 100644
index 00000000..454406ee
--- /dev/null
+++ b/installed-tests/debugger/step.debugger.js
@@ -0,0 +1,10 @@
+function a() {
+    b();
+    print('A line in a');
+}
+
+function b() {
+    print('A line in b');
+}
+
+a();
diff --git a/installed-tests/debugger/step.debugger.output b/installed-tests/debugger/step.debugger.output
new file mode 100644
index 00000000..02092f2d
--- /dev/null
+++ b/installed-tests/debugger/step.debugger.output
@@ -0,0 +1,36 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at step.debugger.js:1:0
+db> s
+toplevel at step.debugger.js:1:0
+db> s
+toplevel at step.debugger.js:10:0
+entered frame: a() at step.debugger.js:2:4
+db> s
+a() at step.debugger.js:2:4
+entered frame: b() at step.debugger.js:7:4
+db> s
+b() at step.debugger.js:7:4
+A line in b
+db> s
+b() at step.debugger.js:8:0
+No value returned.
+db> s
+b() at step.debugger.js:8:0
+a() at step.debugger.js:2:4
+db> s
+a() at step.debugger.js:2:4
+db> s
+a() at step.debugger.js:3:4
+A line in a
+db> s
+a() at step.debugger.js:4:0
+No value returned.
+db> s
+a() at step.debugger.js:4:0
+toplevel at step.debugger.js:10:0
+db> s
+toplevel at step.debugger.js:10:0
+No value returned.
+db> s
+toplevel at step.debugger.js:10:4
+Program exited with code 0
diff --git a/installed-tests/debugger/throw.debugger b/installed-tests/debugger/throw.debugger
new file mode 100644
index 00000000..0c74e674
--- /dev/null
+++ b/installed-tests/debugger/throw.debugger
@@ -0,0 +1,4 @@
+c
+throw 'foobar' + 3.14;
+fin
+throw
diff --git a/installed-tests/debugger/throw.debugger.js b/installed-tests/debugger/throw.debugger.js
new file mode 100644
index 00000000..2ff94439
--- /dev/null
+++ b/installed-tests/debugger/throw.debugger.js
@@ -0,0 +1,10 @@
+function a() {
+    debugger;
+    return 5;
+}
+
+try {
+    a();
+} catch (e) {
+    print(`Exception: ${e}`);
+}
diff --git a/installed-tests/debugger/throw.debugger.output b/installed-tests/debugger/throw.debugger.output
new file mode 100644
index 00000000..cabf14ba
--- /dev/null
+++ b/installed-tests/debugger/throw.debugger.output
@@ -0,0 +1,21 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at throw.debugger.js:1:18
+db> c
+Debugger statement, a() at throw.debugger.js:2:4
+db> throw 'foobar' + 3.14;
+Unwinding due to exception. (Type 'c' to continue unwinding.)
+#0    a() at throw.debugger.js:2:4
+Exception value is:
+$1 = "foobar3.14"
+db> fin
+Run till exit from a() at throw.debugger.js:2:4
+Frame terminated by exception:
+$2 = "foobar3.14"
+(To rethrow it, type 'throw'.)
+Unwinding due to exception. (Type 'c' to continue unwinding.)
+#0    toplevel at throw.debugger.js:7:4
+Exception value is:
+$3 = "foobar3.14"
+db> throw
+Exception: foobar3.14
+Program exited with code 0
diff --git a/installed-tests/debugger/until.debugger b/installed-tests/debugger/until.debugger
new file mode 100644
index 00000000..fb077881
--- /dev/null
+++ b/installed-tests/debugger/until.debugger
@@ -0,0 +1,4 @@
+until 3
+upto 5
+u 7
+c
diff --git a/installed-tests/debugger/until.debugger.js b/installed-tests/debugger/until.debugger.js
new file mode 100644
index 00000000..8a7b0671
--- /dev/null
+++ b/installed-tests/debugger/until.debugger.js
@@ -0,0 +1,7 @@
+print('1');
+print('2');
+print('3');
+(function () {
+    print('4');
+})();
+print('5');
diff --git a/installed-tests/debugger/until.debugger.output b/installed-tests/debugger/until.debugger.output
new file mode 100644
index 00000000..856b85dc
--- /dev/null
+++ b/installed-tests/debugger/until.debugger.output
@@ -0,0 +1,18 @@
+GJS debugger. Type "help" for help
+Debugger statement, toplevel at until.debugger.js:1:0
+db> until 3
+toplevel at until.debugger.js:1:0
+1
+2
+db> upto 5
+toplevel at until.debugger.js:3:0
+3
+entered frame: <anonymous>() at until.debugger.js:5:4
+db> u 7
+<anonymous>() at until.debugger.js:5:4
+4
+No value returned.
+toplevel at until.debugger.js:7:0
+db> c
+5
+Program exited with code 0
diff --git a/modules/_bootstrap/debugger.js b/modules/_bootstrap/debugger.js
new file mode 100644
index 00000000..6c0f2265
--- /dev/null
+++ b/modules/_bootstrap/debugger.js
@@ -0,0 +1,749 @@
+/* global Debugger, debuggee, quit, readline, uneval */
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/*
+ * This is a simple command-line debugger for GJS programs. It is based on
+ * jorendb, which is a toy debugger for shell-js programs included in the
+ * SpiderMonkey source.
+ *
+ * To run it: gjs -d path/to/file.js
+ * Execution will stop at debugger statements (one will be inserted at the start
+ * of the program), and you'll get a prompt.
+ */
+
+// Debugger state.
+var focusedFrame = null;
+var topFrame = null;
+var debuggeeValues = {};
+var nextDebuggeeValueIndex = 1;
+var lastExc = null;
+var options = {pretty: true};
+var breakpoints = [undefined];  // Breakpoint numbers start at 1
+
+// Cleanup functions to run when we next re-enter the repl.
+var replCleanups = [];
+
+// Convert a debuggee value v to a string.
+function dvToString(v) {
+    if (typeof v === 'undefined')
+        return 'undefined';  // uneval(undefined) === '(void 0)', confusing
+    if (v === null)
+        return 'null';  // typeof null === 'object', so avoid that case
+    return (typeof v !== 'object' || v === null) ? uneval(v) : `[object ${v.class}]`;
+}
+
+function summarizeObject(dv) {
+    const obj = {};
+    for (var name of dv.getOwnPropertyNames()) {
+        var v = dv.getOwnPropertyDescriptor(name).value;
+        if (v instanceof Debugger.Object) {
+            v = '(...)';
+        }
+        obj[name] = v;
+    }
+    return obj;
+}
+
+function debuggeeValueToString(dv, style = {pretty: options.pretty}) {
+    const dvrepr = dvToString(dv);
+    if (!style.pretty || dv === null || typeof dv !== 'object')
+        return [dvrepr, undefined];
+
+    if (['Error', 'GIRespositoryNamespace', 'GObject_Object'].includes(dv.class)) {
+        const errval = debuggeeGlobalWrapper.executeInGlobalWithBindings(
+            'v.toString()', {v: dv});
+        return [dvrepr, errval['return']];
+    }
+
+    if (style.brief)
+        return [dvrepr, JSON.stringify(summarizeObject(dv), null, 4)];
+
+    const str = debuggeeGlobalWrapper.executeInGlobalWithBindings(
+        'JSON.stringify(v, null, 4)', {v: dv});
+    if ('throw' in str) {
+        if (style.noerror)
+            return [dvrepr, undefined];
+
+        const substyle = {};
+        Object.assign(substyle, style);
+        substyle.noerror = true;
+        return [dvrepr, debuggeeValueToString(str.throw, substyle)];
+    }
+
+    return [dvrepr, str['return']];
+}
+
+function showDebuggeeValue(dv, style = {pretty: options.pretty}) {
+    const i = nextDebuggeeValueIndex++;
+    debuggeeValues[`$${i}`] = dv;
+    const [brief, full] = debuggeeValueToString(dv, style);
+    print(`$${i} = ${brief}`);
+    if (full !== undefined)
+        print(full);
+}
+
+Object.defineProperty(Debugger.Frame.prototype, 'num', {
+    configurable: true,
+    enumerable: false,
+    get: function() {
+        let i = 0;
+        for (var f = topFrame; f && f !== this; f = f.older)
+            i++;
+        return f === null ? undefined : i;
+    }
+});
+
+Debugger.Frame.prototype.describeFrame = function() {
+    if (this.type == 'call')
+        return `${this.callee.name || '<anonymous>'}(${
+            this.arguments.map(dvToString).join(', ')})`;
+    else if (this.type == 'global')
+        return 'toplevel';
+    else
+        return this.type + ' code';
+};
+
+Debugger.Frame.prototype.describePosition = function() {
+    if (this.script)
+        return this.script.describeOffset(this.offset);
+    return null;
+};
+
+Debugger.Frame.prototype.describeFull = function() {
+    const fr = this.describeFrame();
+    const pos = this.describePosition();
+    if (pos)
+        return `${fr} at ${pos}`;
+    return fr;
+};
+
+Object.defineProperty(Debugger.Frame.prototype, 'line', {
+    configurable: true,
+    enumerable: false,
+    get: function() {
+        if (this.script)
+            return this.script.getOffsetLocation(this.offset).lineNumber;
+        else
+            return null;
+    }
+});
+
+Debugger.Script.prototype.describeOffset = function describeOffset(offset) {
+    const {lineNumber, columnNumber} = this.getOffsetLocation(offset);
+    const url = this.url || '<unknown>';
+    return `${url}:${lineNumber}:${columnNumber}`;
+};
+
+function showFrame(f, n) {
+    if (f === undefined || f === null) {
+        f = focusedFrame;
+        if (f === null) {
+            print('No stack.');
+            return;
+        }
+    }
+    if (n === undefined) {
+        n = f.num;
+        if (n === undefined)
+            throw new Error('Internal error: frame not on stack');
+    }
+
+    print(`#${n.toString().padEnd(4)} ${f.describeFull()}`);
+}
+
+function saveExcursion(fn) {
+    const tf = topFrame, ff = focusedFrame;
+    try {
+        return fn();
+    } finally {
+        topFrame = tf;
+        focusedFrame = ff;
+    }
+}
+
+// Accept debugger commands starting with '#' so that scripting the debugger
+// can be annotated
+function commentCommand(comment) {
+    void comment;
+}
+
+// Evaluate an expression in the Debugger global - used for debugging the
+// debugger
+function evalCommand(expr) {
+    eval(expr);
+}
+
+function quitCommand() {
+    dbg.enabled = false;
+    quit(0);
+}
+
+function backtraceCommand() {
+    if (topFrame === null)
+        print('No stack.');
+    for (var i = 0, f = topFrame; f; i++, f = f.older)
+        showFrame(f, i);
+}
+
+function setCommand(rest) {
+    var space = rest.indexOf(' ');
+    if (space == -1) {
+        print('Invalid set <option> <value> command');
+    } else {
+        var name = rest.substr(0, space);
+        var value = rest.substr(space + 1);
+
+        var yes = ['1', 'yes', 'true', 'on'];
+        var no = ['0', 'no', 'false', 'off'];
+
+        if (yes.includes(value))
+            options[name] = true;
+        else if (no.includes(value))
+            options[name] = false;
+        else
+            options[name] = value;
+    }
+}
+
+function splitPrintOptions(s, style) {
+    const m = /^\/(\w+)/.exec(s);
+    if (!m)
+        return [s, style];
+    if (m[1].indexOf('p') != -1)
+        style.pretty = true;
+    if (m[1].indexOf('b') != -1)
+        style.brief = true;
+    return [s.substr(m[0].length).trimLeft(), style];
+}
+
+function doPrint(expr, style) {
+    // This is the real deal.
+    const cv = saveExcursion(
+        () => focusedFrame === null
+            ? debuggeeGlobalWrapper.executeInGlobalWithBindings(expr, debuggeeValues)
+            : focusedFrame.evalWithBindings(expr, debuggeeValues));
+    if (cv === null) {
+        if (!dbg.enabled)
+            return [cv];
+        print('Debuggee died.');
+    } else if ('return' in cv) {
+        if (!dbg.enabled)
+            return [undefined];
+        showDebuggeeValue(cv['return'], style);
+    } else {
+        if (!dbg.enabled)
+            return [cv];
+        print("Exception caught. (To rethrow it, type 'throw'.)");
+        lastExc = cv.throw;
+        showDebuggeeValue(lastExc, style);
+    }
+}
+
+function printCommand(rest) {
+    var [expr, style] = splitPrintOptions(rest, {pretty: options.pretty});
+    return doPrint(expr, style);
+}
+
+function keysCommand(rest) {
+    return doPrint(`Object.keys(${rest})`);
+}
+
+function detachCommand() {
+    dbg.enabled = false;
+    return [undefined];
+}
+
+function continueCommand() {
+    if (focusedFrame === null) {
+        print('No stack.');
+        return;
+    }
+    return [undefined];
+}
+
+function throwCommand(rest) {
+    if (focusedFrame !== topFrame) {
+        print("To throw, you must select the newest frame (use 'frame 0').");
+        return;
+    } else if (focusedFrame === null) {
+        print('No stack.');
+        return;
+    } else if (rest === '') {
+        return [{throw: lastExc}];
+    } else {
+        var cv = saveExcursion(() => focusedFrame.eval(rest));
+        if (cv === null) {
+            if (!dbg.enabled)
+                return [cv];
+            print('Debuggee died while determining what to throw. Stopped.');
+        } else if ('return' in cv) {
+            return [{throw: cv['return']}];
+        } else {
+            if (!dbg.enabled)
+                return [cv];
+            print('Exception determining what to throw. Stopped.');
+            showDebuggeeValue(cv.throw);
+        }
+        return;
+    }
+}
+
+function frameCommand(rest) {
+    let n, f;
+    if (rest.match(/[0-9]+/)) {
+        n = +rest;
+        f = topFrame;
+        if (f === null) {
+            print('No stack.');
+            return;
+        }
+        for (let i = 0; i < n && f; i++) {
+            if (!f.older) {
+                print(`There is no frame ${rest}.`);
+                return;
+            }
+            f.older.younger = f;
+            f = f.older;
+        }
+        focusedFrame = f;
+        showFrame(f, n);
+    } else if (rest === '') {
+        if (topFrame === null) {
+            print('No stack.');
+        } else {
+            showFrame();
+        }
+    } else {
+        print('do what now?');
+    }
+}
+
+function upCommand() {
+    if (focusedFrame === null)
+        print('No stack.');
+    else if (focusedFrame.older === null)
+        print('Initial frame selected; you cannot go up.');
+    else {
+        focusedFrame.older.younger = focusedFrame;
+        focusedFrame = focusedFrame.older;
+        showFrame();
+    }
+}
+
+function downCommand() {
+    if (focusedFrame === null)
+        print('No stack.');
+    else if (!focusedFrame.younger)
+        print('Youngest frame selected; you cannot go down.');
+    else {
+        focusedFrame = focusedFrame.younger;
+        showFrame();
+    }
+}
+
+function returnCommand(rest) {
+    const f = focusedFrame;
+    if (f !== topFrame) {
+        print("To return, you must select the newest frame (use 'frame 0').");
+    } else if (f === null) {
+        print('Nothing on the stack.');
+    } else if (rest === '') {
+        return [{return: undefined}];
+    } else {
+        const cv = saveExcursion(() => f.eval(rest));
+        if (cv === null) {
+            if (!dbg.enabled)
+                return [cv];
+            print('Debuggee died while determining what to return. Stopped.');
+        } else if ('return' in cv) {
+            return [{return: cv['return']}];
+        } else {
+            if (!dbg.enabled)
+                return [cv];
+            print('Error determining what to return. Stopped.');
+            showDebuggeeValue(cv.throw);
+        }
+    }
+}
+
+function printPop(c) {
+    if (c['return']) {
+        print('Value returned is:');
+        showDebuggeeValue(c['return'], {brief: true});
+    } else if (c['throw']) {
+        print('Frame terminated by exception:');
+        showDebuggeeValue(c['throw']);
+        print("(To rethrow it, type 'throw'.)");
+        lastExc = c['throw'];
+    } else {
+        print('No value returned.');
+    }
+}
+
+// Set |prop| on |obj| to |value|, but then restore its current value
+// when we next enter the repl.
+function setUntilRepl(obj, prop, value) {
+    var saved = obj[prop];
+    obj[prop] = value;
+    replCleanups.push(() => {
+        obj[prop] = saved;
+    });
+}
+
+function doStepOrNext(kind) {
+    if (topFrame === null) {
+        print('Program not running.');
+        return;
+    }
+
+    // TODO: step or finish from any frame in the stack, not just the top one
+    var startFrame = topFrame;
+    var startLine = startFrame.line;
+    if (kind.finish)
+        print(`Run till exit from ${startFrame.describeFull()}`);
+    else
+        print(startFrame.describeFull());
+
+    function stepPopped(completion) {
+        // Note that we're popping this frame; we need to watch for
+        // subsequent step events on its caller.
+        this.reportedPop = true;
+        printPop(completion);
+        topFrame = focusedFrame = this;
+        if (kind.finish || kind.until) {
+            // We want to continue, but this frame is going to be invalid as
+            // soon as this function returns, which will make the replCleanups
+            // assert when it tries to access the dead frame's 'onPop'
+            // property. So clear it out now while the frame is still valid,
+            // and trade it for an 'onStep' callback on the frame we're popping to.
+            preReplCleanups();
+            setUntilRepl(this.older, 'onStep', stepStepped);
+            return undefined;
+        }
+        return repl();
+    }
+
+    function stepEntered(newFrame) {
+        print('entered frame: ' + newFrame.describeFull());
+        if (!kind.until || newFrame.line == kind.stopLine) {
+            topFrame = focusedFrame = newFrame;
+            return repl();
+        }
+        if (kind.until)
+            setUntilRepl(newFrame, 'onStep', stepStepped);
+    }
+
+    function stepStepped() {
+        // print('stepStepped: ' + this.describeFull());
+        var stop = false;
+
+        if (kind.finish) {
+            // 'finish' set a one-time onStep for stopping at the frame it
+            // wants to return to
+            stop = true;
+        } else if (kind.until) {
+            // running until a given line is reached
+            if (this.line == kind.stopLine)
+                stop = true;
+        } else {
+            // regular step; stop whenever the line number changes
+            if ((this.line != startLine) || (this != startFrame))
+                stop = true;
+        }
+
+        if (stop) {
+            topFrame = focusedFrame = this;
+            if (focusedFrame != startFrame)
+                print(focusedFrame.describeFull());
+            return repl();
+        }
+
+        // Otherwise, let execution continue.
+        return undefined;
+    }
+
+    if (kind.step || kind.until)
+        setUntilRepl(dbg, 'onEnterFrame', stepEntered);
+
+    // If we're stepping after an onPop, watch for steps and pops in the
+    // next-older frame; this one is done.
+    var stepFrame = startFrame.reportedPop ? startFrame.older : startFrame;
+    if (!stepFrame || !stepFrame.script)
+        stepFrame = null;
+    if (stepFrame) {
+        if (!kind.finish)
+            setUntilRepl(stepFrame, 'onStep', stepStepped);
+        setUntilRepl(stepFrame, 'onPop', stepPopped);
+    }
+
+    // Let the program continue!
+    return [undefined];
+}
+
+function stepCommand() {
+    return doStepOrNext({step: true});
+}
+
+function nextCommand() {
+    return doStepOrNext({next: true});
+}
+
+function finishCommand() {
+    return doStepOrNext({finish: true});
+}
+
+function untilCommand(line) {
+    return doStepOrNext({until: true, stopLine: Number(line)});
+}
+
+function findBreakpointOffsets(line, currentScript) {
+    const offsets = currentScript.getLineOffsets(line);
+    if (offsets.length !== 0)
+        return [{script: currentScript, offsets}];
+
+    const scripts = dbg.findScripts({line, url: currentScript.url});
+    if (scripts.length === 0)
+        return [];
+
+    return scripts
+        .map(script => ({script, offsets: script.getLineOffsets(line)}))
+        .filter(({offsets}) => offsets.length !== 0);
+}
+
+class BreakpointHandler {
+    constructor(num, script, offset) {
+        this.num = num;
+        this.script = script;
+        this.offset = offset;
+    }
+
+    hit(frame) {
+        return saveExcursion(() => {
+            topFrame = focusedFrame = frame;
+            print(`Breakpoint ${this.num}, ${frame.describeFull()}`);
+            return repl();
+        });
+    }
+
+    toString() {
+        return `Breakpoint ${this.num} at ${this.script.describeOffset(this.offset)}`;
+    }
+}
+
+function breakpointCommand(where) {
+    // Only handles line numbers of the current file
+    // TODO: make it handle function names and other files
+    const line = Number(where);
+    const possibleOffsets = findBreakpointOffsets(line, focusedFrame.script);
+
+    if (possibleOffsets.length === 0) {
+        print('Unable to break at line ' + where);
+        return;
+    }
+
+    possibleOffsets.forEach(({script, offsets}) => {
+        offsets.forEach(offset => {
+            const bp = new BreakpointHandler(breakpoints.length, script, offset);
+            script.setBreakpoint(offset, bp);
+            breakpoints.push(bp);
+            print(bp);
+        });
+    });
+}
+
+function deleteCommand(breaknum) {
+    const bp = breakpoints[breaknum];
+
+    if (bp === undefined) {
+        print(`Breakpoint ${breaknum} already deleted.`);
+        return;
+    }
+
+    const {script, offset} = bp;
+    script.clearBreakpoint(bp, offset);
+    breakpoints[breaknum] = undefined;
+    print(`${bp} deleted`);
+}
+
+// Build the table of commands.
+var commands = {};
+// clang-format off
+var commandArray = [
+    backtraceCommand, 'bt', 'where',
+    breakpointCommand, 'b', 'break',
+    commentCommand, '#',
+    continueCommand, 'c', 'cont',
+    deleteCommand, 'd', 'del',
+    detachCommand,
+    downCommand, 'dn',
+    evalCommand, '!',
+    finishCommand, 'fin',
+    frameCommand, 'f',
+    helpCommand, 'h',
+    keysCommand, 'k',
+    nextCommand, 'n',
+    printCommand, 'p',
+    quitCommand, 'q',
+    returnCommand, 'ret',
+    setCommand,
+    stepCommand, 's',
+    throwCommand, 't',
+    untilCommand, 'u', 'upto',
+    upCommand,
+];
+// clang-format on
+var currentCmd = null;
+for (var i = 0; i < commandArray.length; i++) {
+    var cmd = commandArray[i];
+    if (typeof cmd === 'string')
+        commands[cmd] = currentCmd;
+    else
+        currentCmd = commands[cmd.name.replace(/Command$/, '')] = cmd;
+}
+
+function helpCommand() {
+    print('Available commands:');
+    var printcmd = function(group) {
+        print('  ' + group.join(', '));
+    };
+
+    var group = [];
+    for (var cmd of commandArray) {
+        if (typeof cmd === 'string') {
+            group.push(cmd);
+        } else {
+            // Don't print commands for debugging the debugger
+            if (['comment', 'eval'].includes(group[0]))
+                continue;
+            if (group.length)
+                printcmd(group);
+            group = [cmd.name.replace(/Command$/, '')];
+        }
+    }
+    printcmd(group);
+}
+
+// Break cmd into two parts: its first word and everything else. If it begins
+// with punctuation, treat that as a separate word. The first word is
+// terminated with whitespace or the '/' character. So:
+//
+//   print x         => ['print', 'x']
+//   print           => ['print', '']
+//   !print x        => ['!', 'print x']
+//   ?!wtf!?         => ['?', '!wtf!?']
+//   print/b x       => ['print', '/b x']
+//
+function breakcmd(cmd) {
+    cmd = cmd.trimLeft();
+    if ("!@#$%^&*_+=/?.,<>:;'\"".indexOf(cmd.substr(0, 1)) != -1)
+        return [cmd.substr(0, 1), cmd.substr(1).trimLeft()];
+    var m = /\s+|(?=\/)/.exec(cmd);
+    if (m === null)
+        return [cmd, ''];
+    return [cmd.slice(0, m.index), cmd.slice(m.index + m[0].length)];
+}
+
+function runcmd(cmd) {
+    var pieces = breakcmd(cmd);
+    if (pieces[0] === '')
+        return undefined;
+
+    var first = pieces[0], rest = pieces[1];
+    if (!commands.hasOwnProperty(first)) {
+        print("unrecognized command '" + first + "'");
+        return undefined;
+    }
+
+    cmd = commands[first];
+    if (cmd.length === 0 && rest !== '') {
+        print('this command cannot take an argument');
+        return undefined;
+    }
+
+    return cmd(rest);
+}
+
+function preReplCleanups() {
+    while (replCleanups.length > 0)
+        replCleanups.pop()();
+}
+
+var prevcmd;
+function repl() {
+    preReplCleanups();
+
+    var cmd;
+    for (;;) {
+        cmd = readline();
+        if (cmd === null)
+            return null;
+        else if (cmd === '')
+            cmd = prevcmd;
+
+        try {
+            prevcmd = cmd;
+            var result = runcmd(cmd);
+            if (result === undefined)
+                void result;  // do nothing, return to prompt
+            else if (Array.isArray(result))
+                return result[0];
+            else if (result === null)
+                return null;
+            else
+                throw new Error(
+                    `Internal error: result of runcmd wasn't array or undefined: ${result}`);
+        } catch (exc) {
+            logError(exc, '*** Internal error: exception in the debugger code');
+        }
+    }
+}
+
+var dbg = new Debugger();
+dbg.onNewPromise = function({promiseID, promiseAllocationSite}) {
+    const site = promiseAllocationSite.toString().split('\n')[0];
+    print(`Promise ${promiseID} started from ${site}`);
+    return undefined;
+};
+dbg.onPromiseSettled = function(promise) {
+    let message = `Promise ${promise.promiseID} ${promise.promiseState} `;
+    message += `after ${promise.promiseTimeToResolution.toFixed(3)} ms`;
+    let brief, full;
+    if (promise.promiseState === 'fulfilled' && typeof promise.promiseValue !== 'undefined') {
+        [brief, full] = debuggeeValueToString(promise.promiseValue);
+        message += ` with ${brief}`;
+    } else if (promise.promiseState === 'rejected' &&
+               typeof promise.promiseReason !== 'undefined') {
+        [brief, full] = debuggeeValueToString(promise.promiseReason);
+        message += ` with ${brief}`;
+    }
+    print(message);
+    if (full !== undefined)
+        print(full);
+    return undefined;
+};
+dbg.onDebuggerStatement = function(frame) {
+    return saveExcursion(() => {
+        topFrame = focusedFrame = frame;
+        print(`Debugger statement, ${frame.describeFull()}`);
+        return repl();
+    });
+};
+dbg.onExceptionUnwind = function(frame, value) {
+    return saveExcursion(() => {
+        topFrame = focusedFrame = frame;
+        print("Unwinding due to exception. (Type 'c' to continue unwinding.)");
+        showFrame();
+        print('Exception value is:');
+        showDebuggeeValue(value);
+        return repl();
+    });
+};
+
+var debuggeeGlobalWrapper = dbg.addDebuggee(debuggee);
+
+print('GJS debugger. Type "help" for help');
diff --git a/modules/modules.gresource.xml b/modules/modules.gresource.xml
index 72cad226..85b70c5c 100644
--- a/modules/modules.gresource.xml
+++ b/modules/modules.gresource.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/gjs">
+    <file>modules/_bootstrap/debugger.js</file>
     <file>modules/_bootstrap/default.js</file>
     <file>modules/_bootstrap/coverage.js</file>
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]