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
| command | on Windows |
|---|---|
wolf build, wolf run | works — a native hello.exe, with Build Tools installed |
wolf run --checked | works — the same program against the checked runtime |
wolf test | works |
wolf fmt, wolf fmt --check | works |
wolf --version, wolf --explain E0001 | works — the whole diagnostic catalogue is in the binary |
wolf conform-run file --checked | works — runs your program on the checked machine and prints a record |
tasks, procs, channels, select, region transfer, os.signal, network deadlines | works — the whole concurrency and OS surface, at macOS parity on the corpus |
wolf build --release | refuses, exit 2 — the LLVM tier serves linux and macOS |
an external reload/upgrade signal | no Windows analog — your own program can raise and receive both; another process cannot send them |
net_listen_unix, net_connect_unix | the unsupported row, by name — a local control endpoint stays on loopback TCP here |
net_wait, os_cpus | works — readiness over a whole set of handles, and the core count to size a server by |
net_accept | works, 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_with | works without reuse_port, backlog hint included; with it, the unsupported row, by name |
os_spawn_with, net_adopt_listener | works with an empty inherit set; a descriptor handed to a child is the unsupported row, by name |
lupin, the reference interpreter | works — 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.