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

Debugging Mako

This guide covers the implemented Mako debugging tools, from quick inline prints to generated-C debugger sessions and sanitizer runs.

Product tip: 0.2.1.


Table of contents

  1. Debug vs release builds
  2. Inline debugging: dbg and dbg_str 2b. DAP JSON seed
  3. Running with lldb
  4. Address sanitizer
  5. Thread sanitizer
  6. Compiler error messages
  7. Common error patterns
  8. Inspecting generated code with --emit-c
  9. Tooling integration with mako check --json
  10. Testing and test failures
  11. Example debugging session

DAP JSON seed

Mako ships helpers for Debug Adapter Protocol–shaped JSON (not a full DAP server). Use them to prototype adapters; real source-level locals still go through lldb on generated C (-g + #line mapping).

let init = dap_initialize_response(1)
let stop = dap_stopped_event("breakpoint", 1)
let cmd = dap_request_command(req_json)  // extract "command"
let resp = dap_handle_request(req_json)  // one-shot dispatch
let snap = debug_snapshot_json()         // tasks + locals + frames

CLI seed (not a full IDE debugger):

# One-shot
mako dap --request '{"seq":1,"type":"request","command":"initialize"}'

# Content-Length multi-message loop (exit on disconnect; cap with --max-messages)
mako dap --stdio --max-messages 4

Profile continuous seed:

mako profile-serve --port 9470 --max-requests 3
# GET /debug/pprof/text | /debug/pprof/json | /debug/profile | /health

Also: debug_line_bp_*, debug_push_frame / debug_frames_json, soft debug_break / optional debug_trap_enable.

Debug vs release builds

Debug is the default. Every mako build, mako run, and mako 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

For deeper investigation, use lldb (macOS) to step through your program interactively.

Building and launching

mako build main.mko -o /tmp/myapp
lldb /tmp/myapp

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.c --line 12 b main.c:12 Break at a specific line in the generated C
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.c:5
    frame #1: myapp`main at main.c:12
(lldb) c                    # continue to end or next breakpoint

Debugging crashes

When a Mako 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

Mako's type checker (mako 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

Mako 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, mako check can output diagnostics as JSON:

mako check --json main.mko

Output format (one JSON object per line):

{
  "file": "main.mko",
  "line": 12,
  "col": 5,
  "severity": "error",
  "message": "type mismatch: expected int, got string",
  "help": "convert with parse_int() or change the type annotation"
}

Use this for:

Example in a CI script:

mako check --json 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: mako 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) mako build main.mko
Release build mako build --release main.mko
Inline debug print dbg(value) / dbg_str(value)
Type check only mako check main.mko
Type check as JSON mako check --json main.mko
Inspect generated C mako build --emit-c main.mko
Run in lldb mako build main.mko && lldb ./main
Address sanitizer mako build --sanitize address main.mko
Thread sanitizer mako test --race
Verbose tests mako test dir/ -v
Run one test mako test dir/ -run TestName
Edit this page on GitHub Report an issue