Zig's 0.14 release inches the project toward stability
The Zig project has announced the release of the 0.14 version of the language, including changes from more than 250 contributors. Zig is a low-level, memory-unsafe programming language that aims to compete with C instead of depending on it. Even though the language has not yet had a stable release, there are a number of projects using it as an alternative to C with better metaprogramming. While the project's release schedule has been a bit inconsistent, with the release of version 0.14 being delayed several times, the release contains a number of new convenience features, broader architecture support, and the next steps toward removing Zig's dependency on LLVM.
More targets
Zig tracks the different architectures it supports using a tier system to describe different levels of support. This release looks as though it demotes all tier-1 targets except for x86_64 on Linux to tier-2, but that's actually an artifact of changing how the project defines its tiers. tier-1 targets used to be the ones with working code generation, a fully implemented standard library, regular continuous-integration testing, all language features known to work, and a few other conditions. The 0.14 release adds a requirement that the compiler can generate machine code for the target without relying on LLVM for it to be considered tier-1.
Zig has been trying to drop its dependency on LLVM for some time. The first step of that process was switching to the self-hosting compiler; once that was complete, the project started focusing on adding native code generation. This release includes a mostly-working x86_64 code generator, although LLVM remains the default. If it proves stable, the Zig code generator may become the default for debug builds (where performance impacts from a less-mature optimizer are less important) in the next release. The release also includes an (experimental) incremental-compilation mode. When combined with the Zig code generator, the new mode makes debug builds quite fast.
Besides the code generation changes, Zig's list of supported targets has significantly expanded in this release, largely due to additional support in the standard library for different platforms. The release notes make this bold claim:
The full list of target-specific fixes is too long to list here, but in short, if you've ever tried to target arm/thumb, mips/mips64, powerpc/powerpc64, riscv32/riscv64, or s390x and run into toolchain problems, missing standard library support, or seemingly nonsensical crashes, then there's a good chance you'll find that things will Just Work with Zig 0.14.0.
Pages and allocation
The 0.14 release also saw a major change to the way the language handles different page sizes. It used to be that each architecture was expected to have a single, static page size. This doesn't match how some architectures (notably Apple Silicon) work — the page size can vary at run time. The new Zig release switches to having two compile-time constants: a minimum and maximum size. At run time, the program can detect what the actual page size is.
This change required rewriting Zig's general-purpose memory allocator, because it assumed that the page size was fixed. Andrew Kelley, Zig's founder, wrote about the improvements that he was able to make in rewriting the allocator. In short, the old general-purpose allocator is now Zig's debug allocator, and a new multi-threaded allocator should be Zig users' first choice.
Kelley has some benchmarks showing that the multi-threaded allocator actually outperforms the GNU C library's (glibc's) memory allocator — although, as is always the case with benchmarks, simple tests can be misleading. The multi-threaded allocator is about 10% faster when using the compiler to compile itself, but Zig's compiler has a somewhat unusual approach to memory use, so whether the gains translate to other programs remains to be seen. One potential complication is memory fragmentation; allocators can trade decreased time for increased fragmentation or vice versa. Zig programmers can experiment with the multi-threaded allocator, the debug allocator, glibc's allocator, or others in order to find what works best for their programs.
Labeled switches
The biggest change to the language itself is the addition of labeled switch statements. In Zig, continue statements require specifying the label of the loop that is being continued to. This helps avoid confusion when there are nested loops in a program. Now, switch statements may be given optional labels, so that continue statements can target them. This turns switch statements into implicit loops, since control flow can return to the start of the statement. The intended use case is writing more efficient byte-code interpreters (a topic that LWN discussed in a recent article), although the feature also makes writing some finite-state automatons more straightforward.
state: switch (State.start) { .start => switch (self.buffer[self.index]) { '0'...'9' => { result.tag = .number; continue :state .number; }, else => continue :state .invalid, }, .number => { ... }, .invalid => { ... }, }
When the values given to the continue statements are known at compile time, the compiler produces direct jumps. When they are only known at run time, the compiler produces a separate indirect jump for each case, which helps the CPU's branch predictor learn which cases are likely to follow which other cases. The first implementation of the feature ran into problems with an LLVM optimization pass that combined the jumps into one, but the version present in the release doesn't have that problem.
Another change that makes writing Zig programs more pleasant has to do with how the language interprets enumeration literals. Zig supports a limited form of type inference in the form of "Result Location Semantics"; when the compiler knows what type the result of an expression should have, that information can be used to automatically perform certain casts, or provide a shorter syntax. For example, when the compiler knows that the result of an expression is a value of an enumeration type, the programmer can leave the name of the type out — writing .number in place of State.number, as in the example above.
The 0.14 release extends this shorthand to things that aren't enumerations. Now, when the compiler sees the expression ".name", it will look for an associated constant with that name on the inferred type of the expression; if one exists, the value of that constant will be used. Besides being a useful shorthand, the new syntax lets developers rename an enum variant in-place without needing to update all the callers by defining the old name as an alias for the new one. In combination with improvements to packed structures — allowing them to be compared for equality and operated on atomically — Zig programmers have a good deal of freedom to create types that seem like simple enumerations while containing other information as well.
A practical example of this use in the standard library comes from Zig's support for different calling conventions. Zig functions can optionally be given a callconv() annotation in order to set their calling convention:
pub fn callable_from_c() callconv(.C) void { ... }
Previously, this annotation took an enumeration type specifying one of a handful of alternatives. Now, the standard library has changed it to a tagged union, so that different calling conventions can add additional configuration options. Most of them permit setting the required stack alignment, for example. The previous versions have become constants defined for the type, so the new literal syntax ensures existing code doesn't need to change.
The future
Zig's development community remains active, with 350 commits by 58 authors over the past month — a speed that's fairly typical for the project. Despite this, the 1.0 release of the language remains several years away. Kelley has already made a strong commitment to backward compatibility once Zig reaches version 1.0, and consequently has a long list of experimental features and deficiencies in the current implementation that should be solved before then.
The number of actual changes to the language itself in each release seems to be slowing down, with this release boasting many small changes, as opposed to the wide-reaching differences of previous releases. That stability seems to have afforded the project the chance to spend time improving the tooling instead, as demonstrated by the expanded number of supported architectures and improvements to the compiler's backend. Despite that improvement, there are still a large number of known problems with Zig's tooling, not least of all because of how expansive that tooling is. Zig includes a lot of tools that other languages do not — a translator for C code, a cross-platform C compiler, a build system, and (new in this release) a built-in fuzzer — and it seems likely that the development cycle for the next release, expected in about six months, will be spent improving them.
Posted Mar 12, 2025 15:22 UTC (Wed)
by darcagn (guest, #168667)
[Link]
Posted Mar 12, 2025 18:07 UTC (Wed)
by Cyberax (✭ supporter ✭, #52523)
[Link] (3 responses)
I'd been using `CC="zig cc -target aarch64-macos-gnu"` to cross-compile tools that need Cgo in Go for macOS without having to tear out my hair in frustration.
Posted Mar 13, 2025 23:42 UTC (Thu)
by ringerc (subscriber, #3071)
[Link] (2 responses)
Do you need an unpacked xcode tree etc for the headers? Copies of OSX .dylib s as link targets?
Posted Mar 14, 2025 1:44 UTC (Fri)
by Cyberax (✭ supporter ✭, #52523)
[Link]
Posted Mar 14, 2025 7:19 UTC (Fri)
by mathstuf (subscriber, #69389)
[Link]
Note that Xcode hasn't linked to `.dylib` files for system libraries for years. There are now `.tbd` files which are kind of like Windows' `.lib` files in that they are linkable but direct runtime loading of other files except they are JSON and describe what symbols are available in which architectures along with some other metadata.
Thinking on it, it'd be really nice if ELF linkers supported something like that so that one could link to `glibc-2.18.symtab` or something to limit oneself to a glibc-2.18 runtime without actually having to execute in such a location. Probably doable with linker scripts, but I feel like a declarative file would be easier for tooling across the ecosystem rather than writing (subtly broken) linker script interpreters all over the place.
Posted Mar 12, 2025 18:37 UTC (Wed)
by epa (subscriber, #39769)
[Link] (20 responses)
Perl very nearly gets it right, allowing any loop to have a label, but the labels are not checked at compile time.
Posted Mar 12, 2025 18:39 UTC (Wed)
by mb (subscriber, #50428)
[Link] (2 responses)
Rust also has that feature.
Posted Mar 12, 2025 21:54 UTC (Wed)
by tialaramex (subscriber, #21167)
[Link]
In the middle of your sixteen line expression labelled 'find: which is checking all the Hay in the Haystack with the intent that the expression will have the result None when it can't find what it wanted the code might instead find a Needle and just
break 'find Some(needle);
This is very similar to what you'd write at the function level, where you might ordinarily return None after exhausting the loop, but if instead you find it you can return Some(needle) -- sometimes you can't really justify a whole function and so it's annoying if you must write a function to enable this nice behaviour.
There was considerable push back to break 'label value for Rust years ago, but it was eventually added to the language. I have used it maybe once every couple of years, so, not often but enough that I think it was the right call.
N3355 will add this to some future C and P3568 looked for direction from WG21 and got none, so C++ will probably inherit whatever C standardises.
Posted Mar 25, 2025 15:48 UTC (Tue)
by jezuch (subscriber, #52988)
[Link]
I dare you to use that feature, though ;)
And it's not *requiring* labels on break and continue, which is the most useful part, probably.
Posted Mar 12, 2025 22:33 UTC (Wed)
by fenncruz (subscriber, #81417)
[Link] (1 responses)
Posted Mar 12, 2025 22:47 UTC (Wed)
by Wol (subscriber, #4433)
[Link]
Cheers,
Posted Mar 13, 2025 11:59 UTC (Thu)
by Karellen (subscriber, #67644)
[Link] (14 responses)
Posted Mar 13, 2025 12:54 UTC (Thu)
by excors (subscriber, #95769)
[Link] (13 responses)
No, because it's much more constrained than goto - you can only jump to the top/bottom of an enclosing lexical scope.
> Why not use goto instead?
As Dijkstra pointed out, unconstrained goto leads to programs with control flow that is too hard to reason about. You'll often have code that enforces an invariant, syntactically followed by code that relies on that invariant, and it'll break if another part of the program can jump into the middle of those two sections. (E.g. you have code that initialises a local variable, followed by code that uses that variable.)
continue/break largely avoids that problem because the program can only jump outwards to a boundary between scopes, and the boundary is visually obvious to the programmer (through indentation etc), so you'll know to be careful about invariants across that boundary. It's still harder to understand than a language with very simple control flow and no continue/break/goto/etc, but that's a tradeoff with expressivity.
It looks like Zig used to have goto, but they removed it at the same time as they added labeled continue/break, because they couldn't find compelling use cases that couldn't be rewritten with defer/continue/break (https://github.com/ziglang/zig/issues/630).
Java also has labeled continue/break and no goto, so it's a well established design and probably a good tradeoff for most languages.
Posted Mar 13, 2025 17:44 UTC (Thu)
by tialaramex (subscriber, #21167)
[Link] (12 responses)
C has only a neutered goto statement, you can only goto a label in the same execution of the same function, so you can jump out of a loop (like break) or back to another iteration of that loop (like continue) but not much more.
Break and continue are nicer semantically than goto, but I can see certainly in C you might value having this very terse single feature (the neutered goto) rather than adding labelled-continue and labelled-break. There was some real push-back to labelled-continue for C and C++ but it wasn't very passionate. Some people want it, some people would rather not, nobody was dying on any hills.
Posted Mar 13, 2025 18:12 UTC (Thu)
by Wol (subscriber, #4433)
[Link] (4 responses)
I've used "goto" in C. ONCE.
My colleague said I wasn't allowed to use it (despite the user's spec "requiring" it). I said "you forbid goto, you write the code". He replied "all yours" so I used it.
Cheers,
Posted Mar 13, 2025 21:10 UTC (Thu)
by dskoll (subscriber, #1630)
[Link] (3 responses)
I haven't looked at Linux kernel code in quite a while, but I recall it used this idiom quite a bit.
Posted Mar 14, 2025 0:34 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link] (2 responses)
Posted Mar 14, 2025 1:18 UTC (Fri)
by dskoll (subscriber, #1630)
[Link]
Yes, definitely.
Posted Mar 14, 2025 10:32 UTC (Fri)
by bluca (subscriber, #118303)
[Link]
Posted Mar 14, 2025 4:38 UTC (Fri)
by gutschke (subscriber, #27910)
[Link] (4 responses)
C has only a neutered goto statement, you can only goto a label in the same execution of the same function, so you can jump out of a loop (like break) or back to another iteration of that loop (like continue) but not much more. C is sufficiently flexible that if you absolutely insisted on it, you could put your entire program into the From a pragmatic point of view, you are of course entirely correct and nobody would do that. In most real-world applications, the restrictions that C puts on I see the benefits and the potential risks of labelled control flow, and understand why language designers tread extra carefully. I still would like labelled I also understand why BASIC was prone to all the bad aspects of
Posted Mar 14, 2025 9:28 UTC (Fri)
by tialaramex (subscriber, #21167)
[Link] (1 responses)
As I mentioned above C2b (maybe named C29, another ISO C standard in the next few years) is likely to have the labelled break and continue.
Posted Mar 15, 2025 14:01 UTC (Sat)
by willy (subscriber, #9762)
[Link]
Posted Mar 14, 2025 23:35 UTC (Fri)
by khim (subscriber, #9252)
[Link] (1 responses)
And how would you emulate “Dijkstra's nightmare scenario” is not possible in C, or even BASIC. For it to make any sense one have to live in the world of Wheeler Jump and FORTRAN 66 or ALGOL 68. Then you can nicely combine GOTO with the use of procedures and functions. That world doesn't exist today. Except for assembler and even there it's not common.
Posted Mar 14, 2025 23:55 UTC (Fri)
by gutschke (subscriber, #27910)
[Link]
And if you're ok with GNU extensions such as computed GOTOs and macro trickery, then I'm confident you could implement the general idea.
Posted Mar 14, 2025 15:28 UTC (Fri)
by ballombe (subscriber, #9523)
[Link] (1 responses)
Posted Mar 14, 2025 16:20 UTC (Fri)
by farnz (subscriber, #17727)
[Link]
In this case, because "continue outer" and "break outer" specify a label, they leave both the inner loop (for) and the outer loop (while). A plain break; or continue; would only have acted on the inner for loop.
Posted Mar 25, 2025 1:49 UTC (Tue)
by kmeyer (subscriber, #50720)
[Link]
This is a low-ish bar. Glibc's allocator is notoriously bad. The state of the art here is jemalloc or mimalloc.
I'd love to see Zig work on libgccjit based code generation
Zig for C compilation
Zig for C compilation
Zig for C compilation
Zig for C compilation
Finally, labelled continue
In Zig, continue statements require specifying the label of the loop that is being continued to. This helps avoid confusion when there are nested loops in a program.
Aaaahhh. And can we now have that in other languages? At least the ability to give the label, even if it's not made compulsory as in Zig. And ideally with the ability to continue
or break
an enclosing loop if you specify its name.
Finally, labelled continue
Finally, labelled continue
Finally, labelled continue
Finally, labelled continue
Finally, labelled continue
Wol
Isn't this just a goto? Why not use goto instead?
Finally, labelled continue
Finally, labelled continue
Finally, labelled continue
Finally, labelled continue
Wol
Finally, labelled continue
thing1 = acquire_thing1();
if (!thing1) goto bailout1;
thing2 = acquire_thing2();
if (!thing2) goto bailout2;
thing3 = acquire_thing3();
if (!thing3) goto bailout3;
/* do work */
free_thing3(thing3);
bailout3: free_thing2(thing2);
bailout2: free_thing1(thing1);
bailout1: return whatever;
Finally, labelled continue
Finally, labelled continue
Finally, labelled continue
Finally, labelled continue
main()
function, make all your variables global, and exclusively rely on goto
for control flow. You'd effectively write the infamous BASIC "spaghetti" code using C syntax; and you could jump into the middle of control flow to your heart's content.goto
do apply. And nobody feels the need to go back to Dijkstra's nightmare scenario. I don't often use goto
in my own programs, but it does have its place in C. In other languages, there often are more idiomatically correct solutions.break
and continue
statements in C though. If only, because it makes the intent more clear.goto
. In a language that has barely usable subroutines, function definitions that are all but pointless, no scoping for variables and lacks user-defined data types, it is very hard to see how you'd implement a sane restricted subset of goto
. But that's what you have to put up with, if you want to squeeze the runtime into 1kB of RAM and 8kB of ROM that some of the early devices of that era came with.Finally, labelled continue
Finally, labelled continue
> You'd effectively write the infamous BASIC "spaghetti" code using C syntax; and you could jump into the middle of control flow to your heart's content.
Finally, labelled continue
GOSUB
? With [computed goto](https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html)? That's GNU C extension, not standard C.Finally, labelled continue
Finally, labelled continue
alternatives are worse.
Labelled break and continue do allow you specify how many levels of loops to get out of - in C-like syntax, it'd look something like:
Finally, labelled continue
outer:
while (conditional()) {
for(i = 0; i < limit; i++) {
if (is_cont_condition(data[i])) {
continue outer;
}
} else if (is_break_condition(data[i]) {
break outer;
}
}
allocator performance