# comptime > written against zig 0.15 and 0.16 (merged). 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. 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. 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. ## type-returning functions the core pattern: a function that takes comptime parameters and returns a `type`. you're generating a struct definition: ```zig pub fn Wrapper(comptime T: type) type { return struct { value: T, pub fn get(self: @This()) T { return self.value; } }; } ``` `@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). ## generating tuple types from struct fields 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: ```zig fn BindTuple(comptime Args: type, comptime param_names: []const []const u8) type { const fields = @typeInfo(Args).@"struct".fields; var types: [param_names.len]type = undefined; inline for (param_names, 0..) |name, i| { for (fields) |f| { if (std.mem.eql(u8, f.name, name)) { types[i] = f.type; break; } } } return std.meta.Tuple(&types); } ``` use case: you have named arguments (`.{ .name = "alice", .age = 25 }`) but need to bind them to positional SQL parameters in a specific order. see: [zql/src/Query.zig#L78](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L78) ## compile-time validation `@compileError` stops compilation with a custom message. combine with `inline for` to check things at compile time: ```zig inline for (required_fields) |name| { if (!hasField(T, name)) { @compileError("missing required field: " ++ name); } } ``` if someone forgets a required field, they get a compile error pointing at exactly what's missing. ## branch quota 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: ```zig @setEvalBranchQuota(input.len * 100); ``` scale it with your input size so small inputs compile fast and large inputs still work. see: [zql/src/parse.zig#L48](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/parse.zig#L48) ## simple string validation count occurrences in a comptime string and compare to something else. useful for SQL placeholder validation: ```zig fn validateArgs(comptime sql: []const u8, comptime ArgsType: type) void { const expected = countPlaceholders(sql); const provided = @typeInfo(ArgsType).@"struct".fields.len; if (expected != provided) { @compileError(std.fmt.comptimePrint( "SQL has {} placeholders but {} args provided", .{ expected, provided }, )); } } fn countPlaceholders(comptime sql: []const u8) usize { var count: usize = 0; for (sql) |c| { if (c == '?') count += 1; } return count; } // usage - compile error if mismatch pub fn query(self: *Client, comptime sql: []const u8, args: anytype) !Result { comptime validateArgs(sql, @TypeOf(args)); // ... } ``` the key: `comptime` on the sql parameter means the string is known at compile time, so you can iterate it and count characters. `@TypeOf(args)` gets the tuple type, and you can inspect its fields. see: [pub-search/db/Client.zig](https://tangled.sh/@zzstoatzz.io/pub-search/tree/main/backend/src/db/Client.zig) (pub-search is a search service over atproto publications) ## constraints a few things to know: - no allocation at comptime - you can't call an allocator, so use fixed-size arrays - no runtime values - everything must be known at compile time (that's the point) - comptime code runs during compilation, so complex logic adds build time ## detecting string-like types `@TypeOf` returns different concrete types for things that all conceptually look like strings: | source | `@TypeOf` result | |-----------------------------------|-----------------------| | `"hello"` | `*const [5:0]u8` | | `@tagName(MyEnum.foo)` | `*const [3:0]u8` | | `[]const u8` field / parameter | `[]const u8` | | `[:0]const u8` field / parameter | `[:0]const u8` | `==` against any one type misses the others: ```zig if (T == []const u8) // misses string literals AND @tagName if (T == *const [N:0]u8) // misses slices, also requires knowing N ``` walk the typeinfo for a generic predicate: ```zig fn isU8StringLike(comptime T: type) bool { const info = @typeInfo(T); if (info != .pointer) return false; if (info.pointer.size == .slice and info.pointer.child == u8) return true; if (info.pointer.size == .one) { const child = @typeInfo(info.pointer.child); if (child == .array and child.array.child == u8) return true; } return false; } ``` then coerce to `[]const u8` at the boundary — `const s: []const u8 = v;` works for all four shapes. 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. ## comptime sql constrains runtime adoption a `comptime sql: []const u8` parameter (used for placeholder counting via `@compileError` — see [simple string validation](#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: ```zig pub fn exec(self: *Client, comptime sql: []const u8, args: anytype) !void { ... } // existing pub fn execRuntime(self: *Client, sql: []const u8, args: []const Value) !void { ... } // new ``` 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). real cost: `pub-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. source: [pub-search/db/Client.zig](https://tangled.sh/@zzstoatzz.io/pub-search/tree/main/backend/src/db/Client.zig) — `RuntimeValue`, `execRuntime`, `queryRuntime`. ## comptime inventories a top-level `const` array of structs is a comptime-known inventory: the compiler type-checks every entry, dead fields are impossible, and anything derived from it (routing tables, docs pages, generated clients) is proven in sync with the data at build time because it *is* the data. the pattern beats registration-at-runtime whenever the full set is known at build time — the array is the single source of truth and `inline for` fans it out. ## sources - zql — src/Query.zig, src/parse.zig - pub-search — backend/src/db/Client.zig, RuntimeValue/execRuntime escape hatch