NOCLIP/source/meta.zig

201 lines
6.8 KiB
Zig
Raw Normal View History

const std = @import("std");
const StructField = std.builtin.Type.StructField;
/// Given a type and a struct literal of defaults to add, this function creates
/// a simulacrum type with additional defaults set on its fields.
///
/// This function cannot remove default values from fields, but it can add some
/// to fields that don't have them, and it can overwrite existing defaults
pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
comptime {
const inputInfo = @typeInfo(input);
const fieldcount = switch (inputInfo) {
.Struct => |spec| blk: {
if (spec.decls.len > 0) {
@compileError("UpdateDefaults only works on structs " ++
"without decls due to limitations in @Type.");
}
break :blk spec.fields.len;
},
else => @compileError("can only add default value to struct type"),
};
var fields: [fieldcount]StructField = undefined;
for (inputInfo.Struct.fields, 0..) |field, idx| {
fields[idx] = .{
.name = field.name,
.field_type = field.field_type,
// the cast ostensibly does type checking for us. It also makes
// setting null defaults work, and it converts comptime_int to
// the appropriate type, which is nice for ergonomics. Not sure
// if it introduces weird edge cases. Probably it's fine?
.default_value = if (@hasField(@TypeOf(defaults), field.name))
@ptrCast(?*const anyopaque, &@as(field.field_type, @field(defaults, field.name)))
else
field.default_value,
.is_comptime = field.is_comptime,
.alignment = field.alignment,
};
}
return @Type(.{ .Struct = .{
.layout = inputInfo.Struct.layout,
.backing_integer = inputInfo.Struct.backing_integer,
.fields = &fields,
.decls = inputInfo.Struct.decls,
.is_tuple = inputInfo.Struct.is_tuple,
} });
}
}
pub fn enum_length(comptime T: type) comptime_int {
return @typeInfo(T).Enum.fields.len;
}
pub fn SliceIterator(comptime T: type) type {
// could be expanded to use std.meta.Elem, perhaps
const ResultType = std.meta.Child(T);
return struct {
index: usize,
data: T,
pub const InitError = error{};
pub fn wrap(value: T) @This() {
return @This(){ .index = 0, .data = value };
}
pub fn next(self: *@This()) ?ResultType {
if (self.index == self.data.len) return null;
defer self.index += 1;
return self.data[self.index];
}
pub fn peek(self: *@This()) ?ResultType {
if (self.index == self.data.len) return null;
return self.data[self.index];
}
pub fn skip(self: *@This()) void {
if (self.index == self.data.len) return;
self.index += 1;
}
};
}
/// Stores type-erased pointers to items in comptime extensible data structures,
/// which allows e.g. assembling a tuple through multiple calls rather than all
/// at once.
pub const MutableTuple = struct {
pointers: []const *const anyopaque = &[0]*const anyopaque{},
types: []const type = &[0]type{},
pub fn add(comptime self: *@This(), comptime item: anytype) void {
self.pointers = &(@as([self.pointers.len]*const anyopaque, self.pointers[0..self.pointers.len].*) ++ [1]*const anyopaque{@as(*const anyopaque, &item)});
self.types = &(@as([self.types.len]type, self.types[0..self.types.len].*) ++ [1]type{@TypeOf(item)});
}
pub fn retrieve(comptime self: @This(), comptime index: comptime_int) self.types[index] {
return @ptrCast(*const self.types[index], @alignCast(@alignOf(*const self.types[index]), self.pointers[index])).*;
}
pub fn realTuple(comptime self: @This()) self.TupleType() {
comptime {
var result: self.TupleType() = undefined;
var idx = 0;
while (idx < self.types.len) : (idx += 1) {
result[idx] = self.retrieve(idx);
}
return result;
}
}
pub fn TupleType(comptime self: @This()) type {
comptime {
var fields: [self.types.len]StructField = undefined;
for (self.types, 0..) |Type, idx| {
var num_buf: [128]u8 = undefined;
fields[idx] = .{
.name = std.fmt.bufPrint(&num_buf, "{d}", .{idx}) catch @compileError("failed to write field"),
.type = Type,
.default_value = null,
// TODO: is this the right thing to do?
.is_comptime = false,
.alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0,
};
}
return @Type(.{ .Struct = .{
.layout = .Auto,
.fields = &fields,
.decls = &.{},
.is_tuple = true,
} });
}
}
};
test "add basic default" {
const Base = struct { a: u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = 4 });
const value = Defaulted{};
try std.testing.expectEqual(@as(u8, 4), value.a);
}
test "overwrite basic default" {
const Base = struct { a: u8 = 0 };
const Defaulted = UpdateDefaults(Base, .{ .a = 1 });
const value = Defaulted{};
try std.testing.expectEqual(@as(u8, 1), value.a);
}
test "add string default" {
const Base = struct { a: []const u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = "hello" });
const value = Defaulted{};
try std.testing.expectEqual(@as([]const u8, "hello"), value.a);
}
test "add null default" {
const Base = struct { a: ?u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = null });
const value = Defaulted{};
try std.testing.expectEqual(@as(?u8, null), value.a);
}
test "add enum default" {
const Options = enum { good, bad };
const Base = struct { a: Options };
const Defaulted = UpdateDefaults(Base, .{ .a = .good });
const value = Defaulted{};
try std.testing.expectEqual(Options.good, value.a);
}
test "preserve existing default" {
const Base = struct { a: ?u8 = 2, b: u8 };
const Defaulted = UpdateDefaults(Base, .{ .b = 3 });
const value = Defaulted{};
try std.testing.expectEqual(@as(?u8, 2), value.a);
try std.testing.expectEqual(@as(?u8, 3), value.b);
}
test "add multiple defaults" {
const Base = struct { a: u8, b: i8, c: ?u8 };
const Defaulted = UpdateDefaults(Base, .{ .a = 3, .c = 2 });
const value = Defaulted{ .b = -1 };
try std.testing.expectEqual(@as(u8, 3), value.a);
try std.testing.expectEqual(@as(i8, -1), value.b);
try std.testing.expectEqual(@as(?u8, 2), value.c);
}