function prettyPrint(value)
	local function helper(val, indent, visited)
		-- Handle non-tables and non-userdata
		if type(val) ~= 'table' and type(val) ~= 'userdata' then
			return tostring(val)
		end

		-- Detect cycles (tables or userdata)
		if visited[val] then
			return "{...}"
		end
		visited[val] = true

		-- Check if it's iterable (table or userdata with __pairs)
		local is_iterable = false
		local iterator_func = nil

		if type(val) == 'table' then
			is_iterable = true
			iterator_func = pairs
		elseif type(val) == 'userdata' then
			-- Check for __pairs metamethod (Lua 5.2+)
			local mt = debug.getmetatable(val)
			if mt and mt.__pairs then
				is_iterable = true
				iterator_func = mt.__pairs(val)
			end
		end

		-- If not iterable, just return tostring
		if not is_iterable then
			visited[val] = nil -- Clean up visited
			return tostring(val)
		end

		-- Build key-value pairs
		local entries = {}
		local nextIndent = indent + 1
		for k, v in iterator_func(val) do
			local keyStr = helper(k, nextIndent, visited)
			local valStr = helper(v, nextIndent, visited)
			local entry = string.rep("  ", nextIndent) .. keyStr .. ": " .. valStr
			table.insert(entries, entry)
		end

		-- Format output
		local result = "{\n"
		if #entries > 0 then
			result = result .. table.concat(entries, ",\n") .. "\n"
		end
		result = result .. string.rep("  ", indent) .. "}"

		visited[val] = nil -- Allow reuse in different branches
		return result
	end

	print(helper(value, 0, {}))
end

local test = {
	numbers = { 1, 2, 3 },
	nested = {
		a = "apple",
		b = { c = "cherry" }
	},
	func = function() end,
	self = nil
}
test.self = test

prettyPrint(test)
