1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
const std = @import("std");
const Target = @import("target.zig").Target;
/// Create an enum type with the given keys that is C ABI compatible
/// if we're targeting C, otherwise a Zig enum with smallest possible
/// backing type.
///
/// In all cases, the enum keys will be created in the order given.
/// For C ABI, this means that the order MUST NOT be changed in order
/// to preserve ABI compatibility. You can set a key to null to
/// remove it from the Zig enum while keeping the "hole" in the C enum
/// to preserve ABI compatibility.
///
/// C detection is up to the caller, since there are multiple ways
/// to do that. We rely on the `target` parameter to determine whether we
/// should create a C compatible enum or a Zig enum.
///
/// For the Zig enum, the enum value is not guaranteed to be stable, so
/// it shouldn't be relied for things like serialization.
pub fn Enum(
target: Target,
keys: []const ?[:0]const u8,
) type {
var fields: [keys.len]std.builtin.Type.EnumField = undefined;
var fields_i: usize = 0;
var holes: usize = 0;
for (keys) |key_| {
const key: [:0]const u8 = key_ orelse {
switch (target) {
// For Zig we don't track holes because the enum value
// isn't guaranteed to be stable and we want to use the
// smallest possible backing type.
.zig => {},
// For C we must track holes to preserve ABI compatibility
// with subsequent values.
.c => holes += 1,
}
continue;
};
fields[fields_i] = .{
.name = key,
.value = fields_i + holes,
};
fields_i += 1;
}
// Assigned to var so that the type name is nicer in stack traces.
const Result = @Type(.{ .@"enum" = .{
.tag_type = switch (target) {
.c => c_int,
.zig => std.math.IntFittingRange(0, fields_i - 1),
},
.fields = fields[0..fields_i],
.decls = &.{},
.is_exhaustive = true,
} });
return Result;
}
test "zig" {
const testing = std.testing;
const T = Enum(.zig, &.{ "a", "b", "c", "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(u2, info.tag_type);
}
test "c" {
const testing = std.testing;
const T = Enum(.c, &.{ "a", "b", "c", "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(c_int, info.tag_type);
}
test "abi by removing a key" {
const testing = std.testing;
// C
{
const T = Enum(.c, &.{ "a", "b", null, "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(c_int, info.tag_type);
try testing.expectEqual(3, @intFromEnum(T.d));
}
// Zig
{
const T = Enum(.zig, &.{ "a", "b", null, "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(u2, info.tag_type);
try testing.expectEqual(2, @intFromEnum(T.d));
}
}
/// Verify that for every key in enum T, there is a matching declaration in
/// `ghostty.h` with the correct value. This should only ever be called inside a `test`
/// because the `ghostty.h` module is only available then.
pub fn checkGhosttyHEnum(
comptime T: type,
comptime prefix: []const u8,
) !void {
const info = @typeInfo(T);
try std.testing.expect(info == .@"enum");
try std.testing.expect(info.@"enum".tag_type == c_int);
try std.testing.expect(info.@"enum".is_exhaustive == true);
@setEvalBranchQuota(100_000);
const c = @import("ghostty.h");
var set: std.EnumSet(T) = .initFull();
const enum_fields = info.@"enum".fields;
inline for (enum_fields) |field| {
const expected_name: *const [prefix.len + field.name.len]u8 = comptime e: {
var buf: [prefix.len + field.name.len]u8 = undefined;
@memcpy(buf[0..prefix.len], prefix);
for (buf[prefix.len..], field.name) |*d, s| {
d.* = std.ascii.toUpper(s);
}
break :e &buf;
};
if (@hasDecl(c, expected_name)) {
std.testing.expectEqual(field.value, @field(c, expected_name)) catch |e| {
std.log.err(@typeName(T) ++ " key " ++ field.name ++ " does not have the same backing int as " ++ expected_name, .{});
return e;
};
set.remove(@enumFromInt(field.value));
}
}
std.testing.expect(set.count() == 0) catch |e| {
var it = set.iterator();
while (it.next()) |v| {
var buf: [128]u8 = undefined;
const upper_string = std.ascii.upperString(&buf, @tagName(v));
std.log.err("ghostty.h is missing value for {s}{s}", .{ prefix, upper_string });
}
return e;
};
}
|