aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/ref_counted_set.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/ref_counted_set.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/ref_counted_set.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/ref_counted_set.zig209
1 files changed, 188 insertions, 21 deletions
diff --git a/vendor/libghostty-vt/src/terminal/ref_counted_set.zig b/vendor/libghostty-vt/src/terminal/ref_counted_set.zig
index dd76079f..669c2c57 100644
--- a/vendor/libghostty-vt/src/terminal/ref_counted_set.zig
+++ b/vendor/libghostty-vt/src/terminal/ref_counted_set.zig
@@ -1,5 +1,6 @@
const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
+const testing = std.testing;
const size = @import("size.zig");
const Offset = size.Offset;
@@ -211,6 +212,30 @@ pub fn RefCountedSet(
@memset(table.ptr(base)[0..l.table_cap], 0);
@memset(items.ptr(base)[0..l.cap], .{});
+ return initFromParts(table, items, l, context);
+ }
+
+ /// Like `init`, but for backing memory that the caller guarantees
+ /// is already zero-filled (e.g. fresh OS pages). This writes
+ /// nothing to the backing buffer, so the OS pages behind the table
+ /// and items stay untouched until the first `add`.
+ ///
+ /// Behavior is undefined if the backing memory is not zero.
+ pub fn initAssumeZeroed(base: OffsetBuf, l: Layout, context: Context) Self {
+ return initFromParts(
+ base.member(Id, l.table_start),
+ base.member(Item, l.items_start),
+ l,
+ context,
+ );
+ }
+
+ fn initFromParts(
+ table: Offset(Id),
+ items: Offset(Item),
+ l: Layout,
+ context: Context,
+ ) Self {
return .{
.table = table,
.items = items,
@@ -316,6 +341,19 @@ pub fn RefCountedSet(
if (id < self.next_id) {
if (items[id].meta.ref == 0) {
+ // Requested ID is dead, but if the value exists not under
+ // another ID then ref count that and increase the ID.
+ if (self.lookupContext(base, value, ctx)) |existing_id| {
+ // Notify the context that the value is "deleted"
+ // because we're reusing the existing value in the
+ // set. This allows callers to clean up any
+ // resources associated with the value.
+ if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
+
+ items[existing_id].meta.ref += 1;
+ return existing_id;
+ }
+
// See comment in `addContext` for details.
if (self.psl_stats[self.psl_stats.len - 1] > 0) {
@branchHint(.cold);
@@ -323,8 +361,7 @@ pub fn RefCountedSet(
}
self.deleteItem(base, id, ctx);
-
- const added_id = self.upsert(base, value, id, ctx);
+ const added_id = self.insert(base, value, id, ctx);
items[added_id].meta.ref += 1;
@@ -447,6 +484,47 @@ pub fn RefCountedSet(
return self.living;
}
+ /// A live entry returned by `Iterator`.
+ pub const Entry = struct {
+ id: Id,
+ value_ptr: *T,
+ };
+
+ /// Iterates live entries in ascending ID order.
+ ///
+ /// Released entries whose reference count reached zero are skipped.
+ /// Any mutation of the set invalidates the iterator.
+ pub const Iterator = struct {
+ items: [*]Item,
+ id: Id = 1,
+ end: Id,
+
+ pub fn next(self: *Iterator) ?Entry {
+ while (self.id < self.end) {
+ const id = self.id;
+ self.id += 1;
+
+ const item = &self.items[id];
+ if (item.meta.ref == 0) continue;
+
+ return .{
+ .id = id,
+ .value_ptr = &item.value,
+ };
+ }
+
+ return null;
+ }
+ };
+
+ /// Return an iterator over the live entries in this set.
+ pub fn iterator(self: *const Self, base: anytype) Iterator {
+ return .{
+ .items = self.items.ptr(base),
+ .end = self.next_id,
+ };
+ }
+
/// Delete an item, removing any references from
/// the table, and freeing its ID to be reused.
fn deleteItem(self: *Self, base: anytype, id: Id, ctx: Context) void {
@@ -547,24 +625,6 @@ pub fn RefCountedSet(
return null;
}
- /// Find the provided value in the hash table, or add a new item
- /// for it if not present. If a new item is added, `new_id` will
- /// be used as the ID. If an existing item is found, the `new_id`
- /// is ignored and the existing item's ID is returned.
- fn upsert(self: *Self, base: anytype, value: T, new_id: Id, ctx: Context) Id {
- // If the item already exists, return it.
- if (self.lookupContext(base, value, ctx)) |id| {
- // Notify the context that the value is "deleted" because
- // we're reusing the existing value in the set. This allows
- // callers to clean up any resources associated with the value.
- if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
-
- return id;
- }
-
- return self.insert(base, value, new_id, ctx);
- }
-
/// Insert the given value into the hash table with the given ID.
///
/// If runtime safety is enabled, asserts that
@@ -690,7 +750,9 @@ pub fn RefCountedSet(
var psl_stats: [32]Id = @splat(0);
- for (items[0..self.layout.cap], 0..) |item, id| {
+ // Start at item 1 because item 0 is reserved and never
+ // assigned to. Its metadata doesn't matter.
+ for (items[1..self.next_id], 1..) |item, id| {
if (item.meta.bucket < std.math.maxInt(Id)) {
assert(table[item.meta.bucket] == id);
psl_stats[item.meta.psl] += 1;
@@ -704,6 +766,7 @@ pub fn RefCountedSet(
psl_stats = @splat(0);
for (table[0..self.layout.table_cap], 0..) |id, bucket| {
+ if (id == 0) continue;
const item = items[id];
if (item.meta.bucket < std.math.maxInt(Id)) {
assert(item.meta.bucket == bucket);
@@ -721,3 +784,107 @@ pub fn RefCountedSet(
}
};
}
+
+test "addWithId dead id resolving to an existing value" {
+ const alloc = testing.allocator;
+ const TestSet = RefCountedSet(
+ u32,
+ u16,
+ u16,
+ struct {
+ pub fn hash(_: *const @This(), value: u32) u64 {
+ return std.hash.int(value);
+ }
+
+ pub fn eql(_: *const @This(), a: u32, b: u32) bool {
+ return a == b;
+ }
+ },
+ );
+
+ const layout: TestSet.Layout = .init(8);
+ const buf = try alloc.alignedAlloc(
+ u8,
+ TestSet.base_align,
+ layout.total_size,
+ );
+ defer alloc.free(buf);
+
+ var set: TestSet = .init(.init(buf), layout, .{});
+
+ // Create a dead item between two live ones so that it can't
+ // be reaped by the trim loop in `add`, then release it. This
+ // mirrors a page style set after a styled run is erased.
+ const live = try set.add(buf, 11);
+ const released = try set.add(buf, 22);
+ const last = try set.add(buf, 33);
+ set.release(buf, released);
+ try testing.expectEqual(@as(usize, 2), set.count());
+
+ // Request the dead ID for a value that is already live under
+ // a different ID: we must resolve to the existing item and
+ // take a reference, without changing the living count.
+ const resolved = (try set.addWithId(buf, 11, released)).?;
+ try testing.expectEqual(live, resolved);
+ try testing.expectEqual(@as(u16, 2), set.refCount(buf, live));
+ try testing.expectEqual(@as(usize, 2), set.count());
+
+ // The living count must agree with the iterator.
+ var it = set.iterator(buf);
+ var iterated: usize = 0;
+ while (it.next()) |_| iterated += 1;
+ try testing.expectEqual(set.count(), iterated);
+
+ // The dead slot is still reusable for a value that is not
+ // in the set: the requested ID must be used (null return).
+ try testing.expectEqual(@as(?u16, null), try set.addWithId(buf, 44, released));
+ try testing.expectEqual(@as(u16, 1), set.refCount(buf, released));
+ try testing.expectEqual(@as(usize, 3), set.count());
+
+ _ = last;
+}
+
+test "iterator visits live entries in ID order" {
+ const alloc = testing.allocator;
+ const TestSet = RefCountedSet(
+ u32,
+ u16,
+ u16,
+ struct {
+ pub fn hash(_: *const @This(), value: u32) u64 {
+ return std.hash.int(value);
+ }
+
+ pub fn eql(_: *const @This(), a: u32, b: u32) bool {
+ return a == b;
+ }
+ },
+ );
+
+ const layout: TestSet.Layout = .init(8);
+ const buf = try alloc.alignedAlloc(
+ u8,
+ TestSet.base_align,
+ layout.total_size,
+ );
+ defer alloc.free(buf);
+
+ var set: TestSet = .init(.init(buf), layout, .{});
+ const first = try set.add(buf, 11);
+ const released = try set.add(buf, 22);
+ const last = try set.add(buf, 33);
+ _ = try set.add(buf, 11);
+ set.release(buf, released);
+
+ var it = set.iterator(buf);
+
+ const first_entry = it.next().?;
+ try testing.expectEqual(first, first_entry.id);
+ try testing.expectEqual(@as(u32, 11), first_entry.value_ptr.*);
+
+ const last_entry = it.next().?;
+ try testing.expectEqual(last, last_entry.id);
+ try testing.expectEqual(@as(u32, 33), last_entry.value_ptr.*);
+
+ try testing.expect(it.next() == null);
+}