Original post


Download & Documentation

Zig is a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software. With special thanks to many generous sponsors, the Zig project is financially sustainable and currently supports one full-time developer. Let’s reboot systems programming.

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

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

As far as Zig is concerned, the primary benefits of the new LLVM version are bug fixes, especially for ARM Support, MIPS Support, and RISC-V Support.

This is the first release of LLD that has all of Zig’s patches merged upstream. Consequently, Zig’s source repository no longer includes a fork of LLD sources. Amusingly, it also means that the source tarball zig-0.6.0.tar.xz is 0.5 MiB smaller than zig-0.5.0.tar.xz, since the deletion of LLD sources saved more space than all the rest of the changes made in this release cycle combined. Note that the new Bootstrap Tarball bundles all dependencies of the Zig compiler, which includes LLVM, LLD, and Clang.

Thanks to LemonBoy for submitting patches to update Zig’s codebase to LLVM 10, as well as submitting countless bug reports and patches upstream to LLVM and LLD, to get various cross-compiling issues sorted out.

With zig cc now available, the 0.6.0 release of Zig comes with a special new source tarball: zig-bootstrap-0.6.0.tar.xz

This is made from the ziglang/bootstrap source repository, which contains unpatched LLVM, Clang, LLD, and Zig sources, and a simple build script with no branching logic.

The purpose of the bootstrap tarball is to start with minimum system dependencies and end with a fully operational Zig compiler for any target. It does this in exactly 4 steps:

  1. Build LLVM, Clang, and LLD from source, for the native target, using the native C++ compiler.
  2. Build Zig from source for the native target, linking against LLVM, Clang, and LLD.
  3. Now we have Zig as a cross compiler. Use it to rebuild LLVM, Clang, and LLD for the specified target.
  4. Finally, use Zig to build itself, for the specified target.

And thus, the Grand Bootstrapping Plan is fulfilled. The number of steps will always be these four, or less. Never more.

This bootstrap process provides the five new binary builds available in this release, that were not available previously:

See the download page for a full list of tarballs.

Thanks to Timon Kruiper and LemonBoy for contributions related to this.

Zig uses a Tier System to communicate the level of support for different targets. Notably, in this release:

Thanks to Benjamin Feng and Colin Svingen’s contributions:

  • The WASI OS bits are audited and updated.
  • std.heap.page_allocator gains a WebAssembly implementation.

Tier 1 Support §

  • Not only can Zig generate machine code for these targets, but the standard library cross-platform abstractions have implementations for these targets. Thus it is practical to write a pure Zig application with no dependency on libc.
  • The CI server automatically tests these targets on every commit to master branch, and updates the download page with links to pre-built binaries.
  • These targets have debug info capabilities and therefore produce stack traces on failed assertions.
  • libc is available for this target even when cross compiling.
  • All the behavior tests and applicable standard library tests pass for this target. All language features are known to work correctly.

Tier 2 Support §

  • The standard library supports this target, but it’s possible that some APIs will give an “Unsupported OS” compile error. One can link with libc or other libraries to fill in the gaps in the standard library.
  • These targets are known to work, but may not be automatically tested, so there are occasional regressions.
  • Some tests may be disabled for these targets as we work toward Tier 1 Support.

Tier 3 Support §

  • The standard library has little to no knowledge of the existence of this target.
  • Because Zig is based on LLVM, it has the capability to build for these targets, and LLVM has the target enabled by default.
  • These targets are not frequently tested; one will likely need to contribute to Zig in order to build for these targets.
  • The Zig compiler might need to be updated with a few things such as
    • what sizes are the C integer types
    • C ABI calling convention for this target
    • bootstrap code and default panic handler
  • zig targets is guaranteed to include this target.

Tier 4 Support §

  • Support for these targets is entirely experimental.
  • LLVM may have the target as an experimental target, which means that you need to use Zig-provided binaries for the target to be available, or build LLVM from source with special configure flags. zig targets will display the target if it is available.
  • This target may be considered deprecated by an party, such as macosx/i386 in which case this target will remain forever stuck in Tier 4.
  • This target may only support --emit asm and cannot emit object files.

Zig’s Windows support improved considerably in this release. Counterintuitively, in the Support Table, x86_64-windows went from Tier 1 => Tier 2, but this is due to more SIMD test coverage added, and it was discovered that vectors of f16 are failing some behavior tests. This is the only issue holding Windows (both 32-bit and 64-bit) back from Tier 1.

In this release, the minimum supported Windows version is bumped from 7+ to 8.1+, following the extended support lifecycle of Microsoft.

In addition:

Thanks Jared Miller, emekoi, syscall0, and LemonBoy for contributions related to this.

32-bit Windows Support §

In this release, i386-windows goes from Tier 3 => Tier 2. A pre-made .zip build of 32-bit Windows is newly available.

Thanks to LemonBoy’s work on this:

  • Support for Win32 Thread-Local Storage.
  • Adding more lib32 .def files from mingw-w64.
  • Removing x86/Windows name mangling hack and properly generating correct .lib files from mingw-w64 sources, by adding dlltool functionality to Zig.
  • Adding Test Coverage for i386-windows.
  • Fixing stack-probe symbol redefinition.

The only thing holding 32-bit Windows back from Tier 1 Support is enabling i386-windows CI builds of Zig that update the download page and the same f16 vector issue from 64-bit Windows.

RISC-V support in Zig is now excellent! We even have riscv64 binary tarballs now thanks to the Bootstrap Tarball. It does, however require one workaround due to clang crashing when it tries to build itself for self-hosted riscv64.

riscv64-freestanding went from Tier 4 => Tier 1.

riscv64-linux went from Tier 4 => Tier 2 and is already nearing Tier 1.

Debug Info and Stack Traces on RISC-V is now working.

The default ABI of riscv32-linux and riscv64-linux is changed to be ilp32d and lp64d, respectively. Likewise, the default ABI of non-linux riscv32 and riscv64 are changed to be ilp32 and lp64. This matches Clang’s behavior. (#4863)

Zig now has Test Coverage for riscv64 with no libc and riscv64 with musl libc. The issue for Zig providing glibc for riscv64 is #3340.

Thanks to LemonBoy for contributions related to this, and to Luís Marques for fixing RISC-V issues upstream, which landed in LLVM 10.

aarch64-linux is very nearly Tier 1. The only thing preventing it is some behavior tests are disabled.

In this release, Zig gained CI Test Coverage for aarch64, and the download page is updated with every master branch commit with a binary tarball for aarch64.

Thanks to the Bootstrap Tarball this release additionally gains a 32-bit ARM binary available (armv7a), as well as another 32-bit slightly older ARM binary (armv6kz) which notably works on Raspberry Pi 1 and RPi 0.

Thank you to Timon Kruiper and LemonBoy for working together to solve undefined behavior bugs revealed by building Zig with zig cc.

  • Fix signedness for some fields in ARM stat definition
  • C ABI support is partially implemented.
  • Fix possible unaligned ptr from getauxval. This caused SIGILL on armv7a-linux. (#4796)
  • Fix multiplication overflow in hash_const_val. In some cases the compiler was actually emitting an 64 bit signed multiplication, instead of a 32 bit unsigned one.

i386-linux went from Tier 3 => Tier 2, and is nearing Tier 1.

Thanks to the Bootstrap Tarball this release additionally gains a i386-linux binary available.

Thanks LemonBoy for implementing i386 support during this cycle. (#3808, #4408)

LemonBoy contributed MIPS fixes:

  • Correct signal bits for MIPS. Also enable the segfault handler for all the supported architectures beside MIPS.
  • Fix pipe syscall for MIPS.
  • Implement target_dynamic_linker for MIPS.

LemonBoy contributed NetBSD fixes: (#4793)

  • Fixes some nasty errors in the threading code
  • Makes Zig able to run all the tests (at least on x86-64) except the event ones
  • Audits and corrects some defines for NetBSD

Nick Erdmann and Heppokoyuki contributed UEFI improvements:

  • make the subsystem configurable in zig build
  • fix con_in definition and add EFI_SIMPLE_TEXT_INPUT_PROTOCOL definition
  • add file protocols and improvements
  • add documentation
  • loading images
  • snp, mnp, ip6, and udp6 support
  • protocol handling improvements
  • boot services and runtime services improvements
  • loaded image protocol improvements
  • Add shell parameters protocol
  • device path protocol improvements
  • status reform

In this release, x86_64-macos went from Tier 1 => Tier 2, however, this is not because Zig dropped any kind of support for macOS, but rather because the bar for meeting Tier 1 requirements was raised, to include “libc is available for this target even when cross-compiling.”

Zig’s awareness of CPU model and features as well as operating system versions has broadened.

The standard library now has two distinct concepts: std.Target and std.zig.CrossTarget.

CrossTarget is what Zig’s command line options get parsed into. It contains the concept of “native” and “default”. Once this structure is populated, it can be resolved into a Target.

A Target has all the information available; the CPU, OS, and ABI are all populated. As an example, a CrossTarget might be set to “native”, and then when it is resolved, it turns into a Target which has the triple riscv64-linux-musl.

zig build scripts set the desired CrossTarget of a build artifact; the Zig code being compiled only has access to the resolved Target as std.Target.current.

Zig now supports a more fine-grained sense of what is native and what is not. Some examples:

This is now allowed:

-target native

Different OS but native CPU, default Windows C ABI:

-target native-windows

This could be useful for example when running in Wine.

Different CPU but native OS, native C ABI.

-target x86_64-native -mcpu=skylake

Different C ABI but otherwise native target:

-target native-native-musl
-target native-native-gnu

This is a breaking change to std lib APIs for checking the OS and CPU architecture. To update from 0.5.0 to 0.6.0:

builtin.os => builtin.os.tag

builtin.arch => builtin.cpu.arch

std.build.Builder.standardTargetOptions is changed to accept its parameters as a struct with default values. It now has the ability to specify a whitelist of targets allowed, as well as the default target. Rather than two different ways of collecting the target, it’s now always a string that is validated, and prints helpful diagnostics for invalid targets. This feature should now be actually useful, and contributions welcome to further improve the user experience.

std.build.LibExeObjStep.setTheTarget is removed. std.build.LibExeObjStep.setTarget is updated to take a CrossTarget parameter.

std.build.LibExeObjStep.setTargetGLibC is removed. glibc versions are handled in the CrossTarget API and can be specified with the -target triple.

std.builtin.Version gains a format method.

Thanks to Timon Kruiper for contributions related to this.

Zig now has a database of CPU models and CPU features for every architecture. Now that zig targets is self-hosted and outputs JSON, the easiest way to see this is to pipe zig targets into a JSON file and inspect it with a graphical JSON viewer, such as Firefox.

Here I will show you zig targets | jq .native on the laptop that I am typing these release notes on:

  "triple": "x86_64-linux.5.4.15...5.4.15-gnu.2.27",
  "cpu": {
    "arch": "x86_64",
    "name": "skylake",
    "features": [
  "os": "linux",
  "abi": "gnu"

Here you can see the CPU model and set of CPU features Zig detected. The implementation of this is fully self-hosted. Although Zig properly informs LLVM about CPU features when it does code generation, the awareness of CPU features and detection of CPU features is all implemented in Zig code. Currently, only x86 CPU feature detection is implemented; Zig falls back to LLVM for detecting native CPU model and features on other architectures. Contributions welcome!

Zig now has the ability to parse CPU features as part of the target triple.

Native architecture, OS, and ABI, but baseline CPU features:

-target native -mcpu=baseline

RISC-V 64-bit architecture, OS linux, default ABI, native CPU plus the rdpid feature, minus the sse3 feature:

-target riscv64-linux -mcpu=native+rdpid-sse3

Target the RPi Zero:

-target arm-linux-musleabi -mcpu=arm1176jzf_s

Now that it is possible to select what CPU features are enabled, freestanding no longer has SSE enabled by default.

Thanks to Layne Gustafson for the initial exploration and implementation of this feature, and to alichay for the initial implementation of x86 CPU feature detection.

Thanks to LemonBoy, Michael Dusan, and Noam Preil for related contributions.

The whole point of Zig is to re-examine the premises of system programming, and rework abstractions that have shown to be less than ideal. Naturally, once Zig gained CPU feature awareness, it raised the question, what is the purpose of sub-architectures?

As it turns out, the answer is “none”. Sub-architectures are rendered redundant by the existence of CPU features, and so they no longer exist in Zig.

This has the happy consequence of making std.Target.Cpu.Arch an enum rather than a tagged union.

Rather than:

-target armv7a-linux-gnu

Now it is:

-target arm-linux-gnu

v7a is considered baseline, so to target a different sub-architecture such as v6kz, it would look like:

-target arm-linux-gnu -mcpu=generic+v6kz

Operating System version ranges are now part of the target. This means that comptime code has access to exactly which version(s) of an OS are being targeted. You can see this by looking at the output of zig builtin, which displays the source code provided by std.builtin. Here’s a snippet of the output on the computer I’m using to type release notes:

pub const os = Os{
    .tag = .linux,
    .version_range = .{ .linux = .{
        .range = .{
            .min = .{
                .major = 5,
                .minor = 4,
                .patch = 15,
            .max = .{
                .major = 5,
                .minor = 4,
                .patch = 15,
        .glibc = .{
            .major = 2,
            .minor = 27,
            .patch = 0,

Updated syntax for -target to take into account OS version ranges:

# still valid. default version range
-target x86_64-windows-msvc

# minimum windows version: XP
# maximum windows version: 10
-target x86_64-windows.xp...win10-msvc

# minimum windows version: 7
# maximum windows version: latest
-target x86_64-windows.win7-msvc

# linux example
-target aarch64-linux.3.16...5.3.1-musl

# specifying glibc version
-target mipsel-linux.4.10-gnu.2.1

Here’s what it will look like to populate a CrossTarget:

-        tc.target = tests.Target{
-            .Cross = .{
-                .arch = .x86_64,
-                .os = .linux,
-                .abi = .gnu,
-            },
+        tc.target = std.zig.CrossTarget{
+            .cpu_arch = .x86_64,
+            .os_tag = .linux,
+            .abi = .gnu,

Code that used Target.parse need not be updated.

Checking for the OS when doing conditional compilation:

--- a/lib/std/build/run.zig
+++ b/lib/std/build/run.zig
@@ -82,7 +82,7 @@ pub const RunStep = struct {

         var key: []const u8 = undefined;
         var prev_path: ?[]const u8 = undefined;
-        if (builtin.os == .windows) {
+        if (builtin.os.tag == .windows) {
             key = "Path";
             prev_path = env_map.get(key);
             if (prev_path == null) {

std.Target.getStandardDynamicLinkerPath is renamed to std.Target.standardDynamicLinkerPath and no longer requires an allocator.

Zig’s method of detecting the native system ABI and dynamic linker is now simple but portable: it inspects the dynamic linker path of its own executable. If statically linked, Zig looks at the dynamic linker path of /usr/bin/env, which is ubiquitous due to its use in shebang lines. Based on the dynamic linker file name, the ABI can be deduced. The same static Zig build will correctly detect the native ABI and dynamic linker path on Debian, NixOS, and Apline Linux, for example.

No more std.os.foo.is_the_target. It had the downside of running all the comptime blocks and resolving all the usingnamespaces of each system, when just trying to discover if the current system is a particular one. For Darwin, where it’s nice to use std.Target.current.isDarwin(), this demonstrates the utility that the proposal #425 would provide.

This change allowed the removal of special Darwin OS version min handling. Now it is integrated with Zig’s target OS range. The command line options -mios-version-min and -mmacosx-version-min are removed.

Thanks LemonBoy for contributing OS version detection implementations for Windows and OSX.

  • Improved names of error sets when using merge error sets operator (||).
  • pub syntax for container fields is removed.
  • Type coercion from *[0]T to E![]const T is now allowed. This is an unambiguous, safe cast.
  • asm now accepts comptime-known values, rather than requiring string literal syntax.
  • Removed compile error for peer result ?comptime_int and null. (#2763)
  • Ability to pass comptime types and non comptime types to same parameter.
  • @typeOf is renamed to @TypeOf. zig fmt automatically performs the conversion, and the next release of Zig after this one will remove the automatic conversion.
  • Ability to switch on pointer types. (#4074)
  • Multiline strings in test and library names are disallowed.
  • Zig language no longer requires the expression a else unreachable with comptime a to produce a comptime result.
  • Timon Kruiper implemented casting between [*c]T and ?[*:0]T on fn parameter. (#4176)
  • Timon Kruiper improved @typeInfo to lazily resolve declarations. This way all the declarations in a namespace won’t be resolved until the user actually uses the declarations slice in the builtin TypeInfo union. (#2594, #3893, #4435)
  • @ptrCast supports casting a slice to a pointer.
  • LemonBoy implemented peer type resolution between ?[]T and *[N]T. (#4767)
  • There is now peer type resolution between mixed-const []T and *[N]T. (#4766)

Thanks to Vexu and LemonBoy for contributions related to the above list.

Type coercion (previously called “implicit casting”) is now performed with the @as builtin, rather than by calling a type as a function. (#1757)

While a bit more verbose, Zig now has the property that all function calls are always function calls and not type casts, and thus it is no longer required for someone reading Zig code to know the type to determine whether something is a type cast or a function call.

Type coercion is now hooked up into the result location mechanism and additionally now hooked up to variable declarations; this maintains the property that:

var a: T = b;

is semantically equivalent to:

var a = @as(T, b);

With this change, one feature was added to the language, and one feature was removed.

There are no longer any C string literals such as c"hello". Instead, the type of all string literals is changed from

[]const u8


*const [N:0]u8

Where N is the number of bytes in the string literal.

Let’s unpack that. Reading the type from left-to-right, they are a reference to: an immutable array of N elements that is followed by an element with value 0, the element type is u8.

Note that the sentinel value is not counted in the length.

This type has the length encoded in multiple ways. This means that it can automatically coerce to both []const u8 (because the length is encoded in the type), and it can also automatically coerce to [*:0]const u8 (because both types are null-terminated and with the help of Slicing with Comptime Indexes).

With this change, Zig string literals now can be passed directly to both C functions which expect null-terminated strings and to Zig functions which accept slices.


const std = @import("std");

pub fn main() void {

fn do_it_the_zig_way(arg: []const u8) void {
    std.debug.warn("hello {}n", .{arg});

fn do_it_the_c_way(arg: [*:0]const u8) void {
    _ = std.c.printf("hello %sn", arg);
$ zig build-exe sentinel_ptrs.zig -lc
$ ./sentinel_ptrs
hello world
hello world

Additionally, slicing syntax now has a way to assert that a sentinel exists at a particular element:


const std = @import("std");

test "slice with sentinel" {
    var array = [_]i32{ 'a', 'b', 'c', 'd', 'e' };
    const slice = array[1..3 :'d'];
    const result = foo(slice);
    std.testing.expect(result == 'b' + 'c');

fn foo(s: [*:'d']i32) i32 {
    var sum: i32 = 0;
    var index: usize = 0;
    while (s[index] != 'd') : (index += 1) {
        sum += s[index];
    return sum;
$ zig test slice_sentinel.zig
1/1 test "slice with sentinel"...OK
All 1 tests passed.

If the sentinel is incorrect, a safety check is activated:


test "slice with sentinel" {
    var array = [_]i32{ 'a', 'b', 'c', 'd', 'e' };
    const slice = array[1..3 :'f'];
$ zig test test.zig
1/1 test "slice with sentinel"...sentinel mismatch
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:3:24: 0x204c07 in test "slice with sentinel" (test)
    const slice = array[1..3 :'f'];
/home/andy/Downloads/zig/lib/std/special/test_runner.zig:47:28: 0x22bb2e in std.special.main (test)
        } else test_fn.func();
/home/andy/Downloads/zig/lib/std/start.zig:253:37: 0x20565d in std.start.posixCallMainAndExit (test)
            const result = root.main() catch |err| {
/home/andy/Downloads/zig/lib/std/start.zig:123:5: 0x20539f in std.start._start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});

Tests failed. Use the following command to reproduce the failure:

Thanks to LemonBoy, Raul Leal, daurnimator, and Michael Dusan for contributions related to this feature.

Now that Sentinel-Terminated Pointers is done, the main motivation for type coercion from array values to slices is gone. It’s a footgun for Zig to automatically convert a value into a pointer to that value; such an operation should be explicit.


test "coerce array value to slice" {
    var array: []const i32 = [_]i32{ 1, 2, 3, 4 };
$ zig test test.zig
./docgen_tmp/test.zig:2:36: error: array literal requires address-of operator to coerce to slice type '[]const i32'
    var array: []const i32 = [_]i32{ 1, 2, 3, 4 };
./docgen_tmp/test.zig:2:38: note: referenced here
    var array: []const i32 = [_]i32{ 1, 2, 3, 4 };

How to upgrade code for these new semantics:


test "coerce array pointer to slice" {
    var array: []const i32 = &[_]i32{ 1, 2, 3, 4 };
$ zig test coerce_array_ptr.zig
1/1 test "coerce array pointer to slice"...OK
All 1 tests passed.

This change to simplifies the result location semantics, which helps with reasoning about Zig code, as well as reducing the complexity of a Zig compiler.

All numerical comparisons are now allowed no matter the type combinations. For example, small signed integers can be compared against large unsigned integers, and floats can be compared against integers.

For a demonstration of this, you can look at the new std.math.compare function added to the Standard Library and the test cases for it:


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

pub const CompareOperator = enum {






pub fn compare(a: var, op: CompareOperator, b: var) bool {
    return switch (op) {
        .lt => a < b,
        .lte => a <= b,
        .eq => a == b,
        .neq => a != b,
        .gt => a > b,
        .gte => a >= b,

test "compare between signed and unsigned" {
    expect(compare(@as(i8, -1), .lt, @as(u8, 255)));
    expect(compare(@as(i8, 2), .gt, @as(u8, 1)));
    expect(!compare(@as(i8, -1), .gte, @as(u8, 255)));
    expect(compare(@as(u8, 255), .gt, @as(i8, -1)));
    expect(!compare(@as(u8, 255), .lte, @as(i8, -1)));
    expect(compare(@as(i8, -1), .lt, @as(u9, 255)));
    expect(!compare(@as(i8, -1), .gte, @as(u9, 255)));
    expect(compare(@as(u9, 255), .gt, @as(i8, -1)));
    expect(!compare(@as(u9, 255), .lte, @as(i8, -1)));
    expect(compare(@as(i9, -1), .lt, @as(u8, 255)));
    expect(!compare(@as(i9, -1), .gte, @as(u8, 255)));
    expect(compare(@as(u8, 255), .gt, @as(i9, -1)));
    expect(!compare(@as(u8, 255), .lte, @as(i9, -1)));
    expect(compare(@as(u8, 1), .lt, @as(u8, 2)));
    expect(@bitCast(u8, @as(i8, -1)) == @as(u8, 255));
    expect(!compare(@as(u8, 255), .eq, @as(i8, -1)));
    expect(compare(@as(u8, 1), .eq, @as(u8, 1)));
$ zig test compare.zig
1/1 test "compare between signed and unsigned"...OK
All 1 tests passed.

Thanks to Shawn Landden for the proposal.

Zig now allows omitting the struct type of a literal. When the result is coerced, the struct literal will directly instantiate the result location, with no copy:


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

test "anonymous struct literal" {
        .x = 13,
        .y = 67,

fn checkPoint(pt: struct {x: i32, y: i32}) void {
    expect(pt.x == 13);
    expect(pt.y == 67);
$ zig test struct_result.zig
1/1 test "anonymous struct literal"...OK
All 1 tests passed.

The struct type can be inferred. Here the result location does not include a type, and so Zig infers the type:


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

test "fully anonymous struct" {
        .int = 1234,
        .float = 12.34,
        .b = true,
        .s = "hi",

fn dump(args: var) void {
    expect(args.int == 1234);
    expect(args.float == 12.34);
    expect(args.s[0] == 'h');
    expect(args.s[1] == 'i');
$ zig test struct_anon.zig
1/1 test "fully anonymous struct"...OK
All 1 tests passed.

This syntax can also be used to initialize unions without specifying the type:


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

const Number = union {
    int: i32,
    float: f64,

test "anonymous union literal syntax" {
    var i: Number = .{.int = 42};
    var f = makeNumber();
    expect(i.int == 42);
    expect(f.float == 12.34);

fn makeNumber() Number {
    return .{.float = 12.34};
$ zig test anon_union.zig
1/1 test "anonymous union literal syntax"...OK
All 1 tests passed.

Thanks to Vexu, LemonBoy, dbandstra, and Alexander Naskos for contributing fixes related to this feature.

Similar to Anonymous Enum Literals and Anonymous Struct Literals, the type can be omitted from array literals. In this example, tuple syntax directly populates the array elements:


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

test "tuple syntax" {
    var array: [4]u8 = .{11, 22, 33, 44};
    expect(array[0] == 11);
    expect(array[1] == 22);
    expect(array[2] == 33);
    expect(array[3] == 44);
$ zig test tuple.zig
1/1 test "tuple syntax"...OK
All 1 tests passed.

A tuple is a struct with auto-numbered field names:


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

test "fully anonymous tuple" {
    dump(.{ @as(u32, 1234), @as(f64, 12.34), true, "hi"});

fn dump(args: var) void {
    expect(args.@"0" == 1234);
    expect(args.@"1" == 12.34);
    expect(args.@"3"[0] == 'h');
    expect(args.@"3"[1] == 'i');
$ zig test infer_tuple.zig
1/1 test "fully anonymous tuple"...OK
All 1 tests passed.

However, the @"" syntax is not needed, because although tuples are structs, they also have array-like qualities:


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

test "tuples support element access and .len field" {
    var x: i32 = 1234;
    var y: i32 = 4567;
    var tup = .{ x, y };
    tup[0] += 1; 
    tup[1] -= 1;

    expect(tup[0] == 1235);
    expect(tup[1] == 4566);

    var sum: i32 = 0;
    comptime var index = 0;
    inline while (index < tup.len) : (index += 1) {
        sum += tup[index];
    expect(sum == 1235 + 4566);

test "tuple concatenation" {
    var one = .{ "hi", true };
    var two = .{ 12.34, .ok };
    var combined = one ++ two;

    expect(combined[3] == .ok);
$ zig test tuples_are_array_like.zig
1/2 test "tuples support element access and .len field"...OK
2/2 test "tuple concatenation"...OK
All 2 tests passed.

Zig is determined to remain a small language. With the addition of tuples comes the removal of variadic parameter functions (#208). Printing and formatting are no exception. Formatted printing now uses tuples for the parameters to print, rather than var args:


const std = @import("std");

pub fn main() void {
    std.debug.warn("Hello, {}n", .{"World!"});
$ zig build-exe hello.zig
$ ./hello
Hello, World!

Note: Zig still supports C ABI functions with var args. Nothing is changed there.

Zig’s var args design was flawed, with many issues such as var args can’t handle void or number literal arguments. With tuples, these issues are resolved. Zig’s Tuples are much more robust and generally useful than its var args ever was.

It is planned to add tuple type declaration syntax.

Thanks to Vexu, LemonBoy, dbandstra, and Alexander Naskos for fixes related to this feature.

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

Vectors gain element access syntax (#3575, #3580). This introduces the concept of vector index being part of a pointer type. This avoids vectors having well-defined in-memory layout, and allows vectors of any integer bit width to work the same way.

When a vector is indexed with a scalar, this is vector element access, which is implemented in 0.6.0. When a vector is indexed with a vector, this is gather/scatter, which is not available in this release.


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

test "vector element access" {
    var v: @Vector(4, i32) = [_]i32{ 1, 5, 3, undefined };

    v[2] = 42;
    expect(v[1] == 5);
    v[3] = -364;
    expect(v[2] == 42);
    expect(-364 == v[3]);

    storev(&v[0], 100);
    expect(v[0] == 100);
fn storev(ptr: var, x: i32) void {
    ptr.* = x;
$ zig test vector_elem.zig
1/1 test "vector element access"...OK
All 1 tests passed.

Vectors now support comparisons, which returns a vector of bool:


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

test "vector comparisons" {
    var v: @Vector(4, i32) = [4]i32{ 2147483647, -2, 30, 40 };
    var x: @Vector(4, i32) = [4]i32{ 1, 2147483647, 30, 4 };
    expect(mem.eql(bool, &@as([4]bool, v == x), &[4]bool{ false, false, true, false }));
    expect(mem.eql(bool, &@as([4]bool, v != x), &[4]bool{ true, true, false, true }));
    expect(mem.eql(bool, &@as([4]bool, v < x), &[4]bool{ false, true, false, false }));
    expect(mem.eql(bool, &@as([4]bool, v > x), &[4]bool{ true, false, false, true }));
    expect(mem.eql(bool, &@as([4]bool, v <= x), &[4]bool{ false, true, true, false }));
    expect(mem.eql(bool, &@as([4]bool, v >= x), &[4]bool{ true, false, true, true }));
$ zig test vector_cmp.zig
1/1 test "vector comparisons"...OK
All 1 tests passed.

Floating-point vector operations were broken; now they are fixed and no longer require a type parameter (#4027).

Vector division is now supported, including with runtime-safety checks for integer overflow (#4737):


const std = @import("std");

test "vector division safety" {
    var a: @Vector(4, i16) = [_]i16{ 1, 2, -32768, 4 };
    var b: @Vector(4, i16) = [_]i16{ 1, 2, -1, 4 };
    const x = div(a, b);
    if (x[2] == 32767) return error.Whatever;
fn div(a: @Vector(4, i16), b: @Vector(4, i16)) @Vector(4, i16) {
    return @divTrunc(a, b);
$ zig test test.zig
1/1 test "vector division safety"...integer overflow
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:10:12: 0x2053be in div (test)
    return @divTrunc(a, b);
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:6:18: 0x204bd6 in test "vector division safety" (test)
    const x = div(a, b);
/home/andy/Downloads/zig/lib/std/special/test_runner.zig:47:28: 0x22bc4e in std.special.main (test)
        } else test_fn.func();
/home/andy/Downloads/zig/lib/std/start.zig:253:37: 0x2057cd in std.start.posixCallMainAndExit (test)
            const result = root.main() catch |err| {
/home/andy/Downloads/zig/lib/std/start.zig:123:5: 0x20550f in std.start._start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});

Tests failed. Use the following command to reproduce the failure:

See #903 for more details.

Thanks to Shawn Landden, data-man, and LemonBoy for contributions related to SIMD.

The original purpose of @newStackCall was as an exploration for safe recursion, but now the plan for safe recursion is via async functions.

This plus the fact that this builtin had serious flaws, it is now removed from the language.

Whether this builtin will be revived before Zig 1.0 or permanently gone is yet to be determined. To update to Zig 0.6.0, users of this builtin will have to resort to inline assembly.

@call(options: std.builtin.CallOptions, function: var, args: var) var

This new builtin calls a function, in the same way that invoking an expression with parentheses does, except the parameters are a tuple:


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

test "noinline function call" {
    assert(@call(.{}, add, .{3, 9}) == 12);

fn add(a: i32, b: i32) i32 {
    return a + b;
$ zig test call.zig
1/1 test "noinline function call"...OK
All 1 tests passed.

@call allows more flexibility than normal function call syntax does. The CallOptions struct is reproduced here:

pub const CallOptions = struct {
    modifier: Modifier = .auto,

    stack: ?[]align(std.Target.stack_align) u8 = null,

    pub const Modifier = enum {








The builtins @noInlineCall and @inlineCall are removed; instead @call supports .modifier = .never_inline, and .modifier = .always_inline.

Additionally, the .never_tail and .always_tail modifiers are available (#3732). These are still experimental; proper compile errors are not implemented to detect when these modifiers are used incorrectly.

For an explanation of .no_async, see noasync.

Thanks to LemonBoy for contributions related to this feature.

Old syntax for a function that has the C calling convention:

extern fn foo() void {}

New syntax:

fn foo() callconv(.C) void {}

In Zig 0.6.0, zig fmt automatically transforms the old syntax to the new syntax. In Zig 0.7.0, it will no longer do that.

Similarly the keywords stdcallcc and nakedcc are obsoleted by callconv(.Stdcall) and callconv(.Naked).

The enum that callconv takes as a parameter is defined in std.builtin.CallingConvention:

pub const CallingConvention = enum {

This allows the calling convention of a function to depend on comptime logic, which can be useful for dealing with code that works differently on different architectures.

Thanks to LemonBoy for implementing this.

A Non-exhaustive enum can be created by adding a trailing ‘_’ field. It must specify an integer tag type and may not consume every enumeration value.

@intToEnum on a non-exhaustive enum never fails.

A switch on a non-exhaustive enum can include a ‘_’ prong as an alternative to an else prong with the difference being that it makes it a compile error if all the known tag names are not handled by the switch.


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

const Number = enum(u8) {

test "switch on non-exhaustive enum" {
    const number = Number.One;
    const result = switch (number) {
        .One => true,
        .Three => false,
        _ => false,
    const is_one = switch (number) {
        .One => true,
        else => false,
$ zig test switch_non_exhaustive_enum.zig
1/1 test "switch on non-exhaustive enum"...OK
All 1 tests passed.

Non-exhaustive enums are useful for future-proofing code, so that it will continue to work correctly even when encountering values that were not present at the time the code was written.

Various bits in the Standard Library have been updated to use non-exhaustive enums rather than numerical constants.

Thanks to Vexu, LemonBoy, and daurnimator for contributions related to this feature.


const std = @import("std");

test "utf8 character literal" {
    const x = '💩';
    std.testing.expect(x == 128169);
$ zig test unicode_char_lit.zig
1/1 test "utf8 character literal"...OK
All 1 tests passed.

This makes sense because Zig is defined to have UTF-8 Source Encoding. A unicode character literal is a comptime_int with the value equal to the code point.

Thanks to Nick Erdmann for implementing this feature.

Thanks to Vexu:

  • Atomic operations additionally support enums, bools, non-power-of-two integers, and floats.
  • There is a new @atomicStore builtin.
  • @cmpxchgWeak, @cmpxchgStrong, and @atomicRmw now support being evaluated in comptime code.

const foo = bar;

Thanks Marc Tiehuis for the proposal (#2288) and Vexu for the implementation (#3697).


const std = @import("std");

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

test "example" {
    var foo: Foo = undefined;
    comptime std.debug.assert(foo.b == 1234);
$ zig test comptime_struct_field.zig
1/1 test "example"...OK
All 1 tests passed.

A comptime struct field requires a default initialization value. Loads from a comptime struct field result in a comptime value of the default initialization value. Stores to a comptime struct field assert that the stored value is the default initialization value.

Generally, one should use a global const instead of a comptime field. The reason for using a comptime field is when you want reflection over struct fields to find the data as a field. For example:


const std = @import("std");

fn dump(args: var) void {
    inline for (std.meta.fields(@TypeOf(args))) |field| {
        std.debug.warn("{} = {}n", .{field.name, @field(args, field.name)});

pub fn main() void {
    var runtime_float: f32 = 12.34;
        .int = 1234,
        .float = runtime_float,
        .b = true,
        .s = "hi",
        .T = [*]f32,
$ zig build-exe csf_example.zig
$ ./csf_example
int = 1234
float = 1.23400001e+01
b = true
s = hi
T = type

This will construct an anonymous struct with all comptime fields (except float) and pass it to dump. Each iteration in the for loop will evaluate the @field(...) expression and produce a comptime value, except float, which will be a runtime value.

This feature makes formatted printing, and tuples in general, support mixed comptime and runtime values (#3677).

It’s now possible to omit the type from struct fields. This allows the field to have any value of any type. The catch is that it causes the entire struct to be required to be comptime-known.


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

test "struct with var field" {
    const Point = struct {
        x: var,
        y: var,
    comptime var pt = Point {
        .x = 1,
        .y = 2,
    expect(pt.x == 1);
    expect(pt.y == 2);

    pt.x = true;
    pt.y = "hello";
    expect(std.mem.eql(u8, pt.y, "hello"));
$ zig test untyped_struct_fields.zig
1/1 test "struct with var field"...OK
All 1 tests passed.

The motivation behind this feature is to expose default struct field initialization values and sentinel values in @typeInfo:

pub const StructField = struct {
    name: []const u8,
    offset: ?comptime_int,
    field_type: type,
    default_value: var,

With Zig 0.6.0, this works now:


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

test "access default initialization value" {
    const Foo = struct {
        x: i32 = 1234,
        y: i32,
    const info = @typeInfo(Foo).Struct;
    expect(info.fields[0].default_value.? == 1234);
    expect(info.fields[1].default_value == null);
$ zig test type_info_struct.zig
1/1 test "access default initialization value"...OK
All 1 tests passed.

Similarly, the @typeInfo for Sentinel-Terminated Pointers now exposes the sentinel value.

It is planned to rename var to anytype in this context, to disambiguate it from variable declarations.

Thanks to LemonBoy for contributions related to this feature.

Pointer arithmetic now appropriately modifies the alignment of a pointer type:


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

test "pointer math alignment" {
    var arr: [10]u8 align(4) = undefined;
    var runtime_known_2: usize = 2;

    const ptr: [*]u8 = &arr;
    const ptr2 = ptr + 1;
    const ptr3 = ptr + 2;
    const ptr4 = ptr + runtime_known_2;

    comptime {
        expect(@TypeOf(ptr) == [*]align(4) u8);
        expect(@TypeOf(ptr2) == [*]u8);
        expect(@TypeOf(ptr3) == [*]align(2) u8);
        expect(@TypeOf(ptr4) == [*]u8);
$ zig test ptr_arith_align.zig
1/1 test "pointer math alignment"...OK
All 1 tests passed.

Thanks to LemonBoy for implementing this (#1528).

@export(target: var, comptime options: std.builtin.ExportOptions) void

@export now uses std.builtin.ExportOptions to accept its parameters:

pub const ExportOptions = struct {
    name: []const u8,
    linkage: GlobalLinkage = .Strong,
    section: ?[]const u8 = null,

The section option is new; it is now possible to specify the linksection using @export.

Thanks LemonBoy for implementing this (#2679).

@bitSizeOf(comptime T: type) comptime_int

This function returns the number of bits it takes to store T in memory. The result is a target-specific compile time constant.

This function measures the size allocated at runtime. For types that are disallowed at runtime, such as comptime_int and type, the result is 0.

Note that this value does not necessarily equal @sizeOf(T) * 8. For example, @bitSizeOf(u7) is 7, but @sizeOf(u7) is 1.

When the accepted proposal for align(0) fields is implemented, @bitSizeOf measures how many bits a type would take up in a struct if all fields were align(0).

Thanks to Vexu for the implementation of this.

Captured payloads from optionals and tagged-unions are no longer aliases to the same memory of the optional or tagged-union. The (unwrapped) payloads are copies.


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

test "no capture value aliasing" {
    expect(foo() == 1234);

fn foo() i32 {
    var optional_x: ?i32 = 1234;

    if (optional_x) |x| {
        optional_x = 5678;
        return x;

$ zig test no_capture_aliasing.zig
1/1 test "no capture value aliasing"...OK
All 1 tests passed.

There are two competing proposals for non-copyable data structures: #3803 #3804

When one of these is accepted, it will be a compile error to copy some types. to avoid copying, one can denote the capture value to make it a pointer:


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

test "capture value aliasing" {
    expect(foo() == 5678);

fn foo() i32 {
    var optional_x: ?i32 = 1234;

    if (optional_x) |*x| {
        optional_x = 5678;
        return x.*;

$ zig test capture_aliasing.zig
1/1 test "capture value aliasing"...OK
All 1 tests passed.

Thanks to LemonBoy for implementing this.

noasync, similar to comptime, creates a scope in which the programmer asserts there will be no suspension points.

Normally, async function calls and awaiting an async function frame introduce a suspension point at the callsite, causing the containing function to have the async calling convention. However, inside a noasync scope, async function calls and awaiting async function frames do not cause a suspension point. Instead, the code asserts that the callee never suspends, or in the case of await, that the function frame already has the result completed.

This allows a non-async function to call an async function:


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

test "noasync function call" {
    const result = noasync add(50, 100);
    expect(result == 150);

fn add(a: i32, b: i32) i32 {
    if (a > 100) {
    return a + b;
$ zig test noasync.zig
1/1 test "noasync function call"...OK
All 1 tests passed.

This is especially useful for main() to set up async functions initially:


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

var global_frame_1: anyframe = undefined;
var global_frame_2: anyframe = undefined;

pub fn main() void {
    var main_frame = async asyncMain();
    resume global_frame_1;
    resume global_frame_2;
    const result = noasync await main_frame;
    std.debug.warn("result: {}n", .{result});

fn asyncMain() i32 {
    var a = async foo();
    var b = async bar();
    return await a + await b;

fn foo() i32 {
    global_frame_1 = @frame();
    return 1;

fn bar() i32 {
    global_frame_2 = @frame();
    return 2;
$ zig build-exe async_main.zig
$ ./async_main
result: 3

Notice that the function asyncMain is able to participate in the async/await abstraction without having to care about the setup and teardown happening in main.

For Async I/O in the Standard Library, Zig handles this setup and teardown in the Start Code that calls main.

Now, watch what happens when we remove noasync from the above example:


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

var global_frame_1: anyframe = undefined;
var global_frame_2: anyframe = undefined;

pub fn main() void {
    var main_frame = async asyncMain();
    resume global_frame_1;
    resume global_frame_2;
    const result = await main_frame;
    std.debug.warn("result: {}n", .{result});

fn asyncMain() i32 {
    var a = async foo();
    var b = async bar();
    return await a + await b;

fn foo() i32 {
    global_frame_1 = @frame();
    return 1;

fn bar() i32 {
    global_frame_2 = @frame();
    return 2;
$ zig build-exe oops_await.zig
/home/andy/Downloads/zig/lib/std/start.zig:83:1: error: function with calling convention 'Naked' cannot be async
fn _start() callconv(.Naked) noreturn {
/home/andy/Downloads/zig/lib/std/start.zig:123:5: note: async function call here
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
/home/andy/Downloads/zig/lib/std/start.zig:179:17: note: async function call here
    std.os.exit(@call(.{ .modifier = .always_inline }, callMainWithArgs, .{ argc, argv, envp }));
/home/andy/Downloads/zig/lib/std/start.zig:188:36: note: async function call here
    return initEventLoopAndCallMain();
/home/andy/Downloads/zig/lib/std/start.zig:225:12: note: async function call here
    return @call(.{ .modifier = .always_inline }, callMain, .{});
/home/andy/Downloads/zig/lib/std/start.zig:243:22: note: async function call here
./docgen_tmp/oops_await.zig:11:20: note: await here is a suspend point
    const result = await main_frame;
./docgen_tmp/oops_await.zig:18:12: note: await here is a suspend point
    return await a + await b;
./docgen_tmp/oops_await.zig:22:22: note: @frame() causes function to be async
    global_frame_1 = @frame();

Here, the await inside main is a suspension point, which causes main to have the async calling convention, which has a cascading effect, causing _start to have the async calling convention. But _start already has the “naked” calling convention, because it is the entry point from the kernel!

We can use noasync to create a “seam” between async code and blocking code, because in this example, we know that main_frame has already completed by the time we call await.

Thanks to Vexu for contributions related to this feature.

Many deprecated builtins have been removed.

Thanks to Maciej Walczak for removing these and implementing the corresponding std lib functions:

  • @bytesToSlice becomes mem.bytesAsSlice
  • @sliceToBytes becomes mem.sliceAsBytes

Thanks to Vexu for removing these:

  • @typeId becomes @typeInfo tag-type
  • @memberCount becomes std.meta.fields(T).len
  • @memberName becomes std.meta.fields(T)[i].name
  • @memberType becomes std.meta.fields(T)[i].field_type
  • @ArgType becomes @typeInfo(T).Fn.args[i].arg_type.?
  • @IntType becomes std.meta.IntType

The body of functions returning inferred error sets are no longer required to return any possible errors.


fn foo() !void {}

test "" {
    foo() catch |err| switch (err) {};
$ zig test empty_inferred_error_set.zig
1/1 test ""...OK
All 1 tests passed.

Thanks to LemonBoy for implementing this.

Multiple parameters can now be specified with @TypeOf in cases where Peer Type Resolution is needed.

pub fn max(x: var, y: var) @TypeOf(x, y) {
    return if (x > y) x else y;

Thanks to Josh Wolfe for proposal and LemonBoy for implementing this.

Underscores may be placed between two digits as a visual separator. Consecutive underscores are not allowed.

fn digits() void {
    _ = 1_234_567;
    _ = 0xff00_00ff;
    _ = 0b10000000_10101010;
    _ = 0b1000_0000_1010_1010;
    _ = 0x123_190.109_038_018p102;
    _ = 3.14159_26535_89793;

Thanks to Marc Tiehuis for original proposal and momumi for making a strong case to re-open the proposal, and for implementing it.

When slicing and the length is comptime-known, the expression type is now a single-item pointer to array *[N]T . Prior to this change an error-prone @ptrCast was required.


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

test "slicing with comptime indexes" {
    var a = "abcdefgh".*;
    assert(@TypeOf(a) == [8:0]u8);

    var b = a[3..6];
    assert(@TypeOf(b) == *[3]u8);

    var runtime_i: usize = 3;
    var c = a[runtime_i..6];
    assert(@TypeOf(c) == []u8);

    a[0..3].* = a[5..8].*;
    assert(std.mem.eql(u8, &a, "fghdefgh"));
$ zig test slice_comptime_indexes.zig
1/1 test "slicing with comptime indexes"...OK
All 1 tests passed.

Thanks to Jimmi Holst Christensen for proposing this.

errdefer now provides syntax to access the in-flight error.


const std = @import("std");

fn perform() !void {
    errdefer |err| std.debug.assert(err == error.Overflow);
    _ = try std.math.add(u8, 255, 1);

test "errdefer with payload" {
    perform() catch return;
$ zig test errdefer_payload.zig
1/1 test "errdefer with payload"...OK
All 1 tests passed.

Thanks to Byron Heads for the proposal and LemonBoy for implementing this.

Follow-up proposal: errdefer with unreachable should allow function type to not have an error union

There are so many breaking changes that it is not feasible to list them all here. Instead, the release notes will cover contributions and high-level topics. In the future, it should be possible to use the same backend of Documentation Generation to make a tool that detects all API changes – additions, removals, and modifications.

  • Various contributors updated the standard library to use newer Zig syntax, such as anonymous enum literals, and to fix regressions from breaking language changes.
  • LemonBoy added support for the statx syscall.
  • Jonathan Marler fixed accept function API.
  • std.os.accept4: improve docs and integrate with evented I/O
  • Jonathan Marler improved TTY detection to take into account TERM=dumb.
  • std.os.dup2 makes EBADF more obvious in stack traces.
  • daurnimator updated the standard library OS bits to Linux 5.6, added missing OS bits, organized declarations, and swapped constants for Non-Exhaustive Enums.
  • Brendan Hansknecht improved big ints to use the more efficient karatsuba algorithm for multiplication.
  • daurnimator contributed LinearFifo which is useful for buffers. schroffl and Tetralux contributed improvements.
  • std.ChildProcess.spawn now has a consistent error set across targets.
  • std.io.getStdOut and related functions no longer can error. Thanks to the Windows Process Environment Block, it is possible to obtain handles to the standard input, output, and error streams without the possibility of failure.
  • dbandstra added std.math.tau constant (equivalent to 2 * pi).
  • Johan Bolmsjö improved std.testing.expectEqual to show differing pointer values, avoiding confusion when the values pointed to are the same.
  • std.heap.direct_allocator is renamed to std.heap.page_allocator, to make it more clear that this is not an appropriate general-purpose allocator.
  • Benjamin Feng size-optimized the std.sort internal binary search algorithm.
  • LemonBoy added std.sort.binarySearch. (#4337)
  • std.elf API updated to remove redundant namespacing, and integrate with std.Target.Arch.
  • Felix Queißner implemented std.testing.expectEqual for tagged unions. (#3773)
  • std.math: remove constants that should be expressions. There were four cases where the value can be represented in fewer characters with expressions, which will be guaranteed to happen at compile-time, and have the same or better precision.
  • Robin Voetter made improvements to std.sort:
    • Added isSorted.
    • Updated max to accept const slices, and added tests.
    • Updated min and max to return ?T.
    • Added argMax and argMin which return indexes rather than values.
  • std.fmt.ParseUnsignedError is now public.
  • frmdstryr put in a hot path for std.io.BufferedInStream.readByte, speeding it up by ~75% (#3858).
  • Dynamic library loading API functions are improved to follow the standard conventions with regards to filename parameters.
  • LemonBoy improved std.ChildProcess to use eventfds on Linux rather than pipe for communicating an error from child to parent process. (#819)
  • Dmitry Atamanov and daurnimator improved std.unicode.utf8ToUtf16Le to support surrogate pairs. (#3923)
  • daurnimator improved the performance of unicode functions. (#3987)
  • daurnimator updated std.meta.TagPayloadType to take the tag type of the union.
  • lukechampine implemented ChaCha20-Poly1305 AEAD. (#4011)
  • Luna added std.os.memfd_create. (#3687)
  • data-man added std.os.getrusage. (#3854)
  • Nathan Michaels added removeIndex function to PriorityQueue. (#4070)
  • Jonathan Marler added std.os.windows.WaitForSingleObject.
  • Hersh Krishna added std.math.clamp.
  • Shawn Landden made breaking changes to std.rb to make it thread-safe.
  • daurnimator updated std.mem.Allocator interface to set memory to undefined when freed (#4087). However note that it is planned to revert this and implement this as part of allocator implementations rather than the interface.
  • LemonBoy made writeByteNTimes faster and leaner.
  • nofmal added a basic Linux termios implementation.
  • Felix (xq) Queißner made std.heap.ArenaAllocator.deinit not require a mutable reference.
  • Implement std.os.faccessat for Windows.
  • Support the concept of a target not having a dynamic linker.
  • Improved handling of environment variables on Windows.
    • std.os.getenv and std.os.getenvZ have nice compile errors when not linking libc and using Windows.
    • std.os.getenvW is provided as a Windows-only API that does not require an allocator. It uses the Process Environment Block. std.process.getEnvVarOwned is improved to be a simple wrapper on top of std.os.getenvW.
    • std.process.getEnvMap is improved to use the Process Environment Block rather than calling GetEnvironmentVariableW.
    • std.zig.system.NativePaths uses process.getEnvVarOwned instead of std.os.getenvZ, which works on Windows as well as POSIX.
  • Heide Onas Auri improved std.time.Timer.lap to only read system time once. (#4533)
  • std.Thread.cpuCount on Windows uses the PEB, rather than calling GetSystemInfo from kernel32.dll. Also remove OutOfMemory from the error set.
  • Jared Miller implemented std.unicode.utf8ToUtf16LeStringLiteral which can be used to provide convenient “wide string literals”: w("foo")
  • LemonBoy added std.os.fnctl
  • Joachim Schmidt improved bigint comparison code to use math.Order rather than i8 (#4791)
  • Ilmari Autio improved std.os.getenv and related functions to be ascii-case-insensitive on Windows. (#4608)
  • joachimschmidt557 moved std.big.rational.gcd to std.big.int.gcd
  • Phil Schumann improved std.zig.parseStringLiteral to support hex and unicode escapes. (#4678)
  • std.io.readLine is removed.

    This was deceptive. It was always meant to be sort of a “GNU readline” sort of thing where it provides a Command Line Interface to input text. However that functionality did not exist and it was basically a red herring for people trying to read line-delimited input from a stream. (See I/O Streams for that.) The API is now deleted, so that people can find the proper API more easily.

    A CLI text input abstraction would be useful but may not even need to be in the standard library. The guess_number CLI game example gets by just fine by using std.fs.File.read.

  • std.os.execvpe related functions support optionally expanding argv[0] into the absolute path based on the PATH environment variable. This can be useful to work around a third party program which improperly uses argv[0] to find the path to its own executable.
  • std.os.execve had the wrong name; it should have been std.os.execvpe. This is now corrected. It is also improved to handle ENOTDIR (#3415).
  • Introduce std.os.execveZ which does not look at PATH, and uses null terminated parameters, matching POSIX ABIs. It does not require an allocator.
  • Introduce std.os.execvpeZ, which is like execvpe except it uses null terminated parameters, matching POSIX ABIs, and thus does not require an allocator.
  • Sebsatian Keller added std.math constants such as log2e and sqrt2. Note that with #425 solved these would not be needed, and would be removed.

Async I/O in 0.6.0 is still experimental, but rapidly approaching usable.

kprotty contributed significant improvements to synchronization primitives. kprotty writes:

std.Mutex uses a simple locking scheme for Linux, relies on CriticalSection for Windows and falls back to spinlocking on other platforms. There are two parts towards improving it:

1) Adaptive Locking

For high contention cases, eager blocking mutexes incur a penalty of a syscall when they may not need to. In order to address this, the mutex can spin for a little bit trying to acquire the lock similar to a spinlock before deciding to block. This improves performance when the time spent in the critical section is minimal and acquiring/releasing is done frequently. The implementation chosen for this was that of lock_futex.go from Golang 1.13 as it provides a nice balance between spinning and deciding to block (another possibility could be rust/webkit `parking_lot`). Because this implementation only needs a futex interface, it can be reused:

2) Parker API

Most synchronization primitives such as Mutexes, RwLocks, Condvars, Events and Semaphores can be built upon atomic instructions and futexes for handling blocking. Another point of this change was to setup a cross-platform futex (Parker) interface in which other primitives as listed above could be built off of. The default one provided is ThreadParker which differs from the current blocking implementation scheme:

  • On Windows, it detects at runtime whether to use WaitOnAddress (supported since Win8+ and most similar to linux futex) or NT Keyed Events (supported since WinXP+ and is the inner backing of CriticalSection). This allows the distinction between std.Mutex and std.StaticallyInitializedMutex to dissapear as it can now be initialized statically
  • On POSIX platforms, it uses pthread_cond_t for synchronization which also supports static initialization. This fares better for longer blocking critical sections compared to the spinlock default of the current std.Mutex implementation.
  • On Linux, it still uses linux_futex so not many improvements besides adaptive spinning there

Results & Future Implications

Because the Parker now has a standardized interface, one could replace ThreadParker with something like AsyncParker and reuse the synchronization primitive code for std.event synchronization objects. In order to demonstrate this, kprotty provided some example code for AsyncParker as well as a naive benchmark to test the performance of std.Mutex in comparison to this new adaptive mutex: zig-adaptive-lock

The results for high contended, small critical section cases are promising:

  • Windows 10: 7-8x speedup
  • MacOSX: 19-22x speedup
  • Linux (Pthread): 2x speedup
  • Linux (Futex): 2x speedup

These synchronization primitives are building blocks for an event loop, which is what drives async I/O.

In 0.6.0, you can start to see the ideas behind the standard library’s event loop coming together. Notably, Start Code will set up an event loop before calling main(), when the root source file defines:

pub const io_mode = .evented;

This means that main() is now allowed to use await. Same with tests, and zig test supports --test-evented-io which affects whether the test runner sets io_mode to evented or blocking.

The standard library’s async I/O integration, together with I/O Streams improvements, are now capable of passing behavior tests with --test-evented-io enabled. Standard library tests are now compiling successfully with evented I/O mode, but the event loop implementation needs to be improved in order for tests to pass. Additionally, “glue” code is needed to be added to the event loop implementation to support more operating systems. It’s not quite stable enough to be added to CI Test Coverage.

In previous versions of Zig, there was a std.event namespace for APIs that only applied to evented I/O, but in this release, many of these APIs have been removed, superceded by normal APIs integrating properly into async I/O. For example, std.event.fs is removed and all the normal std.fs (Filesystem) APIs work correctly for both blocking and evented I/O modes.

Here is an example of a simple program that writes to a file:


const std = @import("std");

pub fn main() anyerror!void {
    const file = try std.fs.cwd().createFile("hello.txt", .{});
    defer file.close();

    try file.writeAll("hellon");
$ zig build-exe write_file_blocking.zig
$ ./write_file_blocking

Looking at the strace, we can see it is quite simple:

arch_prctl(ARCH_SET_FS, 0x233190)       = 0
rt_sigaction(SIGSEGV, {sa_handler=0x22a8d0, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_RESETHAND|SA_SIGINFO, sa_restorer=0x204310}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGILL, {sa_handler=0x22a8d0, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_RESETHAND|SA_SIGINFO, sa_restorer=0x204310}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGBUS, {sa_handler=0x22a8d0, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_RESETHAND|SA_SIGINFO, sa_restorer=0x204310}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
openat(AT_FDCWD, "hello.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
write(3, "hellon", 6)                  = 6
close(3)                                = 0
exit_group(0)                           = ?

Set up thread-local storage, attach some signal handlers for debugging, openat, write, close, done. Now we enable evented I/O:


const std = @import("std");

pub const io_mode = .evented;

pub fn main() anyerror!void {
    const file = try std.fs.cwd().createFile("hello.txt", .{});
    defer file.close();

    try file.writeAll("hellon");
$ zig build-exe write_file_evented.zig
$ ./write_file_evented

I can’t paste the full strace output here, because it is too long, but I’ll highlight some of the interesting parts:

, parent_tid=[8891], tls=0x7fe36dbdf028, child_tidptr=0x7fe36dbdf000) = 8891
futex(0x7fe36dbdf000, FUTEX_WAIT, 8891, NULL <unfinished ...>
[pid  8891] futex(0x260b50, FUTEX_WAIT, 0, NULL <unfinished ...>
[pid  8891] <... futex resumed>)        = 0
[pid  8891] openat(AT_FDCWD, "hello.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 20
[pid  8891] epoll_ctl(18, EPOLL_CTL_ADD, 17, {EPOLLIN|EPOLLOUT|EPOLLONESHOT|EPOLLET, {u32=1841168864, u64=140614775472608}}) = 0
[pid  8891] futex(0x260b50, FUTEX_WAIT, 0, NULL <unfinished ...>
[pid  8891] <... futex resumed>)        = 0
[pid  8891] write(20, "hellon", 6)     = 6
[pid  8891] epoll_ctl(18, EPOLL_CTL_MOD, 17, {EPOLLIN|EPOLLOUT|EPOLLONESHOT|EPOLLET, {u32=1841168864, u64=140614775472608}}) = 0
[pid  8891] futex(0x260b50, FUTEX_WAIT, 0, NULL <unfinished ...>
[pid  8891] <... futex resumed>)        = -1 EAGAIN (Resource temporarily unavailable)
[pid  8891] close(20 <unfinished ...>
[pid  8891] <... close resumed>)        = 0
[pid  8891] exit(0 <unfinished ...>
[pid  8891] <... exit resumed>)         = ?
<... futex resumed>)                    = -1 EAGAIN (Resource temporarily unavailable)
exit_group(0)                           = ?

Here we can see that a separate thread is created, which ends up doing the file system I/O. Some operating systems such as Linux do not have async file system support, and so the technique used by evented I/O libraries is to have a thread pool for doing blocking operations. In this way, you can make anything async by giving the task to another thread.

Now that Linux has io_uring, this could be improved. With Zig’s OS Version Ranges, the event loop code could be improved to detect if io_uring is within the target OS version range, and take advantage of it if so. If the minimum OS version is high enough, the non-io_uring code could be omitted, and if the maximum OS version is low enough, the io_uring code could be omitted. If the OS version range includes both, then the code should try io_uring, and fall back at runtime to a non-io_uring strategy.

Anyway, the point here is that because evented I/O is enabled, it now becomes meaningful to express concurrency:


const std = @import("std");

pub const io_mode = .evented;

pub fn main() anyerror!void {
    var a_frame = async doA();
    var b_frame = async doB();

    try await a_frame;
    try await b_frame;

fn doA() !void {
    const file = try std.fs.cwd().createFile("a.txt", .{});
    defer file.close();

    try file.writeAll("An");

fn doB() !void {
    const file = try std.fs.cwd().createFile("b.txt", .{});
    defer file.close();

    try file.writeAll("Bn");
$ zig build-exe concurrent.zig
$ ./concurrent

I’ll refrain from pasting more strace output here, but now we can start to see things happening in parallel (depending on the OS support for async file system I/O, or the file system thread pool size).

Finally, I want to point out one crucial point about Zig’s async I/O. It still works if you switch back to blocking I/O:


const std = @import("std");

pub fn main() anyerror!void {
    var a_frame = async doA();
    var b_frame = async doB();

    try await a_frame;
    try await b_frame;

fn doA() !void {
    const file = try std.fs.cwd().createFile("a.txt", .{});
    defer file.close();

    try file.writeAll("An");

fn doB() !void {
    const file = try std.fs.cwd().createFile("b.txt", .{});
    defer file.close();

    try file.writeAll("Bn");
$ zig build-exe async_blocking.zig --release-fast
$ ./async_blocking

This time I will show the strace since it’s very short:

arch_prctl(ARCH_SET_FS, 0x203cf0)       = 0
openat(AT_FDCWD, "a.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
write(3, "An", 2)                      = 2
close(3)                                = 0
openat(AT_FDCWD, "b.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
write(3, "Bn", 2)                      = 2
close(3)                                = 0
exit_group(0)                           = ?

You can see that the async stuff folded into simple, linear, blocking code.

This is a big deal. It means that Zig code can express concurrency, yet be reusable in both a blocking I/O and an evented I/O environment. There is no “async-std”. The Zig Standard Library supports both async and blocking I/O with the same codebase.

Thanks to Benjamin Feng, Vexu, daurnimator, and Timon Kruiper for contributions related to this feature.

LemonBoy made a number of improvements to Zig’s debug info code:

  • Fix stack iteration stop condition, and further improve the frame-walking strategy. The code is now stable enough not to cause panics during the call frame walking.
  • Make the leb module available to non-std code
  • Don’t generate any type info for void return types. Closely matches what the LLVM debug emitter expects, the generated DWARF infos are now standard-compliant.
  • Show a nice error message on SIGBUS.
  • Support handling DWARF version 3.
  • Properly handle multiple threads panicking at the same time. Instead of walking all over each other, both stack traces will be printed sequentially.

Now, stack traces work even in release builds!

Additionally, Rocknest brought Windows segfault handler code on par with POSIX. (#4319)

Formatted printing is now documented fairly well thanks to Felix Queißner (#3474).

std.fmt.format is modified for the new I/O Streams API, and because of Tuples Added, Var Args Removed. It now operates cleanly with Async I/O.

Formatting capabilities were improved:

An API overhaul is still planned.

Thanks to daurnimator, LemonBoy, Benjamin Feng, Felix Queißner, Michael Dusan, Nathan Michaels, data-man, frmdstryr, markfirmware, shiimizu, and vegecode for contributions related to this feature.

The bad news: there were breaking changes to I/O streams and you have to update your code.

The good news:

  • The new API is objectively simpler and more ergonomic.
  • Empirically, it produces significantly faster runtime code.
  • It works cleanly with Async I/O.

Example code using the old streams API (lifted from my advent-of-code repository):

const std = @import("std");

pub fn main() anyerror!void {
    var stdin_unbuf = std.io.getStdIn().inStream();
    const in = &std.io.BufferedInStream(@TypeOf(stdin_unbuf).Error).init(&stdin_unbuf.stream).stream;

    var sum: u64 = 0;
    var line_buf: [50]u8 = undefined;
    while (try in.readUntilDelimiterOrEof(&line_buf, 'n')) |line| {
        if (line.len == 0) break;
        const module_mass = try std.fmt.parseInt(u64, line, 10);
        const fuel_required = (module_mass / 3) - 2;
        sum += fuel_required;

    const out = &std.io.getStdOut().outStream().stream;
    try out.print("{}n", .{sum});

New streams API:

const std = @import("std");

pub fn main() anyerror!void {
    const in = std.io.bufferedInStream(std.io.getStdIn().inStream()).inStream();

    var sum: u64 = 0;
    var line_buf: [50]u8 = undefined;
    while (try in.readUntilDelimiterOrEof(&line_buf, 'n')) |line| {
        if (line.len == 0) break;
        const module_mass = try std.fmt.parseInt(u64, line, 10);
        const fuel_required = (module_mass / 3) - 2;
        sum += fuel_required;

    const out = std.io.getStdOut().outStream();
    try out.print("{}n", .{sum});

And unlike before, it works if you utilize Async I/O with:

pub const io_mode = .evented;

InStream, OutStream, and SeekableStream were already generic across error sets, it’s not really worse to make them generic across the vtable as well, which is what this change did.

See #764 for the open issue acknowledging that using generics for these abstractions is a design flaw.

See #130 for the efforts to make these abstractions non-generic.

This also changes the OutStream API so that write returns number of bytes written, and writeAll is the one that loops until the whole buffer is written.

The file system APIs are starting to come together. I think this is an area where Zig really shines.

There were many breaking changes during this release cycle, however they are all working directly towards a clear vision:

  • Operations which are relative to a directory, should be methods of the std.fs.Dir namespace.
  • Filesystem functions should not take unnecessary Allocator parameters.

These goals go hand-in-hand. By avoiding string manipulation of paths, Zig code simultaneously avoids the need for allocators, prevents error.NameTooLong errors from the kernel, and avoids TOCTOU bugs.

Here is an example from zig build of what it looks like to “install” all the files from a source directory into a destination directory:

var src_dir = try std.fs.cwd().openDir(build_output_dir, .{ .iterate = true });
defer src_dir.close();

var dest_dir = try std.fs.cwd().openDir(output_dir, .{});
defer dest_dir.close();

var it = src_dir.iterate();
while (try it.next()) |entry| {
    _ = try src_dir.updateFile(entry.name, dest_dir, entry.name, .{});

No allocator needed. Resource management is trivial. Works correctly when the file paths are so deeply nested that they would be longer than PATH_MAX. Avoids copying the file when the destination is already up-to-date. When a file copy does happen, it uses sendfile if supported, so that the file copy happens in the kernel. The ignored return value there is whether or not the destination file was found to be out-of-date, so that the code could know which files were fresh and which were stale.

When upgrading your filesystem code to Zig 0.6.0, you should ask yourself the question, can I rework the logic to avoid path manipulation?

I won’t list all the functions added, removed, and changed here because it is too many, but I will highlight some notable changes:

  • sendfile support. File system implementations which copy files now use this when possible.
  • Better support for writev, readv, pwritev, preadv, etc.
  • Various APIs and implementations improved to take advantage of directory handles.
  • Error sets of various functions are improved to no longer have some kinds of errors, such as error.OutOfMemory, or in the case of deleting a file, error.NoSpaceLeft.
  • The new convention for APIs with null-terminated path parameters is the ‘Z’ suffix rather than ‘C’ suffix.
  • Some APIs are modified to take “options” structs which have default fields values.
  • inode number / file index is exposed into File.Stat.
  • Cross-platform file locking flags are supported.
  • fs.File supports setEndPos to grow/shrink the file size as needed. (#4716)
  • Filesystem APIs now integrate with Async I/O, including automatically performing operations on a file system thread pool to avoid blocking.

Thank you to contributors LeRoyce Pearson, Jonathan S, daurnimator, LemonBoy, Terin Stock, dimenus, and stratact.

The basics of networking are starting to come together, at least on POSIX.

Added std.net.getAddressList – basic DNS address resolution.

Added std.net.StreamServer.

Added std.net.tcpConnectToHost.

Added std.net.tcpConnectToAddress.

Added std.net.Address.parseIp which supports IPv4 and IPv6.

Added std.net.Address.parseExpectingFamily which additionally accepts a family parameter.

std.os IPPROTO constants are canonicalized.

Example of a simple TCP chat server using Async I/O.

Thank you to contributors Luna, Vexu, Jonathan Marler, Sebastian, frmdstryr, and LemonBoy.

  • Added std.json.WriteStream.writeJson.
  • std.json.Value: added dumpStream(), utilize WriteStream for dump().
  • std.json.Token is now a union(enum).
  • Improve json.unescapeString to no longer take an allocator
  • Add json.stringify to encode arbitrary values to JSON
  • Add json.parse to automatically decode json into a struct
  • Add json.WriteStream.stringify
  • Disallow overlong and out-of-range UTF-8
  • Support unescaping JSON strings
  • Surrogate pair support
  • Implement copy_strings=false

Here is an example of parsing into an arbitrary struct:


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

test "parse into struct with misc fields" {
    const options = json.ParseOptions{ .allocator = std.testing.allocator };
    const T = struct {
        int: i64,
        float: f64,
        @"withescape": bool,
        @"withąunicode😂": bool,
        language: []const u8,
        optional: ?bool,
        default_field: i32 = 42,
        static_array: [3]f64,
        dynamic_array: []f64,

        const Bar = struct {
            nested: []const u8,
        complex: Bar,

        const Baz = struct {
            foo: []const u8,
        veryComplex: []Baz,

        const Union = union(enum) {
            x: u8,
            float: f64,
            string: []const u8,
        a_union: Union,
    const r = try json.parse(T, &json.TokenStream.init(
          "int": 420,
          "float": 3.14,
          "withescape": true,
          "withu0105unicodeud83dude02": false,
          "language": "zig",
          "optional": null,
          "static_array": [66.6, 420.420, 69.69],
          "dynamic_array": [66.6, 420.420, 69.69],
          "complex": {
            "nested": "zig"
          "veryComplex": [
              "foo": "zig"
            }, {
              "foo": "rocks"
          "a_union": 100000
    ), options);
    defer json.parseFree(T, r, options);
    std.testing.expectEqual(@as(i64, 420), r.int);
    std.testing.expectEqual(@as(f64, 3.14), r.float);
    std.testing.expectEqual(true, r.@"withescape");
    std.testing.expectEqual(false, r.@"withąunicode😂");
    std.testing.expectEqualSlices(u8, "zig", r.language);
    std.testing.expectEqual(@as(?bool, null), r.optional);
    std.testing.expectEqual(@as(i32, 42), r.default_field);
    std.testing.expectEqual(@as(f64, 66.6), r.static_array[0]);
    std.testing.expectEqual(@as(f64, 420.420), r.static_array[1]);
    std.testing.expectEqual(@as(f64, 69.69), r.static_array[2]);
    std.testing.expectEqual(@as(usize, 3), r.dynamic_array.len);
    std.testing.expectEqual(@as(f64, 66.6), r.dynamic_array[0]);
    std.testing.expectEqual(@as(f64, 420.420), r.dynamic_array[1]);
    std.testing.expectEqual(@as(f64, 69.69), r.dynamic_array[2]);
    std.testing.expectEqualSlices(u8, r.complex.nested, "zig");
    std.testing.expectEqualSlices(u8, "zig", r.veryComplex[0].foo);
    std.testing.expectEqualSlices(u8, "rocks", r.veryComplex[1].foo);
    std.testing.expectEqual(T.Union{ .float = 100000 }, r.a_union);
$ zig test json_parse_struct.zig
1/1 test "parse into struct with misc fields"...OK
All 1 tests passed.

Thank you daurnimator, Sebastian Keller, xackus, hryx, and Lachlan Easton for related contributions.

In the previous release of Zig (0.5.0), the std.Target.Os enum recognized the following operating systems:


This list was the list of targets that LLVM supported + zen(a hobby OS not maintained for over 1 year) + UEFI.

It doesn’t make sense to put every hobby OS into this list, but it does make sense to support them! It should be possible for people to take advantage of Zig’s cross platform abstractions without having to get support for their hobby OS upstreamed into Zig.

Zig 0.6.0 does two things:

  • Add an other tag to std.Target.Os
  • Support an OS layer struct exposed in the root source file (next to pub fn main())

This allows hobby OS developers to maintain a zig package that makes the Zig Standard Library support their OS. Application developers could use it like this:

pub const os = @import("my_hobby_os_package");

pub fn main() void {

Next, standard library abstractions will detect when they should utilize this. If the operating system is POSIX compliant, then many things will Just Work. For example, std.os.read was defined like this:

pub fn read(fd: fd_t, buf: []u8) ReadError!usize {
    if (builtin.os == .windows) {
        return windows.ReadFile(fd, buf);

    if (builtin.os == .wasi and !builtin.link_libc) {
        const iovs = [1]iovec{iovec{
            .iov_base = buf.ptr,
            .iov_len = buf.len,

        var nread: usize = undefined;
        switch (wasi.fd_read(fd, &iovs, iovs.len, &nread)) {
            0 => return nread,
            else => |err| return unexpectedErrno(err),

    while (true) {
        const rc = system.read(fd, buf.ptr, buf.len);
        switch (errno(rc)) {
            0 => return @intCast(usize, rc),
            EINTR => continue,
            EINVAL => unreachable,
            EFAULT => unreachable,
            EAGAIN => if (std.event.Loop.instance) |loop| {
            } else {
                return error.WouldBlock;
            EBADF => unreachable, 
            EIO => return error.InputOutput,
            EISDIR => return error.IsDir,
            ENOBUFS => return error.SystemResources,
            ENOMEM => return error.SystemResources,
            ECONNRESET => return error.ConnectionResetByPeer,
            else => |err| return unexpectedErrno(err),
    return index;

system in this refered to:

pub const system = if (builtin.link_libc) std.c else switch (builtin.os) {
    .macosx, .ios, .watchos, .tvos => darwin,
    .freebsd => freebsd,
    .linux => linux,
    .netbsd => netbsd,
    .dragonfly => dragonfly,
    .wasi => wasi,
    .windows => windows,
    .zen => zen,
    else => struct {},

else => struct{}, is modified to look for @import("root").os if it is provided. With this modification, as long as the OS package defines all the constants (such as fd_t and EISDIR), then std.os.write would end up calling the write function from the hobby OS package, and everything Just Works.

Some abstractions do not work so smoothly; in this case there is code that looks something like this:

fn doTheOsThing() void {
    if (@hasDecl(root, "os") and @hasDecl(root.os, "doTheOsThing")) {
        return @import("root").os.doTheOsThing();

Note there is not even a check for “other” here. Allowing applications to override fundamental OS functions is useful on any operating system.

Now that this is implemented, Zig has first class support for all operating systems. The main difference between upstream-recognized OSes and “other” OSes would be where the support is maintained – upstream zig std lib, or in a third party “OS layer” package.

With this new feature, upstream support for Zen hobby OS is removed. This has the additional benefit of clearing up some confusion, since there is already a Zen programming language.

This feature is still experimental, and contributions are welcome if you need to tweak the std lib to get it working for your hobby OS use case.

Thanks Christine Dodrill and Noam Preil for related contributions.

Follow-up proposal: BYO os should work at the zig level

Apologies – when I first created the ArrayList API, I got it backwards. I made items the slice of allocated memory, and you had to call a function to get the slice of valid objects.

Now, the items field is safe to use directly, and is always the slice of valid objects, and the capacity is maintained separately.

This breaks callsites of ArrayList but it removes a footgun from this API. It also allows removing a bunch of no-longer-needed API.

Iterator API is removed from std.ArrayList, since it is now possible to use a for loop on the items field.


  • There is now a function appendNTimes (#4460).
  • Many functions are deprecated, such as toSlice and toSliceConst, and at. Instead, access the items slice directly.
  • Added outStream – creates a stream to append to the ArrayList.

Thanks daurnimator, xackus, Bas, MCRusher, and Benoit Giannangeli for related contributions.

Special thanks to daurnimator for making the case to rename std.Buffer to std.ArrayListSentineled, and replace usages of that API with usages to ArrayList where applicable.

  • std.mem.len no longer takes a type parameter, and uses Sentinel-Terminated Pointers.
  • Added std.mem.span.
  • Added std.mem.spanZ.
  • Added std.mem.zeros which zero-initializes types which have well-defined memory layouts. (#4544)
  • Added std.mem.Allocator.allocSentinel.
  • Added std.mem.indexOfSentinel.
  • Rename std.mem.separate to std.mem.split.
  • Rename std.mem.Compare to std.math.Order.
  • Rename std.mem.compare to std.mem.order.
  • Added std.math.order.
  • Deprecated std.mem.toSlice.
  • Deprecated std.mem.toSliceConst.

Thanks to Bas van den Berg, Emeka Nkurumeh, Jonathan Marler, Michaël Larouche, Sebastian, Timon Kruiper, daurnimator, and xackus for related contributions.

lukechampine added an AES implementation to std.crypto. data-man improved the code, replacing variables with constants. lukechampine additionally added support for AES-CTR.

daurnimator added a Gimli based PRNG to std.rand, added gimli to the crypto hash benchmark, and added AEAD modes for Gimli. (#4369)

Jay Petacat added a BLAKE3 hashing algorithm (#4366). Jay writes:

This is a translation of the official reference implementation with few other changes. The bad news is that the reference implementation is designed for simplicity and not speed, so there’s a lot of room for performance improvement. The good news is that, according to the crypto benchmark, the implementation is still fast relative to the other hashing algorithms:

             md5: 430 MiB/s
            sha1: 386 MiB/s
          sha256: 191 MiB/s
          sha512: 275 MiB/s
        sha3-256: 233 MiB/s
        sha3-512: 137 MiB/s
         blake2s: 464 MiB/s
         blake2b: 526 MiB/s
          blake3: 576 MiB/s
        poly1305: 1479 MiB/s
        hmac-md5: 653 MiB/s
       hmac-sha1: 553 MiB/s
     hmac-sha256: 222 MiB/s
          x25519: 8685 exchanges/s

J.W fixed index out of bounds logic in some hashing algorithms.

Logic involving startup code has been moved from being hard-coded in the compiler to comptime logic inside the start.zig file from the standard library.

Additionally, the startup code is un-special-cased.

Previously, the compiler had special logic to determine whether to include the startup code, which was in std/special/start.zig. Now, the file is moved to std/start.zig, and there is no special logic in the compiler. Instead, the standard library unconditionally imports the start.zig file, which then has a comptime block that does the logic of determining what, if any, start symbols to export. Instead of start.zig being in its own special package, it is just another normal file that is part of the standard library.

std.builtin.TestFn is now part of the standard library rather than specially generated by the compiler.

Additionally, some minor changes to Thread-Local Storage handling (#4807):

  • Always allocate an info block per-thread so that libc can store important stuff there.
  • Respect ABI-mandated alignment in more places.
  • Nicer code, use slices/pointers instead of raw addresses whenever possible.

Thanks to LemonBoy, Vexu, Jared Miller, and Nick Erdmann for contributions related to this.

  • Language reference updated to take into account Language Changes.
  • Language reference makes it more obvious that if is an expression.
  • New section: Function Parameter Type Inference
  • Clarify allowzero interaction with optional pointers.
  • Language reference has table of contents in a separate column on large displays.

Thanks to Vexu, xackus, LemonBoy, data-man, Benjamin Feng, Emilio G. Cota, Jonathan Marler, MateuszOkulus, Matt Keeter, Maximilian Hunt, Nathan Michaels, Nick Erdmann, Robin Voetter, Shritesh, hryx, momumi, and yvt for contributions to the language reference.

This feature is still experimental.

There is a new -fdump-analysis command line option, which creates a $NAME-analysis.json file with all of the finished semantic analysis that the stage1 compiler produced. It contains types, packages, declarations, and files.

This feature can be used to power IDE integration features until such time as the self-hosted compiler is available and supports such features more directly.

Additionally, there is a proof-of-concept documentation generation feature (#21):

The new -femit-docs CLI option outputs:

  • doc/index.html
  • doc/data.js
  • doc/main.js

In this strategy, we have 1 static html page and 1 static javascript file, which loads the semantic analysis dump directly and renders it using DOM manipulation.

There is now experimental std lib documentation.

There are still some missing features. For example, it does not handle generic types ideally, multiple packages are not handled well, and some URLs are broken. Additionally, the merge_anal_dumps tool is not yet complete, so generated documentation can only apply to a single build configuration. For example, if the generated docs targeted Windows, then Linux-only functions will not be shown in the documentation, and vice-versa. Due to Zig’s lazy analysis many declarations are not semantically analyzed, causing them to be omitted in the generated documentation. These are all open issues to be addressed.

Despite the flaws it can still be a useful way to explore the Standard Library. It has motivated some contributions to improve doc comments to various APIs:

  • Felix Queißner added documentation for std.fmt.format grammar and customization. (#3474)
  • Nathan Michaels added docs for ArrayList, failing_allocator, and Allocator. (#3540)
  • Nathan Michaels documented std.Mutex.
  • Nathan Michaels documented PriorityQueue.
  • Josh Wolfe added documentation for mutable HashMap KV pointers.
  • Felix (xq) Queißner added documentation to std.atomic.Queue.

Thank you Rocknest, Timon Kruiper, Henry Wu, Felix Queißner, Vexu, dtw-waleee, pfg, and xackus for related contributions.

Related to Async I/O, resuming non-suspended functions now has runtime safety (#3469):


const std = @import("std");

fn foo() void {
    var f = async bar(@frame());

fn bar(frame: anyframe) void {
    suspend {
        resume frame;

pub fn main() void {
    _ = async foo();
$ zig build-exe bad_resume.zig
$ ./bad_resume
resumed a non-suspended function
/home/andy/dev/www.ziglang.org/docgen_tmp/bad_resume.zig:3:1: 0x22ea7c in foo (bad_resume)
fn foo() void {
/home/andy/dev/www.ziglang.org/docgen_tmp/bad_resume.zig:10:9: 0x230419 in bar (bad_resume)
        resume frame;
/home/andy/dev/www.ziglang.org/docgen_tmp/bad_resume.zig:4:13: 0x22ea4f in foo (bad_resume)
    var f = async bar(@frame());
/home/andy/dev/www.ziglang.org/docgen_tmp/bad_resume.zig:16:9: 0x22a875 in main (bad_resume)
    _ = async foo();
/home/andy/Downloads/zig/lib/std/start.zig:243:22: 0x20476f in std.start.posixCallMainAndExit (bad_resume)
/home/andy/Downloads/zig/lib/std/start.zig:123:5: 0x20454f in std.start._start (bad_resume)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
(process terminated by signal)

Additionally the following safety checks have been added:

  • Slicing operator with a sentinel mismatch.
  • Shifting by an amount greater than the type size (for non-power-of-two integers).
  • @intToPtr with misaligned address.
  • Slicing a null C pointer.

Thanks LemonBoy, Alexandros Naskos, and xackus for related contributions.

Much more safety is planned. This was a relatively quiet release cycle as far as safety is concerned, despite the ambitions of the 0.5.0 roadmap.

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

The most notable changes to zig build have to do with the new Target Details. See that section for how to use the new setTarget API.

One new trick that may be useful to Windows developers is to set the default target to be native-native-gnu. This will use the native OS and version range as well as the native CPU, but take advantage of mingw-w64 rather than trying to integrate with system MSVC. This is more likely to “just work” for all your project contributors, because it eliminates a problematic system dependency.

Additionally the following improvements were made:

  • Bumped default max exec output size to 400 KB. (#3415)
  • addIncludeDir does -I instead of -isystem.
  • Initial support for using vcpkg libraries. However it is not integrated automatically yet.
  • Fixed failure to recognize the PATH environment variable due to incorrectly treating the environment variable as case sensitive on Windows.
  • Rework and improve some of the zig build steps
    • RunStep gains ability to compare output and exit code against expected values. Multiple redundant locations in the test harness code are replaced to use RunStep.
    • WriteFileStep gains ability to write more than one file into the cache directory, for when the files need to be relative to each other. This makes usage of WriteFileStep no longer problematic when parallelizing zig build.
    • Added CheckFileStep, which can be used to validate that the output of another step produced a valid file. Multiple redundant locations in the test harness code are replaced to use CheckFileStep.
    • Added TranslateCStep. This exposes zig translate-c to the build system, which is likely to be rarely useful by most Zig users; however Zig’s own test suite uses it both for translate-c tests and for run-translated-c tests.
    • Refactored ad-hoc code to handle source files coming from multiple kinds of sources, into std.build.FileSource.
    • Added std.build.Builder.addExecutableFromWriteFileStep.
    • Added std.build.Builder.addExecutableSource.
    • Added std.build.Builder.addWriteFiles.
    • Added std.build.Builder.addTranslateC.
    • Added std.build.LibExeObjStep.addCSourceFileSource.
    • Added std.build.LibExeObjStep.addAssemblyFileFromWriteFileStep.
    • Added std.build.LibExeObjStep.addAssemblyFileSource.
  • -- can be used to pass args to zig build commands.
  • -D now supports “list” type options.
  • Nesting package dependencies is now supported.
  • InstallRawStep is available to do a similar job to objcopy. It can be used with exe.installRaw("kernel.bin"); where exe is a LibExeObjStep. (#2826)
  • zig build now correctly handles multiple output artifacts (#4733, #4735)
    Previously the zig build system incorrectly assumed that the only build artifact was a binary. Now, when you enable the cache, only the output dir is printed to stdout, and the zig build system iterates over the files in that directory, copying them to the output directory.
  • -ffunction-sections switch exposed to zig build scripts.
  • The default stdin behavior of RunStep is now .Inherit. Since this is a breaking change, previous behavior can be restored by doing: RunStep.stdin_behavior = .Ignore.
  • Configuring the subsystem is exposed to build scripts.

Thanks to Benjamin Feng, David Cao, Layne Gustafson, LemonBoy, Michael Dusan, Michaël Larouche, Nick Erdmann, Noam Preil, Sahnvour, Timon Kruiper, Valentin Anger, dbandstra, emekoi, frmdstryr, meme, mogud, pwzk, stratact, syscall0, and xackus for related contributions.

In this release, zig fmt performs a few automatic syntax upgrades for you, for example, renaming @typeOf to @TypeOf, as well as updating to the new callconv syntax.

  • Updates to support Language Changes.
  • Trailing comma is respected for builtin calls, field declarations, and error set declarations.
  • Handle declarations in line with the opening brace.
  • Allow single newline before doc comments.

Thanks to LemonBoy, Vexu, Robin Voetter, Brendan Hansknecht, Michael Raymond, and xackus for contributions to zig fmt.

Many people discovered this feature from the blog post, `zig cc`: a Powerful Drop-In Replacement for GCC/Clang.

In summary, since Zig links against libclang, Zig has the ability to act as a C compiler. And since Zig ships with libc, it has the ability to act as a cross-compiling C compiler. This feature has been available since 0.4.0, however, what’s new is the sub-command, zig cc, which has the ability to parse C compiler flags.

In this release, zig cc has full compatibility with Clang’s command line options. Clang is not invoked directly; some components are replaced with Zig’s own. For example, Zig provides all the include paths for libc, and acts as the linker driver. Zig translates the semantics of the arguments to its own internal build logic. Clang options that Zig is not aware of are forwarded to Clang directly. Some parameters are handled specially.

Since the writing of the blog post a few weeks ago, this feature has been further improved with more flag integration, as well as the ability to provide -lc++. The sub-command zig c++ is added for convenience.

Yes, that’s right, Zig now acts as a C++ cross-compiler as well.


#include <iostream>

int main() {
    std::cout << "Hello World!" << std::endl;
    return 0;

My host is currently x86-64 linux, but I’ll build it for Windows as well as RISC-V:

$ zig c++ -o hello hello.cpp -lc
$ ./hello
Hello World!
$ zig c++ -o hello hello.cpp -lc -target x86_64-windows-gnu
$ wine64 hello.exe
Hello World!
$ zig c++ -o hello hello.cpp -lc -target riscv64-linux
$ qemu-riscv64 ./hello
Hello World!

Thanks to this new ability, Zig is now able to bootstrap itself. Bootstrapping is not to be confused with self-hosting.

I forgot to mention in the blog post, when you don’t pass any optimization flags to zig cc, Zig determines that Debug Mode is appropriate, and enables Clang’s UBSAN.

People are finding out now that their C code has undefined behavior: #4830 #4965

zig cc is intentionally an interface to the Zig compiler, a bit higher level than using Clang directly. With no optimization flags specified, zig cc infers debug mode. As you know from writing Zig code, debug mode has safety checks to prevent undefined behavior at runtime. This applies for C code as well, taking advantage of clang’s UBSAN. The fact that debug mode is “default” is entirely intentional. I expect this to identify many bugs in existing codebases as people use zig cc out of convenience or curiosity and their code gets vetted by UBSAN for the first time.

Note that the presence of -O2,-O3 will cause zig to select release-fast, -Os will cause zig to select release-small, and optimization flags plus -fsanitize=undefined will cause zig to select release-safe.

I wrote a FAQ entry so that people can easily link to it to help explain when this situation comes up: Why do I get illegal instruction when using with zig cc to build C code?

So – is it production ready?

As long as you are aware of the open issues, and you have tested zig cc for your use case, and it successfully builds your project, then it should be safe to use zig cc with 0.6.0. It’s not expected for this feature to change much; it is already very nearly in its final form.

However, this is not a guarantee. Until Zig 1.0, the project reserves the right to make breaking changes as necessary.

Nim has added zig cc as one of the C compiler backend options.

Thank you to Michael Dusan and Ryan Liptak for contributions related to this feature.

Because cross-compiling is a first-class use case, Zig provides libc whenever possible, rather than depending on the host libc.

Zig ships with the source code to musl. When the musl C ABI is selected, Zig builds musl from source for the selected target.

This release updates the bundled musl source code to v1.2.0.

With this release, Zig no longer has any patches against upstream.

Zig gains the ability to target glibc 2.31 in addition to the other 41 glibc versions.

In this release, Zig’s glibc support is improved to additionally provide -lutil and symbols provided by the dynamic linker. (#4748)

Zig ships with the source code to mingw-w64. When targeting *-windows-gnu and linking against libc, Zig builds mingw-w64 from source for the selected target.

This release updates the bundled mingw-w64 source code to v7.0.0.


  • Linking is now aware -lm is provided by mingw-w64.
  • Zig additionally provides -lpsapi, -luuid, and -lshlwapi. (#3711)

During this release cycle, all of the Clang C++ API that Zig’s translate-c feature relies on has been extracted into:

  • zig_clang.h (C)
  • zig_clang.cpp (C++)

Next, zig_clang.zig was added to match the zig_clang.h C ABI.

Once translate_c.cpp was fully updated to use zig_clang.h instead of the C++ includes, compilation of that source file went from 5 seconds to 0.5 seconds.

At this point the self-hosting effort began. I started src-self-hosted/translate-c.zig and hooked up the translate-c test suite to support adding cases that are intended to pass in the new implementation.

Slowly the contributions started coming in and gradually improving self-hosted translate-c, and it started passing more and more test cases.

After a few individual contributions, Vexu got the hang of things, and finished the entire self-hosted implementation of translate-c, ultimately deleting the 5,000 line C++ implementation in one final blow.

All the previous test cases pass now, plus new ones, and there is even a new kind of test, zig build test-run-translated-c, which attempts to compile and run the Zig code that was translated from C code.

Vexu didn’t stop there. He implemented a full C tokenizer and a partial C AST parser, which are both now available in the Standard Library in the std.c namespace. As a result, Zig’s support for macro translation is much improved in 0.6.0.

This is as big deal for Self-Hosted Compiler Progress, because it means that contributions to Zig’s translate-c feature apply not only to the stage1 compiler, but to the (incomplete) self-hosted compiler as well.

I won’t list translate-c improvements or bug fixes because they are too numerous, but the following things stick out:

  • There is now a zig build step for translate-c.
  • @cImport now automatically caches results, so subsequent builds are fast.
  • There is now an Improving Translate-C section in CONTRIBUTING.md, to help contributors get started.

Additional thanks to LemonBoy, Merlyn Morgan-Graham, Feix Weiglhofer, Josh Wolfe, Lachlan Easton, Layne Gustafson, Michael Dusan, Rocknest, Tadeo Kondrak, frmdstryr, travisstaloch, and via for contributions related to this feature.

Although there has been no progress during this release cycle directly on self-hosted semantic analysis and code generation, there has been significant progress towards self-hosting in other areas:

  • C Translation is now self-hosted. To be clear, it still uses libclang to parse C code, however, 5,000 lines of C++ code were deleted in favor of a zig implementation.
  • zig targets is now self-hosted, and outputs JSON.
  • Native system libc detection is now self-hosted.
  • Native system dynamic linker detection is now self-hosted.
  • Native system include search path detection is now self-hosted.
  • Native system CPU Features detection is now self-hosted for x86 (contributions welcome for other architectures).

More and more of the compiler is moving to be implemented in Zig rather than in C++. The C++ percent is shrinking and Zig percent is increasing. However, bootstrapping remains forever a fixed, four-step process.

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 GNU 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 spent two days on an epic bug sleuthing quest, which was only reproducible inside docker, on AArch64, and finally managed to solve the problem. An unrelated change to compiler-rt had exposed a latent bug, where __floatunditf was accidentally defined with parameter type u128 rather than u64. Typos on the ABI boundary are especially nasty!

In addition to that, LemonBoy contributed most of the improvements to compiler-rt during this release cycle.

  • compiler_rt and freestanding libc are always built with optimizations, even when used by Debug Mode Zig code.
  • Fix __stack_chk_guard emitted even when not linking libc.
  • Add __clzsi2 – it is required for using std.fmt.format on some ARM architecture.
  • Add clear_cache for aarch64, arm32-linux, and more.
  • Remove x86/Windows name mangling hack
  • Fix stack-probe symbol redefinition
  • Add more compiler-rt functions for ARM platform
  • Use the correct calling convention for AEABI intrinsics
  • Fix div builtins to use the correct calling convention
  • Remove useless wrappers around f32/f64 aeabi builtins
  • Export MSVC builtins unconditionally
  • Port __mulsi3 builtin
  • Export the AEABI builtins when targeting thumb
  • Add __divtf3
  • Fix __floatunditf
  • Implement all the shift builtins
  • Add the __atomic family of builtins
  • Separate max size allowed for load/store and CAS

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

Additional thanks to Michael Dusan, daurnimator, Michaël Larouche, and Timon Kruiper for related contributions.

Zig uses a Continuous-Integration system to run Zig’s test suite in various environments. In this release cycle, the system gained more test coverage:

  • Full Aarch64 CI test coverage using Drone CI. This includes non-libc, musl, and glibc.
  • QEMU test coverage for i386-linux-none behavior tests and standard library tests.
  • QEMU test coverage for i386-linux-musl behavior tests and standard library tests.
  • QEMU test coverage for mipsel-linux-none behavior tests and standard library tests.
  • QEMU test coverage for mipsel-linux-musl behavior tests and standard library tests.
  • QEMU test coverage for riscv64-linux-none behavior tests and standard library tests.
  • QEMU test coverage for riscv64-linux-musl behavior tests and standard library tests.
  • The Azure Windows CI server additionally runs i386-windows-msvc behavior tests and standard library tests.
  • The Azure Windows CI server additionally runs i386-windows-gnu (building and linking mingw-w64) behavior tests and standard library tests.
  • The Azure Windows CI server additionally runs x86_64-windows-gnu (building and linking mingw-w64) behavior tests and standard library tests.
  • CI tests that building in a mingw-w64 environment succeeds. Thanks emekoi

Thanks to Michael Dusan for getting QEMU building statically into a nice tarball that the CI server can download, extract, and run. This allows us to use a newer QEMU version than available in the Ubuntu repositories, on which SIMD tests pass.

During this release cycle, many bugs were discovered to have been fixed as a side-effect of other changes. Rather than simply closing these bug reports, regression test cases were added for them.

C Translation now has a new category of test: “run-translated-c”

Benjamin Feng moved std.debug.global_allocator to std.testing.allocator, and improved it to add leak checking. This caught several leaks in the Standard Library. daurnimator helped migrate more tests to use std.testing.allocator.

More FreeBSD tests are now passing (#3210, #4455).

More RISC-V tests are now passing (#3338).

More tests are now passing since upgrading to LLVM 10 (#4492, #4724).

The large portion of Zig compiler that is (currently) implemented in C++ is memory hungry and our continuous-integration process exacerbates the issue to the point where the RAM/VRAM sizes of open-source CI providers are sometimes insufficient. Additionally as we add more features and tests, yet more memory pressure is applied.

The following bits are related to reducing the max RSS footprint of the compiler. Implementations by Andrew Kelley (ak) and Michael Dusan (md).

  • (ak) Zig driver built with -DZIG_ENABLE_MEM_PROFILE accepts command-line option -fmem-report to produce a list of objects allocated by compiler code. Some useful invocations:
    • ./zig test ../lib/std/std.zig --cache off -fmem-report
    • /usr/bin/time -v ./zig test ../lib/std/std.zig --cache off -fmem-report

    Reduce the size of IrInstruction by 8 bytes on 64 bit targets. (#3482) RSS savings of ~3%

  • (md) Unembed ZigValue from IrInstruction. Add const interning for 1-possible-value types. (#3502) RSS savings of ~6%
  • (ak) Sometimes free stuff from Zig IR pass 1. RSS savings of ~6%
  • (ak) Inline ConstGlobalRefs into ZigValue. (#3817) RSS savings of ~1.1%
  • (ak) Free IrAnalyze sometimes. RSS savings of ~1%
  • (ak) Split IrInstruction into IrInst, IrInstSrc, IrInstGen. This makes it so that less memory is used for IR instructions, as well as catching bugs when one expected one kind of instruction and received the other. (#4290)
  • (md) Overhaul C++ memory allocation:
    • new mem::Allocator interface
    • new heap::CAllocator impl with global heap::c_allocator
    • new heap::ArenaAllocator impl
    • new mem::List takes explicit Allocator& parameter
    • new mem::HashMap takes explicit Allocator& parameter
    • add Codegen.pass1_arena and use for all ZigValue allocs
    • deinit Codegen.pass1_arena early in zig_llvm_emit_output()

    (#4389) RSS savings of ~13-15%

  • (md) Free more heap after analysis. (#4515) RSS savings of ~5.5-6.3%

The main goal of memory usage reduction is to ensure that bootstrapping takes 3.5 GiB or less on the host system (#471).

Andrew added native-debug helper functions for the Zig compiler.

Print triplet of (source:line:col) by calling member function src() for types IrExecutable{Src,Gen}, AstNode, IrInst, IrInst{Src,Gen} .

Dump IR segment by calling member function dump() for types IrExecutable{Src,Gen}, AstNode, IrInst, IrInst{Src,Gen} .

Dump ZigValue type-as-string by calling member function dump .

When --verbose-ir is enabled, call dbg_ir_break(src_file_zig, line) to breakpoint inside ir_analyze() .

Call dbg_ir_clear() to clear all breakpoints.


  • --eh-frame-hdr – enable C++ exception handling by passing –eh-frame-hdr to link
  • -fsanitize-c – enable C undefined behavior detection in unsafe builds
  • -fno-sanitize-c – disable C undefined behavior detection in safe builds
  • -fmem-report – print memory usage diagnostics
  • -fdump-analysis – write analysis.json file with type information
  • -femit-docs – create a docs/ dir with html documentation
  • -fno-emit-docs – do not produce docs/ dir with html documentation
  • -femit-bin – (default) output machine code
  • -fno-emit-bin – do not output machine code
  • -femit-asm – output .s (assembly code)
  • -fno-emit-asm – (default) do not output .s (assembly code)
  • -femit-llvm-ir – produce a .ll file with LLVM IR
  • -fno-emit-llvm-ir – (default) do not produce a .ll file with LLVM IR
  • -femit-h – generate a C header file (.h)
  • -fno-emit-h – (default) do not generate a C header file (.h)
  • --verbose-llvm-cpu-features – enable compiler debug output for LLVM CPU features
  • -I[dir] – add directory to include search path
  • -mcpu [cpu] – specify target CPU and feature set
  • -code-model [default|tiny|small|kernel|medium|large] – set target code model
  • --test-evented-io – runs the test in evented I/O mode



  • --emit [asm|bin|llvm-ir] – prefer the new -femit-* or -fno-emit* options above.

This will be removed once Compiler Explorer updates to the new CLI.

The command line interface now supports detecting native system headers and libraries (include/ and lib/ search paths). The implementation of this is self-hosted (#2041).

The error output is improved when an invalid CPU model or CPU feature is specified:

andy@ark ~> zig build-exe hello.zig -mcpu=bogus
Unknown CPU: 'bogus'
Available CPUs for architecture 'x86_64':
andy@ark ~> zig build-exe hello.zig -mcpu=x86_64+bogus
Unknown CPU feature: 'bogus'
Available CPU features for architecture 'x86_64':
 3dnow: Enable 3DNow! instructions
 3dnowa: Enable 3DNow! Athlon instructions
 64bit: Support 64-bit instructions
 adx: Support ADX instructions
 aes: Enable AES instructions
 avx: Enable AVX instructions
 avx2: Enable AVX2 instructions
 avx512bf16: Support bfloat16 floating point
 avx512bitalg: Enable AVX-512 Bit Algorithms
 avx512bw: Enable AVX-512 Byte and Word Instructions
 avx512cd: Enable AVX-512 Conflict Detection Instructions
 avx512dq: Enable AVX-512 Doubleword and Quadword Instructions
 avx512er: Enable AVX-512 Exponential and Reciprocal Instructions
 avx512f: Enable AVX-512 instructions
 avx512ifma: Enable AVX-512 Integer Fused Multiple-Add
 avx512pf: Enable AVX-512 PreFetch Instructions
 avx512vbmi: Enable AVX-512 Vector Byte Manipulation Instructions
 avx512vbmi2: Enable AVX-512 further Vector Byte Manipulation Instructions
 avx512vl: Enable AVX-512 Vector Length eXtensions
 avx512vnni: Enable AVX-512 Vector Neural Network Instructions
 avx512vp2intersect: Enable AVX-512 vp2intersect
 avx512vpopcntdq: Enable AVX-512 Population Count Instructions
 bmi: Support BMI instructions
 bmi2: Support BMI2 instructions
 branchfusion: CMP/TEST can be fused with conditional branches
 cldemote: Enable Cache Demote
 clflushopt: Flush A Cache Line Optimized
 clwb: Cache Line Write Back
 clzero: Enable Cache Line Zero
 cmov: Enable conditional move instructions
 cx16: 64-bit with cmpxchg16b
 cx8: Support CMPXCHG8B instructions
 enqcmd: Has ENQCMD instructions
 ermsb: REP MOVS/STOS are fast
 f16c: Support 16-bit floating point conversion instructions
 false_deps_lzcnt_tzcnt: LZCNT/TZCNT have a false dependency on dest register
 false_deps_popcnt: POPCNT has a false dependency on dest register
 fast_11bytenop: Target can quickly decode up to 11 byte NOPs
 fast_15bytenop: Target can quickly decode up to 15 byte NOPs
 fast_bextr: Indicates that the BEXTR instruction is implemented as a single uop with good throughput
 fast_gather: Indicates if gather is reasonably fast
 fast_hops: Prefer horizontal vector math instructions (haddp, phsub, etc.) over normal vector instructions with shuffles
 fast_lzcnt: LZCNT instructions are as fast as most simple integer ops
 fast_scalar_fsqrt: Scalar SQRT is fast (disable Newton-Raphson)
 fast_scalar_shift_masks: Prefer a left/right scalar logical shift pair over a shift+and pair
 fast_shld_rotate: SHLD can be used as a faster rotate
 fast_variable_shuffle: Shuffles with variable masks are fast
 fast_vector_fsqrt: Vector SQRT is fast (disable Newton-Raphson)
 fast_vector_shift_masks: Prefer a left/right vector logical shift pair over a shift+and pair
 fma: Enable three-operand fused multiple-add
 fma4: Enable four-operand fused multiple-add
 fsgsbase: Support FS/GS Base instructions
 fxsr: Support fxsave/fxrestore instructions
 gfni: Enable Galois Field Arithmetic Instructions
 idivl_to_divb: Use 8-bit divide for positive values less than 256
 idivq_to_divl: Use 32-bit divide for positive values less than 2^32
 invpcid: Invalidate Process-Context Identifier
 lea_sp: Use LEA for adjusting the stack pointer
 lea_uses_ag: LEA instruction needs inputs at AG stage
 lwp: Enable LWP instructions
 lzcnt: Support LZCNT instruction
 macrofusion: Various instructions can be fused with conditional branches
 merge_to_threeway_branch: Merge branches to a three-way conditional branch
 mmx: Enable MMX instructions
 movbe: Support MOVBE instruction
 movdir64b: Support movdir64b instruction
 movdiri: Support movdiri instruction
 mpx: Deprecated. Support MPX instructions
 mwaitx: Enable MONITORX/MWAITX timer functionality
 nopl: Enable NOPL instruction
 pad_short_functions: Pad short functions
 pclmul: Enable packed carry-less multiplication instructions
 pconfig: platform configuration instruction
 pku: Enable protection keys
 popcnt: Support POPCNT instruction
 prefer_128_bit: Prefer 128-bit AVX instructions
 prefer_256_bit: Prefer 256-bit AVX instructions
 prefer_mask_registers: Prefer AVX512 mask registers over PTEST/MOVMSK
 prefetchwt1: Prefetch with Intent to Write and T1 Hint
 prfchw: Support PRFCHW instructions
 ptwrite: Support ptwrite instruction
 rdpid: Support RDPID instructions
 rdrnd: Support RDRAND instruction
 rdseed: Support RDSEED instruction
 retpoline: Remove speculation of indirect branches from the generated code, either by avoiding them entirely or lowering them with a speculation blocking construct
 retpoline_external_thunk: When lowering an indirect call or branch using a `retpoline`, rely on the specified user provided thunk rather than emitting one ourselves. Only has effect when combined with some other retpoline feature
 retpoline_indirect_branches: Remove speculation of indirect branches from the generated code
 retpoline_indirect_calls: Remove speculation of indirect calls from the generated code
 rtm: Support RTM instructions
 sahf: Support LAHF and SAHF instructions
 sgx: Enable Software Guard Extensions
 sha: Enable SHA instructions
 shstk: Support CET Shadow-Stack instructions
 slow_3ops_lea: LEA instruction with 3 ops or certain registers is slow
 slow_incdec: INC and DEC instructions are slower than ADD and SUB
 slow_lea: LEA instruction with certain arguments is slow
 slow_pmaddwd: PMADDWD is slower than PMULLD
 slow_pmulld: PMULLD instruction is slow
 slow_shld: SHLD instruction is slow
 slow_two_mem_ops: Two memory operand instructions are slow
 slow_unaligned_mem_16: Slow unaligned 16-byte memory access
 slow_unaligned_mem_32: Slow unaligned 32-byte memory access
 soft_float: Use software floating point features
 sse: Enable SSE instructions
 sse_unaligned_mem: Allow unaligned memory operands with SSE instructions
 sse2: Enable SSE2 instructions
 sse3: Enable SSE3 instructions
 sse4_1: Enable SSE 4.1 instructions
 sse4_2: Enable SSE 4.2 instructions
 sse4a: Support SSE 4a instructions
 ssse3: Enable SSSE3 instructions
 tbm: Enable TBM instructions
 use_aa: Use alias analysis during codegen
 use_glm_div_sqrt_costs: Use Goldmont specific floating point div/sqrt costs
 vaes: Promote selected AES instructions to AVX512/AVX registers
 vpclmulqdq: Enable vpclmulqdq instructions
 vzeroupper: Should insert vzeroupper instructions
 waitpkg: Wait and pause enhancements
 wbnoinvd: Write Back No Invalidate
 x87: Enable X87 float instructions
 xop: Enable XOP instructions
 xsave: Support xsave instructions
 xsavec: Support xsavec instructions
 xsaveopt: Support xsaveopt instructions
 xsaves: Support xsaves instructions

Of course, this works for any architecture, not only x86_64.

Thanks Noam Preil, Christine Dodrill, David Cao, and Layne Gustafson for related contributions.

  • Add -I command line parameter.
  • POSIX terminals now have a progress indicator when compilation takes a long time. Thanks to Luna for the initial implementation.
  • Michael Dusan added linux XDG Base Directory integration to the cache system. #3573
  • Add compiler note for bad int coercion (#3724)
  • Private linkage for unnamed internal constants.
  • Vexu implemented better support for extern enums.
  • LemonBoy added a compile error for @bitCast to enum types, preventing invalid enum values.
  • David Cao added --eh-frame-hdr CLI option. (#3981)
  • LemonBoy improved debug info type sizes, making debuggers happy and no longer report incorrect values for bool. (#2685)
  • LemonBoy improved the compiler to not special case the "builtin" import with regards to usingnamespace.
  • Michael Dusan improved the compiler to strip cwd from compile error paths. (#4138)
  • Michael Dusan improved the BREAKPOINT util within the C++ compiler code to support non-x86 architectures.
  • LemonBoy improved the C++ compiler code internal debugging utilities.
  • There is no longer a native_libc.txt file in zig-cache, and thus there is no longer a possibility for this file to become stale and cause problems. The libc installation path detection code is always run when needed. (#3975, #4186, #4940)
  • Michael Dusan improved the development process on POSIX to support make without being required to make install. On Windows the INSTALL target is still a required part of the development process.
  • LemonBoy improved @tagName to work on enum literals. (#4214)
  • Valentin Anger added support for code model selection.
  • Michael Dusan improved debuggability of the compiler by showing “Const” IR instructions trailing after they are referenced. (#4511)
  • LemonBoy implemented safety checks for shl/shr when the integer size is not a power-of-two. (#2096)
  • daurnimator removed unused static_crt_dir field from zig libc config.
  • Bodie Solomon improved Zig’s cmake build script to use appropriate compiler flags when building with MSVC. (#4877)
  • Michael Dusan added a compiler flag to Zig’s C++ compiler code that makes accidental switch case fallthrough a compile error, which uncovered a bug in the tokenizer.
  • The zig BUILD_INFO hack is removed. Rather than stuffing configuration information into the Zig binary, the build script reads it from config.h. This solves a problem for package maintainers and improves the use case of deterministic builds. (#3758)
  • libc installation detection can correctly detect MSVC libc even when the compiler is built using the gnu target triple (taking advantage of mingw
  • Tse contributed DragonFlyBSD Support.

Full list of bug reports closed during this release cycle. Note: many bugs were both introduced and resolved within this release cycle. Listed below are fixed bugs that were not reported on the issue tracker.

Special thanks to LemonBoy, who solved a sizeable chunk of those issues, in many different parts of the Zig project.

  • Better debug info for integers. Now we use ABI size * 8 instead of size_in_bits which makes gdb work instead of hang for non-power-of-two integers.
  • LemonBoy fixed user-defined function alignment not getting propagated to LLVM IR.
  • LemonBoy fixed crash when generating constant unions with single field.
  • Various fixes related to 32-bit architectures.
  • Async function recursion is detected and compile error emitted.
  • Brendan Hansknecht fixed parsing of .*=.
  • Vexu fixed NodeErrorSetDecl rendering.
  • Timon Kruiper added a compile error for an empty switch on a integer.
  • Quetzal Bradley implemented correct buffer wrapping logic in std.event.Channel.
  • LemonBoy fixed WinMain not having its calling convention type-checked.
  • xackus fixed integers parsed as floats.
  • emekoi fixed Windows dynamic library loading and added loading for Darwin.
  • ForLoveOfCats fixed a memory leak in std.math.big.Int.toString. (#3992)
  • LemonBoy improved the compiler’s type resolution phase to catch more errors.
  • daurnimator fixed an off-by-one error in Windows process creation.
  • LemonBoy added missing validation for switch range endpoints. (#4172)
  • Vexu fixed a crash when parsing a multiline library name.
  • Michaël Larouche fixed std.child_process.ChildProcess.spawnWindows when looking in PATH environment variable, it applied cwd+app_name instead of just using the app_name.
  • daurnimator fixed bug in std.http.headers where .put captures user-held variable.
  • Fixed automatically created local variables sometimes having incorrect alignment.
  • LemonBoy fixed an edge case in isAbsolute path functions. Empty strings are no longer considered absolute paths. (#4382)
  • Rocknest fixed a double close in openElfDebugInfo.
  • Fixed not checking type of return pointers. (#3224, #3269, #3327, #3422, #3646)
  • std lib updated to integrate with libc for environment variables, even when building a static library. (#3511)
  • J.W fixed logic and index out of bounds in hashing algorithms.
  • Exported variables now respect linkage.
  • Alexandros Naskos fixed slicing of C pointers to no longer produce allowzero slices. Instead they insert a runtime assertion. (#4462)
  • Alexandros Naskos improved made the std lib VDSO code more robust and it now operates successfully inside Windows Subsystem for Linux. (#3997)
  • LemonBoy improved arrray subscripts to properly type coerce to usize. (#4169)
  • LemonBoy implemented a compile error for comparison between enum literal and untagged enum. (#4770)
  • xackus fixed an overflow in std.fmt.parseFloat (#4845)
  • Michaël Larouche fixed adler32 returning incorrect value with large input.
  • LemonBoy improved big-endian compatibility. (#4935)
  • Zig no longer caches the results of native system libc detection into a native_libc.txt file. Instead, it always runs native libc detection when it needs to know native libc paths. (#4772)

Zig has known bugs and even some miscompilations.

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

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

I am pleased to announce our newest Zig team member, Vexu.

Vexu has shown continued dedication and discipline in contributions to the Zig programming language project. The quality of Vexu’s work speaks for itself.

In addition, Vexu has proven to be a steadfast community leader, setting an example for how to treat others with kindness and respect.

I look forward to working with Vexu as we continue to push Zig toward 1.0.0 and beyond.

According to the 0.5.0 Roadmap, the major theme of the 0.6.0 release was supposed to be Safety. I also wrote:

I expect to complete [Networking] along with at least an early prototype of the package manager during the next release cycle.

Clearly, this release cycle went in a different direction than planned. I realized that stabilizing the language is a top priority that everything else rests on. I also prioritized merging pull requests (at the time of writing, there are only 21 open pull requests, with the oldest one 36 days old), and unblocking contributors from accomplishing their goals.

The theme of the 0.7.0 release cycle will be stabilizing the language, creating a first draft of the language specification, and self-hosting the compiler.

It would be a major accomplishment if Zig 0.7.0 could ship with self-hosted instead of stage1.

Having a package manager built into the Zig compiler is a long-anticipated feature. Zig 0.6.0 does not have this feature.

If the package manager works well, people will use it, which means building Zig projects will involve compiling more lines of Zig code, which means the Zig compiler must get faster, better at incremental compilation, and better at resource management.

Therefore, the package manager depends on finishing the self-hosted compiler, since it is planned to have these improved performance characteristics, while stage1 is not planned to have them.

There were two improvements:

  • Support for exporting variables (#3284).
  • Properly generate header in separate folder, respecting -femit-* options.

Thanks Sahnvour and mogud.

However, C header file generation is now disabled by default. The proof-of-concept is complete; but now it’s a maintenance burden to implement this feature both in stage1 and in self-hosted.

The plan is to implement this feature in the self-hosted compiler, and then remove the feature from stage1, since it is not needed to bootstrap.

If you want more of a sense of the direction Zig is heading, you can look at the set of accepted proposals.

  • ZigGBA – Work in Progress SDK to write Game Boy Advance in Zig
  • TM35-Metronome – Tools for modifying and randomizing Pokémon games.
  • pluto – An x86 kernel, with plans to port it to x86_64, arm and aarch64.
  • scritcher – Glitch art scripting language
  • tinyfx – Mid-level OpenGL rendering library in C with Zig wrapper.
  • embed-dir – A small Zig library for embedding directory trees with @embedFile.
  • Fundude – Gameboy emulator running in wasm
  • river – A dynamic wayland compositor.
  • wazm – wasm interpreter
  • sudokuinzig – A basic sudoku solver
  • bog – Embeddable scripting language written in Zig
  • zigimg – Library to read (and soon write) image files.
  • oxid – arcade-style game where you fight waves of monsters in a fixed-screen maze
  • LoLa – Script language written in Zig and C++

The Zig project is financially sustainable. It currently supports one full-time developer – yours truly, Andrew Kelley.

If you flip through the previous release notes, you can see the number of commits and number of contributors per release increasing super-linearly.

The project is succeeding!

Consequently, merging pull requests and providing troubleshooting, support, and moderation for the quickly-growing community creates a strong demand on time that is too much for just one person.

That is why I decided to start the Zig Software Foundation, a non-profit organization with the mission of raising the bar of software standards, ethics, and quality, and paying open source contributors for their valuable time.

I hope you will stay tuned for an official announcement about the ZSF, which I expect to happen within 6 months.

Special thanks to those who sponsor Zig. Because of you, Zig is driven by the open source community, rather than the goal of making profit. In particular, these fine folks sponsor Zig for $15/month or more: