From e289f9579c73b4f6da105dfad87bd90e0dc6d973 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 26 Mar 2026 15:25:13 +0000 Subject: [PATCH] fix(lua): make vim.deep_equal cycle-safe AI-assisted: Codex --- runtime/doc/lua.txt | 1 + runtime/lua/vim/_core/shared.lua | 69 ++++++++++++++++++++++---------- test/functional/lua/vim_spec.lua | 3 ++ 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index e28b151a4d..5fc4e05d5c 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1630,6 +1630,7 @@ vim.deep_equal({a}, {b}) *vim.deep_equal()* Tables are compared recursively unless they both provide the `eq` metamethod. All other types are compared using the equality `==` operator. + Cyclic tables are supported. Parameters: ~ • {a} (`any`) First value diff --git a/runtime/lua/vim/_core/shared.lua b/runtime/lua/vim/_core/shared.lua index c4a515fad5..f390e3c1e9 100644 --- a/runtime/lua/vim/_core/shared.lua +++ b/runtime/lua/vim/_core/shared.lua @@ -661,36 +661,61 @@ function vim.tbl_deep_extend(behavior, ...) return tbl_extend(behavior, true, ...) end +---@param left any +---@param right any +---@param seen? table> +---@return boolean +local function deep_equal(left, right, seen) + if left == right then + return true + end + + if type(left) ~= type(right) then + return false + end + + if type(left) ~= 'table' then + return false + end + + seen = seen or {} + local seen_left = seen[left] + if seen_left and seen_left[right] ~= nil then + return seen_left[right] + end + + seen_left = seen_left or {} + seen[left] = seen_left + -- Assume equality while descending so recursive structures can terminate. + seen_left[right] = true + + for k, v in pairs(left) do + if not deep_equal(v, right[k], seen) then + seen_left[right] = false + return false + end + end + + for k in pairs(right) do + if left[k] == nil then + seen_left[right] = false + return false + end + end + + return true +end + --- Deep compare values for equality --- --- Tables are compared recursively unless they both provide the `eq` metamethod. --- All other types are compared using the equality `==` operator. +--- Cyclic tables are supported. ---@param a any First value ---@param b any Second value ---@return boolean `true` if values are equals, else `false` function vim.deep_equal(a, b) - if a == b then - return true - end - if type(a) ~= type(b) then - return false - end - if type(a) == 'table' then - --- @cast a table - --- @cast b table - for k, v in pairs(a) do - if not vim.deep_equal(v, b[k]) then - return false - end - end - for k in pairs(b) do - if a[k] == nil then - return false - end - end - return true - end - return false + return deep_equal(a, b) end --- Add the reverse lookup values to an existing table. diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 7ab615afa7..e9a1779367 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -1249,10 +1249,13 @@ describe('lua stdlib', function() eq(true, exec_lua [[ return vim.deep_equal({a={b=1}}, {a={b=1}}) ]]) eq(true, exec_lua [[ return vim.deep_equal({a={b={nil}}}, {a={b={}}}) ]]) eq(true, exec_lua [[ return vim.deep_equal({a=1, [5]=5}, {nil,nil,nil,nil,5,a=1}) ]]) + eq(true, exec_lua [[ local shared = {}; return vim.deep_equal({ 1, shared, 1, shared }, { 1, {}, 1, {} }) ]]) + eq(true, exec_lua [[ local a,b={},{}; a[1]=a; b[1]=b; return vim.deep_equal(a, b) ]]) eq(false, exec_lua [[ return vim.deep_equal(1, {nil,nil,nil,nil,5,a=1}) ]]) eq(false, exec_lua [[ return vim.deep_equal(1, 3) ]]) eq(false, exec_lua [[ return vim.deep_equal(nil, 3) ]]) eq(false, exec_lua [[ return vim.deep_equal({a=1}, {a=2}) ]]) + eq(false, exec_lua [[ local a,b={},{}; a[1]=a; b[1]={}; return vim.deep_equal(a, b) ]]) end) it('vim.list_extend', function()