Statements Expressions
Statements and expressions
Nim uses the common statement/expression paradigm: Statements do not produce a value in contrast to expressions. However, some expressions are statements.
Statements are separated into simple statements
{.interpreted-text
role="idx"} and complex statements
.
Simple statements are statements that cannot contain other statements
like assignments, calls, or the return
statement; complex statements
can contain other statements. To avoid the
dangling else problem
, complex
statements always have to be indented. The details can be found in the
grammar.
Statement list expression
Statements can also occur in an expression context that looks like
(stmt1; stmt2; ...; ex)
. This is called a statement list expression or
(;)
. The type of (stmt1; stmt2; ...; ex)
is the type of ex
. All
the other statements must be of type void
. (One can use discard
to
produce a void
type.) (;)
does not introduce a new scope.
Discard statement
Example:
The discard
statement evaluates its expression for side-effects and
throws the expression\'s resulting value away, and should only be used
when ignoring this value is known not to cause problems.
Ignoring the return value of a procedure without using a discard statement is a static error.
The return value can be ignored implicitly if the called proc/iterator
has been declared with the discardable
pragma:
however the discardable pragma does not work on templates as templates substitute the AST in place. For example:
This template will resolve into \"https://nim-lang.org\" which is a string literal and since {.discardable.} doesn\'t apply to literals, the compiler will error.
An empty discard
statement is often used as a null statement:
proc classify(s: string) =
case s[0]
of SymChars, '_': echo "an identifier"
of '0'..'9': echo "a number"
else: discard
Void context
In a list of statements, every expression except the last one needs to
have the type void
. In addition to this rule an assignment to the
builtin result
symbol also triggers a mandatory void
context for the
subsequent expressions:
proc invalid*(): string =
result = "foo"
"invalid" # Error: value of type 'string' has to be discarded
Var statement
Var statements declare new local and global variables and initialize them. A comma-separated list of variables can be used to specify variables of the same type:
If an initializer is given, the type can be omitted: the variable is then of the same type as the initializing expression. Variables are always initialized with a default value if there is no initializing expression. The default value depends on the type and is always a zero in binary.
Type default value
any integer type 0
any float 0.0
char \'\0\'
bool false
ref or pointer type nil
procedural type nil
sequence @[]
string ""
tuple[x: A, y: B, ...] (default(A), default(B), ...) (analogous for objects)
array[0..., T] [default(T), ...]
range[T] default(T); this may be out of the valid range
T = enum cast[T](0); this may be an invalid value
The implicit initialization can be avoided for optimization reasons with
the noinit
pragma:
If a proc is annotated with the noinit
pragma, this refers to its
implicit result
variable:
The implicit initialization can also be prevented by the
requiresInit
type pragma. The compiler
requires an explicit initialization for the object and all of its
fields. However, it does a control flow analysis
{.interpreted-text
role="idx"} to prove the variable has been initialized and does not rely
on syntactic properties:
type
MyObject = object {.requiresInit.}
proc p() =
# the following is valid:
var x: MyObject
if someCondition():
x = a()
else:
x = a()
# use x
requiresInit
pragma can also be applyied to distinct
types.
Given the following distinct type definitions:
type
DistinctObject {.requiresInit, borrow: `.`.} = distinct MyObject
DistinctString {.requiresInit.} = distinct string
The following code blocks will fail to compile:
But these ones will compile successfully:
Let statement
A let
statement declares new local and global
single assignment
variables and binds a
value to them. The syntax is the same as that of the var
statement,
except that the keyword var
is replaced by the keyword let
. Let
variables are not l-values and can thus not be passed to var
parameters nor can their address be taken. They cannot be assigned new
values.
For let variables, the same pragmas are available as for ordinary variables.
As let
statements are immutable after creation they need to define a
value when they are declared. The only exception to this is if the
{.importc.}
pragma (or any of the other importX
pragmas) is applied,
in this case the value is expected to come from native code, typically a
C/C++ const
.
Tuple unpacking
In a var
or let
statement tuple unpacking can be performed. The
special identifier _
can be used to ignore some parts of the tuple:
Const section
A const section declares constants whose values are constant expressions:
``` import std/[strutils] const roundPi = 3.1415 constEval = contains("abc", 'b') # computed at compile time!
Once declared, a constant\'s symbol can be used as a constant
expression.
See [Constants and Constant
Expressions](#constants-and-constant-expressions) for details.
### Static statement/expression
A static statement/expression explicitly requires compile-time
execution. Even some code that has side effects is permitted in a static
block:
There are limitations on what Nim code can be executed at compile time;
see [Restrictions on Compile-Time
Execution](#restrictions-on-compileminustime-execution) for details.
It\'s a static error if the compiler cannot execute the block at compile
time.
### If statement
Example:
``` nim
var name = readLine(stdin)
if name == "Andreas":
echo "What a nice name!"
elif name == "":
echo "Don't you have a name?"
else:
echo "Boring name..."
The if
statement is a simple way to make a branch in the control flow:
The expression after the keyword if
is evaluated, if it is true the
corresponding statements after the :
are executed. Otherwise the
expression after the elif
is evaluated (if there is an elif
branch),
if it is true the corresponding statements after the :
are executed.
This goes on until the last elif
. If all conditions fail, the else
part is executed. If there is no else
part, execution continues with
the next statement.
In if
statements, new scopes begin immediately after the
if
/elif
/else
keywords and ends after the corresponding then
block. For visualization purposes the scopes have been enclosed in
{| |}
in the following example:
if {| (let m = input =~ re"(\w+)=\w+"; m.isMatch):
echo "key ", m[0], " value ", m[1] |}
elif {| (let m = input =~ re""; m.isMatch):
echo "new m in this scope" |}
else: {|
echo "m not declared here" |}
Case statement
Example:
let line = readline(stdin)
case line
of "delete-everything", "restart-computer":
echo "permission denied"
of "go-for-a-walk": echo "please yourself"
elif line.len == 0: echo "empty" # optional, must come after `of` branches
else: echo "unknown command" # ditto
# indentation of the branches is also allowed; and so is an optional colon
# after the selecting expression:
case readline(stdin):
of "delete-everything", "restart-computer":
echo "permission denied"
of "go-for-a-walk": echo "please yourself"
else: echo "unknown command"
The case
statement is similar to the if
statement, but it represents
a multi-branch selection. The expression after the keyword case
is
evaluated and if its value is in a slicelist the corresponding
statements (after the of
keyword) are executed. If the value is not in
any given slicelist, trailing elif
and else
parts are executed
using same semantics as for if
statement, and elif
is handled just
like else: if
. If there are no else
or elif
parts and not all
possible values that expr
can hold occur in a slicelist, a static
error occurs. This holds only for expressions of ordinal types. \"All
possible values\" of expr
are determined by expr
\'s type. To
suppress the static error an else: discard
should be used.
For non-ordinal types, it is not possible to list every possible value
and so these always require an else
part. An exception to this rule is
for the string
type, which currently doesn\'t require a trailing
else
or elif
branch; it\'s unspecified whether this will keep
working in future versions.
Because case statements are checked for exhaustiveness during semantic
analysis, the value in every of
branch must be a constant expression.
This restriction also allows the compiler to generate more performant
code.
As a special semantic extension, an expression in an of
branch of a
case statement may evaluate to a set or array constructor; the set or
array is then expanded into a list of its elements:
const
SymChars: set[char] = {'a'..'z', 'A'..'Z', '\x80'..'\xFF'}
proc classify(s: string) =
case s[0]
of SymChars, '_': echo "an identifier"
of '0'..'9': echo "a number"
else: echo "other"
# is equivalent to:
proc classify(s: string) =
case s[0]
of 'a'..'z', 'A'..'Z', '\x80'..'\xFF', '_': echo "an identifier"
of '0'..'9': echo "a number"
else: echo "other"
The case
statement doesn\'t produce an l-value, so the following
example won\'t work:
type
Foo = ref object
x: seq[string]
proc get_x(x: Foo): var seq[string] =
# doesn't work
case true
of true:
x.x
else:
x.x
var foo = Foo(x: @[])
foo.get_x().add("asd")
This can be fixed by explicitly using result
or `return`:
When statement
Example:
when sizeof(int) == 2:
echo "running on a 16 bit system!"
elif sizeof(int) == 4:
echo "running on a 32 bit system!"
elif sizeof(int) == 8:
echo "running on a 64 bit system!"
else:
echo "cannot happen!"
The when
statement is almost identical to the if
statement with some
exceptions:
- Each condition (
expr
) has to be a constant expression (of typebool
). - The statements do not open a new scope.
- The statements that belong to the expression that evaluated to true are translated by the compiler, the other statements are not checked for semantics! However, each condition is checked for semantics.
The when
statement enables conditional compilation techniques. As a
special syntactic extension, the when
construct is also available
within object
definitions.
When nimvm statement
nimvm
is a special symbol that may be used as the expression of a
when nimvm
statement to differentiate the execution path between
compile-time and the executable.
Example:
proc someProcThatMayRunInCompileTime(): bool =
when nimvm:
# This branch is taken at compile time.
result = true
else:
# This branch is taken in the executable.
result = false
const ctValue = someProcThatMayRunInCompileTime()
let rtValue = someProcThatMayRunInCompileTime()
assert(ctValue == true)
assert(rtValue == false)
A when nimvm
statement must meet the following requirements:
- Its expression must always be
nimvm
. More complex expressions are not allowed. - It must not contain
elif
branches. - It must contain an
else
branch. - Code in branches must not affect semantics of the code that follows
the
when nimvm
statement. E.g. it must not define symbols that are used in the following code.
Return statement
Example:
The return
statement ends the execution of the current procedure. It
is only allowed in procedures. If there is an expr
, this is syntactic
sugar for:
return
without an expression is a short notation for return result
if the proc has a return type. The result
{.interpreted-text
role="idx"} variable is always the return value of the procedure. It is
automatically declared by the compiler. As all variables, result
is
initialized to (binary) zero:
Yield statement
Example:
The yield
statement is used instead of the return
statement in
iterators. It is only valid in iterators. Execution is returned to the
body of the for loop that called the iterator. Yield does not end the
iteration process, but the execution is passed back to the iterator if
the next iteration starts. See the section about iterators (Iterators
and the for statement) for further
information.
Block statement
Example:
var found = false
block myblock:
for i in 0..3:
for j in 0..3:
if a[j][i] == 7:
found = true
break myblock # leave the block, in this case both for-loops
echo found
The block statement is a means to group statements to a (named) block
.
Inside the block, the break
statement is allowed to leave the block
immediately. A break
statement can contain a name of a surrounding
block to specify which block is to be left.
Break statement
Example:
The break
statement is used to leave a block immediately. If symbol
is given, it is the name of the enclosing block that is to be left. If
it is absent, the innermost block is left.
While statement
Example:
echo "Please tell me your password:"
var pw = readLine(stdin)
while pw != "12345":
echo "Wrong password! Next try:"
pw = readLine(stdin)
The while
statement is executed until the expr
evaluates to false.
Endless loops are no error. while
statements open an implicit block
so that they can be left with a break
statement.
Continue statement
A continue
statement leads to the immediate next iteration of the
surrounding loop construct. It is only allowed within a loop. A continue
statement is syntactic sugar for a nested block:
Is equivalent to:
Assembler statement
The direct embedding of assembler code into Nim code is supported by the
unsafe asm
statement. Identifiers in the assembler code that refer to
Nim identifiers shall be enclosed in a special character which can be
specified in the statement\'s pragmas. The default special character is
`\'`\'`:
{.push stackTrace:off.}
proc addInt(a, b: int): int =
# a in eax, and b in edx
asm """
mov eax, `a`
add eax, `b`
jno theEnd
call `raiseOverflow`
theEnd:
"""
{.pop.}
If the GNU assembler is used, quotes and newlines are inserted automatically:
proc addInt(a, b: int): int =
asm """
addl %%ecx, %%eax
jno 1
call `raiseOverflow`
1:
:"=a"(`result`)
:"a"(`a`), "c"(`b`)
"""
Instead of:
proc addInt(a, b: int): int =
asm """
"addl %%ecx, %%eax\n"
"jno 1\n"
"call `raiseOverflow`\n"
"1: \n"
:"=a"(`result`)
:"a"(`a`), "c"(`b`)
"""
Using statement
The using
statement provides syntactic convenience in modules where
the same parameter names and types are used over and over. Instead of:
proc foo(c: Context; n: Node) = ...
proc bar(c: Context; n: Node, counter: int) = ...
proc baz(c: Context; n: Node) = ...
One can tell the compiler about the convention that a parameter of name
c
should default to type Context
, n
should default to Node
etc.:
using
c: Context
n: Node
counter: int
proc foo(c, n) = ...
proc bar(c, n, counter) = ...
proc baz(c, n) = ...
proc mixedMode(c, n; x, y: int) =
# 'c' is inferred to be of the type 'Context'
# 'n' is inferred to be of the type 'Node'
# But 'x' and 'y' are of type 'int'.
The using
section uses the same indentation based grouping syntax as a
var
or let
section.
Note that using
is not applied for template
since the untyped
template parameters default to the type system.untyped
.
Mixing parameters that should use the using
declaration with
parameters that are explicitly typed is possible and requires a
semicolon between them.
If expression
An if
expression is almost like an if statement, but it is an
expression. This feature is similar to ternary operators in other
languages. Example:
An if expression always results in a value, so the else
part is
required. Elif
parts are also allowed.
When expression
Just like an if
expression, but corresponding to the when
statement.
Case expression
The case
expression is again very similar to the case statement:
var favoriteFood = case animal
of "dog": "bones"
of "cat": "mice"
elif animal.endsWith"whale": "plankton"
else:
echo "I'm not sure what to serve, but everybody loves ice cream"
"ice cream"
As seen in the above example, the case expression can also introduce side effects. When multiple statements are given for a branch, Nim will use the last expression as the result value.
Block expression
A block
expression is almost like a block statement, but it is an
expression that uses the last expression under the block as the value.
It is similar to the statement list expression, but the statement list
expression does not open a new block scope.
Table constructor
A table constructor is syntactic sugar for an array constructor:
{"key1": "value1", "key2", "key3": "value2"}
# is the same as:
[("key1", "value1"), ("key2", "value2"), ("key3", "value2")]
The empty table can be written {:}
(in contrast to the empty set which
is {}
) which is thus another way to write the empty array constructor
[]
. This slightly unusual way of supporting tables has lots of
advantages:
- The order of the (key,value)-pairs is preserved, thus it is easy to
support ordered dicts with for example
{key: val}.newOrderedTable
. - A table literal can be put into a
const
section and the compiler can easily put it into the executable\'s data section just like it can for arrays and the generated data section requires a minimal amount of memory. - Every table implementation is treated equally syntactically.
- Apart from the minimal syntactic sugar, the language core does not need to know about tables.
Type conversions
Syntactically a type conversion is like a procedure call, but a type name replaces the procedure name. A type conversion is always safe in the sense that a failure to convert a type to another results in an exception (if it cannot be determined statically).
Ordinary procs are often preferred over type conversions in Nim: For
instance, $
is the toString
operator by convention and toFloat
and
toInt
can be used to convert from floating-point to integer or vice
versa.
Type conversion can also be used to disambiguate overloaded routines:
proc p(x: int) = echo "int"
proc p(x: string) = echo "string"
let procVar = (proc(x: string))(p)
procVar("a")
Since operations on unsigned numbers wrap around and are unchecked so are type conversions to unsigned integers and between unsigned integers. The rationale for this is mostly better interoperability with the C Programming language when algorithms are ported from C to Nim.
Exception: Values that are converted to an unsigned type at compile time
are checked so that code like byte(-1)
does not compile.
Note: Historically the operations were unchecked and the conversions were sometimes checked but starting with the revision 1.0.4 of this document and the language implementation the conversions too are now always unchecked.
Type casts
Type casts are a crude mechanism to interpret the bit pattern of an expression as if it would be of another type. Type casts are only needed for low-level programming and are inherently unsafe.
The target type of a cast must be a concrete type, for instance, a target type that is a type class (which is non-concrete) would be invalid:
Type casts should not be confused with type conversions, as mentioned
in the prior section. Unlike type conversions, a type cast cannot change
the underlying bit pattern of the data being casted (aside from that the
size of the target type may differ from the source type). Casting
resembles type punning in other languages or C++\'s
reinterpret_cast
{.interpreted-text role="cpp"} and
bit_cast
{.interpreted-text role="cpp"} features.
The addr operator
The addr
operator returns the address of an l-value. If the type of
the location is T
, the addr
operator result is of the type ptr T
.
An address is always an untraced reference. Taking the address of an
object that resides on the stack is unsafe, as the pointer may live
longer than the object on the stack and can thus reference a
non-existing object. One can get the address of variables, but one
can\'t use it on variables declared through let
statements:
let t1 = "Hello"
var
t2 = t1
t3 : pointer = addr(t2)
echo repr(addr(t2))
# --> ref 0x7fff6b71b670 --> 0x10bb81050"Hello"
echo cast[ptr string](t3)[]
# --> Hello
# The following line doesn't compile:
echo repr(addr(t1))
# Error: expression has no address
The unsafeAddr operator
For easier interoperability with other compiled languages such as C,
retrieving the address of a let
variable, a parameter, or a for
loop
variable can be accomplished by using the unsafeAddr
operation: