Subject: bug#66575: [PATCH] Gud lldb support

From: Gerd Möllmann <[email-masked]>
Date: Tue, 17 Oct 2023 12:03:01 +0200

Mattias Engdegård <[email-masked]> writes:

> 16 okt. 2023 kl. 16.09 skrev Gerd Möllmann <[email-masked]>:
>
>>> So what is the currently blessed way to communicate with LLDB?
>> 
>> Don't really know, I'm afraid.
>
> Maybe it's worth a try anyway, since lldb-mi still seems maintained as a separate tool.
> (But don't let it hold up your contribution -- it's useful on its
> own.)

Maybe it would be worth it, don't know, but lldb-mi isn't even
installable via Homebrew at the moment...

>>> If thñe file name can have an absolute directory part, then it may run
>>> into trouble on Windows (C:\Some\Dir\File.c).
>> 
>> I guess I'll leave that as an exercise for someone having Windows :-).
>
> I don't, but we could try to be nice anyway. What about replacing
>
>   (group (1+ (not ":")))
>
> with something like
>
>   (group (opt (opt "\\\\?\\")         ; Windows fun
>               (in "a-zA-Z") ":")      ; floppy letter
>          (1+ (not ":")))
>
> and ignore NTFS Alternate Data Streams, as we ignore the possibility
> of colons in file names on other systems?

Hm.  I'd rather see someone run lldb on Windows, and tell us if it even
prints absolute file names.  It doesn't seem to do that on macOS.

> Onto your most recent patch -- looks fine in general, only a few notes:
>
>>  * Editing Changes in Emacs 30.1
>>  
>> ++++
>> +** New command 'lldb'.
>
> The text can be rearranged later on but this entry probably belongs under 'Changes in Specialized Modes and Packages'.
>
>> +Run the LLDB debugger, analogous to the 'gud-gdb' command.  Note that
>> +might want to add these settings to your .lldbinit file, to reduce the
>
> Missing 'you' in the second sentence?
>
>> +output in the LLDB output when stepping through source files.
>> +
>> +settings set stop-line-count-before 0
>> +settings set stop-line-count-after 0
>
> Indent these lines by 4 spaces (NEWS has its own formatting rules).

Fixed, and moved.  Thanks.

> But shouldn't your new code issue these commands to lldb automatically? The user might not always run lldb under Emacs.
>

I've thought about doing that as an initialization, but then it would
have to be manually undone once lldb is running, since .lldbinit is
loaded first, AFAIK, if enabled (default = no), which one could
suppress and load it later, but that might also not be what the user
wants. And so on...

So, to keep things simple, I'd rather leave the user's settings alone.
It doesn't make a difference, functionally.

>> +  ;; First line is the print command itself.
>> +  (unless (string-match (regexp-quote (gud-tooltip-print-command ""))
>
> Isn't this just (string-search ...) ? Or did you want a more precise
> match, in which case it probably should be anchored in some way and
> not just a substring search?

That's true.  I was too lazy to change that after experimenting with
this stuff :-).  I've changed that.

>> +# This is for M-x lldb: don't show source lines when stopping.
>> +settings set stop-line-count-before 0
>> +settings set stop-line-count-after 0
>
> As mentioned above, I'd hope we could do without this.

New version attached.  I guess I'll commit that soon :-).

>From 2172061b1f73705bce22970cc0921657912e0b61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gerd=20M=C3=B6llmann?= <[email-masked]>
Date: Mon, 16 Oct 2023 13:54:02 +0200
Subject: [PATCH] Gud lldb support (bug#66575)

* lisp/progmodes/gud.el (lldb): New command.
* etc/NEWS: Mention M-x lldb.
* src/.lldbinit: Show no souece lines on stop.
* doc/building.texi: Mention LLDB.
---
 doc/emacs/building.texi |   6 +-
 etc/NEWS                |   9 ++
 lisp/progmodes/gud.el   | 194 +++++++++++++++++++++++++++++++++++-----
 src/.lldbinit           |   4 +
 4 files changed, 188 insertions(+), 25 deletions(-)

diff --git a/doc/emacs/building.texi b/doc/emacs/building.texi
index 2a98bffdc2d..a2639ce6d3e 100644
--- a/doc/emacs/building.texi
+++ b/doc/emacs/building.texi
@@ -567,7 +567,7 @@ Debuggers
 
 The GUD (Grand Unified Debugger) library provides an Emacs interface
 to a wide variety of symbolic debuggers.  It can run the GNU Debugger
-(GDB), as well as DBX, SDB, XDB, Guile REPL debug commands, Perl's
+(GDB), as well as LLDB, DBX, SDB, XDB, Guile REPL debug commands, Perl's
 debugging mode, the Python debugger PDB, and the Java Debugger JDB.
 
   Emacs provides a special interface to GDB, which uses extra Emacs
@@ -609,6 +609,10 @@ Starting GUD
 The other commands in this list do the same, for other debugger
 programs.
 
+@item M-x lldb
+@findex lldb
+Run the LLDB debugger.
+
 @item M-x perldb
 @findex perldb
 Run the Perl interpreter in debug mode.
diff --git a/etc/NEWS b/etc/NEWS
index 3bd47a0112b..83612f84e17 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -283,6 +283,15 @@ functions in CJK locales.
 
 * Changes in Specialized Modes and Packages in Emacs 30.1
 
++++
+** New command 'lldb'.
+Run the LLDB debugger, analogous to the 'gud-gdb' command.  Note that
+you might want to add these settings to your .lldbinit file, to reduce
+the output in the LLDB output when stepping through source files.
+
+    settings set stop-line-count-before 0
+    settings set stop-line-count-after 0
+
 ** gdb-mi
 
 ---
diff --git a/lisp/progmodes/gud.el b/lisp/progmodes/gud.el
index d4b954a7203..ea5a3580629 100644
--- a/lisp/progmodes/gud.el
+++ b/lisp/progmodes/gud.el
@@ -80,7 +80,7 @@ hl-line-sticky-flag
 
 (defgroup gud nil
   "The \"Grand Unified Debugger\" interface.
-Supported debuggers include gdb, sdb, dbx, xdb, perldb,
+Supported debuggers include gdb, lldb, sdb, dbx, xdb, perldb,
 pdb (Python), and jdb."
   :group 'processes
   :group 'tools)
@@ -173,13 +173,13 @@ gud-text-menu-bar-map
   "<next>" `(,(propertize "next" 'face 'font-lock-doc-face) . gud-next)
   "<until>" `(menu-item
               ,(propertize "until" 'face 'font-lock-doc-face) gud-until
-              :visible (memq gud-minor-mode '(gdbmi gdb perldb)))
+              :visible (memq gud-minor-mode '(gdbmi gdb lldb perldb)))
   "<cont>" `(menu-item
            ,(propertize "cont" 'face 'font-lock-doc-face) gud-cont
            :visible (not (eq gud-minor-mode 'gdbmi)))
   "<run>" `(menu-item
           ,(propertize "run" 'face 'font-lock-doc-face) gud-run
-          :visible (memq gud-minor-mode '(gdbmi gdb dbx jdb)))
+          :visible (memq gud-minor-mode '(gdbmi gdb lldb dbx jdb)))
   "<go>" `(menu-bar-item
          ,(propertize " go " 'face 'font-lock-doc-face) gud-go
          :visible (and (eq gud-minor-mode 'gdbmi)
@@ -231,13 +231,13 @@ gud-menu-map
      :enable (not gud-running)]
     ["Next Instruction" gud-nexti
      :enable (not gud-running)
-     :visible (memq gud-minor-mode '(gdbmi gdb dbx))]
+     :visible (memq gud-minor-mode '(gdbmi gdb lldb dbx))]
     ["Step Instruction" gud-stepi
      :enable (not gud-running)
-     :visible (memq gud-minor-mode '(gdbmi gdb dbx))]
+     :visible (memq gud-minor-mode '(gdbmi gdb lldb dbx))]
     ["Finish Function" gud-finish
      :enable (not gud-running)
-     :visible (memq gud-minor-mode '(gdbmi gdb guiler xdb jdb pdb))]
+     :visible (memq gud-minor-mode '(gdbmi gdb lldb guiler xdb jdb pdb))]
     ["Watch Expression" gud-watch
      :enable (not gud-running)
      :visible (eq gud-minor-mode 'gdbmi)]
@@ -248,7 +248,7 @@ gud-menu-map
 	        "Dump object"
               "Print Dereference")
      :enable (not gud-running)
-     :visible (memq gud-minor-mode '(gdbmi gdb jdb))]
+     :visible (memq gud-minor-mode '(gdbmi gdb lldb jdb))]
     ["Print S-expression" gud-pp
      :enable (and (not gud-running)
 		  (bound-and-true-p gdb-active-process))
@@ -259,23 +259,23 @@ gud-menu-map
 		   (eq gud-minor-mode 'gdbmi))]
     ["Down Stack" gud-down
      :enable (not gud-running)
-     :visible (memq gud-minor-mode '(gdbmi gdb guiler dbx xdb jdb pdb))]
+     :visible (memq gud-minor-mode '(gdbmi gdb lldb guiler dbx xdb jdb pdb))]
     ["Up Stack" gud-up
      :enable (not gud-running)
      :visible (memq gud-minor-mode
-		    '(gdbmi gdb guiler dbx xdb jdb pdb))]
+		    '(gdbmi gdb lldb guiler dbx xdb jdb pdb))]
     ["Set Breakpoint" gud-break
      :enable (or (not gud-running) gud-async-running)
      :visible (gud-tool-bar-item-visible-no-fringe)]
     ["Temporary Breakpoint" gud-tbreak
      :enable (or (not gud-running) gud-async-running)
-     :visible (memq gud-minor-mode '(gdbmi gdb sdb xdb))]
+     :visible (memq gud-minor-mode '(gdbmi gdb lldb sdb xdb))]
     ["Remove Breakpoint" gud-remove
      :enable (or (not gud-running) gud-async-running)
      :visible (gud-tool-bar-item-visible-no-fringe)]
     ["Continue to selection" gud-until
      :enable (not gud-running)
-     :visible (and (memq gud-minor-mode '(gdbmi gdb perldb))
+     :visible (and (memq gud-minor-mode '(gdbmi gdb lldb perldb))
 		   (gud-tool-bar-item-visible-no-fringe))]
     ["Stop" gud-stop-subjob
      :visible (or (not (memq gud-minor-mode '(gdbmi pdb)))
@@ -288,7 +288,7 @@ gud-menu-map
                    (gdb-show-run-p))]
     ["Run" gud-run
      :enable (or (not gud-running) gud-async-running)
-     :visible (or (memq gud-minor-mode '(gdb dbx jdb))
+     :visible (or (memq gud-minor-mode '(gdb lldb dbx jdb))
 		  (and (eq gud-minor-mode 'gdbmi)
 		       (or (not (gdb-show-run-p))
 			   (bound-and-true-p
@@ -299,7 +299,7 @@ gud-menu-map
 		  (display-graphic-p)
 		  (fboundp 'x-show-tip))
      :visible (memq gud-minor-mode
-		    '(gdbmi guiler dbx sdb xdb pdb))
+		    '(gdbmi lldb guiler dbx sdb xdb pdb))
      :button (:toggle . gud-tooltip-mode)]
     ["Info (debugger)" gud-goto-info]))
 
@@ -973,6 +973,7 @@ gud-gdb-fetch-lines-filter
       (setq gud-gdb-fetch-lines-string string)
       "")))
 
+
 ;; gdb speedbar functions
 
 ;; Part of the macro expansion of dframe-with-attached-buffer.
@@ -2702,10 +2703,12 @@ gud-delete-prompt-marker
 (define-derived-mode gud-mode comint-mode "Debugger"
   "Major mode for interacting with an inferior debugger process.
 
-   You start it up with one of the commands \\[gdb], \\[sdb], \\[dbx],
-\\[perldb], \\[xdb], or \\[jdb].  Each entry point finishes by executing a
-hook; `gdb-mode-hook', `sdb-mode-hook', `dbx-mode-hook',
-`perldb-mode-hook', `xdb-mode-hook', or `jdb-mode-hook' respectively.
+   You start it up with one of the commands \\[gdb], \\[lldb],
+\\[sdb], \\[dbx], \\[perldb], \\[xdb], or \\[jdb].  Each entry
+point finishes by executing a hook; `gdb-mode-hook',
+`lldb-mode-hook' `sdb-mode-hook', `dbx-mode-hook',
+`perldb-mode-hook', `xdb-mode-hook', or `jdb-mode-hook'
+respectively.
 
 After startup, the following commands are available in both the GUD
 interaction buffer and any source buffer GUD visits due to a breakpoint stop
@@ -2735,11 +2738,11 @@ gud-mode
 except that the breakpoint is temporary; that is, it is removed when
 execution stops on it.
 
-Under gdb, dbx, and xdb, \\[gud-up] pops up through an enclosing stack
-frame.  \\[gud-down] drops back down through one.
+Under gdb, lldb, dbx, and xdb, \\[gud-up] pops up through an
+enclosing stack frame.  \\[gud-down] drops back down through one.
 
-If you are using gdb or xdb, \\[gud-finish] runs execution to the return from
-the current function and stops.
+If you are using gdb, lldb, or xdb, \\[gud-finish] runs execution
+to the return from the current function and stops.
 
 All the keystrokes above are accessible in the GUD buffer
 with the prefix C-c, and in all buffers through the prefix C-x C-a.
@@ -3767,13 +3770,17 @@ gud-tooltip-dereference
 ; gdb-mi.el gets around this problem.
 (defun gud-tooltip-process-output (process output)
   "Process debugger output and show it in a tooltip window."
-  (remove-function (process-filter process) #'gud-tooltip-process-output)
-  (tooltip-show (tooltip-strip-prompt process output)
-                (or gud-tooltip-echo-area (not tooltip-mode))))
+  ;; First line is the print command itself.
+  (unless (string-search (gud-tooltip-print-command "") output)
+    (remove-function (process-filter process)
+                     #'gud-tooltip-process-output)
+    (tooltip-show (tooltip-strip-prompt process output)
+                  (or gud-tooltip-echo-area (not tooltip-mode)))))
 
 (defun gud-tooltip-print-command (expr)
   "Return a suitable command to print the expression EXPR."
   (pcase gud-minor-mode
+    ('lldb (format "dwim-print -- %s" expr))
     ('gdbmi (concat "-data-evaluate-expression \"" expr "\""))
     ('guiler expr)
     ('dbx (concat "print " expr))
@@ -3835,11 +3842,150 @@ gud-tooltip-tips
                       (gdb-input
 		       (concat cmd "\n")
 		       (lambda () (gdb-tooltip-print expr))))
+                  ;; Not gdbmi.
                   (add-function :override (process-filter process)
                                 #'gud-tooltip-process-output)
 		  (gud-basic-call cmd))
 		expr))))))))
 
+
+;; 'gud-lldb-history' and 'gud-gud-lldb-command-name' are required
+;; because gud-symbol uses their values if they are present.  Their
+;; names are deduced from the minor-mode name.
+(defvar gud-lldb-history nil)
+
+(defcustom gud-gud-lldb-command-name "lldb"
+  "Default command to run an executable under LLDB in text command mode."
+  :type 'string)
+
+(defun gud-lldb-marker-filter (string)
+  "Deduce interesting stuff from output STRING."
+  (cond (;; Process 72668 stopped
+         ;; * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
+         ;;     frame #0: ...) at emacs.c:1310:9 [opt]
+         (string-match (rx (and line-start (0+ blank) "frame"
+                                (0+ not-newline) " at "
+                                (group (1+ (not ":")))
+                                ":"
+                                (group (1+ digit))))
+                       string)
+         (setq gud-last-frame
+               (cons (match-string 1 string)
+                     (string-to-number (match-string 2 string)))))
+        (;; Process 72874 exited with status = 9 (0x00000009) killed
+         (string-match (rx "Process " (1+ digit) " exited with status")
+                       string)
+         (setq gud-last-last-frame nil)
+         (setq gud-overlay-arrow-position nil)))
+  string)
+
+;;;###autoload
+(defun lldb (command-line)
+  "Run lldb passing it COMMAND-LINE as arguments.
+If COMMAND-LINE names a program FILE to debug, lldb will run in
+a buffer named *gud-FILE*, and the directory containing FILE
+becomes the initial working directory and source-file directory
+for your debugger.  If you don't want `default-directory' to
+change to the directory of FILE, specify FILE without leading
+directories, in which case FILE should reside either in the
+directory of the buffer from which this command is invoked, or
+it can be found by searching PATH.
+
+If COMMAND-LINE requests that lldb attaches to a process PID, lldb
+will run in *gud-PID*, otherwise it will run in *gud*; in these
+cases the initial working directory is the `default-directory' of
+the buffer in which this command was invoked."
+  (interactive (list (gud-query-cmdline 'lldb)))
+
+  (when (and gud-comint-buffer
+	     (buffer-name gud-comint-buffer)
+	     (get-buffer-process gud-comint-buffer)
+	     (with-current-buffer gud-comint-buffer (eq gud-minor-mode 'gud-lldb)))
+    (gdb-restore-windows)
+    ;; FIXME: Copied from gud-gdb, but what does that even say?
+    (error "Multiple debugging requires restarting in text command mode"))
+
+  (gud-common-init command-line nil 'gud-lldb-marker-filter)
+  (setq-local gud-minor-mode 'lldb)
+
+  (gud-def gud-break
+           "breakpoint set --joint-specifier %f:%l"
+           "\C-b"
+           "Set breakpoint at current line.")
+  (gud-def gud-tbreak
+           "_regexp-break %f:%l"
+           "\C-t"
+	   "Set temporary breakpoint at current line.")
+  (gud-def gud-remove
+           "breakpoint clear  --line %l --file %f"
+           "\C-d"
+           "Remove breakpoint at current line")
+  (gud-def gud-step "thread step-in --count %p"
+           "\C-s"
+           "Step one source line with display.")
+  (gud-def gud-stepi
+           "thread step-inst --count %p"
+           "\C-i"
+           "Step one instruction with display.")
+  (gud-def gud-next
+           "thread step-over --count %p"
+           "\C-n"
+           "Step one line (skip functions).")
+  (gud-def gud-nexti
+           "thread step-inst-over --count %p"
+           nil
+           "Step one instruction (skip functions).")
+  (gud-def gud-cont
+           "process continue --ignore-count %p"
+           "\C-r"
+           "Continue with display.")
+  (gud-def gud-finish
+           "thread step-out"
+           "\C-f"
+           "Finish executing current function.")
+  (gud-def gud-jump
+	   (progn
+             (gud-call "_regexp-break %f:%l" arg)
+             (gud-call "_regexp-jump %f:%l"))
+	   "\C-j"
+           "Set execution address to current line.")
+  (gud-def gud-up
+           "_regexp-up %p"
+           "<"
+           "Up N stack frames (numeric arg).")
+  (gud-def gud-down
+           "_regexp-down %p"
+           ">"
+           "Down N stack frames (numeric arg).")
+  (gud-def gud-print
+           "dwim-print %e"
+           "\C-p"
+           "Evaluate C expression at point.")
+  (gud-def gud-pstar
+           "dwim-print *%e"
+           nil
+	   "Evaluate C dereferenced pointer expression at point.")
+  (gud-def gud-pv
+           "xprint %e"
+           "\C-v"
+           "Print value of lisp variable (for debugging Emacs only).")
+  (gud-def gud-until
+           "thread until %l"
+           "\C-u"
+           "Continue to current line.")
+  (gud-def gud-run
+           ;; Extension for process launch --tty?
+           "process launch -X true"
+	   nil
+           "Run the program.")
+
+  (gud-set-repeat-map-property 'gud-gdb-repeat-map)
+  (setq comint-prompt-regexp (rx line-start "(lldb)" (0+ blank)))
+  (setq paragraph-start comint-prompt-regexp)
+  (setq gud-running nil)
+  (setq gud-filter-pending-text nil)
+  (run-hooks 'lldb-mode-hook))
+
 (provide 'gud)
 
 ;;; gud.el ends here
diff --git a/src/.lldbinit b/src/.lldbinit
index a5789f49122..430c48f91f0 100644
--- a/src/.lldbinit
+++ b/src/.lldbinit
@@ -33,4 +33,8 @@ command script import emacs_lldb
 # Print with children provider, depth 2.
 command alias xprint frame variable -P 2
 
+# This is for M-x lldb: don't show source lines when stopping.
+settings set stop-line-count-before 0
+settings set stop-line-count-after 0
+
 # end.
-- 
2.42.0