0.6.0 Release Notes

Download & Documentation

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

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

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

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

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

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

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

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

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

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

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

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

See the download page for a full list of tarballs.

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

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

Thanks to Benjamin Feng and Colin Svingen's contributions:

The WASI OS bits are audited and updated.

std.heap.page_allocator gains a WebAssembly implementation.

Not only can Zig generate machine code for these targets, but the standard library cross-platform abstractions have implementations for these targets. Thus it is practical to write a pure Zig application with no dependency on libc.

The CI server automatically tests these targets on every commit to master branch, and updates the download page with links to pre-built binaries.

These targets have debug info capabilities and therefore produce stack traces on failed assertions.

libc is available for this target even when cross compiling.

All the behavior tests and applicable standard library tests pass for this target. All language features are known to work correctly.

The standard library supports this target, but it's possible that some APIs will give an "Unsupported OS" compile error. One can link with libc or other libraries to fill in the gaps in the standard library.

These targets are known to work, but may not be automatically tested, so there are occasional regressions.

Some tests may be disabled for these targets as we work toward Tier 1 Support.

The standard library has little to no knowledge of the existence of this target.

Because Zig is based on LLVM, it has the capability to build for these targets, and LLVM has the target enabled by default.

These targets are not frequently tested; one will likely need to contribute to Zig in order to build for these targets.

The Zig compiler might need to be updated with a few things such as what sizes are the C integer types C ABI calling convention for this target bootstrap code and default panic handler

zig targets is guaranteed to include this target.

Support for these targets is entirely experimental.

LLVM may have the target as an experimental target, which means that you need to use Zig-provided binaries for the target to be available, or build LLVM from source with special configure flags. zig targets will display the target if it is available.

will display the target if it is available. This target may be considered deprecated by an official party, such as macosx/i386 in which case this target will remain forever stuck in Tier 4.

This target may only support --emit asm and cannot emit object files.

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

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

In addition:

More cross compiling support for Windows system DLLs.

wWinMain , wWinMainCRTStartup , and DllMain are now recognized entrypoints. (#4376)

, , and are now recognized entrypoints. (#4376) Improved Debug Info and Stack Traces

The hack to disable native CPU Features on Windows is removed thanks to bug fixes in LLVM 10. (#508)

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

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

Thanks to LemonBoy's work on this:

Support for Win32 Thread-Local Storage.

Adding more lib32 .def files from mingw-w64.

Removing x86/Windows name mangling hack and properly generating correct .lib files from mingw-w64 sources, by adding dlltool functionality to Zig.

Adding Test Coverage for i386-windows.

Fixing stack-probe symbol redefinition.

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

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

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

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

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

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

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

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

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

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

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

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

Fix signedness for some fields in ARM stat definition

C ABI support is partially implemented.

Fix possible unaligned ptr from getauxval . This caused SIGILL on armv7a-linux. (#4796)

. This caused SIGILL on armv7a-linux. (#4796) Fix multiplication overflow in hash_const_val . In some cases the compiler was actually emitting an 64 bit signed multiplication, instead of a 32 bit unsigned one.

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

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

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

LemonBoy contributed MIPS fixes:

Correct signal bits for MIPS. Also enable the segfault handler for all the supported architectures beside MIPS.

Fix pipe syscall for MIPS.

Implement target_dynamic_linker for MIPS.

LemonBoy contributed NetBSD fixes: (#4793)

Fixes some nasty errors in the threading code

Makes Zig able to run all the tests (at least on x86-64) except the event ones

Audits and corrects some defines for NetBSD

Nick Erdmann and Heppokoyuki contributed UEFI improvements:

make the subsystem configurable in zig build

fix con_in definition and add EFI_SIMPLE_TEXT_INPUT_PROTOCOL definition

add file protocols and improvements

add documentation

loading images

snp, mnp, ip6, and udp6 support

protocol handling improvements

boot services and runtime services improvements

loaded image protocol improvements

Add shell parameters protocol

device path protocol improvements

status reform

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

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

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

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

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

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

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

This is now allowed:

-target native

Different OS but native CPU, default Windows C ABI:

-target native-windows

This could be useful for example when running in Wine.

Different CPU but native OS, native C ABI.

-target x86_64-native -mcpu=skylake

Different C ABI but otherwise native target:

-target native-native-musl

-target native-native-gnu

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

builtin.os => builtin.os.tag

builtin.arch => builtin.cpu.arch

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

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

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

std.builtin.Version gains a format method.

Thanks to Timon Kruiper for contributions related to this.

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

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

{ "triple": "x86_64-linux.5.4.15...5.4.15-gnu.2.27", "cpu": { "arch": "x86_64", "name": "skylake", "features": [ "64bit", "adx", "aes", "avx", "avx2", "bmi", "bmi2", "clflushopt", "cmov", "cx16", "cx8", "ermsb", "f16c", "false_deps_popcnt", "fast_gather", "fast_scalar_fsqrt", "fast_shld_rotate", "fast_variable_shuffle", "fast_vector_fsqrt", "fma", "fsgsbase", "fxsr", "idivq_to_divl", "invpcid", "lzcnt", "macrofusion", "merge_to_threeway_branch", "mmx", "movbe", "nopl", "pclmul", "popcnt", "prfchw", "rdrnd", "rdseed", "rtm", "sahf", "sgx", "slow_3ops_lea", "sse", "sse2", "sse3", "sse4_1", "sse4_2", "ssse3", "vzeroupper", "x87", "xsave", "xsavec", "xsaveopt", "xsaves" ] }, "os": "linux", "abi": "gnu" }

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

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

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

-target native -mcpu=baseline

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

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

Target the RPi Zero:

-target arm-linux-musleabi -mcpu=arm1176jzf_s

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

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

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

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

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

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

Rather than:

-target armv7a-linux-gnu

Now it is:

-target arm-linux-gnu

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

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

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

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

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

# still valid. default version range -target x86_64-windows-msvc # minimum windows version: XP # maximum windows version: 10 -target x86_64-windows.xp...win10-msvc # minimum windows version: 7 # maximum windows version: latest -target x86_64-windows.win7-msvc # linux example -target aarch64-linux.3.16...5.3.1-musl # specifying glibc version -target mipsel-linux.4.10-gnu.2.1

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

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

Code that used Target.parse need not be updated.

Checking for the OS when doing conditional compilation:

--- a/lib/std/build/run.zig +++ b/lib/std/build/run.zig @@ -82,7 +82,7 @@ pub const RunStep = struct { var key: []const u8 = undefined; var prev_path: ?[]const u8 = undefined; - if (builtin.os == .windows) { + if (builtin.os.tag == .windows) { key = "Path"; prev_path = env_map.get(key); if (prev_path == null) {

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

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

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

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

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

Improved names of error sets when using merge error sets operator ( || ).

). pub syntax for container fields is removed.

syntax for container fields is removed. Type coercion from *[ 0 ]T to E![] const T is now allowed. This is an unambiguous, safe cast.

to is now allowed. This is an unambiguous, safe cast. asm now accepts comptime-known values, rather than requiring string literal syntax.

now accepts comptime-known values, rather than requiring string literal syntax. Removed compile error for peer result ? comptime_int and null . (#2763)

and . (#2763) Ability to pass comptime types and non comptime types to same parameter.

types and non types to same parameter. @typeOf is renamed to @TypeOf . zig fmt automatically performs the conversion, and the next release of Zig after this one will remove the automatic conversion.

is renamed to . zig fmt automatically performs the conversion, and the next release of Zig after this one will remove the automatic conversion. Ability to switch on pointer types. (#4074)

Multiline strings in test and library names are disallowed.

Zig language no longer requires the expression a else unreachable with comptime a to produce a comptime result.

with to produce a result. Timon Kruiper implemented casting between [*c]T and ?[*: 0 ]T on fn parameter. (#4176)

and on parameter. (#4176) Timon Kruiper improved @typeInfo to lazily resolve declarations. This way all the declarations in a namespace won't be resolved until the user actually uses the declarations slice in the builtin TypeInfo union. (#2594, #3893, #4435)

to lazily resolve declarations. This way all the declarations in a namespace won't be resolved until the user actually uses the declarations slice in the builtin TypeInfo union. (#2594, #3893, #4435) @ptrCast supports casting a slice to a pointer.

supports casting a slice to a pointer. LemonBoy implemented peer type resolution between ?[]T and *[N]T . (#4767)

and . (#4767) There is now peer type resolution between mixed-const []T and *[N]T . (#4766)

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

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

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

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

var a: T = b;

is semantically equivalent to:

var a = @as (T, b);

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

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

[]const u8

to

*const [N:0]u8

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

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

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

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

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

sentinel_ptrs.zig

const std = @import ( "std" ); pub fn main () void { do_it_the_zig_way( "world" ); do_it_the_c_way( "world" ); } fn do_it_the_zig_way (arg: [] const u8 ) void { std.debug.warn( "hello {}

" , .{arg}); } fn do_it_the_c_way (arg: [*: 0 ] const u8 ) void { _ = std.c.printf( "hello %s

" , arg); }

$ zig build-exe sentinel_ptrs.zig -lc $ ./sentinel_ptrs hello world hello world

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

slice_sentinel.zig

const std = @import ( "std" ); test "slice with sentinel" { var array = [_] i32 { 'a' , 'b' , 'c' , 'd' , 'e' }; const slice = array[ 1 .. 3 : 'd' ]; const result = foo(slice); std.testing.expect(result == 'b' + 'c' ); } fn foo (s: [*: 'd' ] i32 ) i32 { var sum: i32 = 0 ; var index: usize = 0 ; while (s[index] != 'd' ) : (index += 1 ) { sum += s[index]; } return sum; }

$ zig test slice_sentinel.zig 1/1 test "slice with sentinel"...OK All 1 tests passed.

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

test.zig

test "slice with sentinel" { var array = [_] i32 { 'a' , 'b' , 'c' , 'd' , 'e' }; const slice = array[ 1 .. 3 : 'f' ]; }

$ zig test test.zig 1/1 test "slice with sentinel"...sentinel mismatch /home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:3:24 : 0x204c07 in test "slice with sentinel" (test) const slice = array[1..3 :'f']; ^ /home/andy/Downloads/zig/lib/std/special/test_runner.zig:47:28 : 0x22bb2e in std.special.main (test) } else test_fn.func(); ^ /home/andy/Downloads/zig/lib/std/start.zig:253:37 : 0x20565d in std.start.posixCallMainAndExit (test) const result = root.main() catch |err| { ^ /home/andy/Downloads/zig/lib/std/start.zig:123:5 : 0x20539f in std.start._start (test) @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{}); ^ Tests failed. Use the following command to reproduce the failure: /home/andy/dev/www.ziglang.org/docgen_tmp/test

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

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

test.zig

test "coerce array value to slice" { var array: [] const i32 = [_] i32 { 1 , 2 , 3 , 4 }; }

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

How to upgrade code for these new semantics:

coerce_array_ptr.zig

test "coerce array pointer to slice" { var array: [] const i32 = &[_] i32 { 1 , 2 , 3 , 4 }; }

$ zig test coerce_array_ptr.zig 1/1 test "coerce array pointer to slice"...OK All 1 tests passed.

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

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

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

compare.zig

const std = @import ( "std" ); const expect = std.testing.expect; pub const CompareOperator = enum { lt, lte, eq, gte, gt, neq, }; pub fn compare (a: var , op: CompareOperator, b: var ) bool { return switch (op) { .lt => a < b, .lte => a <= b, .eq => a == b, .neq => a != b, .gt => a > b, .gte => a >= b, }; } test "compare between signed and unsigned" { expect(compare( @as ( i8 , - 1 ), .lt, @as ( u8 , 255 ))); expect(compare( @as ( i8 , 2 ), .gt, @as ( u8 , 1 ))); expect(!compare( @as ( i8 , - 1 ), .gte, @as ( u8 , 255 ))); expect(compare( @as ( u8 , 255 ), .gt, @as ( i8 , - 1 ))); expect(!compare( @as ( u8 , 255 ), .lte, @as ( i8 , - 1 ))); expect(compare( @as ( i8 , - 1 ), .lt, @as ( u9 , 255 ))); expect(!compare( @as ( i8 , - 1 ), .gte, @as ( u9 , 255 ))); expect(compare( @as ( u9 , 255 ), .gt, @as ( i8 , - 1 ))); expect(!compare( @as ( u9 , 255 ), .lte, @as ( i8 , - 1 ))); expect(compare( @as ( i9 , - 1 ), .lt, @as ( u8 , 255 ))); expect(!compare( @as ( i9 , - 1 ), .gte, @as ( u8 , 255 ))); expect(compare( @as ( u8 , 255 ), .gt, @as ( i9 , - 1 ))); expect(!compare( @as ( u8 , 255 ), .lte, @as ( i9 , - 1 ))); expect(compare( @as ( u8 , 1 ), .lt, @as ( u8 , 2 ))); expect( @bitCast ( u8 , @as ( i8 , - 1 )) == @as ( u8 , 255 )); expect(!compare( @as ( u8 , 255 ), .eq, @as ( i8 , - 1 ))); expect(compare( @as ( u8 , 1 ), .eq, @as ( u8 , 1 ))); }

$ zig test compare.zig 1/1 test "compare between signed and unsigned"...OK All 1 tests passed.

Thanks to Shawn Landden for the proposal.

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

struct_result.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "anonymous struct literal" { checkPoint(.{ .x = 13 , .y = 67 , }); } fn checkPoint (pt: struct {x: i32 , y: i32 }) void { expect(pt.x == 13 ); expect(pt.y == 67 ); }

$ zig test struct_result.zig 1/1 test "anonymous struct literal"...OK All 1 tests passed.

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

struct_anon.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "fully anonymous struct" { dump(.{ .int = 1234 , .float = 12.34 , .b = true , .s = "hi" , }); } fn dump (args: var ) void { expect(args.int == 1234 ); expect(args.float == 12.34 ); expect(args.b); expect(args.s[ 0 ] == 'h' ); expect(args.s[ 1 ] == 'i' ); }

$ zig test struct_anon.zig 1/1 test "fully anonymous struct"...OK All 1 tests passed.

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

anon_union.zig

const std = @import ( "std" ); const expect = std.testing.expect; const Number = union { int: i32 , float: f64 , }; test "anonymous union literal syntax" { var i: Number = .{.int = 42 }; var f = makeNumber(); expect(i.int == 42 ); expect(f.float == 12.34 ); } fn makeNumber () Number { return .{.float = 12.34 }; }

$ zig test anon_union.zig 1/1 test "anonymous union literal syntax"...OK All 1 tests passed.

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

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

tuple.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "tuple syntax" { var array: [ 4 ] u8 = .{ 11 , 22 , 33 , 44 }; expect(array[ 0 ] == 11 ); expect(array[ 1 ] == 22 ); expect(array[ 2 ] == 33 ); expect(array[ 3 ] == 44 ); }

$ zig test tuple.zig 1/1 test "tuple syntax"...OK All 1 tests passed.

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

infer_tuple.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "fully anonymous tuple" { dump(.{ @as ( u32 , 1234 ), @as ( f64 , 12.34 ), true , "hi" }); } fn dump (args: var ) void { expect(args.@"0" == 1234 ); expect(args.@"1" == 12.34 ); expect(args.@"2"); expect(args.@"3"[ 0 ] == 'h' ); expect(args.@"3"[ 1 ] == 'i' ); }

$ zig test infer_tuple.zig 1/1 test "fully anonymous tuple"...OK All 1 tests passed.

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

tuples_are_array_like.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "tuples support element access and .len field" { var x: i32 = 1234 ; var y: i32 = 4567 ; var tup = .{ x, y }; tup[ 0 ] += 1 ; tup[ 1 ] -= 1 ; expect(tup[ 0 ] == 1235 ); expect(tup[ 1 ] == 4566 ); var sum: i32 = 0 ; comptime var index = 0 ; inline while (index < tup.len) : (index += 1 ) { sum += tup[index]; } expect(sum == 1235 + 4566 ); } test "tuple concatenation" { var one = .{ "hi" , true }; var two = .{ 12.34 , .ok }; var combined = one ++ two; expect(combined[ 3 ] == .ok); }

$ zig test tuples_are_array_like.zig 1/2 test "tuples support element access and .len field"...OK 2/2 test "tuple concatenation"...OK All 2 tests passed.

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

hello.zig

const std = @import ( "std" ); pub fn main () void { std.debug.warn( "Hello, {}

" , .{ "World!" }); }

$ zig build-exe hello.zig $ ./hello Hello, World!

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

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

It is planned to add tuple type declaration syntax.

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

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

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

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

vector_elem.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "vector element access" { var v: @Vector ( 4 , i32 ) = [_] i32 { 1 , 5 , 3 , undefined }; v[ 2 ] = 42 ; expect(v[ 1 ] == 5 ); v[ 3 ] = - 364 ; expect(v[ 2 ] == 42 ); expect(- 364 == v[ 3 ]); storev(&v[ 0 ], 100 ); expect(v[ 0 ] == 100 ); } fn storev (ptr: var , x: i32 ) void { ptr.* = x; }

$ zig test vector_elem.zig 1/1 test "vector element access"...OK All 1 tests passed.

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

vector_cmp.zig

const std = @import ( "std" ); const expect = std.testing.expect; const mem = std.mem; test "vector comparisons" { var v: @Vector ( 4 , i32 ) = [ 4 ] i32 { 2147483647 , - 2 , 30 , 40 }; var x: @Vector ( 4 , i32 ) = [ 4 ] i32 { 1 , 2147483647 , 30 , 4 }; expect(mem.eql( bool , & @as ([ 4 ] bool , v == x), &[ 4 ] bool { false , false , true , false })); expect(mem.eql( bool , & @as ([ 4 ] bool , v != x), &[ 4 ] bool { true , true , false , true })); expect(mem.eql( bool , & @as ([ 4 ] bool , v < x), &[ 4 ] bool { false , true , false , false })); expect(mem.eql( bool , & @as ([ 4 ] bool , v > x), &[ 4 ] bool { true , false , false , true })); expect(mem.eql( bool , & @as ([ 4 ] bool , v <= x), &[ 4 ] bool { false , true , true , false })); expect(mem.eql( bool , & @as ([ 4 ] bool , v >= x), &[ 4 ] bool { true , false , true , true })); }

$ zig test vector_cmp.zig 1/1 test "vector comparisons"...OK All 1 tests passed.

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

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

test.zig

const std = @import ( "std" ); test "vector division safety" { var a: @Vector ( 4 , i16 ) = [_] i16 { 1 , 2 , - 32768 , 4 }; var b: @Vector ( 4 , i16 ) = [_] i16 { 1 , 2 , - 1 , 4 }; const x = div(a, b); if (x[ 2 ] == 32767 ) return error .Whatever; } fn div (a: @Vector ( 4 , i16 ), b: @Vector ( 4 , i16 )) @Vector ( 4 , i16 ) { return @divTrunc (a, b); }

$ zig test test.zig 1/1 test "vector division safety"...integer overflow /home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:10:12 : 0x2053be in div (test) return @divTrunc(a, b); ^ /home/andy/dev/www.ziglang.org/docgen_tmp/test.zig:6:18 : 0x204bd6 in test "vector division safety" (test) const x = div(a, b); ^ /home/andy/Downloads/zig/lib/std/special/test_runner.zig:47:28 : 0x22bc4e in std.special.main (test) } else test_fn.func(); ^ /home/andy/Downloads/zig/lib/std/start.zig:253:37 : 0x2057cd in std.start.posixCallMainAndExit (test) const result = root.main() catch |err| { ^ /home/andy/Downloads/zig/lib/std/start.zig:123:5 : 0x20550f in std.start._start (test) @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{}); ^ Tests failed. Use the following command to reproduce the failure: /home/andy/dev/www.ziglang.org/docgen_tmp/test

See #903 for more details.

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

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

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

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

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

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

call.zig

const assert = @import ( "std" ).debug.assert; test "noinline function call" { assert( @call (.{}, add, .{ 3 , 9 }) == 12 ); } fn add (a: i32 , b: i32 ) i32 { return a + b; }

$ zig test call.zig 1/1 test "noinline function call"...OK All 1 tests passed.

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

pub const CallOptions = struct { modifier: Modifier = .auto, stack: ?[] align (std.Target.stack_align) u8 = null , pub const Modifier = enum { auto, async_kw, never_tail, never_inline, no_async, always_tail, always_inline, compile_time, }; };

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

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

For an explanation of .no_async , see noasync.

Thanks to LemonBoy for contributions related to this feature.

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

extern fn foo () void {}

New syntax:

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

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

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

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

pub const CallingConvention = enum { Unspecified, C, Cold, Naked, Async, Interrupt, Signal, Stdcall, Fastcall, Vectorcall, Thiscall, APCS, AAPCS, AAPCSVFP, };

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

Thanks to LemonBoy for implementing this.

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

@intToEnum on a non-exhaustive enum never fails.

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

switch_non_exhaustive_enum.zig

const std = @import ( "std" ); const assert = std.debug.assert; const Number = enum ( u8 ) { One, Two, Three, _, }; test "switch on non-exhaustive enum" { const number = Number.One; const result = switch (number) { .One => true , .Two, .Three => false , _ => false , }; assert(result); const is_one = switch (number) { .One => true , else => false , }; assert(is_one); }

$ zig test switch_non_exhaustive_enum.zig 1/1 test "switch on non-exhaustive enum"...OK All 1 tests passed.

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

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

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

unicode_char_lit.zig

const std = @import ( "std" ); test "utf8 character literal" { const x = '💩' ; std.testing.expect(x == 128169 ); }

$ zig test unicode_char_lit.zig 1/1 test "utf8 character literal"...OK All 1 tests passed.

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

Thanks to Nick Erdmann for implementing this feature.

Thanks to Vexu:

Atomic operations additionally support enums, bools, non-power-of-two integers, and floats.

There is a new @atomicStore builtin.

builtin. @cmpxchgWeak , @cmpxchgStrong , and @atomicRmw now support being evaluated in comptime code.

const foo = bar;

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

comptime_struct_field.zig

const std = @import ( "std" ); const Foo = struct { a: i32 , comptime b: i32 = 1234 , }; test "example" { var foo: Foo = undefined ; comptime std.debug.assert(foo.b == 1234 ); }

$ zig test comptime_struct_field.zig 1/1 test "example"...OK All 1 tests passed.

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

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

csf_example.zig

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

" , .{field.name, @field (args, field.name)}); } } pub fn main () void { var runtime_float: f32 = 12.34 ; dump(.{ .int = 1234 , .float = runtime_float, .b = true , .s = "hi" , .T = [*] f32 , }); }

$ zig build-exe csf_example.zig $ ./csf_example int = 1234 float = 1.23400001e+01 b = true s = hi T = type

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

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

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

untyped_struct_fields.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "struct with var field" { const Point = struct { x: var , y: var , }; comptime var pt = Point { .x = 1 , .y = 2 , }; expect(pt.x == 1 ); expect(pt.y == 2 ); pt.x = true ; pt.y = "hello" ; expect(pt.x); expect(std.mem.eql( u8 , pt.y, "hello" )); }

$ zig test untyped_struct_fields.zig 1/1 test "struct with var field"...OK All 1 tests passed.

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

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

With Zig 0.6.0, this works now:

type_info_struct.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "access default initialization value" { const Foo = struct { x: i32 = 1234 , y: i32 , }; const info = @typeInfo (Foo).Struct; expect(info.fields[ 0 ].default_value.? == 1234 ); expect(info.fields[ 1 ].default_value == null ); }

$ zig test type_info_struct.zig 1/1 test "access default initialization value"...OK All 1 tests passed.

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

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

Thanks to LemonBoy for contributions related to this feature.

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

ptr_arith_align.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "pointer math alignment" { var arr: [ 10 ] u8 align ( 4 ) = undefined ; var runtime_known_2: usize = 2 ; const ptr: [*] u8 = &arr; const ptr2 = ptr + 1 ; const ptr3 = ptr + 2 ; const ptr4 = ptr + runtime_known_2; comptime { expect( @TypeOf (ptr) == [*] align ( 4 ) u8 ); expect( @TypeOf (ptr2) == [*] u8 ); expect( @TypeOf (ptr3) == [*] align ( 2 ) u8 ); expect( @TypeOf (ptr4) == [*] u8 ); } }

$ zig test ptr_arith_align.zig 1/1 test "pointer math alignment"...OK All 1 tests passed.

Thanks to LemonBoy for implementing this (#1528).

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

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

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

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

Thanks LemonBoy for implementing this (#2679).

@bitSizeOf ( comptime T: type ) comptime_int

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

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

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

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

Thanks to Vexu for the implementation of this.

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

no_capture_aliasing.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "no capture value aliasing" { expect(foo() == 1234 ); } fn foo () i32 { var optional_x: ? i32 = 1234 ; if (optional_x) |x| { optional_x = 5678 ; return x; } unreachable ; }

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

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

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

capture_aliasing.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "capture value aliasing" { expect(foo() == 5678 ); } fn foo () i32 { var optional_x: ? i32 = 1234 ; if (optional_x) |*x| { optional_x = 5678 ; return x.*; } unreachable ; }

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

Thanks to LemonBoy for implementing this.

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

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

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

noasync.zig

const std = @import ( "std" ); const expect = std.testing.expect; test "noasync function call" { const result = noasync add( 50 , 100 ); expect(result == 150 ); } fn add (a: i32 , b: i32 ) i32 { if (a > 100 ) { suspend ; } return a + b; }

$ zig test noasync.zig 1/1 test "noasync function call"...OK All 1 tests passed.

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

async_main.zig

const std = @import ( "std" ); const expect = std.testing.expect; var global_frame_1: anyframe = undefined ; var global_frame_2: anyframe = undefined ; pub fn main () void { var main_frame = async asyncMain(); resume global_frame_1; resume global_frame_2; const result = noasync await main_frame; std.debug.warn( "result: {}

" , .{result}); } fn asyncMain () i32 { var a = async foo(); var b = async bar(); return await a + await b; } fn foo () i32 { global_frame_1 = @frame (); suspend ; return 1 ; } fn bar () i32 { global_frame_2 = @frame (); suspend ; return 2 ; }

$ zig build-exe async_main.zig $ ./async_main result: 3

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

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

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

oops_await.zig

const std = @import ( "std" ); const expect = std.testing.expect; var global_frame_1: anyframe = undefined ; var global_frame_2: anyframe = undefined ; pub fn main () void { var main_frame = async asyncMain(); resume global_frame_1; resume global_frame_2; const result = await main_frame; std.debug.warn( "result: {}

" , .{result}); } fn asyncMain () i32 { var a = async foo(); var b = async bar(); return await a + await b; } fn foo () i32 { global_frame_1 = @frame (); suspend ; return 1 ; } fn bar () i32 { global_frame_2 = @frame (); suspend ; return 2 ; }

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

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

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

Thanks to Vexu for contributions related to this feature.

Many deprecated builtins have been removed.

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

@bytesToSlice becomes mem.bytesAsSlice

becomes @sliceToBytes becomes mem.sliceAsBytes

Thanks to Vexu for removing these:

@typeId becomes @typeInfo tag-type

becomes tag-type @memberCount becomes std.meta.fields(T).len

becomes @memberName becomes std.meta.fields(T)[i].name

becomes @memberType becomes std.meta.fields(T)[i].field_type

becomes @ArgType becomes @typeInfo (T).Fn.args[i].arg_type.?

becomes @IntType becomes std.meta.IntType

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

empty_inferred_error_set.zig

fn foo () ! void {} test "" { foo() catch |err| switch (err) {}; }

$ zig test empty_inferred_error_set.zig 1/1 test ""...OK All 1 tests passed.

Thanks to LemonBoy for implementing this.

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

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

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

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

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

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

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

slice_comptime_indexes.zig

const std = @import ( "std" ); const assert = std.debug.assert; test "slicing with comptime indexes" { var a = "abcdefgh" .*; assert( @TypeOf (a) == [ 8 : 0 ] u8 ); var b = a[ 3 .. 6 ]; assert( @TypeOf (b) == *[ 3 ] u8 ); var runtime_i: usize = 3 ; var c = a[runtime_i.. 6 ]; assert( @TypeOf (c) == [] u8 ); a[ 0 .. 3 ].* = a[ 5 .. 8 ].*; assert(std.mem.eql( u8 , &a, "fghdefgh" )); }

$ zig test slice_comptime_indexes.zig 1/1 test "slicing with comptime indexes"...OK All 1 tests passed.

Thanks to Jimmi Holst Christensen for proposing this.

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

errdefer_payload.zig

const std = @import ( "std" ); fn perform () ! void { errdefer |err| std.debug.assert(err == error .Overflow); _ = try std.math.add( u8 , 255 , 1 ); } test "errdefer with payload" { perform() catch return ; unreachable ; }

$ zig test errdefer_payload.zig 1/1 test "errdefer with payload"...OK All 1 tests passed.

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

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

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

Various contributors updated the standard library to use newer Zig syntax, such as anonymous enum literals, and to fix regressions from breaking language changes.

LemonBoy added support for the statx syscall.

Jonathan Marler fixed accept function API.

std.os.accept4: improve docs and integrate with evented I/O

Jonathan Marler improved TTY detection to take into account TERM=dumb.

std.os.dup2 makes EBADF more obvious in stack traces.

makes EBADF more obvious in stack traces. daurnimator updated the standard library OS bits to Linux 5.6, added missing OS bits, organized declarations, and swapped constants for Non-Exhaustive Enums.

Brendan Hansknecht improved big ints to use the more efficient karatsuba algorithm for multiplication.

daurnimator contributed LinearFifo which is useful for buffers. schroffl and Tetralux contributed improvements.

std.ChildProcess.spawn now has a consistent error set across targets.

now has a consistent error set across targets. std.io.getStdOut and related functions no longer can error. Thanks to the Windows Process Environment Block, it is possible to obtain handles to the standard input, output, and error streams without the possibility of failure.

and related functions no longer can error. Thanks to the Windows Process Environment Block, it is possible to obtain handles to the standard input, output, and error streams without the possibility of failure. dbandstra added std.math.tau constant (equivalent to 2 * pi ).

constant (equivalent to ). Johan Bolmsjö improved std.testing.expectEqual to show differing pointer values, avoiding confusion when the values pointed to are the same.

to show differing pointer values, avoiding confusion when the values pointed to are the same. std.heap.direct_allocator is renamed to std.heap.page_allocator , to make it more clear that this is not an appropriate general-purpose allocator.

is renamed to , to make it more clear that this is not an appropriate general-purpose allocator. Benjamin Feng size-optimized the std.sort internal binary search algorithm.

LemonBoy added std.sort.binarySearch . (#4337)

. (#4337) std.elf API updated to remove redundant namespacing, and integrate with std.Target.Arch .

API updated to remove redundant namespacing, and integrate with . Felix Queißner implemented std.testing.expectEqual for tagged unions. (#3773)

for tagged unions. (#3773) std.math : remove constants that should be expressions. There were four cases where the value can be represented in fewer characters with expressions, which will be guaranteed to happen at compile-time, and have the same or better precision.

: remove constants that should be expressions. There were four cases where the value can be represented in fewer characters with expressions, which will be guaranteed to happen at compile-time, and have the same or better precision. Robin Voetter made improvements to std.sort : Added isSorted . Updated max to accept const slices, and added tests. Updated min and max to return ?T . Added argMax and argMin which return indexes rather than values.

: std.fmt.ParseUnsignedError is now public.

is now public. frmdstryr put in a hot path for std.io.BufferedInStream.readByte , speeding it up by ~75% (#3858).

, speeding it up by ~75% (#3858). Dynamic library loading API functions are improved to follow the standard conventions with regards to filename parameters.

LemonBoy improved std.ChildProcess to use eventfds on Linux rather than pipe for communicating an error from child to parent process. (#819)

to use eventfds on Linux rather than pipe for communicating an error from child to parent process. (#819) Dmitry Atamanov and daurnimator improved std.unicode.utf8ToUtf16Le to support surrogate pairs. (#3923)

to support surrogate pairs. (#3923) daurnimator improved the performance of unicode functions. (#3987)

daurnimator updated std.meta.TagPayloadType to take the tag type of the union.

to take the tag type of the union. lukechampine implemented ChaCha20-Poly1305 AEAD. (#4011)

Luna added std.os.memfd_create . (#3687)

. (#3687) data-man added std.os.getrusage . (#3854)

. (#3854) Nathan Michaels added removeIndex function to PriorityQueue. (#4070)

Jonathan Marler added std.os.windows.WaitForSingleObject .

. Hersh Krishna added std.math.clamp .

. Shawn Landden made breaking changes to std.rb to make it thread-safe.

to make it thread-safe. daurnimator updated std.mem.Allocator interface to set memory to undefined when freed (#4087). However note that it is planned to revert this and implement this as part of allocator implementations rather than the interface.

interface to set memory to undefined when freed (#4087). However note that it is planned to revert this and implement this as part of allocator implementations rather than the interface. LemonBoy made writeByteNTimes faster and leaner.

faster and leaner. nofmal added a basic Linux termios implementation.

Felix (xq) Queißner made std.heap.ArenaAllocator.deinit not require a mutable reference.

not require a mutable reference. Implement std.os.faccessat for Windows.

for Windows. Support the concept of a target not having a dynamic linker.

Improved handling of environment variables on Windows. std.os.getenv and std.os.getenvZ have nice compile errors when not linking libc and using Windows. std.os.getenvW is provided as a Windows-only API that does not require an allocator. It uses the Process Environment Block. std.process.getEnvVarOwned is improved to be a simple wrapper on top of std.os.getenvW. std.process.getEnvMap is improved to use the Process Environment Block rather than calling GetEnvironmentVariableW. std.zig.system.NativePaths uses process.getEnvVarOwned instead of std.os.getenvZ, which works on Windows as well as POSIX.

Heide Onas Auri improved std.time.Timer.lap to only read system time once. (#4533)

to only read system time once. (#4533) std.Thread.cpuCount on Windows uses the PEB, rather than calling GetSystemInfo from kernel32.dll. Also remove OutOfMemory from the error set.

on Windows uses the PEB, rather than calling GetSystemInfo from kernel32.dll. Also remove OutOfMemory from the error set. Jared Miller implemented std.unicode.utf8ToUtf16LeStringLiteral which can be used to provide convenient "wide string literals": w( "foo" )

which can be used to provide convenient "wide string literals": LemonBoy added std.os.fnctl

Joachim Schmidt improved bigint comparison code to use math.Order rather than i8 (#4791)

rather than (#4791) Ilmari Autio improved std.os.getenv and related functions to be ascii-case-insensitive on Windows. (#4608)

and related functions to be ascii-case-insensitive on Windows. (#4608) joachimschmidt557 moved std.big.rational.gcd to std.big.int.gcd

to Phil Schumann improved std.zig.parseStringLiteral to support hex and unicode escapes. (#4678)

to support hex and unicode escapes. (#4678) std.io.readLine is removed.



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



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

is removed. This was deceptive. It was always meant to be sort of a "GNU readline" sort of thing where it provides a Command Line Interface to input text. However that functionality did not exist and it was basically a red herring for people trying to read line-delimited input from a stream. (See I/O Streams for that.) The API is now deleted, so that people can find the proper API more easily. A CLI text input abstraction would be useful but may not even need to be in the standard library. The guess_number CLI game example gets by just fine by using . std.os.execvpe related functions support optionally expanding argv[0] into the absolute path based on the PATH environment variable. This can be useful to work around a third party program which improperly uses argv[ 0 ] to find the path to its own executable.

related functions support optionally expanding argv[0] into the absolute path based on the PATH environment variable. This can be useful to work around a third party program which improperly uses to find the path to its own executable. std.os.execve had the wrong name; it should have been std.os.execvpe . This is now corrected. It is also improved to handle ENOTDIR (#3415).

had the wrong name; it should have been . This is now corrected. It is also improved to handle (#3415). Introduce std.os.execveZ which does not look at PATH, and uses null terminated parameters, matching POSIX ABIs. It does not require an allocator.

which does not look at PATH, and uses null terminated parameters, matching POSIX ABIs. It does not require an allocator. Introduce std.os.execvpeZ , which is like execvpe except it uses null terminated parameters, matching POSIX ABIs, and thus does not require an allocator.

, which is like except it uses null terminated parameters, matching POSIX ABIs, and thus does not require an allocator. Sebastian Keller added std.math constants such as log2e and sqrt2. Note that with #425 solved these would not be needed, and would be removed.

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

kprotty contributed significant improvements to synchronization primitives. kprotty writes:

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

1) Adaptive Locking

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

2) Parker API

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

On Windows, it detects at runtime whether to use WaitOnAddress (supported since Win8+ and most similar to linux futex) or NT Keyed Events (supported since WinXP+ and is the inner backing of CriticalSection). This allows the distinction between std.Mutex and std.StaticallyInitializedMutex to dissapear as it can now be initialized statically

On POSIX platforms, it uses pthread_cond_t for synchronization which also supports static initialization. This fares better for longer blocking critical sections compared to the spinlock default of the current std.Mutex implementation.

for synchronization which also supports static initialization. This fares better for longer blocking critical sections compared to the spinlock default of the current std.Mutex implementation. On Linux, it still uses linux_futex so not many improvements besides adaptive spinning there

Results & Future Implications

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

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

Windows 10: 7-8x speedup

MacOSX: 19-22x speedup

Linux (Pthread): 2x speedup

Linux (Futex): 2x speedup

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

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

pub const io_mode = .evented;

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

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

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

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

write_file_blocking.zig

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

" ); }

$ zig build-exe write_file_blocking.zig $ ./write_file_blocking

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

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

", 6) = 6 close(3) = 0 exit_group(0) = ?

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

write_file_evented.zig

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

" ); }

$ zig build-exe write_file_evented.zig $ ./write_file_evented

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

clone(child_stack=0x7fe36dbdeff8, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID|0x400000strace: Process 8891 attached , parent_tid=[8891], tls=0x7fe36dbdf028, child_tidptr=0x7fe36dbdf000) = 8891 futex(0x7fe36dbdf000, FUTEX_WAIT, 8891, NULL <unfinished ...> [pid 8891] futex(0x260b50, FUTEX_WAIT, 0, NULL <unfinished ...> [pid 8891] <... futex resumed>) = 0 [pid 8891] openat(AT_FDCWD, "hello.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 20 [pid 8891] epoll_ctl(18, EPOLL_CTL_ADD, 17, {EPOLLIN|EPOLLOUT|EPOLLONESHOT|EPOLLET, {u32=1841168864, u64=140614775472608}}) = 0 [pid 8891] futex(0x260b50, FUTEX_WAIT, 0, NULL <unfinished ...> [pid 8891] <... futex resumed>) = 0 [pid 8891] write(20, "hello

", 6) = 6 [pid 8891] epoll_ctl(18, EPOLL_CTL_MOD, 17, {EPOLLIN|EPOLLOUT|EPOLLONESHOT|EPOLLET, {u32=1841168864, u64=140614775472608}}) = 0 [pid 8891] futex(0x260b50, FUTEX_WAIT, 0, NULL <unfinished ...> [pid 8891] <... futex resumed>) = -1 EAGAIN (Resource temporarily unavailable) [pid 8891] close(20 <unfinished ...> [pid 8891] <... close resumed>) = 0 [pid 8891] exit(0 <unfinished ...> [pid 8891] <... exit resumed>) = ? <... futex resumed>) = -1 EAGAIN (Resource temporarily unavailable) exit_group(0) = ?

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

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

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

concurrent.zig

const std = @import ( "std" ); pub const io_mode = .evented; pub fn main () anyerror ! void { var a_frame = async doA(); var b_frame = async doB(); try await a_frame; try await b_frame; } fn doA () ! void { const file = try std.fs.cwd().createFile( "a.txt" , .{}); defer file.close(); try file.writeAll( "A

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

" ); }

$ zig build-exe concurrent.zig $ ./concurrent

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

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

async_blocking.zig

const std = @import ( "std" ); pub fn main () anyerror ! void { var a_frame = async doA(); var b_frame = async doB(); try await a_frame; try await b_frame; } fn doA () ! void { const file = try std.fs.cwd().createFile( "a.txt" , .{}); defer file.close(); try file.writeAll( "A

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

" ); }

$ zig build-exe async_blocking.zig --release-fast $ ./async_blocking

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

arch_prctl(ARCH_SET_FS, 0x203cf0) = 0 openat(AT_FDCWD, "a.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3 write(3, "A

", 2) = 2 close(3) = 0 openat(AT_FDCWD, "b.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3 write(3, "B

", 2) = 2 close(3) = 0 exit_group(0) = ?

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

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

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

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

Fix stack iteration stop condition, and further improve the frame-walking strategy. The code is now stable enough not to cause panics during the call frame walking.

Make the leb module available to non-std code

module available to non-std code Don't generate any type info for void return types. Closely matches what the LLVM debug emitter expects, the generated DWARF infos are now standard-compliant.

Show a nice error message on SIGBUS.

Support handling DWARF version 3.

Properly handle multiple threads panicking at the same time. Instead of walking all over each other, both stack traces will be printed sequentially.

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

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

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

Formatting capabilities were improved:

Ability to format enum-literals

Handle {s} format for Sentinel-Terminated Pointers. (#4219)

Ability to format vectors

Ability to format Non-Exhaustive Enums

Ability to format contents of sentinel terminated many pointers

Format decmial 0.0 with no precision as just 0

An API overhaul is still planned.

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

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

The good news:

The new API is simpler and more ergonomic.

Empirically, it produces significantly faster runtime code.

It works cleanly with Async I/O.

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

const std = @import ( "std" ); pub fn main () anyerror ! void { var stdin_unbuf = std.io.getStdIn().inStream(); const in = &std.io.BufferedInStream( @TypeOf (stdin_unbuf).Error).init(&stdin_unbuf.stream).stream; var sum: u64 = 0 ; var line_buf: [ 50 ] u8 = undefined ; while ( try in.readUntilDelimiterOrEof(&line_buf, '

' )) |line| { if (line.len == 0 ) break ; const module_mass = try std.fmt.parseInt( u64 , line, 10 ); const fuel_required = (module_mass / 3 ) - 2 ; sum += fuel_required; } const out = &std.io.getStdOut().outStream().stream; try out.print( "{}

" , .{sum}); }

New streams API:

const std = @import ( "std" ); pub fn main () anyerror ! void { const in = std.io.bufferedInStream(std.io.getStdIn().inStream()).inStream(); var sum: u64 = 0 ; var line_buf: [ 50 ] u8 = undefined ; while ( try in.readUntilDelimiterOrEof(&line_buf, '

' )) |line| { if (line.len == 0 ) break ; const module_mass = try std.fmt.parseInt( u64 , line, 10 ); const fuel_required = (module_mass / 3 ) - 2 ; sum += fuel_required; } const out = std.io.getStdOut().outStream(); try out.print( "{}

" , .{sum}); }

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

pub const io_mode = .evented;

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

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

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

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

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

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

Operations which are relative to a directory, should be methods of the std.fs.Dir namespace.

namespace. Filesystem functions should not take unnecessary Allocator parameters.

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

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

var src_dir = try std.fs.cwd().openDir(build_output_dir, .{ .iterate = true }); defer src_dir.close(); var dest_dir = try std.fs.cwd().openDir(output_dir, .{}); defer dest_dir.close(); var it = src_dir.iterate(); while ( try it.next()) |entry| { _ = try src_dir.updateFile(entry.name, dest_dir, entry.name, .{}); }

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

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

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

sendfile support. File system implementations which copy files now use this when possible.

Better support for writev, readv, pwritev, preadv, etc.

Various APIs and implementations improved to take advantage of directory handles.

Error sets of various functions are improved to no longer have some kinds of errors, such as error .OutOfMemory , or in the case of deleting a file, error .NoSpaceLeft .

, or in the case of deleting a file, . The new convention for APIs with null-terminated path parameters is a 'Z' suffix rather than a 'C' suffix.

Some APIs are modified to take "options" structs which have default field values.

inode number / file index is exposed in File.Stat.

Cross-platform file locking flags are supported.

fs.File supports setEndPos to grow/shrink the file size as needed. (#4716)

Filesystem APIs now integrate with Async I/O, including automatically performing operations on a file system thread pool to avoid blocking.

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

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

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

Added std.net.StreamServer .

Added std.net.tcpConnectToHost .

Added std.net.tcpConnectToAddress .

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

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

std.os IPPROTO constants are canonicalized.

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

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

Added std.json.WriteStream.writeJson .

. std.json.Value: added dumpStream(), utilize WriteStream for dump().

std.json.Token is now a union ( enum ) .

. Improve json.unescapeString to no longer take an allocator

Add json.stringify to encode arbitrary values to JSON

Add json.parse to automatically decode json into a struct

Add json.WriteStream.stringify

Disallow overlong and out-of-range UTF-8

Support unescaping JSON strings

Surrogate pair support

Implement copy_strings=false

Here is an example of parsing into an arbitrary struct:

json_parse_struct.zig

const std = @import ( "std" ); const json = std.json; test "parse into struct with misc fields" { @setEvalBranchQuota ( 10000 ); const options = json.ParseOptions{ .allocator = std.testing.allocator }; const T = struct { int: i64 , float: f64 , @"with\\escape": bool , @"withąunicode😂": bool , language: [] const u8 , optional: ? bool , default_field: i32 = 42 , static_array: [ 3 ] f64 , dynamic_array: [] f64 , const Bar = struct { nested: [] const u8 , }; complex: Bar, const Baz = struct { foo: [] const u8 , }; veryComplex: []Baz, const Union = union ( enum ) { x: u8 , float: f64 , string: [] const u8 , }; a_union: Union, }; const r = try json.parse(T, &json.TokenStream.init( \\{ \\ "int": 420, \\ "float": 3.14, \\ "with\\escape": true, \\ "with\u0105unicode\ud83d\ude02": false, \\ "language": "zig", \\ "optional": null, \\ "static_array": [66.6, 420.420, 69.69], \\ "dynamic_array": [66.6, 420.420, 69.69], \\ "complex": { \\ "nested": "zig" \\ }, \\ "veryComplex": [ \\ { \\ "foo": "zig" \\ }, { \\ "foo": "rocks" \\ } \\ ], \\ "a_union": 100000 \\} ), options); defer json.parseFree(T, r, options); std.testing.expectEqual( @as ( i64 , 420 ), r.int); std.testing.expectEqual( @as ( f64 , 3.14 ), r.float); std.testing.expectEqual( true , r.@"with\\escape"); std.testing.expectEqual( false , r.@"withąunicode😂"); std.testing.expectEqualSlices( u8 , "zig" , r.language); std.testing.expectEqual( @as (? bool , null ), r.optional); std.testing.expectEqual( @as ( i32 , 42 ), r.default_field); std.testing.expectEqual( @as ( f64 , 66.6 ), r.static_array[ 0 ]); std.testing.expectEqual( @as ( f64 , 420.420 ), r.static_array[ 1 ]); std.testing.expectEqual( @as ( f64 , 69.69 ), r.static_array[ 2 ]); std.testing.expectEqual( @as ( usize , 3 ), r.dynamic_array.len); std.testing.expectEqual( @as ( f64 , 66.6 ), r.dynamic_array[ 0 ]); std.testing.expectEqual( @as ( f64 , 420.420 ), r.dynamic_array[ 1 ]); std.testing.expectEqual( @as ( f64 , 69.69 ), r.dynamic_array[ 2 ]); std.testing.expectEqualSlices( u8 , r.complex.nested, "zig" ); std.testing.expectEqualSlices( u8 , "zig" , r.veryComplex[ 0 ].foo); std.testing.expectEqualSlices( u8 , "rocks" , r.veryComplex[ 1 ].foo); std.testing.expectEqual(T.Union{ .float = 100000 }, r.a_union); }

$ zig test json_parse_struct.zig 1/1 test "parse into struct with misc fields"...OK All 1 tests passed.

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

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

freestanding ananas cloudabi dragonfly freebsd fuchsia ios kfreebsd linux lv2 macosx netbsd openbsd solaris windows haiku minix rtems nacl cnk aix cuda nvcl amdhsa ps4 elfiamcu tvos watchos mesa3d contiki amdpal hermit hurd wasi emscripten zen uefi

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

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

Zig 0.6.0 does two things:

Add an other tag to std.Target.Os

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

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

pub const os = @import ( "my_hobby_os_package" ); pub fn main () void { }

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

pub fn read (fd: fd_t, buf: [] u8 ) ReadError! usize { if (builtin.os == .windows) { return windows.ReadFile(fd, buf); } if (builtin.os == .wasi and !builtin.link_libc) { const iovs = [ 1 ]iovec{iovec{ .iov_base = buf.ptr, .iov_len = buf.len, }}; var nread: usize = undefined ; switch (wasi.fd_read(fd, &iovs, iovs.len, &nread)) { 0 => return nread, else => |err| return unexpectedErrno(err), } } while ( true ) { const rc = system.read(fd, buf.ptr, buf.len); switch (errno(rc)) { 0 => return @intCast ( usize , rc), EINTR => continue , EINVAL => unreachable , EFAULT => unreachable , EAGAIN => if (std.event.Loop.instance) |loop| { loop.waitUntilFdReadable(fd); continue ; } else { return error .WouldBlock; }, EBADF => unreachable , EIO => return error .InputOutput, EISDIR => return error .IsDir, ENOBUFS => return error .SystemResources, ENOMEM => return error .SystemResources, ECONNRESET => return error .ConnectionResetByPeer, else => |err| return unexpectedErrno(err), } } return index; }

Where system referred to:

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

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

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

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

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

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

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

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

Thanks Christine Dodrill and Noam Preil for related contributions.

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

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

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

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

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

Additionally:

There is now a function appendNTimes (#4460).

(#4460). Many functions are deprecated, such as toSlice and toSliceConst , and at . Instead, access the items slice directly.

and , and . Instead, access the slice directly. Added outStream - creates a stream to append to the ArrayList.

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

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

std.mem.len no longer takes a type parameter, and uses Sentinel-Terminated Pointers.

no longer takes a type parameter, and uses Sentinel-Terminated Pointers. Added std.mem.span .

. Added std.mem.spanZ .

. Added std.mem.zeroes which zero-initializes types which have well-defined memory layouts. (#4544)

which zero-initializes types which have well-defined memory layouts. (#4544) Added std.mem.Allocator.allocSentinel .

. Added std.mem.indexOfSentinel .

. Rename std.mem.separate to std.mem.split .

to . Rename std.mem.Compare to std.math.Order .

to . Rename std.mem.compare to std.mem.order .

to . Added std.math.order .

. Deprecated std.mem.toSlice .

. Deprecated std.mem.toSliceConst .

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

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

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

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

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

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

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

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

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

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

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

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

Always allocate an info block per-thread so that libc can store important stuff there.

Respect ABI-mandated alignment in more places.

Nicer code, use slices/pointers instead of raw addresses whenever possible.

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

Language reference updated to take into account Language Changes.

Language reference makes it more obvious that if is an expression.

is an expression. New section: Function Parameter Type Inference

Clarify allowzero interaction with optional pointers.

interaction with optional pointers. Language reference has table of contents in a separate column on large displays.

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

This feature is still experimental.

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

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

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

The new -femit-docs CLI option outputs:

doc/index.html

doc/data.js

doc/main.js

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

There is now experimental std lib documentation.

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

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

Felix Queißner added documentation for std.fmt.format grammar and customization. (#3474)

Nathan Michaels added docs for ArrayList, failing_allocator, and Allocator. (#3540)

Nathan Michaels documented std.Mutex.

Nathan Michaels documented PriorityQueue.

Josh Wolfe added documentation for mutable HashMap KV pointers.

Felix (xq) Queißner added documentation to std.atomic.Queue .

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

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

bad_resume.zig

const std = @import ( "std" ); fn foo () void { var f = async bar( @frame ()); std.process.exit( 0 ); } fn bar (frame: anyframe ) void { suspend { resume frame; } std.process.exit( 0 ); } pub fn main () void { _ = async foo(); }

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

Additionally the following safety checks have been added:

Slicing operator with a sentinel mismatch.

Shifting by an amount greater than the type size (for non-power-of-two integers).

@intToPtr with misaligned address.

with misaligned address. Slicing a null C pointer.

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

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

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

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

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

Additionally the following improvements were made:

Bumped default max exec output size to 400 KB. (#3415)

addIncludeDir does -I instead of -isystem .

does instead of . Initial support for using vcpkg libraries. However it is not integrated automatically yet.

Fixed failure to recognize the PATH environment variable due to incorrectly treating the environment variable as case sensitive on Windows.

Rework and improve some of the zig build steps RunStep gains ability to compare output and exit code against expected values. Multiple redundant locations in the test harness code are replaced to use RunStep. WriteFileStep gains ability to write more than one file into the cache directory, for when the files need to be relative to each other. This makes usage of WriteFileStep no longer problematic when parallelizing zig build. Added CheckFileStep, which can be used to validate that the output of another step produced a valid file. Multiple redundant locations in the test harness code are replaced to use CheckFileStep. Added TranslateCStep. This exposes zig translate-c to the build system, which is likely to be rarely useful by most Zig users; however Zig's own test suite uses it both for translate-c tests and for run-translated-c tests. Refactored ad-hoc code to handle source files coming from multiple kinds of sources, into std.build.FileSource. Added std.build.Builder.addExecutableFromWriteFileStep. Added std.build.Builder.addExecutableSource. Added std.build.Builder.addWriteFiles. Added std.build.Builder.addTranslateC. Added std.build.LibExeObjStep.addCSourceFileSource. Added std.build.LibExeObjStep.addAssemblyFileFromWriteFileStep. Added std.build.LibExeObjStep.addAssemblyFileSource.

-- can be used to pass args to zig build commands.

can be used to pass args to zig build commands. -D now supports "list" type options.

now supports "list" type options. Nesting package dependencies is now supported.

InstallRawStep is available to do a similar job to objcopy. It can be used with exe.installRaw( "kernel.bin" ); where exe is a LibExeObjStep. (#2826)

where exe is a LibExeObjStep. (#2826) zig build now correctly handles multiple output artifacts (#4733, #4735)

Previously the zig build system incorrectly assumed that the only build artifact was a binary. Now, when you enable the cache, only the output dir is printed to stdout, and the zig build system iterates over the files in that directory, copying them to the output directory.

Previously the zig build system incorrectly assumed that the only build artifact was a binary. Now, when you enable the cache, only the output dir is printed to stdout, and the zig build system iterates over the files in that directory, copying them to the output directory. -ffunction-sections switch exposed to zig build scripts.

The default stdin behavior of RunStep is now .Inherit. Since this is a breaking change, previous behavior can be restored by doing: RunStep.stdin_behavior = .Ignore .

. Configuring the subsystem is exposed to build scripts.

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