Pangram verdict · v3.3
We believe that this entire text is human-written.
AI likelihood · overall
HumanArticle text · 1,434 words · 1 segments analyzed
This page contains a curated list of recent changes to main branch Zig. This page contains entries for the year 2026. Other years are available in the Devlog archive page. August 27, 2026Pointer Stability for ArrayLists Author: Robbie LymanPointer Stability Locks were added to std’s Hash Map containers in 2024. A pull request initially opened by Leo Emar-Kar in 2025 now brings this technique for ensuring memory safety to std.ArrayList.To make use of this in your code, add a call to lockPointers() when you first store a pointer to an element or a slice of elements backed by the ArrayList, and call unlockPointers() when those pointers are no longer needed.Here’s a somewhat contrived example. Let’s suppose we are managing two ArrayLists, say one of which is holding in memory the contents of some input, while the other is storing chunks of interest; maybe each line. Here’s a version of this process which has a bug; see if you can spot it.const std = @import("std"); const Context = struct { history: std.ArrayList(u8), lines: std.ArrayList([]const u8), fn parse(ctx: *Context, allocator: std.mem.Allocator, input: []const u8) !void { const slice = try ctx.history.addManyAsSlice(allocator, input.len); @memcpy(slice, input); var it = std.mem.tokenizeScalar(u8, slice, '\n'); while (it.next()) |line| { try ctx.lines.append(allocator, line); } } }; Did you spot the bug? The problem is that elements of Context.lines.items depend on the location of Context.history.items, but this location may change if Context.history needs to grow beyond its current capacity. Here’s a reproduction of the bug:test "Context.parse" { const input = "I'm first!\n"; const input_two = \\But this text \\is juuuuuuuuuuuuuuuuuuuuuuuuust long enough that it \\causes a problem! \\And the problem could be that we segfault! \\Which is no fun to run into. ; var ctx: Context = .{ .history = .empty, .lines = .empty, }; const gpa = std.testing.allocator; defer ctx.history.deinit(gpa); defer ctx.lines.deinit(gpa); try ctx.parse(gpa, input); try ctx.parse(gpa, input_two); try std.testing.expectEqualStrings("I'm first!", ctx.lines.items[0]); } If I run this code with zig test, I get the following output (plus a little more).====== expected this output: ========= I'm first!␃ ======== instead found this: ========= UUUUUUUUUU␃ ====================================== First difference occurs on line 1: expected: I'm first! ^ ('\x49') found: UUUUUUUUUU ^ ('\x55') 1/1 blah.test.Context.parse...FAIL (TestExpectedEqual) Not great, right? This does tell us that we have a bug, but depending on your comfort debugging memory issues (and your choice of allocator, which will change how the bug manifests!), you might be lost for quite a while before you spot the fix.Since we’ve stored pointers after the first call to parse in our test, what happens if we make this change? try ctx.parse(gpa, input); + ctx.history.lockPointers(); + defer ctx.history.unlockPointers(); try ctx.parse(gpa, input_two); try std.testing.expectEqualStrings("I'm first!", ctx.lines.items[0]); We get a panic with a stack trace that shows us where our assumption about pointer stability was violated!thread 3023222 panic: reached unreachable code /Users/robbie/bin/lib/std/debug.zig:442:14: 0x102d2506f in assert (test) if (!ok) unreachable; // assertion failure ^ /Users/robbie/bin/lib/std/debug.zig:1880:15: 0x102d31ef7 in assertUnlocked (test) assert(l.state == .unlocked); ^ /Users/robbie/bin/lib/std/array_list.zig:1348:50: 0x102e3ced7 in ensureTotalCapacityPrecise (test) self.pointer_stability.assertUnlocked(); ^ /Users/robbie/bin/lib/std/array_list.zig:1341:51: 0x102e3cdff in ensureTotalCapacity (test) return self.ensureTotalCapacityPrecise(gpa, growCapacity(new_capacity)); ^ /Users/robbie/bin/lib/std/array_list.zig:1237:41: 0x102e4e5c3 in resize (test) try self.ensureTotalCapacity(gpa, new_len); ^ /Users/robbie/bin/lib/std/array_list.zig:1461:28: 0x102e4e40f in addManyAsSlice (test) try self.resize(gpa, try addOrOom(self.items.len, n)); ^ /Users/robbie/src/advent-of-code/2024/blah.zig:8:51: 0x102e4dc1f in parse (test) const ptr = try ctx.history.addManyAsSlice(allocator, input.len); ^ /Users/robbie/src/advent-of-code/2024/blah.zig:35:18: 0x102e4e167 in test.Context.parse (test) try ctx.parse(gpa, input_two); Nice, that’s already a big help: now I can see that I should consider memory safety issues as a probable cause of my test failure in addition or instead of a logic issue. Obviously this example was somewhat contrived, but I do find myself reaching for std.ArrayList as this type of backing storage in real code, so I hope you can see real-world use cases for it yourself.Before I close, I want to point out something subtle: unlike HashMap and its friends, ArrayList is ordered, which means that operations on the list may move elements around even without moving, resizing or freeing the backing memory of the list as a whole. For example, the pointer (well, slice) returned by addManyAsSlice(gpa, n) may not point to the final n elements of the list if you call orderedRemove() or pop(). For this reason, although orderedRemove() and pop() never allocate, they will trigger the same assertion above after a call to lockPointers(). June 30, 2026All Package Management Functionality Moved from Compiler to Build System Author: Andrew KelleyNow that there is a separate process for users’ build.zig scripts and the build system itself, it makes sense for that to be the place that package management logic lives.I moved these subcommands to the maker process:zig buildzig fetchzig initzig libcThis means that large parts of what used to be included in the compiler executable are now shipped in source form instead, including:package fetching logicHTTP client and networkingTLS (Transport Layer Security) and associated cryptoGit protocolxz, gzip, zstd, flate, zipparsing, validation, and otherwise dealing with build.zig.zon filesConsequently, this functionality can now be patched without rebuilding the compiler, making it easier for users and contributors to tinker.Furthermore, it means that package management in zig now has safety checks enabled when doing networking, since the maker executable is compiled in ReleaseSafe mode. Plus, all the crypto used for networking and file hashing can now take advantage of special CPU instructions available on the host, even the ones that are too rare to normally depend on when distributing software. We can have AOT cake and eat JIT, too!My original motivation for doing this was in relation to exposing a build server protocol in order to unblock ZLS after maker/configurer process separation made breaking changes to the --build-runner override flag.Originally, the process tree looked like this:zig build (the zig compiler + package manager) └─ builder (the user's build.zig logic + build system implementation) The process separation changeset made it look like this instead:zig build (the zig compiler + package manager) ├─ configurer (the user's build.zig logic) └─ maker (build system) At this point, consider a long-running zig build --watch process, watching files and rebuilding on source code changes. If any changes to build.zig are detected, or any files observed during execution of that logic, it means configurer needs to be rerun, meaning that maker process must exit to give zig build a chance to repeat the package management logic.Now, after the changes described in this devlog entry, it looks like this:zig build (the zig compiler) └─ maker (build system + package manager) └─ configurer (the user's build.zig logic) Thus, when configuration needs to be rerun, maker process can continue to live because it is the parent process rather than a sibling. In terms of the upcoming build server, it means avoiding an awkward situation where the server has to exit and the client has to reconnect, rather than simply informing the client of a configuration change.This is almost entirely a non-breaking change, but there are some observable differences:Zig executable binary size: shrinks 4% from 14.1 to 13.5 MiB (no LLVM, ReleaseSmall)--maker-opt flag is replaced by ZIG_DEBUG_MAKER environment variable--zig-lib-dir flag is replaced by ZIG_LIB_DIR environment variableThe follow-up issues to this changeset are the main blockers until we tag Zig 0.17.0:build server protocol MVP (needed to unblock ZLS)introduce the concept of adding path dependencies of the build script itselfmake zig build --watch detect modifications to the build script and rerun itselfdifferent cwd causes build script cache missI have two conferences coming up in July and I need to work on my talks, so being realistic, I don’t think I will have time to wrap these up until early August. Contributions welcome, of course.Big thanks to Techatrix from the ZLS team for reaching out and working with me on the build server protocol! They are seeking sponsorship, by the way. June 26, 2026SPIR-V Backend Progress Author: Ali CheraghiThere’s quite a bit to cover. The SPIR-V backend had bitrotted in a number of places after the recent compiler changes, so I spent the past several weeks dragging it into a better state.@SpirvTypeSPIR-V has a handful of types that couldn’t be expressed in Zig’s type system. The new @SpirvType builtin has been introduced to address the longest-standing blocker for writing shaders. See #20550, #23326 and #35461 to trace the background.const Sampler = @SpirvType(.sampler); const Image = @SpirvType(.{ .image = .{ .usage = .{ .sampled = u32 }, .format = .unknown, .dim = .@"2d", .depth = .unknown, .arrayed = false, .multisampled = false, .access = .unknown, } }); const SampledImage = @SpirvType(.{ .sampled_image = Image }); const RuntimeArray = @SpirvType(.{ .runtime_array = u32 }); const sampled_image = @extern(*addrspace(.constant) const SampledImage, .{ .name = "sampled_image", .decoration = .{ .descriptor = .{ .set = 0, .binding = 1 } }, }); Execution Mode on the Calling ConventionExecution mode info (workgroup size,