New way to check if number is inside of enum

It just occured to me with switch on non exhaustive enums landing in 0.15.1 it is now trivial to check if value is valid enum variant or not.

const Piece = enum(u8) {
    king = 0,
    queen,
    knight,
    bishop, 
    rock,
    pawn,
    _,
    
    pub fn isValid(p: Piece) bool {
        return switch (p) {
            else => true,
            _ => false,
        };
    }
}

So for example std.enums.fromInt can be reimplemented without for loop

const std = @import("std");

const Piece = enum(u8) {
    king = 0,
    queen,
    knight,
    bishop,
    rock,
    pawn,
};

pub fn oldFromInt(comptime E: type, integer: anytype) ?E {
    const enum_info = @typeInfo(E).@"enum";
    if (!enum_info.is_exhaustive) {
        if (std.math.cast(enum_info.tag_type, integer)) |tag| {
            return @enumFromInt(tag);
        }
        return null;
    }
    // We don't directly iterate over the fields of E, as that
    // would require an inline loop. Instead, we create an array of
    // values that is comptime-know, but can be iterated at runtime
    // without requiring an inline loop.
    // This generates better machine code.
    for (std.enums.values(E)) |value| {
        if (@intFromEnum(value) == integer) return @enumFromInt(integer);
    }
    return null;
}

pub fn NonExhaustive(comptime E: type) type {
    var info = @typeInfo(E);
    if (!info.@"enum".is_exhaustive) return E;
    info.@"enum".is_exhaustive = false;
    info.@"enum".decls = &.{};
    return @Type(info);
}

pub fn newFromInt(comptime E: type, integer: anytype) ?E {
    const enum_info = @typeInfo(E).@"enum";
    const trunked = std.math.cast(enum_info.tag_type, integer) orelse return null;

    const value: NonExhaustive(E) = @enumFromInt(trunked);
    switch (value) {
        else => return @enumFromInt(@intFromEnum(value)),
        _ => return if (!enum_info.is_exhaustive) value else null,
    }
    return null;
}

test oldFromInt {
    try std.testing.expectEqual(Piece.king, oldFromInt(Piece, @as(usize, 0)).?);
    try std.testing.expectEqual(null, oldFromInt(Piece, @as(u8, 7)));
}
test newFromInt {
    try std.testing.expectEqual(Piece.king, newFromInt(Piece, @as(usize, 0)).?);
    try std.testing.expectEqual(null, newFromInt(Piece, @as(u8, 7)));

    // even works with comptime_int. This test case will give compile error for `fromInt`
    try std.testing.expectEqual(Piece.king, newFromInt(Piece, 0).?);
    try std.testing.expectEqual(null, newFromInt(Piece, 7));
}

I checked godbolt and it seems to generate roughly similar assembly Compiler Explorer.
I would assume it would faster for comptime evaluation tho (at least its O(1) branch quota instead of O(n))

Maybe relevant to previous discussion How to check if deserialized exhaustive enum value is legal?

9 Likes

Nevermind it actually produces better code for large enums. Compiler Explorer

4 Likes

In old version comment says not using inline for produces better code but in this specific example inline for gives better results compared to my method.
Compiler Explorer :frowning:

Lets inspect produced assembly to check why is that

pub fn newFromInt(comptime E: type, integer: anytype) ?E {
    const enum_info = @typeInfo(E).@"enum";
    const trunked = std.math.cast(enum_info.tag_type, integer) orelse return null;

    const value: NonExhaustive(E) = @enumFromInt(trunked);
    switch (value) {
        else => return @enumFromInt(@intFromEnum(value)),
        _ => return if (!enum_info.is_exhaustive) value else null,
    }
    return null;
}
new:
        cmp     edi, 127
        jbe     .LBB1_2 // this is clearly std.math.cast() orelse return null;
        xor     eax, eax
        ret
.LBB1_2:
        push    rbp
        mov     rbp, rsp
        and     dil, 127
        cmp     dil, 119
        mov     byte ptr [rbp - 1], dil
        setb    al
        pop     rbp
        ret

I tried multiple variations but they all produced this extra jump. Crazy idea what if we cast tag type to number type instead of casting number to tag type?

pub fn NonExhaustive(comptime E: type, comptime Tag: type) type {
    var info = @typeInfo(E);
    if (!info.@"enum".is_exhaustive) return E;
    info.@"enum".is_exhaustive = false;
    info.@"enum".decls = &.{};
    info.@"enum".tag_type = Tag;
    return @Type(info);
}

pub fn newFromInt(comptime E: type, integer: anytype) ?E {    
    const value: NonExhaustive(E, @TypeOf(integer)) = @enumFromInt(integer);
  switch (value) {
      else => return @enumFromInt(integer),
      _ => return null,
    }
}

It works and produces same assembly as inline for option Compiler Explorer

For some reason second option produces much worse assembly. Seems like an optimization bug.

switch (value) {
    else => return @enumFromInt(integer),
    _ => return null,
}
    // and (newFromInt2 in godbolt link above)
return switch (value) {
    else => @enumFromInt(integer),
    _ => null,
};

EDIT: last version works as originally designed only for Exhaustive enums but it wont affect results in any way so I dropped it for readability

1 Like

Both newFromInt and OldFromIntInline are leveraging the fact that Tag is contiguous, but newFromInt is doing some extra unnecessary work, which I’m not sure where it came from, maybe it’s not able to optimize away the call to cast. It’s possible to leverage this advantage using comptime (goldbolt), which removes the guesswork about what the compiler is doing. When I implemented the current version, I did lots of tests, and inline loops always generated worse code, not only in this case, but also many different loops in my own project.
In any case, I’ve been meaning to update the previous implementation of fromInt for a while, but review of pull requests has been kind of slow lately, so I thought it wouldn’t get anywhere.
When the enum is not contiguous, there are also options for optimizing it:
1 - Order the values and do a binary search or
2 - Create a bitset, where each index represents whether that value is valid or not.

2 Likes

Maybe a little bit off topic but just flying by here and triggered by the chess piece.
Mostly when I need to do (simple) enum / int stuff I use this trick: no need for @conversions.

pub const PieceType = packed union
{
    pub const all: [6]PieceType = .{ PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING };

    pub const NO_PIECETYPE: PieceType = .{ .e = .no_piecetype };
    pub const PAWN: PieceType = .{ .e = .pawn };
    pub const KNIGHT: PieceType = .{ .e = .knight };
    pub const BISHOP: PieceType = .{ .e = .bishop };
    pub const ROOK: PieceType = .{ .e = .rook };
    pub const QUEEN: PieceType = .{ .e = .queen };
    pub const KING: PieceType = .{ .e = .king };

    pub const Enum = enum(u3)
    {
        no_piecetype = 0,
        pawn = 1,
        knight = 2,
        bishop = 3,
        rook = 4,
        queen = 5,
        king = 6,
    };

    /// The enum value.
    e: Enum,
    /// The numeric value.
    u: u3,
};