1. Hello, Wolf
Bruckner's Fourth opens with one horn call over a tremolo — everything the symphony will do, said once, quietly, before anyone explains it.
1.1 A program worth keeping
Books like this one open with a program that prints a greeting. That program is in this chapter too, and you should type it. First, though, a program you might keep.
You have a file of rows: a name, a comma, an amount in cents. The question is what they add up to.
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
total += cents
print("{name:<10}{cents:>6}")
}
print("{"total":<10}{total:>6}")
0
}
It answers:
$ lupin receipt.lu
espresso 340
pastry 275
tip 100
total 715
Eighteen lines, and nothing in them is a preview of syntax you meet
later — this is the language. let names a value; var names one that
changes. """ opens a block literal whose common indentation is trimmed
away, so the data sits where you would put it in a file. Braces inside a
string interpolate the expression between them, and {cents:>6} means
right-align in six columns. row[..i] slices bytes. for walks what
lines() hands it.
Two lines lie for convenience. The while loop trusts every row to hold
a comma, and else 0 decides that an amount which is not a number is
worth nothing. §1.5 shows what the first lie does when the input
disagrees, and chapter 6 takes both apart; this program is Part 1’s
running example, and it gets more honest in every chapter.
Now the greeting. The smallest wolf program that does anything:
fn main() -> !int {
print("hello, wolf")
0
}
main is where a program starts. print writes one line to standard
output. The 0 on its own line is the value main returns, and the
process exit code — §1.3 makes that concrete. The ! in !int says
main may fail instead; chapter 6 is where that becomes a tool, and
until then it is the shape main has.
No imports, no enclosing class, no guard against being imported: there
is nothing above fn main because there is nothing a wolf file needs
above fn main.
Exercise 1-1 (fingers · lupin) — Type the hello program exactly as printed and run it. Then make it print a second line of your choosing.
Exercise 1-3 (fingers · lupin) — Braces interpolate expressions,
not only names. Print a sentence that computes 6 * 7 twice inside one
string literal.
1.2 Two implementations, one language
Wolf is one binary. wolf compiles your program, runs it, formats it,
and answers your editor’s questions; there is no separate package
manager, no build-system layer, no second executable to keep in step
with the first. Save the greeting as hello.lu and point the compiler
at it:
$ wolf build hello.lu && ./hello
hello, wolf
hello is a program. It has no runtime to install beside it, it starts
in the time the kernel takes to map it, and you can hand it to somebody
who has never heard of wolf. The compiler said nothing on its way
there, which is the only thing a compiler should say when it agrees
with you.
While you are still editing, the build and the run fold into one command:
$ wolf run hello.lu
hello, wolf
wolf run compiles into a cache beside your file and executes what
comes out, so an unchanged program’s second run does not visit the
compiler at all. It is the same machinery with the shell step folded
in.
Now the same file through the other door:
$ lupin hello.lu
hello, wolf
Nothing was compiled that time. lupin read the program and ran it,
and the bytes on your terminal are the same bytes.
Why there are two
lupin is not the compiler with the optimizer switched off. It is an
independent implementation of the wolf specification, written from the
spec rather than from the compiler’s source, and its day job is to
disagree: a program whose behavior differs between the two is a bug in
one of them or a hole in the spec, and it gets found before you meet
it. A language with one implementation cannot tell the difference
between what it specified and what it happens to do.
So the answer to “which one is wolf” is neither. The specification is wolf, and these are two readings of it that have to keep matching. Every page in this book says which of them is speaking, and the pages where they speak in turn are the ones worth slowing down for — §1.5 begins the habit.
Ask either who it is and it tells you:
$ wolf --version
wolf 0.1.0 (wolfgang)
paired with lupin 0.1.8 (reference interpreter), pin 7886559
$ lupin --version
lupin 0.1.10 (wolf-interp, reference interpreter at pin 613c3dc)
Each names the other, and that is not decoration. The compiler’s second line is the interpreter revision it was tested against; the interpreter’s is the compiler revision it was tested against. Two implementations agreeing is worth something only when you can find out which two agreed.
The colophon records the versions this printing is true for; those lines are how you check that they are yours. Output that differs from a book’s printed output is a version question before it is a bug.
Both are cargo projects, and both build the same way on every tier-1 platform (linux x86-64 and aarch64, macOS aarch64, windows x86-64). Given a Rust toolchain and the two repositories, the commands are the ones this book’s own CI runs:
$ (cd wolf-lang && cargo build --bin wolf)
$ (cd wolf-interp && cargo build --release --bin lupin)
Cargo prints its usual progress and leaves wolf under
wolf-lang/target/debug/ and lupin under
wolf-interp/target/release/. Put both on your PATH, run the two
--version commands above, and the rest of the chapter works.
Exercise 1-7 (fingers · wolf + lupin) — Compile the greeting with
wolf build, run the binary, then run the source under lupin. Compare
the two outputs byte for byte — diff <(./hello) <(lupin hello.lu) will
do it. Then say which of the two runs could have printed something
different, and what it would mean about the language if it had.
1.3 Scripts before projects
A wolf program can be one file, run in place:
$ wolf run hello.lu
hello, wolf
There is no project to create first, no manifest to write, no directory layout to honor, no virtual environment to activate, no lockfile to drift out of date. The file is the program. Chapter 22 covers what happens when one file stops being enough, and that is a later problem than most languages make it.
Coming from Python: the equivalent Python is about as short —
print("hello, wolf")— and the difference is not in the file, it is around it. Python’s one-liner needs an interpreter on the machine that runs it, of a version the script never states; wolf’s compiles to a program that states its own requirements by being one. Neither answer is free. Python’s costs you an environment to reproduce; wolf’s costs you a compile, and §1.5 is where you watch what that compile buys.
The value main returns is the process exit code:
$ wolf run hello.lu
hello, wolf
$ echo $?
0
That is the last expression of main, handed to the shell. Wolf has no
return ceremony at the end of a function — the last expression of a
block is its value, which is a rule you will meet again in every if
and match you write.
Loops come in the two shapes you expect. for x in xs walks a sequence;
while cond repeats until the condition fails. Both are in §1.1’s
program, and the exercises below are where you write one.
Exercise 1-2 (comprehension · lupin) — Before running, write down
what this program prints and what echo $? shows afterward:
fn main() -> !int {
print("working")
3
}
Exercise 1-5 (fingers + extension · lupin) — The first table in The C Programming Language converts Fahrenheit to Celsius. Write wolf’s: 0 to 120 degrees in steps of 20, one line per row. Then look hard at the 20-degree row. Is it right?
1.4 The REPL: a spec you can interrogate
Run lupin with no arguments and it opens a session. Expressions
evaluate and print their value and their type; declarations persist for
as long as the session lives.
wolf> 6 * 7
42 : i32
wolf> "wolf".len
4 : i64
wolf> :type "wolf".len
i64
wolf> let howl = "awoo"
wolf> "{howl} {howl}"
awoo awoo : str
wolf> fn double(n: int) -> int { n * 2 }
defined fn `double`
wolf> double(21)
42 : i64
wolf> :quit
Two things in that session are worth more than they look. The type is
printed on every line, so a question about types is one keystroke and
never a guess: 6 * 7 is i32 because that is what an integer literal
infers to, while "wolf".len is i64 because a length is. And :type
answers without running the program for effect, which makes the prompt a
way to interrogate the language rather than a calculator that happens to
speak it.
Because this is the reference interpreter, the answers are the specification’s answers. When a later chapter claims wolf does something, you can ask.
A fault does not end the session:
wolf> let s = "wolf"
wolf> s[2..9]
trap(bounds): byte range 2..9 is outside a 4-byte string [mem.ub.defined] at 0..7
the session survives the trap; the world is as the fault left it [repl.trap.alive]
wolf> s.len
4 : i64
wolf> :quit
The prompt comes back, the world is whatever the fault left behind, and
s is still four bytes long. Chapter 2 is where that message stops
being a surprise.
One directive to note and set aside: :mem prints the state of the
memory model — what exists, what owns it, what is still open. Part 2 is
where it becomes the most useful four characters in the tool. :help
lists the rest.
Exercise 1-4 (fingers · lupin REPL) — Open the REPL. Compute the
number of seconds in a day, ask :type what type that expression has,
then define a function mid-session and call it twice. Before you ask
:type, write down your guess.
1.5 What run was doing for you
wolf run hello.lu looks like one action. It is six: the file is
tokenized, parsed, its names resolved, its types checked, its memory
discipline checked, and then — only then — run. lupin hello.lu is the
same six. The two implementations part company at the last one, and
only there: the compiler writes machine code and hands it to the
kernel; the interpreter walks the program itself.
The phases matter because they divide the ways a program can end. There are three verdicts, and the interpreter gives each of them a distinct exit code.
The program never starts (exit 2). Delete a closing brace and the file is not a wolf program at all:
fn main() -> !int {
print("almost")
0
$ lupin almost.lu
almost.lu: E0202: the file ends where `}` was required [gram.expr.block] at 45..45
$ echo $?
2
The compiler refuses the same file, and takes more room to do it:
$ wolf build almost.lu
error[E0202]: this `{` is never closed
--> ./almost.lu:1:19
|
1 | fn main() -> !int {
| ^ opened here
...
4 |
| - the parser expected the closing `}` by here
|
wolf build: the package does not compile; fix the errors above (`wolf --explain E0201` explains any code by name)
Two implementations, one rule, one error code, two voices: the
interpreter reports the byte offset where the file ran out, the compiler
points at the opener because that is where your hands go. Neither is
paraphrasing the other, and neither of them produced a program. wolf --explain E0202 prints the rule behind both.
The program runs and hits a rule (exit 3). §1.1’s scan trusted every row to hold a comma. Here is the row that does not, and the loop alone:
fn main() -> !int {
let row = "tip 100"
var i = 0
while row[i..i + 1] != "," { i += 1 }
print(row[..i])
0
}
$ lupin scan.lu
scan.lu: trap(bounds): byte range 7..8 is outside a 7-byte string [mem.ub.defined] at 68..81
$ echo $?
3
This program was legal. It started, it ran, and it walked off the end of a seven-byte string, which wolf defines as a fault rather than leaving it to luck: the run stops, names the operation, the range, the length, and the clause it enforces. Chapter 3 gives faults like this one their own section, and chapter 6 is where the receipt learns to say “this row has no comma” instead.
The program hands back an error (exit 1). The ! in !int has
been sitting in every main since §1.1. It says the function may
finish with an error instead of a number, and ? is how one travels:
fn main() -> !int {
let cents = "twelve".to_int()?
print("{cents}")
0
}
$ lupin amount.lu
error: NotAnInt
$ echo $?
1
Nothing went wrong with the machine. "twelve" is not a number,
to_int said so, ? handed the answer up rather than inventing one,
and main ended the process with it. The print never ran. This is
the verdict chapter 6 spends itself on; here it is enough to know that
an error is a value that travels, and that a program which ends this
way ended on purpose.
The thing you can keep
Those three verdicts are what the interpreter reports, and the interpreter is where most of this book’s programs run. What the compiler adds is a file:
$ wolf build hello.lu
$ ./hello
hello, wolf
$ echo $?
0
main’s value is the process’s value in both implementations, so the
shell sees the same 0 whichever door the program came through. The
binary is built with debug information, so a debugger breaks on the
line you name in your own source, in your own file, and prints the
numbers in scope there.
Exercise 1-6 (spelunking · lupin) — Delete the closing brace of a
working program’s main and run it. Read the whole diagnostic: the
code, the message, the clause tag, the span. Then run echo $?. Which
of this section’s exit codes is this, and why is it not the code a trap
would produce?
Exercise 1-8 (comprehension · wolf + lupin) — Build the greeting,
then run wolf build hello.lu a second time without editing the file.
Predict what the second build does before you run it, and where it put
what it kept.