datastruct: split tree node removal

pull/8165/head
Mitchell Hashimoto 2025-08-06 07:29:20 -07:00
parent 52e264948d
commit 3e767c166c
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 367 additions and 20 deletions

View File

@ -7,7 +7,6 @@ pub const resourcesDir = internal_os.resourcesDir;
// The exported API, custom for the apprt.
pub const class = @import("gtk-ng/class.zig");
pub const SplitTree = @import("gtk-ng/split_tree.zig").SplitTree;
pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef;
test {

View File

@ -6,6 +6,7 @@ const cache_table = @import("cache_table.zig");
const circ_buf = @import("circ_buf.zig");
const intrusive_linked_list = @import("intrusive_linked_list.zig");
const segmented_pool = @import("segmented_pool.zig");
const split_tree = @import("split_tree.zig");
pub const lru = @import("lru.zig");
pub const BlockingQueue = blocking_queue.BlockingQueue;
@ -13,6 +14,7 @@ pub const CacheTable = cache_table.CacheTable;
pub const CircBuf = circ_buf.CircBuf;
pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList;
pub const SegmentedPool = segmented_pool.SegmentedPool;
pub const SplitTree = split_tree.SplitTree;
test {
@import("std").testing.refAllDecls(@This());

View File

@ -31,6 +31,12 @@ const Allocator = std.mem.Allocator;
///
/// - `fn eql(*const View, *const View) bool` - Check if two views are equal.
///
/// Optionally the following functions can also be implemented:
///
/// - `fn splitTreeLabel(*const View) []const u8` - Return a label that is used
/// for the debug view. If this isn't specified then the node handle
/// will be used.
///
pub fn SplitTree(comptime V: type) type {
return struct {
const Self = @This();
@ -107,6 +113,38 @@ pub fn SplitTree(comptime V: type) type {
self.* = undefined;
}
/// An iterator over all the views in the tree.
pub fn iterator(
self: *const Self,
) Iterator {
return .{ .nodes = self.nodes };
}
pub const Iterator = struct {
i: Node.Handle = 0,
nodes: []const Node,
pub const Entry = struct {
handle: Node.Handle,
view: *View,
};
pub fn next(self: *Iterator) ?Entry {
// If we have no nodes, return null.
if (self.i >= self.nodes.len) return null;
// Get the current node and increment the index.
const handle = self.i;
self.i += 1;
const node = self.nodes[handle];
return switch (node) {
.leaf => |v| .{ .handle = handle, .view = v },
.split => self.next(),
};
}
};
/// Insert another tree into this tree at the given node in the
/// specified direction. The other tree will be inserted in the
/// new direction. For example, if the direction is "right" then
@ -169,6 +207,159 @@ pub fn SplitTree(comptime V: type) type {
.right = @intCast(if (left) nodes.len - 1 else self.nodes.len),
} };
// We need to increase the reference count of all the nodes.
try refNodes(gpa, nodes);
return .{ .arena = arena, .nodes = nodes };
}
/// Remove a node from the tree.
pub fn remove(
self: *Self,
gpa: Allocator,
at: Node.Handle,
) Allocator.Error!Self {
assert(at < self.nodes.len);
// If we're removing node zero then we're clearing the tree.
if (at == 0) return .empty;
// The new arena for our new tree.
var arena = ArenaAllocator.init(gpa);
errdefer arena.deinit();
const alloc = arena.allocator();
// Allocate our new nodes list with the number of nodes we'll
// need after the removal.
const nodes = try alloc.alloc(Node, self.countAfterRemoval(
0,
at,
0,
));
// Traverse the tree and copy all our nodes into place.
assert(self.removeNode(
nodes,
0,
0,
at,
) > 0);
// Increase the reference count of all the nodes.
try refNodes(gpa, nodes);
return .{
.arena = arena,
.nodes = nodes,
};
}
fn removeNode(
self: *Self,
nodes: []Node,
new_offset: Node.Handle,
current: Node.Handle,
target: Node.Handle,
) Node.Handle {
assert(current != target);
switch (self.nodes[current]) {
// Leaf is simple, just copy it over. We don't ref anything
// yet because it'd make undo (errdefer) harder. We do that
// all at once later.
.leaf => |view| {
nodes[new_offset] = .{ .leaf = view };
return 1;
},
.split => |s| {
// If we're removing one of the split node sides then
// we remove the split node itself as well and only add
// the other (non-removed) side.
if (s.left == target) return self.removeNode(
nodes,
new_offset,
s.right,
target,
);
if (s.right == target) return self.removeNode(
nodes,
new_offset,
s.left,
target,
);
// Neither side is being directly removed, so we traverse.
const left = self.removeNode(
nodes,
new_offset + 1,
s.left,
target,
);
assert(left > 0);
const right = self.removeNode(
nodes,
new_offset + 1 + left,
s.right,
target,
);
assert(right > 0);
nodes[new_offset] = .{ .split = .{
.layout = s.layout,
.ratio = s.ratio,
.left = new_offset + 1,
.right = new_offset + 1 + left,
} };
return left + right + 1;
},
}
}
/// Returns the number of nodes that would be needed to store
/// the tree if the target node is removed.
fn countAfterRemoval(
self: *Self,
current: Node.Handle,
target: Node.Handle,
acc: usize,
) usize {
assert(current != target);
return switch (self.nodes[current]) {
// Leaf is simple, always takes one node.
.leaf => acc + 1,
// Split is slightly more complicated. If either side is the
// target to remove, then we remove the split node as well
// so our count is just the count of the other side.
//
// If neither side is the target, then we count both sides
// and add one to account for the split node itself.
.split => |s| if (s.left == target) self.countAfterRemoval(
s.right,
target,
acc,
) else if (s.right == target) self.countAfterRemoval(
s.left,
target,
acc,
) else self.countAfterRemoval(
s.left,
target,
acc,
) + self.countAfterRemoval(
s.right,
target,
acc,
) + 1,
};
}
/// Reference all the nodes in the given slice, handling unref if
/// any fail. This should be called LAST so you don't have to undo
/// the refs at any further point after this.
fn refNodes(gpa: Allocator, nodes: []Node) Allocator.Error!void {
// We need to increase the reference count of all the nodes.
// Careful accounting here so that we properly unref on error
// only the nodes we referenced.
@ -187,8 +378,6 @@ pub fn SplitTree(comptime V: type) type {
reffed = i;
}
assert(reffed == nodes.len - 1);
return .{ .arena = arena, .nodes = nodes };
}
/// Spatial representation of the split tree. This can be used to
@ -360,13 +549,28 @@ pub fn SplitTree(comptime V: type) type {
// Get our spatial representation.
const sp = try self.spatial(alloc);
// The width we need for the largest label.
const max_label_width: usize = max_label_width: {
if (!@hasDecl(View, "splitTreeLabel")) {
break :max_label_width std.math.log10(sp.slots.len) + 1;
}
var max: usize = 0;
for (self.nodes) |node| switch (node) {
.split => {},
.leaf => |view| {
const label = view.splitTreeLabel();
max = @max(max, label.len);
},
};
break :max_label_width max;
};
// We need space for whitespace and ASCII art so add that.
// We need to accommodate the leaf handle, whitespace, and
// then the border.
const cell_width = cell_width: {
// The width we need for the largest label.
const max_label_width = std.math.log10(sp.slots.len) + 1;
// Border + whitespace + label + whitespace + border.
break :cell_width 2 + max_label_width + 2;
};
@ -400,6 +604,13 @@ pub fn SplitTree(comptime V: type) type {
// Draw each node
for (sp.slots, 0..) |slot, handle| {
// We only draw leaf nodes. Splits are only used for layout.
const node = self.nodes[handle];
switch (node) {
.leaf => {},
.split => continue,
}
var x: usize = @intFromFloat(@ceil(slot.x));
var y: usize = @intFromFloat(@ceil(slot.y));
var width: usize = @intFromFloat(@ceil(slot.width));
@ -429,13 +640,20 @@ pub fn SplitTree(comptime V: type) type {
for (y + 1..y + height - 1) |y_cur| grid[y_cur][x] = '|';
for (y + 1..y + height - 1) |y_cur| grid[y_cur][x + width - 1] = '|';
// Get our label text
var buf: [10]u8 = undefined;
const label: []const u8 = if (@hasDecl(View, "splitTreeLabel"))
node.leaf.splitTreeLabel()
else
try std.fmt.bufPrint(&buf, "{d}", .{handle});
// Draw the handle in the center
const x_mid = width / 2 + x;
const y_mid = height / 2 + y;
const label_width = std.math.log10(handle + 1) + 1;
const label_width = label.len;
const label_start = x_mid - label_width / 2;
const row = grid[y_mid][label_start..];
_ = try std.fmt.bufPrint(row, "{d}", .{handle});
_ = try std.fmt.bufPrint(row, "{s}", .{label});
}
// Output every row
@ -451,6 +669,8 @@ const TestTree = SplitTree(TestView);
const TestView = struct {
const Self = @This();
label: []const u8,
pub fn ref(self: *Self, alloc: Allocator) Allocator.Error!*Self {
const ptr = try alloc.create(Self);
ptr.* = self.*;
@ -460,6 +680,10 @@ const TestView = struct {
pub fn unref(self: *Self, alloc: Allocator) void {
alloc.destroy(self);
}
pub fn splitTreeLabel(self: *const Self) []const u8 {
return self.label;
}
};
test "SplitTree: empty tree" {
@ -478,7 +702,7 @@ test "SplitTree: empty tree" {
test "SplitTree: single node" {
const testing = std.testing;
const alloc = testing.allocator;
var v: TestTree.View = .{};
var v: TestTree.View = .{ .label = "A" };
var t: TestTree = try .init(alloc, &v);
defer t.deinit();
@ -486,7 +710,7 @@ test "SplitTree: single node" {
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---+
\\| 0 |
\\| A |
\\+---+
\\
);
@ -495,11 +719,11 @@ test "SplitTree: single node" {
test "SplitTree: split horizontal" {
const testing = std.testing;
const alloc = testing.allocator;
var v: TestTree.View = .{};
var t1: TestTree = try .init(alloc, &v);
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var t2: TestTree = try .init(alloc, &v);
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
var t3 = try t1.split(
@ -514,7 +738,7 @@ test "SplitTree: split horizontal" {
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---++---+
\\| 2 || 1 |
\\| A || B |
\\+---++---+
\\
);
@ -523,11 +747,12 @@ test "SplitTree: split horizontal" {
test "SplitTree: split vertical" {
const testing = std.testing;
const alloc = testing.allocator;
var v: TestTree.View = .{};
var t1: TestTree = try .init(alloc, &v);
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var t2: TestTree = try .init(alloc, &v);
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
var t3 = try t1.split(
@ -542,11 +767,132 @@ test "SplitTree: split vertical" {
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---+
\\| 2 |
\\| A |
\\+---+
\\+---+
\\| 1 |
\\| B |
\\+---+
\\
);
}
test "SplitTree: remove leaf" {
const testing = std.testing;
const alloc = testing.allocator;
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
var t3 = try t1.split(
alloc,
0, // at root
.right, // split right
&t2, // insert t2
);
defer t3.deinit();
// Remove "A"
var it = t3.iterator();
var t4 = try t3.remove(
alloc,
while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "A")) {
break entry.handle;
}
} else return error.NotFound,
);
defer t4.deinit();
const str = try std.fmt.allocPrint(alloc, "{}", .{t4});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---+
\\| B |
\\+---+
\\
);
}
test "SplitTree: split twice, remove intermediary" {
const testing = std.testing;
const alloc = testing.allocator;
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
var v3: TestTree.View = .{ .label = "C" };
var t3: TestTree = try .init(alloc, &v3);
defer t3.deinit();
// A | B horizontal.
var split1 = try t1.split(
alloc,
0, // at root
.right, // split right
&t2, // insert t2
);
defer split1.deinit();
// Insert C below that.
var split2 = try split1.split(
alloc,
0, // at root
.down, // split down
&t3, // insert t3
);
defer split2.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{}", .{split2});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---++---+
\\| A || B |
\\+---++---+
\\+--------+
\\| C |
\\+--------+
\\
);
}
// Remove "B"
var it = split2.iterator();
var split3 = try split2.remove(
alloc,
while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "B")) {
break entry.handle;
}
} else return error.NotFound,
);
defer split3.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{}", .{split3});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---+
\\| A |
\\+---+
\\+---+
\\| C |
\\+---+
\\
);
}
// Remove every node from split2 (our most complex one), which should
// never crash. We don't test the result is correct, this just verifies
// we don't hit any assertion failures.
for (0..split2.nodes.len) |i| {
var t = try split2.remove(alloc, @intCast(i));
t.deinit();
}
}