Nim Development
Decision-making for Nim, plus a research discipline for a small language with sparse, version-fragmented docs. Verify against primary sources — do not trust stale priors.
Read references/nim4friends_rules.md in full before anything else —
mandatory. It governs how you read the canon (nim4friends.txt) and
where candidates go (trap-inbox.txt). All three ship in the same repo as
this skill — do not assume the entries file is empty. This skill does
not repeat those entries; it complements them with decision-making and a
way to learn new traps correctly.
And, non-negotiably, feed lessons back into it — see Recording lessons. The value of this file compounds only if every session that learns something writes it down.
⚠️ Why Nim needs a research discipline
Nim is small and its training-data footprint is thin, so model priors on Nim
are often wrong or version-stale. Behavior changed materially across
versions (e.g. CatchableError is 2.0+, --mm:orc became the default in 2.0,
the exception hierarchy was refactored). Never answer a Nim API question from
memory alone — anchor to the version and confirm against a primary source.
1. Anchor to the version first
nim --version
Nim docs are versioned; an idiom that is correct on devel may not compile on
the installed compiler, and vice-versa. State the target version before
committing to an API.
2. Source hierarchy (most authoritative first)
- Official versioned docs
- Manual: https://nim-lang.org/docs/manual.html
- Stdlib index: https://nim-lang.org/docs/lib.html
- Per-module:
https://nim-lang.org/docs/<module>.html(e.g./docs/times.html) - Docs index / other versions: https://nim-lang.org/documentation.html
- The library's own source +
tests/+examples/on GitHub. For sparse-doc libraries (pixie, arraymancer, nimhdf5) the source is the documentation — thetests/directory shows real, compiling API usage. Read it before guessing.Semantics exception: when the answer depends on what a template, macro, or heavy generic expands to (result shape/rank, ordering, mutation vs copy), a minimal runtime probe (
nim c -r, ≤10 lines, written under /tmp) outranks reading the source — template source answers "does this exist / which path runs"; only running code answers "what comes back" (see the[arraymancer]rank-preservation entry for the canonical instance). - Version diffs & community
- Nim changelog (release notes / migration): the
changelog.mdinnim-lang/Nimon GitHub. - Forum: https://forum.nim-lang.org/
- Nim changelog (release notes / migration): the
- GitHub code search across
.nimfiles for real-world usage patterns when docs are silent on an idiom.
3. Package discovery
nimble search <term> # CLI is primary — reliable
nimble install <pkg>
nimble.directory (the web UI) is frequently down (502) — don't depend on it.
Fallback package index: the packages.json in nim-lang/packages on GitHub
(raw: raw.githubusercontent.com/nim-lang/packages/master/packages.json).
4. Local fallback when online docs are missing
nimble path <pkg> # locate installed package source, then read it
nim doc <file.nim> # generate HTML docs from source
nim jsondoc <file.nim> # machine-readable API dump
5. Verify, then record
Confirm the API/flag against a primary source before trusting it. When you discover a new trap, record it — see Recording lessons. This closes the loop: the memory file stays the trap log, this skill stays the method.
6. Vet the library before committing to it
Nim's ecosystem is small — usually one library covers a given task, and unmaintained libraries go stale quickly. So selection is a liveness and compatibility check, not a feature comparison:
- Prefer recent activity and explicit Nim 2.x support. Check the repo's last commit and whether its docs mention your Nim version.
- Prove it compiles on your version first.
nim cone of the library'sexamples/as a throwaway before writing project code —nimble installonly confirms it downloaded, not that it builds for you. - Several wrappers for one C library? Pick the maintained, version-matched one; drift in the wrapper (not the C library) is the usual breakage.
Recording lessons (MANDATORY)
This is the single most important habit in this skill. Nim's docs are thin;
references/nim4friends.txt is the only thing that stops the next session from
re-hitting a trap you already paid for. A lesson learned and not written down is
wasted.
When to record — mechanical trigger
If the Nim compiler or runtime emitted any error or warning you fixed, a test failed then passed, or a result was wrong then corrected — at least one entry is mandatory. Zero entries after such a session is a violation, not an option. If none of that happened, there is nothing to record — do not invent entries.
Qualifying events:
- A Nim compile error or runtime crash you had to diagnose.
- A result that was silently wrong (parsed to zero, dropped rows, off-by-N).
- A version-specific behavior you confirmed (e.g. differs across 1.x/2.x).
- A library/API surprise where the obvious usage was wrong and you found the right one by reading source/tests.
Rationalizations that mean "record it now"
| Excuse | Reality |
|---|---|
| "Too basic — everyone knows X needs import Y" | Basic is model- and version-dependent; the next session may not know. If it cost a compile cycle, it qualifies. |
| "One-cycle fix — I didn't really diagnose it" | The trigger is hitting and fixing an error, not effort spent. A 30-second fix is 30 seconds saved in every future session. |
| "It's a principle, not a lesson" | The gate rejects entries that can't name the API and quote the literal error/wrong behavior. If you can quote both, it passes by definition. The gate filters style opinions, not small traps. |
| "Already covered by entry X" | The placement rule says extend by reference ("extends the X entry"), not skip. |
| "SKILL.md already warns about this" | Then hitting it anyway proves the warning didn't land — record the trap (and consider fixing the skill). |
| "Not version-specific" | Version stamps annotate version-dependent behavior; plain traps still belong. |
| "I'll record it at the end" | Only if you copy the literal error text the moment you hit it — end-of-session recall paraphrases, and the error string is the grep key. |
Red flags — STOP and record
- "too basic" / "everyone knows"
- "one-line fix"
- "same class as entry X"
- "I'll remember it"
All of these mean: append the entry now.
How to record
Append your candidate to references/trap-inbox.txt following the ADDING
rules in references/nim4friends_rules.md (read it in full before writing).
The inbox is unverified staging — you write here, you do not read here for
guidance. references/nim4friends.txt is the verified canon (and the only
place you read entries from). Only the owner verifies and promotes a candidate
into canon; your job is to submit it canon-shaped so promotion is a
verify-then-move.
Close the loop (this repo)
references/nim4friends.txt and references/trap-inbox.txt live in a git repo.
After appending a candidate (or editing the rules file), commit and push so it
reaches your other machines:
git -C ~/.agents/skills/nim-development add references/nim4friends.txt references/nim4friends_rules.md references/trap-inbox.txt
git -C ~/.agents/skills/nim-development commit -m "nim4friends: <what you learned>"
git -C ~/.agents/skills/nim-development push
Promoting a candidate into canon is the owner's step (see "Promoting
(owner)" in nim4friends_rules.md) — models submit to the inbox, they do not
write canon directly.
This commit-and-push is pre-authorized for the skill repo only and overrides the global "ask before commit" rule; the user's project repos are unaffected and still require explicit request.
Memory management (--mm:)
Decide deliberately; the default changed in 2.0.
--mm:orc → default in Nim 2.0+. ARC + cycle collector. Use for general code
(graphs, closures, anything that can form reference cycles).
--mm:arc → deterministic, no cycle collector. Lowest overhead; use when you
know there are no cycles (or break them with `weak`/manual).
--mm:refc → legacy tracing GC. Only for old code that depends on its behavior.
--mm:none → manual. Only for embedded / no-runtime targets.
Rule of thumb: leave orc unless you have a measured reason. Match the --mm
across all compilation units of a project.
Project structure & nimble
Small (script / one tool):
main.nim
project.nimble
Library or app:
src/<pkg>.nim # entry module, same name as the package
src/<pkg>/*.nim # submodules
tests/t*.nim # testament picks up test files
project.nimble
- Split into modules once a file grows past ~500 lines: cleaner boundaries, per-file compile caching, no accidental reach into private state.
- The
.nimblefile declaresrequires,bin,srcDir;nimble build,nimble test,nimble installdrive it. nimble testdoesn't addsrc/to the import path — import the library from tests via a relative path (import ../src/<pkg>) or a customtesttask with--path:src. See the[nimble]entries innim4friends.txt.- Never define a custom
task build— it shadows nimble's built-inbuild(see[nimble]).nimble buildis debug by default; for an optimized binary addtask releasethat runsnim c -d:release src/<pkg>.nim.
Concurrency — pick the model
async / await (std/asyncdispatch or chronos)
→ I/O-bound: many sockets, HTTP, timers on one thread. Single-threaded
cooperative. chronos is the more actively developed alternative.
threads + channels (std/threads, --threads:on default in 2.x)
→ CPU-bound parallelism or truly independent workers. Share via `Channel`
or `--mm:orc` isolated refs; avoid sharing mutable GC'd refs across threads.
std/threadpool / malebolgia / weave / taskpools
→ data-parallel workloads; prefer a maintained lib over legacy threadpool.
Don't reach for threads to solve an I/O-bound problem — use async.
Error handling
Standardize handlers on except CatchableError as e: — in Nim 2.0 Defect
became a child of Exception, so except Exception swallows programmer-error
crashes you'd rather let propagate. See the [exn] entry in
nim4friends.txt for the full rationale and the 1.x caveat.
Build / compile decisions
(default) debug: all checks, no C optimization. Development.
-d:release C optimization, KEEPS runtime checks (bounds/overflow).
Correct default for unattended/production code parsing untrusted input.
-d:danger strips all checks + release. Maximum speed, no safety net.
--checks:off strips checks without the rest of release.
For cross-compilation and cache pitfalls, see the [build] entries in
nim4friends.txt before touching --cpu/--passC/--nimcache.
C FFI / interop
- Wrap C symbols with
{.importc, header: "foo.h".}; pass includes/libs via{.passC.}/{.passL.}pragmas or in the.nimble/nim.cfg. - For a small, stable C API: write the pragmas by hand.
- For a large header: consider
c2nimorfutharkto generate bindings, then read and trim the output. - Match calling convention (
{.cdecl.}) and struct layout; verify with a tiny round-trip test before building on the binding. - C-library wrappers bind in two modes: link-time (
{.passL.}, shows as NEEDED inldd, fails atnim cif the lib is missing) or runtime ({.push dynlib: ...}, common in thin wrappers around C libraries) — dlopens at first call, solddshows no lib andnim csucceeds without it; a missing lib surfaces at runtime. Check the wrapper for{.push dynlib: ...}to know which.
Testing & style (CI gates)
nim c -r tests/tfoo.nim # unittest suites
testament pattern "tests/t*" # official test runner (categories, spec comments)
nimpretty src/*.nim # reformats files in place
nim check --styleCheck:error src/... # naming; needs a value (:error/:hint/:usages)
nimpretty rewrites files in place. To verify formatting without changing
anything, format a temp copy and diff it against the original (non-empty
diff ⇒ not canonical). nimpretty (formatting) and nim check --styleCheck:error (naming) catch different things — run both.
Deterministic tests for time/IO/random code. Don't call now(), rand(),
or read the environment inside the unit under test — make the dependency an
injectable parameter (e.g. pass a reference DateTime/Time into the proc)
so tests use a fixed value and are reproducible.
Reading Nim errors
Two compiler errors recur constantly and are easy to misread:
type mismatch … first mismatch at [position]— an overload of the proc exists, but your arguments don't bind to it (often a wrong param order or a missing default param). Re-read the candidate signature's param list; e.g.dateTimeis(year, month, day, h, m, s, nanosecond, zone)— year-first, with an extrananosecondparam.undeclared identifier: 'X'/attempting to call undeclared routine: 'X'— you used a symbol without importing its module (Nim does not auto-import most ofstd). Add theimport(e.g.commandLineParamsneedsstd/os;getoptis instd/parseopt;splitis instd/strutils).
Choosing diagnostic evidence
Pick the instrument by symptom class, not by habit. Rows below verified on
Nim 2.2.4 — full evidence in the [build]/[footgun] entries of
nim4friends.txt; re-verify flags on other versions.
| Symptom | First evidence |
|---|---|
| Unhandled exception, default build | Default stack trace — full chain, paths + line numbers |
Crash in -d:release/-d:danger |
Trace shows only the raising frame — re-run the failing command with --stackTrace:on --lineTrace:on (both; --lineTrace:on alone restores nothing) |
| Unexpected copy/move/destroy under ORC/ARC | nim c --expandArc:<proc> file.nim — must name a proc; no --expandArc: <name> header block in the output means wrong target, not "no hooks injected" |
| "What does this macro expand to?" | nim c --expandMacro:<name> file.nim — prints Hint: expanded macro: ... [ExpandMacro] by default; no hint line = wrong name or template target (templates need a runtime probe) |
| Wrong runtime value | echo / repr at invariant boundaries, labeled with the invariant being checked |
| Evidence missing before a crash | Only hard crashes (segfault, exit 139) lose buffered stdout.write — echo auto-flushes and unhandled exceptions flush; add stdout.flushFile() before ptr/FFI/danger-mode code |
ptr/FFI memory fault |
ASan — recipe in the [build] entries; -d:useMalloc is load-bearing (without it the same bug exits 0) |
debugEchoworks inside{.noSideEffect.}code whereechois rejected.compiles(expr)answers yes/no only — if it returnsfalse, compile the candidate normally to get the actual diagnostic.- Change one diagnostic dimension at a time (input, build mode, or code — never two at once); a debug build that stops failing is a comparison point, not a fix.
Decision checklist
- Read
nim4friends_rules.mdin full (mandatory); accessednim4friends.txtper its reading rules? - Confirmed the target Nim version?
- Verified any uncertain API against a primary source (docs/source/tests — or a minimal
nim c -rprobe when the question is what template/macro code does at runtime)? - Vetted the chosen library's liveness + Nim-version fit (§6)?
- Chosen
--mmdeliberately (not by accident)? - Picked the right concurrency model (I/O vs CPU)?
- Selected build flags for this context (release keeps checks)?
- Handlers use
except CatchableError? - When debugging: picked the first evidence by symptom class (§ Choosing diagnostic evidence), one diagnostic dimension at a time?
- Ran
nimpretty(format) andnim check --styleCheck:error? - Recorded any newly-learned trap in
references/trap-inbox.txtand committed/pushed it? (mandatory if you hit an error, a silent-wrong result, or a version-specific behavior — see Recording lessons)
Limitations
- Nim evolves; treat any specific API here as needing confirmation against the installed compiler's docs/source (that is the whole point of this skill).
- Does not replace
nim4friends.txt— that file is the authoritative, evidence-based trap log; this skill is the method and the decisions. - Does not authorize destructive or environment-changing actions without validation against the user's real sources.