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 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]()
(mut names).push("ada")
(mut names).push("grace")
let roster = names
print("{roster.len} names")
0
}
$ lupin roster.lu
2 names
Two spellings in that program are new, and the smaller one first.
push puts an element on the end of the list (lists get their own
chapter, chapter 5), and it changes the list it is called on,
which is why the call is written (mut names).push(…) and not
names.push(…): a call that writes through a name says so at the call,
where a reader can see it without looking up what push does. For now,
read (mut names) as “this call writes names” and nothing more.
Chapter 7 tells why the language insists, and the whole system the
marker belongs to; until then this is the only
place the word appears: on the left of a method call that writes.
The fifth line shows the rule: assignment hands the
value over. roster now holds the list names was holding, not a
second list with the same contents and not a view onto somebody else’s
list.
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 4:26; `names` moved here at 3:18
$ 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.
Take the second note 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]()
(mut names).push("ada")
let roster = names
names = List[str]()
(mut names).push("grace")
print("{roster.len} {names.len}")
0
}
$ lupin reinit.lu
1 1
That is the whole of the story this chapter tells. The much longer one
is chapter 7’s: 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. Chapter 7 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..=2 => "a dollar or two",
d if d < 10 => "{d} dollars",
_ => "expensive",
}
let mark = if cents > 300 { "*" } else { "" }
let padded = {
let body = "{cents}"
"{body:>6}"
}
print("{padded} {band}{mark}")
0
}
$ wolf run bands.lu
275 a dollar or two
Its arms are the four things an arm can be, and any switch you have written already spells three of them. 0 is
a literal. 1..=2 is a range: ..= takes its high end, .. stops
before it, and both ends are literals — over char the order is scalar
order, so 'a'..='z' is twenty-six letters and nothing else.
d if d < 10 binds the value and then asks a question about it, and an
arm whose guard answers no falls through to the next one. _ takes what
is left, and it is still required: nothing adds these arms up, so an
int still has values none of them names.
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.
An if whose two branches are each one expression has a second
spelling, with then in place of the braces:
fn main() -> !int {
let cents = 275
let band = match cents / 100 {
0 => "under a dollar",
d if d < 10 => if d == 1 then "a dollar" else "{d} dollars",
_ => "expensive",
}
let mark = if cents > 300 then "*" else ""
print("{cents:>6} {band}{mark}")
0
}
$ lupin bare.lu
275 2 dollars
then closes the condition, the if’s own else closes the first
branch, and the arm’s comma closes the second. A value on one line is
where the bare form earns its keep, and a match arm is the usual
place for one. Three rules cover it. The braces are always valid, and a
then written before a brace is dropped as noise. then is required
only without the braces, because the condition is read greedily (if x -1 else 0 would take the -1 as arithmetic) and then does the job
the { did. And then is not a reserved word: a bool named then is
a condition like any other, and after a . it is a member name; it is
the keyword in one position only, after a complete condition. Both
branches of one if take the same form, and an else if in a chain
picks its own.
wolf fmt never converts between the two spellings: the form you
wrote is the form on every later pass. The one thing the formatter
does to a bare if is break it to the braced form when its line
outgrows the width, and that goes one way only. Appendix E is the
formatter’s page.
else may also start its own line, which is worth knowing because the
alignment that buys is sometimes the clearest thing on the page:
fn main() -> !int {
let s = "a\tb\nc"
var t = ""
var i = 0
while i < s.len {
if s[i..i + 1] == "\t" { t += "<tab>" }
else if s[i..i + 1] == "\n" { t += "<nl>" }
else { t += s[i..i + 1] }
i += 1
}
print(t)
0
}
A line whose first token is else continues the statement above it, so
the chain is one construct however it is laid out. Both layouts parse;
wolf fmt writes them as } else. Appendix E is the formatter’s page,
and it runs the tool on a chain shaped like this one.
The reading rule has one consequence: 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
--> ./s9.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, 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 loop itself carries the answer out; the other
two needed a var to do 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.
The range in that for header is a value in its own right, with a type
you can write down: range[int], and range[char] for 'a'..='z'. It
has two endpoints, read as start and end the way a string’s length
is read as len, and end is exclusive whichever spelling built the
range, so 1..=12 ends at 13:
fn count(r: range[int]) -> int { r.end - r.start }
fn main() -> !int {
let digits = 0..10
print("{digits.start} {digits.end} {count(digits)} {count(1..=12)}")
0
}
$ wolf run span.lu
0 10 10 12
count subtracts because a range keeps no length of its own: it is two
numbers and a direction, not a collection. The half-open spellings a
subscript accepts, row[i..] and row[..i], are not values of this
type. A string’s length is what finishes them, and outside the brackets
there is no length to finish them with.
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?
Exercise 3-9 (fingers · lupin). The pack drill: print 1 through
15, except that multiples of 3 print howl, multiples of 5 print
scratch, and multiples of both print howlscratch. One if-chain
as a value, one print. Why must the both test come first, and
what prints if it comes last?
Exercise 3-15 (fingers · lupin). Days in a month, decided by a
match on the month number with a bare if in the February arm, 2 => if leap then 29 else 28, printed for January, both Februaries and
April. Then make one branch outgrow the line, a sentence in place of
the number, and run wolf fmt on the file: which of your two ifs
does the formatter leave as written, what does it do to the other, and
would it ever go the other way?
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 4:9
$ 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 9:18
$ echo $?
3
An empty input means the loop never ran, so count is still 0 and 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, not 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 answer to “what does the checking cost” is a measurement: chapter 21 runs the comparison against C on a dated machine, prints the numbers CI produced, and names the cases where the check is still visible. What the decision buys is not in dispute: the alternative 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`
--> ./s16.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. §2.4’s char matches the
same way: literal arms, a witness for whatever they miss, and no set of
'…' arms the checker will ever count as complete.
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, not 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
}
Exercise 3-10 (extension · lupin). Print 1 through 16 with each
number’s binary spelling right-aligned beside it. No format spec you
have met writes base 2, so build the bits yourself: % 2 peels the
low bit, / 2 shifts, and prepending assembles them in the right
order. Do not reach for a fixed-width buffer to poke bits into: a str
cannot be written in place — s[a..b] reads a view of one, not a place
— so every step makes a new string. What does your loop produce for
zero, and is that a spelling or an absence?
Exercise 3-11 (fingers · lupin). One pass over a block of
readings: lowest, highest, and mean, integer arithmetic throughout.
The first reading has to seed lowest and highest — why is
starting them at zero wrong, and what kind of input would have
exposed it?
Exercise 3-12 (fingers · lupin). Print every three-digit
palindrome divisible by 7, using arithmetic only: / and % take
the digits, and a flipped number is three multiplies away. No
strings. How many are there, and why does your divisibility test not
need the middle digit?
Exercise 3-14 (fingers · lupin). 3-9’s pack drill again, decided
once instead of in a chain. That version’s order was load-bearing and
nothing checked it: put the both test last and an arm goes dead in
silence. Write the same 1-through-15 drill as a single match over
the pair (n % 3, n % 5). Then move (_, 0) above (0, 0), run it
again, and say what is different this time about being wrong.