ch20.md 23 KB

Chapter 20. 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 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. 20.1. 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. 20.2. Discard statement Example: proc p(x, y: int): int =

result = x + y

discard p(3, 4) # discard the return value of p 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.

                                                                      125

Ignoring the return value of a routine without using a discard statement is a static error. The return value can be ignored implicitly if the called routine has been declared with the discardable pragma: proc p(x, y: int): int {.discardable.} =

result = x + y

p(3, 4) # now valid 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
           The discardable pragma is not available for templates or
           macros. This is a consequence of the fact that templates and
          macros are expanded directly at their call sites: The expanded
           expression does not contain the information that it was
           discardable.

20.3. 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

proc valid*(): string =

let x = 317
"valid"

126 20.4. 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: var

a: int = 0
x, y, z: int

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. There is one exception from this rule: Assuming g() returns a var T type then the x of var x = g() is of type T. This can imply a copy operation: proc [](x: var Container[T]; i: int): var T proc access(c: var Container[int]) =

var x = c[0] 1
x = 4 2
echo c[0] 3

1 Even though [] returns a var int x is of type int. The value of c[0] is

copied into x.

2 x is modified but c[0] is not. 3 c[0] is unmodified. Table 8. Default values Type default value any integer type 0 any float 0.0 char '\0' bool false ref or pointer type nil

                                                                        127

Type default value procedural type nil sequence @[] string "" tuplex: A, y: B, ... (analogous for objects) array[0..., T] [default(T), ...] range[T] default(T); this may be out of the valid range T = enum castT; this may be an invalid value The implicit initialization can be avoided for optimization reasons with the noinit pragma: var

a {.noInit.}: array[0..1023, char]

If a proc is annotated with the noinit pragma, this refers to its implicit result variable: proc returnUndefinedValue: int {.noinit.} = discard The implicit initialization can also be prevented by the requiresInit type pragma. With requiresInit an explicit initialization for an object and all of its fields is required. However, it does a form of control flow analysis to prove the variable was initialized: type

MyObject = object {.requiresInit.}

proc use(x: MyObject) = discard proc valid() =

var x: MyObject
if someCondition():
   x = a()
else:
   x = a()
# valid: all paths set `x` before it is used:
use x

128 proc invalid() =

var x: MyObject
if someCondition():
  x = a()
# invalid: not all paths set `x` before it is used:
use x

The requiresInit pragma can be applied to object or distinct types. 20.5. 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 rule is if the {.importc.} pragma (or any of the other importX pragmas) is applied, in this case the value is expected to come from a foreign source, typically a C/C++ const. 20.6. 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: proc returnsTuple(): (int, int, int) = (4, 2, 3) let (x, _, z) = returnsTuple()

                                                                      129

20.7. Const statement A const statement declares constants whose values are constant expressions: import std/[strutils] const

roundPi = 3.1415
constEval = contains("abc", 'b') # computed at compile time!

A constant is a constant expression. See Chapter 18, Constant expressions for further details. A good example for a more complex const is a lookup table: const

LookupTable = {
  "one": (1, false, "a"),
  "two": (2, true, "b"),
  "three": (3, false, "c")
}

There are restrictions on how these can be formed, however: The type ref cannot be part of a constant: type

T = ref object
   a, b: int

const

LookupTable = {
  "invalid": T(a: 2, b: 3)
}

The reason for this is that Nim’s type system in general lacks constant pointers, the data that a pointer points to is always mutable. Later versions of the language might provide such a facility. 20.8. Type section New types in Nim are introduced within type section. A newly introduced type can be one of: 130

  1. Type aliases type NewNameForOldThing = int IntArray = openArray[int]
  2. Nominal types (every nominal type must be introduced via a type section): type Sex = enum Male, Female Id = distinct string Person = object id: Id name: string sex: Sex age: Natural The nominal types are object, ref object, ptr object, distinct and enum.
  3. Generic types: type KeyValuePair[A, B] = tuple[hcode: Hash, key: A, val: B] KeyValuePairSeq[A, B] = seq[KeyValuePair[A, B]] Table*[A, B] = object

     data: KeyValuePairSeq[A, B]
     counter: int
    

    TableRef*[A, B] = ref Table[A, B]

                                                                     131
    

    Mutually recursive types have to be in the same type section, for example:

    example demonstrating mutually recursive types

    type Node = ref object # a Node in a tree le, ri: Node # left and right subtrees sym: ref Sym # leaves contain a reference to a Sym Sym = object # a symbol name: string # the symbol's name line: int # the line the symbol was declared in code: Node # the symbol's abstract syntax tree In summary: • A type section begins with the type keyword. • It contains multiple type definitions. • A type definition binds a type to a name. • Type definitions can be recursive or even mutually recursive. • Mutually recursive types are only possible within a single type section. • Nominal types like objects or enums can only be defined in a type section. 20.9. Static statement/expression A static statement/expression explicitly requests compile-time execution. Even code that has side effects is permitted in a static block: static: echo "echo at compile time" There are limitations on what Nim code can be executed at compile time; the limitations change with every release of the Nim compiler and are beyond the scope of this book. It is a static error if the Nim compiler cannot execute the block at compile time. 132 20.10. If statement Example: var name = readLine(stdin) if name.endsWith('a'): 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 end after the corresponding then block. For visualization purposes the scopes are 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" |}

                                                                       133
    

    20.11. 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 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 a Nim compiler to generate more performant code. 134 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" 20.12. 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 type bool). • The statements do not open a new scope. • The statements that belong to the expression that evaluated to true are processed, the other statements are not checked for semantics! However, each condition is checked for semantics. The when statement enables conditional compilation techniques. The when

                                                                       135
    

    construct is also available within object definitions: type Option*[T] = object ## An optional type that may or may not contain a value of type T. ## When T is a a pointer type (ptr, pointer, ref or proc), ## none(T) is represented as nil. when T is SomePointer:

    val: T
    

    else:

    val: T
    has: bool
    

    20.13. Return statement Example: return 40+2 The return statement ends the execution of the current routine. If there is a value to return, this is syntactic sugar for: result = expr return result return without an expression is a short notation for return result if the routine has an implicit result variable. The result variable is always the return value of the procedure. The return statement raises a special return exception. break and return statements are defined in terms of raising an exception in order to specify their interactions with the exception handling statements. The return exception is a pseudo-exception otherwise, it is ignored for Nim’s effect system. 136 20.14. Yield statement Example: yield (1, 2, 3) 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 (Chapter 23, Iterators and the for statement) for further information. 20.15. 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 should be left. 20.16. Break statement Example: while cond: break # leave the while loop block symbol: break symbol # leave the block named symbol The break statement is used to leave a block immediately. If symbol is given, it

                                                                        137
    

    is the name of the enclosing block that is to be left. If it is absent, the innermost block is left. A break statement must be textually enclosed by a block, while or for statement. The break statement raises a break exception. break and return statements are defined in terms of raising an exception in order to specify their interactions with the exception handling statements. The break exception is a pseudo- exception otherwise, it is ignored for Nim’s effect system. 20.17. 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 condition evaluates to false. Endless loops are no error. while statements open an implicit block so that they can be left with a break statement. 20.18. 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: while expr1: stmt1 continue stmt2 Is equivalent to: while expr1: block myBlockName: stmt1 break myBlockName stmt2 138 20.19. 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 specify 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) = ... The using section uses the same indentation based grouping syntax as a var or let statements. 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: 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'.

    20.20. If expression An if expression is almost like an if statement, but it is an expression which means that it produces a value. Example: var y = if x > 8: 9 else: 10

                                                                      139
    

    An if expression always results in a value, so the else part is required. Elif parts are also allowed. 20.21. When expression Just like an if expression there is a when expression available: const PathSep* = when defined(Windows): '\'

            else: '/'
    

    20.22. 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" When multiple statements are given for a branch, the last expression as the result value is used. The case expression does not produce an l-value, so the following example does not work: type Foo = ref object x: seq[string] proc getX(x: Foo): var seq[string] = case true of true: x.x # invalid: case does not produce an l-value else: x.x var foo = Foo(x: @[]) foo.getX().add("asd") 140 Instead an explicit result or return has to be used: proc getX(x: Foo): var seq[string] = case true of true: result = x.x else: result = x.x 20.23. Block expression A block expression is a block statement that produces a value. The evaluation of the block’s last expression produces this value. Like in a block statement, the block keyword introduces a new scope. For example: let a = block: var fib = @[0, 1] # fib is not accessible outside of the block for i in 0..10: fib.add fib[^1] + fib[^2] fib 20.24. 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 readonly section and it requires a

                                                                        141
    

    minimal amount of memory. • Every table implementation is treated equally syntactically. • Apart from the minimal syntactic sugar, the language core does not need to contain more support for tables. 20.25. Type conversions Syntactically a type conversion is like a routine call, but a type name replaces the routine 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. Exception: Values that are converted to an unsigned type at compile time are checked so that code like byte(-1) does not compile. 20.26. Type casts Type casts are a crude mechanism to interpret the bit pattern of an expression to be of another type. Type casts are only needed for low-level programming and are inherently unsafe. A type cast is written as castT where x is a value to be interpreted as to be of type T. Example: 142 type Obj = object a: int32 # at offset 0 b: int32 # at offset 4 var obj: Obj castptr int32[] = 123

    On most machine architectures this is a

    convoluted, inherently weakly portable

    way for doing:

    obj.b = 123 20.27. 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 pointer. Taking the address of an object is unsafe, as the pointer may live longer than the object and can thus reference a non-existing object. One can get the address of an l-value: let t1 = "Hello" var t2 = t1 t3 : pointer = addr(t2) echo repr(addr(t2))

    --> ref 0x7fff6b71b670 --> 0x10bb81050"Hello"

    echo castptr string[]

    --> Hello

    20.28. The unsafeAddr operator In some versions of Nim addr is not allowed to be performed on inherently immutable entities such as let variables, routine parameters or for loop variables. Instead of addr unsafeAddr can be used for these entities: let myArray = [1, 2, 3] foreignProcThatTakesAnAddr(unsafeAddr myArray) Note however that the name is somewhat misleading as addr is an unsafe operation too.