split 0.15 notes into topic files

- arraylist.md: why allocator is now passed to every method
- io.md: writergate, explicit buffers, tls reading quirk
- build.md: createModule pattern, build.zig.zon
- comptime.md: type-returning functions, inline for, reflection
- concurrency.md: thread pools, mutexes, atomics, backoff

README is now just an index with links

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+488 -481
languages
+8 -481
languages/ziglang/0.15/README.md
··· 1 1 # zig 0.15 2 2 3 - patterns from zig 0.15+ development. the 0.15 release was a [major breaking change](https://ziglang.org/download/0.15.1/release-notes.html) with a complete i/o overhaul. 4 - 5 - ## contents 6 - 7 - - [build system](#build-system) - module imports, dependencies 8 - - [memory](#memory) - allocators, arenas, lifecycle 9 - - [error handling](#error-handling) - try, catch, labeled blocks 10 - - [concurrency](#concurrency) - threads, pools, atomics, backoff 11 - - [http server](#http-server) - new reader/writer interfaces 12 - - [websockets](#websockets) - tls client pattern 13 - - [json](#json) - parsing and formatting 14 - - [comptime](#comptime) - type-returning functions, reflection 15 - - [strings](#strings) - splitting, searching, encoding 16 - 17 - --- 18 - 19 - ## build system 20 - 21 - ### executable with dependencies 22 - 23 - the 0.15 pattern uses `createModule` with an `imports` array instead of `addModule`: 24 - 25 - ```zig 26 - const exe = b.addExecutable(.{ 27 - .name = "myapp", 28 - .root_module = b.createModule(.{ 29 - .root_source_file = b.path("src/main.zig"), 30 - .target = target, 31 - .optimize = optimize, 32 - .imports = &.{ 33 - .{ .name = "websocket", .module = websocket.module("websocket") }, 34 - .{ .name = "zqlite", .module = zqlite.module("zqlite") }, 35 - }, 36 - }), 37 - }); 38 - ``` 3 + [release notes](https://ziglang.org/download/0.15.1/release-notes.html) · [migration guide](https://sngeth.com/zig/systems-programming/breaking-changes/2025/10/24/zig-0-15-migration-roadblocks/) 39 4 40 - see: [music-atmosphere-feed/build.zig](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/build.zig) 5 + major breaking release with i/o overhaul ("writergate") and build system changes. 41 6 42 - ### library module 7 + ## notes 43 8 44 - for libraries that expose a module to consumers: 45 - 46 - ```zig 47 - const mod = b.addModule("zql", .{ 48 - .root_source_file = b.path("src/root.zig"), 49 - .target = target, 50 - .optimize = optimize, 51 - }); 52 - 53 - const tests = b.addTest(.{ .root_module = mod }); 54 - ``` 55 - 56 - see: [zql/build.zig](https://tangled.sh/@zzstoatzz.io/zql/tree/main/build.zig) 57 - 58 - ### build.zig.zon 59 - 60 - dependency declaration format: 61 - 62 - ```zig 63 - .{ 64 - .name = .my_project, 65 - .version = "0.0.1", 66 - .minimum_zig_version = "0.15.0", 67 - .dependencies = .{ 68 - .websocket = .{ 69 - .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.tar.gz", 70 - .hash = "...", 71 - }, 72 - }, 73 - } 74 - ``` 75 - 76 - --- 77 - 78 - ## memory 79 - 80 - ### general purpose allocator 81 - 82 - use for application-lifetime allocations. check for leaks on deinit: 83 - 84 - ```zig 85 - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 86 - defer _ = gpa.deinit(); 87 - const allocator = gpa.allocator(); 88 - ``` 89 - 90 - see: [music-atmosphere-feed/src/main.zig:14-16](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L14) 91 - 92 - ### arena allocator 93 - 94 - use for request-scoped memory where everything can be freed at once: 95 - 96 - ```zig 97 - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 98 - defer arena.deinit(); 99 - const alloc = arena.allocator(); 100 - // all allocations freed when arena deinits 101 - ``` 102 - 103 - this pattern appears throughout http handlers where we need temporary allocations for building responses: 104 - 105 - see: [leaflet-search/backend/src/server.zig:65-67](https://tangled.sh/@zzstoatzz.io/leaflet-search/tree/main/backend/src/server.zig#L65) 106 - 107 - ### arraylist 108 - 109 - 0.15 changed arraylist to require allocator on each method call: 110 - 111 - ```zig 112 - var buf: std.ArrayList(u8) = .{}; 113 - defer buf.deinit(alloc); 114 - const w = buf.writer(alloc); 115 - try w.writeAll("data"); 116 - ``` 117 - 118 - see: [leaflet-search/backend/src/server.zig:129-132](https://tangled.sh/@zzstoatzz.io/leaflet-search/tree/main/backend/src/server.zig#L129) 119 - 120 - --- 121 - 122 - ## error handling 123 - 124 - ### try (propagation) 125 - 126 - most common pattern - propagate errors to caller: 127 - 128 - ```zig 129 - const result = try doSomething(); 130 - ``` 131 - 132 - ### catch with specific handling 133 - 134 - handle specific errors differently: 135 - 136 - ```zig 137 - const n = tls_client.reader.stream(&w, .limited(len)) catch |err| { 138 - if (err == error.EndOfStream) break :outer; 139 - return error.Failed; 140 - }; 141 - ``` 142 - 143 - ### catch with default 144 - 145 - provide fallback value: 146 - 147 - ```zig 148 - const port = std.fmt.parseInt(u16, port_str, 10) catch 3000; 149 - const host = std.posix.getenv("HOST") orelse "localhost"; 150 - ``` 151 - 152 - see: [music-atmosphere-feed/src/main.zig:37-40](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L37) 153 - 154 - ### labeled blocks for complex fallbacks 155 - 156 - when you need multi-step fallback logic: 157 - 158 - ```zig 159 - const port: u16 = blk: { 160 - const port_str = posix.getenv("PORT") orelse "3000"; 161 - break :blk std.fmt.parseInt(u16, port_str, 10) catch 3000; 162 - }; 163 - ``` 164 - 165 - see: [music-atmosphere-feed/src/main.zig:37-40](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L37) 166 - 167 - ### defer / errdefer 168 - 169 - cleanup resources on scope exit: 170 - 171 - ```zig 172 - const resource = try allocate(); 173 - errdefer deallocate(resource); // only runs if error after this point 174 - defer deallocate(resource); // always runs 175 - ``` 176 - 177 - --- 178 - 179 - ## concurrency 180 - 181 - ### thread pool 182 - 183 - for handling concurrent http connections: 184 - 185 - ```zig 186 - var pool: Thread.Pool = undefined; 187 - try pool.init(.{ .allocator = allocator, .n_jobs = 16 }); 188 - defer pool.deinit(); 189 - 190 - pool.spawn(handleConnection, .{conn}) catch |err| { 191 - conn.stream.close(); 192 - }; 193 - ``` 194 - 195 - see: [music-atmosphere-feed/src/main.zig:29-34](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L29) 196 - 197 - ### background thread 198 - 199 - for long-running tasks like stream consumers: 200 - 201 - ```zig 202 - const thread = try Thread.spawn(.{}, consumer, .{allocator}); 203 - defer thread.join(); 204 - ``` 205 - 206 - see: [music-atmosphere-feed/src/main.zig:25-26](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L25) 207 - 208 - ### mutex 209 - 210 - for protecting shared state across threads: 211 - 212 - ```zig 213 - const BotState = struct { 214 - mutex: Thread.Mutex = .{}, 215 - // ... other fields 216 - }; 217 - 218 - state.mutex.lock(); 219 - defer state.mutex.unlock(); 220 - // critical section 221 - ``` 222 - 223 - see: [find-bufo/bot/src/main.zig:21](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L21), [find-bufo/bot/src/main.zig:102-103](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L102) 224 - 225 - ### atomic values 226 - 227 - for lock-free counters and flags: 228 - 229 - ```zig 230 - messages: std.atomic.Value(u64) = .init(0), 231 - 232 - // increment 233 - _ = self.messages.fetchAdd(1, .monotonic); 234 - 235 - // read 236 - const count = self.messages.load(.monotonic); 237 - ``` 238 - 239 - see: [music-atmosphere-feed/src/stats.zig:12](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/stats.zig#L12) 240 - 241 - ### exponential backoff 242 - 243 - for reconnection loops: 244 - 245 - ```zig 246 - var backoff: u64 = 1; 247 - const max_backoff: u64 = 60; 248 - 249 - while (true) { 250 - connect() catch {}; 251 - posix.nanosleep(backoff, 0); 252 - backoff = @min(backoff * 2, max_backoff); 253 - } 254 - ``` 255 - 256 - see: [music-atmosphere-feed/src/jetstream.zig:22-31](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/jetstream.zig#L22) 257 - 258 - --- 259 - 260 - ## http server 261 - 262 - 0.15 completely rewrote i/o interfaces. the new pattern uses explicit buffers: 263 - 264 - ```zig 265 - var read_buffer: [8192]u8 = undefined; 266 - var write_buffer: [8192]u8 = undefined; 267 - 268 - var reader = conn.stream.reader(&read_buffer); 269 - var writer = conn.stream.writer(&write_buffer); 270 - 271 - var server = http.Server.init(reader.interface(), &writer.interface); 272 - 273 - while (true) { 274 - var request = server.receiveHead() catch return; 275 - handleRequest(&request) catch return; 276 - if (!request.head.keep_alive) return; 277 - } 278 - ``` 279 - 280 - see: [music-atmosphere-feed/src/http.zig:12-36](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/http.zig#L12) 281 - 282 - ### responding 283 - 284 - ```zig 285 - try request.respond(body, .{ 286 - .status = .ok, 287 - .extra_headers = &.{ 288 - .{ .name = "content-type", .value = "application/json" }, 289 - }, 290 - }); 291 - ``` 292 - 293 - see: [leaflet-search/backend/src/server.zig:137-147](https://tangled.sh/@zzstoatzz.io/leaflet-search/tree/main/backend/src/server.zig#L137) 294 - 295 - ### socket timeouts 296 - 297 - ```zig 298 - const timeout = std.mem.toBytes(posix.timeval{ .sec = 30, .usec = 0 }); 299 - try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout); 300 - try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); 301 - ``` 302 - 303 - see: [music-atmosphere-feed/src/main.zig:65-71](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L65) 304 - 305 - --- 306 - 307 - ## websockets 308 - 309 - using [websocket.zig](https://github.com/karlseguin/websocket.zig) for tls websocket connections: 310 - 311 - ```zig 312 - var client = websocket.Client.init(allocator, .{ 313 - .host = host, 314 - .port = 443, 315 - .tls = true, 316 - .max_size = 1024 * 1024, 317 - }) catch return err; 318 - defer client.deinit(); 319 - 320 - client.handshake(path, .{ .headers = host_header }) catch return err; 321 - 322 - var handler = Handler{ .allocator = allocator }; 323 - client.readLoop(&handler) catch return err; 324 - ``` 325 - 326 - the handler struct implements `serverMessage` and `close`: 327 - 328 - ```zig 329 - const Handler = struct { 330 - allocator: Allocator, 331 - 332 - pub fn serverMessage(self: *Handler, data: []const u8) !void { 333 - // process message 334 - } 335 - 336 - pub fn close(_: *Handler) void { 337 - std.debug.print("connection closed\n", .{}); 338 - } 339 - }; 340 - ``` 341 - 342 - see: [music-atmosphere-feed/src/jetstream.zig:40-66](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/jetstream.zig#L40) 343 - 344 - --- 345 - 346 - ## json 347 - 348 - ### parsing 349 - 350 - ```zig 351 - const parsed = json.parseFromSlice(json.Value, allocator, data, .{}) catch return; 352 - defer parsed.deinit(); 353 - 354 - const root = parsed.value.object; 355 - const val = root.get("key") orelse return; 356 - if (val != .string) return; 357 - const s = val.string; 358 - ``` 359 - 360 - see: [find-bufo/bot/src/jetstream.zig:95-98](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/jetstream.zig#L95) 361 - 362 - ### type checking pattern 363 - 364 - common pattern for safely extracting nested json: 365 - 366 - ```zig 367 - const kind = root.get("kind") orelse return error.NotAPost; 368 - if (kind != .string or !mem.eql(u8, kind.string, "commit")) return error.NotAPost; 369 - 370 - const commit = root.get("commit") orelse return error.NotAPost; 371 - if (commit != .object) return error.NotAPost; 372 - ``` 373 - 374 - see: [music-atmosphere-feed/src/jetstream.zig:99-109](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/jetstream.zig#L99) 375 - 376 - --- 377 - 378 - ## comptime 379 - 380 - ### type-returning functions 381 - 382 - create types at compile time based on input: 383 - 384 - ```zig 385 - pub fn Query(comptime sql: []const u8) type { 386 - comptime { 387 - const parsed = parser.parse(sql); 388 - return struct { 389 - pub const raw = sql; 390 - pub const params: []const []const u8 = parsed.params[0..parsed.params_len]; 391 - pub const columns: []const []const u8 = parsed.columns[0..parsed.columns_len]; 392 - 393 - pub fn validateArgs(comptime Args: type) void { 394 - // compile-time validation 395 - } 396 - }; 397 - } 398 - } 399 - ``` 400 - 401 - see: [zql/src/Query.zig:10-108](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L10) 402 - 403 - ### inline for 404 - 405 - loop unrolled at compile time: 406 - 407 - ```zig 408 - inline for (params) |p| { 409 - if (!hasField(fields, p)) { 410 - @compileError("missing param :" ++ p); 411 - } 412 - } 413 - ``` 414 - 415 - see: [zql/src/Query.zig:23-27](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L23) 416 - 417 - ### reflection 418 - 419 - access struct fields at comptime: 420 - 421 - ```zig 422 - const fields = @typeInfo(Args).@"struct".fields; 423 - inline for (fields) |f| { 424 - @field(result, f.name) = @field(args, f.name); 425 - } 426 - ``` 427 - 428 - see: [zql/src/Query.zig:55-64](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L55) 429 - 430 - ### branch quota 431 - 432 - for complex comptime parsing, increase the default branch quota: 433 - 434 - ```zig 435 - @setEvalBranchQuota(sql.len * 100); 436 - ``` 437 - 438 - see: [zql/src/parse.zig:48](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/parse.zig#L48) 439 - 440 - --- 441 - 442 - ## strings 443 - 444 - ### splitting 445 - 446 - ```zig 447 - var parts = mem.splitScalar(u8, data, '/'); 448 - const first = parts.next() orelse return error.Invalid; 449 - ``` 450 - 451 - ### prefix/suffix checks 452 - 453 - ```zig 454 - if (mem.startsWith(u8, target, "/api/")) { } 455 - if (mem.endsWith(u8, id, ".gif")) { } 456 - ``` 457 - 458 - see: [find-bufo/bot/src/main.zig:142](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L142) 459 - 460 - ### searching 461 - 462 - ```zig 463 - if (mem.indexOf(u8, response, "\r\n\r\n")) |idx| { 464 - const body = response[idx + 4 ..]; 465 - } 466 - ``` 467 - 468 - ### url decoding 469 - 470 - ```zig 471 - const buf = try alloc.dupe(u8, encoded); 472 - const decoded = std.Uri.percentDecodeInPlace(buf); 473 - ``` 474 - 475 - see: [leaflet-search/backend/src/server.zig:115-116](https://tangled.sh/@zzstoatzz.io/leaflet-search/tree/main/backend/src/server.zig#L115) 476 - 477 - ### multi-line strings 478 - 479 - ```zig 480 - const sql = 481 - \\CREATE TABLE IF NOT EXISTS posts ( 482 - \\ uri TEXT PRIMARY KEY, 483 - \\ cid TEXT NOT NULL 484 - \\) 485 - ; 486 - ``` 9 + - [arraylist](./arraylist.md) - now requires allocator on every method call 10 + - [io](./io.md) - new reader/writer interfaces with explicit buffers 11 + - [build](./build.md) - module imports pattern with `createModule` 12 + - [comptime](./comptime.md) - type-returning functions, inline for, reflection 13 + - [concurrency](./concurrency.md) - thread pools, mutexes, atomics, backoff
+64
languages/ziglang/0.15/arraylist.md
··· 1 + # arraylist 2 + 3 + in 0.15, `std.ArrayList` became "unmanaged" by default - it no longer stores the allocator internally. you pass the allocator to every method call. 4 + 5 + ## why this changed 6 + 7 + the [release notes](https://ziglang.org/download/0.15.1/release-notes.html) explain the reasoning: "having an extra field is more complicated than not having an extra field, so not having it is the null hypothesis." 8 + 9 + storing the allocator had downsides: 10 + - worse method signatures when dealing with capacity reservations 11 + - can't statically initialize (no allocator available at comptime) 12 + - extra memory cost, especially for nested containers (arraylist of arraylists) 13 + 14 + the supposed upside - "avoiding accidentally using the wrong allocator" - isn't worth it since the correct allocator is always nearby and misuse can be safety-checked. 15 + 16 + ## the pattern 17 + 18 + ```zig 19 + var buf: std.ArrayList(u8) = .{}; 20 + defer buf.deinit(alloc); 21 + 22 + try buf.append(alloc, 'x'); 23 + try buf.appendSlice(alloc, "hello"); 24 + 25 + const w = buf.writer(alloc); 26 + try w.print("{d}", .{42}); 27 + ``` 28 + 29 + every mutating method takes allocator as the first argument. this is the part i keep forgetting - `append(alloc, item)` not `append(item)`. 30 + 31 + ## initialization 32 + 33 + you can now statically initialize because there's no allocator field: 34 + 35 + ```zig 36 + var buf: std.ArrayList(u8) = .{}; // zero-init, no allocator needed yet 37 + ``` 38 + 39 + vs the old managed pattern which required an allocator upfront. 40 + 41 + ## writer 42 + 43 + the writer also needs the allocator: 44 + 45 + ```zig 46 + const w = buf.writer(alloc); 47 + try w.writeAll("data"); 48 + try w.print("{s}: {d}", .{ name, value }); 49 + ``` 50 + 51 + see: [leaflet-search/backend/src/server.zig#L129](https://tangled.sh/@zzstoatzz.io/leaflet-search/tree/main/backend/src/server.zig#L129) 52 + 53 + ## managed still exists 54 + 55 + if you really want the old behavior: 56 + 57 + ```zig 58 + var buf = std.array_list.Managed(u8).init(alloc); 59 + defer buf.deinit(); 60 + 61 + try buf.append('x'); // no allocator argument 62 + ``` 63 + 64 + but this is marked for eventual removal. get used to passing the allocator.
+95
languages/ziglang/0.15/build.md
··· 1 + # build system 2 + 3 + 0.15 changed how you declare module dependencies in build.zig. the new pattern uses `createModule` with an `imports` array. 4 + 5 + ## executable with dependencies 6 + 7 + ```zig 8 + const websocket = b.dependency("websocket", .{ 9 + .target = target, 10 + .optimize = optimize, 11 + }); 12 + 13 + const exe = b.addExecutable(.{ 14 + .name = "myapp", 15 + .root_module = b.createModule(.{ 16 + .root_source_file = b.path("src/main.zig"), 17 + .target = target, 18 + .optimize = optimize, 19 + .imports = &.{ 20 + .{ .name = "websocket", .module = websocket.module("websocket") }, 21 + }, 22 + }), 23 + }); 24 + ``` 25 + 26 + the key difference from pre-0.15: 27 + - use `root_module` with `createModule()` instead of just `root_source_file` 28 + - dependencies go in the `imports` array, not via `addModule()` calls 29 + 30 + see: [music-atmosphere-feed/build.zig](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/build.zig) 31 + 32 + ## library module 33 + 34 + for a library that exposes a module to consumers: 35 + 36 + ```zig 37 + const mod = b.addModule("zql", .{ 38 + .root_source_file = b.path("src/root.zig"), 39 + .target = target, 40 + .optimize = optimize, 41 + }); 42 + 43 + const tests = b.addTest(.{ .root_module = mod }); 44 + ``` 45 + 46 + the module name ("zql") is what consumers use in their `imports` array. 47 + 48 + see: [zql/build.zig](https://tangled.sh/@zzstoatzz.io/zql/tree/main/build.zig) 49 + 50 + ## build.zig.zon 51 + 52 + dependencies are declared in build.zig.zon: 53 + 54 + ```zig 55 + .{ 56 + .name = .my_project, 57 + .version = "0.0.1", 58 + .minimum_zig_version = "0.15.0", 59 + .dependencies = .{ 60 + .websocket = .{ 61 + .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.tar.gz", 62 + .hash = "...", 63 + }, 64 + }, 65 + } 66 + ``` 67 + 68 + to get the hash, run `zig build` with a wrong hash - it'll tell you the correct one. 69 + 70 + ## linking system libraries 71 + 72 + for sqlite, link both libc and the system library: 73 + 74 + ```zig 75 + exe.linkLibC(); 76 + exe.linkSystemLibrary("sqlite3"); 77 + ``` 78 + 79 + see: [music-atmosphere-feed/build.zig#L30-31](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/build.zig#L30) 80 + 81 + ## run step 82 + 83 + ```zig 84 + const run_cmd = b.addRunArtifact(exe); 85 + run_cmd.step.dependOn(b.getInstallStep()); 86 + 87 + if (b.args) |args| { 88 + run_cmd.addArgs(args); 89 + } 90 + 91 + const run_step = b.step("run", "Run the server"); 92 + run_step.dependOn(&run_cmd.step); 93 + ``` 94 + 95 + this lets you do `zig build run -- --port 8080`.
+109
languages/ziglang/0.15/comptime.md
··· 1 + # comptime 2 + 3 + zig's comptime is where it gets interesting. you can generate types, validate inputs, and unroll loops at compile time. 4 + 5 + ## type-returning functions 6 + 7 + the pattern: a function that takes comptime parameters and returns a `type`. the returned type is a struct with fields and methods derived from the input. 8 + 9 + ```zig 10 + pub fn Query(comptime sql: []const u8) type { 11 + comptime { 12 + const parsed = parser.parse(sql); 13 + return struct { 14 + pub const raw = sql; 15 + pub const params: []const []const u8 = parsed.params[0..parsed.params_len]; 16 + pub const columns: []const []const u8 = parsed.columns[0..parsed.columns_len]; 17 + 18 + pub fn bind(args: anytype) BindTuple(@TypeOf(args)) { 19 + // ... 20 + } 21 + }; 22 + } 23 + } 24 + ``` 25 + 26 + usage: 27 + 28 + ```zig 29 + const Q = Query("SELECT id, name FROM users WHERE age > :min_age"); 30 + // Q.params = ["min_age"] 31 + // Q.columns = ["id", "name"] 32 + ``` 33 + 34 + the entire SQL is parsed at compile time. no runtime overhead, and typos in parameter names are compile errors. 35 + 36 + see: [zql/src/Query.zig](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig) 37 + 38 + ## inline for 39 + 40 + `inline for` unrolls the loop at compile time. each iteration becomes separate code: 41 + 42 + ```zig 43 + inline for (params) |p| { 44 + if (!hasField(fields, p)) { 45 + @compileError("missing param :" ++ p); 46 + } 47 + } 48 + ``` 49 + 50 + this checks at compile time that your args struct has all required SQL parameters. missing one? compile error with the exact parameter name. 51 + 52 + see: [zql/src/Query.zig#L23](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L23) 53 + 54 + ## reflection 55 + 56 + access struct fields at comptime with `@typeInfo`: 57 + 58 + ```zig 59 + const fields = @typeInfo(Args).@"struct".fields; 60 + inline for (fields) |f| { 61 + @field(result, f.name) = @field(args, f.name); 62 + } 63 + ``` 64 + 65 + note the `@"struct"` syntax - struct is a keyword, so it needs quoting. 66 + 67 + this pattern maps an arbitrary struct's fields to another structure, checking types and names at compile time. 68 + 69 + see: [zql/src/Query.zig#L55](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L55) 70 + 71 + ## compile-time validation 72 + 73 + `@compileError` stops compilation with a message: 74 + 75 + ```zig 76 + if (!hasColumn(f.name)) { 77 + @compileError("struct field '" ++ f.name ++ "' not found in query columns"); 78 + } 79 + ``` 80 + 81 + this is how you make invalid states unrepresentable - if your code compiles, your query parameters match your struct fields. 82 + 83 + ## branch quota 84 + 85 + complex comptime parsing can hit the default branch quota (1000 backwards branches). increase it: 86 + 87 + ```zig 88 + @setEvalBranchQuota(sql.len * 100); 89 + ``` 90 + 91 + put this at the start of your comptime block. without it, complex SQL parsing fails with "evaluation exceeded maximum branch quota." 92 + 93 + see: [zql/src/parse.zig#L48](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/parse.zig#L48) 94 + 95 + ## why this matters 96 + 97 + the zql library parses SQL at compile time and generates type-safe bindings: 98 + 99 + ```zig 100 + const Q = Query("INSERT INTO users (name, age) VALUES (:name, :age)"); 101 + 102 + // this is checked at compile time: 103 + const args = Q.bind(.{ .name = "alice", .age = 25 }); 104 + // args is a tuple: ("alice", 25) in parameter order 105 + 106 + // if you typo .nme instead of .name, compile error 107 + ``` 108 + 109 + no runtime SQL parsing, no string concatenation, no injection vulnerabilities (because the SQL is a comptime string literal).
+107
languages/ziglang/0.15/concurrency.md
··· 1 + # concurrency 2 + 3 + zig's concurrency primitives are in `std.Thread`. no async/await - just threads, mutexes, and atomics. 4 + 5 + ## thread pool 6 + 7 + for handling concurrent connections without spawning unbounded threads: 8 + 9 + ```zig 10 + var pool: Thread.Pool = undefined; 11 + try pool.init(.{ 12 + .allocator = allocator, 13 + .n_jobs = 16, 14 + }); 15 + defer pool.deinit(); 16 + 17 + // in accept loop: 18 + pool.spawn(handleConnection, .{conn}) catch |err| { 19 + conn.stream.close(); // cleanup on spawn failure 20 + }; 21 + ``` 22 + 23 + the pool maintains a fixed number of worker threads. `spawn` queues work; if the pool is busy, it blocks or fails depending on configuration. 24 + 25 + see: [music-atmosphere-feed/src/main.zig#L29](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L29) 26 + 27 + ## background threads 28 + 29 + for long-running tasks like stream consumers: 30 + 31 + ```zig 32 + const thread = try Thread.spawn(.{}, consumer, .{allocator}); 33 + defer thread.join(); // wait for completion on shutdown 34 + ``` 35 + 36 + the third argument is a tuple of arguments passed to the function. `join()` blocks until the thread exits. 37 + 38 + see: [music-atmosphere-feed/src/main.zig#L25](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L25) 39 + 40 + ## mutex 41 + 42 + for protecting shared state: 43 + 44 + ```zig 45 + const State = struct { 46 + mutex: Thread.Mutex = .{}, 47 + data: SomeData, 48 + }; 49 + 50 + state.mutex.lock(); 51 + defer state.mutex.unlock(); 52 + // critical section - only one thread at a time 53 + ``` 54 + 55 + always use `defer` for unlock. if you return early or error, the mutex still unlocks. 56 + 57 + see: [find-bufo/bot/src/main.zig#L102](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L102) 58 + 59 + ## atomics 60 + 61 + for lock-free counters and flags: 62 + 63 + ```zig 64 + const Stats = struct { 65 + messages: std.atomic.Value(u64) = .init(0), 66 + }; 67 + 68 + // increment (returns previous value) 69 + _ = self.messages.fetchAdd(1, .monotonic); 70 + 71 + // read 72 + const count = self.messages.load(.monotonic); 73 + 74 + // write 75 + self.messages.store(42, .monotonic); 76 + ``` 77 + 78 + `.monotonic` is the memory ordering - sufficient for simple counters. use stricter orderings (`.acquire`, `.release`, `.seq_cst`) when you need synchronization guarantees between threads. 79 + 80 + see: [music-atmosphere-feed/src/stats.zig#L12](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/stats.zig#L12) 81 + 82 + ## exponential backoff 83 + 84 + for reconnection loops that don't hammer servers: 85 + 86 + ```zig 87 + var backoff: u64 = 1; 88 + const max_backoff: u64 = 60; 89 + 90 + while (true) { 91 + connect() catch |err| { 92 + std.debug.print("error: {}, retry in {}s\n", .{ err, backoff }); 93 + }; 94 + posix.nanosleep(backoff, 0); 95 + backoff = @min(backoff * 2, max_backoff); 96 + } 97 + ``` 98 + 99 + starts at 1 second, doubles each failure, caps at 60 seconds. simple and effective. 100 + 101 + see: [music-atmosphere-feed/src/jetstream.zig#L22](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/jetstream.zig#L22) 102 + 103 + ## no async 104 + 105 + zig removed async/await. the reasoning: it added complexity without clear benefits over threads for most use cases. if you need high concurrency, use a thread pool or io_uring (linux) / kqueue (macos). 106 + 107 + for bluesky bots and feed generators, a thread pool with 16 workers handles thousands of concurrent connections fine.
+105
languages/ziglang/0.15/io.md
··· 1 + # i/o interfaces 2 + 3 + 0.15 completely rewrote `std.io.Reader` and `std.io.Writer`. this was called "writergate" (referencing the earlier "allocgate" breaking change). the [release notes](https://ziglang.org/download/0.15.1/release-notes.html) acknowledge it as "extremely breaking" but necessary. 4 + 5 + ## what changed 6 + 7 + the old interfaces were generic (`anytype`), which forced every function that touched a reader/writer to also be generic. this "poisoned" APIs throughout codebases. 8 + 9 + the new interfaces are concrete types with explicit buffers. the buffer is part of the interface, not the implementation. 10 + 11 + ## the pattern 12 + 13 + you now create explicit buffers and pass them to readers/writers: 14 + 15 + ```zig 16 + var read_buffer: [8192]u8 = undefined; 17 + var write_buffer: [8192]u8 = undefined; 18 + 19 + var reader = conn.stream.reader(&read_buffer); 20 + var writer = conn.stream.writer(&write_buffer); 21 + 22 + var server = http.Server.init(reader.interface(), &writer.interface); 23 + ``` 24 + 25 + notice: 26 + - buffers are stack-allocated arrays you own 27 + - `.interface()` extracts the concrete interface type 28 + - http.Server no longer depends on `std.net` - it just takes interfaces 29 + 30 + see: [music-atmosphere-feed/src/http.zig#L14-21](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/http.zig#L14) 31 + 32 + ## why this matters 33 + 34 + before 0.15, if you wrote a function that accepted a writer, you'd write: 35 + 36 + ```zig 37 + fn writeStuff(writer: anytype) !void { ... } 38 + ``` 39 + 40 + now you write: 41 + 42 + ```zig 43 + fn writeStuff(writer: *std.Io.Writer) !void { ... } 44 + ``` 45 + 46 + concrete types mean better error messages, actual type checking, and no generic explosion through your codebase. 47 + 48 + ## flushing 49 + 50 + with buffered i/o, you need to flush explicitly. the buffer accumulates writes and only sends when full or flushed: 51 + 52 + ```zig 53 + try writer.print("hello", .{}); 54 + try writer.flush(); // actually sends 55 + ``` 56 + 57 + forgetting to flush is a common bug - data sits in the buffer forever. 58 + 59 + ## allocating writer 60 + 61 + for dynamic output (like building json responses), use an allocating writer: 62 + 63 + ```zig 64 + var aw: std.Io.Writer.Allocating = .init(allocator); 65 + defer aw.deinit(); 66 + 67 + const result = client.fetch(.{ 68 + .response_writer = &aw.writer, 69 + ... 70 + }); 71 + 72 + const response = aw.toArrayList().items; 73 + ``` 74 + 75 + see: [find-bufo/bot/src/main.zig#L202-220](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L202) 76 + 77 + ## fixed writer 78 + 79 + for writing into a fixed-size buffer: 80 + 81 + ```zig 82 + var buf: [256]u8 = undefined; 83 + var w: std.Io.Writer = .fixed(&buf); 84 + try w.print("{s}: {d}", .{ name, value }); 85 + ``` 86 + 87 + ## tls reading quirk 88 + 89 + when reading from tls streams, you must loop until data arrives. a return of 0 doesn't mean EOF - it means "try again": 90 + 91 + ```zig 92 + outer: while (total_read < response_buf.len) { 93 + var w: std.Io.Writer = .fixed(response_buf[total_read..]); 94 + while (true) { 95 + const n = tls_client.reader.stream(&w, .limited(remaining)) catch break :outer; 96 + if (n != 0) { 97 + total_read += n; 98 + break; 99 + } 100 + // n == 0 means keep trying, not EOF 101 + } 102 + } 103 + ``` 104 + 105 + this pattern comes from [websocket.zig](https://github.com/karlseguin/websocket.zig). without the inner loop, you'll read 0 bytes and think you're done.