Skip to content

Variables and control flow

Inside a function body, statements run in order. jihoo has one way to declare a variable, two branching forms (if and match), two loops (while and for, with break and continue), and return.

fn collatz(n: i64) -> i64 {
let steps = 0
while n != 1 {
if n % 2 == 0 {
n = n / 2
} else {
n = 3 * n + 1
}
steps = steps + 1
}
return steps
}

let name = value declares a variable and gives it its first value; there are no uninitialized variables. The type is inferred from the value, or written out, in which case the value must have that type (and literals take it):

let n = 10 // i64
let b: u8 = 200 // the literal is a u8
let p = Point { x: 1, y: 2 }

Variables can be assigned again (n = n + 1); there is no mut. Parameters are variables too, except comptime parameters, which cannot be assigned.

A variable is visible from its let to the end of the block it is declared in. Declaring a name that is already visible makes a new variable that hides the old one from then on, even with a different type:

let x = "42"
let x = 42 // a new variable; the str is no longer reachable by name

A block { ... } is also an expression. Its statements run in a scope of their own, and if the last one is an expression, that is the block’s value; otherwise the block’s value is unit.

let area = {
let w = right - left
let h = bottom - top
w * h
}
  • Variables declared in the block end with it; a name it declares hides an outer one only inside the block.
  • return, break and continue inside the block leave the function or loop around it, as anywhere else.
  • Where a block follows, a { starts that block and not a block expression: if { c } { ... } is an error, as struct literals are in conditions.
  • An expression macro can make statements this way, returning quote({ ... }).

place = value stores into a place: a variable, a field (p.x), an array element (a[i]), and in compiled code what a pointer points to (*p, p[i], p.x through a pointer), or the contents of a cell (*c, c.x). Places nest: line.a.x = 1, grid[r][c] = 0.

Since structs and arrays are values, assigning to a part of one changes only that variable.

place op= value combines the place’s value with value and stores the result back, for every arithmetic and bitwise operator:

i += 1 // i = i + 1
total -= cost
grid[r][c] *= 2
flags |= 0x4 // also &=, ^=, <<=, >>=, /= and %=
name += "!" // concatenation, on str
  • It works on the same places as =, and follows the operator’s rules: += on two i32s, on floats, on strings, and on a pointer and an i64 (p += 1); &=, |= and ^= on bools too.
  • The value takes the place’s type, so x += 1 works for any integer x, and the result must have the place’s type again: x += y with x: i32 and y: i64 is an error.
  • The place is evaluated once. In a[next()] += 1, next is called one time, and the element is read before value is evaluated, as in a[i] = a[i] + value.
  • It is a statement, like =, not an expression: let y = x += 1 is an error. There is no ++, &&= or ||=.
if n < 0 {
print("negative")
} else if n == 0 {
print("zero")
} else {
print("positive")
}

The condition must be a bool — there is no implicit conversion from integers or pointers — and needs no parentheses; the braces are required. if is a statement, not an expression; to choose a value, use a match expression:

let sign = match n < 0 { true => -1, false => 1 }

A struct literal cannot appear directly in a condition, since if p == Point { ... } would be ambiguous; wrap it in parentheses: if p == (Point { x: 0, y: 0 }) { ... }.

while cond { body } runs the body as long as the condition, a bool, holds:

let n = 1
while n < limit {
n = n * 2
}

for walks a range of integers, the elements of a list, an array or a str (its bytes, as u8), or the items of a value whose type has an Iterator. With two names, the first is the index, an i64:

for i in 0..len(xs) { ... } // 0, 1, ... len(xs) - 1
for x in xs { total += x }
for i, x in xs { print(i) }
for b in "hi" { print(b) } // 104, 105
  • lo..hi counts up from lo to hi - 1, and is empty when hi <= lo. Both are integers of the same type (a literal takes the other’s type, so 0..n works for any integer n), and both are evaluated once, before the loop starts. The counter never steps past hi, so a range may end at its type’s largest value.
  • for x in xs walks the value xs had when the loop started: xs is evaluated once, and the body changing it (push(xs, x), xs[i] = 0) does not change what the loop sees, since lists are values. Neither the VM nor native code copies the list for this unless the body does change it.
  • for x in it, for a type with an Iterator, calls next on it and then on what each round’s next left, until it gives None. it is a value, so the loop leaves the variable as it was.
  • The names are new variables in each round, visible only in the body. x is a copy of the element: assigning to it changes neither the list nor the next round. Assigning to the index does not change the next round either.

break leaves the innermost loop at once, and continue skips the rest of its body and goes on with the loop’s next round (the next test of a while condition, the next element or number of a for):

let i = 0
let sum = 0
while true {
i = i + 1
if i % 2 == 0 { continue } // skip even numbers
if i > 9 { break } // done
sum = sum + i
}
print(sum) // 1 + 3 + 5 + 7 + 9 = 25
  • Only the innermost loop is affected; there are no loop labels. To leave several loops at once, put them in a function and return.
  • They work anywhere inside the loop’s body: in if blocks, in match and select arms, and in the code of a statement macro.
  • A closure body is a function of its own, so a break in a closure cannot leave a loop around the closure; it is an error, as is a break or continue outside any loop.
  • Code right after a break or continue in the same block never runs.

return value leaves the function with a value; a function without a result type ends with return or by reaching the end of its body. A function that has a result must return on every path that reaches the end of its body. The compiler does not reason about conditions, so a loop such as while true { ... } still needs a return after it:

error: f.jh:3:1: missing `return`: `find` must return i64

An exhaustive match whose arms all return is enough, since it has no way to fall through.

statement page
an expression on its own, usually a call: f(x) Functions
match value { pattern => ... } Pattern matching
go f(x) and select { ... } Tasks and channels
name!(...), a statement macro Macros

There are no nested function declarations; use a named function at the top level, or a closure (let f = fn(x: i64) -> i64 { ... }).