Skip to content

Instantly share code, notes, and snippets.

@SimonLSchlee
Last active May 27, 2024 19:12
Show Gist options
  • Save SimonLSchlee/32d6a9a66de9e74797c5a935350ac996 to your computer and use it in GitHub Desktop.
Save SimonLSchlee/32d6a9a66de9e74797c5a935350ac996 to your computer and use it in GitHub Desktop.
Overloaded Function Sets Refactored + TypeConverters + implicitTypeConverter + implicitArrayToSlice + implicitComptimeInt
// This code is heavily based on the code from:
// https://ziggit.dev/t/making-overloaded-function-sets-using-comptime/2475
//
// This version is a bit of a rewrite, it gets rid of the indexing
// and instead uses the type of the functions to identify which function to call.
//
// Instead of optional types which can't really handle function types for some reason,
// we just pretend that type was an "optional", we just treat everything as a value,
// except the type "noreturn" which we treat as if it was our null value.
// This way we just have to find the type of the right function, if it is null("noreturn")
// we create a compile error that we didn't find a matching function.
//
// When we have the type of the function we can simply find the function by looping
// over all functions and comparing with the type, this works because we don't allow
// duplicate function signatures.
//
// ------
//
// Additionally to this rewrite there is also a newly added feature: an optional
// TypeConverter function, this function is a comptime function that can be added
// as argument to OverloadSet and it gets called if none of the functions match
// the given argument types exactly.
//
// This means that now users of OverloadSet can customize what happens when there
// is no matching function, the main function demonstrates how this can be used to
// implement implicit type conversion (like calling functions taking a slice with
// string literals, or functions taking an int32 with a comptime_int)
//
// If somebody comes up with other usecases for similar customizations, that might
// reveal a need to change the type of TypeConverter, however lets wait and see
// whether that is actually the case / needed.
//
// It would also be possible to add configuration options, for changed behavior
// I tried that in a different version, however this version without the added
// complexity of configuration seemed easier and I didn't have good ideas for
// what should actually be configurable, if you have good ideas, let me know.
//
const std = @import("std");
pub fn isTuple(comptime T: type) bool {
return switch (@typeInfo(T)) {
.Struct => |s| s.is_tuple,
else => false,
};
}
fn isFunction(comptime T: type) bool {
return switch (@typeInfo(T)) {
.Fn => true,
else => false,
};
}
fn isFunctionsTuple(args: anytype) bool {
return isTuple(@TypeOf(args)) and for (args) |arg| {
const T = @TypeOf(arg);
if (skip(T)) continue;
if (!isFunction(T)) break false;
} else true;
}
fn skip(comptime T: type) bool {
return T == TypeConverter;
}
fn countElements(comptime args: anytype, comptime Element: type) comptime_int {
var i = 0;
for (args) |a| {
if (@TypeOf(a) == Element) i += 1;
}
return i;
}
fn getElement(comptime args: anytype, comptime Element: type) ?Element {
for (args) |a| {
if (@TypeOf(a) == Element) return a;
}
return null;
}
fn detectArgsError(comptime args: anytype) ?[]const u8 {
const count = countElements(args, TypeConverter);
if (count > 1) {
return std.fmt.comptimePrint("Only 0 or 1 TypeConverter functions are allowed, but got {}", .{count});
}
if (!isFunctionsTuple(args)) {
return "Non-function argument in overload set.";
}
for (args) |f| {
const T = @TypeOf(f);
if (skip(T)) continue;
for (@typeInfo(T).Fn.params) |param| {
if (param.type == null) {
return "Generic parameter types in overload set.";
}
}
}
for (0..args.len) |i| {
const T0 = @TypeOf(args[i]);
if (skip(T0)) continue;
const params0 = @typeInfo(T0).Fn.params;
for (i + 1..args.len) |j| {
const T1 = @TypeOf(args[j]);
if (skip(T1)) continue;
const params1 = @typeInfo(T1).Fn.params;
const signatures_are_identical = params0.len == params1.len and
for (params0, params1) |param0, param1|
{
if (param0.type != param1.type) break false;
} else true;
if (signatures_are_identical) {
return "Identical function signatures in overload set.";
}
}
}
return null;
}
// a typeconverter function gets the functions and the type of an arg tuple at comptime
// and it can return the type of a function contained in functions that can be called
// with that arg tuple (e.g. by being compatible via implicit conversion, which needs
// to be checked explicitly by the type converter function)
// or it returns the type noreturn in that case no conversion was found
pub fn DefaultTypeConverter(comptime def: anytype, comptime args_type: type) type {
_ = def;
_ = args_type;
return noreturn;
}
pub const TypeConverter = @TypeOf(DefaultTypeConverter);
pub fn getTypeConverter(comptime def: anytype) ?TypeConverter {
return getElement(def, TypeConverter);
}
// this is a bit crazy, but we basically use type at
// comptime like an optional where noreturn is the null value
fn FindMatchingFunctionType(comptime def: anytype, comptime args_type: type) type {
const function = FindFunctionMatchingParameters(def, args_type);
if (function != noreturn) return function;
return if (getTypeConverter(def)) |converter| converter(def, args_type) else noreturn;
}
// this works because we only allow unique types in functions thus the correct function
// can be identified by its type
fn findMatchingFunction(comptime def: anytype, comptime args_type: type) FindMatchingFunctionType(def, args_type) {
const function_type = FindMatchingFunctionType(def, args_type);
return for (def) |function| {
if (@TypeOf(function) == function_type) break function;
} else noreturn;
}
fn GetResultType(comptime function: type) type {
if (function == noreturn) return noreturn;
return @typeInfo(function).Fn.return_type.?;
}
pub fn FindFunctionMatchingParameters(comptime def: anytype, comptime args_type: type) type {
const args_fields = @typeInfo(args_type).Struct.fields;
for (def) |function| {
const T = @TypeOf(function);
if (skip(T)) continue;
const function_type_info = @typeInfo(T).Fn;
const params = function_type_info.params;
const match = params.len == args_fields.len and
for (params, args_fields) |param, field|
{
if (param.type.? != field.type) break false;
} else true;
if (match) return @TypeOf(function);
}
return noreturn;
}
pub fn OverloadSet(comptime def: anytype) type {
if (comptime detectArgsError(def)) |error_message| {
@compileError(error_message);
}
return struct {
fn candidatesMessage() []const u8 {
var msg: []const u8 = "";
for (def) |f| {
const T = @TypeOf(f);
if (skip(T)) continue;
msg = msg ++ " " ++ @typeName(T) ++ "\n";
}
return msg;
}
fn formatArguments(comptime args_type: type) []const u8 {
const params = @typeInfo(args_type).Struct.fields;
var msg: []const u8 = "{ ";
for (params, 0..) |arg, i| {
msg = msg ++ @typeName(arg.type) ++ if (i < params.len - 1) ", " else "";
}
return msg ++ " }";
}
pub fn call(args: anytype) GetResultType(FindMatchingFunctionType(def, @TypeOf(args))) {
const args_type = @TypeOf(args);
if (comptime !isTuple(args_type)) {
@compileError("OverloadSet's call argument must be a tuple.");
}
if (comptime FindMatchingFunctionType(def, args_type) == noreturn) {
@compileError("No overload for " ++ formatArguments(args_type) ++ "\n" ++ "Candidates are:\n" ++ candidatesMessage());
}
return @call(.always_inline, findMatchingFunction(def, args_type), args);
}
};
}
fn nothing() void {}
fn otherNothing() void {}
fn everything() i32 {
return 42;
}
fn string(s: []const u8) i32 {
var count: i32 = 0;
for (s) |c| count += c;
return count;
}
fn string2(s: []const u8, t: []const u8) i32 {
var count: i32 = 0;
for (s) |c| count += c;
for (t) |c| count += c;
return count;
}
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn addMul(a: i32, b: i32, c: i32) i32 {
return (a + b) * c;
}
const ImplicitConversion = @TypeOf(implicitArrayToSlice);
fn implicitArrayToSlice(comptime param_type: type, comptime field: std.builtin.Type.StructField) bool {
const field_type = field.type;
switch (@typeInfo(param_type)) {
.Pointer => |s| {
if (s.size == .Slice) {
switch (@typeInfo(field_type)) {
.Pointer => |p| {
if (p.size == .One and s.is_const == p.is_const and s.alignment == p.alignment) {
switch (@typeInfo(p.child)) {
.Array => |c| {
return s.child == c.child;
},
else => {},
}
}
},
else => {},
}
}
},
else => {},
}
return false;
}
fn implicitComptimeInt(comptime param_type: type, comptime field: std.builtin.Type.StructField) bool {
switch (@typeInfo(param_type)) {
.Int => |val| {
if (field.is_comptime and field.type == comptime_int) {
const T = std.meta.Int(val.signedness, val.bits);
const min: comptime_int = std.math.minInt(T);
const max: comptime_int = std.math.maxInt(T);
const actual: comptime_int = @as(*const comptime_int, @alignCast(@ptrCast(field.default_value.?))).*;
if (min <= actual and actual <= max) {
return true;
}
}
},
else => {},
}
return false;
}
fn checkConverters(comptime converters: anytype) void {
for (converters) |c| {
const C = @TypeOf(c);
if (C != ImplicitConversion) {
@compileError("Function '" ++ @typeName(C) ++ "' is not a valid implicit conversion function.");
}
}
}
fn implicitTypeConverters(comptime converters: anytype) TypeConverter {
comptime checkConverters(converters);
return struct {
fn ImplicitTypeConverter(comptime def: anytype, comptime args_type: type) type {
const args_fields = @typeInfo(args_type).Struct.fields;
for (def) |function| {
const T = @TypeOf(function);
if (skip(T)) continue;
const function_type_info = @typeInfo(T).Fn;
const params = function_type_info.params;
const match = params.len == args_fields.len and
for (params, args_fields) |param, field|
{
const convertible = for (converters) |c| {
if (c(param.type.?, field)) break true;
} else false;
if (!convertible and param.type.? != field.type) break false;
} else true;
if (match) return @TypeOf(function);
}
return noreturn;
}
}.ImplicitTypeConverter;
}
pub fn sqr8(x: u8) u16 {
const c: u16 = x;
return c * c;
}
pub fn sqr16(x: u16) u32 {
const c: u32 = x;
return c * c;
}
pub fn sqr32(x: u32) u64 {
const c: u64 = x;
return c * c;
}
pub fn sqr64(x: u64) u128 {
const c: u128 = x;
return c * c;
}
// make the overload set
const sqr = OverloadSet(.{
implicitTypeConverters(.{implicitComptimeInt}),
sqr8,
sqr16,
sqr32,
sqr64,
});
fn intAndString(x: u32, s: []const u8) u64 {
var count: u32 = x;
for (s) |c| count += c;
return count;
}
const set = OverloadSet(.{
implicitTypeConverters(.{ implicitArrayToSlice, implicitComptimeInt }),
nothing,
string,
string2,
add,
addMul,
intAndString,
});
pub fn main() !void {
std.debug.print("sqr 1: {}\n", .{sqr.call(.{0xFF})});
std.debug.print("sqr 2: {}\n", .{sqr.call(.{0xFFFF})});
std.debug.print("sqr 3: {}\n", .{sqr.call(.{0xFFFF_FFFF})});
std.debug.print("sqr 4: {}\n", .{sqr.call(.{0xFFFF_FFFF_FFFF_FFFF})});
std.debug.print("string conversion 1: {}\n", .{set.call(.{"hello"})});
std.debug.print("string_res_2: {}\n", .{set.call(.{ "hello", "world" })});
const a: i32, const b: i32 = .{ 42, 12 };
std.debug.print("number: {}\n", .{set.call(.{ a, b })});
std.debug.print("number with conversion: {}\n", .{set.call(.{ 42, 1211 })});
std.debug.print("int and string: {}\n", .{set.call(.{ 230045, "pizza" })});
}
@SimonLSchlee
Copy link
Author

Output:

sqr 1: 65025
sqr 2: 4294836225
sqr 3: 18446744065119617025
sqr 4: 340282366920938463426481119284349108225
string conversion 1: 532
string_res_2: 1084
number: 54
number with conversion: 1253

@SimonLSchlee
Copy link
Author

This version was mostly me experimenting with the code and language features, the customization embedded in the main tuple was an experiment, but I think the comments in the forum are right, that it doesn't make sense to mix the functions with the customization options. While I think there may be cases where customization makes sense, using explicit parameters for that makes everything easier, also I initially thought I would have much more ideas for customization then I actually had in the end.

In the end I think the best is to go back to the more simple implementation, by taking out the customization and just pass the tuple of function bodies, then just directly add for example const conversion if that is something you want to use.

I currently don't have a project where I need this overload set, If I need it someday I would create a version that is simple, non customizable and adapted to that project.

In the mean time I suggest, you take a look at this project https://github.com/andrewCodeDev/Metaphor/blob/main/src/overloadset.zig of AndrewCodeDev where he uses his cleaned up and improved version, to see a project where it fits a practical use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment