I think I prefer the no-abstraction version, of the same thing (because it is essentially the same thing but much simpler).
fn bar(s: Specialization, input: u8) i32 {
return switch (s.a) {
inline else => |a| switch (s.b) {
inline else => |b| switch (s.c) {
inline else => |c| if (a) 32 * input else @as(i32, c) * 9,
},
},
};
}
Which has the added benefit that it gives you a compile error because the b capture isn’t used, giving you the hint to remove a dimmension/switch/nesting-level that has no influence on the result. I think it also makes it easier to add or remove the inline in front of some of the else prongs, allowing you to experiment with inlining some parts but not others.
It also allows you to pick different orders what gets switched on first (but I don’t know whether that will have an influence on the compiler’s output if all are inline, or whether it will create some normalized ordering in the generated code).
Writing the code with all those wrapper functions makes discovering something like the unused b more difficult.
I think it would be possible, essentially you could produce an enum with all possible permutations from the struct and then switch on that.
After watching this lengthy talk about using bit patterns and using them in different convenient ways and trying to relate that to Zig, I think here is a way we can do something equivalent in Zig:
- use a
packed struct to define the parameter space of Specialization, that gives us something we can use with @bitCast
- use comptime to construct an enum type that represents the enumeration of the different permutations that are possible for
Specialization, basically we can iterate through all bit patterns of Specialization and generate field names corresponding with the selected options per dimmension. So .{ .a = false, .b = false, .c = 0 } is used to generate a enum field false_false_0 with the value that corresponds to the bitpattern of the packed struct value.
- now we can create a function
permutations(packed_struct_instance) that automatically casts the bitdata of the packed struct and returns it cast to the corresponding enum value (that also has this bit pattern). That way we can easily switch between multi-dimmensional data (in the packed struct) and one-dimmensional enumeration of the possible permutations, mostly through reinterpreting the same bits as different types.
- now that we have an enum we can switch on it, and you are free to use
inline else => |enum_val| func(parameters(Specialization, enum_val)) to convert the comptime known enum_val back to a packed struct value
- now we can switch between both views (I haven’t done much analysis of the generated code, but because the same bitpatterns get treated as different types, there should be almost no, or actually no overhead)
- switching on the permutations allows us to choose whether we want to treat all prongs the same or not and whether we want to use inline or not (based on what we need)
const Specialization = packed struct(u4) {
a: bool,
b: bool,
c: u2,
};
// expensive comptime-specialized function
fn foo(comptime s: Specialization, input: u8) i32 {
if (s.a) return 32 * input;
return @as(i32, s.c) * 9;
}
fn bar(s: Specialization, input: u8) i32 {
return switch (permutations(s)) {
.true_true_0 => @as(i32, 13279),
inline else => |val| foo(parameters(Specialization, val), input),
};
}
pub fn main() !void {
var val: Specialization = .{ .a = false, .b = true, .c = 2 };
if (true) {
val.c = 3;
}
std.debug.print("res: {}\n", .{bar(val, 100)});
val.a = true;
val.c = 0;
std.debug.print("res: {}\n", .{bar(val, 3)});
}
pub fn debugging(val: Specialization) void {
const int1: u4 = @bitCast(val);
const int2: u4 = @intFromEnum(permutations(val));
const e1: Permutations(Specialization) = @enumFromInt(int2);
const e2: Permutations(Specialization) = .true_true_0;
const int3: u4 = @intFromEnum(e2);
std.debug.print("int1: {} int2: {} e1: {s} e2: {s} int3: {}\n", .{ int1, int2, @tagName(e1), @tagName(e2), int3 });
var it: Enumerate(Specialization) = .init;
var i: usize = 0;
while (it.next()) |x| {
std.debug.print(
\\i: {}
\\x: {}
\\e: {}
\\x': {}
\\
\\
, .{
i,
x,
permutations(x),
parameters(Specialization, permutations(x)),
});
i += 1;
}
}
pub fn Permutations(comptime T: type) type {
std.debug.assert(@typeInfo(T).@"struct".backing_integer != null); // bitcast support required
const fields = std.meta.fields(T);
const combinations = 1 << @bitSizeOf(T);
const BackingInt = std.math.IntFittingRange(0, combinations - 1);
const EnumField = std.builtin.Type.EnumField;
var enum_fields: [combinations]EnumField = undefined;
for (0..combinations) |c| {
const int: BackingInt = @intCast(c);
const s: T = @bitCast(int);
var name: []const u8 = "";
for (fields) |field| {
const val = @field(s, field.name);
name = name ++ "_" ++ getName(field.type, index(val));
}
const field_name = std.fmt.comptimePrint("{s}", .{name[1..]});
enum_fields[c] = .{
.name = field_name,
.value = c,
};
}
return @Type(.{ .@"enum" = .{
.tag_type = BackingInt,
.fields = &enum_fields,
.decls = &.{},
.is_exhaustive = true,
} });
}
/// Given a packed struct instance `value`, returns the corresponding permutation enum value.
/// TODO check the machine code
pub fn permutations(value: anytype) Permutations(@TypeOf(value)) {
const V = @TypeOf(value);
const info = @typeInfo(V).@"struct";
std.debug.assert(info.layout == .@"packed");
const BackingInt = info.backing_integer.?;
const int: BackingInt = @bitCast(value);
return @enumFromInt(int);
}
/// Given a packed struct type `T` and a permutations enum `value`, returns the corresponding packed struct instance.
pub fn parameters(comptime T: type, value: anytype) T {
const info = @typeInfo(T).@"struct";
std.debug.assert(info.layout == .@"packed");
const BackingInt = info.backing_integer.?;
const int: BackingInt = @intFromEnum(value);
return @bitCast(int);
}
// helper function for testing
fn check(comptime T: type) !void {
const E = Permutations(T);
const BackingInt = @typeInfo(T).@"struct".backing_integer.?;
const len = std.meta.fields(E).len;
for (0..len) |i| {
const int: BackingInt = @intCast(i);
const e: E = @enumFromInt(i);
const s: T = @bitCast(int);
try std.testing.expectEqual(e, permutations(s));
}
}
test permutations {
try check(Specialization);
try check(packed struct { a: bool, b: bool });
const Target = enum(u1) { current, prev };
const Dir = enum(u2) { up, down, left, right };
const TargetDir = packed struct(u3) {
target: Target,
dir: Dir,
};
try check(TargetDir);
const context = struct {
const Vec2 = struct {
x: f32,
y: f32,
pub fn v(x: f32, y: f32) Vec2 {
return .{ .x = x, .y = y };
}
pub fn s(scalar: f32) Vec2 {
return .v(scalar, scalar);
}
pub fn mul(a: Vec2, b: Vec2) Vec2 {
return .v(a.x * b.x, a.y * b.y);
}
};
fn move(comptime td: TargetDir, speed: f32) Vec2 {
// making up something
const invert: Vec2 = .s(if (td.target == .current) 1 else -1);
const dir: Vec2 = switch (td.dir) {
.up => .v(0, -1),
.down => .v(0, 1),
.left => .v(-1, 0),
.right => .v(1, 0),
};
return invert.mul(dir).mul(.s(speed));
}
};
const example: TargetDir = .{
.dir = .down,
.target = .current,
};
var speed: f32 = 0;
speed = 3.5;
const result = switch (permutations(example)) {
// this doesn't make sense here but you also could handle some cases different:
// .current_left => |enum_val| context.move(doSomethingElse(parameters(TargetDir, enum_val)), speed),
inline else => |enum_val| context.move(parameters(TargetDir, enum_val), speed),
};
try std.testing.expectEqual(context.Vec2.v(0, 3.5), result);
// var it: Enumerate(TargetDir) = .init;
// while (it.next()) |td| {
// const result2: i4 = switch (permutations(td)) {
// .current_down => 1,
// .current_right => 1,
// inline .current_left, .current_up => -1,
//
// .prev_up => -2,
// .prev_down => 2,
// .prev_left => -2,
// .prev_right => 2,
// };
// std.debug.print("result2: {}\n", .{result2});
// }
}
// index, getName define the mapping between types field-names and bit patterns
// a generic implementation of `Permutations` could accept these mapping functions
// as a parameter, ideally it would check the functions for consistency,
// so that an user supplied implementation causes an erro if it has a bug.
//
// Then it would be possible to provide different default implementations that result,
// in different naming schemes etc.
pub fn index(val: anytype) comptime_int {
const Val = @TypeOf(val);
return switch (@typeInfo(Val)) {
.bool => if (val) 1 else 0,
.int => val,
.@"enum" => @intFromEnum(val),
else => @compileError(@typeName(Val) ++ " is not implemented"),
};
}
pub fn getName(comptime T: type, comptime i: comptime_int) []const u8 {
return switch (@typeInfo(T)) {
.bool => switch (i) {
0 => "false",
1 => "true",
else => @compileError(std.fmt.comptimePrint("invalid index {}", .{i})),
},
.int => std.fmt.comptimePrint("{}", .{i}),
.@"enum" => |_| @tagName(@as(T, @enumFromInt(i))),
else => @compileError(@typeName(T) ++ " is not implemented"),
};
}
pub fn Enumerate(comptime T: type) type {
const Int = @typeInfo(T).@"struct".backing_integer.?;
const last = std.math.maxInt(Int);
const zero: Int = 0;
return struct {
pub const init: @This() = .{
.current = @as(Int, @bitCast(zero)),
};
current: ?Int,
pub fn next(self: *@This()) ?T {
const c = self.current orelse return null;
const res: T = @bitCast(c);
self.current = if (c == last) null else c + 1;
return res;
}
};
}
pub fn enumerate(comptime T: type) Enumerate(T) {
const int: Enumerate(T).Int = @bitCast(0);
return .{ .current = int };
}
const std = @import("std");