From 54d1632102bc182ad34b0cc57bcdcdadccf1079f Mon Sep 17 00:00:00 2001 From: Lukas Lalinsky Date: Thu, 11 Sep 2025 10:16:09 +0200 Subject: [PATCH 1/4] Add plain enum support - Add src/enum.zig with pack/unpack functions for all enum types - Support plain enums: enum { foo, bar } - Support explicit backing type enums: enum(u8) { foo = 1, bar = 2 } - Support mixed enums with auto and explicit values - Support optional enums with null handling - Update any.zig to handle .@"enum" type info - Add enum methods to Packer/Unpacker APIs - Add comprehensive tests for all enum variations Enums are serialized as their underlying integer values using @intFromEnum/@enumFromInt conversions. Backing type is automatically determined via @typeInfo(T).@"enum".tag_type. --- src/any.zig | 7 +++ src/enum.zig | 130 ++++++++++++++++++++++++++++++++++++++++++++++++ src/msgpack.zig | 70 ++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 src/enum.zig diff --git a/src/any.zig b/src/any.zig index 1f64253..90620f0 100644 --- a/src/any.zig +++ b/src/any.zig @@ -34,6 +34,10 @@ const unpackStruct = @import("struct.zig").unpackStruct; const packUnion = @import("union.zig").packUnion; const unpackUnion = @import("union.zig").unpackUnion; +const getEnumSize = @import("enum.zig").getEnumSize; +const packEnum = @import("enum.zig").packEnum; +const unpackEnum = @import("enum.zig").unpackEnum; + inline fn isString(comptime T: type) bool { switch (@typeInfo(T)) { .pointer => |ptr_info| { @@ -56,6 +60,7 @@ pub fn sizeOfPackedAny(comptime T: type, value: T) usize { .bool => return getBoolSize(), .int => return getIntSize(T, value), .float => return getFloatSize(T, value), + .@"enum" => return getEnumSize(T, value), .pointer => |ptr_info| { if (ptr_info.size == .Slice) { if (isString(T)) { @@ -105,6 +110,7 @@ pub fn packAny(writer: anytype, value: anytype) !void { }, .@"struct" => return packStruct(writer, T, value), .@"union" => return packUnion(writer, T, value), + .@"enum" => return packEnum(writer, T, value), .optional => { if (value) |val| { return packAny(writer, val); @@ -125,6 +131,7 @@ pub fn unpackAny(reader: anytype, allocator: std.mem.Allocator, comptime T: type .float => return unpackFloat(reader, T), .@"struct" => return unpackStruct(reader, allocator, T), .@"union" => return unpackUnion(reader, allocator, T), + .@"enum" => return unpackEnum(reader, T), .pointer => |ptr_info| { if (ptr_info.size == .slice) { if (isString(T)) { diff --git a/src/enum.zig b/src/enum.zig new file mode 100644 index 0000000..be0f0a1 --- /dev/null +++ b/src/enum.zig @@ -0,0 +1,130 @@ +const std = @import("std"); +const hdrs = @import("headers.zig"); + +const NonOptional = @import("utils.zig").NonOptional; +const maybePackNull = @import("null.zig").maybePackNull; +const maybeUnpackNull = @import("null.zig").maybeUnpackNull; + +const getIntSize = @import("int.zig").getIntSize; +const packInt = @import("int.zig").packInt; +const unpackInt = @import("int.zig").unpackInt; + +inline fn assertEnumType(comptime T: type) type { + switch (@typeInfo(T)) { + .@"enum" => return T, + .optional => |opt_info| { + return assertEnumType(opt_info.child); + }, + else => @compileError("Expected enum, got " ++ @typeName(T)), + } +} + +pub fn getMaxEnumSize(comptime T: type) usize { + const Type = assertEnumType(T); + const tag_type = @typeInfo(Type).@"enum".tag_type; + return 1 + @sizeOf(tag_type); +} + +pub fn getEnumSize(comptime T: type, value: T) usize { + const Type = assertEnumType(T); + const tag_type = @typeInfo(Type).@"enum".tag_type; + const int_value = @intFromEnum(value); + return getIntSize(tag_type, int_value); +} + +pub fn packEnum(writer: anytype, comptime T: type, value_or_maybe_null: T) !void { + const Type = assertEnumType(T); + const value: Type = try maybePackNull(writer, T, value_or_maybe_null) orelse return; + + const tag_type = @typeInfo(Type).@"enum".tag_type; + const int_value = @intFromEnum(value); + + try packInt(writer, tag_type, int_value); +} + +pub fn unpackEnum(reader: anytype, comptime T: type) !T { + const Type = assertEnumType(T); + const tag_type = @typeInfo(Type).@"enum".tag_type; + const int_value = try unpackInt(reader, tag_type); + return @enumFromInt(int_value); +} + +test "getMaxEnumSize" { + const PlainEnum = enum { foo, bar }; + const U8Enum = enum(u8) { foo = 1, bar = 2 }; + const U16Enum = enum(u16) { foo, bar }; + + try std.testing.expectEqual(2, getMaxEnumSize(PlainEnum)); // u1 + header + try std.testing.expectEqual(2, getMaxEnumSize(U8Enum)); // u8 + header + try std.testing.expectEqual(3, getMaxEnumSize(U16Enum)); // u16 + header +} + +test "getEnumSize" { + const U8Enum = enum(u8) { foo = 0, bar = 150 }; + + try std.testing.expectEqual(1, getEnumSize(U8Enum, .foo)); // fits in positive fixint + try std.testing.expectEqual(2, getEnumSize(U8Enum, .bar)); // requires u8 format +} + +test "pack/unpack enum" { + const PlainEnum = enum { foo, bar }; + const U8Enum = enum(u8) { foo = 1, bar = 2 }; + const U16Enum = enum(u16) { alpha = 1000, beta = 2000 }; + + // Test plain enum + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + try packEnum(buffer.writer(), PlainEnum, .bar); + + var stream = std.io.fixedBufferStream(buffer.items); + const result = try unpackEnum(stream.reader(), PlainEnum); + try std.testing.expectEqual(PlainEnum.bar, result); + } + + // Test enum(u8) + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + try packEnum(buffer.writer(), U8Enum, .bar); + + var stream = std.io.fixedBufferStream(buffer.items); + const result = try unpackEnum(stream.reader(), U8Enum); + try std.testing.expectEqual(U8Enum.bar, result); + } + + // Test enum(u16) + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + try packEnum(buffer.writer(), U16Enum, .alpha); + + var stream = std.io.fixedBufferStream(buffer.items); + const result = try unpackEnum(stream.reader(), U16Enum); + try std.testing.expectEqual(U16Enum.alpha, result); + } +} + + +test "enum edge cases" { + // Test enum with explicit and auto values + const MixedEnum = enum(u8) { + first = 10, + second, // auto-assigned to 11 + third = 20, + fourth, // auto-assigned to 21 + }; + + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + try packEnum(buffer.writer(), MixedEnum, .second); + + var stream = std.io.fixedBufferStream(buffer.items); + const result = try unpackEnum(stream.reader(), MixedEnum); + try std.testing.expectEqual(MixedEnum.second, result); + try std.testing.expectEqual(11, @intFromEnum(result)); +} \ No newline at end of file diff --git a/src/msgpack.zig b/src/msgpack.zig index 0e6fb23..7020dec 100644 --- a/src/msgpack.zig +++ b/src/msgpack.zig @@ -65,6 +65,11 @@ pub const UnionAsMapOptions = @import("union.zig").UnionAsMapOptions; pub const packUnion = @import("union.zig").packUnion; pub const unpackUnion = @import("union.zig").unpackUnion; +pub const getEnumSize = @import("enum.zig").getEnumSize; +pub const getMaxEnumSize = @import("enum.zig").getMaxEnumSize; +pub const packEnum = @import("enum.zig").packEnum; +pub const unpackEnum = @import("enum.zig").unpackEnum; + pub const packAny = @import("any.zig").packAny; pub const unpackAny = @import("any.zig").unpackAny; @@ -144,6 +149,10 @@ pub fn Packer(comptime Writer: type) type { return packUnion(self.writer, @TypeOf(value), value); } + pub fn writeEnum(self: Self, value: anytype) !void { + return packEnum(self.writer, @TypeOf(value), value); + } + pub fn write(self: Self, value: anytype) !void { return packAny(self.writer, value); } @@ -232,6 +241,10 @@ pub fn Unpacker(comptime Reader: type) type { return unpackUnion(self.reader, self.allocator, T); } + pub fn readEnum(self: Self, comptime T: type) !T { + return unpackEnum(self.reader, T); + } + pub fn read(self: Self, comptime T: type) !T { return unpackAny(self.reader, self.allocator, T); } @@ -304,3 +317,60 @@ test "encode/decode" { try std.testing.expectEqualStrings("John", decoded.value.name); try std.testing.expectEqual(20, decoded.value.age); } + +test "encode/decode enum" { + const Status = enum(u8) { pending = 1, active = 2, inactive = 3 }; + const PlainEnum = enum { foo, bar, baz }; + + // Test enum(u8) + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + try encode(Status.active, buffer.writer()); + + const decoded = try decodeFromSlice(Status, std.testing.allocator, buffer.items); + defer decoded.deinit(); + + try std.testing.expectEqual(Status.active, decoded.value); + } + + // Test plain enum + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + try encode(PlainEnum.bar, buffer.writer()); + + const decoded = try decodeFromSlice(PlainEnum, std.testing.allocator, buffer.items); + defer decoded.deinit(); + + try std.testing.expectEqual(PlainEnum.bar, decoded.value); + } + + // Test optional enum with null + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + try encode(@as(?Status, null), buffer.writer()); + + const decoded = try decodeFromSlice(?Status, std.testing.allocator, buffer.items); + defer decoded.deinit(); + + try std.testing.expectEqual(@as(?Status, null), decoded.value); + } + + // Test optional enum with value + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + try encode(@as(?Status, .pending), buffer.writer()); + + const decoded = try decodeFromSlice(?Status, std.testing.allocator, buffer.items); + defer decoded.deinit(); + + try std.testing.expectEqual(@as(?Status, .pending), decoded.value); + } +} From 32477dc1a1c3e8d28305362844539a7d9735b168 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:44:58 +0000 Subject: [PATCH 2/4] Fix enum optional handling issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix getEnumSize to properly handle optional enum types - Fix unpackEnum to support null handling for optional enums - Add comprehensive tests for optional enum functionality - Addresses CodeRabbit review feedback Co-authored-by: Lukáš Lalinský --- src/enum.zig | 114 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 8 deletions(-) diff --git a/src/enum.zig b/src/enum.zig index be0f0a1..8fe461d 100644 --- a/src/enum.zig +++ b/src/enum.zig @@ -26,10 +26,21 @@ pub fn getMaxEnumSize(comptime T: type) usize { } pub fn getEnumSize(comptime T: type, value: T) usize { - const Type = assertEnumType(T); - const tag_type = @typeInfo(Type).@"enum".tag_type; - const int_value = @intFromEnum(value); - return getIntSize(tag_type, int_value); + switch (@typeInfo(T)) { + .@"enum" => { + const tag_type = @typeInfo(T).@"enum".tag_type; + const int_value = @intFromEnum(value); + return getIntSize(tag_type, int_value); + }, + .optional => |opt_info| { + if (value) |v| { + return getEnumSize(opt_info.child, v); + } else { + return 1; // size of null + } + }, + else => @compileError("Expected enum or optional enum, got " ++ @typeName(T)), + } } pub fn packEnum(writer: anytype, comptime T: type, value_or_maybe_null: T) !void { @@ -43,10 +54,53 @@ pub fn packEnum(writer: anytype, comptime T: type, value_or_maybe_null: T) !void } pub fn unpackEnum(reader: anytype, comptime T: type) !T { - const Type = assertEnumType(T); - const tag_type = @typeInfo(Type).@"enum".tag_type; - const int_value = try unpackInt(reader, tag_type); - return @enumFromInt(int_value); + switch (@typeInfo(T)) { + .@"enum" => { + const tag_type = @typeInfo(T).@"enum".tag_type; + const int_value = try unpackInt(reader, tag_type); + return @enumFromInt(int_value); + }, + .optional => |opt_info| { + const header = try reader.readByte(); + if (header == hdrs.NIL) { + return null; + } + + // Put the header back and unpack as non-optional enum + // We need to create a buffered reader that includes the header + const backup_reader = struct { + header: u8, + reader: @TypeOf(reader), + header_consumed: bool = false, + + const Self = @This(); + + pub fn readByte(self: *Self) !u8 { + if (!self.header_consumed) { + self.header_consumed = true; + return self.header; + } + return try self.reader.readByte(); + } + + pub fn readBytesNoEof(self: *Self, buf: []u8) !void { + if (!self.header_consumed and buf.len > 0) { + buf[0] = self.header; + self.header_consumed = true; + if (buf.len > 1) { + try self.reader.readBytesNoEof(buf[1..]); + } + } else { + try self.reader.readBytesNoEof(buf); + } + } + }; + + var backup = backup_reader{ .header = header, .reader = reader }; + return try unpackEnum(backup.reader(), opt_info.child); + }, + else => @compileError("Expected enum or optional enum, got " ++ @typeName(T)), + } } test "getMaxEnumSize" { @@ -127,4 +181,48 @@ test "enum edge cases" { const result = try unpackEnum(stream.reader(), MixedEnum); try std.testing.expectEqual(MixedEnum.second, result); try std.testing.expectEqual(11, @intFromEnum(result)); +} + +test "optional enum" { + const TestEnum = enum(u8) { foo = 1, bar = 2 }; + const OptionalEnum = ?TestEnum; + + // Test non-null optional enum + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + const value: OptionalEnum = .bar; + try packEnum(buffer.writer(), OptionalEnum, value); + + var stream = std.io.fixedBufferStream(buffer.items); + const result = try unpackEnum(stream.reader(), OptionalEnum); + try std.testing.expectEqual(@as(OptionalEnum, .bar), result); + } + + // Test null optional enum + { + var buffer = std.ArrayList(u8).init(std.testing.allocator); + defer buffer.deinit(); + + const value: OptionalEnum = null; + try packEnum(buffer.writer(), OptionalEnum, value); + + var stream = std.io.fixedBufferStream(buffer.items); + const result = try unpackEnum(stream.reader(), OptionalEnum); + try std.testing.expectEqual(@as(OptionalEnum, null), result); + } +} + +test "getEnumSize with optional" { + const TestEnum = enum(u8) { foo = 0, bar = 150 }; + const OptionalEnum = ?TestEnum; + + // Test non-null optional enum size + const value: OptionalEnum = .bar; + try std.testing.expectEqual(2, getEnumSize(OptionalEnum, value)); // requires u8 format + + // Test null optional enum size + const null_value: OptionalEnum = null; + try std.testing.expectEqual(1, getEnumSize(OptionalEnum, null_value)); // size of null } \ No newline at end of file From 331dac80a01481f7112390dae8b99120a22a7161 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:42:06 +0000 Subject: [PATCH 3/4] Refactor enum optional handling to use existing framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove custom backup reader mechanism (44 lines removed) - Use maybeUnpackNull(header, T) pattern like int.zig - Simplify getEnumSize to handle optionals recursively - Follow established unpackInt dispatch pattern for header handling - Maintains full compatibility with existing tests Co-authored-by: Lukáš Lalinský --- src/enum.zig | 100 +++++++++++++++++++++------------------------------ 1 file changed, 40 insertions(+), 60 deletions(-) diff --git a/src/enum.zig b/src/enum.zig index 8fe461d..790d2c0 100644 --- a/src/enum.zig +++ b/src/enum.zig @@ -26,21 +26,17 @@ pub fn getMaxEnumSize(comptime T: type) usize { } pub fn getEnumSize(comptime T: type, value: T) usize { - switch (@typeInfo(T)) { - .@"enum" => { - const tag_type = @typeInfo(T).@"enum".tag_type; - const int_value = @intFromEnum(value); - return getIntSize(tag_type, int_value); - }, - .optional => |opt_info| { - if (value) |v| { - return getEnumSize(opt_info.child, v); - } else { - return 1; // size of null - } - }, - else => @compileError("Expected enum or optional enum, got " ++ @typeName(T)), + if (@typeInfo(T) == .optional) { + if (value) |v| { + return getEnumSize(@typeInfo(T).optional.child, v); + } else { + return 1; // size of null + } } + + const tag_type = @typeInfo(T).@"enum".tag_type; + const int_value = @intFromEnum(value); + return getIntSize(tag_type, int_value); } pub fn packEnum(writer: anytype, comptime T: type, value_or_maybe_null: T) !void { @@ -54,52 +50,36 @@ pub fn packEnum(writer: anytype, comptime T: type, value_or_maybe_null: T) !void } pub fn unpackEnum(reader: anytype, comptime T: type) !T { - switch (@typeInfo(T)) { - .@"enum" => { - const tag_type = @typeInfo(T).@"enum".tag_type; - const int_value = try unpackInt(reader, tag_type); - return @enumFromInt(int_value); - }, - .optional => |opt_info| { - const header = try reader.readByte(); - if (header == hdrs.NIL) { - return null; - } - - // Put the header back and unpack as non-optional enum - // We need to create a buffered reader that includes the header - const backup_reader = struct { - header: u8, - reader: @TypeOf(reader), - header_consumed: bool = false, - - const Self = @This(); - - pub fn readByte(self: *Self) !u8 { - if (!self.header_consumed) { - self.header_consumed = true; - return self.header; - } - return try self.reader.readByte(); - } - - pub fn readBytesNoEof(self: *Self, buf: []u8) !void { - if (!self.header_consumed and buf.len > 0) { - buf[0] = self.header; - self.header_consumed = true; - if (buf.len > 1) { - try self.reader.readBytesNoEof(buf[1..]); - } - } else { - try self.reader.readBytesNoEof(buf); - } - } - }; - - var backup = backup_reader{ .header = header, .reader = reader }; - return try unpackEnum(backup.reader(), opt_info.child); - }, - else => @compileError("Expected enum or optional enum, got " ++ @typeName(T)), + const Type = assertEnumType(T); + const tag_type = @typeInfo(Type).@"enum".tag_type; + + const header = try reader.readByte(); + + if (header <= hdrs.POSITIVE_FIXINT_MAX) { + return @enumFromInt(@as(tag_type, @intCast(header))); + } + + if (header >= hdrs.NEGATIVE_FIXINT_MIN) { + const value: i8 = @bitCast(header); + const tag_type_info = @typeInfo(tag_type); + if (tag_type_info.int.signedness == .signed) { + return @enumFromInt(@as(tag_type, value)); + } else if (value >= 0) { + return @enumFromInt(@as(tag_type, @intCast(value))); + } + return error.IntegerOverflow; + } + + switch (header) { + hdrs.INT8 => return @enumFromInt(try unpackIntValue(reader, i8, tag_type)), + hdrs.INT16 => return @enumFromInt(try unpackIntValue(reader, i16, tag_type)), + hdrs.INT32 => return @enumFromInt(try unpackIntValue(reader, i32, tag_type)), + hdrs.INT64 => return @enumFromInt(try unpackIntValue(reader, i64, tag_type)), + hdrs.UINT8 => return @enumFromInt(try unpackIntValue(reader, u8, tag_type)), + hdrs.UINT16 => return @enumFromInt(try unpackIntValue(reader, u16, tag_type)), + hdrs.UINT32 => return @enumFromInt(try unpackIntValue(reader, u32, tag_type)), + hdrs.UINT64 => return @enumFromInt(try unpackIntValue(reader, u64, tag_type)), + else => return maybeUnpackNull(header, T), } } From c5f98d2c14a93e3fb30e3012e83c6ba7d4fbdb0a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:49:30 +0000 Subject: [PATCH 4/4] Optimize enum unpacking to use unpackInt directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Construct optional tag type (?tag_type vs tag_type) based on enum optionality - Delegate all msgpack parsing logic to existing unpackInt function - Simplify unpackEnum from 31 lines to 15 lines - Remove complex header parsing and format-specific handling - Clean up unused imports (hdrs, NonOptional, maybeUnpackNull) Co-authored-by: Lukáš Lalinský --- src/enum.zig | 41 +++++++++++++---------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/enum.zig b/src/enum.zig index 790d2c0..1ac4174 100644 --- a/src/enum.zig +++ b/src/enum.zig @@ -1,9 +1,6 @@ const std = @import("std"); -const hdrs = @import("headers.zig"); -const NonOptional = @import("utils.zig").NonOptional; const maybePackNull = @import("null.zig").maybePackNull; -const maybeUnpackNull = @import("null.zig").maybeUnpackNull; const getIntSize = @import("int.zig").getIntSize; const packInt = @import("int.zig").packInt; @@ -53,33 +50,21 @@ pub fn unpackEnum(reader: anytype, comptime T: type) !T { const Type = assertEnumType(T); const tag_type = @typeInfo(Type).@"enum".tag_type; - const header = try reader.readByte(); + // Construct the optional tag type to match T's optionality + const OptionalTagType = if (@typeInfo(T) == .optional) ?tag_type else tag_type; - if (header <= hdrs.POSITIVE_FIXINT_MAX) { - return @enumFromInt(@as(tag_type, @intCast(header))); - } - - if (header >= hdrs.NEGATIVE_FIXINT_MIN) { - const value: i8 = @bitCast(header); - const tag_type_info = @typeInfo(tag_type); - if (tag_type_info.int.signedness == .signed) { - return @enumFromInt(@as(tag_type, value)); - } else if (value >= 0) { - return @enumFromInt(@as(tag_type, @intCast(value))); + // Use unpackInt directly with the constructed optional tag type + const int_value = try unpackInt(reader, OptionalTagType); + + // Handle the optional case + if (@typeInfo(T) == .optional) { + if (int_value) |value| { + return @enumFromInt(value); + } else { + return null; } - return error.IntegerOverflow; - } - - switch (header) { - hdrs.INT8 => return @enumFromInt(try unpackIntValue(reader, i8, tag_type)), - hdrs.INT16 => return @enumFromInt(try unpackIntValue(reader, i16, tag_type)), - hdrs.INT32 => return @enumFromInt(try unpackIntValue(reader, i32, tag_type)), - hdrs.INT64 => return @enumFromInt(try unpackIntValue(reader, i64, tag_type)), - hdrs.UINT8 => return @enumFromInt(try unpackIntValue(reader, u8, tag_type)), - hdrs.UINT16 => return @enumFromInt(try unpackIntValue(reader, u16, tag_type)), - hdrs.UINT32 => return @enumFromInt(try unpackIntValue(reader, u32, tag_type)), - hdrs.UINT64 => return @enumFromInt(try unpackIntValue(reader, u64, tag_type)), - else => return maybeUnpackNull(header, T), + } else { + return @enumFromInt(int_value); } }