diff --git a/src/config/Config.zig b/src/config/Config.zig index 2f0bef6ff..ac52595be 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -17,6 +17,7 @@ const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const global_state = &@import("../global.zig").state; +const deepEqual = @import("../datastruct/comparison.zig").deepEqual; const fontpkg = @import("../font/main.zig"); const inputpkg = @import("../input.zig"); const internal_os = @import("../os/main.zig"); @@ -4120,7 +4121,7 @@ pub fn changeConditionalState( // Conditional set contains the keys that this config uses. So we // only continue if we use this key. - if (self._conditional_set.contains(key) and !equalField( + if (self._conditional_set.contains(key) and !deepEqual( @TypeOf(@field(self._conditional_state, field.name)), @field(self._conditional_state, field.name), @field(new, field.name), @@ -4826,7 +4827,7 @@ pub fn changed(self: *const Config, new: *const Config, comptime key: Key) bool const old_value = @field(self, field.name); const new_value = @field(new, field.name); - return !equalField(field.type, old_value, new_value); + return !deepEqual(field.type, old_value, new_value); } /// This yields a key for every changed field between old and new. @@ -4854,91 +4855,6 @@ pub const ChangeIterator = struct { } }; -/// A config-specific helper to determine if two values of the same -/// type are equal. This isn't the same as std.mem.eql or std.testing.equals -/// because we expect structs to implement their own equality. -/// -/// This also doesn't support ALL Zig types, because we only add to it -/// as we need types for the config. -fn equalField(comptime T: type, old: T, new: T) bool { - // Do known named types first - switch (T) { - inline []const u8, - [:0]const u8, - => return std.mem.eql(u8, old, new), - - []const [:0]const u8, - => { - if (old.len != new.len) return false; - for (old, new) |a, b| { - if (!std.mem.eql(u8, a, b)) return false; - } - - return true; - }, - - else => {}, - } - - // Back into types of types - switch (@typeInfo(T)) { - .void => return true, - - inline .bool, - .int, - .float, - .@"enum", - => return old == new, - - .optional => |info| { - if (old == null and new == null) return true; - if (old == null or new == null) return false; - return equalField(info.child, old.?, new.?); - }, - - .@"struct" => |info| { - if (@hasDecl(T, "equal")) return old.equal(new); - - // If a struct doesn't declare an "equal" function, we fall back - // to a recursive field-by-field compare. - inline for (info.fields) |field_info| { - if (!equalField( - field_info.type, - @field(old, field_info.name), - @field(new, field_info.name), - )) return false; - } - return true; - }, - - .@"union" => |info| { - if (@hasDecl(T, "equal")) return old.equal(new); - - const tag_type = info.tag_type.?; - const old_tag = std.meta.activeTag(old); - const new_tag = std.meta.activeTag(new); - if (old_tag != new_tag) return false; - - inline for (info.fields) |field_info| { - if (@field(tag_type, field_info.name) == old_tag) { - return equalField( - field_info.type, - @field(old, field_info.name), - @field(new, field_info.name), - ); - } - } - - unreachable; - }, - - else => { - @compileLog(T); - @compileError("unsupported field type"); - }, - } -} - /// This runs a heuristic to determine if we are likely running /// Ghostty in a CLI environment. We need this to change some behaviors. /// We should keep the set of behaviors that depend on this as small @@ -6885,7 +6801,7 @@ pub const Keybinds = struct { const self_leaf = self_entry.value_ptr.*.leaf; const other_leaf = other_entry.value_ptr.*.leaf; - if (!equalField( + if (!deepEqual( inputpkg.Binding.Set.Leaf, self_leaf, other_leaf, @@ -6899,7 +6815,7 @@ pub const Keybinds = struct { if (self_chain.flags != other_chain.flags) return false; if (self_chain.actions.items.len != other_chain.actions.items.len) return false; for (self_chain.actions.items, other_chain.actions.items) |a1, a2| { - if (!equalField( + if (!deepEqual( inputpkg.Binding.Action, a1, a2, diff --git a/src/datastruct/comparison.zig b/src/datastruct/comparison.zig index 4427c143c..6a862bb9c 100644 --- a/src/datastruct/comparison.zig +++ b/src/datastruct/comparison.zig @@ -1,11 +1,102 @@ // The contents of this file is largely based on testing.zig from the Zig 0.15.1 // stdlib, distributed under the MIT license, copyright (c) Zig contributors const std = @import("std"); +const testing = std.testing; + +/// A deep equality comparison function that works for most types. We +/// add types as necessary. It defers to `equal` decls on types that support +/// decls. +pub fn deepEqual(comptime T: type, old: T, new: T) bool { + // Do known named types first + switch (T) { + inline []const u8, + [:0]const u8, + => return std.mem.eql(u8, old, new), + + []const [:0]const u8, + => { + if (old.len != new.len) return false; + for (old, new) |a, b| { + if (!std.mem.eql(u8, a, b)) return false; + } + + return true; + }, + + else => {}, + } + + // Back into types of types + switch (@typeInfo(T)) { + .void => return true, + + inline .bool, + .int, + .float, + .@"enum", + => return old == new, + + .optional => |info| { + if (old == null and new == null) return true; + if (old == null or new == null) return false; + return deepEqual(info.child, old.?, new.?); + }, + + .array => |info| for (old, new) |old_elem, new_elem| { + if (!deepEqual( + info.child, + old_elem, + new_elem, + )) return false; + } else return true, + + .@"struct" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + + // If a struct doesn't declare an "equal" function, we fall back + // to a recursive field-by-field compare. + inline for (info.fields) |field_info| { + if (!deepEqual( + field_info.type, + @field(old, field_info.name), + @field(new, field_info.name), + )) return false; + } + return true; + }, + + .@"union" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + + const tag_type = info.tag_type.?; + const old_tag = std.meta.activeTag(old); + const new_tag = std.meta.activeTag(new); + if (old_tag != new_tag) return false; + + inline for (info.fields) |field_info| { + if (@field(tag_type, field_info.name) == old_tag) { + return deepEqual( + field_info.type, + @field(old, field_info.name), + @field(new, field_info.name), + ); + } + } + + unreachable; + }, + + else => { + @compileLog(T); + @compileError("unsupported field type"); + }, + } +} /// Generic, recursive equality testing utility using approximate comparison for /// floats and equality for everything else /// -/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`. +/// Based on `testing.expectEqual` and `testing.expectEqualSlices`. /// /// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`. pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void { @@ -59,7 +150,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { if (union_info.tag_type == null) { // untagged unions can only be compared bitwise, // so expectEqual is all we need - std.testing.expectEqual(expected, actual) catch { + testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }; } @@ -69,7 +160,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { const expectedTag = @as(Tag, expected); const actualTag = @as(Tag, actual); - std.testing.expectEqual(expectedTag, actualTag) catch { + testing.expectEqual(expectedTag, actualTag) catch { return error.TestExpectedApproxEqual; }; @@ -84,23 +175,23 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { }; // we only reach this point if there's at least one null or error, // in which case expectEqual is all we need - std.testing.expectEqual(expected, actual) catch { + testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }; }, // fall back to expectEqual for everything else - else => std.testing.expectEqual(expected, actual) catch { + else => testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }, } } -/// Copy of std.testing.print (not public) +/// Copy of testing.print (not public) fn print(comptime fmt: []const u8, args: anytype) void { if (@inComptime()) { @compileError(std.fmt.comptimePrint(fmt, args)); - } else if (std.testing.backend_can_print) { + } else if (testing.backend_can_print) { std.debug.print(fmt, args); } } @@ -145,3 +236,195 @@ test "expectApproxEqual struct" { try expectApproxEqual(a, b); } + +test "deepEqual void" { + try testing.expect(deepEqual(void, {}, {})); +} + +test "deepEqual bool" { + try testing.expect(deepEqual(bool, true, true)); + try testing.expect(deepEqual(bool, false, false)); + try testing.expect(!deepEqual(bool, true, false)); + try testing.expect(!deepEqual(bool, false, true)); +} + +test "deepEqual int" { + try testing.expect(deepEqual(i32, 42, 42)); + try testing.expect(deepEqual(i32, -100, -100)); + try testing.expect(!deepEqual(i32, 42, 43)); + try testing.expect(deepEqual(u64, 0, 0)); + try testing.expect(!deepEqual(u64, 0, 1)); +} + +test "deepEqual float" { + try testing.expect(deepEqual(f32, 1.0, 1.0)); + try testing.expect(!deepEqual(f32, 1.0, 1.1)); + try testing.expect(deepEqual(f64, 3.14159, 3.14159)); + try testing.expect(!deepEqual(f64, 3.14159, 3.14158)); +} + +test "deepEqual enum" { + const Color = enum { red, green, blue }; + try testing.expect(deepEqual(Color, .red, .red)); + try testing.expect(deepEqual(Color, .blue, .blue)); + try testing.expect(!deepEqual(Color, .red, .green)); + try testing.expect(!deepEqual(Color, .green, .blue)); +} + +test "deepEqual []const u8" { + try testing.expect(deepEqual([]const u8, "hello", "hello")); + try testing.expect(deepEqual([]const u8, "", "")); + try testing.expect(!deepEqual([]const u8, "hello", "world")); + try testing.expect(!deepEqual([]const u8, "hello", "hell")); + try testing.expect(!deepEqual([]const u8, "hello", "hello!")); +} + +test "deepEqual [:0]const u8" { + try testing.expect(deepEqual([:0]const u8, "foo", "foo")); + try testing.expect(!deepEqual([:0]const u8, "foo", "bar")); + try testing.expect(!deepEqual([:0]const u8, "foo", "fo")); +} + +test "deepEqual []const [:0]const u8" { + const a: []const [:0]const u8 = &.{ "one", "two", "three" }; + const b: []const [:0]const u8 = &.{ "one", "two", "three" }; + const c: []const [:0]const u8 = &.{ "one", "two" }; + const d: []const [:0]const u8 = &.{ "one", "two", "four" }; + const e: []const [:0]const u8 = &.{}; + + try testing.expect(deepEqual([]const [:0]const u8, a, b)); + try testing.expect(!deepEqual([]const [:0]const u8, a, c)); + try testing.expect(!deepEqual([]const [:0]const u8, a, d)); + try testing.expect(deepEqual([]const [:0]const u8, e, e)); + try testing.expect(!deepEqual([]const [:0]const u8, a, e)); +} + +test "deepEqual optional" { + try testing.expect(deepEqual(?i32, null, null)); + try testing.expect(deepEqual(?i32, 42, 42)); + try testing.expect(!deepEqual(?i32, null, 42)); + try testing.expect(!deepEqual(?i32, 42, null)); + try testing.expect(!deepEqual(?i32, 42, 43)); +} + +test "deepEqual optional nested" { + const Nested = struct { x: i32, y: i32 }; + try testing.expect(deepEqual(?Nested, null, null)); + try testing.expect(deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 2 })); + try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 3 })); + try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, null)); +} + +test "deepEqual array" { + try testing.expect(deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 3 })); + try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 4 })); + try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 0, 2, 3 })); + try testing.expect(deepEqual([0]i32, .{}, .{})); +} + +test "deepEqual nested array" { + const a = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } }; + const b = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } }; + const c = [2][2]i32{ .{ 1, 2 }, .{ 3, 5 } }; + + try testing.expect(deepEqual([2][2]i32, a, b)); + try testing.expect(!deepEqual([2][2]i32, a, c)); +} + +test "deepEqual struct" { + const Point = struct { x: i32, y: i32 }; + try testing.expect(deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 20 })); + try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 21 })); + try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 11, .y = 20 })); +} + +test "deepEqual struct nested" { + const Inner = struct { value: i32 }; + const Outer = struct { a: Inner, b: Inner }; + + const x = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } }; + const y = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } }; + const z = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 3 } }; + + try testing.expect(deepEqual(Outer, x, y)); + try testing.expect(!deepEqual(Outer, x, z)); +} + +test "deepEqual struct with equal decl" { + const Custom = struct { + value: i32, + + pub fn equal(self: @This(), other: @This()) bool { + return @mod(self.value, 10) == @mod(other.value, 10); + } + }; + + try testing.expect(deepEqual(Custom, .{ .value = 5 }, .{ .value = 15 })); + try testing.expect(deepEqual(Custom, .{ .value = 100 }, .{ .value = 0 })); + try testing.expect(!deepEqual(Custom, .{ .value = 5 }, .{ .value = 6 })); +} + +test "deepEqual union" { + const Value = union(enum) { + int: i32, + float: f32, + none, + }; + + try testing.expect(deepEqual(Value, .{ .int = 42 }, .{ .int = 42 })); + try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .int = 43 })); + try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .float = 42.0 })); + try testing.expect(deepEqual(Value, .none, .none)); + try testing.expect(!deepEqual(Value, .none, .{ .int = 0 })); +} + +test "deepEqual union with equal decl" { + const Value = union(enum) { + num: i32, + str: []const u8, + + pub fn equal(self: @This(), other: @This()) bool { + return switch (self) { + .num => |n| switch (other) { + .num => |m| @mod(n, 10) == @mod(m, 10), + else => false, + }, + .str => |s| switch (other) { + .str => |t| s.len == t.len, + else => false, + }, + }; + } + }; + + try testing.expect(deepEqual(Value, .{ .num = 5 }, .{ .num = 25 })); + try testing.expect(!deepEqual(Value, .{ .num = 5 }, .{ .num = 6 })); + try testing.expect(deepEqual(Value, .{ .str = "abc" }, .{ .str = "xyz" })); + try testing.expect(!deepEqual(Value, .{ .str = "abc" }, .{ .str = "ab" })); +} + +test "deepEqual array of structs" { + const Item = struct { id: i32, name: []const u8 }; + const a = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } }; + const b = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } }; + const c = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "TWO" } }; + + try testing.expect(deepEqual([2]Item, a, b)); + try testing.expect(!deepEqual([2]Item, a, c)); +} + +test "deepEqual struct with optional field" { + const Config = struct { name: []const u8, port: ?u16 }; + + try testing.expect(deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8080 })); + try testing.expect(deepEqual(Config, .{ .name = "app", .port = null }, .{ .name = "app", .port = null })); + try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = null })); + try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8081 })); +} + +test "deepEqual struct with array field" { + const Data = struct { values: [3]i32 }; + + try testing.expect(deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 3 } })); + try testing.expect(!deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 4 } })); +} diff --git a/src/datastruct/main.zig b/src/datastruct/main.zig index 64a29269e..bfee23427 100644 --- a/src/datastruct/main.zig +++ b/src/datastruct/main.zig @@ -19,4 +19,6 @@ pub const SplitTree = split_tree.SplitTree; test { @import("std").testing.refAllDecls(@This()); + + _ = @import("comparison.zig"); }