0.10.0 Release Notes

Zero the Ziguana

Download & Documentation

Zig is a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.

Backed by the Zig Software Foundation, the project is financially sustainable. These core team members work on Zig full-time:

Please consider a recurring donation to the ZSF to help us pay more contributors!

This release features 10 months of work: changes from 272 different contributors, spread among 4737 commits. It is the début of the Self-Hosted Compiler.

Table of Contents §

Cody Tapscott Joins the Core Zig Team §

Ziggy the Ziguana

I am pleased to announce our newest Zig team member, Cody Tapscott (screenname: topolarity).

Cody excels at troubleshooting and debugging. He fearlessly deep dives into unfamiliar areas of code, relentlessly attacking the problem until a solution presents itself.

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

Please give him a warm welcome to the core Zig team.

Veikka Tuominen Goes Full-Time §

If you have been following along with Zig's development process lately, you might have noticed a distinct uptick in pull requests being merged, Bug Fixes to the Self-Hosted Compiler, and implementation of Language Changes.

Vexu has been doing brilliant work, and we are all fortunate that, one, he has decided to take on full-time hours lately, and two, donations have been steadily rising so that there are enough funds to offer the hours.

Support Table §

Tier System §

A green check mark (✅) indicates the target meets all the requirements for the support tier. The other icons indicate what is preventing the target from reaching the support tier. In other words, the icons are to-do items. If you find any wrong data here please submit a pull request!

Tier 1 Support §

freestanding Linux 3.16+ macOS 11+ Windows 8.1+ WASI
x86_64 N/A
x86 #1929 🐛📦 💀 #537 🐛📦 N/A
aarch64 #2443 🐛 🐛📦🧪 N/A
arm #3174 🐛📦 💀 🐛📦🧪 N/A
mips #3345 🐛📦 N/A N/A N/A
riscv64 #4456 🐛📦 N/A N/A N/A
sparc64 #4931 🐛📦🧪 N/A N/A N/A
powerpc64 🐛📦 N/A N/A N/A
wasm32 N/A N/A N/A

Tier 2 Support §

free standing Linux 3.16+ macOS 11+ Windows 8.1+ FreeBSD 12.0+ NetBSD 8.0+ DragonFlyBSD 5.8+ UEFI
x86_64 Tier 1 Tier 1 Tier 1 Tier 1
x86 Tier 1 💀 🔍 🔍 N/A
aarch64 Tier 1 Tier 1 🔍 🔍 🔍 N/A 🔍
arm Tier 1 💀 🔍 🔍 🔍 N/A 🔍
mips64 N/A N/A 🔍 🔍 N/A N/A
mips Tier 1 N/A N/A 🔍 🔍 N/A N/A
powerpc64 💀 N/A 🔍 🔍 N/A N/A
powerpc 💀 N/A 🔍 🔍 N/A N/A
riscv64 Tier 1 N/A N/A 🔍 🔍 N/A 🔍
sparc64 Tier 1 N/A N/A 🔍 🔍 N/A N/A

Tier 3 Support §

freestanding Linux 3.16+ Windows 8.1+ FreeBSD 12.0+ NetBSD 8.0+ UEFI
x86_64 Tier 1 Tier 1 Tier 1 Tier 2 Tier 2 Tier 2
x86 Tier 1 Tier 2 Tier 2 Tier 2
aarch64 Tier 1 Tier 2
arm Tier 1 Tier 2
mips64 Tier 2 Tier 2 N/A N/A
mips Tier 1 Tier 2 N/A N/A
riscv64 Tier 1 Tier 2 N/A
powerpc32 Tier 2 Tier 2 N/A N/A
powerpc64 Tier 2 Tier 2 N/A N/A
bpf N/A N/A
hexagon N/A N/A
amdgcn N/A N/A
sparc N/A N/A
s390x N/A N/A
lanai N/A N/A
csky N/A N/A
freestanding emscripten
wasm32 Tier 1

Tier 4 Support §

Tier 4 targets:

WebAssembly §

During this release cycle, a lot of time was spent on both visible and invisible improvements. Refactors were done to increase readability, maintainability, and also the performance of the backend and its generated code. With this release, we're one step closer to being able to make the WebAssembly Backend the default for debug mode.

Resolved issues:

Windows §

Improvements to the Standard Library:

The 32-bit Windows tarball is missing for this release due to #12886. It will be available again in the 0.10.1 bugfix release.

Linux §

Previously, updating the SYS enum for each architecture required manually looking at the syscall tables and inserting any new additions. Now there is a tool, generate_linux_syscalls.zig, that automates this process using the syscall tables in the Linux source tree. On architectures without a table, it runs zig cc as a pre-processor to extract the system-call numbers from the Linux headers.

Improvements to the Standard Library:

macOS §

Added support for macOS 13 (Ventura); dropped support for macOS 10 (Catalina). This matches Apple's security policy.

Default C ABI changed from gnu to none §

Prior to this change Zig would assume the ABI for Apple targets to be GNU which could result in subtle errors of emitting calls to non-existent system libc provided functions such as _sincosf which is a GNU extension and therefore not provided by macOS. This would result in linker errors due to not finding the symbol in libSystem.tbd.

With this change, Zig now correctly identifies macOS (and other Apple platforms such as iPhoneOS, tvOS, and watchOS) as having ABI none (#11684).

In LLVM, this translates to unknown:

// main.ll
target triple = "aarch64-unknown-macos-unknown"

OpenBSD §

SPARC §

PowerPC §

powerpc64 moved from Tier 3 Support to Tier 2 Support.

powerpc64le now has CI test coverage for the behavior tests, the Standard Library, targeting musl, and targeting glibc.

Softfloat support was added in compiler-rt.

WASI §

Zig can now build itself for the WASI target.

Shell
$ uname -m
x86_64
$ zig build -Dtarget=wasm32-wasi -Drelease
$ file zig-out/bin/zig.wasm
zig-out/bin/zig.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
$ ls -hl zig-out/bin/zig.wasm
-rwxr-xr-x 1 andy users 37M Oct 19 12:24 zig-out/bin/zig.wasm
$ cat hello.zig
const std = @import("std");
pub fn main() void {
    std.io.getStdOut().writeAll("Hello, World!\n") catch {};
}
$ wasmtime --dir=. --mapdir=/cwd::. --mapdir=/cache::"$HOME/.cache/zig" --mapdir=/zig::zig-out zig-out/bin/zig.wasm build-exe /cwd/hello.zig -target x86_64-linux-musl
$ chmod +x hello
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
$ ./hello
Hello, World!

The chmod is because of WebAssembly/wasi-filesystem#33.

This snippet demonstrates creating a WASI build of Zig, then executing it with wasmtime 1.0.1, cross compiling an executable for x86_64-linux, and then executing the cross-compiled executable. Computers are fun!

Additionally:

UEFI §

GPU Programming §

Documentation §

There were minimal changes to the Language Reference in 0.10.0, however, the big news is that Autodoc has been completely rewritten!

Language Reference §

Ziggy the Ziguana

Autodoc §

This feature is still experimental.

Zig gained a first (experimental) implementation of automated doc generation in version 0.6.0. That version integrated with the Bootstrap Compiler and, more specifically, it observed the data resulting from semantic analysis.

With the new Self-Hosted Compiler we needed to re-implement the main part of the system (now internally named Autodoc) to make it consume the new data format, but we took the opportunity of also re-designing its entire approach to data collection.

The first Autodoc proof-of-concept (from Zig 0.6.0) relied entirely on semantic analysis data, which had the main downside of only showing the information that had been typechecked. The most annoying result of this fact was that the docs would leave holes in the documentation, making them feel unreliable.

The new Autodoc is instead based on a two pass approach.

The main source of information consumed by Autodoc is ZIR, an untyped intermediate representation close to an AST, which gets generated unconditionally for the entire codebase everytime a Zig project is built. This makes sure that Autodoc doesn't have blind spots.

The problem with ZIR is that it is untyped, which means that it describes the program before any comptime logic is run. This means that ZIR alone won't be enough to find out all the concrete instantiations of a generic type, for example.

To solve this problem, we plan to use ZIR information as the main skeleton for Autodoc, and to overlay semantic analysis information on top of that, in order to obtain both full coverage and also full resolution of comptime metaprogramming.

Autodoc is still a work in progress and in fact it is still relying on ZIR information only. There is a tremendous amount of work ahead of us and, while we encourage you to check out Zig's stdlib docs, there are plenty of basic things that still need to be implemented. If you see something missing or broken in Autodoc, please file an issue or, even better, consider becoming a contributor.

Language Changes §

Function Pointers §

In Zig 0.9.x and the Bootstrap Compiler, the type fn () void is a function pointer and acts like a pointer. However, the language spec, which is not written yet, will have this type be a function body type. It must be compile-time-known, and in order to get a function pointer, *const fn () void must be used.

The Self-Hosted Compiler implements this correctly, which means your project will likely break everywhere that you use a function pointer.

Here is the problem most people will run into:

test.zig
test "function pointer" {
    const s = foo();
    s.fnPtr();
}

const S = struct {
    fnPtr: fn () void,
};

fn bar() void {}
fn baz() void {}

var runtime: bool = true;

fn foo() S {
    if (runtime) {
        return .{
            .fnPtr = bar,
        };
    } else {
        return .{
            .fnPtr = baz,
        };
    }
}
Shell
$ zig test test.zig
docgen_tmp/test.zig:16:9: error: unable to resolve comptime value
    if (runtime) {
        ^~~~~~~
docgen_tmp/test.zig:16:9: note: condition in comptime branch must be comptime-known
docgen_tmp/test.zig:15:10: note: expression is evaluated at comptime because the function returns a comptime-only type 'test.S'
fn foo() S {
         ^
docgen_tmp/test.zig:7:12: note: struct requires comptime because of this field
    fnPtr: fn () void,
           ^~~~~~~~~~
docgen_tmp/test.zig:7:12: note: use '*const fn() void' for a function pointer type
    fnPtr: fn () void,
           ^~~~~~~~~~
docgen_tmp/test.zig:2:18: note: called from here
    const s = foo();
              ~~~^~

The solution becomes obvious upon reading the compile error notes:

-    fnPtr: fn () void,+    fnPtr: *const fn () void,

Packed Structs §

In previous releases of Zig there have been some nasty bugs that unfortunately did not get fixed for a long time, resulting in packed structs having a deservedly poor reputation. Finally, the situation is resolved. Packed structs have been reworked, both in terms of the Zig language specification, as well as the implementation.

Now, one can reason about packed structs in a similar way as one reasons about enums - that is - they are merely integers wearing fancy hats.

Each packed struct has a backing integer which defines the ABI of the type as well as the memory layout. The backing integer can be specified explicitly, or it can be inferred from the total bits of all the fields. If the backing integer is explicitly specified, then a compile error enforces that all the fields add up to that number of bits:

test.zig
const S = packed struct(u32) {
    a: u29,
    b: u2,
};

test "packed struct" {
    var s: S = .{
        .a = 1234,
        .b = 3,
    };
    _ = s;
}
Shell
$ zig test test.zig
docgen_tmp/test.zig:1:25: error: backing integer type 'u32' has bit size 32 but the struct fields have a total bit size of 31
const S = packed struct(u32) {
                        ^~~
referenced by:
    S: docgen_tmp/test.zig:1:18
    test.packed struct: docgen_tmp/test.zig:7:12
    remaining reference traces hidden; use '-freference-trace' to see all reference traces


The fields are specified in order starting from logically least-significant and ending with logically most-significant within the backing integer. The backing integer is stored in memory with the same endianness as the equivalent integer type for that target. This makes Zig's packed structs have a well-defined memory layout, unlike in C where the memory layout is implementation-defined and therefore usually cannot be relied upon.

In the near future, the syntax will change to avoid confusion with C's packed structs, which is a different concept that relates to padding. In Zig, such a struct is done by using extern and setting each field's alignment to 1 byte:

align-1.zig
const std = @import("std");
const assert = std.debug.assert;

// Equivalent of C's __attribute__((__packed__))
const S = extern struct {
    a: u64 align(1),
    b: u8 align(1),
};

comptime {
    assert(@sizeOf([2]S) == 18);
}
Shell
$ zig test align-1.zig
All 0 tests passed.

Inline Switch Cases §

The inline keyword can now be placed in front of switch cases. This effectively turns a runtime-known value into a compile-time-known value:

inline-switch.zig
const std = @import("std");

pub fn main() !void {
    var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    const arena = arena_instance.allocator();

    const args = try std.process.argsAlloc(arena);
    const arg = if (args.len >= 2) args[1] else "50";
    const some_number = try std.fmt.parseInt(i32, arg, 10);

    switch (some_number) {
        inline 1...100 => |x| {
            foo(x);
        },
        else => @panic("not in range"),
    }
}

fn foo(comptime x: i32) void {
    // Doing something that requires a comptime number, such as
    // using it as the length of an array:
    var array: [x]u8 = undefined;
    for (array) |*elem, i| {
        elem.* = @intCast(u8, i);
    }
}
Shell
$ zig build-exe inline-switch.zig

$ ./inline-switch

In this example, we take a number that was passed in as a command line parameter at runtime, and pass it to a function that requires a comptime parameter. This works because the switch gets inlined - 100 prongs are generated, similar to unrolling an inline for loop that looped from 1 to 100.

Let's look at a less contrived example. Here is the use case where this pattern is the most useful: dynamic dispatch for enums and tagged unions.

Before:

pub fn iterate(base: *Node, index: usize) ?*Node {
    inline for (@typeInfo(Tag).Enum.fields) |field| {
        const tag = @intToEnum(Tag, field.value);
        if (base.tag == tag) {
            return @fieldParentPtr(tag.Type(), "base", base).iterate(index);
        }
    }
    unreachable;
}

After:

pub fn iterate(base: *Node, index: usize) ?*Node {
    return switch (base.tag) {
        inline else => |tag| @fieldParentPtr(tag.Type(), "base", base).iterate(index),
    };
}

Unfortunately, we cannot use this feature in the Standard Library or the Self-Hosted Compiler yet, because this feature is not implemented in the Bootstrap Compiler. The next steps to accomplish this will be enhancing the C Backend until we can replace the bootstrap compiler.

See the full inline switch documentation in the Language Reference.

Equality Testing of Error Unions §

Similar to how the == operator can be used to check if an optional is null, it can now also be used to check if an error union is a specific error code:

error-equality.zig
const std = @import("std");
const expect = std.testing.expect;

test "equality testing of error unions" {
    try expect(foo() == error.Bad);
    try expect(bar() == error.RealBad);
}
fn foo() anyerror {
    return error.Bad;
}
fn bar() anyerror!void {
    return error.RealBad;
}
Shell
$ zig test error-equality.zig
1/1 test.equality testing of error unions... OK
All 1 tests passed.

Pointers to Zero-Bit Types §

Before, a pointer to a zero-bit type was itself a zero bit type. Now, pointers to zero-bit types store addresses the same as regular pointers.

This change is unlikely to affect many codebases. A use case where it might be relevant is representing a struct that has a variable-sized last field, as demonstrated by the name field here:

vla.zig
const std = @import("std");
const testing = std.testing;

test "struct with variable length last field" {
    const foo = try make(testing.allocator, "Blah blah");
    defer foo.free(testing.allocator);

    try testing.expect(foo.a == 1234);
    try testing.expectEqualStrings(foo.getName(), "Blah blah");

    // The pointer address of the 'name' field is still well-defined and meaningful:
    try testing.expect(@ptrCast(*u8, &foo.name).* == 'B');
}

const S = extern struct {
    a: i32,
    name: [0]u8 = .{},

    fn getName(s: *const S) []const u8 {
        return std.mem.sliceTo(@ptrCast([*:0]const u8, &s.name), 0);
    }

    fn free(s: *S, gpa: std.mem.Allocator) void {
        const buf = @ptrCast([*]u8, s)[0 .. @sizeOf(S) + s.getName().len + 1];
        gpa.free(buf);
    }
};

fn make(gpa: std.mem.Allocator, name: []const u8) !*S {
    const s = try make_vla(gpa, S, name.len + 1);
    s.* = .{ .a = 1234 };
    const name_ptr = @ptrCast([*]u8, &s.name);
    std.mem.copy(u8, name_ptr[0..name.len], name);
    name_ptr[name.len] = 0;
    return s;
}

fn make_vla(gpa: std.mem.Allocator, comptime T: type, extra_bytes: usize) !*T {
    const buf = try gpa.alignedAlloc(u8, @alignOf(T), @sizeOf(T) + extra_bytes);
    const ptr = @ptrCast(*T, buf);
    return ptr;
}
Shell
$ zig test vla.zig
1/1 test.struct with variable length last field... OK
All 1 tests passed.

This assertion does not pass with the Bootstrap Compiler but it does with the Self-Hosted Compiler:

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

comptime {
    assert(@sizeOf(*u0) == @sizeOf(*u8));
}
Shell
$ zig test ptrs.zig
All 0 tests passed.

However, pointers to comptime-only types are still zero bit types, such as *comptime_int.

@minimum and @maximum renamed to @min and @max §

See the Language Reference: @min @max

zig fmt will automatically perform the rename.

Removed Type Parameter from Bit Builtins §

The following builtins no longer have types as the first parameter:

Instead, the type is inferred from the operand. This makes the builtins more ergonomic for SIMD.

Runtime Array Concatenation & Multiplication §

The ++ and ** operators now support runtime-known operands, as long as the length is compile-time-known:

array_cat.zig
const std = @import("std");

pub fn main() !void {
    var array1 = "hellp".*;
    var array2 = "!world ".*;

    array1[array1.len - 1] -= 1;
    std.mem.swap(u8, &array2[0], &array2[array2.len - 1]);

    const final = array1 ++ array2 ++ "\n";
    try std.io.getStdOut().writeAll(final);
}
Shell
$ zig build-exe array_cat.zig

$ ./array_cat
hello world!

This may be a breaking change for some people who were relying on Zig to automatically evaluate the operands in a comptime scope, like this:

test.zig
const std = @import("std");

test {
    const a = foo() ++ "bb";
    try std.testing.expect(a.len == 5);
}

fn foo() []const u8 {
    return "aaa";
}
Shell
$ zig test test.zig
docgen_tmp/test.zig:4:18: error: unable to resolve comptime value
    const a = foo() ++ "bb";
              ~~~^~
docgen_tmp/test.zig:4:18: note: slice value being concatenated must be comptime-known

The previous behavior can be obtained with explicit use of the comptime keyword:

-    const a = foo() ++ "bb";+    const a = comptime foo() ++ "bb";

This language change is implemented in Self-Hosted Compiler only.

Address-of Temporaries Now Produces Const Pointers §

Zig 0.9.x and the Bootstrap Compiler allow this code:

test.zig
const std = @import("std");

test {
    const arena = std.heap.ArenaAllocator.init(std.heap.page_allocator).allocator();

    const ptr = try arena.create(i32);
    ptr.* = 1234;
}
Shell
$ zig test test.zig
docgen_tmp/test.zig:4:72: error: expected type '*heap.arena_allocator.ArenaAllocator', found '*const heap.arena_allocator.ArenaAllocator'
    const arena = std.heap.ArenaAllocator.init(std.heap.page_allocator).allocator();
                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~
docgen_tmp/test.zig:4:72: note: cast discards const qualifier

Zig 0.10.0 does not allow this. Address of temporaries produces constant pointers for safety reasons. To fix this code, extract the temporary into an explicit var. This makes the mutable temporary more obvious, which is safer.

-    const arena = std.heap.ArenaAllocator.init(std.heap.page_allocator).allocator();+    var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+    const arena = arena_state.allocator();

std.builtin.CompilerBackend §

Now that there are multiple competing Code Generation backends, it may be necessary to work around bugs in one of them. Similarly, as Zig gains popularity, there may be third party compiler implementations which have quirks. Zig now provides a comptime enum value so that one can use conditional compilation to work around such issues.

Documentation comments are reproduced here:

test.zig
/// This enum is set by the compiler and communicates which compiler backend is
/// used to produce machine code.
/// Think carefully before deciding to observe this value. Nearly all code should
/// be agnostic to the backend that implements the language. The use case
/// to use this value is to **work around problems with compiler implementations.**
///
/// Avoid failing the compilation if the compiler backend does not match a
/// whitelist of backends; rather one should detect that a known problem would
/// occur in a blacklist of backends.
///
/// The enum is nonexhaustive so that alternate Zig language implementations may
/// choose a number as their tag (please use a random number generator rather
/// than a "cute" number) and codebases can interact with these values even if
/// this upstream enum does not have a name for the number. Of course, upstream
/// is happy to accept pull requests to add Zig implementations to this enum.
///
/// This data structure is part of the Zig language specification, however, the
/// specific tag names and values are implementation defined.
pub const CompilerBackend = enum(u64) {
    /// It is allowed for a compiler implementation to not reveal its identity,
    /// in which case this value is appropriate. Be cool and make sure your
    /// code supports `other` Zig compilers!
    other = 0,
    /// The original Zig compiler created in 2015 by Andrew Kelley.
    /// Implemented in C++. Uses LLVM.
    stage1 = 1,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// LLVM backend.
    stage2_llvm = 2,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// backend that generates C source code.
    /// Note that one can observe whether the compilation will output C code
    /// directly with `object_format` value rather than the `compiler_backend` value.
    stage2_c = 3,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// WebAssembly backend.
    stage2_wasm = 4,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// arm backend.
    stage2_arm = 5,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// x86_64 backend.
    stage2_x86_64 = 6,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// aarch64 backend.
    stage2_aarch64 = 7,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// x86 backend.
    stage2_x86 = 8,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// riscv64 backend.
    stage2_riscv64 = 9,
    /// The reference implementation self-hosted compiler of Zig, using the
    /// sparc64 backend.
    stage2_sparc64 = 10,

    _,
};

Please read the documentation comments carefully before using this value. In summary, you can work around compiler issues by doing something like this:

const FnPtr = switch (builtin.zig_backend) {
    .stage1 => fn()void,
    else => *const fn()void,
};

f80 §

Zig now has f80, a primitive type that provides x86 extended precision format. This type is fully supported on all systems. On those which lack hardware support for this type, softfloat implementations are provided by compiler-rt.

When would you use this? Well if you were implementing a C compiler for instance. It is also required for C ABI Compatibility - for some C targets long double is an alias for f80.

This is implemented in both the Bootstrap Compiler and Self-Hosted Compiler.

TypeInfo.Declaration no longer has a data field §

Zig 0.9.x allows inspecting certain kinds of extra information about declarations via @typeInfo, but 0.10.0 does not (#10753):

test.zig
const std = @import("std");
test "type info on declarations" {
    _ = @typeInfo(@This()).Struct.decls[0].data;
}
Shell
$ zig test test.zig
docgen_tmp/test.zig:3:44: error: no field named 'data' in struct 'builtin.Type.Declaration'
    _ = @typeInfo(@This()).Struct.decls[0].data;
                                           ^~~~
/home/andy/Downloads/zig/lib/std/builtin.zig:410:29: note: struct declared here
    pub const Declaration = struct {
                            ^~~~~~

The data field looked like this:

pub const Data = union(enum) {
    Type: type,
    Var: type,
    Fn: FnDecl,
};

This was removed because it was an unnecessary complication. There are already alternatives for each of these fields:

This is implemented in both the Bootstrap Compiler and Self-Hosted Compiler.

Removed anytype fields §

Zig 0.9.x allows declaring a struct field anytype which made the struct be a compile-time-only type and made the field's type allowed to be changed during comptime code execution.

test.zig
const S = struct { x: anytype };
Shell
$ zig test test.zig
docgen_tmp/test.zig:1:23: error: expected type expression, found 'anytype'
const S = struct { x: anytype };
                      ^~~~~~~

This was removed in order to simplify the language (#10766).

This is implemented in both the Bootstrap Compiler and Self-Hosted Compiler.

Address Spaces §

New builtin: @addrSpaceCast

Compile Error for Misleading Whitespace §

Have a look at this code:

const directions = [_]isize{
    -pitch-1,
    -pitch,
    -pitch+1
    -1,
    1,
    pitch-1,
    pitch,
    pitch+1,
};

See the bug? It took me a minute.

Answer: the , is missing from the third item in the list. This makes the list 7 items long instead of 8, and the third item -pitch+1-1.

This code is now a compile error due to a new rule in the language: misleading whitespace is a compile error. In this case the misleading whitespace is being inconsistent on either side of a binary operator.

test.zig
const directions = [_]isize{
    -pitch-1,
    -pitch,
    -pitch+1
    -1,
    1,
    pitch-1,
    pitch,
    pitch+1,
};
Shell
$ zig test test.zig
docgen_tmp/test.zig:5:5: error: binary operator `-` has whitespace on one side, but not the other.
    -1,
    ^

More compile errors for misleading whitespace are planned.

Compile Error for Pointless Discards §

It is no longer possible to discard a local variable or parameter that is referenced:

test.zig
test "example" {
    var x: i32 = 1234;
    _ = x;
    var y = x;
    _ = y;
}
Shell
$ zig test test.zig
docgen_tmp/test.zig:3:9: error: pointless discard of local variable
    _ = x;
        ^
docgen_tmp/test.zig:4:13: note: used here
    var y = x;
            ^

Zero the Ziguana

The fix is simple - delete the pointless discard. This compile error dovetails with the compile error for unused locals introduced in Zig 0.9.0. Together, these compile errors ensure that when you are reading Zig code, whether it is yours or some random codebase on the Internet, you can be assured that all locals are used somewhere, and that any discarded locals are otherwise unused.

This is a divisive feature of Zig. Generally, those with large codebases favor the errors since they help prevent bugs and aid refactoring, while those with small codebases find these errors annoying and unproductive.

There is an upcoming autofix feature that unfortunately did not make it in time for 0.10.0 which can be used by those who find this pair of compile errors problematic for their workflow.

Disallow Leading Zeroes in Literals §

This new compile error helps catch bugs (#11963) (#12417):

test.zig
var x: i032 = 1234;
var y: i32 = 01234;
Shell
$ zig test test.zig
docgen_tmp/test.zig:1:8: error: primitive integer type 'i032' has leading zero
var x: i032 = 1234;
       ^~~~
docgen_tmp/test.zig:2:14: error: number '01234' has leading zero
var y: i32 = 01234;
             ^~~~~
docgen_tmp/test.zig:2:14: note: use '0o' prefix for octal literals

This caught two C header translation mistakes in haiku.zig.

More Calling Conventions §

The Win64 calling convention is the same as Cdecl on Windows x64, however using the C calling convention as a stand-in for the Win64 calling convention only works when the compiler targets Windows. This means, before Zig 0.10.0, one was not able to create a function with the Win64 calling convention when targeting an OS other than Windows. Now the Win64 calling convention can be used from Zig code on any target, using std.builtin.CallingConvention.Win64 (#11585).

Also added in this release:

This is implemented in both the Bootstrap Compiler and Self-Hosted Compiler.

Inline Assembly §

The sub-project of parsing inline assembly in the Self-Hosted Compiler has been delayed in the past due to the large tables of instructions that would be declared in source files, likely running into the unwieldly memory consumption properties of the Bootstrap Compiler. But now that we are shipping a stage3 build, this obstacle has been overcome.

Zig 0.9.x allowed comptime expressions to produce the assembly template for inline assembly. For most of the 0.10.0 release cycle, Zig emitted a compile error when inline assembly source code was not a string literal, however, that restriction was lifted just before this 0.10.0 release.

Zig is at a crossroads right now with regards to inline assembly. On one hand, there is this proposal: parse inline assembly syntax according to a set of dialects; integrate inline assembly more closely with the zig language

Meanwhile, other stakeholders have rightfully pointed out that inline assembly abstractions based on string concatenation are easy to reason about and do not require any more language features added.

Either way, Zig needs to gain the ability to parse inline assembly for all targets, so that it can get lowered to the LLVM backend as well as the other Code Generation backends, so that error handling, semantic analysis, and language features of inline assembly can be uniform across backends, something that would not be the case if LLVM is sometimes responsible for assembly code parsing and sometimes not.

Whichever path Zig ends up going down, we will make sure to involve the users who will ultimately be affected, such as the Zig Embedded Group, and the various folks who have started operating system projects written in Zig.

Boolean Logic with comptime RHS §

foo() and false as well as bar() or true are now compile-time-known expressions. The left-hand side is allowed to contain runtime side effects, which are preserved as expected. Allows code like this to compile:

test.zig
const std = @import("std");

var i: usize = 0;
fn foo() bool {
    const stdout = std.io.getStdOut().writer();
    stdout.print("i = {}\n", .{i}) catch unreachable;
    return true;
}

pub fn main() void {
    if (false and foo()) { // This works already today
        @compileError("Condition should be comptime-false");
    }
    i += 1;
    if (foo() and false) { // PR allows this condition to be comptime-known
        @compileError("Condition should be comptime-false");
    }
}
Shell
$ zig build-exe test.zig

$ ./test
i = 1

Note that side effects are still respected, but the result can be compile-time-known despite them. This is the same way Zig handles, e.g. x * 0 and 0 * x (#6768).

Bootstrap Compiler §

In the previous release, the main Zig compiler everybody used was the bootstrap compiler, written in C++, also known as stage1. This release changes the default to become the Self-Hosted Compiler, however, the legacy compiler can be selected with -fstage1.

Despite the main focus being the Self-Hosted Compiler, this release brings some improvements to the legacy compiler codebase as well:

Self-Hosted Compiler §

Zero the Ziguana

The main feature of this release cycle is the début of the self-hosted compiler. It is now enabled by default, however the Bootstrap Compiler is available behind the -fstage1 flag for those not ready to upgrade.

This compiler implementation beats the older one in terms of performance and memory usage. Here are two data points for the compiler building itself (measured on an Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz):

The new compiler implementation is slightly faster despite that, unlike the Bootstrap Compiler, it is capable of incremental compilation due to being tightly coupled with the linker. The bigger compilation speed wins will come with the Code Generation backends that do not use LLVM.

The vast majority of the development effort of this release cycle was spent on this new compiler implementation, and therefore the Language Changes are minimal. Instead, this new codebase lays the groundwork for major enhancements, mainly concerned with speeding up the edit/test/debug cycle. Now that the self-hosted compiler is no longer sucking up so much effort, you should expect to see significant, visible progress on these Roadmap milestones.

Miscellaneous improvements:

Is it time to upgrade? §

One option you have is to simply wait for 0.10.1, or even 0.11.0, before attempting an upgrade. This gives you the smoothest experience, letting other, more brave souls, help with quality assurance before you get your hands dirty. There is no shame in this; do what works for you.

If anything goes wrong in the upgrade, you can always start using -fstage1 to get the old compiler, or put the equivalent in your build.zig file, so that your users can continue using zig build as usual.

exe.use_stage1 = true;

The new compiler, sometimes called "stage2" or "stage3", is in many ways better than the old compiler (also called "stage1"), however it is not yet strictly better.

All 0.10.x releases will have the -fstage1 option; the upgrade only will become mandatory starting with 0.11.0. For some users, sticking with stage1 for the duration of the 0.10.x release will be the best move; for others, upgrading to self-hosted earlier will be right for them. This guide should help you decide which category you fall into.

Improvements over stage1 §

Falling short of stage1 §

Although we fully intend to make the self-hosted compiler a strict improvement, that is not the case yet.

How to Upgrade §

Assuming that you have decided the time is nigh, here are some tips.

Although most of the language is the same between stage1 and self-hosted, there are some incompatibilities. Here are two strategies to deal with this:

  1. Have a different branch for when using Zig self-hosted. Once you decide to commit to the upgrade, merge that branch into your main branch.
  2. Make the codebase support both stage1 and self-hosted at the same time using conditional compilation based on std.builtin.CompilerBackend.

In addition to the sub-sections below, see also these Language Changes which should be considered part of the upgrade guide:

Void Literal Syntax §

{} vs .{}

This one is pretty simple:

test.zig
test {
    var x: void = .{};
    _ = x;
}
Shell
$ zig test test.zig
docgen_tmp/test.zig:2:20: error: expected type 'void', found '@TypeOf(.{})'
    var x: void = .{};
                  ~^~

.{} is a struct literal; {} is a void value. It has always been a bug for the Bootstrap Compiler to accept this code. Just change .{} to {}.

-    var x: void = .{};+    var x: void = {};

Escaped Pointer to Parameter §

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

test "escaped pointer to parameter" {
    var s: S = .{ .field = 1234 };
    const value = s.foo();
    try expect(value == &s.field);
}

const S = struct {
    field: i32,

    fn foo(s: S) *const i32 {
        return &s.field; // XXX: escaped pointer to parameter
    }
};
Shell
$ zig test test.zig
1/1 test.escaped pointer to parameter... FAIL (TestUnexpectedResult)
/home/andy/Downloads/zig/lib/std/testing.zig:347:14: 0x21156f in expect (test)
    if (!ok) return error.TestUnexpectedResult;
             ^
docgen_tmp/test.zig:7:5: 0x2116a7 in test.escaped pointer to parameter (test)
    try expect(value == &s.field);
    ^
0 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/home/andy/.cache/zig/o/92074f0ff860192f15f1d46f1cf33c8d/test

This test passes with the Bootstrap Compiler despite having a critical bug: a temporary is created by taking the address of the the pass-by-value parameter which is then returned from the function.

The legacy compiler is naive, always passing structs by pointer in code such as this. Meanwhile, the self-hosted compiler efficiently takes advantage of smaller arguments such as this, passing them truly by value, revealing the bug.

Hopefully in the future Zig will have runtime safety for this, however, currently this will manifest as a use-after-free. So if you find a pointer to bogus data, double check that the pointer was not created this way.

Once the problem has been identified, the fix is simple:

-    fn foo(s: S) *const i32 {+    fn foo(s: *const S) *const i32 {

C ABI Compatibility §

C ABI compatibility is notoriously difficult when using LLVM. In Zig 0.9.x, there were several major miscompilations for functions with C calling convention, especially when using an architecture other than x86_64.

Thanks to a nicer abstraction for dealing with the C ABI, plus some good old-fashioned hard work, in 0.10.0, Zig has near full C ABI compliance on several targets:

You can have a look at the new test harness to see what exactly is covered. Each of these targets are now tested with every CI run with QEMU.

C Translation §

Cache System §

Zig now places the zig-cache directory next to build.zig. If no build.zig file is found, it falls back to using the global cache directory (#11672).

Internally, there are now two cache modes: incremental and whole. Whole cache mode is what everyone is used to from previous Zig releases. It relies on hashing the full set of inputs, including every source file that is directly or indirectly imported. Incremental cache mode is new and is based on re-opening existing build artifacts and rebuilding only the modified symbols. Although Zig's incremental compilation system is still experimental, it can be enabled with the --watch flag.

Incremental cache mode is the default for the self-hosted code generation backends, and for the LLVM backend when not using the Build System. Because the compiler does not yet serialize and restore its state, without the --watch flag, this will result in recompiling with every invocation of the compiler. On the other hand, it results in less garbage accumulating into the zig-cache directory.

Safety for Switching on Corrupted Enums §

The following code, when run with Zig 0.9.1, will invoke undefined behavior:

test.zig
const std = @import("std");

const E = enum(u32) {
    one = 1,
    two = 2,
};

test "example" {
    var a: E = .two;
    @ptrCast(*u32, &a).* = 255;
    switch (a) {
        .one => @panic("one"),
        else => return,
    }
}
Shell
$ zig test test.zig
1/1 test.example... thread 1665374 panic: switch on corrupt value
docgen_tmp/test.zig:12:17: 0x2115b6 in test.example (test)
        .one => @panic("one"),
                ^
/home/andy/Downloads/zig/lib/test_runner.zig:63:28: 0x212be8 in main (test)
        } else test_fn.func();
                           ^
/home/andy/Downloads/zig/lib/std/start.zig:596:22: 0x211ebd in posixCallMainAndExit (test)
            root.main();
                     ^
/home/andy/Downloads/zig/lib/std/start.zig:368:5: 0x211981 in _start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^
error: the following test command crashed:
/home/andy/.cache/zig/o/92074f0ff860192f15f1d46f1cf33c8d/test

As you can see, with Zig 0.10.0, this problem is caught with a safety check. A future enhancement will further refine this to make the stack trace point at the switch expression rather than the prong.

Referenced-By Traces §

You can find referenced-by traces in a few other example snippets throughout this document, but here is illustrated a real world example that was particularly helpful:

Shell
$ stage3/bin/zig build -p wasi -Dskip-install-lib-files -Dtarget=wasm32-wasi
/home/andy/Downloads/zig/lib/std/os.zig:110:28: error: root struct of file 'os.wasi' has no member named 'PATH_MAX'
pub const PATH_MAX = system.PATH_MAX;
                     ~~~~~~^~~~~~~~~
referenced by:
    calcInstallNameLen: /home/andy/Downloads/zig/src/link/MachO.zig:3540:53
    calcLCsSize: /home/andy/Downloads/zig/src/link/MachO.zig:3577:24
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

Here I tried to build the Self-Hosted Compiler for WASI but ran into some code that incorrectly tried to reference std.os.PATH_MAX even though no such definition exists for the WASI target.

You can see that without the referenced-by trace, this bug would have been annoying to fix, but with it, the fix was trivial.

Error Return Traces §

Zig's error return traces are a resource-efficient alternative to the tracebacks provided by exceptions in other languages. With the default limit of 32 stack frames, only 256 bytes per-thread are needed to maintain the error trace.

Unwrapping Errors in Switch §

A small quality-of-life improvement Vexu thought of while trying to figure out where an error.GenericPoison was coming from (#12889). Example situation:

test.zig
fn foo() !void {
    return error.Foo;
}
fn bar() !void {
    try foo();
}
fn baz() void {
    // bar() catch unreachable;
    // bar() catch @panic("foo");
    // const S = struct {
    //     const S = struct {
    //         const fooo = "foo";
    //     };
    // };
    bar() catch |err| switch (err) {
        // error.Foo => @panic(S.S.fooo),
        error.Foo => unreachable,
    };
}
test {
    baz();
}
Shell
$ zig test test.zig
1/1 test_0... thread 1665418 panic: attempt to unwrap error: Foo
docgen_tmp/test.zig:2:5: 0x211ef8 in foo (test)
    return error.Foo;
    ^
docgen_tmp/test.zig:5:5: 0x211ff3 in bar (test)
    try foo();
    ^
docgen_tmp/test.zig:17:22: 0x2115d1 in baz (test)
        error.Foo => unreachable,
                     ^
docgen_tmp/test.zig:21:8: 0x211548 in test_0 (test)
    baz();
       ^
/home/andy/Downloads/zig/lib/test_runner.zig:63:28: 0x212d58 in main (test)
        } else test_fn.func();
                           ^
/home/andy/Downloads/zig/lib/std/start.zig:596:22: 0x211ebd in posixCallMainAndExit (test)
            root.main();
                     ^
/home/andy/Downloads/zig/lib/std/start.zig:368:5: 0x211981 in _start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^
error: the following test command crashed:
/home/andy/.cache/zig/o/92074f0ff860192f15f1d46f1cf33c8d/test

Before, it did this instead:

Shell
Test [1/1] test_0... thread 18432 panic: reached unreachable code
./a.zig:17:22: 0x20f0fc in baz (test)
        error.Foo => unreachable,
                     ^
./a.zig:21:8: 0x20f078 in test_0 (test)
    baz();
       ^
/home/vexu/Documents/zig/zig/lib/test_runner.zig:79:28: 0x2105b5 in main (test)
        } else test_fn.func();
                           ^
/home/vexu/Documents/zig/zig/lib/std/start.zig:568:22: 0x20f9bd in posixCallMainAndExit (test)
            root.main();
                     ^
/home/vexu/Documents/zig/zig/lib/std/start.zig:340:5: 0x20f3c2 in _start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^
error: the following test command crashed:

This also makes @panic show error return trace in these same situations which you can test by uncommenting lines in the example.

Removal of Useless Stack Frames §

Prior to this release, error return traces suffered from a major drawback: handled errors were never cleaned up and would pollute the error trace. Zig now intelligently removes any error traces corresponding to errors handled in a catch { ... } or if ... else |err| { ... } block.

pub fn main() !void {
    makeEggs() catch {
        if (money < 10)  
            return error.OutOfMoney; // If executed: trace shows makeEggs()'s error, followed by this line

        try buyIngredients();           // On error: trace shows makeEggs()'s error, then buyIngredients()'s error
    };
    // makeEggs() removed from the trace here

    try serveBreakfast();               // On error: trace shows serveBreakfast()'s error only
}

Very long error return traces are also trimmed correctly and print a helpful note to let you know about the missing entries:

...
./test_err.zig:5:5: 0x10224b39e in recursive__anon_1055 (test_err)
    return recursive(N - 1);
    ^
./test_err.zig:5:5: 0x10224b35e in recursive__anon_1054 (test_err)
    return recursive(N - 1);
    ^
(98 additional stack frames skipped...)

There is one restriction to be aware of: error traces do not follow errors into local variables (var) by default. This restriction is what allows all functions in the thread to share the same 256-byte error trace storage.

The new std.debug.Trace API allows users to copy and store error return traces for mutable variables where needed.

Here is a real world example. Before, one might get debug output that looks like this:

Shell
$ ~/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/zig run tools/copy-from-glibc.zig -- ~/glibc/multi-2.32/install/glibcs headers 2.32
error: FileNotFound
tools/copy-from-glibc.zig:21:23: 0x218e39 in main (copy-from-glibc)
        var sub_dir = try glibcs_dir.openIterableDir(sub_path, .{});
                      ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2755:19: 0x21f936 in mkdiratZ (copy-from-glibc)
        .EXIST => return error.PathAlreadyExists,
                  ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:2715:9: 0x218537 in mkdirat (copy-from-glibc)
        return mkdiratZ(dir_fd, &sub_dir_path_c, mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1402:9: 0x21842b in makeDir (copy-from-glibc)
        try os.mkdirat(self.fd, sub_path, default_new_dir_mode);
        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/os.zig:1689:23: 0x24b498 in openatZ (copy-from-glibc)
            .NOENT => return error.FileNotFound,
                      ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1747:25: 0x23dbf1 in openDirFlagsZ (copy-from-glibc)
            else => |e| return e,
                        ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1717:13: 0x21e0a6 in openDirZ (copy-from-glibc)
            return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | symlink_flags);
            ^
/home/andy/tmp/zig-linux-x86_64-0.10.0-dev.4474+b41b35f57/lib/std/fs.zig:1654:40: 0x2180e5 in openIterableDir (copy-from-glibc)
            return IterableDir{ .dir = try self.openDirZ(&sub_path_c, args, true) };
                                       ^

After this improvement, we see this instead:

Shell
$ ~/zig-bootstrap/out/host/bin/zig run tools/copy-from-glibc.zig -- ~/glibc/multi-2.32/install/glibcs headers 2.32
error: FileNotFound
/home/andy/zig-bootstrap/out/host/lib/zig/std/os.zig:1689:23: 0x24c038 in openatZ (copy-from-glibc)
            .NOENT => return error.FileNotFound,
                      ^
/home/andy/zig-bootstrap/out/host/lib/zig/std/fs.zig:1747:25: 0x23e4e8 in openDirFlagsZ (copy-from-glibc)
            else => |e| return e,
                        ^
/home/andy/zig-bootstrap/out/host/lib/zig/std/fs.zig:1717:13: 0x21e406 in openDirZ (copy-from-glibc)
            return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | symlink_flags);
            ^
/home/andy/zig-bootstrap/out/host/lib/zig/std/fs.zig:1654:40: 0x2182d5 in openIterableDir (copy-from-glibc)
            return IterableDir{ .dir = try self.openDirZ(&sub_path_c, args, true) };
                                       ^
tools/copy-from-glibc.zig:21:23: 0x219059 in main (copy-from-glibc)
        var sub_dir = try glibcs_dir.openIterableDir(sub_path, .{});
                      ^

This was an old issue that plagued Zig developers for a long time. We have Cody Tapscott to thank for solving it.

Code Generation §

While the default backend is still LLVM, Zig is starting to invest in providing its own code generation. The primary motivation for this is compilation speed. However, there is also a benefit to providing a bootstrapping process that does not depend on C++, CMake, Python, libxml, zlib, and zstd, all of which LLVM require in order to build. Furthermore, there is value in providing healthy competition so that compiler backends do not become a monoculture.

As a point of comparison, a stripped release build of Zig with LLVM is 169 MiB, while without LLVM (but with all the code generation backends you see here) it is 4.4 MiB.

WebAssembly Backend §

One of the visible improvements to the self-hosted Wasm backend is its capabilities to generate code. The backend is now capable of passing 75% of the behavior tests, compared to 13% in Zig 0.9.x. At this point, most Zig features are supported. For features of which the WebAsssembly architecture has no support, the backend is already capable of generating calls into compiler-rt, and correctly link with it. This is thanks to the C ABI Compatibility work that was done during this release. This also benefitted the LLVM backend with regards to its WebAssembly target, as the logic for the C ABI implemtation is fully shared between the backends. This will ensure that regardless of the chosen backend, the function signatures they generate are equal. One notable feature that was previously missing was support for DWARF Linking when targetting WebAssembly. This is now fully supported in the LLVM backend, and partly in the native backend. This allows for a great developer experience when targetting WebAssembly. The goal is to add full support to the native backend by its next release.

Some invisible improvements were done also. The backend was refactored to be aware of the Wasm stack and leave locals there when possible, avoiding unnecessary stores. This means we not only generate fewer instructions, but also create fewer locals. Besides this, the backend now integrates with Liveness Analysis, reusing locals that are no longer being referenced. All of those changes are with the goal of making the native backends not only compile fast, but also generate code which runs faster than when using the LLVM backend.

Some notable changes that were partly responsible for the above include:

C Backend §

A new contributor jacobly0 came out of nowhere and did a massive amount of work to Zig's C backend, reaching these notable milestones:

Additionally, in this release zig.h moved to become an installation file. Instead of zig.h being baked into the compiler binary, it is a header file distributed along with all the other header files distributed with Zig (#11643).

Soon, this backend will be used to replace the Bootstrap Compiler with a C implementation that outputs C code (#5246).

x86 Backend §

Although this backend remains experimental, it has seen considerable progress during this release cycle. Notable milestones reached:

All of the above culminates in the x86_64 self-hosted backend passing 875/1365 (64%) of the behavior tests compared to the LLVM backend. It will need to get very close to 100% before being enabled by default for debug mode.

arm Backend §

This backend remains experimental, however it now supports error unions, structs, and switch statements and is thereby passing 790/1365 (58%) of the behavior tests. It will need to get very close to 100% before being enabled by default for debug mode.

aarch64 Backend §

This backend remains experimental, however it now supports error unions, structs, and switch statements and is thereby passing 560/1365 (41%) of the behavior tests. It will need to get very close to 100% before being enabled by default for debug mode.

Self-Hosted Linker §

The self-hosted linker is tightly coupled with the Self-Hosted Compiler. There has been progress on ELF and COFF however they are not generally usable yet. By default, LLD will be used for these object formats, however, one can opt in to the self-hosted linker with -fno-LLD.

MachO §

Ziggy the Ziguana

The incremental MachO linker, like its traditional linking counterpart, has seen a major rewrite where Zig now lays out the incrementally linked binary such that there is one section per segment. This might sound wasteful but it allows Zig to use the quick file space allocation algorithm used already in the ELF and COFF linkers.

How does it work? By enclosing a single section within a single segment Zig is able to disassociate the file offsets from the allocated virtual memory addresses. This provides the uncanny ability to reorder and move the sections within the underlying binary file without affecting the ordering in the virtual memory space. This implies that if the section grows beyond what was originally allocated, Zig only needs to copy its contents in the file to a new location without any virtual address space recalculations unless the section has also exceeded its allocated virtual address space. But even then only some atoms are affected in subsequent sections in the virtual address space.

Thanks to this trick, Zig is on the path of achieving truly incremental linking for MachO which should also make implementing hot-code swapping for MachO much simpler and more efficient.

For a more in-depth look into the changes involved have a look at this pull request: macho: rewrite incremental linker, and init splitting of linking contexts

In addition, the following improvements have been made:

COFF §

The COFF linker has been reimplemented from scratch giving our Windows users a chance to finally be able to play with the incremental linking (using the --watch flag and x86 Backend via -fno-LLVM). In addition, the linker now matches the implementation progress of both ELF and MachO, and hence, it is now possible to run the behavior tests on x86_64-windows successfully:

Shell
> C:\Users\kubkon\dev\zig\stage3\bin\zig.exe test test\behavior.zig -Itest -fno-LLVM -fno-unwind-tables -fno-emit-implib
817 passed; 571 skipped; 0 failed.

For more detailed description of the implementation, have a look at this pull request: coff: implement enough of the incremental linker to pass behavior and incremental tests on Windows

ELF §

The self-hosted ELF linker remains experimental, capable of incremental compilation of simple Zig programs but not capable of acting as a traditional linker.

Wasm Modules §

Major progress has been made on the in-house WebAssembly linker. Zig is nearing feature-parity with wasm-ld, which is the linker the compiler uses today when targeting WebAssembly. The linker is now capable of linking with most object files, while also being able to link with archive files such as compiler-rt. During linking, it now correctly relocates Debugging information. The next milestone would be to make the in-house linker the default linker. To accomplish this, the following features must be implemented:

Those features will bring the linker to near feature parity. By making the switch before being fully feature complete, we can begin the debugging process sooner, as well as focusing on optimizing the memory usage and runtime performance of the linker.

The following changes highlight some of the features mentioned above:

DWARF Linking §

The incremental DWARF emitter/linker is now capable of emitting correctly relocated debug info for x86_64, arm and aarch64 architectures on Linux and macOS. The state of implementation varies slightly between architectures with x86_64 being the furthest ahead.

Example progress includes correctly generating DWARF debug info for slices including []const u8 for passing as formal parameters. Breaking on a function accepting a slice in gdb will now yield the same behavior as the Bootstrap Compiler and LLVM backend:

test.zig
fn sumArrayLens(a: []const u32, b: []const u8) usize {
    return a.len + b.len;
}

Both a and b can now be inspected in the debugger:

Breakpoint 1, sumArrayLens (a=..., b=...) at arr.zig:59
(gdb) p a
$1 = {ptr = 0x7fffffff685c, len = 5}
(gdb) p b
$2 = {ptr = 0x7fffffff683d "\252\252\252\\h\377\377\377\177", len = 3}
(gdb)

Linker Performance §

This release saw a general rewrite of the majority of the linker in the spirit of data-oriented design which led to:

In the benchmarks below, zld refers to our linker as a standalone binary (which you can find at kubkon/zld), lld to LLVM's linker, and ld64 to Apple's linker. I should also point out that we are still missing a number of optimisations in the linker such as cstring deduplication, compression of dynamic linker's relocations, and synthesising of the unwind info section, so this difference between zld and other linkers will most likely shrink a little.

I've run the benchmarks on a MacBook Pro 16" with M1 Pro SoC and 32GB RAM. The generated code for the linker was not using hardware sha256 extensions.

Linking redis-server binary:

Shell
❯ hyperfine ./zld ./lld ./ld64 --warmup 60
Benchmark 1: ./zld
  Time (mean ± σ):      35.6 ms ±   0.4 ms    [User: 35.9 ms, System: 10.4 ms]
  Range (min … max):    34.8 ms …  36.4 ms    79 runs

Benchmark 2: ./lld
  Time (mean ± σ):      49.2 ms ±   0.8 ms    [User: 42.6 ms, System: 17.6 ms]
  Range (min … max):    48.0 ms …  51.2 ms    59 runs

Benchmark 3: ./ld64
  Time (mean ± σ):      47.2 ms ±   0.5 ms    [User: 60.1 ms, System: 14.4 ms]
  Range (min … max):    46.2 ms …  48.1 ms    61 runs

Summary
  './zld' ran
    1.32 ± 0.02 times faster than './ld64'
    1.38 ± 0.03 times faster than './lld'

Linking Self-Hosted Compiler binary:

Shell
❯ hyperfine ./zld ./lld ./ld64 --warmup 5
Benchmark 1: ./zld
  Time (mean ± σ):      1.934 s ±  0.012 s    [User: 2.870 s, System: 0.468 s]
  Range (min … max):    1.923 s …  1.962 s    10 runs

Benchmark 2: ./lld
  Time (mean ± σ):      1.153 s ±  0.014 s    [User: 1.289 s, System: 0.230 s]
  Range (min … max):    1.141 s …  1.179 s    10 runs

Benchmark 3: ./ld64
  Time (mean ± σ):      2.349 s ±  0.006 s    [User: 3.875 s, System: 0.218 s]
  Range (min … max):    2.341 s …  2.357 s    10 runs

Summary
  './lld' ran
    1.68 ± 0.02 times faster than './zld'
    2.04 ± 0.02 times faster than './ld64'

Linker Test Harness §

The incremental compilation that utilises Zig's self-hosted native backends has seen the addition of a vastly improved and easily extensible test harness. In order to add a new incremental test (emulating the --watch flag), it is now only required to drop a Zig source file in the test harness directory with a specially crafted comment instructing the harness of several test parameters such as test target, and that's it. Thanks to this dynamic approach there is no longer any need in recompiling the harness (#11530) (#11572).

Linkers also have received a new test harness, which is based on the StandaloneTestContext and CheckFileStep, and as such assumes each test case specifies a build.zig script where we instruct the harness what the link flags/conditions are the expected results such as a runtime result or a greppable contents of the generated binary file (#11910).

Notable features:

Standard Library §

The Zig standard library is still unstable and mainly serves as a testbed for the language. After the Self-Hosted Compiler is completed, the language stabilized, and Package Manager completed, then it will be time to start working on stabilizing the standard library. Until then, experimentation and breakage without warning is allowed.

Math §

File System §

Optimized Tree Deletion §

Ryan Liptak improved the performance of std.fs.Dir.deleteTree (#13073). The new implementation has the same constraints:

The new implementation has better performance than rm -rf (at least on Linux - see benchmark details):

$ hyperfine "./rm-0.10.0 my-app-copy" "./rm-0.9.1 my-app-copy" "rm -rf my-app-copy" --prepare="cp -r my-app my-app-copy" --warmup 1 --runs 20
Benchmark #1: ./rm-0.10.0 my-app-copy
  Time (mean ± σ):     382.7 ms ±   7.8 ms    [User: 13.6 ms, System: 358.9 ms]
  Range (min … max):   372.2 ms … 395.9 ms    20 runs

Benchmark #2: ./rm-0.9.1 my-app-copy
  Time (mean ± σ):     701.2 ms ±  28.3 ms    [User: 36.8 ms, System: 646.2 ms]
  Range (min … max):   673.4 ms … 799.4 ms    20 runs

Benchmark #3: rm -rf my-app-copy
  Time (mean ± σ):     423.9 ms ±  35.4 ms    [User: 17.5 ms, System: 379.4 ms]
  Range (min … max):   391.6 ms … 518.9 ms    20 runs

Summary
  './rm-0.10.0 my-app-copy' ran
    1.11 ± 0.10 times faster than 'rm -rf my-app-copy'
    1.83 ± 0.08 times faster than './rm-0.9.1 my-app-copy'

The new implementation accomplishes this by having a fixed-size buffer on the stack, temporarily falling back to a slower algorithm when it encounters deeply nested file system trees. The fallback implementation is also optimized to reduce the number of syscalls emitted.

Data Structures §

Crypto §

We have Frank Denis to thank for Zig's state-of-the-art cryptography capabilities. In this release:

ECDSA Signatures §

Zig 0.10.0 adds support for ECDSA signatures (#11855).

ECDSA is the most commonly used signature scheme today, mainly for historical and conformance reasons. It is a necessary evil for many standard protocols such as TLS and JWT.

It is tricky to implement securely and has been the root cause of multiple security disasters, from the Playstation 3 hack to multiple critical issues in OpenSSL and Java.

This implementation combines lessons learned from the past with recent recommendations.

In Zig, the NIST curves that ECDSA is almost always instantied with use formally verified field arithmetic, giving us peace of mind even on edge cases. And the API rejects neutral elements where it matters, and unconditionally checks for non-canonical encoding for scalars and group elements. This automatically eliminates common vulnerabilities.

ECDSA's security heavily relies on the security of the random number generator, which is a concern in some environments.

This implementation mitigates this by computing deterministic nonces using the conservative scheme from Pornin et al. with the optional addition of randomness as proposed in Ericsson's "Deterministic ECDSA and EdDSA Signatures with Additional Randomness" document. This approach mitigates both the implications of a weak RNG and the practical implications of fault attacks.

Project Wycheproof is a Google project to test crypto libraries against known attacks by triggering edge cases. It discovered vulnerabilities in virtually all major ECDSA implementations.

The entire set of ECDSA-P256-SHA256 test vectors from Project Wycheproof is included here. Zero defects were found in this implementation.

Instead of raw byte strings for keys and signatures, we introduce the Signature, PublicKey and SecretKey structures.

The reason is that a raw byte representation would not be optimal. There are multiple standard representations for keys and signatures, and decoding/encoding them may not be cheap (field elements have to be converted from/to the montgomery domain).

Ed25519 API changes §

The std.crypto.sign.ed25519 API has been changed to adopt the same structure as the ECDSA API. This is a breaking change.

Instead of raw bytes, Ed25519 keys are now represented with the newly introduced Ed25519.PublicKey and Ed25519.SecretKey types.

Similarly, the Ed25519.Signature type is now used to store signatures.

In Zig 0.9.x, key pair creation, signature computation and verification were typically done this way:

const Ed25519 = std.crypto.sign.Ed25519;

// Key pair creation
const key_pair = try Ed25519.KeyPair.create(null);

// Signature computation
const sig = try Ed25519.sign("message", key_pair, null);

// Signature verification
try Ed25519.verify(sig, "message", key_pair.public_key);

In Zig 0.10.0, the code above becomes:

const Ed25519 = std.crypto.sign.Ed25519;

// Key pair creation
const key_pair = try Ed25519.KeyPair.create(null);

// Signature computation
const sig = try key_pair.sign("message", null);

// Signature verification
try sig.verify("message", key_pair.public_key);

In Zig 0.9.x, the entire message to be signed had to be loaded in memory before signing. In Zig 0.10, the message can be streamed, which is especially useful for large messages.

For example, the following code signs a message that is too large to fit in memory:

var signer = try key_pair.signer(null);
signer.update("message_part_1");
signer.update("message_part_2");
signer.update("message_part_3");
const sig = signer.finalize();

Similarly, the following code verifies a multi-part message:

var verifier = try sig.verifier(key_pair.public_key);
verifier.update("message_part_1");
verifier.update("message_part_2");
verifier.update("message_part_3");
try verifier.verify();

Add Xoodoo, Deprecate Gimli §

Zig 0.10.0 adds the Xoodoo permutation, preparing for Gimli deprecation (#11866).

Gimli was a game changer. A permutation that is large enough to be used in sponge-like constructions, yet small enough to be compact to implement and fast on a wide range of platforms.

And Gimli being part of the Zig standard library was awesome.

But since then, Gimli entered the NIST Lightweight Cryptography Competition, competing againt other candidates sharing a similar set of properties.

Unfortunately, Gimli didn't pass the 3rd round.

There are no practical attacks against Gimli when used correctly, but NIST's decision means that Gimli is unlikely to ever get any traction.

So, maybe the time has come to move Gimli from the standard library to another repository.

We shouldn't do it without providing an alternative, though. And the best candidate for this is probably Xoodoo.

Xoodoo is the core function of Xoodyak, one of the finalists of the NIST LWC competition, and the most direct competitor to Gimli. It is also a 384-bit permutation, so it can easily be used everywhere Gimli was used with no parameter changes.

It is the building block of Xoodyak (for actual encryption and hashing) as well as Charm, that some Zig applications are already using.

Like Gimli that it was heavily inspired from, it is compact and suitable for constrained environments.

This change adds the Xoodoo permutation to std.crypto.core.

The set of public functions includes everything required to later implement existing Xoodoo-based constructions.

In order to prepare for the Gimli deprecation, the default CSPRNG was changed to a Xoodoo-based that works exactly the same way.

Exploit CPU Features for SHA-256 §

On x86_64 and AArch64, SHA-256 now takes advantage of specialized CPU features when available.

This can affect MachO linking performance due to Apple's signature requirement.

On my Intel(R) Core(TM) i9-9980HK, for example, this CPU feature is not available, and I therefore do not observe any improved performance. However, Apple's M1 laptops do have this feature, and I observed SHA-256 increase from 199 MiB/s to 2.1 GiB/s in the crypto benchmark.

Concurrency §

Networking §

Networking has mostly been quiet until now, but it is about to get lit as we dive into the Package Manager in the next release cycle.

Testing §

Check All Allocation Failures §

There is a new API that checks for memory leaks (and other problems) caused by allocation failures by taking advantage of the FailingAllocator and inducing failure at every allocation point within the provided function (#10586).

Below is the function's giant doc comment in a more readable form:

Exhaustively check that allocation failures within test_fn are handled without introducing memory leaks. If used with the testing.allocator as the backing_allocator, it will also perform memory sanitization when runtime safety is enabled.

The provided test_fn must have a std.mem.Allocator as its first argument, and must have a return type of !void. Any extra arguments of test_fn can be provided via the extra_args tuple.

Any relevant state shared between runs of test_fn must be reset within test_fn.

Expects that the test_fn has a deterministic number of memory allocations. An error will be returned if non-deterministic allocations are detected.

The strategy employed is to:

Here is an example of using a simple test case that will cause a leak when the allocation of bar fails (but will pass normally):

cleanup_leak.zig
const std = @import("std");

test {
    const length: usize = 10;
    const allocator = std.testing.allocator;
    var foo = try allocator.alloc(u8, length);
    var bar = try allocator.alloc(u8, length);

    allocator.free(foo);
    allocator.free(bar);
}
Shell
$ zig test cleanup_leak.zig
1/1 test_0... OK
All 1 tests passed.

When the test case is converted to use the new checkAllAllocationFailures API, the test fails:

test.zig
const std = @import("std");

fn testImpl(allocator: std.mem.Allocator, length: usize) !void {
    var foo = try allocator.alloc(u8, length);
    var bar = try allocator.alloc(u8, length);

    allocator.free(foo);
    allocator.free(bar);
}

test {
    const length: usize = 10;
    const allocator = std.testing.allocator;
    try std.testing.checkAllAllocationFailures(allocator, testImpl, .{length});
}
Shell
$ zig test test.zig
1/1 test_0...
fail_index: 1/2
allocated bytes: 10
freed bytes: 0
allocations: 1
deallocations: 0
allocation that was made to fail:
docgen_tmp/test.zig:5:34: 0x213b19 in testImpl (test)
    var bar = try allocator.alloc(u8, length);
                                 ^
/home/andy/Downloads/zig/lib/std/testing.zig:693:13: 0x214081 in checkAllAllocationFailures__anon_1936 (test)
        if (@call(.{}, test_fn, args)) |_| {
            ^
docgen_tmp/test.zig:14:47: 0x2145fa in test_0 (test)
    try std.testing.checkAllAllocationFailures(allocator, testImpl, .{length});
                                              ^
/home/andy/Downloads/zig/lib/test_runner.zig:63:28: 0x21c568 in main (test)
        } else test_fn.func();
                           ^
/home/andy/Downloads/zig/lib/std/start.zig:596:22: 0x214e9d in posixCallMainAndExit (test)
            root.main();
                     ^
/home/andy/Downloads/zig/lib/std/start.zig:368:5: 0x214961 in _start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^

FAIL (MemoryLeakDetected)
/home/andy/Downloads/zig/lib/std/mem/Allocator.zig:72:13: 0x229226 in allocImpl (test)
            return @call(.{ .modifier = .always_inline }, allocFn, .{ self, len, ptr_align, len_align, ret_addr });
            ^
/home/andy/Downloads/zig/lib/std/mem/Allocator.zig:302:24: 0x21b552 in allocAdvancedWithRetAddr__anon_3779 (test)
    const byte_slice = try self.rawAlloc(byte_count, a, len_align, return_address);
                       ^
/home/andy/Downloads/zig/lib/std/mem/Allocator.zig:194:5: 0x213c1b in alloc__anon_2377 (test)
    return self.allocAdvancedWithRetAddr(T, null, n, .exact, @returnAddress());
    ^
docgen_tmp/test.zig:5:15: 0x213b37 in testImpl (test)
    var bar = try allocator.alloc(u8, length);
              ^
/home/andy/Downloads/zig/lib/std/testing.zig:714:21: 0x2141d6 in checkAllAllocationFailures__anon_1936 (test)
                    return error.MemoryLeakDetected;
                    ^
docgen_tmp/test.zig:14:5: 0x21460d in test_0 (test)
    try std.testing.checkAllAllocationFailures(allocator, testImpl, .{length});
    ^
[gpa] (err): memory address 0x7f4ef5538000 leaked:
docgen_tmp/test.zig:4:34: 0x213aa4 in testImpl (test)
    var foo = try allocator.alloc(u8, length);
                                 ^
/home/andy/Downloads/zig/lib/std/testing.zig:693:13: 0x214081 in checkAllAllocationFailures__anon_1936 (test)
        if (@call(.{}, test_fn, args)) |_| {
            ^
docgen_tmp/test.zig:14:47: 0x2145fa in test_0 (test)
    try std.testing.checkAllAllocationFailures(allocator, testImpl, .{length});
                                              ^
/home/andy/Downloads/zig/lib/test_runner.zig:63:28: 0x21c568 in main (test)
        } else test_fn.func();
                           ^
/home/andy/Downloads/zig/lib/std/start.zig:596:22: 0x214e9d in posixCallMainAndExit (test)
            root.main();
                     ^
/home/andy/Downloads/zig/lib/std/start.zig:368:5: 0x214961 in _start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^


0 passed; 0 skipped; 1 failed.
1 errors were logged.
1 tests leaked memory.
error: the following test command failed with exit code 1:
/home/andy/.cache/zig/o/92074f0ff860192f15f1d46f1cf33c8d/test

Running this test will show that foo is leaked when the allocation of bar fails. The simplest fix, in this case, would be to use defer:

leak_fixed.zig
const std = @import("std");

fn testImpl(allocator: std.mem.Allocator, length: usize) !void {
    var foo = try allocator.alloc(u8, length);
    defer allocator.free(foo);
    var bar = try allocator.alloc(u8, length);
    defer allocator.free(bar);
}

test {
    const length: usize = 10;
    const allocator = std.testing.allocator;
    try std.testing.checkAllAllocationFailures(allocator, testImpl, .{length});
}
Shell
$ zig test leak_fixed.zig
1/1 test_0... OK
All 1 tests passed.

Debugging §

std.debug.Trace §

This is a new API that you can use to efficiently save small stack traces for later, along with relevant data, so that when a problematic behavior is observed, you can dump the corresponding stack trace (#11819):

test.zig
const std = @import("std");

pub fn main() !void {
    var s1 = make();
    var s2 = make();

    if (std.crypto.random.uintLessThanBiased(u8, 100) < 100) {
        s1.trace.add("got lucky");
    }
    if (std.crypto.random.uintLessThanBiased(u8, 100) < 100) {
        s2.trace.add("got very lucky");
    }

    if (std.crypto.random.uintLessThanBiased(u8, 100) < 50) {
        s1.trace.dump();
    } else {
        s2.trace.dump();
    }
}

const S = struct {
    blah: i32,
    trace: std.debug.Trace = .{},
};

fn make() S {
    var result: S = .{
        .blah = 1234,
    };
    result.trace.add("initial creation");
    return result;
}
Shell
$ zig build-exe test.zig

$ ./test
initial creation:
docgen_tmp/test.zig:30:21: 0x210f86 in make (test)
    result.trace.add("initial creation");
                    ^
docgen_tmp/test.zig:5:18: 0x210e67 in main (test)
    var s2 = make();
                 ^
/home/andy/Downloads/zig/lib/std/start.zig:578:37: 0x2109f9 in posixCallMainAndExit (test)
            const result = root.main() catch |err| {
                                    ^
/home/andy/Downloads/zig/lib/std/start.zig:340:5: 0x210482 in _start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^
got very lucky:
docgen_tmp/test.zig:11:21: 0x210eee in main (test)
        s2.trace.add("got very lucky");
                    ^
/home/andy/Downloads/zig/lib/std/start.zig:578:37: 0x2109f9 in posixCallMainAndExit (test)
            const result = root.main() catch |err| {
                                    ^
/home/andy/Downloads/zig/lib/std/start.zig:340:5: 0x210482 in _start (test)
    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
    ^

This provides insight about the state of the program at a cross-section according to specific objects, which can lead directly to identifying bugs that otherwise would be completely opaque.

By default the Trace objects are empty in release modes, so the cost is only paid when debugging. Even when debugging, however, the size of the Trace objects are small - only one usize per stack frame. It defaults to 2 events, 4 stack frames, but can be configured with std.debug.ConfigurableTrace.

New Deflate Compressor §

Hadrien Dorio provided a new deflate implementation (#10552).

Old API:

inflateStream(reader: anytype, window_slice: []u8)

New API:

decompressor(allocator: mem.Allocator, reader: anytype, dictionary: ?[]const u8)
compressor(allocator: mem.Allocator, writer: anytype, options: CompressorOptions)

The implementation is a port of the compress/flate package from Go, and appears to be about 2-3x faster.

Shell
$ zig run bench.zig -O ReleaseFast

size: 100B
deflate_old: 20.459ms
deflate_new: 38.101us
new is 536.98x faster

size: 1KiB
deflate_old: 30.71us
deflate_new: 15.159us
new is 2.03x faster

size: 1MiB
deflate_old: 2.824ms
deflate_new: 916.366us
new is 3.08x faster

size: 10MiB
deflate_old: 28.298ms
deflate_new: 8.246ms
new is 3.43x faster

size: 22MiB
deflate_old: 62.643ms
deflate_new: 24.133ms
new is 2.60x faster

size: 100MiB
deflate_old: 288.08ms
deflate_new: 107.465ms
new is 2.68x faster

size: 220MiB
deflate_old: 628.471ms
deflate_new: 232.427ms
new is 2.70x faster

Benchmark details

Big thanks to Ryan Liptak for fuzz testing.

There is still a lot of room for performance improvement beyond this.

Formatted Printing §

Bug Fixes §

Full list of the 452 bug reports closed during this release cycle.

Many bugs were both introduced and resolved within this release cycle.

This Release Contains Bugs §

Zero the Ziguana

Zig has known bugs and even some miscompilations.

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

When Zig reaches 1.0.0, Tier 1 Support will gain a bug policy as an additional requirement.

A 0.10.1 release is planned. Waiting until 0.10.1 to upgrade is recommended for users who desire a more stable experience.

Build System §

Toolchain §

LLVM 15 §

This release of Zig upgrades to LLVM 15.0.3.

Note that the Zig 0.9.x series used LLVM 13 - there is no Zig release that depends on LLVM 14.

musl 1.2.3 §

Zig ships with the source code to musl. When the musl C ABI is selected, Zig builds static musl from source for the selected target. Zig also supports targeting dynamically linked musl which is useful for Linux distributions that use it as their system libc, such as Alpine Linux.

This release upgrades from v1.2.2 to v1.2.3.

glibc 2.34 §

Zero the Ziguana

The bad news is that although glibc 2.35 and 2.36 have already been released, Zig is behind in adding cross-compilation support for them due to exploring the Universal Headers project. The good news, however, is that Zig can now integrate with system glibc that is newer than what Zig can provide (#12797).

Additionally:

mingw-w64 10.0.0 §

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 v10.0.0.

Zig now passes -D__USE_MINGW_ANSI_STDIO=0 for crt files. This was supposed to be happening all along, and it was a bug that Zig did not do this before (#7356). Thanks to Martin Storsjö for helping us discover this on IRC. This fix means that Zig no longer carries any patches on top of upstream mingw-w64 sources!

Speaking of Martin, he has just a couple days ago signed up for GitHub Sponsors. He does brilliant work on mingw-w64, and he has patiently helped us when we go into the IRC channel asking for troubleshooting tips from Zig's unusual way of building mingw-w64 from source. Most people would (understandably) say, "sorry, that's unsupported." We are proud to be Martin's first sponsor and hope you will join us in showing support!

zig cc §

zig cc is Zig's drop-in C compiler tool. Enhancements in this release:

Please be aware that there are still open zig cc issues. However, these are use cases that never worked in the first place, making them bugs and enhancements rather than regressions.

zig c++ §

zig c++ is equivalent to zig cc with an added -lc++ parameter, but I made a separate heading here because I realized that some people are not aware that Zig supports compiling C++ code and providing libc++ too!

hello.cpp
#include <iostream>
int main() {
    std::cout << "Hello World!" >> std::endl;
    return 0;
}
Shell
$ zig c++ -o hello hello.cpp
$ ./hello
Hello World!

Cross-compiling too, of course:

Shell
$ zig c++ -o hello hello.cpp -target riscv64-linux
$ qemu-riscv64 ./hello
Hello World!

One thing that trips people up when they use this feature is that the C++ ABI is not stable across compilers, so always remember the rule: You must use the same C++ compiler to compile all your objects and static libraries. This is an unfortunate limitation of C++ which Zig can never fix.

Improvements to libc++ in this release:

zig fmt §

compiler-rt §

compiler-rt is the library that provides, for example, 64-bit integer multiplication for 32-bit architectures which do not have a machine code instruction for it. In the GNU world, it is 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 (#1290).

Roadmap §

Ziggy the Ziguana

Now that the Self-Hosted Compiler is launched, we can move on with the roadmap!

The major themes of the 0.11.0 release cycle will be language changes, compilation speed, and package management.

Some upcoming milestones we will be working towards in the 0.11.0 release cycle:

Here are the steps for Zig to reach 1.0:

  1. Stabilize the language. No more Language Changes after this.
  2. Complete the language specification first draft.
  3. Implement the official package manager.
  4. Stabilize the Standard Library. That means to add any missing functionality, audit the existing functionality, curate it, re-organize everything, and fix all the bugs.
  5. Go one full release cycle without any breaking changes.
  6. Finally we can tag 1.0.

Package Manager §

Having a package manager built into the Zig compiler is a long-anticipated feature. Zig 0.10.0 does not have this feature. However, all the prerequisites are finally done, and work begins now!

We will do our best to keep in mind the needs of system package maintainers while working on this language package manager.

Accepted Proposals §

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

Thank You Contributors! §

Here are all the people who landed at least one contribution into this release:

Thank You Sponsors! §

Ziggy the Ziguana

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 $50/month or more: