From eee35af340245477d66d3950b1d4e59ee49efb5f Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Thu, 28 May 2026 00:19:20 +0530 Subject: [PATCH 1/2] add ssz clone --- src/lib.zig | 192 +++++++++++++++++++++++++++++++++ src/tests.zig | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.zig | 17 +++ 3 files changed, 498 insertions(+) diff --git a/src/lib.zig b/src/lib.zig index 6146ce1..66136cb 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -663,6 +663,198 @@ pub fn deserialize(T: type, serialized: []const u8, out: *T, allocator: ?Allocat } } +/// Returns true when `T` can be cloned with a direct value copy. +/// Types with custom clone functions return false so their functions are honored. +/// Unsupported type kinds return false conservatively. +pub fn hasNoPointers(T: type) bool { + if (comptime std.meta.hasFn(T, "sszClone")) return false; + + return switch (@typeInfo(T)) { + .int, .bool, .float, .void, .null, .@"enum" => true, + .array => |array| hasNoPointers(array.child), + .vector => |vector| hasNoPointers(vector.child), + .optional => |optional| hasNoPointers(optional.child), + .@"struct" => |str| blk: { + inline for (str.fields) |field| { + if (field.is_comptime) continue; + if (!hasNoPointers(field.type)) break :blk false; + } + break :blk true; + }, + .@"union" => |un| blk: { + if (un.tag_type == null) break :blk false; + inline for (un.fields) |field| { + if (!hasNoPointers(field.type)) break :blk false; + } + break :blk true; + }, + else => false, + }; +} + +fn cleanupClone(T: type, data: T, allocator: Allocator) void { + if (comptime std.meta.hasFn(T, "deinit")) { + var clone_data = data; + clone_data.deinit(); + return; + } + if (comptime hasNoPointers(T)) return; + + switch (@typeInfo(T)) { + .int, .bool, .float, .null, .@"enum" => {}, + .array => |array| { + if (comptime !hasNoPointers(array.child)) { + for (data) |item| cleanupClone(array.child, item, allocator); + } + }, + .optional => |optional| { + if (data) |value| cleanupClone(optional.child, value, allocator); + }, + .pointer => |ptr| switch (ptr.size) { + .slice => { + if (comptime ptr.is_const and @sizeOf(ptr.child) == 1) return; + if (comptime !hasNoPointers(ptr.child)) { + for (data) |item| cleanupClone(ptr.child, item, allocator); + } + allocator.free(data); + }, + .one => { + cleanupClone(ptr.child, data.*, allocator); + const alignment = comptime ptr.alignment orelse @alignOf(ptr.child); + if (comptime alignment == @alignOf(ptr.child)) { + allocator.destroy(@constCast(data)); + } else { + const slice: []align(alignment) ptr.child = @as([*]align(alignment) ptr.child, @ptrCast(@constCast(data)))[0..1]; + allocator.free(slice); + } + }, + else => @compileError("clone cleanup does not support " ++ @tagName(ptr.size) ++ " pointers"), + }, + .@"struct" => |str| { + inline for (str.fields) |field| { + if (field.is_comptime) continue; + cleanupClone(field.type, @field(data, field.name), allocator); + } + }, + .@"union" => |un| { + if (un.tag_type == null) @compileError("clone cleanup does not support untagged unions"); + switch (data) { + inline else => |payload| cleanupClone(@TypeOf(payload), payload, allocator), + } + }, + else => {}, + } +} + +/// Clones `data` into `cloned`. +/// Cloned values follow the same cleanup model as normally constructed SSZ +/// values: containers use `deinit`, allocated slices use `allocator.free`, and +/// borrowed `[]const u8` data remains borrowed. Types providing `sszClone` +/// must also provide `deinit`; partial-failure unwinds rely on `deinit` to +/// release whatever the hook allocated. +pub fn clone(T: type, data: T, cloned: *T, allocator: ?Allocator) !void { + if (comptime std.meta.hasFn(T, "sszClone")) { + if (comptime !std.meta.hasFn(T, "deinit")) { + @compileError("type " ++ @typeName(T) ++ " provides sszClone but no deinit; cleanup on partial clone failure would be undefined"); + } + return data.sszClone(cloned, allocator); + } + if (comptime hasNoPointers(T)) { + cloned.* = data; + return; + } + + switch (@typeInfo(T)) { + .int, .bool, .float, .void, .null, .@"enum" => cloned.* = data, + .array => |array| { + var done: usize = 0; + errdefer if (allocator) |alloc| { + for (cloned.*[0..done]) |item| cleanupClone(array.child, item, alloc); + }; + while (done < data.len) : (done += 1) { + try clone(array.child, data[done], &cloned.*[done], allocator); + } + }, + .optional => |optional| { + if (data == null) { + cloned.* = null; + return; + } + var child_clone: optional.child = undefined; + try clone(optional.child, data.?, &child_clone, allocator); + cloned.* = child_clone; + }, + .pointer => |ptr| switch (ptr.size) { + .slice => { + if (comptime ptr.is_const and @sizeOf(ptr.child) == 1) { + cloned.* = data; + return; + } + const alloc = allocator orelse return error.AllocatorRequired; + const alignment: std.mem.Alignment = comptime .fromByteUnits(ptr.alignment orelse @alignOf(ptr.child)); + const sentinel = comptime ptr.sentinel(); + const out = try alloc.allocWithOptions(ptr.child, data.len, alignment, sentinel); + errdefer alloc.free(out); + if (comptime hasNoPointers(ptr.child)) { + @memcpy(out, data); + } else { + var done: usize = 0; + errdefer for (out[0..done]) |item| cleanupClone(ptr.child, item, alloc); + while (done < data.len) : (done += 1) { + try clone(ptr.child, data[done], &out[done], allocator); + } + } + cloned.* = out; + }, + .one => { + const alloc = allocator orelse return error.AllocatorRequired; + const alignment = comptime ptr.alignment orelse @alignOf(ptr.child); + if (comptime alignment == @alignOf(ptr.child)) { + const slot = try alloc.create(ptr.child); + errdefer alloc.destroy(slot); + try clone(ptr.child, data.*, slot, allocator); + cloned.* = slot; + } else { + const alloc_alignment: std.mem.Alignment = comptime .fromByteUnits(alignment); + const slot = try alloc.allocWithOptions(ptr.child, 1, alloc_alignment, null); + errdefer alloc.free(slot); + try clone(ptr.child, data.*, &slot[0], allocator); + cloned.* = &slot[0]; + } + }, + else => return error.UnSupportedPointerType, + }, + .@"struct" => |str| { + var fields_done: usize = 0; + errdefer if (allocator) |alloc| { + var seen: usize = 0; + inline for (str.fields) |field| { + if (field.is_comptime) continue; + if (seen >= fields_done) break; + cleanupClone(field.type, @field(cloned.*, field.name), alloc); + seen += 1; + } + }; + inline for (str.fields) |field| { + if (field.is_comptime) continue; + try clone(field.type, @field(data, field.name), &@field(cloned.*, field.name), allocator); + fields_done += 1; + } + }, + .@"union" => |un| { + if (un.tag_type == null) @compileError("clone does not support untagged unions"); + switch (data) { + inline else => |payload, tag| { + var payload_clone: @TypeOf(payload) = undefined; + try clone(@TypeOf(payload), payload, &payload_clone, allocator); + cloned.* = @unionInit(T, @tagName(tag), payload_clone); + }, + } + }, + else => return error.UnknownType, + } +} + pub fn mixInLength2(Hasher: type, root: [Hasher.digest_length]u8, length: usize, out: *[Hasher.digest_length]u8) void { var hasher = Hasher.init(Hasher.Options{}); hasher.update(root[0..]); diff --git a/src/tests.zig b/src/tests.zig index a19a63b..cea2ece 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -14,6 +14,8 @@ const Sha256 = std.crypto.hash.sha2.Sha256; const zeros = @import("zeros.zig"); const hashes_of_zero = zeros.hashes_of_zero; const Allocator = std.mem.Allocator; +const clone = libssz.clone; +const hasNoPointers = libssz.hasNoPointers; test "serializes uint8" { const data: u8 = 0x55; @@ -2574,6 +2576,293 @@ test "utils.List dynamic-item: offsets decrease" { try expectError(error.OffsetOrdering, L.sszDecode(&buf, &out, std.testing.allocator)); } +test "clone: pointer-free value works without allocator" { + const S = struct { + n: u32, + bytes: [4]u8, + tag: enum { a, b }, + }; + const data = S{ .n = 42, .bytes = .{ 1, 2, 3, 4 }, .tag = .b }; + var cloned: S = undefined; + + try clone(S, data, &cloned, null); + + try expect(hasNoPointers(S)); + try expect(std.meta.eql(data, cloned)); +} + +test "clone: const byte slice is borrowed without allocator" { + const data: []const u8 = "hello"; + var cloned: []const u8 = undefined; + + try clone([]const u8, data, &cloned, null); + + try expect(std.mem.eql(u8, cloned, data)); + try expect(cloned.ptr == data.ptr); +} + +test "clone: mutable slice requires allocator" { + const data = try std.testing.allocator.dupe(u8, "hello"); + defer std.testing.allocator.free(data); + var cloned: []u8 = undefined; + + try expectError(error.AllocatorRequired, clone([]u8, data, &cloned, null)); +} + +test "clone: mutable slice copy is independent" { + const data = try std.testing.allocator.dupe(u8, "hello"); + defer std.testing.allocator.free(data); + var cloned: []u8 = undefined; + + try clone([]u8, data, &cloned, std.testing.allocator); + defer std.testing.allocator.free(cloned); + + cloned[0] = 'H'; + try expect(data[0] == 'h'); + try expect(std.mem.eql(u8, cloned, "Hello")); +} + +test "clone: struct follows normal slice cleanup ownership" { + const S = struct { + a: []u8, + b: []const u8, + }; + const a = try std.testing.allocator.dupe(u8, "left"); + defer std.testing.allocator.free(a); + const data = S{ .a = a, .b = "right" }; + var cloned: S = undefined; + + try clone(S, data, &cloned, std.testing.allocator); + defer std.testing.allocator.free(cloned.a); + + try expect(std.mem.eql(u8, cloned.a, data.a)); + try expect(std.mem.eql(u8, cloned.b, data.b)); + try expect(cloned.a.ptr != data.a.ptr); + try expect(cloned.b.ptr == data.b.ptr); +} + +test "clone: single pointer is deep-copied" { + var value: u32 = 123; + const data: *u32 = &value; + var cloned: *u32 = undefined; + + try clone(*u32, data, &cloned, std.testing.allocator); + defer std.testing.allocator.destroy(cloned); + + try expect(cloned != data); + try expect(cloned.* == data.*); + cloned.* = 456; + try expect(value == 123); +} + +test "clone: const single pointer is deep-copied" { + const value: u32 = 123; + const data: *const u32 = &value; + var cloned: *const u32 = undefined; + + try clone(*const u32, data, &cloned, std.testing.allocator); + defer std.testing.allocator.destroy(@constCast(cloned)); + + try expect(cloned != data); + try expect(cloned.* == data.*); +} + +test "clone: over-aligned single pointer preserves alignment" { + const data = try std.testing.allocator.alignedAlloc(u32, .fromByteUnits(16), 1); + defer std.testing.allocator.free(data); + data[0] = 0xcafebabe; + const ptr: *align(16) u32 = &data[0]; + var cloned: *align(16) u32 = undefined; + + try clone(*align(16) u32, ptr, &cloned, std.testing.allocator); + defer { + const cloned_slice: []align(16) u32 = @as([*]align(16) u32, @ptrCast(cloned))[0..1]; + std.testing.allocator.free(cloned_slice); + } + + try expect(cloned != ptr); + try expect(cloned.* == ptr.*); + try expect(@intFromPtr(cloned) % 16 == 0); +} + +test "clone: optional pointer" { + var value: u32 = 99; + const some: ?*u32 = &value; + var cloned_some: ?*u32 = undefined; + var cloned_none: ?*u32 = undefined; + + try clone(?*u32, some, &cloned_some, std.testing.allocator); + defer if (cloned_some) |ptr| std.testing.allocator.destroy(ptr); + try clone(?*u32, null, &cloned_none, null); + + try expect(cloned_some.?.* == 99); + try expect(cloned_some.? != some.?); + try expect(cloned_none == null); +} + +test "clone: sentinel-terminated slice preserves sentinel" { + const data = try std.testing.allocator.dupeZ(u8, "hello"); + defer std.testing.allocator.free(data); + var cloned: [:0]u8 = undefined; + + try clone([:0]u8, data, &cloned, std.testing.allocator); + defer std.testing.allocator.free(cloned); + + try expect(std.mem.eql(u8, cloned, data)); + try expect(cloned.ptr != data.ptr); + try expect(cloned[cloned.len] == 0); +} + +test "clone: utils.List uses clone hook" { + const L = utils.List([]const u8, 4); + var data = try L.init(std.testing.allocator); + defer data.deinit(); + try data.append("aa"); + try data.append("bb"); + var cloned: L = undefined; + + try clone(L, data, &cloned, std.testing.allocator); + defer cloned.deinit(); + + try expect(cloned.len() == data.len()); + try expect(cloned.inner.items.ptr != data.inner.items.ptr); + try expect(std.mem.eql(u8, (try cloned.get(0)), "aa")); + try expect((try cloned.get(0)).ptr == (try data.get(0)).ptr); +} + +test "clone: utils.Bitlist uses clone hook" { + const B = utils.Bitlist(32); + var data = try B.init(std.testing.allocator); + defer data.deinit(); + try data.append(true); + try data.append(false); + try data.append(true); + var cloned: B = undefined; + + try clone(B, data, &cloned, std.testing.allocator); + defer cloned.deinit(); + + try expect(cloned.eql(&data)); + try cloned.set(0, false); + try expect((try data.get(0)) == true); +} + +test "clone: struct unwind frees earlier fields when later allocation fails" { + const S = struct { + a: []u8, + b: []u8, + }; + const a = try std.testing.allocator.dupe(u8, "first"); + defer std.testing.allocator.free(a); + const b = try std.testing.allocator.dupe(u8, "second"); + defer std.testing.allocator.free(b); + const data = S{ .a = a, .b = b }; + var cloned: S = undefined; + + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 }); + try expectError(error.OutOfMemory, clone(S, data, &cloned, failing.allocator())); +} + +test "clone: array of const byte slices keeps borrowed items" { + const data: [3][]const u8 = .{ "one", "two", "three" }; + var cloned: [3][]const u8 = undefined; + + try clone([3][]const u8, data, &cloned, null); + + inline for (0..3) |i| { + try expect(std.mem.eql(u8, cloned[i], data[i])); + try expect(cloned[i].ptr == data[i].ptr); + } +} + +test "clone: tagged union active variant follows slice ownership" { + const Payload = union(enum) { + bytes: []const u8, + n: u32, + }; + + var cloned_bytes: Payload = undefined; + try clone(Payload, Payload{ .bytes = "abc" }, &cloned_bytes, std.testing.allocator); + try expect(std.mem.eql(u8, cloned_bytes.bytes, "abc")); + try expect(cloned_bytes.bytes.ptr == @as([]const u8, "abc").ptr); + + var cloned_n: Payload = undefined; + try clone(Payload, Payload{ .n = 7 }, &cloned_n, null); + try expect(cloned_n.n == 7); +} + +test "clone: tagged union unwind frees payload when nested allocation fails" { + const Payload = union(enum) { + pair: struct { + a: []u8, + b: []u8, + }, + n: u32, + }; + const a = try std.testing.allocator.dupe(u8, "first"); + defer std.testing.allocator.free(a); + const b = try std.testing.allocator.dupe(u8, "second"); + defer std.testing.allocator.free(b); + const data = Payload{ .pair = .{ .a = a, .b = b } }; + var cloned: Payload = undefined; + + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 }); + try expectError(error.OutOfMemory, clone(Payload, data, &cloned, failing.allocator())); +} + +test "clone: nested utils.List(List(...)) clones outer storage only" { + const Inner = utils.List(u32, 8); + const Outer = utils.List(Inner, 4); + + var inner_a = try Inner.init(std.testing.allocator); + defer inner_a.deinit(); + try inner_a.append(1); + try inner_a.append(2); + var inner_b = try Inner.init(std.testing.allocator); + defer inner_b.deinit(); + try inner_b.append(3); + + var data = try Outer.init(std.testing.allocator); + // Outer.deinit only releases the spine; the per-element inner Lists are + // value-copied into the spine and still owned by inner_a/inner_b above. + defer data.deinit(); + try data.append(inner_a); + try data.append(inner_b); + + var cloned: Outer = undefined; + try clone(Outer, data, &cloned, std.testing.allocator); + defer cloned.deinit(); + + try expect(cloned.len() == 2); + try expect(cloned.inner.items.ptr != data.inner.items.ptr); + try expect((try cloned.get(0)).len() == 2); + try expect((try (try cloned.get(0)).get(1)) == 2); + try expect((try cloned.get(1)).len() == 1); + try expect((try cloned.get(0)).inner.items.ptr == inner_a.inner.items.ptr); +} + +test "clone of round-trippable struct matches serialize+deserialize" { + const allocator = std.testing.allocator; + const original = pastries[1]; + + var via_clone: Pastry = undefined; + try clone(Pastry, original, &via_clone, allocator); + + var buf: ArrayList(u8) = .empty; + defer buf.deinit(allocator); + try serialize(Pastry, original, &buf, allocator); + // Pastry.name is `[]const u8`; deserialize shares it into buf.items + // rather than allocating, so no free is needed. + var via_roundtrip: Pastry = undefined; + try deserialize(Pastry, buf.items, &via_roundtrip, allocator); + + try expect(via_clone.weight == via_roundtrip.weight); + try expect(std.mem.eql(u8, via_clone.name, via_roundtrip.name)); + try expect(via_clone.weight == original.weight); + try expect(std.mem.eql(u8, via_clone.name, original.name)); + try expect(via_clone.name.ptr == original.name.ptr); +} + test { _ = @import("beacon_tests.zig"); } diff --git a/src/utils.zig b/src/utils.zig index 1f7d7c8..c0664f1 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -35,6 +35,14 @@ pub fn List(T: type, comptime N: usize) type { try serialize([]const Item, self.constSlice(), l, allocator); } + /// Clones this list's backing storage; item ownership matches normal List values. + pub fn sszClone(self: *const Self, cloned: *Self, allocator: ?Allocator) !void { + const alloc = allocator orelse return error.AllocatorRequired; + cloned.* = try Self.init(alloc); + errdefer cloned.deinit(); + try cloned.inner.appendSlice(alloc, self.inner.items); + } + pub fn isFixedSizeObject() bool { return false; } @@ -254,6 +262,15 @@ pub fn Bitlist(comptime N: usize) type { } } + /// Clones this bitlist's backing storage. + pub fn sszClone(self: *const Self, cloned: *Self, allocator: ?Allocator) !void { + const alloc = allocator orelse return error.AllocatorRequired; + cloned.* = try Self.init(alloc); + errdefer cloned.deinit(); + cloned.length = self.length; + try cloned.inner.appendSlice(alloc, self.inner.items); + } + pub fn sszDecode(serialized: []const u8, out: *Self, allocator: ?std.mem.Allocator) !void { const alloc = allocator orelse return error.AllocatorRequired; out.* = try init(alloc); From 200d4d4e155a7246c04b956ea9a3406b062506ee Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Fri, 29 May 2026 23:06:14 +0530 Subject: [PATCH 2/2] address comments --- src/lib.zig | 192 ---------------------------------- src/tests.zig | 284 ++++---------------------------------------------- src/utils.zig | 16 +-- 3 files changed, 31 insertions(+), 461 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index 66136cb..6146ce1 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -663,198 +663,6 @@ pub fn deserialize(T: type, serialized: []const u8, out: *T, allocator: ?Allocat } } -/// Returns true when `T` can be cloned with a direct value copy. -/// Types with custom clone functions return false so their functions are honored. -/// Unsupported type kinds return false conservatively. -pub fn hasNoPointers(T: type) bool { - if (comptime std.meta.hasFn(T, "sszClone")) return false; - - return switch (@typeInfo(T)) { - .int, .bool, .float, .void, .null, .@"enum" => true, - .array => |array| hasNoPointers(array.child), - .vector => |vector| hasNoPointers(vector.child), - .optional => |optional| hasNoPointers(optional.child), - .@"struct" => |str| blk: { - inline for (str.fields) |field| { - if (field.is_comptime) continue; - if (!hasNoPointers(field.type)) break :blk false; - } - break :blk true; - }, - .@"union" => |un| blk: { - if (un.tag_type == null) break :blk false; - inline for (un.fields) |field| { - if (!hasNoPointers(field.type)) break :blk false; - } - break :blk true; - }, - else => false, - }; -} - -fn cleanupClone(T: type, data: T, allocator: Allocator) void { - if (comptime std.meta.hasFn(T, "deinit")) { - var clone_data = data; - clone_data.deinit(); - return; - } - if (comptime hasNoPointers(T)) return; - - switch (@typeInfo(T)) { - .int, .bool, .float, .null, .@"enum" => {}, - .array => |array| { - if (comptime !hasNoPointers(array.child)) { - for (data) |item| cleanupClone(array.child, item, allocator); - } - }, - .optional => |optional| { - if (data) |value| cleanupClone(optional.child, value, allocator); - }, - .pointer => |ptr| switch (ptr.size) { - .slice => { - if (comptime ptr.is_const and @sizeOf(ptr.child) == 1) return; - if (comptime !hasNoPointers(ptr.child)) { - for (data) |item| cleanupClone(ptr.child, item, allocator); - } - allocator.free(data); - }, - .one => { - cleanupClone(ptr.child, data.*, allocator); - const alignment = comptime ptr.alignment orelse @alignOf(ptr.child); - if (comptime alignment == @alignOf(ptr.child)) { - allocator.destroy(@constCast(data)); - } else { - const slice: []align(alignment) ptr.child = @as([*]align(alignment) ptr.child, @ptrCast(@constCast(data)))[0..1]; - allocator.free(slice); - } - }, - else => @compileError("clone cleanup does not support " ++ @tagName(ptr.size) ++ " pointers"), - }, - .@"struct" => |str| { - inline for (str.fields) |field| { - if (field.is_comptime) continue; - cleanupClone(field.type, @field(data, field.name), allocator); - } - }, - .@"union" => |un| { - if (un.tag_type == null) @compileError("clone cleanup does not support untagged unions"); - switch (data) { - inline else => |payload| cleanupClone(@TypeOf(payload), payload, allocator), - } - }, - else => {}, - } -} - -/// Clones `data` into `cloned`. -/// Cloned values follow the same cleanup model as normally constructed SSZ -/// values: containers use `deinit`, allocated slices use `allocator.free`, and -/// borrowed `[]const u8` data remains borrowed. Types providing `sszClone` -/// must also provide `deinit`; partial-failure unwinds rely on `deinit` to -/// release whatever the hook allocated. -pub fn clone(T: type, data: T, cloned: *T, allocator: ?Allocator) !void { - if (comptime std.meta.hasFn(T, "sszClone")) { - if (comptime !std.meta.hasFn(T, "deinit")) { - @compileError("type " ++ @typeName(T) ++ " provides sszClone but no deinit; cleanup on partial clone failure would be undefined"); - } - return data.sszClone(cloned, allocator); - } - if (comptime hasNoPointers(T)) { - cloned.* = data; - return; - } - - switch (@typeInfo(T)) { - .int, .bool, .float, .void, .null, .@"enum" => cloned.* = data, - .array => |array| { - var done: usize = 0; - errdefer if (allocator) |alloc| { - for (cloned.*[0..done]) |item| cleanupClone(array.child, item, alloc); - }; - while (done < data.len) : (done += 1) { - try clone(array.child, data[done], &cloned.*[done], allocator); - } - }, - .optional => |optional| { - if (data == null) { - cloned.* = null; - return; - } - var child_clone: optional.child = undefined; - try clone(optional.child, data.?, &child_clone, allocator); - cloned.* = child_clone; - }, - .pointer => |ptr| switch (ptr.size) { - .slice => { - if (comptime ptr.is_const and @sizeOf(ptr.child) == 1) { - cloned.* = data; - return; - } - const alloc = allocator orelse return error.AllocatorRequired; - const alignment: std.mem.Alignment = comptime .fromByteUnits(ptr.alignment orelse @alignOf(ptr.child)); - const sentinel = comptime ptr.sentinel(); - const out = try alloc.allocWithOptions(ptr.child, data.len, alignment, sentinel); - errdefer alloc.free(out); - if (comptime hasNoPointers(ptr.child)) { - @memcpy(out, data); - } else { - var done: usize = 0; - errdefer for (out[0..done]) |item| cleanupClone(ptr.child, item, alloc); - while (done < data.len) : (done += 1) { - try clone(ptr.child, data[done], &out[done], allocator); - } - } - cloned.* = out; - }, - .one => { - const alloc = allocator orelse return error.AllocatorRequired; - const alignment = comptime ptr.alignment orelse @alignOf(ptr.child); - if (comptime alignment == @alignOf(ptr.child)) { - const slot = try alloc.create(ptr.child); - errdefer alloc.destroy(slot); - try clone(ptr.child, data.*, slot, allocator); - cloned.* = slot; - } else { - const alloc_alignment: std.mem.Alignment = comptime .fromByteUnits(alignment); - const slot = try alloc.allocWithOptions(ptr.child, 1, alloc_alignment, null); - errdefer alloc.free(slot); - try clone(ptr.child, data.*, &slot[0], allocator); - cloned.* = &slot[0]; - } - }, - else => return error.UnSupportedPointerType, - }, - .@"struct" => |str| { - var fields_done: usize = 0; - errdefer if (allocator) |alloc| { - var seen: usize = 0; - inline for (str.fields) |field| { - if (field.is_comptime) continue; - if (seen >= fields_done) break; - cleanupClone(field.type, @field(cloned.*, field.name), alloc); - seen += 1; - } - }; - inline for (str.fields) |field| { - if (field.is_comptime) continue; - try clone(field.type, @field(data, field.name), &@field(cloned.*, field.name), allocator); - fields_done += 1; - } - }, - .@"union" => |un| { - if (un.tag_type == null) @compileError("clone does not support untagged unions"); - switch (data) { - inline else => |payload, tag| { - var payload_clone: @TypeOf(payload) = undefined; - try clone(@TypeOf(payload), payload, &payload_clone, allocator); - cloned.* = @unionInit(T, @tagName(tag), payload_clone); - }, - } - }, - else => return error.UnknownType, - } -} - pub fn mixInLength2(Hasher: type, root: [Hasher.digest_length]u8, length: usize, out: *[Hasher.digest_length]u8) void { var hasher = Hasher.init(Hasher.Options{}); hasher.update(root[0..]); diff --git a/src/tests.zig b/src/tests.zig index cea2ece..f0abcaa 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -14,8 +14,6 @@ const Sha256 = std.crypto.hash.sha2.Sha256; const zeros = @import("zeros.zig"); const hashes_of_zero = zeros.hashes_of_zero; const Allocator = std.mem.Allocator; -const clone = libssz.clone; -const hasNoPointers = libssz.hasNoPointers; test "serializes uint8" { const data: u8 = 0x55; @@ -2576,152 +2574,14 @@ test "utils.List dynamic-item: offsets decrease" { try expectError(error.OffsetOrdering, L.sszDecode(&buf, &out, std.testing.allocator)); } -test "clone: pointer-free value works without allocator" { - const S = struct { - n: u32, - bytes: [4]u8, - tag: enum { a, b }, - }; - const data = S{ .n = 42, .bytes = .{ 1, 2, 3, 4 }, .tag = .b }; - var cloned: S = undefined; - - try clone(S, data, &cloned, null); - - try expect(hasNoPointers(S)); - try expect(std.meta.eql(data, cloned)); -} - -test "clone: const byte slice is borrowed without allocator" { - const data: []const u8 = "hello"; - var cloned: []const u8 = undefined; - - try clone([]const u8, data, &cloned, null); - - try expect(std.mem.eql(u8, cloned, data)); - try expect(cloned.ptr == data.ptr); -} - -test "clone: mutable slice requires allocator" { - const data = try std.testing.allocator.dupe(u8, "hello"); - defer std.testing.allocator.free(data); - var cloned: []u8 = undefined; - - try expectError(error.AllocatorRequired, clone([]u8, data, &cloned, null)); -} - -test "clone: mutable slice copy is independent" { - const data = try std.testing.allocator.dupe(u8, "hello"); - defer std.testing.allocator.free(data); - var cloned: []u8 = undefined; - - try clone([]u8, data, &cloned, std.testing.allocator); - defer std.testing.allocator.free(cloned); - - cloned[0] = 'H'; - try expect(data[0] == 'h'); - try expect(std.mem.eql(u8, cloned, "Hello")); -} - -test "clone: struct follows normal slice cleanup ownership" { - const S = struct { - a: []u8, - b: []const u8, - }; - const a = try std.testing.allocator.dupe(u8, "left"); - defer std.testing.allocator.free(a); - const data = S{ .a = a, .b = "right" }; - var cloned: S = undefined; - - try clone(S, data, &cloned, std.testing.allocator); - defer std.testing.allocator.free(cloned.a); - - try expect(std.mem.eql(u8, cloned.a, data.a)); - try expect(std.mem.eql(u8, cloned.b, data.b)); - try expect(cloned.a.ptr != data.a.ptr); - try expect(cloned.b.ptr == data.b.ptr); -} - -test "clone: single pointer is deep-copied" { - var value: u32 = 123; - const data: *u32 = &value; - var cloned: *u32 = undefined; - - try clone(*u32, data, &cloned, std.testing.allocator); - defer std.testing.allocator.destroy(cloned); - - try expect(cloned != data); - try expect(cloned.* == data.*); - cloned.* = 456; - try expect(value == 123); -} - -test "clone: const single pointer is deep-copied" { - const value: u32 = 123; - const data: *const u32 = &value; - var cloned: *const u32 = undefined; - - try clone(*const u32, data, &cloned, std.testing.allocator); - defer std.testing.allocator.destroy(@constCast(cloned)); - - try expect(cloned != data); - try expect(cloned.* == data.*); -} - -test "clone: over-aligned single pointer preserves alignment" { - const data = try std.testing.allocator.alignedAlloc(u32, .fromByteUnits(16), 1); - defer std.testing.allocator.free(data); - data[0] = 0xcafebabe; - const ptr: *align(16) u32 = &data[0]; - var cloned: *align(16) u32 = undefined; - - try clone(*align(16) u32, ptr, &cloned, std.testing.allocator); - defer { - const cloned_slice: []align(16) u32 = @as([*]align(16) u32, @ptrCast(cloned))[0..1]; - std.testing.allocator.free(cloned_slice); - } - - try expect(cloned != ptr); - try expect(cloned.* == ptr.*); - try expect(@intFromPtr(cloned) % 16 == 0); -} - -test "clone: optional pointer" { - var value: u32 = 99; - const some: ?*u32 = &value; - var cloned_some: ?*u32 = undefined; - var cloned_none: ?*u32 = undefined; - - try clone(?*u32, some, &cloned_some, std.testing.allocator); - defer if (cloned_some) |ptr| std.testing.allocator.destroy(ptr); - try clone(?*u32, null, &cloned_none, null); - - try expect(cloned_some.?.* == 99); - try expect(cloned_some.? != some.?); - try expect(cloned_none == null); -} - -test "clone: sentinel-terminated slice preserves sentinel" { - const data = try std.testing.allocator.dupeZ(u8, "hello"); - defer std.testing.allocator.free(data); - var cloned: [:0]u8 = undefined; - - try clone([:0]u8, data, &cloned, std.testing.allocator); - defer std.testing.allocator.free(cloned); - - try expect(std.mem.eql(u8, cloned, data)); - try expect(cloned.ptr != data.ptr); - try expect(cloned[cloned.len] == 0); -} - -test "clone: utils.List uses clone hook" { +test "utils.List.clone copies backing storage independently" { const L = utils.List([]const u8, 4); var data = try L.init(std.testing.allocator); defer data.deinit(); try data.append("aa"); try data.append("bb"); - var cloned: L = undefined; - try clone(L, data, &cloned, std.testing.allocator); + var cloned = try data.clone(std.testing.allocator); defer cloned.deinit(); try expect(cloned.len() == data.len()); @@ -2730,16 +2590,34 @@ test "clone: utils.List uses clone hook" { try expect((try cloned.get(0)).ptr == (try data.get(0)).ptr); } -test "clone: utils.Bitlist uses clone hook" { +test "utils.List.clone with variable-sized struct items" { + const L = utils.List(Pastry, 8); + var data = try L.init(std.testing.allocator); + defer data.deinit(); + try data.append(pastries[0]); + try data.append(pastries[1]); + + var cloned = try data.clone(std.testing.allocator); + defer cloned.deinit(); + + try expect(cloned.len() == 2); + try expect(cloned.inner.items.ptr != data.inner.items.ptr); + try expect((try cloned.get(0)).weight == pastries[0].weight); + try expect((try cloned.get(1)).weight == pastries[1].weight); + // Item-level []const u8 fields stay borrowed (shallow item copy). + try expect((try cloned.get(0)).name.ptr == pastries[0].name.ptr); + try expect((try cloned.get(1)).name.ptr == pastries[1].name.ptr); +} + +test "utils.Bitlist.clone copies backing storage independently" { const B = utils.Bitlist(32); var data = try B.init(std.testing.allocator); defer data.deinit(); try data.append(true); try data.append(false); try data.append(true); - var cloned: B = undefined; - try clone(B, data, &cloned, std.testing.allocator); + var cloned = try data.clone(std.testing.allocator); defer cloned.deinit(); try expect(cloned.eql(&data)); @@ -2747,122 +2625,6 @@ test "clone: utils.Bitlist uses clone hook" { try expect((try data.get(0)) == true); } -test "clone: struct unwind frees earlier fields when later allocation fails" { - const S = struct { - a: []u8, - b: []u8, - }; - const a = try std.testing.allocator.dupe(u8, "first"); - defer std.testing.allocator.free(a); - const b = try std.testing.allocator.dupe(u8, "second"); - defer std.testing.allocator.free(b); - const data = S{ .a = a, .b = b }; - var cloned: S = undefined; - - var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 }); - try expectError(error.OutOfMemory, clone(S, data, &cloned, failing.allocator())); -} - -test "clone: array of const byte slices keeps borrowed items" { - const data: [3][]const u8 = .{ "one", "two", "three" }; - var cloned: [3][]const u8 = undefined; - - try clone([3][]const u8, data, &cloned, null); - - inline for (0..3) |i| { - try expect(std.mem.eql(u8, cloned[i], data[i])); - try expect(cloned[i].ptr == data[i].ptr); - } -} - -test "clone: tagged union active variant follows slice ownership" { - const Payload = union(enum) { - bytes: []const u8, - n: u32, - }; - - var cloned_bytes: Payload = undefined; - try clone(Payload, Payload{ .bytes = "abc" }, &cloned_bytes, std.testing.allocator); - try expect(std.mem.eql(u8, cloned_bytes.bytes, "abc")); - try expect(cloned_bytes.bytes.ptr == @as([]const u8, "abc").ptr); - - var cloned_n: Payload = undefined; - try clone(Payload, Payload{ .n = 7 }, &cloned_n, null); - try expect(cloned_n.n == 7); -} - -test "clone: tagged union unwind frees payload when nested allocation fails" { - const Payload = union(enum) { - pair: struct { - a: []u8, - b: []u8, - }, - n: u32, - }; - const a = try std.testing.allocator.dupe(u8, "first"); - defer std.testing.allocator.free(a); - const b = try std.testing.allocator.dupe(u8, "second"); - defer std.testing.allocator.free(b); - const data = Payload{ .pair = .{ .a = a, .b = b } }; - var cloned: Payload = undefined; - - var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 }); - try expectError(error.OutOfMemory, clone(Payload, data, &cloned, failing.allocator())); -} - -test "clone: nested utils.List(List(...)) clones outer storage only" { - const Inner = utils.List(u32, 8); - const Outer = utils.List(Inner, 4); - - var inner_a = try Inner.init(std.testing.allocator); - defer inner_a.deinit(); - try inner_a.append(1); - try inner_a.append(2); - var inner_b = try Inner.init(std.testing.allocator); - defer inner_b.deinit(); - try inner_b.append(3); - - var data = try Outer.init(std.testing.allocator); - // Outer.deinit only releases the spine; the per-element inner Lists are - // value-copied into the spine and still owned by inner_a/inner_b above. - defer data.deinit(); - try data.append(inner_a); - try data.append(inner_b); - - var cloned: Outer = undefined; - try clone(Outer, data, &cloned, std.testing.allocator); - defer cloned.deinit(); - - try expect(cloned.len() == 2); - try expect(cloned.inner.items.ptr != data.inner.items.ptr); - try expect((try cloned.get(0)).len() == 2); - try expect((try (try cloned.get(0)).get(1)) == 2); - try expect((try cloned.get(1)).len() == 1); - try expect((try cloned.get(0)).inner.items.ptr == inner_a.inner.items.ptr); -} - -test "clone of round-trippable struct matches serialize+deserialize" { - const allocator = std.testing.allocator; - const original = pastries[1]; - - var via_clone: Pastry = undefined; - try clone(Pastry, original, &via_clone, allocator); - - var buf: ArrayList(u8) = .empty; - defer buf.deinit(allocator); - try serialize(Pastry, original, &buf, allocator); - // Pastry.name is `[]const u8`; deserialize shares it into buf.items - // rather than allocating, so no free is needed. - var via_roundtrip: Pastry = undefined; - try deserialize(Pastry, buf.items, &via_roundtrip, allocator); - - try expect(via_clone.weight == via_roundtrip.weight); - try expect(std.mem.eql(u8, via_clone.name, via_roundtrip.name)); - try expect(via_clone.weight == original.weight); - try expect(std.mem.eql(u8, via_clone.name, original.name)); - try expect(via_clone.name.ptr == original.name.ptr); -} - test { _ = @import("beacon_tests.zig"); } diff --git a/src/utils.zig b/src/utils.zig index c0664f1..d0eb5ef 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -36,11 +36,11 @@ pub fn List(T: type, comptime N: usize) type { } /// Clones this list's backing storage; item ownership matches normal List values. - pub fn sszClone(self: *const Self, cloned: *Self, allocator: ?Allocator) !void { - const alloc = allocator orelse return error.AllocatorRequired; - cloned.* = try Self.init(alloc); + pub fn clone(self: *const Self, allocator: Allocator) !Self { + var cloned = try Self.init(allocator); errdefer cloned.deinit(); - try cloned.inner.appendSlice(alloc, self.inner.items); + try cloned.inner.appendSlice(allocator, self.inner.items); + return cloned; } pub fn isFixedSizeObject() bool { @@ -263,12 +263,12 @@ pub fn Bitlist(comptime N: usize) type { } /// Clones this bitlist's backing storage. - pub fn sszClone(self: *const Self, cloned: *Self, allocator: ?Allocator) !void { - const alloc = allocator orelse return error.AllocatorRequired; - cloned.* = try Self.init(alloc); + pub fn clone(self: *const Self, allocator: Allocator) !Self { + var cloned = try Self.init(allocator); errdefer cloned.deinit(); cloned.length = self.length; - try cloned.inner.appendSlice(alloc, self.inner.items); + try cloned.inner.appendSlice(allocator, self.inner.items); + return cloned; } pub fn sszDecode(serialized: []const u8, out: *Self, allocator: ?std.mem.Allocator) !void {