//! Repository State — draft-fossil-repo-state-00.
//!
//! Repository state is derived, not stored: branches, tags, leaves, closed
//! markers, and the current-trunk check-in are computed from the crosslinked
//! set of artifacts. This module owns that derivation over the parent DAG and
//! the tag records defined by the Artifact Format.
//!
//! Input is a `Record` — one check-in's linkage/tag facts — rather than raw
//! manifest bytes; a thin manifest-to-Record adapter arrives with manifest
//! assembly. Artifact identity (hash-to-rid, phantoms) lives behind the
//! `ArtifactStore` boundary so tests and the simulation harness can inject
//! an in-memory store. Nothing here touches ambient time, randomness, the
//! filesystem, or the network.
const std = @import("std");
/// A stored artifact's local integer identifier (Section 2). Local to a
/// repository; not part of the artifact model and not synchronised.
pub const Rid = u64;
/// A check-in's modification time as an opaque ordinal. Only its ordering is
/// consumed by the derived views (Sections 8 and 9); no wall-clock meaning
/// is attached here.
pub const Mtime = u64;
/// Tag type values (Section 5.1). The integer encoding is normative: every
/// tag reference is persisted with one of exactly these three small
/// integers.
pub const TagType = enum(u2) {
/// value 0, surface syntax `-`: removes or blocks a tag.
cancel = 0,
/// value 1, surface syntax `+`: applies to a single check-in only.
singleton = 1,
/// value 2, surface syntax `*`: applies to the target and, absent an
/// intervening cancel, its primary-chain descendants.
propagate = 2,
};
/// One validated T-card, post-parse (Section 4.1 validation happens in the
/// artifact layer). A null `target` designates a self-tag: the tag applies
/// to the artifact being crosslinked (Section 4.2 step 1).
pub const TagOp = struct {
type: TagType,
name: []const u8,
target: ?[]const u8,
value: ?[]const u8,
};
/// One check-in's crosslink input: its hash, event time, parent references
/// in P-card order, and validated tag cards.
pub const Record = struct {
hash: []const u8,
mtime: Mtime,
/// Parent hashes in manifest order; `parents[0]` is the primary parent
/// (Section 3.2). Empty for an initial check-in.
parents: []const []const u8 = &.{},
tags: []const TagOp = &.{},
};
/// The artifact identity boundary: hash-to-rid interning, content storage,
/// and phantom placeholders (Section 4.2 step 1). A runtime vtable so the
/// simulation harness and tests inject an in-memory implementation while a
/// durable store can slot in later without touching the derivation code.
pub const ArtifactStore = struct {
context: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
/// Resolve a hash to its rid, creating a phantom placeholder — an
/// rid that carries no content yet — when the hash is unknown
/// (Section 4.2 step 1).
intern: *const fn (context: *anyopaque, hash: []const u8) error{OutOfMemory}!Rid,
/// Store content for a hash, upgrading a phantom in place.
/// Idempotent: content is addressed by hash, so a repeat put of the
/// same hash carries the same bytes.
put: *const fn (context: *anyopaque, hash: []const u8, content: []const u8) error{OutOfMemory}!Rid,
/// The stored bytes for a rid, or null while it is a phantom.
content: *const fn (context: *anyopaque, rid: Rid) ?[]const u8,
/// The hash that names a rid. Every rid, phantom or not, has one.
hashOf: *const fn (context: *anyopaque, rid: Rid) []const u8,
};
pub fn intern(self: ArtifactStore, hash: []const u8) error{OutOfMemory}!Rid {
return self.vtable.intern(self.context, hash);
}
pub fn put(self: ArtifactStore, hash: []const u8, content_bytes: []const u8) error{OutOfMemory}!Rid {
return self.vtable.put(self.context, hash, content_bytes);
}
pub fn content(self: ArtifactStore, rid: Rid) ?[]const u8 {
return self.vtable.content(self.context, rid);
}
pub fn hashOf(self: ArtifactStore, rid: Rid) []const u8 {
return self.vtable.hashOf(self.context, rid);
}
};
/// In-memory `ArtifactStore`: rids are assigned densely in intern order
/// (index + 1, so rid 0 stays invalid), and a phantom is an entry whose
/// bytes are still null.
pub const MemStore = struct {
gpa: std.mem.Allocator,
by_hash: std.StringHashMapUnmanaged(Rid) = .empty,
entries: std.ArrayList(Entry) = .empty,
const Entry = struct {
hash: []u8,
bytes: ?[]u8,
};
pub fn init(gpa: std.mem.Allocator) MemStore {
return .{ .gpa = gpa };
}
pub fn deinit(self: *MemStore) void {
for (self.entries.items) |entry| {
self.gpa.free(entry.hash);
if (entry.bytes) |bytes| self.gpa.free(bytes);
}
self.entries.deinit(self.gpa);
self.by_hash.deinit(self.gpa);
self.* = undefined;
}
/// The type-erased store interface over this instance.
pub fn store(self: *MemStore) ArtifactStore {
return .{ .context = self, .vtable = &vtable };
}
const vtable = ArtifactStore.VTable{
.intern = internImpl,
.put = putImpl,
.content = contentImpl,
.hashOf = hashOfImpl,
};
fn internImpl(context: *anyopaque, hash: []const u8) error{OutOfMemory}!Rid {
const self: *MemStore = @ptrCast(@alignCast(context));
std.debug.assert(hash.len != 0);
if (self.by_hash.get(hash)) |rid| return rid;
// New hash: create a phantom placeholder (Section 4.2 step 1).
// Capacity is reserved before the map insert so a failure leaves no
// partial entry behind.
try self.entries.ensureUnusedCapacity(self.gpa, 1);
const owned = try self.gpa.dupe(u8, hash);
errdefer self.gpa.free(owned);
const rid: Rid = self.entries.items.len + 1;
try self.by_hash.put(self.gpa, owned, rid);
self.entries.appendAssumeCapacity(.{ .hash = owned, .bytes = null });
return rid;
}
fn putImpl(context: *anyopaque, hash: []const u8, content_bytes: []const u8) error{OutOfMemory}!Rid {
const self: *MemStore = @ptrCast(@alignCast(context));
const rid = try internImpl(context, hash);
const entry = self.entryOf(rid);
if (entry.bytes) |existing| {
// Content is addressed by hash: a repeat put carries identical
// bytes, so upgrading twice is a no-op.
std.debug.assert(std.mem.eql(u8, existing, content_bytes));
} else {
entry.bytes = try self.gpa.dupe(u8, content_bytes);
}
return rid;
}
fn contentImpl(context: *anyopaque, rid: Rid) ?[]const u8 {
const self: *MemStore = @ptrCast(@alignCast(context));
return self.entryOf(rid).bytes;
}
fn hashOfImpl(context: *anyopaque, rid: Rid) []const u8 {
const self: *MemStore = @ptrCast(@alignCast(context));
return self.entryOf(rid).hash;
}
fn entryOf(self: *MemStore, rid: Rid) *Entry {
std.debug.assert(rid >= 1 and rid <= self.entries.items.len);
return &self.entries.items[@intCast(rid - 1)];
}
};
/// The derived repository state: the check-in DAG (Section 3) and the leaf
/// table (Section 7), maintained by the crosslink procedure (Section 4.2).
/// Tag cross-references, branch membership, and name resolution build on
/// this next.
pub const RepoState = struct {
gpa: std.mem.Allocator,
store: ArtifactStore,
/// One row per crosslinked check-in: rid to event mtime. Presence here
/// is the Section 4.2 step 5 idempotency guard, and its key set is the
/// leaf-candidate set (Section 7.3: every check-in, phantoms excluded).
events: std.AutoArrayHashMapUnmanaged(Rid, Mtime) = .empty,
/// Parent linkage rows, unique per (parent, child) pair (Section 3.1).
plinks: std.AutoArrayHashMapUnmanaged(PlinkKey, PlinkRow) = .empty,
/// The precomputed leaf table (Section 7.1): membership only, no
/// open/closed metadata of its own.
leaf_table: std.AutoArrayHashMapUnmanaged(Rid, void) = .empty,
/// Deferred leaf rechecks (Section 7.3), applied as a batch before the
/// table is considered consistent.
pending_leaf: std.AutoArrayHashMapUnmanaged(Rid, void) = .empty,
const PlinkKey = struct {
parent: Rid,
child: Rid,
};
const PlinkRow = struct {
/// Set on exactly the row for the first parent listed (Section 3.2).
is_primary: bool,
/// The child check-in's modification time (Section 3.1).
mtime: Mtime,
/// Delta-manifest baseline id (Section 3.1); resolved once the
/// manifest adapter supplies B-card linkage.
baseline: Rid,
};
pub fn init(gpa: std.mem.Allocator, artifact_store: ArtifactStore) RepoState {
return .{ .gpa = gpa, .store = artifact_store };
}
pub fn deinit(self: *RepoState) void {
self.events.deinit(self.gpa);
self.plinks.deinit(self.gpa);
self.leaf_table.deinit(self.gpa);
self.pending_leaf.deinit(self.gpa);
self.* = undefined;
}
/// Crosslink one check-in record (Section 4.2): intern its hash, derive
/// its linkage rows with parents interned to phantoms when absent
/// (step 1), and schedule-then-apply the leaf rechecks that the new
/// edges require (Section 7.3). Linkage derivation is idempotent
/// (step 5): a rid that already has an event row is left untouched.
/// T-card routing and propagation (steps 2-4) follow once tag state
/// lands. Only allocation can fail.
pub fn crosslink(self: *RepoState, rec: Record) error{OutOfMemory}!Rid {
std.debug.assert(rec.hash.len != 0);
const rid = try self.store.intern(rec.hash);
if (self.events.contains(rid)) return rid;
try self.events.put(self.gpa, rid, rec.mtime);
for (rec.parents, 0..) |parent_hash, i| {
std.debug.assert(parent_hash.len != 0);
const parent = try self.store.intern(parent_hash);
// A manifest cannot name its own hash as a parent: the hash
// covers the manifest's content, including its P card.
std.debug.assert(parent != rid);
const gop = try self.plinks.getOrPut(self.gpa, .{ .parent = parent, .child = rid });
if (!gop.found_existing) {
// Section 3.1: a repeated parent reference collapses into
// the single row for the pair, so the first occurrence wins.
gop.value_ptr.* = .{ .is_primary = i == 0, .mtime = rec.mtime, .baseline = 0 };
}
// Section 7.3: a new parent-link schedules a recheck of each
// direct parent — a new child can retroactively remove the
// parent's leaf status.
try self.pending_leaf.put(self.gpa, parent, {});
}
try self.pending_leaf.put(self.gpa, rid, {});
try self.applyPendingLeafChecks();
const post = self.events.count();
std.debug.assert(post >= 1 and self.pending_leaf.count() == 0);
return rid;
}
/// Whether `rid` is a leaf, answered from the precomputed leaf table
/// (Section 7.1).
pub fn isLeaf(self: *const RepoState, rid: Rid) bool {
return self.leaf_table.contains(rid);
}
/// Iterate the leaf table (Section 7.1). Invalidated by any mutation of
/// the repository state.
pub fn leaves(self: *const RepoState) LeafIterator {
return .{ .rids = self.leaf_table.keys() };
}
/// Recompute the entire leaf table from scratch (Section 7.3 full
/// rebuild): every check-in is a candidate — roots included — and is a
/// leaf exactly when it has no same-branch child, with no
/// primary/merge filter. Whole-table cost; reserved for explicit
/// rebuilds rather than routine updates.
pub fn rebuildLeaves(self: *RepoState) error{OutOfMemory}!void {
self.leaf_table.clearRetainingCapacity();
for (self.events.keys()) |rid| {
if (!self.hasSameBranchChild(rid)) {
try self.leaf_table.put(self.gpa, rid, {});
}
}
}
/// The number of distinct parents of `child` (Section 3.3
/// child-to-parent lookup); merge parents included.
pub fn parentCount(self: *const RepoState, child: Rid) usize {
var n: usize = 0;
for (self.plinks.keys()) |key| {
if (key.child == child) n += 1;
}
return n;
}
/// The primary parent of `child` (Section 3.2), or null for a root or
/// an unknown rid. At most one linkage row per child carries the
/// primary indicator.
pub fn primaryParentOf(self: *const RepoState, child: Rid) ?Rid {
for (self.plinks.keys(), self.plinks.values()) |key, row| {
if (key.child == child and row.is_primary) return key.parent;
}
return null;
}
/// The event mtime recorded for a crosslinked check-in, or null when
/// `rid` has no event row (a phantom, or never crosslinked).
pub fn mtimeOf(self: *const RepoState, rid: Rid) ?Mtime {
return self.events.get(rid);
}
/// Apply every scheduled leaf recheck and clear the schedule
/// (Section 7.3 batch application). The check adds no new entries to
/// the schedule, so the batch is bounded by its size at entry.
fn applyPendingLeafChecks(self: *RepoState) error{OutOfMemory}!void {
const n = self.pending_leaf.count();
for (self.pending_leaf.keys()[0..n]) |rid| {
try self.leafCheckOne(rid);
}
std.debug.assert(self.pending_leaf.count() == n);
self.pending_leaf.clearRetainingCapacity();
}
/// Recompute leaf membership for a single rid (Section 7.3 incremental
/// check): removed when it has a same-branch child, inserted otherwise,
/// tolerating already-present. A rid without an event row is not a
/// check-in (a phantom placeholder) and never enters the table.
fn leafCheckOne(self: *RepoState, rid: Rid) error{OutOfMemory}!void {
const is_leaf = self.events.contains(rid) and !self.hasSameBranchChild(rid);
if (is_leaf) {
try self.leaf_table.put(self.gpa, rid, {});
} else {
_ = self.leaf_table.swapRemove(rid);
}
}
/// The canonical leaf test's disqualifier (Section 7.2): any
/// parent-link naming `rid` as parent whose child is on the same
/// branch. Deliberately NOT filtered by the primary/merge distinction —
/// a same-branch child reached only via a merge edge still counts.
fn hasSameBranchChild(self: *const RepoState, rid: Rid) bool {
for (self.plinks.keys()) |key| {
if (key.parent == rid and self.sameBranch(rid, key.child)) return true;
}
return false;
}
/// Whether two check-ins are on the same branch. Branch tags (tagid 8)
/// are not derived yet, so every check-in sits on the default branch
/// (Section 6.4) and the test is uniformly true; tag cross-reference
/// state refines this.
fn sameBranch(self: *const RepoState, a: Rid, b: Rid) bool {
_ = self;
_ = a;
_ = b;
return true;
}
};
/// Iterator over the leaf table, in table order.
pub const LeafIterator = struct {
rids: []const Rid,
index: usize = 0,
pub fn next(self: *LeafIterator) ?Rid {
if (self.index == self.rids.len) return null;
defer self.index += 1;
return self.rids[self.index];
}
};
// ---------------------------------------------------------------------------
// Tests. Expected values come from draft-fossil-repo-state-00 only.
// ---------------------------------------------------------------------------
const t = std.testing;
// 64-hex-character hashes, distinct per letter.
const h_a = "a" ** 64;
const h_b = "b" ** 64;
const h_c = "c" ** 64;
const h_d = "d" ** 64;
const h_e = "e" ** 64;
fn countLeaves(rs: *const RepoState) usize {
var it = rs.leaves();
var n: usize = 0;
while (it.next()) |_| n += 1;
return n;
}
test "repostate §2: intern assigns one stable rid per hash" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
const s = ms.store();
const ra = try s.intern(h_a);
const rb = try s.intern(h_b);
try t.expect(ra != rb);
try t.expectEqual(ra, try s.intern(h_a));
try t.expectEqualStrings(h_a, s.hashOf(ra));
try t.expectEqualStrings(h_b, s.hashOf(rb));
}
test "repostate §4.2: an interned but absent artifact is a phantom with no content" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
const s = ms.store();
const rid = try s.intern(h_a);
try t.expectEqual(@as(?[]const u8, null), s.content(rid));
// The referenced artifact arrives: the phantom upgrades in place.
try t.expectEqual(rid, try s.put(h_a, "payload"));
try t.expectEqualStrings("payload", s.content(rid).?);
// A repeat put of the same hash is a no-op.
try t.expectEqual(rid, try s.put(h_a, "payload"));
try t.expectEqualStrings("payload", s.content(rid).?);
}
test "repostate §5.1: tag types encode cancel=0, singleton=1, propagate=2" {
try t.expectEqual(0, @intFromEnum(TagType.cancel));
try t.expectEqual(1, @intFromEnum(TagType.singleton));
try t.expectEqual(2, @intFromEnum(TagType.propagate));
}
test "repostate §3.1: a repeated parent reference collapses to one linkage row" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{ h_a, h_a } });
try t.expectEqual(@as(usize, 1), rs.parentCount(rb));
try t.expectEqual(@as(?Rid, ra), rs.primaryParentOf(rb));
try t.expect(!rs.isLeaf(ra));
try t.expect(rs.isLeaf(rb));
}
test "repostate §3.2: the first listed parent is the primary parent" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
_ = try rs.crosslink(.{ .hash = h_b, .mtime = 2 });
const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{ h_a, h_b } });
try t.expectEqual(@as(usize, 2), rs.parentCount(rc));
try t.expectEqual(@as(?Rid, ra), rs.primaryParentOf(rc));
}
test "repostate §3.2: a sole parent is the primary parent" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
try t.expectEqual(@as(?Rid, ra), rs.primaryParentOf(rb));
}
test "repostate §4.2: re-crosslinking a linked artifact is a linkage no-op" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
_ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
const rb_again = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
try t.expectEqual(rb, rb_again);
try t.expectEqual(@as(usize, 1), rs.parentCount(rb));
try t.expectEqual(@as(usize, 1), countLeaves(&rs));
try t.expect(rs.isLeaf(rb));
}
test "repostate §4.2: an absent parent hash yields a phantom, resolved when it arrives" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
// The child arrives first; its parent hash resolves to a phantom
// placeholder rather than a fatal error.
const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
const ra = rs.primaryParentOf(rb).?;
try t.expectEqual(@as(?Mtime, null), rs.mtimeOf(ra));
// A phantom is not a check-in: it never enters the leaf table.
try t.expect(!rs.isLeaf(ra));
try t.expect(rs.isLeaf(rb));
try t.expectEqual(@as(usize, 1), countLeaves(&rs));
// The referenced artifact arrives and is crosslinked: the recorded
// linkage becomes visible in the derived views.
try t.expectEqual(ra, try rs.crosslink(.{ .hash = h_a, .mtime = 1 }));
try t.expectEqual(@as(?Mtime, 1), rs.mtimeOf(ra));
try t.expect(!rs.isLeaf(ra));
try t.expect(rs.isLeaf(rb));
try t.expectEqual(@as(usize, 1), countLeaves(&rs));
}
test "repostate §7.2: a child reached only via a merge edge disqualifies the leaf" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
_ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} });
const rd = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{ h_b, h_c } });
// rc's only child rd is reached via a non-primary (merge) edge; the
// canonical test applies no primary/merge filter.
try t.expect(!rs.isLeaf(rb));
try t.expect(!rs.isLeaf(rc));
try t.expect(rs.isLeaf(rd));
try t.expectEqual(@as(usize, 1), countLeaves(&rs));
}
test "repostate §7.3: a root check-in with no children is a leaf" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
try t.expect(rs.isLeaf(ra));
var it = rs.leaves();
try t.expectEqual(@as(?Rid, ra), it.next());
try t.expectEqual(@as(?Rid, null), it.next());
}
test "repostate §7.3: a new child retroactively removes its parent's leaf status" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
try t.expect(rs.isLeaf(ra));
const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
try t.expect(!rs.isLeaf(ra));
try t.expect(rs.isLeaf(rb));
}
test "repostate §7.3: full rebuild reproduces the incrementally maintained table" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
_ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} });
_ = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{ h_b, h_c } });
// An independent root with no children: a candidate the rebuild MUST
// include even though it appears in no parent-link.
const re = try rs.crosslink(.{ .hash = h_e, .mtime = 5 });
var before: [8]Rid = undefined;
const n_before = collectLeaves(&rs, &before);
try rs.rebuildLeaves();
var after: [8]Rid = undefined;
const n_after = collectLeaves(&rs, &after);
try t.expectEqual(n_before, n_after);
std.mem.sort(Rid, before[0..n_before], {}, std.sort.asc(Rid));
std.mem.sort(Rid, after[0..n_after], {}, std.sort.asc(Rid));
try t.expectEqualSlices(Rid, before[0..n_before], after[0..n_after]);
try t.expect(rs.isLeaf(re));
try t.expect(!rs.isLeaf(rb));
try t.expect(!rs.isLeaf(rc));
}
fn collectLeaves(rs: *const RepoState, out: []Rid) usize {
var it = rs.leaves();
var n: usize = 0;
while (it.next()) |rid| : (n += 1) out[n] = rid;
return n;
}
fn crosslinkNoLeak(gpa: std.mem.Allocator) !void {
var ms = MemStore.init(gpa);
defer ms.deinit();
var rs = RepoState.init(gpa, ms.store());
defer rs.deinit();
_ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
_ = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
// Phantom parent plus a merge, so every allocation site is exercised.
_ = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{ h_b, h_c } });
_ = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} });
try rs.rebuildLeaves();
}
test "repostate §4.2: crosslink survives allocation failure without leaking" {
try t.checkAllAllocationFailures(t.allocator, crosslinkNoLeak, .{});
}