Skip to content

Instantly share code, notes, and snippets.

@Nathan-Franck
Created August 24, 2023 22:58
Show Gist options
  • Save Nathan-Franck/6c7cb058aadaf578ac60ac0558fd9bfb to your computer and use it in GitHub Desktop.
Save Nathan-Franck/6c7cb058aadaf578ac60ac0558fd9bfb to your computer and use it in GitHub Desktop.
Forcing language features into zig at comptime
const std = @import("std");
const sfy = @import("structify.zig");
pub fn main() !void {
// Object builders just like c# or typescript, easy peasy! Tooling could be better.
{
const Config = struct {
a: i32,
b: i16,
};
const default_config = Config{
.a = 1,
.b = 2,
};
// Recreate the F#/OCaml/C# "with" statement to create new data from existing with some fields modified.
const extended_config = sfy.with(default_config, .{
.b = 3,
});
std.debug.print("default: {?}, extended: {?}\n", .{ default_config, extended_config });
// Actual magic, not doable in C# but is possible with Typescript (and with good tooling).
// With ZLS, this is not perfect, but the build-on-save option helps a lot with getting linting at least when things are incorrect.
// Currently it only complains that there's no field found of that name, where it would be nice to have a suggestion for a name that does exist that's close to the one you typed.
const foo_config = sfy.WithPrefix(Config, "foo_"){
.foo_a = 1,
.foo_b = 2,
};
std.debug.print("Behold! The foo config! It's incredible! {?}\n", .{foo_config});
// If you miss the ZLS auto-complete and go-to-definition, we can just validate a new struct against the transformed type!
// We will get a compiler error if the structs don't match. Granted, we need to preserve the order of the fields, which could be a pain.
const FooConfig = sfy.Validate(sfy.WithPrefix(Config, "foo_"), struct {
foo_a: i32, // Now we have to explicitly declare the fields, but there are some unmentionable new IDE plugins can help us generate boilerplate.
foo_b: i16,
});
const foo_config2 = FooConfig{
.foo_a = 1,
.foo_b = 2,
};
std.debug.print("Ergonomic and safe! Wowee! {?}\n", .{foo_config2});
// Going nuts over here - do some generic callbacks!
const float_config = sfy.WithFieldChanges(Config, struct {
pub fn convertFieldType(comptime input: type) type {
return switch (input) {
i16 => f16,
i32 => f32,
else => @compileError("Unsupported type"),
};
}
pub fn convertFieldName(comptime input: []const u8) []const u8 {
return input ++ "_but_its_a_float"; // How 'bout a postfix?
}
}){
.a_but_its_a_float = 1.0,
.b_but_its_a_float = 2.0,
};
std.debug.print("Float config! {?}\n", .{float_config});
}
}
const std = @import("std");
/// Take some data, and modify the fields with the same name in the changes struct.
pub fn with(original: anytype, changes: anytype) @TypeOf(original) {
return switch (@typeInfo(@TypeOf(changes))) {
.Struct => |struct_info| {
var modified = original;
inline for (struct_info.fields) |field| {
@field(modified, field.name) = @field(changes, field.name);
}
return modified;
},
else => @compileError("Expected a struct"),
};
}
test "with" {
const Config = struct {
a: i32,
b: i32,
};
const default_config = Config{
.a = 1,
.b = 2,
};
const extended_config = with(default_config, .{
.b = 3,
});
try std.testing.expect(std.meta.eql(extended_config, Config{
.a = 1,
.b = 3,
}));
// It's a shame you don't get lint errors on tests from zls after saving with save-on-build.
// const extended_config = with(default_config, .{ .qwer = 3 }); // <- this should be a linting error.
}
/// Given all field types, pass the type of the field to a fn on a given struct that returns a new type, this is the new struct.
/// This might be slow if used way too much, since we're branching on every field.
pub fn WithFieldChanges(comptime original: type, comptime callback: anytype) type {
return switch (@typeInfo(original)) {
.Struct => |_struct| @Type(.{
.Struct = with(_struct, .{
.fields = fields: {
var result: [_struct.fields.len]std.builtin.Type.StructField = undefined;
for (_struct.fields, 0..) |field, i| {
var new_field = field;
if (@hasDecl(callback, "convertFieldType")) {
new_field.type = callback.convertFieldType(field.type);
}
if (@hasDecl(callback, "convertFieldName")) {
new_field.name = callback.convertFieldName(field.name);
}
result[i] = new_field;
}
break :fields &result;
},
}),
}),
else => @compileError("Expected a struct"),
};
}
/// Takes a type with fields and adds a prefix to the beginning of each field name.
pub fn WithPrefix(comptime original: type, comptime prefix: []const u8) type {
return switch (@typeInfo(original)) {
.Struct => |_struct| @Type(.{
.Struct = with(_struct, .{
.fields = fields: {
var result: [_struct.fields.len]std.builtin.Type.StructField = undefined;
for (_struct.fields, 0..) |field, i| {
result[i] = with(field, .{ .name = prefix ++ field.name });
}
break :fields &result;
},
}),
}),
else => @compileError("Expected a struct"),
};
}
test "WithPrefix" {
const Config = struct {
a: i32,
b: i32,
};
const foo_config = WithPrefix(Config, "foo_"){
.foo_a = 1,
.foo_b = 2,
};
_ = foo_config;
}
/// Creates a quick and dirty allocator on the stack so we can print an error message at compile time.
fn errorFormatter(format: []const u8, args: anytype) []const u8 {
var buf: [256]u8 = undefined;
var errorAllocator = std.heap.FixedBufferAllocator.init(&buf);
var allocator = errorAllocator.allocator();
return try std.fmt.allocPrint(allocator, format, args);
}
/// Given a source struct and a test struct, ensure that all fields are the same name and type.
pub fn Validate(comptime reference: type, comptime definition: type) type {
comptime {
_ = switch (@typeInfo(definition)) {
.Struct => |source_struct| {
switch (@typeInfo(reference)) {
.Struct => |reference_struct| {
if (source_struct.fields.len != reference_struct.fields.len) {
@compileError(errorFormatter("Structs do not have the same number of fields ({d} != {d})", .{ source_struct.fields.len, reference_struct.fields.len }));
}
inline for (source_struct.fields, 0..) |definition_field, i| {
const reference_field = reference_struct.fields[i];
if (!std.mem.eql(u8, definition_field.name, reference_field.name)) {
@compileError(errorFormatter("Field names do not match ({s} != {s})", .{ definition_field.name, reference_field.name }));
}
if (definition_field.type != reference_field.type) {
@compileError(errorFormatter("Field types do not match ({s} != {s})", .{ definition_field.type, reference_field.type }));
}
}
},
else => @compileError("Expected a struct"),
}
},
else => @compileError("Expected a struct"),
};
}
// Return the source outside of any branching so ZLS can easily trace back to the original symbols.
return definition;
}
test "Validate" {
const Config = struct {
a: i32,
b: i32,
};
const FooConfig = Validate(WithPrefix(Config, "foo_"), struct {
foo_a: i32,
foo_b: i32,
});
const foo_config2 = FooConfig{
.foo_a = 1,
.foo_b = 2,
};
_ = foo_config2;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment