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()