Created
August 24, 2023 22:58
-
-
Save Nathan-Franck/6c7cb058aadaf578ac60ac0558fd9bfb to your computer and use it in GitHub Desktop.
Forcing language features into zig at comptime
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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