Debugger (DAP)
Sema Lisp includes a built-in Debug Adapter Protocol (DAP) server that allows step-by-step debugging of Sema programs. By running over standard I/O (stdin/stdout) using the standard JSON-RPC DAP protocol, it integrates directly with modern editor debuggers.
sema dapRuns on the bytecode VM
The debugger operates on the stack-based bytecode VM — Sema's sole evaluator. Programs debugged via DAP are compiled to bytecode automatically upon launch.
Features
The sema dap server implements the core features of the Debug Adapter Protocol:
Launch Configuration
- Program Target: Specify the absolute path to the
.semafile to debug. - Stop on Entry: Set
stopOnEntry: trueto pause execution on the first bytecode instruction before any user forms run, allowing you to set up breakpoints or inspect the entry environment.
Breakpoints
- Dynamic Breakpoints: Set, toggle, or clear line breakpoints before launching or in real-time while execution is paused.
- Breakpoint Verification: The compiler maps source file line numbers to bytecode instructions and returns verified locations back to the editor.
- Conditional Breakpoints: Attach a
conditionexpression to a breakpoint; execution only stops there when the condition evaluates truthy in the paused frame (using the same evaluator asevaluate/hover). A condition that errors fails open — it stops — so the problem surfaces rather than being silently swallowed. - Exception Breakpoints: Enable the
uncaughtfilter to stop on a runtime error that escapes to the top level (errors handled bytry/catchdo not trigger it). TheexceptionInforequest reports the error message. Note: at an uncaught-exception stop the VM has already unwound its frames, so the stack/variables there are best-effort — the message is the load-bearing detail.
Stepping Controls
- Step Over (
next): Execute the current form and pause at the next sibling expression in the same frame. - Step Into (
stepIn): Follow execution inside user-defined functions or lambda expressions. - Step Out (
stepOut): Execute all remaining instructions in the active function frame and pause immediately upon returning to the caller. - Continue: Resume program execution until the next breakpoint is hit or the program finishes.
- Pause: Pause execution of a running VM thread.
Call Stack Inspection
- Renders the call hierarchy stack trace with frame IDs, function/closure names, active line numbers, columns, and absolute source file paths.
Variable & Scope Inspection
- Locals, Closure & Globals: Separates variables into local bindings (inside
let, function parameters), captured upvalues (shown by name under a Closure scope), and global variables. Locals are scoped to the current instruction, so bindings that are not yet in scope or whose block has already exited are not shown. - Type Inspection: Displays the value alongside its data type (e.g.,
list,vector,map,string,number,boolean,closure). - Complex Objects: Fully supports lazily expandable nested objects — lists, vectors, maps, hash maps, byte vectors, and named record fields — using hierarchical variable references.
Evaluate & Set Values (while paused)
- Evaluate Expressions: Evaluate arbitrary Sema expressions in the context of the selected stack frame (editor watch expressions, hover, and the debug console REPL). A top-level
(set! …)of an in-scope local, upvalue, or global is written back to the running program. - Set Variable: Edit a local or upvalue value in place from the editor's Variables pane; the new value is written through to the live frame.
Console Output Redirection
- Program output (
stdout) and standard error (stderr) prints are intercepted and redirected into standard DAPoutputevent frames. This prevents user outputs from corrupting the JSON-RPC protocol transport channel while ensuring they appear in the editor's debug console.
Editor Setup
Helix
Helix supports debuggers out-of-the-box via the lldb-dap architecture. Merge the following configuration into your ~/.config/helix/languages.toml:
[[language]]
name = "sema"
language-servers = ["sema-lsp"]
debugger = { name = "sema-dap", transport = "stdio" }
[debugger.sema-dap]
command = "sema"
args = ["dap"]
transport = "stdio"
templates = [
{ name = "launch", completion = [ { name = "program", completion = "filename" } ] }
]To debug in Helix:
- Open a
.semafile. - Press Space + g to open the debug menu.
- Select
launchand press Enter to start.
VS Code
Configure a debug launch task in your project's .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "sema",
"request": "launch",
"name": "Debug Sema Script",
"program": "${file}",
"stopOnEntry": true
}
]
}Neovim
Using nvim-dap, register the sema adapter and configuration:
local dap = require('dap')
dap.adapters.sema = {
type = 'executable',
command = 'sema',
args = { 'dap' }
}
dap.configurations.sema = {
{
type = 'sema',
request = 'launch',
name = "Launch file",
program = "${file}",
stopOnEntry = true,
},
}Emacs
Using dap-mode, register the debugging template:
(dap-register-adapter
"sema-dap"
(lambda (conf)
(list :type "executable"
:command "sema"
:args '("dap"))))
(dap-register-debug-template
"Sema Launch"
(list :type "sema"
:request "launch"
:name "Sema Debug"
:program "${file}"
:stopOnEntry t))Architecture
The sema-dap server uses an async-synchronous bridge structure to ensure standard I/O responsiveness while handling the single-threaded nature of Lisp evaluation:
- Frontend Async Loop: Runs a Tokio event loop handling stdin/stdout messages. Incoming requests are parsed into DAP protocol actions, and outgoing events (like VM output prints or stop events) are serialized to stdout.
- Backend Executor Thread: Runs VM bytecode execution inside a dedicated OS thread.
- VM Debug Hook: Utilizes the
sema-vm::VMdebug hooks (execute_debug). Every instruction step checks if a breakpoint is hit, stepped, or paused, updatingDebugStateand sending notifications back to the async frontend.