This guide covers the implemented Mako debugging tools, from quick inline prints to source-level debugger sessions (DAP and lldb) and sanitizer runs.
Product tip: 0.6.5.
mako dap is a real Debug Adapter Protocol (DAP) adapter. It speaks DAP over
stdio (Content-Length framing) and proxies the session to a spawned
lldb-dap child. Editors with a DAP client (VS Code, Neovim, …) get
breakpoints, stepping, and stack frames on .mko source lines.
launch request and looks at its program
field.program is a .mko file, the adapter builds it with debug flags
(-O0 -g, C backend) to $TMPDIR/mako_dbg_<stem>_<pid>. If program
already points at a binary, it is used as-is.lldb-dap. Stack frames and breakpoints resolve
to .mko source because the C codegen emits per-statement #line
directives.lldb-dap child and the debugged process are killed —
no orphan processes.mako dap takes no flags. The old canned-response seed flags (--request,
--stdio, --max-messages) are removed.
The adapter locates lldb-dap in this order:
| Order | Source |
|---|---|
| 1 | $MAKO_LLDB_DAP |
| 2 | xcrun -f lldb-dap (macOS) |
| 3 | lldb-dap, then lldb-dap-21 … lldb-dap-16 on PATH |
The Makori VS Code extension (editors/vscode) spawns mako dap directly via a
DebugAdapterDescriptorFactory — no CodeLLDB or Microsoft C/C++ extension
needed, and no preLaunchTask (the adapter builds on launch). Launch config:
{
"type": "mako-native",
"request": "launch",
"name": "Mako: Debug active file",
"program": "${file}",
"args": []
}
Source-level debugging requires the C backend. The default native
(Cranelift) backend emits no DWARF line info, so breakpoints cannot resolve
to .mko lines. mako dap and mako debug force the C backend
automatically; manual lldb users must build with --backend c (see
Running with lldb).
The in-process builtins (debug_break, debug_line_bp_set,
debug_push_frame, dap_initialize_response, debug_snapshot_json, …)
remain available as manual instrumentation helpers for prototyping. The
compiler never auto-instruments them; real debugging goes through mako dap
or mako debug.
Tests: scripts/test-dap.sh drives a scripted DAP session
(scripts/dap_drive.py) against examples/testing/debug_probe.mko. It skips
cleanly when lldb-dap is absent and includes a negative case: launching
examples/bad/assign_string_to_int.mko must return a DAP error. Unit tests
live in src/dap.rs.
mako debug is the easy path for terminal debugging: it builds the program
with debug info (C backend) and launches an interactive lldb session with the
Mako data formatters preloaded.
mako debug # debug the package in .
mako debug main.mko # debug a specific file
mako debug . -p app # debug one workspace member
mako debug main.mko -- arg1 # arguments after -- go to the program
| Flag | Description |
|---|---|
[path] |
Source file or package directory (default: .) |
-p, --package <NAME> |
Workspace member to debug |
-- [ARGS]... |
Arguments forwarded to the program |
lldb discovery: $MAKO_LLDB, then xcrun -f lldb (macOS), then lldb on
PATH.
Mako ships lldb data formatters at editors/lldb/mako_formatters.py, loaded
automatically by mako dap and mako debug:
| Type | Display |
|---|---|
MakoString |
Quoted UTF-8 string |
MakoIntArray / MakoByteArray / MakoStrArray / MakoFloatArray / MakoBoolArray / MakoArr_* |
[e0, e1, ...] (first 16 elements), with expandable synthetic children |
Load them manually inside lldb:
(lldb) command script import /path/to/mako_formatters.py
Override the formatter path with $MAKO_LLDB_FORMATTERS. The installers place
the script at $PREFIX/share/mako/mako_formatters.py.
Debug is the default. With the C backend (makori build --backend c), every
makori build, makori run, and makori test invocation compiles with clang
-O0 -g, which means:
For release builds, pass --release:
mako build --release main.mko # -O2, still linkable
Release builds strip debug info and enable optimizations. Only use them for benchmarking, profiling, and shipping.
The fastest way to inspect values during development is dbg() for integers
and dbg_str() for strings. Both print to stderr and return the value
unchanged, so you can drop them into any expression without altering program
flow.
Prints an integer value with file and line information, then returns it:
fn process(n: int) -> int {
let doubled = dbg(n * 2) // stderr: [dbg] main.c:5: n * 2 = 84
return doubled + 1
}
fn main() {
let x = 42
let y = dbg(x) // stderr: [dbg] main.c:9: x = 42
print_int(y) // stdout: 42 (dbg returns the value)
}
Same behavior for strings:
fn greet(name: string) -> string {
let msg = dbg_str(name) // stderr: [dbg] main.c:2: name = "Alice"
return msg
}
fn main() {
let s = dbg_str("hello") // stderr: [dbg] main.c:7: "hello" = hello
print(s) // stdout: hello
}
--emit-c if the line numbers seem off.dbg calls before shipping. They are for development only.print_int(dbg(a) + dbg(b)) prints both values, then their sum.The easy path is mako debug, which
builds and launches lldb for you. To drive lldb manually, read on.
Backend caveat: source-level debugging requires the C backend. The
default native (Cranelift) backend emits no DWARF line info, so a binary
from plain makori build cannot resolve .mko breakpoints. Build manually
with:
mako build --backend c main.mko -o /tmp/myapp
lldb /tmp/myapp
Because the C codegen emits per-statement #line directives, breakpoints and
stack frames resolve to .mko source lines. On macOS the binary's dSYM is
generated automatically (explicit dsymutil runs on the incremental path).
Release builds strip debug info.
Load the Mako data formatters once per session:
(lldb) command script import /path/to/mako_formatters.py
If your program takes arguments:
lldb -- /tmp/myapp arg1 arg2
| Command | Shortcut | What it does |
|---|---|---|
breakpoint set --name main |
b main |
Set a breakpoint on the main function |
breakpoint set --file main.mko --line 12 |
b main.mko:12 |
Break at a .mko source line (via #line mapping) |
breakpoint list |
br l |
Show all breakpoints |
breakpoint delete 1 |
br del 1 |
Remove breakpoint number 1 |
run |
r |
Start (or restart) the program |
run arg1 arg2 |
r arg1 arg2 |
Run with command-line arguments |
step |
s |
Step into the next function call |
next |
n |
Step over (execute function, stop at next line) |
finish |
f |
Run until the current function returns |
continue |
c |
Continue until the next breakpoint |
print variable_name |
p variable_name |
Print a variable's value |
print/x variable_name |
p/x variable_name |
Print in hexadecimal |
frame variable |
fr v |
Show all local variables in current frame |
bt |
bt |
Print the full backtrace (call stack) |
bt all |
Backtrace of all threads | |
thread list |
Show all threads | |
quit |
q |
Exit lldb |
(lldb) b main
Breakpoint 1: where = myapp`main ...
(lldb) r
Process launched ...
(lldb) n # step over lines
(lldb) p n # inspect variable n
(int64_t) $0 = 42
(lldb) bt # see call stack
* thread #1, ...
* frame #0: myapp`process at main.mko:5
frame #1: myapp`main at main.mko:12
(lldb) c # continue to end or next breakpoint
When a Makori program aborts at runtime (out-of-bounds, integer overflow, failed
assert), it prints an error: ... message. To catch the exact point:
lldb /tmp/myapp
(lldb) b abort # break when the runtime calls abort()
(lldb) r
# ... program runs until the abort ...
(lldb) bt # see what triggered it
(lldb) frame variable # inspect locals at the crash site
The address sanitizer (ASan) detects memory bugs at runtime: out-of-bounds access, use-after-free, double-free, and stack buffer overflows.
mako build --sanitize address main.mko
Then run the binary normally. If ASan detects a violation, it prints a detailed report with the exact source location and a stack trace.
| Bug class | Example |
|---|---|
| Heap buffer overflow | Writing past the end of a slice's backing array |
| Stack buffer overflow | Overflowing a fixed-size local buffer |
| Use after free | Accessing arena memory after the arena block exits |
| Double free | Freeing the same allocation twice |
| Memory leak | Allocations never freed (reported at exit) |
ASan reports look like this:
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
READ of size 8 at 0x... thread T0
#0 0x... in process main.c:14
#1 0x... in main main.c:22
The stack trace points to the generated C file. Use --emit-c to map it
back to your .mko source.
ASan adds roughly 2x overhead. Do not use it in production. It is a development and CI tool.
The thread sanitizer (TSan) detects data races in concurrent programs that use
crew, channels, or shared mutable state.
For tests:
mako test --race
# CI smoke (subset):
mako test --race examples/testing/crew_fan_test.mko
mako test --race examples/testing/kick_send_test.mko
mako test --race examples/testing/chan_struct_test.mko
mako test --race examples/testing/crew_drain_test.mko
For a standalone build:
mako build --sanitize thread main.mko
CI: .github/workflows/ci.yml job TSan concurrency smoke (ubuntu).
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 8 at 0x... by thread T2:
#0 worker main.c:18
Previous read of size 8 at 0x... by thread T1:
#0 reader main.c:24
Fix: protect the shared state with a mutex_new() / mutex_lock() /
mutex_unlock() pair, or use channels to communicate instead of shared memory.
Makori's type checker (makori check) produces structured error messages with
three parts: location, message, and optional help.
main.mko:12:5: error: type mismatch: expected int, got string
This means: - main.mko -- the source file - 12 -- the line number (1-based) - 5 -- the column number (1-based) - error: -- the severity (error or warning)
For many errors, Mako prints the source line with a caret (^) pointing at the
exact position:
main.mko:12:5: error: type mismatch: expected int, got string
let x: int = "hello"
^~~~~~~
Some errors include a help: line with a suggested fix:
main.mko:8:12: error: use of moved value: name
print(name)
^~~~
help: value was moved on line 6; consider using `share` instead of `hold`
main.mko:10:12: error: use of moved value: data
What happened: You declared a binding with hold (unique ownership) and
then used it after passing it to another function or binding, which moved the
value away.
let hold data = read_file("input.txt")
process(data) // data is moved here
print(data) // error: use of moved value
Fix: Either use share for shared ownership, or restructure so you do not
access the value after the move.
let share data = read_file("input.txt")
process(data) // shared, not moved
print(data) // ok
main.mko:5:5: warning: unused Result value
What happened: A function returned a Result[T, E] and you ignored it.
This usually means you are silently discarding an error.
write_file("out.txt", contents) // warning: unused Result
Fix: Handle the result with ?, match, or assign to let _ if you
truly do not care:
write_file("out.txt", contents)? // propagate error
// or
let _ = write_file("out.txt", contents) // explicitly discard
main.mko:7:18: error: type mismatch: expected int, got string
What happened: You passed a value of the wrong type to a function, assigned it to a variable with a different type annotation, or returned the wrong type.
fn double(n: int) -> int {
return n * 2
}
fn main() {
let x = double("five") // error: expected int, got string
}
Fix: Pass the correct type. Use conversion functions (parse_int,
int_to_string, int64(), etc.) when you need to bridge types.
main.mko:15:9: error: break outside loop
What happened: You used break or continue outside a for or while
loop. These keywords only make sense inside loops.
fn check(n: int) {
if n == 0 {
break // error: break outside loop
}
}
Fix: Use return to exit a function early. Use break only inside loops.
| Error message | Meaning |
|---|---|
undeclared name: foo |
You used a name that has not been declared in scope |
cannot assign to immutable binding |
You tried to modify a let binding; use let mut |
call arity mismatch: expected 2, got 3 |
Wrong number of arguments to a function |
unreachable code after return |
Code after a return statement can never execute |
non-exhaustive match |
Your match does not cover all enum variants |
duplicate field: name |
A struct has two fields with the same name |
Makori compiles .mko to C, then invokes clang. You can inspect the intermediate
C to understand what the compiler generates:
mako build --emit-c main.mko
This writes the generated C file alongside the output. Use it to:
free() calls are inserted.Example workflow:
mako build --emit-c main.mko -o /tmp/myapp
# Inspect the generated C:
cat /tmp/myapp.c
# The generated code has comments mapping back to .mko line numbers
For editor integrations, CI pipelines, and custom tooling, makori check can
emit a stable, versioned JSON report:
mako check --json=v1 main.mko
The report contains one entry per checked target:
{
"schemaVersion": 1,
"command": "check",
"ok": false,
"targets": [
{
"file": "main.mko",
"ok": false,
"symbols": null,
"diagnostics": [
{
"severity": "error",
"file": "main.mko",
"line": 12,
"column": 5,
"message": "type mismatch: expected int, got string"
}
]
}
],
"summary": {
"checked": 1,
"passed": 0,
"failed": 1,
"diagnostics": 1
},
"errors": []
}
Bare --json retains the original array output for existing integrations.
New consumers should use v1 and ignore unknown fields added to that version.
Use this for:
Example in a CI script:
mako check --json=v1 src/main.mko > diagnostics.json
if [ $? -ne 0 ]; then
echo "Type errors found:"
cat diagnostics.json
exit 1
fi
mako test examples/testing -v # verbose: show each test name
mako test examples/testing --race # with thread sanitizer
Passing tests:
TestAdd ... ok
TestMul ... ok
2 passed, 0 failed
Failing tests:
TestAdd ... FAIL
assert_eq failed: got 4, want 5
at add_test.mko:4
1 passed, 1 failed
Use t_run for table-driven subtests:
fn TestParse() {
t_run("positive", fn() {
assert_eq(parse_int("42"), Ok(42))
})
t_run("negative", fn() {
assert_eq(parse_int("-1"), Ok(-1))
})
t_run("bad input", fn() {
match parse_int("abc") {
Err(_) => {}
Ok(_) => assert(false)
}
})
}
Run a single test by name:
mako test examples/testing -run TestAdd
Here is a complete walkthrough of finding and fixing a bug.
// buggy.mko
fn sum_positive(nums: []int) -> int {
let mut total = 0
for i in len(nums) {
total = total + nums[i] // bug: adds ALL numbers, not just positive
}
return total
}
fn main() {
let data = [3, -1, 4, -2, 5]
let result = sum_positive(data)
print_int(result) // prints 9, expected 12
}
fn sum_positive(nums: []int) -> int {
let mut total = 0
for i in len(nums) {
let val = dbg(nums[i]) // see each value on stderr
total = total + val
let _ = dbg(total) // see running total
}
return total
}
Run: makori run buggy.mko
stderr output reveals negative numbers being added:
[dbg] buggy.c:8: nums[i] = 3
[dbg] buggy.c:10: total = 3
[dbg] buggy.c:8: nums[i] = -1
[dbg] buggy.c:10: total = 2
...
fn sum_positive(nums: []int) -> int {
let mut total = 0
for i in len(nums) {
if nums[i] > 0 {
total = total + nums[i]
}
}
return total
}
Create buggy_test.mko in the same directory:
fn TestSumPositive() {
assert_eq(sum_positive([3, -1, 4, -2, 5]), 12)
assert_eq(sum_positive([]), 0)
assert_eq(sum_positive([-1, -2]), 0)
}
mako test . -v
TestSumPositive ... ok
1 passed, 0 failed
mako test . --race
mako build --sanitize address buggy.mko && ./buggy
Both pass cleanly. The bug is fixed and guarded by a test.
| Task | Command |
|---|---|
| Debug build (default) | makori build main.mko |
| Release build | makori build --release main.mko |
| Debug in an editor (DAP adapter) | mako dap |
| Debug in a terminal (lldb + formatters) | mako debug main.mko |
| Inline debug print | dbg(value) / dbg_str(value) |
| Type check only | makori check main.mko |
| Type check as JSON | makori check --json=v1 main.mko |
| Inspect generated C | makori build --emit-c main.mko |
| Run in lldb manually | makori build --backend c main.mko && lldb ./main |
| Address sanitizer | makori build --sanitize address main.mko |
| Thread sanitizer | makori test --race |
| Verbose tests | makori test dir/ -v |
| Run one test | makori test dir/ -run TestName |