Skip to content

Releases

Sword 0.4.2 — Temper

Choose a tag to compare

@github-actions github-actions released this 02 Nov 22:27
7f186d7

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
    [][]i64 are elements of their own, but two rows can be the same slice, so
    rows[i][0] = v in a parallel for, or a piece of rows.chunks(n) handed
    to a mut parameter of a task, counts as writing everything the slice
    reaches. Writing rows[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 a lock, 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=footprints builds 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=bounds is the old
    --audit, and the two combine (#197).

Nothing else that compiled stops compiling.

Sword 0.4.1 — Temper

Choose a tag to compare

@github-actions github-actions released this 02 Nov 17:18
edc5ba8

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 write a and b, and work(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 of head(buf[..], 4) comes from buf; 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 a shared[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[..]) makes bx reach buf (#181).
  • A det function 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 lost det; now every caller does. mem.Arena.Alloc reads its buffer's
    address only to align, and keeps det (#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 not det, each
    with the chain to what took it away, in the form of a diagnostic; -all
    lists the det ones too, and the last line counts them. Given a library, a
    function that loses det only by calling through an interface is listed as
    det when its mem.Allocator is, and counted apart.

Standard library

  • Request.Cookie(name) ?string reads one cookie from the request's Cookie
    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= names steady among the pieces it knows.

Sword 0.4.0 — Temper

Choose a tag to compare

@github-actions github-actions released this 01 Nov 20:21
dbf77e0

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's Alloc and Release and fmt.Sink's writes
    are mut; calling them on an allocator that is not mut stops compiling,
    and the message says what to add (#78).
  • ...nil replaces 0.3.2's else nil: Request{path: "/", ...nil}, and
    T{...nil} for a literal that names no field, which replaces T{nil}. Both
    old spellings are refused with the one to write instead.
  • break in a parallel for is refused. It ended only the piece of the
    range it ran in, so which indices ran depended on where the range was cut.
    continue still 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 << 70 on a u64 gave 216, and
    so did 7 / 0.
  • Two races in a parallel for are 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 xs used 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 no reduce.
  • An else may start the line after its if.

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] in for i in a..xs.len
    over a slice nothing can change, and out[i] in a parallel for: a reducing
    loop with overflow checks 182.6 → 140.7 ms, out[i] = xs[i] *% 2 224.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] or a[k + w] checks only its first and last
    turns.
    A stencil: 44.0 → 28.5 ms. A mut piece of xs.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
    for f64 in cache, 171.6 → 43.0 ms over 128 MB.
  • What a parallel for only reads is copied into each piece, so LLVM
    stops reloading it every iteration: out[i] = xs[i] * k + 1 179 → 54 ms.
  • A short parallel for costs 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 with reduce: 487 → 123 ns on one thread, 44 223 → 15 463 on ten.
  • num.Sqrt is the processor's instruction, with the same bits; a
    negative argument still gives NaN() on every machine. The n-body that
    calls it 67 million times: 11 048 → 98 ms.
  • num.Exact adds 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
    reaches std/tls does 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=small drops 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; -all lists the proven ones too.
  • The language server shows mut on an interface method.
  • The documentation's programs that say what they print are run and held to
    it, and docs/examples is Sword by example: whole programs, explained a
    piece at a time.

Standard library

  • num.Sin and num.Cos of an infinity answer NaN(), 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 a select on two channels; and the
    lock buckets were built with a call_once that raced on Linux.
  • A loop over elements inside a parallel for gave clang bad IR.

Sword 0.3.2 — Quench

Choose a tag to compare

@PedroTessaro PedroTessaro released this 30 Oct 19:22
597450d

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 nil gives every optional field it does not
    name the value nil: Request{path: "/", else nil}. A literal that names no
    field is written T{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

Choose a tag to compare

@PedroTessaro PedroTessaro released this 30 Oct 16:51
2e67d40

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 with chunks, which only a slice has. The note now says atomic[T]
    for an integer or a bool, shared[T] with lock for 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

Choose a tag to compare

@PedroTessaro PedroTessaro released this 24 Oct 22:32
3bf468d

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, where xs was
    lent read-only, used to write through to it, and &x passed an immutable x
    to a mut parameter. 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 := xs with both
    spawned to write, two pointers taken from one variable, a task given
    p := &n while its parent writes n: each compiled and each is a data race.
    The error names the memory and the name it was reached through.
  • ~x is the complement, at the width of x. a ~ b used 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 mut and no ..., and defer takes no declaration;
    all three were accepted and meant nothing.
  • Tests and writers ask for less try. t.Equal, Same, Check, Fail,
    Failf and Logf record and carry on, so one run reports every mismatch;
    try t.Must compares and ends the test. Writes to a json.Writer and to
    an http.Response keep their first error and do nothing after it:
    w.Err() and res.Err() report it, and the server answers 500 for a failed
    response, or closes the connection when the head has gone. A try left in
    front of one of these no longer compiles.
  • Allocator.Release and the Mux's Handle and its shortcuts take what they
    write as mut.
  • 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.mod names a module and what it requires; replace points 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 against sword.sum, which holds a hash of the files
    the compiler reads. A missing line is added; --locked refuses to.
  • shield get path[@version] adds or moves a dependency, @none removes
    it, and a bare path takes the newest stable tag. shield vendor copies
    everything into vendor/ 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 read lets 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 lock takes a guard only when it is free, and runs else when it is
    not. Both work for either kind of lock.
  • if mut x := opt and for mut x := opt bind the unwrapped value as
    writable.
  • extern blocking func declares 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 new starts a module: a program, or a library with --lib, with
    a test and git init.
  • shield doc prints what a package exports, or one type or method, from
    the comments above the declarations.
  • shield --split compiles 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 clean removes them all.

Sword 0.2.4 — Fold

Choose a tag to compare

@PedroTessaro PedroTessaro released this 21 Oct 19:29
f6181c3

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/c names C's types the way a header does — c.Int, c.Long,
    c.SizeT, c.SSizeT, c.Double, c.Bool, c.String for a
    const char *, c.Ptr for a void *, and the rest — so a declaration
    copied from a header reads like the original. Each is an alias, so a c.Int
    is an i32 and passes for one. c.Char is u8, for text; a plain char
    passed by value takes c.SChar or c.UChar, since whether it is signed is
    the platform's choice.
  • os.Errno reads the errno a C function just left, and
    os.ClearErrno zeroes it for strtol and 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
    swordls under Lua's conventions.

Nothing that compiled before stops compiling.

Sword 0.2.3 — Fold

Choose a tag to compare

@PedroTessaro PedroTessaro released this 20 Oct 20:34
066a640

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.FromC reads a C string up to its zero, and strings.FromCMax
    also stops at a limit, for a char[N] field the text fills. Both answer with
    a view of the C memory and copy nothing.
  • strings.ToC writes a string and its zero into a buffer of the caller's
    for a C function that wants a const 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

Choose a tag to compare

@PedroTessaro PedroTessaro released this 20 Oct 19:05
03be0a8

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 = T gives a type a second name. A Name is a T — 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_SEED the 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, ParseRfc3339 and ParseHttpDate,
    which reads all three forms RFC 9110 requires a recipient to accept.
  • std/http: static files carry Last-Modified and a weak ETag, and
    Files answers If-None-Match and If-Modified-Since with 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.sword lost 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; graceful hung about once in three hundred runs (#48).
  • shield test -sim on a standard library package warned about the runtime's
    own calls.

Sword 0.2.1 — Fold

Choose a tag to compare

@PedroTessaro PedroTessaro released this 20 Oct 03:14
7308293

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 extern function, used as a value, is an extern func,
    not a func. Code that handed one where a func was expected no longer
    compiles; a func can only point at Sword code, which is what lets det
    follow a call through one.
  • An integer narrower than 32 bits reaches C widened. On Apple's arm64 an
    extern taking an int8_t, an int16_t or a bool read whatever the upper
    bits of the register held.
  • Every function but main is 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 --static links 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 how qsort gets a comparison; an address converts to one, which
    is how a loader's answer gets a type.
  • std/dl opens a library while the program runs, finds functions in it,
    and keeps the system's own account of what went wrong in Loader.Why().
  • --emit-link prints 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 with make TARGET=<triple>. -c stops at
    the object file and needs nothing but clang.

Fixes

  • shield test on a program — a package with a main — 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), and shield test no longer puts
    a binary beside the package for the length of a run.
  • The value of shield test -p reached the shell unquoted.
  • -sim warns about a call through an extern func, as it does about a call to
    an extern declaration.