Debug your Erlang code directly in your favourite text-editor.
Roberto Aloi (Klarna -Stockholm, Sweden) Juan Facorro (Klarna - Stockholm, Sweden) Alan Zimmermann (WhatsApp - London, UK)
Erlang LS is an editor-agnostic language server which provides language features for the Erlang programming language using the Language Server Protocol, or LSP in short.
The Debug Adapter Protocol, or DAP in short, is a similar protocol for communication between a client (a text editor or an IDE) and a Debug Server. The protocol is designed to allow the creation of step-by-step debuggers directly in the editor.
One of the strengths of the Erlang programming language is the ability to seamlessly debug and trace Erlang code. Many tools and libraries exist, but they are sometimes under-utilized by the Community, either because their API is not intuitive (think to the dbg Erlang module), or because they offer a limited, obsolete, UI (think the debugger application).
We want to solve this problem by leveraging some of the existing debugging and tracing facilities provided by Erlang/OTP and bringing the debugging experience directly in the text-editor, next to the code, improving the user experience when using such tools.
This video shows what debugging Erlang code is like via the debugger application. This other video shows what the same experience looks like from Emacs.
Due to the editor-agnostic nature of the DAP protocol, a very similar experience is delivered to users of a different development tool, be it Vim, VS Code or Sublime Text 3.
We would like to use the opportunity provided by the Spawnfest 2020 to implement a Proof of Concept. In the POC we demonstrate that it is possible, with a relatively small effort, to raise the usability standards for an Erlang user with regards to debugging and tracing.
Specifically, we aim at creating a step-by-step debugger based on the Erlang Interpreter, a not very well known module in Erlang/OTP, which is used as a low-level API to build tools such as debugger and the cedb debugger.
We will inspire our work to the implementation from the Elixir LS language server, which already uses this approach.
During the weekend, we will try to:
- Get some familiarity with the DAP protocol
- Get some familiarity with the Erlang Interpreter (aka the int module)
- Figure out a strategy to add support for the DAP protocol into the Erlang LS project
- Pair most of the times and learn from each other
- Learn some Elixir along the way
- Basic understanding of the DAP protocol
- Basic understanding of the Erlang Interpreter (aka the int module)
- Installed and configured the dap-mode for Emacs to work with a new, Erlang, Debug Adapter
- Plug support for the DAP protocol in Erlang LS
- Handle the following DAP requests (some requests have been simplified for timing reasons)
- initialize
- launch
- configurationDone
- setBreakpoints
- setExceptionBreakpoints
- threads
- stackTrace
- scopes
- next
- continue
- stepIn
- evaluate
- Creation of a toy project to showcase the new Debug Adapter
- Creation of the two videos above to showcase the past and future of Erlang debugging
- Add unit, integration and property-based tests for the new code
- Properly decouple the "language server" and the "debug adapter" code in two separate applications
- Support the full DAP protocol
- Support the "Attach" and "Run in Terminal" configurations in addition to the "Launch" one
- Robustify the code by making it resilient to errors (eg regarding Erlang distribution)
While working on the project, we noticed a few issues with third-party projects that we plan to address:
- The
dap-mode
Emacs can be improved:- Logs should be generated in separate buffers
- The client ID should not be hard-coded in the
initialize
message - The
configurationDone
request should respect server capabilities
- The
int
Erlang module should be improved:- Part of the API (eg the
next
function) is internal or undocumented - The API to retrieve bindings could be simplified
- The callback function could be extended to accept a
fun
instead of aMFA
- The attached process concept could be extended into a proper Erlang behaviour
- Part of the API (eg the
- Erlang LS: https://erlang-ls.github.io/editors/emacs/
- DAP mode for Emacs: https://emacs-lsp.github.io/dap-mode/
- Checkout the
dap
branch from the https://github.com/spawnfest/frj repository - Run
rebar3 as dap escriptize
- Ensure the
_build/dap/bin/els_dap
escript is in yourPATH
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Erlang DAP ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(require 'dap-mode)
;; Log io to *Messages*. Seems to be the only option at the moment. Optional
(setq dap-inhibit-io nil)
(setq dap-print-io t)
(defun dap-erlang--populate-start-file-args (conf)
"Populate CONF with the required arguments."
(-> conf
(dap--put-if-absent :dap-server-path '("els_dap"))
(dap--put-if-absent :request "launch")
(dap--put-if-absent :projectDir (lsp-find-session-folder (lsp-session) (buffer-file-name)))
(dap--put-if-absent :cwd (lsp-find-session-folder (lsp-session) (buffer-file-name)))
))
;; Add a Run Configuration for running 'rebar3 shell'
(dap-register-debug-provider "Erlang" 'dap-erlang--populate-start-file-args)
(dap-register-debug-template "Erlang rebar3 shell"
(list :type "Erlang"
:program "rebar3"
:args "shell"
:name "Erlang::Run"))
;; Add a Run Configuration for executing a given MFA
(dap-register-debug-template "Erlang MFA"
(list :type "Erlang"
:module "daptoy_fact"
:function "fact"
;; :function "dummy"
:args "[3]"
:name "Erlang::Run MFA"))
;; Add a Run Configuration for running 'rebar3 shell'
;; in an integrated terminal in the client
;; NOTE: set the projectnode hostname in two places
(dap-register-debug-template "Erlang Terminal"
(list :type "Erlang"
:runinterminal '("rebar3" "shell" "--name" "dapnode@alanzimm-mbp")
:projectnode "dapnode@alanzimm-mbp"
:name "Erlang::Terminal"))
;; Add a Run Configuration for running 'rebar3 shell'
;; in an integrated terminal in the client, running a given MFA
;; NOTE: set the projectnode hostname in two places
(dap-register-debug-template "Erlang Terminal MFA"
(list :type "Erlang"
:runinterminal '("rebar3" "shell" "--name" "dapnode@alanzimm-mbp")
:projectnode "dapnode@alanzimm-mbp"
:module "daptoy_fact"
:function "fact"
;; :function "dummy"
:args "[4]"
:name "Erlang::Terminal MFA"))
(require 'dap-mode)
;; Show debug logs
(setq dap-inhibit-io nil)
(setq dap-print-io t)
(defun dap-erlang--populate-start-file-args (conf)
"Populate CONF with the required arguments."
(-> conf
(dap--put-if-absent :dap-server-path '("els_dap"))
(dap--put-if-absent :request "launch")
(dap--put-if-absent :cwd (lsp-find-session-folder (lsp-session) (buffer-file-name)))))
;; Add a Run Configuration for running 'rebar3 shell'
(dap-register-debug-provider "Erlang" 'dap-erlang--populate-start-file-args)
(dap-register-debug-template "Erlang rebar3 shell"
(list :type "Erlang"
:program "rebar3"
:args "shell"
:name "Erlang::Run"))
;; Add a Run Configuration for executing a given MFA
(dap-register-debug-template "Erlang MFA"
(list :type "Erlang"
:module "daptoy_fact"
:function "fact"
:args "[5]"
:name "Erlang::Run"))
git clone https://github.com/erlang-ls/daptoy
cd daptoy
- Open the
src/daptoy_fact.erl
file in Emacs - Add a breakpoint at a given line using
dap-breakpoints-add
on that line - Run
dap-debug
- Select the
Erlang MFA
Run Configuration (that will run thedaptoy_fact:fact(5)
by default) - Step through code via
dap-next
You can also select the Erlang rebar3 shell
configuration as an alternative.
You can then attach to the spawned node and run custom MFAs to see the debugger in action.
See epmd -names
for details about the node name.
Since the setup for the project can be time-consuming, you can also enjoy the video to get the Quickstart experience: