install

What the published archives do on each platform, measured rather than assumed.

Every tagged release uploads an archive per host to the releases page. An archive is a folder with the wolf binary in it, the runtime archive it links programs against and, since v0.2.27, the standard library as std/ beside the binary, which is where use std.… resolves when nothing else is configured — there is no installer and nothing to register. On macOS and linux a package manager will do it for you instead: the doors are below. Building from source is the path the project itself uses; the recipe is on the front page.

Wolf is version 0.2.29 and pre-alpha. This page describes that release and no other.

Package managers

Two channels carry wolf, and the project publishes both. On macOS and linux, a Homebrew tap — three lines, and the first one is not optional:

brew trust wolffe-lang/wolf
brew tap wolffe-lang/wolf
brew install wolf

Skip brew trust and Homebrew tells you something that is not true. It declines to load formulae from a third-party tap you have not trusted, and then reports the refusal in its generic words:

Refusing to load formula wolffe-lang/wolf/wolf from untrusted tap wolffe-lang/wolf.
Error: Cannot tap wolffe-lang/wolf: invalid syntax in tap!

There is no syntax error. Both formulae parse, and the second line is what Homebrew says about any tap it declined to read, whatever the reason. Trust the tap and it taps cleanly. The tap carries three formulae: wolf, the compiler; lupin, the reference interpreter; and lobo, the web server this project is writing in wolf. The first two build from the tagged git source rather than a release archive, which is deliberate — a build made at the tag is what lets wolf --version print a bare version and a pin instead of the +dev suffix an unverifiable build carries, and each formula's test block asserts that before the binary reaches you.

Arch

On the AUR the packages are named for the language rather than for the compiler, because wolf was taken a long time ago: it is Return to Castle Wolfenstein, and installing it gets you a first-person shooter that wants the retail game files. What you want is

yay -S wolf-lang-bin

and there are five packages in all. wolf-lang and wolf-lang-bin are the compiler, built at the tag and unpacked from the published archive respectively; lupin and lupin-bin are the interpreter the same two ways; and lobo-bin is the server. The -bin packages are the fast door, and they install the same archives the releases page serves.

Neither channel is built by this site, and nothing on this page can tell you they are current. What can is a gate in the tap's own repository that reads what is published — the live AUR, the live tap, the live release pages — and goes red when a package trails its upstream or names a digest the release does not carry. Read live at this bump, the two channels agree. The tap's wolf formula builds tag v0.2.29 and its lupin builds 0.1.52, and the AUR's four packages carry the same pair, the compiler's packages and the interpreter's alike — both channels shipped them before this page advertised them. At v0.2.16's bump they had parted for the first time: the tap was current and the AUR trailed by two tags, so yay -S wolf-lang-bin installed a compiler two tags older than the one this page then described. The AUR's packages are built from sources that now live in the tap's own repository, each push behind an install proof, and at every release since both channels have been bumped in one pass. A release and its packaging are still separate acts and do not land in the same minute, so whenever this page and a package disagree, the four archives below are the release itself and need nothing installed first.

Windows

The compiler builds and runs native Windows programs, and the concurrency and operating-system surface is among what it runs. wolf build hello.lu writes hello.exe and wolf run hello.lu compiles and executes it — machine code on the host, not an interpreter. Native compilation arrived at v0.2.2 refusing a whole region by name — tasks, channels, os.signal, twenty-one programs of the conformance corpus that this page listed one by one — and v0.2.3 took that paragraph off the page. Three of those programs run on a Windows box every time this site changes, which is how the sentence stays a measurement.

There is one thing to install first, and it is not wolf's: Visual Studio Build Tools with the "Desktop development with C++" workload. 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. It is the same requirement Rust's own windows-msvc toolchain carries. Without it wolf refuses before it links, and the refusal is quoted in full below.

Nothing here is guessed. A job in this site's own CI downloads the archive linked below onto a Windows runner, unpacks it with the same command you are about to type, and asserts every claim on this page — the successes and the refusals alike, word for word and exit code for exit code. It runs on every change to this site and once a week besides, so a sentence here that stops being true goes red before you find out the hard way. The no-toolchain refusal is measured too: the job takes the Windows SDK away from its own runner for the length of one command, so the sentence below is a transcript rather than a paraphrase.

Get it

Download wolf-0.2.29-x86_64-pc-windows-msvc.tar.gz and unpack it in PowerShell. tar has shipped with Windows since Windows 10 build 1803, so nothing needs installing to open this:

tar -xzf wolf-0.2.29-x86_64-pc-windows-msvc.tar.gz

That leaves a folder holding wolf.exe, the C-import worker, the runtime library wolf_rt.lib, the licenses and std/, the standard library, with std/STD-REV naming the wolf-std commit it is. Keep them together: wolf.exe cannot link a program without wolf_rt.lib beside it, and reads std/ beside it unless --std-root, WOLF_STD or a wolf.pkg std dependency names another. Move the folder wherever you keep tools and put it on your PATH. For one PowerShell session:

$env:Path = "C:\tools\wolf-0.2.29-x86_64-pc-windows-msvc;$env:Path"

To keep it, add the same folder to your user PATH under Settings → System → About → Advanced system settings → Environment Variables. Then, in a new window:

wolf --version
wolf 0.2.29 (wolfgang, pin …)
paired with lupin 0.1.52 (reference interpreter), pin …

Two lines: the release and the commit it was built at, then the lupin release it was differentially tested against and the specification pin that release was built to. A binary never claims a release it is not — anything but a build made at the tag says so in that first line. The second line is the compiler's memory of the interpreter it was cut beside, and at these pins that is the interpreter this page serves.

Read the second line closely, because it is the one that moves, and at these pins it has not: the interpreter released once more without re-reading the specification, so the gap widened another step. lupin 0.1.52 names 294d626 as the specification it was built to. That is a release tag rather than a revision of the compiler's trunk, and the compiler has tagged release after release since while the interpreter kept that pin, so the newest release whose text the interpreter carries whole is five releases behind the compiler you are installing, six hundred and eleven commits back. At v0.2.28's pin the same tag was a step nearer: lupin 0.1.49 had kept lupin 0.1.48's pin, lupin 0.1.50 and lupin 0.1.51 kept it after that, and the interpreter above has kept it again. Each of them mirrored instead what the compiler had added — volatile access and the atomics on raw pointers, the kernel's papercuts, a child's descriptors and copy region (wolf-interp#185, #194, #200), then the unsigned and wrapping integers, a repeated json name, env_unset and the calls that change a file, and now as much of the shell's signals as it can serve without unsafe — which is why the programs those releases brought run in the tab, where a page has what they need, while the text it reads has fallen further behind. From v0.2.19's pin to v0.2.25's the interpreter had re-pinned on the compiler's last tag but one at every pair this site served; lupin 0.1.47 shipped beside v0.2.24 reading v0.2.22's text, two tags back, and this site never served that pair; v0.2.26's pair was the first it served two tags apart since v0.2.18's, and this pair is the widest gap the check below has ever counted. It agrees with the compiler's own second line: the release above was tested against the interpreter version this page names, and the playground serves that interpreter, as it did at v0.2.28's pin, at v0.2.26's, at v0.2.25's, at v0.2.23's, at v0.2.22's, at v0.2.21's, at v0.2.19's, and at v0.2.18's, when the interpreter had just narrowed the gap from the two tags it had fallen behind. At v0.2.15's pin it was not, and the reason is worth keeping: the interpreter release the compiler had been tested against declared, as the specification checkout it was built to, a compiler revision the released compiler's history does not contain — a branch head the rebase-merge that landed it rewrote under other shas — so there was no distance along one line to state for it, and this page refused to state one it could not count. The playground carries the same paragraph from the other end. A gap opens whenever the compiler tags and the interpreter has not yet followed, which is the ordinary shape of a two-implementation project, and it closes when the mirror catches up. While it is open the playground can answer a program differently from the compiler you just installed. At these pins it refuses none of the programs the compiler runs; it declines some by name, as it does at a terminal, and the list below says which, why, and how that was measured. When the two disagree, the specification is what gets read.

What a gap costs, concretely, is a short list of corpus programs, and at these pins the list of refusals is empty: no program the compiler runs is refused here, in the tab or at a terminal. What is left is declines, each by name, and each answers the same way under lupin 0.1.52 at a terminal. Three programs store a whole struct through a raw pointer — corpus/memory/raw_repr_c_layout.lu, raw_repr_packed_layout.lu and raw_repr_align_layout.lu — which the interpreter declines by name, as the compiler's own checked machine does; corpus/membrane/extern_let_image.lu names a symbol only a link defines, and corpus/membrane/extern_libc.lu calls a bodyless C declaration, and the interpreter models neither a link nor a C ABI. The declines this page named at v0.2.25's pin are gone: the atomics and the volatile program, corpus/conc/atomic_fence.lu, atomic_orders.lu, atomic_widths.lu and corpus/memory/volatile_widths.lu, which lupin 0.1.48 had no fence, Order or write_volatile for, and corpus/memory/packed_field_raw_read.lu, whose raw pointer to a struct it read as bytes, all print what the compiler prints (wolf-interp#194, #185, #205). corpus/conc/atomic_counter.lu runs at a terminal and is declined in the tab only, for the tab's reason: it spawns tasks, and a page has no thread to give them. One program answers differently in the tab without being declined, and it is the tab's too: corpus/os/chdir_relative.lu moves the working directory, which a page does not have, so the tab's os_chdir answers the io row and the program exits 1 printing error: io, where at a terminal it prints what its header says and exits 0 (wolf-interp#222). None of them is worked around here.

The tab reads cfg as an x86-64 Linux host. The interpreter evaluates a #[cfg(target = …)] against the target it was itself built for, and the module the playground serves is built for WebAssembly, a target no gate in the corpus names, so the tab observes every program as x86_64-unknown-linux-gnu, the target the interpreter's own conformance bundle is observed as, and says so on the page. corpus/grammar/cfg_target_arch.lu defines one function twice, gated to two architectures, and prints 64 in the tab as it does on every tier of the compiler and under lupin 0.1.52 at a terminal. Before the tab took that target it read cfg as WebAssembly, both definitions were dropped, and the program was declined, naming the function that does not resolve (wolf-web#55).

The release above brought one program into the corpus's running set, and it is declined here for the tab's reason. corpus/os/signal_shell_rows.lu walks the shapes of the shell's signal calls — a poll, a disposition, a send, a signal's number, this process's id and its parent's — and the first of them is a call the page has no process to answer, so the tab declines the program by name with the process tier. It declines each of the release's new calls the same way when a program makes one alone: os_pid, os_ppid, os_signal_send, os_signal_number and os_signal_ignored all answer unsupported, naming the call. Under lupin 0.1.52 at a terminal the program prints what its header says: that interpreter serves the two ids and the number, answers every shape row, and refuses the send and the ignored set by name, because it admits no unsafe and its host library has neither call. lupin 0.1.51, the interpreter this site served until now, declined the program in the tab at the same call, and did not know the new names at all. corpus/os/job_rows.lu, whose outside bit the release moved because its old one became a meaning, was declined in the tab before and is declined now, at its job spawn. The census counts verdicts; a second walk feeds every running program in the corpus to both modules and compares what each prints. The measurement is four walks, not one — both corpora through both modules — and no program that both corpora carry changed its answer, in its verdict or in its output.

At v0.2.28's pin the additions were v0.2.27's and v0.2.28's together, and each answered as its header says or was declined for the tab's reason. The unsigned and wrapping integers in corpus/typecheck/'s int_truth_*.lu print their headers' values, and the int_truth_* programs in corpus/faults/ trap overflow or div-zero where their headers say; the repeated names in corpus/json/dup_*.lu answer last-wins; corpus/os/env_unset.lu removes its variable; corpus/typecheck/numlit_radix.lu reads its octal and binary literals; and the calls that keep or send what they are handed and stay legal, corpus/memory/region_kept_arg_legal.lu and corpus/conc/chan_sent_arg_legal.lu, print their headers' lines. Under lupin 0.1.49, the interpreter this site served before that pin, the json programs answered first-wins, env_unset.lu was declined and int_truth_wrap_mul_wide.lu trapped overflow where the compiler prints the wrapped product. The rest were declined for the tab's reasons, and still are: the calls that change a file with the filesystem (corpus/fs/), os_exec, a job and the umask with the process tier (corpus/os/), a net read with the net tier, the channel program's task-spawning twin with the task tier, and corpus/link/module_c.lu, which names a sibling module the editor does not have.

At v0.2.26's pin the additions were the raw field stores, corpus/memory/raw_field_store.lu and its compound, nested and packed twins, the complements in corpus/typecheck/int_not_byte.lu, int_not_mask.lu and int_not_signed.lu, the -> never handlers in fn_never_handler_arm.lu and fn_never_extern.lu, the copy region programs and the byte scan in corpus/memory/bytes_scan.lu, and each printed its header's lines under lupin 0.1.49, as each still does here; fn_never_trap.lu stops at its assert as its header says it does. Under lupin 0.1.48 most of those were declined or refused, and three of the copy region programs trapped region-fault, because that interpreter read the block as a plain region. That release's one fixed wrong answer never reached the tab: corpus/memory/region_str_call_inside.lu and region_str_call_static.lu print the same under both of those interpreters, and the shape the compiled tiers read from freed bytes is one lupin traps.

At v0.2.23's pin the gap cost refusals, and none is left. Four programs laid structs out with #[repr(c, packed)] and #[repr(c, align(N))], which lupin 0.1.46's closed attribute set refused by name, fail(E0817) (wolf-interp#188), and corpus/membrane/extern_let_image.lu named a linker symbol with extern "c" let, a line that interpreter could not parse, fail(E0201) (wolf-interp#190). Under the interpreter above, corpus/comptime/layout_query_repr_c.lu and corpus/memory/packed_fields_at_offset_of.lu print what the compiler prints. corpus/memory/raw_repr_packed_layout.lu and raw_repr_align_layout.lu store a whole struct through a raw pointer, which the interpreter declines by name, as the compiler's own checked machine does: the ABI document says neither models a byte-level aggregate; extern_let_image.lu is declined by name because the interpreter models no link to define the symbol.

At v0.2.22's pin the gap cost six programs, and all but one are healed or declined by name. Two gated code by #[cfg(target = …)], which lupin 0.1.45 did not read, and three carried a *T in a module-private signature or across the C membrane, which it refused in every signature, fail(E1302); and corpus/rows/unit_discard_if_value.lu exited 0 on both sides and printed () none where the compiler prints () (), binding the row's tag where an else-less if is () (wolf-interp#179) — a disagreement inside the text both read, not the distance. The interpreter above prints () (). The sixth, cfg_target_arch.lu, was the tab's decline above, and prints 64 in the tab now.

At v0.2.21's pin the gap cost one program, and it is healed. corpus/rows/eu_bind_empty_row_handled.lu binds a function's result whose row the compiler infers to be empty, then reads it in a match, an else and a return; every tier of the compiler prints 43 42 42, and lupin 0.1.44 read the inferred row as open and refused the program, fail(E0801), in the tab and at a terminal alike (wolf-interp#176). That release named the parting in its own notes and pinned the interpreter's answer on it by version; the interpreter above prints what the compiler prints. Every other addition of that release — a match over a fallible value, an errdefer run by its block's own error value, an else that handles only its scrutinee's row, and the two-phase arguments of the release before it — printed in the tab what its header states; the two-phase reads trapped exclusivity under lupin 0.1.42, which took a later argument's read of a claimed place for a conflict.

At v0.2.19's pin the gap cost nothing: every addition of that release — two mut claims on two different elements of one list, a length or count() read beside a claimed or moved element, and the two field claims that stopped the 0.2.18 release tier with an internal compiler error — printed what its header states. Three of them trapped in lupin 0.1.41, which read a member beside a claim as the whole container, and a Pool's length read after one of its elements moved out was declined by name, as it still is.

At v0.2.18's pin the gap cost one program's output: corpus/memory/ctl_store_order_nested_index.lu stores through two and then three index levels with a call in every index, and lupin 0.1.41 called each index but the last twice, so it printed its index lines out of the order the header states (wolf-interp#145). It exited 0 in both places, so only the second walk could see it. The interpreter above calls each index once and prints what the compiler prints. At that pin one program already in the corpus had been rewritten by the release: corpus/memory/list_session_struct.lu read an element it had moved, and compiled only through the hole v0.2.18 closed; its new text copies the element and exits 0 in both places.

The one program on the list at v0.2.16's pin, corpus/conc/proc_join_param.lu, wrote Proc as the type of a parameter before the interpreter knew the name; the interpreter has known it since, and the program reaches the proc tier, which a browser tab declines for want of a thread — a decline of this build, not a parting.

v0.2.15's pin carried a longer list, and every entry on it was a construct the interpreter had been built before — the list literal, a nullary variant as a pattern, the range types, the error alias for a row, and the method surface with its combinators. All of them are read on both sides now. Six of those programs did not simply start running: they moved from a refusal to a decline, because the interpreter parses the method call and then has to reach the receiver's home module for it, and a browser tab has no standard library on disk to reach. They still do, and the playground spells that difference out, since to a reader "refused" and "this build cannot serve it" look alike and are not.

The two messages that parted at an earlier pin are settled — a bare row used as an operand is E0409 in both places now, and a write through a str slice is E0416 in both (wolf-interp#85, closed) — and the range arms that parted the pin before that have been on both sides since: corpus/grammar/match_range.lu and corpus/grammar/match_range_char.lu run in the tab and with the compiler above and agree, as do the empty range and the open end the same ruling brought in (wolf-interp#83, closed). Both witnesses stay named here after their parting closed, because a page that records a disagreement only while it is open leaves a reader no way to check that it ever ended.

This page once described a parting running the other way — the interpreter took a one-line if, if c then a else b, that the compiler refused. The compiler took it at its next release. Every running witness of that ruling agrees in both places, so the reversed parting lasted a single release and is recorded here because it happened, not because it lasted.

What an earlier gap cost, while it was open, is the useful thing to record now that it costs nothing. It landed somewhere a reader actually stands: v0.2.11 ruled how a Map answers and how an operator reaches a trait, and the interpreter had been built before either text existed, so counting words with a Map alone — corpus/memory/map_count.lu, the program chapter 5 of the book is built around — ran under the compiler and not in the tab. Four others went with it: map_int_keys.lu, traits/op_money.lu and traits/op_ord_struct.lu, where + and < could not reach their impl yet, and traits/op_total_num.lu, refused outright. This interpreter mirrors both texts, so every one of them runs in the tab now and answers what the compiler answers; the playground counts them exactly. No program that ran in the tab at an earlier pin has stopped, and of what the releases above add to the corpus none parts from the compiler on a clause the interpreter has not mirrored: what does not run here is declined for the tab's own reasons, named above, and the one program that runs and parts in its output, chdir_relative.lu, is the tab's too.

A gap of any width does not make the playground run everything, either, and it never did. net_writev, net_nodelay and fs_fstat are all in this interpreter — the filesystem tier is the newest tier in it — and all three are refused in the tab whatever the pins say: a browser has no sockets and no files to open. Run the same programs under lupin at a terminal and all three answer. The check that measures this page counts the gap against the compiler's own changelog — by ancestry, because the interpreter's pin need not be a tag — and requires this page and the playground's to spell what it counted, so the sentence goes red when it stops being true rather than quietly rotting.

Your first program

Make a folder and put one file in it. The folder matters: every .lu file in a directory is one module, so two files each with a main is a duplicate definition (E0302) rather than two programs. hello.lu:

fn main() -> !int {
    let who = "wolf"
    print("hello, {who}")
    0
}

From that folder:

wolf run hello.lu
hello, wolf

Exit code 0. wolf build hello.lu stops one step earlier and leaves hello.exe beside the source, which you can run, move, or send to someone who has no wolf installed at all — it is an ordinary Windows executable. wolf run --checked hello.lu builds the same program against the checked runtime, which watches for the undefined behaviour an ordinary build is allowed to assume away, and runs that instead; it prints the same hello, wolf.

If you are curious which linker it used, wolf build --verbose says so on the last line — on the runner that measures this page it reads wolf build: windows linker: lld-link (lld-link on PATH). Wolf looks for WOLF_LINKER, then lld-link, then the rust-lld a Rust toolchain already carries, then MSVC link.exe, and takes the first one it finds.

The byte, the moment you touch a string's octets

Wolf has a byte-width scalar — byte, an 8-bit unsigned octet, 0..=255 — and every builtin that produces or takes raw octets speaks List[byte] where it used to say List[int]. It arrived at v0.2.4 as a breaking change, and it stays on an install page rather than sinking into a changelog because it is the first hour of wolf, not the release notes: the first thing anyone does with a byte is compare it to a number, and a byte adopts no numeric literal. So this does not build:

let b = s.bytes()[0]
if b == 119 { print("w") }

The compiler finishes the sentence rather than leaving you to guess which side to change:

error[E0401]: this is `{integer}`, but the other side of `==` makes it `byte`
  = note: `byte` adopts no literal and takes no `int` implicitly ([type.byte]):
    widen the byte — `b as int` — or narrow this side with `as byte`, which
    keeps the value's low eight bits ([type.byte.cast]).

Exit code 2 — a program the compiler rejected, which this release made a different number from the 1 a program that ran and returned an error out of main exits with, and a different number again from the 4 a refusal answers with. One line per site, and the note names the line. Comparisons and arithmetic against integers want as int, which widens by zero-extension; a value going back into a byte list wants as byte, which keeps the low eight bits and never traps. The same refusal, with the same note, meets an index (table[b]), a binding written let n: int = b, and a literal match arm. Nothing widens behind your back, which is the point of a scalar that carries its width. The job that measures this page builds the broken program above on a Windows runner and reads that note back, then builds the fixed one and runs it.

What you see without Build Tools

You do not need a Developer Command Prompt: wolf finds the Visual Studio installation itself, and the CI job asserts that from an ordinary shell with LIB unset and PATH cut back to Windows plus the wolf folder. What you do need is for the toolset to be installed at all. On a machine where it is not, wolf build hello.lu answers:

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, wrapped here and printed on one line there; wolf run says the same thing in the same words. This one is the toolchain saying it could not do the job — no linker — which the exit-status clause puts with a rejection at 2 rather than with a refusal at 4. It stops before the link rather than failing at it, which is the difference between a message that tells you what to install and a wall of unresolved symbols. Installing the workload is the whole fix. Shipping the import libraries inside the archive so that nothing has to be installed is planned work, and the refusal names it.

What still refuses

Five things a program can reach, and every one of them is named to your face rather than quietly becoming something else. One is the compiler's own refusal, exit 2. One is a gap in what the Windows console gives a program. Three are the unsupported row — a value your program can read and branch on, which is the difference between code that can serve this host differently and code that cannot. Two of those three arrived at v0.2.5, the release that made a prefork server writable, because this is the host where a prefork server is written a different way. (The ledger at the end of this section has the rest, none of which a first program meets.)

wolf build --release, the LLVM optimizing tier, refuses this host with exit code 2:

wolf build: this host cannot run the release tier: the release
tier targets linux/x86-64 and macOS/aarch64 (s41 + s127; c13 owns the
matrix) — this host is neither, and further platforms follow as their
own sprints

Wrapped here and printed on one line there. The plain wolf build is the one you want on Windows today, and it is a real native build: the same machine code through the compiler's own backend, without an LLVM pass pipeline over it.

External reload and upgrade signals have nothing on Windows to arrive through. The console offers Ctrl+C, Ctrl+Break and the window closing — which wolf reads as terminate and quit — and nothing at all that means "reload". Inside your own program every meaning works: the corpus's signal witness listens for reload, raises it, waits for it and prints reload, and the CI job runs that program on a Windows runner and reads exactly that back. What you cannot do is send one from another process, so a long-running program that wants reload or upgrade on this host takes them over a control channel instead.

Unix-domain sockets — net_listen_unix and net_connect_unix, new at v0.2.4 — answer the unsupported row on this host. By name, rather than as a bare io failure: the difference is whether a program can branch on the host at all, and it is the whole reason the row exists. AF_UNIX has been in Windows since build 1803 and the compiler's own test suite measures the kernel's answer on a runner, but the runtime carries no winsock binding for the family, so the ledger names the rung instead of claiming it. A program that wants a local control endpoint here keeps it on loopback TCP. The job that measures this page runs the corpus's unix witness on a Windows box and reads the named row back from it.

A shared listening address — net_listen_with's reuse_port option — answers the unsupported row here, and this one is a refusal on purpose rather than a binding nobody has written yet. Windows has no SO_REUSEPORT. It has SO_REUSEADDR, which is spelled like a synonym in a manual and means something else entirely: it lets any process take a port another process already holds, and promises nothing about which of them a connection reaches. The compiler's own Windows suite measured what the alias would have bought — two sockets both carrying it bound one port, and every one of sixteen dials went to the first socket bound, none to the second — so a worker that thought it had joined a group would sit idle forever with nothing to report. Refusing by name is the better answer, and it is the one you get. net_listen_with without the option serves this host exactly like net_listen, backlog hint included, and an address already held answers exists by name rather than a bare io.

Handing a descriptor to a child — os_spawn_with's inherit set, and the net_adopt_listener on the other side of it — answers the unsupported row too. Two things are missing rather than one, and both were measured rather than assumed: a listener's SOCKET marked inheritable and named to a re-executed child is not a usable socket in that child at all, because the spawn the runtime performs publishes only stdio handles to it; and a SOCKET is not the small stable number the clause hands over by position, so "the listener is 3" has nothing to be true of. os_spawn_with with an empty set already works — it is os_spawn with the program named apart from its arguments — so what a Windows port of a prefork master waits on is the handoff, not the spawn.

What that leaves is the call v0.2.5 was named for, and it is here. net_wait — readiness over a whole set of handles, the thing a serving loop has instead of a scheduler — is the one socket clause s137 added that names no refusal on any host: poll(2) and WSAPoll answer the same question, and the corpus's witness for it runs on this host in the job that measures this page. os_cpus() answers here too. So a single-process server that waits on many sockets is written the same way on Windows as anywhere; it is the many-processes-one-port shape that takes a different route.

The fair accept v0.2.6 was named for is here too, and it is not a new call — it is what net_accept already did, corrected. Windows is one of the three reactor hosts the clause names, so a listener lives non-blocking under the runtime here as well: a hand woken by an arrival that another hand took waits again against the budget its net_deadline armed, rather than in the kernel's accept queue. Nothing a program written against v0.2.5 does changes. What the correction would buy this host is the prefork shape, and that still waits on the descriptor handoff above — a single-process Windows server accepting on its own listener never loses the race in the first place.

Everything else the language has is here. Tasks and scopes, proc, channels and select, sync/ when, region transfer, os.signal and network deadlines all compile and run, alongside everything a first month of wolf touches — printing, strings, lists, JSON, files, environment and child processes, time, random, regions and the region ledger, and TCP sockets. The release records this host at macOS parity across the conformance corpus, with no row refused by construct name at all: the thirty-six rows the native lane does not execute here are exactly the thirty-six it does not execute on macOS, which makes them the lanes' own scope gaps rather than this host's. A stack overflow inside a task reports in wolf's own words — wolf-rt: stack overflow in task '…', exit 134 — while one in a program that never spawns still dies as a bare 0xC00000FD, because the reporter is installed when the task pool starts and a program with no tasks never starts one.

The compiler's own per-host ledger is docs/platforms.md at the release tag: the table of what each host serves, the refusals row by row, the linker order in full, and the named sprints that close the gaps — including two this page does not measure because no first program meets them, structs passed by value across the extern "c" boundary and the debugger.

Line endings

If you wrote hello.lu in Notepad, it has Windows line endings, and:

wolf fmt --check .\hello.lu
wolf fmt --check: ./hello.lu is not canonically formatted

Exit code 1. Canonical wolf source is LF, and wolf fmt hello.lu rewrites the file — line endings included — after which the check passes. The formatter has no options, so there is nothing to configure and nothing to argue about.

Two more verbs

wolf test hello.lu runs the program and reports a verdict instead of its output — test hello.lu::main ... ok, then 1 passed; 0 failed. It is the short way to ask whether a program still works.

wolf conform-run hello.lu --checked is also available, and it is a different thing: a conformance harness rather than a runner. It executes your program on the compiler's own checked machine — no code generation, no linker, so it works on a box with no Build Tools at all — and prints a one-line machine record instead of your output:

{"commit":"…","diagnostics":[],"file":"./hello.lu","impl":"wolfgang",
 "impl_version":"0.2.29","phase_reached":"run","protocol":1,
 "seeded":false,"stdout_inline":"hello, wolf\n","stdout_sha256":"…",
 "verdict":"exit(0)","warnings":[]}

Wrapped here; the build commit and the output digest are elided. Your program's output is the stdout_inline field. Drop --checked and it stops before code generation and answers "verdict":"pass" with "phase_reached":"wir" — a shape v0.2.16 gave the record, because a run that got as far as it was asked to and found nothing wrong had no way to say so and had been borrowing the word for a gap in the implementation. Through v0.2.1 this was the only way to run a program on Windows, and this page led with it; it is now the specialist's tool it was always meant to be.

What runs on Windows today

commandon Windows
wolf build, wolf runworks — a native hello.exe, with Build Tools installed
wolf run --checkedworks — the same program against the checked runtime
wolf testworks
wolf fmt, wolf fmt --checkworks
wolf --version, wolf --explain E0001works — the whole diagnostic catalogue is in the binary
wolf conform-run file --checkedworks — runs your program on the checked machine and prints a record
tasks, procs, channels, select, region transfer, os.signal, network deadlinesworks — the whole concurrency and OS surface, at macOS parity on the corpus
wolf build --releaserefuses, exit 2 — the LLVM tier serves linux and macOS
an external reload/upgrade signalno Windows analog — your own program can raise and receive both; another process cannot send them
net_listen_unix, net_connect_unixthe unsupported row, by name — a local control endpoint stays on loopback TCP here
net_wait, os_cpusworks — readiness over a whole set of handles, and the core count to size a server by
net_acceptworks, and this host is one of the three the fair accept names — a hand that loses a race comes back inside its budget rather than parking in the kernel
net_listen_withworks without reuse_port, backlog hint included; with it, the unsupported row, by name
os_spawn_with, net_adopt_listenerworks with an empty inherit set; a descriptor handed to a child is the unsupported row, by name
lupin, the reference interpreterworks — lupin.exe, one file, no install

The interpreter, and the playground

Wolf has a second implementation: lupin, a reference interpreter written against the same specification, sharing no code with the compiler. It is now published for Windows as a single file — lupin.exe, no installer and nothing beside it. Running a file takes no subcommand:

lupin hello.lu
hello, wolf

It is a good second opinion when the compiler and your expectations disagree — a different implementation reading the same specification, not a fallback for what the backend cannot do. That distinction had to be won: through v0.2.2 lupin was also the only way to run a program on this host that used a task or a channel, and it has not been that since v0.2.3. lupin --version names the release and the specification pin it was built against — 294d626 at these pins, a release tag on the compiler's own line, as measured above — and it is the pin the second line of wolf --version quotes back, because this compiler was tested against this interpreter. That sentence carries none of the words the lag check reserves, so nothing holds it: it is a second statement of the same fact, in words no gate reads, and it is re-read by hand at every bump on either clock. It has been wrong before for exactly that reason.

The playground runs that same interpreter compiled to WebAssembly — lupin 0.1.52, in the tab, with the corpus programs to load, and nothing to download at all. Its editor is exercised on a Windows runner in this site's CI, in the Ctrl spelling a Windows browser sends rather than the Cmd one; the single Windows-specific catch is a hotkey some graphics drivers take before the browser sees it, and the playground page names it beside its key bindings.

Everywhere else

linux x86-64 and macOS aarch64 are the two hosts the whole toolchain is built and tested on: wolf run compiles to native machine code through the compiler's own backend and executes it, wolf build --release goes through LLVM, and the concurrency runtime is all there. Take the archive for your platform from the releases page and put its folder on your PATH — the binary and the runtime archive have to stay together, because the binary cannot link a program without it.

0.2.29 carries four archives, one for every tier-1 host: those two, windows x86-64, and linux aarch64. That last one has no compiled tier behind it yet, neither the native backend nor the LLVM one. The release workflow builds its archive on an arm runner when the tag is pushed, and the gate there asserts what it serves: wolf run and wolf run --release each refuse the host by name with exit code 2, and wolf conform-run --checked runs the program. So that archive checks and runs programs on the checked machine and compiles none (wolf-lang#614); wolf test runs its tests on the same machine everywhere, though no gate measures it on that host. v0.2.2 shipped no arm archive at all, because the smoke that gates the upload could not tell an unserved host from a broken build. Every release since carries it. All four URLs are asked for a 200 by the same job that measures the Windows section, every time this site changes. For a host outside the matrix, the source recipe is the path.

The front page has the four lines that build the same toolchain from source, which is the path the project itself is known to work on.