From cfe51bfebdfbc83875a34803c6a1ad48546aa866 Mon Sep 17 00:00:00 2001 From: DidaS Date: Tue, 11 Nov 2025 21:22:40 +0000 Subject: [PATCH 01/30] Zig 0.15.1 --- zig/build.zig | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/zig/build.zig b/zig/build.zig index b8864d5..f11d19d 100644 --- a/zig/build.zig +++ b/zig/build.zig @@ -7,13 +7,17 @@ pub fn build(b: *std.Build) void { const config_options = b.addOptions(); config_options.addOption(u8, "HBP_VERSION", 1); - const exe = b.addExecutable(.{ - .name = "hbp", + const exe_mod = b.addModule("hbp", .{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); + const exe = b.addExecutable(.{ + .name = "hbp", + .root_module = exe_mod, + }); + b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); @@ -26,12 +30,7 @@ pub fn build(b: *std.Build) void { const check = b.step("check", "Check if it compiles"); check.dependOn(&exe.step); - const exe_test = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - + const exe_test = b.addTest(.{ .root_module = exe_mod }); exe_test.root_module.addOptions("config", config_options); const test_artifact = b.addRunArtifact(exe_test); From bacd8cb19e15c7221579a67803b560102dbae6b3 Mon Sep 17 00:00:00 2001 From: DidaS Date: Wed, 12 Nov 2025 02:53:17 +0000 Subject: [PATCH 02/30] Initial deserializer implementation --- .vscode/cspell/zig.cspell | 26 ++-- README.md | 2 + zig/build.zig | 15 ++- zig/build.zig.zon | 3 +- zig/src/Deserializer.zig | 85 ------------- zig/src/Scanner.zig | 261 ++++++++++++++++++++++++++++++++++++++ zig/src/Serializer.zig | 47 +++++-- zig/src/main.zig | 77 ++++++++++- zig/src/root.zig | 8 ++ zig/src/scanner_test.zig | 98 ++++++++++++++ zig/src/static.zig | 80 ++++++++++++ 11 files changed, 586 insertions(+), 116 deletions(-) delete mode 100644 zig/src/Deserializer.zig create mode 100644 zig/src/Scanner.zig create mode 100644 zig/src/root.zig create mode 100644 zig/src/scanner_test.zig create mode 100644 zig/src/static.zig diff --git a/.vscode/cspell/zig.cspell b/.vscode/cspell/zig.cspell index 466d704..e5859b8 100644 --- a/.vscode/cspell/zig.cspell +++ b/.vscode/cspell/zig.cspell @@ -1,18 +1,16 @@ -comptime -struct -nosuspend -usingnamespace +anyerror +anyopaque +anytype +bitstack callconv -orelse +comptime +deinit errdefer - -usize isize -anytype -anyopaque -anyerror - -deinit - memcpy -memset \ No newline at end of file +memset +nosuspend +orelse +struct +usingnamespace +usize diff --git a/README.md b/README.md index af72c38..77d5f44 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,8 @@ The string marker is always followed by a list marker with the type byte set to | `20` | UTF-8 | | `21` | UTF-16 | +The sender ensures the encoding of the strings is valid therefor, validation on the client is unnecessary. + ## Vector Marker: `E3` diff --git a/zig/build.zig b/zig/build.zig index f11d19d..6338aee 100644 --- a/zig/build.zig +++ b/zig/build.zig @@ -7,7 +7,7 @@ pub fn build(b: *std.Build) void { const config_options = b.addOptions(); config_options.addOption(u8, "HBP_VERSION", 1); - const exe_mod = b.addModule("hbp", .{ + const exe_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, @@ -20,6 +20,19 @@ pub fn build(b: *std.Build) void { b.installArtifact(exe); + const hbp = b.addModule("hbp", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const libhbp = b.addLibrary(.{ + .name = "hbp", + .root_module = hbp, + }); + + b.installArtifact(libhbp); + const run_cmd = b.addRunArtifact(exe); exe.root_module.addOptions("config", config_options); run_cmd.step.dependOn(b.getInstallStep()); diff --git a/zig/build.zig.zon b/zig/build.zig.zon index 384c630..1125095 100644 --- a/zig/build.zig.zon +++ b/zig/build.zig.zon @@ -1,7 +1,8 @@ .{ .fingerprint = 0x8af3a6b7cf3baeb7, .name = .libhbp, - .version = "0.0.0", + .version = "0.1.0", + .minimum_zig_version = "0.15.1", .paths = .{ "build.zig", "build.zig.zon", diff --git a/zig/src/Deserializer.zig b/zig/src/Deserializer.zig deleted file mode 100644 index fe2b5a9..0000000 --- a/zig/src/Deserializer.zig +++ /dev/null @@ -1,85 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const HBP_VERSION = @import("config").HBP_VERSION; - -const assert = std.debug.assert; -const expect = std.testing.expect; - -const native_endian = builtin.cpu.arch.endian(); - -const Deserializer = @This(); - -pub fn deserializeBool(buffer: []const u8) bool { - return switch (buffer[1]) { - 0x01 => false, - 0x02 => true, - else => unreachable, - }; -} - -test deserializeBool { - try expect(deserializeBool(&.{ 0x01, 0x01 }) == false); - try expect(deserializeBool(&.{ 0x01, 0x02 }) == true); -} - -/// Fast path deserialization for integers. -/// Skips checks on the type and assumes it will be the same -/// or fit in the given integer type. -pub fn deserializeIntAssumeType(comptime T: type, buffer: []const u8) std.math.ByteAlignedInt(T) { - const alignedType = std.math.ByteAlignedInt(T); - const info = @typeInfo(alignedType).int; - - const base = if (comptime info.signedness == .signed) 0x10 else 0x20; - const buf = if (buffer[0] == HBP_VERSION) buffer[1..] else buffer; - - switch (buf[0]) { - base...(base + 6) => { - const slice = buf[1..]; - assert(slice.len <= (info.bits / 8)); - const data = std.mem.bytesToValue(alignedType, slice); - return std.mem.nativeToBig(alignedType, data); - }, - base + 0x0F => { - const slice = buf[3..]; - assert(slice.len <= (info.bits / 8)); - const data = std.mem.bytesToValue(alignedType, slice); - return std.mem.nativeToBig(alignedType, data); - }, - else => unreachable, - } -} - -pub fn deserializeStringAssumeLength(comptime max_len: u32, buffer: []const u8) !struct { [max_len]u8, u32 } { - var buf = if (buffer[0] == HBP_VERSION) buffer[1..] else buffer; - if (buf[0] != 0xE0) return error.InvalidBuffer; - - buf = buf[3..]; - - var temp: [max_len]u8 = undefined; - @memcpy(temp[0..buf.len], buf); - return .{ temp, @intCast(buf.len) }; -} - -test deserializeIntAssumeType { - try expect(deserializeIntAssumeType(i8, &.{ 0x01, 0x10, 0x2D }) == 45); - try expect(deserializeIntAssumeType(i16, &.{ 0x01, 0x11, 0x18, 0xCB }) == 6347); - try expect(deserializeIntAssumeType(i32, &.{ 0x01, 0x12, 0x00, 0x8B, 0x36, 0x60 }) == 9123424); - try expect(deserializeIntAssumeType(i64, &.{ 0x01, 0x13, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF }) == 5294967295); - try expect(deserializeIntAssumeType(i128, &.{ 0x01, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8A, 0xC7, 0x23, 0x04, 0x89, 0xE7, 0xFF, 0xFF }) == 28446744073709551615); - try expect(deserializeIntAssumeType(i256, &.{ 0x01, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 }) == 340282366920938463463375607431768211456); - try expect(deserializeIntAssumeType(i512, &.{ 0x01, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }) == 115792089237316395423570985008687907853269984665640564039457584007913129639935); - try expect(deserializeIntAssumeType(i6, &.{ 0x01, 0x10, 0x1E }) == 30); - try expect(deserializeIntAssumeType(i38, &.{ 0x01, 0x1F, 0x00, 0x28, 0x00, 0x00, 0x8B, 0x36, 0x60 }) == 9123424); - try expect(deserializeIntAssumeType(i80, &.{ 0x01, 0x1F, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF }) == 5294967295); - - try expect(deserializeIntAssumeType(u8, &.{ 0x01, 0x20, 0xFA }) == 250); - try expect(deserializeIntAssumeType(u16, &.{ 0x01, 0x21, 0x18, 0xCB }) == 6347); - try expect(deserializeIntAssumeType(u32, &.{ 0x01, 0x22, 0x00, 0x8B, 0x36, 0x60 }) == 9123424); - try expect(deserializeIntAssumeType(u64, &.{ 0x01, 0x23, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF }) == 5294967295); - try expect(deserializeIntAssumeType(u128, &.{ 0x01, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8A, 0xC7, 0x23, 0x04, 0x89, 0xE7, 0xFF, 0xFF }) == 28446744073709551615); - try expect(deserializeIntAssumeType(u256, &.{ 0x01, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 }) == 340282366920938463463375607431768211456); - try expect(deserializeIntAssumeType(u512, &.{ 0x01, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }) == 115792089237316395423570985008687907853269984665640564039457584007913129639935); - try expect(deserializeIntAssumeType(u6, &.{ 0x01, 0x20, 0x1E }) == 30); - try expect(deserializeIntAssumeType(u38, &.{ 0x01, 0x2F, 0x00, 0x28, 0x00, 0x00, 0x8B, 0x36, 0x60 }) == 9123424); - try expect(deserializeIntAssumeType(u80, &.{ 0x01, 0x2F, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF }) == 5294967295); -} diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig new file mode 100644 index 0000000..1e9a6f2 --- /dev/null +++ b/zig/src/Scanner.zig @@ -0,0 +1,261 @@ +const std = @import("std"); + +const BitStack = std.BitStack; + +const Scanner = @This(); + +stack: BitStack, +state: State = .version, +value_start: usize = undefined, +input: []const u8 = undefined, +cursor: usize = 0, + +/// Allocator used purely for the bitstack +pub fn init(gpa: std.mem.Allocator, input: []const u8) Scanner { + return .{ + .stack = .init(gpa), + .input = input, + }; +} + +pub fn deinit(self: *Scanner) void { + self.stack.deinit(); + self.* = undefined; +} + +pub fn next(self: *Scanner) !Token { + state: switch (self.state) { + .version => { + self.cursor += 1; + self.state = .marker; + return .{ .version = self.input[0] }; + }, + .marker => { + const m = std.enums.fromInt(MarkerType, self.input[self.cursor]) orelse return error.UnknownMarker; + switch (m) { + .null => { + self.cursor += 1; + self.state = .post_value; + return .null; + }, + .false => { + self.cursor += 1; + self.state = .post_value; + return .false; + }, + .true => { + self.cursor += 1; + self.state = .post_value; + return .true; + }, + .signed_int_8, + .signed_int_16, + .signed_int_32, + .signed_int64, + .signed_int_128, + .signed_int_256, + .signed_int_512, + => { + continue :state .int; + }, + .unsigned_int_8, + .unsigned_int_16, + .unsigned_int_32, + .unsigned_int64, + .unsigned_int_128, + .unsigned_int_256, + .unsigned_int_512, + => { + continue :state .uint; + }, + .half_float => { + self.cursor += 3; + self.state = .post_value; + return .{ .float = .{ .bits = 16, .view = self.input[self.cursor - 2 .. self.cursor] } }; + }, + // .minifloat, + .single_float => { + self.cursor += 5; + self.state = .post_value; + return .{ .float = .{ .bits = 32, .view = self.input[self.cursor - 4 .. self.cursor] } }; + }, + // .extended_float_40, + .double_float => { + self.cursor += 9; + self.state = .post_value; + return .{ .float = .{ .bits = 64, .view = self.input[self.cursor - 8 .. self.cursor] } }; + }, + .extended_float_80 => { + self.cursor += 11; + self.state = .post_value; + return .{ .float = .{ .bits = 80, .view = self.input[self.cursor - 10 .. self.cursor] } }; + }, + .quadruple_float => { + self.cursor += 17; + self.state = .post_value; + return .{ .float = .{ .bits = 128, .view = self.input[self.cursor - 16 .. self.cursor] } }; + }, + // .octuple_float, + // .brain_float, + else => return error.NotImplemented, + } + }, + .int => { + const marker = self.input[self.cursor]; + self.cursor += 1; + + const byte_length = try calculateIntegerByteLength(marker); + const value_start = self.cursor; + self.cursor += byte_length; + self.state = .post_value; + + return .{ .int = .{ + .signedness = .signed, + .view = self.input[value_start..self.cursor], + } }; + }, + .uint => { + const marker = self.input[self.cursor]; + self.cursor += 1; + + const byte_length = try calculateIntegerByteLength(marker); + const value_start = self.cursor; + self.cursor += byte_length; + self.state = .post_value; + + return .{ .int = .{ + .signedness = .unsigned, + .view = self.input[value_start..self.cursor], + } }; + }, + .post_value => { + if (try self.checkEnd()) return .eos; + return error.NeedToImplement; + }, + } +} + +fn calculateIntegerByteLength(marker: u8) !usize { + return switch (marker) { + 0x10...0x16 => std.math.pow(usize, 2, (marker - 0x10)), + 0x20...0x26 => std.math.pow(usize, 2, (marker - 0x20)), + else => error.InvalidMarker, + }; +} + +fn checkEnd(self: *Scanner) !bool { + if (self.cursor >= self.input.len) { + if (self.stack.bit_len == 0) return true; + return error.BufferUnderrun; + } + return false; +} + +pub const MarkerType = enum(u8) { + null = 0x00, + false = 0x01, + true = 0x02, + signed_int_8 = 0x10, + signed_int_16 = 0x11, + signed_int_32 = 0x12, + signed_int64 = 0x13, + signed_int_128 = 0x14, + signed_int_256 = 0x15, + signed_int_512 = 0x16, + arbitrary_signed_int = 0x1F, + unsigned_int_8 = 0x20, + unsigned_int_16 = 0x21, + unsigned_int_32 = 0x22, + unsigned_int64 = 0x23, + unsigned_int_128 = 0x24, + unsigned_int_256 = 0x25, + unsigned_int_512 = 0x26, + arbitrary_unsigned_int = 0x2F, + half_float = 0x30, + minifloat = 0x31, + single_float = 0x32, + extended_float_40 = 0x33, + double_float = 0x34, + extended_float_80 = 0x35, + quadruple_float = 0x36, + octuple_float = 0x37, + brain_float = 0x3F, + decimal_32 = 0x3A, + decimal_64 = 0x3B, + decimal_128 = 0x3C, + string = 0xE0, + vector = 0xE3, + optional = 0xF0, + @"enum" = 0xF1, + @"error" = 0xFF, + empty_array = 0x70, + array_1 = 0x71, + array_2 = 0x72, + array_3 = 0x73, + array_4 = 0x74, + array_5 = 0x75, + array_6 = 0x76, + array_7 = 0x77, + array_8 = 0x78, + array_9 = 0x79, + array_10 = 0x7A, + array_11 = 0x7B, + array_12 = 0x7C, + array_13 = 0x7D, + array_14 = 0x7E, + array_15 = 0x7F, + dyn_array_1 = 0xDA, + dyn_array_2 = 0xDB, + dyn_array_4 = 0xDC, + list_1 = 0x81, + list_2 = 0x82, + list_3 = 0x83, + list_4 = 0x84, + list_5 = 0x85, + list_6 = 0x86, + list_7 = 0x87, + list_8 = 0x88, + list_9 = 0x89, + list_10 = 0x8A, + list_11 = 0x8B, + list_12 = 0x8C, + list_13 = 0x8D, + list_14 = 0x8E, + list_15 = 0x8F, + dyn_list_1 = 0xDD, + dyn_list_2 = 0xDE, + dyn_list_4 = 0xDF, + + // TODO: Have optimized markers like lists + dyn_dict_1 = 0xD0, + dyn_dict_2 = 0xD1, + dyn_dict_4 = 0xD2, +}; + +pub const State = enum { + version, + marker, + post_value, + int, + uint, +}; + +pub const Token = union(enum) { + version: u8, + null, + false, + true, + int: struct { + signedness: std.builtin.Signedness, + view: []const u8, + }, + float: struct { + bits: u16, + view: []const u8, + }, + eos, +}; + +test { + _ = @import("./scanner_test.zig"); +} diff --git a/zig/src/Serializer.zig b/zig/src/Serializer.zig index e986eec..89f7d15 100644 --- a/zig/src/Serializer.zig +++ b/zig/src/Serializer.zig @@ -12,7 +12,7 @@ const native_endian = builtin.cpu.arch.endian(); const Serializer = @This(); -pub fn calculateMarker(comptime T: type) []const u8 { +fn calculateMarker(comptime T: type) []const u8 { comptime { const type_info = @typeInfo(T); @@ -32,6 +32,16 @@ pub fn calculateMarker(comptime T: type) []const u8 { else => |bits| &[_]u8{base + 0x0F} ++ &@as([2]u8, @bitCast(if (native_endian == .big) bits else @byteSwap(bits))), }; }, + .float => |f| { + return switch (f.bits) { + 16 => &.{0x30}, + 32 => &.{0x32}, + 64 => &.{0x34}, + 80 => &.{0x35}, + 128 => &.{0x36}, + else => unreachable, + }; + }, .array => |arr| { const is_string = arr.sentinel() != null; const base = 0x80; @@ -47,7 +57,7 @@ pub fn calculateMarker(comptime T: type) []const u8 { } } -pub fn calculateBufferLen(comptime T: type) comptime_int { +fn calculateBufferLen(comptime T: type) comptime_int { comptime { const type_info = @typeInfo(T); @@ -55,6 +65,9 @@ pub fn calculateBufferLen(comptime T: type) comptime_int { .int => |i| { return @divExact(i.bits, 8); }, + .float => |f| { + return @divExact(f.bits, 8); + }, .array => |arr| { return calculateBufferLen(arr.child) * arr.len; }, @@ -63,13 +76,14 @@ pub fn calculateBufferLen(comptime T: type) comptime_int { } } -pub fn Buffer(comptime T: type) type { +fn Buffer(comptime T: type) type { comptime { const type_info = @typeInfo(T); return switch (type_info) { .null, .bool => [2]u8, .int => [1 + calculateMarker(T).len + calculateBufferLen(ByteAlignedInt(T))]u8, + .float => [2 + calculateBufferLen(T)]u8, .array => [1 + calculateMarker(T).len + calculateBufferLen(T)]u8, else => unreachable, }; @@ -88,7 +102,7 @@ test serializeNull { } pub fn serializeBool(value: bool) Buffer(bool) { - var buffer: Buffer(@TypeOf(null)) = undefined; + var buffer: Buffer(bool) = undefined; buffer[0] = HBP_VERSION; buffer[1] = if (value) 0x02 else 0x01; return buffer; @@ -99,6 +113,20 @@ test serializeBool { try expect(eql(u8, &serializeBool(true), &.{ 0x01, 0x02 })); } +pub fn serializeFloat(comptime T: type, value: T) Buffer(T) { + const marker = comptime calculateMarker(T); + var buffer: Buffer(T) = undefined; + buffer[0] = HBP_VERSION; + + inline for (marker, 1..) |byte, i| { + buffer[i] = byte; + } + + buffer[1 + marker.len ..].* = @bitCast(value); + + return buffer; +} + pub fn serializeInt(comptime T: type, value: T) Buffer(T) { const aligned_type = ByteAlignedInt(T); const marker = comptime calculateMarker(T); @@ -110,11 +138,15 @@ pub fn serializeInt(comptime T: type, value: T) Buffer(T) { buffer[i] = byte; } - buffer[1 + marker.len ..].* = @bitCast(std.mem.nativeToBig(aligned_type, value)); + serializeIntInternal(aligned_type, buffer[1 + marker.len ..], value); return buffer; } +fn serializeIntInternal(comptime T: type, buf: *[calculateBufferLen(T)]u8, value: T) void { + buf.* = @bitCast(std.mem.nativeToBig(T, value)); +} + test serializeInt { try expect(eql(u8, &serializeInt(i8, 45), &.{ 0x01, 0x10, 0x2D })); try expect(eql(u8, &serializeInt(i16, 6347), &.{ 0x01, 0x11, 0x18, 0xCB })); @@ -139,7 +171,7 @@ test serializeInt { try expect(eql(u8, &serializeInt(u80, 5294967295), &.{ 0x01, 0x2F, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); } -pub fn serializeList(comptime T: type, comptime max_len: u32, value: []const T) struct { Buffer([max_len]T), u32 } { +pub fn serializeList(comptime T: type, comptime max_len: u32, value: []T) struct { Buffer([max_len]T), u32 } { assert(value.len <= max_len); const type_info = @typeInfo(T); @@ -159,14 +191,13 @@ pub fn serializeList(comptime T: type, comptime max_len: u32, value: []const T) var real_length: u32 = 1 + marker.len; for (value) |el| { - @memcpy(buffer[real_length .. real_length + byte_size], &@as([byte_size]u8, @bitCast(std.mem.nativeToBig(T, el)))); + serializeIntInternal(T, buffer[real_length .. real_length + byte_size], el); real_length += byte_size; } return .{ buffer, real_length }; } -// TODO: Merge this properly with `serializeList` pub fn serializeString(comptime max_len: u32, value: []const u8) struct { Buffer([max_len:0]u8), u32 } { assert(value.len <= max_len); var buffer: Buffer([max_len:0]u8) = undefined; diff --git a/zig/src/main.zig b/zig/src/main.zig index f07a19b..957a336 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -1,14 +1,77 @@ const std = @import("std"); const Serializer = @import("./Serializer.zig"); -const Deserializer = @import("./Deserializer.zig"); +const Deserializer = @import("./static.zig"); +const Scanner = @import("./Scanner.zig"); + +const test_bool = [_][]const u8{ + &.{ 0x01, 0x01 }, + &.{ 0x01, 0x02 }, +}; + +const test_int = [_][]const u8{ + &Serializer.serializeInt(i8, 45), + &Serializer.serializeInt(i16, 6347), + &Serializer.serializeInt(i32, 9123424), + &Serializer.serializeInt(i64, 5294967295), + &Serializer.serializeInt(i128, 28446744073709551615), + &Serializer.serializeInt(i256, 340282366920938463463375607431768211456), + &Serializer.serializeInt(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), +}; +const test_uint = [_][]const u8{ + &Serializer.serializeInt(u8, 250), + &Serializer.serializeInt(u16, 6347), + &Serializer.serializeInt(u32, 9123424), + &Serializer.serializeInt(u64, 5294967295), + &Serializer.serializeInt(u128, 28446744073709551615), + &Serializer.serializeInt(u256, 340282366920938463463375607431768211456), + &Serializer.serializeInt(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), +}; + +const test_float = [_][]const u8{ + &Serializer.serializeFloat(f16, 20.11), + &Serializer.serializeFloat(f32, 202.5456), + &Serializer.serializeFloat(f64, 220.563564), + &Serializer.serializeFloat(f80, 25460.565667657), + &Serializer.serializeFloat(f128, 2024.5689076899345), +}; pub fn main() !void { - const serialized, const length = Serializer.serializeString(15, "Hello World"); - const deserialized, const len = try Deserializer.deserializeStringAssumeLength(15, serialized[0..length]); + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + const allocator = gpa.allocator(); + + var buf: [512]u8 = undefined; + for (test_bool) |in| { + std.debug.print("-------------------------------------------------\n", .{}); + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(bool, allocator, in, .{})}); + buf = undefined; + } + for (test_int) |in| { + std.debug.print("-------------------------------------------------\n", .{}); + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(i512, allocator, in, .{})}); + buf = undefined; + } + for (test_uint) |in| { + std.debug.print("-------------------------------------------------\n", .{}); + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(u512, allocator, in, .{})}); + buf = undefined; + } + for (test_float) |in| { + std.debug.print("-------------------------------------------------\n", .{}); + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(f128, allocator, in, .{ .assert_floats = false })}); + buf = undefined; + } + + // var s: Scanner = .init(allocator, &serialized); + // while (true) { + // const token = try s.next(); + // if (token == .eos) break; - var buf2: [64]u8 = undefined; - std.debug.print("HBP payload: {s} ({s})\n", .{ readableOutput(&buf2, serialized[0..length]), deserialized[0..len] }); - // std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf2, serialized[0..length])}); + // std.debug.print("Scanner: {any}\n", .{token}); + // } } fn readableOutput(buffer: []u8, input: []const u8) []const u8 { @@ -34,5 +97,5 @@ fn readableOutput(buffer: []u8, input: []const u8) []const u8 { } test { - std.testing.refAllDecls(@This()); + std.testing.refAllDeclsRecursive(@This()); } diff --git a/zig/src/root.zig b/zig/src/root.zig new file mode 100644 index 0000000..b6ec846 --- /dev/null +++ b/zig/src/root.zig @@ -0,0 +1,8 @@ +const static = @import("./static.zig"); + +pub const ParseOptions = static.ParseOptions; +pub const parseFromSlice = static.parseFromSlice; +pub const parseFromTokenSource = static.parseFromTokenSource; +pub const innerParse = static.innerParse; + +pub const Serializer = @import("./Serializer.zig"); diff --git a/zig/src/scanner_test.zig b/zig/src/scanner_test.zig new file mode 100644 index 0000000..f7f4336 --- /dev/null +++ b/zig/src/scanner_test.zig @@ -0,0 +1,98 @@ +const std = @import("std"); +const Scanner = @import("Scanner.zig"); +const Serializer = @import("Serializer.zig"); +const Token = Scanner.Token; + +const expect = std.testing.expect; +const eql = std.mem.eql; + +test "null" { + var scanner: Scanner = .init(std.testing.allocator, &.{ 0x01, 0x00 }); + defer scanner.deinit(); + + try expectNext(&scanner, .{ .version = 1 }); + try expectNext(&scanner, .null); + try expectNext(&scanner, .eos); +} + +test "bool" { + var scanner: Scanner = .init(std.testing.allocator, &.{ 0x01, 0x01 }); + + try expectNext(&scanner, .{ .version = 1 }); + try expectNext(&scanner, .false); + try expectNext(&scanner, .eos); + + scanner.deinit(); + scanner = .init(std.testing.allocator, &.{ 0x01, 0x02 }); + defer scanner.deinit(); + + try expectNext(&scanner, .{ .version = 1 }); + try expectNext(&scanner, .true); + try expectNext(&scanner, .eos); +} + +test "int" { + const test_input = [_][]const u8{ + &Serializer.serializeInt(i8, 45), + &Serializer.serializeInt(i16, 6347), + &Serializer.serializeInt(i32, 9123424), + &Serializer.serializeInt(i64, 5294967295), + &Serializer.serializeInt(i128, 28446744073709551615), + &Serializer.serializeInt(i256, 340282366920938463463375607431768211456), + &Serializer.serializeInt(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), + &Serializer.serializeInt(u8, 250), + &Serializer.serializeInt(u16, 6347), + &Serializer.serializeInt(u32, 9123424), + &Serializer.serializeInt(u64, 5294967295), + &Serializer.serializeInt(u128, 28446744073709551615), + &Serializer.serializeInt(u256, 340282366920938463463375607431768211456), + &Serializer.serializeInt(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), + }; + const test_output = [_]Token{ + Token{ .int = .{ .signedness = .signed, .view = &.{0x2D} } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0x18, 0xCB } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x8B, 0x36, 0x60 } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8A, 0xC7, 0x23, 0x04, 0x89, 0xE7, 0xFF, 0xFF } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } } }, + + Token{ .int = .{ .signedness = .unsigned, .view = &.{0xFA} } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x18, 0xCB } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x8B, 0x36, 0x60 } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8A, 0xC7, 0x23, 0x04, 0x89, 0xE7, 0xFF, 0xFF } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } } }, + }; + + for (test_input, 0..) |in, i| { + var scanner: Scanner = .init(std.testing.allocator, in); + defer scanner.deinit(); + + const out = test_output[i]; + try expectNext(&scanner, .{ .version = 1 }); + try expectNext(&scanner, out); + try expectNext(&scanner, .eos); + } +} + +fn expectNext(scanner: *Scanner, expected_token: Scanner.Token) !void { + const token = try scanner.next(); + try std.testing.expectEqual(std.meta.activeTag(expected_token), std.meta.activeTag(token)); + + switch (expected_token) { + .int => |expected_value| { + try expect(eql(u8, expected_value.view, token.int.view)); + }, + .float => |expected_value| { + try expect(eql(u8, expected_value.view, token.int.view)); + }, + .version, + .null, + .false, + .true, + .eos, + => {}, + } +} diff --git a/zig/src/static.zig b/zig/src/static.zig new file mode 100644 index 0000000..a0edb3f --- /dev/null +++ b/zig/src/static.zig @@ -0,0 +1,80 @@ +const std = @import("std"); +const Scanner = @import("Scanner.zig"); + +const assert = std.debug.assert; + +pub const ParseOptions = struct { + /// When set to `true` it will only allow parsing floats of the same size + /// Setting to `false` allows float widening to happen, this can cause imprecisions + float_behavior: enum(u1) { + widen, + preserve, + } = .preserve, +}; + +pub fn parseFromSlice(comptime T: type, gpa: std.mem.Allocator, slice: []const u8, comptime options: ParseOptions) !T { + var scanner: Scanner = .init(gpa, slice); + defer scanner.deinit(); + + return parseFromTokenSource(T, gpa, &scanner, options); +} + +pub fn parseFromTokenSource(comptime T: type, gpa: std.mem.Allocator, scanner: *Scanner, comptime options: ParseOptions) !T { + assert(try scanner.next() == .version); + const value = try innerParse(T, gpa, scanner, options); + assert(try scanner.next() == .eos); + return value; +} + +pub fn innerParse(comptime T: type, gpa: std.mem.Allocator, scanner: *Scanner, comptime options: ParseOptions) !T { + _ = gpa; + switch (@typeInfo(T)) { + .bool => { + return switch (try scanner.next()) { + .false => false, + .true => true, + else => error.UnexpectedToken, + }; + }, + .int => |int| { + const token = try scanner.next(); + if (token != Scanner.Token.int) return error.UnexpectedToken; + if (token.int.signedness != int.signedness) return error.WrongIntegerType; + + return sliceToInt(T, token.int.view); + }, + .float => |float| { + const token = try scanner.next(); + if (token != Scanner.Token.float) return error.UnexpectedToken; + if (comptime options.float_behavior != .widen) { + if (float.bits != token.float.bits) return error.CannotWidenFloat; + } + assert(float.bits >= token.float.bits); + + return @as(T, switch (token.float.bits) { + 16 => std.mem.bytesToValue(f16, token.float.view), + 32 => std.mem.bytesToValue(f32, token.float.view), + 64 => std.mem.bytesToValue(f64, token.float.view), + 80 => std.mem.bytesToValue(f80, token.float.view), + 128 => std.mem.bytesToValue(f128, token.float.view), + else => unreachable, + }); + }, + .comptime_int, .comptime_float => error.IncompatibleTypes, + else => return error.TODO, + } +} + +fn sliceToInt(comptime T: type, slice: []const u8) T { + const alignedType = std.math.ByteAlignedInt(T); + const byte_length = @divExact(@typeInfo(alignedType).int.bits, 8); + assert(slice.len <= byte_length); + + if (slice.len < byte_length) { + var buf = std.mem.zeroes([byte_length]u8); + @memcpy(buf[byte_length - slice.len ..], slice); + return std.mem.readInt(alignedType, &buf, .big); + } + + return std.mem.readInt(alignedType, slice[0..byte_length], .big); +} From 7ba7252efce57c00088b65760a19357b6f3d7369 Mon Sep 17 00:00:00 2001 From: DidaS Date: Wed, 12 Nov 2025 03:12:01 +0000 Subject: [PATCH 03/30] Support arbitrary sized integers --- zig/src/Scanner.zig | 34 ++++++++++++++++++++++++++++++++++ zig/src/Serializer.zig | 2 +- zig/src/main.zig | 8 +++++++- zig/src/static.zig | 8 +++++--- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index 1e9a6f2..d8650be 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -58,6 +58,10 @@ pub fn next(self: *Scanner) !Token { => { continue :state .int; }, + .arbitrary_signed_int => { + self.cursor += 1; + continue :state .arbitrary_int; + }, .unsigned_int_8, .unsigned_int_16, .unsigned_int_32, @@ -68,6 +72,10 @@ pub fn next(self: *Scanner) !Token { => { continue :state .uint; }, + .arbitrary_unsigned_int => { + self.cursor += 1; + continue :state .arbitrary_uint; + }, .half_float => { self.cursor += 3; self.state = .post_value; @@ -114,6 +122,18 @@ pub fn next(self: *Scanner) !Token { .view = self.input[value_start..self.cursor], } }; }, + .arbitrary_int => { + const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + self.cursor += 2; + const value_start = self.cursor; + self.cursor += byte_length; + self.state = .post_value; + + return .{ .int = .{ + .signedness = .signed, + .view = self.input[value_start..self.cursor], + } }; + }, .uint => { const marker = self.input[self.cursor]; self.cursor += 1; @@ -128,6 +148,18 @@ pub fn next(self: *Scanner) !Token { .view = self.input[value_start..self.cursor], } }; }, + .arbitrary_uint => { + const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + self.cursor += 2; + const value_start = self.cursor; + self.cursor += byte_length; + self.state = .post_value; + + return .{ .int = .{ + .signedness = .unsigned, + .view = self.input[value_start..self.cursor], + } }; + }, .post_value => { if (try self.checkEnd()) return .eos; return error.NeedToImplement; @@ -237,7 +269,9 @@ pub const State = enum { marker, post_value, int, + arbitrary_int, uint, + arbitrary_uint, }; pub const Token = union(enum) { diff --git a/zig/src/Serializer.zig b/zig/src/Serializer.zig index 89f7d15..53bef27 100644 --- a/zig/src/Serializer.zig +++ b/zig/src/Serializer.zig @@ -29,7 +29,7 @@ fn calculateMarker(comptime T: type) []const u8 { 128 => &.{base + 4}, 256 => &.{base + 5}, 512 => &.{base + 6}, - else => |bits| &[_]u8{base + 0x0F} ++ &@as([2]u8, @bitCast(if (native_endian == .big) bits else @byteSwap(bits))), + else => |bits| &[_]u8{base + 0x0F} ++ &@as([2]u8, @bitCast(std.mem.nativeToBig(u16, @divExact(bits, 8)))), }; }, .float => |f| { diff --git a/zig/src/main.zig b/zig/src/main.zig index 957a336..ecda0a1 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -16,6 +16,9 @@ const test_int = [_][]const u8{ &Serializer.serializeInt(i128, 28446744073709551615), &Serializer.serializeInt(i256, 340282366920938463463375607431768211456), &Serializer.serializeInt(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), + &Serializer.serializeInt(i6, 30), + &Serializer.serializeInt(i38, 9123424), + &Serializer.serializeInt(i80, 5294967295), }; const test_uint = [_][]const u8{ &Serializer.serializeInt(u8, 250), @@ -25,6 +28,9 @@ const test_uint = [_][]const u8{ &Serializer.serializeInt(u128, 28446744073709551615), &Serializer.serializeInt(u256, 340282366920938463463375607431768211456), &Serializer.serializeInt(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), + &Serializer.serializeInt(u6, 30), + &Serializer.serializeInt(u38, 9123424), + &Serializer.serializeInt(u80, 5294967295), }; const test_float = [_][]const u8{ @@ -61,7 +67,7 @@ pub fn main() !void { for (test_float) |in| { std.debug.print("-------------------------------------------------\n", .{}); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(f128, allocator, in, .{ .assert_floats = false })}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(f128, allocator, in, .{ .float_behavior = .widen })}); buf = undefined; } diff --git a/zig/src/static.zig b/zig/src/static.zig index a0edb3f..0f4f925 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -4,8 +4,8 @@ const Scanner = @import("Scanner.zig"); const assert = std.debug.assert; pub const ParseOptions = struct { - /// When set to `true` it will only allow parsing floats of the same size - /// Setting to `false` allows float widening to happen, this can cause imprecisions + /// Allow parsing `i8` as `u8` and vice-versa + ignore_integer_signedness: bool = false, float_behavior: enum(u1) { widen, preserve, @@ -39,7 +39,9 @@ pub fn innerParse(comptime T: type, gpa: std.mem.Allocator, scanner: *Scanner, c .int => |int| { const token = try scanner.next(); if (token != Scanner.Token.int) return error.UnexpectedToken; - if (token.int.signedness != int.signedness) return error.WrongIntegerType; + if (comptime !options.ignore_integer_signedness) { + if (token.int.signedness != int.signedness) return error.WrongIntegerType; + } return sliceToInt(T, token.int.view); }, From 716b43f9bc6d285ae0803949c12e15676ff81d15 Mon Sep 17 00:00:00 2001 From: DidaS Date: Wed, 12 Nov 2025 03:18:56 +0000 Subject: [PATCH 04/30] Reduce duplication --- zig/src/Scanner.zig | 55 +++++++++++++++--------------------------- zig/src/Serializer.zig | 8 +++--- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index d8650be..dabb194 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -56,10 +56,12 @@ pub fn next(self: *Scanner) !Token { .signed_int_256, .signed_int_512, => { + self.state = .int; continue :state .int; }, .arbitrary_signed_int => { self.cursor += 1; + self.state = .arbitrary_int; continue :state .arbitrary_int; }, .unsigned_int_8, @@ -70,10 +72,12 @@ pub fn next(self: *Scanner) !Token { .unsigned_int_256, .unsigned_int_512, => { + self.state = .uint; continue :state .uint; }, .arbitrary_unsigned_int => { self.cursor += 1; + self.state = .arbitrary_uint; continue :state .arbitrary_uint; }, .half_float => { @@ -108,58 +112,39 @@ pub fn next(self: *Scanner) !Token { else => return error.NotImplemented, } }, - .int => { + .int, .uint => { const marker = self.input[self.cursor]; self.cursor += 1; const byte_length = try calculateIntegerByteLength(marker); const value_start = self.cursor; self.cursor += byte_length; + const state = self.state; self.state = .post_value; - return .{ .int = .{ - .signedness = .signed, - .view = self.input[value_start..self.cursor], - } }; + return .{ + .int = .{ + .signedness = if (state == .int) .signed else .unsigned, + .view = self.input[value_start..self.cursor], + }, + }; }, - .arbitrary_int => { + .arbitrary_int, .arbitrary_uint => { const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); self.cursor += 2; const value_start = self.cursor; self.cursor += byte_length; + const state = self.state; self.state = .post_value; - return .{ .int = .{ - .signedness = .signed, - .view = self.input[value_start..self.cursor], - } }; - }, - .uint => { - const marker = self.input[self.cursor]; - self.cursor += 1; - - const byte_length = try calculateIntegerByteLength(marker); - const value_start = self.cursor; - self.cursor += byte_length; - self.state = .post_value; - - return .{ .int = .{ - .signedness = .unsigned, - .view = self.input[value_start..self.cursor], - } }; + return .{ + .int = .{ + .signedness = if (state == .arbitrary_int) .signed else .unsigned, + .view = self.input[value_start..self.cursor], + }, + }; }, - .arbitrary_uint => { - const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); - self.cursor += 2; - const value_start = self.cursor; - self.cursor += byte_length; - self.state = .post_value; - return .{ .int = .{ - .signedness = .unsigned, - .view = self.input[value_start..self.cursor], - } }; - }, .post_value => { if (try self.checkEnd()) return .eos; return error.NeedToImplement; diff --git a/zig/src/Serializer.zig b/zig/src/Serializer.zig index 53bef27..5a1cb45 100644 --- a/zig/src/Serializer.zig +++ b/zig/src/Serializer.zig @@ -156,8 +156,8 @@ test serializeInt { try expect(eql(u8, &serializeInt(i256, 340282366920938463463375607431768211456), &.{ 0x01, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 })); try expect(eql(u8, &serializeInt(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), &.{ 0x01, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })); try expect(eql(u8, &serializeInt(i6, 30), &.{ 0x01, 0x10, 0x1E })); - try expect(eql(u8, &serializeInt(i38, 9123424), &.{ 0x01, 0x1F, 0x00, 0x28, 0x00, 0x00, 0x8B, 0x36, 0x60 })); - try expect(eql(u8, &serializeInt(i80, 5294967295), &.{ 0x01, 0x1F, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); + try expect(eql(u8, &serializeInt(i38, 9123424), &.{ 0x01, 0x1F, 0x00, 0x05, 0x00, 0x00, 0x8B, 0x36, 0x60 })); + try expect(eql(u8, &serializeInt(i80, 5294967295), &.{ 0x01, 0x1F, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); try expect(eql(u8, &serializeInt(u8, 250), &.{ 0x01, 0x20, 0xFA })); try expect(eql(u8, &serializeInt(u16, 6347), &.{ 0x01, 0x21, 0x18, 0xCB })); @@ -167,8 +167,8 @@ test serializeInt { try expect(eql(u8, &serializeInt(u256, 340282366920938463463375607431768211456), &.{ 0x01, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 })); try expect(eql(u8, &serializeInt(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), &.{ 0x01, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })); try expect(eql(u8, &serializeInt(u6, 30), &.{ 0x01, 0x20, 0x1E })); - try expect(eql(u8, &serializeInt(u38, 9123424), &.{ 0x01, 0x2F, 0x00, 0x28, 0x00, 0x00, 0x8B, 0x36, 0x60 })); - try expect(eql(u8, &serializeInt(u80, 5294967295), &.{ 0x01, 0x2F, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); + try expect(eql(u8, &serializeInt(u38, 9123424), &.{ 0x01, 0x2F, 0x00, 0x05, 0x00, 0x00, 0x8B, 0x36, 0x60 })); + try expect(eql(u8, &serializeInt(u80, 5294967295), &.{ 0x01, 0x2F, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); } pub fn serializeList(comptime T: type, comptime max_len: u32, value: []T) struct { Buffer([max_len]T), u32 } { From a5ef6a850b1f9ae98d312ee18c183af47abf9b58 Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 14 Nov 2025 03:12:14 +0000 Subject: [PATCH 05/30] Spec update --- README.md | 121 ++++++++++++++++++++++-------------------------------- 1 file changed, 49 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 77d5f44..61e0a86 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ Harmony Binary Protocol (HBP) is a general purpose serialization protocol inspired by the protocols like Bolt's PackStream and Redis's RESP3, that aims to provide a standardized type aware way to serialize and deserialize data. -It consists of a set of basic types, composite types, and meta types that together allow you to represent data in the way you need. +It consists of a set of primitive types, composite types, and meta types that together allow you to represent data in the way you need. # Table of Contents - [About](#about) - [Table of Contents](#table-of-contents) - [Representation](#representation) -- [Basic Data Types](#basic-data-types) +- [Primitive Data Types](#primitive-data-types) - [Null](#null) - [Bool](#bool) - [Numbers](#numbers) @@ -39,9 +39,9 @@ Every serialized HBP value begins with the HBP version used to encode it followe ![representation image](representation.png) -# Basic Data Types +# Primitive Data Types -Basic types (or primitives) are the fundamental blocks used to represent the encoded data. +Primitive types (or primitives) are the fundamental blocks used to represent the encoded data. ## Null @@ -127,27 +127,6 @@ Meta data types are special types that acts as metadata for other types and they HBP reserves all the `E0-FF` range for meta types. -## String - -Marker: `E0` - -A String is a character encoded list of bytes. - -The string marker is always followed by a list marker with the type byte set to one of the following: - -| Byte | Encoding | -| ---- | -------- | -| `20` | UTF-8 | -| `21` | UTF-16 | - -The sender ensures the encoding of the strings is valid therefor, validation on the client is unnecessary. - -## Vector - -Marker: `E3` - -The vector marker must **always** be followed by a list marker to indicate the size and type of the vector, the data should follow the same encoding as the indicated type. - ## Optional Marker: `F0` @@ -174,11 +153,11 @@ Serialized: 01 FF 6B 54 68 69 73 20 46 61 69 6C 65 64 # Composite Data Types -## Array +## Tuple -Small arrays: +Small tuples: -| Marker | Array size | +| Marker | Tuple size | | :----: | :--------: | | `70` | 0 | | `71` | 1 | @@ -197,7 +176,7 @@ Small arrays: | `7E` | 14 | | `7F` | 15 | -Long arrays: +Long tuples: | Marker | Extra bytes | Maximum Size | | :----: | :---------: | :-----------: | @@ -205,7 +184,7 @@ Long arrays: | `DB` | 2 | 65_535 | | `DC` | 4 | 4_294_967_295 | -An array is a list of values, each one serializing their own type alongside like a basic hbp payload. If its a long array, the length will come **after** the value type. +A tuple is a list of values, each one serializing their own type alongside like a basic hbp payload. If its a long array, the length will come **after** the value type. ```txt Original: [3, 6, 9] @@ -214,30 +193,30 @@ Serialized: 01 73 10 03 10 06 10 09 ``` -## List - -Small lists: - -| Marker | List Size | -| :----: | :-------: | -| `80` | 0 | -| `81` | 1 | -| `82` | 2 | -| `83` | 3 | -| `84` | 4 | -| `85` | 5 | -| `86` | 6 | -| `87` | 7 | -| `88` | 8 | -| `89` | 9 | -| `8A` | 10 | -| `8B` | 11 | -| `8C` | 12 | -| `8D` | 13 | -| `8E` | 14 | -| `8F` | 15 | +## Vector -Long lists: +Small vectors: + +| Marker | Vector Size | +| :----: | :---------: | +| `80` | 0 | +| `81` | 1 | +| `82` | 2 | +| `83` | 3 | +| `84` | 4 | +| `85` | 5 | +| `86` | 6 | +| `87` | 7 | +| `88` | 8 | +| `89` | 9 | +| `8A` | 10 | +| `8B` | 11 | +| `8C` | 12 | +| `8D` | 13 | +| `8E` | 14 | +| `8F` | 15 | + +Long vectors: | Marker | Extra bytes | Maximum Size | | :----: | :---------: | :-----------: | @@ -245,12 +224,12 @@ Long lists: | `DE` | 2 | 65_535 | | `DF` | 4 | 4_294_967_295 | -A list as the name indicates is a list of values where all the values have the same type which has to be indicated right after the list marker. If its a long list, the length will come **after** the value type. +A vector is a known-type list of items. The vector marker is followed by [primitive data type](#primitive-data-types) and all elements will follow the encoding of that type. ```txt -Original: List([3, 6, 9]) +Original: Vector(u8, [3, 6, 9]) -Serialized: 01 83 10 03 06 09 +Serialized: 01 83 20 03 06 09 ``` ## Dictionary @@ -286,24 +265,22 @@ A map is just like a dictionary but instead, the keys can be of any type. | Marker | Name | Type | | :-----: | :--------------------------------------: | :--------------------------------: | -| `00` | [`null`](#null) | [Primitive](#basic-data-types) | -| `01` | [`false`](#bool) | [Primitive](#basic-data-types) | -| `02` | [`true`](#bool) | [Primitive](#basic-data-types) | -| `10-16` | [`signed integer`](#signed-integers) | [Primitive](#basic-data-types) | -| `1F` | [`signed integer`](#signed-integers) | [Primitive](#basic-data-types) | -| `20-26` | [`unsigned integer`](#unsigned-integers) | [Primitive](#basic-data-types) | -| `2F` | [`unsigned integer`](#unsigned-integers) | [Primitive](#basic-data-types) | -| `30-37` | [`float`](#floats) | [Primitive](#basic-data-types) | -| `3A-3C` | [`decimal`](#floats) | [Primitive](#basic-data-types) | -| `3F` | [`bfloat16`](#floats) | [Primitive](#basic-data-types) | -| `70-7F` | [`array`](#array) | [Composite](#composite-data-types) | -| `80-8F` | [`list`](#list) | [Composite](#composite-data-types) | +| `00` | [`null`](#null) | [Primitive](#primitive-data-types) | +| `01` | [`false`](#bool) | [Primitive](#primitive-data-types) | +| `02` | [`true`](#bool) | [Primitive](#primitive-data-types) | +| `10-16` | [`signed integer`](#signed-integers) | [Primitive](#primitive-data-types) | +| `1F` | [`signed integer`](#signed-integers) | [Primitive](#primitive-data-types) | +| `20-26` | [`unsigned integer`](#unsigned-integers) | [Primitive](#primitive-data-types) | +| `2F` | [`unsigned integer`](#unsigned-integers) | [Primitive](#primitive-data-types) | +| `30-37` | [`float`](#floats) | [Primitive](#primitive-data-types) | +| `3A-3C` | [`decimal`](#floats) | [Primitive](#primitive-data-types) | +| `3F` | [`bfloat16`](#floats) | [Primitive](#primitive-data-types) | +| `70-7F` | [`tuple`](#tuple) | [Composite](#composite-data-types) | +| `80-8F` | [`vector`](#vector) | [Composite](#composite-data-types) | | `D0-D2` | [`dictionary`](#dictionary) | [Composite](#composite-data-types) | | `D3-D5` | [`map`](#map) | [Composite](#composite-data-types) | -| `DA-DC` | [`array`](#array) | [Composite](#composite-data-types) | -| `DD-DF` | [`list`](#list) | [Composite](#composite-data-types) | -| `E0` | [`string`](#string) | [Meta](#meta-data-types) | -| `E3` | [`vector`](#vector) | [Meta](#meta-data-types) | +| `DA-DC` | [`tuple`](#tuple) | [Composite](#composite-data-types) | +| `DD-DF` | [`vector`](#vector) | [Composite](#composite-data-types) | | `F0` | [`optional`](#optional) | [Meta](#meta-data-types) | | `F1` | [`enum`](#enum) | [Meta](#meta-data-types) | | `FF` | [`error`](#error) | [Meta](#meta-data-types) | \ No newline at end of file From 8bc4e5206f092fc4c0ddc5d612c0e0e7be2daad8 Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 14 Nov 2025 04:01:13 +0000 Subject: [PATCH 06/30] Update zls --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 453bb17..58329e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,5 @@ "Minifloat", "signedness" ], - "zig.zls.enableBuildOnSave": true + "zig.buildOnSaveProvider": "zls" } \ No newline at end of file From 0e9c1bead4b60eebf391981cad6f4798cb26c9c1 Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 14 Nov 2025 04:01:27 +0000 Subject: [PATCH 07/30] Add support for tuples --- zig/src/Scanner.zig | 159 ++++++++++++++++++++++++++++---------------- zig/src/main.zig | 17 +++-- zig/src/static.zig | 32 ++++++--- 3 files changed, 134 insertions(+), 74 deletions(-) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index dabb194..4c804d4 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -1,25 +1,20 @@ const std = @import("std"); -const BitStack = std.BitStack; - const Scanner = @This(); -stack: BitStack, state: State = .version, value_start: usize = undefined, input: []const u8 = undefined, cursor: usize = 0, /// Allocator used purely for the bitstack -pub fn init(gpa: std.mem.Allocator, input: []const u8) Scanner { +pub fn init(input: []const u8) Scanner { return .{ - .stack = .init(gpa), .input = input, }; } pub fn deinit(self: *Scanner) void { - self.stack.deinit(); self.* = undefined; } @@ -109,13 +104,52 @@ pub fn next(self: *Scanner) !Token { }, // .octuple_float, // .brain_float, + .empty_tuple, + .tuple_1, + .tuple_2, + .tuple_3, + .tuple_4, + .tuple_5, + .tuple_6, + .tuple_7, + .tuple_8, + .tuple_9, + .tuple_10, + .tuple_11, + .tuple_12, + .tuple_13, + .tuple_14, + .tuple_15, + => { + const byte_length = self.input[self.cursor] - 0x70; + self.cursor += 1; + return self.parseTupleState(byte_length); + }, + .arbitrary_tuple_1 => { + self.cursor += 1; + const byte_length = std.mem.nativeToBig(u8, self.input[self.cursor]); + self.cursor += 1; + return self.parseTupleState(byte_length); + }, + .arbitrary_tuple_2 => { + self.cursor += 1; + const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + self.cursor += 2; + return self.parseTupleState(byte_length); + }, + .arbitrary_tuple_4 => { + self.cursor += 1; + const byte_length = std.mem.nativeToBig(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); + self.cursor += 4; + return self.parseTupleState(byte_length); + }, + else => return error.NotImplemented, } }, .int, .uint => { const marker = self.input[self.cursor]; self.cursor += 1; - const byte_length = try calculateIntegerByteLength(marker); const value_start = self.cursor; self.cursor += byte_length; @@ -144,10 +178,12 @@ pub fn next(self: *Scanner) !Token { }, }; }, - .post_value => { if (try self.checkEnd()) return .eos; - return error.NeedToImplement; + // NOTE: If no changes are necessary here after all types have been implemented + // remove this and add a simple check to the beginning of `marker` instead. + self.state = .marker; + continue :state .marker; }, } } @@ -161,11 +197,17 @@ fn calculateIntegerByteLength(marker: u8) !usize { } fn checkEnd(self: *Scanner) !bool { - if (self.cursor >= self.input.len) { - if (self.stack.bit_len == 0) return true; - return error.BufferUnderrun; - } - return false; + return self.cursor >= self.input.len; + // if (self.cursor >= self.input.len) { + // if (self.stack.bit_len == 0) return true; + // return error.BufferUnderrun; + // } + // return false; +} + +fn parseTupleState(self: *Scanner, byte_length: u32) Token { + self.state = if (byte_length == 0) .post_value else .marker; + return .{ .tuple = byte_length }; } pub const MarkerType = enum(u8) { @@ -200,53 +242,52 @@ pub const MarkerType = enum(u8) { decimal_32 = 0x3A, decimal_64 = 0x3B, decimal_128 = 0x3C, - string = 0xE0, - vector = 0xE3, + empty_tuple = 0x70, + tuple_1 = 0x71, + tuple_2 = 0x72, + tuple_3 = 0x73, + tuple_4 = 0x74, + tuple_5 = 0x75, + tuple_6 = 0x76, + tuple_7 = 0x77, + tuple_8 = 0x78, + tuple_9 = 0x79, + tuple_10 = 0x7A, + tuple_11 = 0x7B, + tuple_12 = 0x7C, + tuple_13 = 0x7D, + tuple_14 = 0x7E, + tuple_15 = 0x7F, + arbitrary_tuple_1 = 0xDA, + arbitrary_tuple_2 = 0xDB, + arbitrary_tuple_4 = 0xDC, + empty_vector = 0x80, + vector_1 = 0x81, + vector_2 = 0x82, + vector_3 = 0x83, + vector_4 = 0x84, + vector_5 = 0x85, + vector_6 = 0x86, + vector_7 = 0x87, + vector_8 = 0x88, + vector_9 = 0x89, + vector_10 = 0x8A, + vector_11 = 0x8B, + vector_12 = 0x8C, + vector_13 = 0x8D, + vector_14 = 0x8E, + vector_15 = 0x8F, + arbitrary_vector_1 = 0xDD, + arbitrary_vector_2 = 0xDE, + arbitrary_vector_4 = 0xDF, + + // TODO: Have optimized markers like lists + arbitrary_dict_1 = 0xD0, + arbitrary_dict_2 = 0xD1, + arbitrary_dict_4 = 0xD2, optional = 0xF0, @"enum" = 0xF1, @"error" = 0xFF, - empty_array = 0x70, - array_1 = 0x71, - array_2 = 0x72, - array_3 = 0x73, - array_4 = 0x74, - array_5 = 0x75, - array_6 = 0x76, - array_7 = 0x77, - array_8 = 0x78, - array_9 = 0x79, - array_10 = 0x7A, - array_11 = 0x7B, - array_12 = 0x7C, - array_13 = 0x7D, - array_14 = 0x7E, - array_15 = 0x7F, - dyn_array_1 = 0xDA, - dyn_array_2 = 0xDB, - dyn_array_4 = 0xDC, - list_1 = 0x81, - list_2 = 0x82, - list_3 = 0x83, - list_4 = 0x84, - list_5 = 0x85, - list_6 = 0x86, - list_7 = 0x87, - list_8 = 0x88, - list_9 = 0x89, - list_10 = 0x8A, - list_11 = 0x8B, - list_12 = 0x8C, - list_13 = 0x8D, - list_14 = 0x8E, - list_15 = 0x8F, - dyn_list_1 = 0xDD, - dyn_list_2 = 0xDE, - dyn_list_4 = 0xDF, - - // TODO: Have optimized markers like lists - dyn_dict_1 = 0xD0, - dyn_dict_2 = 0xD1, - dyn_dict_4 = 0xD2, }; pub const State = enum { @@ -272,6 +313,8 @@ pub const Token = union(enum) { bits: u16, view: []const u8, }, + tuple: u32, + eos, }; diff --git a/zig/src/main.zig b/zig/src/main.zig index ecda0a1..1ca5139 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -42,35 +42,38 @@ const test_float = [_][]const u8{ }; pub fn main() !void { - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; - const allocator = gpa.allocator(); - var buf: [512]u8 = undefined; for (test_bool) |in| { std.debug.print("-------------------------------------------------\n", .{}); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(bool, allocator, in, .{})}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(bool, in, .{})}); buf = undefined; } for (test_int) |in| { std.debug.print("-------------------------------------------------\n", .{}); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(i512, allocator, in, .{})}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(i512, in, .{})}); buf = undefined; } for (test_uint) |in| { std.debug.print("-------------------------------------------------\n", .{}); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(u512, allocator, in, .{})}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(u512, in, .{})}); buf = undefined; } for (test_float) |in| { std.debug.print("-------------------------------------------------\n", .{}); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(f128, allocator, in, .{ .float_behavior = .widen })}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(f128, in, .{ .float_behavior = .widen })}); buf = undefined; } + std.debug.print("-------------------------------------------------\n", .{}); + const in = &.{ 0x01, 0x72, 0x20, 0xFF, 0x21, 0x00, 0x13 }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(struct { u8, u16 }, in, .{ .float_behavior = .widen })}); + buf = undefined; + // var s: Scanner = .init(allocator, &serialized); // while (true) { // const token = try s.next(); diff --git a/zig/src/static.zig b/zig/src/static.zig index 0f4f925..f996d96 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -12,22 +12,21 @@ pub const ParseOptions = struct { } = .preserve, }; -pub fn parseFromSlice(comptime T: type, gpa: std.mem.Allocator, slice: []const u8, comptime options: ParseOptions) !T { - var scanner: Scanner = .init(gpa, slice); +pub fn parseFromSlice(comptime T: type, slice: []const u8, comptime options: ParseOptions) !T { + var scanner: Scanner = .init(slice); defer scanner.deinit(); - return parseFromTokenSource(T, gpa, &scanner, options); + return parseFromTokenSource(T, &scanner, options); } -pub fn parseFromTokenSource(comptime T: type, gpa: std.mem.Allocator, scanner: *Scanner, comptime options: ParseOptions) !T { +pub fn parseFromTokenSource(comptime T: type, scanner: *Scanner, comptime options: ParseOptions) !T { assert(try scanner.next() == .version); - const value = try innerParse(T, gpa, scanner, options); + const value = try innerParse(T, scanner, options); assert(try scanner.next() == .eos); return value; } -pub fn innerParse(comptime T: type, gpa: std.mem.Allocator, scanner: *Scanner, comptime options: ParseOptions) !T { - _ = gpa; +pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOptions) !T { switch (@typeInfo(T)) { .bool => { return switch (try scanner.next()) { @@ -38,7 +37,7 @@ pub fn innerParse(comptime T: type, gpa: std.mem.Allocator, scanner: *Scanner, c }, .int => |int| { const token = try scanner.next(); - if (token != Scanner.Token.int) return error.UnexpectedToken; + if (token != .int) return error.UnexpectedToken; if (comptime !options.ignore_integer_signedness) { if (token.int.signedness != int.signedness) return error.WrongIntegerType; } @@ -47,7 +46,7 @@ pub fn innerParse(comptime T: type, gpa: std.mem.Allocator, scanner: *Scanner, c }, .float => |float| { const token = try scanner.next(); - if (token != Scanner.Token.float) return error.UnexpectedToken; + if (token != .float) return error.UnexpectedToken; if (comptime options.float_behavior != .widen) { if (float.bits != token.float.bits) return error.CannotWidenFloat; } @@ -62,6 +61,21 @@ pub fn innerParse(comptime T: type, gpa: std.mem.Allocator, scanner: *Scanner, c else => unreachable, }); }, + .@"struct" => |structInfo| { + if (structInfo.is_tuple) { + const token = try scanner.next(); + if (token != .tuple) return error.UnexpectedToken; + assert(structInfo.fields.len == token.tuple); + + var r: T = undefined; + + inline for (structInfo.fields, 0..) |field, i| { + r[i] = try innerParse(field.type, scanner, options); + } + + return r; + } + }, .comptime_int, .comptime_float => error.IncompatibleTypes, else => return error.TODO, } From b306d4586b7b0984c62bcdd4a240642087418d2f Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 14 Nov 2025 04:33:31 +0000 Subject: [PATCH 08/30] Add support for optionals --- zig/src/Scanner.zig | 84 +++++++++++++++++++++++++++++++++++++++++++-- zig/src/main.zig | 34 +++++++++++++----- zig/src/static.zig | 25 ++++++++++++++ 3 files changed, 132 insertions(+), 11 deletions(-) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index 4c804d4..e24690d 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -18,6 +18,69 @@ pub fn deinit(self: *Scanner) void { self.* = undefined; } +pub fn peekNextTokenType(self: *Scanner) !TokenType { + switch (self.state) { + .marker => { + const m = std.enums.fromInt(MarkerType, self.input[self.cursor]) orelse return error.UnknownMarker; + return switch (m) { + .null => .null, + .false => .false, + .true => .true, + .signed_int_8, + .signed_int_16, + .signed_int_32, + .signed_int64, + .signed_int_128, + .signed_int_256, + .signed_int_512, + .arbitrary_signed_int, + .unsigned_int_8, + .unsigned_int_16, + .unsigned_int_32, + .unsigned_int64, + .unsigned_int_128, + .unsigned_int_256, + .unsigned_int_512, + .arbitrary_unsigned_int, + => .int, + .half_float, + // .minifloat, + .single_float, + // .extended_float_40, + .double_float, + .extended_float_80, + .quadruple_float, + // .octuple_float, + // .brain_float, + => .float, + .empty_tuple, + .tuple_1, + .tuple_2, + .tuple_3, + .tuple_4, + .tuple_5, + .tuple_6, + .tuple_7, + .tuple_8, + .tuple_9, + .tuple_10, + .tuple_11, + .tuple_12, + .tuple_13, + .tuple_14, + .tuple_15, + .arbitrary_tuple_1, + .arbitrary_tuple_2, + .arbitrary_tuple_4, + => .tuple, + .optional => .optional, + else => error.NotImplemented, + }; + }, + else => return error.UnsupportedLookup, + } +} + pub fn next(self: *Scanner) !Token { state: switch (self.state) { .version => { @@ -143,6 +206,11 @@ pub fn next(self: *Scanner) !Token { self.cursor += 4; return self.parseTupleState(byte_length); }, + .optional => { + self.cursor += 1; + self.state = .marker; + return .optional; + }, else => return error.NotImplemented, } @@ -300,7 +368,19 @@ pub const State = enum { arbitrary_uint, }; -pub const Token = union(enum) { +pub const TokenType = enum { + version, + null, + false, + true, + int, + float, + tuple, + optional, + eos, +}; + +pub const Token = union(TokenType) { version: u8, null, false, @@ -314,7 +394,7 @@ pub const Token = union(enum) { view: []const u8, }, tuple: u32, - + optional, eos, }; diff --git a/zig/src/main.zig b/zig/src/main.zig index 1ca5139..fc1536d 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -69,18 +69,34 @@ pub fn main() !void { } std.debug.print("-------------------------------------------------\n", .{}); - const in = &.{ 0x01, 0x72, 0x20, 0xFF, 0x21, 0x00, 0x13 }; - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(struct { u8, u16 }, in, .{ .float_behavior = .widen })}); + const in1 = &.{ 0x01, 0x72, 0x20, 0xFF, 0x21, 0x00, 0x13 }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in1)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(struct { u8, u16 }, in1, .{})}); buf = undefined; - // var s: Scanner = .init(allocator, &serialized); - // while (true) { - // const token = try s.next(); - // if (token == .eos) break; + std.debug.print("-------------------------------------------------\n", .{}); + const in2 = &.{ 0x01, 0xF0, 0x20, 0xFA }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in2)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in2, .{})}); + buf = undefined; + + std.debug.print("-------------------------------------------------\n", .{}); + const in3 = &.{ 0x01, 0xF0, 0x00 }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in3)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in3, .{})}); + buf = undefined; - // std.debug.print("Scanner: {any}\n", .{token}); - // } + std.debug.print("-------------------------------------------------\n", .{}); + const in4 = &.{ 0x01, 0x20, 0xFA }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in4)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in4, .{})}); + buf = undefined; + + std.debug.print("-------------------------------------------------\n", .{}); + const in5 = &.{ 0x01, 0x20, 0xFA }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in5)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in5, .{ .non_typed_optionals = .allow })}); + buf = undefined; } fn readableOutput(buffer: []u8, input: []const u8) []const u8 { diff --git a/zig/src/static.zig b/zig/src/static.zig index f996d96..1f68625 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -10,6 +10,11 @@ pub const ParseOptions = struct { widen, preserve, } = .preserve, + /// Wether to try parsing types that do not start with `0xF0` (optional marker) + non_typed_optionals: enum(u1) { + @"error", + allow, + } = .@"error", }; pub fn parseFromSlice(comptime T: type, slice: []const u8, comptime options: ParseOptions) !T { @@ -28,6 +33,12 @@ pub fn parseFromTokenSource(comptime T: type, scanner: *Scanner, comptime option pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOptions) !T { switch (@typeInfo(T)) { + .null => { + return switch (try scanner.next()) { + .null => null, + else => error.UnexpectedToken, + }; + }, .bool => { return switch (try scanner.next()) { .false => false, @@ -61,6 +72,20 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp else => unreachable, }); }, + .optional => |optional| { + switch (try scanner.peekNextTokenType()) { + .optional => { + _ = try scanner.next(); + if (try scanner.peekNextTokenType() == .null) { + _ = try scanner.next(); + return null; + } + + return try innerParse(optional.child, scanner, options); + }, + else => return if (comptime options.non_typed_optionals == .allow) try innerParse(optional.child, scanner, options) else error.UnexpectedToken, + } + }, .@"struct" => |structInfo| { if (structInfo.is_tuple) { const token = try scanner.next(); From b257d1259e6dbfc9c06588c82fd6f5fb2485499a Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 14 Nov 2025 05:26:19 +0000 Subject: [PATCH 09/30] Add support for enums --- zig/src/Scanner.zig | 12 ++++++++++++ zig/src/main.zig | 6 ++++++ zig/src/scanner_test.zig | 11 +++++++---- zig/src/static.zig | 27 +++++++++++++++++++++------ 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index e24690d..74f1fec 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -211,7 +211,17 @@ pub fn next(self: *Scanner) !Token { self.state = .marker; return .optional; }, + .@"enum" => { + self.cursor += 1; + if (self.input[self.cursor] == @intFromEnum(MarkerType.arbitrary_unsigned_int)) { + self.cursor += 1; + self.state = .arbitrary_uint; + } else { + self.state = .uint; + } + return .@"enum"; + }, else => return error.NotImplemented, } }, @@ -377,6 +387,7 @@ pub const TokenType = enum { float, tuple, optional, + @"enum", eos, }; @@ -395,6 +406,7 @@ pub const Token = union(TokenType) { }, tuple: u32, optional, + @"enum", eos, }; diff --git a/zig/src/main.zig b/zig/src/main.zig index fc1536d..79dd7fa 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -97,6 +97,12 @@ pub fn main() !void { std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in5)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in5, .{ .non_typed_optionals = .allow })}); buf = undefined; + + std.debug.print("-------------------------------------------------\n", .{}); + const in6 = &.{ 0x01, 0xF1, 0x20, 0x00 }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in6)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(enum { TEST }, in6, .{})}); + buf = undefined; } fn readableOutput(buffer: []u8, input: []const u8) []const u8 { diff --git a/zig/src/scanner_test.zig b/zig/src/scanner_test.zig index f7f4336..c24822b 100644 --- a/zig/src/scanner_test.zig +++ b/zig/src/scanner_test.zig @@ -7,7 +7,7 @@ const expect = std.testing.expect; const eql = std.mem.eql; test "null" { - var scanner: Scanner = .init(std.testing.allocator, &.{ 0x01, 0x00 }); + var scanner: Scanner = .init(&.{ 0x01, 0x00 }); defer scanner.deinit(); try expectNext(&scanner, .{ .version = 1 }); @@ -16,14 +16,14 @@ test "null" { } test "bool" { - var scanner: Scanner = .init(std.testing.allocator, &.{ 0x01, 0x01 }); + var scanner: Scanner = .init(&.{ 0x01, 0x01 }); try expectNext(&scanner, .{ .version = 1 }); try expectNext(&scanner, .false); try expectNext(&scanner, .eos); scanner.deinit(); - scanner = .init(std.testing.allocator, &.{ 0x01, 0x02 }); + scanner = .init(&.{ 0x01, 0x02 }); defer scanner.deinit(); try expectNext(&scanner, .{ .version = 1 }); @@ -67,7 +67,7 @@ test "int" { }; for (test_input, 0..) |in, i| { - var scanner: Scanner = .init(std.testing.allocator, in); + var scanner: Scanner = .init(in); defer scanner.deinit(); const out = test_output[i]; @@ -92,6 +92,9 @@ fn expectNext(scanner: *Scanner, expected_token: Scanner.Token) !void { .null, .false, .true, + .tuple, + .optional, + .@"enum", .eos, => {}, } diff --git a/zig/src/static.zig b/zig/src/static.zig index 1f68625..5978b2c 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -86,6 +86,13 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp else => return if (comptime options.non_typed_optionals == .allow) try innerParse(optional.child, scanner, options) else error.UnexpectedToken, } }, + .@"enum" => |enumInfo| { + if (try scanner.next() != .@"enum") return error.UnexpectedToken; + const token = try scanner.next(); + if (token != .int) return error.UnexpectedToken; + + return @enumFromInt(sliceToInt(enumInfo.tag_type, token.int.view)); + }, .@"struct" => |structInfo| { if (structInfo.is_tuple) { const token = try scanner.next(); @@ -106,16 +113,24 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp } } -fn sliceToInt(comptime T: type, slice: []const u8) T { - const alignedType = std.math.ByteAlignedInt(T); - const byte_length = @divExact(@typeInfo(alignedType).int.bits, 8); - assert(slice.len <= byte_length); +inline fn alignIntegerType(comptime T: type) type { + const int = @typeInfo(T).int; + // 0 bit integers mostly happen when using enums with 1 single element + // We require every type to be at least 1 byte long to be parsed + if (int.bits == 0) return std.meta.Int(int.signedness, 8); + return std.math.ByteAlignedInt(T); +} + +fn sliceToInt(comptime T: type, slice: []const u8) alignIntegerType(T) { + const N = alignIntegerType(T); + const byte_length = @divExact(@typeInfo(N).int.bits, 8); if (slice.len < byte_length) { var buf = std.mem.zeroes([byte_length]u8); @memcpy(buf[byte_length - slice.len ..], slice); - return std.mem.readInt(alignedType, &buf, .big); + return std.mem.readInt(N, &buf, .big); } - return std.mem.readInt(alignedType, slice[0..byte_length], .big); + assert(slice.len == byte_length); + return std.mem.readInt(N, slice[0..byte_length], .big); } From bd77a6333f4de579d238f2963966eb0025e27367 Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 14 Nov 2025 05:31:19 +0000 Subject: [PATCH 10/30] opt-in parsing safety for enums --- zig/src/static.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/zig/src/static.zig b/zig/src/static.zig index 5978b2c..de5036b 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -6,6 +6,8 @@ const assert = std.debug.assert; pub const ParseOptions = struct { /// Allow parsing `i8` as `u8` and vice-versa ignore_integer_signedness: bool = false, + /// Use `std.enums.fromInt` instead of attempting to cast + safe_enum_parsing: bool = false, float_behavior: enum(u1) { widen, preserve, @@ -91,7 +93,11 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp const token = try scanner.next(); if (token != .int) return error.UnexpectedToken; - return @enumFromInt(sliceToInt(enumInfo.tag_type, token.int.view)); + if (comptime options.safe_enum_parsing) { + return std.enums.fromInt(T, sliceToInt(enumInfo.tag_type, token.int.view)) orelse error.InvalidEnumTag; + } else { + return @enumFromInt(sliceToInt(enumInfo.tag_type, token.int.view)); + } }, .@"struct" => |structInfo| { if (structInfo.is_tuple) { From 83a8dccfeb1089d32a83d2a74eb6465307d82455 Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 14 Nov 2025 05:41:41 +0000 Subject: [PATCH 11/30] Small update to meta markers --- README.md | 8 ++++---- zig/src/Scanner.zig | 14 +++++--------- zig/src/main.zig | 6 +++--- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 61e0a86..960885f 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,13 @@ HBP reserves all the `E0-FF` range for meta types. ## Optional -Marker: `F0` +Marker: `E0` This marker is used as an indicator that the following marker can either be [`null`](#null) or another type. ## Enum -Marker: `F1` +Marker: `E1` The enum marker must **always** be followed by an integer marker to indicate the maximum size of the enum, the data should follow the same encoding as the indicated type. @@ -281,6 +281,6 @@ A map is just like a dictionary but instead, the keys can be of any type. | `D3-D5` | [`map`](#map) | [Composite](#composite-data-types) | | `DA-DC` | [`tuple`](#tuple) | [Composite](#composite-data-types) | | `DD-DF` | [`vector`](#vector) | [Composite](#composite-data-types) | -| `F0` | [`optional`](#optional) | [Meta](#meta-data-types) | -| `F1` | [`enum`](#enum) | [Meta](#meta-data-types) | +| `E0` | [`optional`](#optional) | [Meta](#meta-data-types) | +| `E1` | [`enum`](#enum) | [Meta](#meta-data-types) | | `FF` | [`error`](#error) | [Meta](#meta-data-types) | \ No newline at end of file diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index 74f1fec..c93e2d3 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -211,6 +211,7 @@ pub fn next(self: *Scanner) !Token { self.state = .marker; return .optional; }, + // NOTE: Maybe it could be worth to allow enums to omit the type marker for `u8` enums .@"enum" => { self.cursor += 1; if (self.input[self.cursor] == @intFromEnum(MarkerType.arbitrary_unsigned_int)) { @@ -257,7 +258,7 @@ pub fn next(self: *Scanner) !Token { }; }, .post_value => { - if (try self.checkEnd()) return .eos; + if (self.checkEnd()) return .eos; // NOTE: If no changes are necessary here after all types have been implemented // remove this and add a simple check to the beginning of `marker` instead. self.state = .marker; @@ -274,13 +275,8 @@ fn calculateIntegerByteLength(marker: u8) !usize { }; } -fn checkEnd(self: *Scanner) !bool { +fn checkEnd(self: *Scanner) bool { return self.cursor >= self.input.len; - // if (self.cursor >= self.input.len) { - // if (self.stack.bit_len == 0) return true; - // return error.BufferUnderrun; - // } - // return false; } fn parseTupleState(self: *Scanner, byte_length: u32) Token { @@ -363,8 +359,8 @@ pub const MarkerType = enum(u8) { arbitrary_dict_1 = 0xD0, arbitrary_dict_2 = 0xD1, arbitrary_dict_4 = 0xD2, - optional = 0xF0, - @"enum" = 0xF1, + optional = 0xE0, + @"enum" = 0xE1, @"error" = 0xFF, }; diff --git a/zig/src/main.zig b/zig/src/main.zig index 79dd7fa..4471941 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -75,13 +75,13 @@ pub fn main() !void { buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in2 = &.{ 0x01, 0xF0, 0x20, 0xFA }; + const in2 = &.{ 0x01, 0xE0, 0x20, 0xFA }; std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in2)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in2, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in3 = &.{ 0x01, 0xF0, 0x00 }; + const in3 = &.{ 0x01, 0xE0, 0x00 }; std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in3)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in3, .{})}); buf = undefined; @@ -99,7 +99,7 @@ pub fn main() !void { buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in6 = &.{ 0x01, 0xF1, 0x20, 0x00 }; + const in6 = &.{ 0x01, 0xE1, 0x20, 0x00 }; std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in6)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(enum { TEST }, in6, .{})}); buf = undefined; From eac73d0f1d3bf7392bbf3fd4747b4cb784656dd3 Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 14 Nov 2025 11:35:42 +0000 Subject: [PATCH 12/30] Introduce `Identifier` in favor of `Version` --- README.md | 15 +++++++++++---- zig/src/Scanner.zig | 12 ++++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 960885f..6c7e129 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ It consists of a set of primitive types, composite types, and meta types that to - [About](#about) - [Table of Contents](#table-of-contents) - [Representation](#representation) +- [Identifier](#identifier) - [Primitive Data Types](#primitive-data-types) - [Null](#null) - [Bool](#bool) @@ -21,14 +22,12 @@ It consists of a set of primitive types, composite types, and meta types that to - [Floats](#floats) - [Decimals](#decimals) - [Meta Data Types](#meta-data-types) - - [String](#string) - - [Vector](#vector) - [Optional](#optional) - [Enum](#enum) - [Error](#error) - [Composite Data Types](#composite-data-types) - - [Array](#array) - - [List](#list) + - [Tuple](#tuple) + - [Vector](#vector) - [Dictionary](#dictionary) - [Map](#map) - [Cheat Sheet](#cheat-sheet) @@ -39,6 +38,14 @@ Every serialized HBP value begins with the HBP version used to encode it followe ![representation image](representation.png) +# Identifier + +The HBP identifier is the byte at the beginning of every payload that tells you the version of the protocol and allows developers to pass custom flags. + +The identifier is a single byte subdivided into 2 parts of 4 bits. +The first 4 bits are reserved for the protocol version, parsers use this information to adapt to version-specific changes. +The last 4 bits are used for user defined flags, spec compliant parsers will not validate or make use of this bits for anything, they are given to the user as-is. + # Primitive Data Types Primitive types (or primitives) are the fundamental blocks used to represent the encoded data. diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index c93e2d3..62af00d 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Scanner = @This(); -state: State = .version, +state: State = .identifier, value_start: usize = undefined, input: []const u8 = undefined, cursor: usize = 0, @@ -83,10 +83,10 @@ pub fn peekNextTokenType(self: *Scanner) !TokenType { pub fn next(self: *Scanner) !Token { state: switch (self.state) { - .version => { + .identifier => { self.cursor += 1; self.state = .marker; - return .{ .version = self.input[0] }; + return .{ .identifier = self.input[0] }; }, .marker => { const m = std.enums.fromInt(MarkerType, self.input[self.cursor]) orelse return error.UnknownMarker; @@ -365,7 +365,7 @@ pub const MarkerType = enum(u8) { }; pub const State = enum { - version, + identifier, marker, post_value, int, @@ -375,7 +375,7 @@ pub const State = enum { }; pub const TokenType = enum { - version, + identifier, null, false, true, @@ -388,7 +388,7 @@ pub const TokenType = enum { }; pub const Token = union(TokenType) { - version: u8, + identifier: u8, null, false, true, From 54c375fbd6f5c4c105384525bf240b80f27d985e Mon Sep 17 00:00:00 2001 From: DidaS Date: Mon, 17 Nov 2025 11:37:07 +0000 Subject: [PATCH 13/30] `version` -> `identifier` --- zig/src/scanner_test.zig | 10 +++++----- zig/src/static.zig | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/zig/src/scanner_test.zig b/zig/src/scanner_test.zig index c24822b..3dcda01 100644 --- a/zig/src/scanner_test.zig +++ b/zig/src/scanner_test.zig @@ -10,7 +10,7 @@ test "null" { var scanner: Scanner = .init(&.{ 0x01, 0x00 }); defer scanner.deinit(); - try expectNext(&scanner, .{ .version = 1 }); + try expectNext(&scanner, .{ .identifier = 1 }); try expectNext(&scanner, .null); try expectNext(&scanner, .eos); } @@ -18,7 +18,7 @@ test "null" { test "bool" { var scanner: Scanner = .init(&.{ 0x01, 0x01 }); - try expectNext(&scanner, .{ .version = 1 }); + try expectNext(&scanner, .{ .identifier = 1 }); try expectNext(&scanner, .false); try expectNext(&scanner, .eos); @@ -26,7 +26,7 @@ test "bool" { scanner = .init(&.{ 0x01, 0x02 }); defer scanner.deinit(); - try expectNext(&scanner, .{ .version = 1 }); + try expectNext(&scanner, .{ .identifier = 1 }); try expectNext(&scanner, .true); try expectNext(&scanner, .eos); } @@ -71,7 +71,7 @@ test "int" { defer scanner.deinit(); const out = test_output[i]; - try expectNext(&scanner, .{ .version = 1 }); + try expectNext(&scanner, .{ .identifier = 1 }); try expectNext(&scanner, out); try expectNext(&scanner, .eos); } @@ -88,7 +88,7 @@ fn expectNext(scanner: *Scanner, expected_token: Scanner.Token) !void { .float => |expected_value| { try expect(eql(u8, expected_value.view, token.int.view)); }, - .version, + .identifier, .null, .false, .true, diff --git a/zig/src/static.zig b/zig/src/static.zig index de5036b..9603876 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -27,7 +27,7 @@ pub fn parseFromSlice(comptime T: type, slice: []const u8, comptime options: Par } pub fn parseFromTokenSource(comptime T: type, scanner: *Scanner, comptime options: ParseOptions) !T { - assert(try scanner.next() == .version); + assert(try scanner.next() == .identifier); const value = try innerParse(T, scanner, options); assert(try scanner.next() == .eos); return value; From 56e3690616c548dc55eed71335b8cf1876ca3415 Mon Sep 17 00:00:00 2001 From: DidaS Date: Mon, 17 Nov 2025 11:38:07 +0000 Subject: [PATCH 14/30] Remove build config --- zig/build.zig | 5 ----- zig/src/Serializer.zig | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/zig/build.zig b/zig/build.zig index 6338aee..233990c 100644 --- a/zig/build.zig +++ b/zig/build.zig @@ -4,9 +4,6 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const config_options = b.addOptions(); - config_options.addOption(u8, "HBP_VERSION", 1); - const exe_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, @@ -34,7 +31,6 @@ pub fn build(b: *std.Build) void { b.installArtifact(libhbp); const run_cmd = b.addRunArtifact(exe); - exe.root_module.addOptions("config", config_options); run_cmd.step.dependOn(b.getInstallStep()); const run_step = b.step("run", "Run the app"); @@ -44,7 +40,6 @@ pub fn build(b: *std.Build) void { check.dependOn(&exe.step); const exe_test = b.addTest(.{ .root_module = exe_mod }); - exe_test.root_module.addOptions("config", config_options); const test_artifact = b.addRunArtifact(exe_test); const test_step = b.step("test", "Run unit tests on the exports"); diff --git a/zig/src/Serializer.zig b/zig/src/Serializer.zig index 5a1cb45..d1de68e 100644 --- a/zig/src/Serializer.zig +++ b/zig/src/Serializer.zig @@ -1,6 +1,5 @@ const std = @import("std"); const builtin = @import("builtin"); -const HBP_VERSION = @import("config").HBP_VERSION; const ByteAlignedInt = std.math.ByteAlignedInt; @@ -12,6 +11,8 @@ const native_endian = builtin.cpu.arch.endian(); const Serializer = @This(); +const HBP_VERSION = 1; + fn calculateMarker(comptime T: type) []const u8 { comptime { const type_info = @typeInfo(T); From b6adfbdcddc2df5722f55f3c9ca473263785323f Mon Sep 17 00:00:00 2001 From: DidaS Date: Tue, 18 Nov 2025 17:08:20 +0000 Subject: [PATCH 15/30] Scanner should be public --- zig/src/Scanner.zig | 2 +- zig/src/root.zig | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index 62af00d..f91d4ac 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const Scanner = @This(); +pub const Scanner = @This(); state: State = .identifier, value_start: usize = undefined, diff --git a/zig/src/root.zig b/zig/src/root.zig index b6ec846..65c3780 100644 --- a/zig/src/root.zig +++ b/zig/src/root.zig @@ -5,4 +5,5 @@ pub const parseFromSlice = static.parseFromSlice; pub const parseFromTokenSource = static.parseFromTokenSource; pub const innerParse = static.innerParse; +pub const Scanner = @import("./Scanner.zig"); pub const Serializer = @import("./Serializer.zig"); From eb7b4a426bfc283e00dabb657fd83d8dd38b944a Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 01:26:58 +0000 Subject: [PATCH 16/30] Unions and Structs (Dictionaries) --- README.md | 42 ++++++++++++++++++ zig/src/Scanner.zig | 105 ++++++++++++++++++++++++++++++++++++++++++-- zig/src/main.zig | 28 +++++++++++- zig/src/static.zig | 62 +++++++++++++++++++++++--- 4 files changed, 225 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6c7e129..b5e5c2e 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,12 @@ Marker: `E1` The enum marker must **always** be followed by an integer marker to indicate the maximum size of the enum, the data should follow the same encoding as the indicated type. +## Union + +Marker: `E2` + +The union marker is **always** followed by a string marker indicating the active tag followed by the union data. + ## Error Marker: `FF` @@ -160,6 +166,42 @@ Serialized: 01 FF 6B 54 68 69 73 20 46 61 69 6C 65 64 # Composite Data Types +## Strings + +Strings are `UTF-8` encoded arrays of bytes. + +> Why aren't strings a meta type on top of Vector? There was a long discussion about this that i need to append here. . . + +Small strings: + +| Marker | String size | +| :----: | :---------: | +| `60` | 0 | +| `61` | 1 | +| `62` | 2 | +| `63` | 3 | +| `64` | 4 | +| `65` | 5 | +| `66` | 6 | +| `67` | 7 | +| `68` | 8 | +| `69` | 9 | +| `6A` | 10 | +| `6B` | 11 | +| `6C` | 12 | +| `6D` | 13 | +| `6E` | 14 | +| `6F` | 15 | + +Long Strings: + +| Marker | Extra bytes | Maximum Size | +| :----: | :---------: | :-----------: | +| `C0` | 1 | 255 | +| `C1` | 2 | 65_535 | +| `C2` | 4 | 4_294_967_295 | + + ## Tuple Small tuples: diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index f91d4ac..c711185 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -91,6 +91,10 @@ pub fn next(self: *Scanner) !Token { .marker => { const m = std.enums.fromInt(MarkerType, self.input[self.cursor]) orelse return error.UnknownMarker; switch (m) { + .optional => { + self.cursor += 1; + return .optional; + }, .null => { self.cursor += 1; self.state = .post_value; @@ -167,6 +171,47 @@ pub fn next(self: *Scanner) !Token { }, // .octuple_float, // .brain_float, + .empty_string, + .string_1, + .string_2, + .string_3, + .string_4, + .string_5, + .string_6, + .string_7, + .string_8, + .string_9, + .string_10, + .string_11, + .string_12, + .string_13, + .string_14, + .string_15, + => { + const byte_length = self.input[self.cursor] - 0x60; + self.cursor += 1; + return self.parseStringState(byte_length); + }, + // TODO: The spec for arbitrary strings is a bit more complex than this + // But since its not yet documented we aren't implementing it fully + .arbitrary_string_1 => { + self.cursor += 1; + const byte_length = std.mem.nativeToBig(u8, self.input[self.cursor]); + self.cursor += 1; + return self.parseStringState(byte_length); + }, + .arbitrary_string_2 => { + self.cursor += 1; + const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + self.cursor += 2; + return self.parseStringState(byte_length); + }, + .arbitrary_string_4 => { + self.cursor += 1; + const byte_length = std.mem.nativeToBig(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); + self.cursor += 4; + return self.parseStringState(byte_length); + }, .empty_tuple, .tuple_1, .tuple_2, @@ -206,11 +251,25 @@ pub fn next(self: *Scanner) !Token { self.cursor += 4; return self.parseTupleState(byte_length); }, - .optional => { + .arbitrary_dict_1 => { self.cursor += 1; - self.state = .marker; - return .optional; + const byte_length = std.mem.nativeToBig(u8, self.input[self.cursor]); + self.cursor += 1; + return self.parseStructState(byte_length); + }, + .arbitrary_dict_2 => { + self.cursor += 1; + const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + self.cursor += 2; + return self.parseStructState(byte_length); }, + .arbitrary_dict_4 => { + self.cursor += 1; + const byte_length = std.mem.nativeToBig(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); + self.cursor += 4; + return self.parseStructState(byte_length); + }, + //? TODO?: Could be worth optimizing u1 enums in the sense of booleans, this has to be discussed further // NOTE: Maybe it could be worth to allow enums to omit the type marker for `u8` enums .@"enum" => { self.cursor += 1; @@ -223,6 +282,10 @@ pub fn next(self: *Scanner) !Token { return .@"enum"; }, + .@"union" => { + self.cursor += 1; + return .@"union"; + }, else => return error.NotImplemented, } }, @@ -279,11 +342,21 @@ fn checkEnd(self: *Scanner) bool { return self.cursor >= self.input.len; } +fn parseStringState(self: *Scanner, byte_length: u32) Token { + self.state = if (byte_length == 0) .post_value else .marker; + return .{ .string = byte_length }; +} + fn parseTupleState(self: *Scanner, byte_length: u32) Token { self.state = if (byte_length == 0) .post_value else .marker; return .{ .tuple = byte_length }; } +fn parseStructState(self: *Scanner, kv_pairs: u32) Token { + self.state = if (kv_pairs == 0) .post_value else .marker; + return .{ .@"struct" = kv_pairs }; +} + pub const MarkerType = enum(u8) { null = 0x00, false = 0x01, @@ -316,6 +389,25 @@ pub const MarkerType = enum(u8) { decimal_32 = 0x3A, decimal_64 = 0x3B, decimal_128 = 0x3C, + empty_string = 0x60, + string_1 = 0x61, + string_2 = 0x62, + string_3 = 0x63, + string_4 = 0x64, + string_5 = 0x65, + string_6 = 0x66, + string_7 = 0x67, + string_8 = 0x68, + string_9 = 0x69, + string_10 = 0x6A, + string_11 = 0x6B, + string_12 = 0x6C, + string_13 = 0x6D, + string_14 = 0x6E, + string_15 = 0x6F, + arbitrary_string_1 = 0xC0, + arbitrary_string_2 = 0xC1, + arbitrary_string_4 = 0xC2, empty_tuple = 0x70, tuple_1 = 0x71, tuple_2 = 0x72, @@ -361,6 +453,7 @@ pub const MarkerType = enum(u8) { arbitrary_dict_4 = 0xD2, optional = 0xE0, @"enum" = 0xE1, + @"union" = 0xE2, @"error" = 0xFF, }; @@ -381,9 +474,12 @@ pub const TokenType = enum { true, int, float, + string, tuple, + @"struct", optional, @"enum", + @"union", eos, }; @@ -400,9 +496,12 @@ pub const Token = union(TokenType) { bits: u16, view: []const u8, }, + string: u32, tuple: u32, + @"struct": u32, optional, @"enum", + @"union", eos, }; diff --git a/zig/src/main.zig b/zig/src/main.zig index 4471941..617638e 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -77,13 +77,13 @@ pub fn main() !void { std.debug.print("-------------------------------------------------\n", .{}); const in2 = &.{ 0x01, 0xE0, 0x20, 0xFA }; std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in2)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in2, .{})}); + std.debug.print("Parsed Payload: {any}\n", .{try Deserializer.parseFromSlice(?u8, in2, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); const in3 = &.{ 0x01, 0xE0, 0x00 }; std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in3)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in3, .{})}); + std.debug.print("Parsed Payload: {any}\n", .{try Deserializer.parseFromSlice(?u8, in3, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); @@ -103,6 +103,30 @@ pub fn main() !void { std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in6)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(enum { TEST }, in6, .{})}); buf = undefined; + + std.debug.print("-------------------------------------------------\n", .{}); + const in7 = &.{ 0x01, 0x65, 0x77, 0x6F, 0x72, 0x6C, 0x64 }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in7)}); + std.debug.print("Parsed Payload: {s}\n", .{try Deserializer.parseFromSlice([]const u8, in7, .{})}); + buf = undefined; + + std.debug.print("-------------------------------------------------\n", .{}); + const in8 = &.{ 0x01, 0xD0, 0x01, 0x61, 0x78, 0x22, 0x00, 0x8B, 0x36, 0x60 }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in8)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(struct { x: u32 }, in8, .{})}); + buf = undefined; + + std.debug.print("-------------------------------------------------\n", .{}); + const in9 = &.{ 0x01, 0xE2, 0x61, 0x78, 0x22, 0x00, 0x8B, 0x36, 0x60 }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in9)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(union(enum(u1)) { x: u32, y: []const u8 }, in9, .{})}); + buf = undefined; + + std.debug.print("-------------------------------------------------\n", .{}); + const in10 = &.{ 0x01, 0xE2, 0x61, 0x79, 0x65, 0x77, 0x6F, 0x72, 0x6C, 0x64 }; + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in10)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(union(enum(u1)) { x: u32, y: []const u8 }, in10, .{})}); + buf = undefined; } fn readableOutput(buffer: []u8, input: []const u8) []const u8 { diff --git a/zig/src/static.zig b/zig/src/static.zig index 9603876..8dca0fe 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -4,6 +4,10 @@ const Scanner = @import("Scanner.zig"); const assert = std.debug.assert; pub const ParseOptions = struct { + string_behavior: union(enum(u1)) { + reference: void, + dupe: std.mem.Allocator, + } = .{ .reference = {} }, /// Allow parsing `i8` as `u8` and vice-versa ignore_integer_signedness: bool = false, /// Use `std.enums.fromInt` instead of attempting to cast @@ -88,31 +92,75 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp else => return if (comptime options.non_typed_optionals == .allow) try innerParse(optional.child, scanner, options) else error.UnexpectedToken, } }, - .@"enum" => |enumInfo| { + .@"enum" => |enum_info| { if (try scanner.next() != .@"enum") return error.UnexpectedToken; const token = try scanner.next(); if (token != .int) return error.UnexpectedToken; if (comptime options.safe_enum_parsing) { - return std.enums.fromInt(T, sliceToInt(enumInfo.tag_type, token.int.view)) orelse error.InvalidEnumTag; + return std.enums.fromInt(T, sliceToInt(enum_info.tag_type, token.int.view)) orelse error.InvalidEnumTag; } else { - return @enumFromInt(sliceToInt(enumInfo.tag_type, token.int.view)); + return @enumFromInt(sliceToInt(enum_info.tag_type, token.int.view)); } }, - .@"struct" => |structInfo| { - if (structInfo.is_tuple) { + .@"union" => |union_info| { + if (union_info.tag_type == null) @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); + const token = try scanner.next(); + if (token != .@"union") return error.UnexpectedToken; + const union_key = try innerParse([]const u8, scanner, options); + inline for (union_info.fields) |field| { + if (std.mem.eql(u8, field.name, union_key)) return @unionInit(T, field.name, try innerParse(field.type, scanner, options)); + } + + return error.InvalidUnion; + }, + .pointer => |pointer_info| { + // TODO: Strings should probably be namespaced... + // As of now we assume all `[]const u8` types are strings which is wrong (?) + // We could force a unique sentinel to decide on this (DEL code point could work?) + if (pointer_info.size != .slice or !pointer_info.is_const) @compileError("Unsupported pointer type"); + assert(pointer_info.child == u8); + const token = try scanner.next(); + if (token != .string) return error.UnexpectedToken; + + const str = switch (options.string_behavior) { + .reference => scanner.input[scanner.cursor .. scanner.cursor + token.string], + .dupe => |gpa| try gpa.dupe(u8, scanner.input[scanner.cursor .. scanner.cursor + token.string]), + }; + scanner.cursor += token.string; + scanner.state = .post_value; + return str; + }, + .@"struct" => |struct_info| { + if (struct_info.is_tuple) { const token = try scanner.next(); if (token != .tuple) return error.UnexpectedToken; - assert(structInfo.fields.len == token.tuple); + assert(struct_info.fields.len == token.tuple); var r: T = undefined; - inline for (structInfo.fields, 0..) |field, i| { + inline for (struct_info.fields, 0..) |field, i| { r[i] = try innerParse(field.type, scanner, options); } return r; } + + const token = try scanner.next(); + if (token != .@"struct") return error.UnexpectedToken; + assert(struct_info.fields.len == token.@"struct"); + var r: T = undefined; + + for (0..token.@"struct") |_| { + const field_name = try innerParse([]const u8, scanner, options); + inline for (struct_info.fields) |field| { + if (std.mem.eql(u8, field.name, field_name)) { + @field(r, field.name) = try innerParse(field.type, scanner, options); + } + } + } + + return r; }, .comptime_int, .comptime_float => error.IncompatibleTypes, else => return error.TODO, From c2ceb81b64e182c03995241e6ef2e15e082fb8b7 Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 03:13:56 +0000 Subject: [PATCH 17/30] New serializer & changed to little endian --- zig/src/Scanner.zig | 20 ++-- zig/src/Serializer.zig | 218 --------------------------------------- zig/src/main.zig | 76 +++++++------- zig/src/root.zig | 2 +- zig/src/scanner_test.zig | 30 +++--- zig/src/serializer.zig | 162 +++++++++++++++++++++++++++++ zig/src/static.zig | 6 +- 7 files changed, 229 insertions(+), 285 deletions(-) delete mode 100644 zig/src/Serializer.zig create mode 100644 zig/src/serializer.zig diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index c711185..99c39ec 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -196,19 +196,19 @@ pub fn next(self: *Scanner) !Token { // But since its not yet documented we aren't implementing it fully .arbitrary_string_1 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u8, self.input[self.cursor]); + const byte_length = std.mem.nativeToLittle(u8, self.input[self.cursor]); self.cursor += 1; return self.parseStringState(byte_length); }, .arbitrary_string_2 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + const byte_length = std.mem.nativeToLittle(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); self.cursor += 2; return self.parseStringState(byte_length); }, .arbitrary_string_4 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); + const byte_length = std.mem.nativeToLittle(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); self.cursor += 4; return self.parseStringState(byte_length); }, @@ -235,37 +235,37 @@ pub fn next(self: *Scanner) !Token { }, .arbitrary_tuple_1 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u8, self.input[self.cursor]); + const byte_length = std.mem.nativeToLittle(u8, self.input[self.cursor]); self.cursor += 1; return self.parseTupleState(byte_length); }, .arbitrary_tuple_2 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + const byte_length = std.mem.nativeToLittle(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); self.cursor += 2; return self.parseTupleState(byte_length); }, .arbitrary_tuple_4 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); + const byte_length = std.mem.nativeToLittle(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); self.cursor += 4; return self.parseTupleState(byte_length); }, .arbitrary_dict_1 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u8, self.input[self.cursor]); + const byte_length = std.mem.nativeToLittle(u8, self.input[self.cursor]); self.cursor += 1; return self.parseStructState(byte_length); }, .arbitrary_dict_2 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + const byte_length = std.mem.nativeToLittle(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); self.cursor += 2; return self.parseStructState(byte_length); }, .arbitrary_dict_4 => { self.cursor += 1; - const byte_length = std.mem.nativeToBig(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); + const byte_length = std.mem.nativeToLittle(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); self.cursor += 4; return self.parseStructState(byte_length); }, @@ -306,7 +306,7 @@ pub fn next(self: *Scanner) !Token { }; }, .arbitrary_int, .arbitrary_uint => { - const byte_length = std.mem.nativeToBig(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + const byte_length = std.mem.nativeToLittle(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); self.cursor += 2; const value_start = self.cursor; self.cursor += byte_length; diff --git a/zig/src/Serializer.zig b/zig/src/Serializer.zig deleted file mode 100644 index d1de68e..0000000 --- a/zig/src/Serializer.zig +++ /dev/null @@ -1,218 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); - -const ByteAlignedInt = std.math.ByteAlignedInt; - -const assert = std.debug.assert; -const expect = std.testing.expect; -const eql = std.mem.eql; - -const native_endian = builtin.cpu.arch.endian(); - -const Serializer = @This(); - -const HBP_VERSION = 1; - -fn calculateMarker(comptime T: type) []const u8 { - comptime { - const type_info = @typeInfo(T); - - switch (type_info) { - .int => { - const alignedType = ByteAlignedInt(T); - const info = @typeInfo(alignedType).int; - const base = if (info.signedness == .signed) 0x10 else 0x20; - return switch (info.bits) { - 8 => &.{base}, - 16 => &.{base + 1}, - 32 => &.{base + 2}, - 64 => &.{base + 3}, - 128 => &.{base + 4}, - 256 => &.{base + 5}, - 512 => &.{base + 6}, - else => |bits| &[_]u8{base + 0x0F} ++ &@as([2]u8, @bitCast(std.mem.nativeToBig(u16, @divExact(bits, 8)))), - }; - }, - .float => |f| { - return switch (f.bits) { - 16 => &.{0x30}, - 32 => &.{0x32}, - 64 => &.{0x34}, - 80 => &.{0x35}, - 128 => &.{0x36}, - else => unreachable, - }; - }, - .array => |arr| { - const is_string = arr.sentinel() != null; - const base = 0x80; - const marker = blk: { - if (arr.len <= 15) break :blk [_]u8{base + arr.len} ++ calculateMarker(arr.child); - @compileError("Not yet implemented"); - }; - if (is_string) return [_]u8{0xE0} ++ marker; - return marker; - }, - else => unreachable, - } - } -} - -fn calculateBufferLen(comptime T: type) comptime_int { - comptime { - const type_info = @typeInfo(T); - - switch (type_info) { - .int => |i| { - return @divExact(i.bits, 8); - }, - .float => |f| { - return @divExact(f.bits, 8); - }, - .array => |arr| { - return calculateBufferLen(arr.child) * arr.len; - }, - else => unreachable, - } - } -} - -fn Buffer(comptime T: type) type { - comptime { - const type_info = @typeInfo(T); - - return switch (type_info) { - .null, .bool => [2]u8, - .int => [1 + calculateMarker(T).len + calculateBufferLen(ByteAlignedInt(T))]u8, - .float => [2 + calculateBufferLen(T)]u8, - .array => [1 + calculateMarker(T).len + calculateBufferLen(T)]u8, - else => unreachable, - }; - } -} - -pub fn serializeNull() Buffer(@TypeOf(null)) { - var buffer: Buffer(@TypeOf(null)) = undefined; - buffer[0] = HBP_VERSION; - buffer[1] = 0x00; - return buffer; -} - -test serializeNull { - try expect(eql(u8, &serializeNull(), &.{ 0x01, 0x00 })); -} - -pub fn serializeBool(value: bool) Buffer(bool) { - var buffer: Buffer(bool) = undefined; - buffer[0] = HBP_VERSION; - buffer[1] = if (value) 0x02 else 0x01; - return buffer; -} - -test serializeBool { - try expect(eql(u8, &serializeBool(false), &.{ 0x01, 0x01 })); - try expect(eql(u8, &serializeBool(true), &.{ 0x01, 0x02 })); -} - -pub fn serializeFloat(comptime T: type, value: T) Buffer(T) { - const marker = comptime calculateMarker(T); - var buffer: Buffer(T) = undefined; - buffer[0] = HBP_VERSION; - - inline for (marker, 1..) |byte, i| { - buffer[i] = byte; - } - - buffer[1 + marker.len ..].* = @bitCast(value); - - return buffer; -} - -pub fn serializeInt(comptime T: type, value: T) Buffer(T) { - const aligned_type = ByteAlignedInt(T); - const marker = comptime calculateMarker(T); - - var buffer: Buffer(T) = undefined; - buffer[0] = HBP_VERSION; - - inline for (marker, 1..) |byte, i| { - buffer[i] = byte; - } - - serializeIntInternal(aligned_type, buffer[1 + marker.len ..], value); - - return buffer; -} - -fn serializeIntInternal(comptime T: type, buf: *[calculateBufferLen(T)]u8, value: T) void { - buf.* = @bitCast(std.mem.nativeToBig(T, value)); -} - -test serializeInt { - try expect(eql(u8, &serializeInt(i8, 45), &.{ 0x01, 0x10, 0x2D })); - try expect(eql(u8, &serializeInt(i16, 6347), &.{ 0x01, 0x11, 0x18, 0xCB })); - try expect(eql(u8, &serializeInt(i32, 9123424), &.{ 0x01, 0x12, 0x00, 0x8B, 0x36, 0x60 })); - try expect(eql(u8, &serializeInt(i64, 5294967295), &.{ 0x01, 0x13, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); - try expect(eql(u8, &serializeInt(i128, 28446744073709551615), &.{ 0x01, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8A, 0xC7, 0x23, 0x04, 0x89, 0xE7, 0xFF, 0xFF })); - try expect(eql(u8, &serializeInt(i256, 340282366920938463463375607431768211456), &.{ 0x01, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 })); - try expect(eql(u8, &serializeInt(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), &.{ 0x01, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })); - try expect(eql(u8, &serializeInt(i6, 30), &.{ 0x01, 0x10, 0x1E })); - try expect(eql(u8, &serializeInt(i38, 9123424), &.{ 0x01, 0x1F, 0x00, 0x05, 0x00, 0x00, 0x8B, 0x36, 0x60 })); - try expect(eql(u8, &serializeInt(i80, 5294967295), &.{ 0x01, 0x1F, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); - - try expect(eql(u8, &serializeInt(u8, 250), &.{ 0x01, 0x20, 0xFA })); - try expect(eql(u8, &serializeInt(u16, 6347), &.{ 0x01, 0x21, 0x18, 0xCB })); - try expect(eql(u8, &serializeInt(u32, 9123424), &.{ 0x01, 0x22, 0x00, 0x8B, 0x36, 0x60 })); - try expect(eql(u8, &serializeInt(u64, 5294967295), &.{ 0x01, 0x23, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); - try expect(eql(u8, &serializeInt(u128, 28446744073709551615), &.{ 0x01, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8A, 0xC7, 0x23, 0x04, 0x89, 0xE7, 0xFF, 0xFF })); - try expect(eql(u8, &serializeInt(u256, 340282366920938463463375607431768211456), &.{ 0x01, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 })); - try expect(eql(u8, &serializeInt(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), &.{ 0x01, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })); - try expect(eql(u8, &serializeInt(u6, 30), &.{ 0x01, 0x20, 0x1E })); - try expect(eql(u8, &serializeInt(u38, 9123424), &.{ 0x01, 0x2F, 0x00, 0x05, 0x00, 0x00, 0x8B, 0x36, 0x60 })); - try expect(eql(u8, &serializeInt(u80, 5294967295), &.{ 0x01, 0x2F, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF })); -} - -pub fn serializeList(comptime T: type, comptime max_len: u32, value: []T) struct { Buffer([max_len]T), u32 } { - assert(value.len <= max_len); - const type_info = @typeInfo(T); - - // TODO: Support more types - if (type_info != .int) @compileError("Only integer lists are supported"); - - var buffer: Buffer([max_len]T) = undefined; - buffer[0] = HBP_VERSION; - - const marker = comptime calculateMarker([max_len]T); - - inline for (marker, 1..) |byte, i| { - buffer[i] = byte; - } - - const byte_size = calculateBufferLen(T); - var real_length: u32 = 1 + marker.len; - - for (value) |el| { - serializeIntInternal(T, buffer[real_length .. real_length + byte_size], el); - real_length += byte_size; - } - - return .{ buffer, real_length }; -} - -pub fn serializeString(comptime max_len: u32, value: []const u8) struct { Buffer([max_len:0]u8), u32 } { - assert(value.len <= max_len); - var buffer: Buffer([max_len:0]u8) = undefined; - buffer[0] = HBP_VERSION; - - const marker = comptime calculateMarker([max_len:0]u8); - - inline for (marker, 1..) |byte, i| { - buffer[i] = byte; - } - - const initial_len: usize = 1 + marker.len; - - @memcpy(buffer[initial_len .. initial_len + value.len], value); - - return .{ buffer, @intCast(initial_len + value.len) }; -} diff --git a/zig/src/main.zig b/zig/src/main.zig index 617638e..ddffed0 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -1,44 +1,44 @@ const std = @import("std"); -const Serializer = @import("./Serializer.zig"); +const Serializer = @import("./serializer.zig"); const Deserializer = @import("./static.zig"); const Scanner = @import("./Scanner.zig"); const test_bool = [_][]const u8{ - &.{ 0x01, 0x01 }, - &.{ 0x01, 0x02 }, + Serializer.serializeComptime(bool, false), + Serializer.serializeComptime(bool, true), }; const test_int = [_][]const u8{ - &Serializer.serializeInt(i8, 45), - &Serializer.serializeInt(i16, 6347), - &Serializer.serializeInt(i32, 9123424), - &Serializer.serializeInt(i64, 5294967295), - &Serializer.serializeInt(i128, 28446744073709551615), - &Serializer.serializeInt(i256, 340282366920938463463375607431768211456), - &Serializer.serializeInt(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), - &Serializer.serializeInt(i6, 30), - &Serializer.serializeInt(i38, 9123424), - &Serializer.serializeInt(i80, 5294967295), + Serializer.serializeComptime(i8, 45), + Serializer.serializeComptime(i16, 6347), + Serializer.serializeComptime(i32, 9123424), + Serializer.serializeComptime(i64, 5294967295), + Serializer.serializeComptime(i128, 28446744073709551615), + Serializer.serializeComptime(i256, 340282366920938463463375607431768211456), + Serializer.serializeComptime(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), + Serializer.serializeComptime(i6, 30), + Serializer.serializeComptime(i38, 9123424), + Serializer.serializeComptime(i80, 5294967295), }; const test_uint = [_][]const u8{ - &Serializer.serializeInt(u8, 250), - &Serializer.serializeInt(u16, 6347), - &Serializer.serializeInt(u32, 9123424), - &Serializer.serializeInt(u64, 5294967295), - &Serializer.serializeInt(u128, 28446744073709551615), - &Serializer.serializeInt(u256, 340282366920938463463375607431768211456), - &Serializer.serializeInt(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), - &Serializer.serializeInt(u6, 30), - &Serializer.serializeInt(u38, 9123424), - &Serializer.serializeInt(u80, 5294967295), + Serializer.serializeComptime(u8, 250), + Serializer.serializeComptime(u16, 6347), + Serializer.serializeComptime(u32, 9123424), + Serializer.serializeComptime(u64, 5294967295), + Serializer.serializeComptime(u128, 28446744073709551615), + Serializer.serializeComptime(u256, 340282366920938463463375607431768211456), + Serializer.serializeComptime(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), + Serializer.serializeComptime(u6, 30), + Serializer.serializeComptime(u38, 9123424), + Serializer.serializeComptime(u80, 5294967295), }; const test_float = [_][]const u8{ - &Serializer.serializeFloat(f16, 20.11), - &Serializer.serializeFloat(f32, 202.5456), - &Serializer.serializeFloat(f64, 220.563564), - &Serializer.serializeFloat(f80, 25460.565667657), - &Serializer.serializeFloat(f128, 2024.5689076899345), + Serializer.serializeComptime(f16, 20.11), + Serializer.serializeComptime(f32, 202.5456), + Serializer.serializeComptime(f64, 220.563564), + Serializer.serializeComptime(f80, 25460.565667657), + Serializer.serializeComptime(f128, 2024.5689076899345), }; pub fn main() !void { @@ -69,61 +69,61 @@ pub fn main() !void { } std.debug.print("-------------------------------------------------\n", .{}); - const in1 = &.{ 0x01, 0x72, 0x20, 0xFF, 0x21, 0x00, 0x13 }; + const in1 = comptime Serializer.serializeComptime(struct { u8, u16 }, .{ 36, 3204 }); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in1)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(struct { u8, u16 }, in1, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in2 = &.{ 0x01, 0xE0, 0x20, 0xFA }; + const in2 = comptime Serializer.serializeComptime(?u8, 250); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in2)}); std.debug.print("Parsed Payload: {any}\n", .{try Deserializer.parseFromSlice(?u8, in2, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in3 = &.{ 0x01, 0xE0, 0x00 }; + const in3 = comptime Serializer.serializeComptime(?u8, null); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in3)}); std.debug.print("Parsed Payload: {any}\n", .{try Deserializer.parseFromSlice(?u8, in3, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in4 = &.{ 0x01, 0x20, 0xFA }; + const in4 = comptime Serializer.serializeComptime(u8, 246); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in4)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in4, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in5 = &.{ 0x01, 0x20, 0xFA }; + const in5 = comptime Serializer.serializeComptime(u8, 246); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in5)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in5, .{ .non_typed_optionals = .allow })}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in6 = &.{ 0x01, 0xE1, 0x20, 0x00 }; + const in6 = comptime Serializer.serializeComptime(enum { TEST }, .TEST); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in6)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(enum { TEST }, in6, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in7 = &.{ 0x01, 0x65, 0x77, 0x6F, 0x72, 0x6C, 0x64 }; + const in7 = comptime Serializer.serializeComptime([]const u8, "hello world"); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in7)}); std.debug.print("Parsed Payload: {s}\n", .{try Deserializer.parseFromSlice([]const u8, in7, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in8 = &.{ 0x01, 0xD0, 0x01, 0x61, 0x78, 0x22, 0x00, 0x8B, 0x36, 0x60 }; + const in8 = comptime Serializer.serializeComptime(struct { x: u32 }, .{ .x = 43545 }); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in8)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(struct { x: u32 }, in8, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in9 = &.{ 0x01, 0xE2, 0x61, 0x78, 0x22, 0x00, 0x8B, 0x36, 0x60 }; + const in9 = comptime Serializer.serializeComptime(union(enum(u1)) { x: u32, y: []const u8 }, .{ .x = 760589 }); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in9)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(union(enum(u1)) { x: u32, y: []const u8 }, in9, .{})}); buf = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in10 = &.{ 0x01, 0xE2, 0x61, 0x79, 0x65, 0x77, 0x6F, 0x72, 0x6C, 0x64 }; + const in10 = comptime Serializer.serializeComptime(union(enum(u1)) { x: u32, y: []const u8 }, .{ .y = "yo world" }); std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in10)}); std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(union(enum(u1)) { x: u32, y: []const u8 }, in10, .{})}); buf = undefined; diff --git a/zig/src/root.zig b/zig/src/root.zig index 65c3780..4872210 100644 --- a/zig/src/root.zig +++ b/zig/src/root.zig @@ -6,4 +6,4 @@ pub const parseFromTokenSource = static.parseFromTokenSource; pub const innerParse = static.innerParse; pub const Scanner = @import("./Scanner.zig"); -pub const Serializer = @import("./Serializer.zig"); +pub const serialize = @import("./serializer.zig").serialize; diff --git a/zig/src/scanner_test.zig b/zig/src/scanner_test.zig index 3dcda01..2808b57 100644 --- a/zig/src/scanner_test.zig +++ b/zig/src/scanner_test.zig @@ -1,6 +1,6 @@ const std = @import("std"); const Scanner = @import("Scanner.zig"); -const Serializer = @import("Serializer.zig"); +const Serializer = @import("./serializer.zig"); const Token = Scanner.Token; const expect = std.testing.expect; @@ -33,20 +33,20 @@ test "bool" { test "int" { const test_input = [_][]const u8{ - &Serializer.serializeInt(i8, 45), - &Serializer.serializeInt(i16, 6347), - &Serializer.serializeInt(i32, 9123424), - &Serializer.serializeInt(i64, 5294967295), - &Serializer.serializeInt(i128, 28446744073709551615), - &Serializer.serializeInt(i256, 340282366920938463463375607431768211456), - &Serializer.serializeInt(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), - &Serializer.serializeInt(u8, 250), - &Serializer.serializeInt(u16, 6347), - &Serializer.serializeInt(u32, 9123424), - &Serializer.serializeInt(u64, 5294967295), - &Serializer.serializeInt(u128, 28446744073709551615), - &Serializer.serializeInt(u256, 340282366920938463463375607431768211456), - &Serializer.serializeInt(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), + Serializer.serializeComptime(i8, 45), + Serializer.serializeComptime(i16, 6347), + Serializer.serializeComptime(i32, 9123424), + Serializer.serializeComptime(i64, 5294967295), + Serializer.serializeComptime(i128, 28446744073709551615), + Serializer.serializeComptime(i256, 340282366920938463463375607431768211456), + Serializer.serializeComptime(i512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), + Serializer.serializeComptime(u8, 250), + Serializer.serializeComptime(u16, 6347), + Serializer.serializeComptime(u32, 9123424), + Serializer.serializeComptime(u64, 5294967295), + Serializer.serializeComptime(u128, 28446744073709551615), + Serializer.serializeComptime(u256, 340282366920938463463375607431768211456), + Serializer.serializeComptime(u512, 115792089237316395423570985008687907853269984665640564039457584007913129639935), }; const test_output = [_]Token{ Token{ .int = .{ .signedness = .signed, .view = &.{0x2D} } }, diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig new file mode 100644 index 0000000..f0392f9 --- /dev/null +++ b/zig/src/serializer.zig @@ -0,0 +1,162 @@ +const std = @import("std"); +const MarkerType = @import("./Scanner.zig").MarkerType; + +const HBP_VERSION = 1; + +pub fn serializeComptime(comptime T: type, value: T) []const u8 { + comptime { + var buf: [512]u8 = undefined; + var w: std.Io.Writer = .fixed(&buf); + + serialize(T, value, &w) catch unreachable; + + const buffered = w.buffered(); + const x: [buffered.len]u8 = buffered[0..buffered.len].*; + return &x; + } +} + +pub fn serialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { + // TODO: Use proper identifier + try writer.writeByte(HBP_VERSION); + try innerSerialize(T, value, writer); +} + +fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { + const type_info = @typeInfo(T); + + switch (type_info) { + .optional => |optional_info| { + try writer.writeByte(@intFromEnum(MarkerType.optional)); + if (value) |opt| { + try innerSerialize(optional_info.child, opt, writer); + } else { + try writer.writeByte(@intFromEnum(MarkerType.null)); + } + }, + .null => { + try writer.writeByte(@intFromEnum(MarkerType.null)); + }, + .bool => { + try writer.writeByte(0x01 + @as(u8, @intFromBool(value))); + }, + .int => { + const aligned_type = std.math.ByteAlignedInt(T); + const int = @typeInfo(aligned_type).int; + + const marker = blk: { + const base = @intFromEnum(if (int.signedness == .unsigned) MarkerType.unsigned_int_8 else MarkerType.signed_int_8); + + break :blk switch (int.bits) { + 8 => base, + 16 => base + 1, + 32 => base + 2, + 64 => base + 3, + 128 => base + 4, + 256 => base + 5, + 512 => base + 6, + else => base + 0x0F, + }; + }; + + try writer.writeByte(marker); + if (marker & 0x0F == 0x0F) try writer.writeAll(&@as([2]u8, @bitCast(std.mem.nativeToLittle(u16, @divExact(int.bits, 8))))); + try writeInt(aligned_type, value, writer); + }, + // TODO: How are we supporting our other float types? + .float => |float_info| { + const marker = switch (float_info.bits) { + 16 => @intFromEnum(MarkerType.half_float), + 32 => @intFromEnum(MarkerType.single_float), + 64 => @intFromEnum(MarkerType.double_float), + 80 => @intFromEnum(MarkerType.extended_float_80), + 128 => @intFromEnum(MarkerType.quadruple_float), + else => unreachable, + }; + + try writer.writeByte(marker); + try writer.writeAll(&@as([@divExact(float_info.bits, 8)]u8, @bitCast(std.mem.nativeToLittle(T, value)))); + }, + .pointer => |pointer_info| { + switch (pointer_info.size) { + .slice => { + // ! This check is erroneous + // Right now we assume all slices ([]const u8) are strings + std.debug.assert(pointer_info.child == u8); + if (value.len <= 15) { + try writer.writeByte(@intFromEnum(MarkerType.empty_string) + value.len); + } else if (value.len <= std.math.maxInt(u8)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_1)); + try writeInt(u8, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u16)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_2)); + try writeInt(u16, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u32)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_4)); + try writeInt(u32, @intCast(value.len), writer); + } + + try writer.writeAll(value); + }, + else => @compileError("Unsupported pointer type"), + } + }, + .@"enum" => |enum_info| { + std.debug.assert(@typeInfo(enum_info.tag_type).int.signedness == .unsigned); + try writer.writeByte(@intFromEnum(MarkerType.@"enum")); + try innerSerialize(enum_info.tag_type, @intFromEnum(value), writer); + }, + .@"union" => |union_info| { + if (union_info.tag_type) |tag_type| { + inline for (union_info.fields) |field| { + if (value == @field(tag_type, field.name)) { + try writer.writeByte(@intFromEnum(MarkerType.@"union")); + try innerSerialize([]const u8, @tagName(value), writer); + try innerSerialize(field.type, @field(value, field.name), writer); + } + } + } else @compileError("Unable to parse non tagged union '" ++ @typeName(T) ++ "'"); + }, + .@"struct" => |struct_info| { + if (struct_info.is_tuple) { + if (struct_info.fields.len <= 15) { + try writer.writeByte(@intFromEnum(MarkerType.empty_tuple) + struct_info.fields.len); + } else if (struct_info.fields.len <= std.math.maxInt(u8)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_1)); + try writeInt(u8, @intCast(struct_info.fields.len), writer); + } else if (struct_info.fields.len <= std.math.maxInt(u16)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_2)); + try writeInt(u16, @intCast(struct_info.fields.len), writer); + } else if (struct_info.fields.len <= std.math.maxInt(u32)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_4)); + try writeInt(u32, @intCast(struct_info.fields.len), writer); + } + + inline for (struct_info.fields, 0..) |field, i| { + try innerSerialize(field.type, value[i], writer); + } + } else { + if (struct_info.fields.len <= std.math.maxInt(u8)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_dict_1)); + try writeInt(u8, @intCast(struct_info.fields.len), writer); + } else if (struct_info.fields.len <= std.math.maxInt(u16)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_dict_2)); + try writeInt(u16, @intCast(struct_info.fields.len), writer); + } else if (struct_info.fields.len <= std.math.maxInt(u32)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_dict_4)); + try writeInt(u32, @intCast(struct_info.fields.len), writer); + } + + inline for (struct_info.fields) |field| { + try innerSerialize([]const u8, field.name, writer); + try innerSerialize(field.type, @field(value, field.name), writer); + } + } + }, + else => @compileError("Unsupported type"), + } +} + +fn writeInt(comptime T: type, value: T, writer: *std.Io.Writer) !void { + try writer.writeAll(&@as([@divExact(@typeInfo(T).int.bits, 8)]u8, @bitCast(std.mem.nativeToLittle(T, @intCast(value))))); +} diff --git a/zig/src/static.zig b/zig/src/static.zig index 8dca0fe..0bc8252 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -181,10 +181,10 @@ fn sliceToInt(comptime T: type, slice: []const u8) alignIntegerType(T) { if (slice.len < byte_length) { var buf = std.mem.zeroes([byte_length]u8); - @memcpy(buf[byte_length - slice.len ..], slice); - return std.mem.readInt(N, &buf, .big); + @memcpy(buf[0..slice.len], slice); + return std.mem.readInt(N, &buf, .little); } assert(slice.len == byte_length); - return std.mem.readInt(N, slice[0..byte_length], .big); + return std.mem.readInt(N, slice[0..byte_length], .little); } From 743202abb41840158ab74295b6789fb920b8d3ec Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 04:27:09 +0000 Subject: [PATCH 18/30] Vectors (and arrays) --- zig/src/Scanner.zig | 46 +++++++++++++ zig/src/main.zig | 96 +++++++-------------------- zig/src/serializer.zig | 147 +++++++++++++++++++++++++++++------------ zig/src/static.zig | 121 ++++++++++++++++++++++++--------- 4 files changed, 265 insertions(+), 145 deletions(-) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index 99c39ec..c2dc609 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -251,6 +251,45 @@ pub fn next(self: *Scanner) !Token { self.cursor += 4; return self.parseTupleState(byte_length); }, + .empty_vector, + .vector_1, + .vector_2, + .vector_3, + .vector_4, + .vector_5, + .vector_6, + .vector_7, + .vector_8, + .vector_9, + .vector_10, + .vector_11, + .vector_12, + .vector_13, + .vector_14, + .vector_15, + => { + const byte_length = self.input[self.cursor] - 0x80; + self.cursor += 1; + return self.parseVectorState(byte_length); + }, + .arbitrary_vector_1 => { + self.cursor += 1; + const byte_length = std.mem.nativeToLittle(u8, self.input[self.cursor]); + self.cursor += 1; + return self.parseVectorState(byte_length); + }, + .arbitrary_vector_2 => { + self.cursor += 1; + const byte_length = std.mem.nativeToLittle(u16, std.mem.bytesToValue(u16, self.input[self.cursor .. self.cursor + 2])); + self.cursor += 2; + return self.parseVectorState(byte_length); + }, + .arbitrary_vector_4 => { + self.cursor += 1; + const byte_length = std.mem.nativeToLittle(u32, std.mem.bytesToValue(u32, self.input[self.cursor .. self.cursor + 4])); + self.cursor += 4; + return self.parseVectorState(byte_length); + }, .arbitrary_dict_1 => { self.cursor += 1; const byte_length = std.mem.nativeToLittle(u8, self.input[self.cursor]); @@ -352,6 +391,11 @@ fn parseTupleState(self: *Scanner, byte_length: u32) Token { return .{ .tuple = byte_length }; } +fn parseVectorState(self: *Scanner, elements: u32) Token { + self.state = if (elements == 0) .post_value else .marker; + return .{ .vector = elements }; +} + fn parseStructState(self: *Scanner, kv_pairs: u32) Token { self.state = if (kv_pairs == 0) .post_value else .marker; return .{ .@"struct" = kv_pairs }; @@ -476,6 +520,7 @@ pub const TokenType = enum { float, string, tuple, + vector, @"struct", optional, @"enum", @@ -498,6 +543,7 @@ pub const Token = union(TokenType) { }, string: u32, tuple: u32, + vector: u32, @"struct": u32, optional, @"enum", diff --git a/zig/src/main.zig b/zig/src/main.zig index ddffed0..9659ae7 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -42,90 +42,40 @@ const test_float = [_][]const u8{ }; pub fn main() !void { - var buf: [512]u8 = undefined; + var allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; + const gpa = allocator.allocator(); + for (test_bool) |in| { - std.debug.print("-------------------------------------------------\n", .{}); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(bool, in, .{})}); - buf = undefined; + print(bool, gpa, in); } for (test_int) |in| { - std.debug.print("-------------------------------------------------\n", .{}); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(i512, in, .{})}); - buf = undefined; + print(i512, gpa, in); } for (test_uint) |in| { - std.debug.print("-------------------------------------------------\n", .{}); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(u512, in, .{})}); - buf = undefined; + print(u512, gpa, in); } for (test_float) |in| { - std.debug.print("-------------------------------------------------\n", .{}); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(f128, in, .{ .float_behavior = .widen })}); - buf = undefined; + print(f128, gpa, in); } - std.debug.print("-------------------------------------------------\n", .{}); - const in1 = comptime Serializer.serializeComptime(struct { u8, u16 }, .{ 36, 3204 }); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in1)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(struct { u8, u16 }, in1, .{})}); - buf = undefined; - - std.debug.print("-------------------------------------------------\n", .{}); - const in2 = comptime Serializer.serializeComptime(?u8, 250); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in2)}); - std.debug.print("Parsed Payload: {any}\n", .{try Deserializer.parseFromSlice(?u8, in2, .{})}); - buf = undefined; - - std.debug.print("-------------------------------------------------\n", .{}); - const in3 = comptime Serializer.serializeComptime(?u8, null); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in3)}); - std.debug.print("Parsed Payload: {any}\n", .{try Deserializer.parseFromSlice(?u8, in3, .{})}); - buf = undefined; - - std.debug.print("-------------------------------------------------\n", .{}); - const in4 = comptime Serializer.serializeComptime(u8, 246); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in4)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in4, .{})}); - buf = undefined; - - std.debug.print("-------------------------------------------------\n", .{}); - const in5 = comptime Serializer.serializeComptime(u8, 246); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in5)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(?u8, in5, .{ .non_typed_optionals = .allow })}); - buf = undefined; - - std.debug.print("-------------------------------------------------\n", .{}); - const in6 = comptime Serializer.serializeComptime(enum { TEST }, .TEST); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in6)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(enum { TEST }, in6, .{})}); - buf = undefined; - - std.debug.print("-------------------------------------------------\n", .{}); - const in7 = comptime Serializer.serializeComptime([]const u8, "hello world"); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in7)}); - std.debug.print("Parsed Payload: {s}\n", .{try Deserializer.parseFromSlice([]const u8, in7, .{})}); - buf = undefined; - - std.debug.print("-------------------------------------------------\n", .{}); - const in8 = comptime Serializer.serializeComptime(struct { x: u32 }, .{ .x = 43545 }); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in8)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(struct { x: u32 }, in8, .{})}); - buf = undefined; - - std.debug.print("-------------------------------------------------\n", .{}); - const in9 = comptime Serializer.serializeComptime(union(enum(u1)) { x: u32, y: []const u8 }, .{ .x = 760589 }); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in9)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(union(enum(u1)) { x: u32, y: []const u8 }, in9, .{})}); - buf = undefined; + print(struct { u8, u16 }, gpa, comptime Serializer.serializeComptime(struct { u8, u16 }, .{ 36, 3204 })); + print(?u8, gpa, comptime Serializer.serializeComptime(?u8, 250)); + print(?u8, gpa, comptime Serializer.serializeComptime(?u8, null)); + print(enum { TEST }, gpa, comptime Serializer.serializeComptime(enum { TEST }, .TEST)); + print([]const u8, gpa, comptime Serializer.serializeComptime([]const u8, "hello world")); + print(struct { x: u32 }, gpa, comptime Serializer.serializeComptime(struct { x: u32 }, .{ .x = 43545 })); + print(union(enum(u1)) { x: u32, y: []const u8 }, gpa, comptime Serializer.serializeComptime(union(enum(u1)) { x: u32, y: []const u8 }, .{ .x = 760589 })); + print(union(enum(u1)) { x: u32, y: []const u8 }, gpa, comptime Serializer.serializeComptime(union(enum(u1)) { x: u32, y: []const u8 }, .{ .y = "yo world" })); + print([]u8, gpa, comptime Serializer.serializeComptime([]u8, @constCast(@as([]const u8, &.{ 10, 60, 134 })))); + const t = struct { x: u32 }; + print([]t, gpa, comptime Serializer.serializeComptime([]t, @constCast(@as([]const t, &.{ .{ .x = 10 }, .{ .x = 60 }, .{ .x = 134 } })))); +} +fn print(comptime T: type, gpa: std.mem.Allocator, payload: []const u8) void { + var buf: [512]u8 = undefined; std.debug.print("-------------------------------------------------\n", .{}); - const in10 = comptime Serializer.serializeComptime(union(enum(u1)) { x: u32, y: []const u8 }, .{ .y = "yo world" }); - std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, in10)}); - std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(union(enum(u1)) { x: u32, y: []const u8 }, in10, .{})}); + std.debug.print("HBP payload: {s}\n", .{readableOutput(&buf, payload)}); + std.debug.print("Parsed Payload: {any}\n", .{Deserializer.parseFromSlice(T, payload, gpa, .{})}); buf = undefined; } diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig index f0392f9..490a544 100644 --- a/zig/src/serializer.zig +++ b/zig/src/serializer.zig @@ -26,6 +26,7 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { const type_info = @typeInfo(T); switch (type_info) { + .void => return, .optional => |optional_info| { try writer.writeByte(@intFromEnum(MarkerType.optional)); if (value) |opt| { @@ -44,59 +45,76 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { const aligned_type = std.math.ByteAlignedInt(T); const int = @typeInfo(aligned_type).int; - const marker = blk: { - const base = @intFromEnum(if (int.signedness == .unsigned) MarkerType.unsigned_int_8 else MarkerType.signed_int_8); - - break :blk switch (int.bits) { - 8 => base, - 16 => base + 1, - 32 => base + 2, - 64 => base + 3, - 128 => base + 4, - 256 => base + 5, - 512 => base + 6, - else => base + 0x0F, - }; - }; - + const marker = getIntMarker(int); try writer.writeByte(marker); if (marker & 0x0F == 0x0F) try writer.writeAll(&@as([2]u8, @bitCast(std.mem.nativeToLittle(u16, @divExact(int.bits, 8))))); try writeInt(aligned_type, value, writer); }, // TODO: How are we supporting our other float types? .float => |float_info| { - const marker = switch (float_info.bits) { - 16 => @intFromEnum(MarkerType.half_float), - 32 => @intFromEnum(MarkerType.single_float), - 64 => @intFromEnum(MarkerType.double_float), - 80 => @intFromEnum(MarkerType.extended_float_80), - 128 => @intFromEnum(MarkerType.quadruple_float), - else => unreachable, - }; - - try writer.writeByte(marker); - try writer.writeAll(&@as([@divExact(float_info.bits, 8)]u8, @bitCast(std.mem.nativeToLittle(T, value)))); + try writer.writeByte(getFloatMarker(float_info)); + try writeFloat(T, value, writer); }, .pointer => |pointer_info| { switch (pointer_info.size) { .slice => { - // ! This check is erroneous - // Right now we assume all slices ([]const u8) are strings - std.debug.assert(pointer_info.child == u8); - if (value.len <= 15) { - try writer.writeByte(@intFromEnum(MarkerType.empty_string) + value.len); - } else if (value.len <= std.math.maxInt(u8)) { - try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_1)); - try writeInt(u8, @intCast(value.len), writer); - } else if (value.len <= std.math.maxInt(u16)) { - try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_2)); - try writeInt(u16, @intCast(value.len), writer); - } else if (value.len <= std.math.maxInt(u32)) { - try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_4)); - try writeInt(u32, @intCast(value.len), writer); - } + if (pointer_info.is_const and pointer_info.child == u8) { + if (value.len <= 15) { + try writer.writeByte(@intFromEnum(MarkerType.empty_string) + value.len); + } else if (value.len <= std.math.maxInt(u8)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_1)); + try writeInt(u8, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u16)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_2)); + try writeInt(u16, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u32)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_4)); + try writeInt(u32, @intCast(value.len), writer); + } - try writer.writeAll(value); + try writer.writeAll(value); + } else { + switch (@typeInfo(pointer_info.child)) { + .int => |int| { + try writeVectorMarker(value, writer); + try writer.writeByte(getIntMarker(int)); + for (value) |val| { + try writeInt(std.math.ByteAlignedInt(pointer_info.child), val, writer); + } + }, + .float => |float| { + try writeVectorMarker(value, writer); + try writer.writeByte(getFloatMarker(float)); + for (value) |val| { + try writeInt(std.math.ByteAlignedInt(pointer_info.child), val, writer); + } + }, + .bool => { + try writeVectorMarker(value, writer); + for (value) |val| { + try writer.writeByte(0x01 + @as(u8, @intFromBool(val))); + } + }, + else => { + if (value.len <= 15) { + try writer.writeByte(@intFromEnum(MarkerType.empty_tuple) + value.len); + } else if (value.len <= std.math.maxInt(u8)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_1)); + try writeInt(u8, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u16)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_2)); + try writeInt(u16, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u32)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_4)); + try writeInt(u32, @intCast(value.len), writer); + } + + for (value) |val| { + try innerSerialize(pointer_info.child, val, writer); + } + }, + } + } }, else => @compileError("Unsupported pointer type"), } @@ -160,3 +178,48 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { fn writeInt(comptime T: type, value: T, writer: *std.Io.Writer) !void { try writer.writeAll(&@as([@divExact(@typeInfo(T).int.bits, 8)]u8, @bitCast(std.mem.nativeToLittle(T, @intCast(value))))); } + +fn writeFloat(comptime T: type, value: T, writer: *std.Io.Writer) !void { + try writer.writeAll(&@as([@divExact(@typeInfo(T).float.bits, 8)]u8, @bitCast(std.mem.nativeToLittle(T, value)))); +} + +fn writeVectorMarker(value: anytype, writer: *std.Io.Writer) !void { + if (value.len <= 15) { + try writer.writeByte(@intFromEnum(MarkerType.empty_vector) + value.len); + } else if (value.len <= std.math.maxInt(u8)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_vector_1)); + try writeInt(u8, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u16)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_vector_2)); + try writeInt(u16, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u32)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_vector_4)); + try writeInt(u32, @intCast(value.len), writer); + } +} + +fn getIntMarker(int: std.builtin.Type.Int) u8 { + const base = @intFromEnum(if (int.signedness == .unsigned) MarkerType.unsigned_int_8 else MarkerType.signed_int_8); + + return switch (int.bits) { + 8 => base, + 16 => base + 1, + 32 => base + 2, + 64 => base + 3, + 128 => base + 4, + 256 => base + 5, + 512 => base + 6, + else => base + 0x0F, + }; +} + +fn getFloatMarker(float: std.builtin.Type.Float) u8 { + return switch (float.bits) { + 16 => @intFromEnum(MarkerType.half_float), + 32 => @intFromEnum(MarkerType.single_float), + 64 => @intFromEnum(MarkerType.double_float), + 80 => @intFromEnum(MarkerType.extended_float_80), + 128 => @intFromEnum(MarkerType.quadruple_float), + else => unreachable, + }; +} diff --git a/zig/src/static.zig b/zig/src/static.zig index 0bc8252..73ffec9 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -4,10 +4,6 @@ const Scanner = @import("Scanner.zig"); const assert = std.debug.assert; pub const ParseOptions = struct { - string_behavior: union(enum(u1)) { - reference: void, - dupe: std.mem.Allocator, - } = .{ .reference = {} }, /// Allow parsing `i8` as `u8` and vice-versa ignore_integer_signedness: bool = false, /// Use `std.enums.fromInt` instead of attempting to cast @@ -23,22 +19,24 @@ pub const ParseOptions = struct { } = .@"error", }; -pub fn parseFromSlice(comptime T: type, slice: []const u8, comptime options: ParseOptions) !T { +pub fn parseFromSlice(comptime T: type, slice: []const u8, gpa: std.mem.Allocator, comptime options: ParseOptions) !T { var scanner: Scanner = .init(slice); defer scanner.deinit(); - return parseFromTokenSource(T, &scanner, options); + return parseFromTokenSource(T, &scanner, gpa, options); } -pub fn parseFromTokenSource(comptime T: type, scanner: *Scanner, comptime options: ParseOptions) !T { +pub fn parseFromTokenSource(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, comptime options: ParseOptions) !T { assert(try scanner.next() == .identifier); - const value = try innerParse(T, scanner, options); + const value = try innerParse(T, scanner, gpa, options); assert(try scanner.next() == .eos); return value; } -pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOptions) !T { +/// Allocator is only used for dynamic slices and strings +pub fn innerParse(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, comptime options: ParseOptions) !T { switch (@typeInfo(T)) { + .void => return, .null => { return switch (try scanner.next()) { .null => null, @@ -87,9 +85,9 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp return null; } - return try innerParse(optional.child, scanner, options); + return try innerParse(optional.child, scanner, gpa, options); }, - else => return if (comptime options.non_typed_optionals == .allow) try innerParse(optional.child, scanner, options) else error.UnexpectedToken, + else => return if (comptime options.non_typed_optionals == .allow) try innerParse(optional.child, scanner, gpa, options) else error.UnexpectedToken, } }, .@"enum" => |enum_info| { @@ -107,29 +105,91 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp if (union_info.tag_type == null) @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); const token = try scanner.next(); if (token != .@"union") return error.UnexpectedToken; - const union_key = try innerParse([]const u8, scanner, options); + const union_key = try innerParse([]const u8, scanner, gpa, options); + defer gpa.free(union_key); inline for (union_info.fields) |field| { - if (std.mem.eql(u8, field.name, union_key)) return @unionInit(T, field.name, try innerParse(field.type, scanner, options)); + if (std.mem.eql(u8, field.name, union_key)) return @unionInit(T, field.name, try innerParse(field.type, scanner, gpa, options)); } return error.InvalidUnion; }, .pointer => |pointer_info| { - // TODO: Strings should probably be namespaced... - // As of now we assume all `[]const u8` types are strings which is wrong (?) - // We could force a unique sentinel to decide on this (DEL code point could work?) - if (pointer_info.size != .slice or !pointer_info.is_const) @compileError("Unsupported pointer type"); - assert(pointer_info.child == u8); - const token = try scanner.next(); - if (token != .string) return error.UnexpectedToken; + switch (pointer_info.size) { + .slice => { + if (pointer_info.is_const and pointer_info.child == u8) { + const token = try scanner.next(); + if (token != .string) return error.UnexpectedToken; + + const str = try gpa.dupe(u8, scanner.input[scanner.cursor .. scanner.cursor + token.string]); + scanner.cursor += token.string; + scanner.state = .post_value; + return str; + } - const str = switch (options.string_behavior) { - .reference => scanner.input[scanner.cursor .. scanner.cursor + token.string], - .dupe => |gpa| try gpa.dupe(u8, scanner.input[scanner.cursor .. scanner.cursor + token.string]), - }; - scanner.cursor += token.string; - scanner.state = .post_value; - return str; + switch (@typeInfo(pointer_info.child)) { + .int => |int| { + const token = try scanner.next(); + if (token != .vector) return error.UnexpectedToken; + + const first = try scanner.next(); + if (first != .int) return error.UnexpectedToken; + + if (comptime !options.ignore_integer_signedness) { + if (first.int.signedness != int.signedness) return error.WrongIntegerType; + } + + const element_byte_length = first.int.view.len; + const N = alignIntegerType(pointer_info.child); + var arr: std.ArrayList(N) = try .initCapacity(gpa, 1); + + try arr.append(gpa, sliceToInt(N, first.int.view)); + + // The first element is already retrieved + for (1..token.vector) |_| { + const value_start = scanner.cursor; + scanner.cursor += element_byte_length; + try arr.append(gpa, sliceToInt(N, scanner.input[value_start..scanner.cursor])); + } + + return try arr.toOwnedSlice(gpa); + }, + .float => { + const token = try scanner.next(); + if (token != .vector) return error.UnexpectedToken; + var arr: std.ArrayList(pointer_info.child) = .empty; + return try arr.toOwnedSlice(gpa); + }, + .bool => { + const token = try scanner.next(); + if (token != .vector) return error.UnexpectedToken; + var arr: std.ArrayList(bool) = .empty; + + for (0..token.tuple) |_| { + const b = try scanner.next(); + if (b != .bool) return error.UnexpectedToken; + try arr.append(gpa, switch (b.bool) { + .false => false, + .true => true, + }); + } + + return try arr.toOwnedSlice(gpa); + }, + else => { + const token = try scanner.next(); + if (token != .tuple) return error.UnexpectedToken; + var arr: std.ArrayList(pointer_info.child) = .empty; + + for (0..token.tuple) |_| { + try arr.append(gpa, try innerParse(pointer_info.child, scanner, gpa, options)); + } + + return try arr.toOwnedSlice(gpa); + }, + } + }, + else => @compileError("Unsupported pointer type"), + } }, .@"struct" => |struct_info| { if (struct_info.is_tuple) { @@ -140,7 +200,7 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp var r: T = undefined; inline for (struct_info.fields, 0..) |field, i| { - r[i] = try innerParse(field.type, scanner, options); + r[i] = try innerParse(field.type, scanner, gpa, options); } return r; @@ -152,10 +212,11 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, comptime options: ParseOp var r: T = undefined; for (0..token.@"struct") |_| { - const field_name = try innerParse([]const u8, scanner, options); + const field_name = try innerParse([]const u8, scanner, gpa, options); + defer gpa.free(field_name); inline for (struct_info.fields) |field| { if (std.mem.eql(u8, field.name, field_name)) { - @field(r, field.name) = try innerParse(field.type, scanner, options); + @field(r, field.name) = try innerParse(field.type, scanner, gpa, options); } } } From ec3eb36051a01b5be0497be066c7a63cfdaaced8 Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 04:42:35 +0000 Subject: [PATCH 19/30] length should be properly cast for addition --- zig/src/serializer.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig index 490a544..1c2106c 100644 --- a/zig/src/serializer.zig +++ b/zig/src/serializer.zig @@ -60,7 +60,7 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { .slice => { if (pointer_info.is_const and pointer_info.child == u8) { if (value.len <= 15) { - try writer.writeByte(@intFromEnum(MarkerType.empty_string) + value.len); + try writer.writeByte(@intFromEnum(MarkerType.empty_string) + @as(u8, @intCast(value.len))); } else if (value.len <= std.math.maxInt(u8)) { try writer.writeByte(@intFromEnum(MarkerType.arbitrary_string_1)); try writeInt(u8, @intCast(value.len), writer); @@ -97,7 +97,7 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { }, else => { if (value.len <= 15) { - try writer.writeByte(@intFromEnum(MarkerType.empty_tuple) + value.len); + try writer.writeByte(@intFromEnum(MarkerType.empty_tuple) + @as(u8, @intCast(value.len))); } else if (value.len <= std.math.maxInt(u8)) { try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_1)); try writeInt(u8, @intCast(value.len), writer); From 83ecaba6bdcae520239bb79c8776e00e9b80eb73 Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 05:02:52 +0000 Subject: [PATCH 20/30] Peek new types --- zig/src/Scanner.zig | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index c2dc609..5c0cdfb 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -53,6 +53,26 @@ pub fn peekNextTokenType(self: *Scanner) !TokenType { // .octuple_float, // .brain_float, => .float, + .empty_string, + .string_1, + .string_2, + .string_3, + .string_4, + .string_5, + .string_6, + .string_7, + .string_8, + .string_9, + .string_10, + .string_11, + .string_12, + .string_13, + .string_14, + .string_15, + .arbitrary_string_1, + .arbitrary_string_2, + .arbitrary_string_4, + => .string, .empty_tuple, .tuple_1, .tuple_2, @@ -73,7 +93,33 @@ pub fn peekNextTokenType(self: *Scanner) !TokenType { .arbitrary_tuple_2, .arbitrary_tuple_4, => .tuple, + .empty_vector, + .vector_1, + .vector_2, + .vector_3, + .vector_4, + .vector_5, + .vector_6, + .vector_7, + .vector_8, + .vector_9, + .vector_10, + .vector_11, + .vector_12, + .vector_13, + .vector_14, + .vector_15, + .arbitrary_vector_1, + .arbitrary_vector_2, + .arbitrary_vector_4, + => .vector, + .arbitrary_dict_1, + .arbitrary_dict_2, + .arbitrary_dict_4, + => .@"struct", .optional => .optional, + .@"enum" => .@"enum", + .@"union" => .@"union", else => error.NotImplemented, }; }, From 790a05fd4421254296f258a95bd609eb7a9965c8 Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 05:06:20 +0000 Subject: [PATCH 21/30] Allow peeking post value --- zig/src/Scanner.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index 5c0cdfb..9be7e9e 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -20,7 +20,7 @@ pub fn deinit(self: *Scanner) void { pub fn peekNextTokenType(self: *Scanner) !TokenType { switch (self.state) { - .marker => { + .marker, .post_value => { const m = std.enums.fromInt(MarkerType, self.input[self.cursor]) orelse return error.UnknownMarker; return switch (m) { .null => .null, From e5117d5509030d98115f5e97f43b973e09318d3d Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 08:58:46 +0000 Subject: [PATCH 22/30] Use integers for serializing union tags --- README.md | 2 +- zig/src/serializer.zig | 2 +- zig/src/static.zig | 29 ++++++++++++++++++----------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b5e5c2e..2af3575 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ The enum marker must **always** be followed by an integer marker to indicate the Marker: `E2` -The union marker is **always** followed by a string marker indicating the active tag followed by the union data. +The union marker is **always** followed by an integer marker indicating the active tag followed by the union data. ## Error diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig index 1c2106c..aca076d 100644 --- a/zig/src/serializer.zig +++ b/zig/src/serializer.zig @@ -129,7 +129,7 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { inline for (union_info.fields) |field| { if (value == @field(tag_type, field.name)) { try writer.writeByte(@intFromEnum(MarkerType.@"union")); - try innerSerialize([]const u8, @tagName(value), writer); + try innerSerialize(@typeInfo(tag_type).@"enum".tag_type, @intFromEnum(value), writer); try innerSerialize(field.type, @field(value, field.name), writer); } } diff --git a/zig/src/static.zig b/zig/src/static.zig index 73ffec9..00677e4 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -19,14 +19,21 @@ pub const ParseOptions = struct { } = .@"error", }; -pub fn parseFromSlice(comptime T: type, slice: []const u8, gpa: std.mem.Allocator, comptime options: ParseOptions) !T { +fn ParseOutputType(comptime T: type) type { + return switch (@typeInfo(T)) { + .int => alignIntegerType(T), + else => T, + }; +} + +pub fn parseFromSlice(comptime T: type, slice: []const u8, gpa: std.mem.Allocator, comptime options: ParseOptions) !ParseOutputType(T) { var scanner: Scanner = .init(slice); defer scanner.deinit(); return parseFromTokenSource(T, &scanner, gpa, options); } -pub fn parseFromTokenSource(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, comptime options: ParseOptions) !T { +pub fn parseFromTokenSource(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, comptime options: ParseOptions) !ParseOutputType(T) { assert(try scanner.next() == .identifier); const value = try innerParse(T, scanner, gpa, options); assert(try scanner.next() == .eos); @@ -34,7 +41,7 @@ pub fn parseFromTokenSource(comptime T: type, scanner: *Scanner, gpa: std.mem.Al } /// Allocator is only used for dynamic slices and strings -pub fn innerParse(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, comptime options: ParseOptions) !T { +pub fn innerParse(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, comptime options: ParseOptions) !ParseOutputType(T) { switch (@typeInfo(T)) { .void => return, .null => { @@ -102,14 +109,14 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, c } }, .@"union" => |union_info| { - if (union_info.tag_type == null) @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); - const token = try scanner.next(); - if (token != .@"union") return error.UnexpectedToken; - const union_key = try innerParse([]const u8, scanner, gpa, options); - defer gpa.free(union_key); - inline for (union_info.fields) |field| { - if (std.mem.eql(u8, field.name, union_key)) return @unionInit(T, field.name, try innerParse(field.type, scanner, gpa, options)); - } + if (union_info.tag_type) |tag_type| { + const token = try scanner.next(); + if (token != .@"union") return error.UnexpectedToken; + const union_tag = try innerParse(@typeInfo(tag_type).@"enum".tag_type, scanner, gpa, options); + inline for (union_info.fields) |field| { + if (std.mem.eql(u8, field.name, @tagName(@as(tag_type, @enumFromInt(union_tag))))) return @unionInit(T, field.name, try innerParse(field.type, scanner, gpa, options)); + } + } else @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); return error.InvalidUnion; }, From 7dda943b714ebf055707d8eeb20febeffe2c2b25 Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 23:31:06 +0000 Subject: [PATCH 23/30] Properly serialize vectors of floats --- zig/src/serializer.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig index aca076d..5753262 100644 --- a/zig/src/serializer.zig +++ b/zig/src/serializer.zig @@ -86,7 +86,7 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { try writeVectorMarker(value, writer); try writer.writeByte(getFloatMarker(float)); for (value) |val| { - try writeInt(std.math.ByteAlignedInt(pointer_info.child), val, writer); + try writeFloat(std.math.ByteAlignedInt(pointer_info.child), val, writer); } }, .bool => { From 4fe8d13c9fa00b78830b46cfb3435aeac7b58f55 Mon Sep 17 00:00:00 2001 From: DidaS Date: Thu, 29 Jan 2026 23:35:54 +0000 Subject: [PATCH 24/30] Helper serializer --- zig/src/root.zig | 1 + zig/src/serializer.zig | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/zig/src/root.zig b/zig/src/root.zig index 4872210..eadaf9f 100644 --- a/zig/src/root.zig +++ b/zig/src/root.zig @@ -7,3 +7,4 @@ pub const innerParse = static.innerParse; pub const Scanner = @import("./Scanner.zig"); pub const serialize = @import("./serializer.zig").serialize; +pub const serializeFromValue = @import("./serializer.zig").serializeFromValue; diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig index 5753262..7646a13 100644 --- a/zig/src/serializer.zig +++ b/zig/src/serializer.zig @@ -16,6 +16,12 @@ pub fn serializeComptime(comptime T: type, value: T) []const u8 { } } +pub fn serializeFromValue(gpa: std.mem.Allocator, value: anytype) ![]const u8 { + var w: std.Io.Writer.Allocating = .init(gpa); + try serialize(@TypeOf(value), value, &w.writer); + return try w.toOwnedSlice(); +} + pub fn serialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { // TODO: Use proper identifier try writer.writeByte(HBP_VERSION); From 5021477e1ca77e0ce3d17cf38414081bb39b5d01 Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 30 Jan 2026 00:13:53 +0000 Subject: [PATCH 25/30] Add support for packed structs (as bitfields) --- zig/src/main.zig | 1 + zig/src/serializer.zig | 6 +++++- zig/src/static.zig | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/zig/src/main.zig b/zig/src/main.zig index 9659ae7..5c7f8b9 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -69,6 +69,7 @@ pub fn main() !void { print([]u8, gpa, comptime Serializer.serializeComptime([]u8, @constCast(@as([]const u8, &.{ 10, 60, 134 })))); const t = struct { x: u32 }; print([]t, gpa, comptime Serializer.serializeComptime([]t, @constCast(@as([]const t, &.{ .{ .x = 10 }, .{ .x = 60 }, .{ .x = 134 } })))); + print(packed struct(u64) { t: bool, z: bool, _: u62 }, gpa, comptime Serializer.serializeComptime(packed struct(u64) { t: bool, z: bool, _: u62 = 0 }, .{ .t = false, .z = true })); } fn print(comptime T: type, gpa: std.mem.Allocator, payload: []const u8) void { diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig index 7646a13..801c8c8 100644 --- a/zig/src/serializer.zig +++ b/zig/src/serializer.zig @@ -142,7 +142,11 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { } else @compileError("Unable to parse non tagged union '" ++ @typeName(T) ++ "'"); }, .@"struct" => |struct_info| { - if (struct_info.is_tuple) { + // Packed structs are encoded as their backing integer and treated as bitfields + // TODO: Maybe it could be a good idea to have a Meta type to indicate bitfields + if (struct_info.layout == .@"packed") { + try innerSerialize(struct_info.backing_integer.?, @bitCast(value), writer); + } else if (struct_info.is_tuple) { if (struct_info.fields.len <= 15) { try writer.writeByte(@intFromEnum(MarkerType.empty_tuple) + struct_info.fields.len); } else if (struct_info.fields.len <= std.math.maxInt(u8)) { diff --git a/zig/src/static.zig b/zig/src/static.zig index 00677e4..6a749e4 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -199,6 +199,12 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, c } }, .@"struct" => |struct_info| { + if (struct_info.layout == .@"packed") { + const token = try scanner.peekNextTokenType(); + if (token != .int) return error.UnexpectedToken; + return @bitCast(try innerParse(struct_info.backing_integer.?, scanner, gpa, options)); + } + if (struct_info.is_tuple) { const token = try scanner.next(); if (token != .tuple) return error.UnexpectedToken; From dc6ce81030998bf1059d2e816155b5130b01ba5f Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 30 Jan 2026 07:00:19 +0000 Subject: [PATCH 26/30] Array support This is very dirty and lacks support for parsing vectors as arrays, only intended as a POC --- zig/src/main.zig | 2 + zig/src/serializer.zig | 87 +++++++++++++++++++++++------------------- zig/src/static.zig | 52 +++++++++++++++++++++++-- 3 files changed, 97 insertions(+), 44 deletions(-) diff --git a/zig/src/main.zig b/zig/src/main.zig index 5c7f8b9..a6ff5ba 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -70,6 +70,8 @@ pub fn main() !void { const t = struct { x: u32 }; print([]t, gpa, comptime Serializer.serializeComptime([]t, @constCast(@as([]const t, &.{ .{ .x = 10 }, .{ .x = 60 }, .{ .x = 134 } })))); print(packed struct(u64) { t: bool, z: bool, _: u62 }, gpa, comptime Serializer.serializeComptime(packed struct(u64) { t: bool, z: bool, _: u62 = 0 }, .{ .t = false, .z = true })); + print([3]t, gpa, comptime Serializer.serializeComptime([3]t, .{ .{ .x = 10 }, .{ .x = 60 }, .{ .x = 134 } })); + print([10]?t, gpa, comptime Serializer.serializeComptime([3]?t, .{ .{ .x = 10 }, .{ .x = 60 }, .{ .x = 134 } })); } fn print(comptime T: type, gpa: std.mem.Allocator, payload: []const u8) void { diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig index 801c8c8..dc4f879 100644 --- a/zig/src/serializer.zig +++ b/zig/src/serializer.zig @@ -80,51 +80,15 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { try writer.writeAll(value); } else { - switch (@typeInfo(pointer_info.child)) { - .int => |int| { - try writeVectorMarker(value, writer); - try writer.writeByte(getIntMarker(int)); - for (value) |val| { - try writeInt(std.math.ByteAlignedInt(pointer_info.child), val, writer); - } - }, - .float => |float| { - try writeVectorMarker(value, writer); - try writer.writeByte(getFloatMarker(float)); - for (value) |val| { - try writeFloat(std.math.ByteAlignedInt(pointer_info.child), val, writer); - } - }, - .bool => { - try writeVectorMarker(value, writer); - for (value) |val| { - try writer.writeByte(0x01 + @as(u8, @intFromBool(val))); - } - }, - else => { - if (value.len <= 15) { - try writer.writeByte(@intFromEnum(MarkerType.empty_tuple) + @as(u8, @intCast(value.len))); - } else if (value.len <= std.math.maxInt(u8)) { - try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_1)); - try writeInt(u8, @intCast(value.len), writer); - } else if (value.len <= std.math.maxInt(u16)) { - try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_2)); - try writeInt(u16, @intCast(value.len), writer); - } else if (value.len <= std.math.maxInt(u32)) { - try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_4)); - try writeInt(u32, @intCast(value.len), writer); - } - - for (value) |val| { - try innerSerialize(pointer_info.child, val, writer); - } - }, - } + try parseArrayOrSlice(pointer_info.child, value, writer); } }, else => @compileError("Unsupported pointer type"), } }, + .array => |array_info| { + try parseArrayOrSlice(array_info.child, &value, writer); + }, .@"enum" => |enum_info| { std.debug.assert(@typeInfo(enum_info.tag_type).int.signedness == .unsigned); try writer.writeByte(@intFromEnum(MarkerType.@"enum")); @@ -185,6 +149,49 @@ fn innerSerialize(comptime T: type, value: T, writer: *std.Io.Writer) !void { } } +fn parseArrayOrSlice(comptime T: type, value: []const T, writer: *std.Io.Writer) !void { + switch (@typeInfo(T)) { + .int => |int| { + try writeVectorMarker(value, writer); + try writer.writeByte(getIntMarker(int)); + for (value) |val| { + try writeInt(std.math.ByteAlignedInt(T), val, writer); + } + }, + .float => |float| { + try writeVectorMarker(value, writer); + try writer.writeByte(getFloatMarker(float)); + for (value) |val| { + try writeFloat(std.math.ByteAlignedInt(T), val, writer); + } + }, + .bool => { + try writeVectorMarker(value, writer); + for (value) |val| { + try writer.writeByte(0x01 + @as(u8, @intFromBool(val))); + } + }, + else => { + if (value.len <= 15) { + try writer.writeByte(@intFromEnum(MarkerType.empty_tuple) + @as(u8, @intCast(value.len))); + } else if (value.len <= std.math.maxInt(u8)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_1)); + try writeInt(u8, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u16)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_2)); + try writeInt(u16, @intCast(value.len), writer); + } else if (value.len <= std.math.maxInt(u32)) { + try writer.writeByte(@intFromEnum(MarkerType.arbitrary_tuple_4)); + try writeInt(u32, @intCast(value.len), writer); + } + + for (value) |val| { + try innerSerialize(T, val, writer); + } + }, + } +} + fn writeInt(comptime T: type, value: T, writer: *std.Io.Writer) !void { try writer.writeAll(&@as([@divExact(@typeInfo(T).int.bits, 8)]u8, @bitCast(std.mem.nativeToLittle(T, @intCast(value))))); } diff --git a/zig/src/static.zig b/zig/src/static.zig index 6a749e4..adf9ade 100644 --- a/zig/src/static.zig +++ b/zig/src/static.zig @@ -4,15 +4,22 @@ const Scanner = @import("Scanner.zig"); const assert = std.debug.assert; pub const ParseOptions = struct { - /// Allow parsing `i8` as `u8` and vice-versa + /// Wether arrays should be allowed to be partially filled. + /// + /// This only affects arrays with optionals meaning + /// `[50]?u8` can be `[30]u8 ++ [20]null` + /// + /// The default behavior (false) checks if the HBP payload has the exact same length as the array. + allow_empty_array_elements: bool = false, + /// Allow parsing `i8` as `u8` and vice-versa. ignore_integer_signedness: bool = false, - /// Use `std.enums.fromInt` instead of attempting to cast + /// Use `std.enums.fromInt` instead of attempting to cast. safe_enum_parsing: bool = false, float_behavior: enum(u1) { widen, preserve, } = .preserve, - /// Wether to try parsing types that do not start with `0xF0` (optional marker) + /// Wether to try parsing types that do not start with `0xF0` (optional marker). non_typed_optionals: enum(u1) { @"error", allow, @@ -120,6 +127,28 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, c return error.InvalidUnion; }, + .array => |array_info| { + switch (@typeInfo(array_info.child)) { + else => |t| { + const token = try scanner.next(); + if (token != .tuple) return error.UnexpectedToken; + + if (array_info.len != token.tuple) { + if (!options.allow_empty_array_elements) + return error.InvalidArrayComponent + else if (t != .optional) + return error.PartialOptionalsOnly; + } + var arr: [array_info.len]array_info.child = if (t == .optional) @splat(null) else undefined; + + for (0..token.tuple) |i| { + arr[i] = try innerParse(array_info.child, scanner, gpa, options); + } + + return arr; + }, + } + }, .pointer => |pointer_info| { switch (pointer_info.size) { .slice => { @@ -163,7 +192,22 @@ pub fn innerParse(comptime T: type, scanner: *Scanner, gpa: std.mem.Allocator, c .float => { const token = try scanner.next(); if (token != .vector) return error.UnexpectedToken; - var arr: std.ArrayList(pointer_info.child) = .empty; + + const first = try scanner.next(); + if (first != .float) return error.UnexpectedToken; + + const element_byte_length = first.float.view.len; + const N = alignIntegerType(pointer_info.child); + var arr: std.ArrayList(N) = try .initCapacity(gpa, 1); + + try arr.append(gpa, sliceToInt(N, first.float.view)); + + // The first element is already retrieved + for (1..token.vector) |_| { + const value_start = scanner.cursor; + scanner.cursor += element_byte_length; + try arr.append(gpa, sliceToInt(N, scanner.input[value_start..scanner.cursor])); + } return try arr.toOwnedSlice(gpa); }, .bool => { From c63ca6e022a1938416a96b637ae3e94496912c71 Mon Sep 17 00:00:00 2001 From: DidaS Date: Wed, 4 Mar 2026 06:45:07 +0000 Subject: [PATCH 27/30] Minor readme changes --- README.md | 40 +++++++++++++--------------------------- representation.png | Bin 80349 -> 0 bytes zig/src/Scanner.zig | 17 +++++++++++++---- 3 files changed, 26 insertions(+), 31 deletions(-) delete mode 100644 representation.png diff --git a/README.md b/README.md index 2af3575..60eaeca 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,6 @@ It consists of a set of primitive types, composite types, and meta types that to Every serialized HBP value begins with the HBP version used to encode it followed by a marker that represents the type of the data. -![representation image](representation.png) - # Identifier The HBP identifier is the byte at the beginning of every payload that tells you the version of the protocol and allows developers to pass custom flags. @@ -108,6 +106,9 @@ Arbitrary sized integers are followed by `2` bytes designating their bit-width, ### Floats +> [!CAUTION] +> Not all float formats are implemented yet, they are present on the spec for future proofing. + | Marker | Data Size (bytes) | Type | | :----: | :---------------: | :-----------------------------------------------------------------------------------------------------------: | | `30` | 2 | [IEEE 754 Half precision float](https://en.wikipedia.org/wiki/Half-precision_floating-point_format) | @@ -122,6 +123,9 @@ Arbitrary sized integers are followed by `2` bytes designating their bit-width, ### Decimals +> [!CAUTION] +> Decimals are not yet supported, progress can be tracked [here](https://github.com/ziglang/zig/issues/4221). + | Marker | Data Size (bytes) | Type | | :----: | :---------------: | :-----------------------------------------------------------------------------------: | | `3A` | 4 | [IEEE 754 Decimal32](https://en.wikipedia.org/wiki/Decimal32_floating-point_format) | @@ -154,6 +158,9 @@ The union marker is **always** followed by an integer marker indicating the acti ## Error +> [!WARNING] +> Error meta types are not yet supported by the zig implementation + Marker: `FF` Example: @@ -170,7 +177,7 @@ Serialized: 01 FF 6B 54 68 69 73 20 46 61 69 6C 65 64 Strings are `UTF-8` encoded arrays of bytes. -> Why aren't strings a meta type on top of Vector? There was a long discussion about this that i need to append here. . . +> Why aren't strings a meta type on top of Vector? There was a long discussion about this that needs to be appended here. . . Small strings: @@ -302,6 +309,9 @@ The encoding of a dictionary is as follows: ## Map +> [!CAUTION] +> Maps are not yet implemented. + | Marker | Extra bytes | Maximum Size | | :----: | :---------: | :-----------: | | `D3` | 1 | 255 | @@ -309,27 +319,3 @@ The encoding of a dictionary is as follows: | `D5` | 4 | 4_294_967_295 | A map is just like a dictionary but instead, the keys can be of any type. - -# Cheat Sheet - -| Marker | Name | Type | -| :-----: | :--------------------------------------: | :--------------------------------: | -| `00` | [`null`](#null) | [Primitive](#primitive-data-types) | -| `01` | [`false`](#bool) | [Primitive](#primitive-data-types) | -| `02` | [`true`](#bool) | [Primitive](#primitive-data-types) | -| `10-16` | [`signed integer`](#signed-integers) | [Primitive](#primitive-data-types) | -| `1F` | [`signed integer`](#signed-integers) | [Primitive](#primitive-data-types) | -| `20-26` | [`unsigned integer`](#unsigned-integers) | [Primitive](#primitive-data-types) | -| `2F` | [`unsigned integer`](#unsigned-integers) | [Primitive](#primitive-data-types) | -| `30-37` | [`float`](#floats) | [Primitive](#primitive-data-types) | -| `3A-3C` | [`decimal`](#floats) | [Primitive](#primitive-data-types) | -| `3F` | [`bfloat16`](#floats) | [Primitive](#primitive-data-types) | -| `70-7F` | [`tuple`](#tuple) | [Composite](#composite-data-types) | -| `80-8F` | [`vector`](#vector) | [Composite](#composite-data-types) | -| `D0-D2` | [`dictionary`](#dictionary) | [Composite](#composite-data-types) | -| `D3-D5` | [`map`](#map) | [Composite](#composite-data-types) | -| `DA-DC` | [`tuple`](#tuple) | [Composite](#composite-data-types) | -| `DD-DF` | [`vector`](#vector) | [Composite](#composite-data-types) | -| `E0` | [`optional`](#optional) | [Meta](#meta-data-types) | -| `E1` | [`enum`](#enum) | [Meta](#meta-data-types) | -| `FF` | [`error`](#error) | [Meta](#meta-data-types) | \ No newline at end of file diff --git a/representation.png b/representation.png deleted file mode 100644 index 036080027f8dcc6d0e045eeebdc25da9a7f395da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80349 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

V~Xc2-&3ck5?YbAZ{} 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 diff --git a/zig/src/Scanner.zig b/zig/src/Scanner.zig index 9be7e9e..c69e03f 100644 --- a/zig/src/Scanner.zig +++ b/zig/src/Scanner.zig @@ -193,13 +193,11 @@ pub fn next(self: *Scanner) !Token { self.state = .post_value; return .{ .float = .{ .bits = 16, .view = self.input[self.cursor - 2 .. self.cursor] } }; }, - // .minifloat, .single_float => { self.cursor += 5; self.state = .post_value; return .{ .float = .{ .bits = 32, .view = self.input[self.cursor - 4 .. self.cursor] } }; }, - // .extended_float_40, .double_float => { self.cursor += 9; self.state = .post_value; @@ -215,8 +213,19 @@ pub fn next(self: *Scanner) !Token { self.state = .post_value; return .{ .float = .{ .bits = 128, .view = self.input[self.cursor - 16 .. self.cursor] } }; }, - // .octuple_float, - // .brain_float, + .minifloat, + .extended_float_40, + .octuple_float, + .brain_float, + => { + return error.FloatFormatNotImplemented; + }, + .decimal_32, + .decimal_64, + .decimal_128, + => { + return error.DecimalsNotImplemented; + }, .empty_string, .string_1, .string_2, From 09c1584c49856748ffc7c70b7531b9caa1969fb1 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Thu, 12 Mar 2026 09:30:46 +0100 Subject: [PATCH 28/30] chore: update TOC I used marksman to generate it --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 60eaeca..5769a23 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ It consists of a set of primitive types, composite types, and meta types that to # Table of Contents + - [About](#about) - [Table of Contents](#table-of-contents) - [Representation](#representation) @@ -24,13 +25,15 @@ It consists of a set of primitive types, composite types, and meta types that to - [Meta Data Types](#meta-data-types) - [Optional](#optional) - [Enum](#enum) + - [Union](#union) - [Error](#error) - [Composite Data Types](#composite-data-types) + - [Strings](#strings) - [Tuple](#tuple) - [Vector](#vector) - [Dictionary](#dictionary) - [Map](#map) -- [Cheat Sheet](#cheat-sheet) + # Representation From b623562a3238a955302bf6c9222a9b7cfbd4fc3b Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 24 Apr 2026 14:18:59 +0100 Subject: [PATCH 29/30] Zig 0.16 --- zig/build.zig.zon | 2 +- zig/src/main.zig | 4 ++-- zig/src/serializer.zig | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zig/build.zig.zon b/zig/build.zig.zon index 1125095..4de03bf 100644 --- a/zig/build.zig.zon +++ b/zig/build.zig.zon @@ -2,7 +2,7 @@ .fingerprint = 0x8af3a6b7cf3baeb7, .name = .libhbp, .version = "0.1.0", - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.16.0", .paths = .{ "build.zig", "build.zig.zon", diff --git a/zig/src/main.zig b/zig/src/main.zig index a6ff5ba..f9f758b 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -42,7 +42,7 @@ const test_float = [_][]const u8{ }; pub fn main() !void { - var allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; + var allocator: std.heap.DebugAllocator(.{}) = .init; const gpa = allocator.allocator(); for (test_bool) |in| { @@ -105,5 +105,5 @@ fn readableOutput(buffer: []u8, input: []const u8) []const u8 { } test { - std.testing.refAllDeclsRecursive(@This()); + std.testing.refAllDecls(@This()); } diff --git a/zig/src/serializer.zig b/zig/src/serializer.zig index dc4f879..3aa94b3 100644 --- a/zig/src/serializer.zig +++ b/zig/src/serializer.zig @@ -3,7 +3,7 @@ const MarkerType = @import("./Scanner.zig").MarkerType; const HBP_VERSION = 1; -pub fn serializeComptime(comptime T: type, value: T) []const u8 { +pub inline fn serializeComptime(comptime T: type, comptime value: T) []const u8 { comptime { var buf: [512]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); From a445fa846e99d2f84ea6012613e3844856f2cb9b Mon Sep 17 00:00:00 2001 From: DidaS Date: Fri, 24 Apr 2026 14:30:56 +0100 Subject: [PATCH 30/30] Update tests to reflect byte order change (Little Endian) --- zig/src/scanner_test.zig | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/zig/src/scanner_test.zig b/zig/src/scanner_test.zig index 2808b57..2494880 100644 --- a/zig/src/scanner_test.zig +++ b/zig/src/scanner_test.zig @@ -50,20 +50,20 @@ test "int" { }; const test_output = [_]Token{ Token{ .int = .{ .signedness = .signed, .view = &.{0x2D} } }, - Token{ .int = .{ .signedness = .signed, .view = &.{ 0x18, 0xCB } } }, - Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x8B, 0x36, 0x60 } } }, - Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF } } }, - Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8A, 0xC7, 0x23, 0x04, 0x89, 0xE7, 0xFF, 0xFF } } }, - Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 } } }, - Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0xCB, 0x18 } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0x60, 0x36, 0x8B, 0x00 } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0xFF, 0xC9, 0x9A, 0x3B, 0x01, 0x00, 0x00, 0x00 } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0xFF, 0xFF, 0xE7, 0x89, 0x04, 0x23, 0xC7, 0x8A, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0x00, 0x80, 0xC6, 0xA4, 0x7E, 0x8D, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, + Token{ .int = .{ .signedness = .signed, .view = &.{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x94, 0x29, 0x59, 0xEF, 0xD3, 0xB2, 0xC3, 0x44, 0x70, 0xE9, 0xC5, 0x6A, 0x70, 0xC2, 0x95, 0xD6, 0x75, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, Token{ .int = .{ .signedness = .unsigned, .view = &.{0xFA} } }, - Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x18, 0xCB } } }, - Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x8B, 0x36, 0x60 } } }, - Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x00, 0x00, 0x01, 0x3B, 0x9A, 0xC9, 0xFF } } }, - Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8A, 0xC7, 0x23, 0x04, 0x89, 0xE7, 0xFF, 0xFF } } }, - Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00 } } }, - Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x75, 0xD6, 0x95, 0xC2, 0x70, 0x6A, 0xC5, 0xE9, 0x70, 0x44, 0xC3, 0xB2, 0xD3, 0xEF, 0x59, 0x29, 0x94, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0xCB, 0x18 } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x60, 0x36, 0x8B, 0x00 } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0xFF, 0xC9, 0x9A, 0x3B, 0x01, 0x00, 0x00, 0x00 } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0xFF, 0xFF, 0xE7, 0x89, 0x04, 0x23, 0xC7, 0x8A, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0x00, 0x80, 0xC6, 0xA4, 0x7E, 0x8D, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, + Token{ .int = .{ .signedness = .unsigned, .view = &.{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x94, 0x29, 0x59, 0xEF, 0xD3, 0xB2, 0xC3, 0x44, 0x70, 0xE9, 0xC5, 0x6A, 0x70, 0xC2, 0x95, 0xD6, 0x75, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } }, }; for (test_input, 0..) |in, i| { @@ -95,6 +95,10 @@ fn expectNext(scanner: *Scanner, expected_token: Scanner.Token) !void { .tuple, .optional, .@"enum", + .string, + .@"struct", + .@"union", + .vector, .eos, => {}, }