I’ve tried to do some simple encapsulation of the Context pattern. I haven’t figured out how to handle generic function declarations yet, so for now I simply don’t support all requirements for generic function declarations, which might lead to some inference errors set not being judged as expected.
pub fn noCallingConvertionRestriction(comptime calling_convertion: std.builtin.CallingConvention) bool {
_ = calling_convertion;
return true;
}
pub const FnOrMethodDeclaration = union(enum) {
// I haven't yet figured out how to use generic function declarations,
// so for now I'm adopting a simple "disallow generics" strategy.
// This could potentially cause function declarations using the inference error set to fail the checks.
@"fn": struct {
name: [:0]const u8,
callingConvertionRestrictionFn: fn (comptime calling_convertion: std.builtin.CallingConvention) bool = noCallingConvertionRestriction,
is_var_args: ?bool = false,
return_type: type,
params: []const Param,
},
method: struct {
name: [:0]const u8,
callingConvertionRestrictionFn: fn (comptime calling_convertion: std.builtin.CallingConvention) bool = noCallingConvertionRestriction,
// `null` means do not care whether it is var args.
is_var_args: ?bool = false,
return_type: type,
params_except_self: []const Param,
},
pub const Param = struct {
// `null` means do not care whether it is noalias.
is_noalias: ?bool = null,
type: type,
};
};
pub fn validateContextWithFnOrMethodDeclarations(
comptime Context: type,
comptime allowed_container_types_option: struct {
@"struct": bool = false,
@"enum": bool = false,
@"union": bool = false,
@"opaque": bool = false,
},
comptime expected_fn_or_method_declarations_info: []const FnOrMethodDeclaration,
) void {
switch (@typeInfo(Context)) {
.@"struct" => if (!allowed_container_types_option.@"struct") @compileError(std.fmt.comptimePrint("`{s}` is not allowed to be a `struct`", .{@typeName(Context)})),
.@"enum" => if (!allowed_container_types_option.@"enum") @compileError(std.fmt.comptimePrint("`{s}` is not allowed to be an `enum`", .{@typeName(Context)})),
.@"union" => if (!allowed_container_types_option.@"union") @compileError(std.fmt.comptimePrint("`{s}` is not allowed to be a `union`", .{@typeName(Context)})),
.@"opaque" => if (!allowed_container_types_option.@"opaque") @compileError(std.fmt.comptimePrint("`{s}` is not allowed to be an `opaque`", .{@typeName(Context)})),
else => @compileError(std.fmt.comptimePrint("`{s}` must be a container", .{@typeName(Context)})),
}
for (expected_fn_or_method_declarations_info) |expected_fn_or_method_declaration_info| {
const name: [:0]const u8, const callingConvertionRestrictionFn: fn (comptime calling_convertion: std.builtin.CallingConvention) bool, const maybe_expected_is_var_args: ?bool, const ExpectedReturnType: type, const params_except_self_start_from: usize, const expected_params_except_self_info = blk: {
switch (expected_fn_or_method_declaration_info) {
.@"fn" => |@"fn"| break :blk .{ @"fn".name, @"fn".callingConvertionRestrictionFn, @"fn".is_var_args, @"fn".return_type, 0, @"fn".params },
.method => |method| break :blk .{ method.name, method.callingConvertionRestrictionFn, method.is_var_args, method.return_type, 1, method.params_except_self },
}
};
if (!@hasDecl(Context, name)) @compileError(std.fmt.comptimePrint("`{s}.{s}` does exist", .{ @typeName(Context), name }));
const decl_type_info: std.builtin.Type.Fn = blk: switch (@typeInfo(@TypeOf(@field(Context, name)))) {
.@"fn" => |decl_type_info| break :blk decl_type_info,
else => @compileError(std.fmt.comptimePrint("`{s}.{s}` is not a function", .{ @typeName(Context), name })),
};
if (decl_type_info.is_generic) @compileError(std.fmt.comptimePrint("`{s}.{s}` does not support generic yet.", .{ @typeName(Context), name }));
if (!callingConvertionRestrictionFn(decl_type_info.calling_convention)) return false;
if (maybe_expected_is_var_args) |expected_is_var_args| {
if (expected_is_var_args != decl_type_info.is_var_args) @compileError(std.fmt.comptimePrint(
"`{s}.{s}` should {s}be var args",
.{ @typeName(Context), name, if (expected_is_var_args) "" else "not " },
));
}
if (decl_type_info.return_type) |ReturnType| {
if (ReturnType != ExpectedReturnType) @compileError(std.fmt.comptimePrint("The return type of `{s}.{s}` is not `{s}`.", .{ @typeName(Context), name, @typeName(ExpectedReturnType) }));
} else @compileError(std.fmt.comptimePrint("`{s}.{s}` does not support generic yet.", .{ @typeName(Context), name }));
const expected_params_num: usize = expected_params_except_self_info.len + params_except_self_start_from;
if (decl_type_info.params.len != expected_params_num) @compileError(std.fmt.comptimePrint("The params num of `{s}.{s}` is not `{d}`.", .{ @typeName(Context), name, expected_params_num }));
switch (expected_fn_or_method_declaration_info) {
.@"fn" => {},
.method => {
const SelfHandle: type = if (decl_type_info.params[0].type) |S| S else @compileError(std.fmt.comptimePrint("`{s}.{s}` does not support generic yet.", .{ @typeName(Context), name }));
switch (@typeInfo(SelfHandle)) {
.pointer => |SelfPtr| switch (SelfPtr.size) {
.one => if (SelfPtr.child != Context) @compileError(std.fmt.comptimePrint("`{s}.{s}` is not a method.", .{ @typeName(Context), name })),
.many, .slice, .c => @compileError(std.fmt.comptimePrint("`{s}.{s}` is not a method.", .{ @typeName(Context), name })),
},
else => if (SelfHandle != Context) @compileError(std.fmt.comptimePrint("`{s}.{s}` is not a method.", .{ @typeName(Context), name })),
}
},
}
const LoggerHelper = struct {
fn printOrdinal(comptime id: usize) []const u8 {
return switch (id) {
0 => "1st",
1 => "2nd",
2 => "3rd",
else => std.fmt.comptimePrint("{d}th", .{id + 1}),
};
}
};
for (
decl_type_info.params[params_except_self_start_from..],
expected_params_except_self_info,
params_except_self_start_from..,
) |
param,
expected_param_info,
param_id,
| {
if (expected_param_info.is_noalias) |expected_is_noalias| {
if (param.is_noalias != expected_is_noalias) @compileError(std.fmt.comptimePrint(
"The {s} param of `{s}.{s}` should {s}be noalias",
.{ LoggerHelper.printOrdinal(param_id), @typeName(Context), name, if (expected_is_noalias) "" else "not " },
));
}
if (param.type) |ParamType| {
if (ParamType != expected_param_info.type) @compileError(std.fmt.comptimePrint("The {s} param of `{s}.{s}` is not `{s}`", LoggerHelper.printOrdinal(param_id), @typeName(Context), name, @typeName(expected_param_info.type)));
} else @compileError(std.fmt.comptimePrint("`{s}.{s}` does not support generic yet.", .{ @typeName(Context), name }));
}
}
}
After using the above encapsulation, verifying the Context pattern becomes much simpler:
pub fn HashMapUnmanaged(
comptime K: type,
comptime V: type,
comptime Context: type,
comptime max_load_percentage: u64,
) R: {
validateContextWithFnOrMethodDeclarations(Context, .{ .@"struct" = true }, &[_]FnOrMethodDeclaration{
.{ .method = .{
.name = "hash",
.return_type = u64,
.params_except_self = &[_]FnOrMethodDeclaration.Param{.{ .type = K }},
} },
.{ .method = .{
.name = "eql",
.return_type = bool,
.params_except_self = &[_]FnOrMethodDeclaration.Param{ .{ .type = K }, .{ .type = K } },
} },
});
break :R type;
} {
...
}