Nim Best Practices
Idiomatic Nim: the partial case/underscore-insensitive identifier rule, the module export system, choosing proc/func/template/macro/iterator/method, ARC/ORC value semantics, exception-based error handling with the raises effect system, the implicit result variable, discard, and release/danger compilation flags.
Introduction / Overview
Nim reads like Python and, since version 2.0, behaves at runtime much more like modern C++ or Rust: values are moved and destroyed deterministically by compiler-inserted hooks rather than tracked by a background garbage collector. Writing idiomatic Nim means understanding a handful of semantics that have no close analogue in Python, JavaScript, or even Go - most critically, the language's partial case/underscore-insensitive identifier equality, the distinction between six different callable-like constructs (proc, func, template, macro, iterator, method), and the ARC/ORC value-semantics model that replaced the pre-2.0 default GC.
This document assumes the ecosystem orientation in README.md and focuses on writing correct, idiomatic Nim day to day. For an exhaustive catalogue of traps beyond what is covered here, see commonpitfalls.md; for deep macro/template technique, see metaprogramming.md.
When to use this guidance: You are writing or reviewing production Nim code - a library, a service, or a CLI tool - and need patterns verified against the current (2.0+) language semantics rather than pre-2.0 assumptions.
Core Concepts
- Partial style-insensitivity: Nim identifier equality ignores all underscores and is case-insensitive for every character except the first. This is a parser-level rule, not a linter suggestion - it affects what the compiler considers a name clash or a match.
- Deterministic destruction: Under the ARC/ORC default, every value's lifetime is governed by compile-time-inserted
=destroy/=copy/=sinkhook calls, not a runtime tracing collector. - Exceptions, tracked statically by an opt-in effect system:
raise/try/exceptis the default control-flow mechanism for errors; the{.raises: [...].}pragma lets the compiler prove which exceptions a routine can propagate. resultis implicit: any non-voidproc/func/method/converter/iterator(context-dependent) has an implicitly declaredresultvariable, pre-initialized to that type's default (zero) value.- Nothing is silently discarded: a call's return value must be explicitly consumed or
discard-ed unless the routine is marked{.discardable.}.
Best Practices
1. Naming Conventions and Nim's Partial Case/Underscore-Insensitivity
Rationale: The Nim manual defines identifier equality precisely as: two identifiers are equal if their first characters are identical (case-sensitively) and the remainder of each identifier is equal after removing all underscores and lower-casing every other character. Formally, a[0] == b[0] and a[1..^1].replace("", "").toLowerAscii == b[1..^1].replace("", "").toLowerAscii. This means myVariable, my_variable, and myvariable are the same identifier to the compiler - not merely a style nit, a hard equality rule that determines redeclaration errors, overload matches, and import name clashes. MyVariable, by contrast, is a genuinely distinct identifier from myVariable, because the first character's case is compared exactly.
let myVariable = 1
# let my_variable = 2 # compile error: redeclaration - this is the SAME identifier as myVariable
let MyVariable = 3 # OK - distinct identifier, first letter's case differs
This rule is exactly why Nim's style convention exists and matters: use camelCase for procs, variables, and fields, and PascalCase for type names - the first-letter case difference is then the only thing separating a type name (Foo) from a conventionally-named local variable or constructor-like proc (foo), a pattern used throughout the standard library (var s: Stack; let s = newStack()-style code depends on this). Never rely on an underscore or internal capitalization alone to distinguish two identifiers (e.g., naming both orderId and order_id as "different" variables in the same scope) - that is a compile error waiting to happen, not a stylistic choice, and it is one of the most common mistakes an LLM makes when porting variable-naming habits from Python or JavaScript into Nim.
Why this works well in production: Consistent camelCase/PascalCase usage sidesteps the ambiguity entirely - code that never depends on case/underscore variation to distinguish identifiers never triggers this rule's sharp edges. See commonpitfalls.md for further worked examples of this rule interacting with imports and overloads.
2. The Module and Export System
Rationale: import and include are semantically distinct and not interchangeable. import loads another file as a module and brings only its explicitly exported symbols into scope. include performs a raw textual paste of the target file's contents into the current file at compile time - no module boundary is created, and included files typically share the includer's scope directly (used for splitting one logical module across files, not for consuming a library).
# inventory.nim
proc reserveStock*(sku: string, qty: int): bool =
result = qty > 0
proc internalHelper(sku: string): string =
sku.toUpperAscii()
# main.nim
import inventory
discard reserveStock("SKU-1", 5) # OK: exported with *
# discard internalHelper("SKU-1") # compile error: not exported, invisible outside its module
Every symbol (proc, type, variable, constant, field) is private to its defining module by default; appending * immediately after the name marks it as exported and visible to importers. A common re-export pattern gathers several internal modules behind one public "facade" module using export:
# api.nim - re-exports selected internals as one public surface
import ./inventory, ./billing
export inventory, billing
Why this works well in production: Explicit * export makes a module's public API surface grep-able and intentional - nothing is accidentally public - and prevents the extremely common LLM-generated-code failure of importing a stdlib or third-party module and calling a symbol that exists in the source file but was never marked exported (the compiler reports it as simply undeclared, which is confusing until this rule is understood). See the dedicated pitfall in commonpitfalls.md.
3. proc vs func vs template vs macro vs iterator vs method
Rationale: Nim has six callable-like constructs; defaulting to the wrong one either hides bugs (side effects in something that looks pure) or reaches for far more machinery than the problem needs (a macro where a template would do, or a method where a plain proc would do).
| Construct | Dispatch / evaluation | Side effects | Typical use |
|---|---|---|---|
proc | Ordinary compiled routine, statically resolved (or generic) | Allowed | The default for almost all functions |
func | Exactly a proc, sugar for proc ... {.noSideEffect.} | Compiler-enforced none - cannot mutate globals or call non-func side-effecting code | Pure computations; the compiler rejects the declaration if it actually has a side effect |
template | Compile-time, hygienic syntactic substitution, inlined at every call site before type-checking | Whatever the substituted code does | Simple inline code reuse, DSL sugar, avoiding call overhead, injecting boilerplate |
macro | Compile-time, operates on and returns the AST (NimNode) directly | Whatever the generated code does | Genuine compile-time code generation/transformation that a template's simple substitution cannot express - see metaprogramming.md |
iterator | Lazily produces a sequence of values via yield, driven by a for loop (or, if {.closure.}, a first-class iterator value) | Allowed | Custom traversal logic, lazy/multi-step generation without building an intermediate collection |
method | Runtime, dynamic dispatch based on an object's runtime type; requires participating types to inherit from a common (typically RootObj-derived) base | Allowed | Genuine runtime polymorphism over a class hierarchy known only at runtime |
func square(x: int): int = x * x # compiler-verified pure
proc logAndSquare(x: int): int = # proc: side effects (logging) permitted
echo "squaring ", x
x * x
template unless(cond: bool, body: untyped): untyped =
if not cond: body # pasted inline at each call site, no proc-call overhead
iterator countTo(n: int): int =
var i = 0
while i <= n:
yield i
inc i
type
Shape = ref object of RootObj
Circle = ref object of Shape
radius: float
method area(s: Shape): float {.base.} = 0.0
method area(s: Circle): float = 3.14159 * s.radius * s.radius
Why this works well in production: prefer static dispatch - plain proc overloading or generics with a type-class-style constraint - over method as the default polymorphism mechanism. Static dispatch is resolved and inlinable at compile time, avoids the runtime type-information and vtable-like overhead method requires, and does not force every participating type into a shared inheritance hierarchy; reach for method only when the set of concrete types genuinely is not known until runtime (e.g., a plugin registry). Prefer func over proc wherever a routine is genuinely pure - the compiler then guarantees purity for you, catching accidental side effects at compile time rather than in production. Prefer template over macro whenever simple substitution suffices; a macro's full AST access is powerful but harder to read, harder to debug, and unnecessary for most inlining/boilerplate-elimination use cases - see metaprogramming.md for the deep-dive on both.
4. Memory Management: ARC/ORC and Value Semantics
Rationale: Since Nim 2.0, ORC (ARC plus a back-up cycle collector) is the default memory management mode, replacing the deferred reference-counting tracing GC (--mm:refc) that was the default throughout Nim's 1.x line. Under ARC/ORC, the compiler inserts calls to compiler-synthesized (or user-overridden) =destroy, =copy, and =sink hooks at well-defined points - variable declaration, assignment, and routine calls - rather than relying on a background collector to reclaim memory eventually. The practical consequence: ordinary seq, string, and object values behave as value types with move semantics, not as hidden references. Passing a seq into a function that takes ownership, or returning one from a function, is compiled into a move (a cheap pointer/length transfer with the source invalidated) whenever the compiler can prove the source is not used again, and a real copy only when it cannot.
proc consume(data: sink seq[int]) =
echo data.len # `data` is owned here; the compiler moves rather than copies when possible
var big = @[1, 2, 3, 4, 5]
consume(big) # moved into `consume` - no deep copy, `big` should not be read afterward
Most ordinary Nim code needs no manual ref/pointer juggling at all: define plain object types, pass them by value or by var/sink parameter, and let the compiler manage lifetime. Reach for ref object (heap-allocated, ORC-cycle-collected) specifically when you need: genuinely shared mutable state across multiple owners, an explicit heap allocation (large objects you do not want copied, or objects whose address must stay stable), or a recursive/cyclic data structure (trees with parent pointers, doubly linked lists, graphs) where value semantics cannot express the shape at all.
type
Node = ref object # ref: heap-allocated, shared, ORC collects cycles automatically
value: int
next: Node # a plain `object` (value type) could not express this self-reference
Why this works well in production: deterministic destruction means no GC pause to reason about under load, predictable latency, and RAII-style resource cleanup (closing files, releasing locks) exactly at scope exit - while still freeing the programmer from manual malloc/free bookkeeping for the common case. --mm:refc remains available (nim c --mm:refc) for legacy codebases or specific compatibility needs, but new code should target ORC (the default) or ARC explicitly.
5. Error Handling
Rationale: Nim's default error-handling mechanism is exceptions - raise SomeError.newException("message"), caught with try/except/finally - not return-value-based error codes. This is a deliberate design choice distinct from Rust's Result<T, E>-everywhere or Go's if err != nil, and it is easy for an LLM trained heavily on Rust/Go conventions to under-use.
type InsufficientInventoryError = object of CatchableError
proc reserveStock(available, requested: int) =
if requested > available:
raise newException(InsufficientInventoryError, "requested more than available")
try:
reserveStock(available = 2, requested = 10)
except InsufficientInventoryError as e:
echo "reservation failed: ", e.msg
finally:
echo "reservation attempt complete"
For statically constraining which exceptions a routine may raise, use the {.raises.} effect-tracking pragma - an empty list is a compile-time guarantee that a routine raises nothing:
proc parsePositive(s: string): int {.raises: [ValueError].} =
result = parseInt(s) # parseInt itself may raise ValueError; anything else is a compile error
if result <= 0:
raise newException(ValueError, "must be positive")
proc pureAdd(a, b: int): int {.raises: [].} =
a + b # compiler verifies this genuinely cannot raise
For expected, non-exceptional absence-of-value or recoverable-failure cases, prefer an explicit type over exceptions: the standard library's std/options (Option[T], some/none) for "a value might legitimately be absent," and the widely used community results package (arnetheduck/nim-results, installable via nimble install results) for a Result[T, E] value-or-error type modeled closely on Rust's Result. Prefer exceptions for genuinely exceptional, rarely-handled-at-the-call-site failures (a malformed config file aborting startup); prefer Option/Result for expected, frequently-handled-at-the-call-site outcomes (a lookup that legitimately may miss, a validation that legitimately may fail) where forcing the caller to handle both branches at the type level is valuable.
import std/options
proc findUser(id: int): Option[string] =
if id == 1: some("alice") else: none(string)
let user = findUser(2)
if user.isSome:
echo user.get()
else:
echo "no such user"
6. The Implicit result Variable
Rationale: Every proc, func, method, or converter with a non-void return type has an implicitly declared variable named result, pre-initialized to that type's default (zero) value - 0 for integers, "" for strings, false for bool, nil for ref/ptr, an empty seq/default-initialized object otherwise. If control flow reaches the end of the routine body without an explicit return, the routine returns whatever result currently holds.
proc classify(x: int): string =
if x < 0:
result = "negative"
elif x == 0:
result = "zero"
else:
result = "positive"
# no explicit `return` needed - `result`'s final value is returned
Two mistakes are common here, both worth checking for in generated code: forgetting result exists and declaring a redundant local variable that is then explicitly return-ed instead (harmless, but non-idiomatic and easy to get wrong when refactoring), and assuming result starts uninitialized - it does not; it always starts at the type's default zero value, so a routine that fails to assign it on every path silently returns that zero value rather than erroring, which can mask logic bugs.
# Bug: forgot to set result on one branch - silently returns "" (string default) instead of erroring
proc riskyClassify(x: int): string =
if x > 0:
result = "positive"
# missing branch for x <= 0 - result stays "" here, easy to overlook in review
7. discard and the Discardable-by-Default Rule
Rationale: Nim requires every call whose return value is not used to be explicitly wrapped in a discard statement - an unused, non-void return value is a compile-time error (not merely a warning), unless the callee is marked {.discardable.}. This exists to catch the extremely common bug class of calling a function for its return value and then accidentally ignoring it (a failed-write check, an error code, a builder-pattern result used only for its side effect).
proc computeChecksum(data: string): int = data.len # ordinary proc, return value matters
# computeChecksum("abc") # compile error: value of type `int` has to be used (or discarded)
discard computeChecksum("abc") # OK: explicitly acknowledges the value is intentionally unused
proc logEvent(msg: string): bool {.discardable.} =
echo msg
result = true
logEvent("startup complete") # OK without discard - explicitly opted in via {.discardable.}
Why this works well in production: mark a proc {.discardable.} only when ignoring its result is a genuinely valid, common usage pattern (logging helpers, builder-style chained calls) - for everything else, the compiler-enforced discard requirement turns a whole class of "I forgot to check this" bugs into a compile error instead of a silent runtime mistake.
8. Compilation Flags for Development vs. Production
Rationale: Nim's runtime safety checks (bounds checking, integer overflow checking, nil dereference checking, and more) are on by default and during ordinary -d:release builds; only -d:danger strips them all for maximum throughput, and the memory-management mode and threading support are independent, explicitly selectable flags.
| Flag | Effect |
|---|---|
| (no flag, debug build) | All runtime checks on; no optimization; best compile-time error messages and stack traces |
-d:release | Enables compiler optimizations; keeps runtime safety checks (bounds, overflow, nil) enabled - the right default for production binaries that still want safety nets |
-d:danger | Enables optimizations and disables all runtime safety checks (bounds, overflow, nil, assertions) for maximum performance - use only after the code is proven correct by tests, and treat it the same way you would treat disabling -fno-stack-protector in C |
--mm:orc | Explicitly select ORC (already the default since Nim 2.0) |
--mm:arc | ARC without the cycle collector - smaller/faster, but leaks genuine reference cycles; avoid with the stdlib async dispatcher, which creates cycles |
--mm:refc | The pre-2.0 default deferred reference-counting tracing GC; mainly relevant for legacy-codebase compatibility |
--threads:on | Enables the threading library/runtime support (implicitly on by default in recent Nim versions for most configurations, but still worth setting explicitly for clarity and for older-Nim compatibility) |
# nim.cfg or config.nims at the project root - checked in, not typed per-invocation
--mm:orc
--threads:on
# then simply:
# nim c -d:release src/orders.nim (safety checks kept, optimized)
# nim c -d:danger src/orders.nim (all runtime checks stripped, fastest)
Why this works well in production: put these flags in a project-level nim.cfg/config.nims rather than expecting every developer or CI job to remember and retype them - this guarantees every build (local, CI, release) uses the same memory-management mode and threading configuration, and makes the release-vs-danger safety trade-off an explicit, reviewed project decision rather than an individual invocation's accident. See concurrency.md for the full implications of --threads:on combined with ARC/ORC.
Common Pitfalls & Anti-Patterns
This section lists a starter set; commonpitfalls.md is the canonical, exhaustive reference and should be consulted before trusting any nontrivial generated Nim code.
Pitfall: Confusing Inclusive .. with Exclusive ..< Ranges
Problem: 0..5 is an inclusive range (0, 1, 2, 3, 4, 5 - six elements), while 0..<5 is exclusive of the upper bound (0, 1, 2, 3, 4 - five elements). Code ported from a language where slicing/ranges default to exclusive (Python's range(5), Rust's 0..5) frequently uses 0..n in Nim expecting n exclusive semantics, silently processing one extra element.
Recommended approach: Use 0..<n (equivalently 0 ..< n) whenever the upper bound should be exclusive - most commonly when iterating a 0-indexed collection by its .len.
let items = @["a", "b", "c"]
for i in 0..<items.len: # correct: indices 0, 1, 2 - matches items.len == 3
echo items[i]
# for i in 0..items.len: # bug: would include index 3, an out-of-bounds access
Pitfall: Treating seq, array, and openArray as Interchangeable Parameter Types
Problem: seq[T] is a dynamically resizable, heap-allocated sequence; array[N, T] is a fixed-size, stack-allocatable collection whose length is part of its type; openArray[T] is a parameter-only, backend-agnostic view that accepts either a seq[T] or an array[_, T] without copying. Writing a function parameter as seq[T] when it only ever reads elements needlessly restricts callers (an array argument would need conversion) and can force an unnecessary copy.
Recommended approach: Accept openArray[T] for read-only parameters whenever the concrete container type (fixed vs. growable) does not matter to the function body; reserve seq[T] for parameters/return types that specifically need growable, owned storage.
proc sum(xs: openArray[int]): int = # accepts both seq[int] and array[N, int] with no copy
for x in xs: result += x
echo sum(@[1, 2, 3]) # seq
echo sum([1, 2, 3]) # array - also works, no conversion needed
Pitfall: Forgetting the * Export Marker, Then Concluding the Symbol "Doesn't Exist"
Problem: A generated or hand-written module defines a proc/type without a trailing *, and an importer's call fails with "undeclared identifier" - which reads exactly like a typo or missing import, not like a visibility rule, and often leads to adding an unnecessary re-declaration or unrelated import fix instead of the one-character actual fix.
Recommended approach: When an imported symbol is reported as undeclared despite the module clearly defining it, check for a missing * on its definition before assuming the import path or spelling is wrong (see Best Practice #2).
Pitfall: Assuming Pre-2.0 Memory-Management Behavior
Problem: Generated code (or advice recalled from training data) that manually calls GCref/GCunref, assumes ref objects behave like the old refc deferred tracing GC, or explains Nim's memory model purely in terms of a background collector is describing Nim 1.x's previous default, not the ARC/ORC model that has been default since Nim 2.0 (August 2023).
Recommended approach: Default to describing and relying on ARC/ORC's compile-time-inserted destructor/copy/move hooks (Best Practice #4); treat any pre-2.0-flavored memory-management explanation as suspect until checked against the current Nim manual or mm.html.
Pitfall: Reaching for method and Inheritance as the Default Polymorphism Tool
Problem: Developers arriving from Java/C++/Python instinctively model every "different behavior per type" problem as a class hierarchy with virtual methods, reaching for Nim's method + ref object of Base by default - incurring runtime dispatch overhead and forcing an inheritance hierarchy onto types that do not otherwise need one.
Recommended approach: Default to proc overloading or generics with a constraint (a concept or a simple type-class-style where) for the common case where the concrete types involved are known at compile time; reserve method/inheritance for genuine runtime polymorphism over types not known until runtime (see Best Practice #3).
Pitfall: Ignoring a discard-Required Compile Error by Wrapping Everything in discard
Problem: Once a developer hits their first discard-required compile error, a common overcorrection is to reflexively prefix every call with discard, including calls whose return value genuinely should be checked (e.g., a parsing function's success flag, a validation result).
Recommended approach: Treat each discard-required error as a prompt to decide, case by case, whether the value is truly unneeded (then discard it) or whether the caller has a latent bug from ignoring it (then handle it properly) - see Best Practice #7.
Testing Strategies
std/unittestfor everyday project-level tests:suite/testblocks,check/requireassertions, straightforward integration intonimble test.testamentfor larger projects or the kind of cross-backend (C/C++/JS), process-isolated test suites the Nim compiler itself uses; adds statistics, HTML reports, and parallel execution unittest lacks. Seetesting.mdfor setup and CI integration of both.- Property-based / fuzz-style testing: exercise parsers and serializers with generated inputs rather than only hand-picked examples, particularly around the range/openArray pitfalls above.
- Prefer testing the public (
*-exported) API surface of a module directly, mirroring how consumers actually call it, over reaching into private internals.
Recommended test stack for this topic:
- Primary:
std/unittest(bundled) for unit-level coverage. - Scale-up:
testamentonce a project needs multi-backend or process-isolated test execution. - See
testing.mdfor the full comparison and CI wiring.
Performance, Security & Scalability Considerations
-d:releasefor production by default,-d:dangeronly after correctness is established: stripping bounds/overflow/nil checks (Best Practice #8) trades a real safety net for throughput - treat it the same way as disabling equivalent C compiler protections.- ARC/ORC's deterministic destruction avoids GC-pause latency spikes entirely for typical workloads, a meaningful advantage for latency-sensitive services versus tracing-GC languages.
sinkparameters and explicit moves reduce copy overhead in hot paths - annotate parameters that take ownership of largeseq/string/objectvalues assink Trather than relying on the compiler to infer a move is safe.- Effect tracking (
{.raises.},{.noSideEffect.}/func) is a compile-time security and correctness tool, not just documentation - use it on public library APIs so callers get a compiler-enforced contract about failure modes and purity. - Supply-chain hygiene: pin dependencies via
nimble.lock(or Atlas's Git-commit pinning) and review third-party.nimblepackage sources before adopting them, the same discipline recommended generally in Autonomous, Least-Privilege, Portable Execution Environments.
Edge Cases & Advanced Usage
- Mixing
--mm:arcwith the standard async dispatcher: the stdlibasyncdispatchimplementation creates reference cycles; using bare ARC (no cycle collector) with it leaks memory - use the default ORC, orchronos, for async code (seeconcurrency.md). - Overriding
=destroy/=copy/=sinkmanually: advanced object types (custom containers, FFI-owned resources) can define their own hooks to control exactly how ARC/ORC manages their lifetime - seeinteropffi.mdfor the FFI-ownership use case specifically. {.raises: [].}on library boundary procs: adopt this aggressively on public APIs to give callers a compiler-checked guarantee, while accepting it requires auditing every transitive call for its own effect annotations.- Migrating a pre-2.0 codebase: expect to revisit any code that manually managed
GCref/GCunrefor assumedrefcsemantics; the Nim 2.0 release notes document thestrictFuncsdefinition tightening (a store through aref/ptrdereference is now disallowed in afuncin more cases than before) as a specific, common migration friction point.
When Not to Use / Alternatives
| Dimension | Nim (idiomatic, as above) | Rust equivalent | Go equivalent | Winner |
|---|---|---|---|---|
| Error-handling verbosity | Exceptions by default, Option/Result opt-in for expected failures | Result<T, E> everywhere, more upfront design | Explicit but simple (if err != nil) | Context-dependent |
| Compile-time safety guarantees | Effect system opt-in ({.raises.}, func), not enforced by default | Strongest (ownership + exhaustive Result) | Weaker | Rust |
| Metaprogramming power | Very high (macro full AST access) | High but more ceremony (proc macros) | Low (no generative macros) | Nim |
| Iteration speed for CRUD-style services | Fast (Python-like syntax, fast compiles) | Slower (borrow checker friction) | Fast | Nim / Go |
| Ecosystem breadth | Smaller | Larger, fast-growing | Very large | Rust / Go |
Choose Rust (see ../rust/README.md) when compile-time-enforced memory/thread safety is non-negotiable and the team can absorb the ownership learning curve.
Choose Go (see ../go/README.md) when team velocity and a very large existing ecosystem outweigh Nim's metaprogramming flexibility and multi-backend compilation.
Related Documentation
- Nim Language Overview & Ecosystem
- Nim Common Pitfalls & Anti-Patterns
- Nim Metaprogramming
- Nim Concurrency
- Nim Testing
- Nim FFI/Interop
- SOLID Principles
- Autonomous, Least-Privilege, Portable Execution Environments
Glossary
- Partial style-insensitivity: Nim's identifier equality rule - case-sensitive on the first character only, case-insensitive and underscore-ignoring thereafter.
func: Sugar forproc ... {.noSideEffect.}- a compiler-enforced pure routine.sinkparameter: A parameter declaredsink Tthat takes ownership of its argument, enabling the compiler to move rather than copy when it can prove the source is not reused.=destroy/=copy/=sink: The compiler-synthesizable (or user-overridable) hook procs that implement ARC/ORC's deterministic destruction, copying, and move semantics for a type.- Effect system: Nim's compile-time tracking of a routine's side effects and raised exceptions, surfaced via pragmas like
{.raises.}and{.noSideEffect.}. result: The implicitly declared, zero-initialized variable available in any non-voidroutine, returned automatically if no explicitreturnexecutes.{.discardable.}: A pragma opting a routine's return value out of the otherwise-mandatorydiscardrequirement.openArray[T]: A parameter-only, copy-free view type accepting either aseq[T]or anarray[_, T]argument.
Maintenance note: When updating this document, also update last_updated and re-verify the ARC/ORC default, effect-system pragma syntax, and any package (results, nimble, atlas) maturity claims against the current Nim manual and Nim blog.