A language that links with C but cannot be debugged with C has sold you half a boundary. The claim this tutorial demonstrates — and every transcript below is a real session, captured against the shipped compiler — is the whole one: Mica emits standard DWARF v5, so one ordinary gdb session walks Mica frames, C frames, and back, with breakpoints, stepping, watchpoints and variable inspection working identically on both sides of every crossing.

The example is DebugWalk, built to be stopped in: the Mica program body calls SumScaled, a generic Mica function; each round calls DwScale, a C function lifted through a contract; and the C side calls back into the exported Mica function Clamp through its linkage name. Mica → generic → C → Mica, in one binary.

Setup, step by step

Everything below works in the tutorial container out of the box — the three files this section shows are checked in at its root. On your own machine you need exactly three things:

1. gdb. sudo apt-get install gdb (the container has it).

2. The C/C++ extension. Mica needs no debugger extension of its own — gdb reads its DWARF like any native program’s, so the debugger driver is Microsoft’s stock C/C++ extension (ms-vscode.cpptools), the same one C developers already run. Install it from the Extensions view; the container image ships it pre-installed.

3. A task and a launch configuration. The task builds the directory of the file you have open, through its own Makefile, at the debug tier:

// .vscode/tasks.json (excerpt)
{
  "label": "Mica: build current example/benchmark",
  "type": "shell",
  "command": "make",
  "args": ["-C", "${fileDirname}"],
  "group": "build",
  "problemMatcher": []
}

and the launch configuration starts gdb on the binary that build produced:

// .vscode/launch.json (excerpt)
{
  "name": "Debug current example/benchmark",
  "type": "cppdbg",
  "request": "launch",
  "program": "${fileDirname}/../build/${fileDirnameBasename}/${fileDirnameBasename}",
  "stopAtEntry": true,
  "preLaunchTask": "Mica: build current example/benchmark",
  "cwd": "${fileDirname}",
  "MIMode": "gdb",
  "miDebuggerPath": "/usr/bin/gdb",
  "setupCommands": [
    { "text": "-enable-pretty-printing", "ignoreFailures": true }
  ]
}

Open any file of examples/DebugWalk/, press F5, and you are stopped at the program’s entry with both languages’ sources clickable. Everything below is shown as terminal gdb for exactness — the VS Code experience is the same sessions with a UI.

Session 1: the sandwich backtrace

Set one breakpoint in the Mica function the C library calls, and run:

(gdb) break helpers.mica:14
(gdb) run

Breakpoint 1, Clamp (sample=20, low=-100, high=100) at helpers.mica:14
14          if sample < low then
(gdb) bt
#0  Clamp (sample=20, low=-100, high=100) at helpers.mica:14
#1  0x0000aaaaaaaa2974 in dw_scale (sample=10, factor=2) at dw_lib.c:18
#2  0x0000aaaaaaaa1774 in SumScaled__int32 (v=..., factor=2) at DebugWalk.mica:55
#3  0x0000aaaaaaaa14cc in main.main () at DebugWalk.mica:73

Read it from the bottom: the Mica program body (#3), a monomorphized Mica generic (#2), a C frame with its own file, line and typed arguments (#1), and a Mica frame the C code called (#0) — four frames, three language crossings, one unbroken backtrace with arguments readable in every frame. Nothing was configured to get this; it is what sharing the platform’s ABI and DWARF buys.

The separately built library direction works the same way — here is the C-calls-Mica example, a C main linking the Utilities archive, stopped inside the library by its C-side linkage name:

(gdb) break Utilities__Fibonacci
Breakpoint 1, Fibonacci (n=10) at Fibonacci.mica:8
(gdb) bt
#0  Fibonacci (n=10) at Fibonacci.mica:8
#1  0x0000aaaaaaaa142c in main ()
(gdb) continue
Breakpoint 1, Fibonacci (n=9) at Fibonacci.mica:8

A breakpoint spelled with the C name lands in the Mica source, and the recursion counts down in the args display — object files, archives, and both directions, one debugger.

Session 2: the generic, really

Generics are where debugging usually goes dark — erased types, mangled soup, synthetic frames. Mica monomorphizes, and the debug information says so plainly:

(gdb) info functions SumScaled
File DebugWalk.mica:
43: static float64 SumScaled__float64(Vector__float64, float64);
43: static float64 SumScaled__int32(Vector__int32, float64);

One source line, two real functions, each named by its concrete type argument. Break on each specialization separately and inspect v in both:

(gdb) break SumScaled__int32.f1.1
(gdb) break SumScaled__float64.f1.2
(gdb) run

Breakpoint 1, SumScaled__int32 (v=..., factor=2) at DebugWalk.mica:50
(gdb) print v
$1 = {10, 20, 30}
(gdb) ptype v
type = int32 [3]

(gdb) continue
Breakpoint 2, SumScaled__float64 (v=..., factor=2) at DebugWalk.mica:50
(gdb) print v
$3 = {1.5, 2.5, 90}
(gdb) ptype v
type = float64 [3]
(gdb) finish
Value returned is $4 = 108

The same parameter, typed int32 [3] in one stop and float64 [3] in the next — the type parameter is gone at run time, replaced by the concrete truth of whichever instantiation is executing, and finish reports the Mica function’s return value like any native call. This is the question “do generics really debug?” answered with a transcript rather than a promise.

(The .f1.1 suffix on the breakpoint spelling is the compiler’s internal uniquing tag, shown by info functions; tab completion finds it, and breakpoints by file:line never need it.)

Session 3: stepping across, and watching through

step at the DwScale call site walks into the C source, and two more steps walk through C into the called-back Mica:

Breakpoint 1, SumScaled__int32 (v=..., factor=2) at DebugWalk.mica:55
55          acc := acc + DwScale(sample, factor);
(gdb) step
dw_scale (sample=10, factor=2) at dw_lib.c:17
17          double scaled = sample * factor;
(gdb) step
18          return DebugWalk__Clamp(scaled, -100.0, 100.0);
(gdb) step
Clamp (...) at helpers.mica:11
(gdb) finish
(gdb) finish
#0  SumScaled__int32 (v=..., factor=2) at DebugWalk.mica:55

Stepping into a Mica function from Mica lands after the prologue with every argument readable — the compiler pins each function’s entry line-table row to its declaration line, exactly as GCC does, so gdb’s prologue skip works unaided. One honest edge remains: when the step arrives from a C frame (the third step above), gdb stops on the declaration line before the parameters are homed, and arguments read as garbage for that one stop — a cross-unit stepping nuance of the debugger, not of the DWARF; one more step homes everything, and breakpoints never see it.

A watchpoint on a generic’s local survives the round-trips through C:

(gdb) watch acc
Hardware watchpoint 2: acc
(gdb) continue
Old value = 0
New value = 3        # 1.5 scaled by 2, back from C and Clamp
(gdb) continue
Old value = 3
New value = 8        # + 2.5 scaled

Inspecting Mica data

Bit records render field by field — gdb reads the packed layout from DWARF bitfields, widths and all (session captured against the BitRecords example):

(gdb) print status
$1 = {ready = true, error = false, priority = 5, channel = 129, rest = 0}
(gdb) ptype status
type = struct Status {
    bool ready : 1;
    bool error : 1;
    <range type> priority : 3;
    <range type> channel : 8;
    <range type> rest : 3;
}

Strings show their honest two-field shape — pointer and rune count — and the data reads with one idiom:

(gdb) ptype line
type = struct string {
    unicode *data;
    uint64 length;
}
(gdb) print/c *line.data@8
$2 = {32 ' ', 32 ' ', 32 ' ', 101 'e', 114 'r', 114 'r', 111 'o', 114 'r'}

Mica does not ship gdb pretty-printers yet, so a bare print line shows the raw UTF-32 bytes rather than readable text. gdb’s pretty-printer mechanism is, by gdb’s own design, Python scripts the debugger loads — whether Mica ships such scripts, and how they reach the debugger without becoming installation-resident magic, is an open decision; until it is made, the print/c idiom above is the workaround this site teaches.

Any script’s identifiers work — a breakpoint on UtfSources’ Korean-named procedure, quoted like any symbol with a dot in it:

(gdb) break '절차의긴이름.f1.1'
Breakpoint 1, 절차의긴이름 () at UtfSources.mica:40

Why this works

The debug tier compiles with full optimization off and full fidelity on: standard DWARF v5, real function names, every local addressable, line tables that match the source you wrote. There is no “debug build of the runtime”, no special debugger protocol, and no difference between debugging a pure Mica program and this two-language sandwich — the C toolchain’s whole ecosystem (gdb, VS Code’s cpptools, core dumps, addr2line) applies to Mica because Mica speaks the platform’s formats natively instead of bringing its own.

What this does not do

  • No Mica-aware pretty-printers yet — strings and dynamic arrays read raw, with the idioms above. An open row, honestly labeled.
  • Step-into from a C frame stops one step early — the cross-unit edge above; take the extra step. From Mica, stepping lands post-prologue.
  • Release-tier debugging is real but reordered — the optimizer moves work, so stepping release code jumps; debug what you develop at the debug tier, profile what you ship at release, same as C.

Try it

git clone https://gitlab.com/mica-lang/mica-container.git
code mica-container            # open in VS Code, install ms-vscode.cpptools
# open examples/DebugWalk/DebugWalk.mica and press F5

Then reproduce Session 2 by hand: break SumScaled + tab completion, print v, continue, print v again — and watch the type change under your cursor. That is monomorphization, visible.

Next

The learn series’ boundary is told: contracts in, archives out, the kernel, and the debugger across all of it. The remaining ground is the tooling picker: every example in the container debugs through the same three files you just read.