about things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

0.16: testing.md + comptime.md from leaflet-search/zug work

testing.md: zig build test silently runs zero tests when blocks live
in non-root modules and only re-exports point at them. document the
test { _ = @import(...) } pattern, the verify-by-deliberate-failure
trick, and --summary all for spotting zero-discovery regressions.

comptime.md: string-type detection for anytype adapters (literals
and @tagName return *const [N:0]u8, not []const u8) and the
comptime-sql escape hatch — when adopting an external library that
builds sql at runtime, you need a parallel runtime path; don't try
to stringify through the comptime api.

both come out of leaflet-search adopting zug as a sqlite migration
runner: the test-discovery rule had been silently skipping ~5 test
blocks for months, and the @tagName type bit me when testing an
anytype adapter with pre-coerced []const u8 args.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

+108
+2
languages/ziglang/0.16/README.md
··· 10 10 - [synchronization](./io/synchronization.md) — Mutex, Condition, CancelProtection, cancellation model 11 11 - [patterns](./io/patterns.md) — backend selection, InitOptions, debug_io, long-lived tasks, networking 12 12 - [migration](./migration.md) — practical API changes from 0.15, verified patterns 13 + - [comptime](./comptime.md) — string-type detection for anytype adapters, comptime-sql escape hatches 14 + - [testing](./testing.md) — test discovery in non-root modules, the `_ = @import` pattern
+55
languages/ziglang/0.16/comptime.md
··· 1 + # comptime 2 + 3 + patterns that came up while wiring `anytype` adapters and comptime-validated SQL clients in 0.16. for the broader comptime intro see [0.15/comptime.md](../0.15/comptime.md) — the basics there still apply. 4 + 5 + ## detecting string-like types 6 + 7 + `@TypeOf` returns different concrete types for things that all conceptually look like strings: 8 + 9 + | source | `@TypeOf` result | 10 + |-----------------------------------|-----------------------| 11 + | `"hello"` | `*const [5:0]u8` | 12 + | `@tagName(MyEnum.foo)` | `*const [3:0]u8` | 13 + | `[]const u8` field / parameter | `[]const u8` | 14 + | `[:0]const u8` field / parameter | `[:0]const u8` | 15 + 16 + `==` against any one type misses the others: 17 + 18 + ```zig 19 + if (T == []const u8) // misses string literals AND @tagName 20 + if (T == *const [N:0]u8) // misses slices, also requires knowing N 21 + ``` 22 + 23 + walk the typeinfo for a generic predicate: 24 + 25 + ```zig 26 + fn isU8StringLike(comptime T: type) bool { 27 + const info = @typeInfo(T); 28 + if (info != .pointer) return false; 29 + if (info.pointer.size == .slice and info.pointer.child == u8) return true; 30 + if (info.pointer.size == .one) { 31 + const child = @typeInfo(info.pointer.child); 32 + if (child == .array and child.array.child == u8) return true; 33 + } 34 + return false; 35 + } 36 + ``` 37 + 38 + then coerce to `[]const u8` at the boundary — `const s: []const u8 = v;` works for all four shapes. 39 + 40 + caught me when wrapping `anytype` args from an external library: tests passed because they used pre-coerced `const id: []const u8 = ...`, but the real call site bound `@tagName(class)` and would have compile-failed only at integration time. lesson: when testing an `anytype` adapter, pass the *exact* shapes the caller will pass, not pre-coerced equivalents. 41 + 42 + ## comptime sql constrains runtime adoption 43 + 44 + a `comptime sql: []const u8` parameter (used for placeholder counting via `@compileError` — see [0.15/comptime.md#simple-string-validation](../0.15/comptime.md#simple-string-validation)) means that function cannot be called with any runtime-built sql. when adopting an external library that builds sql at runtime — migration runners, query builders, dynamic table-name queries — you need a parallel runtime path: 45 + 46 + ```zig 47 + pub fn exec(self: *Client, comptime sql: []const u8, args: anytype) !void { ... } // existing 48 + pub fn execRuntime(self: *Client, sql: []const u8, args: []const Value) !void { ... } // new 49 + ``` 50 + 51 + both can share the same wire / http layer underneath via a single private `doRequest` helper. don't try to route the runtime path through the comptime api via stringification — the type system stops you for a reason (placeholder validation only works at comptime). 52 + 53 + real cost: `leaflet-search`'s `db/Client.zig` had `query`/`exec` taking `comptime sql`, which forced a 5-arm `switch` over an enum just to call a different sql string per branch in the timeline endpoint, and would have blocked [zug](https://tangled.sh/@zzstoatzz.io/zug) (sqlite migration runner) adoption entirely. adding a runtime escape hatch fixed both. 54 + 55 + source: [leaflet-search/db/Client.zig](https://tangled.sh/@zzstoatzz.io/leaflet-search/tree/main/backend/src/db/Client.zig) — `RuntimeValue`, `execRuntime`, `queryRuntime`.
+51
languages/ziglang/0.16/testing.md
··· 1 + # testing 2 + 3 + `zig build test` reliably *compiles* test blocks, but only *runs* the ones the test root actually analyzes. easy to lose tests silently. 4 + 5 + ## test discovery in non-root modules 6 + 7 + re-exporting types is not enough — the rest of the imported file stays dormant: 8 + 9 + ```zig 10 + // in db.zig (transitively imported from main.zig) 11 + pub const Foo = @import("foo.zig").Foo; // pulls in Foo, but NOT foo.zig's `test` blocks 12 + ``` 13 + 14 + surface them with an explicit `test { _ = @import(...) }` block somewhere in the test root's analysis graph (typically the bottom of `main.zig`): 15 + 16 + ```zig 17 + test { 18 + _ = @import("db/Client.zig"); 19 + _ = @import("db/zug_conn.zig"); 20 + _ = @import("ingest/extractor.zig"); 21 + _ = @import("server/search.zig"); 22 + } 23 + ``` 24 + 25 + without this, tests compile (so type errors still surface) but never *execute*. `zig build test` happily reports success when running zero tests. 26 + 27 + verify: temporarily break a test (`try expectEqual(@as(usize, 999), 0)`). if `zig build test` still passes, your test isn't being discovered. 28 + 29 + prefer `zig build test --summary all` for daily use — it prints `N pass (N total)` so a zero-discovery regression is visible at a glance. 30 + 31 + found the hard way in [leaflet-search](https://tangled.sh/@zzstoatzz.io/leaflet-search) — five test blocks across `extractor.zig` and `search.zig` had been silently skipping for months. 32 + 33 + ## testing allocator + leaky parsers 34 + 35 + the testing allocator catches leaks. if you use `parseFromValueLeaky` or similar "leaky" apis, wrap in an arena: 36 + 37 + ```zig 38 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 39 + defer arena.deinit(); 40 + const result = try leakyFunction(arena.allocator(), input); 41 + ``` 42 + 43 + ## expectEqual argument order 44 + 45 + argument order changed from 0.15 — expected first, actual second: 46 + 47 + ```zig 48 + try std.testing.expectEqual(@as(usize, 5), buf.len); // expected, actual 49 + ``` 50 + 51 + (also covered in [migration.md](./migration.md#stdtestingexpectequal).)