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
}