Dusting Off ishi for Zig 0.16.0 | Louis LeFebvre (✿◠‿◠)Dusting Off ishi for Zig 0.16.0<br>2026-05-15<br>The Wolves are getting hammered. Game 6 Western Semis against the Spurs, who<br>lead the series 3-2. I decided it wouldn’t hurt to do a little multi-tasking —<br>half-watching the game, half-staring at a GitHub Actions tab — and figured if<br>my evening plans were a little bitter, I might as well make it a bitter-sweet<br>and dust off a side project that needed some love, ishi.<br>A quick background on ishi, it’s essentially “pgvector storage for git<br>intelligence”. You point it at a repo, it embeds your commit history into a<br>pgvector database, and then you can ask it semantic questions like “what<br>changed about the build system?” and it’ll pull the most relevant commits<br>back. I hadn’t touched it in a few weeks. So I opened the repo and noticed the<br>CI was red, and I’d been poking at Zig 0.16.0 in a tiny toy repo<br>called hash to learn swiss tables, so I figured it was a good time to<br>upgrade.
A new fingerprint, fetch some resources<br>The CI log was unambiguous:<br>/home/runner/work/ishi/ishi/build.zig.zon:41:21: error: hash mismatch:<br>manifest declares 'zul-0.0.0-1oDot9yRBwDA_ovd6GC1M_ViW3LarywMaGrH6vcuEjqv'<br>but the fetched package has 'zul-0.0.0-1oDot4qXBwCHBYRdqx2aqkVPgcXiIOm9GXT5zLOCf_H3'<br>A quick peek at build.zig.zon revealed why — I’d lazily pinned the zul dep<br>to …/zul/archive/master.tar.gz, which is the exact opposite of a pin:<br>.{<br>.name = .ishi,<br>.version = "0.0.0",<br>.fingerprint = 0xa89789cfdd52da45, // never changes<br>.minimum_zig_version = "0.15.2",<br>.dependencies = .{<br>.zul = .{<br>.url = "https://github.com/karlseguin/zul/archive/master.tar.gz",<br>.hash = "zul-0.0.0-1oDot9yRBwDA_ovd6GC1M_ViW3LarywMaGrH6vcuEjqv",<br>},<br>},<br>Karl Seguin had merged “Writergate part 2” into zul/master since my last<br>commit, so the tarball moved, the hash changed, CI cried. The two-second fix<br>would have been zig fetch --save=zul against an immutable commit URL:<br>$ zig fetch --save=zul https://github.com/karlseguin/zul/archive/776ba3b7db3bb03784a768d90c23dee26dee3268.tar.gz<br>warning: overwriting existing dependency named 'zul'<br>That regenerates the .hash entry in build.zig.zon for you — no<br>hand-editing required. But before I knew it, I was rolling my sleeves up: if I<br>was already in this file, I might as well bump<br>.minimum_zig_version = "0.16.0" and do the whole upgrade properly. Zig<br>0.16.0 had landed in January1 and I’d been wanting an excuse to play with<br>it.<br>Goodbye, zul<br>The first surprise: with .minimum_zig_version = "0.16.0" set, zig build<br>died inside zul/src/arc.zig:<br>zul/src/arc.zig:34:18: error: invalid builtin function: '@Type'<br>const Args = @Type(.{<br>^~~~~<br>The Zig 0.16.0 release notes confirm: @Type was removed and<br>replaced by individual builtins (@Int, @Struct, @Tuple, …). Karl hasn’t<br>patched arc.zig yet, and there’s a closed PR that didn’t land. So zul on<br>0.16.0 is a no-go until upstream catches up.<br>I almost forked it. Then I looked at how much of zul I was actually using:<br>$ grep -rn 'zul' src/<br>src/lib/runner.zig:2:const zul = @import("zul");<br>src/lib/runner.zig:67: var client = zul.http.Client.init(allocator);<br>src/lib/runner.zig:114: var client = zul.http.Client.init(allocator);<br>Two call sites of zul.http.Client. That’s it. And meanwhile the 0.16.0<br>release notes were bragging about the new std.http.Client —<br>first-class, async DNS, happy-eyeballs, the works. Why was I hauling around a<br>dep?<br>So out it went. The replacement uses the high-level client.fetch helper plus<br>an std.Io.Writer.Allocating to capture the response body:<br>fn postJson(<br>allocator: std.mem.Allocator,<br>io: std.Io,<br>endpoint: []const u8,<br>body: []const u8,<br>) ![]u8 {<br>var client: std.http.Client = .{ .allocator = allocator, .io = io };<br>defer client.deinit();
var response_buf: std.Io.Writer.Allocating = .init(allocator);<br>defer response_buf.deinit();
const result = try client.fetch(.{<br>.location = .{ .url = endpoint },<br>.method = .POST,<br>.payload = body,<br>.headers = .{ .content_type = .{ .override = "application/json" } },<br>.response_writer = &response_buf.writer,<br>});
const status_int = @intFromEnum(result.status);<br>if (status_int 200 or status_int >= 300) {<br>return error.RunnerRequestFailed;<br>return response_buf.toOwnedSlice();<br>Karl, if you’re reading this — your libraries got me a long way. Just got<br>lapped by the standard library this time around.<br>Juicy main<br>This is the part of the upgrade that makes me so happy I decided to learn Zig.<br>Zig 0.16.0 introduced “Juicy Main”: pub fn main now takes a<br>std.process.Init parameter that hands you a bunch of “pre-initialized<br>goodies”.<br>pub fn main(init: std.process.Init) !void {<br>const allocator = init.gpa;<br>// const io = init.io;<br>// const args = try init.minimal.args.toSlice(init.arena.allocator());
const f = try Flags.init(allocator, init);<br>defer f.deinit();<br>// ...<br>init.gpa is a debug-mode-leak-checked general-purpose allocator.<br>init.arena...