3. Values and expressions

Chapter 2 was about one type. This chapter is about the language around it: what a name holds, what a piece of code hands back, and what arithmetic does when the answer will not fit.

The receipt has been printing what it read. Now it has an opinion about it:

fn kind(name: str) -> str {
    match name {
        "tip" => "gratuity",
        "espresso" => "drink",
        _ => "food",
    }
}
fn main() -> !int {
    let rows = """
        espresso,340
        pastry,275
        tip,100
        """
    var total = 0
    for row in rows.lines() {
        var i = 0
        while row[i..i + 1] != "," { i += 1 }
        let name = row[..i]
        let cents = row[i + 1..].to_int() else 0
        let mark = if cents > 300 { "*" } else { " " }
        total += cents
        print("{mark}{name:<10}{kind(name):<9}{cents:>5}")
    }
    print(" {"total":<19}{total:>5}")
    0
}
$ lupin categories.lu
*espresso  drink      340
 pastry    food       275
 tip       gratuity   100
 total                715

Look at what kind does not contain: a return. Its whole body is one match, and the value of that match is the value of the call. mark is the value of an if. Neither is a special form; both are the ordinary way wolf spells a choice, and the two rules behind them run this whole chapter. A name holds one value for good, or holds a series of them. Code that looks like a statement is an expression with a value you may keep or discard.

3.1 let, var, and handing values over

Both spellings appeared in chapter 1 without explanation. Here they are side by side at the prompt:

wolf> let width = 10
wolf> width
10 : i32
wolf> var total = 0
wolf> total += 715
wolf> total
715 : i32
wolf> let frozen = total
wolf> total = 0
wolf> "{frozen} {total}"
715 0 : str
wolf> :type frozen
i32

let names a value once. var names a place that takes a series of them: total is 0, then 715, then 0 again, and it is the same total throughout — one name, one type, new contents. frozen is a let, so it is 715 for as long as it exists, and the later total = 0 does not reach it.

Reach for let first. It is not a stylistic preference; it is the narrower claim, and the compiler holds you to it:

fn main() -> !int {
    let width = 10
    width = 12
    print("{width}")
    0
}
error[E0410]: `width` is bound with `let`, so it cannot be assigned again
 --> ./s1.lu:6:5
  |
5 |     let width = 10
  |     --- the binding is made immutable here
6 |     width = 12
  |     ^^^^^ this assignment needs a mutable binding
  |
  = note: `let` names a value once. Declare the binding with `var` to update it in place, or
    shadow it with a second `let` if the next value is really a new thing.
help: make the binding mutable: `var`
  |
5 |     var width = 10
  |

The note names both repairs, and they are different claims about your program. var width says this quantity changes over time. A second let width says the old value is finished and a new thing is taking its name. Wolf lets you do either and asks you to say which.

So far every value has been a number or a string slice — two words at most, copied wherever they go. Values get bigger than that. A List owns its elements:

fn main() -> !int {
    var names = List[str]()
    names.push("ada")
    names.push("grace")
    let roster = names
    print("{roster.len} names")
    0
}
$ lupin roster.lu
2 names

That third line is the sentence of this section: assignment hands the value over. roster holds the list now. Not a second list with the same contents, and not a view onto somebody else’s list — the list, the one names was holding, handed across.

Which leaves a question about names, and wolf answers it before the program runs:

fn main() -> !int {
    var names = List[str]()
    let roster = names
    print("{roster.len} {names.len}")
    0
}
error[E1001]: `names.len` is used here after its value moved away
 --> ./s3.lu:7:26
  |
6 |     let roster = names
  |                  ----- `names` moved here
7 |     print("{roster.len} {names.len}")
  |                          ^^^^^^^^^ used after the move
  |
  = note: `names.len` is part of `names`; moving one empties the other. Disjoint fields stay
    usable.
  = note: re-initializing the place (assigning to it) also makes it usable again.
help: to keep the original, copy it at the move
  |
6 |     let roster = copy names
  |

The interpreter enforces the same rule at the other end of the day — the program starts, reaches the same read, and stops there:

$ lupin roster.lu
roster.lu: trap(use-after-move): `names` was moved out and is uninitialized here [mem.tier0.move.2] at 96..105; `names` moved here at 65..70
$ echo $?
3

Two tools, one rule, the two enforcement moments chapter 1 set up: the compiler proves it before the program starts, the interpreter catches it in the act. Neither invents a value for names to have.

The second note is worth taking literally. names is not poisoned, it is empty — hand it something and it is a working name again:

fn main() -> !int {
    var names = List[str]()
    names.push("ada")
    let roster = names
    names = List[str]()
    names.push("grace")
    print("{roster.len} {names.len}")
    0
}
$ lupin reinit.lu
1 1

That is the whole of the story this chapter tells. There is a much longer one — why the rule exists, what it buys, why a language would choose it over a garbage collector, and what the copy in the compiler’s suggestion costs — and it is chapter 7’s, which opens on this diagnostic and does not stop for a while. Part 1 needs only the direction of the handover and the two verdicts above. You will write a great deal of wolf on that much.

Exercise 3-8 (comprehension · lupin) — Predict the one printed line, then answer the pointed part: after name = "grace", what happened to "ada" — and why does first not care?

fn main() -> !int {
    var name = "ada"
    let first = name
    name = "grace"
    print("{first} {name}")
    0
}

Exercise 3-2 (comprehension · wolf + lupin) — The pack loses its lead. On the exercise page is a short program: a struct with two string fields, a function that takes one of them away, and two later reads — one of the field that left, one of the field that stayed. Before running anything, write down two predictions: what wolf says about it, and what lupin does with it. Which line does each tool blame, and why is the read of the other field not the one? (The program spells the handover with one word this chapter has not taught, take; read it as “hands the field over” and chapter 7 will do the rest.)

3.2 Everything is an expression

An expression is anything with a value. In wolf that includes the constructs other languages make statements, which is why there is no ternary operator and nothing missing where one would be:

fn main() -> !int {
    let cents = 275
    let band = match cents / 100 {
        0 => "under a dollar",
        1 => "a dollar and change",
        2 => "two-ish",
        _ => "expensive",
    }
    let mark = if cents > 300 { "*" } else { "" }
    let padded = {
        let body = "{cents}"
        "{body:>6}"
    }
    print("{padded} {band}{mark}")
    0
}
$ lupin bands.lu
   275 two-ish

Three shapes, one rule. A match has the value of the arm that applied. An if/else has the value of the branch that ran. A brace-delimited block has the value of its last expression — which is why padded is a string and why main ends with a bare 0 and no return. A block also scopes what it declares: body exists inside those braces and nowhere else, so a temporary name in a long computation costs nothing outside it.

Because the value comes from the last expression, an if used for its effect and an if used for its value are the same if. Chapter 1’s receipt wrote if i > width { width = i } and threw the value away. Here it is kept. Nothing about the construct changed.

The reading rule has one consequence worth meeting on purpose: if a match is a value, its arms have to agree what type that value is.

fn main() -> !int {
    let n = 1
    let x = match n {
        0 => "none",
        _ => 7,
    }
    print("{x}")
    0
}
error[E0401]: the arms of this `match` disagree about its type
 --> ./s7.lu:8:14
  |
7 |         0 => "none",
  |              ------ but this one is `str`
8 |         _ => 7,
  |              ^ this arm is `{integer}`
  |
  = note: a `match` used as a value produces one type; neither arm is more "right" — make both
    `str`, or both `{integer}`, or move the `match` into statement position.

“Neither arm is more right” is the compiler declining to guess. A language that picked one would be picking for you, in a program where one of the two arms is the typo.

Loops are the exception that proves the rule, and there are three of them. while repeats while a condition holds. for walks a sequence — including a range, written a..b, which yields a up to but not including b. And loop repeats until something in the body says stop, which is the one that carries a value out: break v makes v the value of the whole loop.

fn main() -> !int {
    let row = "espresso,340"
    var a = 0
    while row[a..a + 1] != "," { a += 1 }
    var b = 0
    for i in 0..row.len {
        if row[i..i + 1] == "," {
            b = i
            break
        }
    }
    var c = 0
    let d = loop {
        if row[c..c + 1] == "," { break c }
        c += 1
    }
    print("{a} {b} {d}")
    0
}
$ lupin three.lu
8 8 8

Three spellings of chapter 2’s comma scan, same answer, and the third is the only one where the answer is the loop’s own value rather than something a var was carrying out for it. break with no value leaves the loop; continue skips to the next turn. When the count is known, for i in 0..n says so; when the exit condition is the point, loop plus break v says that instead.

Exercise 3-5 (design) — Wolf has no ternary operator. Write the expression you would have used one for, in wolf, and then argue either side: is if-as-expression enough?

3.3 Arithmetic that traps

The receipt’s till is nearly full. Two more rows go in:

fn main() -> !int {
    var till: i32 = 2147483000
    for row in "700\n700".lines() {
        till += row.to_int() else 0
    }
    print("{till}")
    0
}
$ lupin till.lu
till.lu: trap(overflow): `+` produced 2147483700, outside `i32` — checked arithmetic traps in every profile (X3); spell intended overflow `wrapping[i32]` [arith.checked] at 95..122
$ echo $?
3

Push an i32 past its ceiling and the program does not wrap, does not continue, and does not negotiate. That is a trap: the fault of a defined execution. The program was legal, it ran, and it hit a rule wolf enforces at runtime — in release builds too, which is the part that surprises C programmers. The line names the operation, the true product, the type that could not hold it, the decision it enforces, and the spelling for the rare case where wrapping was the plan. Nothing here is undefined; the program’s last act is to tell you exactly what happened. Most bugs should be so polite.

Division by zero is the same kind of event, and it does not matter how the zero arrived:

fn main() -> !int {
    let rows = ""
    var total = 0
    var count = 0
    for row in rows.lines() {
        total += row.to_int() else 0
        count += 1
    }
    print("mean {total / count}")
    0
}
$ lupin mean.lu
mean.lu: trap(div-zero): division by zero is defined behavior in wolf: it traps [mem.ub.defined] at 183..196
$ echo $?
3

An empty input means the loop never ran, which means count is still 0, which means the average of nothing is a trap rather than whatever the hardware felt like. The bug is in the program — averages of empty collections are the caller’s problem — and the trap is where you find it, with the clause it enforces attached.

When wraparound is what you want, the type says so, and then it is not a fault at all:

fn main() -> !int {
    var checksum: wrapping[i32] = 2147483647
    checksum += 1
    var clamped: saturating[i32] = 2147483647
    clamped += 1
    print("{checksum} {clamped}")
    0
}
$ lupin wrap.lu
-2147483648 2147483647

wrapping[i32] wraps to the bottom of the range; saturating[i32] stops at the top and stays. Both are ordinary types you can put on a binding, a field, or a parameter, which means the decision is written where the data is declared rather than at each operation, and a reader of checksum += 1 can look up what that line meant. A hash function wants wrapping. A gauge wants saturating. A receipt total wants neither, which is why the trap above is the right answer.

The bill for checking. Every arithmetic operation in wolf carries a test the C version does not: compute, check the flag the hardware already set, branch to the trap on failure. The branch is never taken in a working program, which is the case processors predict best, and the check is hoistable out of loops where the compiler can prove the range — the same analysis that removes bounds checks. So the honest answer to “what does X3 cost” is a measurement, and this book does not make measured claims without the measurement: chapter 21 runs the comparison against C, prints the numbers CI produced on a dated machine, and names the cases where the check is still visible. What the decision buys is not in dispute: the alternative is not “free arithmetic”, it is arithmetic whose failures are undefined, and an undefined result is a bug that surfaces somewhere other than where it happened.

Exercise 3-3 (comprehension · lupin)2147483647 is i32’s ceiling. Predict what big + 1 does in a release build. (Trick warning: the answer is the same in every build.)

Exercise 3-4 (comprehension · lupin) — The divisor is computed, not literal. Does that change anything?

fn main() -> !int {
    let n = 10
    let d = n - 10
    print("{n / d}")
    0
}

3.4 match, exhaustively

A match compares a value against patterns in order and takes the first that applies. The exit codes from chapter 1 make a small table of one:

fn label(code: int) -> str {
    match code {
        0 => "ok",
        2 => "rejected",
        3 => "trapped",
        4 => "unsupported",
        _ => "the program's own",
    }
}
fn main() -> !int {
    for code in 0..5 {
        print("{code} {label(code)}")
    }
    0
}
$ lupin codes.lu
0 ok
1 the program's own
2 rejected
3 trapped
4 unsupported

The _ arm matches anything and has to come last, because an arm after it could never run. It is also the only reason this match compiles: delete it, and the compiler produces a counterexample.

fn label(code: int) -> str {
    match code {
        0 => "ok",
        2 => "rejected",
        3 => "trapped",
        4 => "unsupported",
    }
}
fn main() -> !int {
    print("{label(1)}")
    0
}
error[E0801]: this `match` does not cover `1`
  --> ./s14.lu:5:5
   |
 5 |     match code {
   |     ^^^^^^^^^^^^
 6 |         0 => "ok",
...
10 |     }
   | ^^^^^ not every value is matched
   |
   = note: add arms for the missing cases, or end the `match` with a `_` arm to catch the rest
     deliberately.

1 is a witness. The compiler did not say “this looks incomplete”; it handed you a value your program has no answer for, which is a fact you can check by hand and cannot argue with. A match on a bool gets the same treatment with false as the witness, and the value the compiler picks is always one of the cases you forgot.

Under the interpreter the same program starts, gets as far as the call, and declines to invent an arm:

$ lupin partial.lu
partial.lu: unsupported: no `match` arm applied; exhaustiveness is the type checker's
$ echo $?
4

That is the division of labor: totality is a static property, the type checker owns it, and the interpreter refuses the program rather than guessing which arm the author meant. Run the file through wolf and you get the witness instead of the refusal.

Arms take more than one pattern when the answer is the same:

fn main() -> !int {
    for code in 0..5 {
        let verdict = match code {
            0 => "the program's own",
            2 | 3 | 4 => "the toolchain's",
            _ => "the program's own",
        }
        print("{code} {verdict}")
    }
    0
}
$ lupin verdicts.lu
0 the program's own
1 the program's own
2 the toolchain's
3 the toolchain's
4 the toolchain's

Two arms say the same words here, and that is a hint about the shape of the data rather than a flaw in the match: exit codes are not really one number, they are a choice between “the program returned” and “the toolchain refused”. Chapter 6 gives that kind of choice a type of its own, and the match that walks it gets its exhaustiveness checked the same way this one does.

Exercise 3-1 (comprehension · lupin) — Predict the one line this prints. Both match and if are expressions here; nothing is a statement:

fn main() -> !int {
    let n = 3
    let kind = match n {
        0 => "none",
        1 => "one",
        _ => "many",
    }
    let parity = if n % 2 == 0 { "even" } else { "odd" }
    print("{kind} and {parity}")
    0
}

Exercise 3-6 (extension (break-it-on-purpose) · lupin) — Using one i32 binding and one *, write the smallest program that traps with overflow on i32. State, before running it, why the number you chose is the smallest one that works, and what the trap line will say the product was.

Exercise 3-7 (comprehension · lupin) — Predict both lines. If you arrived from Python, predict them twice:

fn main() -> !int {
    let a = 0 - 7
    let b = 2
    print("{a / b} {a % b}")
    print("{(a / b) * b + a % b}")
    0
}