Getting Started
OverviewLanguage GuideFull Reference
Book
Table of ContentsIntroductionPrefaceGetting StartedLanguage TourOwnershipErrorsConcurrencyStdlibNetworkingDataPackagesSpeed & SafetyCross-PlatformToolingCookbookAppendix
Reference
Standard LibraryKeywordsPerformanceSecurityBuilt-in FunctionsStatusDebuggingABI
How-To
Getting StartedHTTP APIsErrorsPackagesConcurrencyMemoryWASITestingRelease Builds
Project
RoadmapVisionChangelogContributing

Debugging Mako

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.


Table of contents

  1. Source-level debugging with mako dap (DAP)
  2. Interactive lldb with mako debug
  3. lldb data formatters
  4. Debug vs release builds
  5. Inline debugging: dbg and dbg_str
  6. Running with lldb (manual)
  7. Address sanitizer
  8. Thread sanitizer
  9. Compiler error messages
  10. Common error patterns
  11. Inspecting generated code with --emit-c
  12. Tooling integration with mako check --json
  13. Testing and test failures
  14. Example debugging session

Source-level debugging with mako dap (DAP)

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.

How it works

  1. The adapter reads the DAP launch request and looks at its program field.
  2. If 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.
  3. The session is proxied to lldb-dap. Stack frames and breakpoints resolve to .mko source because the C codegen emits per-statement #line directives.
  4. A failed build returns a DAP error response to the client.
  5. On disconnect the 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.

lldb-dap discovery

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-21lldb-dap-16 on PATH

VS Code

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": []
}

Backend caveat

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).

dap_ / debug_ builtins are not the debugger

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.


Interactive lldb with mako debug

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.


lldb data formatters

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 vs release builds

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.


Inline debugging: dbg and dbg_str

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.

dbg(value)

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)
}

dbg_str(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
}

Tips for dbg / dbg_str


Running with lldb (manual)

The easy path is mako debug, which builds and launches lldb for you. To drive lldb manually, read on.

Building and launching

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

Essential lldb commands

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

Typical lldb workflow

(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

Debugging crashes

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

Address sanitizer

The address sanitizer (ASan) detects memory bugs at runtime: out-of-bounds access, use-after-free, double-free, and stack buffer overflows.

Building with ASan

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.

What ASan catches

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)

Reading ASan output

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.

Performance note

ASan adds roughly 2x overhead. Do not use it in production. It is a development and CI tool.


Thread sanitizer

The thread sanitizer (TSan) detects data races in concurrent programs that use crew, channels, or shared mutable state.

Running with TSan

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).

What TSan catches

Reading TSan output

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.


Compiler error messages

Makori's type checker (makori check) produces structured error messages with three parts: location, message, and optional help.

Location format

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)

Caret pointing

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"
                 ^~~~~~~

Help hints

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`

Warning vs error


Common error patterns

"use of moved value"

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

"unused Result"

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

"type mismatch"

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.

"break outside loop"

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.

Other common errors

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

Inspecting generated code with --emit-c

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:

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

Tooling integration with mako check --json

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

Testing and test failures

Running tests

mako test examples/testing -v          # verbose: show each test name
mako test examples/testing --race      # with thread sanitizer

Test output

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

Subtests

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)
        }
    })
}

Filtering tests

Run a single test by name:

mako test examples/testing -run TestAdd

Example debugging session

Here is a complete walkthrough of finding and fixing a bug.

The buggy program

// 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
}

Step 1: Add dbg to narrow it down

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
...

Step 2: Fix the logic

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
}

Step 3: Add a test

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)
}

Step 4: Run the test

mako test . -v
TestSumPositive ... ok
1 passed, 0 failed

Step 5: Run with sanitizers in CI

mako test . --race
mako build --sanitize address buggy.mko && ./buggy

Both pass cleanly. The bug is fixed and guarded by a test.


Quick reference

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
Edit this page on GitHub Report an issue