469 lines
13 KiB
Markdown
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
|