add orienting language to 0.15 notes

make each file more accessible - explain what the thing is for,
why you'd care, and what the patterns mean in context.

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

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

Changed files
+82 -50
languages
+15 -11
languages/ziglang/0.15/arraylist.md
··· 1 1 # arraylist 2 2 3 - 0.15 made arraylist unmanaged - allocator passed to each method. the compiler catches missing allocators immediately, so that's not worth documenting. what matters is ownership. 3 + `ArrayList` is zig's growable buffer - you use it when you don't know the size upfront. common for building strings, collecting results, or accumulating data before sending it somewhere. 4 + 5 + in 0.15, arraylist became "unmanaged" - you pass the allocator to each method instead of storing it in the struct. the compiler catches missing allocators immediately, so that's not the tricky part. the tricky part is ownership. 4 6 5 7 ## ownership patterns 6 8 7 - **build and discard** - most common. defer cleanup, use `.items` to borrow: 9 + when you build up data in an arraylist, you eventually need to do something with it. there are two paths: 10 + 11 + **build and discard** - you use the data, then throw it away. this is most common (e.g., building an http response): 8 12 9 13 ```zig 10 14 var buf: std.ArrayList(u8) = .empty; 11 - defer buf.deinit(alloc); 15 + defer buf.deinit(alloc); // cleanup when we're done 12 16 13 17 try buf.print(alloc, "{s}: {d}", .{ name, value }); 14 - sendResponse(buf.items); // borrow the slice 18 + sendResponse(buf.items); // borrow the slice, arraylist still owns it 15 19 ``` 16 20 17 - **build and return** - transfer ownership, no defer: 21 + **build and return** - you're building something to give to a caller. they'll own the memory: 18 22 19 23 ```zig 20 24 var buf: std.ArrayList(u8) = .empty; 21 - // no defer - caller owns the memory 25 + // no defer here - we're transferring ownership 22 26 23 27 try buf.appendSlice(alloc, data); 24 - return buf.toOwnedSlice(alloc); 28 + return buf.toOwnedSlice(alloc); // caller must free this 25 29 ``` 26 30 27 - the difference: `.items` borrows (arraylist still owns the memory), `.toOwnedSlice()` transfers (caller must free). 31 + the key difference: `.items` gives you a view into the arraylist's memory (it still owns it). `.toOwnedSlice()` hands ownership to you (arraylist forgets about it, you must free it). 28 32 29 33 see: [dashboard.zig#L187](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/dashboard.zig#L187) for the return pattern 30 34 31 35 ## direct methods vs writer 32 36 33 - arraylist has `.print()` directly - you don't always need a writer: 37 + you might think you need to get a writer to write formatted output, but arraylist has `.print()` built in: 34 38 35 39 ```zig 36 40 try buf.print(alloc, "{{\"count\":{d}}}", .{count}); 37 41 ``` 38 42 39 - use `.writer(alloc)` when you need to pass to something expecting `std.Io.Writer`: 43 + use `.writer(alloc)` when you need to pass to something that expects a generic `std.Io.Writer`: 40 44 41 45 ```zig 42 46 const w = buf.writer(alloc); ··· 45 49 46 50 ## why unmanaged 47 51 48 - from the [release notes](https://ziglang.org/download/0.15.1/release-notes.html): storing the allocator had costs - worse method signatures for reservations, can't statically initialize, extra memory for nested containers. the benefits (convenience, avoiding wrong allocator) didn't justify it since the allocator is always nearby. 52 + from the [release notes](https://ziglang.org/download/0.15.1/release-notes.html): storing the allocator had costs - worse method signatures for reservations, can't statically initialize, extra memory for nested containers. the benefits (convenience, avoiding wrong allocator) didn't justify it since the allocator is always nearby anyway.
+15 -3
languages/ziglang/0.15/build.md
··· 1 1 # build 2 2 3 + `build.zig` is where you configure how your project compiles - what files to include, what dependencies to pull in, what artifacts to produce. zig's build system is written in zig itself, so it's just code. 4 + 3 5 ## 0.15 change 4 6 5 - pre-0.15 used `exe.addModule()`. now use `createModule` with `imports` array: 7 + the way you attach dependencies to your executable changed. before 0.15, you'd call `exe.addModule()` after creating the executable. now you declare everything upfront in a `createModule` call with an `imports` array: 6 8 7 9 ```zig 8 10 const exe = b.addExecutable(.{ ··· 18 20 }); 19 21 ``` 20 22 23 + the `.name` in the imports array is what you'll use in your code: `@import("websocket")`. 24 + 21 25 ## dependency hash trick 22 26 23 - to get the hash for build.zig.zon, run `zig build` with a wrong hash. it tells you the correct one: 27 + dependencies are declared in `build.zig.zon` with a hash for verification. to get the correct hash for a new dependency, just put any placeholder hash and run `zig build`. the error message tells you what the hash should be: 24 28 25 29 ``` 26 30 error: hash mismatch... expected 1220abc..., found 1220def... 27 31 ``` 28 32 33 + copy the "found" value into your .zon file. 34 + 29 35 ## don't forget 30 36 31 - `b.installArtifact(exe)` - without this, `zig build` produces nothing. 37 + after creating your executable, you need to tell zig to actually install it: 38 + 39 + ```zig 40 + b.installArtifact(exe); 41 + ``` 42 + 43 + without this line, `zig build` runs successfully but produces no output. easy to miss.
+17 -12
languages/ziglang/0.15/comptime.md
··· 1 1 # comptime 2 2 3 - comptime lets you generate types, validate inputs, and catch errors at compile time. for a complete example, see [zql](https://tangled.sh/@zzstoatzz.io/zql) which parses SQL at comptime and generates type-safe bindings. 3 + zig runs code at compile time. not just constants - actual logic, loops, conditionals. you can generate types, validate inputs, and catch errors before your program ever runs. 4 + 5 + the payoff: things that would be runtime checks in other languages become compile errors in zig. if your code compiles, certain classes of bugs are impossible. 6 + 7 + for a complete example, see [zql](https://tangled.sh/@zzstoatzz.io/zql) - it parses SQL at compile time and generates type-safe bindings. typo in a parameter name? compile error. 4 8 5 9 ## type-returning functions 6 10 7 - a function that takes comptime params and returns a `type`: 11 + the core pattern: a function that takes comptime parameters and returns a `type`. you're generating a struct definition: 8 12 9 13 ```zig 10 14 pub fn Wrapper(comptime T: type) type { ··· 18 22 } 19 23 ``` 20 24 21 - `@This()` refers to the struct being defined - necessary since the struct is anonymous. 25 + `@This()` refers to the struct being defined - you need it because the struct doesn't have a name (it's an anonymous struct returned from a function). 22 26 23 27 ## generating tuple types from struct fields 24 28 25 - extract field types in a specific order to build a tuple: 29 + sometimes you need to reorder or extract types from a struct. this pattern builds a tuple type by pulling field types in a specific order: 26 30 27 31 ```zig 28 32 fn BindTuple(comptime Args: type, comptime param_names: []const []const u8) type { ··· 41 45 } 42 46 ``` 43 47 44 - this reorders struct fields into a tuple matching the parameter order. useful for binding named args to positional parameters. 48 + use case: you have named arguments (`.{ .name = "alice", .age = 25 }`) but need to bind them to positional SQL parameters in a specific order. 45 49 46 50 see: [zql/src/Query.zig#L78](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L78) 47 51 48 52 ## compile-time validation 49 53 50 - `@compileError` stops compilation with a message: 54 + `@compileError` stops compilation with a custom message. combine with `inline for` to check things at compile time: 51 55 52 56 ```zig 53 57 inline for (required_fields) |name| { ··· 57 61 } 58 62 ``` 59 63 60 - if your code compiles, it's valid. invalid states are unrepresentable. 64 + if someone forgets a required field, they get a compile error pointing at exactly what's missing. 61 65 62 66 ## branch quota 63 67 64 - complex comptime parsing hits the default branch quota (1000 backwards branches). scale it with input: 68 + zig limits how much work comptime code can do (prevents infinite loops from hanging compilation). the default is 1000 "backwards branches" (loops, recursion). for complex parsing, you'll hit this: 65 69 66 70 ```zig 67 71 @setEvalBranchQuota(input.len * 100); 68 72 ``` 69 73 70 - without this, complex parsing fails with "evaluation exceeded maximum branch quota." 74 + scale it with your input size so small inputs compile fast and large inputs still work. 71 75 72 76 see: [zql/src/parse.zig#L48](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/parse.zig#L48) 73 77 74 78 ## constraints 75 79 76 - - no allocation at comptime - use fixed-size arrays 77 - - no runtime values - everything must be known at compile time 78 - - comptime code runs during compilation, adding build time 80 + a few things to know: 81 + - no allocation at comptime - you can't call an allocator, so use fixed-size arrays 82 + - no runtime values - everything must be known at compile time (that's the point) 83 + - comptime code runs during compilation, so complex logic adds build time
+19 -12
languages/ziglang/0.15/concurrency.md
··· 1 1 # concurrency 2 2 3 - zig has threads, mutexes, and atomics. no async/await. for syntax, see std.Thread docs. these notes cover design decisions. 3 + when your program needs to do multiple things at once - handle many connections, run background tasks, update stats while processing requests. zig gives you threads, mutexes, and atomics. no async/await. 4 + 5 + these notes focus on design decisions, not syntax. for api details, see std.Thread docs. 4 6 5 7 ## when to use atomics vs mutex 6 8 7 - **atomics for simple counters:** 9 + you often need to share state between threads. the question is how to protect it. 10 + 11 + **atomics** are for simple counters - things where each operation is independent: 8 12 9 13 ```zig 10 14 posts_checked: std.atomic.Value(u64) = .init(0), 11 15 16 + // in some thread: 12 17 _ = self.posts_checked.fetchAdd(1, .monotonic); 13 18 ``` 14 19 15 - **mutex for complex data structures:** 20 + **mutex** is for complex data or multi-step operations: 16 21 17 22 ```zig 18 23 bufo_matches: std.StringHashMap(MatchInfo), 19 24 bufo_mutex: Thread.Mutex = .{}, 20 25 26 + // in some thread: 21 27 self.bufo_mutex.lock(); 22 28 defer self.bufo_mutex.unlock(); 23 29 try self.bufo_matches.put(name, info); 24 30 ``` 25 31 26 - the pattern in [find-bufo/bot/src/stats.zig](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/stats.zig): atomics for the five simple counters (posts_checked, matches_found, etc.), mutex for the hashmap of per-bufo match data. 32 + the pattern in [find-bufo/bot/src/stats.zig](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/stats.zig): five simple counters use atomics (posts_checked, matches_found, etc.), but the hashmap of per-bufo match data uses a mutex. 27 33 28 - rule: if it's a single integer, use atomic. if it's a container or multi-field update, use mutex. 34 + rule of thumb: single integer that threads increment independently? atomic. anything else? mutex. 29 35 30 36 ## memory ordering 31 37 32 - all usages in these projects use `.monotonic` - sufficient for independent counters where you just need eventual visibility, not synchronization between threads. 38 + you'll see `.monotonic` everywhere in these projects. it's the weakest ordering - just means "this operation is atomic, but i don't care about ordering relative to other operations." 33 39 34 - use stricter orderings (`.acquire`, `.release`) when one thread's write must be visible to another thread before proceeding. none of these projects need that. 40 + that's fine for independent counters. you'd use stricter orderings (`.acquire`, `.release`) when one thread's write must be visible to another thread before it proceeds - like signaling that data is ready. none of these projects need that. 35 41 36 42 ## callback pattern 37 43 38 - jetstream doesn't use channels or message passing. it takes a function pointer: 44 + the jetstream client doesn't use channels or complicated message passing. it just takes a function pointer and calls it when a message arrives: 39 45 40 46 ```zig 41 47 callback: *const fn (Post) void, 42 48 49 + // when a message comes in: 43 50 self.callback(.{ 44 51 .uri = uri, 45 52 .text = text, 46 53 }); 47 54 ``` 48 55 49 - simpler than channels when you just need to notify one consumer. 56 + simpler than channels when you have one producer and one consumer. the callback runs on the producer's thread, so keep it fast. 50 57 51 58 see: [find-bufo/bot/src/jetstream.zig#L18](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/jetstream.zig#L18) 52 59 53 - ## reconnection 60 + ## reconnection with backoff 54 61 55 - exponential backoff for network consumers: 62 + network connections fail. when they do, don't hammer the server - back off exponentially: 56 63 57 64 ```zig 58 65 var backoff: u64 = 1; ··· 65 72 } 66 73 ``` 67 74 68 - starts at 1s, doubles each failure, caps at 60s. 75 + starts at 1 second, doubles each failure, caps at 60 seconds. simple and effective.
+16 -12
languages/ziglang/0.15/io.md
··· 1 1 # i/o 2 2 3 - 0.15 replaced generic `anytype` reader/writer with concrete types using explicit buffers. see [release notes](https://ziglang.org/download/0.15.1/release-notes.html) for the rationale. 3 + reading and writing data - files, sockets, http. zig 0.15 overhauled this entirely, replacing generic `anytype` interfaces with concrete types that use explicit buffers. the [release notes](https://ziglang.org/download/0.15.1/release-notes.html) explain why (better error messages, no generic pollution, clearer ownership). 4 + 5 + the main thing to know: you provide the buffers, and you call `.interface()` to get the type that APIs expect. 4 6 5 - ## http server pattern 7 + ## http server 8 + 9 + when handling incoming connections, you set up buffers for reading requests and writing responses: 6 10 7 11 ```zig 8 12 var read_buffer: [8192]u8 = undefined; ··· 14 18 var server = http.Server.init(reader.interface(), &writer.interface); 15 19 ``` 16 20 17 - the buffers are yours - stack allocated, explicit size. `.interface()` extracts the concrete type that http.Server expects. 21 + you own these buffers (they're on your stack). the http.Server borrows them. `.interface()` extracts the concrete reader/writer type. 18 22 19 23 see: [http.zig#L14](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/http.zig#L14) 20 24 21 - ## http client pattern 25 + ## http client 22 26 23 - for api calls, use `Io.Writer.Allocating` to collect the response: 27 + when making outgoing requests (calling APIs, fetching data), use `Io.Writer.Allocating` to collect the response body: 24 28 25 29 ```zig 26 30 var client = http.Client{ .allocator = allocator }; ··· 36 40 37 41 if (result.status != .ok) return error.FetchFailed; 38 42 39 - const response = aw.toArrayList().items; 43 + const response = aw.toArrayList().items; // the response body 40 44 ``` 45 + 46 + the allocating writer grows as needed to hold whatever the server sends back. 41 47 42 48 see: [find-bufo/bot/src/main.zig#L196](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L196) 43 49 44 50 ## tls reading quirk 45 51 46 - when reading from raw tls (not http.Client), you must loop until data arrives. `n == 0` means "try again", not EOF: 52 + if you're doing raw tls (not using http.Client), there's a gotcha: when reading, `n == 0` doesn't mean end-of-stream. it means "i consumed some input but don't have output yet" - tls may be buffering partial records or handling renegotiation. you have to keep trying: 47 53 48 54 ```zig 49 55 outer: while (total_read < response_buf.len) { ··· 54 60 total_read += n; 55 61 break; 56 62 } 57 - // n == 0: tls may have consumed input without producing output 58 - // (buffering partial records, renegotiation, etc.) 63 + // n == 0: keep trying, tls isn't done yet 59 64 } 60 65 } 61 66 ``` 62 67 63 - this happens because tls decryption can consume input bytes without producing output yet. the inner loop keeps trying until actual data appears. 68 + also, raw tls needs explicit flushes at both the tls layer and the underlying stream: 64 69 65 - also: raw tls needs explicit flush at both layers: 66 70 ```zig 67 71 tls_client.writer.flush() catch return error.Failed; 68 72 stream_writer.interface.flush() catch return error.Failed; ··· 72 76 73 77 ## when you don't need to flush 74 78 75 - high-level apis (http.Server, http.Client) handle flushing internally. `request.respond()` flushes for you. only raw tls/stream code needs explicit flushes. 79 + the high-level apis handle this for you. `http.Server`'s `request.respond()` flushes internally. `http.Client` flushes when the request completes. you only need manual flushes when working with raw streams or tls directly.