zig

0.4.0 Release Notes

Download & Documentation

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

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

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

Table of Contents §

LLVM 8 §

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

Notably this means that Zig now has WebAssembly Support, as well as recognizing the HermitCore and Hurd operating systems. On the other hand, support for the Nios II architecture is dropped, due to it being an experimental target which was largely unmaintained.

FreeBSD Support §

Marc Tiehuis started an initial FreeBSD branch. Greg V picked up the figurative baton in #1661, and Marcio Giaxa completed the effort.

Now, FreeBSD works in the stage1 C++ compiler code, as well as the Zig Standard Library, which means it now has Tier 2 Support. To achieve Tier 1 Support, these things are needed:

Despite these issues, thanks to SourceHut, FreeBSD does have some CI testing support, as well as automated binary builds available for x86_64 on the download page.

See #1759 for more details.

NetBSD Support §

Maya Rashish ported the stage1 C compiler as well as the Zig Standard Library to be compatible with NetBSD. Have a look at their writeup: Porting Zig to NetBSD - a fun, speedy port

NetBSD is limited to Tier 2 Support due to lack of a continuous integration service, as well as a missing implementation of building the system's libc startup files lazily from source.

WebAssembly Support §

Now that Zig uses LLVM 8, the wasm32 target architecture is guaranteed to be available in Zig.

Ben Noordhuis fixed zig build-exe for the wasm32 target (#1570).

Shritesh Bhattarai added --allow-undefined and --export-all to the wasm linker line, solving #1622. There may be a better way to catch link errors in the future, but for now, this makes WebAssembly work out-of-the-box in Zig:

extern fn print(i32) void;

export fn add(a: i32, b: i32) void {
    print(a + b);
}
$ zig build-exe math.zig -target wasm32-freestanding

test.js

const fs = require('fs');
const source = fs.readFileSync("./math");
const typedArray = new Uint8Array(source);

WebAssembly.instantiate(typedArray, {
  env: {
    print: (result) => { console.log(`The result is ${result}`); }
  }
}).then(result => {
  const add = result.instance.exports.add;
  add(1, 2);
});
$ node test.js
The result is 3

There is also the "WebAssembly System Interface" operating system target, wasi. I haven't really explored this use case yet, but I did make the C integer types work for it. More research and exploration is needed.

64-bit ARM Linux Support §

Shawn Landden implemented arm64 Linux support in the Zig Standard Library, as well as the necessary stage1 C++ compiler changes.

The arm64-linux target has now graduated to Tier 2 Support. To achieve Tier 1 Support, we would need some way to run Continuous Integration tests on arm64 linux.

SourceHut has experimental arm64 Debian images, which may be worth looking into.

UEFI Support §

GitHub user nebulaeonline approached the Zig project with an interesting use case: creating a UEFI application.

nebulaeonline submitted a pull request adding UEFI as a new operating system that Zig recognizes. The patch contained the linker configuration and a few updates to the Zig Standard Library.

With this patch merged, nebulaeonline went on to work on their UEFI barebones project. It looks like they ended up using C rather than Zig in the end, most likely due to Zig's lack of maturity. Here I have linked to the last revision before they removed the Zig code, in case anyone wants to poke around. Since then Zig has gained C Pointers which was a large pain point for nebulaeonline.

Tier System §

Zig now uses a "support tier" system to communicate the level of support for different targets.

Explanation of Tiers §

Tier 1 Support §

Tier 2 Support §

Tier 3 Support §

Tier 4 Support §

Support Table §

freestanding linux macosx windows freebsd netbsd UEFI other
x86_64 Tier 2 Tier 1 Tier 1 Tier 1 Tier 2 Tier 2 Tier 2 Tier 3
i386 Tier 2 Tier 2 Tier 4 Tier 2 Tier 3 Tier 3 Tier 3 Tier 3
arm Tier 2 Tier 3 Tier 3 Tier 3 Tier 3 Tier 3 Tier 3 Tier 3
arm64 Tier 2 Tier 2 Tier 3 Tier 3 Tier 3 Tier 3 Tier 3 Tier 3
bpf Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A Tier 3
hexagon Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A Tier 3
mips Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A Tier 3
powerpc Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A Tier 3
amdgcn Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A Tier 3
sparc Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A Tier 3
s390x Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A Tier 3
lanai Tier 3 Tier 3 N/A N/A Tier 3 Tier 3 N/A Tier 3
wasm32 Tier 3 N/A N/A N/A N/A N/A N/A N/A
wasm64 Tier 3 N/A N/A N/A N/A N/A N/A N/A
avr Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
riscv32 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 Tier 4 Tier 4
riscv64 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 Tier 4 Tier 4
xcore Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
nvptx Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
msp430 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
r600 Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
arc Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
tce Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
le Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
amdil Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
hsail Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
spir Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
kalimba Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
shave Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4
renderscript Tier 4 Tier 4 N/A N/A Tier 4 Tier 4 N/A Tier 4

Zig is also a C Compiler §

One of the main features of Zig is @cImport and C Translation. This is accomplished by linking against Clang libraries. Clang is a fully featured C and C++ compiler. Zig has all this functionality - so we may as well expose it!

The zig cc command exposes clang, which you can see with this amusing output:

$ zig cc --version
clang version 8.0.0
...

zig cc is a bit of a low level command. The higher level command line interface --c-source (or the corresponding Zig Build System API) is recommended instead, for a few reasons:

Here's an example of Zig building some C code:

hello.c

#include <stdio.h>

int main(int argc, char **argv) {
    printf("Hello world\n");
    return 0;
}
$ zig build-exe --c-source hello.c --library c
$ ./hello
Hello world

You can use --verbose-cc to see what C compiler command this executed:

$ zig build-exe --c-source hello.c --library c --verbose-cc
zig cc -MD -MV -MF zig-cache/tmp/42zL6fBH8fSo-hello.o.d -nostdinc -fno-spell-checking -isystem /home/andy/dev/zig/build/lib/zig/include -isystem /home/andy/dev/zig/build/lib/zig/libc/include/x86_64-linux-gnu -isystem /home/andy/dev/zig/build/lib/zig/libc/include/generic-glibc -isystem /home/andy/dev/zig/build/lib/zig/libc/include/x86_64-linux-any -isystem /home/andy/dev/zig/build/lib/zig/libc/include/any-linux-any -march=native -g -fstack-protector-strong --param ssp-buffer-size=4 -fno-omit-frame-pointer -o zig-cache/tmp/42zL6fBH8fSo-hello.o -c hello.c -fPIC

Note that if I run the command again, there is no output, and it finishes instantly:

$ time zig build-exe --c-source hello.c --library c --verbose-cc

real	0m0.027s
user	0m0.018s
sys	0m0.009s

This is thanks to Build Artifact Caching. Zig automatically parses the .d file that clang produces and uses a robust caching system to avoid duplicating work.

In the Zig Build System, builder.addCExecutable used to be a system C compiler driver. That functionality is deleted. Instead, build scripts can attach C source files to any executable, library, or object with foo.addCSourceFile. You can see a nice example of this in Michael Dusan's benchmark.unicode project.

Don't forget to use foo.linkSystemLibrary("c"); if the C code expects to use libc.

You may have noticed that the Zig-generated C compilation command included -nostdinc.

This is a crucial step in the direction Zig is headed - providing consistent, reliable builds that are insulated from the system-specific differences in the wild.

This affects @cImport and C Translation as well. By default, Zig no longer looks in system paths for C header files, instead relying only on what Zig ships and what users explicitly request with -isystem.

However, when one uses the Zig Build System, and uses linkSystemLibrary API, Zig takes that as a hint to look in system default search paths. Marc Tiehuis proposed to expose this functionality to the command line interface, and that is likely to be an accepted proposal.

Thanks to Akuli for adding /lib/x86_64-linux-gnu or similar to default system library search paths for zig build scripts.

Zig ships with libc §

One piece to this puzzle is that Zig now ships with libc. You can find the available libc targets with zig targets:

...
Available libcs:
  aarch64_be-linux-gnu
  aarch64_be-linux-musl
  aarch64-linux-gnu
  aarch64-linux-musleabi
  armeb-linux-gnueabi
  armeb-linux-gnueabihf
  armeb-linux-musleabi
  armeb-linux-musleabihf
  arm-linux-gnueabi
  arm-linux-gnueabihf
  arm-linux-musleabi
  arm-linux-musleabihf
  i386-linux-gnu
  i386-linux-musl
  mips64el-linux-gnuabi64
  mips64el-linux-gnuabin32
  mips64el-linux-musl
  mips64-linux-gnuabi64
  mips64-linux-gnuabin32
  mips64-linux-musl
  mipsel-linux-gnu
  mipsel-linux-musl
  mips-linux-gnu
  mips-linux-musl
  powerpc64le-linux-gnu
  powerpc64le-linux-musl
  powerpc64-linux-gnu
  powerpc64-linux-musl
  powerpc-linux-gnu
  powerpc-linux-musl
  riscv32-linux-musl
  riscv64-linux-gnu
  riscv64-linux-musl
  s390x-linux-gnu
  s390x-linux-musl
  sparc-linux-gnu
  sparcv9-linux-gnu
  x86_64-linux-gnu
  x86_64-linux-gnux32
  x86_64-linux-musl

What this means is that --library c for these targets does not depend on any system files!

Let's look at that C hello world example again:

$ zig build-exe --c-source hello.c --library c
$ ./hello
Hello world
$ ldd ./hello
	linux-vdso.so.1 (0x00007ffd03dc9000)
	libc.so.6 => /lib/libc.so.6 (0x00007fc4b62be000)
	libm.so.6 => /lib/libm.so.6 (0x00007fc4b5f29000)
	libpthread.so.0 => /lib/libpthread.so.0 (0x00007fc4b5d0a000)
	libdl.so.2 => /lib/libdl.so.2 (0x00007fc4b5b06000)
	librt.so.1 => /lib/librt.so.1 (0x00007fc4b58fe000)
	/lib/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fc4b6672000)

glibc does not support building statically, but musl does:

$ zig build-exe --c-source hello.c --library c -target x86_64-linux-musl
$ ./hello
Hello world
$ ldd hello
  not a dynamic executable

In this example, Zig built musl libc from source and then linked against it. The build of musl libc for x86_64-linux remains available thanks to the caching system, so any time this libc is needed again it will be available instantly.

This means that this functionality is available on any platform. Windows and macOS users can build Zig and C code, and link against libc, for any of the targets listed above. Similarly code can be cross compiled for other architectures:

$ zig build-exe --c-source hello.c --library c -target aarch64v8-linux-gnu
$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped

In some ways, Zig is a better C compiler than C compilers!

This functionality is more than bundling a cross-compilation toolchain along with Zig. For example, the total size of libc headers that Zig ships is 22MiB uncompressed. Meanwhile, the headers for musl libc + linux headers on x86_64 alone are 8MiB, and for glibc are 3.1MiB (glibc is missing the linux headers), yet Zig currently ships with 40 libcs. With a naive bundling that would be 444MiB. However, thanks to this process_headers tool that I made, and some good old manual labor, Zig binary tarballs remain roughly 30MiB total, despite supporting libc for all these targets, as well as compiler-rt, libunwind, and libcxx, and despite being a clang-compatible C compiler. For comparison, the Windows binary build of clang 8.0.0 itself from llvm.org is 132MiB.

Note that only the Tier 1 Support targets have been thoroughly tested. It is planned to add more libcs (including for Windows), and to add test coverage for building against all the libcs. Support for building the FreeBSD libc startup files is the main feature needed to achieve Tier 1 Support for the target.

Thanks to Jay Weisskopf for helping with the glibc targets by removing an obsolete compat shim file.

Libc Detection §

Although Zig ships with libc for some targets, it still supports building against the system-native libc.

0.4.0 introduces a new command zig libc which prints the various paths of libc files. It outputs them to stdout in a simple text file format that it is capable of parsing. You can use zig libc libc.txt to validate a file.

Here's what it looks like on my NixOS laptop:

$ zig libc
# The directory that contains `stdlib.h`.
# On POSIX-like systems, include directories be found with: `cc -E -Wp,-v -xc /dev/null`
include_dir=/nix/store/q2q1sg5sljia8sihhwcpbxir70yw33bw-glibc-2.27-dev/include
# The system-specific include directory. May be the same as `include_dir`.
# On Windows it's the directory that includes `vcruntime.h`.
# On POSIX it's the directory that includes `sys/errno.h`.
sys_include_dir=/nix/store/q2q1sg5sljia8sihhwcpbxir70yw33bw-glibc-2.27-dev/include

# The directory that contains `crt1.o`.
# On POSIX, can be found with `cc -print-file-name=crt1.o`.
# Not needed when targeting MacOS.
crt_dir=/nix/store/fivq0nbggp4y8mhy3ixprqd7qyn1hy2j-glibc-2.27/lib

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

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

And here's what it looks like on my Windows laptop:

> zig.exe libc
# The directory that contains `stdlib.h`.
# On POSIX-like systems, include directories be found with: `cc -E -Wp,-v -xc /dev/null`
include_dir=C:\Program Files (x86)\Windows Kits\10\\Include\10.0.17134.0\ucrt
# The system-specific include directory. May be the same as `include_dir`.
# On Windows it's the directory that includes `vcruntime.h`.
# On POSIX it's the directory that includes `sys/errno.h`.
sys_include_dir=C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.15.26726\lib\x64\\..\..\include

# The directory that contains `crt1.o`.
# On POSIX, can be found with `cc -print-file-name=crt1.o`.
# Not needed when targeting MacOS.
crt_dir=C:\Program Files (x86)\Windows Kits\10\\Lib\10.0.17134.0\ucrt\x64\

# The directory that contains `vcruntime.lib`.
# Only needed when targeting MSVC on Windows.
msvc_lib_dir=C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.15.26726\lib\x64\

# The directory that contains `kernel32.lib`.
# Only needed when targeting MSVC on Windows.
kernel32_lib_dir=C:\Program Files (x86)\Windows Kits\10\\Lib\10.0.17134.0\um\x64\

These arguments are gone:

--libc-lib-dir [path]        directory where libc crt1.o resides
--libc-static-lib-dir [path] directory where libc crtbegin.o resides
--msvc-lib-dir [path]        (windows) directory where vcruntime.lib resides
--kernel32-lib-dir [path]    (windows) directory where kernel32.lib resides

Instead we have this argument:

--libc [file]                Provide a file which specifies libc paths

This is used to pass a libc text file (which can be generated with zig libc). So it is easier to manage multiple cross compilation environments.

When Zig must depend on the native libc installation, it first looks for zig-cache/native_libc.txt. If found, this file will be used. If this file does not exist or has a parse error, Zig will perform native libc detection and then overwrite this file. This can save time, because native libc detection can be time consuming. For example it may need to invoke the system C compiler several times to find out paths of things, or it may invoke some Windows COM API which is a bit non-deterministic in terms of performance.

Static by Default §

The --static CLI option is removed and the -dynamic option is added.

Instead of an explicit --static flag, Zig makes things as static as possible by default. Even zig build-lib will now create a static library by default, but -dynamic can be used to choose a dynamic library.

When targeting Linux, Zig now automatically links statically when not linking against any shared libraries. It also disables Position Independent Code in this case, which improves performance, especially with regards to Thread-Local Variables. --enable-pic and --disable-pic can be used to override the default, but this should be a rare use case.

Position Independent Code is also disabled by default for the freestanding OS target. When targeting Windows, Position Independent Code is always enabled.

It is proposed to rename these options to -fPIC and -fno-PIC to match C compilers.

Single-Threaded Builds §

For some use cases, it is known at compile-time that a given application or executable will never participate in multi-threading. For these use cases Zig has Single-Threaded Builds.

There is now a compile option --single-threaded which has the following effects:

The feature is exposed in the Zig Build System with foo.single_threaded = true;

Zig Build System §

When one invokes the zig build command, this executes a user-defined build script. This is called the zig build system. It received a few improvements during this release cycle.

Notably, Build Artifact Caching is enabled by default. This means that by default build artifacts are now output into the zig-cache directory. The output path is not predictable and if the build script desires to run an executable, rather than hard coding a path and using builder.addCommand, build scripts must use executable.run API, or addArtifactArg. An example of this can be found in the tetris example.

builder.addSystemCommand is available to run commands that are not created by the build system itself. An example of this can be found in the Raspberry Pi OS example.

Another option build scripts have is to specify the output directory of an artifact with artifact.setOutputDir. This disables caching and makes artifact.getOutputPath, artifact.getOutputLibPath, and artifact.getOutputHPath available.

Finally, the best way to make the output path of an artifact predictable is to install it with the build system's install step. There are some ergonomics and defaults to improve with this strategy, which will be tackled during the next release cycle.

Please note that Zig does no cache evicting yet. You may have to manually delete zig-cache directories periodically to keep disk usage down. It's planned for this to be a simple Least Recently Used eviction system eventually.

--output, --output-lib, and --output-h are removed. Instead, use --output-dir which defaults to the current working directory. Or take advantage of --cache on, which will print the main output path to stdout, and the other artifacts will be in the same directory with predictable file names. --disable-gen-h is available when one wants to prevent .h file generation.

@cImport is always independently cached now. It always writes the generated Zig code to disk which makes debug info and compile errors better. No more "TODO: remember C source location to display here" #2015. The --verbose-cimport command no longer dumps Zig AST to stderr; instead it prints the file paths of the generated .zig files for inspection. In addition to this improved behavior, the common case of @cImport with all the C header files unchanged is now fast due to being cached, yet still correct if any of the C files change.

In addition, these improvements were made:

Build Artifact Caching §

Zig comes with a robust caching system. When compiling Zig code, Zig knows all the files that it depends on, and when compiling C code, Zig uses -MV -MD -MF parameters to Clang and then parses the generated .d file.

The other component of this is the "compiler id" concept:

$ zig id
Cxb7A-sAjt6VDq35pHZj3ACcwiSFqcCndt0NJkdQQlZVKolwm-QKnce1KEMUTqPr

This is the hash of the compiler binary itself, plus all the dynamic libraries it depends on, directly or indirectly. That means if you fix a bug in memcpy in the system libc, Zig will detect this change and the value of zig id will be different. However, the value is quickly computed because it also participates in the caching system.

Here is a "manifest file" in the caching system for zig id:

4850313 1554740311 416647968 cOOP0xpsfiYfuMJCS3V_fLKdNmxFxLih_UACAUqwynGp1Y616NJ2lHL0hOpWOp_o /home/andy/dev/zig/build/zig
3077799 1 0 bBgfkt9ZSVN_HgBbzwHhzKmQxpssdDYq_HcbEkdu7KDcUhanATUER2_5l_U__9DE /nix/store/5dphwv1xs46n0qbhynny2lbhmx4xh1fc-zlib-1.2.11/lib/libz.so.1
1165671 1 0 jSVrSQyNkPFoz0Z6GesezdVP1ZBRpHL2UbbX7HbNEprcU4aBuIyG9z6bAOvvPZr0 /nix/store/fivq0nbggp4y8mhy3ixprqd7qyn1hy2j-glibc-2.27/lib/librt.so.1
1165626 1 0 scJm8rYLhDLp_SDXmbvdlcj9fEWQadEm_kHUBJn2oGnht3pSh2MNzsUJZjR1wwa1 /nix/store/fivq0nbggp4y8mhy3ixprqd7qyn1hy2j-glibc-2.27/lib/libdl.so.2
1165664 1 0 pGDNQn5x_j1NAWzjh4AOUP8OKyJj1KSPd4SJYzRVbTTp7TRb4IQpDs7ZAU3muqfz /nix/store/fivq0nbggp4y8mhy3ixprqd7qyn1hy2j-glibc-2.27/lib/libpthread.so.0
1165631 1 0 4tmxoKlbVwB42gYxT7B3CKIBXpbGdb_vuqfYiXlOZdyngmFJdQ7DBZxWqZKSO0Ss /nix/store/fivq0nbggp4y8mhy3ixprqd7qyn1hy2j-glibc-2.27/lib/libm.so.6
3077645 1 0 dxe6qw_yMCi8Bp7ocrVcl2XMdRlKfa0S3ATqbxsEMISRQdQ1dyxTEE1uDbv0R54z /nix/store/sf0wnp30savqz9ljn6fsrn8f63w5v0za-gcc-7.4.0-lib/lib/libstdc++.so.6
1165630 1 0 5boikua2jQubswAtj8MICJzjmGNUxTZrNreQm_DmtspgRsXULyCkyUUMYcNFamII /nix/store/fivq0nbggp4y8mhy3ixprqd7qyn1hy2j-glibc-2.27/lib/libgcc_s.so.1
1165616 1 0 qiXB510da3ryfL7LcPUvL2EQgY2tIwszLRvgQdDrQtnWVl4zA1Nfpq4SDpIz3IYW /nix/store/fivq0nbggp4y8mhy3ixprqd7qyn1hy2j-glibc-2.27/lib/libc.so.6
1165607 1 0 MpHwajxu3h0TtsHViKMAmHHeGMbi9IuDLlybX6PpqT66Xfa1h2GAT3XFIX19dEz- /nix/store/fivq0nbggp4y8mhy3ixprqd7qyn1hy2j-glibc-2.27/lib/ld-linux-x86-64.so.2

The fields are, respectively:

  1. inode
  2. mtime seconds
  3. mtime nanoseconds
  4. blake hash of file contents
  5. path to file

This same system is used for build artifact caching. For example, here is the manifest file for hello world in Zig (when built with --cache on):

5505089 1548876783 204467480 rcnRrxBgZSiWki_XN9XKlQ2yfWkM6KLYhUWprzniEBtjgmeUSmtlv5mAguA4l2Q1 /home/andy/dev/zig/example/hello_world/hello.zig
4850617 1554305190 0 GXP-IfocPj934vVJYrccBKIVoOcWULaUtkBoR-iHnEKEIoXtdchyuQi8tk8FTncS /home/andy/dev/zig/build/lib/zig/std/std.zig
5114024 1551557778 0 6u-vFMcHH_FcfkAyqCcoYXAfF2TLMCkoP86Q3Ykecmage7_E4ObRsfP52-YXDHH6 /home/andy/dev/zig/build/lib/zig/std/special/panic.zig
4850617 1554305190 0 GXP-IfocPj934vVJYrccBKIVoOcWULaUtkBoR-iHnEKEIoXtdchyuQi8tk8FTncS /home/andy/dev/zig/build/lib/zig/std/std.zig
4850277 1553179780 0 9S5aQg0ORbkQnvDCVeW2aNPSeCu993I4iO3JoGrif4py7zBS4si4mCaeHHuo9TEn /home/andy/dev/zig/build/lib/zig/std/debug.zig
4849956 1554500038 0 8c76uDlKxK5jZ1L0Uup4Lu0y1NlU5-KrUCKFyZnzoi7Hv3dDByN8pvujTjxjaf0m /home/andy/dev/zig/build/lib/zig/std/os.zig
4850678 1554330534 0 wFvlet_2y9YT6NlFrsqybubwMafHFCIZqq4AxLlcUGuTSZ7VjI8kGmM-3VKPrhkt /home/andy/dev/zig/build/lib/zig/std/io.zig
5112957 1551560716 0 f29y95JhSfGVwEFrutPnquGPPuvvtOLu6_vUUgGbaqn-9uulgFvBnyPU8Zp_sH7r /home/andy/dev/zig/build/lib/zig/std/os/file.zig
5113386 1552707664 0 Mb9m-jj_wRe6yt-TFko9wUGNToAA-IAbVfmR2TYZyPjKP3D87df6BGALjai5uKr2 /home/andy/dev/zig/build/lib/zig/std/os/windows/util.zig
5113248 1551562209 0 F1infSSAdOgbOWl3y2FdUiB-6xrpTaeqUJ5xt_F9IZOeBvvTSLNpkGEg8mrYMcli /home/andy/dev/zig/build/lib/zig/std/os/linux.zig
5113251 1536869149 0 LAlDVvl1TX-3-SB3MOBBjxuEk2Ms8SpkcYBLXnJONgZuo7aV9OYfXLfih9bfGyBS /home/andy/dev/zig/build/lib/zig/std/os/linux/errno.zig
5113278 1551560715 0 TpIIt3DNczfgMKjhDA5mx-4JgoGd7SIV9lXK8GQs9Bix4EmPiL7VlV1oQVwv4gkO /home/andy/dev/zig/build/lib/zig/std/os/linux/x86_64.zig
4849832 1554482295 0 AszHJ9beyMCotgQx4vwYWMBUcRd1dBspsou64fATNYXsSqwB5zAiqVEokfpV5xkF /home/andy/dev/zig/build/lib/zig/std/fmt.zig
4850680 1551559869 0 Polq06O1t-jJ0SobBVqLaQrJVXaKzH4lQj6Na8NRN3Y5N0pZ2I4W2rpwL816MSz- /home/andy/dev/zig/build/lib/zig/std/io/seekable_stream.zig
4850605 1552687041 0 Al_VWoytLHeozYOvM69cCR2Dw1HGYdJc2lDGQDXYoiJy1VuD6GWLIQhxSJuf3Oza /home/andy/dev/zig/build/lib/zig/std/array_list.zig
4877695 1552687041 0 ouDIQ9yX5LXanXk6mc2ypFP-oy2zEt7uLLlPDVUFEfx4AKmeFP1cJWI7YSjYAVt9 /home/andy/dev/zig/build/lib/zig/std/mem.zig
4850649 1551560732 0 rVa_7s1plylcGIUAHEt3eGG8XwSJAEOQzuIaYcnq0KHnb1IoGWUEXUGIW7QHH7fi /home/andy/dev/zig/build/lib/zig/std/elf.zig
4849844 1554305190 0 58Tg88pYb1sgK8SI5_65Gy7lgCqaJmsW2NV-Hi4FeXOK8e2f2T1PofNfdUDgbYit /home/andy/dev/zig/build/lib/zig/std/math.zig
4850647 1536869149 0 9-R2p6FQ_7UIjQMt3N-chgeh-_e_o29Gr2Pybfh2PurmRYT9mIIwsdW9rs6kEJR8 /home/andy/dev/zig/build/lib/zig/std/dwarf.zig
5113295 1552687041 0 6oc1oIiWQJ1OIZGh5KFPtul93GCeAaLFx2VTICI3JMt-Ubnj-vjXvBBclnoo5UQ6 /home/andy/dev/zig/build/lib/zig/std/os/path.zig
4850677 1552707664 0 qom60op_QDptmhKO6SIlpm48SlVybcFOT0gHCjncJvtZhM6uUNqZZ2Q0WV5AsmRP /home/andy/dev/zig/build/lib/zig/std/heap.zig
4850683 1551560704 0 6t3PH8KaWwsz2MElBWZezzyZyeRS-Ttq6k2pJWwRK__NvVQ2hZSu1gk4jA7oDT_Y /home/andy/dev/zig/build/lib/zig/std/linked_list.zig
4850685 1548876783 0 EsROc7EV9MKNd74wRR9tYFhMGs4CL4eqYSPvJVzKTzSoXABBfxVoQwO3N_VivhNG /home/andy/dev/zig/build/lib/zig/std/macho.zig
4850663 1554667129 0 oWDxArJXQLKaeAqd-Uf52hiAjktfqEhJA4XIdlBKCeKOxbYZosChXsfReGM0azCQ /home/andy/dev/zig/build/lib/zig/std/hash_map.zig
4877700 1551559918 0 0SY3kFjpRYGQ1eVmh4UHluGBDFA9LcwANsSlEZLKfBO6hWxoZWcowtXk1SJvSALu /home/andy/dev/zig/build/lib/zig/std/pdb.zig
4850628 1551559906 0 QEww1cNolzNrXqhOMV8uMTXRXQmbekZ7nyUaEqj35x6ZjO8BRVzqvYxQeSmW8c_5 /home/andy/dev/zig/build/lib/zig/std/coff.zig
4877702 1551562259 0 9cFnTGm01DAKqMZim8dzEXoPyN8w3R8UAer53ycBv2PFgg-pM6AsNd6hDf0jlseT /home/andy/dev/zig/build/lib/zig/std/rand.zig
5113402 1554353707 0 m2d1Awh2J4qJuWpBI5sEZJyszBZB2VIZlHvVpXZlt9Vz9JdjXvMN2Cig4ohevME_ /home/andy/dev/zig/build/lib/zig/std/special/bootstrap.zig
4877697 1551560726 0 y2XL97U7jZ9PY4d5TVVZmBzYQc5wrCE_j_utDeU8zfLp5W6iLdFEf_HLbG4-W9Mf /home/andy/dev/zig/build/lib/zig/std/mutex.zig
4877707 1551560708 0 jd03IjTqKqI-sTTbTTL63EjXX2xp9vPInAn4fA0rRis3TeXGgWnP-44_8N0OTRPP /home/andy/dev/zig/build/lib/zig/std/spinlock.zig

The way the cache checking works, is that it calls fstat on all the files in the manifest. For each file, if the inodes and mtimes from the manifest match the results from fstat, then the hash is trusted to be up-to-date for that particular file. Any files with outdated hashes, have their contents hashed, and the manifest file updated with the updated hashes. Finally, all the hashes are hashed together, and that final hash is used as the directory name within zig-cache that Zig looks for previously created build artifacts.

Because Zig uses -MD -MF args to clang when doing @cImport, Zig has a complete list of files that the C code read from, which Zig adds to the cache. So even when using @cImport, Zig's caching system remains perfect. This is a proof of concept for the mechanism that the self-hosted compiler will use to watch and rebuild files.

There is one really fascinating problem to solve when relying on file modification times to determine whether a file's contents have changed. Some file systems, such as macOS's HFS+, do not support nanosecond precision; only seconds. However even nanosecond precision exhibits the problem for very fast systems.

Imagine that a file was modified, and then immediately after, added to the cache. When added to the manifest, the current time and the file's mtime will be the same. If the file is modified again quickly, and then the cache checked, it will still be the same instant in time; the modification to the file did not change the mtime.

There is a beautiful solution to this problem, which is to disqualify a file from being cached if the current time, truncated to the file system time granularity, matches the mtime:

// If the wall clock time, rounded to the same precision as the
// mtime, is equal to the mtime, then we cannot rely on this mtime
// yet. We will instead save an mtime value that indicates the hash
// must be unconditionally computed.
static bool is_problematic_timestamp(const OsTimeStamp *fs_clock) {
    OsTimeStamp wall_clock = os_timestamp_calendar();
    // First make all the least significant zero bits in the fs_clock, also zero bits in the wall clock.
    if (fs_clock->nsec == 0) {
        wall_clock.nsec = 0;
        if (fs_clock->sec == 0) {
            wall_clock.sec = 0;
        } else {
            wall_clock.sec &= (-1ull) << ctzll(fs_clock->sec);
        }
    } else {
        wall_clock.nsec &= (-1ull) << ctzll(fs_clock->nsec);
    }
    return wall_clock.nsec == fs_clock->nsec && wall_clock.sec == fs_clock->sec;
}

What's beautiful about this is that it generalizes to any granularity. If a file system had a 1 day mtime granularity, then this would mean any files modified within 1 day would have to have their contents hashed, but files older than that could have their hashes trusted. If a file system always wrote 0 for the mtime, Zig would never trust it.

Thanks to apenwarr's insightful blog post about build system caching for inspiration.

The caching system is disabled by default for the command line interface, except for zig test. zig test does respect --output-dir however, which disables caching. The default caching behavior can be overridden with --cache [on|off]. The Zig Build System enables caching by default.

Caching is always enabled, using the global cache directory, when building libc and compiler-rt. This saves a considerable amount of accumulated time, since they must be available for every compilation.

Valgrind Compatibility §

Zig's compatibility with Valgrind has improved. Notably, Valgrind 3.14 fixes a bug where Zig programs' debug symbols would not be detected. Because of this, the --no-rosegment command line option is removed from Zig. It was only meant to be a temporary workaround.

Thanks to daurnimator for adding a valgrind module to the standard library. This allows Zig code to easily make Valgrind client requests.

In addition to userland API, Zig integrates more tightly with Valgrind. For example, one thing that Zig does in Debug builds is write 0xaa bytes to undefined variables:

undef.zig

const std = @import("std");

pub fn main() void {
    var array: [10]u8 = undefined;
    std.debug.warn("array: {x}\n", array);
}
$ zig build-exe undef.zig
$ ./undef
array: aaaaaaaaaaaaaaaaaaaa

Valgrind has detection for branching on undefined values, but if Zig was writing 0xaa values to uninitialized variables, Valgrind would not know that this is supposed to be an undefined canary. It would see the values as defined. However thanks to integrated Valgrind client requests with the Zig language, Valgrind knows what's going on:

undef.zig

const std = @import("std");

pub fn main() void {
    var x: i32 = 1234;
    std.debug.warn("x = {}\n", x);
    x = undefined;
    if (x > 10) {
        std.debug.warn("greater than 10\n");
    }
}
$ zig build-exe undef.zig
$ ./undef
x = 1234

When run in valgrind:

==16484== Conditional jump or move depends on uninitialised value(s)
==16484==    at 0x224B6C: main (undef.zig:7)
==16484==    by 0x22455A: posixCallMainAndExit (bootstrap.zig:86)
==16484==    by 0x224350: _start (bootstrap.zig:43)

This is a debugging tool available in Zig that is not available in C - setting things to undefined so that Valgrind can catch it if you accidentally use it.

Language integration with Valgrind is enabled by default in Debug mode, and disabled by default in other modes. The feature can be force enabled or force disabled with --enable-valgrind and --disable-valgrind, respectively. Also available is @import("builtin").valgrind_support which is a comptime bool available for code to find out whether the programmer wants the executable to have Valgrind integration. It's always disabled for compiler_rt and libc/builtin.

The cost of this feature is a few assembly instructions with each assignment to undefined. Only support for Linux, macOS, Solaris, and MinGW on x86_64 is currently implemented.

zig fmt §

zig fmt is a tool to format your Zig code to a canonical style. It's implemented in Zig and it's been available as a self-hosted compiler subcommand for a while, but the self-hosted compiler is not done yet and it's not what's available on the download page.

Zig 0.4.0 ships with zig fmt capabilities. Under the hood, it uses zig run and the Build Artifact Caching system, to lazily build zig fmt from source, and then run it. That means even the stage1 C++ Zig compiler is a sort of hybrid - part C++, and part Zig.

Several editors have zig fmt integration available, such as the vim plugin that I use. I believe the VSCode plugin does as well.

There is now has a --check flag, which does everything except modify file contents. That is, it lists non-conforming files on stdout, and then exits with an error if the list is non-empty. #1558 #1555

Other improvements during this release cycle:

There is also a discussion about whether or not to align struct fields, which has some interesting arguments on either side.

C Translation Status §

I have come up with a plan to migrate translate-c to userland:

  1. Create a C API wrapper for Clang's C++ API.
  2. Implement translate-c in Zig with a C API, using the C API wrapper for libclang.
  3. The build process for stage1 will link zig.exe without translate-c support, build the translate-c userland library, and then re-link zig.exe with translate-c support.

This will allow us to move thousands of lines of C++ to Zig, and to only have to maintain one implementation of C translation rather than two. This same strategy could be used for other pieces of the Zig project, such as the package manager.

I started this effort with a proof of concept, but there is still a long way to go before this is done.

In addition the following improvements were made:

Grammar Formalization and Specification §

Jimmi lead this effort. Starting with solving the return type ambiguity (#1628), he then moved on to create zig-spec, update the grammar section of the language reference, and update the stage1 parser to conform.

Thanks to his efforts, Zig syntax now has a formal grammar specification and three independent implementations, despite the fact that the syntax is not fully stablized.

Documentation §

The language reference manual. now has a light theme by default, but respects the user's light/dark preference via the prefers-color-scheme media query. Most browsers don't support this yet, so we just have to wait patiently for the future to arrive.

Instead of a side bar index, the index is inline with the rest of the content. This is simpler and more friendly to all user agents, and means we don't need the media query for mobile devices. It also makes back-references work, so now headers link to the table of contents and the table of contents links to headers.

Both of these things apply to this release notes document as well as the language reference manual.

There is still no HTML documentation available for the Zig Standard Library. See the Roadmap for more details.

The master branch language reference manual is now automatically updated on master branch commits that pass all tests on all platforms.

In addition to the above, the following changes were made to the documentation:

Language Changes §

Implicit Casting Comptime-Known Integers §

All numbers with comptime known values now implicitly cast to all number types. If the value does not fit, a compile error is emitted.

comptime_ints.zig

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

test "implicit cast large integer to smaller with comptime value" {
    const x: u64 = 255;
    const y: u8 = x;
    assert(y == 255);
}
$ zig test comptime_ints.zig
Test 1/1 implicit cast large integer to smaller with comptime value...OK
All tests passed.

#422 #1712

Vectors and SIMD §

Zig 0.4.0 ships with a new Vector type. This feature is brand new and not well documented yet. Here are some example tests:

vectors.zig

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

test "vector wrap operators" {
    const S = struct {
        fn doTheTest() void {
            var v: @Vector(4, i32) = [4]i32{ 2147483647, -2, 30, 40 };
            var x: @Vector(4, i32) = [4]i32{ 1, 2147483647, 3, 4 };
            expect(mem.eql(i32, ([4]i32)(v +% x), [4]i32{ -2147483648, 2147483645, 33, 44 }));
            expect(mem.eql(i32, ([4]i32)(v -% x), [4]i32{ 2147483646, 2147483647, 27, 36 }));
            expect(mem.eql(i32, ([4]i32)(v *% x), [4]i32{ 2147483647, 2, 90, 160 }));
            var z: @Vector(4, i32) = [4]i32{ 1, 2, 3, -2147483648 };
            expect(mem.eql(i32, ([4]i32)(-%z), [4]i32{ -1, -2, -3, -2147483648 }));
        }
    };
    S.doTheTest();
    comptime S.doTheTest();
}

test "vector int operators" {
    const S = struct {
        fn doTheTest() void {
            var v: @Vector(4, i32) = [4]i32{ 10, 20, 30, 40 };
            var x: @Vector(4, i32) = [4]i32{ 1, 2, 3, 4 };
            expect(mem.eql(i32, ([4]i32)(v + x), [4]i32{ 11, 22, 33, 44 }));
            expect(mem.eql(i32, ([4]i32)(v - x), [4]i32{ 9, 18, 27, 36 }));
            expect(mem.eql(i32, ([4]i32)(v * x), [4]i32{ 10, 40, 90, 160 }));
            expect(mem.eql(i32, ([4]i32)(-v), [4]i32{ -10, -20, -30, -40 }));
        }
    };
    S.doTheTest();
    comptime S.doTheTest();
}

test "vector float operators" {
    const S = struct {
        fn doTheTest() void {
            var v: @Vector(4, f32) = [4]f32{ 10, 20, 30, 40 };
            var x: @Vector(4, f32) = [4]f32{ 1, 2, 3, 4 };
            expect(mem.eql(f32, ([4]f32)(v + x), [4]f32{ 11, 22, 33, 44 }));
            expect(mem.eql(f32, ([4]f32)(v - x), [4]f32{ 9, 18, 27, 36 }));
            expect(mem.eql(f32, ([4]f32)(v * x), [4]f32{ 10, 40, 90, 160 }));
            expect(mem.eql(f32, ([4]f32)(-x), [4]f32{ -1, -2, -3, -4 }));
        }
    };
    S.doTheTest();
    comptime S.doTheTest();
}

test "vector bit operators" {
    const S = struct {
        fn doTheTest() void {
            var v: @Vector(4, u8) = [4]u8{ 0b10101010, 0b10101010, 0b10101010, 0b10101010 };
            var x: @Vector(4, u8) = [4]u8{ 0b11110000, 0b00001111, 0b10101010, 0b01010101 };
            expect(mem.eql(u8, ([4]u8)(v ^ x), [4]u8{ 0b01011010, 0b10100101, 0b00000000, 0b11111111 }));
            expect(mem.eql(u8, ([4]u8)(v | x), [4]u8{ 0b11111010, 0b10101111, 0b10101010, 0b11111111 }));
            expect(mem.eql(u8, ([4]u8)(v & x), [4]u8{ 0b10100000, 0b00001010, 0b10101010, 0b00000000 }));
        }
    };
    S.doTheTest();
    comptime S.doTheTest();
}
$ zig test vectors.zig
Test 1/4 vector wrap operators...OK
Test 2/4 vector int operators...OK
Test 3/4 vector float operators...OK
Test 4/4 vector bit operators...OK
All tests passed.

Note that Zig of course supports vectors at comptime and likewise all the safety features of arithmetic operators are available for vectors as well.

Vectors provide a way to do Single Instruction Multiple Data in a portable and efficient way. It's much easier for the optimizer to break vectors into scalars than it is to turn scalars into vectors. Even in debug builds which have safety checks, vector operations provide more data throughput than scalar operations.

Thanks to Jimmi Holst Christensen for many contributions to vector support.

To follow progress on vectors and SIMD in Zig, watch issue #903.

comptime memory reinterpretation §

Zig's comptime facilities are improved to support reinterpreting memory, as long as the types in question have guaranteed in-memory representation.

bitcast.zig

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

test "@bitCast extern structs at runtime and comptime" {
    const Full = extern struct {
        number: u16,
    };
    const TwoHalves = extern struct {
        half1: u8,
        half2: u8,
    };
    const S = struct {
        fn doTheTest() void {
            var full = Full{ .number = 0x1234 };
            var two_halves = @bitCast(TwoHalves, full);
            switch (builtin.endian) {
                builtin.Endian.Big => {
                    expect(two_halves.half1 == 0x12);
                    expect(two_halves.half2 == 0x34);
                },
                builtin.Endian.Little => {
                    expect(two_halves.half1 == 0x34);
                    expect(two_halves.half2 == 0x12);
                },
            }
        }
    };
    S.doTheTest();
    comptime S.doTheTest();
}

test "reinterpret bytes of an array into an extern struct" {
    testReinterpretBytesAsExternStruct();
    comptime testReinterpretBytesAsExternStruct();
}

fn testReinterpretBytesAsExternStruct() void {
    var bytes align(2) = []u8{ 1, 2, 3, 4, 5, 6 };

    const S = extern struct {
        a: u8,
        b: u16,
        c: u8,
    };

    var ptr = @ptrCast(*const S, &bytes);
    var val = ptr.c;
    expect(val == 5);
}
$ zig test bitcast.zig
Test 1/2 @bitCast extern structs at runtime and comptime...OK
Test 2/2 reinterpret bytes of an array into an extern struct...OK
All tests passed.

These examples work because extern struct and packed struct have guaranteed in-memory layout.

In addition, as long as the memory is never accessed, pointers with hard coded addresses and pointer arithmetic are supported at comptime:

comptime_ptrs.zig

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

test "C pointer comparison and arithmetic" {
    const S = struct {
        fn doTheTest() void {
            var one: usize = 1;
            var ptr1: [*c]u32 = 0;
            var ptr2 = ptr1 + 10;
            expect(ptr1 == 0);
            expect(ptr1 >= 0);
            expect(ptr1 <= 0);
            expect(ptr1 < 1);
            expect(ptr1 < one);
            expect(1 > ptr1);
            expect(one > ptr1);
            expect(ptr1 < ptr2);
            expect(ptr2 > ptr1);
            expect(ptr2 >= 40);
            expect(ptr2 == 40);
            expect(ptr2 <= 40);
            ptr2 -= 10;
            expect(ptr1 == ptr2);
        }
    };
    S.doTheTest();
    comptime S.doTheTest();
}
$ zig test comptime_ptrs.zig
Test 1/1 C pointer comparison and arithmetic...OK
All tests passed.

Thread-Local Variables §

A variable may be specified to be a thread-local variable using the threadlocal keyword:

tls.zig

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

threadlocal var x: i32 = 1234;

test "thread local storage" {
    const thread1 = try std.os.spawnThread({}, testTls);
    const thread2 = try std.os.spawnThread({}, testTls);
    testTls({});
    thread1.wait();
    thread2.wait();
}

fn testTls(context: void) void {
    assert(x == 1234);
    x += 1;
    assert(x == 1235);
}
$ zig test tls.zig
Test 1/1 thread local storage...OK
All tests passed.

For Single-Threaded Builds, all thread local variables are treated as Global Variables.

Thread local variables may not be const.

Note that thread local variables have better performance for non- position independent code, which is one reason for Static by Default.

Thanks to Rich Felker from the musl libc project, who gave me this crucial information, when I was implementing thread local storage for static builds:

to satisfy the abi, your init code has to write the same value to that memory location as the value passed to the [arch_prctl] syscall

Avoid Creating Unnecessary Global Constants §

This deletes some legacy cruft, and produces leaner object files. Example:

var x: i32 = 1234;

export fn entry() i32 {
    return x;
}

This produced:

@x = internal unnamed_addr global i32 1234, align 4
@0 = internal unnamed_addr constant i32* @x, align 8

and @0 was never even used. After Zig 0.4.0, @0 is not produced.

This fixes a bug: Zig was creating invalid LLVM IR when one of these globals that shouldn't exist takes the address of a thread local variable. In LLVM 8, it would produce a linker error. But probably after my bug report is solved it will be caught by the IR verifier.

Thanks to Rui Ueyama and George Rimar who helped me troubleshoot this problem when I filed an LLVM bug report.

C Pointers §

One of the use cases of Zig is to beat C at its own game, and this means being a better language for using C libraries than C is. It's a tall order to fulfill, and it was compromised by Zig's pointer reform. An unfortunate side effect of having more type safety in pointer types was that C's very unsafe pointer types became awkward to use. It is ambiguous whether pointers should be translated as single-item pointers (*T) or unknown-length pointers ([*]T).

C Pointers are a compromise so that Zig code can utilize translated header files directly.

This type is to be avoided whenever possible. The only valid reason for using a C pointer is in auto-generated code from translating C code.

[*c]T - C pointer.

It's planned to support `if`, `orelse`, `null`, and `.?` for C pointers.

While C pointers themselves have all the footguns associated with, well, C pointers, there is safety when converting to Zig pointers:

test.zig

test "cast null C pointer to Zig pointer" {
    var c_ptr: [*c]i32 = 0;
    var zig_ptr: *i32 = c_ptr;
}
$ zig test test.zig
Test 1/1 cast null C pointer to Zig pointer...cast causes pointer to be null
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:3:25: 0x2040f1 in ??? (test)
    var zig_ptr: *i32 = c_ptr;
                        ^
/home/andy/dev/zig/build/lib/zig/std/special/test_runner.zig:13:25: 0x225bdb in ??? (test)
        if (test_fn.func()) |_| {
                        ^
/home/andy/dev/zig/build/lib/zig/std/special/bootstrap.zig:122:22: 0x225366 in ??? (test)
            root.main() catch |err| {
                     ^
/home/andy/dev/zig/build/lib/zig/std/special/bootstrap.zig:43:5: 0x2250d1 in ??? (test)
    @noInlineCall(posixCallMainAndExit);
    ^

Tests failed. Use the following command to reproduce the failure:
/home/andy/dev/www.ziglang.org/docgen_tmp/test

Removal of the Namespace Type §

In Zig 0.3.0, @typeOf(@import("builtin")) is (import), which was a special "namespace" type. In Zig 0.4.0, @typeOf(@import("builtin")) is type. The special "namespace" type is removed; instead all files are structs with no fields.

This makes the language smaller and simpler, and makes imported files easier to inspect with reflection, since they are structs.

With this change, struct types get fully qualified names and function symbol names become fully qualified. This means that when using zig test it's no longer necessary to manually namespace test names as they will be automatically namespaced.

There is a new compile error for importing a file outside the package path. The new CLI option --main-pkg-path can be used to choose a different root package directory besides the one inferred from the root source file. The corresponding Zig Build System API is artifact.setMainPkgPath(path).

Anonymous Enum Literal Type §

Zig has a new type to make dealing with enums more attractive. It can be thought of as the enum equivalent of comptime_int. The enum equivalent of a number literal.

enum-lit.zig

const std = @import("std");

pub fn main() void {
    print(1234, .hex);
    print(1234, .normal);
}

fn print(x: i32, mode: enum { normal, hex }) void {
    switch (mode) {
        .normal => std.debug.warn("{}\n", x),
        .hex => std.debug.warn("0x{x}\n", x),
    }
}
$ zig build-exe enum-lit.zig
$ ./enum-lit
0x4d2
1234

Here you can see an anonymous enum was used rather than an opaque bool parameter. The idea here is to make it so easy to have meaningful values, that programmers will want to use them, even if they're feeling lazy.

The ergonomics of this feature feel great. Here's tgschultz updating some of the standard library API to use anonymous enum literals.

Note that this feature is checked by the compiler. It's completely safe:

test.zig

const std = @import("std");

test "invalid enum literal" {
    print(1234, .hex);
    print(1234, .invalid);
}

fn print(x: i32, mode: enum { normal, hex }) void {
    switch (mode) {
        .normal => std.debug.warn("{}\n", x),
        .hex => std.debug.warn("0x{x}\n", x),
    }
}
$ zig test test.zig
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:5:17: error: enum 'enum:8:24' has no field named 'invalid'
    print(1234, .invalid);
                ^
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:8:24: note: 'enum:8:24' declared here
fn print(x: i32, mode: enum { normal, hex }) void {
                       ^

Standard Library API Changes §

assert vs expect §

Previously, std.debug.assert would @panic in test builds, if the assertion failed. Now, it's always unreachable.

This makes release mode test builds more accurately test the actual code that will be run.

However this requires tests to call std.testing.expect rather than std.debug.assert to make sure output is correct.

Here is the explanation of when to use either one, copied from the assert doc comments:

Inside a test block, it is best to use the std.testing module rather than assert, because assert may not detect a test failure in ReleaseFast and ReleaseSmall modes. Outside of a test block, assert is the correct function to use.

See #1304.

std.debug.captureStackTrace §

Zig code can use std.debug.captureStackTrace at any time for debugging purposes:

stack-traces.zig

const std = @import("std");
const builtin = @import("builtin");

var address_buffer: [8]usize = undefined;

var trace1 = builtin.StackTrace{
    .instruction_addresses = address_buffer[0..4],
    .index = 0,
};

var trace2 = builtin.StackTrace{
    .instruction_addresses = address_buffer[4..],
    .index = 0,
};

pub fn main() void {
    foo();
    bar();

    std.debug.warn("first one:\n");
    std.debug.dumpStackTrace(trace1);
    std.debug.warn("\n\nsecond one:\n");
    std.debug.dumpStackTrace(trace2);
}

fn foo() void {
    std.debug.captureStackTrace(null, &trace1);
}

fn bar() void {
    std.debug.captureStackTrace(null, &trace2);
}
$ zig build-exe stack-traces.zig
$ ./stack-traces
first one:
/home/andy/dev/www.ziglang.org/docgen_tmp/stack-traces.zig:27:32: 0x22586d in ??? (stack-traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/andy/dev/www.ziglang.org/docgen_tmp/stack-traces.zig:17:8: 0x225819 in ??? (stack-traces)
    foo();
       ^
/home/andy/dev/zig/build/lib/zig/std/special/bootstrap.zig:112:22: 0x22528b in ??? (stack-traces)
            root.main();
                     ^
/home/andy/dev/zig/build/lib/zig/std/special/bootstrap.zig:43:5: 0x225081 in ??? (stack-traces)
    @noInlineCall(posixCallMainAndExit);
    ^


second one:
/home/andy/dev/www.ziglang.org/docgen_tmp/stack-traces.zig:31:32: 0x22588d in ??? (stack-traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/andy/dev/www.ziglang.org/docgen_tmp/stack-traces.zig:18:8: 0x22581e in ??? (stack-traces)
    bar();
       ^
/home/andy/dev/zig/build/lib/zig/std/special/bootstrap.zig:112:22: 0x22528b in ??? (stack-traces)
            root.main();
                     ^
/home/andy/dev/zig/build/lib/zig/std/special/bootstrap.zig:43:5: 0x225081 in ??? (stack-traces)
    @noInlineCall(posixCallMainAndExit);
    ^

This feature is used in the GeneralPurposeDebugAllocator project for helping detect and diagnose memory issues.

Allocator interface §

Zig 0.4.0 brings breaking changes to the std.mem.Allocator interface and API.

Before, allocator implementations had to provide allocFn, reallocFn, and freeFn.

Now, they must provide only reallocFn and shrinkFn. Reallocating from a zero length slice is allocation, and shrinking to a zero length slice is freeing.

When the new memory size is less than or equal to the previous allocation size, reallocFn now has the option to return error.OutOfMemory to indicate that the allocator would not be able to take advantage of the new size.

For more details see #1306.

I have reproduced the documentation comments here:

pub const Allocator = struct {
    pub const Error = error{OutOfMemory};

    /// Realloc is used to modify the size or alignment of an existing allocation,
    /// as well as to provide the allocator with an opportunity to move an allocation
    /// to a better location.
    /// When the size/alignment is greater than the previous allocation, this function
    /// returns `error.OutOfMemory` when the requested new allocation could not be granted.
    /// When the size/alignment is less than or equal to the previous allocation,
    /// this function returns `error.OutOfMemory` when the allocator decides the client
    /// would be better off keeping the extra alignment/size. Clients will call
    /// `shrinkFn` when they require the allocator to track a new alignment/size,
    /// and so this function should only return success when the allocator considers
    /// the reallocation desirable from the allocator's perspective.
    /// As an example, `std.ArrayList` tracks a "capacity", and therefore can handle
    /// reallocation failure, even when `new_n` <= `old_mem.len`. A `FixedBufferAllocator`
    /// would always return `error.OutOfMemory` for `reallocFn` when the size/alignment
    /// is less than or equal to the old allocation, because it cannot reclaim the memory,
    /// and thus the `std.ArrayList` would be better off retaining its capacity.
    /// When `reallocFn` returns,
    /// `return_value[0..min(old_mem.len, new_byte_count)]` must be the same
    /// as `old_mem` was when `reallocFn` is called. The bytes of
    /// `return_value[old_mem.len..]` have undefined values.
    /// The returned slice must have its pointer aligned at least to `new_alignment` bytes.
    reallocFn: fn (
        self: *Allocator,
    /// Guaranteed to be the same as what was returned from most recent call to
    /// `reallocFn` or `shrinkFn`.
    /// If `old_mem.len == 0` then this is a new allocation and `new_byte_count`
    /// is guaranteed to be >= 1.
        old_mem: []u8,
    /// If `old_mem.len == 0` then this is `undefined`, otherwise:
    /// Guaranteed to be the same as what was returned from most recent call to
    /// `reallocFn` or `shrinkFn`.
    /// Guaranteed to be >= 1.
    /// Guaranteed to be a power of 2.
        old_alignment: u29,
    /// If `new_byte_count` is 0 then this is a free and it is guaranteed that
    /// `old_mem.len != 0`.
        new_byte_count: usize,
    /// Guaranteed to be >= 1.
    /// Guaranteed to be a power of 2.
    /// Returned slice's pointer must have this alignment.
        new_alignment: u29,
    ) Error![]u8,

    /// This function deallocates memory. It must succeed.
    shrinkFn: fn (
        self: *Allocator,
    /// Guaranteed to be the same as what was returned from most recent call to
    /// `reallocFn` or `shrinkFn`.
        old_mem: []u8,
    /// Guaranteed to be the same as what was returned from most recent call to
    /// `reallocFn` or `shrinkFn`.
        old_alignment: u29,
    /// Guaranteed to be less than or equal to `old_mem.len`.
        new_byte_count: usize,
    /// If `new_byte_count == 0` then this is `undefined`, otherwise:
    /// Guaranteed to be less than or equal to `old_alignment`.
        new_alignment: u29,
    ) []u8,
};

This allows allocators and data structures to "negotiate" with each other. For example, here is std.ArrayList.shrink:

pub fn shrink(self: *Self, new_len: usize) void {
    assert(new_len <= self.len);
    self.len = new_len;
    self.items = self.allocator.realloc(self.items, new_len) catch |e| switch (e) {
        error.OutOfMemory => return, // no problem, capacity is still correct then.
    };
}

Please Welcome Antoine Vugliano to the Core Zig Team §

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

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

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

Syntax Highlighting on GitHub §

Thanks to Jeff Fowler for creating sublime-zig-language and for creating the pull request to GitHub to add support for Zig.

Finally, GitHub decided that Zig is popular enough to gain official recognition.

As a fun last step, Marc Tiehuis marked third-party dependencies as vendored, so that we can have this pretty language bar on GitHub:

Now you can discover Zig projects on GitHub via their "trending" web page.

Miscellaneous Improvements §

Recursive Type Mismatch Error Notes §

Function type mismatches now have an error note which explains in more detail why the function types were incompatible:

test.zig

fn do_the_thing(func: fn (arg: i32) void) void {}
fn bar(arg: bool) void {}
test "fn types" {
    do_the_thing(bar);
}
$ zig test test.zig
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:4:18: error: expected type 'fn(i32) void', found 'fn(bool) void'
    do_the_thing(bar);
                 ^
/home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:4:18: note: parameter 0: 'bool' cannot cast into 'i32'
    do_the_thing(bar);
                 ^

Bug Fixes §

This Release Contains Bugs §

Zig has known bugs.

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

Roadmap §

Here is the roadmap from 0.3.0 release notes:

I am sad to report that this list was too ambitious for one release cycle, and while there has been considerable progress, none of these items are complete. And so the roadmap for 0.4.0 is the same.

Type-Based Alias Analysis Status §

It's no longer planned for Zig to have Type Based Alias Analysis. However, there are some open research topics on potentially outlawing all kinds of aliasing unless explicitly declared. Subscribe to #1108 for more details.

For now, Zig has a well-defined memory model, and aliasing is always allowed. Note, however, that not all types have a guaranteed in-memory layout.

macOS/Mach-O Linker Status §

Linking macOS/COFF files is still in a sad state. The Mach-O code in LLD, Zig's linker, has been barely maintained for several years now. Zig has a fork of LLD in its source tree with a hacky patch to fix linking "Hello World" on macOS.

It's now planned for the Zig project to have its own linker. This is in part due to the lack of Mach-O maintenance, and in part because LLD has no plans to do incremental linking - a feature that I foresee to be necessary to achieve the performance we want to have for large projects in the self-hosted compiler.

For now the self-hosted compiler uses --system-linker-hack to be able to link successfully, which is a compromise of Zig's promise that it can build on any target, for any target.

There is also a workaround in the linker phase of Zig for compiler-rt.a and builtin.a. I had to make them object files rather than archive files to avoid crashing LLD.

Self-Hosted Compiler Status §

There has been no progress on the self-hosted compiler in this release cycle. Progress is blocked on the volume of core language changes still happening, as well as no-copy semantics, and Reworking Coroutines.

Package Manager Status §

The package manager will be one of the focus areas of the 0.5.0 release cycle. It depends on networking, which depends on Reworking Coroutines, which depends on no-copy semantics.

Coroutine Status §

There is no progress on coroutines in this release cycle. In fact there has been anti-progress.

Zig 0.4.0 introduces a memory leak to all coroutines. There is an issue where a coroutine calls the function and it frees its own stack frame, but then the return value of shrinkFn is a slice, which is implemented as an sret struct. Writing to the return pointer causes invalid memory write. We could work around it by having a global helper function which has a void return type and calling that instead. But instead this hack will suffice until I rework coroutines to be non-allocating. Basically coroutines are not supported right now until they are reworked as in #1194.

Reworking coroutines is to be a major focus of 0.5.0 as it is blocking networking, the Package Manager, and the Self-Hosted Compiler.

Accepted Proposals §

Here is a small selection of interesting proposals that have been accepted, to give you an idea of the upcoming changes to Zig.

Full list of accepted proposals

Active External Projects Using Zig §

Thank you financial supporters! §

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