Skip to content
Pre-release. v0.1.0 is not tagged and there are no published binaries yet. Install from source. Commands on this site were run against the current main.

Quickstart

Build a small C program, stop it inside a loop, read the loop counter, and let it finish.

Every command and every reply below was run against the current build. Home directories are rewritten to /Users/you, and some replies are shown with fields removed to keep the point visible — those are marked underneath. Nothing is reworded.

Terminal window
mkdir -p ~/lazydap-demo && cd ~/lazydap-demo
cat > hello.c <<'EOF'
#include <stdio.h>
int total(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
int main(void) {
printf("starting\n");
fflush(stdout);
int answer = total(10);
printf("total=%d\n", answer);
return 0;
}
EOF
gcc -g -O0 hello.c -o hello

-g puts the debug info in; without it there are no source lines to break on. -O0 stops the optimiser from moving code around, which keeps line numbers meaning what you think they mean.

Line 6 is sum += i;, inside the loop.

Terminal window
$ lazydap break hello.c:6 --format json
{
"action": "added",
"applied_to_session": false,
"breakpoints": [
{
"enabled": true,
"id": 1,
"line": 6,
"source": "/Users/you/lazydap-demo/hello.c",
"verified": false
}
],
"dry_run": false,
"not_found": []
}

No program is running yet, which is what "applied_to_session": false means, and why verified is false — nothing has checked whether line 6 is a real place to stop. The breakpoint is recorded in .lazydap/state.toml and will be applied to whatever you launch next, now or tomorrow.

Terminal window
$ lazydap launch ./hello --stop-on-entry --format json
{
"breakpoints": [
{
"enabled": true,
"id": 1,
"line": 6,
"message": "Resolved locations: 0",
"source": "/Users/you/lazydap-demo/hello.c",
"verified": true
}
],
"capabilities": {
"supports_conditional_breakpoints": true,
"supports_configuration_done_request": true,
"supports_function_breakpoints": true
},
"raw_reason": "exception",
"reason": "entry",
"session_id": "3a2c4335-0af9-4363-bf2e-2dc866c9045b",
"state": "paused",
"thread_id": 27982711
}

Two things in that reply are worth a moment.

--stop-on-entry is doing real work. Without it, the program starts running the instant launch returns. If it reaches your breakpoint before your next command arrives, that continue resumes from the stop you wanted instead of running to it. Stopping at entry means the program does not move until you say so.

reason and raw_reason disagree on purpose. codelldb implements entry-stop by sending the process a SIGSTOP, which LLDB files under exceptions, so the adapter reports exception. reason is lazydap’s normalised answer; raw_reason is what the adapter actually said. Branch on reason; read raw_reason when you need to know what really happened.

Terminal window
$ lazydap continue --wait --format json
{
"additional_stopped_threads": [],
"all_threads_stopped": true,
"breakpoint_updates": [
{
"adapter_id": 1,
"id": 1,
"line": 6,
"message": "Resolved locations: 1",
"verified": true
}
],
"captured_output": [
{
"category": "console",
"output": "Launching: /Users/you/lazydap-demo/hello\n",
"timestamp_ms": 1785446339349
},
{
"category": "stdout",
"output": "starting\r\n",
"timestamp_ms": 1785446339847
}
],
"elapsed_ms": 95,
"exit_code": null,
"frame": {
"column": 16,
"id": 1001,
"line": 6,
"name": "total",
"source": {
"name": "hello.c",
"path": "/Users/you/lazydap-demo/hello.c"
}
},
"hit_breakpoint_ids": [1],
"output_truncated": false,
"raw_reason": null,
"reason": "breakpoint",
"state": "paused",
"thread_id": 27982711,
"thread_updates": []
}

Three console chunks were elided from captured_output above; the stdout one is the program’s own printf.

This one object is the whole trip: where it stopped (frame), why (reason), which breakpoint (hit_breakpoint_ids), what the program printed on the way (captured_output), and that the breakpoint went from “resolved 0 locations” to “resolved 1” once the binary was loaded (breakpoint_updates).

Without --wait, continue returns the moment the debugger accepts the request and tells you none of that. The --wait contract covers the rest of the outcomes.

scopes tells you what is in view and hands back the reference numbers variables needs:

Terminal window
$ lazydap scopes --format json
{
"scopes": [
{ "expensive": false, "name": "Local", "variables_reference": 1006 },
{ "expensive": false, "name": "Static", "variables_reference": 1007 },
{ "expensive": false, "name": "Global", "variables_reference": 1008 },
{ "expensive": false, "name": "Registers", "variables_reference": 1009 }
]
}
Terminal window
$ lazydap variables --reference 1006 --format table
NAME VALUE TYPE REFERENCE
n 10 int 0
sum 0 int 0
i 1 int 0

First iteration: i is 1 and sum is still 0.

eval runs an expression in the program’s own language:

Terminal window
$ lazydap eval 'n * 2' --format json
{
"type_name": "long long",
"value": "20",
"variables_reference": 0
}
Terminal window
$ lazydap step --wait --format json
{
"additional_stopped_threads": [],
"all_threads_stopped": true,
"breakpoint_updates": [],
"captured_output": [],
"elapsed_ms": 57,
"exit_code": null,
"frame": {
"column": 5,
"id": 1001,
"line": 7,
"name": "total",
"source": {
"name": "hello.c",
"path": "/Users/you/lazydap-demo/hello.c"
}
},
"hit_breakpoint_ids": [],
"output_truncated": false,
"raw_reason": null,
"reason": "step",
"state": "paused",
"thread_id": 28409651,
"thread_updates": []
}

That is the whole object. The same fields come back every time, filled in as far as the outcome allows — a step that printed nothing still carries an empty captured_output.

Line 6 to line 7. The breakpoint is still on line 6, so continue would come straight back here on the next iteration — nine more times. Take it out first:

Terminal window
$ lazydap break --remove --all --dry-run --format json
{
"action": "removed",
"applied_to_session": false,
"breakpoints": [
{
"enabled": true,
"id": 3,
"line": 6,
"message": "Resolved locations: 1",
"source": "/Users/you/lazydap-demo/hello.c",
"verified": true
}
],
"dry_run": true,
"not_found": []
}
$ lazydap break --remove --all --format json
{
"action": "removed",
"applied_to_session": true,
"breakpoints": [
{
"enabled": true,
"id": 3,
"line": 6,
"source": "/Users/you/lazydap-demo/hello.c",
"verified": false
}
],
"dry_run": false,
"not_found": []
}

applied_to_session is the difference between the two: the dry run changed nothing, so it touched no live session; the real one did. The id is 3 because this was the third breakpoint created in this project — ids are not reused.

--dry-run reported exactly what the real run then removed. Every lazydap mutation takes it, and it uses the same selection logic as the real thing rather than a second implementation that might disagree.

Terminal window
$ lazydap continue --wait --format json
{
"additional_stopped_threads": [],
"all_threads_stopped": false,
"breakpoint_updates": [],
"captured_output": [
{
"category": "stdout",
"output": "total=55\r\n",
"timestamp_ms": 1785448521428
},
{
"category": "console",
"output": "Process exited with code 0.\n",
"timestamp_ms": 1785448521429
}
],
"elapsed_ms": 1,
"exit_code": 0,
"frame": null,
"hit_breakpoint_ids": [],
"output_truncated": false,
"raw_reason": null,
"reason": null,
"state": "exited",
"thread_id": null,
"thread_updates": []
}

"state": "exited" with "exit_code": 0. There is no frame, because there is no longer a program to have one. The answer the program computed is in captured_output.

Read state to find out what happened to the program. The command’s own exit code tells you whether the command worked, which is a different question — lazydap continue exits 0 here even though it is reporting the program’s death.

Terminal window
lazydap disconnect # end the session, leave the daemon running