Nim Debugging
Debug by reducing uncertainty: preserve the failure, state one hypothesis, and collect the smallest piece of evidence that can reject it. Find the first violated invariant rather than stopping at the final crash.
Rules
Preserve the failure
- Record the exact input, command, working directory, backend, memory manager, thread setting, build mode, and relevant environment.
- Make the failure deterministic before changing code. If it is intermittent, preserve a seed, event order, or failing fixture.
- Change one diagnostic dimension at a time. A debug build that no longer fails is a comparison point, not a reproduction.
- Turn each confirmed defect into a regression test before removing instrumentation.
Choose evidence by failure class
| Failure | First evidence |
|---|---|
| Compile-time type or generic question | Normal compiler diagnostic; use compiles only for a yes/no probe |
| Unhandled exception or defect | Default stack trace, or --lineTrace:on in the failing optimized mode |
| Wrong runtime value | echo, debugEcho, or repr at invariant boundaries |
| Buffered output lost at a crash | stdout.write followed by stdout.flushFile() |
| Macro-generated code | --expandMacro:<name> |
| Unexpected copy, move, or destruction | --expandArc:<proc> under the project memory manager |
| Pointer, manual allocation, or FFI memory error | AddressSanitizer with the complete required recipe |
| Independent leak or invalid-access check | Valgrind |
Stack traces and build modes
Default build behavior:
| Mode | Unhandled exception trace | writeStackTrace() |
|---|---|---|
default or -d:debug | full paths and line numbers | full trace |
-d:release | raising frame only | no traceback available |
-d:danger | raising frame only | no traceback available |
- Add
--lineTrace:onto the failing release or danger command to restore the call chain and line numbers. It implies--stackTrace:on. - Use
--excessiveStackTrace:offwhen filenames are sufficient; the defaultonform includes full paths. - Call
writeStackTrace()when the current path matters before an exception exists. - Read the trace from the failure backward, then inspect values along that path until the first invalid state is found.
Runtime inspection
- Use
echofor ordinary values andreprwhen a type lacks useful$formatting. - Use
debugEchoinside{.noSideEffect.}code; ordinaryechois rejected there. echoflushes automatically. Flushstdout.writeexplicitly when a crash could occur before the buffer drains.- Label diagnostic values with the phase and invariant being checked. Avoid dumping large structures when one length, index, tag, or identity answers the hypothesis.
compiles(expr)is a compile-time boolean probe. If it returnsfalse, compile the candidate normally to obtain the actual diagnostic.fieldsandfieldPairsuse the expression's static type, not its runtime subtype. Usewhen, notif, for code that depends on a field's name or type.
Compiler-generated code
- Run
nim c --expandMacro:<name> file.nimto inspect a macro's expanded AST. The compiler emits an[ExpandMacro]hint. - Run
nim c --expandArc:<proc> file.nimto inspect injected copy, sink, move, and destroy operations. - Keep an
--expandArctarget reachable from the entry point and use the target project's--mm:mode. - Measure copy behavior inside a named proc; last-use analysis does not fire at module top level.
- Treat expansion output as evidence about generated operations, not as a
reason to optimize by itself.
moveis destructive and requires deliberate last use plus behavior tests. - Nim defaults to ORC. Read the compiler build hint because project configuration may select ARC or atomic ARC.
AddressSanitizer
Use ASan for ptr, addr, cstring, cstringArray, manual allocation, and
C-FFI memory faults. The required Linux/GCC recipe is:
bashnim c \ --passC:"-fsanitize=address -fno-omit-frame-pointer" \ --passL:"-fsanitize=address -fno-omit-frame-pointer" \ -g \ -d:useMalloc \ -d:noSignalHandler \ file.nim
- Pass sanitizer flags to both compilation and linking.
-d:useMallocis required so ASan observes memory obtained through Nim's allocator.-d:noSignalHandleris required so ASan, rather than Nim's signal handler, reports signal faults.-ggives symbolized Nim locations with GCC. Add--cc:clangfor Clang and ensurellvm-symbolizeris inPATH.- Preserve the project's memory manager and failing input.
- Read both the invalid-access stack and the allocation/free stack before editing code.
See references/memory_sanitizers.md for a complete runnable example. Platform
recipes not verified in this repository are intentionally omitted.
Valgrind
Use Valgrind as an independent native-memory check:
bashnim c -g -o:program file.nim valgrind --leak-check=full --error-exitcode=1 ./program
It requires no sanitizer instrumentation and is slower than ASan. A non-zero exit distinguishes detected errors in automated checks.
Debug symbols and native debuggers
-gand--debugger:nativeenable native debug information and line directives.- This project does not use GDB: Nim name mangling and generated C make variable inspection slower and less reliable than stack traces, focused inspection, ASan, Valgrind, and compiler expansion.
Workflow
- Reproduce the failure with an exact command and fixed input.
- Minimize unrelated setup while keeping the same failure and configuration.
- State one falsifiable hypothesis and the invariant it predicts.
- Select the narrowest evidence source from the failure-class table.
- Follow the failing path backward and locate the first invalid state.
- Fix the invariant at its owner instead of suppressing the final symptom.
- Re-run the original reproduction and relevant sanitizer or expansion check.
- Add a regression test, then remove temporary prints and scratch artifacts.
Common Mistakes
| Mistake | Why it is wrong |
|---|---|
| Changing input, build mode, and code together | The result cannot identify which change affected the failure |
| Reading only the final crashing line | Corruption or invalid state commonly begins earlier |
| Expecting full traces from release or danger defaults | Add --lineTrace:on to the failing optimized command |
Using compiles as a replacement for compiler diagnostics | It answers only yes or no and suppresses the candidate diagnostic |
Printing with stdout.write immediately before a crash without flushing | Buffered evidence can be lost |
Running ASan without -d:useMalloc | Nim allocator overflows can remain invisible to ASan |
Running ASan without -d:noSignalHandler | Nim can intercept signal faults before ASan reports them |
| Passing ASan flags only to C compilation | The sanitizer runtime must also be linked |
Using Clang ASan without llvm-symbolizer | Reports can contain raw addresses instead of Nim locations |
Adding move merely because --expandArc shows a copy | move changes ownership semantics and must be safe at last use |
| Using GDB for routine Nim debugging | The project uses faster Nim-aware evidence sources instead |
References
references/stack_trace_diagnosis.md— Boundary instrumentation for finding the first bad value behind a crash.references/memory_sanitizers.md— ASan and Valgrind commands for unsafe access and leaks.references/arc_optimization.md—expandArccomparison for unexpected ownership copies.


