Repository navigation
Releases
Sword 0.4.2 — Temper
2026-11-02. A patch to Temper that closes two more ways a program with a
race compiled: pieces of a slice whose elements lead to the same memory, and
a function's frame left behind where a task could still write it.
What changes for existing programs
Programs that compiled stop compiling, under the same exception as 0.4.1:
each is a race the checker was meant to refuse, or a write into a frame that
has returned. The suite, the standard library's tests and the
documentation's examples pass, with five tests that sent a local buffer on a
channel now taking it from the arena. Now refused:
- Writing through a piece whose elements lead elsewhere. The rows of a
[][]i64are elements of their own, but two rows can be the same slice, so
rows[i][0] = vin aparallel for, or a piece ofrows.chunks(n)handed
to amutparameter of a task, counts as writing everything the slice
reaches. Writingrows[i]itself is still a piece (#195). - A function's frame stored where it outlives the call. Returning
local[..]was refused; now so is storing it into what a parameter
reaches, into a shared value under alock, or through a call or an
interface method that keeps it, and returning it under another name
(ys := local[..]; return ys) or through a call (head(local[..], 2))
(#198). - A function's frame sent on a channel, on any channel, including one
made in the same function: the receiver is out of the checker's sight.
What travels on a channel comes from an allocator (#198).
Tools
--audit=footprintsbuilds a program that checks, as it runs, what
the race checker promises: every task and every piece of a parallel loop
is granted what its arguments reach, and two live ones reaching one byte,
one of them to write, abort with both places, on every run and at any
number of threads. It catches what the checker cannot see, such as two
tasks writing one struct a C function hands back from two calls. The
suite, the generated programs the checker accepts and the evaluation's
programs run under it with no overlap.--audit=boundsis the old
--audit, and the two combine (#197).
Nothing else that compiled stops compiling.
Sword 0.4.1 — Temper
2026-11-02. A patch to Temper that closes five ways memory reached a task
under a name the race checker did not follow, and one way a function was
called det when it was not.
What changes for existing programs
Programs that compiled stop compiling, against what a patch promises, because
each of them is a data race the race checker was meant to refuse, or claims a
det that does not hold. 0.3.1 did the same for partitions. Everything else
that compiled still does: the suite, the standard library's tests and the
documentation's examples pass unchanged. Now refused:
- One memory handed to two parameters that a function's tasks need apart.
two(&x, &x), whose two tasks writeaandb, andwork(xs[..], xs[..]),
whose tasks write pieces of one and read the other. Where nothing runs at
once both may still be one memory:scale(xs, xs, k)compiles (#175). - A task writing a slice a function returned, beside one writing what it was
cut from. The result ofhead(buf[..], 4)comes frombuf; so does a
struct returned holding it, or part of a field a method hands back. What an
allocator hands out is still new memory (#177). - A task writing by its own name memory a shared value holds. A slice put
into ashared[T]is lent to it, and every use of it through the lock
counts as a write against any other use (#178). - A task writing a slice it sent on a channel, beside the one that received
it, and several receivers of one channel that carries a slice, since
nothing says which of them gets each value. A channel of numbers carries no
memory and is unchanged (#179). - A task writing a slice a call stored into another argument:
store(&bx, buf[..])makesbxreachbuf(#181). - A
detfunction that calls one reading an address as a number. The
number moves with the machine and the run, and only the function that read
it lostdet; now every caller does.mem.Arena.Allocreads its buffer's
address only to align, and keepsdet(#188).
One program that was refused compiles: a det function that calls through an
interface nothing in the program implements, since no value of it can exist.
Tools
shield det <file | dir>lists the functions that are notdet, each
with the chain to what took it away, in the form of a diagnostic;-all
lists thedetones too, and the last line counts them. Given a library, a
function that losesdetonly by calling through an interface is listed as
det when its mem.Allocator is, and counted apart.
Standard library
Request.Cookie(name) ?stringreads one cookie from the request'sCookie
header, without the quotes around a quoted value, and allocates nothing.
Fixes
- A local buffer (
[N]u8{}) starts on a multiple of 16. An arena aligns from
its buffer's address, so where the frame put the buffer decided how much
padding an aligned allocation took; one byte and then eight aligned to 16,
from a buffer of its own and from one inside a struct, took 48 bytes
between the two arenas on an M4 and now take 47, the same on every machine. --without=namessteadyamong the pieces it knows.
Sword 0.4.0 — Temper
2026-11-01. Tempering takes the brittleness out of hardened steel without
softening it: this release takes out the checks the language proves are never
needed, keeps every other one where it was, and makes a parallel float sum give
the same bits on every machine.
What changes for existing programs
- A float
reduce(+)gives different bits than in 0.3, and from now on the
same ones on every thread count and every machine. Each of the 64 pieces
keeps sixteen sums, element k of the piece going into sum k % 16, folded by
halves, and the pieces are added in order. Against an exact sum of 16.7
million values the error went from −2.2e-5 to 1.7e-6. - Interface methods that write say
mut:mut Write(p []u8) !void. A
call through an interface is checked against its signature like any other,
so a method that writes cannot be called on a value lent read-only, and a
method satisfies an interface only if it writes nothing the interface does
not allow.mem.Allocator'sAllocandReleaseandfmt.Sink's writes
aremut; calling them on an allocator that is notmutstops compiling,
and the message says what to add (#78). ...nilreplaces 0.3.2'selse nil:Request{path: "/", ...nil}, and
T{...nil}for a literal that names no field, which replacesT{nil}. Both
old spellings are refused with the one to write instead.breakin aparallel foris refused. It ended only the piece of the
range it ran in, so which indices ran depended on where the range was cut.
continuestill skips one index.- A shift out of range and a division that has no answer are never
undefined. With checks on, a shift by the width or more panics; with them
off the count is taken modulo the width. A division by zero, and the most
negative value divided by −1, panic in every mode, and a constant that shifts
outside 0..63 is a compile error. Before,1 << 70on au64gave 216, and
so did7 / 0. - Two races in a
parallel forare refused: writing through a local copy
of a slice from outside (mut r := ys; r[0] = …), and reaching the memory
the loop writes under another name anywhere but[i]
(xs[i] = ys[i + 1]with both over one array). parallel for x in xsused to pass the checker and fail in clang; it is
refused with what to write instead.
Language
parallel for off, part in xs.chunks(n)spreads a partition's turns over
the workers, each turn writing only its own piece. A grid stored by rows is
written a row at a time;xs, and any other name for its memory, cannot
appear in the body. There is noreduce.- An
elsemay start the line after itsif.
Speed
Measured on an Apple M4, one thread unless said, alternating rounds against the
build before, medians; the same answers on both sides.
- Bounds checks the language proves go.
xs[i]infor i in a..xs.len
over a slice nothing can change, andout[i]in aparallel for: a reducing
loop with overflow checks 182.6 → 140.7 ms,out[i] = xs[i] *% 2224.9 →
181.3 ms. - A loop's indexing is tested once, before it, when no turn can move what
it indexes; a copy of the loop runs without checks when the test passes, and
the loop as written when it fails, so a panic comes on the same turn and
line. The all-pairs n-body: 99.8 → 55.8 ms, against 52.3 for C. - A loop reading
a[k - 1]ora[k + w]checks only its first and last
turns. A stencil: 44.0 → 28.5 ms. Amutpiece ofxs.chunks(n)its turn
never reassigns counts as fixed too: a stencil by rows 30 → 26 ms. - A loop that only adds settles its overflow check after it, with the same
panic on the same line when there is one:u64, 2 MB summed 2000 times,
144.9 → 38.4 ms. When the bound gives up without an overflow the loop runs
twice, 142.4 → 186.3 ms. - A float
reduce(+)vectorises through its sixteen sums: 266.0 → 40.3 ms
forf64in cache, 171.6 → 43.0 ms over 128 MB. - What a
parallel foronly reads is copied into each piece, so LLVM
stops reloading it every iteration:out[i] = xs[i] * k + 1179 → 54 ms. - A short
parallel forcosts a quarter of what it did: the pieces come
from a counter, and on one worker the loop runs in the caller. A loop of 64
elements withreduce: 487 → 123 ns on one thread, 44 223 → 15 463 on ten. num.Sqrtis the processor's instruction, with the same bits; a
negative argument still givesNaN()on every machine. The n-body that
calls it 67 million times: 11 048 → 98 ms.num.Exactadds into four banks: 23.4 → 12.2 ms for 10⁷ terms, 2.5× a
plain sum. It takes about two kilobytes.- Smaller binaries. The link drops what nothing calls, a program that never
reachesstd/tlsdoes not link OpenSSL, the parking lot's buckets are no
longer initialised data, and each panic message is one string:return 42
stripped, 106 904 → 54 256 bytes.--mode=smalldrops the unwind tables.
noalias on slices was measured and left out: restrict in C bought about
3 %, inside the noise, and the race checker proves iterations independent, not
slices disjoint, so a wrong one would be undefined behaviour.
Tools
shield checks <file | dir>lists every bounds check the program still
pays for, one line each in the form of a diagnostic: checked on every turn,
tested once before the loop at line N, checked only on the first and last
turns;-alllists the proven ones too.- The language server shows
muton an interface method. - The documentation's programs that say what they print are run and held to
it, anddocs/examplesis Sword by example: whole programs, explained a
piece at a time.
Standard library
num.Sinandnum.Cosof an infinity answerNaN(), the same bits on
every machine; they gave the processor's NaN.
Fixes
- Indexing through a pointer to an array or a slice (
a[1]with
a *[4]i64) read and wrote the pointer's own slot. - A task woken while queued for a lock went on with its queue node still
linked, which showed as a segfault in aselecton two channels; and the
lock buckets were built with acall_oncethat raced on Linux. - A loop over elements inside a
parallel forgave clang bad IR.
Sword 0.3.2 — Quench
2026-10-30. A patch to Quench that adds one form: a struct literal can leave
its optional fields empty without naming each one.
What changes for existing programs
Nothing that compiled stops compiling or behaves differently. A new form in a
patch goes past what semantic versioning allows; it ships here because it only
adds what was refused before.
Added
- A struct literal ending in
else nilgives every optional field it does not
name the valuenil:Request{path: "/", else nil}. A literal that names no
field is writtenT{nil}. A field that is not optional still has to be
named, so adding one to a struct still stops every literal that forgot it. - A literal missing only optional fields gets a note saying how to write it.
Sword 0.3.1 — Quench
2026-10-30. A patch to Quench: the race checker closes a hole in partitions
and stops refusing the parent's own uses, and two smaller mistakes are fixed.
What changes for existing programs
One kind of program that compiled stops compiling, against what a patch
promises, because each of them is a data race the race checker was meant to
refuse. A piece of a chunks partition belongs to its task for one turn of the
loop that cut it; the checker took it to belong to it for good. Now refused:
nested chunks loops that spawn with a piece of each, a loop around the
chunks loop or inside it, two spawns given one piece in a turn, and the parent
writing a piece beside its task (#135). A spawn in each branch of an if and
the parent reading part.len are still fine. Everything else that compiled
still does.
Fixes
- Inside a
scope, the parent reading a local of its own and then writing it
was refused as a race. Its own uses run one after another and race only with
its tasks (#136). - Lending one counter or struct to every task of a loop came with a note to
walk it withchunks, which only a slice has. The note now saysatomic[T]
for an integer or a bool,shared[T]withlockfor anything else, and
a[..].chunks(n)for an array (#137). - A package constant as an array length in a generic struct's field, a generic
method's parameter or a generic function's parameter was "not an integer
constant" (#138).
Sword 0.3.0 — Quench
2026-10-24. Quenching sets the steel: this release lets a program be built
from packages other people wrote, fetched from git and pinned by hash, and
closes the ways read-only memory could still be written.
What changes for existing programs
- A copy of read-only memory cannot write it.
mut ys := xs, wherexswas
lent read-only, used to write through to it, and&xpassed an immutablex
to amutparameter. The copy is still legal and can be reassigned; writing
through it is refused. What a call writes through its arguments is worked out
for every function, so a method that only moves its own position can still be
called on a parser that holds its input read-only. - The race checker sees one memory under two names.
ys := xswith both
spawned to write, two pointers taken from one variable, a task given
p := &nwhile its parent writesn: each compiled and each is a data race.
The error names the memory and the name it was reached through. ~xis the complement, at the width ofx.a ~ bused to be a xor;
it no longer compiles into anything else.->is no longer a token.- A statement on its own has to call, send or receive.
total + i, a bare
name or a comparison computed a value and dropped it, which is almost always
a+=or a=that lost a character. - A struct field takes no
mutand no..., anddefertakes no declaration;
all three were accepted and meant nothing. - Tests and writers ask for less
try.t.Equal,Same,Check,Fail,
FailfandLogfrecord and carry on, so one run reports every mismatch;
try t.Mustcompares and ends the test. Writes to ajson.Writerand to
anhttp.Responsekeep their first error and do nothing after it:
w.Err()andres.Err()report it, and the server answers 500 for a failed
response, or closes the connection when the head has gone. Atryleft in
front of one of these no longer compiles. Allocator.Releaseand theMux'sHandleand its shortcuts take what they
write asmut.- Two imports that give one name are an error, where the first used to win
silently.import name "path"gives one of them another. - The standard library's errors keep the same codes in every program, 1 to
75, whether the program imports their package or not, and a program's own
come after them. Exit statuses of fallible programs change accordingly, and an
error code past 255 exits with 255: a code of 256 used to leave with 0.
Packages
sword.modnames a module and what it requires;replacepoints a
dependency at a local directory. Versions are git tags, chosen by minimal
version selection; a branch or a commit with no tag gets a version of its
own that sorts before every release.- A build fetches what the cache (
~/.sword) is missing, over git, and
checks each version againstsword.sum, which holds a hash of the files
the compiler reads. A missing line is added;--lockedrefuses to. shield get path[@version]adds or moves a dependency,@noneremoves
it, and a bare path takes the newest stable tag.shield vendorcopies
everything intovendor/for building without a network.- Fetching never runs a package's code: a Sword package has no build script.
import name "path"gives an imported package another name.
Language
lock readlets any number of readers in at once and no writer; inside it
the name is an immutable pointer. A writer waiting keeps new readers out.if locktakes a guard only when it is free, and runselsewhen it is
not. Both work for either kind of lock.if mut x := optandfor mut x := optbind the unwrapped value as
writable.extern blocking funcdeclares a C function that may block: each call
runs on the io pool while the task is put down, so it does not hold a
scheduler thread, and the errno it leaves comes back with the answer. It costs
about 5 µs a call, so it is for calls that wait, not ones that compute.- Writing a closure is answered with "Sword has no closures" and what to write
instead, rather than a parse error about function types.
Tools
shield newstarts a module: a program, or a library with--lib, with
a test andgit init.shield docprints what a package exports, or one type or method, from
the comments above the declarations.shield --splitcompiles each package to an object of its own and keeps
it in~/.sword/build, so only what changed goes through LLVM. Rebuilding a
small HTTP server after editing only its own package took 86 ms instead of
235 here. It is not the default: inlining stops at the edge of a package, and
a hot loop around a small call into the standard library can run several
times slower. Objects unused for thirty days go by themselves;shield cleanremoves them all.
Sword 0.2.4 — Fold
2026-10-21. A patch to Fold for programs that talk to C: C's types by their
C names, and the errno a failing C function leaves behind.
Standard library
std/cnames C's types the way a header does —c.Int,c.Long,
c.SizeT,c.SSizeT,c.Double,c.Bool,c.Stringfor a
const char *,c.Ptrfor avoid *, and the rest — so a declaration
copied from a header reads like the original. Each is an alias, so ac.Int
is ani32and passes for one.c.Charisu8, for text; a plainchar
passed by value takesc.SCharorc.UChar, since whether it is signed is
the platform's choice.os.Errnoreads the errno a C function just left, and
os.ClearErrnozeroes it forstrtoland its family, which say they
failed only through errno. errno belongs to the thread, so it is read
straight after the call, before anything that could wait.
Documentation
- The Neovim section of the editors' README, with a fix for installing
swordlsunder Lua's conventions.
Nothing that compiled before stops compiling.
Sword 0.2.3 — Fold
2026-10-20. A hotfix to Fold for programs that talk to C: strings cross
between the two without each program writing its own conversion.
Standard library
strings.FromCreads a C string up to its zero, andstrings.FromCMax
also stops at a limit, for achar[N]field the text fills. Both answer with
a view of the C memory and copy nothing.strings.ToCwrites a string and its zero into a buffer of the caller's
for a C function that wants aconst char *, and refuses a string holding a
zero, which C would read as a different, shorter one.
Nothing that compiled before stops compiling.
Sword 0.2.2 — Fold
2026-10-20. A patch to Fold: what a server needs to authenticate, sign, stamp
and cache, a second name for a type, the grammar written down, and three bugs
in the runtime and its tests that had been showing up as flakes.
What changes for existing programs
Nothing that compiled before stops compiling. type is not a keyword: it is
special only at the start of a top-level declaration followed by a name, so a
field or a variable called type is still a name.
The language
type Name = Tgives a type a second name. ANameis aT— values
pass between them without a conversion — and an alias of a struct can write
a literal or declare a method. It takes no type parameters, though it may
name an instance:type Set = collections.Map[string, bool]. The language
server and the editors' own colouring know it.- An array's length may be another package's constant:
[crypto.Sha256Size]u8. - The grammar, in EBNF: the lexical rules, including when
a newline ends a statement, and every production of the syntax.
Standard library
std/crypto: HMAC-SHA-256,crypto.Equal(a comparison that takes the
same time wherever the bytes differ), PBKDF2-HMAC-SHA-256, and
crypto.Random, bytes from the kernel's random source. Under
SWORD_SIM_SEEDthe random bytes come from the seed, so a simulated run that
makes a token makes the same one again.std/encoding: hex, base64 and base64url into the caller's buffer.
Decoding is strict, so each byte string has one spelling.std/time:Rfc3339,HttpDate,ParseRfc3339andParseHttpDate,
which reads all three forms RFC 9110 requires a recipient to accept.std/http: static files carryLast-Modifiedand a weakETag, and
FilesanswersIf-None-MatchandIf-Modified-Sincewith 304.
Nothing in any of them allocates.
Fixes
- A dial whose connect failed closed its socket outside the poller; on macOS
that is the close beside kqueue that can hang in the kernel until reboot
(#41). tests/dialreuse.swordlost dials on macOS because it could fill the listen
queue, which there answers with a reset (#53).- Under ThreadSanitizer, a signal could be held in a task that parked and never
came back;gracefulhung about once in three hundred runs (#48). shield test -simon a standard library package warned about the runtime's
own calls.
Sword 0.2.1 — Fold
2026-10-20. A patch to Fold that finishes the way out to C: a library named in the
source and linked either way, a C function as a value, and a library loaded
while the program runs.
What changes for existing programs
- The name of an
externfunction, used as a value, is anextern func,
not afunc. Code that handed one where afuncwas expected no longer
compiles; afunccan only point at Sword code, which is what letsdet
follow a call through one. - An integer narrower than 32 bits reaches C widened. On Apple's arm64 an
externtaking anint8_t, anint16_tor aboolread whatever the upper
bits of the register held. - Every function but
mainis internal to the program. LLVM drops what
nothing calls: on an Apple M4 a hello world went from 145 376 to 119 400 bytes
and the HTTP test from 281 752 to 168 552, and building the latter takes about
half the time. A program that uses part of a package no longer references the
C symbols the rest of it needs.
C
extern "z" func ...names the library a function comes from, and it is
linked without being mentioned on the command line. pkg-config is asked where
it is when the machine has pkg-config.shield --staticlinks each declared library from its archive and refuses
to build without one. The system's own libraries stay dynamic.extern weak func ...— a function the program can run without. Its name
is an?extern func, to be checked before it is called.extern func(T) R— a C function as a value. A Sword function converts to
one, which is howqsortgets a comparison; an address converts to one, which
is how a loader's answer gets a type.std/dlopens a library while the program runs, finds functions in it,
and keeps the system's own account of what went wrong inLoader.Why().--emit-linkprints the rest of the link line, for a build that links by
its own means.
Other machines
--target=<triple>builds for another 64-bit, little-endian machine,
against a runtime built for it withmake TARGET=<triple>.-cstops at
the object file and needs nothing but clang.
Fixes
shield teston a program — a package with amain— failed in clang every
time (#4).- A function can return a function value; the parser read the result type as
the start of the body. - Two copies of the test suite on one machine no longer overwrite each other's
certificates, files and test binaries (#36), andshield testno longer puts
a binary beside the package for the length of a run. - The value of
shield test -preached the shell unquoted. -simwarns about a call through anextern func, as it does about a call to
anexterndeclaration.