0.5.0 Release Notes

Download & Documentation

Zig is a general-purpose programming language designed for robustness, optimality, and maintainability. Zig is aggressively pursuing its goal of overthrowing C as the de facto language for system programming. Zig intends to be so practical that people find themselves using it, because it "just works".

This release features 6 months of work and changes from 67 different contributors, spread among 1541 commits.

Special thanks to my sponsors who provide financial support. You're making Zig sustainable.

Table of Contents §

LLVM 9 §

This release of Zig upgrades to LLVM 9. Zig operates in lockstep with LLVM; Zig 0.5.0 is not compatible with LLVM 8.

Notably this means that Zig now has RISC-V Support.

Zig also gains emscripten as a target OS. emscripten cannot self-host yet, but when it can, it will be interesting to explore this as an option for a Zig-in-the-browser sandbox, using WebAssembly.

Support Table §

A support table for master branch can be found on the home page. Here the support table for 0.5.0 is reproduced:

free standing Linux 3.16+ macOS 10.13+ Windows 7+ FreeBSD 12.0+ NetBSD 8.0+ UEFI WASI Android
x86_64 Tier 2 Tier 1 Tier 1 Tier 1 Tier 2 Tier 2 Tier 2 N/A Tier 2
wasm32 Tier 2 N/A N/A N/A N/A N/A N/A Tier 2 N/A
arm64 Tier 2 Tier 2 N/A Tier 3 Tier 3 Tier 3 Tier 3 N/A Tier 2
arm32 Tier 2 Tier 2 N/A Tier 3 Tier 3 Tier 3 Tier 3 N/A Tier 2
mips32 LE Tier 2 Tier 2 N/A N/A Tier 3 Tier 3 N/A N/A N/A
i386 Tier 2 Tier 3 Tier 4 Tier 3 Tier 3 Tier 3 Tier 3 N/A Tier 2
bpf Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A N/A
hexagon Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A N/A
mips32 BE Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A N/A
mips64 Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A N/A
amdgcn Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A N/A
sparc Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A N/A
s390x Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A N/A
lanai Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A N/A N/A
powerpc32 Tier 3 Tier 3 Tier 4 N/A Tier 3 Tier 3 N/A N/A N/A
powerpc64 Tier 3 Tier 3 Tier 4 N/A Tier 3 Tier 3 N/A N/A N/A
wasm64 Tier 4 N/A N/A N/A N/A N/A N/A Tier 4 N/A
avr Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
riscv32 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 Tier 4 N/A N/A
riscv64 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 Tier 4 N/A N/A
xcore Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
nvptx Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
msp430 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
r600 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
arc Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
tce Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
le Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
amdil Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
hsail Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
spir Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
kalimba Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
shave Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A
renderscript Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A N/A N/A

Tier System §

Tier 1 Support §

Tier 2 Support §

Tier 3 Support §

Tier 4 Support §

RISC-V Support §

This release updates to LLVM 9, musl 1.1.23 with patches, and glibc 2.30. This plus updates to the Standard Library means that Zig's (64-bit) RISC-V support has gone from Tier 4 Support to Tier 3 Support in this release.

RISC-V is a very flexible target, with features such as atomics, and even integer multiplication being optional. Since Zig does not yet have ability to specify target CPU features, the default set of cross-compilation features are "+a,+c,+d,+f,+m,+relax", which matches Clang.

The RISC-V target does not yet pass Zig's test suite:

The next step for all these issues is to create test case reductions and then file upstream bug reports.

However, I did work with Rich Felker to get musl building with Clang for the RISC-V target, which means that we can do this:


#include <stdio.h>

int main(int argc, char **argv) {
    printf("Hello world\n");
    return 0;
$ zig build-exe --c-source hello.c -lc -target riscv64-linux-musl
$ qemu-riscv64 ./hello
Hello world

Zig 0.5.0 carries a few patches to musl which makes this work. A new musl release is expected soon which contains these patches.

To be clear - the above simple example also works in Zig - it's just that all the language features such as f16 are not working, and there is no automated Test Coverage for this target.


const std = @import("std");

pub fn main() void {
    std.debug.warn("Hello from zig\n");
$ zig build-exe hello.zig -target riscv64-linux
$ qemu-riscv64 ./hello
Hello from zig

Although glibc 2.30 gained RISC-V support, Zig is not able to build glibc for this target yet. See #3340 for more details. Looks like it could be as simple as importing a couple more .h files from the glibc source tree.

64-bit ARM Support §

LemonBoy worked on Standard Library support for aarch64 during this release cycle:

After this work, and improvements made to Test Coverage, the following targets are now covered by the Zig test suite:

This test coverage led to the following bug fixes:

However, due to LLVM miscompiling trivial switch for AArch64, some failing tests are disabled, which means 64-bit ARM remains a Tier 2 Support target. The good news is we filed an LLVM bug report, and it has already been solved in LLVM trunk, scheduled to be included in LLVM 9.0.1.

After that, the only remaining issues standing in the way of Tier 1 Support for ARM 64-bit (aarch64) Linux are:


const builtin = @import("builtin");
const std = @import("std");
const assert = std.debug.assert;

test "cross compiled unit test" {
    assert(builtin.arch == .aarch64);
$ zig test arm64-test.zig -target aarch64v8-linux --test-cmd qemu-aarch64 --test-cmd-bin
1/1 test "cross compiled unit test"...OK
All tests passed.

32-bit ARM Support §

Thanks to compiler-rt improvements by vegecode and LemonBoy, Zig's 32-bit ARM support is much stronger in version 0.5.0.

Alongside these efforts, LemonBoy improved the Standard Library by making I/O offsets and sizes u64 instead of usize, decoupling the concepts of address-space size and file size. This solved many compile errors when trying to target 32-bit ARM, as well as any other 32-bit architecture. #637

He also made several improvements to Thread Local Storage for 32-bit ARM.

Robin Voetter joined the Zig community during this release cycle, and hammered away at the Standard Library:

After all these improvements, 32-bit ARM support is leveled-up to Tier 2 Support. Along with improvements made to Test Coverage, the following targets are now covered by the Zig test suite:


const builtin = @import("builtin");
const std = @import("std");
const assert = std.debug.assert;

test "cross compiled unit test" {
    assert(builtin.arch == .arm);
$ zig test arm32-test.zig -target armv8-linux --test-cmd qemu-arm --test-cmd-bin
1/1 test "cross compiled unit test"...OK
All tests passed.

After updating to musl 1.1.23, Zig's clone on arm32 is updated to latest musl implementation.

Remaining issues to solve in order to achieve Tier 1 Support for ARM 32-bit Linux:

MSYS2 Support §

Although Zig does not officially support MSYS2 as a host target, emekoi has dutifully maintained unofficial support. Thanks to emekoi's efforts, one can build and run the stage1 C++ compiler of Zig in an MSYS2 environment.

Note: sometimes "MinGW" is used as a shorthand to mean "MSYS2". However, it is not to be confused with mingw-w64, or with the unrelated project, MinGW. MinGW-w64 is a fork of MinGW, which adds support for more architectures (such as 64-bit Windows) and more system APIs. When someone says "MinGW", it's almost certain they either mean "MSYS2" (which is based on MinGW-w64) or "MinGW-w64" instead.

Here is the list of things emekoi did to maintain unofficial support for MSYS2:

FreeBSD Support §

stratact made the following changes:

This combined with disabling some of the failing standard library tests for FreeBSD, stratact was able to enable more Test Coverage for FreeBSD. Now the Continuous Integration server runs 7 additional kinds of tests from the test suite, instead of only the behavior tests.

stratact reports all tests passing locally, however, we have run into memory limits of SourceHut, which is the service used to run FreeBSD tests. Drew DeVault has understandably denied our request for more RAM, and so we are left with disabled test coverage until Zig can finish self-hosting, or improve the memory usage of the C++ stage1 compiler.

The set of remaining issues until Tier 1 FreeBSD Support for x86_64 is now:

WebAssembly Support §

During the 0.5.0 release cycle, Shritesh Bhattarai joined the Zig community and made significant contributions to Zig's WebAssembly and WASI (Web Assembly System Interface) support.

He got compiler-rt working and tweaked the target settings such as:

Thanks to this as well as Shritesh adding basic standard library support for the WASI target, as well as improving the linker settings that Zig uses, WebAssembly and WASI are now Tier 2 Support targets!

As a demo, Shritesh created zigfmt-web, which is a web page that will run zig fmt on a block of code, using the same implementation as official zig fmt.

Shritesh created a basic allocator intended to be used on the WebAssembly target, std.heap.wasm_allocator. This uses the WebAssembly intrinsics to request memory from the host, and is not capable of freeing memory. The standard library does not yet have an allocator for WebAssembly that can reclaim freed memory. See zee_alloc for a community project attempting to solve this use case.

Shritesh also created a demo of Zig interacting with the DOM via JS (source).

Other miscellaneous improvements to WebAssembly:

Zig now provides a Freestanding libc, which is available when linking libc for the WebAssembly target. It is not yet fully complete, but you can get a sense of the use case for it with this demo project: lua-in-the-browser

This use case led to several improvements to Zig's WebAssembly support:

Zig is particularly well suited to creating reasonably small & fast WebAssembly binaries. Here are some demos of WebAssembly projects from Zig community members:

UEFI Support §

Nick Erdmann has been reading the UEFI specification and improving Zig support for this target.

Zig's Standard Library now integrates more cleanly with UEFI, and other things now "just work" such as PDB files and 0x0 addresses.

Many of the UEFI protocol definitions are now available in std.os.uefi.protocols.

Nick has clean and well-organized demo projects which serve as resources to help others learn how to do UEFI programming:

iOS Support §

Matthew Iannucci added initial support for iOS targets (#2237).

However iOS remains a Tier 3 Support target. There are no known active Zig projects targeting iOS.

MIPS Support §

LemonBoy implemented Thread Local Storage for architectures that have thread pointer offsets, such as mipsel. He updated the Standard Library with the Linux system bits for the mipsel architecture, and worked with musl upstream to get it patched enough to be able to successfully build with Clang for this target. Zig carries this patch in 0.5.0.

After these changes, MIPS now has Tier 2 Support! LemonBoy reports running a Zig binary on his router:

18:59 <TheLemonMan> just got a Zig binary running on my mips32 router, yay

These targets are now covered by the Zig test suite:

Aside from investigating mipsel-linux-gnu, the only remaining issues standing in the way of Tier 1 Support for MIPS Little-Endian Linux (mipsel-linux) are:


const builtin = @import("builtin");
const std = @import("std");
const assert = std.debug.assert;

test "cross compiled unit test" {
    assert(builtin.arch == .mipsel);
$ zig test mips-test.zig -target mipsel-linux --test-cmd qemu-mipsel --test-cmd-bin
1/1 test "cross compiled unit test"...OK
All tests passed.

Android Support §

meme joined the Zig community and contributed improvements to the target aarch64-linux-android. Thanks to their efforts, Zig now has Tier 2 Support for Android.

Here's an example of building an Android executable with Zig:


const std = @import("std");

pub fn main() void {
    std.debug.warn("Hello, Android!");
$ zig build-exe hello_android.zig -target aarch64-linux-android
$ file ./hello_android
./hello_android: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, with debug_info, not stripped

In this example, there is no libc dependency. However, Zig does know how to integrate with Android's libc. The first step is to create a libc text file describing where various paths are. One can obtain a template for this file by executing zig libc. In this example, I've taken the template and populated it based on the path to the Android NDK in my downloads folder:


# The directory that contains `stdlib.h`.
# On POSIX-like systems, include directories be found with: `cc -E -Wp,-v -xc /dev/null`

# The system-specific include directory. May be the same as `include_dir`.
# On Windows it's the directory that includes `vcruntime.h`.
# On POSIX it's the directory that includes `sys/errno.h`.

# The directory that contains `crt1.o` or `crt2.o`.
# On POSIX, can be found with `cc -print-file-name=crt1.o`.
# Not needed when targeting MacOS.

# The directory that contains `crtbegin.o`.
# On POSIX, can be found with `cc -print-file-name=crtbegin.o`.
# Not needed when targeting MacOS.

# The directory that contains `vcruntime.lib`.
# Only needed when targeting MSVC on Windows.

# The directory that contains `kernel32.lib`.
# Only needed when targeting MSVC on Windows.


const std = @import("std");

extern fn printf(msg: [*]const u8, ...) c_int;

pub fn main() void {
    _ = printf(c"hello android libc\n");
$ zig build-exe hello_libc.zig -target aarch64-linux-android -lc --libc android_libc.txt
$ file ./hello_libc
./hello_libc: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, with debug_info, not stripped

Language Changes §

usingnamespace §

usingnamespace is a top level declaration that imports all the public declarations of the operand, which must be a struct, union, or enum, into the current scope:


usingnamespace @import("std");

test "using std namespace" {
$ zig test usingnamespace.zig
1/1 test "using std namespace"...OK
All tests passed.

Instead of the above pattern, it is generally recommended to explicitly alias individual declarations. However, usingnamespace has an important use case when organizing the public API of a file or package. For example, one might have c.zig with all of the C imports:

pub usingnamespace @cImport({
    @cDefine("STBI_ONLY_PNG", "");
    @cDefine("STBI_NO_STDIO", "");

The above example demonstrates using pub to qualify the usingnamespace additionally makes the imported declarations pub. This can be used to forward declarations, giving precise control over what declarations a given file exposes.

In Zig 0.4.0, this feature existed as use, but it only worked at the top-level scope, and only for structs. The feature was also considered unstable.

Thank you LemonBoy for fixing usingnamespace outside the top-level scope, and making it work with arbitrary structs.

In Zig 0.5.0, both use and usingnamespace are accepted, and zig fmt automatically converts to the canonical syntax. The next release of Zig after this one will remove the old syntax.

This feature is now stable and planned to be included in the language specification.

External Thread Local Variables §

Zig now always respects threadlocal for variables with external linkage.

Previously, if you had, for example:

extern "c" threadlocal var errno: c_int;

This would turn errno into a normal variable for --single-threaded builds. However for variables with external linkage, there is an ABI to uphold.

This is needed to make errno work for DragonFly BSD. See #2381.

@hasField and @hasDecl §

@hasField(comptime Container: type, comptime name: []const u8) bool
@hasDecl(comptime Container: type, comptime name: []const u8) bool

The new builtin function @hasField returns whether the field name of a struct, union, or enum exists. The result is a compile time boolean. It does not include functions, variables, or constants.

The new builtin function @hasDecl returns whether or not a struct, enum, or union has a declaration.


const std = @import("std");
const assert = std.debug.assert;

const Foo = struct {
    nope: i32,

    pub var blah = "xxx";
    const hi = 1;

test "@hasDecl and @hasField" {
    assert(@hasDecl(Foo, "blah"));

    // Even though `hi` is private, @hasDecl returns true because this test is
    // in the same file scope as Foo. It would return false if Foo was declared
    // in a different file.
    assert(@hasDecl(Foo, "hi"));

    // @hasDecl is for declarations; not fields.
    assert(!@hasDecl(Foo, "nope"));
    assert(!@hasDecl(Foo, "nope1234"));

    assert(@hasField(Foo, "nope"));
$ zig test has_builtins.zig
1/1 test "@hasDecl and @hasField"...OK
All tests passed.

Thanks to Shawn Landden for initial implementation and documentation of @hasField.

C Pointers Support Optional Syntax §

When translating C code, Zig does not know whether pointer types should be translated to * or [*] pointers. Instead, they are translated to C Pointers.

As the documentation notes, this type is to be avoided whenever possible. The only valid reason for using a C pointer is in auto-generated code from translating C code.

Interfacing with C pointer types happens due to direct interop with translated .h files. It's always a future possibility to rewrite the .h file in .zig to gain better type-safety. Previously, optional syntax, such as if, orelse, null, and .? did not work for C pointers. This would cause compile errors if the type signature of the external function prototypes were improved to have the real pointer types rather than C pointers.

Now, this syntax works, and so there is no penalty for starting out with auto-translated headers, and then later "upgrading" to better typed bindings.


const getenv = @cImport(@cInclude("stdlib.h")).getenv;

// note: this is just a demo of C pointers with optional syntax.
// std.process has better API for getenv.

test "C pointers with optional syntax" {
    const ptr1 = getenv(c"HOME").?; // don't do this 💥
    const ptr2 = getenv(c"HOME") orelse return error.Homeless; // OK

    if (getenv(c"HOME")) |ptr3| {
        // also OK

    const ptr4 = getenv(c"HOME");
    if (ptr4 == null) {
        // also works
$ zig test getenv.zig -lc
1/1 test "C pointers with optional syntax"...OK
All tests passed.

The auto-translated getenv prototype looks like this:

pub extern fn getenv(__name: [*c]const u8) [*c]u8;

If we were to improve this prototype with correct pointer types, the test will still pass:


pub extern fn getenv(name: [*]const u8) ?[*]u8;

// note: this is just a demo of C pointers with optional syntax.
// std.process has better API for getenv.

test "C pointers with optional syntax" {
    const ptr1 = getenv(c"HOME").?; // don't do this 💥
    const ptr2 = getenv(c"HOME") orelse return error.Homeless; // OK

    if (getenv(c"HOME")) |ptr3| {
        // also OK

    const ptr4 = getenv(c"HOME");
    if (ptr4 == null) {
        // also works
$ zig test getenv2.zig -lc
1/1 test "C pointers with optional syntax"...OK
All tests passed.

Switching on Error Sets §

Using switch on an error set now provides a way to capture an error value with a subset type:


const std = @import("std");
const os = std.os;

const Error = error{

pub fn main() Error!void {
    // open a normal, blocking file descriptor.
    const fd = try os.open("/dev/urandom", os.O_RDONLY, 0);
    defer os.close(fd);

    // we did *not* use O_NONBLOCK, so the OS will not give us
    var buf: [100]u8 = undefined;
    const nbytes = try readBlocking(fd, &buf);

/// Asserts that fd was opened in a blocking fashion.
fn readBlocking(fd: os.fd_t, buffer: []u8) !usize {
    return std.os.read(fd, buffer) catch |err| switch (err) {
        error.WouldBlock => unreachable, // Remove this to observe compile error
        else => |e| return e,
$ zig build-exe switch_err_set_1.zig
$ ./switch_err_set_1

Here you can see that the program compiled just fine, even though error.WouldBlock is not found in the error set. This is because the function readBlocking switched on the error set, and handled the error.WouldBlock case. This means the error set type of value captured by the else does not include the value error.WouldBlock.

In addition to this, Zig allows capturing the payload from multiple error set values:


const std = @import("std");
const os = std.os;

const Error = error{

pub fn main() Error!void {
    // open a normal, blocking file descriptor.
    const fd = try os.open("/dev/urandom", os.O_RDONLY, 0);
    defer os.close(fd);

    // we did *not* use O_NONBLOCK, so the OS will not give us
    var buf: [100]u8 = undefined;
    const nbytes = try readBlocking(fd, &buf);

/// Asserts that fd was opened in a blocking fashion.
fn readBlocking(fd: os.fd_t, buffer: []u8) !usize {
    return std.os.read(fd, buffer) catch |err| switch (err) {
        error.WouldBlock, error.InputOutput => |e| {
            std.debug.panic("unexpected: {}\n", e);
        else => |e| return e,
$ zig build-exe switch_err_set_2.zig
$ ./switch_err_set_2

In this example, error.InputOutput was lifted out of Error since it is handled inside readBlocking. The e capture value has type error{WouldBlock,InputOutput}.

Bit Manipulation Builtin Functions §

Shawn Landden improved the consistency of the names and parameters of bit manipulation intrinsics.

@bitReverse(comptime T: type, integer: T) T
@byteSwap(comptime T: type, operand: T) T
@clz(comptime T: type, integer: T)
@ctz(comptime T: type, integer: T)
@popCount(comptime T: type, integer: T)

#2119 #2120

Undeclared Identifiers in Compile-Time Dead Branches §

Zig no longer validates whether identifiers exist in dead comptime branches:


test "dead comptime branch" {
    if (false) {
        does_not_exist = thisFunctionAlsoDoesNotExist();
$ zig test dead_comptime_branch.zig
1/1 test "dead comptime branch"...OK
All tests passed.

This is counter-intuitive, but consider that the set of available identifiers may depend on comptime parameters, such as the target OS:

const builtin = @import("builtin");

usingnamespace switch (builtin.os) {
    .windows => @import("windows_stuff.zig"),
    else => @import("posix_stuff.zig"),

test "example" {
    if (builtin.os == .windows) {
    } else {

In practice, this has resulted in various code cleanups throughout the standard library.

Zig's lazy analysis, while convenient, surfaces the inherent problems of conditional compilation. See the related proposal: "multibuilds" - a plan to harmonize conditional compilation with compile errors, documentation, and IDEs

Default Struct Field Values §

Each struct field may now have an expression indicating the default field value. Such expressions are executed at comptime, and allow the field to be omitted in a struct literal expression:


const Foo = struct {
    a: i32 = 1234,
    b: i32,

test "default struct field values" {
    const x = Foo{
        .b = 5,
    if (x.a + x.b != 1239) {
        @compileError("it's even comptime known!");
$ zig test default_fields.zig
1/1 test "default struct field values"...OK
All tests passed.

Array Literal Syntax §

The array literal syntax has changed, when inferring the size.

Old syntax:

[]i32{1, 2, 3}

New syntax:

[_]i32{1, 2, 3}

The previous syntax used to look too much like instantiating a slice. This caused all kinds of confusion. Now it's pretty clear that the type is an array.


@import("root") §

The Root Source File (in the case of build-exe, the file with pub fn main) is now available to import anywhere, using @import("root"). Combined with @hasDecl, this allows library code to support global configuration settings based on declarations in the root source file.

The Standard Library takes advantage of this for several use cases. One example is the Default Segfault Handler. It works like this (from std.debug):

const root = @import("root");

/// Whether or not the current target can print useful debug information when a segfault occurs.
pub const have_segfault_handling_support = builtin.os == .windows or
    (builtin.arch == builtin.Arch.x86_64 and builtin.os == .linux);

pub const enable_segfault_handler: bool = if (@hasDecl(root, "enable_segfault_handler"))
    runtime_safety and have_segfault_handling_support;

pub fn maybeEnableSegfaultHandler() void {
    if (enable_segfault_handler) {

And then Zig's startup code calls std.debug.maybeEnableSegfaultHandler() just before calling main().

Another place this is used in the standard library is to decide a global "I/O mode", which is related to Async Functions.

This feature has the capability to be abused, and should be used with care. Any root source file declarations that can affect a library's behavior should be well-documented. When Zig gains documentation generation, the auto-generated docs will have the capability to enumerate all the places that depend on a root source file declaration.

Thank you emekoi for the initial implementation of this.

@mulAdd §

Zig now has @mulAdd, otherwise known as "fused-multiply-add".

@mulAdd(comptime T: type, a: T, b: T, c: T) T

Performs (a * b) + c, except only rounds once, and is thus more accurate. Additionally, some targets have a hardware instruction for this, making it potentially faster than a userland implementation.


const std = @import("std");

test "@mulAdd" {
    // In this example we use numbers small enough to avoid rounding errors that would occur
    // without @mulAdd.
    std.testing.expect(@mulAdd(f32, 2.0, 3.0, 4.0) == (2.0 * 3.0) + 4.0);
$ zig test mul_add.zig
1/1 test "@mulAdd"...OK
All tests passed.

Currently this instruction only works for floating point types, as well as vectors of floating point types. However, there is an open proposal to make this work for any types that support * and + operators, such as integers. The proposal also suggests to remove the explicit type parameter requirement.

Thank you Shawn Landden for the initial implementation of this.

Builtins for Math Functions §

Shawn Landden added:

@sin(comptime T: type, value: T) T
@cos(comptime T: type, value: T) T
@exp(comptime T: type, value: T) T
@exp2(comptime T: type, value: T) T
@ln(comptime T: type, value: T) T
@log2(comptime T: type, value: T) T
@log10(comptime T: type, value: T) T
@fabs(comptime T: type, value: T) T
@floor(comptime T: type, value: T) T
@ceil(comptime T: type, value: T) T
@trunc(comptime T: type, value: T) T
@round(comptime T: type, value: T) T

These are builtin functions because some architectures have hardware instructions for these. Furthermore, because these functions are well-defined, the optimizer may sometimes be able to convert calls to these builtins into better forms.

It's planned for Zig to provide libmvec in the future, and these functions will become SIMD-capable.

Result Location Semantics §

What I'm calling Result Location Semantics was a large branch of Zig that fundamentally changed the way that expressions are semantically analyzed. This was the third attempt, which finally succeeded. I abandoned the first attempt after 1 week. The second attempt, which took place during the 0.4.0 release cycle, lasted 2 months, but again was regretfully abandoned. However, there were significant parts of the second attempt that landed in the eventual implementation.

During my work on this branch, the Zig community stepped up and continued to improve master branch all the while. You can observe this by seeing how many names are mentioned in these release notes. I am proud and grateful of the Zig community for this.

Although the implementation was difficult, the user-facing differences of Result Location Semantics are nearly impossible to detect. The main purpose was to pave the way for the redesign of Async Functions.

The main thing that this change does is semantically guarantee that no copying happens in expressions. As an example:


const std = @import("std");

const Object = struct {
    tag: i32,
    pt: [2]Point,

const Point = struct {
    x: i32,
    y: i32,

test "result location semantics" {
    const result = if (condition()) foo(10) else bar();
    std.testing.expect(result.tag == 10);
    std.testing.expect(result.pt[0].x == 69);
    std.testing.expect(result.pt[1].y == 420);

fn condition() bool {
    return true;

fn foo(arg: i32) Object {
    return baz(arg);

fn bar() Object {
    return Object{
        .tag = 1,
        .pt = undefined,

fn baz(arg: i32) Object {
    return Object{
        .tag = arg,
        .pt = [_]Point{ nice(), blazet() },

fn nice() Point {
    return Point{
        .x = 69,
        .y = 69,

fn blazet() Point {
    return Point{
        .x = 420,
        .y = 420,
$ zig test result_loc.zig
1/1 test "result location semantics"...OK
All tests passed.

The important thing to note here is that the functions nice() and blazet() write directly to result in the main test function. There are no intermediate copies, and this is semantically guaranteed by the language.

With Zig's current semantics, it is actually not possible to observe the difference between 0.4.0 and 0.5.0 (except with async function calls). However, with future proposals to the language it would matter a great deal:

The point here is that initialization functions would be able to set up pointer references relative to the return value, and have the value be guaranteed to be valid.

Before moving on to the next section I want to say a huge thank you to Michael Dusan. This branch was a dizzying amount of effort, and towards the end of it, Michael started contributing. He created test case reductions and even solved some of the regressions, such as vector to array conversion not being aligned correctly. This was both unexpected and helpful. It made a serious difference in getting me through to the end of the branch, so that we could merge it into master.

The other thing that came out of this branch was preferring the "result type" to Peer Type Resolution:


const std = @import("std");
const expect = std.testing.expect;

test "result location type resolution" {
      var f = false;
      var x: i32 = 0;
      x = if (f) 1 else 2;
      expect(x == 2);
$ zig test result_loc_peer.zig
1/1 test "result location type resolution"...OK
All tests passed.


@unionInit §

@unionInit(comptime Union: type, comptime active_field_name: []const u8, init_expr) Union

This is the same thing as union initialization syntax, except that the field name is a comptime-known value rather than an identifier token.

@unionInit forwards its result location to init_expr.

Thank you Robert Scott for the initial implementation of @unionInit.

Unicode Escapes §

Zig's unicode escape syntax is changed to match most other popular programming languages.

Old syntax:


New syntax:


This matches JavaScript (since ES6), Lua (since 5.3), Swift (who swapped from our previous syntax!), and Rust.

Thank you daurnimator for doing the research on other languages, and Shawn Landden for the implementation.

It is planned to additionally allow unicode escapes in character literals, since character literals have type comptime_int. That accepted proposal is marked "Contributor Friendly" because it is limited in scope and/or knowledge of Zig internals.

Async Functions §

async functions have been completely reworked in Zig 0.5.0. Previously, I was calling these "stackless coroutines". However I'm now avoiding the word "coroutine" since it means different things to different people, and instead using the phrase "async functions".

In Zig 0.4.0, all async functions were generic across an allocator type, all async functions took an allocator parameter, and calling an async function could fail. Additionally, async functions were required to be annotated as such.

In Zig 0.5.0, calling an async function can no longer fail. The async function frame is provided by the caller via Result Location Semantics, and can be in the caller's stack frame. Async functions are no longer generic, and do not require the async keyword. Zig infers that a function is async when it observes that the function contains a suspension point. Async functions can be called the same as normal functions. A function call of an async function is a suspend point.

When a regular function is called, a frame is pushed to the stack, the function runs until it reaches a return statement, and then the frame is popped from the stack. At the callsite, the following code does not run until the function returns.

An async function is a function whose callsite is split into an async initiation, followed by an await completion. Its frame is provided explicitly by the caller, and it can be suspended and resumed any number of times.

Here's a simple example of an async function:


const std = @import("std");

var frame: anyframe = undefined;

pub fn main() void {
    std.debug.warn("begin main\n");
    _ = async func();
    std.debug.warn("resume func\n");
    resume frame;
    std.debug.warn("end main\n");

fn func() void {
    std.debug.warn("begin func\n");
    frame = @frame();
    std.debug.warn("end func\n");
$ zig build-exe async_fn.zig
$ ./async_fn
begin main
begin func
resume func
end func
end main

Here we have a seam between non-async (main) and async (func) code. A more typical usage of this feature:


const std = @import("std");
const expect = std.testing.expect;

// Try toggling these
const simulate_fail_download = false;
const simulate_fail_file = false;
const suspend_download = true;
const suspend_file = true;

pub fn main() void {
    _ = async amainWrap();
    // This simulates an event loop
    if (suspend_file) {
        resume global_file_frame;
    if (suspend_download) {
        resume global_download_frame;
fn amainWrap() void {
    if (amain()) |_| {
    } else |e| switch (e) {
        error.NoResponse => expect(simulate_fail_download),
        error.FileNotFound => expect(simulate_fail_file),
        else => @panic("test failure"),

fn amain() !void {
    const allocator = std.heap.direct_allocator;
    var download_frame = async fetchUrl(allocator, "https://example.com/");
    var download_awaited = false;
    errdefer if (!download_awaited) {
        if (await download_frame) |x| allocator.free(x) else |_| {}

    var file_frame = async readFile(allocator, "something.txt");
    var file_awaited = false;
    errdefer if (!file_awaited) {
        if (await file_frame) |x| allocator.free(x) else |_| {}

    download_awaited = true;
    const download_text = try await download_frame;
    defer allocator.free(download_text);

    file_awaited = true;
    const file_text = try await file_frame;
    defer allocator.free(file_text);

    expect(std.mem.eql(u8, "expected download text", download_text));
    expect(std.mem.eql(u8, "expected file text", file_text));

var global_download_frame: anyframe = undefined;
fn fetchUrl(allocator: *std.mem.Allocator, url: []const u8) anyerror![]u8 {
    const result = try std.mem.dupe(allocator, u8, "expected download text");
    errdefer allocator.free(result);
    if (suspend_download) {
        suspend {
            global_download_frame = @frame();
    if (simulate_fail_download) return error.NoResponse;
    std.debug.warn("fetchUrl returning\n");
    return result;

var global_file_frame: anyframe = undefined;
fn readFile(allocator: *std.mem.Allocator, filename: []const u8) anyerror![]u8 {
    const result = try std.mem.dupe(allocator, u8, "expected file text");
    errdefer allocator.free(result);
    if (suspend_file) {
        suspend {
            global_file_frame = @frame();
    if (simulate_fail_file) return error.FileNotFound;
    std.debug.warn("readFile returning\n");
    return result;
$ zig build-exe typical_async_await.zig
$ ./typical_async_await
readFile returning
fetchUrl returning

The important thing to note here is that the async/await mechanism did not bring in a dependency on the host OS, and it did not bring in a dependency on an allocator.

Now watch what happens when we do this:

const suspend_download = false;
const suspend_file = false;
$ zig build-exe typical_async_await.zig
$ ./typical_async_await
fetchUrl returning
readFile returning

It's the same output, except in reversed order. With these modifications, there are no async functions in the entire program! The expression async fetchUrl(allocator, "https://example.com/") is evaluated as a normal, blocking function, as is async readFile(allocator, "something.txt"). The awaits are no-ops.

The point here is that the amain function, which is the demo of typical async/await usage, works in both an async context and blocking context. The programmer was able to express the inherent parallelism of the logic, without resorting to function coloring.

There is admittedly a bit of boilerplate in the example. Here's the tracking issue for that.

Now for the related Standard Library updates:

This introduces the concept of "IO mode" which is configurable by the Root Source File (e.g. next to pub fn main). Applications can put this in their root source file:

pub const io_mode = .evented;

This will populate std.io.mode to be std.io.Mode.evented. When I/O mode is evented, std.os.read handles EAGAIN by suspending until the file descriptor becomes available for reading. Although the std lib event loop supports epoll, kqueue, and Windows I/O Completion Ports, this integration with std.os.read currently only works on Linux.

This integration is currently only hooked up to std.os.read, and not, for example, std.os.write, child processes, and timers. The fact that we can do this and still have a working master branch is thanks to Zig's lazy analysis, comptime, and inferred async. We can continue to make incremental progress on async std lib features, enabling more and more test cases and coverage.

In addition to std.io.mode there is std.io.is_async which is equal to std.io.mode == .evented. In case I/O mode is async, std.io.InStream notices this and the read function pointer becomes an async function pointer rather than a blocking function pointer. Even in this case, std.io.InStream can still be used as a blocking input stream. Users of the API control whether it is blocking or async at runtime by whether or not the read function suspends. In case of file descriptors, for example, this might correspond to whether it was opened with O_NONBLOCK. The noasync keyword makes a function call or await assert that no suspension happens. This assertion has runtime safety enabled.

std.io.InStream, in the case of async I/O, uses by default a 1 MiB frame size for calling the read function. If this is too large or too small, the application can globally increase the frame size used by declaring pub const stack_size_std_io_InStream = 1234; in their root source file. This way, std.io.InStream will only be generated once, avoiding bloat, and as long as this number is configured to be high enough, everything works fine. Zig has runtime safety to detect when @asyncCall is given too small of a buffer for the frame size.

This merge introduces -fstack-report which can help identify large async function frame sizes and explain what is making them so big.

-fstack-report outputs JSON format, which can then be viewed in a GUI that represents the tree structure. As an example, Firefox does a decent job of this.

One feature that is currently missing is detecting that the call stack upper bound is greater than the default for a given target, and passing this upper bound to the linker. As an example, if Zig detects that 20 MiB stack upper bound is needed - which would be quite reasonable - currently on Linux the application would only be given the default of 16 MiB.

There is so much to go over with this feature, and these release notes are already ridiculously long. I'm going to have to resort to listing out some things here, and rely on a future post to elaborate on these features.

@frameSize() usize
@frame() *@Frame(func)
@Frame(func: var) type

It is confirmed that async functions will solve safe recursion in Zig.


Zig's SIMD support in 0.5.0 is still far from complete, but significant progress has been made.

Shawn Landden has a branch of Zig with SIMD fairly complete, and has been maintaining this patchset, as I slowly upstream the commits one-by-one (with adjustments, fixups, etc). Shawn is giving a talk on his work at the October LLVM Dev Meeting: Using LLVM's portable SIMD with Zig

See #903 for more details.

Alignment of Struct Fields §

In Zig 0.4.0 there was this ugly kludge in the C++ stage1 compiler:

// TODO If we have no type_entry for the field, we've already failed to
// compile the program correctly. This stage1 compiler needs a deeper
// reworking to make this correct, or we can ignore the problem
// and make sure it is fixed in stage2. This workaround is for when
// there is a false positive of a dependency loop, of alignment depending
// on itself. When this false positive happens we assume a pointer-aligned
// field, which is usually fine but could be incorrectly over-aligned or
// even under-aligned. See https://github.com/ziglang/zig/issues/1512
} else if (field->type_entry == nullptr) {
    this_field_align = g->builtin_types.entry_usize->abi_align;

It was a mistake to ever let this kludge into the C++ stage1 compiler, and it was difficult to remove this kludge in 0.5.0. But it's gone now.

In Zig 0.5.0, the C++ stage1 compiler has the concept of "Lazy Values". This solved the problem of false positive dependencies without a kludge such as this. It also enabled Zig programs to explicitly specify struct field alignment:


const std = @import("std");
const expect = std.testing.expect;

const Node = struct {
    next: *Node,
    massive_byte: u8 align(64),

test "struct field explicit alignment" {
    var node: Node = undefined;
    node.massive_byte = 100;
    expect(node.massive_byte == 100);
    comptime expect(@typeOf(&node.massive_byte) == *align(64) u8);
    expect(@ptrToInt(&node.massive_byte) % 64 == 0);
$ zig test field_align.zig
1/1 test "struct field explicit alignment"...OK
All tests passed.

In addition to this, "Lazy Values" solved the following bugs:

"Lazy Values" also paved the way for Standard Library integrations with Async Functions.

@Type §

@Type(comptime info: @import("builtin").TypeInfo) type

This function is the inverse of @typeInfo. It reifies type information into a type.

It is available for the following types:

For these types it is a TODO in the compiler to implement:

For these types, @Type is not available. There is an open proposal to allow unions and structs.

Thank you to Jonathan Marler for the original implementation of @Type.

Variable Declarations as Methods §

Variable declarations can now be called as methods:


const std = @import("std");
const expect = std.testing.expect;

const Foo = struct {
    a: u64 = 10,

    fn one(self: Foo) u64 {
        return self.a + 1;

    const two = __two;

    fn __two(self: Foo) u64 {
        return self.a + 2;

    const three = __three;

    const four = custom(Foo, 4);

fn __three(self: Foo) u64 {
    return self.a + 3;

fn custom(comptime T: type, comptime num: u64) fn (T) u64 {
    return struct {
        fn function(self: T) u64 {
            return self.a + num;

test "fn delegation" {
    const foo = Foo{};
    expect(foo.one() == 11);
    expect(foo.two() == 12);
    expect(foo.three() == 13);
    expect(foo.four() == 14);
$ zig test var_decl_methods.zig
1/1 test "fn delegation"...OK
All tests passed.

Thank you to Michael Dusan for proposing and implementing this. #3306

Standard Library §

Debug Info and Stack Traces §

Previously, due to a bug, the stack trace iteration code was using the number of frames collected as the number of frames to print, not recognizing the fixed size of the buffer. So it would redundantly print items, matching the total number of frames ever collected. Now the iteration code is limited to the actual stack trace frame count, and will not print duplicate frames. #2447 #2151

LemonBoy implemented a bunch of fixes for the DWARF parser (#2254):

Thanks to this, and some other debug info fixes from LemonBoy, stack traces now work in release builds:


const std = @import("std");

fn foo() !void {
    return error.TheSkyIsFalling;

pub fn main() !void {
    try foo();
$ zig build-exe test.zig --release-safe
$ ./test
error: TheSkyIsFalling
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:4:5: 0x20de63 in std.special.posixCallMainAndExit (test)
    return error.TheSkyIsFalling;
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:8:5: 0x20de6b in std.special.posixCallMainAndExit (test)
    try foo();

LemonBoy additionally implemented reading symbol names from DWARF sections, so stack traces on Linux now have actual function names instead of ???'s. You can see this in the above stack trace.

There is an open issue with this, however - scanning all the symbol names is a bit slow. A future improvement will improve the "Big-O" performance of this function, and bring Zig stack traces up to the same speed that one gets with, for example, gdb.

Despite this trouble for debug info of very large Linux binaries, most projects will experience faster stack traces thanks to Marc Tiehuis switching to using mmap to read the debug info. Usually, such techniques are frowned upon due to making error handling difficult, however, this debug info code is currently designed for the use case of dumping a stack trace when the application has already panicked, and is about to abort. And so, an error in loading debug info from disk would be handled by aborting anyway.

In addition, the following improvements were made:

Formatted Printing §

Marc Tiehuis created a plan to overhaul the std.fmt API. The plan is partially implemented in 0.5.0, but there is remaining work.

The main difference in 0.5.0 is the positional, precision, and width support. This removes the odd width and precision specifiers found and replacing them with the more consistent api described in #1358.

Take the following example:


This refers to the second argument (0-indexed) in the argument list. It will be printed with a minimum width of 5 and will have a precision of 9 (if applicable).

Not all types correctly use these parameters just yet. There are still some missing gaps to fill in. Fill characters and alignment have yet to be implemented.

In addition:

Math §

Marc Tiehuis ported upstream changes from musl's math functions.

This also starts the documentation effort for the math/ subdirectory. The intent is to use this as a somewhat representative test-case for any work on the documentation generator.

Thread Local Storage §

Thanks to LemonBoy, Zig now has support for threadlocal variables on Linux without relying on libc, on the following architectures:

In addition to wider architecture support, he implemented on-demand TLS allocation. Previously, if there were too many thread local variables, Zig would panic on startup.

With these changes, Zig has proper support for TLS on Linux.

Reorganization of Operating System Abstractions §

In Zig 0.5.0, OS abstractions are organized in a straightforward manner. std.os is "Zig-flavored POSIX". All the "bits" familiar to C programmers are available in this namespace, such as O_RDONLY and open. The functions have errno translated to Zig errors (on Linux without libc, there is no thread local variable for errno 🎉) and slices are used rather than raw pointers where appropriate.

Higher level, cross-platform abstractions are available in category-specific namespaces, for example std.fs.File.openRead.

std.os.windows has "Zig-flavored Windows", with GetLastError translated to Zig errors. Raw Windows APIs are available directly via namespaces named after their DLLs, for example std.os.windows.kernel32.ExitProcess.

Zig's optional integration with libc is significantly more robust. std.os functions call libc functions when linking against libc, and otherwise use the operating system's syscall ABI directly.

After some experimentation, it was concluded that Windows does not integrate well with libc, and so on Windows, even when linking libc, the native Windows API calls are used rather than libc API.

See #2380 for more details and discussion.

Recursive Rewrite of Self-Hosted Parser §

Zig's self-hosted parser is in the standard library - std.zig.parse. It's the backbone of zig fmt.

I've said before that recursion is one of the enemies of perfect software, because it represents a way that a program can fail with no foolproof way of preventing it. With recursion, pick any stack size and I'll give you an input that will crash your program. Embedded developers are all too familiar with this problem.

It's always possible to rewrite code using an explicit stack using heap allocations, and that's exactly what Jimmi did in the self-hosted parser.

This implementation of the self-hosted parser is an interesting case study of avoiding recursion by using an explicit stack. It is essentially a hand-written recursive descent parser, but with heap allocations instead of recursion. This code is truly a work of art. I like to call it "Jimmi's non-recursive recursive-descent parser".

When Jimmi originally implemented the code, we thought that we could not solve the unbounded stack growth problem of recursion. However, now we have a plan for safe recursion.

And so it was time to lay the code to rest. This is where Stevie Hryciw came in. Stevie rewrote the entire self-hosted parser, to the full grammar specification. This was a large project spanning across several weeks. During this time, Stevie endured painful rebases and dutifully updated the pull request description to keep everyone informed.

Stevie didn't stop there - he followed up by analyzing Performance Impact as well as Readability Impact. He writes:

Performance Impact §

Here are some informal tests of parser_test.zig with perf stat -d on x86_64 Linux.

In the absence of visualizations and more formal testing, some quick findings based on my system:

Readability Impact §

Indentation stats of std/zig/parse.zig:

master                          | stage2-recursive-parser
indent count                    | indent count
------------                    | ------------
    0     92                    |      0   263
    1    241                    |      1   803
    2    123                    |      2   763
    3    291                    |      3   334
    4    654                    |      4   133
    5    662                    |      5    60
    6    700                    |      6    28
    7    347                    |      7     5
    8    229                    |
    9     42                    |
   10     18                    |
avg indentation level: 4.796999 | avg indentation level: 1.827543
source lines of code:      3399 | source lines of code:      2389

Default Segfault Handler §

x86_64-linux and Windows now have, by default, a segfault handler that is attached before main(). Thanks to this, Zig programs now have stack traces for segfaults:


pub fn main() void {
    dereferenceAPointer(@intToPtr(*i32, 0x1));

fn dereferenceAPointer(ptr: *i32) void {
    ptr.* = 10;
$ zig build-exe test.zig
$ ./test
Segmentation fault at address 0x1
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:6:13: 0x2281bd in dereferenceAPointer (test)
    ptr.* = 10;
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:2:24: 0x2280fd in main (test)
    dereferenceAPointer(@intToPtr(*i32, 0x1));
/home/andy/Downloads/zig/lib/std/special/start.zig:194:22: 0x22718b in std.special.posixCallMainAndExit (test)
/home/andy/Downloads/zig/lib/std/special/start.zig:102:5: 0x22706f in std.special._start (test)
(process terminated by signal)

This can be disabled:


pub const enable_segfault_handler = false;

pub fn main() void {
    dereferenceAPointer(@intToPtr(*i32, 0x1));

fn dereferenceAPointer(ptr: *i32) void {
    ptr.* = 10;
$ zig build-exe test.zig
$ ./test
(process terminated by signal)

This works because the standard library uses the new feature @import("root") to check for this opt-out.

Thank you to Rocknest for a proof-of-concept implementation of this in #2355.

HashMap and Hashing §

Notably, Sahnvour made std.HashMap consistent about whether or not it will dereference keys. std.AutoHashMap no longer will dereference slices ([]const u8 or []u8), or any pointer type for that matter:


const std = @import("std");

test "AutoHashMap with slices" {
    var map = std.AutoHashMap([]const u8, bool).init(std.heap.direct_allocator);
$ zig test test.zig
/home/andy/Downloads/zig/lib/std/hash/auto_hash.zig:176:9: error: std.auto_hash.autoHash does not allow slices (here []const u8) because the intent is unclear. Consider using std.auto_hash.hash or providing your own hash function instead. Consider std.StringHashMap for hashing the contents of []const u8.
        @compileError("std.auto_hash.autoHash does not allow slices (here " ++ @typeName(Key) ++
/home/andy/Downloads/zig/lib/std/hash_map.zig:540:21: note: called from here
            autoHash(&hasher, key);
/home/andy/Downloads/zig/lib/std/hash_map.zig:538:29: note: called from here
        fn hash(key: K) u32 {

std.StringHashMap is provided for this use case instead:


const std = @import("std");

test "StringHashMap" {
    var map = std.StringHashMap(bool).init(std.heap.direct_allocator);
    _ = try map.put("hello", true);
$ zig test string_hash_map.zig
1/1 test "StringHashMap"...OK
All tests passed.


Marc Tiehuis made a series of improvements to hashing performance:

Inline full slice hashing - this gives moderate speed improvements when hashing small keys. The crc/adler/fnv inlining did not provide enough speed up to warrant the change.


  small keys: 2277 MiB/s [c14617a1e3800000]
  small keys:  937 MiB/s [b2919222ed400000]
  small keys:  722 MiB/s [3c3d974cc2800000]
  small keys: 1580 MiB/s [70155e1cb7000000]
  small keys: 1898 MiB/s [00013883ef800000]
  small keys: 2323 MiB/s [0035bf3dcac00000]
  small keys:  218 MiB/s [0035bf3dcac00000]


  small keys: 2775 MiB/s [c14617a1e3800000]
  small keys: 1086 MiB/s [b2919222ed400000]
  small keys:  789 MiB/s [3c3d974cc2800000]
  small keys: 1604 MiB/s [70155e1cb7000000]
  small keys: 1856 MiB/s [00013883ef800000]
  small keys: 2336 MiB/s [0035bf3dcac00000]
  small keys:  218 MiB/s [0035bf3dcac00000]

Improve siphash performance for small keys by up to 30% (#3124) - this removes the partial buffer handling from the full slice API.

./benchmark --filter siphash --count 1024


   iterative: 3388 MiB/s [67532e53a0d210bf]
  small keys: 1258 MiB/s [948c91176a000000]
   iterative: 2061 MiB/s [f792d39bff42f819]
  small keys:  902 MiB/s [e1ecba6614000000]


   iterative: 3410 MiB/s [67532e53a0d210bf]
  small keys: 1639 MiB/s [948c91176a000000]
   iterative: 2053 MiB/s [f792d39bff42f819]
  small keys: 1074 MiB/s [e1ecba6614000000]

Simplify wyhash and improve speed - this removes the exposed stateless variant since the standard variant has similar speed now.

Using ./benchmark --filter wyhash --count 1024, the speed change has changed from:

   iterative: 4093 MiB/s [6f76b0d5db7db34c]
  small keys: 3132 MiB/s [28c2f43c70000000]


   iterative: 6515 MiB/s [673e9bb86da93ea4]
  small keys: 10487 MiB/s [28c2f43c70000000]

Documentation §

zig build §

zig build is still in an experimental, proof-of-concept phase, and will remain that way until at least the package manager is complete. However, there were still improvements to zig build this release cycle:

zig fmt §

zig fmt now fixes invalid whitespace instead of rejecting it. The // zig fmt: off directive is ignored for whitespace fixes.

Thanks to WebAssembly Support improvements, there is also a web-based formatter made by Shritesh Bhattarai.

libc §

Zig now provides libc for the following targets (find this information with zig targets):

musl 1.1.23 §

Zig ships with the source code to musl. Zig 0.5.0 updates to the 1.1.23 release of musl, plus a handful of patches to fix various issues with 64-bit ARM Support and RISC-V Support. All these patches are merged into musl upstream and will be part of the next release.

Previously, the way that Zig built musl from source skipped some necessary files. LemonBoy solved this issue.

Additionally, the updating process for musl was brittle and confusing. Now, the process is streamlined and fully documented. Unnecessary patches were dropped.

Now, Zig has excellent Test Coverage of musl. The Zig test suite tests building musl for these targets:

glibc 2.30 §

In Zig 0.5.0, not only can Zig provide dynamically linked glibc for any target, but it can also provide any version of glibc for any target:


const std = @import("std");

pub fn main() void {
    var buf: [16]u8 = undefined;
    _ = std.c.getrandom(&buf, buf.len, 0);
$ ./zig build-exe test.zig -lc -target x86_64-linux-gnu -target-glibc 2.24
lld: error: undefined symbol: getrandom
>>> referenced by test.zig:5 (/home/andy/dev/zig/build/test.zig:5)
>>>               ./test.o:(main.0)

$ ./zig build-exe test.zig -lc -target x86_64-linux-gnu -target-glibc 2.25
$ ./test

Updating to the newest glibc version is now streamlined and fully documented. Even though Zig now supports every version of glibc, the amount of bytes required for a Zig installation with regards to glibc has decreased, because a dummy libc file is no longer required, as it is generated on-the-fly depending on the target version selected.

The target glibc version is exposed in @import("builtin") and is used by the Standard Library to do a glibc version check, to decide whether to use libc getrandom or read from /dev/urandom. #397

The supported glibc version range is increased to include 2.30.

Building glibc now has Test Coverage when the -Denable-qemu and -Denable-foreign-glibc options are enabled, for these targets:

Zig ships with mingw-w64 §

Zig now ships with the source code and header files to mingw-w64 (version 6.0.0), and uses this to provide libc when targeting Windows.

Combining this with Wine, one can cross-compile C code for Windows and run it, without even touching a Windows computer:


#include <stdio.h>

int main(int argc, char **argv) {
    printf("Hello Windows\n");
    return 0;
$ zig build-exe --c-source hello_windows.c -lc -target x86_64-windows-gnu
$ wine hello_windows.exe
Hello Windows

Building and linking against mingw-w64 libc now has Test Coverage for the x86_64-windows-gnu target.

One of the use cases for this is creating Zig packages out of C libraries, and there is now a proof of concept of this with SDL2.

The open-source game Legend of Swarkland is being written in Zig, and it uses this SDL2 package in order to support cross compiling for Windows with nothing installed other than Zig. The developer, Josh, does not have a Windows computer to test on, but has a brother who uses Windows he wants to be able to playtest his game. Using only Zig and Wine, Josh can create Windows builds of his game and even test them, before sending his brother an executable.

$ git clone https://github.com/thejoshwolfe/legend-of-swarkland --recursive
$ cd legend-of-swarkland
$ zig build -Dtarget x86_64-windows-gnu
$ ls zig-cache/bin
legend-of-swarkland.exe           legend-of-swarkland_headless.pdb
legend-of-swarkland_headless.exe  legend-of-swarkland.pdb

Integration with mingw-w64 is easy and clean, because their headers are already multi-architecture. Thank you especially to IRC user wbs, who is largely responsible for that, and for patiently helping me work through my own issues caused by hacking up the mingw-w64 build system to integrate into Zig.

Freestanding libc §

Zig provides libc even when compiling in freestanding mode. This enables some C libraries to work even when there is no host Operating System.

In Zig 0.5.0, this concept is a little bit more fleshed out and clear. One can observe this freestanding libc in action when building C code for WebAssembly.

There is still a lot to do on this front, and it could be an engaging project for contributors.

C Translation §

LemonBoy made several improvements:

Thanks to C pointers supporting optional syntax, NULL pointers now translate to null.


Self-Hosted C Translation §

In Zig 0.4.0, the translate-c and @cImport implementations are 5,000 lines of C++. However, in this release, Zig is transitioning to a fully self-hosted implementation.

The parts of translate_c.cpp that interact with the Clang C++ API have been extracted into zig_clang.h and zig_clang.cpp. This is a C API on top of the C++ API with some careful static assertions to ensure the file is kept up-to-date as Clang's C++ API changes. translate_c.cpp now interacts with the Clang C++ API exclusively via zig_clang.h. These files are generally useful for any project and they are MIT licensed.

Based on zig_clang.h, clang.zig is created, updated, and maintained. This is extern functions and types so that Zig code can utilize the C layer on top of the Clang C++ API. And with this, we have src-self-hosted/translate_c.zig which is the self-hosted implementation of translate-c (and @cImport). This is exposed with zig translate-c-2. Until the self-hosted implementation is brought up to feature parity, zig translate-c and @cImport are still the C++ implementation. This work is partially done thanks to Stevie Hryciw; you can get a sense of progress by examining the test cases. More contributions welcome!

See #1964 for more details.

compiler-rt §

compiler-rt is the library that provides, for example, 64-bit integer multiplication for 32-bit architectures which do not have a machine code instruction for it. In the gcc world, it's called libgcc.

Unlike most compilers, which depend on a binary build of compiler-rt being installed alongside the compiler, Zig builds compiler-rt on-the-fly, from source, as needed for the target platform. This release saw some improvements to Zig's compiler-rt implementation.

LemonBoy also fixed an edge case in addXf3 - since the operands may be zero, he used the wrapping operators to avoid a spurious integer-overflow error. He then proceeded to other fixes:

With Zig 0.5.0, compiler-rt much more complete, but not fully. There are some missing functions, and it's planned to do an audit before 1.0.

Test Coverage §

Zig now has much more exhaustive test coverage of foreign architectures, thanks to QEMU. These new options are available to zig build when running the Zig test suite:

See CONTRIBUTING.md for more details.

Michael Dusan implemented a new kind of test coverage for stack traces, to catch future regressions in Debug Info and Stack Traces.


std.debug.global_allocator is deprecated as far as being used in tests is concerned. Tests should use std.heap.FixedBufferAllocator and stack memory instead.

Miscellaneous Improvements §

Self-Hosted .d File Parsing §

When Zig compiles C code (using libclang), it automatically enables .d file generation so that Zig can learn the dependencies and do proper caching.

Previously, Zig's .d file parser was written in C++ and a bit brittle. Michael Dusan dove head-first into this and implemented a robust .d file parser, in self-hosted Zig code, complete with unit tests.

Unfortunately, there is still an open issue regarding this, because the first line Clang outputs cannot be parsed unambiguously. Clang Bug Report. If you, the reader of these release notes, are a Clang developer, please fix 🙏

Binary Size §

@import("builtin") gained strip_debug_info which is a comptime bool value telling whether --strip was passed to the compiler.

This makes Zig code aware at compile-time of when it will not have any debug information available at runtime. The standard library now takes advantage of this to avoid Zig binaries containing useless debug info code.

This, along with Timon Kruiper's contribution of enabling the equivalent of -ffunction-sections in Zig's LLVM codegen, resulted in tiny ReleaseSmall binaries:


const std = @import("std");

pub fn main() void {
    std.debug.warn("Hello, World!\n");
$ zig build-exe hello.zig --release-small --strip --single-threaded
$ ./hello 
Hello, World!
$ ls -ahl ./hello
-rwxr-xr-x 1 andy users 10K Sep 26 15:56 ./hello
$ ldd ./hello
  not a dynamic executable

The Windows build is even smaller:

$ zig build-exe hello.zig --release-small --strip --single-threaded -target x86_64-windows
$ wine64 ./hello.exe 
Hello, World!
$ ls -ahl ./hello.exe 
-rwxr-xr-x 1 andy users 3.0K Sep 26 15:57 ./hello.exe

Zig's ability to create tiny executables is especially attractive for WebAssembly.

Self-Hosted Installation of Library Files §

Previously, when editing source files, it was required to make install (msbuild -p:Configuration=Release INSTALL.vcxproj on Windows) in order to test changes. This used cmake's install() function for all the library files, such as the Standard Library, as well as the libc files that Zig ships with. Unfortunately, this printed something like this every time:

-- Installing: /home/andy/dev/zig/build/lib/zig/std/array_list.zig
-- Installing: /home/andy/dev/zig/build/lib/zig/std/ascii.zig
-- Installing: /home/andy/dev/zig/build/lib/zig/std/atomic/int.zig

...for all 6,091 lib files. Even when the files are already installed, it would print:

-- Up-to-date: /home/andy/dev/zig/build/lib/zig/std/array_list.zig
-- Up-to-date: /home/andy/dev/zig/build/lib/zig/std/ascii.zig
-- Up-to-date: /home/andy/dev/zig/build/lib/zig/std/atomic/int.zig

There is no way to disable this in cmake. This caused make install on my Linux computer to take 2.4 seconds even when it has to do nothing, and prints all these unnecessary lines to stdout. On my Windows it took even longer, upwards of 5 seconds.

Now, installation of lib files is self-hosted, using zig build. Running make when there is nothing to do takes 0.3 seconds on my Linux computer; 2.4 on Windows.

Unfortunately, lib file installation happens in the make target instead of the make install target, because cmake has no way to add a custom command to the install target. So that's why Zig now has the option -DZIG_SKIP_INSTALL_LIB_FILES=ON, which is recommended to enable for contributors to Zig. It's off by default because otherwise installing Zig would be missing library files. However when contributing to Zig, running the zig binary from the build directory will search upwards for library files and find them directly in the source tree. This means one can directly edit the Standard Library in the source tree, and changes will be picked up without needing to run make at all.

Bug Fixes §

This Release Contains Bugs §

Zig has known bugs.

Zig is immature. Even with Zig 0.5.0, working on a non-trivial project using Zig will likely mean participating in the development process.

The first release that will ship with no known bugs will be 1.0.0.

Roadmap §

The major theme of next release cycle will be safety.

Along with this, it's planned to resume work on the self-hosted compiler now that new Async Functions are done.

Issues that have the possibility of breaking changes to the language will be prioritized, so that the language can be stabilized.

Package Manager Status §

Having a package manager built into the Zig compiler is a long-anticipated feature. Zig 0.5.0 does not have this feature, however the Zig project now that Async Functions are complete, it's time to begin on networking in the Standard Library. I expect to complete this along with at least an early prototype of the package manager during the next release cycle.

Accepted Proposals §

Here are proposals that have been accepted during the 0.5.0 release cycle, to give you an idea of the upcoming changes to Zig:

Active Open-Source Projects Using Zig §

Funding Status §

During this release cycle, I joined the GitHub Sponsors program. This transition went fairly smoothly, thanks to Devon Zuegel. It's pretty clear to me that she's going above and beyond what's professionally required of her to help maintainers get sponsored.

At this point, funding for the Zig project is stable. There are enough funds that I can continue to work full time on Zig without burning down my savings. Based on current trends, I should even be able to get health insurance soon.

However, community growth has outpaced funding growth. My job has become more and more demanding over time. Even just merging pull requests at this point is a full time job. I have averaged merging 1.5 pull requests per day into Zig for the last 4 years. There is more than enough work for 2 full-time developers on Zig, and I would love to get to the point where paying another full-time developer is possible.

To facilitate this, I'm planning on starting Zig Software Foundation non-profit organization. I am looking for recommendations for a lawyer who would help me set this up. If you know one, please send me an email.

Thank You Sponsors! §

Special thanks to those who sponsor Zig. Because of you, Zig is not driven by the needs of a business; instead it exists solely to serve the open source community.