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:73Read 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:8A 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 = 108The 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:55Stepping 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 scaledInspecting 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:40Why 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 F5Then 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.