Syntax, Statements & Expressions
Layout and Statements
Nim is indentation-based: a colon opens a block, and everything indented under it belongs to that block. Beyond that rule the syntax is deliberately quiet — no mandatory semicolons, no parentheses around conditions, and no braces around bodies.
Blocks by Indentation
Indentation must be consistent inside one block. Mixing tabs and spaces in the same file is the most common cause of a confusing invalid indentation error, so configure your editor to insert spaces (the community style uses two).
# A colon opens a block; the indented lines below it are the body.
for i in 1 .. 3:
if i mod 2 == 0: # nested block, one level deeper
echo i, " is even"
else:
echo i, " is odd"
# Parentheses are optional around conditions and around arguments when the
# call is unambiguous. These two statements are identical:
echo(1 + 2)
echo 1 + 2 # preferred form for a command with one argument
Separators, Not Terminators
A newline ends a statement; a semicolon only separates two statements placed on one line. Compact lines are fine in small doses, but a long chain usually means you should introduce a name for the intermediate value.
let a = 1; let b = 2 # semicolon separates two statements on one line
echo a + b # newline terminates; no semicolon needed
# An operator at the end of a line continues the expression on the next line.
let total = 1 +
2 +
3
echo total # 6
Comments That Nest
Nim has three comment forms, and the block form nests. Documentation comments use ## and are extracted by nim doc, so they describe the contract for callers rather than private notes for yourself.
# Ordinary comment: ignored by the compiler and by 'nim doc'.
## Doc comment: rendered into HTML documentation, so write it for callers.
#[ Block comment.
Unlike C, blocks nest: #[ inner ]# stays inside the outer comment. ]#
proc twice(x: int): int =
## Doubles 'x'. The doc comment states the contract, not the mechanics.
result = x * 2 # 'result' is the implicit return variable of every proc
Expressions and Literals
Almost everything is an expression in Nim, which is why if, case and block can produce a value instead of only steering control. Literals carry their type in their spelling whenever the default is not what you want.
Literal Forms
echo 42 # int literal
echo 0b1010'u8 # 10 — binary digits with an explicit unsigned 8-bit type
echo 0xFF'u8 # 255 — hexadecimal keeps bit-level code readable
echo 1_000_000 # underscores group digits: 1000000
echo 3.14 # float literal (float64)
echo 2.0'f32 # force 32-bit float when memory layout matters
echo 'x' # char literal: a single character in single quotes
echo "text" # string literal
echo true # bool literal
Blocks That Produce Values
Because these constructs are expressions, you can assign the result of a decision directly. This removes the "declare an empty variable, then fill it in from every branch" pattern common in older languages.
let grade = if 87 >= 90: "A" else: "B" # 'if' as an expression
echo grade # B
let label = case 2 # 'case' as an expression
of 1: "one"
of 2: "two"
else: "many"
echo label
# A labeled block can 'break' with a value — a structured early exit.
let parsed = block found:
for word in ["no", "7", "8"]:
if word.len == 1 and word[0] in {'0' .. '9'}:
break found word # the block yields this string
"none" # fallback value of the block expression
echo parsed # 7
String Interpolation
Two idioms replace manual concatenation: the & operator joins strings, and the &"...{expr}..." form interpolates. Interpolation wins whenever a sentence mixes fixed text with several values.
import std/strformat
let name = "Ada"
let version = 2.0
echo "Hello, " & name & "!" # concatenation with the & operator
echo &"Hello, {name}!" # interpolation: same result, easier to read
echo &"{name} runs Nim {version:.1f}" # format specs work inside { }
# Naming a format string once keeps long messages maintainable.
const report = "user={name} version={version}"
echo &report # const strings can be interpolated too
Reading Compiler Diagnostics
Nim reports a code, a location and a plain-language explanation. Learning to skim that output saves more time than memorizing syntax.
The Shape of an Error
# Each line names a diagnostic family, then the object it concerns.
# Error: undeclared identifier: 'naem' -> a typo, not a missing import
# Error: type mismatch: got 'string' but expected 'int' -> a wrong literal or argument
# Hint: 'x' is declared but not used -> dead code: delete it
# Warning: ... is deprecated -> the message names the replacement
Traps Worth Knowing Now
| Symptom | Cause | Fix |
|---|---|---|
invalid indentation | Tabs mixed with spaces, or an inconsistent step | One space step per block, never tabs |
expression expected | An operator ended the line | Indent the continuation line |
undeclared identifier | The symbol is not imported, or is misspelled | Add import std/<module>, then check spelling |
| An unused variable still compiles | The compiler emits a hint, not an error | Read the hints — they catch real bugs early |
Practice
Write a program that converts a Celsius literal to Fahrenheit with a let, formats the result through interpolation, and prints it with a single echo. Then reintroduce a deliberate typo and read the compiler output carefully, so the diagnostic shape becomes familiar. Continue with Types & Values.