Nim — Reference

Source: https://nim-lang.org/documentation.html

Nim

  • Created: design started 2005 by Andreas Rumpf (originally “Nimrod”); first public release 2008; renamed to Nim in 2014 (Wikipedia).
  • Latest stable: Nim 2.2.10, released 2026-04-24 (nim-lang.org).
  • Owner: Nim Language Foundation; lead developer Andreas Rumpf. License: MIT.
  • Paradigms: multi-paradigm — imperative, functional, generic, OO, concurrent, metaprogramming-heavy.
  • Typing: static, strong, with bidirectional type inference. Nominal subtyping for object/ref object types.
  • Memory: choice of memory management modes — --mm:orc (default since 2.0, cycle-collecting ARC), --mm:arc, --mm:refc (legacy default), --mm:markAndSweep, --mm:boehm, --mm:go, --mm:none.
  • Compilation: AOT — compiles to C (default), C++, Objective-C, or JavaScript. The C compiler then emits the native binary.
  • Primary domains: systems programming, scripting, web (frontend via JS backend + backend via C), embedded, scientific computing, game dev, CLI tools.
  • Official docs: https://nim-lang.org/documentation.html

At a glance

Nim is “Python-like syntax that compiles to fast C.” Indentation-based, statically typed, with a metaprogramming system (templates, generics, macros) so powerful that much of the stdlib is implemented as macros. The C-backend approach means Nim binaries are small, fast, easy to embed, and trivially portable to anywhere with a C compiler. ARC/ORC (since 2.0) gave Nim deterministic, cycle-aware reference counting that competes with Rust’s ownership model in throughput while keeping the GC veneer.

Getting started

Install:

  • choosenim is the official version manager (the rustup of Nim): curl https://nim-lang.org/choosenim/init.sh -sSf | sh.
  • Or distro packages (brew install nim, pacman -S nim, apt install nim).
  • Nim depends on a working C compiler (gcc, clang, or MSVC).

Hello world (file hello.nim):

echo "Hello, world!"

Compile + run: nim c -r hello.nim. (c selects C backend; r runs after compile.) Other backends: nim cpp, nim js, nim objc.

Project layout (a Nimble package):

mypkg/
  mypkg.nimble        # package manifest
  src/
    mypkg.nim         # entry point (= module name)
    mypkg/
      submod.nim
  tests/
    test1.nim
  README.md

Package/build tool: Nimble (nimble). nimble init creates the scaffold, nimble install <pkg> installs (from https://nimble.directory/), nimble build / nimble test / nimble publish. The .nimble file is itself a Nim script, so build steps are arbitrary Nim code.

REPL: there is no first-class REPL. inim is the popular community REPL; nim secret (deprecated) was a primitive built-in. The modern interactive workflow is nim c -r with editor save-on-write.

Basics

Types and literals:

  • Integers: int (platform-sized), int8/16/32/64, uint, uint8...64, byte (= uint8).
  • Floats: float (= float64), float32, float64.
  • bool, char (single byte, not Unicode codepoint), string (mutable, UTF-8 by convention but bytewise indexed), cstring (null-terminated, FFI).
  • Containers: array[N, T] (fixed-length), seq[T] (growable), set[T] (bit-set, T must be ordinal), Table[K, V] (in tables module).
  • enum, range[a..b], tuple (anonymous structural — (name: string, age: int)), object (nominal, value-type), ref object (heap-allocated, GC-managed).
  • Distinct types: type Meters = distinct float for newtype-style separation.

Variables/scoping: let (immutable single-assignment), var (mutable), const (compile-time constant). Block-scoped; block: ... for explicit. Two indentation levels make a new scope.

Control flow: if/elif/else, case ... of: (exhaustive on enums), for x in iter, while, block name: + break name, continue, return, try/except/finally. Everything is an expression: let x = if cond: 1 else: 2.

Functions:

proc add(x, y: int): int = x + y      # last expr is result
proc greet(name = "world"): string =
  "Hello, " & name
 
# Method-call syntax: x.add(y) == add(x, y)  ("uniform function call")
echo 2.add(3)                          # 5
echo "abc".len                         # 3 (calls len("abc"))

Variants: proc (regular), func ({.noSideEffect.} proc, no I/O / globals), method (dynamic dispatch on ref object), iterator (yields), template (hygienic AST sub), macro (AST manipulation).

Strings: mutable, length-prefixed, UTF-8 by convention. Concat with &. Interpolation via the strformat macro: fmt"x = {x+1}" or &"x = {x+1}". Indexing s[0] returns a byte (char), not a Unicode rune; use unicode module for rune iteration.

Collections: seq is the dynamic array (@[1, 2, 3] literal). array[3, int] for fixed. Table and OrderedTable from tables. HashSet from sets. Deque from deques. The [] operator is overloadable.

Intermediate

Type system depth:

  • Generics: proc f[T](x: T): T = x. Constraints: proc f[T: SomeNumber](x: T). Type classes: concept (structural typing — duck-typed at compile time).
  • Object inheritance: type Animal = ref object of RootObj (must descend from RootObj to participate in OO). Methods (dynamic dispatch) declared with method; regular proc is statically resolved.
  • Variant objects (sum types): case kind: Kind of A: aField; of B: bField.
  • Object construction: Animal(name: "rex", legs: 4).
  • Generics specialize at compile time (monomorphization, like C++/Rust).

Modules: each .nim file is a module. import foo brings public symbols (those marked with *, e.g. proc bar*() = ...); from foo import bar; include foo is a textual include (rarely used, for split implementations).

Error handling: exceptions (with Nim’s own hierarchy under Exception, CatchableError, Defect). raise newException(IOError, "msg"). try/except IOError as e: .... {.raises: [IOError].} pragma documents and statically checks raised exceptions; {.raises: [].} proves no exceptions. Idiom: Result[T, E] (third-party results package) for error-as-value style à la Rust.

Concurrency primitives:

  • Threads: import threadpool; spawn f() (FlowVar future), or raw createThread. Each thread has its own GC heap by default; sharing requires Channel[T] or ptr types.
  • Async/await: import asyncdispatch; proc fetch(): Future[string] {.async.} = .... Cooperative event-loop via asyncdispatch. The chronos library is the modern alternative (used by Status/Nimbus Ethereum client) with nicer cancellation semantics.
  • Parallelism: parallel: block + spawn for safe data-parallel; OpenMP-like || operator from threading/atomics.

I/O: readFile, writeFile, open(f, fmRead). Streams via streams module. httpclient for HTTP, asynchttpserver for serving. db_sqlite, db_postgres, db_mysql for databases. json, parsecsv, parsexml, parsecfg in stdlib.

Stdlib highlights: strutils (string utilities), sequtils (map, filter, zip), strformat (fmt/&), tables/sets/deques/heapqueue, os/osproc/pathutils, math, random, times/monotimes, re (PCRE bindings), nre (alternative), unicode, algorithm (sort, binarySearch), httpclient, asyncdispatch/asyncnet/asynchttpserver, json, streams, marshal, db_*, unittest.

Advanced

Memory / GC modes (--mm: flag):

  • orc (default since 2.0): ARC + cycle collector. Deterministic, low pause, no stop-the-world; throughput competitive with manual.
  • arc: pure reference counting, no cycle collector — must avoid cycles or leak. Smallest runtime; great for embedded/WASM.
  • refc (legacy default pre-2.0): deferred reference counting + mark-and-sweep cycle collector.
  • markAndSweep: simple mark-and-sweep.
  • boehm: Boehm-Demers-Weiser conservative GC (link-time dependency).
  • go: Go runtime’s GC.
  • none: no GC — fully manual; types using GC become illegal.

ARC/ORC also brings move semantics: =copy, =sink, =destroy, =trace, =dup hooks let you implement value types like Rust’s Drop/Clone.

Concurrency deep dive: each thread has an isolated heap with --mm:refc (default for legacy multi-thread). With --mm:orc/arc, heaps may be shared with appropriate annotations. Channel[T] for thread-safe message passing. The chronos async library is the production choice for high-perf I/O.

FFI:

  • proc cprintf(fmt: cstring) {.importc: "printf", varargs, header: "<stdio.h>".} — call C directly.
  • {.exportc.} to expose a Nim symbol with C name. {.cdecl.}, {.stdcall.} for calling conventions.
  • c2nim tool generates Nim wrappers from C headers.
  • JS backend: {.importjs: "Math.sqrt(#)".} to call JS.

Reflection: rich macro-time. getType(), getTypeImpl(), typeof(x), astToStr(expr). The macros module provides full AST walking inside macros.

Performance tools:

  • nim c -d:release -d:danger — release mode (danger strips runtime checks).
  • --profiler:on (built-in sampling profiler), or use valgrind --tool=callgrind on the C output.
  • --debugger:native for gdb/lldb.
  • nim --gen:cpp and feed to gprof.
  • --nimcache:dir to inspect generated C code.
  • --listCmd to see exactly the C compile invocation.

God mode

Three layers of metaprogramming — pick the lightest that does the job:

  1. Generics: monomorphized type-parameterized procs/types.
  2. Templates: hygienic AST substitution, expanded after parsing but before type-checking. Cheap, parameterized.
  3. Macros: arbitrary Nim code that runs at compile time, takes NimNode AST, returns AST.
# Template — like a macro but pattern-only
template times(n: int, body: untyped) =
  for _ in 0 ..< n: body
 
3.times: echo "hi"
 
# Macro — full AST manipulation
import macros
macro debug(args: varargs[untyped]): untyped =
  result = newStmtList()
  for a in args:
    result.add quote do:
      echo astToStr(`a`), " = ", `a`
 
let x = 42
debug(x, x*2)   # prints: x = 42 \n x*2 = 84

Term-rewriting macros ({.rewrite.}) let you pattern-match expressions and substitute — used for peephole optimizations (a*1 -> a).

Compile-time function execution (CTFE): Nim runs ordinary Nim code at compile time. const x = expensiveCompute() — the value is baked in. The CTFE VM is more capable than most language’s constexpr; it can do file I/O via slurp("path") (file embed), HTTP via staticExec("curl ...").

Choosing memory mode at compile time: nim c --mm:arc --opt:speed --d:release .... For embedded: --mm:none --os:freertos --gc:none. For WASM: --cpu:wasm32 --mm:orc --d:release (or via nimwc toolchains).

Multiple backends from one source:

  • when defined(js): ... else: ... blocks branch on backend.
  • nim js --out:app.js src/app.nim → JavaScript output for the browser, Node, or React Native bridges.
  • nim cpp for C++ (allows interop with C++ classes via {.importcpp.}).
  • nim objc for Objective-C / iOS.

Importc / FFI: {.importc.} + {.header: "<file.h>".} directly imports C declarations. {.passC: "-I..."} and {.passL: "-l..."} add to the C compile/link line.

Hot code reloading: --hotCodeReloading:on allows updating proc bodies in a running process (Linux/macOS, mostly used for game/UI dev).

UFCS (uniform function call syntax): x.f(y) is identical to f(x, y). Combined with method-style chaining and operator overloading, Nim supports DSL-heavy code (e.g., karax HTML DSL).

Idioms & style

  • Naming: camelCase for procs and variables, PascalCase for types, SCREAMING_SNAKE for constants is allowed but PascalCase is also common. Nim is case-insensitive and underscore-insensitive for identifiers (except first letter): myVar, myvar, my_var are the same. (Style: pick camelCase and stick.)
  • Formatter: nimpretty (built-in, comes with the compiler).
  • Linter: no canonical separate linter; nim check does basic semantics; nimsuggest powers IDE diagnostics.
  • Idiomatic patterns:
    • Method-call syntax everywhere: seq.map(f).filter(p).len.
    • func for pure code; let the compiler verify.
    • result is implicitly defined as the named return; result.add x instead of building a temp.
    • Use enum + variant object over class hierarchies.
    • Prefer templates over macros when pattern is simple.
  • Expert review focus: GC mode mismatches (cross-mode FFI is dangerous), ref vs value object decisions, macro hygiene leaks, raised-exception specs ({.raises.}), cstring/string boundary safety, ORC cycle handling for graph types, import pollution.

Ecosystem

  • Web (backend): Jester (Sinatra-style), HappyX (full-stack), Prologue (Flask-like), Karax (frontend SPA via JS backend), Nim/Snip on top of asyncdispatch/chronos.
  • Frontend: Karax (virtual DOM, compiles to JS), nigui (cross-platform GUI).
  • Game / graphics: NimForUE (Unreal Engine bindings), naylib (raylib bindings), nico (PICO-8-like), godot-nim.
  • Crypto / blockchain: Nimbus (Status’ Ethereum client) is the flagship Nim production project; large parts of chronos, confutils, nim-eth come from there.
  • Data / sci: Arraymancer (NumPy/PyTorch-like, including CUDA), Datamancer (DataFrames), measuremancer (units), bignum.
  • Testing: unittest (stdlib, suite/test), testament (the compiler’s own test runner).
  • Docs: nim doc generates HTML from ## doc comments; nim doc2tex for LaTeX. Style: https://nim-lang.org/docs/lib.html.
  • Notable users: Status (Nimbus Ethereum client), ETH proof-of-stake validator infrastructure, several CLI utilities (e.g., choosenim itself), Nitter (Twitter frontend), Awesome Nim list.

Gotchas

  • Style insensitivity: myVar and myvar and my_var are the same identifier (after the first letter). This bites users of Python/Go conventions; tools complain but the compiler doesn’t.
  • GC mode is sticky: a library compiled assuming --mm:refc will surprise you under --mm:orc. Pin the mode in your .nimble’s nim task block or in config.nims.
  • string vs cstring: string is length-prefixed and managed; cstring is a char*. Conversion is mostly automatic but lifetimes are easy to mess up at FFI boundaries.
  • char is one byte: not a rune. Iteration for c in s walks bytes; use for r in s.runes from unicode.
  • var parameters mutate in place, like Pascal’s var. Easy to confuse with var declarations.
  • Index from 0, ranges are inclusive on both ends with .. and exclusive-right with ..<0..3 is [0,1,2,3], 0..<3 is [0,1,2].
  • Implicit return result: forgetting to assign result (or leaving the function before doing so) returns the zero value silently.
  • Macro hygiene: macros generate symbols you might collide with; use genSym() or quote do carefully.
  • method vs proc: method is dynamic dispatch (vtable), proc is static. Forgetting to use method on a polymorphic call gives the base-class behavior silently.
  • --threads:on was the old switch (now defaults on): pre-2.0 code might still use it explicitly.
  • Async cancellation in asyncdispatch is poor; production code uses chronos for proper cancellation semantics.
  • Nimble lockfiles (nimble.lock) are newer (2.x) — older projects don’t pin and can drift.
  • nim c vs nim cpp generates different ABIs; mixing in one project requires care.

Citations