nixos/lua-lsp/script/lazytable.lua

354 lines
8.5 KiB
Lua

local type = type
local pairs = pairs
local error = error
local next = next
local load = load
local setmt = setmetatable
local rawset = rawset
local sdump = string.dump
local sbyte = string.byte
local smatch = string.match
local sformat = string.format
local tconcat = table.concat
_ENV = nil
---@class lazytable.builder
---@field source table
---@field codeMap table<integer, string>
---@field dumpMark table<table, integer>
---@field excludes table<table, true>
---@field refMap table<any, integer>
---@field instMap table<integer, table|function|thread|userdata>
local mt = {}
mt.__index = mt
mt.tableID = 1
mt.keyID = 1
local DUMMY = function() end
local RESERVED = {
['and'] = true,
['break'] = true,
['do'] = true,
['else'] = true,
['elseif'] = true,
['end'] = true,
['false'] = true,
['for'] = true,
['function'] = true,
['if'] = true,
['in'] = true,
['local'] = true,
['nil'] = true,
['not'] = true,
['or'] = true,
['repeat'] = true,
['return'] = true,
['then'] = true,
['true'] = true,
['until'] = true,
['while'] = true,
['goto'] = true
}
---@param k string|integer
---@return string
local function formatKey(k)
if type(k) == 'string' then
if not RESERVED[k] and smatch(k, '^[%a_][%w_]*$') then
return k
else
return sformat('[%q]', k)
end
end
if type(k) == 'number' then
return sformat('[%q]', k)
end
error('invalid key type: ' .. type(k))
end
---@param v string|number|boolean
local function formatValue(v)
return sformat('%q', v)
end
---@param info {[1]: table, [2]: integer, [3]: table?}
---@return string
local function dump(info)
local codeBuf = {}
codeBuf[#codeBuf + 1] = 'return{{'
local hasFields
for k, v in pairs(info[1]) do
if hasFields then
codeBuf[#codeBuf + 1] = ','
else
hasFields = true
end
codeBuf[#codeBuf+1] = sformat('%s=%s'
, formatKey(k)
, formatValue(v)
)
end
codeBuf[#codeBuf+1] = '}'
codeBuf[#codeBuf+1] = sformat(',%d', formatValue(info[2]))
if info[3] then
codeBuf[#codeBuf+1] = ',{'
hasFields = false
for k, v in pairs(info[3]) do
if hasFields then
codeBuf[#codeBuf+1] = ','
else
hasFields = true
end
codeBuf[#codeBuf+1] = sformat('%s=%s'
, formatKey(k)
, formatValue(v)
)
end
codeBuf[#codeBuf+1] = '}'
end
codeBuf[#codeBuf + 1] = '}'
return tconcat(codeBuf)
end
---@param obj table|function|userdata|thread
---@return integer
function mt:getObjectID(obj)
if self.dumpMark[obj] then
return self.dumpMark[obj]
end
local id = self.tableID
self.tableID = self.tableID + 1
self.dumpMark[obj] = id
if self.excludes[obj] or type(obj) ~= 'table' then
self.refMap[obj] = id
self.instMap[id] = obj
return id
end
if not next(obj) then
self.codeMap[id] = nil
return id
end
local fields = {}
local objs
for k, v in pairs(obj) do
local tp = type(v)
if tp == 'string' or tp == 'number' or tp == 'boolean' then
fields[k] = v
else
if not objs then
objs = {}
end
objs[k] = self:getObjectID(v)
end
end
local code = dump({fields, #obj, objs})
self.codeMap[id] = code
return id
end
---@param writter fun(id: integer, code: string): boolean
---@param reader fun(id: integer): string?
function mt:bind(writter, reader)
setmt(self.codeMap, {
__newindex = function (t, id, code)
local suc = writter(id, code)
if not suc then
rawset(t, id, code)
end
end,
__index = function (_, id)
return reader(id)
end
})
end
---@param t table
function mt:exclude(t)
self.excludes[t] = true
return self
end
---@return table
function mt:entry()
local entryID = self:getObjectID(self.source)
local codeMap = self.codeMap
local refMap = self.refMap
local instMap = self.instMap
local tableID = self.tableID
---@type table<table, integer>
local idMap = {}
---@type table<table, table[]>
local infoMap = setmt({}, {
__mode = 'v',
__index = function (map, t)
local id = idMap[t]
local code = codeMap[id]
if not code then
return nil
end
local f = load(code)
if not f then
return nil
end
--if sbyte(code, 1, 1) ~= 27 then
-- codeMap[id] = sdump(f, true)
--end
local info = f()
map[t] = info
return info
end
})
local lazyload = {
ref = refMap,
__index = function(t, k)
local info = infoMap[t]
if not info then
return nil
end
local fields = info[1]
local keyID = k
local v = fields[keyID]
if v ~= nil then
return v
end
local refs = info[3]
if not refs then
return nil
end
local ref = refs[keyID]
if not ref then
return nil
end
return instMap[ref]
end,
__newindex = function(t, k, v)
local info = infoMap[t]
local fields = info and info[1] or {}
local len = info and info[2] or 0
local objs = info and info[3]
fields[k] = nil
if objs then
objs[k] = nil
end
if v ~= nil then
local tp = type(v)
if tp == 'string' or tp == 'number' or tp == 'boolean' then
fields[k] = v
else
if not objs then
objs = {}
end
local id = refMap[v] or idMap[v]
if not id then
id = tableID
refMap[v] = id -- 新赋值的对象一定会被引用住
instMap[id] = v
tableID = tableID + 1
end
objs[k] = id
end
end
info = { fields, len, objs }
local id = idMap[t]
local code = dump(info)
infoMap[id] = nil
codeMap[id] = nil
codeMap[id] = code
end,
__len = function (t)
local info = infoMap[t]
if not info then
return 0
end
return info[2]
end,
__pairs = function (t)
local info = infoMap[t]
if not info then
return DUMMY
end
local fields = info[1]
local objs = info[3]
local keys = {}
for k in pairs(fields) do
keys[#keys+1] = k
end
if objs then
for k in pairs(objs) do
keys[#keys+1] = k
end
end
local i = 0
return function()
i = i + 1
local k = keys[i]
return k, t[k]
end
end,
}
setmt(idMap, { __mode = 'k' })
setmt(instMap, {
__mode = 'v',
__index = function (map, id)
local inst = {}
idMap[inst] = id
map[id] = inst
return setmt(inst, lazyload)
end,
})
local entry = instMap[entryID] --[[@as table]]
self.source = nil
self.dumpMark = nil
return entry
end
---@class lazytable
local m = {}
---@param t table
---@param writter? fun(id: integer, code: string): boolean
---@param reader? fun(id: integer): string?
---@return lazytable.builder
function m.build(t, writter, reader)
local builder = setmt({
source = t,
codeMap = {},
refMap = {},
instMap = {},
dumpMark = {},
excludes = setmt({}, { __mode = 'k' }),
}, mt)
if writter and reader then
builder:bind(writter, reader)
end
return builder
end
return m