zig-skills/references/comptime.md

469 lines
13 KiB
Markdown

# Comptime Reference
Zig's comptime system enables metaprogramming through partial evaluation and type reflection. This reference covers comptime fundamentals, type reflection, and common techniques.
## Table of Contents
- [Fundamentals](#fundamentals)
- [Type Reflection](#type-reflection)
- [Loop Variants](#loop-variants)
- [Branch Elimination](#branch-elimination)
- [Type Generation](#type-generation)
- [Limitations](#limitations)
---
## Fundamentals
### Comptime Parameters
Values that must be known at compile time. Types are always comptime.
```zig
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
const result = max(i32, 5, 10); // T=i32 known at compile time
```
A `comptime` parameter means:
- At the callsite, the value must be known at compile time
- In the function definition, the value is comptime-known
### Comptime Variables
Variables whose loads/stores happen at compile time.
```zig
comptime var i: usize = 0;
inline while (i < 3) : (i += 1) {
// i is comptime-known each iteration
}
```
### Comptime Blocks
Force expression evaluation at compile time.
```zig
const primes = comptime blk: {
var result: [10]u32 = undefined;
// ... compute primes ...
break :blk result;
};
comptime {
// All code here runs at compile time
if (@sizeOf(MyStruct) > 64) @compileError("too large");
}
```
### Container-Level Comptime
Top-level constants whose values must be compile-time known are evaluated on demand at compile time. This does not make every top-level declaration a `comptime` block or imply eager evaluation.
```zig
// These are computed at compile time automatically
const lookup_table = generateTable();
const config = parseConfig(@embedFile("config.json"));
```
---
## Type Reflection
### Builtins
| Builtin | Purpose |
|---------|---------|
| `@typeInfo(T)` | Get type metadata as `std.builtin.Type` |
| `@Int` / `@Struct` / `@Union` / `@Enum` / `@Pointer` / `@Fn` / `@Tuple` | Zig 0.16 type-construction builtins replacing removed `@Type` |
| `@TypeOf(expr)` | Get type of expression |
| `@typeName(T)` | Get type name as `[:0]const u8` |
| `@hasDecl(T, name)` | Check if type has declaration |
| `@hasField(T, name)` | Check if type has field |
| `@field(value, name)` | Access field by comptime-known name |
### Zig 0.16 Type Construction
`@Type` is removed in Zig 0.16. Continue using `@typeInfo` for reflection, but construct new types with the specific builtin for the container or scalar type you need:
```zig
const Id = @Int(.unsigned, 32);
const Pair = @Tuple(&.{ []const u8, Id });
```
For generated structs/unions/enums, use `@Struct`, `@Union`, and `@Enum` with separate arrays of field names, field types/values, and field attributes. Prefer plain Zig syntax when the type can be written directly.
Zig 0.16 also changed type resolution:
- Field analysis is lazier, so namespace-like types can often exist without resolving all fields.
- Some dependency-loop diagnostics changed and should be evaluated in context rather than dismissed.
- Pointers to comptime-only types may exist at runtime, but runtime dereference remains invalid.
- Explicitly aligned pointer types are distinct from naturally aligned pointer types, though they often coerce.
- Zero-bit tuple fields are no longer implicitly marked `comptime` in type info.
### Accessing Type Info for Keywords
Use `@"keyword"` syntax because `union`, `struct`, `enum` are reserved:
```zig
const union_info = @typeInfo(MyUnion).@"union";
const struct_info = @typeInfo(MyStruct).@"struct";
const enum_info = @typeInfo(MyEnum).@"enum";
const fn_info = @typeInfo(@TypeOf(myFn)).@"fn";
```
### Common Type Info Fields
**Struct:**
```zig
const info = @typeInfo(MyStruct).@"struct";
// info.fields: []const StructField
// info.decls: []const Declaration
// info.is_tuple: bool
```
**Union:**
```zig
const info = @typeInfo(MyUnion).@"union";
// info.tag_type: ?type (null if untagged)
// info.fields: []const UnionField
// info.layout: .auto, .@"extern", .@"packed"
```
**Enum:**
```zig
const info = @typeInfo(MyEnum).@"enum";
// info.tag_type: type (backing integer)
// info.fields: []const EnumField
// info.is_exhaustive: bool
```
### Creating Union Values with Comptime Tag
Use `@unionInit` when the tag is comptime-known:
```zig
const Action = union(enum) {
move: struct { x: i32, y: i32 },
jump,
attack: u32,
};
// Create union with comptime-known field name
const action = @unionInit(Action, "move", .{ .x = 10, .y = 20 });
```
---
## Loop Variants
### comptime for
Full compile-time evaluation. Can use `break` to return values. Cannot reference runtime values.
```zig
// Return value from comptime loop
fn hasField(comptime T: type, comptime name: []const u8) bool {
const fields = @typeInfo(T).@"struct".fields;
return comptime for (fields) |f| {
if (std.mem.eql(u8, f.name, name)) break true;
} else false;
}
// Computation in comptime block
fn sumComptime(comptime values: []const i32) i32 {
comptime {
var sum: i32 = 0;
for (values) |v| sum += v;
return sum;
}
}
```
**Verified in stdlib:** `std/Build.zig:1953`
### inline for
Loop unrolling with code generation. Body is duplicated per iteration and may reference runtime values. Like other loop expressions, an `inline for` can use `break` with a value when its control flow is valid.
```zig
fn printFields(value: anytype) void {
const T = @TypeOf(value);
const fields = @typeInfo(T).@"struct".fields;
// Each iteration generates separate code
inline for (fields) |field| {
const field_value = @field(value, field.name);
std.debug.print("{s} = {any}\n", .{ field.name, field_value });
}
}
// Runtime comparison via unrolling
fn eqlAny(comptime T: type, a: T, b: T) bool {
const fields = @typeInfo(T).@"struct".fields;
inline for (fields) |field| {
if (@field(a, field.name) != @field(b, field.name)) {
return false; // Runtime return
}
}
return true;
}
```
**Verified in stdlib:** `std/meta.zig:27`, `compiler_rt/fmax.zig:63`
### Decision Table
| Need | Use | Reason |
|------|-----|--------|
| Evaluate the entire loop and its result at compile time | `comptime for` | All values and control flow must be comptime-known |
| Access runtime values in body | `inline for` | Comptime can't see runtime |
| Type-level computation only | `comptime for` | Clearer intent, no code gen |
| Generate code per iteration | `inline for` | Each iteration = separate code |
| Normal runtime iteration | regular `for` | No unrolling needed |
---
## Branch Elimination
Comptime-known conditions eliminate dead branches entirely—no runtime cost.
### Basic Elimination
```zig
fn process(comptime T: type, value: T) T {
if (T == bool) {
return !value; // Only exists for bool
} else {
return value + 1; // Only exists for integers
}
}
```
### Platform-Specific Code
```zig
const builtin = @import("builtin");
const native_endian = builtin.cpu.arch.endian();
pub fn readIntBig(comptime T: type, bytes: []const u8) T {
comptime std.debug.assert(@typeInfo(T) == .int);
std.debug.assert(bytes.len >= @sizeOf(T));
const value: T = @bitCast(bytes[0..@sizeOf(T)].*);
if (comptime native_endian == .big) {
return value;
} else {
return @byteSwap(value);
}
}
```
### Propagating Across Functions
Comptime parameters specialize the function and make conditions that depend on them compile-time known. `inline fn` additionally requests call-site inlining; it is not required merely to eliminate a comptime-known branch:
```zig
// The enabled branch is specialized at compile time.
fn maybeLog(comptime enabled: bool, msg: []const u8) void {
if (enabled) std.debug.print("{s}\n", .{msg});
}
// inline additionally requests call-site inlining.
inline fn maybeLogInline(comptime enabled: bool, msg: []const u8) void {
if (comptime enabled) std.debug.print("{s}\n", .{msg});
}
pub fn example() void {
maybeLogInline(false, "debug"); // Entire call eliminated
maybeLogInline(true, "important"); // Only this generates code
}
```
**Verified in stdlib:** `std/math.zig:708`, `std/log.zig:122`
---
## Type Generation
### Returning Types from Functions
```zig
fn Pair(comptime A: type, comptime B: type) type {
return struct {
first: A,
second: B,
const Self = @This();
pub fn swap(self: Self) Pair(B, A) {
return .{ .first = self.second, .second = self.first };
}
};
}
const IntStr = Pair(i32, []const u8);
var p: IntStr = .{ .first = 42, .second = "hello" };
```
### Generating Union Subsets
Generate a subset union type from a larger union:
**Zig 0.16 note:** the following pattern is historical because it uses removed `@Type`. In new code, keep the reflection idea but build the resulting type with `@Union` and related 0.16 type-construction builtins.
```zig
pub fn Subset(comptime T: type, comptime fields: []const std.meta.FieldEnum(T)) type {
const source_info = @typeInfo(T).@"union";
var new_fields: [fields.len]std.builtin.Type.UnionField = undefined;
for (fields, 0..) |field_enum, i| {
const field_name = @tagName(field_enum);
for (source_info.fields) |source_field| {
if (std.mem.eql(u8, source_field.name, field_name)) {
new_fields[i] = source_field;
break;
}
}
}
return @Type(.{
.@"union" = .{
.layout = source_info.layout,
.tag_type = std.meta.FieldEnum(@Type(.{
.@"union" = .{
.layout = source_info.layout,
.tag_type = null,
.fields = &new_fields,
.decls = &.{},
},
})),
.fields = &new_fields,
.decls = &.{},
},
});
}
```
### Converting Between Union Types
Use `inline else` to capture tag at comptime:
```zig
/// Convert subset to full union type.
pub fn toFull(comptime Full: type, subset: anytype) Full {
return switch (subset) {
inline else => |payload, tag| @unionInit(Full, @tagName(tag), payload),
};
}
/// Try to narrow full union to subset type.
pub fn toSubset(comptime Subset: type, full: anytype) ?Subset {
const subset_fields = @typeInfo(Subset).@"union".fields;
return switch (full) {
inline else => |payload, tag| {
inline for (subset_fields) |sf| {
if (std.mem.eql(u8, sf.name, @tagName(tag))) {
return @unionInit(Subset, sf.name, payload);
}
}
return null;
},
};
}
```
**Verified in stdlib:** `std/meta.zig`, `std/json/static.zig:299-302`
---
## Limitations
Zig's comptime is deliberately constrained for cross-compilation safety and code clarity.
### No Host Architecture Detection
```zig
// This reflects TARGET, not host
const is_64bit = comptime @sizeOf(usize) == 8;
// Use build.zig for host detection
// build.zig runs as a program and can query host
```
### No String-to-Code Evaluation
```zig
// NOT POSSIBLE
const code = "x + y";
const result = @eval(code); // No such builtin
// Alternative: Parse strings to data structures at comptime
const query = comptime sql.parse("SELECT * FROM users");
```
### No Runtime Type Information
```zig
// This works - type known at comptime
fn getTypeName(value: anytype) []const u8 {
return @typeName(@TypeOf(value));
}
// NOT POSSIBLE - can't turn runtime string into type
fn typeFromName(name: []const u8) type { ... }
```
### Comptime I/O Boundary
```zig
// Ordinary runtime filesystem I/O is not available at comptime.
// This is intentionally invalid; Zig 0.16 has no such comptime std.Io call:
// const config = comptime std.Io.Dir.cwd().readFileAlloc(...);
// Compiler-supported static input is available:
const embedded = @embedFile("config.json");
// Alternatives:
const config = @embedFile("config.json"); // Static embedding
// Or use build.zig which runs as a normal program
```
### No Dynamic Method Injection
```zig
// This works - methods defined in type
fn Pair(comptime A: type, comptime B: type) type {
return struct {
first: A,
second: B,
pub fn swap(self: @This()) Pair(B, A) { ... }
};
}
// NOT POSSIBLE - can't add methods to existing types
fn addMethod(comptime T: type, comptime name: []const u8, impl: anytype) type { ... }
```
### Summary Table
| Want to do | Comptime? | Alternative |
|------------|-----------|-------------|
| Type reflection | Yes | `@typeInfo`, `@TypeOf` |
| Generate types | Yes | Return struct from function |
| Add methods to types | No | Define in type definition |
| Read files | Static inputs only | `@embedFile`; use build.zig for ordinary I/O |
| Syscalls | No | build.zig runs as program |
| Parse strings to code | No | Parse to data structures |
| Host detection | No | Build system queries |
### Design Rationale
These constraints ensure:
1. **Cross-compilation works** - comptime sees target, not host
2. **Code is readable** - no hidden code generation
3. **Builds are reproducible** - no I/O side effects
4. **All API is visible** - no dynamic method injection