Zig is an open-source programming language designed forrobustness,optimality, andclarity. Zig is aggressively pursuing its goal of overthrowing C as the de facto language for system programming. Zig intends to be so practical that people find themselves using it even if they dislike it.
This is amassive release, featuring 6 months of work and changes from36 different contributors.
I tried to give credit where credit is due, but it's inevitable I missed some contributions as I had to go through 1,345 commits to type up these release notes. I apologize in advance for any mistakes.
Special thanks to mysponsors who provide financial support. You're making Zig sustainable.
Zig uses LLVM's debug info API to emit native debugging information on all targets. This means that you can use native debugging tools on Zig code, for example:
In addition, Zig's standard library can read its own native debug information. This means that crashes produce stack traces, and errors produceError Return Traces.

This implementation is able to look at the executable's own memory to find out where the .o files are, which have the DWARF info.

Thanks toSahnvour for implementing the PE parsing and starting the effort to PDB parsing. I picked up where he left off and finished Windows stack traces.
Thanks to Zachary Turner from the LLVM project for helping me understand the PDB format. I still owe LLVM some PDB documentation patches in return.
Similar to MacOS, a Windows executable in memory has location information pointing to a .pdb file which contains debug information.

Linux stack traces worked in 0.2.0. However nowstd.debug.dumpStackTrace & friends useArenaAllocator backed byDirectAllocator. This has the downside of failing to print a stack trace when the system is out of memory, but for the more common use case when the system is not out of memory, but the debug info cannot fit instd.debug.global_allocator, now stack traces will work. This is the case for the self hosted compiler. There isa proposal tommap() debug info rather than usingread().
See alsoCompatibility with Valgrind.
Thanks to Jimmi Holst Christensen's diligent work, the Zig standard library now supports parsing Zig code. This API is used to implementzig fmt, a tool that reformats code to fit the canonical style.
As an example,zig fmt will change this code:
test"fmt" {const a = []u8{1,2,//3,4,// foo5,6,7 };switch (0) {0 => {},1 =>unreachable,2,3 => {},4...7 => {},1 +4 *3 +22 => {},else => {const a =1;const b = a; }, } foo(a, b, c, d, e, f, g,);}...into this code:
test"fmt" {const a = []u8{1,2,3,4,// foo5,6,7, };switch (0) {0 => {},1 =>unreachable,2,3 => {},4...7 => {},1 +4 *3 +22 => {},else => {const a =1;const b = a; }, } foo( a, b, c, d, e, f, g, );}It does not make any decisions about line widths. That is left up to the user. However, it follows certain cues about when to line break. For example, it will put the same number of array items in a line as there are in the first one. And it will put a function call all on one line if there is no trailing comma, but break every parameter into its own line if there is a trailing comma.
Thanks to Marc Tiehuis, there are currently two editor plugins that integrate withzig fmt:
zig fmt is only implemented in the self-hosted compiler, which isnot finished yet. To use it, one must follow the README instructions to build the self-hosted compiler from source.
Theimplementation of the self-hosted parser is an interesting case study of avoiding recursion by using an explicit stack. It is essentially a hand-written recursive descent parser, but with heap allocations instead of recursion. When Jimmi originally implemented the code, we thought that we could not solve the unbounded stack growth problem of recursion. However, since then, I prototyped several solutions that provide the ability to have recursive function calls without giving up statically known upper bound stack growth. SeeRecursion Status for more details.
Automatic formatting can be disabled in source files with a comment like this:
// zig fmt: offtest"this is left alone" { }// zig fmt: onzig fmt is written using the standard library's event-based I/O abstractions andasync/await syntax, which means that it is multi-threaded with non-blocking I/O. A debug build ofzig fmt on my laptop formats the entire Zig standard library in 2.1 seconds, which is 75,516 lines per second. SeeConcurrency Status for more details.
zig run file.zig can now be used to execute a file directly.
Thanks to Marc Tiehuis for the initial implementation of this feature. Marc writes:
On a POSIX system, a shebang can be used to run a zig file directly. An example shebang would be#!/usr/bin/zig run. You may not be able pass extra compile arguments currently as part of the shebang. Linux for example treats all arguments after the first as a single argument which will result in an 'invalid command'.
Note: there isa proposal to change this tozig file.zig to match the interface of other languages, as well as enable the common pattern#!/usr/bin/env zig.
Zig caches the binary generated byzig run so that subsequent invocations have low startup cost. SeeBuild Artifact Caching for more details.
Zig now supports building statically againstmusl libc.
On every master branch push, the continuous integration server creates a static Linux build of zig and updates the URLhttps://ziglang.org/builds/zig-linux-x86_64-master.tar.xz to redirect to it.
In addition, Zig now looks for libc and the Zig standard library at runtime. This makes static builds the easiest and most reliable way to start using the latest version of Zig immediately.
Windows has automated static builds of master branchvia AppVeyor.
MacOS static CI builds arein progress and should be available soon.
During this release cycle, two design flaws were fixed, which led to a chain reaction of changes that I called Pointer Reform, resulting in a more consistent syntax with simpler semantics.
The first design flaw was that the syntax for pointers was ambiguous if the pointed to type was atype. Consider this 0.2.0 code:
const assert =@import("std").debug.assert;comptime {var a:i32 =1;const b = &a;@compileLog(@typeOf(b)); *b =2; assert(a ==2);}This works fine. The value printed from the@compileLog statement is&i32. This makes sense becauseb is a pointer toa.
Now let's do it with atype:
const assert =@import("std").debug.assert;comptime {var a:type =i32;const b = &a;@compileLog(b); *b =f32; assert(a ==f32);}$ zig build-obj test.zig | &i32test.zig:6:5:error:found compile log statement @compileLog(b);^test.zig:7:5:error:attempt to dereference non-pointer type 'type' *b = f32;^It doesn't work in 0.2.0, because the& operator worked differently fortype than other types. Here,b is the type&i32 instead of a pointer to a type which is how we wanted to use it.
This prevented other things from working too; for example if you had a[]type{i32,u8,f64} and you tried to use a for loop,it crashed the compiler because internally a for loop uses the&operator on the array element.
The only reasonable solution to this is to have different syntax for the address-of operator and the pointer operator, rather than them both being&.
So pointer syntax becomes*T, matching syntax from most other languages such as C. Address-of syntax remains&foo, again matching common address-of syntax such as in C. This leaves one problem though.
With this modification, the syntax*foo becomes ambiguous with the syntax for dereferencing. And so dereferencing syntax is changed to a postfix operator:foo.*. This matches post-fix indexing syntax:foo[0], and in practice ends up harmonizing nicely with other postfix operators.
The other design flaw is a problem that has plagued C since its creation: the pointer type doesn't tell you how many items there are at the address. This is now fixed by having two kinds of pointers in Zig:
*T - pointer to exactly one item.ptr.*[*]T - pointer to unknown number of items.ptr[i]ptr[start..end]T must have a known size, which means that it cannot bec_void or any other@OpaqueType().Note that this causes pointers to arrays to fall into place, as a single-item pointer to an array acts as a pointer to a compile-time known number of items:
*[N]T - pointer to N items, same as single-item pointer to array.array_ptr[i]array_ptr[start..end]array_ptr.lenConsider how slices fit into this picture:
[]T - pointer to runtime-known number of items.slice[i]slice[start..end]slice.lenThis makes Zig pointers significantly less error prone. For example, it fixed issue#386, which demonstrates how a pointer to an array in Zig 0.2.0 is a footgun when passed as a parameter. Meanwhile in 0.3.0, equivalent code is nearly impossible to get wrong.
For consistency with the postfix pointer dereference operator, optional unwrapping syntax is now postfix as well:
0.2.0:??x
0.3.0:x.?
And finally, to remove the last inconsistency of optional syntax, the?? operator is now the keywordorelse. This means that Zig now has the property thatall control flow occurs exclusively via keywords.
There isa plan for one more pointer type, which is a pointer that has a null-terminated number of items. This would be the type of the parameter tostrlen for example. Although this will make the language bigger by adding a new type, it allows Zig to delete a feature in exchange, since it will makeC string literals unnecessary. String literals will both have a compile-time known length and be null-terminated; therefore they willimplicitly cast to slices as well as null-terminated pointers.
There is one new issue caused by Pointer Reform. Because C does not have the concept of single-item pointers or unknown-length pointers (or non-null pointers), Zig must translate all C pointers as?[*]T. That is, a pointer to an unknown number of items that might be null. This can cause some friction when using C APIs, which is unfortunate because Zig's types are perfectly compatible with C's types, but .h files are unable to adequately describe pointers. Although it would be much safer to translate .h files offline and fix their prototypes, there isa proposal to add a C pointer type. This new pointer type should never be used on purpose, but would be used when auto-translating C code. It would simply have C pointer semantics, which means it would be just as much of a footgun as C pointers are. The upside is that it would make interaction with C APIs go back to being perfectly seamless.
In response to an overwhelming consensus, floating point operations use Strict mode by default. Code can use@setFloatMode to override the mode on a per-scope basis.
Thanks to Marc Tiehuis for implementing the change.
this was always a weird language feature. An identifier which referred to the thing in the most immediate scope, which could be a module, a type, a function, or even a block of code.
The main use case for it was for anonymous structs to refer to themselves. This use case is solved with a new builtin function,@This(), which always returns the innermost struct or union that the builtin call is inside.
The "block of code" type is removed from Zig, and the first argument of@setFloatMode is removed.@setFloatMode now always refers to the current scope.
Previously, these two lines would have different meanings:
exportfnfoo(x:u32)void {const a:u8 = x;const b =u8(x);}The assignment toa would giveerror: expected type 'u8', found 'u32', because not all values ofu32 can fit in au8. But the assignment tob was "cast harder" syntax, and Zig would truncate bits, with a safety check to ensure that the mathematical meaning of the integer was preserved.
Now, both lines are identical in semantics. There is no more "cast harder" syntax. Both cause the compile error because implicit casts are only allowed when it is completely unambiguous how to get from one type to another, and the transformation is guaranteed to be safe. For other casts, Zig has builtin functions:
Some are safe; some are not. Some perform language-level assertions; some do not. Some are no-ops at runtime; some are not. Each casting function is documented independently.
Having explicit and fine-grained casting like this is a form of intentional redundancy. Casts are often the source of bugs, and therefore it is worth double-checking a cast to verify that it is still correct when the type of the operand changes. For example, imagine that we have the following code:
fnfoo(x:i32)void {var i =@intCast(usize, x);} Now consider what happens when the type ofx changes to a pointer:
test.zig:2:29:error:expected integer type, found '*i32' var i = @intCast(usize, x);^ Although we technically know how to convert a pointer to an integer, because we used@intCast, we are forced to inspect the cast and change it appropriately. Perhaps that means changing it to@ptrToInt, or perhaps the entire function needs to be reworked in response to the type change. Previously, it was illegal to pass structs and unions by value in non-extern functions. Instead, one would have to have the function accept aconst pointer parameter. This was to avoid the ambiguity that C programs face - having to make the decision about whether by-value or by-reference was better. However, there were some problems with this. For example, when the parameter type is inferred, Zig would automatically convert to aconst pointer. This caused problems in generic code, which could not distinguish between a type which is a pointer, and a type which has been automatically converted to a pointer.
Now, parameters can be passed directly:
const assert =@import("std").debug.assert;const Foo =struct { x:i32, y:i32,};fncallee(foo: Foo)void { assert(foo.y ==2);}test"pass directly" { callee(Foo{ .x =1, .y =2 });}I have avoided using the term "by-value" because the semantics of this kind of parameter passing are different:
Because of these semantics, there's a clear flow chart for whether to accept a parameter asT or*const T:
T, unless one of the following is true:Now that we have this kind of parameter passing, Zig's implicit cast fromT to*const T is less important. One might even make the case that such a cast is dangerous. Therefore we havea proposal to remove it.
There is one more area that needs consideration with regards to direct parameter passing, and that is with coroutines. The problem is that if a reference to a stack variable is passed to a coroutine, it may become invalid after the coroutine suspends. This is adesign flaw in Zig that will be addressed in a future version. SeeConcurrency Status for more details.
Note thatextern functions are bound by the C ABI, and therefore none of this applies to them.
Marc Tiehuis writes:
We now use a generic Rand structure which abstracts the core functions from the backing engine.
The old Mersenne Twister engine is removed and replaced instead with three alternatives:
These should provide sufficient coverage for most purposes, including a CSPRNG using Isaac64. Consumers of the library that do not care about the actual engine implementation should useDefaultPrng andDefaultCsprng.
One of the problems with non-blocking programming is that stack traces and exceptions are less useful, because the actual stack trace points back to the event loop code.
In Zig 0.3.0,Error Return Traces work across suspend points. This means you can usetry as the main error handling strategy, and when an error bubbles up all the way, you'll still be able to find out where it came from:
const std =@import("std");const event = std.event;const fs = event.fs;test"unwrap error in async fn" {var da = std.heap.DirectAllocator.init();defer da.deinit();const allocator = &da.allocator;var loop: event.Loop =undefined;try loop.initMultiThreaded(allocator);defer loop.deinit();const handle =tryasync<allocator> openTheFile(&loop);defercancel handle; loop.run();}asyncfnopenTheFile(loop: *event.Loop)void {const future = (async fs.openRead(loop,"does_not_exist.txt")catchunreachable);const fd = (await future)catchunreachable;}$ zig test test.zigTest 1/1 unwrap error in async fn...attempt to unwrap error: FileNotFoundstd/event/fs.zig:367:5:0x22cb15 in ??? (test) return req_node.data.msg.Open.result;^std/event/fs.zig:374:13:0x22e5fc in ??? (test) return await (async openPosix(loop, path, flags, os.File.default_mode) catch unreachable);^test.zig:22:31:0x22f34b in ??? (test) const fd = (await future) catch unreachable;^std/event/loop.zig:664:25:0x20c147 in ??? (test) resume handle;^std/event/loop.zig:543:23:0x206dee in ??? (test) self.workerRun();^test.zig:17:13:0x206178 in ??? (test) loop.run();^Tests failed. Use the following command to reproduce the failure:zig-cache/testNote that this output contains 3 components:
attempt to unwrap error: FileNotFoundfs.zig:367:5 and then returned atfs.zig:374:13. You could go look at those source locations for more information.openRead, the code tried tocatchunreachable which caused the panic. You can see that the stack trace does, in fact, go into the event loop as described above.It is important to note in this example, that the error return trace survived despite the fact that the event loop is multi-threaded, and any one of those threads could be the worker thread that resumes an async function at theawait point.
This feature is enabled by default for Debug and ReleaseSafe builds, and disabled for ReleaseFast and ReleaseSmall builds.
This is just the beginning of an exploration of what debugging non-blocking behavior could look like in the future of Zig. SeeConcurrency Status for more details.
Instead ofasync(allocator) call(), now it isasync<allocator> call().
This fixes syntax ambiguity when leaving off the allocator, and fixes parse failure when call is a field access.
This sets a precedent for using<> to pass arguments to a keyword. This will affectenum,union,fn, andalign (see#661).
Alexandros Naskos contributed anew build mode.
$ zig build-exe example.zig --release-smallAlexandros Naskos bravely dove head-first into the deepest, darkest parts of the Zig compiler and implemented an incredibly useful builtin function:@typeInfo.
This function accepts atype as a parameter, and returns a compile-time known value of this type:
pubconst TypeInfo =union(TypeId) { Type:void, Void:void, Bool:void, NoReturn:void, Int: Int, Float: Float, Pointer: Pointer, Array: Array, Struct: Struct, ComptimeFloat:void, ComptimeInt:void, Undefined:void, Null:void, Optional: Optional, ErrorUnion: ErrorUnion, ErrorSet: ErrorSet, Enum: Enum, Union: Union, Fn: Fn, Namespace:void, BoundFn: Fn, ArgTuple:void, Opaque:void, Promise: Promise,pubconst Int =struct { is_signed:bool, bits:u8, };pubconst Float =struct { bits:u8, };pubconst Pointer =struct { size: Size, is_const:bool, is_volatile:bool, alignment:u32, child:type,pubconst Size =enum { One, Many, Slice, }; };pubconst Array =struct { len:usize, child:type, };pubconst ContainerLayout =enum { Auto, Extern, Packed, };pubconst StructField =struct { name: []constu8, offset: ?usize, field_type:type, };pubconst Struct =struct { layout: ContainerLayout, fields: []StructField, defs: []Definition, };pubconst Optional =struct { child:type, };pubconst ErrorUnion =struct { error_set:type, payload:type, };pubconst Error =struct { name: []constu8, value:usize, };pubconst ErrorSet =struct { errors: []Error, };pubconst EnumField =struct { name: []constu8, value:usize, };pubconst Enum =struct { layout: ContainerLayout, tag_type:type, fields: []EnumField, defs: []Definition, };pubconst UnionField =struct { name: []constu8, enum_field: ?EnumField, field_type:type, };pubconst Union =struct { layout: ContainerLayout, tag_type: ?type, fields: []UnionField, defs: []Definition, };pubconst CallingConvention =enum { Unspecified, C, Cold, Naked, Stdcall, Async, };pubconst FnArg =struct { is_generic:bool, is_noalias:bool, arg_type: ?type, };pubconst Fn =struct { calling_convention: CallingConvention, is_generic:bool, is_var_args:bool, return_type: ?type, async_allocator_type: ?type, args: []FnArg, };pubconst Promise =struct { child: ?type, };pubconst Definition =struct { name: []constu8, is_pub:bool, data: Data,pubconst Data =union(enum) { Type:type, Var:type, Fn: FnDef,pubconst FnDef =struct { fn_type:type, inline_type: Inline, calling_convention: CallingConvention, is_var_args:bool, is_extern:bool, is_export:bool, lib_name: ?[]constu8, return_type:type, arg_names: [][]constu8,pubconst Inline =enum { Auto, Always, Never, }; }; }; };};This kicks open the door for compile-time reflection, especially when combined with the fact that Jimmi Holst Christensen implemented@field, which performs field access with a compile-time known name:
const std =@import("std");const assert = std.debug.assert;test"@field" {const Foo =struct { one:i32, two:bool, };var f = Foo{ .one =42, .two =true, };const names = [][]constu8{"two","one" }; assert(@field(f, names[0]) ==true); assert(@field(f, names[1]) ==42);@field(f,"one") +=1; assert(@field(f,"on" ++"e") ==43);}This has the potential to be abused, and so the feature should be used carefully.
After Jimmi implemented@field, he improved the implementation of@typeInfo and fixed several bugs. And now, the combination of these builtins is used to implement struct printing in userland:
const std =@import("std");const Foo =struct { one:i32, two: *u64, three:bool,};pubfnmain()void {var x:u64 =1234;var f = Foo{ .one =42, .two = &x, .three =false, }; std.debug.warn("here it is: {}\n", f);}Output:
here it is: Foo{ .one = 42, .two = u64@7ffdda208cf0, .three = false }Seestd/fmt/index.zig:15 for the implementation.
Now that we have@typeInfo, there is one more question to answer: should there be a function which accepts aTypeInfo value, and makes a type out of it?
This hypothetical feature is called@reify, and it's ahot topic. Although undeniably powerful and useful, there is concern that it would betoo powerful, leading to complex meta-programming that goes against the spirit of simplicity that Zig stands for.
@cmpxchg is removed.@cmpxchgStrong and@cmpxchgWeak are added.
The functions have operand type as the first parameter.
The return type is?T whereT is the operand type.
Ben Noordhuis implementedf16. This is guaranteed to be IEEE-754-2008 binary16 format, even on systems that have no hardware support, thanks to the additions to compiler_rt that Ben contributed. He also added support forf16 tostd.math functions such asisnormal andfabs.
Zig 0.2.0 had primitive types for integer bit widths of 2-8, 16, 29, 32, 64, 128. Any number other than that, and you had to use@IntType to create the type. But you would get a compile error if you shadowed one of the above bit widths that already existed, for example with
constu29 =@IntType(false,29);// error: u29 shadows primitive typeNeedless to say, this situation was unnecessarily troublesome (#745). And so now arbitrary bit-width integers can be referenced by using an identifier ofi oru followed by digits. For example, the identifieri7 refers to a signed 7-bit integer.
u0 is a 0-bit type, which means:
@sizeOf(u0) ==0u0 as always the compile-time known value of0.i0doesn't make sense and will probably crash the compiler.
Although Zig defines arbitrary integer sizes to support all primitive operations, if you try to use, for example, multiplication on 256-bit integers:
test"large multiplication" {var x:u256 =0xabcd;var y:u256 =0xefef;var z = x * y;}Then you'll get an error like this:
LLVM ERROR: Unsupported library call operation!
Zig isn't supposed to be letting LLVM leak through here, but that's a separate issue. What's happening is that normally if a primitive operation such as multiplication of integers cannot be lowered to a machine instruction, LLVM will emit a library call to compiler_rt to perform the operation. This works for up to 128-bit multiplication, for example. However compiler_rt does not define an arbitrary precision multiplication library function, and so LLVM is not able to generate code.
Itis planned to submit a patch to LLVM which adds the ability to emit a lib call for situations like this, and then Zig will include the arbitrary precision multiplication function in Zig's compiler_rt.
In addition to this, Zig 0.3.0 fixesa bug where@IntType was silently wrapping the bit count parameter if it was greater than pow(2, 32).
Marc Tiehuis & Ben Noordhuis solved the various issues that preventedf128 from being generally useful.
f128. -Marc Tiehuis__floatunditf__floatunsitf__floatunsitf__floatuntitf__floatuntisf__trunctfdf2__trunctfsf2__floattitf__floattidf__floattisfZig now supports global build artifact caching. This feature is one of those things that you can generally ignore, because it "just works" without any babysitting.
By default, compilations are not cached. You can enable the global cache for a compilation by using--cache on:
andy@xps:~/tmp$ time zig build-exe hello.zig real0m0.414suser0m0.369ssys0m0.049sandy@xps:~/tmp$ time zig build-exe hello.zig --cache on/home/andy/.local/share/zig/stage1/artifact/hkGO0PyaOKDrdg2wyhV1vRy0ATyTaT0s0ECa2BiHFJfsb9RDVKK_r3qwHI5gaEfv/helloreal0m0.412suser0m0.377ssys0m0.038sandy@xps:~/tmp$ time zig build-exe hello.zig --cache on/home/andy/.local/share/zig/stage1/artifact/hkGO0PyaOKDrdg2wyhV1vRy0ATyTaT0s0ECa2BiHFJfsb9RDVKK_r3qwHI5gaEfv/helloreal0m0.012suser0m0.009ssys0m0.003sWhen the cache is on, the output is not written to the current directory. Instead, the output is kept in the cache directory, and the path to it is printed to stdout.
This is off by default, because this is an uncommon use case. The real benefit of build artifact caching comes in 3 places:
andy@xps:~/tmp$ time zig run hello.zig Hello, world!real0m0.553suser0m0.500ssys0m0.055sandy@xps:~/tmp$ time zig run hello.zig Hello, world!real0m0.013suser0m0.007ssys0m0.006scompiler_rt.o andbuiltin.o from source, for the given target. This usually only has to be done once ever, which is why other compilers such as gcc ship with these components already built. The problem with that strategy is that you have to build a special version of the compiler for cross-compiling. With Zig, you can always build for any target, on any target. The cache is perfect; there are no false positives. You could even fix a bug inmemcpy in the system's libc, and Zig will detect that its own code has (indirectly) been updated, and invalidate the cache entry.
If you usezig build-exe, Zig will still create azig-cache directory in the current working directory in order to store an intermediate.o file. This is because on MacOS, the intermediate .o file stores the debug information, and therefore it needs to stick around somewhere sensible forStack Traces to work.
Likewise, if you usezig test, Zig will put the test binary in thezig-cache directory in the current working directory. It's useful to leave the test binary here so that the programmer can use a debugger on it or otherwise inspect it.
Thezig-cache directoryis cleaner than before, however. For example, thebuiltin.zig file is no longer created there. It participates in the global caching system, just likecompiler_rt.o. You can usezig builtin to see the contents of@import("builtin").
I noticed that valgrind does not see Zig's debug symbols (#896):
pubfnmain()void { foo().* +=1;}fnfoo() *i32 {return@intToPtr(*i32,10000000);}==24133== Invalid read of size 4==24133== at 0x2226D5: ??? (in /home/andy/downloads/zig/build/test)==24133== by 0x2226A8: ??? (in /home/andy/downloads/zig/build/test)==24133== by 0x222654: ??? (in /home/andy/downloads/zig/build/test)==24133== by 0x2224B7: ??? (in /home/andy/downloads/zig/build/test)==24133== by 0x22236F: ??? (in /home/andy/downloads/zig/build/test)After digging around, I was able to reproduce the problem using only Clang and LLD:
static int *foo(void) { return (int *)10000000;}int main(void) { int *x = foo(); *x += 1;}If this C code is built with Clang and linked with LLD, Valgrind has the same issue as with the Zig code.
I senta message to the Valgrind mailing list, and theysuggested submitting a bug fix to Valgrind. That's a good idea. I'm a little busy with Zig development though - anybody else want to take a crack at it?
In the meantime, Zig now has a--no-rosegment flag, which works around the bug. It should only be used for this purpose; the flag will likely be removed once Valgrind fixes the issue upstream and enough time passes that the new version becomes generally available.
$ zig build-exe test.zig --no-rosegment$ valgrind ./test==24241== Invalid read of size 4==24241== at 0x221FE5: main (test.zig:2)Marc Tiehuis added Zig support, and then worked with theCompiler Explorer team to get it merged upstream and deployed.
The command line API that Compiler Explorer uses is covered by Zig's main test suite to ensure that it continues working as the language evolves.
zig init-lib can be used to initialize azig build project in the current directory which will create a simple library:
$ zig init-libCreated build.zigCreated src/main.zigNext, try `zig build --help` or `zig build test`$ zig build testTest 1/1 basic add functionality...OKAll tests passed.
Likewise,zig init-exe initializes a simple application:
$ zig init-exeCreated build.zigCreated src/main.zigNext, try `zig build --help` or `zig build run`$ zig build runAll your base are belong to us.
The main Zig test suite tests this functionality so that it will not regress as Zig continues to evolve.
Concurrency is now solved. That is, there is a concrete plan for how concurrency will work in Zig, and now it's a matter of implementing all the pieces.
First and foremost, Zig supports low-level control over hardware. That means that it has atomic primitives:
...and it means that you can directly spawn kernel threads using standard library functions:
const std =@import("std");const assert = std.debug.assert;const builtin =@import("builtin");const AtomicRmwOp = builtin.AtomicRmwOp;const AtomicOrder = builtin.AtomicOrder;test"spawn threads" {var shared_ctx:i32 =1;const thread1 =try std.os.spawnThread({}, start1);const thread2 =try std.os.spawnThread(&shared_ctx, start2);const thread3 =try std.os.spawnThread(&shared_ctx, start2);const thread4 =try std.os.spawnThread(&shared_ctx, start2); thread1.wait(); thread2.wait(); thread3.wait(); thread4.wait(); assert(shared_ctx ==4);}fnstart1(ctx:void)u8 {return0;}fnstart2(ctx: *i32)u8 { _ =@atomicRmw(i32, ctx, AtomicRmwOp.Add,1, AtomicOrder.SeqCst);return0;}On POSIX targets, when you link against libc, the standard library uses pthreads; otherwise it uses its own lightweight kernel thread implementation.
You can use mutexes, signals, condition variables, and all those things. Anything you can accomplish in C, you can accomplish in Zig.
However, the standard library provides a higher level concurrency abstraction, designed for optimal performance, debuggability, and structuring code to closely model the problems that concurrency presents.
The abstraction is built on two language features: stackless coroutines andasync/await syntax. Everything else is implemented in userland.
std.event.Loop creates a kernel thread pool matching the number of logical CPUs. It can then be used for non-blocking I/O that will be dispatched across the thread pool, using the platform-native API:
This is a competitor tolibuv, except multi-threaded.
Once you have an event loop, all of thestd.event API becomes available to use:
std.event.Channel - Many producer, many consumer, thread-safe, runtime configurable buffer size.When buffer is empty, consumers suspend and are resumed by producers. When buffer is full, producers suspend and are resumed by consumers.std.event.Future - A value that many consumers canawait.std.event.Group - A way toawait multipleasync operations.std.event.Lock - Ensures only one thread gets access to a resource, without blocking a kernel thread.std.event.RwLock - Same as Lock except allows multiple readers to access data simultaneously.std.event.fs - File system operations based onasync/await syntax.std.event.tcp - Network operations based onasync/await syntax. All of these abstractions provide convenient APIs based onasync/await syntax, making it practical for API users to model their code with maximally efficient concurrency. None of these abstractions block or use mutexes; when an API user must suspend, control flow goes to the next coroutine waiting to run, if any. If no coroutines are waiting to run, the application will sit idly, waiting for an event from the respective platform-native API (e.g. epoll on Linux).
As an example, here is a snippet from a test in the standard library:
asyncfntestFsWatch(loop: *Loop) !void {const file_path =try os.path.join(loop.allocator, test_tmp_dir,"file.txt");defer loop.allocator.free(file_path);const contents =\\line 1\\line 2 ;const line2_offset =7;// first just write then read the filetryawaittryasync fs.writeFile(loop, file_path, contents);const read_contents =tryawaittryasync fs.readFile(loop, file_path,1024 *1024); assert(mem.eql(u8, read_contents, contents));// now watch the filevar watch =try fs.Watch(void).create(loop,0);defer watch.destroy(); assert((tryawaittryasync watch.addFile(file_path, {})) ==null);const ev =tryasync watch.channel.get();var ev_consumed =false;deferif (!ev_consumed)cancel ev;// overwrite line 2const fd =tryawaittryasync fs.openReadWrite(loop, file_path, os.File.default_mode); {defer os.close(fd);tryawaittryasync fs.pwritev(loop, fd, [][]constu8{"lorem ipsum"}, line2_offset); } ev_consumed =true;switch ((tryawait ev).id) { WatchEventId.CloseWrite => {}, WatchEventId.Delete =>@panic("wrong event"), }const contents_updated =tryawaittryasync fs.readFile(loop, file_path,1024 *1024); assert(mem.eql(u8, contents_updated,\\line 1\\lorem ipsum ));}You can see that even though Zig is a language with manual memory management that insists on handling every possible error, it manages to be quite high level using these event-based APIs.
Now, there are some problems to solve:
async functions because LLVM hasa bug where Mem2Reg turns correct coroutine frame spills back into incorrect parameter references.And so, the plan is to rework coroutines, without using any of LLVM's coroutines API. Zig will implement coroutines in the frontend, and LLVM will see only functions and structs. This ishow Rust does it, and I think it was a strong choice.
The coroutine frame will be in a struct, and so Zig will know the size of it at compile-time, and it will solve the problem of guaranteeing allocation elision - theasync callsite will simply have to provide the coroutine frame pointer in order to create the promise.
This will also be relevant for recursion; stackless function calls do not count against the static stack size upper bound calculation. SeeRecursion Status for more details.
The self-hosted compiler is well underway.Here's a 1 minute demo of the self-hosted compiler watching source files and rebuilding.
The self-hosted compiler cannot do much more than Hello World at the moment, but it's being constructed from the ground up to fully take advantage of multiple cores and in-memory caching. In addition, Zig's error system and other safety features are making it easy to write reliable, robust code. Between stack traces, error return traces, and runtime safety checks, I barely even need a debugger.
Marc Tiehuis contributed aBig Integer library, which the self-hosted compiler is using for integer literals and compile-time math operations.
Writing the self-hosted compiler code revealed to me how coroutines should work in Zig. All the little details and ergonomics are clear to me now. And so before I continue any further on the self-hosted compiler, I will use this knowledge to rework coroutines and solve the problems with them. SeeConcurrency Status for more details.
As a reminder, even when the self-hosted compiler is complete, Zig will forever be stuck with the stage1 C++ compiler code. SeeThe Grand Bootstrapping Plan for more details.
The self-hosted compiler is successfully sharing some C++ code with the stage1 compiler. For example the libLLVM C++ API wrapper is built into a static library, which then exports a C API wrapper. The self-hosted compiler links against this static library in order to make libLLVM C++ API calls via the C API wrapper. In addition, the Microsoft Visual Studio detection code requires the Windows COM API, which is also C++, and so a similar strategy is used. I think it's pretty neat that the build system builds a static library once and then ends up linking against it twice - one for each of the two compiler stages!
I'vesaid before that recursion is one of the enemies of perfect software, because it represents a way that a program can fail with no foolproof way of preventing it. With recursion, pick any stack size and I'll give you an input that will crash your program. Embedded developers are all too familiar with this problem.
It's always possible to rewrite code using an explicit stack using heap allocations, andthat's exactly what Jimmi did in the self-hosted parser.
On the other hand, when recursion fits the problem, it's significantly more clear and maintainable. It would be a real shame to have to give it up.
I researched different ways that Zig could keep recursion, even when we introducestatically known stack upper bound size. I came up with a proof of concept for@newStackCall, a builtin function that calls a function using an explicitly provided new stack. You can find a usage example in the documentation by following that link.
This works, and it does break call graph cycles, but it would be a little bit awkward to use. Because if you allocate an entire new stack, it has to be big enough for the rest of the stack upper bound size, but in a recursive call, which should be only one stack frame, it would overallocate every time.
So that's why I think that the actual solution to this problem is Zig's stackless coroutines. Because Zig's coroutines are stackless, they are the perfect solution for recursion (direct or indirect). With the reworking of coroutines, it will be possible to put the coroutine frame of an async function anywhere - in a struct, in the stack, in a global variable - as long as it outlives the duration of the coroutine. SeeConcurrency for more details.
Although recursion is not yet solved, we know enough to know that recursion is OK to use in Zig. It does suffer from the stack overflow issue today, but in the future we will have a compile error to prevent call graph cycles. And then this hypothetical compile error will be solved by using@newStackCall or stackless functions (but probably stackless functions). Once recursion is solved, if stackless functions turn out to be the better solution, Zig will remove@newStackCall from the language, unless someone demonstrates a compelling use case for it.
For now, use recursion whenever you want; you'll know when it's time to update your code.
The pieces for web assembly are starting to come together.
Ben Noordhuis fixed support for--target-arch wasm32 (#1094).
LLVMmerged my patch to make WebAssembly a normal (non-experimental) target. But they didn't do it before the LLVM 7 release. So Zig 0.3.0 will not have WebAssembly support by default, but 0.4.0 will.
That being said, the static builds of Zig provided by ziglang.org have the WebAssembly target enabled.
Apart from this, there appears to bean issue with Zig's WebAssembly linker. Once this is solved, all that is left is to use WebAssembly in real life use cases, to work out the ergonomics, and solve the inevitable issues that arise.
The language reference documentation now contains no JavaScript. The code blocks are pre-formatted withstd.zig.Tokenizer. The same is true for these release notes.
Thebuiltin.zig example code in the documentation is now automatically updated from the output of Zig, so the docs can't get out of date for this.
In addition to the above, the following improvements were made to the documentation:
std.mem.Allocator functions.std.mem.SplitIterator is now publicstd.math.atan2 is now publicstd.os.linux now makes public all the syscall numbers and syscall functionsstd.math.cast handles signed integersstd.zig.parsestd.zig.parseStringLiteralstd.zig.renderstd.zig.aststd.zig.Tokenstd.zig.Tokenizerstd.io.readLineFile.exists withFile.access. -Marc Tiehuisstd.rand.Rand tostd.rand.Randomstd.os.time.sleepstd.os.time.posixSleepstd.os.time.timestampstd.os.time.miliTimestampstd.os.time.Timerstd.math.complex.Complexstd.math.complex.absstd.math.complex.acosstd.math.complex.acoshstd.math.complex.argstd.math.complex.asinstd.math.complex.asinhstd.math.complex.atanstd.math.complex.atanhstd.math.complex.conjstd.math.complex.cosstd.math.complex.coshstd.math.complex.expstd.math.complex.logstd.math.complex.powstd.math.complex.projstd.math.complex.sinhstd.math.complex.sinstd.math.complex.sqrtstd.math.complex.tanhstd.math.complex.tanstd.math.complex.ldexp_cexpstd.mem.trimLeftstd.mem.trimRightstd.mem.trimRightstd.mem.lastIndexOfScalarstd.mem.lastIndexOfAnystd.mem.lastIndexOfstd.mem.endsWithstd.atomic.Stackstd.atomic.Queuestd.os.spawnThread. It works on all targets. On Linux, when linking libc, it uses pthreads, and when not linking libc, it makes syscalls directly.std.json.Tokenstd.json.StreamingParserstd.json.TokenStreamstd.json.validatestd.json.ValueTreestd.json.ObjectMapstd.json.Valuestd.json.Parser - A non-stream JSON parser which constructs a tree of Value's.std.SegmentedListstd.Buffer. Instead users should usestd.io.BufferOutStream.std.Buffer.appendFormatstd.Buffer.appendBytestd.Buffer.appendByteNTimesstd.math.big.Intstd.math.big.Limbstd.math.big.DoubleLimbstd.math.big.Log2Limbstd.os.Dir gains Windows support.std.os.File.access no longer depends on shlwapi.dll on Windows.std.os.path.dirname returns null instead of empty slice when there is no directory component. This makes it harder to write bugs. (#1017)error.IsDir.std.math.floatMantissaBits andstd.math.floatExponentBits -Marc Tiehuisstd.mem.Allocator allows allocation of any 0 sized type, not justvoid. -Jimmi Holst Christensen.std.os.cpuCountstd.sort.asc andstd.sort.desc -Marc Tiehuisstd.fmt.format add* for formatting things as pointers. (#1285)std.fmt.format add integer binary output format. -Marc Tiehuis (#1313)std.mem.secureZero. -Marc Tiehuismem.set(u8, slice, 0) except that it will never be optimized out by the compiler. Intended usage is for clearing secret data.@intToPtr,@ptrToInt to get around known data dependencies but I could not work it out right now.std.fmt.format handles non-pointer struct/union/enums. Adds support for printing structs via reflection. (#1380)std.os file functions no longer require an allocator. They rely onPATH_MAX, because even Windows, Linux, and MacOS syscalls will fail for paths longer thanPATH_MAX.std.crypto.chaCha20IETF andstd.crypto.chaCha20With64BitNonce. -Shawn Landden & Marc TiehuisZig: Poly1305: 1423 MiB/s X25519: 8671 exchanges per secondMonocypher: Poly1305: 1567 MiB/s X25519: 10539 exchanges per secondThere is room for improvement and no real effort has been made at all in optimization beyond a direct translation.
std.os.windows.CryptAcquireContextAstd.os.windows.CryptReleaseContextstd.os.windows.CryptGenRandomstd.ArrayList andstd.HashMap APIs regarding iteration. (#981)std.fmt.format.std.Buffer.replaceContents method (#1065)std.Buffer.fromOwnedSlice. (#1082)std.ArrayList.setOrError so you can set a value without growing the underlying buffer, with range safety checks.{s}. (#1092)zig version compliant with SemVer with regards to the git revision metadata.-1.std.ArrayList.insert and added tests. (#1232)std.ArrayList.swapRemove. (#1230)std.ArrayList.swapRemoveOrError. (#1254)std.os.posix constants.error.SkipZigTeststd.io.PeekStream andstd.io.Slicestream.*[N]T to?[*]T (#1398)std.fmt.format (#1432)*T and[*]T to?*c_void@floatToInt@byteOffsetOf and@bitOffsetOf.clock_gettime uses the VDSO optimization, even for static builds.f32 values. Various changes to better match libc.std.os.time module. -Marc Tiehuisconst,const. -Jimmi Holst Christensenstd.fmt.format supports{B} for human readable bytes using SI prefixes.comptime_int andcomptime_float. -Jimmi Holst Christensenarray.len through a pointer. -Jimmi Holst Christensen*T -> ?*T cast is allowed implicitly, even when it occurs deep inside the type, and the cast is a no-op at runtime.__extenddftf2 and__extendsftf2 to zig's compiler-rt.--emit intest command. -Ben Noordhuis (#1175)builder.addBuildOption API. -Josh Wolfe-Dskip-release option to test faster. -Andrew Kelley & Jimmi Holst Christensen== for comparing optional pointers.#658--enable-timing-info to-ftime-report to match clang, and have it print llvm's internal timing info.promise->T syntax not parsed#857memmove to builtin.o. LLVM occasionally generates a dependency on this function.std.BufMap logic. -Ben Noordhuis.ReturnType and@ArgType on unresolved types. -Jimmi Holst Christensen (#846)type. -Jimmi Holst Christensentype, then the struct is marked as requiring comptime as well. Same goes for unions.type.@tagName crashing sometimes. (#1118)@ptrToInt of a*void (#1192)@setEvalBranchQuota not respected in generic fn calls.#1257@typeInfo unable to distinguish compile error vs no-payload (#1421,#1426)@ptrCast to*void. (#960)@intToPtr of function@ptrCast 0 bit type to non-0 bit type@intCast tou0Buf * directly rather than an array of ConstExprValues for the elements, and then similar to array of undefined, it is expanded into the canonical form when necessary. However many operations can happen directly on theBuf *, which is faster.std.fmt.formatInt to handle upcasting to base int size*c_void (#1588)Zig hasknown bugs.
The first release that will ship with no known bugs will be 1.0.0.
Special thanks to those whodonate monthly. We're now at $1,349 of the $3,000 goal. I hope this release helps to show how much time I've been able to dedicate to the project thanks to your support.
