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
That is lupin, one of the two programs that run wolf; §1.2 introduces
both, and this receipt runs under either. Where a program in this book
runs on one of them and not the other, the page says which.
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.
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.
Nothing sits above fn main, because a wolf file needs nothing there:
no imports, no enclosing class, no guard against being imported.
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.
Exercise 1-11 (fingers · lupin). Find the longest line of a multiline block and print its length and the line itself, in that order. One pass, one comparison. What does your program print when two lines tie, and which line of your code decided that?
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 and no build layer. Save the greeting as hello.lu and point
the compiler at it:
$ wolf build hello.lu && ./hello
hello, wolf
hello is a program. It needs no runtime, starts as fast as the kernel
can map it, and runs for 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. With one implementation, there is no way to tell what the language
specified from 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. Slow down at the pages where they speak in turn; §1.5 begins the habit.
Ask either who it is and it tells you:
$ wolf --version
wolf 0.2.29 (wolfgang, pin 6ffe7c6)
paired with lupin 0.1.52 (reference interpreter), pin 294d626
$ lupin --version
lupin 0.1.52 (wolf-interp, reference interpreter at pin 294d626)
Each names the other for a reason. 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.
Read the compiler’s own version first. 0.2.29 is bare, and a bare
number belongs to one kind of build and to nothing else: one made
exactly at a release tag. Every other build prints the commit it came
from after the number and claims no release. This printing’s compiler
is the first kind, and the pin 6ffe7c6 clause beside it is that tag’s
revision, abbreviated to seven characters — a width the toolchain
chooses on purpose, so that two people who clone the project on
different days read the same stamp.
Both halves of the pair are readable this printing. The compiler names
lupin 0.1.52, the interpreter printed under it and not a release
older. And the interpreter names 294d626, a release tag of the
compiler’s from five releases back: a revision you can check out, on the
branch, behind the compiler above it. Count the commits between the two
and you get 611, which is worth knowing before you trust the number,
because a count is only a distance when one revision is an ancestor of
the other.
That is worth saying because an earlier printing was the other case, and it is the case to watch for. Its interpreter named a revision on no branch — a development head that got rebased as it merged, which rewrites commits — so the two clauses were not a distance at all and no count between them meant anything. Neither tool was wrong then and neither is wrong now. The two projects are cut on their own schedules, each tool is stamped with what it was tested against at the moment it was built, and the question to ask of any pair of clauses is the same one: is either revision reachable from the other? Read either way, the pair answers the question it is for: which two implementations agreed, and when.
The colophon records the versions this printing is true for; those lines are how you check that they are yours. If your terminal differs from the book’s printed output, check the versions before calling it a bug.
Getting them
A tagged release publishes archives on the two projects’ release pages,
one per tier-1 host: at this printing’s tags that is four apiece —
linux x86-64 and aarch64, macOS aarch64, windows x86-64. An archive is
a folder, not an installer: nothing is registered, nothing is written
outside it, and removing it removes wolf. The compiler’s folder holds
the wolf binary, the C-import worker, the runtime library wolf build links your programs against, and the standard library as std/
— keep those together, because the driver looks for the library and
the std/ beside itself. Unpack the folder, put it
on your PATH, and run the two --version commands above. An archive
carries the release it is named for, and this printing’s compiler is
that release: a wolf unpacked from its archive prints the two lines
above, character for character, and so does a lupin unpacked from
the interpreter’s. One of the
four serves less than the others: linux aarch64 has no native backend
yet, so its archive runs your program on the checked machine (wolf test, wolf conform-run --checked) and wolf run refuses that host
by name. And a host outside the tier-1 matrix has no archive at all;
the source recipe below is the path there, and it is the path the
project itself is known to work on.
Windows carries one requirement that the other hosts do not, and it is
worth spelling out, because it is the difference between a first
program and a wall of unresolved symbols. Linking any Windows program
needs import libraries — kernel32.lib, the UCRT, msvcrt.lib — that
ship with the Windows SDK and the MSVC toolset rather than with Windows
itself, which is the same requirement Rust’s own windows-msvc
toolchain carries. Install Visual Studio Build Tools with the
“Desktop development with C++” workload first. Then unpack the archive
in PowerShell (tar has shipped with Windows since Windows 10 build
1803, so nothing has to be installed to open it) and put the folder on
your path for the session:
tar -xzf wolf-0.2.29-x86_64-pc-windows-msvc.tar.gz
$env:Path = "C:\tools\wolf-0.2.29-x86_64-pc-windows-msvc;$env:Path"
From a folder holding one hello.lu, the greeting compiles and runs
the way it does everywhere else:
wolf run hello.lu
hello, wolf
That is machine code on the host, not an interpreter — wolf build hello.lu stops one step earlier and leaves a hello.exe you can move
or send to somebody who has never installed wolf. Without the Build
Tools, the driver refuses before it links rather than after, and the
refusal is the whole fix, printed on one line:
wolf build: windows-native: no MSVC toolset / Windows SDK found — the
import libraries every linker needs (kernel32.lib, the UCRT, msvcrt.lib)
come with Visual Studio Build Tools' "Desktop development with C++"
workload (MSVC x64 + Windows SDK); install that, or run from a Developer
Command Prompt (LIB set). Bundling the libraries so no install is needed
is s47's (the s60 campaign).
Exit code 2, and the program never started. You do not need a Developer Command Prompt to get past it: wolf finds the installation itself once the workload is there.
The interpreter needs nothing at all. lupin.exe is one file on the
wolf-interp release page, about five megabytes, with no installer and
nothing beside it, and running a file takes no subcommand:
lupin hello.lu
hello, wolf
The task layer used to be the thing to warn a Windows reader about.
One release ago twenty-one programs of the compiler’s own corpus
refused on that host by construct name — spawn and scopes, proc,
channels and select, region transfer, signal handling, network
deadlines — and this section had a paragraph listing them. They compile
and run there now, at the same corpus parity as macOS, so chapters 10
through 17 and both halves of chapter 30’s capstone run where you are,
and the paragraph is gone.
Two limits are left, and a reader can meet both.
wolf build --release, the LLVM tier of chapter 19, answers this host cannot run the release tier, because that tier targets linux x86-64
and macOS aarch64: exit 2. Plain wolf build is the one you want on
Windows, and it is a real native build through the compiler’s own
backend. And an external reload or upgrade signal has nothing on
Windows to arrive through: the console offers Ctrl+C, Ctrl+Break and
its window closing, which wolf reads as terminate and quit, and
offers nothing that means “reload”. A program can raise and receive
either signal inside itself; what no process can do on that host is
send one to another, so a long-running program that wants them takes
them over a channel instead. The rest of this book runs where you are.
Those four blocks, and the linker note under Building them yourself
below, are the only five transcripts in this edition that were not
produced by the run that built it. The book’s sample runner does run on
Windows now — the samples lane executes every chapter’s console blocks
on all three tier-1 hosts, and has since the edition before this one —
but it replays a program, and these four are an unpack, a PATH
assignment, and the first run of an archive nobody downloaded for it.
So they are transcribed from the project’s own published
measurement at lupp.us/install/ — where a Windows job unpacks that
same archive, types those same commands, and asserts every line of the
result, the successes and the refusals alike, on every change to that
page and once a week besides. If the page and the archive ever disagree,
the archive is right and the job says so. The linker note is the fifth
for the opposite reason: every machine that builds this edition has the
linker, so none of them has the note to print, and the sentence is
quoted from the compiler’s own source at the pinned revision.
Building them yourself
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, two commands build them:
$ (cd wolf-lang && cargo xtask dist)
$ (cd wolf-interp && cargo build --release --bin lupin)
The first command is wolf’s own release builder: it compiles the
compiler and the runtime library it links programs against, stages the
standard library beside them as the archive does, and stamps the build
with the commit it was made from — the stamp is what the pin 6ffe7c6
clause above is. A plain cargo build compiles the same compiler and
runs everything in this book, but with no std/ beside it (chapter 22’s
wolf interface listing is one place that shows), and its first line
reads wolf 0.2.29+dev.unknown: a build the release machinery did not
stamp cannot name its own commit, let alone claim a release. Cargo
leaves wolf under wolf-lang/target/release/ 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.
On Linux one more thing belongs on that PATH: ld.lld, the LLVM
linker, which is what wolf build looks for when it turns your objects
into a program. Your distribution packages it as lld. A packaged wolf
toolchain will bring its own, and until that packaging exists a
built-from-source wolf uses whatever linker the machine already has —
and says so, once, before it links:
wolf build: note: ld.lld not found on PATH — linking with the system
linker (lld ships with the toolchain; packaging is c13)
The build still succeeds; the note is a heads-up about which linker did
the work, not a failure. It is also the one line standing between your
terminal and the transcripts printed in this book, which were recorded
on hosts where wolf had nothing to say about the linker — macOS uses the
system linker by design and notes nothing, and the book’s own CI
installs lld on its Linux runner for exactly this reason. Install it
and your terminal matches these pages. On Windows the linker question is
the Build Tools one above, and wolf answers it in its own words.
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, 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")The difference is around the file, not in 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, 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?
Exercise 1-9 (fingers · lupin). Drive 1-5’s table the other way: Celsius −20 to 40 in steps of 10, Fahrenheit beside it, both columns right-aligned. This direction has no rounding bug. Say why not before you run it.
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
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 explains that message.
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
leans on it. :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 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 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 3:6
$ 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 E0202` explains this code, or any other, by name)
One rule, two voices: the
interpreter reports the line and column 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 4:11
$ echo $?
3
This program was legal. It started, it ran, and it walked off the end of a seven-byte string, and wolf defines that as a fault: 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: parse
$ echo $?
1
Nothing went wrong with the machine. "twelve" is not a number,
to_int said so — its error is named parse, after the family of
conversions it belongs to — ? handed the answer up,
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 the language’s rather than either tool’s, 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 --verbose a second time without editing
the file. A successful build is silent by default; --verbose is how
you watch it work. Predict what the second build does before you run
it, and where it put what it kept.
Exercise 1-10 (comprehension · lupin). Before running, write down
what this program prints and what echo $? shows afterward:
fn main() -> !int {
let amounts = """
3
four
5
"""
var total = 0
for row in amounts.lines() {
total += row.to_int() else 0
}
print("counted")
total
}